├── .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 | Websock Logo 2 | 3 | # Websocket for Nim 4 | 5 | [![GH Action](https://github.com/status-im/nim-websock/actions/workflows/ci.yml/badge.svg)](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 | image/svg+xml 53 | 56 | 60 | 61 | 64 | 67 | 70 | 73 | 76 | 79 | 82 | 85 | 88 | 91 | 94 | 97 | 100 | 103 | 106 | 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 | --------------------------------------------------------------------------------