Compare commits
557 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
172cd39702 | ||
|
82213fd3a5 | ||
|
a32bf885ba | ||
|
61eb662e5f | ||
|
2887370666 | ||
|
8826d62075 | ||
|
fae284e2e1 | ||
|
2408617ed9 | ||
|
cc10b7f998 | ||
|
3c97d5f668 | ||
|
0accf24320 | ||
|
8ec2ef345c | ||
|
10dbe2d44d | ||
|
6b2cdb6b54 | ||
|
06bc795133 | ||
|
239a08ff9b | ||
|
41dd8d2184 | ||
|
57b4b13b65 | ||
|
a66b116aad | ||
|
5c4102c0be | ||
|
ebb7318895 | ||
|
b11876096b | ||
|
d603a74c6f | ||
|
95d633e71e | ||
|
217d0650f4 | ||
|
45d7bb34d7 | ||
|
2e32319236 | ||
|
8eb0d0b7c3 | ||
|
f18f04c0ee | ||
|
193da820b2 | ||
|
c6198305d4 | ||
|
c77d6ae3f5 | ||
|
c72b2dbd6b | ||
|
835523f77b | ||
|
ec8a35b587 | ||
|
aca18995d1 | ||
|
f9178f58aa | ||
|
2477946e68 | ||
|
7c4d040384 | ||
|
197cf8ed36 | ||
|
dd0d7c268f | ||
|
b2bfccac0a | ||
|
8b8b352e61 | ||
|
0403dd354b | ||
|
b78b453504 | ||
|
f8fef833b8 | ||
|
fc4068f2e5 | ||
|
c300866dcc | ||
|
18485a74e5 | ||
|
4dd5950406 | ||
|
98de54106d | ||
|
4d64272a1a | ||
|
0ccece908b | ||
|
64cd725060 | ||
|
cc2fa55608 | ||
|
4fb268585c | ||
|
3a2495c456 | ||
|
1d4d058ed0 | ||
|
15a1347531 | ||
|
4cbfa71338 | ||
|
705625af0a | ||
|
01bc6654cb | ||
|
eea42bff66 | ||
|
06b4762c19 | ||
|
1ee9479009 | ||
|
73e94ed03a | ||
|
1883519e82 | ||
|
6f6c1f85ef | ||
|
c55ff3cb1b | ||
|
08006ddd97 | ||
|
fa4aee6ddc | ||
|
691502d7ad | ||
|
43deaba547 | ||
|
2d02ae0f0c | ||
|
a8879da4fc | ||
|
5f4a430845 | ||
|
b9231be305 | ||
|
7cb5cc05e4 | ||
|
750a752ac0 | ||
|
61e5f52286 | ||
|
ce0b716f54 | ||
|
aae8e5ec65 | ||
|
2723e8466e | ||
|
f13c610352 | ||
|
55c65b08bf | ||
|
a11aa3e0dd | ||
|
de0bf5ebcd | ||
|
15369e1ae9 | ||
|
d4115880b9 | ||
|
3c80c75e4a | ||
|
5cb72dce4c | ||
|
d2747487e3 | ||
|
12e664fc61 | ||
|
cbf21b4008 | ||
|
68c1bf7017 | ||
|
257c901255 | ||
|
15d8c663da | ||
|
d50125c62d | ||
|
9262880369 | ||
|
2b111e8352 | ||
|
a35cbdfb7c | ||
|
6a41b7389f | ||
|
a187e69650 | ||
|
fcacddbd9f | ||
|
fa84ade6be | ||
|
17eaa323ed | ||
|
6177fe7803 | ||
|
57976cf613 | ||
|
977e8794ec | ||
|
c68848eecc | ||
|
c6dfb14953 | ||
|
5bad02ccae | ||
|
2e379cbf2a | ||
|
0e23584751 | ||
|
49fd2a9e53 | ||
|
6264a8b41d | ||
|
3990d3bcbf | ||
|
aa3f201ced | ||
|
83c261977d | ||
|
6ca28d96bf | ||
|
c4a5647b62 | ||
|
720d5593a5 | ||
|
13fa325134 | ||
|
773cbb4907 | ||
|
a696264b48 | ||
|
b7db5f77fb | ||
|
b11678e636 | ||
|
f746070944 | ||
|
3323a51ab5 | ||
|
0e59927384 | ||
|
5c4840f129 | ||
|
9ac02323ad | ||
|
cdbed26d1f | ||
|
23f171f34d | ||
|
20b625e483 | ||
|
f1604c6460 | ||
|
ba0e007c05 | ||
|
643e1bf20f | ||
|
24a32a0603 | ||
|
c5caf32b77 | ||
|
09956d7500 | ||
|
d91c896e46 | ||
|
042e6a22b8 | ||
|
14ec12d1f0 | ||
|
288b05a048 | ||
|
5af3096070 | ||
|
570fa01c04 | ||
|
2a69038c4c | ||
|
0ba127e447 | ||
|
7714bdf7e0 | ||
|
4e5e7ae50a | ||
|
5741b2f6c1 | ||
|
76172f92e9 | ||
|
f8b547c028 | ||
|
7ccd9e1709 | ||
|
9217b27d40 | ||
|
819e9025b1 | ||
|
53ceab9f91 | ||
|
a7ed4fe5c3 | ||
|
3190cd322d | ||
|
dad2b64e15 | ||
|
e527ab1613 | ||
|
d7a0bc212d | ||
|
aecd5e9c94 | ||
|
e0edca43d5 | ||
|
ce70d3d728 | ||
|
d9be40a0de | ||
|
e469f04c39 | ||
|
11774e6825 | ||
|
42bdfb51c3 | ||
|
fd637bf1e1 | ||
|
8085e1416c | ||
|
671c9f805f | ||
|
ace7a7ccae | ||
|
9c3bdf1a77 | ||
|
f5242b3102 | ||
|
f1272f059a | ||
|
91595ff4c2 | ||
|
3755d29a45 | ||
|
c2b75399ae | ||
|
a33ecd1338 | ||
|
a7e29a9f36 | ||
|
02399dfa5c | ||
|
aec2941bac | ||
|
9315eb5289 | ||
|
5b2b2ea7b0 | ||
|
d90b634e80 | ||
|
6dd8cda074 | ||
|
701be31554 | ||
|
25eaf730bc | ||
|
4edb7447df | ||
|
5f3de60962 | ||
|
79c17aba49 | ||
|
80a90496d9 | ||
|
bbca803840 | ||
|
160d3869a9 | ||
|
afd8f64da8 | ||
|
6d2548b823 | ||
|
642356d353 | ||
|
ba0fa36c2a | ||
|
12f6cd878d | ||
|
9aacebbbaf | ||
|
701c3745c2 | ||
|
a41d08343c | ||
|
156288b17b | ||
|
6467f98241 | ||
|
b24e4334f6 | ||
|
bf8abcbf4a | ||
|
bb484414b1 | ||
|
fc75b13fae | ||
|
78f59b4207 | ||
|
7c5567db56 | ||
|
ed0e23e8a5 | ||
|
4c4f99606e | ||
|
a61586c846 | ||
|
d64d50c978 | ||
|
a64b7b0c4a | ||
|
0caeb81327 | ||
|
edac7a0171 | ||
|
abfadad2e9 | ||
|
2dc1547bbd | ||
|
5eb23c9764 | ||
|
9f4b2856b0 | ||
|
b5fc10326e | ||
|
8d3a47a873 | ||
|
4df58f3059 | ||
|
06b8cb8d3b | ||
|
ff81f5b496 | ||
|
c89f73006e | ||
|
c28951f049 | ||
|
dfaaaca223 | ||
|
c7f0bf3d64 | ||
|
234ce4c173 | ||
|
f60293b2e7 | ||
|
9441095637 | ||
|
f82d38f758 | ||
|
a7f42f35db | ||
|
cb1d1bfd85 | ||
|
28c3f2ea26 | ||
|
7ecaf1f982 | ||
|
d0a41f3894 | ||
|
57562b234f | ||
|
469d127d61 | ||
|
d6e9b61c8e | ||
|
8dc132dbd3 | ||
|
98e2fbca6a | ||
|
fa7f0fadde | ||
|
7fb1b65ddd | ||
|
77c7fdc636 | ||
|
2732dfd0f1 | ||
|
2e4c4b72b6 | ||
|
fc21ad519b | ||
|
c65cfd3d26 | ||
|
8955462f73 | ||
|
205c8c15bd | ||
|
78198a0147 | ||
|
d561e1141e | ||
|
753fc845ac | ||
|
5dbc00bbfe | ||
|
14ec8522ef | ||
|
0c2d1c22bc | ||
|
1d39a9c9a9 | ||
|
b588ed0fa1 | ||
|
d9f7a138b8 | ||
|
d3e04ff619 | ||
|
372dd24cc7 | ||
|
a9422cf34d | ||
|
c7e52e6fcd | ||
|
705e0823cb | ||
|
8e4cf74974 | ||
|
0a7157655b | ||
|
58d65926bb | ||
|
b178ba16af | ||
|
e4c09284b5 | ||
|
9367a1feff | ||
|
d37ed300e2 | ||
|
3207ce37b6 | ||
|
d036ad7138 | ||
|
4fe07579b9 | ||
|
f563d14134 | ||
|
f1b3ecc738 | ||
|
8387f89115 | ||
|
773f92347f | ||
|
8ff1339b80 | ||
|
c85d5da111 | ||
|
9ab7bc652a | ||
|
e5c724eb05 | ||
|
e0300903d9 | ||
|
1ef38afcf7 | ||
|
210d19c8a0 | ||
|
6d24cc44b2 | ||
|
768e8eb074 | ||
|
3dd902e1f9 | ||
|
f85c5002b7 | ||
|
d48bf9249b | ||
|
0dfc66f1c7 | ||
|
4564173b75 | ||
|
b60e5aaf1f | ||
|
da67f4cb9a | ||
|
b041042473 | ||
|
f83263d6a1 | ||
|
b0139c2217 | ||
|
0ba2e2ce96 | ||
|
4a91ad80c8 | ||
|
4cc715b13d | ||
|
0dfd7cd543 | ||
|
56f164ce2b | ||
|
65db8c9b00 | ||
|
4c4137d9f2 | ||
|
e433e8b5e9 | ||
|
bb442021cf | ||
|
91106b7456 | ||
|
309b5ee1b3 | ||
|
4eded01841 | ||
|
e3d0c899d3 | ||
|
d7595b0dd0 | ||
|
f0375e59fa | ||
|
c367435073 | ||
|
dc812c384e | ||
|
10b2d10dbd | ||
|
f96babc6a6 | ||
|
4e2e14fb22 | ||
|
bcf2fc1812 | ||
|
935e6791a3 | ||
|
fbb7c012a3 | ||
|
dac18fcabf | ||
|
d8e83caffc | ||
|
fbf80b9f50 | ||
|
c2a9139d41 | ||
|
6e3dff149a | ||
|
1bacbe38f4 | ||
|
2e9c610ac9 | ||
|
eb063ec60a | ||
|
37fb14646d | ||
|
ae543518d3 | ||
|
c865d64608 | ||
|
3004422cb6 | ||
|
0c46a17443 | ||
|
497373d976 | ||
|
91198aca0d | ||
|
b17a5e5f0b | ||
|
3f0ef59f65 | ||
|
1e96edc293 | ||
|
0afb77393b | ||
|
7614b642bb | ||
|
bc89580dfe | ||
|
358ae13a88 | ||
|
ccf9dcba70 | ||
|
94604fad61 | ||
|
5c4cc7c50d | ||
|
9ed961ec06 | ||
|
e6bd8cc8c4 | ||
|
ee25bd0f92 | ||
|
e77b9176f3 | ||
|
afe8b966ad | ||
|
310724c961 | ||
|
ceba8ae620 | ||
|
fead661ab7 | ||
|
9c8c17f577 | ||
|
a04f83930f | ||
|
c421d19800 | ||
|
521f02c90e | ||
|
c86b6074f2 | ||
|
d5d1a2c5f4 | ||
|
2a90e3f478 | ||
|
1d49ba41ea | ||
|
e1de1f6682 | ||
|
47ed5e4d4d | ||
|
d77f6f5659 | ||
|
05f0045d5d | ||
|
c4afb84f6e | ||
|
b0b2f9b6d2 | ||
|
ee37feb489 | ||
|
6b8337596f | ||
|
250665b92e | ||
|
86b83c889e | ||
|
c9c657c07b | ||
|
4f2babaf54 | ||
|
1b03bf4555 | ||
|
977b995af9 | ||
|
310ab990bd | ||
|
d6b49b54d4 | ||
|
f00cf39462 | ||
|
18550cf1cb | ||
|
168918f807 | ||
|
2750df8aa7 | ||
|
d6597d9f52 | ||
|
892ea375e3 | ||
|
03abe77b5f | ||
|
e46eb8aa49 | ||
|
2c4862e0f1 | ||
|
fd69efa45c | ||
|
e8aa15917f | ||
|
b3d77f8902 | ||
|
9c3b0b08ec | ||
|
fe7d94194c | ||
|
d6c26d6aa8 | ||
|
8a74ddcd13 | ||
|
18e7189a07 | ||
|
785dd42c84 | ||
|
0cff5065d9 | ||
|
e881b82511 | ||
|
d5551e5d68 | ||
|
e8583000b8 | ||
|
d642ef1a89 | ||
|
2df118022d | ||
|
95457c8f4c | ||
|
0a45b7787f | ||
|
b8c397e180 | ||
|
90105fa2b3 | ||
|
24859fef8a | ||
|
73d7280723 | ||
|
262de49c3c | ||
|
3a77e96a05 | ||
|
505dd6d50f | ||
|
3f8027b65c | ||
|
0f2c765f45 | ||
|
49077f8f44 | ||
|
6a23b8530f | ||
|
ae841af91a | ||
|
44f38849b2 | ||
|
ee12fbdb5f | ||
|
316c630830 | ||
|
1ea5db6110 | ||
|
986d9a00c0 | ||
|
7a05a11014 | ||
|
f09434263c | ||
|
335f594165 | ||
|
fa7ef06f4d | ||
|
3c9ec0aed0 | ||
|
c665d65cba | ||
|
5d4e897cc4 | ||
|
05033714bf | ||
|
a02bd3f25c | ||
|
fdbd213fa2 | ||
|
da64d349c8 | ||
|
17b01a8c66 | ||
|
79dd766fab | ||
|
8375b28747 | ||
|
e12551f309 | ||
|
6102f81710 | ||
|
9f678e5962 | ||
|
02a704a8c7 | ||
|
dd2360ed70 | ||
|
c4ab996470 | ||
|
6c54b07d92 | ||
|
7f9bef3b8d | ||
|
12d1c5d956 | ||
|
e9a4bd5617 | ||
|
f34ccbfdb5 | ||
|
1fa75d7fb2 | ||
|
39140ef98c | ||
|
e30ef4a87c | ||
|
9fc94f0487 | ||
|
121acdab6f | ||
|
6deaa03114 | ||
|
f4f30686c5 | ||
|
a21aae521f | ||
|
aed2356fc1 | ||
|
a478f734f6 | ||
|
98c579da03 | ||
|
e80def0cd0 | ||
|
cc8a9e883e | ||
|
4d587e35d8 | ||
|
50f4fd1115 | ||
|
06d2b68696 | ||
|
bf6f057777 | ||
|
b57c1d69f2 | ||
|
ff265d83f9 | ||
|
5b1c97b774 | ||
|
c8c81366f7 | ||
|
9a37fd56d1 | ||
|
7ecaff8c5d | ||
|
e4b0286a25 | ||
|
7ae6972306 | ||
|
59cea0372b | ||
|
78d88a8520 | ||
|
273af25d57 | ||
|
46d00360a8 | ||
|
3f5935a284 | ||
|
c236ff66e9 | ||
|
af3df5e519 | ||
|
d75753ec98 | ||
|
332bb87231 | ||
|
8adbcab441 | ||
|
9bc2e95196 | ||
|
30a0aa0a0f | ||
|
8622ea5cb2 | ||
|
ed3a50d9b5 | ||
|
df6a17dcc2 | ||
|
474985e784 | ||
|
cb904416c3 | ||
|
3e064ec63e | ||
|
b004769552 | ||
|
17270de621 | ||
|
239b5bc02c | ||
|
6bfabd5493 | ||
|
0b90f7df1b | ||
|
00ca7c8fb0 | ||
|
a11952fe22 | ||
|
06b9b2e649 | ||
|
dcfdcc3e1b | ||
|
b13fee16c1 | ||
|
9a7767ecb1 | ||
|
9b82a33aff | ||
|
70ef77a5d5 | ||
|
77903e9d90 | ||
|
de66a87a7c | ||
|
5ea2028c22 | ||
|
58a68ec0be | ||
|
a39278f7be | ||
|
f8373dc666 | ||
|
3febc2431d | ||
|
0bf736831a | ||
|
7710bf793f | ||
|
a6a43bd361 | ||
|
a39209a895 | ||
|
24c9e0abc3 | ||
|
9cc324d78d | ||
|
8574beceb1 | ||
|
0349b7f1c7 | ||
|
ce1ba20db5 | ||
|
395d823f41 | ||
|
6884f9f74f | ||
|
b34eccd749 | ||
|
50b638f7fd | ||
|
5bf1b91528 | ||
|
f77ececc92 | ||
|
58cccbdcf9 | ||
|
5710ffba6a | ||
|
ccd4522b8f | ||
|
28f29b7385 | ||
|
a7a422d6ed | ||
|
43fcf93584 | ||
|
32f4c8305e | ||
|
3cf44c8078 | ||
|
9e899fde2f | ||
|
ffd4f1d322 | ||
|
10dd13deb3 | ||
|
c1ed83a005 | ||
|
7117c74142 | ||
|
dd06a3fb25 | ||
|
45b579447e | ||
|
bb0b1836cd | ||
|
d5c8815438 | ||
|
ac500ed079 | ||
|
2bc38acbb1 | ||
|
977feae1d6 | ||
|
9c872fcc3e | ||
|
ec1ca3c55e | ||
|
16805759d3 | ||
|
88c2e1f6de | ||
|
1dc9b559e9 | ||
|
d31ecfc64e | ||
|
4813a40f2a | ||
|
ea81470f4a | ||
|
2a6b1d5f15 |
46
.clang-format
Normal file
46
.clang-format
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# https://releases.llvm.org/7.0.0/tools/clang/docs/ClangFormatStyleOptions.html
|
||||||
|
|
||||||
|
---
|
||||||
|
Language: Cpp
|
||||||
|
|
||||||
|
BasedOnStyle: WebKit
|
||||||
|
|
||||||
|
AlignAfterOpenBracket: Align
|
||||||
|
AlignOperands: true
|
||||||
|
AlignTrailingComments: true
|
||||||
|
AllowAllParametersOfDeclarationOnNextLine: true
|
||||||
|
AllowShortBlocksOnASingleLine: false
|
||||||
|
AllowShortCaseLabelsOnASingleLine: true
|
||||||
|
AllowShortFunctionsOnASingleLine: InlineOnly
|
||||||
|
AllowShortIfStatementsOnASingleLine: true
|
||||||
|
AllowShortLoopsOnASingleLine: false
|
||||||
|
AlwaysBreakTemplateDeclarations: true
|
||||||
|
BinPackArguments: false
|
||||||
|
BinPackParameters: false
|
||||||
|
BreakBeforeBinaryOperators: None
|
||||||
|
BreakBeforeBraces: Allman
|
||||||
|
BreakConstructorInitializersBeforeComma: true
|
||||||
|
ColumnLimit: 100
|
||||||
|
ConstructorInitializerAllOnOneLineOrOnePerLine: false
|
||||||
|
Cpp11BracedListStyle: true
|
||||||
|
FixNamespaceComments: true
|
||||||
|
IncludeBlocks: Regroup
|
||||||
|
IncludeCategories:
|
||||||
|
- Regex: '^["<](stdafx|pch)\.h[">]$'
|
||||||
|
Priority: -1
|
||||||
|
- Regex: '^<Windows\.h>$'
|
||||||
|
Priority: 3
|
||||||
|
- Regex: '^<(WinIoCtl|winhttp|Shellapi)\.h>$'
|
||||||
|
Priority: 4
|
||||||
|
- Regex: '.*'
|
||||||
|
Priority: 2
|
||||||
|
IndentCaseLabels: true
|
||||||
|
IndentWidth: 4
|
||||||
|
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||||
|
MaxEmptyLinesToKeep: 2
|
||||||
|
NamespaceIndentation: All
|
||||||
|
PenaltyReturnTypeOnItsOwnLine: 1000
|
||||||
|
PointerAlignment: Left
|
||||||
|
SpaceAfterTemplateKeyword: false
|
||||||
|
Standard: Cpp11
|
||||||
|
UseTab: Never
|
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
build
|
||||||
|
CMakeCache.txt
|
||||||
|
ws/CMakeCache.txt
|
||||||
|
test/build
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
build
|
||||||
|
*.pyc
|
||||||
|
venv
|
12
.pre-commit-config.yaml
Normal file
12
.pre-commit-config.yaml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v2.3.0
|
||||||
|
hooks:
|
||||||
|
- id: check-yaml
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: trailing-whitespace
|
||||||
|
|
||||||
|
- repo: https://github.com/pocc/pre-commit-hooks
|
||||||
|
rev: ''
|
||||||
|
hooks:
|
||||||
|
- id: clang-format
|
56
.travis.yml
Normal file
56
.travis.yml
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
language: bash
|
||||||
|
|
||||||
|
# See https://github.com/amaiorano/vectrexy/blob/master/.travis.yml
|
||||||
|
# for ideas on installing vcpkg
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
# macOS
|
||||||
|
- os: osx
|
||||||
|
env:
|
||||||
|
- HOMEBREW_NO_AUTO_UPDATE=1
|
||||||
|
compiler: clang
|
||||||
|
script:
|
||||||
|
- brew install mbedtls
|
||||||
|
- python test/run.py
|
||||||
|
- make ws
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
- os: linux
|
||||||
|
dist: bionic
|
||||||
|
before_install:
|
||||||
|
- sudo apt-get install -y libmbedtls-dev
|
||||||
|
script:
|
||||||
|
- python test/run.py
|
||||||
|
- make ws
|
||||||
|
env:
|
||||||
|
- CC=gcc
|
||||||
|
- CXX=g++
|
||||||
|
|
||||||
|
# Clang + Linux disabled for now
|
||||||
|
# - os: linux
|
||||||
|
# dist: xenial
|
||||||
|
# script: python test/run.py
|
||||||
|
# env:
|
||||||
|
# - CC=clang
|
||||||
|
# - CXX=clang++
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
# - os: windows
|
||||||
|
# env:
|
||||||
|
# - CMAKE_PATH="/c/Program Files/CMake/bin"
|
||||||
|
# script:
|
||||||
|
# - cd third_party/zlib
|
||||||
|
# - cmake .
|
||||||
|
# - cmake --build . --target install
|
||||||
|
# - cd ../..
|
||||||
|
# # - cd third_party/mbedtls
|
||||||
|
# # - cmake .
|
||||||
|
# # - cmake --build . --target install
|
||||||
|
# # - cd ../..
|
||||||
|
# - export PATH=$CMAKE_PATH:$PATH
|
||||||
|
# - cd test
|
||||||
|
# - cmake .
|
||||||
|
# - cmake --build --parallel .
|
||||||
|
# - ixwebsocket_unittest.exe
|
||||||
|
# # - python test/run.py
|
13
CMake/FindMbedTLS.cmake
Normal file
13
CMake/FindMbedTLS.cmake
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
find_path(MBEDTLS_INCLUDE_DIRS mbedtls/ssl.h)
|
||||||
|
|
||||||
|
find_library(MBEDTLS_LIBRARY mbedtls)
|
||||||
|
find_library(MBEDX509_LIBRARY mbedx509)
|
||||||
|
find_library(MBEDCRYPTO_LIBRARY mbedcrypto)
|
||||||
|
|
||||||
|
set(MBEDTLS_LIBRARIES "${MBEDTLS_LIBRARY}" "${MBEDX509_LIBRARY}" "${MBEDCRYPTO_LIBRARY}")
|
||||||
|
|
||||||
|
include(FindPackageHandleStandardArgs)
|
||||||
|
find_package_handle_standard_args(MBEDTLS DEFAULT_MSG
|
||||||
|
MBEDTLS_INCLUDE_DIRS MBEDTLS_LIBRARY MBEDX509_LIBRARY MBEDCRYPTO_LIBRARY)
|
||||||
|
|
||||||
|
mark_as_advanced(MBEDTLS_INCLUDE_DIRS MBEDTLS_LIBRARY MBEDX509_LIBRARY MBEDCRYPTO_LIBRARY)
|
208
CMakeLists.txt
Normal file
208
CMakeLists.txt
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
#
|
||||||
|
# Author: Benjamin Sergeant
|
||||||
|
# Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
cmake_minimum_required(VERSION 3.4.1)
|
||||||
|
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMake;${CMAKE_MODULE_PATH}")
|
||||||
|
|
||||||
|
project(ixwebsocket C CXX)
|
||||||
|
|
||||||
|
set (CMAKE_CXX_STANDARD 14)
|
||||||
|
set (CXX_STANDARD_REQUIRED ON)
|
||||||
|
set (CMAKE_CXX_EXTENSIONS OFF)
|
||||||
|
|
||||||
|
# -Wshorten-64-to-32 does not work with clang
|
||||||
|
if (NOT WIN32)
|
||||||
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pedantic")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang")
|
||||||
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wshorten-64-to-32")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set( IXWEBSOCKET_SOURCES
|
||||||
|
ixwebsocket/IXCancellationRequest.cpp
|
||||||
|
ixwebsocket/IXConnectionState.cpp
|
||||||
|
ixwebsocket/IXDNSLookup.cpp
|
||||||
|
ixwebsocket/IXExponentialBackoff.cpp
|
||||||
|
ixwebsocket/IXHttp.cpp
|
||||||
|
ixwebsocket/IXHttpClient.cpp
|
||||||
|
ixwebsocket/IXHttpServer.cpp
|
||||||
|
ixwebsocket/IXNetSystem.cpp
|
||||||
|
ixwebsocket/IXSelectInterrupt.cpp
|
||||||
|
ixwebsocket/IXSelectInterruptFactory.cpp
|
||||||
|
ixwebsocket/IXSocket.cpp
|
||||||
|
ixwebsocket/IXSocketConnect.cpp
|
||||||
|
ixwebsocket/IXSocketFactory.cpp
|
||||||
|
ixwebsocket/IXSocketServer.cpp
|
||||||
|
ixwebsocket/IXUrlParser.cpp
|
||||||
|
ixwebsocket/IXUserAgent.cpp
|
||||||
|
ixwebsocket/IXWebSocket.cpp
|
||||||
|
ixwebsocket/IXWebSocketCloseConstants.cpp
|
||||||
|
ixwebsocket/IXWebSocketHandshake.cpp
|
||||||
|
ixwebsocket/IXWebSocketHttpHeaders.cpp
|
||||||
|
ixwebsocket/IXWebSocketMessageQueue.cpp
|
||||||
|
ixwebsocket/IXWebSocketPerMessageDeflate.cpp
|
||||||
|
ixwebsocket/IXWebSocketPerMessageDeflateCodec.cpp
|
||||||
|
ixwebsocket/IXWebSocketPerMessageDeflateOptions.cpp
|
||||||
|
ixwebsocket/IXWebSocketServer.cpp
|
||||||
|
ixwebsocket/IXWebSocketTransport.cpp
|
||||||
|
ixwebsocket/LUrlParser.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
set( IXWEBSOCKET_HEADERS
|
||||||
|
ixwebsocket/IXCancellationRequest.h
|
||||||
|
ixwebsocket/IXConnectionState.h
|
||||||
|
ixwebsocket/IXDNSLookup.h
|
||||||
|
ixwebsocket/IXExponentialBackoff.h
|
||||||
|
ixwebsocket/IXHttp.h
|
||||||
|
ixwebsocket/IXHttpClient.h
|
||||||
|
ixwebsocket/IXHttpServer.h
|
||||||
|
ixwebsocket/IXNetSystem.h
|
||||||
|
ixwebsocket/IXProgressCallback.h
|
||||||
|
ixwebsocket/IXSelectInterrupt.h
|
||||||
|
ixwebsocket/IXSelectInterruptFactory.h
|
||||||
|
ixwebsocket/IXSetThreadName.h
|
||||||
|
ixwebsocket/IXSocket.h
|
||||||
|
ixwebsocket/IXSocketConnect.h
|
||||||
|
ixwebsocket/IXSocketFactory.h
|
||||||
|
ixwebsocket/IXSocketServer.h
|
||||||
|
ixwebsocket/IXUrlParser.h
|
||||||
|
ixwebsocket/IXUserAgent.h
|
||||||
|
ixwebsocket/IXWebSocket.h
|
||||||
|
ixwebsocket/IXWebSocketCloseConstants.h
|
||||||
|
ixwebsocket/IXWebSocketCloseInfo.h
|
||||||
|
ixwebsocket/IXWebSocketErrorInfo.h
|
||||||
|
ixwebsocket/IXWebSocketHandshake.h
|
||||||
|
ixwebsocket/IXWebSocketHttpHeaders.h
|
||||||
|
ixwebsocket/IXWebSocketMessage.h
|
||||||
|
ixwebsocket/IXWebSocketMessageQueue.h
|
||||||
|
ixwebsocket/IXWebSocketMessageType.h
|
||||||
|
ixwebsocket/IXWebSocketOpenInfo.h
|
||||||
|
ixwebsocket/IXWebSocketPerMessageDeflate.h
|
||||||
|
ixwebsocket/IXWebSocketPerMessageDeflateCodec.h
|
||||||
|
ixwebsocket/IXWebSocketPerMessageDeflateOptions.h
|
||||||
|
ixwebsocket/IXWebSocketSendInfo.h
|
||||||
|
ixwebsocket/IXWebSocketServer.h
|
||||||
|
ixwebsocket/IXWebSocketTransport.h
|
||||||
|
ixwebsocket/IXWebSocketVersion.h
|
||||||
|
ixwebsocket/LUrlParser.h
|
||||||
|
ixwebsocket/libwshandshake.hpp
|
||||||
|
)
|
||||||
|
|
||||||
|
if (UNIX)
|
||||||
|
# Linux, Mac, iOS, Android
|
||||||
|
list( APPEND IXWEBSOCKET_SOURCES ixwebsocket/IXSelectInterruptPipe.cpp )
|
||||||
|
list( APPEND IXWEBSOCKET_SOURCES ixwebsocket/IXSelectInterruptPipe.h )
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Platform specific code
|
||||||
|
if (APPLE)
|
||||||
|
list( APPEND IXWEBSOCKET_SOURCES ixwebsocket/apple/IXSetThreadName_apple.cpp)
|
||||||
|
elseif (WIN32)
|
||||||
|
list( APPEND IXWEBSOCKET_SOURCES ixwebsocket/windows/IXSetThreadName_windows.cpp)
|
||||||
|
else()
|
||||||
|
list( APPEND IXWEBSOCKET_SOURCES ixwebsocket/linux/IXSetThreadName_linux.cpp)
|
||||||
|
list( APPEND IXWEBSOCKET_SOURCES ixwebsocket/IXSelectInterruptEventFd.cpp)
|
||||||
|
list( APPEND IXWEBSOCKET_HEADERS ixwebsocket/IXSelectInterruptEventFd.h)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (WIN32)
|
||||||
|
set(USE_MBED_TLS TRUE)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(USE_OPEN_SSL FALSE)
|
||||||
|
if (USE_TLS)
|
||||||
|
add_definitions(-DIXWEBSOCKET_USE_TLS)
|
||||||
|
|
||||||
|
if (USE_MBED_TLS)
|
||||||
|
add_definitions(-DIXWEBSOCKET_USE_MBED_TLS)
|
||||||
|
list( APPEND IXWEBSOCKET_HEADERS ixwebsocket/IXSocketMbedTLS.h)
|
||||||
|
list( APPEND IXWEBSOCKET_SOURCES ixwebsocket/IXSocketMbedTLS.cpp)
|
||||||
|
elseif (APPLE)
|
||||||
|
list( APPEND IXWEBSOCKET_HEADERS ixwebsocket/IXSocketAppleSSL.h)
|
||||||
|
list( APPEND IXWEBSOCKET_SOURCES ixwebsocket/IXSocketAppleSSL.cpp)
|
||||||
|
elseif (WIN32)
|
||||||
|
list( APPEND IXWEBSOCKET_HEADERS ixwebsocket/IXSocketSChannel.h)
|
||||||
|
list( APPEND IXWEBSOCKET_SOURCES ixwebsocket/IXSocketSChannel.cpp)
|
||||||
|
else()
|
||||||
|
add_definitions(-DIXWEBSOCKET_USE_OPEN_SSL)
|
||||||
|
set(USE_OPEN_SSL TRUE)
|
||||||
|
list( APPEND IXWEBSOCKET_HEADERS ixwebsocket/IXSocketOpenSSL.h)
|
||||||
|
list( APPEND IXWEBSOCKET_SOURCES ixwebsocket/IXSocketOpenSSL.cpp)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_library( ixwebsocket STATIC
|
||||||
|
${IXWEBSOCKET_SOURCES}
|
||||||
|
${IXWEBSOCKET_HEADERS}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (APPLE AND USE_TLS AND NOT USE_MBED_TLS)
|
||||||
|
target_link_libraries(ixwebsocket "-framework foundation" "-framework security")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (WIN32)
|
||||||
|
target_link_libraries(ixwebsocket wsock32 ws2_32)
|
||||||
|
add_definitions(-D_CRT_SECURE_NO_WARNINGS)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (UNIX)
|
||||||
|
find_package(Threads)
|
||||||
|
target_link_libraries(ixwebsocket ${CMAKE_THREAD_LIBS_INIT})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (USE_TLS AND USE_OPEN_SSL)
|
||||||
|
find_package(OpenSSL REQUIRED)
|
||||||
|
add_definitions(${OPENSSL_DEFINITIONS})
|
||||||
|
message(STATUS "OpenSSL: " ${OPENSSL_VERSION})
|
||||||
|
include_directories(${OPENSSL_INCLUDE_DIR})
|
||||||
|
target_link_libraries(ixwebsocket ${OPENSSL_LIBRARIES})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (USE_TLS AND USE_MBED_TLS)
|
||||||
|
if (USE_VENDORED_THIRD_PARTY)
|
||||||
|
set (ENABLE_PROGRAMS OFF)
|
||||||
|
add_subdirectory(third_party/mbedtls)
|
||||||
|
include_directories(third_party/mbedtls/include)
|
||||||
|
|
||||||
|
target_link_libraries(ixwebsocket mbedtls)
|
||||||
|
else()
|
||||||
|
find_package(MbedTLS REQUIRED)
|
||||||
|
include_directories(${MBEDTLS_INCLUDE_DIRS})
|
||||||
|
target_link_libraries(ixwebsocket ${MBEDTLS_LIBRARIES})
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
find_package(ZLIB)
|
||||||
|
if (ZLIB_FOUND)
|
||||||
|
include_directories(${ZLIB_INCLUDE_DIRS})
|
||||||
|
target_link_libraries(ixwebsocket ${ZLIB_LIBRARIES})
|
||||||
|
else()
|
||||||
|
add_subdirectory(third_party/zlib)
|
||||||
|
include_directories(third_party/zlib ${CMAKE_CURRENT_BINARY_DIR}/third_party/zlib)
|
||||||
|
target_link_libraries(ixwebsocket zlibstatic)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set( IXWEBSOCKET_INCLUDE_DIRS
|
||||||
|
.
|
||||||
|
)
|
||||||
|
|
||||||
|
if (CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
|
||||||
|
# Build with Multiple Processes
|
||||||
|
target_compile_options(ixwebsocket PRIVATE /MP)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_include_directories(ixwebsocket PUBLIC ${IXWEBSOCKET_INCLUDE_DIRS})
|
||||||
|
|
||||||
|
set_target_properties(ixwebsocket PROPERTIES PUBLIC_HEADER "${IXWEBSOCKET_HEADERS}")
|
||||||
|
|
||||||
|
install(TARGETS ixwebsocket
|
||||||
|
ARCHIVE DESTINATION ${CMAKE_INSTALL_PREFIX}/lib
|
||||||
|
PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_PREFIX}/include/ixwebsocket/
|
||||||
|
)
|
||||||
|
|
||||||
|
if (USE_WS)
|
||||||
|
add_subdirectory(ws)
|
||||||
|
endif()
|
1
DOCKER_VERSION
Normal file
1
DOCKER_VERSION
Normal file
@ -0,0 +1 @@
|
|||||||
|
5.1.4
|
1
Dockerfile
Symbolic link
1
Dockerfile
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
docker/Dockerfile.alpine
|
165
README.md
165
README.md
@ -1,164 +1,13 @@
|
|||||||
# General
|
## Hello world
|
||||||
|
|
||||||
## Introduction
|

|
||||||
|
|
||||||
[*WebSocket*](https://en.wikipedia.org/wiki/WebSocket) is a computer communications protocol, providing full-duplex
|
IXWebSocket is a C++ library for WebSocket client and server development. It has minimal dependencies (no boost), is very simple to use and support everything you'll likely need for websocket dev (SSL, deflate compression, compiles on most platforms, etc...). HTTP client and server code is also available, but it hasn't received as much testing.
|
||||||
communication channels over a single TCP connection. This library provides a C++ library for Websocket communication. The code is derived from [easywsclient](https://github.com/dhbaird/easywsclient).
|
|
||||||
|
|
||||||
## Examples
|
It is been used on big mobile video game titles sending and receiving tons of messages since 2017 (iOS and Android).
|
||||||
|
|
||||||
The examples folder countains a simple chat program, using a node.js broadcast server.
|
Interested ? Go read the [docs](https://bsergean.github.io/IXWebSocket/site/) ! If things don't work as expected, please create an issue in github, or even better a pull request if you know how to fix your problem.
|
||||||
|
|
||||||
Here is what the API looks like.
|
IXWebSocket is actively being developed, check out the [changelog](CHANGELOG.md) to know what's cooking. If you are looking for a real time messaging service (the chat-like 'server' your websocket code will talk to) with many features such as history, backed by Redis, look at [cobra](https://github.com/machinezone/cobra).
|
||||||
|
|
||||||
```
|
IXWebSocket is not yet autobahn compliant, but we are working on changing this. See the current compliance [test results](https://bsergean.github.io/IXWebSocket/autobahn/index.html).
|
||||||
ix::WebSocket webSocket;
|
|
||||||
|
|
||||||
std::string url("ws://localhost:8080/");
|
|
||||||
webSocket.configure(url);
|
|
||||||
|
|
||||||
// Setup a callback to be fired when a message or an event (open, close, error) is received
|
|
||||||
webSocket.setOnMessageCallback(
|
|
||||||
[](ix::WebSocketMessageType messageType, const std::string& str, ix::WebSocketErrorInfo error)
|
|
||||||
{
|
|
||||||
if (messageType == ix::WebSocket_MessageType_Message)
|
|
||||||
{
|
|
||||||
std::cout << str << std::endl;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Now that our callback is setup, we can start our background thread and receive messages
|
|
||||||
webSocket.start();
|
|
||||||
|
|
||||||
// Send a message to the server
|
|
||||||
webSocket.send("hello world");
|
|
||||||
|
|
||||||
// ... finally ...
|
|
||||||
|
|
||||||
// Stop the connection
|
|
||||||
webSocket:stop()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Implementation details
|
|
||||||
|
|
||||||
### TLS/SSL
|
|
||||||
|
|
||||||
Connections can be optionally secured and encrypted with TLS/SSL when using a wss:// endpoint, or using normal un-encrypted socket with ws:// endpoints. AppleSSL is used on iOS and OpenSSL is used on Android.
|
|
||||||
|
|
||||||
### Polling and background thread work
|
|
||||||
|
|
||||||
No manual polling to fetch data is required. Data is sent and received instantly by using a background thread for receiving data and the select [system](http://man7.org/linux/man-pages/man2/select.2.html) call to be notified by the OS of incoming data. No timeout is used for select so that the background thread is only woken up when data is available, to optimize battery life. This is also the recommended way of using select according to the select tutorial, section [select law](https://linux.die.net/man/2/select_tut). Read and Writes to the socket are non blocking. Data is sent right away and not enqueued by writing directly to the socket, which is [possible](https://stackoverflow.com/questions/1981372/are-parallel-calls-to-send-recv-on-the-same-socket-valid) since system socket implementations allow concurrent read/writes. However concurrent writes need to be protected with mutex.
|
|
||||||
|
|
||||||
### Automatic reconnection
|
|
||||||
|
|
||||||
If the remote end (server) breaks the connection, the code will try to perpetually reconnect, by using an exponential backoff strategy, capped at one retry every 10 seconds.
|
|
||||||
|
|
||||||
## Limitations
|
|
||||||
|
|
||||||
* There is no per message compression support. That could be useful for retrieving large messages, but could also be implemented at the application level.
|
|
||||||
* There is no text support for sending data, only the binary protocol is supported. Sending json or text over the binary protocol works well.
|
|
||||||
* Automatic reconnection works at the TCP socket level, and will detect remote end disconnects. However, if the device/computer network become unreachable (by turning off wifi), it is quite hard to reliably and timely detect it at the socket level using `recv` and `send` error codes. [Here](https://stackoverflow.com/questions/14782143/linux-socket-how-to-detect-disconnected-network-in-a-client-program) is a good discussion on the subject. This behavior is consistent with other runtimes such as node.js. One way to detect a disconnected device with low level C code is to do a name resolution with DNS but this can be expensive. Mobile devices have good and reliable API to do that.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
1. Bring up a terminal and jump to the examples folder.
|
|
||||||
2. Compile the example C++ code. `sh build.sh`
|
|
||||||
3. Install node.js from [here](https://nodejs.org/en/download/).
|
|
||||||
4. Type `npm install` to install the node.js dependencies. Then `node broadcast-server.js` to run the server.
|
|
||||||
5. Bring up a second terminal. `env USER=bob ./cmd_websocket_chat`
|
|
||||||
6. Bring up a third terminal. `env USER=bill ./cmd_websocket_chat`
|
|
||||||
7. Start typing things in any of those terminals. Hopefully you should see your message being received on the other end.
|
|
||||||
|
|
||||||
## C++ code organization
|
|
||||||
|
|
||||||
Here's a simplistic diagram which explains how the code is structured in term of class/modules.
|
|
||||||
|
|
||||||
```
|
|
||||||
+-----------------------+
|
|
||||||
| | Start the receiving Background thread. Auto reconnection. Simple websocket Ping.
|
|
||||||
| IXWebSocket | Interface used by C++ test clients. No IX dependencies.
|
|
||||||
| |
|
|
||||||
+-----------------------+
|
|
||||||
| |
|
|
||||||
| IXWebSocketTransport | Low level websocket code, framing, managing raw socket. Adapted from easywsclient.
|
|
||||||
| |
|
|
||||||
+-----------------------+
|
|
||||||
| |
|
|
||||||
| IXWebSocket | ws:// Unencrypted Socket handler
|
|
||||||
| IXWebSocketAppleSSL | wss:// TLS encrypted Socket AppleSSL handler. Used on iOS and macOS
|
|
||||||
| IXWebSocketOpenSSL | wss:// TLS encrypted Socket OpenSSL handler. Used on Android and Linux
|
|
||||||
| | Can be used on macOS too.
|
|
||||||
+-----------------------+
|
|
||||||
```
|
|
||||||
|
|
||||||
## Advanced usage
|
|
||||||
|
|
||||||
### Sending messages
|
|
||||||
|
|
||||||
`websocket.send("foo")` will send a message.
|
|
||||||
|
|
||||||
If the connection was closed and sending failed, the return value will be set to false.
|
|
||||||
|
|
||||||
### ReadyState
|
|
||||||
|
|
||||||
`getReadyState()` returns the state of the connection. There are 4 possible states.
|
|
||||||
|
|
||||||
1. WebSocket_ReadyState_Connecting - The connection is not yet open.
|
|
||||||
2. WebSocket_ReadyState_Open - The connection is open and ready to communicate.
|
|
||||||
3. WebSocket_ReadyState_Closing - The connection is in the process of closing.
|
|
||||||
4. WebSocket_MessageType_Close - The connection is closed or couldn't be opened.
|
|
||||||
|
|
||||||
### Open and Close notifications
|
|
||||||
|
|
||||||
The onMessage event will be fired when the connection is opened or closed. This is similar to the [Javascript browser API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket), which has `open` and `close` events notification that can be registered with the browser `addEventListener`.
|
|
||||||
|
|
||||||
```
|
|
||||||
webSocket.setOnMessageCallback(
|
|
||||||
[this](ix::WebSocketMessageType messageType, const std::string& str, ix::WebSocketErrorInfo error)
|
|
||||||
{
|
|
||||||
if (messageType == ix::WebSocket_MessageType_Open)
|
|
||||||
{
|
|
||||||
puts("send greetings");
|
|
||||||
}
|
|
||||||
else if (messageType == ix::WebSocket_MessageType_Close)
|
|
||||||
{
|
|
||||||
puts("disconnected");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error notification
|
|
||||||
|
|
||||||
A message will be fired when there is an error with the connection. The message type will be `ix::WebSocket_MessageType_Error`. Multiple fields will be available on the event to describe the error.
|
|
||||||
|
|
||||||
```
|
|
||||||
webSocket.setOnMessageCallback(
|
|
||||||
[this](ix::WebSocketMessageType messageType, const std::string& str, ix::WebSocketErrorInfo error)
|
|
||||||
{
|
|
||||||
if (messageType == ix::WebSocket_MessageType_Error)
|
|
||||||
{
|
|
||||||
std::stringstream ss;
|
|
||||||
ss << "Error: " << error.reason << std::endl;
|
|
||||||
ss << "#retries: " << event.retries << std::endl;
|
|
||||||
ss << "Wait time(ms): " << event.wait_time << std::endl;
|
|
||||||
ss << "HTTP Status: " << event.http_status << std::endl;
|
|
||||||
std::cout << ss.str() << std::endl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### start, stop
|
|
||||||
|
|
||||||
1. `websocket.start()` connect to the remote server and starts the message receiving background thread.
|
|
||||||
2. `websocket.stop()` disconnect from the remote server and closes the background thread.
|
|
||||||
|
|
||||||
### Configuring the remote url
|
|
||||||
|
|
||||||
The url can be set and queried after a websocket object has been created. You will have to call `stop` and `start` if you want to disconnect and connect to that new url.
|
|
||||||
|
|
||||||
```
|
|
||||||
std::string url("wss://example.com");
|
|
||||||
websocket.configure(url);
|
|
||||||
```
|
|
||||||
|
14
appveyor.yml
Normal file
14
appveyor.yml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
image:
|
||||||
|
- Visual Studio 2017
|
||||||
|
|
||||||
|
install:
|
||||||
|
- ls -al
|
||||||
|
- cmd: call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvars64.bat"
|
||||||
|
- cd test
|
||||||
|
- mkdir build
|
||||||
|
- cd build
|
||||||
|
- cmake -G"NMake Makefiles" ..
|
||||||
|
- nmake
|
||||||
|
- ixwebsocket_unittest.exe
|
||||||
|
|
||||||
|
build: off
|
43
docker-compose.yml
Normal file
43
docker-compose.yml
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
snake:
|
||||||
|
image: bsergean/ws:build
|
||||||
|
entrypoint: ws snake --port 8765 --host 0.0.0.0 --redis_hosts redis1
|
||||||
|
ports:
|
||||||
|
- "8765:8765"
|
||||||
|
networks:
|
||||||
|
- ws-net
|
||||||
|
depends_on:
|
||||||
|
- redis1
|
||||||
|
|
||||||
|
ws:
|
||||||
|
security_opt:
|
||||||
|
- seccomp:unconfined
|
||||||
|
cap_add:
|
||||||
|
- SYS_PTRACE
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
image: bsergean/ws:build
|
||||||
|
entrypoint: bash
|
||||||
|
networks:
|
||||||
|
- ws-net
|
||||||
|
depends_on:
|
||||||
|
- redis1
|
||||||
|
|
||||||
|
redis1:
|
||||||
|
image: redis:alpine
|
||||||
|
networks:
|
||||||
|
- ws-net
|
||||||
|
|
||||||
|
statsd:
|
||||||
|
image: jaconel/statsd
|
||||||
|
ports:
|
||||||
|
- "8125:8125"
|
||||||
|
environment:
|
||||||
|
- STATSD_DUMP_MSG=true
|
||||||
|
- GRAPHITE_HOST=127.0.0.1
|
||||||
|
networks:
|
||||||
|
- ws-net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
ws-net:
|
33
docker/Dockerfile.alpine
Normal file
33
docker/Dockerfile.alpine
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
FROM alpine as build
|
||||||
|
|
||||||
|
RUN apk add --no-cache gcc g++ musl-dev linux-headers cmake openssl-dev
|
||||||
|
RUN apk add --no-cache make
|
||||||
|
RUN apk add --no-cache zlib-dev
|
||||||
|
|
||||||
|
RUN addgroup -S app && adduser -S -G app app
|
||||||
|
RUN chown -R app:app /opt
|
||||||
|
RUN chown -R app:app /usr/local
|
||||||
|
|
||||||
|
# There is a bug in CMake where we cannot build from the root top folder
|
||||||
|
# So we build from /opt
|
||||||
|
COPY --chown=app:app . /opt
|
||||||
|
WORKDIR /opt
|
||||||
|
|
||||||
|
USER app
|
||||||
|
RUN [ "make" ]
|
||||||
|
|
||||||
|
FROM alpine as runtime
|
||||||
|
|
||||||
|
RUN apk add --no-cache libstdc++
|
||||||
|
|
||||||
|
RUN addgroup -S app && adduser -S -G app app
|
||||||
|
COPY --chown=app:app --from=build /usr/local/bin/ws /usr/local/bin/ws
|
||||||
|
RUN chmod +x /usr/local/bin/ws
|
||||||
|
RUN ldd /usr/local/bin/ws
|
||||||
|
|
||||||
|
# Now run in usermode
|
||||||
|
USER app
|
||||||
|
WORKDIR /home/app
|
||||||
|
|
||||||
|
ENTRYPOINT ["ws"]
|
||||||
|
CMD ["--help"]
|
52
docker/Dockerfile.debian
Normal file
52
docker/Dockerfile.debian
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# Build time
|
||||||
|
FROM debian:buster as build
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get -y install wget
|
||||||
|
RUN mkdir -p /tmp/cmake
|
||||||
|
WORKDIR /tmp/cmake
|
||||||
|
RUN wget https://github.com/Kitware/CMake/releases/download/v3.14.0/cmake-3.14.0-Linux-x86_64.tar.gz
|
||||||
|
RUN tar zxf cmake-3.14.0-Linux-x86_64.tar.gz
|
||||||
|
|
||||||
|
RUN apt-get -y install g++
|
||||||
|
RUN apt-get -y install libssl-dev
|
||||||
|
RUN apt-get -y install libz-dev
|
||||||
|
RUN apt-get -y install make
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ARG CMAKE_BIN_PATH=/tmp/cmake/cmake-3.14.0-Linux-x86_64/bin
|
||||||
|
ENV PATH="${CMAKE_BIN_PATH}:${PATH}"
|
||||||
|
|
||||||
|
RUN ["make"]
|
||||||
|
|
||||||
|
# Runtime
|
||||||
|
FROM debian:buster as runtime
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
|
RUN apt-get update
|
||||||
|
# Runtime
|
||||||
|
RUN apt-get install -y libssl1.1
|
||||||
|
RUN apt-get install -y ca-certificates
|
||||||
|
RUN ["update-ca-certificates"]
|
||||||
|
|
||||||
|
# Debugging
|
||||||
|
RUN apt-get install -y strace
|
||||||
|
RUN apt-get install -y procps
|
||||||
|
RUN apt-get install -y htop
|
||||||
|
|
||||||
|
RUN adduser --disabled-password --gecos '' app
|
||||||
|
COPY --chown=app:app --from=build /usr/local/bin/ws /usr/local/bin/ws
|
||||||
|
RUN chmod +x /usr/local/bin/ws
|
||||||
|
RUN ldd /usr/local/bin/ws
|
||||||
|
|
||||||
|
# Now run in usermode
|
||||||
|
USER app
|
||||||
|
WORKDIR /home/app
|
||||||
|
|
||||||
|
COPY --chown=app:app ws/snake/appsConfig.json .
|
||||||
|
COPY --chown=app:app ws/cobraMetricsSample.json .
|
||||||
|
|
||||||
|
ENTRYPOINT ["ws"]
|
||||||
|
CMD ["--help"]
|
43
docker/Dockerfile.fedora
Normal file
43
docker/Dockerfile.fedora
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
FROM fedora:30 as build
|
||||||
|
|
||||||
|
RUN yum install -y gcc-g++
|
||||||
|
RUN yum install -y cmake
|
||||||
|
RUN yum install -y make
|
||||||
|
RUN yum install -y openssl-devel
|
||||||
|
|
||||||
|
RUN yum install -y wget
|
||||||
|
RUN mkdir -p /tmp/cmake
|
||||||
|
WORKDIR /tmp/cmake
|
||||||
|
RUN wget https://github.com/Kitware/CMake/releases/download/v3.14.0/cmake-3.14.0-Linux-x86_64.tar.gz
|
||||||
|
RUN tar zxf cmake-3.14.0-Linux-x86_64.tar.gz
|
||||||
|
|
||||||
|
ARG CMAKE_BIN_PATH=/tmp/cmake/cmake-3.14.0-Linux-x86_64/bin
|
||||||
|
ENV PATH="${CMAKE_BIN_PATH}:${PATH}"
|
||||||
|
|
||||||
|
RUN yum install -y python
|
||||||
|
RUN yum install -y libtsan
|
||||||
|
RUN yum install -y zlib-devel
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
# RUN ["make", "test"]
|
||||||
|
RUN ["make"]
|
||||||
|
|
||||||
|
# Runtime
|
||||||
|
FROM fedora:30 as runtime
|
||||||
|
|
||||||
|
RUN yum install -y libtsan
|
||||||
|
|
||||||
|
RUN groupadd app && useradd -g app app
|
||||||
|
COPY --chown=app:app --from=build /usr/local/bin/ws /usr/local/bin/ws
|
||||||
|
RUN chmod +x /usr/local/bin/ws
|
||||||
|
RUN ldd /usr/local/bin/ws
|
||||||
|
|
||||||
|
# Now run in usermode
|
||||||
|
USER app
|
||||||
|
WORKDIR /home/app
|
||||||
|
|
||||||
|
COPY --chown=app:app ws/snake/appsConfig.json .
|
||||||
|
COPY --chown=app:app ws/cobraMetricsSample.json .
|
||||||
|
|
||||||
|
ENTRYPOINT ["ws"]
|
||||||
|
CMD ["--help"]
|
23
docker/Dockerfile.ubuntu_bionic
Normal file
23
docker/Dockerfile.ubuntu_bionic
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Build time
|
||||||
|
FROM ubuntu:bionic as build
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get -y install wget
|
||||||
|
RUN mkdir -p /tmp/cmake
|
||||||
|
WORKDIR /tmp/cmake
|
||||||
|
RUN wget https://github.com/Kitware/CMake/releases/download/v3.14.0/cmake-3.14.0-Linux-x86_64.tar.gz
|
||||||
|
RUN tar zxf cmake-3.14.0-Linux-x86_64.tar.gz
|
||||||
|
|
||||||
|
RUN apt-get -y install g++
|
||||||
|
RUN apt-get -y install libssl-dev
|
||||||
|
RUN apt-get -y install libz-dev
|
||||||
|
RUN apt-get -y install make
|
||||||
|
RUN apt-get -y install python
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ARG CMAKE_BIN_PATH=/tmp/cmake/cmake-3.14.0-Linux-x86_64/bin
|
||||||
|
ENV PATH="${CMAKE_BIN_PATH}:${PATH}"
|
||||||
|
|
||||||
|
RUN ["make", "ws"]
|
23
docker/Dockerfile.ubuntu_disco
Normal file
23
docker/Dockerfile.ubuntu_disco
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Build time
|
||||||
|
FROM ubuntu:disco as build
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get -y install wget
|
||||||
|
RUN mkdir -p /tmp/cmake
|
||||||
|
WORKDIR /tmp/cmake
|
||||||
|
RUN wget https://github.com/Kitware/CMake/releases/download/v3.14.0/cmake-3.14.0-Linux-x86_64.tar.gz
|
||||||
|
RUN tar zxf cmake-3.14.0-Linux-x86_64.tar.gz
|
||||||
|
|
||||||
|
RUN apt-get -y install g++
|
||||||
|
RUN apt-get -y install libssl-dev
|
||||||
|
RUN apt-get -y install libz-dev
|
||||||
|
RUN apt-get -y install make
|
||||||
|
RUN apt-get -y install python
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ARG CMAKE_BIN_PATH=/tmp/cmake/cmake-3.14.0-Linux-x86_64/bin
|
||||||
|
ENV PATH="${CMAKE_BIN_PATH}:${PATH}"
|
||||||
|
|
||||||
|
RUN ["make", "test"]
|
24
docker/Dockerfile.ubuntu_xenial
Normal file
24
docker/Dockerfile.ubuntu_xenial
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Build time
|
||||||
|
FROM ubuntu:xenial as build
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get -y install wget
|
||||||
|
RUN mkdir -p /tmp/cmake
|
||||||
|
WORKDIR /tmp/cmake
|
||||||
|
RUN wget https://github.com/Kitware/CMake/releases/download/v3.14.0/cmake-3.14.0-Linux-x86_64.tar.gz
|
||||||
|
RUN tar zxf cmake-3.14.0-Linux-x86_64.tar.gz
|
||||||
|
|
||||||
|
RUN apt-get -y install g++
|
||||||
|
RUN apt-get -y install libssl-dev
|
||||||
|
RUN apt-get -y install libz-dev
|
||||||
|
RUN apt-get -y install make
|
||||||
|
RUN apt-get -y install python
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ARG CMAKE_BIN_PATH=/tmp/cmake/cmake-3.14.0-Linux-x86_64/bin
|
||||||
|
ENV PATH="${CMAKE_BIN_PATH}:${PATH}"
|
||||||
|
|
||||||
|
# RUN ["make"]
|
||||||
|
RUN ["make", "test"]
|
101
docs/CHANGELOG.md
Normal file
101
docs/CHANGELOG.md
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
# Changelog
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [5.1.4] - 2019-09-03
|
||||||
|
|
||||||
|
Sending invalid UTF-8 TEXT message should fail and close the connection (fix **tons** of autobahn test: 6.X UTF-8 Handling)
|
||||||
|
|
||||||
|
## [5.1.3] - 2019-09-03
|
||||||
|
|
||||||
|
Message type (TEXT or BINARY) is invalid for received fragmented messages (fix autobahn test: 5.3 through 5.8 Fragmentation)
|
||||||
|
|
||||||
|
## [5.1.2] - 2019-09-02
|
||||||
|
|
||||||
|
Ping and Pong messages cannot be fragmented (fix autobahn test: 5.1 and 5.2 Fragmentation)
|
||||||
|
|
||||||
|
## [5.1.1] - 2019-09-01
|
||||||
|
|
||||||
|
Close connections when reserved bits are used (fix autobahn test: 3.X Reserved Bits)
|
||||||
|
|
||||||
|
## [5.1.0] - 2019-08-31
|
||||||
|
|
||||||
|
ws autobahn / Add code to test websocket client compliance with the autobahn test-suite
|
||||||
|
add utf-8 validation code, not hooked up properly yet
|
||||||
|
Ping received with a payload too large (> 125 bytes) trigger a connection closure
|
||||||
|
cobra / add tracking about published messages
|
||||||
|
cobra / publish returns a message id, that can be used when
|
||||||
|
cobra / new message type in the message received handler when publish/ok is received (can be used to implement an ack system).
|
||||||
|
|
||||||
|
## [5.0.9] - 2019-08-30
|
||||||
|
|
||||||
|
User-Agent header is set when not specified.
|
||||||
|
New option to cap the max wait between reconnection attempts. Still default to 10s. (setMaxWaitBetweenReconnectionRetries).
|
||||||
|
|
||||||
|
```
|
||||||
|
ws connect --max_wait 5000 ws://example.com # will only wait 5 seconds max between reconnection attempts
|
||||||
|
```
|
||||||
|
|
||||||
|
## [5.0.7] - 2019-08-23
|
||||||
|
- WebSocket: add new option to pass in extra HTTP headers when connecting.
|
||||||
|
- `ws connect` add new option (-H, works like [curl](https://stackoverflow.com/questions/356705/how-to-send-a-header-using-a-http-request-through-a-curl-call)) to pass in extra HTTP headers when connecting
|
||||||
|
|
||||||
|
If you run against `ws echo_server` you will see the headers being received printed in the terminal.
|
||||||
|
```
|
||||||
|
ws connect -H "foo: bar" -H "baz: buz" ws://127.0.0.1:8008
|
||||||
|
```
|
||||||
|
|
||||||
|
- CobraConnection: sets a unique id field for all messages sent to [cobra](https://github.com/machinezone/cobra).
|
||||||
|
- CobraConnection: sets a counter as a field for each event published.
|
||||||
|
|
||||||
|
## [5.0.6] - 2019-08-22
|
||||||
|
- Windows: silly compile error (poll should be in the global namespace)
|
||||||
|
|
||||||
|
## [5.0.5] - 2019-08-22
|
||||||
|
- Windows: use select instead of WSAPoll, through a poll wrapper
|
||||||
|
|
||||||
|
## [5.0.4] - 2019-08-20
|
||||||
|
- Windows build fixes (there was a problem with the use of ::poll that has a different name on Windows (WSAPoll))
|
||||||
|
|
||||||
|
## [5.0.3] - 2019-08-14
|
||||||
|
- CobraMetricThreadedPublisher _enable flag is an atomic, and CobraMetricsPublisher is enabled by default
|
||||||
|
|
||||||
|
## [5.0.2] - 2019-08-01
|
||||||
|
- ws cobra_subscribe has a new -q (quiet) option
|
||||||
|
- ws cobra_subscribe knows to and display msg stats (count and # of messages received per second)
|
||||||
|
- ws cobra_subscribe, cobra_to_statsd and cobra_to_sentry commands have a new option, --filter to restrict the events they want to receive
|
||||||
|
|
||||||
|
## [5.0.1] - 2019-07-25
|
||||||
|
- ws connect command has a new option to send in binary mode (still default to text)
|
||||||
|
- ws connect command has readline history thanks to libnoise-cpp. Now ws connect one can use using arrows to lookup previous sent messages and edit them
|
||||||
|
|
||||||
|
## [5.0.0] - 2019-06-23
|
||||||
|
### Changed
|
||||||
|
- New HTTP server / still very early. ws gained a new command, httpd can run a simple webserver serving local files.
|
||||||
|
- IXDNSLookup. Uses weak pointer + smart_ptr + shared_from_this instead of static sets + mutex to handle object going away before dns lookup has resolved
|
||||||
|
- cobra_to_sentry / backtraces are reversed and line number is not extracted correctly
|
||||||
|
- mbedtls and zlib are searched with find_package, and we use the vendored version if nothing is found
|
||||||
|
- travis CI uses g++ on Linux
|
||||||
|
|
||||||
|
## [4.0.0] - 2019-06-09
|
||||||
|
### Changed
|
||||||
|
- WebSocket::send() sends message in TEXT mode by default
|
||||||
|
- WebSocketMessage sets a new binary field, which tells whether the received incoming message is binary or text
|
||||||
|
- WebSocket::send takes a third arg, binary which default to true (can be text too)
|
||||||
|
- WebSocket callback only take one object, a const ix::WebSocketMessagePtr& msg
|
||||||
|
- Add explicit WebSocket::sendBinary method
|
||||||
|
- New headers + WebSocketMessage class to hold message data, still not used across the board
|
||||||
|
- Add test/compatibility folder with small servers and clients written in different languages and different libraries to test compatibility.
|
||||||
|
- ws echo_server has a -g option to print a greeting message on connect
|
||||||
|
- IXSocketMbedTLS: better error handling in close and connect
|
||||||
|
|
||||||
|
## [3.1.2] - 2019-06-06
|
||||||
|
### Added
|
||||||
|
- ws connect has a -x option to disable per message deflate
|
||||||
|
- Add WebSocket::disablePerMessageDeflate() option.
|
||||||
|
|
||||||
|
## [3.0.0] - 2019-06-xx
|
||||||
|
### Changed
|
||||||
|
- TLS, aka SSL works on Windows (websocket and http clients)
|
||||||
|
- ws command line tool build on Windows
|
||||||
|
- Async API for HttpClient
|
||||||
|
- HttpClient API changed to use shared_ptr for response and request
|
62
docs/build.md
Normal file
62
docs/build.md
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
## Build
|
||||||
|
|
||||||
|
### CMake
|
||||||
|
|
||||||
|
CMakefiles for the library and the examples are available. This library has few dependencies, so it is possible to just add the source files into your project. Otherwise the usual way will suffice.
|
||||||
|
|
||||||
|
```
|
||||||
|
mkdir build # make a build dir so that you can build out of tree.
|
||||||
|
cd build
|
||||||
|
cmake -DUSE_TLS=1 ..
|
||||||
|
make -j
|
||||||
|
make install # will install to /usr/local on Unix, on macOS it is a good idea to sudo chown -R `whoami`:staff /usr/local
|
||||||
|
```
|
||||||
|
|
||||||
|
Headers and a static library will be installed to the target dir.
|
||||||
|
There is a unittest which can be executed by typing `make test`.
|
||||||
|
|
||||||
|
Options for building:
|
||||||
|
|
||||||
|
* `-DUSE_TLS=1` will enable TLS support
|
||||||
|
* `-DUSE_MBED_TLS=1` will use [mbedlts](https://tls.mbed.org/) for the TLS support (default on Windows)
|
||||||
|
* `-DUSE_WS=1` will build the ws interactive command line tool
|
||||||
|
|
||||||
|
### vcpkg
|
||||||
|
|
||||||
|
It is possible to get IXWebSocket through Microsoft [vcpkg](https://github.com/microsoft/vcpkg).
|
||||||
|
|
||||||
|
```
|
||||||
|
vcpkg install ixwebsocket
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conan
|
||||||
|
|
||||||
|
Support for building with conan was contributed by Olivia Zoe (thanks !). The package name to reference is `IXWebSocket/5.0.0@LunarWatcher/stable`. The package is in the process to be published to the official conan package repo, but in the meantime, it can be accessed by adding a new remote
|
||||||
|
|
||||||
|
```
|
||||||
|
conan remote add remote_name_here https://api.bintray.com/conan/oliviazoe0/conan-packages
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
There is a Dockerfile for running the unittest on Linux, and to run the `ws` tool. It is also available on the docker registry.
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run bsergean/ws
|
||||||
|
```
|
||||||
|
|
||||||
|
To use docker-compose you must make a docker container first.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ make docker
|
||||||
|
...
|
||||||
|
$ docker compose up &
|
||||||
|
...
|
||||||
|
$ docker exec -it ixwebsocket_ws_1 bash
|
||||||
|
app@ca2340eb9106:~$ ws --help
|
||||||
|
ws is a websocket tool
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
78
docs/design.md
Normal file
78
docs/design.md
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
## Implementation details
|
||||||
|
|
||||||
|
### Per Message Deflate compression.
|
||||||
|
|
||||||
|
The per message deflate compression option is supported. It can lead to very nice bandbwith savings (20x !) if your messages are similar, which is often the case for example for chat applications. All features of the spec should be supported.
|
||||||
|
|
||||||
|
### TLS/SSL
|
||||||
|
|
||||||
|
Connections can be optionally secured and encrypted with TLS/SSL when using a wss:// endpoint, or using normal un-encrypted socket with ws:// endpoints. AppleSSL is used on iOS and macOS, OpenSSL is used on Android and Linux, mbedTLS is used on Windows.
|
||||||
|
|
||||||
|
### Polling and background thread work
|
||||||
|
|
||||||
|
No manual polling to fetch data is required. Data is sent and received instantly by using a background thread for receiving data and the select [system](http://man7.org/linux/man-pages/man2/select.2.html) call to be notified by the OS of incoming data. No timeout is used for select so that the background thread is only woken up when data is available, to optimize battery life. This is also the recommended way of using select according to the select tutorial, section [select law](https://linux.die.net/man/2/select_tut). Read and Writes to the socket are non blocking. Data is sent right away and not enqueued by writing directly to the socket, which is [possible](https://stackoverflow.com/questions/1981372/are-parallel-calls-to-send-recv-on-the-same-socket-valid) since system socket implementations allow concurrent read/writes. However concurrent writes need to be protected with mutex.
|
||||||
|
|
||||||
|
### Automatic reconnection
|
||||||
|
|
||||||
|
If the remote end (server) breaks the connection, the code will try to perpetually reconnect, by using an exponential backoff strategy, capped at one retry every 10 seconds. This behavior can be disabled.
|
||||||
|
|
||||||
|
### Large messages
|
||||||
|
|
||||||
|
Large frames are broken up into smaller chunks or messages to avoid filling up the os tcp buffers, which is permitted thanks to WebSocket [fragmentation](https://tools.ietf.org/html/rfc6455#section-5.4). Messages up to 1G were sent and received succesfully.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
The library has an interactive tool which is handy for testing compatibility ith other libraries. We have tested our client against Python, Erlang, Node.js, and C++ websocket server libraries.
|
||||||
|
|
||||||
|
The unittest tries to be comprehensive, and has been running on multiple platoform, with different sanitizers such as thread sanitizer to catch data races or the undefined behavior sanitizer.
|
||||||
|
|
||||||
|
The regression test is running after each commit on travis.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
* On Windows TLS is not setup yet to validate certificates.
|
||||||
|
* There is no convenient way to embed a ca cert.
|
||||||
|
* No utf-8 validation is made when sending TEXT message with sendText()
|
||||||
|
* Automatic reconnection works at the TCP socket level, and will detect remote end disconnects. However, if the device/computer network become unreachable (by turning off wifi), it is quite hard to reliably and timely detect it at the socket level using `recv` and `send` error codes. [Here](https://stackoverflow.com/questions/14782143/linux-socket-how-to-detect-disconnected-network-in-a-client-program) is a good discussion on the subject. This behavior is consistent with other runtimes such as node.js. One way to detect a disconnected device with low level C code is to do a name resolution with DNS but this can be expensive. Mobile devices have good and reliable API to do that.
|
||||||
|
* The server code is using select to detect incoming data, and creates one OS thread per connection. This is not as scalable as strategies using epoll or kqueue.
|
||||||
|
|
||||||
|
## C++ code organization
|
||||||
|
|
||||||
|
Here is a simplistic diagram which explains how the code is structured in term of class/modules.
|
||||||
|
|
||||||
|
```
|
||||||
|
+-----------------------+ --- Public
|
||||||
|
| | Start the receiving Background thread. Auto reconnection. Simple websocket Ping.
|
||||||
|
| IXWebSocket | Interface used by C++ test clients. No IX dependencies.
|
||||||
|
| |
|
||||||
|
+-----------------------+
|
||||||
|
| |
|
||||||
|
| IXWebSocketServer | Run a server and give each connections its own WebSocket object.
|
||||||
|
| | Each connection is handled in a new OS thread.
|
||||||
|
| |
|
||||||
|
+-----------------------+ --- Private
|
||||||
|
| |
|
||||||
|
| IXWebSocketTransport | Low level websocket code, framing, managing raw socket. Adapted from easywsclient.
|
||||||
|
| |
|
||||||
|
+-----------------------+
|
||||||
|
| |
|
||||||
|
| IXWebSocketHandshake | Establish the connection between client and server.
|
||||||
|
| |
|
||||||
|
+-----------------------+
|
||||||
|
| |
|
||||||
|
| IXWebSocket | ws:// Unencrypted Socket handler
|
||||||
|
| IXWebSocketAppleSSL | wss:// TLS encrypted Socket AppleSSL handler. Used on iOS and macOS
|
||||||
|
| IXWebSocketOpenSSL | wss:// TLS encrypted Socket OpenSSL handler. Used on Android and Linux
|
||||||
|
| | Can be used on macOS too.
|
||||||
|
+-----------------------+
|
||||||
|
| |
|
||||||
|
| IXSocketConnect | Connect to the remote host (client).
|
||||||
|
| |
|
||||||
|
+-----------------------+
|
||||||
|
| |
|
||||||
|
| IXDNSLookup | Does DNS resolution asynchronously so that it can be interrupted.
|
||||||
|
| |
|
||||||
|
+-----------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
|
46
docs/index.md
Normal file
46
docs/index.md
Normal file
@ -0,0 +1,46 @@
|
|||||||
|

|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
[*WebSocket*](https://en.wikipedia.org/wiki/WebSocket) is a computer communications protocol, providing full-duplex and bi-directionnal communication channels over a single TCP connection. *IXWebSocket* is a C++ library for client and server Websocket communication, and for client and server HTTP communication. *TLS* aka *SSL* is supported. The code is derived from [easywsclient](https://github.com/dhbaird/easywsclient) and from the [Satori C SDK](https://github.com/satori-com/satori-rtm-sdk-c). It has been tested on the following platforms.
|
||||||
|
|
||||||
|
* macOS
|
||||||
|
* iOS
|
||||||
|
* Linux
|
||||||
|
* Android
|
||||||
|
* Windows
|
||||||
|
|
||||||
|
## Example code
|
||||||
|
|
||||||
|
```
|
||||||
|
# Required on Windows
|
||||||
|
ix::initNetSystem();
|
||||||
|
|
||||||
|
# Our websocket object
|
||||||
|
ix::WebSocket webSocket;
|
||||||
|
|
||||||
|
std::string url("ws://localhost:8080/");
|
||||||
|
webSocket.setUrl(url);
|
||||||
|
|
||||||
|
// Setup a callback to be fired when a message or an event (open, close, error) is received
|
||||||
|
webSocket.setOnMessageCallback([](const ix::WebSocketMessagePtr& msg)
|
||||||
|
{
|
||||||
|
if (msg->type == ix::WebSocketMessageType::Message)
|
||||||
|
{
|
||||||
|
std::cout << msg->str << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Now that our callback is setup, we can start our background thread and receive messages
|
||||||
|
webSocket.start();
|
||||||
|
|
||||||
|
// Send a message to the server (default to TEXT mode)
|
||||||
|
webSocket.send("hello world");
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why another library ?
|
||||||
|
|
||||||
|
There are 2 main reasons that explain why IXWebSocket got written. First, we needed a C++ cross-platform client library, which should have few dependencies. What looked like the most solid one, [websocketpp](https://github.com/zaphoyd/websocketpp) did depend on boost and this was not an option for us. Secondly, there were other available libraries with fewer dependencies (C ones), but they required calling an explicit poll routine periodically to know if a client had received data from a server, which was not elegant.
|
||||||
|
|
||||||
|
We started by solving those 2 problems, then we added server websocket code, then an HTTP client, and finally a very simple HTTP server.
|
418
docs/usage.md
Normal file
418
docs/usage.md
Normal file
@ -0,0 +1,418 @@
|
|||||||
|
# Examples
|
||||||
|
|
||||||
|
The [*ws*](https://github.com/machinezone/IXWebSocket/tree/master/ws) folder countains many interactive programs for chat, [file transfers](https://github.com/machinezone/IXWebSocket/blob/master/ws/ws_send.cpp), [curl like](https://github.com/machinezone/IXWebSocket/blob/master/ws/ws_http_client.cpp) http clients, demonstrating client and server usage.
|
||||||
|
|
||||||
|
## Windows note
|
||||||
|
|
||||||
|
To use the network system on Windows, you need to initialize it once with *WSAStartup()* and clean it up with *WSACleanup()*. We have helpers for that which you can use, see below. This init would typically take place in your main function.
|
||||||
|
|
||||||
|
```
|
||||||
|
#include <ixwebsocket/IXNetSystem.h>
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
ix::initNetSystem();
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
ix::uninitNetSystem();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## WebSocket client API
|
||||||
|
|
||||||
|
```
|
||||||
|
#include <ixwebsocket/IXWebSocket.h>
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
# Our websocket object
|
||||||
|
ix::WebSocket webSocket;
|
||||||
|
|
||||||
|
std::string url("ws://localhost:8080/");
|
||||||
|
webSocket.setUrl(url);
|
||||||
|
|
||||||
|
// Optional heart beat, sent every 45 seconds when there is not any traffic
|
||||||
|
// to make sure that load balancers do not kill an idle connection.
|
||||||
|
webSocket.setHeartBeatPeriod(45);
|
||||||
|
|
||||||
|
// Per message deflate connection is enabled by default. You can tweak its parameters or disable it
|
||||||
|
webSocket.disablePerMessageDeflate();
|
||||||
|
|
||||||
|
// Setup a callback to be fired when a message or an event (open, close, error) is received
|
||||||
|
webSocket.setOnMessageCallback([](const ix::WebSocketMessagePtr& msg)
|
||||||
|
{
|
||||||
|
if (msg->type == ix::WebSocketMessageType::Message)
|
||||||
|
{
|
||||||
|
std::cout << msg->str << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Now that our callback is setup, we can start our background thread and receive messages
|
||||||
|
webSocket.start();
|
||||||
|
|
||||||
|
// Send a message to the server (default to TEXT mode)
|
||||||
|
webSocket.send("hello world");
|
||||||
|
|
||||||
|
// The message can be sent in BINARY mode (useful if you send MsgPack data for example)
|
||||||
|
webSocket.sendBinary("some serialized binary data");
|
||||||
|
|
||||||
|
// ... finally ...
|
||||||
|
|
||||||
|
// Stop the connection
|
||||||
|
webSocket.stop()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sending messages
|
||||||
|
|
||||||
|
`websocket.send("foo")` will send a message.
|
||||||
|
|
||||||
|
If the connection was closed and sending failed, the return value will be set to false.
|
||||||
|
|
||||||
|
### ReadyState
|
||||||
|
|
||||||
|
`getReadyState()` returns the state of the connection. There are 4 possible states.
|
||||||
|
|
||||||
|
1. ReadyState::Connecting - The connection is not yet open.
|
||||||
|
2. ReadyState::Open - The connection is open and ready to communicate.
|
||||||
|
3. ReadyState::Closing - The connection is in the process of closing.
|
||||||
|
4. ReadyState::Closed - The connection is closed or could not be opened.
|
||||||
|
|
||||||
|
### Open and Close notifications
|
||||||
|
|
||||||
|
The onMessage event will be fired when the connection is opened or closed. This is similar to the [Javascript browser API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket), which has `open` and `close` events notification that can be registered with the browser `addEventListener`.
|
||||||
|
|
||||||
|
```
|
||||||
|
webSocket.setOnMessageCallback([](const ix::WebSocketMessagePtr& msg)
|
||||||
|
{
|
||||||
|
if (msg->type == ix::WebSocketMessageType::Open)
|
||||||
|
{
|
||||||
|
std::cout << "send greetings" << std::endl;
|
||||||
|
|
||||||
|
// Headers can be inspected (pairs of string/string)
|
||||||
|
std::cout << "Handshake Headers:" << std::endl;
|
||||||
|
for (auto it : msg->headers)
|
||||||
|
{
|
||||||
|
std::cout << it.first << ": " << it.second << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (msg->type == ix::WebSocketMessageType::Close)
|
||||||
|
{
|
||||||
|
std::cout << "disconnected" << std::endl;
|
||||||
|
|
||||||
|
// The server can send an explicit code and reason for closing.
|
||||||
|
// This data can be accessed through the closeInfo object.
|
||||||
|
std::cout << msg->closeInfo.code << std::endl;
|
||||||
|
std::cout << msg->closeInfo.reason << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error notification
|
||||||
|
|
||||||
|
A message will be fired when there is an error with the connection. The message type will be `ix::WebSocketMessageType::Error`. Multiple fields will be available on the event to describe the error.
|
||||||
|
|
||||||
|
```
|
||||||
|
webSocket.setOnMessageCallback([](const ix::WebSocketMessagePtr& msg)
|
||||||
|
{
|
||||||
|
if (msg->type == ix::WebSocketMessageType::Error)
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "Error: " << msg->errorInfo.reason << std::endl;
|
||||||
|
ss << "#retries: " << msg->eventInfo.retries << std::endl;
|
||||||
|
ss << "Wait time(ms): " << msg->eventInfo.wait_time << std::endl;
|
||||||
|
ss << "HTTP Status: " << msg->eventInfo.http_status << std::endl;
|
||||||
|
std::cout << ss.str() << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### start, stop
|
||||||
|
|
||||||
|
1. `websocket.start()` connect to the remote server and starts the message receiving background thread.
|
||||||
|
2. `websocket.stop()` disconnect from the remote server and closes the background thread.
|
||||||
|
|
||||||
|
### Configuring the remote url
|
||||||
|
|
||||||
|
The url can be set and queried after a websocket object has been created. You will have to call `stop` and `start` if you want to disconnect and connect to that new url.
|
||||||
|
|
||||||
|
```
|
||||||
|
std::string url("wss://example.com");
|
||||||
|
websocket.configure(url);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ping/Pong support
|
||||||
|
|
||||||
|
Ping/pong messages are used to implement keep-alive. 2 message types exists to identify ping and pong messages. Note that when a ping message is received, a pong is instantly send back as requested by the WebSocket spec.
|
||||||
|
|
||||||
|
```
|
||||||
|
webSocket.setOnMessageCallback([](const ix::WebSocketMessagePtr& msg)
|
||||||
|
{
|
||||||
|
if (msg->type == ix::WebSocketMessageType::Ping ||
|
||||||
|
msg->type == ix::WebSocketMessageType::Pong)
|
||||||
|
{
|
||||||
|
std::cout << "pong data: " << msg->str << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
A ping message can be sent to the server, with an optional data string.
|
||||||
|
|
||||||
|
```
|
||||||
|
websocket.ping("ping data, optional (empty string is ok): limited to 125 bytes long");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Heartbeat.
|
||||||
|
|
||||||
|
You can configure an optional heart beat / keep-alive, sent every 45 seconds
|
||||||
|
when there is no any traffic to make sure that load balancers do not kill an
|
||||||
|
idle connection.
|
||||||
|
|
||||||
|
```
|
||||||
|
webSocket.setHeartBeatPeriod(45);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supply extra HTTP headers.
|
||||||
|
|
||||||
|
You can set extra HTTP headers to be sent during the WebSocket handshake.
|
||||||
|
|
||||||
|
```
|
||||||
|
WebSocketHttpHeaders headers;
|
||||||
|
headers["foo"] = "bar";
|
||||||
|
webSocket.setExtraHeaders(headers);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Automatic reconnection
|
||||||
|
|
||||||
|
Automatic reconnection kicks in when the connection is disconnected without the user consent. This feature is on by default and can be turned off.
|
||||||
|
|
||||||
|
```
|
||||||
|
webSocket.enableAutomaticReconnection(); // turn on
|
||||||
|
webSocket.disableAutomaticReconnection(); // turn off
|
||||||
|
bool enabled = webSocket.isAutomaticReconnectionEnabled(); // query state
|
||||||
|
```
|
||||||
|
|
||||||
|
The technique to calculate wait time is called [exponential
|
||||||
|
backoff](https://docs.aws.amazon.com/general/latest/gr/api-retries.html). Here
|
||||||
|
are the default waiting times between attempts (from connecting with `ws connect ws://foo.com`)
|
||||||
|
|
||||||
|
```
|
||||||
|
> Connection error: Got bad status connecting to foo.com, status: 301, HTTP Status line: HTTP/1.1 301 Moved Permanently
|
||||||
|
|
||||||
|
#retries: 1
|
||||||
|
Wait time(ms): 100
|
||||||
|
#retries: 2
|
||||||
|
Wait time(ms): 200
|
||||||
|
#retries: 3
|
||||||
|
Wait time(ms): 400
|
||||||
|
#retries: 4
|
||||||
|
Wait time(ms): 800
|
||||||
|
#retries: 5
|
||||||
|
Wait time(ms): 1600
|
||||||
|
#retries: 6
|
||||||
|
Wait time(ms): 3200
|
||||||
|
#retries: 7
|
||||||
|
Wait time(ms): 6400
|
||||||
|
#retries: 8
|
||||||
|
Wait time(ms): 10000
|
||||||
|
```
|
||||||
|
|
||||||
|
The waiting time is capped by default at 10s between 2 attempts, but that value can be changed and queried.
|
||||||
|
|
||||||
|
```
|
||||||
|
webSocket.setMaxWaitBetweenReconnectionRetries(5 * 1000); // 5000ms = 5s
|
||||||
|
uint32_t m = webSocket.getMaxWaitBetweenReconnectionRetries();
|
||||||
|
```
|
||||||
|
|
||||||
|
## WebSocket server API
|
||||||
|
|
||||||
|
```
|
||||||
|
#include <ixwebsocket/IXWebSocketServer.h>
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
// Run a server on localhost at a given port.
|
||||||
|
// Bound host name, max connections and listen backlog can also be passed in as parameters.
|
||||||
|
ix::WebSocketServer server(port);
|
||||||
|
|
||||||
|
server.setOnConnectionCallback(
|
||||||
|
[&server](std::shared_ptr<WebSocket> webSocket,
|
||||||
|
std::shared_ptr<ConnectionState> connectionState)
|
||||||
|
{
|
||||||
|
webSocket->setOnMessageCallback(
|
||||||
|
[webSocket, connectionState, &server](const ix::WebSocketMessagePtr msg)
|
||||||
|
{
|
||||||
|
if (msg->type == ix::WebSocketMessageType::Open)
|
||||||
|
{
|
||||||
|
std::cerr << "New connection" << std::endl;
|
||||||
|
|
||||||
|
// A connection state object is available, and has a default id
|
||||||
|
// You can subclass ConnectionState and pass an alternate factory
|
||||||
|
// to override it. It is useful if you want to store custom
|
||||||
|
// attributes per connection (authenticated bool flag, attributes, etc...)
|
||||||
|
std::cerr << "id: " << connectionState->getId() << std::endl;
|
||||||
|
|
||||||
|
// The uri the client did connect to.
|
||||||
|
std::cerr << "Uri: " << msg->openInfo.uri << std::endl;
|
||||||
|
|
||||||
|
std::cerr << "Headers:" << std::endl;
|
||||||
|
for (auto it : msg->openInfo.headers)
|
||||||
|
{
|
||||||
|
std::cerr << it.first << ": " << it.second << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (msg->type == ix::WebSocketMessageType::Message)
|
||||||
|
{
|
||||||
|
// For an echo server, we just send back to the client whatever was received by the server
|
||||||
|
// All connected clients are available in an std::set. See the broadcast cpp example.
|
||||||
|
// Second parameter tells whether we are sending the message in binary or text mode.
|
||||||
|
// Here we send it in the same mode as it was received.
|
||||||
|
webSocket->send(msg->str, msg->binary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
auto res = server.listen();
|
||||||
|
if (!res.first)
|
||||||
|
{
|
||||||
|
// Error handling
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the server in the background. Server can be stoped by calling server.stop()
|
||||||
|
server.start();
|
||||||
|
|
||||||
|
// Block until server.stop() is called.
|
||||||
|
server.wait();
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## HTTP client API
|
||||||
|
|
||||||
|
```
|
||||||
|
#include <ixwebsocket/IXHttpClient.h>
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
//
|
||||||
|
// Preparation
|
||||||
|
//
|
||||||
|
HttpClient httpClient;
|
||||||
|
HttpRequestArgsPtr args = httpClient.createRequest();
|
||||||
|
|
||||||
|
// Custom headers can be set
|
||||||
|
WebSocketHttpHeaders headers;
|
||||||
|
headers["Foo"] = "bar";
|
||||||
|
args->extraHeaders = headers;
|
||||||
|
|
||||||
|
// Timeout options
|
||||||
|
args->connectTimeout = connectTimeout;
|
||||||
|
args->transferTimeout = transferTimeout;
|
||||||
|
|
||||||
|
// Redirect options
|
||||||
|
args->followRedirects = followRedirects;
|
||||||
|
args->maxRedirects = maxRedirects;
|
||||||
|
|
||||||
|
// Misc
|
||||||
|
args->compress = compress; // Enable gzip compression
|
||||||
|
args->verbose = verbose;
|
||||||
|
args->logger = [](const std::string& msg)
|
||||||
|
{
|
||||||
|
std::cout << msg;
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Synchronous Request
|
||||||
|
//
|
||||||
|
HttpResponsePtr out;
|
||||||
|
std::string url = "https://www.google.com";
|
||||||
|
|
||||||
|
// HEAD request
|
||||||
|
out = httpClient.head(url, args);
|
||||||
|
|
||||||
|
// GET request
|
||||||
|
out = httpClient.get(url, args);
|
||||||
|
|
||||||
|
// POST request with parameters
|
||||||
|
HttpParameters httpParameters;
|
||||||
|
httpParameters["foo"] = "bar";
|
||||||
|
out = httpClient.post(url, httpParameters, args);
|
||||||
|
|
||||||
|
// POST request with a body
|
||||||
|
out = httpClient.post(url, std::string("foo=bar"), args);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Result
|
||||||
|
//
|
||||||
|
auto statusCode = response->statusCode; // Can be HttpErrorCode::Ok, HttpErrorCode::UrlMalformed, etc...
|
||||||
|
auto errorCode = response->errorCode; // 200, 404, etc...
|
||||||
|
auto responseHeaders = response->headers; // All the headers in a special case-insensitive unordered_map of (string, string)
|
||||||
|
auto payload = response->payload; // All the bytes from the response as an std::string
|
||||||
|
auto errorMsg = response->errorMsg; // Descriptive error message in case of failure
|
||||||
|
auto uploadSize = response->uploadSize; // Byte count of uploaded data
|
||||||
|
auto downloadSize = response->downloadSize; // Byte count of downloaded data
|
||||||
|
|
||||||
|
//
|
||||||
|
// Asynchronous Request
|
||||||
|
//
|
||||||
|
bool async = true;
|
||||||
|
HttpClient httpClient(async);
|
||||||
|
auto args = httpClient.createRequest(url, HttpClient::kGet);
|
||||||
|
|
||||||
|
// Push the request to a queue,
|
||||||
|
bool ok = httpClient.performRequest(args, [](const HttpResponsePtr& response)
|
||||||
|
{
|
||||||
|
// This callback execute in a background thread. Make sure you uses appropriate protection such as mutex
|
||||||
|
auto statusCode = response->statusCode; // acess results
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ok will be false if your httpClient is not async
|
||||||
|
```
|
||||||
|
|
||||||
|
## HTTP server API
|
||||||
|
|
||||||
|
```
|
||||||
|
#include <ixwebsocket/IXHttpServer.h>
|
||||||
|
|
||||||
|
ix::HttpServer server(port, hostname);
|
||||||
|
|
||||||
|
auto res = server.listen();
|
||||||
|
if (!res.first)
|
||||||
|
{
|
||||||
|
std::cerr << res.second << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
server.start();
|
||||||
|
server.wait();
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to handle how requests are processed, implement the setOnConnectionCallback callback, which takes an HttpRequestPtr as input, and returns an HttpResponsePtr. You can look at HttpServer::setDefaultConnectionCallback for a slightly more advanced callback example.
|
||||||
|
|
||||||
|
```
|
||||||
|
setOnConnectionCallback(
|
||||||
|
[this](HttpRequestPtr request,
|
||||||
|
std::shared_ptr<ConnectionState> /*connectionState*/) -> HttpResponsePtr
|
||||||
|
{
|
||||||
|
// Build a string for the response
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << request->method
|
||||||
|
<< " "
|
||||||
|
<< request->uri;
|
||||||
|
|
||||||
|
std::string content = ss.str();
|
||||||
|
|
||||||
|
return std::make_shared<HttpResponse>(200, "OK",
|
||||||
|
HttpErrorCode::Ok,
|
||||||
|
WebSocketHttpHeaders(),
|
||||||
|
content);
|
||||||
|
}
|
||||||
|
```
|
73
docs/ws.md
Normal file
73
docs/ws.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
## General
|
||||||
|
|
||||||
|
ws is a command line tool that should exercise most of the IXWebSocket code, and provide example code.
|
||||||
|
|
||||||
|
```
|
||||||
|
ws is a websocket tool
|
||||||
|
Usage: ws [OPTIONS] SUBCOMMAND
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h,--help Print this help message and exit
|
||||||
|
|
||||||
|
Subcommands:
|
||||||
|
send Send a file
|
||||||
|
receive Receive a file
|
||||||
|
transfer Broadcasting server
|
||||||
|
connect Connect to a remote server
|
||||||
|
chat Group chat
|
||||||
|
echo_server Echo server
|
||||||
|
broadcast_server Broadcasting server
|
||||||
|
ping Ping pong
|
||||||
|
curl HTTP Client
|
||||||
|
redis_publish Redis publisher
|
||||||
|
redis_subscribe Redis subscriber
|
||||||
|
cobra_subscribe Cobra subscriber
|
||||||
|
cobra_publish Cobra publisher
|
||||||
|
cobra_to_statsd Cobra to statsd
|
||||||
|
cobra_to_sentry Cobra to sentry
|
||||||
|
snake Snake server
|
||||||
|
httpd HTTP server
|
||||||
|
```
|
||||||
|
|
||||||
|
## File transfer
|
||||||
|
|
||||||
|
```
|
||||||
|
# Start transfer server, which is just a broadcast server at this point
|
||||||
|
ws transfer # running on port 8080.
|
||||||
|
|
||||||
|
# Start receiver first
|
||||||
|
ws receive ws://localhost:8080
|
||||||
|
|
||||||
|
# Then send a file. File will be received and written to disk by the receiver process
|
||||||
|
ws send ws://localhost:8080 /file/to/path
|
||||||
|
```
|
||||||
|
|
||||||
|
## HTTP Client
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ws curl --help
|
||||||
|
HTTP Client
|
||||||
|
Usage: ws curl [OPTIONS] url
|
||||||
|
|
||||||
|
Positionals:
|
||||||
|
url TEXT REQUIRED Connection url
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h,--help Print this help message and exit
|
||||||
|
-d TEXT Form data
|
||||||
|
-F TEXT Form data
|
||||||
|
-H TEXT Header
|
||||||
|
--output TEXT Output file
|
||||||
|
-I Send a HEAD request
|
||||||
|
-L Follow redirects
|
||||||
|
--max-redirects INT Max Redirects
|
||||||
|
-v Verbose
|
||||||
|
-O Save output to disk
|
||||||
|
--compress Enable gzip compression
|
||||||
|
--connect-timeout INT Connection timeout
|
||||||
|
--transfer-timeout INT Transfer timeout
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cobra Client
|
||||||
|
|
||||||
|
[cobra](https://github.com/machinezone/cobra) is a real time messenging server. ws has sub-command to interacti with cobra.
|
@ -1,15 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
#
|
|
||||||
# Author: Benjamin Sergeant
|
|
||||||
# Copyright (c) 2017-2018 Machine Zone, Inc. All rights reserved.
|
|
||||||
#
|
|
||||||
|
|
||||||
clang++ --std=c++11 --stdlib=libc++ \
|
|
||||||
../ixwebsocket/IXSocket.cpp \
|
|
||||||
../ixwebsocket/IXWebSocketTransport.cpp \
|
|
||||||
../ixwebsocket/IXSocketAppleSSL.cpp \
|
|
||||||
../ixwebsocket/IXWebSocket.cpp \
|
|
||||||
cmd_websocket_chat.cpp \
|
|
||||||
-o cmd_websocket_chat \
|
|
||||||
-framework Security \
|
|
||||||
-framework Foundation
|
|
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"dependencies": {
|
|
||||||
"msgpack-js": "^0.3.0",
|
|
||||||
"ws": "^3.1.0"
|
|
||||||
}
|
|
||||||
}
|
|
33
ixwebsocket/IXCancellationRequest.cpp
Normal file
33
ixwebsocket/IXCancellationRequest.cpp
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* IXCancellationRequest.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXCancellationRequest.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
CancellationRequest makeCancellationRequestWithTimeout(int secs,
|
||||||
|
std::atomic<bool>& requestInitCancellation)
|
||||||
|
{
|
||||||
|
auto start = std::chrono::system_clock::now();
|
||||||
|
auto timeout = std::chrono::seconds(secs);
|
||||||
|
|
||||||
|
auto isCancellationRequested = [&requestInitCancellation, start, timeout]() -> bool
|
||||||
|
{
|
||||||
|
// Was an explicit cancellation requested ?
|
||||||
|
if (requestInitCancellation) return true;
|
||||||
|
|
||||||
|
auto now = std::chrono::system_clock::now();
|
||||||
|
if ((now - start) > timeout) return true;
|
||||||
|
|
||||||
|
// No cancellation request
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return isCancellationRequested;
|
||||||
|
}
|
||||||
|
}
|
18
ixwebsocket/IXCancellationRequest.h
Normal file
18
ixwebsocket/IXCancellationRequest.h
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* IXCancellationRequest.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
using CancellationRequest = std::function<bool()>;
|
||||||
|
|
||||||
|
CancellationRequest makeCancellationRequestWithTimeout(
|
||||||
|
int seconds, std::atomic<bool>& requestInitCancellation);
|
||||||
|
} // namespace ix
|
43
ixwebsocket/IXConnectionState.cpp
Normal file
43
ixwebsocket/IXConnectionState.cpp
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* IXConnectionState.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXConnectionState.h"
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
std::atomic<uint64_t> ConnectionState::_globalId(0);
|
||||||
|
|
||||||
|
ConnectionState::ConnectionState() : _terminated(false)
|
||||||
|
{
|
||||||
|
computeId();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConnectionState::computeId()
|
||||||
|
{
|
||||||
|
_id = std::to_string(_globalId++);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& ConnectionState::getId() const
|
||||||
|
{
|
||||||
|
return _id;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<ConnectionState> ConnectionState::createConnectionState()
|
||||||
|
{
|
||||||
|
return std::make_shared<ConnectionState>();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ConnectionState::isTerminated() const
|
||||||
|
{
|
||||||
|
return _terminated;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConnectionState::setTerminated()
|
||||||
|
{
|
||||||
|
_terminated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
36
ixwebsocket/IXConnectionState.h
Normal file
36
ixwebsocket/IXConnectionState.h
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* IXConnectionState.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <memory>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
class ConnectionState
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ConnectionState();
|
||||||
|
virtual ~ConnectionState() = default;
|
||||||
|
|
||||||
|
virtual void computeId();
|
||||||
|
virtual const std::string& getId() const;
|
||||||
|
|
||||||
|
void setTerminated();
|
||||||
|
bool isTerminated() const;
|
||||||
|
|
||||||
|
static std::shared_ptr<ConnectionState> createConnectionState();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::atomic<bool> _terminated;
|
||||||
|
std::string _id;
|
||||||
|
|
||||||
|
static std::atomic<uint64_t> _globalId;
|
||||||
|
};
|
||||||
|
} // namespace ix
|
170
ixwebsocket/IXDNSLookup.cpp
Normal file
170
ixwebsocket/IXDNSLookup.cpp
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
/*
|
||||||
|
* IXDNSLookup.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXDNSLookup.h"
|
||||||
|
#include "IXNetSystem.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include <chrono>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
const int64_t DNSLookup::kDefaultWait = 1; // ms
|
||||||
|
|
||||||
|
DNSLookup::DNSLookup(const std::string& hostname, int port, int64_t wait) :
|
||||||
|
_hostname(hostname),
|
||||||
|
_port(port),
|
||||||
|
_wait(wait),
|
||||||
|
_res(nullptr),
|
||||||
|
_done(false)
|
||||||
|
{
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct addrinfo* DNSLookup::getAddrInfo(const std::string& hostname,
|
||||||
|
int port,
|
||||||
|
std::string& errMsg)
|
||||||
|
{
|
||||||
|
struct addrinfo hints;
|
||||||
|
memset(&hints, 0, sizeof(hints));
|
||||||
|
hints.ai_flags = AI_ADDRCONFIG | AI_NUMERICSERV;
|
||||||
|
hints.ai_family = AF_UNSPEC;
|
||||||
|
hints.ai_socktype = SOCK_STREAM;
|
||||||
|
|
||||||
|
std::string sport = std::to_string(port);
|
||||||
|
|
||||||
|
struct addrinfo* res;
|
||||||
|
int getaddrinfo_result = getaddrinfo(hostname.c_str(), sport.c_str(),
|
||||||
|
&hints, &res);
|
||||||
|
if (getaddrinfo_result)
|
||||||
|
{
|
||||||
|
errMsg = gai_strerror(getaddrinfo_result);
|
||||||
|
res = nullptr;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct addrinfo* DNSLookup::resolve(std::string& errMsg,
|
||||||
|
const CancellationRequest& isCancellationRequested,
|
||||||
|
bool cancellable)
|
||||||
|
{
|
||||||
|
return cancellable ? resolveCancellable(errMsg, isCancellationRequested)
|
||||||
|
: resolveUnCancellable(errMsg, isCancellationRequested);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct addrinfo* DNSLookup::resolveUnCancellable(std::string& errMsg,
|
||||||
|
const CancellationRequest& isCancellationRequested)
|
||||||
|
{
|
||||||
|
errMsg = "no error";
|
||||||
|
|
||||||
|
// Maybe a cancellation request got in before the background thread terminated ?
|
||||||
|
if (isCancellationRequested && isCancellationRequested())
|
||||||
|
{
|
||||||
|
errMsg = "cancellation requested";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getAddrInfo(_hostname, _port, errMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct addrinfo* DNSLookup::resolveCancellable(std::string& errMsg,
|
||||||
|
const CancellationRequest& isCancellationRequested)
|
||||||
|
{
|
||||||
|
errMsg = "no error";
|
||||||
|
|
||||||
|
// Can only be called once, otherwise we would have to manage a pool
|
||||||
|
// of background thread which is overkill for our usage.
|
||||||
|
if (_done)
|
||||||
|
{
|
||||||
|
return nullptr; // programming error, create a second DNSLookup instance
|
||||||
|
// if you need a second lookup.
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Good resource on thread forced termination
|
||||||
|
// https://www.bo-yang.net/2017/11/19/cpp-kill-detached-thread
|
||||||
|
//
|
||||||
|
auto ptr = shared_from_this();
|
||||||
|
std::weak_ptr<DNSLookup> self(ptr);
|
||||||
|
|
||||||
|
int port = _port;
|
||||||
|
std::string hostname(_hostname);
|
||||||
|
|
||||||
|
// We make the background thread doing the work a shared pointer
|
||||||
|
// instead of a member variable, because it can keep running when
|
||||||
|
// this object goes out of scope, in case of cancellation
|
||||||
|
auto t = std::make_shared<std::thread>(&DNSLookup::run, this, self, hostname, port);
|
||||||
|
t->detach();
|
||||||
|
|
||||||
|
while (!_done)
|
||||||
|
{
|
||||||
|
// Wait for 1 milliseconds, to see if the bg thread has terminated.
|
||||||
|
// We do not use a condition variable to wait, as destroying this one
|
||||||
|
// if the bg thread is alive can cause undefined behavior.
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(_wait));
|
||||||
|
|
||||||
|
// Were we cancelled ?
|
||||||
|
if (isCancellationRequested && isCancellationRequested())
|
||||||
|
{
|
||||||
|
errMsg = "cancellation requested";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maybe a cancellation request got in before the bg terminated ?
|
||||||
|
if (isCancellationRequested && isCancellationRequested())
|
||||||
|
{
|
||||||
|
errMsg = "cancellation requested";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
errMsg = getErrMsg();
|
||||||
|
return getRes();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DNSLookup::run(std::weak_ptr<DNSLookup> self, std::string hostname, int port) // thread runner
|
||||||
|
{
|
||||||
|
// We don't want to read or write into members variables of an object that could be
|
||||||
|
// gone, so we use temporary variables (res) or we pass in by copy everything that
|
||||||
|
// getAddrInfo needs to work.
|
||||||
|
std::string errMsg;
|
||||||
|
struct addrinfo* res = getAddrInfo(hostname, port, errMsg);
|
||||||
|
|
||||||
|
if (self.lock())
|
||||||
|
{
|
||||||
|
// Copy result into the member variables
|
||||||
|
setRes(res);
|
||||||
|
setErrMsg(errMsg);
|
||||||
|
|
||||||
|
_done = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DNSLookup::setErrMsg(const std::string& errMsg)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_errMsgMutex);
|
||||||
|
_errMsg = errMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& DNSLookup::getErrMsg()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_errMsgMutex);
|
||||||
|
return _errMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DNSLookup::setRes(struct addrinfo* addr)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_resMutex);
|
||||||
|
_res = addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct addrinfo* DNSLookup::getRes()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_resMutex);
|
||||||
|
return _res;
|
||||||
|
}
|
||||||
|
}
|
65
ixwebsocket/IXDNSLookup.h
Normal file
65
ixwebsocket/IXDNSLookup.h
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* IXDNSLookup.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* Resolve a hostname+port to a struct addrinfo obtained with getaddrinfo
|
||||||
|
* Does this in a background thread so that it can be cancelled, since
|
||||||
|
* getaddrinfo is a blocking call, and we don't want to block the main thread on Mobile.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IXCancellationRequest.h"
|
||||||
|
#include <atomic>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
struct addrinfo;
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
class DNSLookup : public std::enable_shared_from_this<DNSLookup>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DNSLookup(const std::string& hostname, int port, int64_t wait = DNSLookup::kDefaultWait);
|
||||||
|
~DNSLookup() = default;
|
||||||
|
|
||||||
|
struct addrinfo* resolve(std::string& errMsg,
|
||||||
|
const CancellationRequest& isCancellationRequested,
|
||||||
|
bool cancellable = true);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct addrinfo* resolveCancellable(std::string& errMsg,
|
||||||
|
const CancellationRequest& isCancellationRequested);
|
||||||
|
struct addrinfo* resolveUnCancellable(std::string& errMsg,
|
||||||
|
const CancellationRequest& isCancellationRequested);
|
||||||
|
|
||||||
|
static struct addrinfo* getAddrInfo(const std::string& hostname,
|
||||||
|
int port,
|
||||||
|
std::string& errMsg);
|
||||||
|
|
||||||
|
void run(std::weak_ptr<DNSLookup> self, std::string hostname, int port); // thread runner
|
||||||
|
|
||||||
|
void setErrMsg(const std::string& errMsg);
|
||||||
|
const std::string& getErrMsg();
|
||||||
|
|
||||||
|
void setRes(struct addrinfo* addr);
|
||||||
|
struct addrinfo* getRes();
|
||||||
|
|
||||||
|
std::string _hostname;
|
||||||
|
int _port;
|
||||||
|
int64_t _wait;
|
||||||
|
const static int64_t kDefaultWait;
|
||||||
|
|
||||||
|
struct addrinfo* _res;
|
||||||
|
std::mutex _resMutex;
|
||||||
|
|
||||||
|
std::string _errMsg;
|
||||||
|
std::mutex _errMsgMutex;
|
||||||
|
|
||||||
|
std::atomic<bool> _done;
|
||||||
|
};
|
||||||
|
} // namespace ix
|
26
ixwebsocket/IXExponentialBackoff.cpp
Normal file
26
ixwebsocket/IXExponentialBackoff.cpp
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* IXExponentialBackoff.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2017-2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXExponentialBackoff.h"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
uint32_t calculateRetryWaitMilliseconds(
|
||||||
|
uint32_t retry_count,
|
||||||
|
uint32_t maxWaitBetweenReconnectionRetries)
|
||||||
|
{
|
||||||
|
uint32_t wait_time = std::pow(2, retry_count) * 100;
|
||||||
|
|
||||||
|
if (wait_time > maxWaitBetweenReconnectionRetries || wait_time == 0)
|
||||||
|
{
|
||||||
|
wait_time = maxWaitBetweenReconnectionRetries;
|
||||||
|
}
|
||||||
|
|
||||||
|
return wait_time;
|
||||||
|
}
|
||||||
|
}
|
16
ixwebsocket/IXExponentialBackoff.h
Normal file
16
ixwebsocket/IXExponentialBackoff.h
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* IXExponentialBackoff.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2017-2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
uint32_t calculateRetryWaitMilliseconds(
|
||||||
|
uint32_t retry_count,
|
||||||
|
uint32_t maxWaitBetweenReconnectionRetries);
|
||||||
|
} // namespace ix
|
138
ixwebsocket/IXHttp.cpp
Normal file
138
ixwebsocket/IXHttp.cpp
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
/*
|
||||||
|
* IXHttp.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXHttp.h"
|
||||||
|
#include "IXCancellationRequest.h"
|
||||||
|
#include "IXSocket.h"
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
std::string Http::trim(const std::string& str)
|
||||||
|
{
|
||||||
|
std::string out;
|
||||||
|
for (auto c : str)
|
||||||
|
{
|
||||||
|
if (c != ' ' && c != '\n' && c != '\r')
|
||||||
|
{
|
||||||
|
out += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::tuple<std::string, std::string, std::string> Http::parseRequestLine(const std::string& line)
|
||||||
|
{
|
||||||
|
// Request-Line = Method SP Request-URI SP HTTP-Version CRLF
|
||||||
|
std::string token;
|
||||||
|
std::stringstream tokenStream(line);
|
||||||
|
std::vector<std::string> tokens;
|
||||||
|
|
||||||
|
// Split by ' '
|
||||||
|
while (std::getline(tokenStream, token, ' '))
|
||||||
|
{
|
||||||
|
tokens.push_back(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string method;
|
||||||
|
if (tokens.size() >= 1)
|
||||||
|
{
|
||||||
|
method = trim(tokens[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string requestUri;
|
||||||
|
if (tokens.size() >= 2)
|
||||||
|
{
|
||||||
|
requestUri = trim(tokens[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string httpVersion;
|
||||||
|
if (tokens.size() >= 3)
|
||||||
|
{
|
||||||
|
httpVersion = trim(tokens[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::make_tuple(method, requestUri, httpVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::tuple<bool, std::string, HttpRequestPtr> Http::parseRequest(std::shared_ptr<Socket> socket)
|
||||||
|
{
|
||||||
|
HttpRequestPtr httpRequest;
|
||||||
|
|
||||||
|
std::atomic<bool> requestInitCancellation(false);
|
||||||
|
|
||||||
|
int timeoutSecs = 5; // FIXME
|
||||||
|
|
||||||
|
auto isCancellationRequested =
|
||||||
|
makeCancellationRequestWithTimeout(timeoutSecs, requestInitCancellation);
|
||||||
|
|
||||||
|
// Read first line
|
||||||
|
auto lineResult = socket->readLine(isCancellationRequested);
|
||||||
|
auto lineValid = lineResult.first;
|
||||||
|
auto line = lineResult.second;
|
||||||
|
|
||||||
|
if (!lineValid)
|
||||||
|
{
|
||||||
|
return std::make_tuple(false, "Error reading HTTP request line", httpRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request line (GET /foo HTTP/1.1\r\n)
|
||||||
|
auto requestLine = Http::parseRequestLine(line);
|
||||||
|
auto method = std::get<0>(requestLine);
|
||||||
|
auto uri = std::get<1>(requestLine);
|
||||||
|
auto httpVersion = std::get<2>(requestLine);
|
||||||
|
|
||||||
|
// Retrieve and validate HTTP headers
|
||||||
|
auto result = parseHttpHeaders(socket, isCancellationRequested);
|
||||||
|
auto headersValid = result.first;
|
||||||
|
auto headers = result.second;
|
||||||
|
|
||||||
|
if (!headersValid)
|
||||||
|
{
|
||||||
|
return std::make_tuple(false, "Error parsing HTTP headers", httpRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
httpRequest = std::make_shared<HttpRequest>(uri, method, httpVersion, headers);
|
||||||
|
return std::make_tuple(true, "", httpRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Http::sendResponse(HttpResponsePtr response, std::shared_ptr<Socket> socket)
|
||||||
|
{
|
||||||
|
// Write the response to the socket
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "HTTP/1.1 ";
|
||||||
|
ss << response->statusCode;
|
||||||
|
ss << " ";
|
||||||
|
ss << response->description;
|
||||||
|
ss << "\r\n";
|
||||||
|
|
||||||
|
if (!socket->writeBytes(ss.str(), nullptr))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write headers
|
||||||
|
ss.str("");
|
||||||
|
ss << "Content-Length: " << response->payload.size() << "\r\n";
|
||||||
|
for (auto&& it : response->headers)
|
||||||
|
{
|
||||||
|
ss << it.first << ": " << it.second << "\r\n";
|
||||||
|
}
|
||||||
|
ss << "\r\n";
|
||||||
|
|
||||||
|
if (!socket->writeBytes(ss.str(), nullptr))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response->payload.empty()
|
||||||
|
? true
|
||||||
|
: socket->writeBytes(response->payload, nullptr);
|
||||||
|
}
|
||||||
|
}
|
122
ixwebsocket/IXHttp.h
Normal file
122
ixwebsocket/IXHttp.h
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
/*
|
||||||
|
* IXHttp.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IXProgressCallback.h"
|
||||||
|
#include "IXWebSocketHttpHeaders.h"
|
||||||
|
#include <tuple>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
enum class HttpErrorCode : int
|
||||||
|
{
|
||||||
|
Ok = 0,
|
||||||
|
CannotConnect = 1,
|
||||||
|
Timeout = 2,
|
||||||
|
Gzip = 3,
|
||||||
|
UrlMalformed = 4,
|
||||||
|
CannotCreateSocket = 5,
|
||||||
|
SendError = 6,
|
||||||
|
ReadError = 7,
|
||||||
|
CannotReadStatusLine = 8,
|
||||||
|
MissingStatus = 9,
|
||||||
|
HeaderParsingError = 10,
|
||||||
|
MissingLocation = 11,
|
||||||
|
TooManyRedirects = 12,
|
||||||
|
ChunkReadError = 13,
|
||||||
|
CannotReadBody = 14,
|
||||||
|
Invalid = 100
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HttpResponse
|
||||||
|
{
|
||||||
|
int statusCode;
|
||||||
|
std::string description;
|
||||||
|
HttpErrorCode errorCode;
|
||||||
|
WebSocketHttpHeaders headers;
|
||||||
|
std::string payload;
|
||||||
|
std::string errorMsg;
|
||||||
|
uint64_t uploadSize;
|
||||||
|
uint64_t downloadSize;
|
||||||
|
|
||||||
|
HttpResponse(int s = 0,
|
||||||
|
const std::string& des = std::string(),
|
||||||
|
const HttpErrorCode& c = HttpErrorCode::Ok,
|
||||||
|
const WebSocketHttpHeaders& h = WebSocketHttpHeaders(),
|
||||||
|
const std::string& p = std::string(),
|
||||||
|
const std::string& e = std::string(),
|
||||||
|
uint64_t u = 0,
|
||||||
|
uint64_t d = 0)
|
||||||
|
: statusCode(s)
|
||||||
|
, description(des)
|
||||||
|
, errorCode(c)
|
||||||
|
, headers(h)
|
||||||
|
, payload(p)
|
||||||
|
, errorMsg(e)
|
||||||
|
, uploadSize(u)
|
||||||
|
, downloadSize(d)
|
||||||
|
{
|
||||||
|
;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using HttpResponsePtr = std::shared_ptr<HttpResponse>;
|
||||||
|
using HttpParameters = std::map<std::string, std::string>;
|
||||||
|
using Logger = std::function<void(const std::string&)>;
|
||||||
|
using OnResponseCallback = std::function<void(const HttpResponsePtr&)>;
|
||||||
|
|
||||||
|
struct HttpRequestArgs
|
||||||
|
{
|
||||||
|
std::string url;
|
||||||
|
std::string verb;
|
||||||
|
WebSocketHttpHeaders extraHeaders;
|
||||||
|
std::string body;
|
||||||
|
int connectTimeout;
|
||||||
|
int transferTimeout;
|
||||||
|
bool followRedirects;
|
||||||
|
int maxRedirects;
|
||||||
|
bool verbose;
|
||||||
|
bool compress;
|
||||||
|
Logger logger;
|
||||||
|
OnProgressCallback onProgressCallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
using HttpRequestArgsPtr = std::shared_ptr<HttpRequestArgs>;
|
||||||
|
|
||||||
|
struct HttpRequest
|
||||||
|
{
|
||||||
|
std::string uri;
|
||||||
|
std::string method;
|
||||||
|
std::string version;
|
||||||
|
WebSocketHttpHeaders headers;
|
||||||
|
|
||||||
|
HttpRequest(const std::string& u,
|
||||||
|
const std::string& m,
|
||||||
|
const std::string& v,
|
||||||
|
const WebSocketHttpHeaders& h = WebSocketHttpHeaders())
|
||||||
|
: uri(u)
|
||||||
|
, method(m)
|
||||||
|
, version(v)
|
||||||
|
, headers(h)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using HttpRequestPtr = std::shared_ptr<HttpRequest>;
|
||||||
|
|
||||||
|
class Http
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static std::tuple<bool, std::string, HttpRequestPtr> parseRequest(
|
||||||
|
std::shared_ptr<Socket> socket);
|
||||||
|
static bool sendResponse(HttpResponsePtr response, std::shared_ptr<Socket> socket);
|
||||||
|
|
||||||
|
static std::tuple<std::string, std::string, std::string> parseRequestLine(
|
||||||
|
const std::string& line);
|
||||||
|
static std::string trim(const std::string& str);
|
||||||
|
};
|
||||||
|
} // namespace ix
|
575
ixwebsocket/IXHttpClient.cpp
Normal file
575
ixwebsocket/IXHttpClient.cpp
Normal file
@ -0,0 +1,575 @@
|
|||||||
|
/*
|
||||||
|
* IXHttpClient.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXHttpClient.h"
|
||||||
|
#include "IXUrlParser.h"
|
||||||
|
#include "IXUserAgent.h"
|
||||||
|
#include "IXWebSocketHttpHeaders.h"
|
||||||
|
#include "IXSocketFactory.h"
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <vector>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <zlib.h>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
const std::string HttpClient::kPost = "POST";
|
||||||
|
const std::string HttpClient::kGet = "GET";
|
||||||
|
const std::string HttpClient::kHead = "HEAD";
|
||||||
|
const std::string HttpClient::kDel = "DEL";
|
||||||
|
const std::string HttpClient::kPut = "PUT";
|
||||||
|
|
||||||
|
HttpClient::HttpClient(bool async) : _async(async), _stop(false)
|
||||||
|
{
|
||||||
|
if (!_async) return;
|
||||||
|
|
||||||
|
_thread = std::thread(&HttpClient::run, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpClient::~HttpClient()
|
||||||
|
{
|
||||||
|
if (!_thread.joinable()) return;
|
||||||
|
|
||||||
|
_stop = true;
|
||||||
|
_condition.notify_one();
|
||||||
|
_thread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpRequestArgsPtr HttpClient::createRequest(const std::string& url,
|
||||||
|
const std::string& verb)
|
||||||
|
{
|
||||||
|
auto request = std::make_shared<HttpRequestArgs>();
|
||||||
|
request->url = url;
|
||||||
|
request->verb = verb;
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HttpClient::performRequest(HttpRequestArgsPtr args,
|
||||||
|
const OnResponseCallback& onResponseCallback)
|
||||||
|
{
|
||||||
|
assert(_async && "HttpClient needs its async parameter set to true "
|
||||||
|
"in order to call performRequest");
|
||||||
|
if (!_async) return false;
|
||||||
|
|
||||||
|
// Enqueue the task
|
||||||
|
{
|
||||||
|
// acquire lock
|
||||||
|
std::unique_lock<std::mutex> lock(_queueMutex);
|
||||||
|
|
||||||
|
// add the task
|
||||||
|
_queue.push(std::make_pair(args, onResponseCallback));
|
||||||
|
} // release lock
|
||||||
|
|
||||||
|
// wake up one thread
|
||||||
|
_condition.notify_one();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpClient::run()
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
HttpRequestArgsPtr args;
|
||||||
|
OnResponseCallback onResponseCallback;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(_queueMutex);
|
||||||
|
|
||||||
|
while (!_stop && _queue.empty())
|
||||||
|
{
|
||||||
|
_condition.wait(lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_stop) return;
|
||||||
|
|
||||||
|
auto p = _queue.front();
|
||||||
|
_queue.pop();
|
||||||
|
|
||||||
|
args = p.first;
|
||||||
|
onResponseCallback = p.second;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_stop) return;
|
||||||
|
|
||||||
|
HttpResponsePtr response = request(args->url, args->verb, args->body, args);
|
||||||
|
onResponseCallback(response);
|
||||||
|
|
||||||
|
if (_stop) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponsePtr HttpClient::request(
|
||||||
|
const std::string& url,
|
||||||
|
const std::string& verb,
|
||||||
|
const std::string& body,
|
||||||
|
HttpRequestArgsPtr args,
|
||||||
|
int redirects)
|
||||||
|
{
|
||||||
|
// We only have one socket connection, so we cannot
|
||||||
|
// make multiple requests concurrently.
|
||||||
|
std::lock_guard<std::mutex> lock(_mutex);
|
||||||
|
|
||||||
|
uint64_t uploadSize = 0;
|
||||||
|
uint64_t downloadSize = 0;
|
||||||
|
int code = 0;
|
||||||
|
WebSocketHttpHeaders headers;
|
||||||
|
std::string payload;
|
||||||
|
std::string description;
|
||||||
|
|
||||||
|
std::string protocol, host, path, query;
|
||||||
|
int port;
|
||||||
|
|
||||||
|
if (!UrlParser::parse(url, protocol, host, path, query, port))
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "Cannot parse url: " << url;
|
||||||
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::UrlMalformed,
|
||||||
|
headers, payload, ss.str(),
|
||||||
|
uploadSize, downloadSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool tls = protocol == "https";
|
||||||
|
std::string errorMsg;
|
||||||
|
_socket = createSocket(tls, errorMsg);
|
||||||
|
|
||||||
|
if (!_socket)
|
||||||
|
{
|
||||||
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::CannotCreateSocket,
|
||||||
|
headers, payload, errorMsg,
|
||||||
|
uploadSize, downloadSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build request string
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << verb << " " << path << " HTTP/1.1\r\n";
|
||||||
|
ss << "Host: " << host << "\r\n";
|
||||||
|
|
||||||
|
if (args->compress)
|
||||||
|
{
|
||||||
|
ss << "Accept-Encoding: gzip" << "\r\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append extra headers
|
||||||
|
for (auto&& it : args->extraHeaders)
|
||||||
|
{
|
||||||
|
ss << it.first << ": " << it.second << "\r\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a default Accept header if none is present
|
||||||
|
if (headers.find("Accept") == headers.end())
|
||||||
|
{
|
||||||
|
ss << "Accept: */*" << "\r\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a default User agent if none is present
|
||||||
|
if (headers.find("User-Agent") == headers.end())
|
||||||
|
{
|
||||||
|
ss << "User-Agent: " << userAgent() << "\r\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verb == kPost || verb == kPut)
|
||||||
|
{
|
||||||
|
ss << "Content-Length: " << body.size() << "\r\n";
|
||||||
|
|
||||||
|
// Set default Content-Type if unspecified
|
||||||
|
if (args->extraHeaders.find("Content-Type") == args->extraHeaders.end())
|
||||||
|
{
|
||||||
|
ss << "Content-Type: application/x-www-form-urlencoded" << "\r\n";
|
||||||
|
}
|
||||||
|
ss << "\r\n";
|
||||||
|
ss << body;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ss << "\r\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string req(ss.str());
|
||||||
|
std::string errMsg;
|
||||||
|
std::atomic<bool> requestInitCancellation(false);
|
||||||
|
|
||||||
|
// Make a cancellation object dealing with connection timeout
|
||||||
|
auto isCancellationRequested =
|
||||||
|
makeCancellationRequestWithTimeout(args->connectTimeout, requestInitCancellation);
|
||||||
|
|
||||||
|
bool success = _socket->connect(host, port, errMsg, isCancellationRequested);
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "Cannot connect to url: " << url << " / error : " << errMsg;
|
||||||
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::CannotConnect,
|
||||||
|
headers, payload, ss.str(),
|
||||||
|
uploadSize, downloadSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a new cancellation object dealing with transfer timeout
|
||||||
|
isCancellationRequested =
|
||||||
|
makeCancellationRequestWithTimeout(args->transferTimeout, requestInitCancellation);
|
||||||
|
|
||||||
|
if (args->verbose)
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "Sending " << verb << " request "
|
||||||
|
<< "to " << host << ":" << port << std::endl
|
||||||
|
<< "request size: " << req.size() << " bytes" << std::endl
|
||||||
|
<< "=============" << std::endl
|
||||||
|
<< req
|
||||||
|
<< "=============" << std::endl
|
||||||
|
<< std::endl;
|
||||||
|
|
||||||
|
log(ss.str(), args);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_socket->writeBytes(req, isCancellationRequested))
|
||||||
|
{
|
||||||
|
std::string errorMsg("Cannot send request");
|
||||||
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::SendError,
|
||||||
|
headers, payload, errorMsg,
|
||||||
|
uploadSize, downloadSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadSize = req.size();
|
||||||
|
|
||||||
|
auto lineResult = _socket->readLine(isCancellationRequested);
|
||||||
|
auto lineValid = lineResult.first;
|
||||||
|
auto line = lineResult.second;
|
||||||
|
|
||||||
|
if (!lineValid)
|
||||||
|
{
|
||||||
|
std::string errorMsg("Cannot retrieve status line");
|
||||||
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::CannotReadStatusLine,
|
||||||
|
headers, payload, errorMsg,
|
||||||
|
uploadSize, downloadSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args->verbose)
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "Status line " << line;
|
||||||
|
log(ss.str(), args);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sscanf(line.c_str(), "HTTP/1.1 %d", &code) != 1)
|
||||||
|
{
|
||||||
|
std::string errorMsg("Cannot parse response code from status line");
|
||||||
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::MissingStatus,
|
||||||
|
headers, payload, errorMsg,
|
||||||
|
uploadSize, downloadSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto result = parseHttpHeaders(_socket, isCancellationRequested);
|
||||||
|
auto headersValid = result.first;
|
||||||
|
headers = result.second;
|
||||||
|
|
||||||
|
if (!headersValid)
|
||||||
|
{
|
||||||
|
std::string errorMsg("Cannot parse http headers");
|
||||||
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::HeaderParsingError,
|
||||||
|
headers, payload, errorMsg,
|
||||||
|
uploadSize, downloadSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect ?
|
||||||
|
if ((code >= 301 && code <= 308) && args->followRedirects)
|
||||||
|
{
|
||||||
|
if (headers.find("Location") == headers.end())
|
||||||
|
{
|
||||||
|
std::string errorMsg("Missing location header for redirect");
|
||||||
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::MissingLocation,
|
||||||
|
headers, payload, errorMsg,
|
||||||
|
uploadSize, downloadSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redirects >= args->maxRedirects)
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "Too many redirects: " << redirects;
|
||||||
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::TooManyRedirects,
|
||||||
|
headers, payload, ss.str(),
|
||||||
|
uploadSize, downloadSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse
|
||||||
|
std::string location = headers["Location"];
|
||||||
|
return request(location, verb, body, args, redirects+1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verb == "HEAD")
|
||||||
|
{
|
||||||
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::Ok,
|
||||||
|
headers, payload, std::string(),
|
||||||
|
uploadSize, downloadSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse response:
|
||||||
|
if (headers.find("Content-Length") != headers.end())
|
||||||
|
{
|
||||||
|
ssize_t contentLength = -1;
|
||||||
|
ss.str("");
|
||||||
|
ss << headers["Content-Length"];
|
||||||
|
ss >> contentLength;
|
||||||
|
|
||||||
|
payload.reserve(contentLength);
|
||||||
|
|
||||||
|
auto chunkResult = _socket->readBytes(contentLength,
|
||||||
|
args->onProgressCallback,
|
||||||
|
isCancellationRequested);
|
||||||
|
if (!chunkResult.first)
|
||||||
|
{
|
||||||
|
errorMsg = "Cannot read chunk";
|
||||||
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::ChunkReadError,
|
||||||
|
headers, payload, errorMsg,
|
||||||
|
uploadSize, downloadSize);
|
||||||
|
}
|
||||||
|
payload += chunkResult.second;
|
||||||
|
}
|
||||||
|
else if (headers.find("Transfer-Encoding") != headers.end() &&
|
||||||
|
headers["Transfer-Encoding"] == "chunked")
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
lineResult = _socket->readLine(isCancellationRequested);
|
||||||
|
line = lineResult.second;
|
||||||
|
|
||||||
|
if (!lineResult.first)
|
||||||
|
{
|
||||||
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::ChunkReadError,
|
||||||
|
headers, payload, errorMsg,
|
||||||
|
uploadSize, downloadSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t chunkSize;
|
||||||
|
ss.str("");
|
||||||
|
ss << std::hex << line;
|
||||||
|
ss >> chunkSize;
|
||||||
|
|
||||||
|
if (args->verbose)
|
||||||
|
{
|
||||||
|
std::stringstream oss;
|
||||||
|
oss << "Reading " << chunkSize << " bytes"
|
||||||
|
<< std::endl;
|
||||||
|
log(oss.str(), args);
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.reserve(payload.size() + (size_t) chunkSize);
|
||||||
|
|
||||||
|
// Read a chunk
|
||||||
|
auto chunkResult = _socket->readBytes((size_t) chunkSize,
|
||||||
|
args->onProgressCallback,
|
||||||
|
isCancellationRequested);
|
||||||
|
if (!chunkResult.first)
|
||||||
|
{
|
||||||
|
errorMsg = "Cannot read chunk";
|
||||||
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::ChunkReadError,
|
||||||
|
headers, payload, errorMsg,
|
||||||
|
uploadSize, downloadSize);
|
||||||
|
}
|
||||||
|
payload += chunkResult.second;
|
||||||
|
|
||||||
|
// Read the line that terminates the chunk (\r\n)
|
||||||
|
lineResult = _socket->readLine(isCancellationRequested);
|
||||||
|
|
||||||
|
if (!lineResult.first)
|
||||||
|
{
|
||||||
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::ChunkReadError,
|
||||||
|
headers, payload, errorMsg,
|
||||||
|
uploadSize, downloadSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunkSize == 0) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (code == 204)
|
||||||
|
{
|
||||||
|
; // 204 is NoContent response code
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::string errorMsg("Cannot read http body");
|
||||||
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::CannotReadBody,
|
||||||
|
headers, payload, errorMsg,
|
||||||
|
uploadSize, downloadSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadSize = payload.size();
|
||||||
|
|
||||||
|
// If the content was compressed with gzip, decode it
|
||||||
|
if (headers["Content-Encoding"] == "gzip")
|
||||||
|
{
|
||||||
|
std::string decompressedPayload;
|
||||||
|
if (!gzipInflate(payload, decompressedPayload))
|
||||||
|
{
|
||||||
|
std::string errorMsg("Error decompressing payload");
|
||||||
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::Gzip,
|
||||||
|
headers, payload, errorMsg,
|
||||||
|
uploadSize, downloadSize);
|
||||||
|
}
|
||||||
|
payload = decompressedPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::Ok,
|
||||||
|
headers, payload, std::string(),
|
||||||
|
uploadSize, downloadSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponsePtr HttpClient::get(const std::string& url,
|
||||||
|
HttpRequestArgsPtr args)
|
||||||
|
{
|
||||||
|
return request(url, kGet, std::string(), args);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponsePtr HttpClient::head(const std::string& url,
|
||||||
|
HttpRequestArgsPtr args)
|
||||||
|
{
|
||||||
|
return request(url, kHead, std::string(), args);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponsePtr HttpClient::del(const std::string& url,
|
||||||
|
HttpRequestArgsPtr args)
|
||||||
|
{
|
||||||
|
return request(url, kDel, std::string(), args);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponsePtr HttpClient::post(const std::string& url,
|
||||||
|
const HttpParameters& httpParameters,
|
||||||
|
HttpRequestArgsPtr args)
|
||||||
|
{
|
||||||
|
return request(url, kPost, serializeHttpParameters(httpParameters), args);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponsePtr HttpClient::post(const std::string& url,
|
||||||
|
const std::string& body,
|
||||||
|
HttpRequestArgsPtr args)
|
||||||
|
{
|
||||||
|
return request(url, kPost, body, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponsePtr HttpClient::put(const std::string& url,
|
||||||
|
const HttpParameters& httpParameters,
|
||||||
|
HttpRequestArgsPtr args)
|
||||||
|
{
|
||||||
|
return request(url, kPut, serializeHttpParameters(httpParameters), args);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponsePtr HttpClient::put(const std::string& url,
|
||||||
|
const std::string& body,
|
||||||
|
const HttpRequestArgsPtr args)
|
||||||
|
{
|
||||||
|
return request(url, kPut, body, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string HttpClient::urlEncode(const std::string& value)
|
||||||
|
{
|
||||||
|
std::ostringstream escaped;
|
||||||
|
escaped.fill('0');
|
||||||
|
escaped << std::hex;
|
||||||
|
|
||||||
|
for (std::string::const_iterator i = value.begin(), n = value.end();
|
||||||
|
i != n; ++i)
|
||||||
|
{
|
||||||
|
std::string::value_type c = (*i);
|
||||||
|
|
||||||
|
// Keep alphanumeric and other accepted characters intact
|
||||||
|
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~')
|
||||||
|
{
|
||||||
|
escaped << c;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any other characters are percent-encoded
|
||||||
|
escaped << std::uppercase;
|
||||||
|
escaped << '%' << std::setw(2) << int((unsigned char) c);
|
||||||
|
escaped << std::nouppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
return escaped.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string HttpClient::serializeHttpParameters(const HttpParameters& httpParameters)
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
size_t count = httpParameters.size();
|
||||||
|
size_t i = 0;
|
||||||
|
|
||||||
|
for (auto&& it : httpParameters)
|
||||||
|
{
|
||||||
|
ss << urlEncode(it.first)
|
||||||
|
<< "="
|
||||||
|
<< urlEncode(it.second);
|
||||||
|
|
||||||
|
if (i++ < (count-1))
|
||||||
|
{
|
||||||
|
ss << "&";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HttpClient::gzipInflate(
|
||||||
|
const std::string& in,
|
||||||
|
std::string& out)
|
||||||
|
{
|
||||||
|
z_stream inflateState;
|
||||||
|
std::memset(&inflateState, 0, sizeof(inflateState));
|
||||||
|
|
||||||
|
inflateState.zalloc = Z_NULL;
|
||||||
|
inflateState.zfree = Z_NULL;
|
||||||
|
inflateState.opaque = Z_NULL;
|
||||||
|
inflateState.avail_in = 0;
|
||||||
|
inflateState.next_in = Z_NULL;
|
||||||
|
|
||||||
|
if (inflateInit2(&inflateState, 16+MAX_WBITS) != Z_OK)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
inflateState.avail_in = (uInt) in.size();
|
||||||
|
inflateState.next_in = (unsigned char *)(const_cast<char *>(in.data()));
|
||||||
|
|
||||||
|
const int kBufferSize = 1 << 14;
|
||||||
|
|
||||||
|
std::unique_ptr<unsigned char[]> compressBuffer =
|
||||||
|
std::make_unique<unsigned char[]>(kBufferSize);
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
inflateState.avail_out = (uInt) kBufferSize;
|
||||||
|
inflateState.next_out = compressBuffer.get();
|
||||||
|
|
||||||
|
int ret = inflate(&inflateState, Z_SYNC_FLUSH);
|
||||||
|
|
||||||
|
if (ret == Z_NEED_DICT || ret == Z_DATA_ERROR || ret == Z_MEM_ERROR)
|
||||||
|
{
|
||||||
|
inflateEnd(&inflateState);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.append(
|
||||||
|
reinterpret_cast<char *>(compressBuffer.get()),
|
||||||
|
kBufferSize - inflateState.avail_out
|
||||||
|
);
|
||||||
|
} while (inflateState.avail_out == 0);
|
||||||
|
|
||||||
|
inflateEnd(&inflateState);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpClient::log(const std::string& msg,
|
||||||
|
HttpRequestArgsPtr args)
|
||||||
|
{
|
||||||
|
if (args->logger)
|
||||||
|
{
|
||||||
|
args->logger(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
90
ixwebsocket/IXHttpClient.h
Normal file
90
ixwebsocket/IXHttpClient.h
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* IXHttpClient.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IXHttp.h"
|
||||||
|
#include "IXSocket.h"
|
||||||
|
#include "IXWebSocketHttpHeaders.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <queue>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
class HttpClient
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
HttpClient(bool async = false);
|
||||||
|
~HttpClient();
|
||||||
|
|
||||||
|
HttpResponsePtr get(const std::string& url, HttpRequestArgsPtr args);
|
||||||
|
HttpResponsePtr head(const std::string& url, HttpRequestArgsPtr args);
|
||||||
|
HttpResponsePtr del(const std::string& url, HttpRequestArgsPtr args);
|
||||||
|
|
||||||
|
HttpResponsePtr post(const std::string& url,
|
||||||
|
const HttpParameters& httpParameters,
|
||||||
|
HttpRequestArgsPtr args);
|
||||||
|
HttpResponsePtr post(const std::string& url,
|
||||||
|
const std::string& body,
|
||||||
|
HttpRequestArgsPtr args);
|
||||||
|
|
||||||
|
HttpResponsePtr put(const std::string& url,
|
||||||
|
const HttpParameters& httpParameters,
|
||||||
|
HttpRequestArgsPtr args);
|
||||||
|
HttpResponsePtr put(const std::string& url,
|
||||||
|
const std::string& body,
|
||||||
|
HttpRequestArgsPtr args);
|
||||||
|
|
||||||
|
HttpResponsePtr request(const std::string& url,
|
||||||
|
const std::string& verb,
|
||||||
|
const std::string& body,
|
||||||
|
HttpRequestArgsPtr args,
|
||||||
|
int redirects = 0);
|
||||||
|
|
||||||
|
// Async API
|
||||||
|
HttpRequestArgsPtr createRequest(const std::string& url = std::string(),
|
||||||
|
const std::string& verb = HttpClient::kGet);
|
||||||
|
|
||||||
|
bool performRequest(HttpRequestArgsPtr request,
|
||||||
|
const OnResponseCallback& onResponseCallback);
|
||||||
|
|
||||||
|
std::string serializeHttpParameters(const HttpParameters& httpParameters);
|
||||||
|
|
||||||
|
std::string urlEncode(const std::string& value);
|
||||||
|
|
||||||
|
const static std::string kPost;
|
||||||
|
const static std::string kGet;
|
||||||
|
const static std::string kHead;
|
||||||
|
const static std::string kDel;
|
||||||
|
const static std::string kPut;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void log(const std::string& msg, HttpRequestArgsPtr args);
|
||||||
|
|
||||||
|
bool gzipInflate(const std::string& in, std::string& out);
|
||||||
|
|
||||||
|
// Async API background thread runner
|
||||||
|
void run();
|
||||||
|
|
||||||
|
// Async API
|
||||||
|
bool _async;
|
||||||
|
std::queue<std::pair<HttpRequestArgsPtr, OnResponseCallback>> _queue;
|
||||||
|
mutable std::mutex _queueMutex;
|
||||||
|
std::condition_variable _condition;
|
||||||
|
std::atomic<bool> _stop;
|
||||||
|
std::thread _thread;
|
||||||
|
|
||||||
|
std::shared_ptr<Socket> _socket;
|
||||||
|
std::mutex _mutex; // to protect accessing the _socket (only one socket per client)
|
||||||
|
};
|
||||||
|
} // namespace ix
|
160
ixwebsocket/IXHttpServer.cpp
Normal file
160
ixwebsocket/IXHttpServer.cpp
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
/*
|
||||||
|
* IXHttpServer.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXHttpServer.h"
|
||||||
|
#include "IXSocketConnect.h"
|
||||||
|
#include "IXSocketFactory.h"
|
||||||
|
#include "IXNetSystem.h"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <fstream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::pair<bool, std::vector<uint8_t>> load(const std::string& path)
|
||||||
|
{
|
||||||
|
std::vector<uint8_t> memblock;
|
||||||
|
|
||||||
|
std::ifstream file(path);
|
||||||
|
if (!file.is_open()) return std::make_pair(false, memblock);
|
||||||
|
|
||||||
|
file.seekg(0, file.end);
|
||||||
|
std::streamoff size = file.tellg();
|
||||||
|
file.seekg(0, file.beg);
|
||||||
|
|
||||||
|
memblock.resize((size_t) size);
|
||||||
|
file.read((char*)&memblock.front(), static_cast<std::streamsize>(size));
|
||||||
|
|
||||||
|
return std::make_pair(true, memblock);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<bool, std::string> readAsString(const std::string& path)
|
||||||
|
{
|
||||||
|
auto res = load(path);
|
||||||
|
auto vec = res.second;
|
||||||
|
return std::make_pair(res.first, std::string(vec.begin(), vec.end()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
HttpServer::HttpServer(int port,
|
||||||
|
const std::string& host,
|
||||||
|
int backlog,
|
||||||
|
size_t maxConnections) : SocketServer(port, host, backlog, maxConnections),
|
||||||
|
_connectedClientsCount(0)
|
||||||
|
{
|
||||||
|
setDefaultConnectionCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpServer::~HttpServer()
|
||||||
|
{
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpServer::stop()
|
||||||
|
{
|
||||||
|
stopAcceptingConnections();
|
||||||
|
|
||||||
|
// FIXME: cancelling / closing active clients ...
|
||||||
|
|
||||||
|
SocketServer::stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpServer::setOnConnectionCallback(const OnConnectionCallback& callback)
|
||||||
|
{
|
||||||
|
_onConnectionCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpServer::handleConnection(
|
||||||
|
int fd,
|
||||||
|
std::shared_ptr<ConnectionState> connectionState)
|
||||||
|
{
|
||||||
|
_connectedClientsCount++;
|
||||||
|
|
||||||
|
std::string errorMsg;
|
||||||
|
auto socket = createSocket(fd, errorMsg);
|
||||||
|
|
||||||
|
// Set the socket to non blocking mode + other tweaks
|
||||||
|
SocketConnect::configure(fd);
|
||||||
|
|
||||||
|
auto ret = Http::parseRequest(socket);
|
||||||
|
// FIXME: handle errors in parseRequest
|
||||||
|
|
||||||
|
if (std::get<0>(ret))
|
||||||
|
{
|
||||||
|
auto response = _onConnectionCallback(std::get<2>(ret), connectionState);
|
||||||
|
if (!Http::sendResponse(response, socket))
|
||||||
|
{
|
||||||
|
logError("Cannot send response");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connectionState->setTerminated();
|
||||||
|
|
||||||
|
_connectedClientsCount--;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t HttpServer::getConnectedClientsCount()
|
||||||
|
{
|
||||||
|
return _connectedClientsCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpServer::setDefaultConnectionCallback()
|
||||||
|
{
|
||||||
|
setOnConnectionCallback(
|
||||||
|
[this](HttpRequestPtr request,
|
||||||
|
std::shared_ptr<ConnectionState> /*connectionState*/) -> HttpResponsePtr
|
||||||
|
{
|
||||||
|
std::string uri(request->uri);
|
||||||
|
if (uri.empty() || uri == "/")
|
||||||
|
{
|
||||||
|
uri = "/index.html";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string path("." + uri);
|
||||||
|
auto res = readAsString(path);
|
||||||
|
bool found = res.first;
|
||||||
|
if (!found)
|
||||||
|
{
|
||||||
|
return std::make_shared<HttpResponse>(404, "Not Found",
|
||||||
|
HttpErrorCode::Ok,
|
||||||
|
WebSocketHttpHeaders(),
|
||||||
|
std::string());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string content = res.second;
|
||||||
|
|
||||||
|
// Log request
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << request->method
|
||||||
|
<< " "
|
||||||
|
<< request->headers["User-Agent"]
|
||||||
|
<< " "
|
||||||
|
<< request->uri
|
||||||
|
<< " "
|
||||||
|
<< content.size();
|
||||||
|
logInfo(ss.str());
|
||||||
|
|
||||||
|
WebSocketHttpHeaders headers;
|
||||||
|
// FIXME: check extensions to set the content type
|
||||||
|
// headers["Content-Type"] = "application/octet-stream";
|
||||||
|
headers["Accept-Ranges"] = "none";
|
||||||
|
|
||||||
|
for (auto&& it : request->headers)
|
||||||
|
{
|
||||||
|
headers[it.first] = it.second;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::make_shared<HttpResponse>(200, "OK",
|
||||||
|
HttpErrorCode::Ok,
|
||||||
|
headers,
|
||||||
|
content);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
49
ixwebsocket/IXHttpServer.h
Normal file
49
ixwebsocket/IXHttpServer.h
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* IXHttpServer.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IXHttp.h"
|
||||||
|
#include "IXSocketServer.h"
|
||||||
|
#include "IXWebSocket.h"
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <utility> // pair
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
class HttpServer final : public SocketServer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using OnConnectionCallback =
|
||||||
|
std::function<HttpResponsePtr(HttpRequestPtr, std::shared_ptr<ConnectionState>)>;
|
||||||
|
|
||||||
|
HttpServer(int port = SocketServer::kDefaultPort,
|
||||||
|
const std::string& host = SocketServer::kDefaultHost,
|
||||||
|
int backlog = SocketServer::kDefaultTcpBacklog,
|
||||||
|
size_t maxConnections = SocketServer::kDefaultMaxConnections);
|
||||||
|
virtual ~HttpServer();
|
||||||
|
virtual void stop() final;
|
||||||
|
|
||||||
|
void setOnConnectionCallback(const OnConnectionCallback& callback);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Member variables
|
||||||
|
OnConnectionCallback _onConnectionCallback;
|
||||||
|
std::atomic<int> _connectedClientsCount;
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
virtual void handleConnection(int fd,
|
||||||
|
std::shared_ptr<ConnectionState> connectionState) final;
|
||||||
|
virtual size_t getConnectedClientsCount() final;
|
||||||
|
|
||||||
|
void setDefaultConnectionCallback();
|
||||||
|
};
|
||||||
|
} // namespace ix
|
111
ixwebsocket/IXNetSystem.cpp
Normal file
111
ixwebsocket/IXNetSystem.cpp
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
* IXNetSystem.cpp
|
||||||
|
* Author: Korchynskyi Dmytro
|
||||||
|
* Copyright (c) 2019 Machine Zone. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXNetSystem.h"
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
bool initNetSystem()
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
WORD wVersionRequested;
|
||||||
|
WSADATA wsaData;
|
||||||
|
int err;
|
||||||
|
|
||||||
|
// Use the MAKEWORD(lowbyte, highbyte) macro declared in Windef.h
|
||||||
|
wVersionRequested = MAKEWORD(2, 2);
|
||||||
|
err = WSAStartup(wVersionRequested, &wsaData);
|
||||||
|
|
||||||
|
return err == 0;
|
||||||
|
#else
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool uninitNetSystem()
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
int err = WSACleanup();
|
||||||
|
return err == 0;
|
||||||
|
#else
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function should be in the global namespace
|
||||||
|
#ifdef _WIN32
|
||||||
|
//
|
||||||
|
// That function could 'return WSAPoll(pfd, nfds, timeout);'
|
||||||
|
// but WSAPoll is said to have weird behaviors on the internet
|
||||||
|
// (the curl folks have had problems with it).
|
||||||
|
//
|
||||||
|
// So we make it a select wrapper
|
||||||
|
//
|
||||||
|
int poll(struct pollfd *fds, nfds_t nfds, int timeout)
|
||||||
|
{
|
||||||
|
int maxfd = 0;
|
||||||
|
fd_set readfds, writefds, errorfds;
|
||||||
|
FD_ZERO(&readfds);
|
||||||
|
FD_ZERO(&writefds);
|
||||||
|
FD_ZERO(&errorfds);
|
||||||
|
|
||||||
|
for (nfds_t i = 0; i < nfds; ++i)
|
||||||
|
{
|
||||||
|
struct pollfd *fd = &fds[i];
|
||||||
|
|
||||||
|
if (fd->fd > maxfd)
|
||||||
|
{
|
||||||
|
maxfd = fd->fd;
|
||||||
|
}
|
||||||
|
if ((fd->events & POLLIN))
|
||||||
|
{
|
||||||
|
FD_SET(fd->fd, &readfds);
|
||||||
|
}
|
||||||
|
if ((fd->events & POLLOUT))
|
||||||
|
{
|
||||||
|
FD_SET(fd->fd, &writefds);
|
||||||
|
}
|
||||||
|
if ((fd->events & POLLERR))
|
||||||
|
{
|
||||||
|
FD_SET(fd->fd, &errorfds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct timeval tv;
|
||||||
|
tv.tv_sec = timeout / 1000;
|
||||||
|
tv.tv_usec = (timeout % 1000) * 1000;
|
||||||
|
|
||||||
|
int ret = select(maxfd + 1, &readfds, &writefds, &errorfds,
|
||||||
|
timeout != -1 ? &tv : NULL);
|
||||||
|
|
||||||
|
if (ret < 0)
|
||||||
|
{
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (nfds_t i = 0; i < nfds; ++i)
|
||||||
|
{
|
||||||
|
struct pollfd *fd = &fds[i];
|
||||||
|
fd->revents = 0;
|
||||||
|
|
||||||
|
if (FD_ISSET(fd->fd, &readfds))
|
||||||
|
{
|
||||||
|
fd->revents |= POLLIN;
|
||||||
|
}
|
||||||
|
if (FD_ISSET(fd->fd, &writefds))
|
||||||
|
{
|
||||||
|
fd->revents |= POLLOUT;
|
||||||
|
}
|
||||||
|
if (FD_ISSET(fd->fd, &errorfds))
|
||||||
|
{
|
||||||
|
fd->revents |= POLLERR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
#endif
|
38
ixwebsocket/IXNetSystem.h
Normal file
38
ixwebsocket/IXNetSystem.h
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* IXNetSystem.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <WS2tcpip.h>
|
||||||
|
#include <WinSock2.h>
|
||||||
|
#include <basetsd.h>
|
||||||
|
#include <io.h>
|
||||||
|
#include <ws2def.h>
|
||||||
|
|
||||||
|
// Define our own poll on Windows
|
||||||
|
typedef unsigned long int nfds_t;
|
||||||
|
|
||||||
|
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
|
||||||
|
|
||||||
|
#else
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <netdb.h>
|
||||||
|
#include <netinet/tcp.h>
|
||||||
|
#include <poll.h>
|
||||||
|
#include <sys/select.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
bool initNetSystem();
|
||||||
|
bool uninitNetSystem();
|
||||||
|
} // namespace ix
|
14
ixwebsocket/IXProgressCallback.h
Normal file
14
ixwebsocket/IXProgressCallback.h
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
* IXProgressCallback.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
using OnProgressCallback = std::function<bool(int current, int total)>;
|
||||||
|
}
|
46
ixwebsocket/IXSelectInterrupt.cpp
Normal file
46
ixwebsocket/IXSelectInterrupt.cpp
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* IXSelectInterrupt.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXSelectInterrupt.h"
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
SelectInterrupt::SelectInterrupt()
|
||||||
|
{
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectInterrupt::~SelectInterrupt()
|
||||||
|
{
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SelectInterrupt::init(std::string& /*errorMsg*/)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SelectInterrupt::notify(uint64_t /*value*/)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t SelectInterrupt::read()
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SelectInterrupt::clear()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int SelectInterrupt::getFd() const
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
27
ixwebsocket/IXSelectInterrupt.h
Normal file
27
ixwebsocket/IXSelectInterrupt.h
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* IXSelectInterrupt.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
class SelectInterrupt
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SelectInterrupt();
|
||||||
|
virtual ~SelectInterrupt();
|
||||||
|
|
||||||
|
virtual bool init(std::string& errorMsg);
|
||||||
|
|
||||||
|
virtual bool notify(uint64_t value);
|
||||||
|
virtual bool clear();
|
||||||
|
virtual uint64_t read();
|
||||||
|
virtual int getFd() const;
|
||||||
|
};
|
||||||
|
} // namespace ix
|
116
ixwebsocket/IXSelectInterruptEventFd.cpp
Normal file
116
ixwebsocket/IXSelectInterruptEventFd.cpp
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
* IXSelectInterruptEventFd.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018-2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
//
|
||||||
|
// On Linux we use eventd to wake up select.
|
||||||
|
//
|
||||||
|
|
||||||
|
//
|
||||||
|
// Linux/Android has a special type of virtual files. select(2) will react
|
||||||
|
// when reading/writing to those files, unlike closing sockets.
|
||||||
|
//
|
||||||
|
// https://linux.die.net/man/2/eventfd
|
||||||
|
// http://www.sourcexr.com/articles/2013/10/26/lightweight-inter-process-signaling-with-eventfd
|
||||||
|
//
|
||||||
|
// eventfd was added in Linux kernel 2.x, and our oldest Android (Kitkat 4.4)
|
||||||
|
// is on Kernel 3.x
|
||||||
|
//
|
||||||
|
// cf Android/Kernel table here
|
||||||
|
// https://android.stackexchange.com/questions/51651/which-android-runs-which-linux-kernel
|
||||||
|
//
|
||||||
|
// On macOS we use UNIX pipes to wake up select.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "IXSelectInterruptEventFd.h"
|
||||||
|
|
||||||
|
#include <sys/eventfd.h>
|
||||||
|
|
||||||
|
#include <unistd.h> // for write
|
||||||
|
#include <string.h> // for strerror
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <assert.h>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
SelectInterruptEventFd::SelectInterruptEventFd()
|
||||||
|
{
|
||||||
|
_eventfd = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectInterruptEventFd::~SelectInterruptEventFd()
|
||||||
|
{
|
||||||
|
::close(_eventfd);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SelectInterruptEventFd::init(std::string& errorMsg)
|
||||||
|
{
|
||||||
|
// calling init twice is a programming error
|
||||||
|
assert(_eventfd == -1);
|
||||||
|
|
||||||
|
_eventfd = eventfd(0, 0);
|
||||||
|
if (_eventfd < 0)
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "SelectInterruptEventFd::init() failed in eventfd()"
|
||||||
|
<< " : " << strerror(errno);
|
||||||
|
errorMsg = ss.str();
|
||||||
|
|
||||||
|
_eventfd = -1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fcntl(_eventfd, F_SETFL, O_NONBLOCK) == -1)
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "SelectInterruptEventFd::init() failed in fcntl() call"
|
||||||
|
<< " : " << strerror(errno);
|
||||||
|
errorMsg = ss.str();
|
||||||
|
|
||||||
|
_eventfd = -1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SelectInterruptEventFd::notify(uint64_t value)
|
||||||
|
{
|
||||||
|
int fd = _eventfd;
|
||||||
|
|
||||||
|
if (fd == -1) return false;
|
||||||
|
|
||||||
|
// we should write 8 bytes for an uint64_t
|
||||||
|
return write(fd, &value, sizeof(value)) == 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: return max uint64_t for errors ?
|
||||||
|
uint64_t SelectInterruptEventFd::read()
|
||||||
|
{
|
||||||
|
int fd = _eventfd;
|
||||||
|
|
||||||
|
uint64_t value = 0;
|
||||||
|
::read(fd, &value, sizeof(value));
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SelectInterruptEventFd::clear()
|
||||||
|
{
|
||||||
|
if (_eventfd == -1) return false;
|
||||||
|
|
||||||
|
// 0 is a special value ; select will not wake up
|
||||||
|
uint64_t value = 0;
|
||||||
|
|
||||||
|
// we should write 8 bytes for an uint64_t
|
||||||
|
return write(_eventfd, &value, sizeof(value)) == 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
int SelectInterruptEventFd::getFd() const
|
||||||
|
{
|
||||||
|
return _eventfd;
|
||||||
|
}
|
||||||
|
}
|
31
ixwebsocket/IXSelectInterruptEventFd.h
Normal file
31
ixwebsocket/IXSelectInterruptEventFd.h
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* IXSelectInterruptEventFd.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018-2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IXSelectInterrupt.h"
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
class SelectInterruptEventFd final : public SelectInterrupt
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SelectInterruptEventFd();
|
||||||
|
virtual ~SelectInterruptEventFd();
|
||||||
|
|
||||||
|
bool init(std::string& errorMsg) final;
|
||||||
|
|
||||||
|
bool notify(uint64_t value) final;
|
||||||
|
bool clear() final;
|
||||||
|
uint64_t read() final;
|
||||||
|
int getFd() const final;
|
||||||
|
|
||||||
|
private:
|
||||||
|
int _eventfd;
|
||||||
|
};
|
||||||
|
} // namespace ix
|
25
ixwebsocket/IXSelectInterruptFactory.cpp
Normal file
25
ixwebsocket/IXSelectInterruptFactory.cpp
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* IXSelectInterruptFactory.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXSelectInterruptFactory.h"
|
||||||
|
|
||||||
|
#if defined(__linux__) || defined(__APPLE__)
|
||||||
|
# include <ixwebsocket/IXSelectInterruptPipe.h>
|
||||||
|
#else
|
||||||
|
# include <ixwebsocket/IXSelectInterrupt.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
std::shared_ptr<SelectInterrupt> createSelectInterrupt()
|
||||||
|
{
|
||||||
|
#if defined(__linux__) || defined(__APPLE__)
|
||||||
|
return std::make_shared<SelectInterruptPipe>();
|
||||||
|
#else
|
||||||
|
return std::make_shared<SelectInterrupt>();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
15
ixwebsocket/IXSelectInterruptFactory.h
Normal file
15
ixwebsocket/IXSelectInterruptFactory.h
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* IXSelectInterruptFactory.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
class SelectInterrupt;
|
||||||
|
std::shared_ptr<SelectInterrupt> createSelectInterrupt();
|
||||||
|
} // namespace ix
|
146
ixwebsocket/IXSelectInterruptPipe.cpp
Normal file
146
ixwebsocket/IXSelectInterruptPipe.cpp
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
/*
|
||||||
|
* IXSelectInterruptPipe.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018-2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
//
|
||||||
|
// On macOS we use UNIX pipes to wake up select.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "IXSelectInterruptPipe.h"
|
||||||
|
|
||||||
|
#include <unistd.h> // for write
|
||||||
|
#include <string.h> // for strerror
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <assert.h>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
// File descriptor at index 0 in _fildes is the read end of the pipe
|
||||||
|
// File descriptor at index 1 in _fildes is the write end of the pipe
|
||||||
|
const int SelectInterruptPipe::kPipeReadIndex = 0;
|
||||||
|
const int SelectInterruptPipe::kPipeWriteIndex = 1;
|
||||||
|
|
||||||
|
SelectInterruptPipe::SelectInterruptPipe()
|
||||||
|
{
|
||||||
|
_fildes[kPipeReadIndex] = -1;
|
||||||
|
_fildes[kPipeWriteIndex] = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectInterruptPipe::~SelectInterruptPipe()
|
||||||
|
{
|
||||||
|
::close(_fildes[kPipeReadIndex]);
|
||||||
|
::close(_fildes[kPipeWriteIndex]);
|
||||||
|
_fildes[kPipeReadIndex] = -1;
|
||||||
|
_fildes[kPipeWriteIndex] = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SelectInterruptPipe::init(std::string& errorMsg)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_fildesMutex);
|
||||||
|
|
||||||
|
// calling init twice is a programming error
|
||||||
|
assert(_fildes[kPipeReadIndex] == -1);
|
||||||
|
assert(_fildes[kPipeWriteIndex] == -1);
|
||||||
|
|
||||||
|
if (pipe(_fildes) < 0)
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "SelectInterruptPipe::init() failed in pipe() call"
|
||||||
|
<< " : " << strerror(errno);
|
||||||
|
errorMsg = ss.str();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fcntl(_fildes[kPipeReadIndex], F_SETFL, O_NONBLOCK) == -1)
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "SelectInterruptPipe::init() failed in fcntl(..., O_NONBLOCK) call"
|
||||||
|
<< " : " << strerror(errno);
|
||||||
|
errorMsg = ss.str();
|
||||||
|
|
||||||
|
_fildes[kPipeReadIndex] = -1;
|
||||||
|
_fildes[kPipeWriteIndex] = -1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fcntl(_fildes[kPipeWriteIndex], F_SETFL, O_NONBLOCK) == -1)
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "SelectInterruptPipe::init() failed in fcntl(..., O_NONBLOCK) call"
|
||||||
|
<< " : " << strerror(errno);
|
||||||
|
errorMsg = ss.str();
|
||||||
|
|
||||||
|
_fildes[kPipeReadIndex] = -1;
|
||||||
|
_fildes[kPipeWriteIndex] = -1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef F_SETNOSIGPIPE
|
||||||
|
if (fcntl(_fildes[kPipeWriteIndex], F_SETNOSIGPIPE, 1) == -1)
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "SelectInterruptPipe::init() failed in fcntl(.... F_SETNOSIGPIPE) call"
|
||||||
|
<< " : " << strerror(errno);
|
||||||
|
errorMsg = ss.str();
|
||||||
|
|
||||||
|
_fildes[kPipeReadIndex] = -1;
|
||||||
|
_fildes[kPipeWriteIndex] = -1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fcntl(_fildes[kPipeWriteIndex], F_SETNOSIGPIPE, 1) == -1)
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "SelectInterruptPipe::init() failed in fcntl(..., F_SETNOSIGPIPE) call"
|
||||||
|
<< " : " << strerror(errno);
|
||||||
|
errorMsg = ss.str();
|
||||||
|
|
||||||
|
_fildes[kPipeReadIndex] = -1;
|
||||||
|
_fildes[kPipeWriteIndex] = -1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SelectInterruptPipe::notify(uint64_t value)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_fildesMutex);
|
||||||
|
|
||||||
|
int fd = _fildes[kPipeWriteIndex];
|
||||||
|
if (fd == -1) return false;
|
||||||
|
|
||||||
|
// we should write 8 bytes for an uint64_t
|
||||||
|
return write(fd, &value, sizeof(value)) == 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: return max uint64_t for errors ?
|
||||||
|
uint64_t SelectInterruptPipe::read()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_fildesMutex);
|
||||||
|
|
||||||
|
int fd = _fildes[kPipeReadIndex];
|
||||||
|
|
||||||
|
uint64_t value = 0;
|
||||||
|
::read(fd, &value, sizeof(value));
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SelectInterruptPipe::clear()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int SelectInterruptPipe::getFd() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_fildesMutex);
|
||||||
|
|
||||||
|
return _fildes[kPipeReadIndex];
|
||||||
|
}
|
||||||
|
}
|
40
ixwebsocket/IXSelectInterruptPipe.h
Normal file
40
ixwebsocket/IXSelectInterruptPipe.h
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* IXSelectInterruptPipe.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018-2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IXSelectInterrupt.h"
|
||||||
|
#include <mutex>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
class SelectInterruptPipe final : public SelectInterrupt
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SelectInterruptPipe();
|
||||||
|
virtual ~SelectInterruptPipe();
|
||||||
|
|
||||||
|
bool init(std::string& errorMsg) final;
|
||||||
|
|
||||||
|
bool notify(uint64_t value) final;
|
||||||
|
bool clear() final;
|
||||||
|
uint64_t read() final;
|
||||||
|
int getFd() const final;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Store file descriptors used by the communication pipe. Communication
|
||||||
|
// happens between a control thread and a background thread, which is
|
||||||
|
// blocked on select.
|
||||||
|
int _fildes[2];
|
||||||
|
mutable std::mutex _fildesMutex;
|
||||||
|
|
||||||
|
// Used to identify the read/write idx
|
||||||
|
static const int kPipeReadIndex;
|
||||||
|
static const int kPipeWriteIndex;
|
||||||
|
};
|
||||||
|
} // namespace ix
|
12
ixwebsocket/IXSetThreadName.h
Normal file
12
ixwebsocket/IXSetThreadName.h
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* IXSetThreadName.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
void setThreadName(const std::string& name);
|
||||||
|
}
|
@ -5,211 +5,177 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "IXSocket.h"
|
#include "IXSocket.h"
|
||||||
|
#include "IXSocketConnect.h"
|
||||||
|
#include "IXNetSystem.h"
|
||||||
|
#include "IXSelectInterrupt.h"
|
||||||
|
#include "IXSelectInterruptFactory.h"
|
||||||
|
|
||||||
#include <netdb.h>
|
|
||||||
#include <netinet/tcp.h>
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <sys/socket.h>
|
|
||||||
#include <sys/time.h>
|
|
||||||
#include <sys/types.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <sys/select.h>
|
|
||||||
#include <errno.h>
|
|
||||||
#include <sys/types.h>
|
|
||||||
#include <sys/stat.h>
|
|
||||||
#include <fcntl.h>
|
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
//
|
#include <algorithm>
|
||||||
// Linux/Android has a special type of virtual files. select(2) will react
|
|
||||||
// when reading/writing to those files, unlike closing sockets.
|
|
||||||
//
|
|
||||||
// https://linux.die.net/man/2/eventfd
|
|
||||||
//
|
|
||||||
// eventfd was added in Linux kernel 2.x, and our oldest Android (Kitkat 4.4)
|
|
||||||
// is on Kernel 3.x
|
|
||||||
//
|
|
||||||
// cf Android/Kernel table here
|
|
||||||
// https://android.stackexchange.com/questions/51651/which-android-runs-which-linux-kernel
|
|
||||||
//
|
|
||||||
#ifndef __APPLE__
|
|
||||||
# include <sys/eventfd.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Android needs extra headers for TCP_NODELAY and IPPROTO_TCP
|
#ifdef min
|
||||||
#ifdef ANDROID
|
#undef min
|
||||||
# include <linux/in.h>
|
|
||||||
# include <linux/tcp.h>
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace ix
|
namespace ix
|
||||||
{
|
{
|
||||||
Socket::Socket() :
|
const int Socket::kDefaultPollNoTimeout = -1; // No poll timeout by default
|
||||||
_sockfd(-1),
|
const int Socket::kDefaultPollTimeout = kDefaultPollNoTimeout;
|
||||||
_eventfd(-1)
|
const uint64_t Socket::kSendRequest = 1;
|
||||||
|
const uint64_t Socket::kCloseRequest = 2;
|
||||||
|
constexpr size_t Socket::kChunkSize;
|
||||||
|
|
||||||
|
Socket::Socket(int fd) :
|
||||||
|
_sockfd(fd),
|
||||||
|
_selectInterrupt(createSelectInterrupt())
|
||||||
{
|
{
|
||||||
#ifndef __APPLE__
|
;
|
||||||
_eventfd = eventfd(0, 0);
|
|
||||||
assert(_eventfd != -1 && "Panic - eventfd not functioning on this platform");
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Socket::~Socket()
|
Socket::~Socket()
|
||||||
{
|
{
|
||||||
close();
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
#ifndef __APPLE__
|
PollResultType Socket::poll(bool readyToRead,
|
||||||
::close(_eventfd);
|
int timeoutMs,
|
||||||
|
int sockfd,
|
||||||
|
std::shared_ptr<SelectInterrupt> selectInterrupt)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// We used to use ::select to poll but on Android 9 we get large fds out of ::connect
|
||||||
|
// which crash in FD_SET as they are larger than FD_SETSIZE.
|
||||||
|
// Switching to ::poll does fix that.
|
||||||
|
//
|
||||||
|
// However poll isn't as portable as select and has bugs on Windows, so we should write a
|
||||||
|
// shim to fallback to select on those platforms.
|
||||||
|
// See https://github.com/mpv-player/mpv/pull/5203/files for such a select wrapper.
|
||||||
|
//
|
||||||
|
nfds_t nfds = 1;
|
||||||
|
struct pollfd fds[2];
|
||||||
|
|
||||||
|
fds[0].fd = sockfd;
|
||||||
|
fds[0].events = (readyToRead) ? POLLIN : POLLOUT;
|
||||||
|
fds[0].events |= POLLERR;
|
||||||
|
|
||||||
|
// File descriptor used to interrupt select when needed
|
||||||
|
int interruptFd = -1;
|
||||||
|
if (selectInterrupt)
|
||||||
|
{
|
||||||
|
interruptFd = selectInterrupt->getFd();
|
||||||
|
|
||||||
|
if (interruptFd != -1)
|
||||||
|
{
|
||||||
|
nfds = 2;
|
||||||
|
fds[1].fd = interruptFd;
|
||||||
|
fds[1].events = POLLIN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int ret = ::poll(fds, nfds, timeoutMs);
|
||||||
|
|
||||||
|
PollResultType pollResult = PollResultType::ReadyForRead;
|
||||||
|
if (ret < 0)
|
||||||
|
{
|
||||||
|
pollResult = PollResultType::Error;
|
||||||
|
}
|
||||||
|
else if (ret == 0)
|
||||||
|
{
|
||||||
|
pollResult = PollResultType::Timeout;
|
||||||
|
}
|
||||||
|
else if (interruptFd != -1 && fds[1].revents & POLLIN)
|
||||||
|
{
|
||||||
|
uint64_t value = selectInterrupt->read();
|
||||||
|
|
||||||
|
if (value == kSendRequest)
|
||||||
|
{
|
||||||
|
pollResult = PollResultType::SendRequest;
|
||||||
|
}
|
||||||
|
else if (value == kCloseRequest)
|
||||||
|
{
|
||||||
|
pollResult = PollResultType::CloseRequest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (sockfd != -1 && readyToRead && fds[0].revents & POLLIN)
|
||||||
|
{
|
||||||
|
pollResult = PollResultType::ReadyForRead;
|
||||||
|
}
|
||||||
|
else if (sockfd != -1 && !readyToRead && fds[0].revents & POLLOUT)
|
||||||
|
{
|
||||||
|
pollResult = PollResultType::ReadyForWrite;
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
// On connect error, in async mode, windows will write to the exceptions fds
|
||||||
|
if (fds[0].revents & POLLERR)
|
||||||
|
{
|
||||||
|
pollResult = PollResultType::Error;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
int optval = -1;
|
||||||
|
socklen_t optlen = sizeof(optval);
|
||||||
|
|
||||||
|
// getsockopt() puts the errno value for connect into optval so 0
|
||||||
|
// means no-error.
|
||||||
|
if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &optval, &optlen) == -1 ||
|
||||||
|
optval != 0)
|
||||||
|
{
|
||||||
|
pollResult = PollResultType::Error;
|
||||||
|
|
||||||
|
// set errno to optval so that external callers can have an
|
||||||
|
// appropriate error description when calling strerror
|
||||||
|
errno = optval;
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
bool connectToAddress(const struct addrinfo *address,
|
return pollResult;
|
||||||
int& sockfd,
|
|
||||||
std::string& errMsg)
|
|
||||||
{
|
|
||||||
sockfd = -1;
|
|
||||||
|
|
||||||
int fd = socket(address->ai_family,
|
|
||||||
address->ai_socktype,
|
|
||||||
address->ai_protocol);
|
|
||||||
if (fd < 0)
|
|
||||||
{
|
|
||||||
errMsg = "Cannot create a socket";
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int maxRetries = 3;
|
PollResultType Socket::isReadyToRead(int timeoutMs)
|
||||||
for (int i = 0; i < maxRetries; ++i)
|
|
||||||
{
|
|
||||||
if (connect(fd, address->ai_addr, address->ai_addrlen) != -1)
|
|
||||||
{
|
|
||||||
sockfd = fd;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// EINTR means we've been interrupted, in which case we try again.
|
|
||||||
if (errno != EINTR) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
::close(fd);
|
|
||||||
sockfd = -1;
|
|
||||||
errMsg = strerror(errno);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int Socket::hostname_connect(const std::string& hostname,
|
|
||||||
int port,
|
|
||||||
std::string& errMsg)
|
|
||||||
{
|
|
||||||
struct addrinfo hints;
|
|
||||||
memset(&hints, 0, sizeof(hints));
|
|
||||||
hints.ai_flags = AI_ADDRCONFIG | AI_NUMERICSERV;
|
|
||||||
hints.ai_family = AF_UNSPEC;
|
|
||||||
hints.ai_socktype = SOCK_STREAM;
|
|
||||||
|
|
||||||
std::string sport = std::to_string(port);
|
|
||||||
|
|
||||||
struct addrinfo *res = nullptr;
|
|
||||||
int getaddrinfo_result = getaddrinfo(hostname.c_str(), sport.c_str(),
|
|
||||||
&hints, &res);
|
|
||||||
if (getaddrinfo_result)
|
|
||||||
{
|
|
||||||
errMsg = gai_strerror(getaddrinfo_result);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int sockfd = -1;
|
|
||||||
|
|
||||||
// iterate through the records to find a working peer
|
|
||||||
struct addrinfo *address;
|
|
||||||
bool success = false;
|
|
||||||
for (address = res; address != nullptr; address = address->ai_next)
|
|
||||||
{
|
|
||||||
success = connectToAddress(address, sockfd, errMsg);
|
|
||||||
if (success)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
freeaddrinfo(res);
|
|
||||||
return sockfd;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Socket::configure()
|
|
||||||
{
|
|
||||||
int flag = 1;
|
|
||||||
setsockopt(_sockfd, IPPROTO_TCP, TCP_NODELAY, (char*) &flag, sizeof(flag)); // Disable Nagle's algorithm
|
|
||||||
fcntl(_sockfd, F_SETFL, O_NONBLOCK); // make socket non blocking
|
|
||||||
|
|
||||||
#ifdef SO_NOSIGPIPE
|
|
||||||
int value = 1;
|
|
||||||
setsockopt(_sockfd, SOL_SOCKET, SO_NOSIGPIPE,
|
|
||||||
(void *)&value, sizeof(value));
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
void Socket::poll(const OnPollCallback& onPollCallback)
|
|
||||||
{
|
{
|
||||||
if (_sockfd == -1)
|
if (_sockfd == -1)
|
||||||
{
|
{
|
||||||
onPollCallback();
|
return PollResultType::Error;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fd_set rfds;
|
bool readyToRead = true;
|
||||||
FD_ZERO(&rfds);
|
return poll(readyToRead, timeoutMs, _sockfd, _selectInterrupt);
|
||||||
FD_SET(_sockfd, &rfds);
|
|
||||||
|
|
||||||
#ifndef __APPLE__
|
|
||||||
FD_SET(_eventfd, &rfds);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
int sockfd = _sockfd;
|
|
||||||
int nfds = std::max(sockfd, _eventfd);
|
|
||||||
select(nfds + 1, &rfds, nullptr, nullptr, nullptr);
|
|
||||||
|
|
||||||
onPollCallback();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Socket::wakeUpFromPollApple()
|
PollResultType Socket::isReadyToWrite(int timeoutMs)
|
||||||
{
|
{
|
||||||
close(); // All OS but Linux will wake up select
|
if (_sockfd == -1)
|
||||||
// when closing the file descriptor watched by select
|
{
|
||||||
|
return PollResultType::Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Socket::wakeUpFromPollLinux()
|
bool readyToRead = false;
|
||||||
{
|
return poll(readyToRead, timeoutMs, _sockfd, _selectInterrupt);
|
||||||
std::string str("\n"); // this will wake up the thread blocked on select
|
|
||||||
const void* buf = reinterpret_cast<const void*>(str.c_str());
|
|
||||||
write(_eventfd, buf, str.size());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Socket::wakeUpFromPoll()
|
// Wake up from poll/select by writing to the pipe which is watched by select
|
||||||
|
bool Socket::wakeUpFromPoll(uint64_t wakeUpCode)
|
||||||
{
|
{
|
||||||
#ifdef __APPLE__
|
return _selectInterrupt->notify(wakeUpCode);
|
||||||
wakeUpFromPollApple();
|
|
||||||
#else
|
|
||||||
wakeUpFromPollLinux();
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Socket::connect(const std::string& host,
|
bool Socket::connect(const std::string& host,
|
||||||
int port,
|
int port,
|
||||||
std::string& errMsg)
|
std::string& errMsg,
|
||||||
|
const CancellationRequest& isCancellationRequested)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(_socketMutex);
|
std::lock_guard<std::mutex> lock(_socketMutex);
|
||||||
|
|
||||||
#ifndef __APPLE__
|
if (!_selectInterrupt->clear()) return false;
|
||||||
if (_eventfd == -1)
|
|
||||||
{
|
|
||||||
return false; // impossible to use this socket if eventfd is broken
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
_sockfd = Socket::hostname_connect(host, port, errMsg);
|
_sockfd = SocketConnect::connect(host, port, errMsg, isCancellationRequested);
|
||||||
return _sockfd != -1;
|
return _sockfd != -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,33 +185,211 @@ namespace ix
|
|||||||
|
|
||||||
if (_sockfd == -1) return;
|
if (_sockfd == -1) return;
|
||||||
|
|
||||||
::close(_sockfd);
|
closeSocket(_sockfd);
|
||||||
_sockfd = -1;
|
_sockfd = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int Socket::send(char* buffer, size_t length)
|
ssize_t Socket::send(char* buffer, size_t length)
|
||||||
{
|
{
|
||||||
int flags = 0;
|
int flags = 0;
|
||||||
#ifdef MSG_NOSIGNAL
|
#ifdef MSG_NOSIGNAL
|
||||||
flags = MSG_NOSIGNAL;
|
flags = MSG_NOSIGNAL;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return (int) ::send(_sockfd, buffer, length, flags);
|
return ::send(_sockfd, buffer, length, flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
int Socket::send(const std::string& buffer)
|
ssize_t Socket::send(const std::string& buffer)
|
||||||
{
|
{
|
||||||
return send((char*)&buffer[0], buffer.size());
|
return send((char*)&buffer[0], buffer.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
int Socket::recv(void* buffer, size_t length)
|
ssize_t Socket::recv(void* buffer, size_t length)
|
||||||
{
|
{
|
||||||
int flags = 0;
|
int flags = 0;
|
||||||
#ifdef MSG_NOSIGNAL
|
#ifdef MSG_NOSIGNAL
|
||||||
flags = MSG_NOSIGNAL;
|
flags = MSG_NOSIGNAL;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return (int) ::recv(_sockfd, buffer, length, flags);
|
return ::recv(_sockfd, (char*) buffer, length, flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int Socket::getErrno()
|
||||||
|
{
|
||||||
|
int err;
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
err = WSAGetLastError();
|
||||||
|
#else
|
||||||
|
err = errno;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Socket::isWaitNeeded()
|
||||||
|
{
|
||||||
|
int err = getErrno();
|
||||||
|
|
||||||
|
if (err == EWOULDBLOCK || err == EAGAIN || err == EINPROGRESS)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Socket::closeSocket(int fd)
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
closesocket(fd);
|
||||||
|
#else
|
||||||
|
::close(fd);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Socket::init(std::string& errorMsg)
|
||||||
|
{
|
||||||
|
return _selectInterrupt->init(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Socket::writeBytes(const std::string& str,
|
||||||
|
const CancellationRequest& isCancellationRequested)
|
||||||
|
{
|
||||||
|
int offset = 0;
|
||||||
|
int len = (int) str.size();
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (isCancellationRequested && isCancellationRequested()) return false;
|
||||||
|
|
||||||
|
ssize_t ret = send((char*)&str[offset], len);
|
||||||
|
|
||||||
|
// We wrote some bytes, as needed, all good.
|
||||||
|
if (ret > 0)
|
||||||
|
{
|
||||||
|
if (ret == len)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
offset += ret;
|
||||||
|
len -= ret;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// There is possibly something to be writen, try again
|
||||||
|
else if (ret < 0 && Socket::isWaitNeeded())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// There was an error during the write, abort
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Socket::readByte(void* buffer,
|
||||||
|
const CancellationRequest& isCancellationRequested)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (isCancellationRequested && isCancellationRequested()) return false;
|
||||||
|
|
||||||
|
ssize_t ret;
|
||||||
|
ret = recv(buffer, 1);
|
||||||
|
|
||||||
|
// We read one byte, as needed, all good.
|
||||||
|
if (ret == 1)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// There is possibly something to be read, try again
|
||||||
|
else if (ret < 0 && Socket::isWaitNeeded())
|
||||||
|
{
|
||||||
|
// Wait with a 1ms timeout until the socket is ready to read.
|
||||||
|
// This way we are not busy looping
|
||||||
|
if (isReadyToRead(1) == PollResultType::Error)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// There was an error during the read, abort
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<bool, std::string> Socket::readLine(
|
||||||
|
const CancellationRequest& isCancellationRequested)
|
||||||
|
{
|
||||||
|
char c;
|
||||||
|
std::string line;
|
||||||
|
line.reserve(64);
|
||||||
|
|
||||||
|
for (int i = 0; i < 2 || (line[i-2] != '\r' && line[i-1] != '\n'); ++i)
|
||||||
|
{
|
||||||
|
if (!readByte(&c, isCancellationRequested))
|
||||||
|
{
|
||||||
|
// Return what we were able to read
|
||||||
|
return std::make_pair(false, line);
|
||||||
|
}
|
||||||
|
|
||||||
|
line += c;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::make_pair(true, line);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<bool, std::string> Socket::readBytes(
|
||||||
|
size_t length,
|
||||||
|
const OnProgressCallback& onProgressCallback,
|
||||||
|
const CancellationRequest& isCancellationRequested)
|
||||||
|
{
|
||||||
|
if (_readBuffer.empty())
|
||||||
|
{
|
||||||
|
_readBuffer.resize(kChunkSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> output;
|
||||||
|
while (output.size() != length)
|
||||||
|
{
|
||||||
|
if (isCancellationRequested && isCancellationRequested())
|
||||||
|
{
|
||||||
|
return std::make_pair(false, std::string());
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t size = std::min(kChunkSize, length - output.size());
|
||||||
|
ssize_t ret = recv((char*)&_readBuffer[0], size);
|
||||||
|
|
||||||
|
if (ret <= 0 && !Socket::isWaitNeeded())
|
||||||
|
{
|
||||||
|
// Error
|
||||||
|
return std::make_pair(false, std::string());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
output.insert(output.end(),
|
||||||
|
_readBuffer.begin(),
|
||||||
|
_readBuffer.begin() + ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onProgressCallback) onProgressCallback((int) output.size(), (int) length);
|
||||||
|
|
||||||
|
// Wait with a 1ms timeout until the socket is ready to read.
|
||||||
|
// This way we are not busy looping
|
||||||
|
if (isReadyToRead(1) == PollResultType::Error)
|
||||||
|
{
|
||||||
|
return std::make_pair(false, std::string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::make_pair(true, std::string(output.begin(),
|
||||||
|
output.end()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,45 +6,110 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <functional>
|
|
||||||
#include <mutex>
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <BaseTsd.h>
|
||||||
|
typedef SSIZE_T ssize_t;
|
||||||
|
|
||||||
|
#undef EWOULDBLOCK
|
||||||
|
#undef EAGAIN
|
||||||
|
#undef EINPROGRESS
|
||||||
|
#undef EBADF
|
||||||
|
#undef EINVAL
|
||||||
|
|
||||||
|
// map to WSA error codes
|
||||||
|
#define EWOULDBLOCK WSAEWOULDBLOCK
|
||||||
|
#define EAGAIN WSATRY_AGAIN
|
||||||
|
#define EINPROGRESS WSAEINPROGRESS
|
||||||
|
#define EBADF WSAEBADF
|
||||||
|
#define EINVAL WSAEINVAL
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "IXCancellationRequest.h"
|
||||||
|
#include "IXProgressCallback.h"
|
||||||
|
|
||||||
namespace ix
|
namespace ix
|
||||||
{
|
{
|
||||||
class Socket {
|
class SelectInterrupt;
|
||||||
|
|
||||||
|
enum class PollResultType
|
||||||
|
{
|
||||||
|
ReadyForRead = 0,
|
||||||
|
ReadyForWrite = 1,
|
||||||
|
Timeout = 2,
|
||||||
|
Error = 3,
|
||||||
|
SendRequest = 4,
|
||||||
|
CloseRequest = 5
|
||||||
|
};
|
||||||
|
|
||||||
|
class Socket
|
||||||
|
{
|
||||||
public:
|
public:
|
||||||
using OnPollCallback = std::function<void()>;
|
Socket(int fd = -1);
|
||||||
|
|
||||||
Socket();
|
|
||||||
virtual ~Socket();
|
virtual ~Socket();
|
||||||
|
bool init(std::string& errorMsg);
|
||||||
|
|
||||||
static int hostname_connect(const std::string& hostname,
|
// Functions to check whether there is activity on the socket
|
||||||
int port,
|
PollResultType poll(int timeoutMs = kDefaultPollTimeout);
|
||||||
std::string& errMsg);
|
bool wakeUpFromPoll(uint64_t wakeUpCode);
|
||||||
void configure();
|
|
||||||
|
|
||||||
virtual void poll(const OnPollCallback& onPollCallback);
|
PollResultType isReadyToWrite(int timeoutMs);
|
||||||
virtual void wakeUpFromPoll();
|
PollResultType isReadyToRead(int timeoutMs);
|
||||||
|
|
||||||
// Virtual methods
|
// Virtual methods
|
||||||
virtual bool connect(const std::string& url,
|
virtual bool connect(const std::string& url,
|
||||||
int port,
|
int port,
|
||||||
std::string& errMsg);
|
std::string& errMsg,
|
||||||
|
const CancellationRequest& isCancellationRequested);
|
||||||
virtual void close();
|
virtual void close();
|
||||||
|
|
||||||
virtual int send(char* buffer, size_t length);
|
virtual ssize_t send(char* buffer, size_t length);
|
||||||
virtual int send(const std::string& buffer);
|
virtual ssize_t send(const std::string& buffer);
|
||||||
virtual int recv(void* buffer, size_t length);
|
virtual ssize_t recv(void* buffer, size_t length);
|
||||||
|
|
||||||
|
// Blocking and cancellable versions, working with socket that can be set
|
||||||
|
// to non blocking mode. Used during HTTP upgrade.
|
||||||
|
bool readByte(void* buffer, const CancellationRequest& isCancellationRequested);
|
||||||
|
bool writeBytes(const std::string& str, const CancellationRequest& isCancellationRequested);
|
||||||
|
|
||||||
|
std::pair<bool, std::string> readLine(const CancellationRequest& isCancellationRequested);
|
||||||
|
std::pair<bool, std::string> readBytes(size_t length,
|
||||||
|
const OnProgressCallback& onProgressCallback,
|
||||||
|
const CancellationRequest& isCancellationRequested);
|
||||||
|
|
||||||
|
static int getErrno();
|
||||||
|
static bool isWaitNeeded();
|
||||||
|
static void closeSocket(int fd);
|
||||||
|
|
||||||
|
static PollResultType poll(bool readyToRead,
|
||||||
|
int timeoutMs,
|
||||||
|
int sockfd,
|
||||||
|
std::shared_ptr<SelectInterrupt> selectInterrupt = nullptr);
|
||||||
|
|
||||||
|
|
||||||
|
// Used as special codes for pipe communication
|
||||||
|
static const uint64_t kSendRequest;
|
||||||
|
static const uint64_t kCloseRequest;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void wakeUpFromPollApple();
|
|
||||||
void wakeUpFromPollLinux();
|
|
||||||
|
|
||||||
std::atomic<int> _sockfd;
|
std::atomic<int> _sockfd;
|
||||||
int _eventfd;
|
|
||||||
std::mutex _socketMutex;
|
std::mutex _socketMutex;
|
||||||
};
|
|
||||||
|
|
||||||
}
|
private:
|
||||||
|
static const int kDefaultPollTimeout;
|
||||||
|
static const int kDefaultPollNoTimeout;
|
||||||
|
|
||||||
|
// Buffer for reading from our socket. That buffer is never resized.
|
||||||
|
std::vector<uint8_t> _readBuffer;
|
||||||
|
static constexpr size_t kChunkSize = 1 << 15;
|
||||||
|
|
||||||
|
std::shared_ptr<SelectInterrupt> _selectInterrupt;
|
||||||
|
};
|
||||||
|
} // namespace ix
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
* Adapted from Satori SDK Apple SSL code.
|
* Adapted from Satori SDK Apple SSL code.
|
||||||
*/
|
*/
|
||||||
#include "IXSocketAppleSSL.h"
|
#include "IXSocketAppleSSL.h"
|
||||||
|
#include "IXSocketConnect.h"
|
||||||
|
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <netdb.h>
|
#include <netdb.h>
|
||||||
@ -19,8 +20,6 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#define socketerrno errno
|
#define socketerrno errno
|
||||||
|
|
||||||
@ -142,7 +141,7 @@ std::string getSSLErrorDescription(OSStatus status)
|
|||||||
|
|
||||||
namespace ix
|
namespace ix
|
||||||
{
|
{
|
||||||
SocketAppleSSL::SocketAppleSSL() :
|
SocketAppleSSL::SocketAppleSSL(int fd) : Socket(fd),
|
||||||
_sslContext(nullptr)
|
_sslContext(nullptr)
|
||||||
{
|
{
|
||||||
;
|
;
|
||||||
@ -156,13 +155,14 @@ namespace ix
|
|||||||
// No wait support
|
// No wait support
|
||||||
bool SocketAppleSSL::connect(const std::string& host,
|
bool SocketAppleSSL::connect(const std::string& host,
|
||||||
int port,
|
int port,
|
||||||
std::string& errMsg)
|
std::string& errMsg,
|
||||||
|
const CancellationRequest& isCancellationRequested)
|
||||||
{
|
{
|
||||||
OSStatus status;
|
OSStatus status;
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(_mutex);
|
std::lock_guard<std::mutex> lock(_mutex);
|
||||||
|
|
||||||
_sockfd = Socket::hostname_connect(host, port, errMsg);
|
_sockfd = SocketConnect::connect(host, port, errMsg, isCancellationRequested);
|
||||||
if (_sockfd == -1) return false;
|
if (_sockfd == -1) return false;
|
||||||
|
|
||||||
_sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType);
|
_sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType);
|
||||||
@ -201,7 +201,7 @@ namespace ix
|
|||||||
Socket::close();
|
Socket::close();
|
||||||
}
|
}
|
||||||
|
|
||||||
int SocketAppleSSL::send(char* buf, size_t nbyte)
|
ssize_t SocketAppleSSL::send(char* buf, size_t nbyte)
|
||||||
{
|
{
|
||||||
ssize_t ret = 0;
|
ssize_t ret = 0;
|
||||||
OSStatus status;
|
OSStatus status;
|
||||||
@ -216,16 +216,16 @@ namespace ix
|
|||||||
|
|
||||||
if (ret == 0 && errSSLClosedAbort != status)
|
if (ret == 0 && errSSLClosedAbort != status)
|
||||||
ret = -1;
|
ret = -1;
|
||||||
return (int) ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
int SocketAppleSSL::send(const std::string& buffer)
|
ssize_t SocketAppleSSL::send(const std::string& buffer)
|
||||||
{
|
{
|
||||||
return send((char*)&buffer[0], buffer.size());
|
return send((char*)&buffer[0], buffer.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
// No wait support
|
// No wait support
|
||||||
int SocketAppleSSL::recv(void* buf, size_t nbyte)
|
ssize_t SocketAppleSSL::recv(void* buf, size_t nbyte)
|
||||||
{
|
{
|
||||||
OSStatus status = errSSLWouldBlock;
|
OSStatus status = errSSLWouldBlock;
|
||||||
while (errSSLWouldBlock == status)
|
while (errSSLWouldBlock == status)
|
||||||
@ -235,7 +235,7 @@ namespace ix
|
|||||||
status = SSLRead(_sslContext, buf, nbyte, &processed);
|
status = SSLRead(_sslContext, buf, nbyte, &processed);
|
||||||
|
|
||||||
if (processed > 0)
|
if (processed > 0)
|
||||||
return (int) processed;
|
return (ssize_t) processed;
|
||||||
|
|
||||||
// The connection was reset, inform the caller that this
|
// The connection was reset, inform the caller that this
|
||||||
// Socket should close
|
// Socket should close
|
||||||
|
@ -6,33 +6,33 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "IXCancellationRequest.h"
|
||||||
#include "IXSocket.h"
|
#include "IXSocket.h"
|
||||||
|
|
||||||
#include <Security/Security.h>
|
|
||||||
#include <Security/SecureTransport.h>
|
#include <Security/SecureTransport.h>
|
||||||
|
#include <Security/Security.h>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
|
||||||
namespace ix
|
namespace ix
|
||||||
{
|
{
|
||||||
class SocketAppleSSL : public Socket
|
class SocketAppleSSL final : public Socket
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
SocketAppleSSL();
|
SocketAppleSSL(int fd = -1);
|
||||||
~SocketAppleSSL();
|
~SocketAppleSSL();
|
||||||
|
|
||||||
virtual bool connect(const std::string& host,
|
virtual bool connect(const std::string& host,
|
||||||
int port,
|
int port,
|
||||||
std::string& errMsg) final;
|
std::string& errMsg,
|
||||||
|
const CancellationRequest& isCancellationRequested) final;
|
||||||
virtual void close() final;
|
virtual void close() final;
|
||||||
|
|
||||||
virtual int send(char* buffer, size_t length) final;
|
virtual ssize_t send(char* buffer, size_t length) final;
|
||||||
virtual int send(const std::string& buffer) final;
|
virtual ssize_t send(const std::string& buffer) final;
|
||||||
virtual int recv(void* buffer, size_t length) final;
|
virtual ssize_t recv(void* buffer, size_t length) final;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SSLContextRef _sslContext;
|
SSLContextRef _sslContext;
|
||||||
mutable std::mutex _mutex; // AppleSSL routines are not thread-safe
|
mutable std::mutex _mutex; // AppleSSL routines are not thread-safe
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
} // namespace ix
|
||||||
|
156
ixwebsocket/IXSocketConnect.cpp
Normal file
156
ixwebsocket/IXSocketConnect.cpp
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
/*
|
||||||
|
* IXSocketConnect.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXSocketConnect.h"
|
||||||
|
#include "IXDNSLookup.h"
|
||||||
|
#include "IXNetSystem.h"
|
||||||
|
#include "IXSocket.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
// Android needs extra headers for TCP_NODELAY and IPPROTO_TCP
|
||||||
|
#ifdef ANDROID
|
||||||
|
# include <linux/in.h>
|
||||||
|
# include <linux/tcp.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// This function can be cancelled every 50 ms
|
||||||
|
// This is important so that we don't block the main UI thread when shutting down a connection which is
|
||||||
|
// already trying to reconnect, and can be blocked waiting for ::connect to respond.
|
||||||
|
//
|
||||||
|
int SocketConnect::connectToAddress(const struct addrinfo *address,
|
||||||
|
std::string& errMsg,
|
||||||
|
const CancellationRequest& isCancellationRequested)
|
||||||
|
{
|
||||||
|
errMsg = "no error";
|
||||||
|
|
||||||
|
int fd = socket(address->ai_family,
|
||||||
|
address->ai_socktype,
|
||||||
|
address->ai_protocol);
|
||||||
|
if (fd < 0)
|
||||||
|
{
|
||||||
|
errMsg = "Cannot create a socket";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the socket to non blocking mode, so that slow responses cannot
|
||||||
|
// block us for too long
|
||||||
|
SocketConnect::configure(fd);
|
||||||
|
|
||||||
|
int res = ::connect(fd, address->ai_addr, address->ai_addrlen);
|
||||||
|
|
||||||
|
if (res == -1 && !Socket::isWaitNeeded())
|
||||||
|
{
|
||||||
|
errMsg = strerror(Socket::getErrno());
|
||||||
|
Socket::closeSocket(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
if (isCancellationRequested && isCancellationRequested()) // Must handle timeout as well
|
||||||
|
{
|
||||||
|
Socket::closeSocket(fd);
|
||||||
|
errMsg = "Cancelled";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int timeoutMs = 10;
|
||||||
|
bool readyToRead = false;
|
||||||
|
PollResultType pollResult = Socket::poll(readyToRead, timeoutMs, fd);
|
||||||
|
|
||||||
|
if (pollResult == PollResultType::Timeout)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else if (pollResult == PollResultType::Error)
|
||||||
|
{
|
||||||
|
Socket::closeSocket(fd);
|
||||||
|
errMsg = std::string("Connect error: ") +
|
||||||
|
strerror(Socket::getErrno());
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
else if (pollResult == PollResultType::ReadyForWrite)
|
||||||
|
{
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Socket::closeSocket(fd);
|
||||||
|
errMsg = std::string("Connect error: ") +
|
||||||
|
strerror(Socket::getErrno());
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Socket::closeSocket(fd);
|
||||||
|
errMsg = "connect timed out after 60 seconds";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int SocketConnect::connect(const std::string& hostname,
|
||||||
|
int port,
|
||||||
|
std::string& errMsg,
|
||||||
|
const CancellationRequest& isCancellationRequested)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// First do DNS resolution
|
||||||
|
//
|
||||||
|
auto dnsLookup = std::make_shared<DNSLookup>(hostname, port);
|
||||||
|
struct addrinfo *res = dnsLookup->resolve(errMsg, isCancellationRequested);
|
||||||
|
if (res == nullptr)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sockfd = -1;
|
||||||
|
|
||||||
|
// iterate through the records to find a working peer
|
||||||
|
struct addrinfo *address;
|
||||||
|
for (address = res; address != nullptr; address = address->ai_next)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Second try to connect to the remote host
|
||||||
|
//
|
||||||
|
sockfd = connectToAddress(address, errMsg, isCancellationRequested);
|
||||||
|
if (sockfd != -1)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
freeaddrinfo(res);
|
||||||
|
return sockfd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: configure is a terrible name
|
||||||
|
void SocketConnect::configure(int sockfd)
|
||||||
|
{
|
||||||
|
// 1. disable Nagle's algorithm
|
||||||
|
int flag = 1;
|
||||||
|
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, (char*) &flag, sizeof(flag));
|
||||||
|
|
||||||
|
// 2. make socket non blocking
|
||||||
|
#ifdef _WIN32
|
||||||
|
unsigned long nonblocking = 1;
|
||||||
|
ioctlsocket(sockfd, FIONBIO, &nonblocking);
|
||||||
|
#else
|
||||||
|
fcntl(sockfd, F_SETFL, O_NONBLOCK); // make socket non blocking
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// 3. (apple) prevent SIGPIPE from being emitted when the remote end disconnect
|
||||||
|
#ifdef SO_NOSIGPIPE
|
||||||
|
int value = 1;
|
||||||
|
setsockopt(sockfd, SOL_SOCKET, SO_NOSIGPIPE,
|
||||||
|
(void *)&value, sizeof(value));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
31
ixwebsocket/IXSocketConnect.h
Normal file
31
ixwebsocket/IXSocketConnect.h
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* IXSocketConnect.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IXCancellationRequest.h"
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
struct addrinfo;
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
class SocketConnect
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static int connect(const std::string& hostname,
|
||||||
|
int port,
|
||||||
|
std::string& errMsg,
|
||||||
|
const CancellationRequest& isCancellationRequested);
|
||||||
|
|
||||||
|
static void configure(int sockfd);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static int connectToAddress(const struct addrinfo* address,
|
||||||
|
std::string& errMsg,
|
||||||
|
const CancellationRequest& isCancellationRequested);
|
||||||
|
};
|
||||||
|
} // namespace ix
|
78
ixwebsocket/IXSocketFactory.cpp
Normal file
78
ixwebsocket/IXSocketFactory.cpp
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* IXSocketFactory.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXSocketFactory.h"
|
||||||
|
|
||||||
|
#ifdef IXWEBSOCKET_USE_TLS
|
||||||
|
|
||||||
|
# ifdef IXWEBSOCKET_USE_MBED_TLS
|
||||||
|
# include <ixwebsocket/IXSocketMbedTLS.h>
|
||||||
|
# elif __APPLE__
|
||||||
|
# include <ixwebsocket/IXSocketAppleSSL.h>
|
||||||
|
# elif defined(_WIN32)
|
||||||
|
# include <ixwebsocket/IXSocketSChannel.h>
|
||||||
|
# elif defined(IXWEBSOCKET_USE_OPEN_SSL)
|
||||||
|
# include <ixwebsocket/IXSocketOpenSSL.h>
|
||||||
|
# endif
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
#include <ixwebsocket/IXSocket.h>
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
std::shared_ptr<Socket> createSocket(bool tls,
|
||||||
|
std::string& errorMsg)
|
||||||
|
{
|
||||||
|
errorMsg.clear();
|
||||||
|
std::shared_ptr<Socket> socket;
|
||||||
|
|
||||||
|
if (!tls)
|
||||||
|
{
|
||||||
|
socket = std::make_shared<Socket>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
#ifdef IXWEBSOCKET_USE_TLS
|
||||||
|
# if defined(IXWEBSOCKET_USE_MBED_TLS)
|
||||||
|
socket = std::make_shared<SocketMbedTLS>();
|
||||||
|
# elif defined(__APPLE__)
|
||||||
|
socket = std::make_shared<SocketAppleSSL>();
|
||||||
|
# elif defined(_WIN32)
|
||||||
|
socket = std::make_shared<SocketSChannel>();
|
||||||
|
# else
|
||||||
|
socket = std::make_shared<SocketOpenSSL>();
|
||||||
|
# endif
|
||||||
|
#else
|
||||||
|
errorMsg = "TLS support is not enabled on this platform.";
|
||||||
|
return nullptr;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!socket->init(errorMsg))
|
||||||
|
{
|
||||||
|
socket.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<Socket> createSocket(int fd,
|
||||||
|
std::string& errorMsg)
|
||||||
|
{
|
||||||
|
errorMsg.clear();
|
||||||
|
|
||||||
|
std::shared_ptr<Socket> socket = std::make_shared<Socket>(fd);
|
||||||
|
if (!socket->init(errorMsg))
|
||||||
|
{
|
||||||
|
socket.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
}
|
19
ixwebsocket/IXSocketFactory.h
Normal file
19
ixwebsocket/IXSocketFactory.h
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
|
||||||
|
/*
|
||||||
|
* IXSocketFactory.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
class Socket;
|
||||||
|
std::shared_ptr<Socket> createSocket(bool tls, std::string& errorMsg);
|
||||||
|
|
||||||
|
std::shared_ptr<Socket> createSocket(int fd, std::string& errorMsg);
|
||||||
|
} // namespace ix
|
178
ixwebsocket/IXSocketMbedTLS.cpp
Normal file
178
ixwebsocket/IXSocketMbedTLS.cpp
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
/*
|
||||||
|
* IXSocketMbedTLS.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* Some code taken from
|
||||||
|
* https://github.com/rottor12/WsClientLib/blob/master/lib/src/WsClientLib.cpp
|
||||||
|
* and mini_client.c example from mbedtls
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXSocketMbedTLS.h"
|
||||||
|
#include "IXSocketConnect.h"
|
||||||
|
#include "IXNetSystem.h"
|
||||||
|
#include "IXSocket.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
SocketMbedTLS::~SocketMbedTLS()
|
||||||
|
{
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SocketMbedTLS::init(const std::string& host, std::string& errMsg)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_mutex);
|
||||||
|
|
||||||
|
mbedtls_ssl_init(&_ssl);
|
||||||
|
mbedtls_ssl_config_init(&_conf);
|
||||||
|
mbedtls_ctr_drbg_init(&_ctr_drbg);
|
||||||
|
|
||||||
|
const char *pers = "IXSocketMbedTLS";
|
||||||
|
|
||||||
|
mbedtls_entropy_init(&_entropy);
|
||||||
|
if (mbedtls_ctr_drbg_seed(&_ctr_drbg,
|
||||||
|
mbedtls_entropy_func,
|
||||||
|
&_entropy,
|
||||||
|
(const unsigned char *) pers,
|
||||||
|
strlen(pers)) != 0)
|
||||||
|
{
|
||||||
|
errMsg = "Setting entropy seed failed";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mbedtls_ssl_config_defaults(&_conf,
|
||||||
|
MBEDTLS_SSL_IS_CLIENT,
|
||||||
|
MBEDTLS_SSL_TRANSPORT_STREAM,
|
||||||
|
MBEDTLS_SSL_PRESET_DEFAULT ) != 0)
|
||||||
|
{
|
||||||
|
errMsg = "Setting config default failed";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mbedtls_ssl_conf_rng(&_conf, mbedtls_ctr_drbg_random, &_ctr_drbg);
|
||||||
|
|
||||||
|
// FIXME: cert verification is disabled
|
||||||
|
mbedtls_ssl_conf_authmode(&_conf, MBEDTLS_SSL_VERIFY_NONE);
|
||||||
|
|
||||||
|
if (mbedtls_ssl_setup(&_ssl, &_conf) != 0)
|
||||||
|
{
|
||||||
|
errMsg = "SSL setup failed";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mbedtls_ssl_set_hostname(&_ssl, host.c_str()) != 0)
|
||||||
|
{
|
||||||
|
errMsg = "SNI setup failed";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SocketMbedTLS::connect(const std::string& host,
|
||||||
|
int port,
|
||||||
|
std::string& errMsg,
|
||||||
|
const CancellationRequest& isCancellationRequested)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_mutex);
|
||||||
|
_sockfd = SocketConnect::connect(host, port, errMsg, isCancellationRequested);
|
||||||
|
if (_sockfd == -1) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!init(host, errMsg))
|
||||||
|
{
|
||||||
|
close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mbedtls_ssl_set_bio(&_ssl, &_sockfd, mbedtls_net_send, mbedtls_net_recv, NULL);
|
||||||
|
|
||||||
|
int res;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_mutex);
|
||||||
|
res = mbedtls_ssl_handshake(&_ssl);
|
||||||
|
}
|
||||||
|
while (res == MBEDTLS_ERR_SSL_WANT_READ || res == MBEDTLS_ERR_SSL_WANT_WRITE);
|
||||||
|
|
||||||
|
if (res != 0)
|
||||||
|
{
|
||||||
|
char buf[256];
|
||||||
|
mbedtls_strerror(res, buf, sizeof(buf));
|
||||||
|
|
||||||
|
errMsg = "error in handshake : ";
|
||||||
|
errMsg += buf;
|
||||||
|
|
||||||
|
close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SocketMbedTLS::close()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_mutex);
|
||||||
|
|
||||||
|
mbedtls_ssl_free(&_ssl);
|
||||||
|
mbedtls_ssl_config_free(&_conf);
|
||||||
|
mbedtls_ctr_drbg_free(&_ctr_drbg);
|
||||||
|
mbedtls_entropy_free(&_entropy);
|
||||||
|
|
||||||
|
Socket::close();
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t SocketMbedTLS::send(char* buf, size_t nbyte)
|
||||||
|
{
|
||||||
|
ssize_t sent = 0;
|
||||||
|
|
||||||
|
while (nbyte > 0)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_mutex);
|
||||||
|
|
||||||
|
ssize_t res = mbedtls_ssl_write(&_ssl, (unsigned char*) buf, nbyte);
|
||||||
|
|
||||||
|
if (res > 0) {
|
||||||
|
nbyte -= res;
|
||||||
|
sent += res;
|
||||||
|
} else if (res == MBEDTLS_ERR_SSL_WANT_READ || res == MBEDTLS_ERR_SSL_WANT_WRITE) {
|
||||||
|
errno = EWOULDBLOCK;
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t SocketMbedTLS::send(const std::string& buffer)
|
||||||
|
{
|
||||||
|
return send((char*)&buffer[0], buffer.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t SocketMbedTLS::recv(void* buf, size_t nbyte)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_mutex);
|
||||||
|
|
||||||
|
ssize_t res = mbedtls_ssl_read(&_ssl, (unsigned char*) buf, (int) nbyte);
|
||||||
|
|
||||||
|
if (res > 0)
|
||||||
|
{
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res == MBEDTLS_ERR_SSL_WANT_READ || res == MBEDTLS_ERR_SSL_WANT_WRITE)
|
||||||
|
{
|
||||||
|
errno = EWOULDBLOCK;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
47
ixwebsocket/IXSocketMbedTLS.h
Normal file
47
ixwebsocket/IXSocketMbedTLS.h
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* IXSocketMbedTLS.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IXSocket.h"
|
||||||
|
#include <mbedtls/ctr_drbg.h>
|
||||||
|
#include <mbedtls/debug.h>
|
||||||
|
#include <mbedtls/entropy.h>
|
||||||
|
#include <mbedtls/error.h>
|
||||||
|
#include <mbedtls/net.h>
|
||||||
|
#include <mbedtls/platform.h>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
class SocketMbedTLS final : public Socket
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SocketMbedTLS() = default;
|
||||||
|
~SocketMbedTLS();
|
||||||
|
|
||||||
|
virtual bool connect(const std::string& host,
|
||||||
|
int port,
|
||||||
|
std::string& errMsg,
|
||||||
|
const CancellationRequest& isCancellationRequested) final;
|
||||||
|
virtual void close() final;
|
||||||
|
|
||||||
|
virtual ssize_t send(char* buffer, size_t length) final;
|
||||||
|
virtual ssize_t send(const std::string& buffer) final;
|
||||||
|
virtual ssize_t recv(void* buffer, size_t length) final;
|
||||||
|
|
||||||
|
private:
|
||||||
|
mbedtls_ssl_context _ssl;
|
||||||
|
mbedtls_ssl_config _conf;
|
||||||
|
mbedtls_entropy_context _entropy;
|
||||||
|
mbedtls_ctr_drbg_context _ctr_drbg;
|
||||||
|
|
||||||
|
std::mutex _mutex;
|
||||||
|
|
||||||
|
bool init(const std::string& host, std::string& errMsg);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ix
|
@ -7,38 +7,37 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "IXSocketOpenSSL.h"
|
#include "IXSocketOpenSSL.h"
|
||||||
|
#include "IXSocketConnect.h"
|
||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
#include <openssl/x509v3.h>
|
#include <openssl/x509v3.h>
|
||||||
|
|
||||||
|
#include <fnmatch.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#define socketerrno errno
|
#define socketerrno errno
|
||||||
|
|
||||||
namespace {
|
namespace ix
|
||||||
|
|
||||||
std::mutex initMutex;
|
|
||||||
bool openSSLInitialized = false;
|
|
||||||
bool openSSLInitializationSuccessful = false;
|
|
||||||
|
|
||||||
bool openSSLInitialize(std::string& errMsg)
|
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(initMutex);
|
std::atomic<bool> SocketOpenSSL::_openSSLInitializationSuccessful(false);
|
||||||
|
std::once_flag SocketOpenSSL::_openSSLInitFlag;
|
||||||
|
|
||||||
if (openSSLInitialized)
|
SocketOpenSSL::SocketOpenSSL(int fd) : Socket(fd),
|
||||||
|
_ssl_connection(nullptr),
|
||||||
|
_ssl_context(nullptr)
|
||||||
{
|
{
|
||||||
return openSSLInitializationSuccessful;
|
std::call_once(_openSSLInitFlag, &SocketOpenSSL::openSSLInitialize, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SocketOpenSSL::~SocketOpenSSL()
|
||||||
|
{
|
||||||
|
SocketOpenSSL::close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SocketOpenSSL::openSSLInitialize()
|
||||||
|
{
|
||||||
#if OPENSSL_VERSION_NUMBER >= 0x10100000L
|
#if OPENSSL_VERSION_NUMBER >= 0x10100000L
|
||||||
if (!OPENSSL_init_ssl(OPENSSL_INIT_LOAD_CONFIG, nullptr))
|
if (!OPENSSL_init_ssl(OPENSSL_INIT_LOAD_CONFIG, nullptr)) return;
|
||||||
{
|
|
||||||
errMsg = "OPENSSL_init_ssl failure";
|
|
||||||
|
|
||||||
openSSLInitializationSuccessful = false;
|
|
||||||
openSSLInitialized = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
#else
|
#else
|
||||||
(void) OPENSSL_config(nullptr);
|
(void) OPENSSL_config(nullptr);
|
||||||
#endif
|
#endif
|
||||||
@ -46,41 +45,7 @@ bool openSSLInitialize(std::string& errMsg)
|
|||||||
(void) OpenSSL_add_ssl_algorithms();
|
(void) OpenSSL_add_ssl_algorithms();
|
||||||
(void) SSL_load_error_strings();
|
(void) SSL_load_error_strings();
|
||||||
|
|
||||||
openSSLInitializationSuccessful = true;
|
_openSSLInitializationSuccessful = true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
int openssl_verify_callback(int preverify, X509_STORE_CTX *x509_ctx)
|
|
||||||
{
|
|
||||||
return preverify;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* create new SSL connection state object */
|
|
||||||
SSL *openssl_create_connection(SSL_CTX *ctx, int socket)
|
|
||||||
{
|
|
||||||
assert(ctx != nullptr);
|
|
||||||
assert(socket > 0);
|
|
||||||
|
|
||||||
SSL *ssl = SSL_new(ctx);
|
|
||||||
if (ssl)
|
|
||||||
SSL_set_fd(ssl, socket);
|
|
||||||
return ssl;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // anonymous namespace
|
|
||||||
|
|
||||||
namespace ix
|
|
||||||
{
|
|
||||||
SocketOpenSSL::SocketOpenSSL() :
|
|
||||||
_ssl_connection(nullptr),
|
|
||||||
_ssl_context(nullptr)
|
|
||||||
{
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
||||||
SocketOpenSSL::~SocketOpenSSL()
|
|
||||||
{
|
|
||||||
SocketOpenSSL::close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string SocketOpenSSL::getSSLError(int ret)
|
std::string SocketOpenSSL::getSSLError(int ret)
|
||||||
@ -149,7 +114,13 @@ namespace ix
|
|||||||
SSL_CTX* ctx = SSL_CTX_new(_ssl_method);
|
SSL_CTX* ctx = SSL_CTX_new(_ssl_method);
|
||||||
if (ctx)
|
if (ctx)
|
||||||
{
|
{
|
||||||
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, openssl_verify_callback);
|
// To skip verification, pass in SSL_VERIFY_NONE
|
||||||
|
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER,
|
||||||
|
[](int preverify, X509_STORE_CTX*) -> int
|
||||||
|
{
|
||||||
|
return preverify;
|
||||||
|
});
|
||||||
|
|
||||||
SSL_CTX_set_verify_depth(ctx, 4);
|
SSL_CTX_set_verify_depth(ctx, 4);
|
||||||
SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
|
SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
|
||||||
}
|
}
|
||||||
@ -158,51 +129,10 @@ namespace ix
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether a hostname matches a pattern
|
* Check whether a hostname matches a pattern
|
||||||
*
|
|
||||||
* The pattern MUST contain at most a single, leading asterisk. This means that
|
|
||||||
* this function cannot serve as a generic validation function, as that would
|
|
||||||
* allow for partial wildcards, too. Also, this does not check whether the
|
|
||||||
* wildcard covers multiple levels of labels. For RTM, this suffices, as we
|
|
||||||
* are only interested in the main domain name.
|
|
||||||
*
|
|
||||||
* @param[in] hostname The hostname of the server
|
|
||||||
* @param[in] pattern The hostname pattern from a SSL certificate
|
|
||||||
* @return TRUE if the pattern matches, FALSE otherwise
|
|
||||||
*/
|
*/
|
||||||
bool SocketOpenSSL::checkHost(const std::string& host, const char *pattern)
|
bool SocketOpenSSL::checkHost(const std::string& host, const char *pattern)
|
||||||
{
|
{
|
||||||
const char* hostname = host.c_str();
|
return fnmatch(pattern, host.c_str(), 0) != FNM_NOMATCH;
|
||||||
|
|
||||||
while (*pattern && *hostname)
|
|
||||||
{
|
|
||||||
if (*pattern == '*')
|
|
||||||
{
|
|
||||||
while (*hostname != '.' && *hostname) hostname++;
|
|
||||||
if (*(++pattern) != '.')
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
char p = *pattern;
|
|
||||||
char h = *hostname;
|
|
||||||
if ((p & ~32) >= 'A' && (p & ~32) <= 'Z')
|
|
||||||
{
|
|
||||||
p &= ~32;
|
|
||||||
h &= ~32;
|
|
||||||
}
|
|
||||||
if (*pattern != *hostname)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pattern++;
|
|
||||||
hostname++;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool success = !(*hostname || *pattern);
|
|
||||||
return success;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SocketOpenSSL::openSSLCheckServerCert(SSL *ssl,
|
bool SocketOpenSSL::openSSLCheckServerCert(SSL *ssl,
|
||||||
@ -310,21 +240,22 @@ namespace ix
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No wait support
|
|
||||||
bool SocketOpenSSL::connect(const std::string& host,
|
bool SocketOpenSSL::connect(const std::string& host,
|
||||||
int port,
|
int port,
|
||||||
std::string& errMsg)
|
std::string& errMsg,
|
||||||
|
const CancellationRequest& isCancellationRequested)
|
||||||
{
|
{
|
||||||
bool handshakeSuccessful = false;
|
bool handshakeSuccessful = false;
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(_mutex);
|
std::lock_guard<std::mutex> lock(_mutex);
|
||||||
|
|
||||||
if (!openSSLInitialize(errMsg))
|
if (!_openSSLInitializationSuccessful)
|
||||||
{
|
{
|
||||||
|
errMsg = "OPENSSL_init_ssl failure";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_sockfd = Socket::hostname_connect(host, port, errMsg);
|
_sockfd = SocketConnect::connect(host, port, errMsg, isCancellationRequested);
|
||||||
if (_sockfd == -1) return false;
|
if (_sockfd == -1) return false;
|
||||||
|
|
||||||
_ssl_context = openSSLCreateContext(errMsg);
|
_ssl_context = openSSLCreateContext(errMsg);
|
||||||
@ -342,18 +273,28 @@ namespace ix
|
|||||||
errMsg += ERR_error_string(ssl_err, nullptr);
|
errMsg += ERR_error_string(ssl_err, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
_ssl_connection = openssl_create_connection(_ssl_context, _sockfd);
|
_ssl_connection = SSL_new(_ssl_context);
|
||||||
if (nullptr == _ssl_connection)
|
if (_ssl_connection == nullptr)
|
||||||
{
|
{
|
||||||
errMsg = "OpenSSL failed to connect";
|
errMsg = "OpenSSL failed to connect";
|
||||||
SSL_CTX_free(_ssl_context);
|
SSL_CTX_free(_ssl_context);
|
||||||
_ssl_context = nullptr;
|
_ssl_context = nullptr;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
SSL_set_fd(_ssl_connection, _sockfd);
|
||||||
|
|
||||||
// SNI support
|
// SNI support
|
||||||
SSL_set_tlsext_host_name(_ssl_connection, host.c_str());
|
SSL_set_tlsext_host_name(_ssl_connection, host.c_str());
|
||||||
|
|
||||||
|
#if OPENSSL_VERSION_NUMBER >= 0x10002000L
|
||||||
|
// Support for server name verification
|
||||||
|
// (The docs say that this should work from 1.0.2, and is the default from
|
||||||
|
// 1.1.0, but it does not. To be on the safe side, the manual test below is
|
||||||
|
// enabled for all versions prior to 1.1.0.)
|
||||||
|
X509_VERIFY_PARAM *param = SSL_get0_param(_ssl_connection);
|
||||||
|
X509_VERIFY_PARAM_set1_host(param, host.c_str(), 0);
|
||||||
|
#endif
|
||||||
|
|
||||||
handshakeSuccessful = openSSLHandshake(host, errMsg);
|
handshakeSuccessful = openSSLHandshake(host, errMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -384,7 +325,7 @@ namespace ix
|
|||||||
Socket::close();
|
Socket::close();
|
||||||
}
|
}
|
||||||
|
|
||||||
int SocketOpenSSL::send(char* buf, size_t nbyte)
|
ssize_t SocketOpenSSL::send(char* buf, size_t nbyte)
|
||||||
{
|
{
|
||||||
ssize_t sent = 0;
|
ssize_t sent = 0;
|
||||||
|
|
||||||
@ -398,8 +339,8 @@ namespace ix
|
|||||||
}
|
}
|
||||||
|
|
||||||
ERR_clear_error();
|
ERR_clear_error();
|
||||||
int write_result = SSL_write(_ssl_connection, buf + sent, (int) nbyte);
|
ssize_t write_result = SSL_write(_ssl_connection, buf + sent, (int) nbyte);
|
||||||
int reason = SSL_get_error(_ssl_connection, write_result);
|
int reason = SSL_get_error(_ssl_connection, (int) write_result);
|
||||||
|
|
||||||
if (reason == SSL_ERROR_NONE) {
|
if (reason == SSL_ERROR_NONE) {
|
||||||
nbyte -= write_result;
|
nbyte -= write_result;
|
||||||
@ -411,16 +352,15 @@ namespace ix
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (int) sent;
|
return sent;
|
||||||
}
|
}
|
||||||
|
|
||||||
int SocketOpenSSL::send(const std::string& buffer)
|
ssize_t SocketOpenSSL::send(const std::string& buffer)
|
||||||
{
|
{
|
||||||
return send((char*)&buffer[0], buffer.size());
|
return send((char*)&buffer[0], buffer.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
// No wait support
|
ssize_t SocketOpenSSL::recv(void* buf, size_t nbyte)
|
||||||
int SocketOpenSSL::recv(void* buf, size_t nbyte)
|
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
@ -432,22 +372,20 @@ namespace ix
|
|||||||
}
|
}
|
||||||
|
|
||||||
ERR_clear_error();
|
ERR_clear_error();
|
||||||
int read_result = SSL_read(_ssl_connection, buf, (int) nbyte);
|
ssize_t read_result = SSL_read(_ssl_connection, buf, (int) nbyte);
|
||||||
|
|
||||||
if (read_result > 0)
|
if (read_result > 0)
|
||||||
{
|
{
|
||||||
return read_result;
|
return read_result;
|
||||||
}
|
}
|
||||||
|
|
||||||
int reason = SSL_get_error(_ssl_connection, read_result);
|
int reason = SSL_get_error(_ssl_connection, (int) read_result);
|
||||||
|
|
||||||
if (reason == SSL_ERROR_WANT_READ || reason == SSL_ERROR_WANT_WRITE)
|
if (reason == SSL_ERROR_WANT_READ || reason == SSL_ERROR_WANT_WRITE)
|
||||||
{
|
{
|
||||||
errno = EWOULDBLOCK;
|
errno = EWOULDBLOCK;
|
||||||
return -1;
|
|
||||||
} else {
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,46 +6,48 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "IXCancellationRequest.h"
|
||||||
#include "IXSocket.h"
|
#include "IXSocket.h"
|
||||||
|
#include <mutex>
|
||||||
#include <openssl/bio.h>
|
#include <openssl/bio.h>
|
||||||
#include <openssl/hmac.h>
|
|
||||||
#include <openssl/conf.h>
|
#include <openssl/conf.h>
|
||||||
#include <openssl/err.h>
|
#include <openssl/err.h>
|
||||||
|
#include <openssl/hmac.h>
|
||||||
#include <openssl/ssl.h>
|
#include <openssl/ssl.h>
|
||||||
|
|
||||||
#include <mutex>
|
|
||||||
|
|
||||||
namespace ix
|
namespace ix
|
||||||
{
|
{
|
||||||
class SocketOpenSSL : public Socket
|
class SocketOpenSSL final : public Socket
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
SocketOpenSSL();
|
SocketOpenSSL(int fd = -1);
|
||||||
~SocketOpenSSL();
|
~SocketOpenSSL();
|
||||||
|
|
||||||
virtual bool connect(const std::string& host,
|
virtual bool connect(const std::string& host,
|
||||||
int port,
|
int port,
|
||||||
std::string& errMsg) final;
|
std::string& errMsg,
|
||||||
|
const CancellationRequest& isCancellationRequested) final;
|
||||||
virtual void close() final;
|
virtual void close() final;
|
||||||
|
|
||||||
virtual int send(char* buffer, size_t length) final;
|
virtual ssize_t send(char* buffer, size_t length) final;
|
||||||
virtual int send(const std::string& buffer) final;
|
virtual ssize_t send(const std::string& buffer) final;
|
||||||
virtual int recv(void* buffer, size_t length) final;
|
virtual ssize_t recv(void* buffer, size_t length) final;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void openSSLInitialize();
|
||||||
std::string getSSLError(int ret);
|
std::string getSSLError(int ret);
|
||||||
SSL_CTX* openSSLCreateContext(std::string& errMsg);
|
SSL_CTX* openSSLCreateContext(std::string& errMsg);
|
||||||
bool openSSLHandshake(const std::string& hostname, std::string& errMsg);
|
bool openSSLHandshake(const std::string& hostname, std::string& errMsg);
|
||||||
bool openSSLCheckServerCert(SSL *ssl,
|
bool openSSLCheckServerCert(SSL* ssl, const std::string& hostname, std::string& errMsg);
|
||||||
const std::string& hostname,
|
|
||||||
std::string& errMsg);
|
|
||||||
bool checkHost(const std::string& host, const char* pattern);
|
bool checkHost(const std::string& host, const char* pattern);
|
||||||
|
|
||||||
SSL_CTX* _ssl_context;
|
|
||||||
SSL* _ssl_connection;
|
SSL* _ssl_connection;
|
||||||
|
SSL_CTX* _ssl_context;
|
||||||
const SSL_METHOD* _ssl_method;
|
const SSL_METHOD* _ssl_method;
|
||||||
mutable std::mutex _mutex; // OpenSSL routines are not thread-safe
|
mutable std::mutex _mutex; // OpenSSL routines are not thread-safe
|
||||||
|
|
||||||
|
static std::once_flag _openSSLInitFlag;
|
||||||
|
static std::atomic<bool> _openSSLInitializationSuccessful;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
} // namespace ix
|
||||||
|
106
ixwebsocket/IXSocketSChannel.cpp
Normal file
106
ixwebsocket/IXSocketSChannel.cpp
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
* IXSocketSChannel.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* See https://docs.microsoft.com/en-us/windows/desktop/WinSock/using-secure-socket-extensions
|
||||||
|
*
|
||||||
|
* https://github.com/pauldotknopf/WindowsSDK7-Samples/blob/master/netds/winsock/securesocket/stcpclient/tcpclient.c
|
||||||
|
*
|
||||||
|
* This is the right example to look at:
|
||||||
|
* https://www.codeproject.com/Articles/1000189/A-Working-TCP-Client-and-Server-With-SSL
|
||||||
|
*/
|
||||||
|
#include "IXSocketSChannel.h"
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
# include <basetsd.h>
|
||||||
|
# include <WinSock2.h>
|
||||||
|
# include <ws2def.h>
|
||||||
|
# include <WS2tcpip.h>
|
||||||
|
# include <schannel.h>
|
||||||
|
# include <io.h>
|
||||||
|
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
|
||||||
|
#ifndef UNICODE
|
||||||
|
#define UNICODE
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <winsock2.h>
|
||||||
|
#include <mstcpip.h>
|
||||||
|
#include <ws2tcpip.h>
|
||||||
|
#include <rpc.h>
|
||||||
|
#include <ntdsapi.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <tchar.h>
|
||||||
|
|
||||||
|
#define RECV_DATA_BUF_SIZE 256
|
||||||
|
|
||||||
|
// Link with ws2_32.lib
|
||||||
|
#pragma comment(lib, "Ws2_32.lib")
|
||||||
|
|
||||||
|
// link with fwpuclnt.lib for Winsock secure socket extensions
|
||||||
|
#pragma comment(lib, "fwpuclnt.lib")
|
||||||
|
|
||||||
|
// link with ntdsapi.lib for DsMakeSpn function
|
||||||
|
#pragma comment(lib, "ntdsapi.lib")
|
||||||
|
|
||||||
|
// The following function assumes that Winsock
|
||||||
|
// has already been initialized
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#else
|
||||||
|
# error("This file should only be built on Windows")
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
SocketSChannel::SocketSChannel()
|
||||||
|
{
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
SocketSChannel::~SocketSChannel()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SocketSChannel::connect(const std::string& host,
|
||||||
|
int port,
|
||||||
|
std::string& errMsg)
|
||||||
|
{
|
||||||
|
return Socket::connect(host, port, errMsg, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void SocketSChannel::secureSocket()
|
||||||
|
{
|
||||||
|
// there will be a lot to do here ...
|
||||||
|
}
|
||||||
|
|
||||||
|
void SocketSChannel::close()
|
||||||
|
{
|
||||||
|
Socket::close();
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t SocketSChannel::send(char* buf, size_t nbyte)
|
||||||
|
{
|
||||||
|
return Socket::send(buf, nbyte);
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t SocketSChannel::send(const std::string& buffer)
|
||||||
|
{
|
||||||
|
return Socket::send(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t SocketSChannel::recv(void* buf, size_t nbyte)
|
||||||
|
{
|
||||||
|
return Socket::recv(buf, nbyte);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
32
ixwebsocket/IXSocketSChannel.h
Normal file
32
ixwebsocket/IXSocketSChannel.h
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* IXSocketSChannel.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2017-2018 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IXSocket.h"
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
class SocketSChannel final : public Socket
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SocketSChannel();
|
||||||
|
~SocketSChannel();
|
||||||
|
|
||||||
|
virtual bool connect(const std::string& host, int port, std::string& errMsg) final;
|
||||||
|
virtual void close() final;
|
||||||
|
|
||||||
|
// The important override
|
||||||
|
virtual void secureSocket() final;
|
||||||
|
|
||||||
|
virtual ssize_t send(char* buffer, size_t length) final;
|
||||||
|
virtual ssize_t send(const std::string& buffer) final;
|
||||||
|
virtual ssize_t recv(void* buffer, size_t length) final;
|
||||||
|
|
||||||
|
private:
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ix
|
312
ixwebsocket/IXSocketServer.cpp
Normal file
312
ixwebsocket/IXSocketServer.cpp
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
/*
|
||||||
|
* IXSocketServer.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXSocketServer.h"
|
||||||
|
#include "IXSocket.h"
|
||||||
|
#include "IXSocketConnect.h"
|
||||||
|
#include "IXNetSystem.h"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string.h>
|
||||||
|
#include <assert.h>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
const int SocketServer::kDefaultPort(8080);
|
||||||
|
const std::string SocketServer::kDefaultHost("127.0.0.1");
|
||||||
|
const int SocketServer::kDefaultTcpBacklog(5);
|
||||||
|
const size_t SocketServer::kDefaultMaxConnections(32);
|
||||||
|
|
||||||
|
SocketServer::SocketServer(int port,
|
||||||
|
const std::string& host,
|
||||||
|
int backlog,
|
||||||
|
size_t maxConnections) :
|
||||||
|
_port(port),
|
||||||
|
_host(host),
|
||||||
|
_backlog(backlog),
|
||||||
|
_maxConnections(maxConnections),
|
||||||
|
_serverFd(-1),
|
||||||
|
_stop(false),
|
||||||
|
_stopGc(false),
|
||||||
|
_connectionStateFactory(&ConnectionState::createConnectionState)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
SocketServer::~SocketServer()
|
||||||
|
{
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SocketServer::logError(const std::string& str)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_logMutex);
|
||||||
|
std::cerr << str << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SocketServer::logInfo(const std::string& str)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_logMutex);
|
||||||
|
std::cout << str << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<bool, std::string> SocketServer::listen()
|
||||||
|
{
|
||||||
|
struct sockaddr_in server; // server address information
|
||||||
|
|
||||||
|
// Get a socket for accepting connections.
|
||||||
|
if ((_serverFd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "SocketServer::listen() error creating socket): "
|
||||||
|
<< strerror(Socket::getErrno());
|
||||||
|
|
||||||
|
return std::make_pair(false, ss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make that socket reusable. (allow restarting this server at will)
|
||||||
|
int enable = 1;
|
||||||
|
if (setsockopt(_serverFd, SOL_SOCKET, SO_REUSEADDR,
|
||||||
|
(char*) &enable, sizeof(enable)) < 0)
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "SocketServer::listen() error calling setsockopt(SO_REUSEADDR) "
|
||||||
|
<< "at address " << _host << ":" << _port
|
||||||
|
<< " : " << strerror(Socket::getErrno());
|
||||||
|
|
||||||
|
Socket::closeSocket(_serverFd);
|
||||||
|
return std::make_pair(false, ss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind the socket to the server address.
|
||||||
|
server.sin_family = AF_INET;
|
||||||
|
server.sin_port = htons(_port);
|
||||||
|
|
||||||
|
// Using INADDR_ANY trigger a pop-up box as binding to any address is detected
|
||||||
|
// by the osx firewall. We need to codesign the binary with a self-signed cert
|
||||||
|
// to allow that, but this is a bit of a pain. (this is what node or python would do).
|
||||||
|
//
|
||||||
|
// Using INADDR_LOOPBACK also does not work ... while it should.
|
||||||
|
// We default to 127.0.0.1 (localhost)
|
||||||
|
//
|
||||||
|
server.sin_addr.s_addr = inet_addr(_host.c_str());
|
||||||
|
|
||||||
|
if (bind(_serverFd, (struct sockaddr *)&server, sizeof(server)) < 0)
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "SocketServer::listen() error calling bind "
|
||||||
|
<< "at address " << _host << ":" << _port
|
||||||
|
<< " : " << strerror(Socket::getErrno());
|
||||||
|
|
||||||
|
Socket::closeSocket(_serverFd);
|
||||||
|
return std::make_pair(false, ss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listen for connections. Specify the tcp backlog.
|
||||||
|
//
|
||||||
|
if (::listen(_serverFd, _backlog) < 0)
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "SocketServer::listen() error calling listen "
|
||||||
|
<< "at address " << _host << ":" << _port
|
||||||
|
<< " : " << strerror(Socket::getErrno());
|
||||||
|
|
||||||
|
Socket::closeSocket(_serverFd);
|
||||||
|
return std::make_pair(false, ss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::make_pair(true, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
void SocketServer::start()
|
||||||
|
{
|
||||||
|
if (!_thread.joinable())
|
||||||
|
{
|
||||||
|
_thread = std::thread(&SocketServer::run, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_gcThread.joinable())
|
||||||
|
{
|
||||||
|
_gcThread = std::thread(&SocketServer::runGC, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SocketServer::wait()
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(_conditionVariableMutex);
|
||||||
|
_conditionVariable.wait(lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SocketServer::stopAcceptingConnections()
|
||||||
|
{
|
||||||
|
_stop = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SocketServer::stop()
|
||||||
|
{
|
||||||
|
// Stop accepting connections, and close the 'accept' thread
|
||||||
|
if (_thread.joinable())
|
||||||
|
{
|
||||||
|
_stop = true;
|
||||||
|
_thread.join();
|
||||||
|
_stop = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join all threads and make sure that all connections are terminated
|
||||||
|
if (_gcThread.joinable())
|
||||||
|
{
|
||||||
|
_stopGc = true;
|
||||||
|
_gcThread.join();
|
||||||
|
_stopGc = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_conditionVariable.notify_one();
|
||||||
|
Socket::closeSocket(_serverFd);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SocketServer::setConnectionStateFactory(
|
||||||
|
const ConnectionStateFactory& connectionStateFactory)
|
||||||
|
{
|
||||||
|
_connectionStateFactory = connectionStateFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// join the threads for connections that have been closed
|
||||||
|
//
|
||||||
|
// When a connection is closed by a client, the connection state terminated
|
||||||
|
// field becomes true, and we can use that to know that we can join that thread
|
||||||
|
// and remove it from our _connectionsThreads data structure (a list).
|
||||||
|
//
|
||||||
|
void SocketServer::closeTerminatedThreads()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_connectionsThreadsMutex);
|
||||||
|
auto it = _connectionsThreads.begin();
|
||||||
|
auto itEnd = _connectionsThreads.end();
|
||||||
|
|
||||||
|
while (it != itEnd)
|
||||||
|
{
|
||||||
|
auto& connectionState = it->first;
|
||||||
|
auto& thread = it->second;
|
||||||
|
|
||||||
|
if (!connectionState->isTerminated())
|
||||||
|
{
|
||||||
|
++it;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thread.joinable()) thread.join();
|
||||||
|
it = _connectionsThreads.erase(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SocketServer::run()
|
||||||
|
{
|
||||||
|
// Set the socket to non blocking mode, so that accept calls are not blocking
|
||||||
|
SocketConnect::configure(_serverFd);
|
||||||
|
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
if (_stop) return;
|
||||||
|
|
||||||
|
// Use poll to check whether a new connection is in progress
|
||||||
|
int timeoutMs = 10;
|
||||||
|
bool readyToRead = true;
|
||||||
|
PollResultType pollResult = Socket::poll(readyToRead, timeoutMs, _serverFd);
|
||||||
|
|
||||||
|
if (pollResult == PollResultType::Error)
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "SocketServer::run() error in select: "
|
||||||
|
<< strerror(Socket::getErrno());
|
||||||
|
logError(ss.str());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pollResult != PollResultType::ReadyForRead)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept a connection.
|
||||||
|
struct sockaddr_in client; // client address information
|
||||||
|
int clientFd; // socket connected to client
|
||||||
|
socklen_t addressLen = sizeof(client);
|
||||||
|
memset(&client, 0, sizeof(client));
|
||||||
|
|
||||||
|
if ((clientFd = accept(_serverFd, (struct sockaddr *)&client, &addressLen)) < 0)
|
||||||
|
{
|
||||||
|
if (!Socket::isWaitNeeded())
|
||||||
|
{
|
||||||
|
// FIXME: that error should be propagated
|
||||||
|
int err = Socket::getErrno();
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "SocketServer::run() error accepting connection: "
|
||||||
|
<< err << ", " << strerror(err);
|
||||||
|
logError(ss.str());
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getConnectedClientsCount() >= _maxConnections)
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "SocketServer::run() reached max connections = "
|
||||||
|
<< _maxConnections << ". "
|
||||||
|
<< "Not accepting connection";
|
||||||
|
logError(ss.str());
|
||||||
|
|
||||||
|
Socket::closeSocket(clientFd);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<ConnectionState> connectionState;
|
||||||
|
if (_connectionStateFactory)
|
||||||
|
{
|
||||||
|
connectionState = _connectionStateFactory();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_stop) return;
|
||||||
|
|
||||||
|
// Launch the handleConnection work asynchronously in its own thread.
|
||||||
|
std::lock_guard<std::mutex> lock(_connectionsThreadsMutex);
|
||||||
|
_connectionsThreads.push_back(std::make_pair(
|
||||||
|
connectionState,
|
||||||
|
std::thread(&SocketServer::handleConnection,
|
||||||
|
this,
|
||||||
|
clientFd,
|
||||||
|
connectionState)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t SocketServer::getConnectionsThreadsCount()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_connectionsThreadsMutex);
|
||||||
|
return _connectionsThreads.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SocketServer::runGC()
|
||||||
|
{
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
// Garbage collection to shutdown/join threads for closed connections.
|
||||||
|
closeTerminatedThreads();
|
||||||
|
|
||||||
|
// We quit this thread if all connections are closed and we received
|
||||||
|
// a stop request by setting _stopGc to true.
|
||||||
|
if (_stopGc && getConnectionsThreadsCount() == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sleep a little bit then keep cleaning up
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
102
ixwebsocket/IXSocketServer.h
Normal file
102
ixwebsocket/IXSocketServer.h
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
* IXSocketServer.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IXConnectionState.h"
|
||||||
|
#include <atomic>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <functional>
|
||||||
|
#include <list>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <utility> // pair
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
class SocketServer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using ConnectionStateFactory = std::function<std::shared_ptr<ConnectionState>()>;
|
||||||
|
|
||||||
|
// Each connection is handled by its own worker thread.
|
||||||
|
// We use a list as we only care about remove and append operations.
|
||||||
|
using ConnectionThreads =
|
||||||
|
std::list<std::pair<std::shared_ptr<ConnectionState>, std::thread>>;
|
||||||
|
|
||||||
|
SocketServer(int port = SocketServer::kDefaultPort,
|
||||||
|
const std::string& host = SocketServer::kDefaultHost,
|
||||||
|
int backlog = SocketServer::kDefaultTcpBacklog,
|
||||||
|
size_t maxConnections = SocketServer::kDefaultMaxConnections);
|
||||||
|
virtual ~SocketServer();
|
||||||
|
virtual void stop();
|
||||||
|
|
||||||
|
// It is possible to override ConnectionState through inheritance
|
||||||
|
// this method allows user to change the factory by returning an object
|
||||||
|
// that inherits from ConnectionState but has its own methods.
|
||||||
|
void setConnectionStateFactory(const ConnectionStateFactory& connectionStateFactory);
|
||||||
|
|
||||||
|
const static int kDefaultPort;
|
||||||
|
const static std::string kDefaultHost;
|
||||||
|
const static int kDefaultTcpBacklog;
|
||||||
|
const static size_t kDefaultMaxConnections;
|
||||||
|
|
||||||
|
void start();
|
||||||
|
std::pair<bool, std::string> listen();
|
||||||
|
void wait();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Logging
|
||||||
|
void logError(const std::string& str);
|
||||||
|
void logInfo(const std::string& str);
|
||||||
|
|
||||||
|
void stopAcceptingConnections();
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Member variables
|
||||||
|
int _port;
|
||||||
|
std::string _host;
|
||||||
|
int _backlog;
|
||||||
|
size_t _maxConnections;
|
||||||
|
|
||||||
|
// socket for accepting connections
|
||||||
|
int _serverFd;
|
||||||
|
|
||||||
|
std::mutex _logMutex;
|
||||||
|
|
||||||
|
// background thread to wait for incoming connections
|
||||||
|
std::atomic<bool> _stop;
|
||||||
|
std::thread _thread;
|
||||||
|
void run();
|
||||||
|
|
||||||
|
// background thread to cleanup (join) terminated threads
|
||||||
|
std::atomic<bool> _stopGc;
|
||||||
|
std::thread _gcThread;
|
||||||
|
void runGC();
|
||||||
|
|
||||||
|
// the list of (connectionState, threads) for each connections
|
||||||
|
ConnectionThreads _connectionsThreads;
|
||||||
|
std::mutex _connectionsThreadsMutex;
|
||||||
|
|
||||||
|
// used to have the main control thread for a server
|
||||||
|
// wait for a 'terminate' notification without busy polling
|
||||||
|
std::condition_variable _conditionVariable;
|
||||||
|
std::mutex _conditionVariableMutex;
|
||||||
|
|
||||||
|
// the factory to create ConnectionState objects
|
||||||
|
ConnectionStateFactory _connectionStateFactory;
|
||||||
|
|
||||||
|
virtual void handleConnection(int fd, std::shared_ptr<ConnectionState> connectionState) = 0;
|
||||||
|
virtual size_t getConnectedClientsCount() = 0;
|
||||||
|
|
||||||
|
// Returns true if all connection threads are joined
|
||||||
|
void closeTerminatedThreads();
|
||||||
|
size_t getConnectionsThreadsCount();
|
||||||
|
};
|
||||||
|
} // namespace ix
|
67
ixwebsocket/IXUrlParser.cpp
Normal file
67
ixwebsocket/IXUrlParser.cpp
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* IXUrlParser.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXUrlParser.h"
|
||||||
|
#include "LUrlParser.h"
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
bool UrlParser::parse(const std::string& url,
|
||||||
|
std::string& protocol,
|
||||||
|
std::string& host,
|
||||||
|
std::string& path,
|
||||||
|
std::string& query,
|
||||||
|
int& port)
|
||||||
|
{
|
||||||
|
LUrlParser::clParseURL res = LUrlParser::clParseURL::ParseURL(url);
|
||||||
|
|
||||||
|
if (!res.IsValid())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol = res.m_Scheme;
|
||||||
|
host = res.m_Host;
|
||||||
|
path = res.m_Path;
|
||||||
|
query = res.m_Query;
|
||||||
|
|
||||||
|
if (!res.GetPort(&port))
|
||||||
|
{
|
||||||
|
if (protocol == "ws" || protocol == "http")
|
||||||
|
{
|
||||||
|
port = 80;
|
||||||
|
}
|
||||||
|
else if (protocol == "wss" || protocol == "https")
|
||||||
|
{
|
||||||
|
port = 443;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Invalid protocol. Should be caught by regex check
|
||||||
|
// but this missing branch trigger cpplint linter.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.empty())
|
||||||
|
{
|
||||||
|
path = "/";
|
||||||
|
}
|
||||||
|
else if (path[0] != '/')
|
||||||
|
{
|
||||||
|
path = '/' + path;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!query.empty())
|
||||||
|
{
|
||||||
|
path += "?";
|
||||||
|
path += query;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
23
ixwebsocket/IXUrlParser.h
Normal file
23
ixwebsocket/IXUrlParser.h
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* IXUrlParser.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
class UrlParser
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static bool parse(const std::string& url,
|
||||||
|
std::string& protocol,
|
||||||
|
std::string& host,
|
||||||
|
std::string& path,
|
||||||
|
std::string& query,
|
||||||
|
int& port);
|
||||||
|
};
|
||||||
|
} // namespace ix
|
83
ixwebsocket/IXUserAgent.cpp
Normal file
83
ixwebsocket/IXUserAgent.cpp
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* IXUserAgent.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2017-2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXUserAgent.h"
|
||||||
|
#include "IXWebSocketVersion.h"
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
|
#include <zlib.h>
|
||||||
|
|
||||||
|
// Platform name
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#define PLATFORM_NAME "windows" // Windows
|
||||||
|
#elif defined(_WIN64)
|
||||||
|
#define PLATFORM_NAME "windows" // Windows
|
||||||
|
#elif defined(__CYGWIN__) && !defined(_WIN32)
|
||||||
|
#define PLATFORM_NAME "windows" // Windows (Cygwin POSIX under Microsoft Window)
|
||||||
|
#elif defined(__ANDROID__)
|
||||||
|
#define PLATFORM_NAME "android" // Android (implies Linux, so it must come first)
|
||||||
|
#elif defined(__linux__)
|
||||||
|
#define PLATFORM_NAME "linux" // Debian, Ubuntu, Gentoo, Fedora, openSUSE, RedHat, Centos and other
|
||||||
|
#elif defined(__unix__) || !defined(__APPLE__) && defined(__MACH__)
|
||||||
|
#include <sys/param.h>
|
||||||
|
#if defined(BSD)
|
||||||
|
#define PLATFORM_NAME "bsd" // FreeBSD, NetBSD, OpenBSD, DragonFly BSD
|
||||||
|
#endif
|
||||||
|
#elif defined(__hpux)
|
||||||
|
#define PLATFORM_NAME "hp-ux" // HP-UX
|
||||||
|
#elif defined(_AIX)
|
||||||
|
#define PLATFORM_NAME "aix" // IBM AIX
|
||||||
|
#elif defined(__APPLE__) && defined(__MACH__) // Apple OSX and iOS (Darwin)
|
||||||
|
#include <TargetConditionals.h>
|
||||||
|
#if TARGET_IPHONE_SIMULATOR == 1
|
||||||
|
#define PLATFORM_NAME "ios" // Apple iOS
|
||||||
|
#elif TARGET_OS_IPHONE == 1
|
||||||
|
#define PLATFORM_NAME "ios" // Apple iOS
|
||||||
|
#elif TARGET_OS_MAC == 1
|
||||||
|
#define PLATFORM_NAME "macos" // Apple OSX
|
||||||
|
#endif
|
||||||
|
#elif defined(__sun) && defined(__SVR4)
|
||||||
|
#define PLATFORM_NAME "solaris" // Oracle Solaris, Open Indiana
|
||||||
|
#else
|
||||||
|
#define PLATFORM_NAME "unknown platform"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// SSL
|
||||||
|
#if defined(IXWEBSOCKET_USE_OPEN_SSL)
|
||||||
|
#include <openssl/opensslv.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
std::string userAgent()
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
|
||||||
|
// IXWebSocket Version
|
||||||
|
ss << "ixwebsocket/" << IX_WEBSOCKET_VERSION;
|
||||||
|
|
||||||
|
// Platform
|
||||||
|
ss << " " << PLATFORM_NAME;
|
||||||
|
|
||||||
|
// TLS
|
||||||
|
#ifdef IXWEBSOCKET_USE_TLS
|
||||||
|
#ifdef IXWEBSOCKET_USE_MBED_TLS
|
||||||
|
ss << " ssl/mbedtls";
|
||||||
|
#elif __APPLE__
|
||||||
|
ss << " ssl/DarwinSSL";
|
||||||
|
#elif defined(IXWEBSOCKET_USE_OPEN_SSL)
|
||||||
|
ss << " ssl/OpenSSL " << OPENSSL_VERSION_TEXT;
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
ss << " nossl";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Zlib version
|
||||||
|
ss << " zlib " << ZLIB_VERSION;
|
||||||
|
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
}
|
14
ixwebsocket/IXUserAgent.h
Normal file
14
ixwebsocket/IXUserAgent.h
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
* IXUserAgent.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
std::string userAgent();
|
||||||
|
} // namespace ix
|
@ -5,38 +5,99 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "IXWebSocket.h"
|
#include "IXWebSocket.h"
|
||||||
|
#include "IXSetThreadName.h"
|
||||||
|
#include "IXWebSocketHandshake.h"
|
||||||
|
#include "IXExponentialBackoff.h"
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
|
|
||||||
namespace {
|
namespace
|
||||||
|
|
||||||
// FIXME: put this in a shared location, and use it in
|
|
||||||
uint64_t calculateRetryWaitMilliseconds(uint64_t retry_count)
|
|
||||||
{
|
{
|
||||||
// This will overflow quite fast for large value of retry_count
|
//
|
||||||
// and will become 0, in which case the wait time will be none
|
// Stolen from here http://www.zedwood.com/article/cpp-is-valid-utf8-string-function
|
||||||
// and we'll be constantly retrying to connect.
|
// There doesn't seem to be anything in the C++ library so far to do that.
|
||||||
uint64_t wait_time = ((uint64_t) std::pow(2, retry_count) * 100L);
|
// The closest thing is code for converting from utf-8 to utf-16 or utf-32 but
|
||||||
|
// that isn't working well for some broken input strings.
|
||||||
|
//
|
||||||
|
bool isValidUtf8(const std::string& str)
|
||||||
|
{
|
||||||
|
size_t i = 0;
|
||||||
|
size_t ix = str.length();
|
||||||
|
int c, n, j;
|
||||||
|
|
||||||
// cap the wait time to 10s, or to retry_count == 10 for which wait_time > 10s
|
for (; i < ix; i++)
|
||||||
uint64_t tenSeconds = 10 * 1000;
|
{
|
||||||
return (wait_time > tenSeconds || retry_count > 10) ? tenSeconds : wait_time;
|
c = (unsigned char) str[i];
|
||||||
|
//if (c==0x09 || c==0x0a || c==0x0d || (0x20 <= c && c <= 0x7e) ) n = 0; // is_printable_ascii
|
||||||
|
if (0x00 <= c && c <= 0x7f)
|
||||||
|
{
|
||||||
|
n = 0; // 0bbbbbbb
|
||||||
|
}
|
||||||
|
else if ((c & 0xE0) == 0xC0)
|
||||||
|
{
|
||||||
|
n = 1; // 110bbbbb
|
||||||
|
}
|
||||||
|
else if ( c==0xed && i<(ix-1) && ((unsigned char)str[i+1] & 0xa0)==0xa0)
|
||||||
|
{
|
||||||
|
return false; //U+d800 to U+dfff
|
||||||
|
}
|
||||||
|
else if ((c & 0xF0) == 0xE0)
|
||||||
|
{
|
||||||
|
n = 2; // 1110bbbb
|
||||||
|
}
|
||||||
|
else if ((c & 0xF8) == 0xF0)
|
||||||
|
{
|
||||||
|
n = 3; // 11110bbb
|
||||||
|
}
|
||||||
|
//else if (($c & 0xFC) == 0xF8) n=4; // 111110bb //byte 5, unnecessary in 4 byte UTF-8
|
||||||
|
//else if (($c & 0xFE) == 0xFC) n=5; // 1111110b //byte 6, unnecessary in 4 byte UTF-8
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (j=0; j<n && i<ix; j++)
|
||||||
|
{ // n bytes matching 10bbbbbb follow ?
|
||||||
|
if ((++i == ix) || (( (unsigned char)str[i] & 0xC0) != 0x80))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace ix {
|
namespace ix
|
||||||
|
{
|
||||||
OnTrafficTrackerCallback WebSocket::_onTrafficTrackerCallback = nullptr;
|
OnTrafficTrackerCallback WebSocket::_onTrafficTrackerCallback = nullptr;
|
||||||
|
const int WebSocket::kDefaultHandShakeTimeoutSecs(60);
|
||||||
|
const int WebSocket::kDefaultPingIntervalSecs(-1);
|
||||||
|
const int WebSocket::kDefaultPingTimeoutSecs(-1);
|
||||||
|
const bool WebSocket::kDefaultEnablePong(true);
|
||||||
|
const uint32_t WebSocket::kDefaultMaxWaitBetweenReconnectionRetries(10 * 1000); // 10s
|
||||||
|
|
||||||
WebSocket::WebSocket() :
|
WebSocket::WebSocket() :
|
||||||
_verbose(false),
|
|
||||||
_onMessageCallback(OnMessageCallback()),
|
_onMessageCallback(OnMessageCallback()),
|
||||||
_stop(false),
|
_stop(false),
|
||||||
_automaticReconnection(true)
|
_automaticReconnection(true),
|
||||||
|
_maxWaitBetweenReconnectionRetries(kDefaultMaxWaitBetweenReconnectionRetries),
|
||||||
|
_handshakeTimeoutSecs(kDefaultHandShakeTimeoutSecs),
|
||||||
|
_enablePong(kDefaultEnablePong),
|
||||||
|
_pingIntervalSecs(kDefaultPingIntervalSecs),
|
||||||
|
_pingTimeoutSecs(kDefaultPingTimeoutSecs)
|
||||||
{
|
{
|
||||||
|
_ws.setOnCloseCallback(
|
||||||
|
[this](uint16_t code, const std::string& reason, size_t wireSize, bool remote)
|
||||||
|
{
|
||||||
|
_onMessageCallback(
|
||||||
|
std::make_shared<WebSocketMessage>(
|
||||||
|
WebSocketMessageType::Close, "", wireSize,
|
||||||
|
WebSocketErrorInfo(), WebSocketOpenInfo(),
|
||||||
|
WebSocketCloseInfo(code, reason, remote)));
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
WebSocket::~WebSocket()
|
WebSocket::~WebSocket()
|
||||||
@ -44,11 +105,101 @@ namespace ix {
|
|||||||
stop();
|
stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebSocket::configure(const std::string& url)
|
void WebSocket::setUrl(const std::string& url)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(_urlMutex);
|
std::lock_guard<std::mutex> lock(_configMutex);
|
||||||
_url = url;
|
_url = url;
|
||||||
}
|
}
|
||||||
|
void WebSocket::setExtraHeaders(const WebSocketHttpHeaders& headers)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_configMutex);
|
||||||
|
_extraHeaders = headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& WebSocket::getUrl() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_configMutex);
|
||||||
|
return _url;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocket::setPerMessageDeflateOptions(const WebSocketPerMessageDeflateOptions& perMessageDeflateOptions)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_configMutex);
|
||||||
|
_perMessageDeflateOptions = perMessageDeflateOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WebSocketPerMessageDeflateOptions& WebSocket::getPerMessageDeflateOptions() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_configMutex);
|
||||||
|
return _perMessageDeflateOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocket::setHeartBeatPeriod(int heartBeatPeriodSecs)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_configMutex);
|
||||||
|
_pingIntervalSecs = heartBeatPeriodSecs;
|
||||||
|
}
|
||||||
|
|
||||||
|
int WebSocket::getHeartBeatPeriod() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_configMutex);
|
||||||
|
return _pingIntervalSecs;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocket::setPingInterval(int pingIntervalSecs)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_configMutex);
|
||||||
|
_pingIntervalSecs = pingIntervalSecs;
|
||||||
|
}
|
||||||
|
|
||||||
|
int WebSocket::getPingInterval() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_configMutex);
|
||||||
|
return _pingIntervalSecs;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocket::setPingTimeout(int pingTimeoutSecs)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_configMutex);
|
||||||
|
_pingTimeoutSecs = pingTimeoutSecs;
|
||||||
|
}
|
||||||
|
|
||||||
|
int WebSocket::getPingTimeout() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_configMutex);
|
||||||
|
return _pingTimeoutSecs;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocket::enablePong()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_configMutex);
|
||||||
|
_enablePong = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocket::disablePong()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_configMutex);
|
||||||
|
_enablePong = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocket::disablePerMessageDeflate()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_configMutex);
|
||||||
|
WebSocketPerMessageDeflateOptions perMessageDeflateOptions(false);
|
||||||
|
_perMessageDeflateOptions = perMessageDeflateOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocket::setMaxWaitBetweenReconnectionRetries(uint32_t maxWaitBetweenReconnectionRetries)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_configMutex);
|
||||||
|
_maxWaitBetweenReconnectionRetries = maxWaitBetweenReconnectionRetries;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t WebSocket::getMaxWaitBetweenReconnectionRetries() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_configMutex);
|
||||||
|
return _maxWaitBetweenReconnectionRetries;
|
||||||
|
}
|
||||||
|
|
||||||
void WebSocket::start()
|
void WebSocket::start()
|
||||||
{
|
{
|
||||||
@ -57,129 +208,212 @@ namespace ix {
|
|||||||
_thread = std::thread(&WebSocket::run, this);
|
_thread = std::thread(&WebSocket::run, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebSocket::stop()
|
void WebSocket::stop(uint16_t code,
|
||||||
|
const std::string& reason)
|
||||||
{
|
{
|
||||||
_automaticReconnection = false;
|
close(code, reason);
|
||||||
|
|
||||||
close();
|
if (_thread.joinable())
|
||||||
|
|
||||||
if (!_thread.joinable())
|
|
||||||
{
|
{
|
||||||
_automaticReconnection = true;
|
// wait until working thread will exit
|
||||||
return;
|
// it will exit after close operation is finished
|
||||||
}
|
|
||||||
|
|
||||||
_stop = true;
|
_stop = true;
|
||||||
_thread.join();
|
_thread.join();
|
||||||
_stop = false;
|
_stop = false;
|
||||||
|
|
||||||
_automaticReconnection = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
WebSocketInitResult WebSocket::connect()
|
|
||||||
{
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(_urlMutex);
|
|
||||||
_ws.configure(_url);
|
|
||||||
}
|
|
||||||
|
|
||||||
_ws.setOnStateChangeCallback(
|
|
||||||
[this](WebSocketTransport::ReadyStateValues readyStateValue)
|
|
||||||
{
|
|
||||||
if (readyStateValue == WebSocketTransport::CLOSED)
|
|
||||||
{
|
|
||||||
_onMessageCallback(WebSocket_MessageType_Close, "", WebSocketErrorInfo());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_verbose)
|
|
||||||
{
|
|
||||||
std::cout << "connection state changed -> "
|
|
||||||
<< readyStateToString(getReadyState())
|
|
||||||
<< std::endl;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
WebSocketInitResult status = _ws.init();
|
WebSocketInitResult WebSocket::connect(int timeoutSecs)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_configMutex);
|
||||||
|
_ws.configure(_perMessageDeflateOptions,
|
||||||
|
_enablePong,
|
||||||
|
_pingIntervalSecs,
|
||||||
|
_pingTimeoutSecs);
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketInitResult status = _ws.connectToUrl(_url, _extraHeaders, timeoutSecs);
|
||||||
if (!status.success)
|
if (!status.success)
|
||||||
{
|
{
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onMessageCallback(WebSocket_MessageType_Open, "", WebSocketErrorInfo());
|
_onMessageCallback(
|
||||||
|
std::make_shared<WebSocketMessage>(
|
||||||
|
WebSocketMessageType::Open, "", 0,
|
||||||
|
WebSocketErrorInfo(),
|
||||||
|
WebSocketOpenInfo(status.uri, status.headers),
|
||||||
|
WebSocketCloseInfo()));
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketInitResult WebSocket::connectToSocket(int fd, int timeoutSecs)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_configMutex);
|
||||||
|
_ws.configure(_perMessageDeflateOptions,
|
||||||
|
_enablePong,
|
||||||
|
_pingIntervalSecs,
|
||||||
|
_pingTimeoutSecs);
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketInitResult status = _ws.connectToSocket(fd, timeoutSecs);
|
||||||
|
if (!status.success)
|
||||||
|
{
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onMessageCallback(
|
||||||
|
std::make_shared<WebSocketMessage>(
|
||||||
|
WebSocketMessageType::Open, "", 0,
|
||||||
|
WebSocketErrorInfo(),
|
||||||
|
WebSocketOpenInfo(status.uri, status.headers),
|
||||||
|
WebSocketCloseInfo()));
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WebSocket::isConnected() const
|
bool WebSocket::isConnected() const
|
||||||
{
|
{
|
||||||
return getReadyState() == WebSocket_ReadyState_Open;
|
return getReadyState() == ReadyState::Open;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WebSocket::isClosing() const
|
bool WebSocket::isClosing() const
|
||||||
{
|
{
|
||||||
return getReadyState() == WebSocket_ReadyState_Closing;
|
return getReadyState() == ReadyState::Closing;
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebSocket::close()
|
void WebSocket::close(uint16_t code,
|
||||||
|
const std::string& reason)
|
||||||
{
|
{
|
||||||
_ws.close();
|
_ws.close(code, reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebSocket::reconnectPerpetuallyIfDisconnected()
|
void WebSocket::checkConnection(bool firstConnectionAttempt)
|
||||||
{
|
{
|
||||||
uint64_t retries = 0;
|
|
||||||
WebSocketErrorInfo connectErr;
|
|
||||||
ix::WebSocketInitResult status;
|
|
||||||
using millis = std::chrono::duration<double, std::milli>;
|
using millis = std::chrono::duration<double, std::milli>;
|
||||||
millis duration;
|
|
||||||
|
|
||||||
|
uint32_t retries = 0;
|
||||||
|
millis duration(0);
|
||||||
|
|
||||||
|
// Try to connect perpertually
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
if (isConnected() || isClosing() || _stop || !_automaticReconnection)
|
if (isConnected() || isClosing() || _stop)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
status = connect();
|
if (!firstConnectionAttempt && !_automaticReconnection)
|
||||||
|
|
||||||
if (!status.success && !_stop)
|
|
||||||
{
|
{
|
||||||
duration = millis(calculateRetryWaitMilliseconds(retries++));
|
// Do not attempt to reconnect
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
firstConnectionAttempt = false;
|
||||||
|
|
||||||
|
// Only sleep if we are retrying
|
||||||
|
if (duration.count() > 0)
|
||||||
|
{
|
||||||
|
// to do: make sleeping conditional
|
||||||
|
std::this_thread::sleep_for(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to connect synchronously
|
||||||
|
ix::WebSocketInitResult status = connect(_handshakeTimeoutSecs);
|
||||||
|
|
||||||
|
if (!status.success)
|
||||||
|
{
|
||||||
|
WebSocketErrorInfo connectErr;
|
||||||
|
|
||||||
|
if (_automaticReconnection)
|
||||||
|
{
|
||||||
|
duration = millis(calculateRetryWaitMilliseconds(retries++, _maxWaitBetweenReconnectionRetries));
|
||||||
|
|
||||||
connectErr.retries = retries;
|
|
||||||
connectErr.wait_time = duration.count();
|
connectErr.wait_time = duration.count();
|
||||||
|
connectErr.retries = retries;
|
||||||
|
}
|
||||||
|
|
||||||
connectErr.reason = status.errorStr;
|
connectErr.reason = status.errorStr;
|
||||||
connectErr.http_status = status.http_status;
|
connectErr.http_status = status.http_status;
|
||||||
_onMessageCallback(WebSocket_MessageType_Error, "", connectErr);
|
|
||||||
|
|
||||||
if (_verbose) std::cout << "Sleeping for " << duration.count() << "ms" << std::endl;
|
_onMessageCallback(
|
||||||
|
std::make_shared<WebSocketMessage>(
|
||||||
std::this_thread::sleep_for(duration);
|
WebSocketMessageType::Error, "", 0,
|
||||||
|
connectErr, WebSocketOpenInfo(),
|
||||||
|
WebSocketCloseInfo()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebSocket::run()
|
void WebSocket::run()
|
||||||
{
|
{
|
||||||
|
setThreadName(getUrl());
|
||||||
|
|
||||||
|
bool firstConnectionAttempt = true;
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
if (_stop) return;
|
|
||||||
|
|
||||||
// 1. Make sure we are always connected
|
// 1. Make sure we are always connected
|
||||||
reconnectPerpetuallyIfDisconnected();
|
checkConnection(firstConnectionAttempt);
|
||||||
|
|
||||||
if (_stop) return;
|
firstConnectionAttempt = false;
|
||||||
|
|
||||||
|
// if here we are closed then checkConnection was not able to connect
|
||||||
|
if (getReadyState() == ReadyState::Closed)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can avoid to poll if we want to stop and are not closing
|
||||||
|
if (_stop && !isClosing()) break;
|
||||||
|
|
||||||
// 2. Poll to see if there's any new data available
|
// 2. Poll to see if there's any new data available
|
||||||
_ws.poll();
|
WebSocketTransport::PollResult pollResult = _ws.poll();
|
||||||
|
|
||||||
if (_stop) return;
|
|
||||||
|
|
||||||
// 3. Dispatch the incoming messages
|
// 3. Dispatch the incoming messages
|
||||||
_ws.dispatch(
|
_ws.dispatch(
|
||||||
[this](const std::string& msg)
|
pollResult,
|
||||||
|
[this](const std::string& msg,
|
||||||
|
size_t wireSize,
|
||||||
|
bool decompressionError,
|
||||||
|
WebSocketTransport::MessageKind messageKind)
|
||||||
{
|
{
|
||||||
_onMessageCallback(WebSocket_MessageType_Message, msg, WebSocketErrorInfo());
|
WebSocketMessageType webSocketMessageType;
|
||||||
|
switch (messageKind)
|
||||||
|
{
|
||||||
|
case WebSocketTransport::MessageKind::MSG_TEXT:
|
||||||
|
case WebSocketTransport::MessageKind::MSG_BINARY:
|
||||||
|
{
|
||||||
|
webSocketMessageType = WebSocketMessageType::Message;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case WebSocketTransport::MessageKind::PING:
|
||||||
|
{
|
||||||
|
webSocketMessageType = WebSocketMessageType::Ping;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case WebSocketTransport::MessageKind::PONG:
|
||||||
|
{
|
||||||
|
webSocketMessageType = WebSocketMessageType::Pong;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case WebSocketTransport::MessageKind::FRAGMENT:
|
||||||
|
{
|
||||||
|
webSocketMessageType = WebSocketMessageType::Fragment;
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketErrorInfo webSocketErrorInfo;
|
||||||
|
webSocketErrorInfo.decompressionError = decompressionError;
|
||||||
|
|
||||||
|
bool binary = messageKind == WebSocketTransport::MessageKind::MSG_BINARY;
|
||||||
|
|
||||||
|
_onMessageCallback(
|
||||||
|
std::make_shared<WebSocketMessage>(
|
||||||
|
webSocketMessageType, msg, wireSize,
|
||||||
|
webSocketErrorInfo, WebSocketOpenInfo(),
|
||||||
|
WebSocketCloseInfo(), binary));
|
||||||
|
|
||||||
WebSocket::invokeTrafficTrackerCallback(msg.size(), true);
|
WebSocket::invokeTrafficTrackerCallback(msg.size(), true);
|
||||||
});
|
});
|
||||||
@ -209,9 +443,45 @@ namespace ix {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WebSocket::send(const std::string& text)
|
WebSocketSendInfo WebSocket::send(const std::string& data,
|
||||||
|
bool binary,
|
||||||
|
const OnProgressCallback& onProgressCallback)
|
||||||
{
|
{
|
||||||
if (!isConnected()) return false;
|
return (binary) ? sendBinary(data, onProgressCallback) : sendText(data, onProgressCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketSendInfo WebSocket::sendBinary(const std::string& text,
|
||||||
|
const OnProgressCallback& onProgressCallback)
|
||||||
|
{
|
||||||
|
return sendMessage(text, SendMessageKind::Binary, onProgressCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketSendInfo WebSocket::sendText(const std::string& text,
|
||||||
|
const OnProgressCallback& onProgressCallback)
|
||||||
|
{
|
||||||
|
if (!isValidUtf8(text))
|
||||||
|
{
|
||||||
|
close(WebSocketCloseConstants::kNormalClosureCode,
|
||||||
|
WebSocketCloseConstants::kInvalidUtf8);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return sendMessage(text, SendMessageKind::Text, onProgressCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketSendInfo WebSocket::ping(const std::string& text)
|
||||||
|
{
|
||||||
|
// Standard limit ping message size
|
||||||
|
constexpr size_t pingMaxPayloadSize = 125;
|
||||||
|
if (text.size() > pingMaxPayloadSize) return WebSocketSendInfo(false);
|
||||||
|
|
||||||
|
return sendMessage(text, SendMessageKind::Ping);
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketSendInfo WebSocket::sendMessage(const std::string& text,
|
||||||
|
SendMessageKind sendMessageKind,
|
||||||
|
const OnProgressCallback& onProgressCallback)
|
||||||
|
{
|
||||||
|
if (!isConnected()) return WebSocketSendInfo(false);
|
||||||
|
|
||||||
//
|
//
|
||||||
// It is OK to read and write on the same socket in 2 different threads.
|
// It is OK to read and write on the same socket in 2 different threads.
|
||||||
@ -223,21 +493,40 @@ namespace ix {
|
|||||||
// incoming messages are arriving / there's data to be received.
|
// incoming messages are arriving / there's data to be received.
|
||||||
//
|
//
|
||||||
std::lock_guard<std::mutex> lock(_writeMutex);
|
std::lock_guard<std::mutex> lock(_writeMutex);
|
||||||
_ws.sendBinary(text);
|
WebSocketSendInfo webSocketSendInfo;
|
||||||
|
|
||||||
WebSocket::invokeTrafficTrackerCallback(text.size(), false);
|
switch (sendMessageKind)
|
||||||
|
{
|
||||||
|
case SendMessageKind::Text:
|
||||||
|
{
|
||||||
|
webSocketSendInfo = _ws.sendText(text, onProgressCallback);
|
||||||
|
} break;
|
||||||
|
|
||||||
return true;
|
case SendMessageKind::Binary:
|
||||||
|
{
|
||||||
|
webSocketSendInfo = _ws.sendBinary(text, onProgressCallback);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case SendMessageKind::Ping:
|
||||||
|
{
|
||||||
|
webSocketSendInfo = _ws.sendPing(text);
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocket::invokeTrafficTrackerCallback(webSocketSendInfo.wireSize, false);
|
||||||
|
|
||||||
|
return webSocketSendInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
ReadyState WebSocket::getReadyState() const
|
ReadyState WebSocket::getReadyState() const
|
||||||
{
|
{
|
||||||
switch (_ws.getReadyState())
|
switch (_ws.getReadyState())
|
||||||
{
|
{
|
||||||
case ix::WebSocketTransport::OPEN: return WebSocket_ReadyState_Open;
|
case ix::WebSocketTransport::ReadyState::OPEN : return ReadyState::Open;
|
||||||
case ix::WebSocketTransport::CONNECTING: return WebSocket_ReadyState_Connecting;
|
case ix::WebSocketTransport::ReadyState::CONNECTING: return ReadyState::Connecting;
|
||||||
case ix::WebSocketTransport::CLOSING: return WebSocket_ReadyState_Closing;
|
case ix::WebSocketTransport::ReadyState::CLOSING : return ReadyState::Closing;
|
||||||
case ix::WebSocketTransport::CLOSED: return WebSocket_ReadyState_Closed;
|
case ix::WebSocketTransport::ReadyState::CLOSED : return ReadyState::Closed;
|
||||||
|
default: return ReadyState::Closed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,16 +534,31 @@ namespace ix {
|
|||||||
{
|
{
|
||||||
switch (readyState)
|
switch (readyState)
|
||||||
{
|
{
|
||||||
case WebSocket_ReadyState_Open: return "OPEN";
|
case ReadyState::Open : return "OPEN";
|
||||||
case WebSocket_ReadyState_Connecting: return "CONNECTING";
|
case ReadyState::Connecting: return "CONNECTING";
|
||||||
case WebSocket_ReadyState_Closing: return "CLOSING";
|
case ReadyState::Closing : return "CLOSING";
|
||||||
case WebSocket_ReadyState_Closed: return "CLOSED";
|
case ReadyState::Closed : return "CLOSED";
|
||||||
|
default: return "UNKNOWN";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string& WebSocket::getUrl() const
|
void WebSocket::enableAutomaticReconnection()
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(_urlMutex);
|
_automaticReconnection = true;
|
||||||
return _url;
|
}
|
||||||
|
|
||||||
|
void WebSocket::disableAutomaticReconnection()
|
||||||
|
{
|
||||||
|
_automaticReconnection = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebSocket::isAutomaticReconnectionEnabled() const
|
||||||
|
{
|
||||||
|
return _automaticReconnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t WebSocket::bufferedAmount() const
|
||||||
|
{
|
||||||
|
return _ws.bufferedAmount();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,41 +9,32 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "IXProgressCallback.h"
|
||||||
|
#include "IXWebSocketCloseConstants.h"
|
||||||
|
#include "IXWebSocketErrorInfo.h"
|
||||||
|
#include "IXWebSocketHttpHeaders.h"
|
||||||
|
#include "IXWebSocketMessage.h"
|
||||||
|
#include "IXWebSocketPerMessageDeflateOptions.h"
|
||||||
|
#include "IXWebSocketSendInfo.h"
|
||||||
|
#include "IXWebSocketTransport.h"
|
||||||
|
#include <atomic>
|
||||||
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <mutex>
|
|
||||||
#include <atomic>
|
|
||||||
|
|
||||||
#include "IXWebSocketTransport.h"
|
|
||||||
|
|
||||||
namespace ix
|
namespace ix
|
||||||
{
|
{
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket#Ready_state_constants
|
// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket#Ready_state_constants
|
||||||
enum ReadyState
|
enum class ReadyState
|
||||||
{
|
{
|
||||||
WebSocket_ReadyState_Connecting = 0,
|
Connecting = 0,
|
||||||
WebSocket_ReadyState_Open = 1,
|
Open = 1,
|
||||||
WebSocket_ReadyState_Closing = 2,
|
Closing = 2,
|
||||||
WebSocket_ReadyState_Closed = 3
|
Closed = 3
|
||||||
};
|
};
|
||||||
|
|
||||||
enum WebSocketMessageType
|
using OnMessageCallback = std::function<void(const WebSocketMessagePtr&)>;
|
||||||
{
|
|
||||||
WebSocket_MessageType_Message = 0,
|
|
||||||
WebSocket_MessageType_Open = 1,
|
|
||||||
WebSocket_MessageType_Close = 2,
|
|
||||||
WebSocket_MessageType_Error = 3
|
|
||||||
};
|
|
||||||
|
|
||||||
struct WebSocketErrorInfo
|
|
||||||
{
|
|
||||||
uint64_t retries;
|
|
||||||
double wait_time;
|
|
||||||
int http_status;
|
|
||||||
std::string reason;
|
|
||||||
};
|
|
||||||
|
|
||||||
using OnMessageCallback = std::function<void(WebSocketMessageType, const std::string&, const WebSocketErrorInfo)>;
|
|
||||||
using OnTrafficTrackerCallback = std::function<void(size_t size, bool incoming)>;
|
using OnTrafficTrackerCallback = std::function<void(size_t size, bool incoming)>;
|
||||||
|
|
||||||
class WebSocket
|
class WebSocket
|
||||||
@ -52,45 +43,109 @@ namespace ix
|
|||||||
WebSocket();
|
WebSocket();
|
||||||
~WebSocket();
|
~WebSocket();
|
||||||
|
|
||||||
void configure(const std::string& url);
|
void setUrl(const std::string& url);
|
||||||
|
|
||||||
|
// send extra headers in client handshake request
|
||||||
|
void setExtraHeaders(const WebSocketHttpHeaders& headers);
|
||||||
|
void setPerMessageDeflateOptions(
|
||||||
|
const WebSocketPerMessageDeflateOptions& perMessageDeflateOptions);
|
||||||
|
void setHeartBeatPeriod(int heartBeatPeriodSecs);
|
||||||
|
void setPingInterval(int pingIntervalSecs); // alias of setHeartBeatPeriod
|
||||||
|
void setPingTimeout(int pingTimeoutSecs);
|
||||||
|
void enablePong();
|
||||||
|
void disablePong();
|
||||||
|
void disablePerMessageDeflate();
|
||||||
|
|
||||||
|
// Run asynchronously, by calling start and stop.
|
||||||
void start();
|
void start();
|
||||||
void stop();
|
|
||||||
bool send(const std::string& text);
|
// stop is synchronous
|
||||||
void close();
|
void stop(uint16_t code = WebSocketCloseConstants::kNormalClosureCode,
|
||||||
|
const std::string& reason = WebSocketCloseConstants::kNormalClosureMessage);
|
||||||
|
|
||||||
|
// Run in blocking mode, by connecting first manually, and then calling run.
|
||||||
|
WebSocketInitResult connect(int timeoutSecs);
|
||||||
|
void run();
|
||||||
|
|
||||||
|
// send is in binary mode by default
|
||||||
|
WebSocketSendInfo send(const std::string& data,
|
||||||
|
bool binary = false,
|
||||||
|
const OnProgressCallback& onProgressCallback = nullptr);
|
||||||
|
WebSocketSendInfo sendBinary(const std::string& text,
|
||||||
|
const OnProgressCallback& onProgressCallback = nullptr);
|
||||||
|
WebSocketSendInfo sendText(const std::string& text,
|
||||||
|
const OnProgressCallback& onProgressCallback = nullptr);
|
||||||
|
WebSocketSendInfo ping(const std::string& text);
|
||||||
|
|
||||||
|
void close(uint16_t code = WebSocketCloseConstants::kNormalClosureCode,
|
||||||
|
const std::string& reason = WebSocketCloseConstants::kNormalClosureMessage);
|
||||||
|
|
||||||
void setOnMessageCallback(const OnMessageCallback& callback);
|
void setOnMessageCallback(const OnMessageCallback& callback);
|
||||||
static void setTrafficTrackerCallback(const OnTrafficTrackerCallback& callback);
|
static void setTrafficTrackerCallback(const OnTrafficTrackerCallback& callback);
|
||||||
static void resetTrafficTrackerCallback();
|
static void resetTrafficTrackerCallback();
|
||||||
|
|
||||||
void setVerbose(bool verbose) { _verbose = verbose; }
|
ReadyState getReadyState() const;
|
||||||
|
static std::string readyStateToString(ReadyState readyState);
|
||||||
|
|
||||||
const std::string& getUrl() const;
|
const std::string& getUrl() const;
|
||||||
ReadyState getReadyState() const;
|
const WebSocketPerMessageDeflateOptions& getPerMessageDeflateOptions() const;
|
||||||
|
int getHeartBeatPeriod() const;
|
||||||
|
int getPingInterval() const;
|
||||||
|
int getPingTimeout() const;
|
||||||
|
size_t bufferedAmount() const;
|
||||||
|
|
||||||
|
void enableAutomaticReconnection();
|
||||||
|
void disableAutomaticReconnection();
|
||||||
|
bool isAutomaticReconnectionEnabled() const;
|
||||||
|
void setMaxWaitBetweenReconnectionRetries(uint32_t maxWaitBetweenReconnectionRetries);
|
||||||
|
uint32_t getMaxWaitBetweenReconnectionRetries() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void run();
|
WebSocketSendInfo sendMessage(const std::string& text,
|
||||||
|
SendMessageKind sendMessageKind,
|
||||||
|
const OnProgressCallback& callback = nullptr);
|
||||||
|
|
||||||
WebSocketInitResult connect();
|
|
||||||
bool isConnected() const;
|
bool isConnected() const;
|
||||||
bool isClosing() const;
|
bool isClosing() const;
|
||||||
void reconnectPerpetuallyIfDisconnected();
|
void checkConnection(bool firstConnectionAttempt);
|
||||||
std::string readyStateToString(ReadyState readyState);
|
|
||||||
static void invokeTrafficTrackerCallback(size_t size, bool incoming);
|
static void invokeTrafficTrackerCallback(size_t size, bool incoming);
|
||||||
|
|
||||||
|
// Server
|
||||||
|
WebSocketInitResult connectToSocket(int fd, int timeoutSecs);
|
||||||
|
|
||||||
WebSocketTransport _ws;
|
WebSocketTransport _ws;
|
||||||
|
|
||||||
std::string _url;
|
std::string _url;
|
||||||
mutable std::mutex _urlMutex;
|
WebSocketHttpHeaders _extraHeaders;
|
||||||
bool _verbose;
|
|
||||||
|
WebSocketPerMessageDeflateOptions _perMessageDeflateOptions;
|
||||||
|
mutable std::mutex _configMutex; // protect all config variables access
|
||||||
|
|
||||||
OnMessageCallback _onMessageCallback;
|
OnMessageCallback _onMessageCallback;
|
||||||
static OnTrafficTrackerCallback _onTrafficTrackerCallback;
|
static OnTrafficTrackerCallback _onTrafficTrackerCallback;
|
||||||
|
|
||||||
std::atomic<bool> _stop;
|
std::atomic<bool> _stop;
|
||||||
std::atomic<bool> _automaticReconnection;
|
|
||||||
std::thread _thread;
|
std::thread _thread;
|
||||||
std::mutex _writeMutex;
|
std::mutex _writeMutex;
|
||||||
|
|
||||||
static int kHeartBeatPeriod;
|
// Automatic reconnection
|
||||||
|
std::atomic<bool> _automaticReconnection;
|
||||||
|
static const uint32_t kDefaultMaxWaitBetweenReconnectionRetries;
|
||||||
|
uint32_t _maxWaitBetweenReconnectionRetries;
|
||||||
|
|
||||||
|
std::atomic<int> _handshakeTimeoutSecs;
|
||||||
|
static const int kDefaultHandShakeTimeoutSecs;
|
||||||
|
|
||||||
|
// enable or disable PONG frame response to received PING frame
|
||||||
|
bool _enablePong;
|
||||||
|
static const bool kDefaultEnablePong;
|
||||||
|
|
||||||
|
// Optional ping and pong timeout
|
||||||
|
int _pingIntervalSecs;
|
||||||
|
int _pingTimeoutSecs;
|
||||||
|
static const int kDefaultPingIntervalSecs;
|
||||||
|
static const int kDefaultPingTimeoutSecs;
|
||||||
|
|
||||||
|
friend class WebSocketServer;
|
||||||
};
|
};
|
||||||
}
|
} // namespace ix
|
||||||
|
27
ixwebsocket/IXWebSocketCloseConstants.cpp
Normal file
27
ixwebsocket/IXWebSocketCloseConstants.cpp
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* IXWebSocketCloseConstants.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXWebSocketCloseConstants.h"
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
const uint16_t WebSocketCloseConstants::kNormalClosureCode(1000);
|
||||||
|
const uint16_t WebSocketCloseConstants::kInternalErrorCode(1011);
|
||||||
|
const uint16_t WebSocketCloseConstants::kAbnormalCloseCode(1006);
|
||||||
|
const uint16_t WebSocketCloseConstants::kProtocolErrorCode(1002);
|
||||||
|
const uint16_t WebSocketCloseConstants::kNoStatusCodeErrorCode(1005);
|
||||||
|
|
||||||
|
const std::string WebSocketCloseConstants::kNormalClosureMessage("Normal closure");
|
||||||
|
const std::string WebSocketCloseConstants::kInternalErrorMessage("Internal error");
|
||||||
|
const std::string WebSocketCloseConstants::kAbnormalCloseMessage("Abnormal closure");
|
||||||
|
const std::string WebSocketCloseConstants::kPingTimeoutMessage("Ping timeout");
|
||||||
|
const std::string WebSocketCloseConstants::kProtocolErrorMessage("Protocol error");
|
||||||
|
const std::string WebSocketCloseConstants::kNoStatusCodeErrorMessage("No status code");
|
||||||
|
const std::string WebSocketCloseConstants::kProtocolErrorReservedBitUsed("Reserved bit used");
|
||||||
|
const std::string WebSocketCloseConstants::kProtocolErrorPingPayloadOversized("Ping reason control frame with payload length > 125 octets");
|
||||||
|
const std::string WebSocketCloseConstants::kProtocolErrorCodeControlMessageFragmented("Control message fragmented");
|
||||||
|
const std::string WebSocketCloseConstants::kInvalidUtf8("Invalid UTF-8");
|
||||||
|
}
|
33
ixwebsocket/IXWebSocketCloseConstants.h
Normal file
33
ixwebsocket/IXWebSocketCloseConstants.h
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* IXWebSocketCloseConstants.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
struct WebSocketCloseConstants
|
||||||
|
{
|
||||||
|
static const uint16_t kNormalClosureCode;
|
||||||
|
static const uint16_t kInternalErrorCode;
|
||||||
|
static const uint16_t kAbnormalCloseCode;
|
||||||
|
static const uint16_t kProtocolErrorCode;
|
||||||
|
static const uint16_t kNoStatusCodeErrorCode;
|
||||||
|
|
||||||
|
static const std::string kNormalClosureMessage;
|
||||||
|
static const std::string kInternalErrorMessage;
|
||||||
|
static const std::string kAbnormalCloseMessage;
|
||||||
|
static const std::string kPingTimeoutMessage;
|
||||||
|
static const std::string kProtocolErrorMessage;
|
||||||
|
static const std::string kNoStatusCodeErrorMessage;
|
||||||
|
static const std::string kProtocolErrorReservedBitUsed;
|
||||||
|
static const std::string kProtocolErrorPingPayloadOversized;
|
||||||
|
static const std::string kProtocolErrorCodeControlMessageFragmented;
|
||||||
|
static const std::string kInvalidUtf8;
|
||||||
|
};
|
||||||
|
} // namespace ix
|
25
ixwebsocket/IXWebSocketCloseInfo.h
Normal file
25
ixwebsocket/IXWebSocketCloseInfo.h
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* IXWebSocketCloseInfo.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2017-2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
struct WebSocketCloseInfo
|
||||||
|
{
|
||||||
|
uint16_t code;
|
||||||
|
std::string reason;
|
||||||
|
bool remote;
|
||||||
|
|
||||||
|
WebSocketCloseInfo(uint16_t c = 0, const std::string& r = std::string(), bool rem = false)
|
||||||
|
: code(c)
|
||||||
|
, reason(r)
|
||||||
|
, remote(rem)
|
||||||
|
{
|
||||||
|
;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // namespace ix
|
21
ixwebsocket/IXWebSocketErrorInfo.h
Normal file
21
ixwebsocket/IXWebSocketErrorInfo.h
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* IXWebSocketErrorInfo.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2017-2018 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
struct WebSocketErrorInfo
|
||||||
|
{
|
||||||
|
uint32_t retries = 0;
|
||||||
|
double wait_time = 0;
|
||||||
|
int http_status = 0;
|
||||||
|
std::string reason;
|
||||||
|
bool decompressionError = false;
|
||||||
|
};
|
||||||
|
} // namespace ix
|
356
ixwebsocket/IXWebSocketHandshake.cpp
Normal file
356
ixwebsocket/IXWebSocketHandshake.cpp
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
/*
|
||||||
|
* IXWebSocketHandshake.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXWebSocketHandshake.h"
|
||||||
|
#include "IXSocketConnect.h"
|
||||||
|
#include "IXUrlParser.h"
|
||||||
|
#include "IXHttp.h"
|
||||||
|
#include "IXUserAgent.h"
|
||||||
|
|
||||||
|
#include "libwshandshake.hpp"
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
|
#include <random>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
WebSocketHandshake::WebSocketHandshake(std::atomic<bool>& requestInitCancellation,
|
||||||
|
std::shared_ptr<Socket> socket,
|
||||||
|
WebSocketPerMessageDeflate& perMessageDeflate,
|
||||||
|
WebSocketPerMessageDeflateOptions& perMessageDeflateOptions,
|
||||||
|
std::atomic<bool>& enablePerMessageDeflate) :
|
||||||
|
_requestInitCancellation(requestInitCancellation),
|
||||||
|
_socket(socket),
|
||||||
|
_perMessageDeflate(perMessageDeflate),
|
||||||
|
_perMessageDeflateOptions(perMessageDeflateOptions),
|
||||||
|
_enablePerMessageDeflate(enablePerMessageDeflate)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebSocketHandshake::insensitiveStringCompare(const std::string& a, const std::string& b)
|
||||||
|
{
|
||||||
|
return std::equal(a.begin(), a.end(),
|
||||||
|
b.begin(), b.end(),
|
||||||
|
[](char a, char b)
|
||||||
|
{
|
||||||
|
return tolower(a) == tolower(b);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string WebSocketHandshake::genRandomString(const int len)
|
||||||
|
{
|
||||||
|
std::string alphanum =
|
||||||
|
"0123456789"
|
||||||
|
"ABCDEFGH"
|
||||||
|
"abcdefgh";
|
||||||
|
|
||||||
|
std::random_device r;
|
||||||
|
std::default_random_engine e1(r());
|
||||||
|
std::uniform_int_distribution<int> dist(0, (int) alphanum.size() - 1);
|
||||||
|
|
||||||
|
std::string s;
|
||||||
|
s.resize(len);
|
||||||
|
|
||||||
|
for (int i = 0; i < len; ++i)
|
||||||
|
{
|
||||||
|
int x = dist(e1);
|
||||||
|
s[i] = alphanum[x];
|
||||||
|
}
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketInitResult WebSocketHandshake::sendErrorResponse(int code, const std::string& reason)
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "HTTP/1.1 ";
|
||||||
|
ss << code;
|
||||||
|
ss << " ";
|
||||||
|
ss << reason;
|
||||||
|
ss << "\r\n";
|
||||||
|
|
||||||
|
// Socket write can only be cancelled through a timeout here, not manually.
|
||||||
|
static std::atomic<bool> requestInitCancellation(false);
|
||||||
|
auto isCancellationRequested =
|
||||||
|
makeCancellationRequestWithTimeout(1, requestInitCancellation);
|
||||||
|
|
||||||
|
if (!_socket->writeBytes(ss.str(), isCancellationRequested))
|
||||||
|
{
|
||||||
|
return WebSocketInitResult(false, 500, "Timed out while sending error response");
|
||||||
|
}
|
||||||
|
|
||||||
|
return WebSocketInitResult(false, code, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketInitResult WebSocketHandshake::clientHandshake(const std::string& url,
|
||||||
|
const WebSocketHttpHeaders& extraHeaders,
|
||||||
|
const std::string& host,
|
||||||
|
const std::string& path,
|
||||||
|
int port,
|
||||||
|
int timeoutSecs)
|
||||||
|
{
|
||||||
|
_requestInitCancellation = false;
|
||||||
|
|
||||||
|
auto isCancellationRequested =
|
||||||
|
makeCancellationRequestWithTimeout(timeoutSecs, _requestInitCancellation);
|
||||||
|
|
||||||
|
std::string errMsg;
|
||||||
|
bool success = _socket->connect(host, port, errMsg, isCancellationRequested);
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "Unable to connect to " << host
|
||||||
|
<< " on port " << port
|
||||||
|
<< ", error: " << errMsg;
|
||||||
|
return WebSocketInitResult(false, 0, ss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Generate a random 24 bytes string which looks like it is base64 encoded
|
||||||
|
// y3JJHMbDL1EzLkh9GBhXDw==
|
||||||
|
// 0cb3Vd9HkbpVVumoS3Noka==
|
||||||
|
//
|
||||||
|
// See https://stackoverflow.com/questions/18265128/what-is-sec-websocket-key-for
|
||||||
|
//
|
||||||
|
std::string secWebSocketKey = genRandomString(22);
|
||||||
|
secWebSocketKey += "==";
|
||||||
|
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "GET " << path << " HTTP/1.1\r\n";
|
||||||
|
ss << "Host: "<< host << ":" << port << "\r\n";
|
||||||
|
ss << "Upgrade: websocket\r\n";
|
||||||
|
ss << "Connection: Upgrade\r\n";
|
||||||
|
ss << "Sec-WebSocket-Version: 13\r\n";
|
||||||
|
ss << "Sec-WebSocket-Key: " << secWebSocketKey << "\r\n";
|
||||||
|
|
||||||
|
// User-Agent can be customized by users
|
||||||
|
if (extraHeaders.find("User-Agent") == extraHeaders.end())
|
||||||
|
{
|
||||||
|
ss << "User-Agent: " << userAgent() << "\r\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& it : extraHeaders)
|
||||||
|
{
|
||||||
|
ss << it.first << ":" << it.second << "\r\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_enablePerMessageDeflate)
|
||||||
|
{
|
||||||
|
ss << _perMessageDeflateOptions.generateHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
ss << "\r\n";
|
||||||
|
|
||||||
|
if (!_socket->writeBytes(ss.str(), isCancellationRequested))
|
||||||
|
{
|
||||||
|
return WebSocketInitResult(false, 0, std::string("Failed sending GET request to ") + url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read HTTP status line
|
||||||
|
auto lineResult = _socket->readLine(isCancellationRequested);
|
||||||
|
auto lineValid = lineResult.first;
|
||||||
|
auto line = lineResult.second;
|
||||||
|
|
||||||
|
if (!lineValid)
|
||||||
|
{
|
||||||
|
return WebSocketInitResult(false, 0,
|
||||||
|
std::string("Failed reading HTTP status line from ") + url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate status
|
||||||
|
int status;
|
||||||
|
|
||||||
|
// HTTP/1.0 is too old.
|
||||||
|
if (sscanf(line.c_str(), "HTTP/1.0 %d", &status) == 1)
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "Server version is HTTP/1.0. Rejecting connection to " << host
|
||||||
|
<< ", status: " << status
|
||||||
|
<< ", HTTP Status line: " << line;
|
||||||
|
return WebSocketInitResult(false, status, ss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want an 101 HTTP status
|
||||||
|
if (sscanf(line.c_str(), "HTTP/1.1 %d", &status) != 1 || status != 101)
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "Got bad status connecting to " << host
|
||||||
|
<< ", status: " << status
|
||||||
|
<< ", HTTP Status line: " << line;
|
||||||
|
return WebSocketInitResult(false, status, ss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto result = parseHttpHeaders(_socket, isCancellationRequested);
|
||||||
|
auto headersValid = result.first;
|
||||||
|
auto headers = result.second;
|
||||||
|
|
||||||
|
if (!headersValid)
|
||||||
|
{
|
||||||
|
return WebSocketInitResult(false, status, "Error parsing HTTP headers");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the presence of the connection field
|
||||||
|
if (headers.find("connection") == headers.end())
|
||||||
|
{
|
||||||
|
std::string errorMsg("Missing connection value");
|
||||||
|
return WebSocketInitResult(false, status, errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the value of the connection field
|
||||||
|
// Some websocket servers (Go/Gorilla?) send lowercase values for the
|
||||||
|
// connection header, so do a case insensitive comparison
|
||||||
|
if (!insensitiveStringCompare(headers["connection"], "Upgrade"))
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "Invalid connection value: " << headers["connection"];
|
||||||
|
return WebSocketInitResult(false, status, ss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
char output[29] = {};
|
||||||
|
WebSocketHandshakeKeyGen::generate(secWebSocketKey, output);
|
||||||
|
if (std::string(output) != headers["sec-websocket-accept"])
|
||||||
|
{
|
||||||
|
std::string errorMsg("Invalid Sec-WebSocket-Accept value");
|
||||||
|
return WebSocketInitResult(false, status, errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_enablePerMessageDeflate)
|
||||||
|
{
|
||||||
|
// Parse the server response. Does it support deflate ?
|
||||||
|
std::string header = headers["sec-websocket-extensions"];
|
||||||
|
WebSocketPerMessageDeflateOptions webSocketPerMessageDeflateOptions(header);
|
||||||
|
|
||||||
|
// If the server does not support that extension, disable it.
|
||||||
|
if (!webSocketPerMessageDeflateOptions.enabled())
|
||||||
|
{
|
||||||
|
_enablePerMessageDeflate = false;
|
||||||
|
}
|
||||||
|
// Otherwise try to initialize the deflate engine (zlib)
|
||||||
|
else if (!_perMessageDeflate.init(webSocketPerMessageDeflateOptions))
|
||||||
|
{
|
||||||
|
return WebSocketInitResult(
|
||||||
|
false, 0, "Failed to initialize per message deflate engine");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return WebSocketInitResult(true, status, "", headers, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketInitResult WebSocketHandshake::serverHandshake(int fd, int timeoutSecs)
|
||||||
|
{
|
||||||
|
_requestInitCancellation = false;
|
||||||
|
|
||||||
|
// Set the socket to non blocking mode + other tweaks
|
||||||
|
SocketConnect::configure(fd);
|
||||||
|
|
||||||
|
auto isCancellationRequested =
|
||||||
|
makeCancellationRequestWithTimeout(timeoutSecs, _requestInitCancellation);
|
||||||
|
|
||||||
|
std::string remote = std::string("remote fd ") + std::to_string(fd);
|
||||||
|
|
||||||
|
// Read first line
|
||||||
|
auto lineResult = _socket->readLine(isCancellationRequested);
|
||||||
|
auto lineValid = lineResult.first;
|
||||||
|
auto line = lineResult.second;
|
||||||
|
|
||||||
|
if (!lineValid)
|
||||||
|
{
|
||||||
|
return sendErrorResponse(400, "Error reading HTTP request line");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate request line (GET /foo HTTP/1.1\r\n)
|
||||||
|
auto requestLine = Http::parseRequestLine(line);
|
||||||
|
auto method = std::get<0>(requestLine);
|
||||||
|
auto uri = std::get<1>(requestLine);
|
||||||
|
auto httpVersion = std::get<2>(requestLine);
|
||||||
|
|
||||||
|
if (method != "GET")
|
||||||
|
{
|
||||||
|
return sendErrorResponse(400, "Invalid HTTP method, need GET, got " + method);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (httpVersion != "HTTP/1.1")
|
||||||
|
{
|
||||||
|
return sendErrorResponse(400, "Invalid HTTP version, need HTTP/1.1, got: " + httpVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve and validate HTTP headers
|
||||||
|
auto result = parseHttpHeaders(_socket, isCancellationRequested);
|
||||||
|
auto headersValid = result.first;
|
||||||
|
auto headers = result.second;
|
||||||
|
|
||||||
|
if (!headersValid)
|
||||||
|
{
|
||||||
|
return sendErrorResponse(400, "Error parsing HTTP headers");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headers.find("sec-websocket-key") == headers.end())
|
||||||
|
{
|
||||||
|
return sendErrorResponse(400, "Missing Sec-WebSocket-Key value");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headers["upgrade"] != "websocket")
|
||||||
|
{
|
||||||
|
return sendErrorResponse(400, "Invalid or missing Upgrade header");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headers.find("sec-websocket-version") == headers.end())
|
||||||
|
{
|
||||||
|
return sendErrorResponse(400, "Missing Sec-WebSocket-Version value");
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << headers["sec-websocket-version"];
|
||||||
|
int version;
|
||||||
|
ss >> version;
|
||||||
|
|
||||||
|
if (version != 13)
|
||||||
|
{
|
||||||
|
return sendErrorResponse(400, "Invalid Sec-WebSocket-Version, "
|
||||||
|
"need 13, got" + ss.str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
char output[29] = {};
|
||||||
|
WebSocketHandshakeKeyGen::generate(headers["sec-websocket-key"], output);
|
||||||
|
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "HTTP/1.1 101 Switching Protocols\r\n";
|
||||||
|
ss << "Sec-WebSocket-Accept: " << std::string(output) << "\r\n";
|
||||||
|
ss << "Upgrade: websocket\r\n";
|
||||||
|
ss << "Connection: Upgrade\r\n";
|
||||||
|
|
||||||
|
// Parse the client headers. Does it support deflate ?
|
||||||
|
std::string header = headers["sec-websocket-extensions"];
|
||||||
|
WebSocketPerMessageDeflateOptions webSocketPerMessageDeflateOptions(header);
|
||||||
|
|
||||||
|
// If the client has requested that extension, enable it.
|
||||||
|
if (webSocketPerMessageDeflateOptions.enabled())
|
||||||
|
{
|
||||||
|
_enablePerMessageDeflate = true;
|
||||||
|
|
||||||
|
if (!_perMessageDeflate.init(webSocketPerMessageDeflateOptions))
|
||||||
|
{
|
||||||
|
return WebSocketInitResult(
|
||||||
|
false, 0,"Failed to initialize per message deflate engine");
|
||||||
|
}
|
||||||
|
ss << webSocketPerMessageDeflateOptions.generateHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
ss << "\r\n";
|
||||||
|
|
||||||
|
if (!_socket->writeBytes(ss.str(), isCancellationRequested))
|
||||||
|
{
|
||||||
|
return WebSocketInitResult(false, 0, std::string("Failed sending response to ") + remote);
|
||||||
|
}
|
||||||
|
|
||||||
|
return WebSocketInitResult(true, 200, "", headers, uri);
|
||||||
|
}
|
||||||
|
}
|
76
ixwebsocket/IXWebSocketHandshake.h
Normal file
76
ixwebsocket/IXWebSocketHandshake.h
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* IXWebSocketHandshake.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IXCancellationRequest.h"
|
||||||
|
#include "IXSocket.h"
|
||||||
|
#include "IXWebSocketHttpHeaders.h"
|
||||||
|
#include "IXWebSocketPerMessageDeflate.h"
|
||||||
|
#include "IXWebSocketPerMessageDeflateOptions.h"
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
struct WebSocketInitResult
|
||||||
|
{
|
||||||
|
bool success;
|
||||||
|
int http_status;
|
||||||
|
std::string errorStr;
|
||||||
|
WebSocketHttpHeaders headers;
|
||||||
|
std::string uri;
|
||||||
|
|
||||||
|
WebSocketInitResult(bool s = false,
|
||||||
|
int status = 0,
|
||||||
|
const std::string& e = std::string(),
|
||||||
|
WebSocketHttpHeaders h = WebSocketHttpHeaders(),
|
||||||
|
const std::string& u = std::string())
|
||||||
|
{
|
||||||
|
success = s;
|
||||||
|
http_status = status;
|
||||||
|
errorStr = e;
|
||||||
|
headers = h;
|
||||||
|
uri = u;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class WebSocketHandshake
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
WebSocketHandshake(std::atomic<bool>& requestInitCancellation,
|
||||||
|
std::shared_ptr<Socket> _socket,
|
||||||
|
WebSocketPerMessageDeflate& perMessageDeflate,
|
||||||
|
WebSocketPerMessageDeflateOptions& perMessageDeflateOptions,
|
||||||
|
std::atomic<bool>& enablePerMessageDeflate);
|
||||||
|
|
||||||
|
WebSocketInitResult clientHandshake(
|
||||||
|
const std::string& url,
|
||||||
|
const WebSocketHttpHeaders& extraHeaders,
|
||||||
|
const std::string& host,
|
||||||
|
const std::string& path,
|
||||||
|
int port,
|
||||||
|
int timeoutSecs);
|
||||||
|
|
||||||
|
WebSocketInitResult serverHandshake(int fd, int timeoutSecs);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string genRandomString(const int len);
|
||||||
|
|
||||||
|
// Parse HTTP headers
|
||||||
|
WebSocketInitResult sendErrorResponse(int code, const std::string& reason);
|
||||||
|
|
||||||
|
bool insensitiveStringCompare(const std::string& a, const std::string& b);
|
||||||
|
|
||||||
|
std::atomic<bool>& _requestInitCancellation;
|
||||||
|
std::shared_ptr<Socket> _socket;
|
||||||
|
WebSocketPerMessageDeflate& _perMessageDeflate;
|
||||||
|
WebSocketPerMessageDeflateOptions& _perMessageDeflateOptions;
|
||||||
|
std::atomic<bool>& _enablePerMessageDeflate;
|
||||||
|
};
|
||||||
|
} // namespace ix
|
82
ixwebsocket/IXWebSocketHttpHeaders.cpp
Normal file
82
ixwebsocket/IXWebSocketHttpHeaders.cpp
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* IXWebSocketHttpHeaders.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXWebSocketHttpHeaders.h"
|
||||||
|
#include "IXSocket.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <locale>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
bool CaseInsensitiveLess::NocaseCompare::operator()(const unsigned char & c1, const unsigned char & c2) const
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
return std::tolower(c1, std::locale()) < std::tolower(c2, std::locale());
|
||||||
|
#else
|
||||||
|
return std::tolower(c1) < std::tolower(c2);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CaseInsensitiveLess::operator()(const std::string & s1, const std::string & s2) const
|
||||||
|
{
|
||||||
|
return std::lexicographical_compare
|
||||||
|
(s1.begin(), s1.end(), // source range
|
||||||
|
s2.begin(), s2.end(), // dest range
|
||||||
|
NocaseCompare()); // comparison
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<bool, WebSocketHttpHeaders> parseHttpHeaders(
|
||||||
|
std::shared_ptr<Socket> socket,
|
||||||
|
const CancellationRequest& isCancellationRequested)
|
||||||
|
{
|
||||||
|
WebSocketHttpHeaders headers;
|
||||||
|
|
||||||
|
char line[1024];
|
||||||
|
int i;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
int colon = 0;
|
||||||
|
|
||||||
|
for (i = 0;
|
||||||
|
i < 2 || (i < 1023 && line[i-2] != '\r' && line[i-1] != '\n');
|
||||||
|
++i)
|
||||||
|
{
|
||||||
|
if (!socket->readByte(line+i, isCancellationRequested))
|
||||||
|
{
|
||||||
|
return std::make_pair(false, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line[i] == ':' && colon == 0)
|
||||||
|
{
|
||||||
|
colon = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (line[0] == '\r' && line[1] == '\n')
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// line is a single header entry. split by ':', and add it to our
|
||||||
|
// header map. ignore lines with no colon.
|
||||||
|
if (colon > 0)
|
||||||
|
{
|
||||||
|
line[i] = '\0';
|
||||||
|
std::string lineStr(line);
|
||||||
|
// colon is ':', colon+1 is ' ', colon+2 is the start of the value.
|
||||||
|
// i is end of string (\0), i-colon is length of string minus key;
|
||||||
|
// subtract 1 for '\0', 1 for '\n', 1 for '\r',
|
||||||
|
// 1 for the ' ' after the ':', and total is -4
|
||||||
|
std::string name(lineStr.substr(0, colon));
|
||||||
|
std::string value(lineStr.substr(colon + 2, i - colon - 4));
|
||||||
|
|
||||||
|
headers[name] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::make_pair(true, headers);
|
||||||
|
}
|
||||||
|
}
|
33
ixwebsocket/IXWebSocketHttpHeaders.h
Normal file
33
ixwebsocket/IXWebSocketHttpHeaders.h
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* IXWebSocketHttpHeaders.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IXCancellationRequest.h"
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
class Socket;
|
||||||
|
|
||||||
|
struct CaseInsensitiveLess
|
||||||
|
{
|
||||||
|
// Case Insensitive compare_less binary function
|
||||||
|
struct NocaseCompare
|
||||||
|
{
|
||||||
|
bool operator()(const unsigned char& c1, const unsigned char& c2) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool operator()(const std::string& s1, const std::string& s2) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
using WebSocketHttpHeaders = std::map<std::string, std::string, CaseInsensitiveLess>;
|
||||||
|
|
||||||
|
std::pair<bool, WebSocketHttpHeaders> parseHttpHeaders(
|
||||||
|
std::shared_ptr<Socket> socket, const CancellationRequest& isCancellationRequested);
|
||||||
|
} // namespace ix
|
49
ixwebsocket/IXWebSocketMessage.h
Normal file
49
ixwebsocket/IXWebSocketMessage.h
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* IXWebSocketMessage.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2017-2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IXWebSocketCloseInfo.h"
|
||||||
|
#include "IXWebSocketErrorInfo.h"
|
||||||
|
#include "IXWebSocketMessageType.h"
|
||||||
|
#include "IXWebSocketOpenInfo.h"
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
struct WebSocketMessage
|
||||||
|
{
|
||||||
|
WebSocketMessageType type;
|
||||||
|
std::string str;
|
||||||
|
size_t wireSize;
|
||||||
|
WebSocketErrorInfo errorInfo;
|
||||||
|
WebSocketOpenInfo openInfo;
|
||||||
|
WebSocketCloseInfo closeInfo;
|
||||||
|
bool binary;
|
||||||
|
|
||||||
|
WebSocketMessage(WebSocketMessageType t,
|
||||||
|
const std::string& s,
|
||||||
|
size_t w,
|
||||||
|
WebSocketErrorInfo e,
|
||||||
|
WebSocketOpenInfo o,
|
||||||
|
WebSocketCloseInfo c,
|
||||||
|
bool b = false)
|
||||||
|
: type(t)
|
||||||
|
, str(std::move(s))
|
||||||
|
, wireSize(w)
|
||||||
|
, errorInfo(e)
|
||||||
|
, openInfo(o)
|
||||||
|
, closeInfo(c)
|
||||||
|
, binary(b)
|
||||||
|
{
|
||||||
|
;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using WebSocketMessagePtr = std::shared_ptr<WebSocketMessage>;
|
||||||
|
} // namespace ix
|
89
ixwebsocket/IXWebSocketMessageQueue.cpp
Normal file
89
ixwebsocket/IXWebSocketMessageQueue.cpp
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* IXWebSocketMessageQueue.cpp
|
||||||
|
* Author: Korchynskyi Dmytro
|
||||||
|
* Copyright (c) 2017-2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXWebSocketMessageQueue.h"
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
|
||||||
|
WebSocketMessageQueue::WebSocketMessageQueue(WebSocket* websocket)
|
||||||
|
{
|
||||||
|
bindWebsocket(websocket);
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketMessageQueue::~WebSocketMessageQueue()
|
||||||
|
{
|
||||||
|
if (!_messages.empty())
|
||||||
|
{
|
||||||
|
// not handled all messages
|
||||||
|
}
|
||||||
|
|
||||||
|
bindWebsocket(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocketMessageQueue::bindWebsocket(WebSocket * websocket)
|
||||||
|
{
|
||||||
|
if (_websocket == websocket) return;
|
||||||
|
|
||||||
|
// unbind old
|
||||||
|
if (_websocket)
|
||||||
|
{
|
||||||
|
// set dummy callback just to avoid crash
|
||||||
|
_websocket->setOnMessageCallback([](const WebSocketMessagePtr&) {});
|
||||||
|
}
|
||||||
|
|
||||||
|
_websocket = websocket;
|
||||||
|
|
||||||
|
// bind new
|
||||||
|
if (_websocket)
|
||||||
|
{
|
||||||
|
_websocket->setOnMessageCallback([this](const WebSocketMessagePtr& msg)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_messagesMutex);
|
||||||
|
_messages.emplace_back(std::move(msg));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocketMessageQueue::setOnMessageCallback(const OnMessageCallback& callback)
|
||||||
|
{
|
||||||
|
_onMessageUserCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocketMessageQueue::setOnMessageCallback(OnMessageCallback&& callback)
|
||||||
|
{
|
||||||
|
_onMessageUserCallback = std::move(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketMessagePtr WebSocketMessageQueue::popMessage()
|
||||||
|
{
|
||||||
|
WebSocketMessagePtr message;
|
||||||
|
std::lock_guard<std::mutex> lock(_messagesMutex);
|
||||||
|
|
||||||
|
if (!_messages.empty())
|
||||||
|
{
|
||||||
|
message = std::move(_messages.front());
|
||||||
|
_messages.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocketMessageQueue::poll(int count)
|
||||||
|
{
|
||||||
|
if (!_onMessageUserCallback)
|
||||||
|
return;
|
||||||
|
|
||||||
|
WebSocketMessagePtr message;
|
||||||
|
|
||||||
|
while (count > 0 && (message = popMessage()))
|
||||||
|
{
|
||||||
|
_onMessageUserCallback(message);
|
||||||
|
--count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
41
ixwebsocket/IXWebSocketMessageQueue.h
Normal file
41
ixwebsocket/IXWebSocketMessageQueue.h
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* IXWebSocketMessageQueue.h
|
||||||
|
* Author: Korchynskyi Dmytro
|
||||||
|
* Copyright (c) 2017-2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IXWebSocket.h"
|
||||||
|
#include <list>
|
||||||
|
#include <memory>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// A helper class to dispatch websocket message callbacks in your thread.
|
||||||
|
//
|
||||||
|
class WebSocketMessageQueue
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
WebSocketMessageQueue(WebSocket* websocket = nullptr);
|
||||||
|
~WebSocketMessageQueue();
|
||||||
|
|
||||||
|
void bindWebsocket(WebSocket* websocket);
|
||||||
|
|
||||||
|
void setOnMessageCallback(const OnMessageCallback& callback);
|
||||||
|
void setOnMessageCallback(OnMessageCallback&& callback);
|
||||||
|
|
||||||
|
void poll(int count = 512);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
WebSocketMessagePtr popMessage();
|
||||||
|
|
||||||
|
private:
|
||||||
|
WebSocket* _websocket = nullptr;
|
||||||
|
OnMessageCallback _onMessageUserCallback;
|
||||||
|
std::mutex _messagesMutex;
|
||||||
|
std::list<WebSocketMessagePtr> _messages;
|
||||||
|
};
|
||||||
|
} // namespace ix
|
21
ixwebsocket/IXWebSocketMessageType.h
Normal file
21
ixwebsocket/IXWebSocketMessageType.h
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* IXWebSocketMessageType.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2017-2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
enum class WebSocketMessageType
|
||||||
|
{
|
||||||
|
Message = 0,
|
||||||
|
Open = 1,
|
||||||
|
Close = 2,
|
||||||
|
Error = 3,
|
||||||
|
Ping = 4,
|
||||||
|
Pong = 5,
|
||||||
|
Fragment = 6
|
||||||
|
};
|
||||||
|
}
|
24
ixwebsocket/IXWebSocketOpenInfo.h
Normal file
24
ixwebsocket/IXWebSocketOpenInfo.h
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* IXWebSocketOpenInfo.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2017-2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
struct WebSocketOpenInfo
|
||||||
|
{
|
||||||
|
std::string uri;
|
||||||
|
WebSocketHttpHeaders headers;
|
||||||
|
|
||||||
|
WebSocketOpenInfo(const std::string& u = std::string(),
|
||||||
|
const WebSocketHttpHeaders& h = WebSocketHttpHeaders())
|
||||||
|
: uri(u)
|
||||||
|
, headers(h)
|
||||||
|
{
|
||||||
|
;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // namespace ix
|
90
ixwebsocket/IXWebSocketPerMessageDeflate.cpp
Normal file
90
ixwebsocket/IXWebSocketPerMessageDeflate.cpp
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2015, Peter Thorson. All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
* * Redistributions of source code must retain the above copyright
|
||||||
|
* notice, this list of conditions and the following disclaimer.
|
||||||
|
* * Redistributions in binary form must reproduce the above copyright
|
||||||
|
* notice, this list of conditions and the following disclaimer in the
|
||||||
|
* documentation and/or other materials provided with the distribution.
|
||||||
|
* * Neither the name of the WebSocket++ Project nor the
|
||||||
|
* names of its contributors may be used to endorse or promote products
|
||||||
|
* derived from this software without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL PETER THORSON BE LIABLE FOR ANY
|
||||||
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* Adapted from websocketpp/extensions/permessage_deflate/enabled.hpp
|
||||||
|
* (same license as MZ: https://opensource.org/licenses/BSD-3-Clause)
|
||||||
|
*
|
||||||
|
* - Reused zlib compression + decompression bits.
|
||||||
|
* - Refactored to have 2 class for compression and decompression, to allow multi-threading
|
||||||
|
* and make sure that _compressBuffer is not shared between threads.
|
||||||
|
* - Original code wasn't working for some reason, I had to add checks
|
||||||
|
* for the presence of the kEmptyUncompressedBlock at the end of buffer so that servers
|
||||||
|
* would start accepting receiving/decoding compressed messages. Original code was probably
|
||||||
|
* modifying the passed in buffers before processing in enabled.hpp ?
|
||||||
|
* - Added more documentation.
|
||||||
|
*
|
||||||
|
* Per message Deflate RFC: https://tools.ietf.org/html/rfc7692
|
||||||
|
* Chrome websocket -> https://github.com/chromium/chromium/tree/2ca8c5037021c9d2ecc00b787d58a31ed8fc8bcb/net/websockets
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXWebSocketPerMessageDeflate.h"
|
||||||
|
#include "IXWebSocketPerMessageDeflateOptions.h"
|
||||||
|
#include "IXWebSocketPerMessageDeflateCodec.h"
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
WebSocketPerMessageDeflate::WebSocketPerMessageDeflate() :
|
||||||
|
_compressor(std::make_unique<WebSocketPerMessageDeflateCompressor>()),
|
||||||
|
_decompressor(std::make_unique<WebSocketPerMessageDeflateDecompressor>())
|
||||||
|
{
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketPerMessageDeflate::~WebSocketPerMessageDeflate()
|
||||||
|
{
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebSocketPerMessageDeflate::init(const WebSocketPerMessageDeflateOptions& perMessageDeflateOptions)
|
||||||
|
{
|
||||||
|
bool clientNoContextTakeover =
|
||||||
|
perMessageDeflateOptions.getClientNoContextTakeover();
|
||||||
|
|
||||||
|
uint8_t deflateBits = perMessageDeflateOptions.getClientMaxWindowBits();
|
||||||
|
uint8_t inflateBits = perMessageDeflateOptions.getServerMaxWindowBits();
|
||||||
|
|
||||||
|
return _compressor->init(deflateBits, clientNoContextTakeover) &&
|
||||||
|
_decompressor->init(inflateBits, clientNoContextTakeover);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebSocketPerMessageDeflate::compress(const std::string& in,
|
||||||
|
std::string& out)
|
||||||
|
{
|
||||||
|
return _compressor->compress(in, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebSocketPerMessageDeflate::decompress(const std::string& in,
|
||||||
|
std::string &out)
|
||||||
|
{
|
||||||
|
return _decompressor->decompress(in, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
60
ixwebsocket/IXWebSocketPerMessageDeflate.h
Normal file
60
ixwebsocket/IXWebSocketPerMessageDeflate.h
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2015, Peter Thorson. All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
* * Redistributions of source code must retain the above copyright
|
||||||
|
* notice, this list of conditions and the following disclaimer.
|
||||||
|
* * Redistributions in binary form must reproduce the above copyright
|
||||||
|
* notice, this list of conditions and the following disclaimer in the
|
||||||
|
* documentation and/or other materials provided with the distribution.
|
||||||
|
* * Neither the name of the WebSocket++ Project nor the
|
||||||
|
* names of its contributors may be used to endorse or promote products
|
||||||
|
* derived from this software without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL PETER THORSON BE LIABLE FOR ANY
|
||||||
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* Adapted from websocketpp/extensions/permessage_deflate/enabled.hpp
|
||||||
|
* (same license as MZ: https://opensource.org/licenses/BSD-3-Clause)
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
class WebSocketPerMessageDeflateOptions;
|
||||||
|
class WebSocketPerMessageDeflateCompressor;
|
||||||
|
class WebSocketPerMessageDeflateDecompressor;
|
||||||
|
|
||||||
|
class WebSocketPerMessageDeflate
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
WebSocketPerMessageDeflate();
|
||||||
|
~WebSocketPerMessageDeflate();
|
||||||
|
|
||||||
|
bool init(const WebSocketPerMessageDeflateOptions& perMessageDeflateOptions);
|
||||||
|
bool compress(const std::string& in, std::string& out);
|
||||||
|
bool decompress(const std::string& in, std::string& out);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<WebSocketPerMessageDeflateCompressor> _compressor;
|
||||||
|
std::unique_ptr<WebSocketPerMessageDeflateDecompressor> _decompressor;
|
||||||
|
};
|
||||||
|
} // namespace ix
|
205
ixwebsocket/IXWebSocketPerMessageDeflateCodec.cpp
Normal file
205
ixwebsocket/IXWebSocketPerMessageDeflateCodec.cpp
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
/*
|
||||||
|
* IXWebSocketPerMessageDeflateCodec.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018-2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXWebSocketPerMessageDeflateCodec.h"
|
||||||
|
#include "IXWebSocketPerMessageDeflateOptions.h"
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// The passed in size (4) is important, without it the string litteral
|
||||||
|
// is treated as a char* and the null termination (\x00) makes it
|
||||||
|
// look like an empty string.
|
||||||
|
const std::string kEmptyUncompressedBlock = std::string("\x00\x00\xff\xff", 4);
|
||||||
|
|
||||||
|
const int kBufferSize = 1 << 14;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Compressor
|
||||||
|
//
|
||||||
|
WebSocketPerMessageDeflateCompressor::WebSocketPerMessageDeflateCompressor()
|
||||||
|
: _compressBufferSize(kBufferSize)
|
||||||
|
{
|
||||||
|
memset(&_deflateState, 0, sizeof(_deflateState));
|
||||||
|
|
||||||
|
_deflateState.zalloc = Z_NULL;
|
||||||
|
_deflateState.zfree = Z_NULL;
|
||||||
|
_deflateState.opaque = Z_NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketPerMessageDeflateCompressor::~WebSocketPerMessageDeflateCompressor()
|
||||||
|
{
|
||||||
|
deflateEnd(&_deflateState);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebSocketPerMessageDeflateCompressor::init(uint8_t deflateBits,
|
||||||
|
bool clientNoContextTakeOver)
|
||||||
|
{
|
||||||
|
int ret = deflateInit2(
|
||||||
|
&_deflateState,
|
||||||
|
Z_DEFAULT_COMPRESSION,
|
||||||
|
Z_DEFLATED,
|
||||||
|
-1*deflateBits,
|
||||||
|
4, // memory level 1-9
|
||||||
|
Z_DEFAULT_STRATEGY
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ret != Z_OK) return false;
|
||||||
|
|
||||||
|
_compressBuffer = std::make_unique<unsigned char[]>(_compressBufferSize);
|
||||||
|
|
||||||
|
_flush = (clientNoContextTakeOver)
|
||||||
|
? Z_FULL_FLUSH
|
||||||
|
: Z_SYNC_FLUSH;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebSocketPerMessageDeflateCompressor::endsWith(const std::string& value,
|
||||||
|
const std::string& ending)
|
||||||
|
{
|
||||||
|
if (ending.size() > value.size()) return false;
|
||||||
|
return std::equal(ending.rbegin(), ending.rend(), value.rbegin());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebSocketPerMessageDeflateCompressor::compress(const std::string& in,
|
||||||
|
std::string& out)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// 7.2.1. Compression
|
||||||
|
//
|
||||||
|
// An endpoint uses the following algorithm to compress a message.
|
||||||
|
//
|
||||||
|
// 1. Compress all the octets of the payload of the message using
|
||||||
|
// DEFLATE.
|
||||||
|
//
|
||||||
|
// 2. If the resulting data does not end with an empty DEFLATE block
|
||||||
|
// with no compression (the "BTYPE" bits are set to 00), append an
|
||||||
|
// empty DEFLATE block with no compression to the tail end.
|
||||||
|
//
|
||||||
|
// 3. Remove 4 octets (that are 0x00 0x00 0xff 0xff) from the tail end.
|
||||||
|
// After this step, the last octet of the compressed data contains
|
||||||
|
// (possibly part of) the DEFLATE header bits with the "BTYPE" bits
|
||||||
|
// set to 00.
|
||||||
|
//
|
||||||
|
size_t output;
|
||||||
|
|
||||||
|
if (in.empty())
|
||||||
|
{
|
||||||
|
uint8_t buf[6] = {0x02, 0x00, 0x00, 0x00, 0xff, 0xff};
|
||||||
|
out.append((char *)(buf), 6);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_deflateState.avail_in = (uInt) in.size();
|
||||||
|
_deflateState.next_in = (Bytef*) in.data();
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
// Output to local buffer
|
||||||
|
_deflateState.avail_out = (uInt) _compressBufferSize;
|
||||||
|
_deflateState.next_out = _compressBuffer.get();
|
||||||
|
|
||||||
|
deflate(&_deflateState, _flush);
|
||||||
|
|
||||||
|
output = _compressBufferSize - _deflateState.avail_out;
|
||||||
|
|
||||||
|
out.append((char *)(_compressBuffer.get()),output);
|
||||||
|
} while (_deflateState.avail_out == 0);
|
||||||
|
|
||||||
|
if (endsWith(out, kEmptyUncompressedBlock))
|
||||||
|
{
|
||||||
|
out.resize(out.size() - 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Decompressor
|
||||||
|
//
|
||||||
|
WebSocketPerMessageDeflateDecompressor::WebSocketPerMessageDeflateDecompressor()
|
||||||
|
: _compressBufferSize(kBufferSize)
|
||||||
|
{
|
||||||
|
memset(&_inflateState, 0, sizeof(_inflateState));
|
||||||
|
|
||||||
|
_inflateState.zalloc = Z_NULL;
|
||||||
|
_inflateState.zfree = Z_NULL;
|
||||||
|
_inflateState.opaque = Z_NULL;
|
||||||
|
_inflateState.avail_in = 0;
|
||||||
|
_inflateState.next_in = Z_NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketPerMessageDeflateDecompressor::~WebSocketPerMessageDeflateDecompressor()
|
||||||
|
{
|
||||||
|
inflateEnd(&_inflateState);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebSocketPerMessageDeflateDecompressor::init(uint8_t inflateBits,
|
||||||
|
bool clientNoContextTakeOver)
|
||||||
|
{
|
||||||
|
int ret = inflateInit2(
|
||||||
|
&_inflateState,
|
||||||
|
-1*inflateBits
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ret != Z_OK) return false;
|
||||||
|
|
||||||
|
_compressBuffer = std::make_unique<unsigned char[]>(_compressBufferSize);
|
||||||
|
|
||||||
|
_flush = (clientNoContextTakeOver)
|
||||||
|
? Z_FULL_FLUSH
|
||||||
|
: Z_SYNC_FLUSH;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebSocketPerMessageDeflateDecompressor::decompress(const std::string& in,
|
||||||
|
std::string& out)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// 7.2.2. Decompression
|
||||||
|
//
|
||||||
|
// An endpoint uses the following algorithm to decompress a message.
|
||||||
|
//
|
||||||
|
// 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the
|
||||||
|
// payload of the message.
|
||||||
|
//
|
||||||
|
// 2. Decompress the resulting data using DEFLATE.
|
||||||
|
//
|
||||||
|
std::string inFixed(in);
|
||||||
|
inFixed += kEmptyUncompressedBlock;
|
||||||
|
|
||||||
|
_inflateState.avail_in = (uInt) inFixed.size();
|
||||||
|
_inflateState.next_in = (unsigned char *)(const_cast<char *>(inFixed.data()));
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
_inflateState.avail_out = (uInt) _compressBufferSize;
|
||||||
|
_inflateState.next_out = _compressBuffer.get();
|
||||||
|
|
||||||
|
int ret = inflate(&_inflateState, Z_SYNC_FLUSH);
|
||||||
|
|
||||||
|
if (ret == Z_NEED_DICT || ret == Z_DATA_ERROR || ret == Z_MEM_ERROR)
|
||||||
|
{
|
||||||
|
return false; // zlib error
|
||||||
|
}
|
||||||
|
|
||||||
|
out.append(
|
||||||
|
reinterpret_cast<char *>(_compressBuffer.get()),
|
||||||
|
_compressBufferSize - _inflateState.avail_out
|
||||||
|
);
|
||||||
|
} while (_inflateState.avail_out == 0);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
49
ixwebsocket/IXWebSocketPerMessageDeflateCodec.h
Normal file
49
ixwebsocket/IXWebSocketPerMessageDeflateCodec.h
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* IXWebSocketPerMessageDeflateCodec.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018-2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "zlib.h"
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
class WebSocketPerMessageDeflateCompressor
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
WebSocketPerMessageDeflateCompressor();
|
||||||
|
~WebSocketPerMessageDeflateCompressor();
|
||||||
|
|
||||||
|
bool init(uint8_t deflateBits, bool clientNoContextTakeOver);
|
||||||
|
bool compress(const std::string& in, std::string& out);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static bool endsWith(const std::string& value, const std::string& ending);
|
||||||
|
|
||||||
|
int _flush;
|
||||||
|
size_t _compressBufferSize;
|
||||||
|
std::unique_ptr<unsigned char[]> _compressBuffer;
|
||||||
|
z_stream _deflateState;
|
||||||
|
};
|
||||||
|
|
||||||
|
class WebSocketPerMessageDeflateDecompressor
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
WebSocketPerMessageDeflateDecompressor();
|
||||||
|
~WebSocketPerMessageDeflateDecompressor();
|
||||||
|
|
||||||
|
bool init(uint8_t inflateBits, bool clientNoContextTakeOver);
|
||||||
|
bool decompress(const std::string& in, std::string& out);
|
||||||
|
|
||||||
|
private:
|
||||||
|
int _flush;
|
||||||
|
size_t _compressBufferSize;
|
||||||
|
std::unique_ptr<unsigned char[]> _compressBuffer;
|
||||||
|
z_stream _inflateState;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ix
|
171
ixwebsocket/IXWebSocketPerMessageDeflateOptions.cpp
Normal file
171
ixwebsocket/IXWebSocketPerMessageDeflateOptions.cpp
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
/*
|
||||||
|
* IXWebSocketPerMessageDeflateOptions.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXWebSocketPerMessageDeflateOptions.h"
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
/// Default values as defined in the RFC
|
||||||
|
const uint8_t WebSocketPerMessageDeflateOptions::kDefaultServerMaxWindowBits = 15;
|
||||||
|
static const int minServerMaxWindowBits = 8;
|
||||||
|
static const int maxServerMaxWindowBits = 15;
|
||||||
|
|
||||||
|
const uint8_t WebSocketPerMessageDeflateOptions::kDefaultClientMaxWindowBits = 15;
|
||||||
|
static const int minClientMaxWindowBits = 8;
|
||||||
|
static const int maxClientMaxWindowBits = 15;
|
||||||
|
|
||||||
|
WebSocketPerMessageDeflateOptions::WebSocketPerMessageDeflateOptions(
|
||||||
|
bool enabled,
|
||||||
|
bool clientNoContextTakeover,
|
||||||
|
bool serverNoContextTakeover,
|
||||||
|
uint8_t clientMaxWindowBits,
|
||||||
|
uint8_t serverMaxWindowBits)
|
||||||
|
{
|
||||||
|
_enabled = enabled;
|
||||||
|
_clientNoContextTakeover = clientNoContextTakeover;
|
||||||
|
_serverNoContextTakeover = serverNoContextTakeover;
|
||||||
|
_clientMaxWindowBits = clientMaxWindowBits;
|
||||||
|
_serverMaxWindowBits = serverMaxWindowBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Four extension parameters are defined for "permessage-deflate" to
|
||||||
|
// help endpoints manage per-connection resource usage.
|
||||||
|
//
|
||||||
|
// - "server_no_context_takeover"
|
||||||
|
// - "client_no_context_takeover"
|
||||||
|
// - "server_max_window_bits"
|
||||||
|
// - "client_max_window_bits"
|
||||||
|
//
|
||||||
|
// Server response could look like that:
|
||||||
|
//
|
||||||
|
// Sec-WebSocket-Extensions: permessage-deflate; client_no_context_takeover; server_no_context_takeover
|
||||||
|
//
|
||||||
|
WebSocketPerMessageDeflateOptions::WebSocketPerMessageDeflateOptions(std::string extension)
|
||||||
|
{
|
||||||
|
extension = removeSpaces(extension);
|
||||||
|
|
||||||
|
_enabled = false;
|
||||||
|
_clientNoContextTakeover = false;
|
||||||
|
_serverNoContextTakeover = false;
|
||||||
|
_clientMaxWindowBits = kDefaultClientMaxWindowBits;
|
||||||
|
_serverMaxWindowBits = kDefaultServerMaxWindowBits;
|
||||||
|
|
||||||
|
// Split by ;
|
||||||
|
std::string token;
|
||||||
|
std::stringstream tokenStream(extension);
|
||||||
|
|
||||||
|
while (std::getline(tokenStream, token, ';'))
|
||||||
|
{
|
||||||
|
if (token == "permessage-deflate")
|
||||||
|
{
|
||||||
|
_enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token == "server_no_context_takeover")
|
||||||
|
{
|
||||||
|
_serverNoContextTakeover = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token == "client_no_context_takeover")
|
||||||
|
{
|
||||||
|
_clientNoContextTakeover = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startsWith(token, "server_max_window_bits="))
|
||||||
|
{
|
||||||
|
std::string val = token.substr(token.find_last_of("=") + 1);
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << val;
|
||||||
|
int x;
|
||||||
|
ss >> x;
|
||||||
|
|
||||||
|
// Sanitize values to be in the proper range [8, 15] in
|
||||||
|
// case a server would give us bogus values
|
||||||
|
_serverMaxWindowBits =
|
||||||
|
std::min(maxServerMaxWindowBits,
|
||||||
|
std::max(x, minServerMaxWindowBits));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startsWith(token, "client_max_window_bits="))
|
||||||
|
{
|
||||||
|
std::string val = token.substr(token.find_last_of("=") + 1);
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << val;
|
||||||
|
int x;
|
||||||
|
ss >> x;
|
||||||
|
|
||||||
|
// Sanitize values to be in the proper range [8, 15] in
|
||||||
|
// case a server would give us bogus values
|
||||||
|
_clientMaxWindowBits =
|
||||||
|
std::min(maxClientMaxWindowBits,
|
||||||
|
std::max(x, minClientMaxWindowBits));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string WebSocketPerMessageDeflateOptions::generateHeader()
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "Sec-WebSocket-Extensions: permessage-deflate";
|
||||||
|
|
||||||
|
if (_clientNoContextTakeover) ss << "; client_no_context_takeover";
|
||||||
|
if (_serverNoContextTakeover) ss << "; server_no_context_takeover";
|
||||||
|
|
||||||
|
ss << "; server_max_window_bits=" << _serverMaxWindowBits;
|
||||||
|
ss << "; client_max_window_bits=" << _clientMaxWindowBits;
|
||||||
|
|
||||||
|
ss << "\r\n";
|
||||||
|
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebSocketPerMessageDeflateOptions::enabled() const
|
||||||
|
{
|
||||||
|
return _enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebSocketPerMessageDeflateOptions::getClientNoContextTakeover() const
|
||||||
|
{
|
||||||
|
return _clientNoContextTakeover;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebSocketPerMessageDeflateOptions::getServerNoContextTakeover() const
|
||||||
|
{
|
||||||
|
return _serverNoContextTakeover;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t WebSocketPerMessageDeflateOptions::getClientMaxWindowBits() const
|
||||||
|
{
|
||||||
|
return _clientMaxWindowBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t WebSocketPerMessageDeflateOptions::getServerMaxWindowBits() const
|
||||||
|
{
|
||||||
|
return _serverMaxWindowBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebSocketPerMessageDeflateOptions::startsWith(const std::string& str,
|
||||||
|
const std::string& start)
|
||||||
|
{
|
||||||
|
return str.compare(0, start.length(), start) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string WebSocketPerMessageDeflateOptions::removeSpaces(const std::string& str)
|
||||||
|
{
|
||||||
|
std::string out(str);
|
||||||
|
out.erase(std::remove_if(out.begin(),
|
||||||
|
out.end(),
|
||||||
|
[](unsigned char x){ return std::isspace(x); }),
|
||||||
|
out.end());
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
45
ixwebsocket/IXWebSocketPerMessageDeflateOptions.h
Normal file
45
ixwebsocket/IXWebSocketPerMessageDeflateOptions.h
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* IXWebSocketPerMessageDeflateOptions.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
class WebSocketPerMessageDeflateOptions
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
WebSocketPerMessageDeflateOptions(
|
||||||
|
bool enabled = false,
|
||||||
|
bool clientNoContextTakeover = false,
|
||||||
|
bool serverNoContextTakeover = false,
|
||||||
|
uint8_t clientMaxWindowBits = kDefaultClientMaxWindowBits,
|
||||||
|
uint8_t serverMaxWindowBits = kDefaultServerMaxWindowBits);
|
||||||
|
|
||||||
|
WebSocketPerMessageDeflateOptions(std::string extension);
|
||||||
|
|
||||||
|
std::string generateHeader();
|
||||||
|
bool enabled() const;
|
||||||
|
bool getClientNoContextTakeover() const;
|
||||||
|
bool getServerNoContextTakeover() const;
|
||||||
|
uint8_t getServerMaxWindowBits() const;
|
||||||
|
uint8_t getClientMaxWindowBits() const;
|
||||||
|
|
||||||
|
static bool startsWith(const std::string& str, const std::string& start);
|
||||||
|
static std::string removeSpaces(const std::string& str);
|
||||||
|
|
||||||
|
static uint8_t const kDefaultClientMaxWindowBits;
|
||||||
|
static uint8_t const kDefaultServerMaxWindowBits;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool _enabled;
|
||||||
|
bool _clientNoContextTakeover;
|
||||||
|
bool _serverNoContextTakeover;
|
||||||
|
int _clientMaxWindowBits;
|
||||||
|
int _serverMaxWindowBits;
|
||||||
|
};
|
||||||
|
} // namespace ix
|
27
ixwebsocket/IXWebSocketSendInfo.h
Normal file
27
ixwebsocket/IXWebSocketSendInfo.h
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* IXWebSocketSendInfo.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
struct WebSocketSendInfo
|
||||||
|
{
|
||||||
|
bool success;
|
||||||
|
bool compressionError;
|
||||||
|
size_t payloadSize;
|
||||||
|
size_t wireSize;
|
||||||
|
|
||||||
|
WebSocketSendInfo(bool s = false, bool c = false, size_t p = 0, size_t w = 0)
|
||||||
|
: success(s)
|
||||||
|
, compressionError(c)
|
||||||
|
, payloadSize(p)
|
||||||
|
, wireSize(w)
|
||||||
|
{
|
||||||
|
;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // namespace ix
|
127
ixwebsocket/IXWebSocketServer.cpp
Normal file
127
ixwebsocket/IXWebSocketServer.cpp
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
* IXWebSocketServer.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXWebSocketServer.h"
|
||||||
|
#include "IXWebSocketTransport.h"
|
||||||
|
#include "IXWebSocket.h"
|
||||||
|
#include "IXSocketConnect.h"
|
||||||
|
#include "IXNetSystem.h"
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
|
#include <future>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
const int WebSocketServer::kDefaultHandShakeTimeoutSecs(3); // 3 seconds
|
||||||
|
const bool WebSocketServer::kDefaultEnablePong(true);
|
||||||
|
|
||||||
|
WebSocketServer::WebSocketServer(int port,
|
||||||
|
const std::string& host,
|
||||||
|
int backlog,
|
||||||
|
size_t maxConnections,
|
||||||
|
int handshakeTimeoutSecs) : SocketServer(port, host, backlog, maxConnections),
|
||||||
|
_handshakeTimeoutSecs(handshakeTimeoutSecs),
|
||||||
|
_enablePong(kDefaultEnablePong)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketServer::~WebSocketServer()
|
||||||
|
{
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocketServer::stop()
|
||||||
|
{
|
||||||
|
stopAcceptingConnections();
|
||||||
|
|
||||||
|
auto clients = getClients();
|
||||||
|
for (auto client : clients)
|
||||||
|
{
|
||||||
|
client->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
SocketServer::stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocketServer::enablePong()
|
||||||
|
{
|
||||||
|
_enablePong = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocketServer::disablePong()
|
||||||
|
{
|
||||||
|
_enablePong = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocketServer::setOnConnectionCallback(const OnConnectionCallback& callback)
|
||||||
|
{
|
||||||
|
_onConnectionCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocketServer::handleConnection(
|
||||||
|
int fd,
|
||||||
|
std::shared_ptr<ConnectionState> connectionState)
|
||||||
|
{
|
||||||
|
auto webSocket = std::make_shared<WebSocket>();
|
||||||
|
_onConnectionCallback(webSocket, connectionState);
|
||||||
|
|
||||||
|
webSocket->disableAutomaticReconnection();
|
||||||
|
|
||||||
|
if (_enablePong)
|
||||||
|
webSocket->enablePong();
|
||||||
|
else
|
||||||
|
webSocket->disablePong();
|
||||||
|
|
||||||
|
// Add this client to our client set
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_clientsMutex);
|
||||||
|
_clients.insert(webSocket);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto status = webSocket->connectToSocket(fd, _handshakeTimeoutSecs);
|
||||||
|
if (status.success)
|
||||||
|
{
|
||||||
|
// Process incoming messages and execute callbacks
|
||||||
|
// until the connection is closed
|
||||||
|
webSocket->run();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "WebSocketServer::handleConnection() error: "
|
||||||
|
<< status.http_status
|
||||||
|
<< " error: "
|
||||||
|
<< status.errorStr;
|
||||||
|
logError(ss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove this client from our client set
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_clientsMutex);
|
||||||
|
if (_clients.erase(webSocket) != 1)
|
||||||
|
{
|
||||||
|
logError("Cannot delete client");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logInfo("WebSocketServer::handleConnection() done");
|
||||||
|
connectionState->setTerminated();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<std::shared_ptr<WebSocket>> WebSocketServer::getClients()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_clientsMutex);
|
||||||
|
return _clients;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t WebSocketServer::getConnectedClientsCount()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_clientsMutex);
|
||||||
|
return _clients.size();
|
||||||
|
}
|
||||||
|
}
|
62
ixwebsocket/IXWebSocketServer.h
Normal file
62
ixwebsocket/IXWebSocketServer.h
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* IXWebSocketServer.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IXSocketServer.h"
|
||||||
|
#include "IXWebSocket.h"
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <utility> // pair
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
class WebSocketServer final : public SocketServer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using OnConnectionCallback =
|
||||||
|
std::function<void(std::shared_ptr<WebSocket>, std::shared_ptr<ConnectionState>)>;
|
||||||
|
|
||||||
|
WebSocketServer(int port = SocketServer::kDefaultPort,
|
||||||
|
const std::string& host = SocketServer::kDefaultHost,
|
||||||
|
int backlog = SocketServer::kDefaultTcpBacklog,
|
||||||
|
size_t maxConnections = SocketServer::kDefaultMaxConnections,
|
||||||
|
int handshakeTimeoutSecs = WebSocketServer::kDefaultHandShakeTimeoutSecs);
|
||||||
|
virtual ~WebSocketServer();
|
||||||
|
virtual void stop() final;
|
||||||
|
|
||||||
|
void enablePong();
|
||||||
|
void disablePong();
|
||||||
|
|
||||||
|
void setOnConnectionCallback(const OnConnectionCallback& callback);
|
||||||
|
|
||||||
|
// Get all the connected clients
|
||||||
|
std::set<std::shared_ptr<WebSocket>> getClients();
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Member variables
|
||||||
|
int _handshakeTimeoutSecs;
|
||||||
|
bool _enablePong;
|
||||||
|
|
||||||
|
OnConnectionCallback _onConnectionCallback;
|
||||||
|
|
||||||
|
std::mutex _clientsMutex;
|
||||||
|
std::set<std::shared_ptr<WebSocket>> _clients;
|
||||||
|
|
||||||
|
const static int kDefaultHandShakeTimeoutSecs;
|
||||||
|
const static bool kDefaultEnablePong;
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
virtual void handleConnection(int fd,
|
||||||
|
std::shared_ptr<ConnectionState> connectionState) final;
|
||||||
|
virtual size_t getConnectedClientsCount() final;
|
||||||
|
};
|
||||||
|
} // namespace ix
|
File diff suppressed because it is too large
Load Diff
@ -10,43 +10,38 @@
|
|||||||
// Adapted from https://github.com/dhbaird/easywsclient
|
// Adapted from https://github.com/dhbaird/easywsclient
|
||||||
//
|
//
|
||||||
|
|
||||||
#include <string>
|
#include "IXCancellationRequest.h"
|
||||||
#include <vector>
|
#include "IXProgressCallback.h"
|
||||||
|
#include "IXWebSocketCloseConstants.h"
|
||||||
|
#include "IXWebSocketHandshake.h"
|
||||||
|
#include "IXWebSocketHttpHeaders.h"
|
||||||
|
#include "IXWebSocketPerMessageDeflate.h"
|
||||||
|
#include "IXWebSocketPerMessageDeflateOptions.h"
|
||||||
|
#include "IXWebSocketSendInfo.h"
|
||||||
|
#include <atomic>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <list>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <atomic>
|
#include <string>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace ix
|
namespace ix
|
||||||
{
|
{
|
||||||
class Socket;
|
class Socket;
|
||||||
|
|
||||||
struct WebSocketInitResult
|
enum class SendMessageKind
|
||||||
{
|
{
|
||||||
bool success;
|
Text,
|
||||||
int http_status;
|
Binary,
|
||||||
std::string errorStr;
|
Ping
|
||||||
|
|
||||||
WebSocketInitResult(bool s, int h, std::string e)
|
|
||||||
{
|
|
||||||
success = s;
|
|
||||||
http_status = h;
|
|
||||||
errorStr = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
// need to define a default
|
|
||||||
WebSocketInitResult()
|
|
||||||
{
|
|
||||||
success = false;
|
|
||||||
http_status = 0;
|
|
||||||
errorStr = "";
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class WebSocketTransport
|
class WebSocketTransport
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
enum ReadyStateValues
|
enum class ReadyState
|
||||||
{
|
{
|
||||||
CLOSING,
|
CLOSING,
|
||||||
CLOSED,
|
CLOSED,
|
||||||
@ -54,43 +49,74 @@ namespace ix
|
|||||||
OPEN
|
OPEN
|
||||||
};
|
};
|
||||||
|
|
||||||
using OnMessageCallback = std::function<void(const std::string&)>;
|
enum class MessageKind
|
||||||
using OnStateChangeCallback = std::function<void(ReadyStateValues)>;
|
{
|
||||||
|
MSG_TEXT,
|
||||||
|
MSG_BINARY,
|
||||||
|
PING,
|
||||||
|
PONG,
|
||||||
|
FRAGMENT
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class PollResult
|
||||||
|
{
|
||||||
|
Succeeded,
|
||||||
|
AbnormalClose
|
||||||
|
};
|
||||||
|
|
||||||
|
using OnMessageCallback =
|
||||||
|
std::function<void(const std::string&, size_t, bool, MessageKind)>;
|
||||||
|
using OnCloseCallback = std::function<void(uint16_t, const std::string&, size_t, bool)>;
|
||||||
|
|
||||||
WebSocketTransport();
|
WebSocketTransport();
|
||||||
~WebSocketTransport();
|
~WebSocketTransport();
|
||||||
|
|
||||||
void configure(const std::string& url);
|
void configure(const WebSocketPerMessageDeflateOptions& perMessageDeflateOptions,
|
||||||
WebSocketInitResult init();
|
bool enablePong,
|
||||||
|
int pingIntervalSecs,
|
||||||
|
int pingTimeoutSecs);
|
||||||
|
|
||||||
void poll();
|
WebSocketInitResult connectToUrl( // Client
|
||||||
void send(const std::string& message);
|
const std::string& url,
|
||||||
void sendBinary(const std::string& message);
|
const WebSocketHttpHeaders& headers,
|
||||||
void sendBinary(const std::vector<uint8_t>& message);
|
int timeoutSecs);
|
||||||
void sendPing();
|
WebSocketInitResult connectToSocket(int fd, // Server
|
||||||
void close();
|
int timeoutSecs);
|
||||||
ReadyStateValues getReadyState() const;
|
|
||||||
void setReadyState(ReadyStateValues readyStateValue);
|
|
||||||
void setOnStateChangeCallback(const OnStateChangeCallback& onStateChangeCallback);
|
|
||||||
void dispatch(const OnMessageCallback& onMessageCallback);
|
|
||||||
|
|
||||||
static void printUrl(const std::string& url);
|
PollResult poll();
|
||||||
static bool parseUrl(const std::string& url,
|
WebSocketSendInfo sendBinary(const std::string& message,
|
||||||
std::string& protocol,
|
const OnProgressCallback& onProgressCallback);
|
||||||
std::string& host,
|
WebSocketSendInfo sendText(const std::string& message,
|
||||||
std::string& path,
|
const OnProgressCallback& onProgressCallback);
|
||||||
std::string& query,
|
WebSocketSendInfo sendPing(const std::string& message);
|
||||||
int& port);
|
|
||||||
|
void close(uint16_t code = WebSocketCloseConstants::kNormalClosureCode,
|
||||||
|
const std::string& reason = WebSocketCloseConstants::kNormalClosureMessage,
|
||||||
|
size_t closeWireSize = 0,
|
||||||
|
bool remote = false);
|
||||||
|
|
||||||
|
void closeSocket();
|
||||||
|
ssize_t send();
|
||||||
|
|
||||||
|
ReadyState getReadyState() const;
|
||||||
|
void setReadyState(ReadyState readyState);
|
||||||
|
void setOnCloseCallback(const OnCloseCallback& onCloseCallback);
|
||||||
|
void dispatch(PollResult pollResult, const OnMessageCallback& onMessageCallback);
|
||||||
|
size_t bufferedAmount() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string _url;
|
std::string _url;
|
||||||
std::string _origin;
|
|
||||||
|
|
||||||
struct wsheader_type {
|
struct wsheader_type
|
||||||
|
{
|
||||||
unsigned header_size;
|
unsigned header_size;
|
||||||
bool fin;
|
bool fin;
|
||||||
|
bool rsv1;
|
||||||
|
bool rsv2;
|
||||||
|
bool rsv3;
|
||||||
bool mask;
|
bool mask;
|
||||||
enum opcode_type {
|
enum opcode_type
|
||||||
|
{
|
||||||
CONTINUATION = 0x0,
|
CONTINUATION = 0x0,
|
||||||
TEXT_FRAME = 0x1,
|
TEXT_FRAME = 0x1,
|
||||||
BINARY_FRAME = 0x2,
|
BINARY_FRAME = 0x2,
|
||||||
@ -103,22 +129,123 @@ namespace ix
|
|||||||
uint8_t masking_key[4];
|
uint8_t masking_key[4];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Tells whether we should mask the data we send.
|
||||||
|
// client should mask but server should not
|
||||||
|
std::atomic<bool> _useMask;
|
||||||
|
|
||||||
|
// Buffer for reading from our socket. That buffer is never resized.
|
||||||
|
std::vector<uint8_t> _readbuf;
|
||||||
|
|
||||||
|
// Contains all messages that were fetched in the last socket read.
|
||||||
|
// This could be a mix of control messages (Close, Ping, etc...) and
|
||||||
|
// data messages. That buffer
|
||||||
std::vector<uint8_t> _rxbuf;
|
std::vector<uint8_t> _rxbuf;
|
||||||
|
|
||||||
|
// Contains all messages that are waiting to be sent
|
||||||
std::vector<uint8_t> _txbuf;
|
std::vector<uint8_t> _txbuf;
|
||||||
mutable std::mutex _txbufMutex;
|
mutable std::mutex _txbufMutex;
|
||||||
std::vector<uint8_t> _receivedData;
|
|
||||||
|
|
||||||
|
// Hold fragments for multi-fragments messages in a list. We support receiving very large
|
||||||
|
// messages (tested messages up to 700M) and we cannot put them in a single
|
||||||
|
// buffer that is resized, as this operation can be slow when a buffer has its
|
||||||
|
// size increased 2 fold, while appending to a list has a fixed cost.
|
||||||
|
std::list<std::vector<uint8_t>> _chunks;
|
||||||
|
|
||||||
|
// Record the message kind (will be TEXT or BINARY) for a fragmented
|
||||||
|
// message, present in the first chunk, since the final chunk will be a
|
||||||
|
// CONTINUATION opcode and doesn't tell the full message kind
|
||||||
|
MessageKind _fragmentedMessageKind;
|
||||||
|
|
||||||
|
// Fragments are 32K long
|
||||||
|
static constexpr size_t kChunkSize = 1 << 15;
|
||||||
|
|
||||||
|
// Underlying TCP socket
|
||||||
std::shared_ptr<Socket> _socket;
|
std::shared_ptr<Socket> _socket;
|
||||||
|
std::mutex _socketMutex;
|
||||||
|
|
||||||
std::atomic<ReadyStateValues> _readyState;
|
// Hold the state of the connection (OPEN, CLOSED, etc...)
|
||||||
|
std::atomic<ReadyState> _readyState;
|
||||||
|
|
||||||
OnStateChangeCallback _onStateChangeCallback;
|
OnCloseCallback _onCloseCallback;
|
||||||
|
uint16_t _closeCode;
|
||||||
|
std::string _closeReason;
|
||||||
|
size_t _closeWireSize;
|
||||||
|
bool _closeRemote;
|
||||||
|
mutable std::mutex _closeDataMutex;
|
||||||
|
|
||||||
|
// Data used for Per Message Deflate compression (with zlib)
|
||||||
|
WebSocketPerMessageDeflate _perMessageDeflate;
|
||||||
|
WebSocketPerMessageDeflateOptions _perMessageDeflateOptions;
|
||||||
|
std::atomic<bool> _enablePerMessageDeflate;
|
||||||
|
|
||||||
|
// Used to cancel dns lookup + socket connect + http upgrade
|
||||||
|
std::atomic<bool> _requestInitCancellation;
|
||||||
|
|
||||||
|
mutable std::mutex _closingTimePointMutex;
|
||||||
|
std::chrono::time_point<std::chrono::steady_clock> _closingTimePoint;
|
||||||
|
static const int kClosingMaximumWaitingDelayInMs;
|
||||||
|
|
||||||
|
// enable auto response to ping
|
||||||
|
std::atomic<bool> _enablePong;
|
||||||
|
static const bool kDefaultEnablePong;
|
||||||
|
|
||||||
|
// Optional ping and pong timeout
|
||||||
|
// if both ping interval and timeout are set (> 0),
|
||||||
|
// then use GCD of these value to wait for the lowest time
|
||||||
|
int _pingIntervalSecs;
|
||||||
|
int _pingTimeoutSecs;
|
||||||
|
int _pingIntervalOrTimeoutGCDSecs;
|
||||||
|
|
||||||
|
static const int kDefaultPingIntervalSecs;
|
||||||
|
static const int kDefaultPingTimeoutSecs;
|
||||||
|
static const std::string kPingMessage;
|
||||||
|
|
||||||
|
// Record time step for ping/ ping timeout to ensure we wait for the right left duration
|
||||||
|
std::chrono::time_point<std::chrono::steady_clock> _nextGCDTimePoint;
|
||||||
|
|
||||||
|
// We record when ping are being sent so that we can know when to send the next one
|
||||||
|
// We also record when pong are being sent as a reply to pings, to close the connections
|
||||||
|
// if no pong were received sufficiently fast.
|
||||||
|
mutable std::mutex _lastSendPingTimePointMutex;
|
||||||
|
mutable std::mutex _lastReceivePongTimePointMutex;
|
||||||
|
std::chrono::time_point<std::chrono::steady_clock> _lastSendPingTimePoint;
|
||||||
|
std::chrono::time_point<std::chrono::steady_clock> _lastReceivePongTimePoint;
|
||||||
|
|
||||||
|
// If this function returns true, it is time to send a new ping
|
||||||
|
bool pingIntervalExceeded();
|
||||||
|
|
||||||
|
// No PONG data was received through the socket for longer than ping timeout delay
|
||||||
|
bool pingTimeoutExceeded();
|
||||||
|
|
||||||
|
// after calling close(), if no CLOSE frame answer is received back from the remote, we
|
||||||
|
// should close the connexion
|
||||||
|
bool closingDelayExceeded();
|
||||||
|
|
||||||
|
void initTimePointsAndGCDAfterConnect();
|
||||||
|
|
||||||
|
void sendCloseFrame(uint16_t code, const std::string& reason);
|
||||||
|
|
||||||
|
void closeSocketAndSwitchToClosedState(uint16_t code,
|
||||||
|
const std::string& reason,
|
||||||
|
size_t closeWireSize,
|
||||||
|
bool remote);
|
||||||
|
|
||||||
void sendOnSocket();
|
void sendOnSocket();
|
||||||
void sendData(wsheader_type::opcode_type type,
|
WebSocketSendInfo sendData(wsheader_type::opcode_type type,
|
||||||
uint64_t message_size,
|
const std::string& message,
|
||||||
std::string::const_iterator message_begin,
|
bool compress,
|
||||||
std::string::const_iterator message_end);
|
const OnProgressCallback& onProgressCallback = nullptr);
|
||||||
|
|
||||||
|
void sendFragment(wsheader_type::opcode_type type,
|
||||||
|
bool fin,
|
||||||
|
std::string::const_iterator begin,
|
||||||
|
std::string::const_iterator end,
|
||||||
|
bool compress);
|
||||||
|
|
||||||
|
void emitMessage(MessageKind messageKind,
|
||||||
|
const std::string& message,
|
||||||
|
const wsheader_type& ws,
|
||||||
|
const OnMessageCallback& onMessageCallback);
|
||||||
|
|
||||||
bool isSendBufferEmpty() const;
|
bool isSendBufferEmpty() const;
|
||||||
void appendToSendBuffer(const std::vector<uint8_t>& header,
|
void appendToSendBuffer(const std::vector<uint8_t>& header,
|
||||||
@ -126,8 +253,10 @@ namespace ix
|
|||||||
std::string::const_iterator end,
|
std::string::const_iterator end,
|
||||||
uint64_t message_size,
|
uint64_t message_size,
|
||||||
uint8_t masking_key[4]);
|
uint8_t masking_key[4]);
|
||||||
void appendToSendBuffer(const std::vector<uint8_t>& buffer);
|
|
||||||
|
|
||||||
unsigned getRandomUnsigned();
|
unsigned getRandomUnsigned();
|
||||||
|
void unmaskReceiveBuffer(const wsheader_type& ws);
|
||||||
|
|
||||||
|
std::string getMergedChunks() const;
|
||||||
};
|
};
|
||||||
}
|
} // namespace ix
|
||||||
|
9
ixwebsocket/IXWebSocketVersion.h
Normal file
9
ixwebsocket/IXWebSocketVersion.h
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/*
|
||||||
|
* IXWebSocketVersion.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#define IX_WEBSOCKET_VERSION "5.1.4"
|
263
ixwebsocket/LUrlParser.cpp
Normal file
263
ixwebsocket/LUrlParser.cpp
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
/*
|
||||||
|
* Lightweight URL & URI parser (RFC 1738, RFC 3986)
|
||||||
|
* https://github.com/corporateshark/LUrlParser
|
||||||
|
*
|
||||||
|
* The MIT License (MIT)
|
||||||
|
*
|
||||||
|
* Copyright (C) 2015 Sergey Kosarevsky (sk@linderdaum.com)
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "LUrlParser.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
// check if the scheme name is valid
|
||||||
|
static bool IsSchemeValid( const std::string& SchemeName )
|
||||||
|
{
|
||||||
|
for ( auto c : SchemeName )
|
||||||
|
{
|
||||||
|
if ( !isalpha( c ) && c != '+' && c != '-' && c != '.' ) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LUrlParser::clParseURL::GetPort( int* OutPort ) const
|
||||||
|
{
|
||||||
|
if ( !IsValid() ) { return false; }
|
||||||
|
|
||||||
|
int Port = atoi( m_Port.c_str() );
|
||||||
|
|
||||||
|
if ( Port <= 0 || Port > 65535 ) { return false; }
|
||||||
|
|
||||||
|
if ( OutPort ) { *OutPort = Port; }
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// based on RFC 1738 and RFC 3986
|
||||||
|
LUrlParser::clParseURL LUrlParser::clParseURL::ParseURL( const std::string& URL )
|
||||||
|
{
|
||||||
|
LUrlParser::clParseURL Result;
|
||||||
|
|
||||||
|
const char* CurrentString = URL.c_str();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* <scheme>:<scheme-specific-part>
|
||||||
|
* <scheme> := [a-z\+\-\.]+
|
||||||
|
* For resiliency, programs interpreting URLs should treat upper case letters as equivalent to lower case in scheme names
|
||||||
|
*/
|
||||||
|
|
||||||
|
// try to read scheme
|
||||||
|
{
|
||||||
|
const char* LocalString = strchr( CurrentString, ':' );
|
||||||
|
|
||||||
|
if ( !LocalString )
|
||||||
|
{
|
||||||
|
return clParseURL( LUrlParserError_NoUrlCharacter );
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the scheme name
|
||||||
|
Result.m_Scheme = std::string( CurrentString, LocalString - CurrentString );
|
||||||
|
|
||||||
|
if ( !IsSchemeValid( Result.m_Scheme ) )
|
||||||
|
{
|
||||||
|
return clParseURL( LUrlParserError_InvalidSchemeName );
|
||||||
|
}
|
||||||
|
|
||||||
|
// scheme should be lowercase
|
||||||
|
std::transform( Result.m_Scheme.begin(), Result.m_Scheme.end(), Result.m_Scheme.begin(), ::tolower );
|
||||||
|
|
||||||
|
// skip ':'
|
||||||
|
CurrentString = LocalString+1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* //<user>:<password>@<host>:<port>/<url-path>
|
||||||
|
* any ":", "@" and "/" must be normalized
|
||||||
|
*/
|
||||||
|
|
||||||
|
// skip "//"
|
||||||
|
if ( *CurrentString++ != '/' ) return clParseURL( LUrlParserError_NoDoubleSlash );
|
||||||
|
if ( *CurrentString++ != '/' ) return clParseURL( LUrlParserError_NoDoubleSlash );
|
||||||
|
|
||||||
|
// check if the user name and password are specified
|
||||||
|
bool bHasUserName = false;
|
||||||
|
|
||||||
|
const char* LocalString = CurrentString;
|
||||||
|
|
||||||
|
while ( *LocalString )
|
||||||
|
{
|
||||||
|
if ( *LocalString == '@' )
|
||||||
|
{
|
||||||
|
// user name and password are specified
|
||||||
|
bHasUserName = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if ( *LocalString == '/' )
|
||||||
|
{
|
||||||
|
// end of <host>:<port> specification
|
||||||
|
bHasUserName = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalString++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// user name and password
|
||||||
|
LocalString = CurrentString;
|
||||||
|
|
||||||
|
if ( bHasUserName )
|
||||||
|
{
|
||||||
|
// read user name
|
||||||
|
while ( *LocalString && *LocalString != ':' && *LocalString != '@' ) LocalString++;
|
||||||
|
|
||||||
|
Result.m_UserName = std::string( CurrentString, LocalString - CurrentString );
|
||||||
|
|
||||||
|
// proceed with the current pointer
|
||||||
|
CurrentString = LocalString;
|
||||||
|
|
||||||
|
if ( *CurrentString == ':' )
|
||||||
|
{
|
||||||
|
// skip ':'
|
||||||
|
CurrentString++;
|
||||||
|
|
||||||
|
// read password
|
||||||
|
LocalString = CurrentString;
|
||||||
|
|
||||||
|
while ( *LocalString && *LocalString != '@' ) LocalString++;
|
||||||
|
|
||||||
|
Result.m_Password = std::string( CurrentString, LocalString - CurrentString );
|
||||||
|
|
||||||
|
CurrentString = LocalString;
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip '@'
|
||||||
|
if ( *CurrentString != '@' )
|
||||||
|
{
|
||||||
|
return clParseURL( LUrlParserError_NoAtSign );
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentString++;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bHasBracket = ( *CurrentString == '[' );
|
||||||
|
|
||||||
|
// go ahead, read the host name
|
||||||
|
LocalString = CurrentString;
|
||||||
|
|
||||||
|
while ( *LocalString )
|
||||||
|
{
|
||||||
|
if ( bHasBracket && *LocalString == ']' )
|
||||||
|
{
|
||||||
|
// end of IPv6 address
|
||||||
|
LocalString++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if ( !bHasBracket && ( *LocalString == ':' || *LocalString == '/' ) )
|
||||||
|
{
|
||||||
|
// port number is specified
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalString++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result.m_Host = std::string( CurrentString, LocalString - CurrentString );
|
||||||
|
|
||||||
|
CurrentString = LocalString;
|
||||||
|
|
||||||
|
// is port number specified?
|
||||||
|
if ( *CurrentString == ':' )
|
||||||
|
{
|
||||||
|
CurrentString++;
|
||||||
|
|
||||||
|
// read port number
|
||||||
|
LocalString = CurrentString;
|
||||||
|
|
||||||
|
while ( *LocalString && *LocalString != '/' ) LocalString++;
|
||||||
|
|
||||||
|
Result.m_Port = std::string( CurrentString, LocalString - CurrentString );
|
||||||
|
|
||||||
|
CurrentString = LocalString;
|
||||||
|
}
|
||||||
|
|
||||||
|
// end of string
|
||||||
|
if ( !*CurrentString )
|
||||||
|
{
|
||||||
|
Result.m_ErrorCode = LUrlParserError_Ok;
|
||||||
|
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip '/'
|
||||||
|
if ( *CurrentString != '/' )
|
||||||
|
{
|
||||||
|
return clParseURL( LUrlParserError_NoSlash );
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentString++;
|
||||||
|
|
||||||
|
// parse the path
|
||||||
|
LocalString = CurrentString;
|
||||||
|
|
||||||
|
while ( *LocalString && *LocalString != '#' && *LocalString != '?' ) LocalString++;
|
||||||
|
|
||||||
|
Result.m_Path = std::string( CurrentString, LocalString - CurrentString );
|
||||||
|
|
||||||
|
CurrentString = LocalString;
|
||||||
|
|
||||||
|
// check for query
|
||||||
|
if ( *CurrentString == '?' )
|
||||||
|
{
|
||||||
|
// skip '?'
|
||||||
|
CurrentString++;
|
||||||
|
|
||||||
|
// read query
|
||||||
|
LocalString = CurrentString;
|
||||||
|
|
||||||
|
while ( *LocalString && *LocalString != '#' ) LocalString++;
|
||||||
|
|
||||||
|
Result.m_Query = std::string( CurrentString, LocalString - CurrentString );
|
||||||
|
|
||||||
|
CurrentString = LocalString;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for fragment
|
||||||
|
if ( *CurrentString == '#' )
|
||||||
|
{
|
||||||
|
// skip '#'
|
||||||
|
CurrentString++;
|
||||||
|
|
||||||
|
// read fragment
|
||||||
|
LocalString = CurrentString;
|
||||||
|
|
||||||
|
while ( *LocalString ) LocalString++;
|
||||||
|
|
||||||
|
Result.m_Fragment = std::string( CurrentString, LocalString - CurrentString );
|
||||||
|
}
|
||||||
|
|
||||||
|
Result.m_ErrorCode = LUrlParserError_Ok;
|
||||||
|
|
||||||
|
return Result;
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user