├── .editorconfig
├── .gitattributes
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── Readme.md
├── autobahn
├── Readme.md
├── fuzzingclient.json
├── fuzzingclient_tls.json
├── fuzzingserver.json
├── fuzzingserver_tls.json
├── index.md
└── tls
│ ├── server.crt
│ └── server.key
├── config.nims
├── examples
├── autobahn_client.nim
├── client.nim
└── server.nim
├── nim.cfg
├── tests
├── all_tests.nim
├── extensions
│ ├── base64ext.nim
│ ├── data
│ │ ├── alice29.txt
│ │ └── fireworks.jpg
│ ├── hexext.nim
│ ├── testcompression.nim
│ ├── testextflow.nim
│ └── testexts.nim
├── helpers.nim
├── keys.nim
├── testextutils.nim
├── testframes.nim
├── testhooks.nim
├── testutf8.nim
└── testwebsockets.nim
├── websock.nimble
├── websock.svg
└── websock
├── extensions.nim
├── extensions
├── compression
│ └── deflate.nim
└── extutils.nim
├── frame.nim
├── http.nim
├── http
├── client.nim
├── common.nim
└── server.nim
├── session.nim
├── types.nim
├── utf8dfa.nim
└── websock.nim
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | indent_style = space
3 | insert_final_newline = true
4 | indent_size = 2
5 | trim_trailing_whitespace = true
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # prevent automatic EOL conversion by git
2 | # on test data, because it will make test
3 | # failed
4 |
5 | tests/extensions/data/* -crlf
6 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | workflow_dispatch:
8 |
9 | # we dont run compression extension
10 | # in regular CI because it is a
11 | # time consuming operation.
12 | # we delegate it to manually triggered CI
13 | # and only run base autobahn tests here.
14 |
15 | jobs:
16 | build:
17 | strategy:
18 | fail-fast: false
19 | matrix:
20 | target:
21 | - os: linux
22 | cpu: amd64
23 | - os: linux
24 | cpu: i386
25 | - os: macos
26 | cpu: amd64
27 | - os: macos
28 | cpu: arm64
29 | - os: windows
30 | cpu: amd64
31 | branch: [version-2-0, version-2-2, devel]
32 | include:
33 | - target:
34 | os: linux
35 | builder: ubuntu-latest
36 | shell: bash
37 | - target:
38 | os: macos
39 | cpu: amd64
40 | builder: macos-13
41 | shell: bash
42 | - target:
43 | os: macos
44 | cpu: arm64
45 | builder: macos-latest
46 | shell: bash
47 | - target:
48 | os: windows
49 | builder: windows-latest
50 | shell: msys2 {0}
51 |
52 | defaults:
53 | run:
54 | shell: ${{ matrix.shell }}
55 |
56 | name: '${{ matrix.target.os }}-${{ matrix.target.cpu }} (Nim ${{ matrix.branch }})'
57 | runs-on: ${{ matrix.builder }}
58 | continue-on-error: ${{ matrix.branch == 'devel' }}
59 | steps:
60 | - name: Checkout
61 | uses: actions/checkout@v4
62 | with:
63 | submodules: true
64 |
65 | - name: Install build dependencies (Linux i386)
66 | if: runner.os == 'Linux' && matrix.target.cpu == 'i386'
67 | run: |
68 | sudo dpkg --add-architecture i386
69 | sudo apt-get update -qq
70 | sudo DEBIAN_FRONTEND='noninteractive' apt-get install \
71 | --no-install-recommends -yq gcc-multilib g++-multilib \
72 | libssl-dev:i386
73 | mkdir -p external/bin
74 | cat << EOF > external/bin/gcc
75 | #!/bin/bash
76 | exec $(which gcc) -m32 "\$@"
77 | EOF
78 | cat << EOF > external/bin/g++
79 | #!/bin/bash
80 | exec $(which g++) -m32 "\$@"
81 | EOF
82 | chmod 755 external/bin/gcc external/bin/g++
83 | echo '${{ github.workspace }}/external/bin' >> $GITHUB_PATH
84 |
85 | - name: MSYS2 (Windows i386)
86 | if: runner.os == 'Windows' && matrix.target.cpu == 'i386'
87 | uses: msys2/setup-msys2@v2
88 | with:
89 | path-type: inherit
90 | msystem: MINGW32
91 | install: >-
92 | base-devel
93 | git
94 | mingw-w64-i686-toolchain
95 |
96 | - name: MSYS2 (Windows amd64)
97 | if: runner.os == 'Windows' && matrix.target.cpu == 'amd64'
98 | uses: msys2/setup-msys2@v2
99 | with:
100 | path-type: inherit
101 | install: >-
102 | base-devel
103 | git
104 | mingw-w64-x86_64-toolchain
105 |
106 | - name: Restore Nim DLLs dependencies (Windows) from cache
107 | if: runner.os == 'Windows'
108 | id: windows-dlls-cache
109 | uses: actions/cache@v4
110 | with:
111 | path: external/dlls-${{ matrix.target.cpu }}
112 | key: 'dlls-${{ matrix.target.cpu }}'
113 |
114 | - name: Install DLLs dependencies (Windows)
115 | if: >
116 | steps.windows-dlls-cache.outputs.cache-hit != 'true' &&
117 | runner.os == 'Windows'
118 | run: |
119 | DLLPATH=external/dlls-${{ matrix.target.cpu }}
120 | mkdir -p external
121 | curl -L "https://nim-lang.org/download/windeps.zip" -o external/windeps.zip
122 | 7z x -y external/windeps.zip -o"$DLLPATH"
123 |
124 | - name: Path to cached dependencies (Windows)
125 | if: >
126 | runner.os == 'Windows'
127 | run: |
128 | echo '${{ github.workspace }}'"/external/dlls-${{ matrix.target.cpu }}" >> $GITHUB_PATH
129 |
130 | - name: Derive environment variables
131 | run: |
132 | if [[ '${{ matrix.target.cpu }}' == 'amd64' ]]; then
133 | PLATFORM=x64
134 | elif [[ '${{ matrix.target.cpu }}' == 'arm64' ]]; then
135 | PLATFORM=arm64
136 | else
137 | PLATFORM=x86
138 | fi
139 | echo "PLATFORM=$PLATFORM" >> $GITHUB_ENV
140 |
141 | ncpu=
142 | MAKE_CMD="make"
143 | case '${{ runner.os }}' in
144 | 'Linux')
145 | ncpu=$(nproc)
146 | ;;
147 | 'macOS')
148 | ncpu=$(sysctl -n hw.ncpu)
149 | ;;
150 | 'Windows')
151 | ncpu=$NUMBER_OF_PROCESSORS
152 | MAKE_CMD="mingw32-make"
153 | ;;
154 | esac
155 | [[ -z "$ncpu" || $ncpu -le 0 ]] && ncpu=1
156 | echo "ncpu=$ncpu" >> $GITHUB_ENV
157 | echo "MAKE_CMD=${MAKE_CMD}" >> $GITHUB_ENV
158 |
159 | - name: Build Nim and Nimble
160 | run: |
161 | curl -O -L -s -S https://raw.githubusercontent.com/status-im/nimbus-build-system/master/scripts/build_nim.sh
162 | env MAKE="${MAKE_CMD} -j${ncpu}" ARCH_OVERRIDE=${PLATFORM} NIM_COMMIT=${{ matrix.branch }} \
163 | QUICK_AND_DIRTY_COMPILER=1 QUICK_AND_DIRTY_NIMBLE=1 CC=gcc \
164 | bash build_nim.sh nim csources dist/nimble NimBinaries
165 | echo '${{ github.workspace }}/nim/bin' >> $GITHUB_PATH
166 |
167 | - name: Run tests
168 | run: |
169 | nim --version
170 | nimble --version
171 | nimble install -y --depsOnly
172 | nimble test
173 | env NIMFLAGS="--mm:refc" nimble test
174 |
175 | autobahn-test:
176 | if: github.event_name == 'push' # || github.event_name == 'pull_request'
177 | name: "Autobahn test suite"
178 | runs-on: ubuntu-latest
179 |
180 | strategy:
181 | fail-fast: false
182 | max-parallel: 20
183 | matrix:
184 | websock: [ws, wsc, wss, wssc]
185 | branch: [version-1-6, version-2-0, devel]
186 |
187 | defaults:
188 | run:
189 | shell: bash
190 |
191 | steps:
192 | - name: Checkout
193 | uses: actions/checkout@v4
194 | with:
195 | submodules: true
196 |
197 | - name: Build Nim and Nimble
198 | run: |
199 | curl -O -L -s -S https://raw.githubusercontent.com/status-im/nimbus-build-system/master/scripts/build_nim.sh
200 | env MAKE="make -j$(nproc)" NIM_COMMIT=${{ matrix.branch }} \
201 | QUICK_AND_DIRTY_COMPILER=1 QUICK_AND_DIRTY_NIMBLE=1 CC=gcc \
202 | bash build_nim.sh nim csources dist/nimble NimBinaries
203 | echo '${{ github.workspace }}/nim/bin' >> $GITHUB_PATH
204 |
205 | - name: Setup Python version
206 | uses: actions/setup-python@v5
207 | with:
208 | python-version: pypy-2.7
209 |
210 | - name: Setup Autobahn.
211 | run: |
212 | sudo apt-get install -y python2.7-dev
213 | pip install virtualenv
214 | pip install markdown2
215 | virtualenv --python=/usr/bin/python2.7 autobahn
216 | source autobahn/bin/activate
217 | pip install autobahntestsuite txaio==2.1.0 autobahn[twisted,accelerate]==0.10.9 jinja2==2.6 markupsafe==0.19 Werkzeug==0.9.6 klein==0.2.3 pyopenssl service_identity==14.0.0 unittest2==1.1.0 wsaccel==0.6.2
218 | pip freeze
219 | nimble install -y --depsOnly
220 |
221 | - name: Generate index.html
222 | if: matrix.websock == 'ws'
223 | run: |
224 | mkdir autobahn/reports
225 | sed -i "s/COMMIT_SHA_SHORT/${GITHUB_SHA::7}/g" autobahn/index.md
226 | sed -i "s/COMMIT_SHA/$GITHUB_SHA/g" autobahn/index.md
227 | markdown2 autobahn/index.md > autobahn/reports/index.html
228 |
229 | - name: Run Autobahn test suite.
230 | run: |
231 | source autobahn/bin/activate
232 | case '${{ matrix.websock }}' in
233 | ws)
234 | nim c -d:release examples/server.nim
235 | examples/server &
236 | server=$!
237 |
238 | cd autobahn
239 | wstest --mode fuzzingclient --spec fuzzingclient.json
240 | ;;
241 | wsc)
242 | nim c -d:tls -d:release -o:examples/tls_server examples/server.nim
243 | examples/tls_server &
244 | server=$!
245 |
246 | cd autobahn
247 | wstest --mode fuzzingclient --spec fuzzingclient_tls.json
248 | ;;
249 | wss)
250 | cd autobahn
251 | wstest --webport=0 --mode fuzzingserver --spec fuzzingserver.json &
252 | server=$!
253 |
254 | cd ..
255 | nim c -d:release examples/autobahn_client
256 | examples/autobahn_client
257 | ;;
258 | wssc)
259 | cd autobahn
260 | wstest --webport=0 --mode fuzzingserver --spec fuzzingserver_tls.json &
261 | server=$!
262 |
263 | cd ..
264 | nim c -d:tls -d:release -o:examples/autobahn_tlsclient examples/autobahn_client
265 | examples/autobahn_tlsclient
266 | ;;
267 | esac
268 |
269 | kill $server
270 |
271 | - name: Upload Autobahn result
272 | uses: actions/upload-artifact@v4
273 | with:
274 | name: autobahn-artifact-${{ matrix.websock }}-${{ matrix.branch }}
275 | path: ./autobahn/reports
276 |
277 | # Issue related to actions/upload-artifact@v4 immutability
278 | # Applying Merging multiple artifacts:
279 | # https://github.com/actions/upload-artifact/blob/main/docs/MIGRATION.md#merging-multiple-artifacts
280 | merge-autobahn-artifact:
281 | runs-on: ubuntu-latest
282 | needs: autobahn-test
283 | steps:
284 | - name: Merge Autobahn Artifacts
285 | uses: actions/upload-artifact/merge@v4
286 | with:
287 | name: autobahn-report
288 | pattern: autobahn-artifact-*
289 |
290 | deploy-test:
291 | name: "Deplay Autobahn results"
292 | needs: merge-autobahn-artifact
293 | runs-on: ubuntu-latest
294 |
295 | defaults:
296 | run:
297 | shell: bash
298 |
299 | steps:
300 | - name: Download Autobahn reports
301 | uses: actions/download-artifact@v4
302 | with:
303 | name: autobahn-report
304 | path: ./autobahn_reports
305 |
306 | - name: Deploy autobahn report.
307 | uses: peaceiris/actions-gh-pages@v4
308 | with:
309 | personal_token: ${{ secrets.GITHUB_TOKEN }}
310 | publish_dir: ./autobahn_reports
311 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.exe
2 | nimcache
3 | nimble.develop
4 | nimble.paths
5 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Websocket for Nim
4 |
5 | [](https://github.com/status-im/nim-websock/actions/workflows/ci.yml)
6 |
7 | This is an implementation of [Websocket](https://tools.ietf.org/html/rfc6455) protocol for
8 | [Nim](https://nim-lang.org/) and [chronos](https://github.com/status-im/nim-chronos/).
9 |
10 | `nim-websock` includes both client and server in regular ws and wss(secure) mode.
11 |
12 | It also pass all autobahn tests [Autobahn summary report](https://status-im.github.io/nim-websock/).
13 |
14 | Building and testing
15 | --------------------
16 |
17 | Install dependencies:
18 |
19 | ```bash
20 | nimble install -d
21 | ```
22 |
23 | Starting HTTP server:
24 |
25 | ```bash
26 | nim c -r examples/server.nim
27 | ```
28 |
29 | Testing Server Response:
30 |
31 | ```bash
32 | curl --location --request GET 'http://localhost:8888'
33 | ```
34 |
35 | Testing Websocket Handshake:
36 | ```bash
37 | curl --include \
38 | --no-buffer \
39 | --header "Connection: Upgrade" \
40 | --header "Upgrade: websocket" \
41 | --header "Host: example.com:80" \
42 | --header "Origin: http://example.com:80" \
43 | --header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
44 | --header "Sec-WebSocket-Version: 13" \
45 | http://localhost:8888/ws
46 | ```
47 |
48 | ## Roadmap
49 |
50 | - [x] Framing
51 | - [x] Text Messages
52 | - [x] Binary Messages
53 | - [x] Pings/Pongs
54 | - [x] Reserved Bits
55 | - [x] Opcodes
56 | - [x] Non-control Opcodes
57 | - [x] Control Opcodes
58 | - [x] Fragmentation
59 | - [x] UTF-8 Handling
60 | - [x] Close Handling
61 | - [x] Basic close behavior
62 | - [x] Close frame structure
63 | - [x] Payload length
64 | - [x] Valid close codes
65 | - [x] Invalid close codes
66 | - [x] Integrate Autobahn Test suite.
67 | - [x] WebSocket Compression
68 | - [x] WebSocket Extensions
69 | - [ ] Performance
70 |
--------------------------------------------------------------------------------
/autobahn/Readme.md:
--------------------------------------------------------------------------------
1 | ## Running autobahn test suite.
2 |
3 | ### Install autobahn
4 | ```bash
5 | # Set up virtualenv in autobahn folder
6 | virtualenv --python=/usr/bin/python2 autobahn
7 |
8 | # Activate the virtualenv
9 | source autobahn/bin/activate
10 |
11 | # Install autobahn
12 | pip install autobahntestsuite
13 | ```
14 |
15 | ### Run the test Websocket client.
16 | * ws server: `nim c -r examples/server.nim`
17 | * autobahn: `wstest --mode fuzzingclient --spec fuzzingclient.json`
18 | * Reports will be generated in `reports/server` which can be configured in `fuzzingclient.json`
19 |
20 | * wss server: `nim c -r -d:tls examples/server.nim`
21 | * autobahn: `wstest --mode fuzzingclient --spec fuzzingclient_tls.json`
22 | * Reports will be generated in `reports/server_tls` which can be configured in `fuzzingclient_tls.json`
23 |
24 | * ws client:
25 | * autobahn: `wstest --mode fuzzingserver --spec fuzzingserver.json`
26 | * ws: `nim c -r examples/autobahn_client.nim`
27 |
28 | * wss client:
29 | * autobahn: `wstest --mode fuzzingserver --spec fuzzingserver_tls.json`
30 | * ws: `nim c -r -d:tls examples/autobahn_client.nim`
31 |
--------------------------------------------------------------------------------
/autobahn/fuzzingclient.json:
--------------------------------------------------------------------------------
1 | {
2 | "outdir": "./reports/server",
3 |
4 | "servers": [{
5 | "agent": "websock server",
6 | "url": "ws://127.0.0.1:8888/ws"
7 | }],
8 |
9 | "cases": ["*"],
10 | "exclude-cases": [],
11 | "exclude-agent-cases": {}
12 | }
13 |
--------------------------------------------------------------------------------
/autobahn/fuzzingclient_tls.json:
--------------------------------------------------------------------------------
1 | {
2 | "outdir": "./reports/server_tls",
3 |
4 | "servers": [{
5 | "agent": "websock secure server",
6 | "url": "wss://127.0.0.1:8889/wss"
7 | }],
8 |
9 | "cases": ["*"],
10 | "exclude-cases": [],
11 | "exclude-agent-cases": {}
12 | }
13 |
--------------------------------------------------------------------------------
/autobahn/fuzzingserver.json:
--------------------------------------------------------------------------------
1 | {
2 | "url": "ws://127.0.0.1:9001",
3 |
4 | "options": {"failByDrop": false},
5 | "outdir": "./reports/client",
6 | "webport": 8080,
7 |
8 | "cases": ["*"],
9 | "exclude-cases": [],
10 | "exclude-agent-cases": {}
11 | }
12 |
--------------------------------------------------------------------------------
/autobahn/fuzzingserver_tls.json:
--------------------------------------------------------------------------------
1 | {
2 | "url": "wss://127.0.0.1:9002",
3 | "key": "tls/server.key",
4 | "cert": "tls/server.crt",
5 |
6 | "options": {"failByDrop": false},
7 | "outdir": "./reports/client_tls",
8 | "webport": 8081,
9 |
10 | "cases": ["*"],
11 | "exclude-cases": [],
12 | "exclude-agent-cases": {}
13 | }
14 |
--------------------------------------------------------------------------------
/autobahn/index.md:
--------------------------------------------------------------------------------
1 | # Autobahn summary report
2 |
3 | Generated by commit [COMMIT_SHA_SHORT](https://github.com/status-im/nim-websock/commit/COMMIT_SHA).
4 |
5 | * [ws server summary report](server/index.html)
6 | * [wss server summary report](server_tls/index.html)
7 | * [ws client summary report](client/index.html)
8 | * [wss client summary report](client_tls/index.html)
9 |
--------------------------------------------------------------------------------
/autobahn/tls/server.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDizCCAnOgAwIBAgIUaCFTPZnfw5V3otjMFvKRynRjhREwDQYJKoZIhvcNAQEL
3 | BQAwVTELMAkGA1UEBhMCSUQxEzARBgNVBAgMClNvbWUtU3RhdGUxDDAKBgNVBAcM
4 | A1BUSzEPMA0GA1UECgwGU3RhdHVzMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMjIw
5 | MzExMDQxMjUwWhcNMzIwMzA4MDQxMjUwWjBVMQswCQYDVQQGEwJJRDETMBEGA1UE
6 | CAwKU29tZS1TdGF0ZTEMMAoGA1UEBwwDUFRLMQ8wDQYDVQQKDAZTdGF0dXMxEjAQ
7 | BgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
8 | AMzJDJGegKDrO0XhzsAYe/d9tDGPw3GDRrlPL2zCCy+BbRBgKeaXSWJyDqIImHBH
9 | ETzcYNiiNcTfBdLAwrGANdZWNedzDK+Oc5PSocB1GWofVU7QstDkfhIYESFkw+pr
10 | gDucDqS7f23UTQ2JwE+iZlDamjqn2H6TQ7tyrujoOnSs4rSadahVW0iRMIFtpG1j
11 | lnAAQjn7UiGMqjWdyt02y5B472iLC6LXJ/iLeSEXaKRV0RTuuwsku1ZQLZVr+Hol
12 | 1s9x+SfR5GoxBU1odAKxaFwDUakK8Z5V6q4fhFMxAQteNttzVwUUtpK/iI4GqvuE
13 | ftRQc9rcMslmCjUpQJNHHkMCAwEAAaNTMFEwHQYDVR0OBBYEFGgtp7Bo01qBAI6X
14 | B86BhZNXuozjMB8GA1UdIwQYMBaAFGgtp7Bo01qBAI6XB86BhZNXuozjMA8GA1Ud
15 | EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAIqcPlrZrCUt0E9uMVZrd+Z6
16 | wgA3EfT/QHI6HiQgUPDRbwLzZ9zLlozT8OW2OyUTiKkGISjMCcCXWMgI/oOTEJ14
17 | aFVGpdtjeFb1hT/kOcFsycgDlyhq56HY4Nirc2FuMUbWdQnnCcum21jdMA+LfKYA
18 | g8k5GwRIce5KmYgdvcgdhitlvsqLUZ6zcKmTts026gdIsK6ysx+KBukbS5bXyL+O
19 | szW6iPjpTyiy7UTqm/xgqy6tROeZ2a7UuHnV8szBR9TI3JNxcDaknCzRzBsoN0e7
20 | o4YeKhNkF+u3LOwPYlPIzNT6PZAyghtoMoLr7N6hz/6q6cY1lk9J9r7xMvQDNys=
21 | -----END CERTIFICATE-----
22 |
--------------------------------------------------------------------------------
/autobahn/tls/server.key:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDMyQyRnoCg6ztF
3 | 4c7AGHv3fbQxj8Nxg0a5Ty9swgsvgW0QYCnml0licg6iCJhwRxE83GDYojXE3wXS
4 | wMKxgDXWVjXncwyvjnOT0qHAdRlqH1VO0LLQ5H4SGBEhZMPqa4A7nA6ku39t1E0N
5 | icBPomZQ2po6p9h+k0O7cq7o6Dp0rOK0mnWoVVtIkTCBbaRtY5ZwAEI5+1IhjKo1
6 | ncrdNsuQeO9oiwui1yf4i3khF2ikVdEU7rsLJLtWUC2Va/h6JdbPcfkn0eRqMQVN
7 | aHQCsWhcA1GpCvGeVequH4RTMQELXjbbc1cFFLaSv4iOBqr7hH7UUHPa3DLJZgo1
8 | KUCTRx5DAgMBAAECggEAZ72x0FMrdlm0PUKXKlk887OKhVD/AZIvUdD7hW0HnZlD
9 | VvhgQaCCXIzLPOp9zuMxqFM7/IKwGnugx5M/DgyVOW+S/x0ZYBYaFtiteIHZzhjx
10 | bmvrKkeWVjQ6+V/CoA61d30nLeyOMWVLY4BVHAJ5MpyLZQuCcRzJQzxlvzdl5ZSG
11 | aiwgosIruDZjRFju8tDkryWC9y3OdQwHVv4PGMO8ccBdgfPh7mw8GAQxjqiWjbRn
12 | P+93uCVht4qgqtvkyGaHfL4qvHgP5Yyy9IQxBEPSw7lqfhrXvVwHs4ZcXJwSx5Xx
13 | sgxk7pP16oxPMWCeXF1tk6Av3r2AUXvq7QJRrjp4AQKBgQD8lHO8iYbzFjuQwcH7
14 | p9JFuED2e8prB4dawv9l8e+HbSeksY7BEMqLds7wmLYzWXF9OtnYz9po9jhBLqVm
15 | dzRtNq3cItHKH1qKy4xC4AD0lvZ/Kdh1at3yINnZQ9emPClZ/iF+1EtDSmluqI4L
16 | KtTekcXZS8ChUiR3onBsUTb/QQKBgQDPjuvKZrYdOpWQ/AeMDvng0VSPv0uhntAq
17 | CtHq2Z03oopLzqmZ4cM/FIx3m8tkfv6RYb2ZgYMe88y5xqnTvIhsKZAdjexkFo2+
18 | +F3wLQPsACzpwaeFg6Hg/a4qmvFbByaabY7cTPiLWhW/mtTo4CPj+92bbk+EBqUp
19 | owdOBgKAgwKBgGG+yXNC/ZiBGBgTA5DyByu3/Fvm2BTHWxhCsjevgvSzx02y8P3C
20 | E8AZAEiXsJr6mDLQXZHMDkfkUSzYcmXO59kD+hBg4TMJIy7nSqZura/54/aeKQh3
21 | jOCw2d18pa3NRmtvL2M3oNCvsVGDpUSpFKv1Wc2XxToo4bfygvNIErKBAoGAG8WS
22 | 1S0zFuwc0R534Ays8KDxwJ4m/4QhdE6oUdU2TRhpisUnOljT6B/Wv7ZP262GYGuE
23 | lAKZSc3zSbRESvmCA374Mown4iiGZNQUtatASBap68kmoh2/zjwDTt4Wh0iIqMca
24 | A24lH3g5Sr7r2BENnFa6Cy8SYqcE+HJA6vaw5QMCgYEA1t7r61aFccE0Y2+M3o/x
25 | uBjAFtJWVWyH3RuVFAkbtSxobROCZUvFutAoBDAXYuvsty7RcBUTSlbXLZ1ACORY
26 | bXmqM1woJF20YPZCm2ZfDTolxcMuxrWdjksS4k31cjioqZfwBfPL4Sx9fHW7P/Q3
27 | N6L4iECROdG7T1+DldDr380=
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/config.nims:
--------------------------------------------------------------------------------
1 | # begin Nimble config (version 1)
2 | when fileExists("nimble.paths"):
3 | include "nimble.paths"
4 | # end Nimble config
5 |
--------------------------------------------------------------------------------
/examples/autobahn_client.nim:
--------------------------------------------------------------------------------
1 | ## nim-websock
2 | ## Copyright (c) 2021 Status Research & Development GmbH
3 | ## Licensed under either of
4 | ## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5 | ## * MIT license ([LICENSE-MIT](LICENSE-MIT))
6 | ## at your option.
7 | ## This file may not be copied, modified, or distributed except according to
8 | ## those terms.
9 |
10 | import
11 | std/[strutils],
12 | pkg/[chronos, chronicles, stew/byteutils],
13 | ../websock/[websock, types, frame, extensions/compression/deflate]
14 |
15 | const
16 | clientFlags = {NoVerifyHost, NoVerifyServerName}
17 |
18 | # we want to run parallel tests in CI,
19 | # so we are using different port
20 | when defined tls:
21 | const
22 | agent = "websock-secure-client"
23 | secure = true
24 | serverPort = 9002
25 | else:
26 | const
27 | agent = "websock-client"
28 | secure = false
29 | serverPort = 9001
30 |
31 | proc connectServer(path: string, factories: seq[ExtFactory] = @[]): Future[WSSession] {.async.} =
32 | let ws = await WebSocket.connect(
33 | host = "127.0.0.1:$1" % [$serverPort],
34 | path = path,
35 | secure = secure,
36 | flags = clientFlags,
37 | factories = factories
38 | )
39 | return ws
40 |
41 | proc getCaseCount(): Future[int] {.async.} =
42 | var caseCount = 0
43 | block:
44 | try:
45 | let ws = await connectServer("/getCaseCount")
46 | let buff = await ws.recvMsg()
47 | let dataStr = string.fromBytes(buff)
48 | caseCount = parseInt(dataStr)
49 | await ws.close()
50 | break
51 | except WebSocketError as exc:
52 | error "WebSocket error", exception = exc.msg
53 | except ValueError as exc:
54 | error "ParseInt error", exception = exc.msg
55 |
56 | return caseCount
57 |
58 | proc generateReport() {.async.} =
59 | try:
60 | trace "request autobahn server to generate report"
61 | let ws = await connectServer("/updateReports?agent=" & agent)
62 | while true:
63 | let buff = await ws.recvMsg()
64 | if buff.len <= 0:
65 | break
66 | await ws.close()
67 | except WebSocketError as exc:
68 | error "WebSocket error", exception = exc.msg
69 |
70 | proc main() {.async.} =
71 | let caseCount = await getCaseCount()
72 | trace "case count", count=caseCount
73 |
74 | var deflateFactory = @[deflateFactory()]
75 | for i in 1..caseCount:
76 | trace "runcase", no=i
77 | let path = "/runCase?case=$1&agent=$2" % [$i, agent]
78 | try:
79 | let ws = await connectServer(path, deflateFactory)
80 |
81 | while ws.readyState != ReadyState.Closed:
82 | # echo back
83 | let data = await ws.recvMsg()
84 | let opCode = if ws.binary:
85 | Opcode.Binary
86 | else:
87 | Opcode.Text
88 |
89 | if ws.readyState == ReadyState.Closed:
90 | break
91 |
92 | await ws.send(data, opCode)
93 |
94 | except WebSocketError as exc:
95 | error "WebSocket error", exception = exc.msg
96 |
97 | await generateReport()
98 |
99 | waitFor main()
100 |
--------------------------------------------------------------------------------
/examples/client.nim:
--------------------------------------------------------------------------------
1 | ## nim-websock
2 | ## Copyright (c) 2021 Status Research & Development GmbH
3 | ## Licensed under either of
4 | ## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5 | ## * MIT license ([LICENSE-MIT](LICENSE-MIT))
6 | ## at your option.
7 | ## This file may not be copied, modified, or distributed except according to
8 | ## those terms.
9 |
10 | import pkg/[
11 | chronos,
12 | chronicles,
13 | stew/byteutils]
14 |
15 | import ../websock/websock
16 |
17 | proc main() {.async.} =
18 | let ws = when defined tls:
19 | await WebSocket.connect(
20 | "127.0.0.1:8889",
21 | path = "/wss",
22 | secure = true,
23 | flags = {TLSFlags.NoVerifyHost, TLSFlags.NoVerifyServerName})
24 | else:
25 | await WebSocket.connect(
26 | "127.0.0.1:8888",
27 | path = "/ws")
28 |
29 | trace "Websocket client: ", State = ws.readyState
30 |
31 | let reqData = "Hello Server"
32 | while true:
33 | try:
34 | await ws.send(reqData)
35 | let buff = await ws.recv()
36 | if buff.len <= 0:
37 | break
38 |
39 | let dataStr = string.fromBytes(buff)
40 | trace "Server Response: ", data = dataStr
41 |
42 | doAssert dataStr == reqData
43 | break
44 | except WebSocketError as exc:
45 | error "WebSocket error:", exception = exc.msg
46 | raise exc
47 |
48 | await sleepAsync(100.millis)
49 |
50 | # close the websocket
51 | await ws.close()
52 |
53 | waitFor(main())
54 |
--------------------------------------------------------------------------------
/examples/server.nim:
--------------------------------------------------------------------------------
1 | ## nim-websock
2 | ## Copyright (c) 2021 Status Research & Development GmbH
3 | ## Licensed under either of
4 | ## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5 | ## * MIT license ([LICENSE-MIT](LICENSE-MIT))
6 | ## at your option.
7 | ## This file may not be copied, modified, or distributed except according to
8 | ## those terms.
9 |
10 | import std/uri
11 | import pkg/[chronos,
12 | chronicles,
13 | httputils]
14 |
15 | import ../websock/[websock, extensions/compression/deflate]
16 | import ../tests/keys
17 |
18 | proc handle(request: HttpRequest) {.async.} =
19 | trace "Handling request:", uri = request.uri.path
20 |
21 | try:
22 | let deflateFactory = deflateFactory()
23 | let server = WSServer.new(factories = [deflateFactory])
24 | let ws = await server.handleRequest(request)
25 | if ws.readyState != Open:
26 | error "Failed to open websocket connection"
27 | return
28 |
29 | trace "Websocket handshake completed"
30 | while ws.readyState != ReadyState.Closed:
31 | let recvData = await ws.recvMsg()
32 | trace "Client Response: ", size = recvData.len, binary = ws.binary
33 |
34 | if ws.readyState == ReadyState.Closed:
35 | # if session already terminated by peer,
36 | # no need to send response
37 | break
38 |
39 | await ws.send(recvData,
40 | if ws.binary: Opcode.Binary else: Opcode.Text)
41 |
42 | except WebSocketError as exc:
43 | error "WebSocket error:", exception = exc.msg
44 |
45 | when isMainModule:
46 | # we want to run parallel tests in CI
47 | # so we are using different port
48 | proc main() {.async.} =
49 | let
50 | socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
51 | server = when defined tls:
52 | TlsHttpServer.create(
53 | address = initTAddress("127.0.0.1:8889"),
54 | tlsPrivateKey = TLSPrivateKey.init(SecureKey),
55 | tlsCertificate = TLSCertificate.init(SecureCert),
56 | flags = socketFlags)
57 | else:
58 | HttpServer.create(initTAddress("127.0.0.1:8888"), flags = socketFlags)
59 |
60 | when defined accepts:
61 | proc accepts() {.async, raises: [].} =
62 | while true:
63 | try:
64 | let req = await server.accept()
65 | await req.handle()
66 | except CatchableError as exc:
67 | error "Transport error", exc = exc.msg
68 |
69 | asyncCheck accepts()
70 | else:
71 | server.handler = handle
72 | server.start()
73 |
74 | trace "Server listening on ", data = $server.localAddress()
75 | await server.join()
76 |
77 | waitFor(main())
78 |
--------------------------------------------------------------------------------
/nim.cfg:
--------------------------------------------------------------------------------
1 | # Avoid some rare stack corruption while using exceptions with a SEH-enabled
2 | # toolchain: https://github.com/status-im/nimbus-eth2/issues/3121
3 | @if windows and not vcc:
4 | --define:nimRawSetjmp
5 | @end
6 |
--------------------------------------------------------------------------------
/tests/all_tests.nim:
--------------------------------------------------------------------------------
1 | ## nim-websock
2 | ## Copyright (c) 2021 Status Research & Development GmbH
3 | ## Licensed under either of
4 | ## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5 | ## * MIT license ([LICENSE-MIT](LICENSE-MIT))
6 | ## at your option.
7 | ## This file may not be copied, modified, or distributed except according to
8 | ## those terms.
9 |
10 | {. warning[UnusedImport]:off .}
11 |
12 | import ./testframes
13 | import ./testutf8
14 | import ./testextutils
15 | import ./extensions/testexts
16 | import ./extensions/testcompression
17 | import ./testhooks
18 |
--------------------------------------------------------------------------------
/tests/extensions/base64ext.nim:
--------------------------------------------------------------------------------
1 | ## nim-websock
2 | ## Copyright (c) 2021 Status Research & Development GmbH
3 | ## Licensed under either of
4 | ## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5 | ## * MIT license ([LICENSE-MIT](LICENSE-MIT))
6 | ## at your option.
7 | ## This file may not be copied, modified, or distributed except according to
8 | ## those terms.
9 |
10 | import
11 | pkg/[stew/results,
12 | stew/base64,
13 | chronos,
14 | chronicles],
15 | ../../websock/types,
16 | ../../websock/frame
17 |
18 | type
19 | Base64Ext = ref object of Ext
20 | padding: bool
21 | transform: bool
22 |
23 | const
24 | extID = "base64"
25 |
26 | method decode(ext: Base64Ext, frame: Frame): Future[Frame] {.async.} =
27 | if frame.opcode notin {Opcode.Text, Opcode.Binary, Opcode.Cont}:
28 | return frame
29 |
30 | if frame.opcode in {Opcode.Text, Opcode.Binary}:
31 | ext.transform = frame.rsv2
32 | frame.rsv2 = false
33 |
34 | if not ext.transform:
35 | return frame
36 |
37 | if frame.length > 0:
38 | var data: seq[byte]
39 | var buf: array[0xFFFF, byte]
40 |
41 | while data.len < frame.length.int:
42 | let len = min(frame.length.int - data.len, buf.len)
43 | let read = await frame.read(ext.session.stream.reader, addr buf[0], len)
44 | data.add toOpenArray(buf, 0, read - 1)
45 |
46 | if data.len > ext.session.frameSize:
47 | raise newException(WSPayloadTooLarge, "payload exceeds allowed max frame size")
48 |
49 | # bug in Base64.Decode when accepts seq[byte]
50 | let instr = cast[string](data)
51 | if ext.padding:
52 | frame.data = Base64Pad.decode(instr)
53 | else:
54 | frame.data = Base64.decode(instr)
55 |
56 | trace "Base64Ext decode", input=frame.length, output=frame.data.len
57 |
58 | frame.length = frame.data.len.uint64
59 | frame.offset = 0
60 | frame.consumed = 0
61 | frame.mask = false
62 |
63 | return frame
64 |
65 | method encode(ext: Base64Ext, frame: Frame): Future[Frame] {.async.} =
66 | if frame.opcode notin {Opcode.Text, Opcode.Binary, Opcode.Cont}:
67 | return frame
68 |
69 | if frame.opcode in {Opcode.Text, Opcode.Binary}:
70 | ext.transform = true
71 | frame.rsv2 = ext.transform
72 |
73 | if not ext.transform:
74 | return frame
75 |
76 | frame.length = frame.data.len.uint64
77 |
78 | if ext.padding:
79 | frame.data = cast[seq[byte]](Base64Pad.encode(frame.data))
80 | else:
81 | frame.data = cast[seq[byte]](Base64.encode(frame.data))
82 |
83 | trace "Base64Ext encode", input=frame.length, output=frame.data.len
84 |
85 | frame.length = frame.data.len.uint64
86 | frame.offset = 0
87 | frame.consumed = 0
88 |
89 | return frame
90 |
91 | method toHttpOptions(ext: Base64Ext): string =
92 | extID & "; pad=" & $ext.padding
93 |
94 | proc base64Factory*(padding: bool): ExtFactory =
95 |
96 | proc factory(isServer: bool,
97 | args: seq[ExtParam]): Result[Ext, string] {.
98 | gcsafe, raises: [].} =
99 |
100 | # you can capture configuration variables via closure
101 | # if you want
102 |
103 | var ext = Base64Ext(
104 | name : extID,
105 | transform: false
106 | )
107 |
108 | for arg in args:
109 | if arg.name == "pad":
110 | ext.padding = arg.value == "true"
111 | break
112 |
113 | ok(ext)
114 |
115 | ExtFactory(
116 | name: extID,
117 | factory: factory,
118 | clientOffer: extID & "; pad=" & $padding
119 | )
120 |
--------------------------------------------------------------------------------
/tests/extensions/data/fireworks.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/status-im/nim-websock/d5cd89062cd2d168ef35193c7d29d2102921d97e/tests/extensions/data/fireworks.jpg
--------------------------------------------------------------------------------
/tests/extensions/hexext.nim:
--------------------------------------------------------------------------------
1 | ## nim-websock
2 | ## Copyright (c) 2021 Status Research & Development GmbH
3 | ## Licensed under either of
4 | ## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5 | ## * MIT license ([LICENSE-MIT](LICENSE-MIT))
6 | ## at your option.
7 | ## This file may not be copied, modified, or distributed except according to
8 | ## those terms.
9 |
10 | import
11 | pkg/[stew/results,
12 | stew/byteutils,
13 | chronos,
14 | chronicles],
15 | ../../websock/types,
16 | ../../websock/frame
17 |
18 | type
19 | HexExt = ref object of Ext
20 | transform: bool
21 |
22 | const
23 | extID = "hex"
24 |
25 | method decode(ext: HexExt, frame: Frame): Future[Frame] {.async.} =
26 | if frame.opcode notin {Opcode.Text, Opcode.Binary, Opcode.Cont}:
27 | return frame
28 |
29 | if frame.opcode in {Opcode.Text, Opcode.Binary}:
30 | ext.transform = frame.rsv3
31 | frame.rsv3 = false
32 |
33 | if not ext.transform:
34 | return frame
35 |
36 | if frame.length > 0:
37 | var data: seq[byte]
38 | var buf: array[0xFFFF, byte]
39 |
40 | while data.len < frame.length.int:
41 | let len = min(frame.length.int - data.len, buf.len)
42 | let read = await frame.read(ext.session.stream.reader, addr buf[0], len)
43 | data.add toOpenArray(buf, 0, read - 1)
44 |
45 | if data.len > ext.session.frameSize:
46 | raise newException(WSPayloadTooLarge, "payload exceeds allowed max frame size")
47 |
48 | frame.data = hexToSeqByte(cast[string](data))
49 | trace "HexExt decode", input=frame.length, output=frame.data.len
50 |
51 | frame.length = frame.data.len.uint64
52 | frame.offset = 0
53 | frame.consumed = 0
54 | frame.mask = false
55 |
56 | return frame
57 |
58 | method encode(ext: HexExt, frame: Frame): Future[Frame] {.async.} =
59 | if frame.opcode notin {Opcode.Text, Opcode.Binary, Opcode.Cont}:
60 | return frame
61 |
62 | if frame.opcode in {Opcode.Text, Opcode.Binary}:
63 | ext.transform = true
64 | frame.rsv3 = ext.transform
65 |
66 | if not ext.transform:
67 | return frame
68 |
69 | frame.length = frame.data.len.uint64
70 | frame.data = cast[seq[byte]](toHex(frame.data))
71 | trace "HexExt encode", input=frame.length, output=frame.data.len
72 |
73 | frame.length = frame.data.len.uint64
74 | frame.offset = 0
75 | frame.consumed = 0
76 |
77 | return frame
78 |
79 | method toHttpOptions(ext: HexExt): string =
80 | extID
81 |
82 | proc hexFactory*(): ExtFactory =
83 |
84 | proc factory(isServer: bool,
85 | args: seq[ExtParam]): Result[Ext, string] {.
86 | gcsafe, raises: [].} =
87 |
88 | # you can capture configuration variables via closure
89 | # if you want
90 |
91 | var ext = HexExt(
92 | name : extID,
93 | transform: false
94 | )
95 |
96 | ok(ext)
97 |
98 | ExtFactory(
99 | name: extID,
100 | factory: factory,
101 | clientOffer: extID
102 | )
103 |
--------------------------------------------------------------------------------
/tests/extensions/testcompression.nim:
--------------------------------------------------------------------------------
1 | ## nim-websock
2 | ## Copyright (c) 2021-2022 Status Research & Development GmbH
3 | ## Licensed under either of
4 | ## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5 | ## * MIT license ([LICENSE-MIT](LICENSE-MIT))
6 | ## at your option.
7 | ## This file may not be copied, modified, or distributed except according to
8 | ## those terms.
9 |
10 | import std/[os, strutils]
11 | import pkg/[chronos/unittest2/asynctests, stew/io2]
12 | import ../../websock/websock
13 | import ../../websock/extensions/compression/deflate
14 |
15 | const
16 | dataFolder = currentSourcePath.rsplit(os.DirSep, 1)[0] / "data"
17 |
18 | suite "permessage deflate compression":
19 | setup:
20 | var server: HttpServer
21 | let address = initTAddress("127.0.0.1:8888")
22 | let deflateFactory = deflateFactory()
23 |
24 | teardown:
25 | if server != nil:
26 | server.stop()
27 | waitFor server.closeWait()
28 |
29 | asyncTest "text compression":
30 | let textData = io2.readAllBytes(dataFolder / "alice29.txt").get()
31 | proc handle(request: HttpRequest) {.async.} =
32 | let server = WSServer.new(
33 | protos = ["proto"],
34 | factories = [deflateFactory],
35 | )
36 | let ws = await server.handleRequest(request)
37 |
38 | while ws.readyState != ReadyState.Closed:
39 | let recvData = await ws.recvMsg()
40 | if ws.readyState == ReadyState.Closed:
41 | break
42 | await ws.send(recvData,
43 | if ws.binary: Opcode.Binary else: Opcode.Text)
44 |
45 | server = HttpServer.create(
46 | address,
47 | handle,
48 | flags = {ReuseAddr})
49 | server.start()
50 |
51 | let client = await WebSocket.connect(
52 | host = "127.0.0.1:8888",
53 | path = "/ws",
54 | protocols = @["proto"],
55 | factories = @[deflateFactory]
56 | )
57 |
58 | await client.send(textData, Opcode.Text)
59 |
60 | var recvData: seq[byte]
61 | while recvData.len < textData.len:
62 | let res = await client.recvMsg()
63 | recvData.add res
64 | if client.readyState == ReadyState.Closed:
65 | break
66 |
67 | check textData == recvData
68 | await client.close()
69 |
70 | asyncTest "binary data compression":
71 | let binaryData = io2.readAllBytes(dataFolder / "fireworks.jpg").get()
72 | proc handle(request: HttpRequest) {.async.} =
73 | let server = WSServer.new(
74 | protos = ["proto"],
75 | factories = [deflateFactory],
76 | )
77 | let ws = await server.handleRequest(request)
78 | while ws.readyState != ReadyState.Closed:
79 | let recvData = await ws.recvMsg()
80 | if ws.readyState == ReadyState.Closed:
81 | break
82 | await ws.send(recvData,
83 | if ws.binary: Opcode.Binary else: Opcode.Text)
84 |
85 | server = HttpServer.create(
86 | address,
87 | handle,
88 | flags = {ReuseAddr})
89 | server.start()
90 |
91 | let client = await WebSocket.connect(
92 | host = "127.0.0.1:8888",
93 | path = "/ws",
94 | protocols = @["proto"],
95 | factories = @[deflateFactory]
96 | )
97 |
98 | await client.send(binaryData, Opcode.Binary)
99 |
100 | var recvData: seq[byte]
101 | while recvData.len < binaryData.len:
102 | let res = await client.recvMsg()
103 | recvData.add res
104 | if client.readyState == ReadyState.Closed:
105 | break
106 |
107 | check binaryData == recvData
108 |
109 | await client.close()
110 |
--------------------------------------------------------------------------------
/tests/extensions/testextflow.nim:
--------------------------------------------------------------------------------
1 | ## nim-websock
2 | ## Copyright (c) 2021-2023 Status Research & Development GmbH
3 | ## Licensed under either of
4 | ## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5 | ## * MIT license ([LICENSE-MIT](LICENSE-MIT))
6 | ## at your option.
7 | ## This file may not be copied, modified, or distributed except according to
8 | ## those terms.
9 |
10 | import std/strutils
11 | import pkg/[chronos, stew/byteutils]
12 | import pkg/asynctest/unittest2
13 |
14 | import ../../ws/ws
15 |
16 | type
17 | ExtHandler = proc(ext: Ext, frame: Frame): Future[Frame] {.raises: [].}
18 |
19 | HelperExtension = ref object of Ext
20 | handler*: ExtHandler
21 |
22 | proc new*(
23 | T: typedesc[HelperExtension],
24 | handler: ExtHandler,
25 | session: WSSession = nil): HelperExtension =
26 | HelperExtension(
27 | handler: handler,
28 | name: "HelperExtension")
29 |
30 | method decode*(
31 | self: HelperExtension,
32 | frame: Frame): Future[Frame] {.async.} =
33 | return await self.handler(self, frame)
34 |
35 | method encode*(
36 | self: HelperExtension,
37 | frame: Frame): Future[Frame] {.async.} =
38 | return await self.handler(self, frame)
39 |
40 | const TestString = "Hello"
41 |
42 | suite "Encode frame extensions flow":
43 | test "should call extension on encode":
44 | var data = ""
45 | proc toUpper(ext: Ext, frame: Frame): Future[Frame] {.async.} =
46 | checkpoint "toUpper executed"
47 | data = string.fromBytes(frame.data).toUpper()
48 | check TestString.toUpper() == data
49 | frame.data = data.toBytes()
50 | return frame
51 |
52 | var frame = Frame(
53 | fin: false,
54 | rsv1: false,
55 | rsv2: false,
56 | rsv3: false,
57 | opcode: Opcode.Text,
58 | mask: false,
59 | data: TestString.toBytes())
60 |
61 | discard await frame.encode(@[HelperExtension.new(toUpper).Ext])
62 | check frame.data == TestString.toUpper().toBytes()
63 |
64 | test "should call extensions in correct order on encode":
65 | var count = 0
66 | proc first(ext: Ext, frame: Frame): Future[Frame] {.async.} =
67 | checkpoint "first executed"
68 | check count == 0
69 | count.inc
70 |
71 | return frame
72 |
73 | proc second(ext: Ext, frame: Frame): Future[Frame] {.async.} =
74 | checkpoint "second executed"
75 | check count == 1
76 | count.inc
77 |
78 | return frame
79 |
80 | var frame = Frame(
81 | fin: false,
82 | rsv1: false,
83 | rsv2: false,
84 | rsv3: false,
85 | opcode: Opcode.Text,
86 | mask: false,
87 | data: TestString.toBytes())
88 |
89 | discard await frame.encode(@[
90 | HelperExtension.new(first).Ext,
91 | HelperExtension.new(second).Ext])
92 |
93 | check count == 2
94 |
95 | test "should allow modifying frame headers":
96 | proc changeHeader(ext: Ext, frame: Frame): Future[Frame] {.async.} =
97 | checkpoint "changeHeader executed"
98 | frame.rsv1 = true
99 | frame.rsv2 = true
100 | frame.rsv3 = true
101 | frame.opcode = Opcode.Binary
102 | return frame
103 |
104 | var frame = Frame(
105 | fin: false,
106 | rsv1: false,
107 | rsv2: false,
108 | rsv3: false,
109 | opcode: Opcode.Text, # fragments have to be `Continuation` frames
110 | mask: false,
111 | data: TestString.toBytes())
112 |
113 | discard await frame.encode(@[HelperExtension.new(changeHeader).Ext])
114 | check:
115 | frame.rsv1 == true
116 | frame.rsv2 == true
117 | frame.rsv2 == true
118 | frame.opcode == Opcode.Binary
119 |
120 | suite "Decode frame extensions flow":
121 | let rng = HmacDrbgContext.new()
122 | var
123 | address: TransportAddress
124 | server: StreamServer
125 | maskKey = MaskKey.random(rng[])
126 | transport: StreamTransport
127 | reader: AsyncStreamReader
128 | frame: Frame
129 |
130 | setup:
131 | server = createStreamServer(
132 | initTAddress("127.0.0.1:0"),
133 | flags = {ServerFlags.ReuseAddr})
134 | address = server.localAddress()
135 |
136 | teardown:
137 | await transport.closeWait()
138 | await server.closeWait()
139 | server.stop()
140 |
141 | test "should call extension on decode":
142 | var data = ""
143 | proc toUpper(ext: Ext, frame: Frame): Future[Frame] {.async.} =
144 | checkpoint "toUpper executed"
145 | try:
146 | var buf = newSeq[byte](frame.length)
147 | # read data
148 | await reader.readExactly(addr buf[0], buf.len)
149 | if frame.mask:
150 | mask(buf, maskKey)
151 | frame.mask = false # we can reset the mask key here
152 |
153 | data = string.fromBytes(buf).toUpper()
154 | check:
155 | TestString.toUpper() == data
156 |
157 | frame.data = data.toBytes()
158 | return frame
159 | except CatchableError as exc:
160 | checkpoint exc.msg
161 | check false
162 |
163 | proc acceptHandler() {.async, gcsafe.} =
164 | let transport = await server.accept()
165 | reader = newAsyncStreamReader(transport)
166 | frame = await Frame.decode(
167 | reader,
168 | false,
169 | @[HelperExtension.new(toUpper).Ext])
170 |
171 | await reader.closeWait()
172 | await transport.closeWait()
173 |
174 | let handlerWait = acceptHandler()
175 | var encodedFrame = (await Frame(
176 | fin: false,
177 | rsv1: false,
178 | rsv2: false,
179 | rsv3: false,
180 | opcode: Opcode.Text,
181 | mask: true,
182 | maskKey: maskKey,
183 | data: TestString.toBytes())
184 | .encode())
185 |
186 | transport = await connect(address)
187 | let wrote = await transport.write(encodedFrame)
188 |
189 | await handlerWait
190 | check:
191 | wrote == encodedFrame.len
192 | frame.data == TestString.toUpper().toBytes()
193 |
194 | test "should call extensions in reverse order on decode":
195 | var count = 0
196 | proc first(ext: Ext, frame: Frame): Future[Frame] {.async.} =
197 | checkpoint "first executed"
198 | check count == 1
199 | count.inc
200 |
201 | return frame
202 |
203 | proc second(ext: Ext, frame: Frame): Future[Frame] {.async.} =
204 | checkpoint "second executed"
205 | check count == 0
206 | count.inc
207 |
208 | return frame
209 |
210 | proc acceptHandler() {.async, gcsafe.} =
211 | let transport = await server.accept()
212 | reader = newAsyncStreamReader(transport)
213 | frame = await Frame.decode(
214 | reader,
215 | false,
216 | @[HelperExtension.new(first).Ext,
217 | HelperExtension.new(second).Ext])
218 |
219 | await reader.closeWait()
220 | await transport.closeWait()
221 |
222 | let handlerWait = acceptHandler()
223 | var encodedFrame = (await Frame(
224 | fin: false,
225 | rsv1: false,
226 | rsv2: false,
227 | rsv3: false,
228 | opcode: Opcode.Text,
229 | mask: true,
230 | maskKey: maskKey,
231 | data: TestString.toBytes())
232 | .encode())
233 |
234 | let transport = await connect(address)
235 | let wrote = await transport.write(encodedFrame)
236 |
237 | await handlerWait
238 | check:
239 | wrote == encodedFrame.len
240 | count == 2
241 |
242 | test "should allow modifying frame headers":
243 | proc changeHeader(ext: Ext, frame: Frame): Future[Frame] {.async.} =
244 | checkpoint "changeHeader executed"
245 | frame.rsv1 = false
246 | frame.rsv2 = false
247 | frame.rsv3 = false
248 | frame.opcode = Opcode.Binary
249 |
250 | return frame
251 |
252 | proc acceptHandler() {.async, gcsafe.} =
253 | let transport = await server.accept()
254 | reader = newAsyncStreamReader(transport)
255 | frame = await Frame.decode(
256 | reader,
257 | false,
258 | @[HelperExtension.new(changeHeader).Ext])
259 |
260 | check:
261 | frame.rsv1 == false
262 | frame.rsv2 == false
263 | frame.rsv2 == false
264 | frame.opcode == Opcode.Binary
265 |
266 | await reader.closeWait()
267 | await transport.closeWait()
268 |
269 | let handlerWait = acceptHandler()
270 | var encodedFrame = (await Frame(
271 | fin: false,
272 | rsv1: true,
273 | rsv2: true,
274 | rsv3: true,
275 | opcode: Opcode.Text,
276 | mask: true,
277 | maskKey: maskKey,
278 | data: TestString.toBytes())
279 | .encode())
280 |
281 | let transport = await connect(address)
282 | let wrote = await transport.write(encodedFrame)
283 |
284 | await handlerWait
285 | check:
286 | wrote == encodedFrame.len
287 |
--------------------------------------------------------------------------------
/tests/extensions/testexts.nim:
--------------------------------------------------------------------------------
1 | ## nim-websock
2 | ## Copyright (c) 2021-2022 Status Research & Development GmbH
3 | ## Licensed under either of
4 | ## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5 | ## * MIT license ([LICENSE-MIT](LICENSE-MIT))
6 | ## at your option.
7 | ## This file may not be copied, modified, or distributed except according to
8 | ## those terms.
9 |
10 | import pkg/[chronos/unittest2/asynctests, stew/byteutils]
11 | import ./base64ext, ./hexext
12 | import ../../websock/websock, ../helpers
13 |
14 | suite "multiple extensions flow":
15 | setup:
16 | var server: HttpServer
17 | let address = initTAddress("127.0.0.1:8888")
18 | let hexFactory = hexFactory()
19 | let base64Factory = base64Factory(padding = true)
20 |
21 | teardown:
22 | if server != nil:
23 | server.stop()
24 | waitFor server.closeWait()
25 |
26 | asyncTest "hex to base64 ext flow":
27 | let testData = "hello world"
28 | proc handle(request: HttpRequest) {.async.} =
29 | let server = WSServer.new(
30 | protos = ["proto"],
31 | factories = [hexFactory, base64Factory],
32 | )
33 | let ws = await server.handleRequest(request)
34 | let recvData = await ws.recvMsg()
35 | await ws.send(recvData,
36 | if ws.binary: Opcode.Binary else: Opcode.Text)
37 |
38 | await waitForClose(ws)
39 |
40 | server = HttpServer.create(
41 | address,
42 | handle,
43 | flags = {ReuseAddr})
44 | server.start()
45 |
46 | let client = await WebSocket.connect(
47 | host = "127.0.0.1:8888",
48 | path = "/ws",
49 | protocols = @["proto"],
50 | factories = @[hexFactory, base64Factory]
51 | )
52 |
53 | await client.send(testData)
54 | let res = await client.recvMsg()
55 | check testData.toBytes() == res
56 | await client.close()
57 |
58 | asyncTest "base64 to hex ext flow":
59 | let testData = "hello world"
60 | proc handle(request: HttpRequest) {.async.} =
61 | let server = WSServer.new(
62 | protos = ["proto"],
63 | factories = [hexFactory, base64Factory],
64 | )
65 | let ws = await server.handleRequest(request)
66 | let recvData = await ws.recvMsg()
67 | await ws.send(recvData,
68 | if ws.binary: Opcode.Binary else: Opcode.Text)
69 |
70 | await waitForClose(ws)
71 |
72 | server = HttpServer.create(
73 | address,
74 | handle,
75 | flags = {ReuseAddr})
76 | server.start()
77 |
78 | let client = await WebSocket.connect(
79 | host = "127.0.0.1:8888",
80 | path = "/ws",
81 | protocols = @["proto"],
82 | factories = @[base64Factory, hexFactory]
83 | )
84 |
85 | await client.send(testData)
86 | let res = await client.recvMsg()
87 | check testData.toBytes() == res
88 | await client.close()
89 |
--------------------------------------------------------------------------------
/tests/helpers.nim:
--------------------------------------------------------------------------------
1 | ## nim-websock
2 | ## Copyright (c) 2021-2023 Status Research & Development GmbH
3 | ## Licensed under either of
4 | ## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5 | ## * MIT license ([LICENSE-MIT](LICENSE-MIT))
6 | ## at your option.
7 | ## This file may not be copied, modified, or distributed except according to
8 | ## those terms.
9 |
10 | {.push raises: [].}
11 |
12 | import std/[strutils, random]
13 | import pkg/[
14 | chronos,
15 | chronos/streams/tlsstream,
16 | httputils,
17 | chronicles]
18 |
19 | import ../websock/websock
20 | import ./keys
21 |
22 | {.push gcsafe, raises: [].}
23 |
24 | const WSPath* = when defined secure: "/wss" else: "/ws"
25 |
26 | proc rndStr*(size: int): string {.gcsafe, raises: [].} =
27 | for _ in 0..= res.len:
1163 | break
1164 |
1165 | res.setLen(pos)
1166 | check res.len == howMuchWood.toBytes().len
1167 | check res == howMuchWood.toBytes()
1168 | await ws.waitForClose()
1169 |
1170 | server = createServer(
1171 | address = address,
1172 | handler = handle,
1173 | flags = {ReuseAddr})
1174 |
1175 | let session = await connectClient(
1176 | address = address,
1177 | frameSize = senderFrameSize)
1178 |
1179 | await session.send(howMuchWood)
1180 | await session.close()
1181 |
1182 | teardown:
1183 | if server != nil:
1184 | server.stop()
1185 | waitFor server.closeWait()
1186 |
1187 | asyncTest "read in chunks less than sender frameSize":
1188 | await lowLevelRecv(7, 7, 5)
1189 |
1190 | asyncTest "read in chunks greater than sender frameSize":
1191 | await lowLevelRecv(3, 7, 5)
1192 |
1193 | asyncTest "sender frameSize greater than receiver":
1194 | await lowLevelRecv(7, 5, 5)
1195 |
1196 | asyncTest "receiver frameSize greater than sender":
1197 | await lowLevelRecv(7, 10, 5)
1198 |
--------------------------------------------------------------------------------
/websock.nimble:
--------------------------------------------------------------------------------
1 | ## nim-websock
2 | ## Copyright (c) 2023 Status Research & Development GmbH
3 | ## Licensed under either of
4 | ## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5 | ## * MIT license ([LICENSE-MIT](LICENSE-MIT))
6 | ## at your option.
7 | ## This file may not be copied, modified, or distributed except according to
8 | ## those terms.
9 |
10 | packageName = "websock"
11 | version = "0.2.0"
12 | author = "Status Research & Development GmbH"
13 | description = "WS protocol implementation"
14 | license = "MIT"
15 | skipDirs = @["examples", "tests"]
16 |
17 | requires "nim >= 1.6.0"
18 | requires "chronos >= 4.0.3 & < 4.1.0"
19 | requires "httputils >= 0.2.0"
20 | requires "chronicles >= 0.10.2"
21 | requires "stew >= 0.1.0"
22 | requires "nimcrypto"
23 | requires "bearssl"
24 | requires "zlib"
25 |
26 | task test, "run tests":
27 | let
28 | envNimflags = getEnv("NIMFLAGS")
29 | nimFlags = envNimFlags &
30 | " --verbosity:0 --hints:off --hint:Name:on " &
31 | "--styleCheck:usages --styleCheck:error" &
32 | " -d:chronosStrictException --mm:refc"
33 |
34 | # dont't need to run it, only want to test if it is compileable
35 | exec "nim c -c " & nimFlags & " -d:chronicles_log_level=TRACE -d:chronicles_sinks:json --styleCheck:usages --styleCheck:hint ./tests/all_tests"
36 |
37 | exec "nim c -r " & nimFlags & " --opt:speed -d:debug -d:chronicles_log_level=INFO ./tests/all_tests.nim"
38 | rmFile "./tests/all_tests"
39 |
40 | exec "nim c -r " & nimFlags & " --opt:speed -d:debug -d:chronicles_log_level=INFO ./tests/testwebsockets.nim"
41 | rmFile "./tests/testwebsockets"
42 |
43 | exec "nim -d:secure c -r " & nimFlags & " --opt:speed -d:debug -d:chronicles_log_level=INFO ./tests/testwebsockets.nim"
44 | rmFile "./tests/testwebsockets"
45 |
46 | exec "nim -d:accepts c -r " & nimFlags & " --opt:speed -d:debug -d:chronicles_log_level=INFO ./tests/testwebsockets.nim"
47 | rmFile "./tests/testwebsockets"
48 |
49 | exec "nim -d:secure -d:accepts c -r " & nimFlags & " --opt:speed -d:debug -d:chronicles_log_level=INFO ./tests/testwebsockets.nim"
50 | rmFile "./tests/testwebsockets"
51 |
--------------------------------------------------------------------------------
/websock.svg:
--------------------------------------------------------------------------------
1 |
2 |
118 |
--------------------------------------------------------------------------------
/websock/extensions.nim:
--------------------------------------------------------------------------------
1 | ## nim-websock
2 | ## Copyright (c) 2021 Status Research & Development GmbH
3 | ## Licensed under either of
4 | ## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5 | ## * MIT license ([LICENSE-MIT](LICENSE-MIT))
6 | ## at your option.
7 | ## This file may not be copied, modified, or distributed except according to
8 | ## those terms.
9 |
10 | import ./extensions/extutils
11 | # import ./extensions/compression/compression
12 |
13 | export extutils
14 |
--------------------------------------------------------------------------------
/websock/extensions/compression/deflate.nim:
--------------------------------------------------------------------------------
1 | ## nim-websock
2 | ## Copyright (c) 2021 Status Research & Development GmbH
3 | ## Licensed under either of
4 | ## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5 | ## * MIT license ([LICENSE-MIT](LICENSE-MIT))
6 | ## at your option.
7 | ## This file may not be copied, modified, or distributed except according to
8 | ## those terms.
9 |
10 | import
11 | std/[strutils],
12 | pkg/[stew/results,
13 | chronos,
14 | chronicles,
15 | zlib],
16 | ../../types,
17 | ../../frame
18 |
19 | logScope:
20 | topics = "websock deflate"
21 |
22 | type
23 | DeflateOpts = object
24 | isServer: bool
25 | decompressLimit: int # max allowed decompression size
26 | threshold: int # size in bytes below which messages
27 | # should not be compressed.
28 | level: ZLevel # compression level
29 | strategy: ZStrategy # compression strategy
30 | memLevel: ZMemLevel # hint for zlib memory consumption
31 | serverNoContextTakeOver: bool
32 | clientNoContextTakeOver: bool
33 | serverMaxWindowBits: int
34 | clientMaxWindowBits: int
35 |
36 | ContextState {.pure.} = enum
37 | Invalid
38 | Initialized
39 | Reset
40 |
41 | DeflateExt = ref object of Ext
42 | paramStr : string
43 | opts : DeflateOpts
44 | compressedMsg : bool
45 | compCtx : ZStream
46 | compCtxState : ContextState
47 | decompCtx : ZStream
48 | decompCtxState: ContextState
49 |
50 | const
51 | extID = "permessage-deflate"
52 | TrailingBytes = [0x00.byte, 0x00.byte, 0xff.byte, 0xff.byte]
53 | ExtDeflateThreshold* = 1024
54 | ExtDeflateDecompressLimit* = 10 shl 20 # 10mb
55 |
56 | proc destroyExt(ext: DeflateExt) =
57 | if ext.compCtxState != ContextState.Invalid:
58 | # zlib.deflateEnd somehow return DATA_ERROR
59 | # when compression succeed some cases.
60 | # we forget to do something?
61 | discard ext.compCtx.deflateEnd()
62 | ext.compCtxState = ContextState.Invalid
63 |
64 | if ext.decompCtxState != ContextState.Invalid:
65 | doAssert(ext.decompCtx.inflateEnd() == Z_OK)
66 | ext.decompCtxState = ContextState.Invalid
67 |
68 | # Need to be declared early and not be generic to work with ARC/ORC
69 | proc newDeflateExt: DeflateExt =
70 | result.new(destroyExt)
71 |
72 | proc concatParam(resp: var string, param: string) =
73 | resp.add "; "
74 | resp.add param
75 |
76 | proc validateWindowBits(arg: ExtParam, res: var int): Result[string, string] =
77 | if arg.value.len == 0:
78 | return ok("")
79 |
80 | if arg.value.len > 2:
81 | return err("window bits expect 2 bytes, got " & $arg.value.len)
82 |
83 | for n in arg.value:
84 | if n notin Digits:
85 | return err("window bits value contains illegal char: " & $n)
86 |
87 | var winbit = 0
88 | for i in 0.. 15:
92 | return err("window bits should between 8-15, got " & $winbit)
93 |
94 | res = winbit
95 | return ok("=" & arg.value)
96 |
97 | proc createParams(args: seq[ExtParam],
98 | opts: var DeflateOpts): Result[string, string] =
99 | # besides validating extensions params, this proc
100 | # also constructing extension params for response
101 | var resp = ""
102 | for arg in args:
103 | case arg.name
104 | of "server_no_context_takeover":
105 | if arg.value.len > 0:
106 | return err("'server_no_context_takeover' should have no param")
107 | opts.serverNoContextTakeOver = true
108 | if opts.isServer:
109 | concatParam(resp, arg.name)
110 | of "client_no_context_takeover":
111 | if arg.value.len > 0:
112 | return err("'client_no_context_takeover' should have no param")
113 | opts.clientNoContextTakeOver = true
114 | if opts.isServer:
115 | concatParam(resp, arg.name)
116 | of "server_max_window_bits":
117 | let res = validateWindowBits(arg, opts.serverMaxWindowBits)
118 | if res.isErr:
119 | return res
120 | if opts.isServer:
121 | concatParam(resp, arg.name)
122 | if opts.serverMaxWindowBits == 8:
123 | # zlib does not support windowBits == 8
124 | resp.add "=9"
125 | else:
126 | resp.add res.get()
127 | of "client_max_window_bits":
128 | let res = validateWindowBits(arg, opts.clientMaxWindowBits)
129 | if res.isErr:
130 | return res
131 | if not opts.isServer:
132 | concatParam(resp, arg.name)
133 | if opts.clientMaxWindowBits == 8:
134 | # zlib does not support windowBits == 8
135 | resp.add "=9"
136 | else:
137 | resp.add res.get()
138 | else:
139 | return err("unrecognized param: " & arg.name)
140 |
141 | ok(resp)
142 |
143 | {.warning[HoleEnumConv]:off.}
144 | proc getWindowBits(opts: DeflateOpts, isServer: bool): ZWindowBits =
145 | if isServer:
146 | if opts.serverMaxWindowBits == 0:
147 | Z_RAW_DEFLATE
148 | else:
149 | ZWindowBits(-opts.serverMaxWindowBits)
150 | else:
151 | if opts.clientMaxWindowBits == 0:
152 | Z_RAW_DEFLATE
153 | else:
154 | ZWindowBits(-opts.clientMaxWindowBits)
155 |
156 | {.warning[HoleEnumConv]:on.}
157 |
158 | proc getContextTakeover(opts: DeflateOpts, isServer: bool): bool =
159 | if isServer:
160 | opts.serverNoContextTakeOver
161 | else:
162 | opts.clientNoContextTakeOver
163 |
164 | proc decompressInit(ext: DeflateExt) =
165 | # server decompression using `client_` prefixed config
166 | # client decompression using `server_` prefixed config
167 | let windowBits = getWindowBits(ext.opts, not ext.opts.isServer)
168 | doAssert(ext.decompCtx.inflateInit2(windowBits) == Z_OK)
169 | ext.decompCtxState = ContextState.Initialized
170 |
171 | proc compressInit(ext: DeflateExt) =
172 | # server compression using `server_` prefixed config
173 | # client compression using `client_` prefixed config
174 | let windowBits = getWindowBits(ext.opts, ext.opts.isServer)
175 | doAssert(ext.compCtx.deflateInit2(
176 | level = ext.opts.level,
177 | meth = Z_DEFLATED,
178 | windowBits,
179 | memLevel = ext.opts.memLevel,
180 | strategy = ext.opts.strategy) == Z_OK
181 | )
182 | ext.compCtxState = ContextState.Initialized
183 |
184 | proc compress(zs: var ZStream, data: openArray[byte]): seq[byte] =
185 | var buf: array[0xFFFF, byte]
186 |
187 | # these casting is needed to prevent compilation
188 | # error with CLANG
189 | zs.next_in = cast[ptr uint8](data[0].unsafeAddr)
190 | zs.avail_in = data.len.cuint
191 |
192 | while true:
193 | zs.next_out = cast[ptr uint8](buf[0].addr)
194 | zs.avail_out = buf.len.cuint
195 |
196 | let r = zs.deflate(Z_SYNC_FLUSH)
197 | let outSize = buf.len - zs.avail_out.int
198 | result.add toOpenArray(buf, 0, outSize-1)
199 |
200 | if r == Z_STREAM_END:
201 | break
202 | elif r == Z_OK:
203 | # need more input or more output available
204 | if zs.avail_in > 0 or zs.avail_out == 0:
205 | continue
206 | else:
207 | break
208 | else:
209 | raise newException(WSExtError, "compression error " & $r)
210 |
211 | proc decompress(zs: var ZStream, limit: int, data: openArray[byte]): seq[byte] =
212 | var buf: array[0xFFFF, byte]
213 |
214 | # these casting is needed to prevent compilation
215 | # error with CLANG
216 | zs.next_in = cast[ptr uint8](data[0].unsafeAddr)
217 | zs.avail_in = data.len.cuint
218 |
219 | while true:
220 | zs.next_out = cast[ptr uint8](buf[0].addr)
221 | zs.avail_out = buf.len.cuint
222 |
223 | let r = zs.inflate(Z_NO_FLUSH)
224 | let outSize = buf.len - zs.avail_out.int
225 | result.add toOpenArray(buf, 0, outSize-1)
226 |
227 | if result.len > limit:
228 | raise newException(WSExtError, "decompression exceeds allowed limit")
229 |
230 | if r == Z_STREAM_END:
231 | break
232 | elif r == Z_OK:
233 | # need more input or more output available
234 | if zs.avail_in > 0 or zs.avail_out == 0:
235 | continue
236 | else:
237 | break
238 | else:
239 | raise newException(WSExtError, "decompression error " & $r)
240 |
241 | return result
242 |
243 | method decode(ext: DeflateExt, frame: Frame): Future[Frame] {.async.} =
244 | if frame.opcode notin {Opcode.Text, Opcode.Binary, Opcode.Cont}:
245 | # only data frames can be decompressed
246 | return frame
247 |
248 | if frame.opcode in {Opcode.Text, Opcode.Binary}:
249 | # we want to know if this message is compressed or not
250 | # if the frame opcode is text or binary, it should also the first frame
251 | ext.compressedMsg = frame.rsv1
252 | # clear rsv1 bit because we already done with it
253 | frame.rsv1 = false
254 |
255 | if not ext.compressedMsg:
256 | # don't bother with uncompressed message
257 | return frame
258 |
259 | if ext.decompCtxState == ContextState.Invalid:
260 | ext.decompressInit()
261 |
262 | # even though the frame.data.len == 0, the stream needs
263 | # to be closed with trailing bytes if it's a final frame
264 |
265 | var data: seq[byte]
266 | var buf: array[0xFFFF, byte]
267 |
268 | while data.len < frame.length.int:
269 | let len = min(frame.length.int - data.len, buf.len)
270 | let read = await frame.read(ext.session.stream.reader, addr buf[0], len)
271 | data.add toOpenArray(buf, 0, read - 1)
272 |
273 | if data.len > ext.session.frameSize:
274 | raise newException(WSPayloadTooLarge, "payload exceeds allowed max frame size")
275 |
276 | if frame.fin:
277 | data.add TrailingBytes
278 |
279 | frame.data = decompress(ext.decompCtx, ext.opts.decompressLimit, data)
280 | trace "DeflateExt decompress", input=frame.length, output=frame.data.len
281 |
282 | frame.length = frame.data.len.uint64
283 | frame.offset = 0
284 | frame.consumed = 0
285 | frame.mask = false # clear mask flag, decompressed content is not masked
286 |
287 | if frame.fin:
288 | # server decompression using `client_` prefixed config
289 | # client decompression using `server_` prefixed config
290 | let noContextTakeover = getContextTakeover(ext.opts, not ext.opts.isServer)
291 | if noContextTakeover:
292 | doAssert(ext.decompCtx.inflateReset() == Z_OK)
293 | ext.decompCtxState = ContextState.Reset
294 |
295 | return frame
296 |
297 | method encode(ext: DeflateExt, frame: Frame): Future[Frame] {.async.} =
298 | if frame.opcode notin {Opcode.Text, Opcode.Binary, Opcode.Cont}:
299 | # only data frames can be compressed
300 | return frame
301 |
302 | if frame.opcode in {Opcode.Text, Opcode.Binary}:
303 | # we only set rsv1 bit to true if the message is compressible
304 | # and only set the first frame's rsv1
305 | # if the frame opcode is text or binary, it should also the first frame
306 | ext.compressedMsg = frame.data.len >= ext.opts.threshold
307 | frame.rsv1 = ext.compressedMsg
308 |
309 | if not ext.compressedMsg:
310 | # don't bother with incompressible message
311 | return frame
312 |
313 | if ext.compCtxState == ContextState.Invalid:
314 | ext.compressInit()
315 |
316 | frame.length = frame.data.len.uint64
317 | frame.data = compress(ext.compCtx, frame.data)
318 | trace "DeflateExt compress", input=frame.length, output=frame.data.len
319 |
320 | if frame.fin:
321 | # remove trailing bytes
322 | when not defined(release):
323 | var trailer: array[4, byte]
324 | trailer[0] = frame.data[^4]
325 | trailer[1] = frame.data[^3]
326 | trailer[2] = frame.data[^2]
327 | trailer[3] = frame.data[^1]
328 | doAssert trailer == TrailingBytes
329 | frame.data.setLen(frame.data.len - 4)
330 |
331 | frame.length = frame.data.len.uint64
332 | frame.offset = 0
333 | frame.consumed = 0
334 |
335 | if frame.fin:
336 | # server compression using `server_` prefixed config
337 | # client compression using `client_` prefixed config
338 | let noContextTakeover = getContextTakeover(ext.opts, ext.opts.isServer)
339 | if noContextTakeover:
340 | doAssert(ext.compCtx.deflateReset() == Z_OK)
341 | ext.compCtxState = ContextState.Reset
342 |
343 | return frame
344 |
345 | method toHttpOptions(ext: DeflateExt): string =
346 | # using paramStr here is a bit clunky
347 | extID & ext.paramStr
348 |
349 | proc makeOffer(
350 | clientNoContextTakeOver: bool,
351 | clientMaxWindowBits: int): string =
352 |
353 | var param = extID
354 | if clientMaxWindowBits in {9..15}:
355 | param.add "; client_max_window_bits=" & $clientMaxWindowBits
356 | else:
357 | param.add "; client_max_window_bits"
358 |
359 | if clientNoContextTakeOver:
360 | param.add "; client_no_context_takeover"
361 |
362 | param
363 |
364 | proc deflateFactory*(
365 | threshold = ExtDeflateThreshold,
366 | decompressLimit = ExtDeflateDecompressLimit,
367 | level = Z_DEFAULT_LEVEL,
368 | strategy = Z_DEFAULT_STRATEGY,
369 | memLevel = Z_DEFAULT_MEM_LEVEL,
370 | clientNoContextTakeOver = false,
371 | clientMaxWindowBits = 15): ExtFactory =
372 |
373 | proc factory(isServer: bool,
374 | args: seq[ExtParam]): Result[Ext, string] {.
375 | gcsafe, raises: [].} =
376 |
377 | # capture user configuration via closure
378 | var opts = DeflateOpts(
379 | isServer: isServer,
380 | threshold: threshold,
381 | decompressLimit: decompressLimit,
382 | level: level,
383 | strategy: strategy,
384 | memLevel: memLevel
385 | )
386 | let resp = createParams(args, opts)
387 | if resp.isErr:
388 | return err(resp.error)
389 |
390 | var ext = newDeflateExt()
391 | ext.name = extID
392 | ext.paramStr = resp.get()
393 | ext.opts = opts
394 | ext.compressedMsg = false
395 | ext.compCtxState = ContextState.Invalid
396 | ext.decompCtxState= ContextState.Invalid
397 |
398 | ok(ext)
399 |
400 | ExtFactory(
401 | name: extID,
402 | factory: factory,
403 | clientOffer: makeOffer(
404 | clientNoContextTakeOver,
405 | clientMaxWindowBits
406 | )
407 | )
408 |
--------------------------------------------------------------------------------
/websock/extensions/extutils.nim:
--------------------------------------------------------------------------------
1 | ## nim-websock
2 | ## Copyright (c) 2021-2022 Status Research & Development GmbH
3 | ## Licensed under either of
4 | ## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5 | ## * MIT license ([LICENSE-MIT](LICENSE-MIT))
6 | ## at your option.
7 | ## This file may not be copied, modified, or distributed except according to
8 | ## those terms.
9 |
10 | import
11 | std/strutils,
12 | pkg/httputils,
13 | ../types
14 |
15 | type
16 | AppExt* = object
17 | name* : string
18 | params*: seq[ExtParam]
19 |
20 | TokenKind = enum
21 | tkError
22 | tkSemCol
23 | tkComma
24 | tkEqual
25 | tkName
26 | tkQuoted
27 | tkEof
28 |
29 | Lexer = object
30 | pos: int
31 | token: string
32 | tok: TokenKind
33 |
34 | const
35 | WHITES = {' ', '\t'}
36 | LCHAR = {'a'..'z', 'A'..'Z', '-', '_', '0'..'9','.','\''}
37 | SEPARATORS = {'`','~','!','@','#','$','%','^','&','*','(',')','+','=',
38 | '[','{',']','}', ';',':','\'',',','<','.','>','/','?','|'}
39 | QCHAR = WHITES + LCHAR + SEPARATORS
40 |
41 | proc parseName[T: BChar](lex: var Lexer, data: openArray[T]) =
42 | while lex.pos < data.len:
43 | let cc = data[lex.pos]
44 | if cc notin LCHAR:
45 | break
46 | lex.token.add cc
47 | inc lex.pos
48 |
49 | proc parseQuoted[T: BChar](lex: var Lexer, data: openArray[T]) =
50 | while lex.pos < data.len:
51 | let cc = data[lex.pos]
52 | case cc:
53 | of QCHAR:
54 | lex.token.add cc
55 | inc lex.pos
56 | of '\\':
57 | inc lex.pos
58 | if lex.pos >= data.len:
59 | lex.tok = tkError
60 | return
61 | lex.token.add data[lex.pos]
62 | inc lex.pos
63 | of '\"':
64 | inc lex.pos
65 | lex.tok = tkQuoted
66 | return
67 | else:
68 | lex.tok = tkError
69 | return
70 |
71 | lex.tok = tkError
72 |
73 | proc next[T: BChar](lex: var Lexer, data: openArray[T]) =
74 | while lex.pos < data.len:
75 | if data[lex.pos] notin WHITES:
76 | break
77 | inc lex.pos
78 | lex.token.setLen(0)
79 |
80 | if lex.pos >= data.len:
81 | lex.tok = tkEof
82 | return
83 |
84 | let c = data[lex.pos]
85 | case c
86 | of ';':
87 | inc lex.pos
88 | lex.tok = tkSemCol
89 | return
90 | of ',':
91 | inc lex.pos
92 | lex.tok = tkComma
93 | return
94 | of '=':
95 | inc lex.pos
96 | lex.tok = tkEqual
97 | return
98 | of LCHAR:
99 | lex.parseName(data)
100 | lex.tok = tkName
101 | return
102 | of '\"':
103 | inc lex.pos
104 | lex.parseQuoted(data)
105 | return
106 | else:
107 | lex.tok = tkError
108 | return
109 |
110 | proc parseExt*[T: BChar](data: openArray[T], output: var seq[AppExt]): bool =
111 | var lex: Lexer
112 | var ext: AppExt
113 | lex.next(data)
114 |
115 | while lex.tok notin {tkEof, tkError}:
116 | if lex.tok != tkName:
117 | return false
118 | ext.name = system.move(lex.token)
119 |
120 | lex.next(data)
121 | var param: ExtParam
122 | while lex.tok == tkSemCol:
123 | lex.next(data)
124 | if lex.tok in {tkEof, tkError}:
125 | return false
126 | if lex.tok != tkName:
127 | return false
128 | param.name = system.move(lex.token)
129 | lex.next(data)
130 | if lex.tok == tkEqual:
131 | lex.next(data)
132 | if lex.tok notin {tkName, tkQuoted}:
133 | return false
134 | param.value = system.move(lex.token)
135 | lex.next(data)
136 | ext.params.setLen(ext.params.len + 1)
137 | ext.params[^1].name = system.move(param.name)
138 | ext.params[^1].value = system.move(param.value)
139 |
140 | if lex.tok notin {tkSemCol, tkComma, tkEof}:
141 | return false
142 |
143 | output.setLen(output.len + 1)
144 | output[^1].name = toLowerAscii(ext.name)
145 | output[^1].params = system.move(ext.params)
146 |
147 | if lex.tok == tkEof:
148 | return true
149 |
150 | if lex.tok == tkComma:
151 | lex.next(data)
152 | if lex.tok != tkName:
153 | return false
154 | continue
155 |
156 | lex.tok != tkError
157 |
--------------------------------------------------------------------------------
/websock/frame.nim:
--------------------------------------------------------------------------------
1 | ## nim-websock
2 | ## Copyright (c) 2021 Status Research & Development GmbH
3 | ## Licensed under either of
4 | ## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5 | ## * MIT license ([LICENSE-MIT](LICENSE-MIT))
6 | ## at your option.
7 | ## This file may not be copied, modified, or distributed except according to
8 | ## those terms.
9 |
10 | {.push gcsafe, raises: [].}
11 |
12 | import pkg/[
13 | chronos,
14 | chronicles,
15 | stew/byteutils,
16 | stew/endians2,
17 | stew/results]
18 |
19 | import ./types
20 |
21 | logScope:
22 | topics = "websock ws-frame"
23 |
24 | #[
25 | +---------------------------------------------------------------+
26 | |0 1 2 3 |
27 | |0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1|
28 | +-+-+-+-+-------+-+-------------+-------------------------------+
29 | |F|R|R|R| opcode|M| Payload len | Extended payload length |
30 | |I|S|S|S| (4) |A| (7) | (16/64) |
31 | |N|V|V|V| |S| | (if payload len==126/127) |
32 | | |1|2|3| |K| | |
33 | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
34 | | Extended payload length continued, if payload len == 127 |
35 | + - - - - - - - - - - - - - - - +-------------------------------+
36 | | |Masking-key, if MASK set to 1 |
37 | +-------------------------------+-------------------------------+
38 | | Masking-key (continued) | Payload Data |
39 | +-------------------------------- - - - - - - - - - - - - - - - +
40 | : Payload Data continued ... :
41 | + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
42 | | Payload Data continued ... |
43 | +---------------------------------------------------------------+
44 | ]#
45 |
46 | proc mask*(
47 | data: var openArray[byte],
48 | maskKey: MaskKey,
49 | offset = 0) =
50 | ## Unmask a data payload using key
51 | ##
52 |
53 | for i in 0 ..< data.len:
54 | data[i] = (data[i].uint8 xor maskKey[(offset + i) mod 4].uint8)
55 |
56 | template remainder*(frame: Frame): uint64 =
57 | frame.length - frame.consumed
58 |
59 | proc read*(
60 | frame: Frame,
61 | reader: AsyncStreamReader,
62 | pbytes: pointer,
63 | nbytes: int): Future[int] {.async.} =
64 |
65 | # read data from buffered payload if available
66 | # e.g. data processed by extensions
67 | var readLen = 0
68 | if frame.offset < frame.data.len:
69 | readLen = min(frame.data.len - frame.offset, nbytes)
70 | copyMem(pbytes, addr frame.data[frame.offset], readLen)
71 | frame.offset += readLen
72 |
73 | var pbuf = cast[ptr UncheckedArray[byte]](pbytes)
74 | if readLen < nbytes:
75 | let len = min(nbytes - readLen, frame.remainder.int - readLen)
76 | readLen += await reader.readOnce(addr pbuf[readLen], len)
77 |
78 | if frame.mask and readLen > 0:
79 | # unmask data using offset
80 | mask(
81 | pbuf.toOpenArray(0, readLen - 1),
82 | frame.maskKey,
83 | frame.consumed.int)
84 |
85 | frame.consumed += readLen.uint64
86 | return readLen
87 |
88 | proc encode*(
89 | frame: Frame,
90 | extensions: seq[Ext] = @[]): Future[seq[byte]] {.async.} =
91 | ## Encodes a frame into a string buffer.
92 | ## See https://tools.ietf.org/html/rfc6455#section-5.2
93 |
94 | var f = frame
95 | if extensions.len > 0:
96 | for e in extensions:
97 | f = await e.encode(f)
98 |
99 | var ret: seq[byte]
100 | var b0 = (f.opcode.uint8 and 0x0f) # 0th byte: opcodes and flags.
101 | if f.fin:
102 | b0 = b0 or 0x80'u8
103 | if f.rsv1:
104 | b0 = b0 or 0x40'u8
105 | if f.rsv2:
106 | b0 = b0 or 0x20'u8
107 | if f.rsv3:
108 | b0 = b0 or 0x10'u8
109 |
110 | ret.add(b0)
111 |
112 | # Payload length can be 7 bits, 7+16 bits, or 7+64 bits.
113 | # 1st byte: payload len start and mask bit.
114 | var b1 = 0'u8
115 |
116 | if f.data.len <= 125:
117 | b1 = f.data.len.uint8
118 | elif f.data.len > 125 and f.data.len <= 0xffff:
119 | b1 = 126'u8
120 | else:
121 | b1 = 127'u8
122 |
123 | if f.mask:
124 | b1 = b1 or (1 shl 7)
125 |
126 | ret.add(uint8 b1)
127 |
128 | # Only need more bytes if data len is 7+16 bits, or 7+64 bits.
129 | if f.data.len > 125 and f.data.len <= 0xffff:
130 | # Data len is 7+16 bits.
131 | var len = f.data.len.uint16
132 | ret.add ((len shr 8) and 0xff).uint8
133 | ret.add (len and 0xff).uint8
134 | elif f.data.len > 0xffff:
135 | # Data len is 7+64 bits.
136 | var len = f.data.len.uint64
137 | ret.add(len.toBytesBE())
138 |
139 | var data = f.data
140 | if f.mask:
141 | # If we need to mask it generate random mask key and mask the data.
142 | mask(data, f.maskKey)
143 |
144 | # Write mask key next.
145 | ret.add(f.maskKey[0].uint8)
146 | ret.add(f.maskKey[1].uint8)
147 | ret.add(f.maskKey[2].uint8)
148 | ret.add(f.maskKey[3].uint8)
149 |
150 | # Write the data.
151 | ret.add(data)
152 | return ret
153 |
154 | proc decode*(
155 | _: typedesc[Frame],
156 | reader: AsyncStreamReader,
157 | masked: bool,
158 | extensions: seq[Ext] = @[]): Future[Frame] {.async.} =
159 | ## Read and Decode incoming header
160 | ##
161 |
162 | var header = newSeq[byte](2)
163 | trace "Reading new frame"
164 | await reader.readExactly(addr header[0], 2)
165 | if header.len != 2:
166 | trace "Invalid websocket header length"
167 | raise newException(WSMalformedHeaderError,
168 | "Invalid websocket header length")
169 |
170 | let b0 = header[0].uint8
171 | let b1 = header[1].uint8
172 |
173 | var frame = Frame()
174 | # Read the flags and fin from the header.
175 |
176 | var hf = cast[HeaderFlags](b0 shr 4)
177 | frame.fin = HeaderFlag.fin in hf
178 | frame.rsv1 = HeaderFlag.rsv1 in hf
179 | frame.rsv2 = HeaderFlag.rsv2 in hf
180 | frame.rsv3 = HeaderFlag.rsv3 in hf
181 |
182 | let opcode = (b0 and 0x0f)
183 | if opcode > ord(Opcode.Pong):
184 | raise newException(WSOpcodeMismatchError, "Wrong opcode!")
185 |
186 | frame.opcode = (opcode).Opcode
187 |
188 | # Payload length can be 7 bits, 7+16 bits, or 7+64 bits.
189 | var finalLen: uint64 = 0
190 |
191 | let headerLen = uint(b1 and 0x7f)
192 | if headerLen == 0x7e:
193 | # Length must be 7+16 bits.
194 | var length = newSeq[byte](2)
195 | await reader.readExactly(addr length[0], 2)
196 | finalLen = uint16.fromBytesBE(length)
197 | elif headerLen == 0x7f:
198 | # Length must be 7+64 bits.
199 | var length = newSeq[byte](8)
200 | await reader.readExactly(addr length[0], 8)
201 | finalLen = uint64.fromBytesBE(length)
202 | else:
203 | # Length must be 7 bits.
204 | finalLen = headerLen
205 |
206 | frame.length = finalLen
207 |
208 | if frame.length > WSMaxMessageSize:
209 | raise newException(WSPayloadLengthError, "Frame too big: " & $frame.length)
210 |
211 | # Do we need to apply mask?
212 | frame.mask = (b1 and 0x80) == 0x80
213 | if masked == frame.mask:
214 | # Server sends unmasked but accepts only masked.
215 | # Client sends masked but accepts only unmasked.
216 | raise newException(WSMaskMismatchError,
217 | "Socket mask mismatch")
218 |
219 | var maskKey = newSeq[byte](4)
220 | if frame.mask:
221 | # Read the mask.
222 | await reader.readExactly(addr maskKey[0], 4)
223 | for i in 0.. 0:
227 | for i in countdown(extensions.high, extensions.low):
228 | frame = await extensions[i].decode(frame)
229 |
230 | # we check rsv bits after extensions,
231 | # because they have special meaning for extensions.
232 | # rsv bits will be cleared by extensions if they are set by peer.
233 | # If any of the rsv are set close the socket.
234 | if frame.rsv1 or frame.rsv2 or frame.rsv3:
235 | raise newException(WSRsvMismatchError, "WebSocket rsv mismatch")
236 |
237 | return frame
238 |
--------------------------------------------------------------------------------
/websock/http.nim:
--------------------------------------------------------------------------------
1 | ## nim-websock
2 | ## Copyright (c) 2021 Status Research & Development GmbH
3 | ## Licensed under either of
4 | ## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5 | ## * MIT license ([LICENSE-MIT](LICENSE-MIT))
6 | ## at your option.
7 | ## This file may not be copied, modified, or distributed except according to
8 | ## those terms.
9 |
10 | import std/uri
11 | import pkg/[
12 | chronos/apps/http/httptable,
13 | chronos/streams/tlsstream,
14 | httputils]
15 |
16 | import ./http/client, ./http/server, ./http/common
17 |
18 | export uri, httputils, client, server, httptable, tlsstream, common
19 |
--------------------------------------------------------------------------------
/websock/http/client.nim:
--------------------------------------------------------------------------------
1 | ## nim-websock
2 | ## Copyright (c) 2021 Status Research & Development GmbH
3 | ## Licensed under either of
4 | ## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5 | ## * MIT license ([LICENSE-MIT](LICENSE-MIT))
6 | ## at your option.
7 | ## This file may not be copied, modified, or distributed except according to
8 | ## those terms.
9 |
10 | {.push gcsafe, raises: [].}
11 |
12 | import std/[uri, strutils]
13 | import pkg/[
14 | chronos,
15 | chronicles,
16 | httputils,
17 | stew/byteutils]
18 |
19 | import ./common
20 |
21 | logScope:
22 | topics = "websock http-client"
23 |
24 | type
25 | HttpClient* = ref object of RootObj
26 | connected*: bool
27 | hostname*: string
28 | address*: TransportAddress
29 | version*: HttpVersion
30 | port*: Port
31 | stream*: AsyncStream
32 | buf*: seq[byte]
33 |
34 | TlsHttpClient* = ref object of HttpClient
35 | tlsFlags*: set[TLSFlags]
36 | minVersion*: TLSVersion
37 | maxVersion*: TLSVersion
38 |
39 | proc close*(client: HttpClient): Future[void] =
40 | client.stream.closeWait()
41 |
42 | proc readResponse(stream: AsyncStreamReader): Future[HttpResponseHeader] {.async.} =
43 | var buffer = newSeq[byte](MaxHttpHeadersSize)
44 | try:
45 | let
46 | hlenfut = stream.readUntil(
47 | addr buffer[0], MaxHttpHeadersSize, sep = HeaderSep)
48 | ores = await withTimeout(hlenfut, HttpHeadersTimeout)
49 |
50 | if not ores:
51 | raise newException(HttpError,
52 | "Timeout expired while receiving headers")
53 |
54 | let hlen = hlenfut.read()
55 | buffer.setLen(hlen)
56 |
57 | return buffer.parseResponse()
58 | except CatchableError as exc:
59 | trace "Exception reading headers", exc = exc.msg
60 | buffer.setLen(0)
61 | raise exc
62 |
63 | proc generateHeaders(
64 | requestUrl: Uri,
65 | httpMethod: HttpMethod,
66 | version: HttpVersion,
67 | headers: HttpTables): string =
68 | var headersData = toUpperAscii($httpMethod)
69 | headersData.add " "
70 |
71 | if not requestUrl.path.startsWith("/"): headersData.add "/"
72 | headersData.add(requestUrl.path)
73 | if requestUrl.query.len > 0:
74 | headersData.add("?" & requestUrl.query)
75 | headersData.add(" ")
76 | headersData.add($version & CRLF)
77 |
78 | for (key, val) in headers.stringItems(true):
79 | headersData.add(key)
80 | headersData.add(": ")
81 | headersData.add(val)
82 | headersData.add(CRLF)
83 |
84 | headersData.add(CRLF)
85 | return headersData
86 |
87 | proc request*(
88 | client: HttpClient,
89 | url: string | Uri,
90 | httpMethod = MethodGet,
91 | headers: HttpTables,
92 | body: seq[byte] = @[]): Future[HttpResponse] {.async.} =
93 | ## Helper that actually makes the request.
94 | ## Does not handle redirects.
95 | ##
96 |
97 | if not client.connected:
98 | raise newException(HttpError, "No connection to host!")
99 |
100 | let requestUrl =
101 | when url is string:
102 | url.parseUri()
103 | else:
104 | url
105 |
106 | let headerString = generateHeaders(requestUrl, httpMethod, client.version, headers)
107 |
108 | await client.stream.writer.write(headerString)
109 | let response = await client.stream.reader.readResponse()
110 | let headers =
111 | block:
112 | var res = HttpTable.init()
113 | for key, value in response.headers():
114 | res.add(key, value)
115 | res
116 |
117 | return HttpResponse(
118 | headers: headers,
119 | stream: client.stream,
120 | code: response.code,
121 | reason: response.reason())
122 |
123 | proc connect*(
124 | T: typedesc[HttpClient | TlsHttpClient],
125 | address: TransportAddress,
126 | version = HttpVersion11,
127 | tlsFlags: set[TLSFlags] = {},
128 | tlsMinVersion = TLSVersion.TLS12,
129 | tlsMaxVersion = TLSVersion.TLS12,
130 | hostName = ""): Future[T] {.async.} =
131 |
132 | let transp = await connect(address)
133 | let client = T(
134 | hostname: address.host,
135 | port: address.port,
136 | address: transp.remoteAddress(),
137 | version: version)
138 |
139 | var stream = AsyncStream(
140 | reader: newAsyncStreamReader(transp),
141 | writer: newAsyncStreamWriter(transp))
142 |
143 | when T is TlsHttpClient:
144 | client.tlsFlags = tlsFlags
145 | client.minVersion = tlsMinVersion
146 | client.maxVersion = tlsMaxVersion
147 |
148 | let tlsStream = newTLSClientAsyncStream(
149 | stream.reader,
150 | stream.writer,
151 | serverName = hostName,
152 | minVersion = tlsMinVersion,
153 | maxVersion = tlsMaxVersion,
154 | flags = tlsFlags)
155 |
156 | stream = AsyncStream(
157 | reader: tlsStream.reader,
158 | writer: tlsStream.writer)
159 |
160 | client.stream = stream
161 | client.connected = true
162 |
163 | return client
164 |
165 | proc connect*(
166 | T: typedesc[HttpClient | TlsHttpClient],
167 | host: string,
168 | version = HttpVersion11,
169 | tlsFlags: set[TLSFlags] = {},
170 | tlsMinVersion = TLSVersion.TLS12,
171 | tlsMaxVersion = TLSVersion.TLS12,
172 | hostName = ""): Future[T]
173 | {.async: (raises: [CatchableError, HttpError]).} =
174 |
175 | let wantedHostName = if hostName.len > 0:
176 | hostName
177 | else:
178 | host.split(":")[0]
179 |
180 | template used(x: typed) =
181 | # silence unused warning
182 | discard
183 |
184 | let addrs = resolveTAddress(host)
185 | for a in addrs:
186 | try:
187 | let conn = await T.connect(
188 | a,
189 | version,
190 | tlsFlags,
191 | tlsMinVersion,
192 | tlsMaxVersion,
193 | hostName = wantedHostName)
194 |
195 | return conn
196 | except TransportError as exc:
197 | used(exc)
198 | trace "Error connecting to address", address = $a, exc = exc.msg
199 |
200 | raise newException(HttpError,
201 | "Unable to connect to host on any address!")
202 |
--------------------------------------------------------------------------------
/websock/http/common.nim:
--------------------------------------------------------------------------------
1 | ## nim-websock
2 | ## Copyright (c) 2021 Status Research & Development GmbH
3 | ## Licensed under either of
4 | ## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5 | ## * MIT license ([LICENSE-MIT](LICENSE-MIT))
6 | ## at your option.
7 | ## This file may not be copied, modified, or distributed except according to
8 | ## those terms.
9 |
10 | {.push gcsafe, raises: [].}
11 |
12 | import std/[uri]
13 | import pkg/[
14 | chronos,
15 | httputils,
16 | stew/byteutils,
17 | chronicles]
18 |
19 | import pkg/[
20 | chronos/apps/http/httptable,
21 | chronos/streams/tlsstream]
22 |
23 | export httputils, httptable, tlsstream, uri
24 |
25 | logScope:
26 | topics = "websock http-common"
27 |
28 | const
29 | MaxHttpHeadersSize* = 8192 # maximum size of HTTP headers in octets
30 | MaxHttpRequestSize* = 128 * 1024 # maximum size of HTTP body in octets
31 | HttpHeadersTimeout* = 120.seconds # timeout for receiving headers (120 sec)
32 | HeaderSep* = @[byte('\c'), byte('\L'), byte('\c'), byte('\L')]
33 | CRLF* = "\r\n"
34 |
35 | type
36 | ReqStatus* {.pure.} = enum
37 | Success, Error, ErrorFailure
38 |
39 | HttpCommon* = ref object of RootObj
40 | headers*: HttpTable
41 | code*: int
42 | version*: HttpVersion
43 | stream*: AsyncStream
44 |
45 | HttpRequest* = ref object of HttpCommon
46 | uri*: Uri
47 | meth*: HttpMethod
48 |
49 | # TODO: add useful response params, like body len
50 | HttpResponse* = ref object of HttpCommon
51 | reason*: string
52 |
53 | HttpError* = object of CatchableError
54 | HttpHeaderError* = HttpError
55 |
56 | proc closeTransp*(transp: StreamTransport) {.async.} =
57 | if not transp.closed():
58 | await transp.closeWait()
59 |
60 | proc closeStream*(stream: AsyncStreamRW) {.async.} =
61 | if not stream.closed():
62 | await stream.closeWait()
63 |
64 | proc closeWait*(stream: AsyncStream) {.async.} =
65 | await allFutures(
66 | stream.reader.closeStream(),
67 | stream.writer.closeStream(),
68 | stream.reader.tsource.closeTransp())
69 |
70 | proc close*(stream: AsyncStream) =
71 | stream.reader.close()
72 | stream.writer.close()
73 | stream.reader.tsource.close()
74 |
75 | proc sendResponse*(
76 | request: HttpRequest,
77 | code: HttpCode,
78 | headers: HttpTables = HttpTable.init(),
79 | data: seq[byte] = @[],
80 | version = HttpVersion11,
81 | content = "") {.async.} =
82 | ## Send response
83 | ##
84 |
85 | var headers = headers
86 | var response: string = $version
87 | response.add(" ")
88 | response.add($code)
89 | response.add(CRLF)
90 | response.add("Date: " & httpDate() & CRLF)
91 |
92 | if data.len > 0:
93 | if headers.getInt("Content-Length").int != data.len:
94 | warn "Wrong content length header, overriding"
95 | headers.set("Content-Length", $data.len)
96 |
97 | if headers.getString("Content-Type") != content:
98 | headers.set("Content-Type",
99 | if content.len > 0: content else: "text/html")
100 |
101 | for key, val in headers.stringItems(true):
102 | response.add(key)
103 | response.add(": ")
104 | response.add(val)
105 | response.add(CRLF)
106 |
107 | response.add(CRLF)
108 | await request.stream.writer.write(
109 | response.toBytes() & data)
110 |
111 | proc sendResponse*(
112 | request: HttpRequest,
113 | code: HttpCode,
114 | headers: HttpTables = HttpTable.init(),
115 | data: string,
116 | version = HttpVersion11,
117 | content = ""): Future[void] =
118 | request.sendResponse(code, headers, data.toBytes(), version, content)
119 |
120 | proc sendError*(
121 | stream: AsyncStreamWriter,
122 | code: HttpCode,
123 | version = HttpVersion11) {.async.} =
124 | let content = $code
125 | var response: string = $version
126 | response.add(" ")
127 | response.add(content & CRLF)
128 | response.add(CRLF)
129 |
130 | await stream.write(
131 | response.toBytes() & content.toBytes())
132 |
133 | proc sendError*(
134 | request: HttpRequest,
135 | code: HttpCode,
136 | version = HttpVersion11): Future[void] =
137 | request.stream.writer.sendError(code, version)
138 |
--------------------------------------------------------------------------------
/websock/http/server.nim:
--------------------------------------------------------------------------------
1 | ## nim-websock
2 | ## Copyright (c) 2021 Status Research & Development GmbH
3 | ## Licensed under either of
4 | ## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5 | ## * MIT license ([LICENSE-MIT](LICENSE-MIT))
6 | ## at your option.
7 | ## This file may not be copied, modified, or distributed except according to
8 | ## those terms.
9 |
10 | {.push gcsafe, raises: [].}
11 |
12 | import std/uri
13 | import pkg/[
14 | chronos,
15 | chronicles,
16 | httputils]
17 |
18 | when isLogFormatUsed(json):
19 | import json_serialization/std/net
20 |
21 | import ./common
22 |
23 | logScope:
24 | topics = "websock http-server"
25 |
26 | type
27 | HttpAsyncCallback* = proc (request: HttpRequest):
28 | Future[void] {.closure, gcsafe, raises: [].}
29 |
30 | HttpServer* = ref object of StreamServer
31 | handler*: HttpAsyncCallback
32 | handshakeTimeout*: Duration
33 | headersTimeout*: Duration
34 | case secure*: bool:
35 | of true:
36 | tlsFlags*: set[TLSFlags]
37 | tlsPrivateKey*: TLSPrivateKey
38 | tlsCertificate*: TLSCertificate
39 | minVersion*: TLSVersion
40 | maxVersion*: TLSVersion
41 | else:
42 | discard
43 |
44 | TlsHttpServer* = HttpServer
45 |
46 | template used(x: typed) =
47 | # silence unused warning
48 | discard
49 |
50 | proc validateRequest(
51 | stream: AsyncStreamWriter,
52 | header: HttpRequestHeader): Future[ReqStatus] {.async.} =
53 | ## Validate Request
54 | ##
55 |
56 | if header.meth notin {MethodGet}:
57 | trace "GET method is only allowed", address = stream.tsource.remoteAddress()
58 | await stream.sendError(Http405, version = header.version)
59 | return ReqStatus.Error
60 |
61 | var hlen = header.contentLength()
62 | if hlen < 0 or hlen > MaxHttpRequestSize:
63 | trace "Invalid header length", address = stream.tsource.remoteAddress()
64 | await stream.sendError(Http413, version = header.version)
65 | return ReqStatus.Error
66 |
67 | return ReqStatus.Success
68 |
69 | proc parseRequest(
70 | server: HttpServer,
71 | stream: AsyncStream): Future[HttpRequest] {.async.} =
72 | ## Process transport data to the HTTP server
73 | ##
74 |
75 | var buffer = newSeq[byte](MaxHttpHeadersSize)
76 | let remoteAddr {.used.} = stream.reader.tsource.remoteAddress()
77 | trace "Received connection", address = $remoteAddr
78 | try:
79 | let hlenfut = stream.reader.readUntil(
80 | addr buffer[0], MaxHttpHeadersSize, sep = HeaderSep)
81 | let ores = await withTimeout(hlenfut, server.headersTimeout)
82 | if not ores:
83 | # Timeout
84 | trace "Timeout expired while receiving headers", address = $remoteAddr
85 | await stream.writer.sendError(Http408, version = HttpVersion11)
86 | raise newException(HttpError, "Didn't read headers in time!")
87 |
88 | let hlen = hlenfut.read()
89 | buffer.setLen(hlen)
90 | let requestData = buffer.parseRequest()
91 | if requestData.failed():
92 | # Header could not be parsed
93 | trace "Malformed header received", address = $remoteAddr
94 | await stream.writer.sendError(Http400, version = HttpVersion11)
95 | raise newException(HttpError, "Malformed header received")
96 |
97 | var vres = await stream.writer.validateRequest(requestData)
98 | let hdrs =
99 | block:
100 | var res = HttpTable.init()
101 | for key, value in requestData.headers():
102 | res.add(key, value)
103 | res
104 |
105 | if vres == ReqStatus.ErrorFailure:
106 | trace "Remote peer disconnected", address = $remoteAddr
107 | raise newException(HttpError, "Remote peer disconnected")
108 |
109 | trace "Received valid HTTP request", address = $remoteAddr
110 | return HttpRequest(
111 | headers: hdrs,
112 | stream: stream,
113 | uri: requestData.uri().parseUri())
114 | except TransportLimitError:
115 | # size of headers exceeds `MaxHttpHeadersSize`
116 | trace "maximum size of headers limit reached", address = $remoteAddr
117 | await stream.writer.sendError(Http413, version = HttpVersion11)
118 | except TransportIncompleteError:
119 | # remote peer disconnected
120 | trace "Remote peer disconnected", address = $remoteAddr
121 | except TransportOsError as exc:
122 | used(exc)
123 | trace "Problems with networking", address = $remoteAddr, error = exc.msg
124 |
125 | proc handleConnCb(
126 | server: StreamServer,
127 | transp: StreamTransport) {.gcsafe, async: (raises: []).} =
128 | var stream: AsyncStream
129 | try:
130 | stream = AsyncStream(
131 | reader: newAsyncStreamReader(transp),
132 | writer: newAsyncStreamWriter(transp))
133 |
134 | let httpServer = HttpServer(server)
135 | let request = await httpServer.parseRequest(stream)
136 |
137 | await httpServer.handler(request)
138 | except CatchableError as exc:
139 | used(exc)
140 | debug "Exception in HttpHandler", exc = exc.msg
141 | finally:
142 | try:
143 | await stream.closeWait()
144 | except CatchableError as exc:
145 | used(exc)
146 | debug "Exception in HttpHandler closewait", exc = exc.msg
147 |
148 | proc handleTlsConnCb(
149 | server: StreamServer,
150 | transp: StreamTransport) {.gcsafe, async: (raises: []).} =
151 |
152 | let tlsHttpServer = TlsHttpServer(server)
153 | var tlsStream: TLSAsyncStream
154 |
155 | try:
156 | tlsStream = newTLSServerAsyncStream(
157 | newAsyncStreamReader(transp),
158 | newAsyncStreamWriter(transp),
159 | tlsHttpServer.tlsPrivateKey,
160 | tlsHttpServer.tlsCertificate,
161 | minVersion = tlsHttpServer.minVersion,
162 | maxVersion = tlsHttpServer.maxVersion,
163 | flags = tlsHttpServer.tlsFlags)
164 | except CatchableError as exc:
165 | used(exc)
166 | debug "Exception when initialize TLS stream", exc = exc.msg
167 | return
168 |
169 | let stream = AsyncStream(
170 | reader: tlsStream.reader,
171 | writer: tlsStream.writer)
172 |
173 | try:
174 | let httpServer = HttpServer(server)
175 | let request = await httpServer.parseRequest(stream)
176 |
177 | await httpServer.handler(request)
178 | except CatchableError as exc:
179 | used(exc)
180 | debug "Exception in HttpsHandler", exc = exc.msg
181 | finally:
182 | try:
183 | await stream.closeWait()
184 | except CatchableError as exc:
185 | used(exc)
186 | debug "Exception in HttpsHandler closewait", exc = exc.msg
187 |
188 | proc accept*(server: HttpServer): Future[HttpRequest] {.async.} =
189 | if not isNil(server.handler):
190 | raise newException(HttpError,
191 | "Callback already registered - cannot mix callback and accepts styles!")
192 |
193 | trace "Awaiting new request"
194 | let transp = await StreamServer(server).accept()
195 | let stream = if server.secure:
196 | let tlsStream = newTLSServerAsyncStream(
197 | newAsyncStreamReader(transp),
198 | newAsyncStreamWriter(transp),
199 | server.tlsPrivateKey,
200 | server.tlsCertificate,
201 | minVersion = server.minVersion,
202 | maxVersion = server.maxVersion,
203 | flags = server.tlsFlags)
204 |
205 | AsyncStream(
206 | reader: tlsStream.reader,
207 | writer: tlsStream.writer)
208 | else:
209 | AsyncStream(
210 | reader: newAsyncStreamReader(transp),
211 | writer: newAsyncStreamWriter(transp))
212 |
213 | trace "Got new request", isTls = server.secure
214 | try:
215 | let
216 | parseFut = server.parseRequest(stream)
217 | if await withTimeout(parseFut, server.handshakeTimeout):
218 | return parseFut.read()
219 | raise newException(HttpError, "Timed out parsing request")
220 | except CatchableError as exc:
221 | # Can't hold up the accept loop
222 | stream.close()
223 | raise exc
224 |
225 |
226 | proc create*(
227 | _: typedesc[HttpServer],
228 | address: TransportAddress | string,
229 | handler: HttpAsyncCallback = nil,
230 | flags: set[ServerFlags] = {},
231 | headersTimeout = HttpHeadersTimeout,
232 | handshakeTimeout = 0.seconds
233 | ): HttpServer
234 | {.raises: [CatchableError].} = # TODO: remove CatchableError
235 | ## Make a new HTTP Server
236 | ##
237 |
238 | var server = HttpServer(
239 | handler: handler,
240 | headersTimeout: headersTimeout,
241 | handshakeTimeout:
242 | if handshakeTimeout == 0.seconds:
243 | # default to headersTimeout * 1.05
244 | headersTimeout + (headersTimeout div 20)
245 | else: handshakeTimeout,
246 | )
247 |
248 | let localAddress =
249 | when address is string:
250 | initTAddress(address)
251 | else:
252 | address
253 |
254 | server = HttpServer(
255 | createStreamServer(
256 | localAddress,
257 | handleConnCb,
258 | flags,
259 | child = StreamServer(server)))
260 |
261 | trace "Created HTTP Server", host = $server.localAddress()
262 |
263 | return server
264 |
265 | proc create*(
266 | _: typedesc[TlsHttpServer],
267 | address: TransportAddress | string,
268 | tlsPrivateKey: TLSPrivateKey,
269 | tlsCertificate: TLSCertificate,
270 | handler: HttpAsyncCallback = nil,
271 | flags: set[ServerFlags] = {},
272 | tlsFlags: set[TLSFlags] = {},
273 | tlsMinVersion = TLSVersion.TLS12,
274 | tlsMaxVersion = TLSVersion.TLS12,
275 | headersTimeout = HttpHeadersTimeout,
276 | handshakeTimeout = 0.seconds
277 | ): TlsHttpServer
278 | {.raises: [CatchableError].} = # TODO: remove CatchableError
279 |
280 | var server = TlsHttpServer(
281 | headersTimeout: headersTimeout,
282 | handshakeTimeout:
283 | if handshakeTimeout == 0.seconds:
284 | # default to headersTimeout * 1.05
285 | headersTimeout + (headersTimeout div 20)
286 | else: handshakeTimeout,
287 | secure: true,
288 | handler: handler,
289 | tlsPrivateKey: tlsPrivateKey,
290 | tlsCertificate: tlsCertificate,
291 | minVersion: tlsMinVersion,
292 | maxVersion: tlsMaxVersion)
293 |
294 | let localAddress =
295 | when address is string:
296 | initTAddress(address)
297 | else:
298 | address
299 |
300 | server = TlsHttpServer(
301 | createStreamServer(
302 | localAddress,
303 | handleTlsConnCb,
304 | flags,
305 | child = StreamServer(server)))
306 |
307 | trace "Created TLS HTTP Server", host = $server.localAddress()
308 |
309 | return server
310 |
--------------------------------------------------------------------------------
/websock/session.nim:
--------------------------------------------------------------------------------
1 | ## nim-websock
2 | ## Copyright (c) 2021-2023 Status Research & Development GmbH
3 | ## Licensed under either of
4 | ## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5 | ## * MIT license ([LICENSE-MIT](LICENSE-MIT))
6 | ## at your option.
7 | ## This file may not be copied, modified, or distributed except according to
8 | ## those terms.
9 |
10 | {.push gcsafe, raises: [].}
11 |
12 | import std/strformat
13 | import pkg/[chronos, chronicles, stew/byteutils, stew/endians2]
14 | import ./types, ./frame, ./utf8dfa, ./http
15 |
16 | import pkg/chronos/streams/asyncstream
17 |
18 | logScope:
19 | topics = "websock ws-session"
20 |
21 | template used(x: typed) =
22 | # silence unused warning
23 | discard
24 |
25 | proc prepareCloseBody(code: StatusCodes, reason: string): seq[byte] =
26 | result = reason.toBytes
27 | if ord(code) > 999:
28 | result = @(ord(code).uint16.toBytesBE()) & result
29 |
30 | proc writeMessage(ws: WSSession,
31 | data: seq[byte] = @[],
32 | opcode: Opcode,
33 | maskKey: MaskKey,
34 | extensions: seq[Ext]) {.async.} =
35 |
36 | if opcode notin {Opcode.Text, Opcode.Cont, Opcode.Binary}:
37 | warn "Attempting to send a data frame with an invalid opcode!"
38 | raise newException(WSInvalidOpcodeError,
39 | &"Attempting to send a data frame with an invalid opcode {opcode}!")
40 |
41 | let maxSize = ws.frameSize
42 | var i = 0
43 | while ws.readyState notin {ReadyState.Closing, ReadyState.Closed}:
44 | let canSend = min(data.len - i, maxSize)
45 | let frame = Frame(
46 | fin: if (canSend + i >= data.len): true else: false,
47 | rsv1: false,
48 | rsv2: false,
49 | rsv3: false,
50 | opcode: if i > 0: Opcode.Cont else: opcode, # fragments have to be `Continuation` frames
51 | mask: ws.masked,
52 | data: data[i ..< canSend + i],
53 | maskKey: maskKey)
54 |
55 | let encoded = await frame.encode(extensions)
56 | await ws.stream.writer.write(encoded)
57 |
58 | i += canSend
59 | if i >= data.len:
60 | break
61 |
62 | proc writeControl(
63 | ws: WSSession,
64 | data: seq[byte] = @[],
65 | opcode: Opcode,
66 | maskKey: MaskKey) {.async.} =
67 | ## Send a frame applying the supplied
68 | ## extensions
69 | ##
70 |
71 | logScope:
72 | opcode = opcode
73 | dataSize = data.len
74 | masked = ws.masked
75 |
76 | if opcode in {Opcode.Text, Opcode.Cont, Opcode.Binary}:
77 | warn "Attempting to send a control frame with an invalid opcode!"
78 | raise newException(WSInvalidOpcodeError,
79 | &"Attempting to send a control frame with an invalid opcode {opcode}!")
80 |
81 | let frame = Frame(
82 | fin: true,
83 | rsv1: false,
84 | rsv2: false,
85 | rsv3: false,
86 | opcode: opcode,
87 | mask: ws.masked,
88 | data: data,
89 | maskKey: maskKey)
90 |
91 | let encoded = await frame.encode()
92 | await ws.stream.writer.write(encoded)
93 |
94 | trace "Wrote control frame"
95 |
96 | func isControl(opcode: Opcode): bool =
97 | opcode notin {Opcode.Text, Opcode.Cont, Opcode.Binary}
98 |
99 | proc nonCancellableSend(
100 | ws: WSSession,
101 | data: seq[byte] = @[],
102 | opcode: Opcode): Future[void]
103 | {.async.} =
104 | ## Send a frame
105 | ##
106 |
107 | if ws.readyState == ReadyState.Closed:
108 | raise newException(WSClosedError, "WebSocket is closed!")
109 |
110 | if ws.readyState in {ReadyState.Closing} and opcode notin {Opcode.Close}:
111 | trace "Can only respond with Close opcode to a closing connection"
112 | return
113 |
114 | logScope:
115 | opcode = opcode
116 | dataSize = data.len
117 | masked = ws.masked
118 |
119 | trace "Sending data to remote"
120 |
121 | let maskKey =
122 | if ws.masked:
123 | MaskKey.random(ws.rng[])
124 | else:
125 | default(MaskKey)
126 |
127 | if opcode.isControl:
128 | await ws.writeControl(data, opcode, maskKey)
129 | else:
130 | await ws.writeMessage(data, opcode, maskKey, ws.extensions)
131 |
132 | proc doSend(
133 | ws: WSSession,
134 | data: seq[byte] = @[],
135 | opcode: Opcode
136 | ): Future[void] =
137 | let
138 | retFut = newFuture[void]("doSend")
139 | sendFut = ws.nonCancellableSend(data, opcode)
140 |
141 | proc handleSend {.async.} =
142 | try:
143 | await sendFut
144 | retFut.complete()
145 | except CatchableError as exc:
146 | retFut.fail(exc)
147 |
148 | asyncSpawn handleSend()
149 | retFut
150 |
151 | proc sendLoop(ws: WSSession) {.gcsafe, async.} =
152 | while ws.sendQueue.len > 0:
153 | let task = ws.sendQueue.popFirst()
154 | if task.fut.cancelled:
155 | continue
156 |
157 | try:
158 | await ws.doSend(task.data, task.opcode)
159 | task.fut.complete()
160 | except CatchableError as exc:
161 | task.fut.fail(exc)
162 |
163 | proc send*(
164 | ws: WSSession,
165 | data: seq[byte] = @[],
166 | opcode: Opcode): Future[void] =
167 | if opcode.isControl:
168 | # Control frames (see Section 5.5) MAY be injected in the middle of
169 | # a fragmented message. Control frames themselves MUST NOT be
170 | # fragmented.
171 | # See RFC 6455 Section 5.4 Fragmentation
172 | return ws.doSend(data, opcode)
173 |
174 | let fut = newFuture[void]("send")
175 |
176 | ws.sendQueue.addLast (data: data, opcode: opcode, fut: fut)
177 |
178 | if isNil(ws.sendLoop) or ws.sendLoop.finished:
179 | ws.sendLoop = sendLoop(ws)
180 |
181 | fut
182 |
183 | proc send*(
184 | ws: WSSession,
185 | data: string): Future[void] =
186 | send(ws, data.toBytes(), Opcode.Text)
187 |
188 | proc handleClose*(
189 | ws: WSSession,
190 | frame: Frame,
191 | payload: seq[byte] = @[]) {.async.} =
192 | ## Handle close sequence
193 | ##
194 |
195 | logScope:
196 | fin = frame.fin
197 | masked = frame.mask
198 | opcode = frame.opcode
199 | readyState = ws.readyState
200 |
201 | trace "Handling close"
202 |
203 | if ws.readyState != ReadyState.Open and ws.readyState != ReadyState.Closing:
204 | trace "Connection isn't open, aborting close sequence!"
205 | return
206 |
207 | var
208 | code = StatusFulfilled
209 | reason = ""
210 |
211 | case payload.len:
212 | of 0:
213 | code = StatusNoStatus
214 | of 1:
215 | raise newException(WSPayloadLengthError,
216 | "Invalid close frame with payload length 1!")
217 | else:
218 | try:
219 | code = StatusCodes(uint16.fromBytesBE(payload[0..<2]))
220 | except RangeDefect:
221 | raise newException(WSInvalidCloseCodeError,
222 | "Status code out of range!")
223 |
224 | if code in StatusNotUsed or
225 | code in StatusReservedProtocol:
226 | raise newException(WSInvalidCloseCodeError,
227 | &"Can't use reserved status code: {code}")
228 |
229 | if code == StatusReserved or
230 | code == StatusNoStatus or
231 | code == StatusClosedAbnormally:
232 | raise newException(WSInvalidCloseCodeError,
233 | &"Can't use reserved status code: {code}")
234 |
235 | # remaining payload bytes are reason for closing
236 | reason = string.fromBytes(payload[2..payload.high])
237 |
238 | if not ws.binary and validateUTF8(reason) == false:
239 | raise newException(WSInvalidUTF8,
240 | "Invalid UTF8 sequence detected in close reason")
241 |
242 | trace "Handling close message", code = ord(code), reason
243 | if not isNil(ws.onClose):
244 | try:
245 | (code, reason) = ws.onClose(code, reason)
246 | except CatchableError as exc:
247 | used(exc)
248 | trace "Exception in Close callback, this is most likely a bug", exc = exc.msg
249 | else:
250 | code = StatusFulfilled
251 | reason = ""
252 |
253 | # don't respond to a terminated connection
254 | if ws.readyState != ReadyState.Closing:
255 | ws.readyState = ReadyState.Closing
256 | trace "Sending close", code = ord(code), reason
257 | try:
258 | await ws.send(prepareCloseBody(code, reason), Opcode.Close).wait(5.seconds)
259 | except CatchableError as exc:
260 | used(exc)
261 | trace "Failed to send Close opcode", err=exc.msg
262 |
263 | ws.readyState = ReadyState.Closed
264 |
265 | # TODO: Under TLS, the response takes longer
266 | # to depart and fails to write the resp code
267 | # and cleanly close the connection. Definitely
268 | # looks like a bug, but not sure if it's chronos
269 | # or us?
270 | await sleepAsync(10.millis)
271 | await ws.stream.closeWait()
272 |
273 | proc handleControl*(ws: WSSession, frame: Frame) {.async.} =
274 | ## Handle control frames
275 | ##
276 |
277 | logScope:
278 | fin = frame.fin
279 | masked = frame.mask
280 | opcode = frame.opcode
281 | readyState = ws.readyState
282 | len = frame.length
283 |
284 | trace "Handling control frame"
285 |
286 | if not frame.fin:
287 | raise newException(WSFragmentedControlFrameError,
288 | "Control frame cannot be fragmented!")
289 |
290 | if frame.length > 125:
291 | raise newException(WSPayloadTooLarge,
292 | "Control message payload is greater than 125 bytes!")
293 |
294 | var payload = newSeq[byte](frame.length.int)
295 | if frame.length > 0:
296 | payload.setLen(frame.length.int)
297 | # Read control frame payload.
298 | await ws.stream.reader.readExactly(addr payload[0], frame.length.int)
299 | if frame.mask:
300 | mask(
301 | payload.toOpenArray(0, payload.high),
302 | frame.maskKey)
303 |
304 | # Process control frame payload.
305 | case frame.opcode:
306 | of Opcode.Ping:
307 | if not isNil(ws.onPing):
308 | try:
309 | ws.onPing(payload)
310 | except CatchableError as exc:
311 | used(exc)
312 | trace "Exception in Ping callback, this is most likely a bug", exc = exc.msg
313 |
314 | # send pong to remote
315 | await ws.send(payload, Opcode.Pong)
316 | of Opcode.Pong:
317 | if not isNil(ws.onPong):
318 | try:
319 | ws.onPong(payload)
320 | except CatchableError as exc:
321 | used(exc)
322 | trace "Exception in Pong callback, this is most likely a bug", exc = exc.msg
323 | of Opcode.Close:
324 | await ws.handleClose(frame, payload)
325 | else:
326 | raise newException(WSInvalidOpcodeError, "Invalid control opcode!")
327 |
328 | {.warning[HoleEnumConv]:off.}
329 |
330 | proc readFrame*(ws: WSSession, extensions: seq[Ext] = @[]): Future[Frame] {.async.} =
331 | ## Gets a frame from the WebSocket.
332 | ## See https://tools.ietf.org/html/rfc6455#section-5.2
333 | ##
334 |
335 | while ws.readyState != ReadyState.Closed:
336 | let frame = await Frame.decode(
337 | ws.stream.reader, ws.masked, extensions)
338 |
339 | logScope:
340 | opcode = frame.opcode
341 | len = frame.length
342 | mask = frame.mask
343 | fin = frame.fin
344 |
345 | trace "Decoded new frame"
346 |
347 | # return the current frame if it's not one of the control frames
348 | if frame.opcode notin {Opcode.Text, Opcode.Cont, Opcode.Binary}:
349 | await ws.handleControl(frame) # process control frames# process control frames
350 | continue
351 |
352 | return frame
353 |
354 | {.warning[HoleEnumConv]:on.}
355 |
356 | proc ping*(
357 | ws: WSSession,
358 | data: seq[byte] = @[]): Future[void] =
359 | ws.send(data, opcode = Opcode.Ping)
360 |
361 | proc recv*(
362 | ws: WSSession,
363 | data: pointer | ptr byte | ref seq[byte], # nim bug: pointer doesn't match ptr byte?
364 | size: int): Future[int] {.async.} =
365 | ## Attempts to read up to ``size`` bytes
366 | ##
367 | ## If ``size`` is less than the data in
368 | ## the frame, allow reading partial frames
369 | ##
370 | ## If no data is left in the pipe await
371 | ## until at least one byte is available
372 | ##
373 | ## Otherwise, read as many frames as needed
374 | ## up to ``size`` bytes, note that we do break
375 | ## at message boundaries (``fin`` flag set).
376 | ##
377 | ## Use this to stream data from frames
378 | ##
379 |
380 | doAssert ws.reading == false, "Only one concurrent read allowed"
381 | ws.reading = true
382 | defer: ws.reading = false
383 |
384 | var consumed = 0
385 | when data is pointer or data is ptr byte:
386 | let pbuffer = cast[ptr UncheckedArray[byte]](data)
387 | try:
388 | if isNil(ws.frame):
389 | ws.frame = await ws.readFrame(ws.extensions)
390 | ws.first = true
391 |
392 | while consumed < size:
393 | if isNil(ws.frame):
394 | assert ws.readyState == ReadyState.Closed
395 | trace "Closed connection, breaking"
396 | break
397 |
398 | logScope:
399 | first = ws.first
400 | fin = ws.frame.fin
401 | len = ws.frame.length
402 | consumed = ws.frame.consumed
403 | remainder = ws.frame.remainder
404 | opcode = ws.frame.opcode
405 | masked = ws.frame.mask
406 |
407 | if ws.first == (ws.frame.opcode == Opcode.Cont):
408 | error "Opcode mismatch!"
409 | raise newException(WSOpcodeMismatchError,
410 | &"Opcode mismatch: first: {ws.first}, opcode: {ws.frame.opcode}")
411 |
412 | if ws.first:
413 | ws.binary = ws.frame.opcode == Opcode.Binary # set binary flag
414 | trace "Setting binary flag"
415 |
416 | let len = min(ws.frame.remainder.int, size - consumed)
417 | if len > 0:
418 | trace "Reading bytes from frame stream", len
419 | when data is ref seq[byte]:
420 | data[].setLen(consumed + len)
421 | let read = await ws.frame.read(ws.stream.reader, addr data[][consumed], len)
422 | else:
423 | let read = await ws.frame.read(ws.stream.reader, addr pbuffer[consumed], len)
424 | if read <= 0:
425 | trace "Didn't read any bytes, stopping"
426 | raise newException(WSClosedError, "WebSocket is closed!")
427 |
428 | trace "Read data from frame", read
429 | consumed += read
430 |
431 | # all has been consumed from the frame
432 | # read the next frame
433 | if ws.frame.remainder <= 0:
434 | ws.first = false
435 |
436 | if ws.frame.fin: # we're at the end of the message, break
437 | trace "Read all frames, breaking"
438 | ws.frame = nil
439 | break
440 |
441 | # read next frame
442 | ws.frame = await ws.readFrame(ws.extensions)
443 | except CatchableError as exc:
444 | trace "Exception reading frames", exc = exc.msg
445 | ws.readyState = ReadyState.Closed
446 | await ws.stream.closeWait()
447 |
448 | raise exc
449 | finally:
450 | if not isNil(ws.frame) and
451 | (ws.frame.fin and ws.frame.remainder <= 0):
452 | trace "Last frame in message and no more bytes left to read, reseting current frame"
453 | ws.frame = nil
454 |
455 | return consumed
456 |
457 | proc recvMsg*(
458 | ws: WSSession,
459 | size = WSMaxMessageSize): Future[seq[byte]] {.async.} =
460 | ## Attempt to read a full message up to max `size`
461 | ## bytes in `frameSize` chunks.
462 | ##
463 | ## If no `fin` flag arrives await until cancelled or
464 | ## closed.
465 | ##
466 | ## If message is larger than `size` a `WSMaxMessageSizeError`
467 | ## exception is thrown.
468 | ##
469 | ## In all other cases it awaits a full message.
470 | ##
471 | try:
472 | var res: seq[byte]
473 | while ws.readyState != ReadyState.Closed:
474 | var buf = new(seq[byte])
475 | let read {.used.} = await ws.recv(buf, min(size, ws.frameSize))
476 |
477 | if res.len + buf[].len > size:
478 | raise newException(WSMaxMessageSizeError, "Max message size exceeded")
479 |
480 | trace "Read message", size = read
481 | res.add(buf[])
482 |
483 | # no more frames
484 | if isNil(ws.frame):
485 | break
486 |
487 | # read the entire message, exit
488 | if ws.frame.fin and ws.frame.remainder <= 0:
489 | trace "Read full message, breaking!"
490 | break
491 |
492 | if ws.readyState == ReadyState.Closed:
493 | # avoid reporting incomplete message
494 | raise newException(WSClosedError, "WebSocket is closed!")
495 |
496 | if not ws.binary and validateUTF8(res.toOpenArray(0, res.high)) == false:
497 | raise newException(WSInvalidUTF8, "Invalid UTF8 sequence detected")
498 |
499 | return res
500 | except CatchableError as exc:
501 | trace "Exception reading message", exc = exc.msg
502 | ws.readyState = ReadyState.Closed
503 | await ws.stream.closeWait()
504 |
505 | raise exc
506 |
507 | proc recv*(
508 | ws: WSSession,
509 | size = WSMaxMessageSize): Future[seq[byte]]
510 | {.deprecated: "deprecated in favor of recvMsg()".} =
511 | ws.recvMsg(size)
512 |
513 | proc close*(
514 | ws: WSSession,
515 | code = StatusFulfilled,
516 | reason: string = "") {.async.} =
517 | ## Close the Socket, sends close packet.
518 | ##
519 |
520 | if ws.readyState != ReadyState.Open:
521 | return
522 |
523 | proc gentleCloser(ws: WSSession, closeBody: seq[byte]) {.async.} =
524 | await ws.send(
525 | closeBody,
526 | opcode = Opcode.Close)
527 |
528 | # read frames until closed
529 | try:
530 | while ws.readyState != ReadyState.Closed:
531 | discard await ws.readFrame()
532 | except CancelledError as exc:
533 | raise exc
534 | except CatchableError as exc:
535 | discard exc # most likely EOF
536 | try:
537 | ws.readyState = ReadyState.Closing
538 | await gentleCloser(ws, prepareCloseBody(code, reason)).wait(10.seconds)
539 | except CancelledError as exc:
540 | trace "Cancellation when closing!", exc = exc.msg
541 | raise exc
542 | except CatchableError as exc:
543 | used(exc)
544 | trace "Exception closing", exc = exc.msg
545 | finally:
546 | await ws.stream.closeWait()
547 | ws.readyState = ReadyState.Closed
548 |
--------------------------------------------------------------------------------
/websock/types.nim:
--------------------------------------------------------------------------------
1 | ## nim-websock
2 | ## Copyright (c) 2021-2023 Status Research & Development GmbH
3 | ## Licensed under either of
4 | ## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5 | ## * MIT license ([LICENSE-MIT](LICENSE-MIT))
6 | ## at your option.
7 | ## This file may not be copied, modified, or distributed except according to
8 | ## those terms.
9 |
10 | {.push gcsafe, raises: [].}
11 |
12 | import std/deques
13 | import pkg/[chronos,
14 | chronos/streams/tlsstream,
15 | chronos/apps/http/httptable,
16 | bearssl/rand,
17 | httputils,
18 | stew/results]
19 |
20 | export deques, rand
21 |
22 | const
23 | SHA1DigestSize* = 20
24 | WSHeaderSize* = 12
25 | WSDefaultVersion* = 13
26 | WSDefaultFrameSize* = 1 shl 20 # 1mb
27 | WSMaxMessageSize* = 20 shl 20 # 20mb
28 | WSGuid* = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
29 |
30 | type
31 | ReadyState* {.pure.} = enum
32 | Connecting = 0 # The connection is not yet open.
33 | Open = 1 # The connection is open and ready to communicate.
34 | Closing = 2 # The connection is in the process of closing.
35 | Closed = 3 # The connection is closed or couldn't be opened.
36 |
37 | Opcode* {.pure.} = enum
38 | ## 4 bits. Defines the interpretation of the "Payload data".
39 | Cont = 0x0 ## Denotes a continuation frame.
40 | Text = 0x1 ## Denotes a text frame.
41 | Binary = 0x2 ## Denotes a binary frame.
42 | # 3-7 are reserved for further non-control frames.
43 | Close = 0x8 ## Denotes a connection close.
44 | Ping = 0x9 ## Denotes a ping.
45 | Pong = 0xa ## Denotes a pong.
46 | # B-F are reserved for further control frames.
47 | Reserved = 0xf
48 |
49 | HeaderFlag* {.pure, size: sizeof(uint8).} = enum
50 | rsv3
51 | rsv2
52 | rsv1
53 | fin
54 |
55 | HeaderFlags* = set[HeaderFlag]
56 |
57 | MaskKey* = array[4, char]
58 | WebSecKey* = array[16, byte]
59 |
60 | Frame* = ref object
61 | fin*: bool ## Indicates that this is the final fragment in a message.
62 | rsv1*: bool ## MUST be 0 unless negotiated that defines meanings
63 | rsv2*: bool ## MUST be 0
64 | rsv3*: bool ## MUST be 0
65 | opcode*: Opcode ## Defines the interpretation of the "Payload data".
66 | mask*: bool ## Defines whether the "Payload data" is masked.
67 | data*: seq[byte] ## Payload data
68 | maskKey*: MaskKey ## Masking key
69 | length*: uint64 ## Message size.
70 | consumed*: uint64 ## how much has been consumed from the frame
71 | offset*: int ## offset of buffered payload data
72 |
73 | StatusCodes* = distinct range[0..4999]
74 |
75 | ControlCb* = proc(data: openArray[byte] = [])
76 | {.gcsafe, raises: [].}
77 |
78 | CloseResult* = tuple
79 | code: StatusCodes
80 | reason: string
81 |
82 | CloseCb* = proc(code: StatusCodes, reason: string):
83 | CloseResult {.gcsafe, raises: [].}
84 |
85 | WebSocket* = ref object of RootObj
86 | extensions*: seq[Ext]
87 | version*: uint
88 | key*: string
89 | readyState*: ReadyState
90 | masked*: bool # send masked packets
91 | binary*: bool # is payload binary?
92 | flags*: set[TLSFlags]
93 | rng*: ref HmacDrbgContext
94 | frameSize*: int # max frame buffer size
95 | onPing*: ControlCb
96 | onPong*: ControlCb
97 | onClose*: CloseCb
98 |
99 | WSSession* = ref object of WebSocket
100 | stream*: AsyncStream
101 | frame*: Frame
102 | first*: bool
103 | reading*: bool
104 | proto*: string
105 |
106 | # The fragments of one message MUST NOT be interleaved between the
107 | # fragments of another message unless an extension has been
108 | # negotiated that can interpret the interleaving.
109 | # See RFC 6455 Section 5.4 Fragmentation
110 | sendLoop*: Future[void]
111 | sendQueue*: Deque[
112 | tuple[data: seq[byte], opcode: Opcode, fut: Future[void]]]
113 |
114 | Ext* = ref object of RootObj
115 | name*: string
116 | session*: WSSession
117 |
118 | ExtParam* = object
119 | name* : string
120 | value*: string
121 |
122 | ExtFactoryProc* = proc(
123 | isServer: bool,
124 | args: seq[ExtParam]): Result[Ext, string]
125 | {.gcsafe, raises: [].}
126 |
127 | ExtFactory* = object
128 | name*: string
129 | factory*: ExtFactoryProc
130 | clientOffer*: string
131 |
132 | # client exec order:
133 | # 1. append to request header
134 | # 2. verify response header
135 | # server exec order:
136 | # 1. verify request header
137 | # 2. append to response header
138 | # ------------------------------
139 | # Handshake exec order:
140 | # 1. client append to request header
141 | # 2. server verify request header
142 | # 3. server reply with response header
143 | # 4. client verify response header from server
144 | Hook* = ref object of RootObj
145 | append*: proc(ctx: Hook,
146 | headers: var HttpTable): Result[void, string]
147 | {.gcsafe, raises: [].}
148 | verify*: proc(ctx: Hook,
149 | headers: HttpTable): Future[Result[void, string]]
150 | {.gcsafe, async: (raises: []).}
151 |
152 | WebSocketError* = object of CatchableError
153 | WSMalformedHeaderError* = object of WebSocketError
154 | WSFailedUpgradeError* = object of WebSocketError
155 | WSVersionError* = object of WebSocketError
156 | WSProtoMismatchError* = object of WebSocketError
157 | WSMaskMismatchError* = object of WebSocketError
158 | WSHandshakeError* = object of WebSocketError
159 | WSOpcodeMismatchError* = object of WebSocketError
160 | WSRsvMismatchError* = object of WebSocketError
161 | WSWrongUriSchemeError* = object of WebSocketError
162 | WSMaxMessageSizeError* = object of WebSocketError
163 | WSClosedError* = object of WebSocketError
164 | WSSendError* = object of WebSocketError
165 | WSPayloadTooLarge* = object of WebSocketError
166 | WSReservedOpcodeError* = object of WebSocketError
167 | WSFragmentedControlFrameError* = object of WebSocketError
168 | WSInvalidCloseCodeError* = object of WebSocketError
169 | WSPayloadLengthError* = object of WebSocketError
170 | WSInvalidOpcodeError* = object of WebSocketError
171 | WSInvalidUTF8* = object of WebSocketError
172 | WSExtError* = object of WebSocketError
173 | WSHookError* = object of WebSocketError
174 |
175 | const
176 | StatusNotUsed* = (StatusCodes(0)..StatusCodes(999))
177 | StatusFulfilled* = StatusCodes(1000)
178 | StatusGoingAway* = StatusCodes(1001)
179 | StatusProtocolError* = StatusCodes(1002)
180 | StatusCannotAccept* = StatusCodes(1003)
181 | StatusReserved* = StatusCodes(1004) # 1004 reserved
182 | StatusNoStatus* = StatusCodes(1005) # use by clients
183 | StatusClosedAbnormally* = StatusCodes(1006) # use by clients
184 | StatusInconsistent* = StatusCodes(1007)
185 | StatusPolicyError* = StatusCodes(1008)
186 | StatusTooLarge* = StatusCodes(1009)
187 | StatusNoExtensions* = StatusCodes(1010)
188 | StatusUnexpectedError* = StatusCodes(1011)
189 | StatusFailedTls* = StatusCodes(1015) # passed to applications to indicate TLS errors
190 | StatusReservedProtocol* = StatusCodes(1016)..StatusCodes(2999) # reserved for this protocol
191 | StatusLibsCodes* = (StatusCodes(3000)..StatusCodes(3999)) # 3000-3999 reserved for libs
192 | StatusAppsCodes* = (StatusCodes(4000)..StatusCodes(4999)) # 4000-4999 reserved for apps
193 |
194 | proc `<=`*(a, b: StatusCodes): bool = a.uint16 <= b.uint16
195 | proc `>=`*(a, b: StatusCodes): bool = a.uint16 >= b.uint16
196 | proc `<`*(a, b: StatusCodes): bool = a.uint16 < b.uint16
197 | proc `>`*(a, b: StatusCodes): bool = a.uint16 > b.uint16
198 | proc `==`*(a, b: StatusCodes): bool = a.uint16 == b.uint16
199 |
200 | proc high*(a: HSlice[StatusCodes, StatusCodes]): uint16 = a.b.uint16
201 | proc low*(a: HSlice[StatusCodes, StatusCodes]): uint16 = a.a.uint16
202 |
203 | proc `$`*(a: StatusCodes): string = $(a.int)
204 |
205 | proc `name=`*(self: Ext, name: string) =
206 | raiseAssert "Can't change extensions name!"
207 |
208 | method decode*(self: Ext, frame: Frame): Future[Frame] {.base, async.} =
209 | raiseAssert "Not implemented!"
210 |
211 | method encode*(self: Ext, frame: Frame): Future[Frame] {.base, async.} =
212 | raiseAssert "Not implemented!"
213 |
214 | method toHttpOptions*(self: Ext): string {.base, gcsafe.} =
215 | raiseAssert "Not implemented!"
216 |
217 | func random*(T: typedesc[MaskKey|WebSecKey], rng: var HmacDrbgContext): T =
218 | rng.generate(result)
219 |
--------------------------------------------------------------------------------
/websock/utf8dfa.nim:
--------------------------------------------------------------------------------
1 | ## nim-websock
2 | ## Copyright (c) 2021 Status Research & Development GmbH
3 | ## Licensed under either of
4 | ## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5 | ## * MIT license ([LICENSE-MIT](LICENSE-MIT))
6 | ## at your option.
7 | ## This file may not be copied, modified, or distributed except according to
8 | ## those terms.
9 |
10 | # DFA based UTF8 decoder/validator
11 | # See http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ for details.
12 |
13 | const
14 | UTF8_ACCEPT* = 0
15 | UTF8_REJECT* = 1
16 |
17 | const utf8Table = [
18 | 0'u8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 00..1f
19 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 20..3f
20 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 40..5f
21 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 60..7f
22 | 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, # 80..9f
23 | 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, # a0..bf
24 | 8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, # c0..df
25 | 0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, # e0..ef
26 | 0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, # f0..ff
27 | 0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, # s0..s0
28 | 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, # s1..s2
29 | 1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, # s3..s4
30 | 1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, # s5..s6
31 | 1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, # s7..s8
32 | ]
33 |
34 | proc validateUTF8*[T: byte | char](text: openArray[T]): bool =
35 | var state = 0
36 | for c in text:
37 | let x = utf8Table[c.int].int
38 | state = utf8Table[256 + state*16 + x].int
39 | state == UTF8_ACCEPT
40 |
--------------------------------------------------------------------------------
/websock/websock.nim:
--------------------------------------------------------------------------------
1 | ## nim-websock
2 | ## Copyright (c) 2021 Status Research & Development GmbH
3 | ## Licensed under either of
4 | ## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5 | ## * MIT license ([LICENSE-MIT](LICENSE-MIT))
6 | ## at your option.
7 | ## This file may not be copied, modified, or distributed except according to
8 | ## those terms.
9 |
10 | {.push gcsafe, raises: [].}
11 |
12 | import std/[tables,
13 | strutils,
14 | strformat,
15 | sequtils,
16 | uri]
17 |
18 | import pkg/[chronos,
19 | chronos/apps/http/httptable,
20 | chronos/streams/asyncstream,
21 | chronos/streams/tlsstream,
22 | chronicles,
23 | httputils,
24 | stew/byteutils,
25 | stew/base64,
26 | stew/base10,
27 | nimcrypto/sha]
28 |
29 | import ./frame, ./session, /types, ./http, ./extensions/extutils
30 |
31 | export session, frame, types, http, httptable
32 |
33 | logScope:
34 | topics = "websock ws-server"
35 |
36 | type
37 | WSServer* = ref object of WebSocket
38 | protocols: seq[string]
39 | factories: seq[ExtFactory]
40 |
41 | func toException(e: cstring): ref WebSocketError =
42 | (ref WebSocketError)(msg: $e)
43 |
44 | func contains(extensions: openArray[Ext], extName: string): bool =
45 | for ext in extensions:
46 | if ext.name == extName:
47 | return true
48 |
49 | proc getFactory(factories: openArray[ExtFactory], extName: string): ExtFactoryProc =
50 | for n in factories:
51 | if n.name == extName:
52 | return n.factory
53 |
54 | proc selectExt(isServer: bool,
55 | extensions: var seq[Ext],
56 | factories: openArray[ExtFactory],
57 | exts: openArray[string]): string {.raises: [WSExtError].} =
58 |
59 | var extList: seq[AppExt]
60 | var response = ""
61 | for ext in exts:
62 | # each of "Sec-WebSocket-Extensions" can have multiple
63 | # extensions or fallback extension
64 | if not parseExt(ext, extList):
65 | raise newException(WSExtError, "extension syntax error: " & ext)
66 |
67 | for i, ext in extList:
68 | if extensions.contains(ext.name):
69 | # don't accept this fallback if prev ext
70 | # configuration already accepted
71 | trace "extension fallback not accepted", ext=ext.name
72 | continue
73 |
74 | # now look for right factory
75 | let factory = factories.getFactory(ext.name)
76 | if factory.isNil:
77 | # no factory? it's ok, just skip it
78 | trace "no extension factory", ext=ext.name
79 | continue
80 |
81 | let extRes = factory(isServer, ext.params)
82 | if extRes.isErr:
83 | # cannot create extension because of
84 | # wrong/incompatible params? skip or fallback
85 | trace "skip extension", ext=ext.name, msg=extRes.error
86 | continue
87 |
88 | let ext = extRes.get()
89 | doAssert(not ext.isNil)
90 | if i > 0:
91 | # add separator if more than one exts
92 | response.add ", "
93 | response.add ext.toHttpOptions
94 |
95 | # finally, accept the extension
96 | trace "extension accepted", ext=ext.name
97 | extensions.add ext
98 |
99 | # HTTP response for "Sec-WebSocket-Extensions"
100 | response
101 |
102 | proc connect*(
103 | _: type WebSocket,
104 | host: string | TransportAddress,
105 | path: string,
106 | hostName: string = "", # override used when the hostname has been externally resolved
107 | protocols: seq[string] = @[],
108 | factories: seq[ExtFactory] = @[],
109 | hooks: seq[Hook] = @[],
110 | secure = false,
111 | flags: set[TLSFlags] = {},
112 | version = WSDefaultVersion,
113 | frameSize = WSDefaultFrameSize,
114 | onPing: ControlCb = nil,
115 | onPong: ControlCb = nil,
116 | onClose: CloseCb = nil,
117 | rng = HmacDrbgContext.new()): Future[WSSession] {.async.} =
118 |
119 | let
120 | key = Base64Pad.encode(WebSecKey.random(rng[]))
121 | hostname = if hostName.len > 0: hostName else: $host
122 |
123 | let client = if secure:
124 | await TlsHttpClient.connect(host, tlsFlags = flags, hostName = hostname)
125 | else:
126 | await HttpClient.connect(host)
127 |
128 | let headerData = [
129 | ("Connection", "Upgrade"),
130 | ("Upgrade", "websocket"),
131 | ("Cache-Control", "no-cache"),
132 | ("Sec-WebSocket-Version", $version),
133 | ("Sec-WebSocket-Key", key),
134 | ("Host", hostname)]
135 |
136 | var headers = HttpTable.init(headerData)
137 | if protocols.len > 0:
138 | headers.add("Sec-WebSocket-Protocol", protocols.join(", "))
139 |
140 | var extOffer = ""
141 | for i, f in factories:
142 | if i > 0:
143 | extOffer.add ", "
144 | extOffer.add f.clientOffer
145 |
146 | if extOffer.len > 0:
147 | headers.add("Sec-WebSocket-Extensions", extOffer)
148 |
149 | for hp in hooks:
150 | if hp.append == nil: continue
151 | let res = hp.append(hp, headers)
152 | if res.isErr:
153 | raise newException(WSHookError,
154 | "Header plugin execution failed: " & res.error)
155 |
156 | let response = try:
157 | await client.request(path, headers = headers)
158 | except CatchableError as exc:
159 | trace "Websocket failed during handshake", exc = exc.msg
160 | await client.close()
161 | raise exc
162 |
163 | if response.code != Http101.toInt():
164 | raise newException(WSFailedUpgradeError,
165 | &"Server did not reply with a websocket upgrade: " &
166 | &"Header code: {response.code} Header reason: {response.reason} " &
167 | &"Address: {client.address}")
168 |
169 | let proto = response.headers.getString("Sec-WebSocket-Protocol")
170 | if proto.len > 0 and protocols.len > 0:
171 | if proto notin protocols:
172 | raise newException(WSFailedUpgradeError,
173 | &"Invalid protocol returned {proto}!")
174 |
175 | for hp in hooks:
176 | if hp.verify == nil: continue
177 | let res = await hp.verify(hp, response.headers)
178 | if res.isErr:
179 | raise newException(WSHookError,
180 | "Header verification failed: " & res.error)
181 |
182 | var extensions: seq[Ext]
183 | let exts = response.headers.getList("Sec-WebSocket-Extensions")
184 | discard selectExt(false, extensions, factories, exts)
185 |
186 | # Client data should be masked.
187 | let session = WSSession(
188 | stream: client.stream,
189 | readyState: ReadyState.Open,
190 | masked: true,
191 | extensions: system.move(extensions),
192 | rng: rng,
193 | frameSize: frameSize,
194 | onPing: onPing,
195 | onPong: onPong,
196 | onClose: onClose)
197 |
198 | for ext in session.extensions:
199 | ext.session = session
200 |
201 | return session
202 |
203 | proc connect*(
204 | _: type WebSocket,
205 | uri: Uri,
206 | protocols: seq[string] = @[],
207 | factories: seq[ExtFactory] = @[],
208 | hooks: seq[Hook] = @[],
209 | flags: set[TLSFlags] = {},
210 | version = WSDefaultVersion,
211 | frameSize = WSDefaultFrameSize,
212 | onPing: ControlCb = nil,
213 | onPong: ControlCb = nil,
214 | onClose: CloseCb = nil,
215 | rng = HmacDrbgContext.new()): Future[WSSession]
216 | {.raises: [WSWrongUriSchemeError].} =
217 | ## Create a new websockets client
218 | ## using a Uri
219 | ##
220 |
221 | let secure = case uri.scheme:
222 | of "wss": true
223 | of "ws": false
224 | else:
225 | raise newException(WSWrongUriSchemeError,
226 | "uri scheme has to be 'ws' or 'wss'")
227 |
228 | var uri = uri
229 | if uri.port.len <= 0:
230 | uri.port = if secure: "443" else: "80"
231 |
232 | return WebSocket.connect(
233 | host = uri.hostname & ":" & uri.port,
234 | path = uri.path,
235 | hostName = uri.hostname,
236 | protocols = protocols,
237 | factories = factories,
238 | hooks = hooks,
239 | secure = secure,
240 | flags = flags,
241 | version = version,
242 | frameSize = frameSize,
243 | onPing = onPing,
244 | onPong = onPong,
245 | onClose = onClose,
246 | rng = rng)
247 |
248 | proc handleRequest*(
249 | ws: WSServer,
250 | request: HttpRequest,
251 | version: uint = WSDefaultVersion,
252 | hooks: seq[Hook] = @[]): Future[WSSession]
253 | {.
254 | async:
255 | (raises: [
256 | CancelledError,
257 | CatchableError,
258 | WSHandshakeError,
259 | WSProtoMismatchError])
260 | .} =
261 | ## Creates a new socket from a request.
262 | ##
263 |
264 | if not request.headers.contains("Sec-WebSocket-Version"):
265 | raise newException(WSHandshakeError, "Missing version header")
266 |
267 | ws.version = Base10.decode(
268 | uint,
269 | request.headers.getString("Sec-WebSocket-Version"))
270 | .tryGet() # this method throws
271 |
272 | if ws.version != version:
273 | await request.stream.writer.sendError(Http426)
274 | trace "Websocket version not supported", version = ws.version
275 |
276 | raise newException(WSVersionError,
277 | &"Websocket version not supported, Version: {version}")
278 |
279 | ws.key = request.headers.getString("Sec-WebSocket-Key").strip()
280 | let wantProtos = if request.headers.contains("Sec-WebSocket-Protocol"):
281 | request.headers.getList("Sec-WebSocket-Protocol")
282 | else:
283 | @[""]
284 |
285 | let protos = wantProtos.filterIt(
286 | it in ws.protocols
287 | )
288 |
289 | for hp in hooks:
290 | if hp.verify == nil: continue
291 | let res = await hp.verify(hp, request.headers)
292 | if res.isErr:
293 | raise newException(WSHookError,
294 | "Header verification failed: " & res.error)
295 |
296 | let
297 | cKey = ws.key & WSGuid
298 | acceptKey = Base64Pad.encode(
299 | sha1.digest(cKey.toOpenArray(0, cKey.high)).data)
300 |
301 | var headers = HttpTable.init([
302 | ("Connection", "Upgrade"),
303 | ("Upgrade", "websocket"),
304 | ("Sec-WebSocket-Accept", acceptKey)])
305 |
306 | let protocol = if protos.len > 0: protos[0] else: ""
307 | if protocol.len > 0:
308 | headers.add("Sec-WebSocket-Protocol", protocol) # send back the first matching proto
309 | else:
310 | trace "Didn't match any protocol", supported = ws.protocols, requested = wantProtos
311 |
312 | # it is possible to have multiple "Sec-WebSocket-Extensions"
313 | let exts = request.headers.getList("Sec-WebSocket-Extensions")
314 | let extResp = selectExt(true, ws.extensions, ws.factories, exts)
315 | if extResp.len > 0:
316 | # send back any accepted extensions
317 | headers.add("Sec-WebSocket-Extensions", extResp)
318 |
319 | for hp in hooks:
320 | if hp.append == nil: continue
321 | let res = hp.append(hp, headers)
322 | if res.isErr:
323 | raise newException(WSHookError,
324 | "Header plugin execution failed: " & res.error)
325 |
326 | try:
327 | await request.sendResponse(Http101, headers = headers)
328 | except CancelledError as exc:
329 | raise exc
330 | except CatchableError as exc:
331 | raise newException(WSHandshakeError,
332 | "Failed to sent handshake response. Error: " & exc.msg)
333 |
334 | let session = WSSession(
335 | readyState: ReadyState.Open,
336 | stream: request.stream,
337 | proto: protocol,
338 | extensions: system.move(ws.extensions),
339 | masked: false,
340 | rng: ws.rng,
341 | frameSize: ws.frameSize,
342 | onPing: ws.onPing,
343 | onPong: ws.onPong,
344 | onClose: ws.onClose)
345 |
346 | for ext in session.extensions:
347 | ext.session = session
348 |
349 | return session
350 |
351 | proc new*(
352 | _: typedesc[WSServer],
353 | protos: openArray[string] = [""],
354 | factories: openArray[ExtFactory] = [],
355 | frameSize = WSDefaultFrameSize,
356 | onPing: ControlCb = nil,
357 | onPong: ControlCb = nil,
358 | onClose: CloseCb = nil,
359 | rng = HmacDrbgContext.new()): WSServer =
360 |
361 | return WSServer(
362 | protocols: @protos,
363 | masked: false,
364 | rng: rng,
365 | frameSize: frameSize,
366 | factories: @factories,
367 | onPing: onPing,
368 | onPong: onPong,
369 | onClose: onClose)
370 |
--------------------------------------------------------------------------------