├── .gitignore ├── LICENSE ├── README.md ├── build-livereload.sh ├── package-lock.json ├── package.json ├── serve-http.d.ts ├── serve-http.js ├── serve-http.js.map └── src ├── dirlist.js ├── fmt.js ├── livereload.js ├── main.js ├── mime.js ├── parseopts.js ├── safe.js ├── server.js ├── util.js └── ws ├── LICENSE ├── async-limiter.js ├── buffer-util.js ├── constants.js ├── event-target.js ├── extension.js ├── index.js ├── permessage-deflate.js ├── receiver.js ├── sender.js ├── stream.js ├── validation.js ├── websocket-server.js └── websocket.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.g 3 | *.g.map 4 | *.sublime* 5 | report.* 6 | /node_modules 7 | /_* 8 | /build 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Rasmus Andersson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # serve-http 2 | 3 | Simple single-file local web server 4 | 5 | `npm i -g serve-http` 6 | 7 | - Single file without dependencies — copy it into your project. 8 | - Livereload — HTML pages reload automatically in web browsers as they change. 9 | - Safety feature: Only serves local connections unless given explicit command-line argument. 10 | - Safety feature: Refuses to serve directories outside home directory to remote connections. 11 | 12 | Install through [`npm i -g serve-http`](https://www.npmjs.com/package/serve-http) or by copying 13 | [`serve-http`](https://raw.githubusercontent.com/rsms/serve-http/master/serve-http) 14 | into your project (no dependencies; entire thing contained in a single small file.) 15 | 16 | ``` 17 | $ ./serve-http -help 18 | Usage: serve-http [options] [] 19 | 20 | 21 | Directory to serve files from. Defaults to the current directory. 22 | 23 | Options: 24 | -p, -port Listen on specific 25 | -host Bind to instead of "localhost" 26 | -public Accept connections from anywhere (same as -host "") 27 | -q, -quiet Don't log requests 28 | -no-livereload Disable livereload 29 | -no-dirlist Disable directory listing 30 | -dirlist-hidden Show files beginning with "." in directory listings 31 | -h, -help Show help and exit 32 | -version Print version to stdout and exit 33 | 34 | Examples: 35 | 36 | serve-http 37 | Serve current directory on some available port 38 | 39 | serve-http -p 8080 docs 40 | Serve directory ./docs locally on port 8080 41 | 42 | serve-http -public -no-dirlist 43 | Serve current directory publicly on some available port, 44 | without directory listing. 45 | 46 | ``` 47 | 48 | ## JavaScript API 49 | 50 | serve-http can also be used as a library: 51 | 52 | ```js 53 | const { createServer } = require("serve-http") 54 | const server = createServer({ pubdir: __dirname, port: 1234 }) 55 | // `server` is a standard nodejs http server instance. 56 | ``` 57 | 58 | See TypeScript type definitions for documentation of the API: 59 | [`serve-http.d.ts`](serve-http.d.ts) 60 | -------------------------------------------------------------------------------- /build-livereload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | cd "$(dirname "$0")" 4 | export PATH=$PWD/node_modules/.bin:$PATH 5 | mkdir -p build 6 | 7 | SRCFILE=node_modules/livereload-js/dist/livereload.js 8 | TMPFILE=build/livereload.min.js 9 | OUTFILE=build/livereload-script.js 10 | 11 | if [ $OUTFILE -nt "$SRCFILE" ]; then 12 | echo "$OUTFILE up to date" 13 | exit 14 | fi 15 | 16 | echo "$SRCFILE -> $TMPFILE" 17 | esbuild \ 18 | --minify \ 19 | --platform=browser \ 20 | --target=es2017 \ 21 | --outfile="$TMPFILE" \ 22 | "$SRCFILE" 23 | 24 | echo "$TMPFILE -> $OUTFILE" 25 | node <= 8" 33 | } 34 | }, 35 | "node_modules/binary-extensions": { 36 | "version": "2.2.0", 37 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 38 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 39 | "dev": true, 40 | "engines": { 41 | "node": ">=8" 42 | } 43 | }, 44 | "node_modules/braces": { 45 | "version": "3.0.2", 46 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 47 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 48 | "dev": true, 49 | "dependencies": { 50 | "fill-range": "^7.0.1" 51 | }, 52 | "engines": { 53 | "node": ">=8" 54 | } 55 | }, 56 | "node_modules/buffer-from": { 57 | "version": "1.1.2", 58 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", 59 | "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", 60 | "optional": true 61 | }, 62 | "node_modules/chokidar": { 63 | "version": "3.5.3", 64 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", 65 | "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", 66 | "dev": true, 67 | "funding": [ 68 | { 69 | "type": "individual", 70 | "url": "https://paulmillr.com/funding/" 71 | } 72 | ], 73 | "dependencies": { 74 | "anymatch": "~3.1.2", 75 | "braces": "~3.0.2", 76 | "glob-parent": "~5.1.2", 77 | "is-binary-path": "~2.1.0", 78 | "is-glob": "~4.0.1", 79 | "normalize-path": "~3.0.0", 80 | "readdirp": "~3.6.0" 81 | }, 82 | "engines": { 83 | "node": ">= 8.10.0" 84 | }, 85 | "optionalDependencies": { 86 | "fsevents": "~2.3.2" 87 | } 88 | }, 89 | "node_modules/esbuild": { 90 | "version": "0.14.49", 91 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.49.tgz", 92 | "integrity": "sha512-/TlVHhOaq7Yz8N1OJrjqM3Auzo5wjvHFLk+T8pIue+fhnhIMpfAzsG6PLVMbFveVxqD2WOp3QHei+52IMUNmCw==", 93 | "dev": true, 94 | "hasInstallScript": true, 95 | "bin": { 96 | "esbuild": "bin/esbuild" 97 | }, 98 | "engines": { 99 | "node": ">=12" 100 | }, 101 | "optionalDependencies": { 102 | "esbuild-android-64": "0.14.49", 103 | "esbuild-android-arm64": "0.14.49", 104 | "esbuild-darwin-64": "0.14.49", 105 | "esbuild-darwin-arm64": "0.14.49", 106 | "esbuild-freebsd-64": "0.14.49", 107 | "esbuild-freebsd-arm64": "0.14.49", 108 | "esbuild-linux-32": "0.14.49", 109 | "esbuild-linux-64": "0.14.49", 110 | "esbuild-linux-arm": "0.14.49", 111 | "esbuild-linux-arm64": "0.14.49", 112 | "esbuild-linux-mips64le": "0.14.49", 113 | "esbuild-linux-ppc64le": "0.14.49", 114 | "esbuild-linux-riscv64": "0.14.49", 115 | "esbuild-linux-s390x": "0.14.49", 116 | "esbuild-netbsd-64": "0.14.49", 117 | "esbuild-openbsd-64": "0.14.49", 118 | "esbuild-sunos-64": "0.14.49", 119 | "esbuild-windows-32": "0.14.49", 120 | "esbuild-windows-64": "0.14.49", 121 | "esbuild-windows-arm64": "0.14.49" 122 | } 123 | }, 124 | "node_modules/esbuild-android-64": { 125 | "version": "0.14.49", 126 | "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.49.tgz", 127 | "integrity": "sha512-vYsdOTD+yi+kquhBiFWl3tyxnj2qZJsl4tAqwhT90ktUdnyTizgle7TjNx6Ar1bN7wcwWqZ9QInfdk2WVagSww==", 128 | "cpu": [ 129 | "x64" 130 | ], 131 | "dev": true, 132 | "optional": true, 133 | "os": [ 134 | "android" 135 | ], 136 | "engines": { 137 | "node": ">=12" 138 | } 139 | }, 140 | "node_modules/esbuild-android-arm64": { 141 | "version": "0.14.49", 142 | "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.49.tgz", 143 | "integrity": "sha512-g2HGr/hjOXCgSsvQZ1nK4nW/ei8JUx04Li74qub9qWrStlysaVmadRyTVuW32FGIpLQyc5sUjjZopj49eGGM2g==", 144 | "cpu": [ 145 | "arm64" 146 | ], 147 | "dev": true, 148 | "optional": true, 149 | "os": [ 150 | "android" 151 | ], 152 | "engines": { 153 | "node": ">=12" 154 | } 155 | }, 156 | "node_modules/esbuild-darwin-64": { 157 | "version": "0.14.49", 158 | "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.49.tgz", 159 | "integrity": "sha512-3rvqnBCtX9ywso5fCHixt2GBCUsogNp9DjGmvbBohh31Ces34BVzFltMSxJpacNki96+WIcX5s/vum+ckXiLYg==", 160 | "cpu": [ 161 | "x64" 162 | ], 163 | "dev": true, 164 | "optional": true, 165 | "os": [ 166 | "darwin" 167 | ], 168 | "engines": { 169 | "node": ">=12" 170 | } 171 | }, 172 | "node_modules/esbuild-darwin-arm64": { 173 | "version": "0.14.49", 174 | "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.49.tgz", 175 | "integrity": "sha512-XMaqDxO846srnGlUSJnwbijV29MTKUATmOLyQSfswbK/2X5Uv28M9tTLUJcKKxzoo9lnkYPsx2o8EJcTYwCs/A==", 176 | "cpu": [ 177 | "arm64" 178 | ], 179 | "dev": true, 180 | "optional": true, 181 | "os": [ 182 | "darwin" 183 | ], 184 | "engines": { 185 | "node": ">=12" 186 | } 187 | }, 188 | "node_modules/esbuild-freebsd-64": { 189 | "version": "0.14.49", 190 | "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.49.tgz", 191 | "integrity": "sha512-NJ5Q6AjV879mOHFri+5lZLTp5XsO2hQ+KSJYLbfY9DgCu8s6/Zl2prWXVANYTeCDLlrIlNNYw8y34xqyLDKOmQ==", 192 | "cpu": [ 193 | "x64" 194 | ], 195 | "dev": true, 196 | "optional": true, 197 | "os": [ 198 | "freebsd" 199 | ], 200 | "engines": { 201 | "node": ">=12" 202 | } 203 | }, 204 | "node_modules/esbuild-freebsd-arm64": { 205 | "version": "0.14.49", 206 | "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.49.tgz", 207 | "integrity": "sha512-lFLtgXnAc3eXYqj5koPlBZvEbBSOSUbWO3gyY/0+4lBdRqELyz4bAuamHvmvHW5swJYL7kngzIZw6kdu25KGOA==", 208 | "cpu": [ 209 | "arm64" 210 | ], 211 | "dev": true, 212 | "optional": true, 213 | "os": [ 214 | "freebsd" 215 | ], 216 | "engines": { 217 | "node": ">=12" 218 | } 219 | }, 220 | "node_modules/esbuild-linux-32": { 221 | "version": "0.14.49", 222 | "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.49.tgz", 223 | "integrity": "sha512-zTTH4gr2Kb8u4QcOpTDVn7Z8q7QEIvFl/+vHrI3cF6XOJS7iEI1FWslTo3uofB2+mn6sIJEQD9PrNZKoAAMDiA==", 224 | "cpu": [ 225 | "ia32" 226 | ], 227 | "dev": true, 228 | "optional": true, 229 | "os": [ 230 | "linux" 231 | ], 232 | "engines": { 233 | "node": ">=12" 234 | } 235 | }, 236 | "node_modules/esbuild-linux-64": { 237 | "version": "0.14.49", 238 | "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.49.tgz", 239 | "integrity": "sha512-hYmzRIDzFfLrB5c1SknkxzM8LdEUOusp6M2TnuQZJLRtxTgyPnZZVtyMeCLki0wKgYPXkFsAVhi8vzo2mBNeTg==", 240 | "cpu": [ 241 | "x64" 242 | ], 243 | "dev": true, 244 | "optional": true, 245 | "os": [ 246 | "linux" 247 | ], 248 | "engines": { 249 | "node": ">=12" 250 | } 251 | }, 252 | "node_modules/esbuild-linux-arm": { 253 | "version": "0.14.49", 254 | "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.49.tgz", 255 | "integrity": "sha512-iE3e+ZVv1Qz1Sy0gifIsarJMQ89Rpm9mtLSRtG3AH0FPgAzQ5Z5oU6vYzhc/3gSPi2UxdCOfRhw2onXuFw/0lg==", 256 | "cpu": [ 257 | "arm" 258 | ], 259 | "dev": true, 260 | "optional": true, 261 | "os": [ 262 | "linux" 263 | ], 264 | "engines": { 265 | "node": ">=12" 266 | } 267 | }, 268 | "node_modules/esbuild-linux-arm64": { 269 | "version": "0.14.49", 270 | "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.49.tgz", 271 | "integrity": "sha512-KLQ+WpeuY+7bxukxLz5VgkAAVQxUv67Ft4DmHIPIW+2w3ObBPQhqNoeQUHxopoW/aiOn3m99NSmSV+bs4BSsdA==", 272 | "cpu": [ 273 | "arm64" 274 | ], 275 | "dev": true, 276 | "optional": true, 277 | "os": [ 278 | "linux" 279 | ], 280 | "engines": { 281 | "node": ">=12" 282 | } 283 | }, 284 | "node_modules/esbuild-linux-mips64le": { 285 | "version": "0.14.49", 286 | "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.49.tgz", 287 | "integrity": "sha512-n+rGODfm8RSum5pFIqFQVQpYBw+AztL8s6o9kfx7tjfK0yIGF6tm5HlG6aRjodiiKkH2xAiIM+U4xtQVZYU4rA==", 288 | "cpu": [ 289 | "mips64el" 290 | ], 291 | "dev": true, 292 | "optional": true, 293 | "os": [ 294 | "linux" 295 | ], 296 | "engines": { 297 | "node": ">=12" 298 | } 299 | }, 300 | "node_modules/esbuild-linux-ppc64le": { 301 | "version": "0.14.49", 302 | "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.49.tgz", 303 | "integrity": "sha512-WP9zR4HX6iCBmMFH+XHHng2LmdoIeUmBpL4aL2TR8ruzXyT4dWrJ5BSbT8iNo6THN8lod6GOmYDLq/dgZLalGw==", 304 | "cpu": [ 305 | "ppc64" 306 | ], 307 | "dev": true, 308 | "optional": true, 309 | "os": [ 310 | "linux" 311 | ], 312 | "engines": { 313 | "node": ">=12" 314 | } 315 | }, 316 | "node_modules/esbuild-linux-riscv64": { 317 | "version": "0.14.49", 318 | "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.49.tgz", 319 | "integrity": "sha512-h66ORBz+Dg+1KgLvzTVQEA1LX4XBd1SK0Fgbhhw4akpG/YkN8pS6OzYI/7SGENiN6ao5hETRDSkVcvU9NRtkMQ==", 320 | "cpu": [ 321 | "riscv64" 322 | ], 323 | "dev": true, 324 | "optional": true, 325 | "os": [ 326 | "linux" 327 | ], 328 | "engines": { 329 | "node": ">=12" 330 | } 331 | }, 332 | "node_modules/esbuild-linux-s390x": { 333 | "version": "0.14.49", 334 | "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.49.tgz", 335 | "integrity": "sha512-DhrUoFVWD+XmKO1y7e4kNCqQHPs6twz6VV6Uezl/XHYGzM60rBewBF5jlZjG0nCk5W/Xy6y1xWeopkrhFFM0sQ==", 336 | "cpu": [ 337 | "s390x" 338 | ], 339 | "dev": true, 340 | "optional": true, 341 | "os": [ 342 | "linux" 343 | ], 344 | "engines": { 345 | "node": ">=12" 346 | } 347 | }, 348 | "node_modules/esbuild-netbsd-64": { 349 | "version": "0.14.49", 350 | "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.49.tgz", 351 | "integrity": "sha512-BXaUwFOfCy2T+hABtiPUIpWjAeWK9P8O41gR4Pg73hpzoygVGnj0nI3YK4SJhe52ELgtdgWP/ckIkbn2XaTxjQ==", 352 | "cpu": [ 353 | "x64" 354 | ], 355 | "dev": true, 356 | "optional": true, 357 | "os": [ 358 | "netbsd" 359 | ], 360 | "engines": { 361 | "node": ">=12" 362 | } 363 | }, 364 | "node_modules/esbuild-openbsd-64": { 365 | "version": "0.14.49", 366 | "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.49.tgz", 367 | "integrity": "sha512-lP06UQeLDGmVPw9Rg437Btu6J9/BmyhdoefnQ4gDEJTtJvKtQaUcOQrhjTq455ouZN4EHFH1h28WOJVANK41kA==", 368 | "cpu": [ 369 | "x64" 370 | ], 371 | "dev": true, 372 | "optional": true, 373 | "os": [ 374 | "openbsd" 375 | ], 376 | "engines": { 377 | "node": ">=12" 378 | } 379 | }, 380 | "node_modules/esbuild-sunos-64": { 381 | "version": "0.14.49", 382 | "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.49.tgz", 383 | "integrity": "sha512-4c8Zowp+V3zIWje329BeLbGh6XI9c/rqARNaj5yPHdC61pHI9UNdDxT3rePPJeWcEZVKjkiAS6AP6kiITp7FSw==", 384 | "cpu": [ 385 | "x64" 386 | ], 387 | "dev": true, 388 | "optional": true, 389 | "os": [ 390 | "sunos" 391 | ], 392 | "engines": { 393 | "node": ">=12" 394 | } 395 | }, 396 | "node_modules/esbuild-windows-32": { 397 | "version": "0.14.49", 398 | "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.49.tgz", 399 | "integrity": "sha512-q7Rb+J9yHTeKr9QTPDYkqfkEj8/kcKz9lOabDuvEXpXuIcosWCJgo5Z7h/L4r7rbtTH4a8U2FGKb6s1eeOHmJA==", 400 | "cpu": [ 401 | "ia32" 402 | ], 403 | "dev": true, 404 | "optional": true, 405 | "os": [ 406 | "win32" 407 | ], 408 | "engines": { 409 | "node": ">=12" 410 | } 411 | }, 412 | "node_modules/esbuild-windows-64": { 413 | "version": "0.14.49", 414 | "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.49.tgz", 415 | "integrity": "sha512-+Cme7Ongv0UIUTniPqfTX6mJ8Deo7VXw9xN0yJEN1lQMHDppTNmKwAM3oGbD/Vqff+07K2gN0WfNkMohmG+dVw==", 416 | "cpu": [ 417 | "x64" 418 | ], 419 | "dev": true, 420 | "optional": true, 421 | "os": [ 422 | "win32" 423 | ], 424 | "engines": { 425 | "node": ">=12" 426 | } 427 | }, 428 | "node_modules/esbuild-windows-arm64": { 429 | "version": "0.14.49", 430 | "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.49.tgz", 431 | "integrity": "sha512-v+HYNAXzuANrCbbLFJ5nmO3m5y2PGZWLe3uloAkLt87aXiO2mZr3BTmacZdjwNkNEHuH3bNtN8cak+mzVjVPfA==", 432 | "cpu": [ 433 | "arm64" 434 | ], 435 | "dev": true, 436 | "optional": true, 437 | "os": [ 438 | "win32" 439 | ], 440 | "engines": { 441 | "node": ">=12" 442 | } 443 | }, 444 | "node_modules/fill-range": { 445 | "version": "7.0.1", 446 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 447 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 448 | "dev": true, 449 | "dependencies": { 450 | "to-regex-range": "^5.0.1" 451 | }, 452 | "engines": { 453 | "node": ">=8" 454 | } 455 | }, 456 | "node_modules/fsevents": { 457 | "version": "2.3.2", 458 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 459 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 460 | "dev": true, 461 | "hasInstallScript": true, 462 | "optional": true, 463 | "os": [ 464 | "darwin" 465 | ], 466 | "engines": { 467 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 468 | } 469 | }, 470 | "node_modules/glob-parent": { 471 | "version": "5.1.2", 472 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 473 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 474 | "dev": true, 475 | "dependencies": { 476 | "is-glob": "^4.0.1" 477 | }, 478 | "engines": { 479 | "node": ">= 6" 480 | } 481 | }, 482 | "node_modules/is-binary-path": { 483 | "version": "2.1.0", 484 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 485 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 486 | "dev": true, 487 | "dependencies": { 488 | "binary-extensions": "^2.0.0" 489 | }, 490 | "engines": { 491 | "node": ">=8" 492 | } 493 | }, 494 | "node_modules/is-extglob": { 495 | "version": "2.1.1", 496 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 497 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 498 | "dev": true, 499 | "engines": { 500 | "node": ">=0.10.0" 501 | } 502 | }, 503 | "node_modules/is-glob": { 504 | "version": "4.0.3", 505 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 506 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 507 | "dev": true, 508 | "dependencies": { 509 | "is-extglob": "^2.1.1" 510 | }, 511 | "engines": { 512 | "node": ">=0.10.0" 513 | } 514 | }, 515 | "node_modules/is-number": { 516 | "version": "7.0.0", 517 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 518 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 519 | "dev": true, 520 | "engines": { 521 | "node": ">=0.12.0" 522 | } 523 | }, 524 | "node_modules/livereload": { 525 | "version": "0.9.3", 526 | "resolved": "https://registry.npmjs.org/livereload/-/livereload-0.9.3.tgz", 527 | "integrity": "sha512-q7Z71n3i4X0R9xthAryBdNGVGAO2R5X+/xXpmKeuPMrteg+W2U8VusTKV3YiJbXZwKsOlFlHe+go6uSNjfxrZw==", 528 | "dev": true, 529 | "dependencies": { 530 | "chokidar": "^3.5.0", 531 | "livereload-js": "^3.3.1", 532 | "opts": ">= 1.2.0", 533 | "ws": "^7.4.3" 534 | }, 535 | "bin": { 536 | "livereload": "bin/livereload.js" 537 | }, 538 | "engines": { 539 | "node": ">=8.0.0" 540 | } 541 | }, 542 | "node_modules/livereload-js": { 543 | "version": "3.4.0", 544 | "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-3.4.0.tgz", 545 | "integrity": "sha512-F/pz9ZZP+R+arY94cECTZco7PXgBXyL+KVWUPZq8AQE9TOu14GV6fYeKOviv02JCvFa4Oi3Rs1hYEpfeajc+ow==", 546 | "dev": true 547 | }, 548 | "node_modules/normalize-path": { 549 | "version": "3.0.0", 550 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 551 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 552 | "dev": true, 553 | "engines": { 554 | "node": ">=0.10.0" 555 | } 556 | }, 557 | "node_modules/opts": { 558 | "version": "1.2.7", 559 | "resolved": "https://registry.npmjs.org/opts/-/opts-1.2.7.tgz", 560 | "integrity": "sha512-hwZhzGGG/GQ7igxAVFOEun2N4fWul31qE9nfBdCnZGQCB5+L7tN9xZ+94B4aUpLOJx/of3zZs5XsuubayQYQjA==", 561 | "dev": true 562 | }, 563 | "node_modules/picomatch": { 564 | "version": "2.3.1", 565 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 566 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 567 | "dev": true, 568 | "engines": { 569 | "node": ">=8.6" 570 | }, 571 | "funding": { 572 | "url": "https://github.com/sponsors/jonschlinkert" 573 | } 574 | }, 575 | "node_modules/readdirp": { 576 | "version": "3.6.0", 577 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 578 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 579 | "dev": true, 580 | "dependencies": { 581 | "picomatch": "^2.2.1" 582 | }, 583 | "engines": { 584 | "node": ">=8.10.0" 585 | } 586 | }, 587 | "node_modules/source-map": { 588 | "version": "0.6.1", 589 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 590 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 591 | "optional": true, 592 | "engines": { 593 | "node": ">=0.10.0" 594 | } 595 | }, 596 | "node_modules/source-map-support": { 597 | "version": "0.5.21", 598 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", 599 | "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", 600 | "optional": true, 601 | "dependencies": { 602 | "buffer-from": "^1.0.0", 603 | "source-map": "^0.6.0" 604 | } 605 | }, 606 | "node_modules/to-regex-range": { 607 | "version": "5.0.1", 608 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 609 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 610 | "dev": true, 611 | "dependencies": { 612 | "is-number": "^7.0.0" 613 | }, 614 | "engines": { 615 | "node": ">=8.0" 616 | } 617 | }, 618 | "node_modules/ws": { 619 | "version": "7.5.9", 620 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", 621 | "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", 622 | "dev": true, 623 | "engines": { 624 | "node": ">=8.3.0" 625 | }, 626 | "peerDependencies": { 627 | "bufferutil": "^4.0.1", 628 | "utf-8-validate": "^5.0.2" 629 | }, 630 | "peerDependenciesMeta": { 631 | "bufferutil": { 632 | "optional": true 633 | }, 634 | "utf-8-validate": { 635 | "optional": true 636 | } 637 | } 638 | } 639 | }, 640 | "dependencies": { 641 | "anymatch": { 642 | "version": "3.1.2", 643 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", 644 | "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", 645 | "dev": true, 646 | "requires": { 647 | "normalize-path": "^3.0.0", 648 | "picomatch": "^2.0.4" 649 | } 650 | }, 651 | "binary-extensions": { 652 | "version": "2.2.0", 653 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 654 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 655 | "dev": true 656 | }, 657 | "braces": { 658 | "version": "3.0.2", 659 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 660 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 661 | "dev": true, 662 | "requires": { 663 | "fill-range": "^7.0.1" 664 | } 665 | }, 666 | "buffer-from": { 667 | "version": "1.1.2", 668 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", 669 | "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", 670 | "optional": true 671 | }, 672 | "chokidar": { 673 | "version": "3.5.3", 674 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", 675 | "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", 676 | "dev": true, 677 | "requires": { 678 | "anymatch": "~3.1.2", 679 | "braces": "~3.0.2", 680 | "fsevents": "~2.3.2", 681 | "glob-parent": "~5.1.2", 682 | "is-binary-path": "~2.1.0", 683 | "is-glob": "~4.0.1", 684 | "normalize-path": "~3.0.0", 685 | "readdirp": "~3.6.0" 686 | } 687 | }, 688 | "esbuild": { 689 | "version": "0.14.49", 690 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.49.tgz", 691 | "integrity": "sha512-/TlVHhOaq7Yz8N1OJrjqM3Auzo5wjvHFLk+T8pIue+fhnhIMpfAzsG6PLVMbFveVxqD2WOp3QHei+52IMUNmCw==", 692 | "dev": true, 693 | "requires": { 694 | "esbuild-android-64": "0.14.49", 695 | "esbuild-android-arm64": "0.14.49", 696 | "esbuild-darwin-64": "0.14.49", 697 | "esbuild-darwin-arm64": "0.14.49", 698 | "esbuild-freebsd-64": "0.14.49", 699 | "esbuild-freebsd-arm64": "0.14.49", 700 | "esbuild-linux-32": "0.14.49", 701 | "esbuild-linux-64": "0.14.49", 702 | "esbuild-linux-arm": "0.14.49", 703 | "esbuild-linux-arm64": "0.14.49", 704 | "esbuild-linux-mips64le": "0.14.49", 705 | "esbuild-linux-ppc64le": "0.14.49", 706 | "esbuild-linux-riscv64": "0.14.49", 707 | "esbuild-linux-s390x": "0.14.49", 708 | "esbuild-netbsd-64": "0.14.49", 709 | "esbuild-openbsd-64": "0.14.49", 710 | "esbuild-sunos-64": "0.14.49", 711 | "esbuild-windows-32": "0.14.49", 712 | "esbuild-windows-64": "0.14.49", 713 | "esbuild-windows-arm64": "0.14.49" 714 | } 715 | }, 716 | "esbuild-android-64": { 717 | "version": "0.14.49", 718 | "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.49.tgz", 719 | "integrity": "sha512-vYsdOTD+yi+kquhBiFWl3tyxnj2qZJsl4tAqwhT90ktUdnyTizgle7TjNx6Ar1bN7wcwWqZ9QInfdk2WVagSww==", 720 | "dev": true, 721 | "optional": true 722 | }, 723 | "esbuild-android-arm64": { 724 | "version": "0.14.49", 725 | "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.49.tgz", 726 | "integrity": "sha512-g2HGr/hjOXCgSsvQZ1nK4nW/ei8JUx04Li74qub9qWrStlysaVmadRyTVuW32FGIpLQyc5sUjjZopj49eGGM2g==", 727 | "dev": true, 728 | "optional": true 729 | }, 730 | "esbuild-darwin-64": { 731 | "version": "0.14.49", 732 | "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.49.tgz", 733 | "integrity": "sha512-3rvqnBCtX9ywso5fCHixt2GBCUsogNp9DjGmvbBohh31Ces34BVzFltMSxJpacNki96+WIcX5s/vum+ckXiLYg==", 734 | "dev": true, 735 | "optional": true 736 | }, 737 | "esbuild-darwin-arm64": { 738 | "version": "0.14.49", 739 | "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.49.tgz", 740 | "integrity": "sha512-XMaqDxO846srnGlUSJnwbijV29MTKUATmOLyQSfswbK/2X5Uv28M9tTLUJcKKxzoo9lnkYPsx2o8EJcTYwCs/A==", 741 | "dev": true, 742 | "optional": true 743 | }, 744 | "esbuild-freebsd-64": { 745 | "version": "0.14.49", 746 | "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.49.tgz", 747 | "integrity": "sha512-NJ5Q6AjV879mOHFri+5lZLTp5XsO2hQ+KSJYLbfY9DgCu8s6/Zl2prWXVANYTeCDLlrIlNNYw8y34xqyLDKOmQ==", 748 | "dev": true, 749 | "optional": true 750 | }, 751 | "esbuild-freebsd-arm64": { 752 | "version": "0.14.49", 753 | "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.49.tgz", 754 | "integrity": "sha512-lFLtgXnAc3eXYqj5koPlBZvEbBSOSUbWO3gyY/0+4lBdRqELyz4bAuamHvmvHW5swJYL7kngzIZw6kdu25KGOA==", 755 | "dev": true, 756 | "optional": true 757 | }, 758 | "esbuild-linux-32": { 759 | "version": "0.14.49", 760 | "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.49.tgz", 761 | "integrity": "sha512-zTTH4gr2Kb8u4QcOpTDVn7Z8q7QEIvFl/+vHrI3cF6XOJS7iEI1FWslTo3uofB2+mn6sIJEQD9PrNZKoAAMDiA==", 762 | "dev": true, 763 | "optional": true 764 | }, 765 | "esbuild-linux-64": { 766 | "version": "0.14.49", 767 | "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.49.tgz", 768 | "integrity": "sha512-hYmzRIDzFfLrB5c1SknkxzM8LdEUOusp6M2TnuQZJLRtxTgyPnZZVtyMeCLki0wKgYPXkFsAVhi8vzo2mBNeTg==", 769 | "dev": true, 770 | "optional": true 771 | }, 772 | "esbuild-linux-arm": { 773 | "version": "0.14.49", 774 | "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.49.tgz", 775 | "integrity": "sha512-iE3e+ZVv1Qz1Sy0gifIsarJMQ89Rpm9mtLSRtG3AH0FPgAzQ5Z5oU6vYzhc/3gSPi2UxdCOfRhw2onXuFw/0lg==", 776 | "dev": true, 777 | "optional": true 778 | }, 779 | "esbuild-linux-arm64": { 780 | "version": "0.14.49", 781 | "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.49.tgz", 782 | "integrity": "sha512-KLQ+WpeuY+7bxukxLz5VgkAAVQxUv67Ft4DmHIPIW+2w3ObBPQhqNoeQUHxopoW/aiOn3m99NSmSV+bs4BSsdA==", 783 | "dev": true, 784 | "optional": true 785 | }, 786 | "esbuild-linux-mips64le": { 787 | "version": "0.14.49", 788 | "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.49.tgz", 789 | "integrity": "sha512-n+rGODfm8RSum5pFIqFQVQpYBw+AztL8s6o9kfx7tjfK0yIGF6tm5HlG6aRjodiiKkH2xAiIM+U4xtQVZYU4rA==", 790 | "dev": true, 791 | "optional": true 792 | }, 793 | "esbuild-linux-ppc64le": { 794 | "version": "0.14.49", 795 | "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.49.tgz", 796 | "integrity": "sha512-WP9zR4HX6iCBmMFH+XHHng2LmdoIeUmBpL4aL2TR8ruzXyT4dWrJ5BSbT8iNo6THN8lod6GOmYDLq/dgZLalGw==", 797 | "dev": true, 798 | "optional": true 799 | }, 800 | "esbuild-linux-riscv64": { 801 | "version": "0.14.49", 802 | "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.49.tgz", 803 | "integrity": "sha512-h66ORBz+Dg+1KgLvzTVQEA1LX4XBd1SK0Fgbhhw4akpG/YkN8pS6OzYI/7SGENiN6ao5hETRDSkVcvU9NRtkMQ==", 804 | "dev": true, 805 | "optional": true 806 | }, 807 | "esbuild-linux-s390x": { 808 | "version": "0.14.49", 809 | "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.49.tgz", 810 | "integrity": "sha512-DhrUoFVWD+XmKO1y7e4kNCqQHPs6twz6VV6Uezl/XHYGzM60rBewBF5jlZjG0nCk5W/Xy6y1xWeopkrhFFM0sQ==", 811 | "dev": true, 812 | "optional": true 813 | }, 814 | "esbuild-netbsd-64": { 815 | "version": "0.14.49", 816 | "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.49.tgz", 817 | "integrity": "sha512-BXaUwFOfCy2T+hABtiPUIpWjAeWK9P8O41gR4Pg73hpzoygVGnj0nI3YK4SJhe52ELgtdgWP/ckIkbn2XaTxjQ==", 818 | "dev": true, 819 | "optional": true 820 | }, 821 | "esbuild-openbsd-64": { 822 | "version": "0.14.49", 823 | "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.49.tgz", 824 | "integrity": "sha512-lP06UQeLDGmVPw9Rg437Btu6J9/BmyhdoefnQ4gDEJTtJvKtQaUcOQrhjTq455ouZN4EHFH1h28WOJVANK41kA==", 825 | "dev": true, 826 | "optional": true 827 | }, 828 | "esbuild-sunos-64": { 829 | "version": "0.14.49", 830 | "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.49.tgz", 831 | "integrity": "sha512-4c8Zowp+V3zIWje329BeLbGh6XI9c/rqARNaj5yPHdC61pHI9UNdDxT3rePPJeWcEZVKjkiAS6AP6kiITp7FSw==", 832 | "dev": true, 833 | "optional": true 834 | }, 835 | "esbuild-windows-32": { 836 | "version": "0.14.49", 837 | "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.49.tgz", 838 | "integrity": "sha512-q7Rb+J9yHTeKr9QTPDYkqfkEj8/kcKz9lOabDuvEXpXuIcosWCJgo5Z7h/L4r7rbtTH4a8U2FGKb6s1eeOHmJA==", 839 | "dev": true, 840 | "optional": true 841 | }, 842 | "esbuild-windows-64": { 843 | "version": "0.14.49", 844 | "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.49.tgz", 845 | "integrity": "sha512-+Cme7Ongv0UIUTniPqfTX6mJ8Deo7VXw9xN0yJEN1lQMHDppTNmKwAM3oGbD/Vqff+07K2gN0WfNkMohmG+dVw==", 846 | "dev": true, 847 | "optional": true 848 | }, 849 | "esbuild-windows-arm64": { 850 | "version": "0.14.49", 851 | "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.49.tgz", 852 | "integrity": "sha512-v+HYNAXzuANrCbbLFJ5nmO3m5y2PGZWLe3uloAkLt87aXiO2mZr3BTmacZdjwNkNEHuH3bNtN8cak+mzVjVPfA==", 853 | "dev": true, 854 | "optional": true 855 | }, 856 | "fill-range": { 857 | "version": "7.0.1", 858 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 859 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 860 | "dev": true, 861 | "requires": { 862 | "to-regex-range": "^5.0.1" 863 | } 864 | }, 865 | "fsevents": { 866 | "version": "2.3.2", 867 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 868 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 869 | "dev": true, 870 | "optional": true 871 | }, 872 | "glob-parent": { 873 | "version": "5.1.2", 874 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 875 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 876 | "dev": true, 877 | "requires": { 878 | "is-glob": "^4.0.1" 879 | } 880 | }, 881 | "is-binary-path": { 882 | "version": "2.1.0", 883 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 884 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 885 | "dev": true, 886 | "requires": { 887 | "binary-extensions": "^2.0.0" 888 | } 889 | }, 890 | "is-extglob": { 891 | "version": "2.1.1", 892 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 893 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 894 | "dev": true 895 | }, 896 | "is-glob": { 897 | "version": "4.0.3", 898 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 899 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 900 | "dev": true, 901 | "requires": { 902 | "is-extglob": "^2.1.1" 903 | } 904 | }, 905 | "is-number": { 906 | "version": "7.0.0", 907 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 908 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 909 | "dev": true 910 | }, 911 | "livereload": { 912 | "version": "0.9.3", 913 | "resolved": "https://registry.npmjs.org/livereload/-/livereload-0.9.3.tgz", 914 | "integrity": "sha512-q7Z71n3i4X0R9xthAryBdNGVGAO2R5X+/xXpmKeuPMrteg+W2U8VusTKV3YiJbXZwKsOlFlHe+go6uSNjfxrZw==", 915 | "dev": true, 916 | "requires": { 917 | "chokidar": "^3.5.0", 918 | "livereload-js": "^3.3.1", 919 | "opts": ">= 1.2.0", 920 | "ws": "^7.4.3" 921 | } 922 | }, 923 | "livereload-js": { 924 | "version": "3.4.0", 925 | "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-3.4.0.tgz", 926 | "integrity": "sha512-F/pz9ZZP+R+arY94cECTZco7PXgBXyL+KVWUPZq8AQE9TOu14GV6fYeKOviv02JCvFa4Oi3Rs1hYEpfeajc+ow==", 927 | "dev": true 928 | }, 929 | "normalize-path": { 930 | "version": "3.0.0", 931 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 932 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 933 | "dev": true 934 | }, 935 | "opts": { 936 | "version": "1.2.7", 937 | "resolved": "https://registry.npmjs.org/opts/-/opts-1.2.7.tgz", 938 | "integrity": "sha512-hwZhzGGG/GQ7igxAVFOEun2N4fWul31qE9nfBdCnZGQCB5+L7tN9xZ+94B4aUpLOJx/of3zZs5XsuubayQYQjA==", 939 | "dev": true 940 | }, 941 | "picomatch": { 942 | "version": "2.3.1", 943 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 944 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 945 | "dev": true 946 | }, 947 | "readdirp": { 948 | "version": "3.6.0", 949 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 950 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 951 | "dev": true, 952 | "requires": { 953 | "picomatch": "^2.2.1" 954 | } 955 | }, 956 | "source-map": { 957 | "version": "0.6.1", 958 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 959 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 960 | "optional": true 961 | }, 962 | "source-map-support": { 963 | "version": "0.5.21", 964 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", 965 | "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", 966 | "optional": true, 967 | "requires": { 968 | "buffer-from": "^1.0.0", 969 | "source-map": "^0.6.0" 970 | } 971 | }, 972 | "to-regex-range": { 973 | "version": "5.0.1", 974 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 975 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 976 | "dev": true, 977 | "requires": { 978 | "is-number": "^7.0.0" 979 | } 980 | }, 981 | "ws": { 982 | "version": "7.5.9", 983 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", 984 | "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", 985 | "dev": true, 986 | "requires": {} 987 | } 988 | } 989 | } 990 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serve-http", 3 | "version": "1.0.7", 4 | "description": "Simple single-file local web server with livereload", 5 | "bin": { 6 | "serve-http": "serve-http.js" 7 | }, 8 | "main": "serve-http.js", 9 | "typings": "serve-http.d.ts", 10 | "files": [ 11 | "LICENSE", 12 | "README.md", 13 | "serve-http.js", 14 | "serve-http.js.map", 15 | "serve-http.d.ts" 16 | ], 17 | "scripts": { 18 | "build_old": "./build.sh", 19 | "dev_old": "./build.sh -w", 20 | "build": "sh build-livereload.sh && esbuild --define:DEBUG=false --define:WITH_LIVERELOAD=true --define:VERSION=$(node -p 'JSON.stringify(require(\"./package.json\").version)') --minify --bundle --platform=node --target=node14 --sourcemap --sources-content=false --banner:js='#!/usr/bin/env node' --outfile=serve-http.js src/main.js && chmod +x serve-http.js", 21 | "build-dev": "sh build-livereload.sh && esbuild --define:DEBUG=true --define:WITH_LIVERELOAD=true --define:VERSION=$(node -p 'JSON.stringify(require(\"./package.json\").version)') --bundle --platform=node --target=node14 --sourcemap --sources-content=false --banner:js='#!/usr/bin/env node\ntry { require(\"source-map-support\").install() }catch(_){};' --outfile=serve-http.js src/main.js && chmod +x serve-http.js", 22 | "build-dev-watch": "sh build-livereload.sh && esbuild --watch --define:DEBUG=true --define:WITH_LIVERELOAD=true --define:VERSION=$(node -p 'JSON.stringify(require(\"./package.json\").version)') --bundle --platform=node --target=node14 --sourcemap --sources-content=false --banner:js='#!/usr/bin/env node\ntry { require(\"source-map-support\").install() }catch(_){};' --outfile=serve-http.js src/main.js && chmod +x serve-http.js" 23 | }, 24 | "author": "Rasmus Andersson ", 25 | "homepage": "https://github.com/rsms/serve-http/", 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/rsms/serve-http.git" 29 | }, 30 | "license": "MIT", 31 | "optionalDependencies": { 32 | "source-map-support": "^0.5.21" 33 | }, 34 | "devDependencies": { 35 | "esbuild": "^0.14.49", 36 | "livereload": "^0.9.3" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /serve-http.d.ts: -------------------------------------------------------------------------------- 1 | import { Server } from "http" 2 | 3 | export interface ServerConfig { 4 | /// bind to port. If falsy, some free user-space port is assigned 5 | port? :number 6 | 7 | /// bind to host (defaults to "localhost") 8 | host? :string 9 | 10 | // bind to public address; make server accessible to the outside world 11 | public? :boolean 12 | 13 | /// directory to serve. (default "."; current directory.) 14 | pubdir? :string 15 | 16 | /// don't log requests (still logs errors) 17 | quiet? :boolean 18 | 19 | /// log "serving DIR at URL" message on startup 20 | showServingMessage? :boolean 21 | 22 | /// mime type for unknown file types 23 | defaultMimeType? :string 24 | 25 | /// file to serve for directory requests (defaults to "index.html") 26 | indexFilename? :string 27 | 28 | /// disable or customize directory listing 29 | dirlist? :DirlistOptions|boolean 30 | 31 | /// disable or customize livereload 32 | livereload? :LiveReloadOptions|boolean 33 | } 34 | 35 | export interface DirlistOptions { 36 | /// disable directory listing 37 | disable? :boolean 38 | 39 | /// include files which name starts with "." 40 | showHidden? :boolean 41 | } 42 | 43 | export interface LiveReloadOptions { 44 | /// disable livereload 45 | disable? :boolean 46 | 47 | /// livereload server bind port. (default based on server port) 48 | port? :number 49 | } 50 | 51 | /// Start a server 52 | export function createServer(config? :ServerConfig) :Server 53 | -------------------------------------------------------------------------------- /src/dirlist.js: -------------------------------------------------------------------------------- 1 | import { dlog } from "./util" 2 | 3 | const Path = require("path") 4 | const fs = require("fs") 5 | const promisify = require("util").promisify 6 | const readdir = promisify(fs.readdir) 7 | const readlink = promisify(fs.readlink) 8 | 9 | const htmlEntities = { 10 | '&': '&', 11 | '<': '<', 12 | '>': '>', 13 | '"': '"', 14 | } 15 | 16 | 17 | function htmlescape(s) { 18 | return s.replace(/[&<>"']/g, m => htmlEntities[m]) 19 | } 20 | 21 | 22 | // f(filename :string, pathname :string, opts :DirlistOptions) :Promise 23 | export async function createDirectoryListingHTML(filename, pathname, opts) { 24 | let ents = await readdir(filename, {withFileTypes:true, encoding:"utf8"}) 25 | let files = [] 26 | for (let f of ents) { 27 | let name = f.name 28 | if (!opts.showHidden && name[0] == ".") { 29 | continue 30 | } 31 | 32 | let extra = "" 33 | if (f.isDirectory()) { 34 | name += "/" 35 | } else if (f.isSymbolicLink()) { 36 | try { 37 | let target = await readlink(Path.join(filename, f.name)) 38 | extra = ` → ${htmlescape(target)}` 39 | } catch (_) {} 40 | } 41 | 42 | files.push(`
  • ${htmlescape(name)}${extra}
  • `) 43 | } 44 | 45 | if (pathname != '/') { 46 | files.unshift('
  • ..
  • ') 47 | } 48 | 49 | let title = [] 50 | let pathComponents = pathname.split("/").filter(s => s.length > 0) 51 | let pardir = "/" 52 | for (let c of pathComponents) { 53 | let dir = pardir + c + "/" 54 | pardir = dir 55 | title.push(`${htmlescape(c)}`) 56 | } 57 | title = `/${title.join("/")}` 58 | 59 | return ` 60 | 61 | 62 | ${htmlescape(pathname)} 63 | 70 | 71 |

    ${title}

    72 |
      73 | ${files.join("\n ")} 74 |
    75 | 76 | \n` 77 | } 78 | -------------------------------------------------------------------------------- /src/fmt.js: -------------------------------------------------------------------------------- 1 | // type Formatter = (v :any, n? :number) => any 2 | const inspect = require("util").inspect 3 | 4 | const formatters = { 5 | s: String, 6 | j: JSON.stringify, 7 | j_: (v, n) => JSON.stringify(v, null, n), 8 | r: inspect, 9 | r_: inspect, 10 | q: v => JSON.stringify(String(v)), 11 | n: Number, 12 | f: Number, 13 | f_: (v, n) => Number(v).toFixed(n), 14 | i: Math.round, 15 | d: Math.round, 16 | x: v => Math.round(v).toString(16), 17 | X: v => Math.round(v).toString(16).toUpperCase(), 18 | } 19 | 20 | 21 | // fmt formats a string 22 | // 23 | // Format specifiers: 24 | // 25 | // %s String(value) 26 | // %r inspect(value) 27 | // %Nr inspect(value, maxdepth=N) 28 | // %j JSON.stringify(value) 29 | // %jN JSON.stringify(value, null, N) 30 | // %q JSON.stringify(String(value)) 31 | // %n, %f Number(value) 32 | // %fN Number(value).toFixed(N) 33 | // %i, %d Math.round(value) 34 | // %x Math.round(value).toString(16) 35 | // %X Math.round(value).toString(16).toUpperCase() 36 | // %% "%" 37 | // 38 | // A value that is a function is called and its return value is used. 39 | // 40 | // fmt(format :string, ...args :any[]) :string 41 | export function fmt(format, ...args) { 42 | let index = 0 43 | let s = format.replace(/%(?:([sjrqnfidxX%])|(\d+)([jrf]))/g, (s, ...m) => { 44 | let spec = m[0] 45 | if (spec == "%") { 46 | return "%" 47 | } else if (!spec) { 48 | // with leading number 49 | spec = m[2] 50 | } 51 | if (index == args.length) { 52 | throw new Error(`superfluous parameter %${spec} at offset ${m[3]}`) 53 | } 54 | let v = args[index++] 55 | if (typeof v == "function") { 56 | v = v() 57 | } 58 | return m[0] ? formatters[spec](v) : formatters[spec + "_"](v, parseInt(m[1])) 59 | }) 60 | if (index < args.length) { 61 | // throw new Error(`superfluous arguments`) 62 | s += `(fmt:extra ${args.slice(index).map(v => `${typeof v}=${v}`).join(", ")})` 63 | } 64 | return s 65 | } 66 | 67 | -------------------------------------------------------------------------------- /src/livereload.js: -------------------------------------------------------------------------------- 1 | /* 2 | livereload by Joshua Peek, converted from CoffeeScript to JS ES6. 3 | 4 | livereload.coffee licensed as follows: (MIT) 5 | Copyright (c) 2010 Joshua Peek 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | "Software"), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | */ 26 | 27 | const fs = require('fs') 28 | , path = require('path') 29 | , http = require('http') 30 | , https = require('https') 31 | , url = require('url') 32 | , EventEmitter = require('events') 33 | 34 | // import * as ws from '../node_modules/ws/index' 35 | // import { WebSocketServer } from '../node_modules/ws/lib/websocket-server' 36 | import { WebSocketServer } from './ws/websocket-server' 37 | 38 | let protocol_version = '7'; 39 | let defaultPort = 35729; 40 | let defaultExts = [ 41 | "html", 42 | "css", 43 | "js", 44 | "wasm", 45 | "png", 46 | "gif", 47 | "jpg", 48 | "php", 49 | "php5", 50 | "py", 51 | "rb", 52 | "erb", 53 | ] 54 | let defaultExclusions = [/\.git\//, /\.svn\//, /\.hg\//, /\.DS_Store/]; 55 | 56 | const dlog = DEBUG ? (s, str) => { s.config.debug && console.log(str) } : function(){} 57 | 58 | export class Server extends EventEmitter { 59 | constructor(config) { 60 | super() 61 | let extraExts = config.extraExts 62 | if (extraExts) { 63 | delete config.extraExts 64 | } 65 | 66 | this.config = config = Object.assign({ 67 | // default options 68 | version: protocol_version, 69 | port: defaultPort, 70 | exts: defaultExts, 71 | exclusions: defaultExclusions, 72 | applyCSSLive: true, 73 | originalPath: "", 74 | overrideURL: "", 75 | usePolling: false, 76 | }, config || {}) 77 | 78 | if (extraExts && extraExts.length > 0) { 79 | config.exts = config.exts.concat(extraExts) 80 | } 81 | } 82 | 83 | listen(callback) { 84 | dlog(this, "LiveReload is waiting for a browser to connect..."); 85 | dlog(this, 86 | "Protocol version: " + this.config.version + "\nExclusions: " + 87 | this.config.exclusions + "\nExtensions: " + this.config.exts + "\nPolling: " + 88 | this.config.usePolling + "\n" 89 | ); 90 | if (this.config.server) { 91 | this.config.server.listen(this.config.port, this.config.bindHost); 92 | this.server = new WebSocketServer({ 93 | server: this.config.server 94 | }); 95 | } else { 96 | this.server = new WebSocketServer({ 97 | port: this.config.port, 98 | host: this.config.bindHost, 99 | }); 100 | } 101 | this.server.on('connection', this.onConnection.bind(this)); 102 | this.server.on('close', this.onClose.bind(this)); 103 | this.server.on('error', this.onError.bind(this)); 104 | if (callback) { 105 | return this.server.once('listening', callback); 106 | } 107 | } 108 | 109 | address() { 110 | return this.server && this.server.address() 111 | } 112 | 113 | onError(err) { 114 | dlog(this, "Error " + err); 115 | return this.emit("error", err); 116 | } 117 | 118 | onConnection(socket) { 119 | dlog(this, "Browser connected."); 120 | socket.on('message', (function(_this) { 121 | return function(message) { 122 | var data, request; 123 | dlog(_this, "Client message: " + message); 124 | request = JSON.parse(message); 125 | if (request.command === "hello") { 126 | dlog(_this, "Client requested handshake..."); 127 | dlog(_this, "Handshaking with client using protocol " + _this.config.version + "..."); 128 | data = JSON.stringify({ 129 | command: 'hello', 130 | protocols: ['http://livereload.com/protocols/official-7', 'http://livereload.com/protocols/official-8', 'http://livereload.com/protocols/official-9', 'http://livereload.com/protocols/2.x-origin-version-negotiation', 'http://livereload.com/protocols/2.x-remote-control'], 131 | serverName: 'node-livereload' 132 | }); 133 | socket.send(data); 134 | } 135 | if (request.command === "info") { 136 | return dlog(_this, "Server received client data. Not sending response."); 137 | } 138 | }; 139 | })(this)); 140 | socket.on('error', (function(_this) { 141 | return function(err) { 142 | return dlog(_this, "Error in client socket: " + err); 143 | }; 144 | })(this)); 145 | return socket.on('close', (function(_this) { 146 | return function(message) { 147 | return dlog(_this, "Client closed connection"); 148 | }; 149 | })(this)); 150 | } 151 | 152 | onClose(socket) { 153 | return dlog(this, "Socket closed."); 154 | } 155 | 156 | watch(dir) { 157 | dlog(this, "Watching " + dir + "...") 158 | this.watcher = fs.watch(dir, { recursive: true }, (event, filename) => { 159 | this.filterRefresh(filename) 160 | }) 161 | } 162 | 163 | filterRefresh(filepath) { 164 | var delayedRefresh, exts, fileext; 165 | exts = this.config.exts; 166 | fileext = path.extname(filepath).substring(1); 167 | if (exts.indexOf(fileext) !== -1) { 168 | if (this.config.delay) { 169 | return delayedRefresh = setTimeout((function(_this) { 170 | return function() { 171 | clearTimeout(delayedRefresh); 172 | return _this.refresh(filepath); 173 | }; 174 | })(this), this.config.delay); 175 | } else { 176 | return this.refresh(filepath); 177 | } 178 | } 179 | } 180 | 181 | refresh(filepath) { 182 | var data; 183 | dlog(this, "Reloading: " + filepath); 184 | data = JSON.stringify({ 185 | command: 'reload', 186 | path: filepath, 187 | liveCSS: this.config.applyCSSLive, 188 | liveImg: this.config.applyImgLive, 189 | originalPath: this.config.originalPath, 190 | overrideURL: this.config.overrideURL 191 | }); 192 | return this.sendAllClients(data); 193 | } 194 | 195 | alert(message) { 196 | var data; 197 | dlog(this, "Alert: " + message); 198 | data = JSON.stringify({ 199 | command: 'alert', 200 | message: message 201 | }); 202 | return this.sendAllClients(data); 203 | } 204 | 205 | sendAllClients(data) { 206 | dlog(this, "broadcasting to all clients: " + data) 207 | this.server.clients.forEach(socket => { 208 | socket.send(data, error => { 209 | if (error) { dlog(this, error) } 210 | }) 211 | }) 212 | } 213 | 214 | debug(str) { 215 | if (this.config.debug) { 216 | return console.log(str + "\n"); 217 | } 218 | } 219 | 220 | close() { 221 | if (this.watcher) { 222 | this.watcher.close(); 223 | } 224 | this.server._server.close(); 225 | return this.server.close(); 226 | } 227 | 228 | } // class 229 | 230 | 231 | export function createServer(config, callback) { 232 | var app, requestHandler, server; 233 | if (config == null) { 234 | config = {}; 235 | } 236 | requestHandler = function(req, res) { 237 | if (url.parse(req.url).pathname === '/livereload.js') { 238 | res.writeHead(200, { 239 | 'Content-Type': 'text/javascript' 240 | }); 241 | return res.end(fs.readFileSync(__dirname + '/../ext/livereload.js')); 242 | } 243 | }; 244 | if (config.https == null) { 245 | app = http.createServer(requestHandler); 246 | } else { 247 | app = https.createServer(config.https, requestHandler); 248 | } 249 | if (config.server == null) { 250 | config.server = app; 251 | } 252 | server = new Server(config); 253 | if (!config.noListen) { 254 | server.listen(callback); 255 | } 256 | return server; 257 | } 258 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { parseopts } from "./parseopts" 2 | import { createServer } from "./server" 3 | import { die, dlog } from "./util" 4 | 5 | const Path = require("path") 6 | 7 | function usage() { 8 | let v = process.argv 9 | let prog = v[0].endsWith("node") ? Path.relative(process.cwd(), Path.resolve(v[1])) : v[0] 10 | let progl = "./" + prog 11 | if (process.env["_"] == progl) { 12 | // common case: ./serve-http (in cwd) 13 | prog = progl 14 | } 15 | prog = Path.basename(prog) 16 | let s = ` 17 | Usage: ${prog} [options] [] 18 | 19 | 20 | Directory to serve files from. Defaults to the current directory. 21 | 22 | Options: 23 | -p, -port Listen on specific 24 | -host Bind to instead of "localhost" 25 | -public Accept connections from anywhere (same as -host "") 26 | -q, -quiet Don't log requests 27 | -no-livereload Disable livereload 28 | -no-dirlist Disable directory listing 29 | -dirlist-hidden Show files beginning with "." in directory listings 30 | -h, -help Show help and exit 31 | -version Print version to stdout and exit 32 | 33 | Examples: 34 | 35 | ${prog} 36 | Serve current directory on some available port 37 | 38 | ${prog} -p 8080 docs 39 | Serve directory ./docs locally on port 8080 40 | 41 | ${prog} -public -no-dirlist 42 | Serve current directory publicly on some available port, 43 | without directory listing. 44 | 45 | `.trim()+"\n" 46 | if (!WITH_LIVERELOAD) { 47 | s = s.replace(/^\s+livereload.+\n/g, "") 48 | } 49 | console.log(s) 50 | process.exit(0) 51 | } 52 | 53 | const opts = { 54 | // available command-line options with default values 55 | port: 0, p: 0, 56 | host: "localhost", 57 | public: false, 58 | q: false, quiet: false, 59 | noLivereload: false, 60 | noDirlist: false, // disable directory listing 61 | dirlistHidden: false, 62 | version: false, 63 | } 64 | 65 | function main() { 66 | let args = parseopts(process.argv.slice(2), opts, usage) 67 | 68 | if (opts.version) { 69 | console.log(VERSION) 70 | process.exit(0) 71 | } 72 | 73 | let pubdir = "" 74 | if (args.length > 0) { 75 | if (args.length > 1) { 76 | console.error(`ignoring extra arguments: ${args.slice(1).join(" ")}`) 77 | } 78 | pubdir = args[0] 79 | } 80 | 81 | if (opts.public && opts.host == "localhost") { 82 | opts.host = "" 83 | } 84 | 85 | opts.quiet = opts.quiet || opts.q 86 | 87 | const server = createServer({ 88 | port: opts.port || opts.p, 89 | host: opts.host, 90 | public: opts.public, 91 | quiet: opts.quiet, 92 | showServingMessage: true, 93 | pubdir, 94 | dirlist: { 95 | disable: opts.noDirlist, 96 | showHidden: opts.dirlistHidden, 97 | }, 98 | livereload: { 99 | disable: opts.noLivereload, 100 | }, 101 | }) 102 | 103 | // stop server and exit cleanly on ^C 104 | process.on('SIGINT', () => { 105 | opts.quiet || console.log(" stopping server") 106 | server.close() // stop accepting new connections 107 | // give ongoing requests a short time to finish processing 108 | setTimeout(() => process.exit(0), 500).unref() 109 | }) 110 | } 111 | 112 | if (module.id == ".") { 113 | main() 114 | } else { 115 | module.exports = { createServer } 116 | } 117 | -------------------------------------------------------------------------------- /src/mime.js: -------------------------------------------------------------------------------- 1 | const plainText = 'text/plain' 2 | const yaml = 'text/x-yaml' 3 | const markdown = 'text/markdown' 4 | const jpeg = 'image/jpeg' 5 | const html = 'text/html' 6 | 7 | export const extToMimeType = { 8 | 'aiff': 'audio/x-aiff', 9 | 'appcache': 'text/cache-manifest', 10 | 'atom': 'application/atom+xml', 11 | 'bmp': 'image/bmp', 12 | 'crx': 'application/x-chrome-extension', 13 | 'css': 'text/css', 14 | 'eot': 'application/vnd.ms-fontobject', 15 | 'gif': 'image/gif', 16 | 'htc': 'text/x-component', 17 | 'html': html, 'htm': html, 18 | 'ico': 'image/vnd.microsoft.icon', 19 | 'ics': 'text/calendar', 20 | 'jpeg': jpeg, 'jpe': jpeg, 'jpg': jpeg, 21 | 'js': 'text/javascript', 22 | 'json': 'application/json', 23 | 'mathml': 'application/mathml+xml', 24 | 'midi': 'audio/midi', 25 | 'mov': 'video/quicktime', 26 | 'mp3': 'audio/mpeg', 27 | 'mp4': 'video/mp4', 28 | 'mpeg': 'video/mpeg', 29 | 'ogg': 'video/ogg', 30 | 'otf': 'font/opentype', 31 | 'pdf': 'application/pdf', 32 | 'png': 'image/png', 33 | 'rtf': 'application/rtf', 34 | 'svg': 'image/svg+xml', 35 | 'swf': 'application/x-shockwave-flash', 36 | 'tar': 'application/x-tar', 37 | 'tiff': 'image/tiff', 38 | 'ttf': 'font/truetype', 39 | 'wav': 'audio/x-wav', 40 | 'webm': 'video/webm', 41 | 'wasm': 'application/wasm', 42 | 'webp': 'image/webp', 43 | 'woff': 'font/woff', 44 | 'woff2': 'font/woff2', 45 | 'xhtml': 'application/xhtml+xml', 46 | 'xml': 'text/xml', 47 | 'xsl': 'application/xml', 48 | 'xslt': 'application/xslt+xml', 49 | 'zip': 'application/zip', 50 | 'txt': plainText, 51 | 'ninja': plainText, 52 | 'md': markdown, 'markdown': markdown, 'mdown': markdown, 53 | 'yaml': yaml, 'yml': yaml, 54 | 'rb': 'text/ruby', 55 | 'ts': 'text/typescript', 56 | 'sh': 'text/x-sh', 57 | 'go': 'text/x-go', 58 | 'py': 'text/x-python', 59 | 'php': 'text/x-php', 60 | } 61 | 62 | extToMimeType["aif"] = extToMimeType["aiff"] 63 | extToMimeType["manifest"] = extToMimeType["appcache"] 64 | extToMimeType["mid"] = extToMimeType["midi"] 65 | extToMimeType["mpg"] = extToMimeType["mpeg"] 66 | extToMimeType["ogv"] = extToMimeType["ogg"] 67 | extToMimeType["svgz"] = extToMimeType["svg"] 68 | extToMimeType["tif"] = extToMimeType["tiff"] 69 | extToMimeType["xht"] = extToMimeType["xhtml"] 70 | -------------------------------------------------------------------------------- /src/parseopts.js: -------------------------------------------------------------------------------- 1 | import { die } from "./util" 2 | 3 | export function parseopts(argv, opts, usage) { 4 | let args = [] 5 | for (let i = 0; i < argv.length; i++) { 6 | let arg = argv[i] 7 | if (arg[0] == "-") { 8 | let k = arg.replace(/^-+/, "") 9 | if (k == "h" || k == "help") { 10 | usage() 11 | } 12 | if (!(k in opts)) { 13 | k = k.replace(/-(\w)/g, (_, m) => m[0].toUpperCase()) 14 | } 15 | if (k in opts) { 16 | let t = typeof opts[k] 17 | let v = true 18 | if (t != "boolean") { 19 | if ((v = argv[++i]) === undefined) { 20 | die(`missing value for option ${arg}`) 21 | } 22 | if (t == "number") { 23 | v = Number(v) 24 | if (isNaN(v)) { 25 | die(`invalid value ${argv[i]} for option ${arg} (not a number)`) 26 | } 27 | } 28 | } 29 | opts[k] = v 30 | } else { 31 | die(`unknown option ${arg}`) 32 | } 33 | } else { 34 | args.push(arg) 35 | } 36 | } 37 | return args 38 | } 39 | -------------------------------------------------------------------------------- /src/safe.js: -------------------------------------------------------------------------------- 1 | const os = require("os") 2 | const fs = require("fs") 3 | const Path = require("path") 4 | 5 | export function checkSafePubdir(pubdirRealpath) { 6 | // If this socket will be opened to the local network, only allow serving a 7 | // directory inside the home directory. Prevent people from serving up their 8 | // SSH keys to the local network unintentionally. 9 | let home = os.homedir() 10 | if (!home) { 11 | return "no home directory" 12 | } 13 | 14 | home = fs.realpathSync(Path.resolve(home)) 15 | if (!pubdirRealpath.startsWith(home)) { 16 | return "directory not contained inside your home directory" 17 | } 18 | 19 | // Not sure why someone would do this but it's not a good idea 20 | if (pubdirRealpath === Path.join(home, '.ssh')) { 21 | return "directory is inside SSH directory" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import { fmt } from "./fmt" 2 | import { extToMimeType } from "./mime" 3 | import { checkSafePubdir } from "./safe" 4 | import { die, addrToURL, dlog, stat } from "./util" 5 | import { createDirectoryListingHTML } from "./dirlist" 6 | import { createServer as createLivereloadServer } from "./livereload" 7 | import livereloadJSBody from "../build/livereload-script" 8 | 9 | const createHttpServer = require("http").createServer 10 | const urlparse = require("url").parse 11 | const Path = require("path") 12 | const os = require("os") 13 | const fs = require("fs") 14 | const promisify = require("util").promisify 15 | 16 | const readfile = promisify(fs.readFile) 17 | const writefile = promisify(fs.writeFile) 18 | 19 | let pubdir = "" // absolute path 20 | let server = null 21 | let livereloadServer = null 22 | let log = ()=>{} 23 | 24 | 25 | export function createServer(opts) { 26 | opts = opts ? Object.assign({}, opts) : {} // copy 27 | opts.host = opts.host || (opts.public ? "" : "localhost") 28 | opts.port = opts.port && opts.port > 0 ? opts.port : undefined 29 | opts.livereload = ( 30 | opts.livereload && typeof opts.livereload == "object" ? opts.livereload : 31 | { disable: opts.livereload !== undefined && !opts.livereload } 32 | ) 33 | opts.dirlist = ( 34 | opts.dirlist && typeof opts.dirlist == "object" ? opts.dirlist : 35 | { disable: opts.dirlist !== undefined && !opts.dirlist } 36 | ) 37 | 38 | pubdir = Path.resolve(opts.pubdir || ".") 39 | try { 40 | pubdir = fs.realpathSync(pubdir) 41 | } catch (err) { 42 | if (err.code == "ENOENT") { 43 | die(`Directory ${pubdir} does not exist`) 44 | } 45 | throw err 46 | } 47 | 48 | let handler = formHandlerChain([ 49 | !opts.quiet && requestLogger(), 50 | handleRequest, 51 | ]) 52 | 53 | let handler2 = (req, res) => handler(req, res).catch(err => { 54 | console.error("Internal server error:", err.stack||String(err)) 55 | return endResponse(res, 500, `Internal server error: ${err}`) 56 | }) 57 | 58 | server = createHttpServer(handler2) 59 | server.options = opts 60 | 61 | server.localOnly = true 62 | if (opts.host != "localhost" && opts.host != "127.0.0.1" && opts.host != "::1") { 63 | server.localOnly = false 64 | if (!opts.public) { 65 | let msg = checkSafePubdir(pubdir) 66 | if (msg) { 67 | die("Refusing to allow external connections for security reasons:\n " + 68 | msg.replace(/\.$/,".") + "." + 69 | "\n Set -public to ignore this safeguard. Please be careful.") 70 | } 71 | } 72 | } 73 | 74 | server.listen(opts.port, opts.host, () => { 75 | let addr = server.address() 76 | 77 | // livereload port 78 | let lrport = 0 79 | if (WITH_LIVERELOAD && !opts.livereload.disable) { 80 | lrport = ( 81 | opts.livereload.port ? opts.livereload.port : 82 | opts.port ? Math.min(65535, opts.port + 10000) : 83 | addr.port >= 65535 ? addr.port - 1 : 84 | addr.port + 1 85 | ) 86 | startLivereloadServer(lrport, opts.host) 87 | } 88 | 89 | if (opts.showServingMessage) { 90 | let dir = Path.relative(".", pubdir) 91 | if (dir[0] != ".") { 92 | dir = "./" + dir 93 | } else if (dir == ".." || dir.startsWith("../")) { 94 | dir = pubdir 95 | let homedir = os.homedir() 96 | if (homedir == dir) { 97 | dir = "~/" 98 | } else if (homedir && dir.startsWith(homedir)) { 99 | dir = "~" + dir.substr(homedir.length) 100 | } 101 | } 102 | let lrmsg = lrport ? ` (livereload on :${lrport})` : "" 103 | console.log( 104 | `serving %s%s at %s/${lrmsg}`, 105 | dir, 106 | server.localOnly ? "" : " PUBLICLY TO THE INTERNET", 107 | addrToURL(addr) 108 | ) 109 | } 110 | 111 | // DEBUG && setTimeout(() => { 112 | // require("http").get("http://localhost:" + addr.port + "/dir/", async res => { 113 | // dlog("headers:\n %s", 114 | // Object.keys(res.headers).map(k => `${k}: ${res.headers[k]}`).join("\n ") 115 | // ) 116 | // let body = await readStream(res) 117 | // console.log("body:", body.toString("utf8")) 118 | // }) 119 | // }, 100) 120 | }) 121 | 122 | server.once("close", () => { 123 | if (livereloadServer) 124 | livereloadServer.close() 125 | }) 126 | 127 | return server 128 | } 129 | 130 | 131 | function startLivereloadServer(port, bindHost) { 132 | if (WITH_LIVERELOAD) { 133 | livereloadServer = createLivereloadServer({ 134 | port, 135 | bindHost, 136 | }, () => { 137 | livereloadServer.watch(pubdir) 138 | }) 139 | } 140 | } 141 | 142 | 143 | function formHandlerChain(handlers) { 144 | // [ h1, h2, h3 ] 145 | // -> 146 | // (req, res) => 147 | // h1(req, res).then(() => 148 | // h2(req, res).then(() => 149 | // h3(req, res))) 150 | // 151 | handlers = handlers.filter(f => !!f) 152 | if (handlers.length == 1) { 153 | return handlers[0] 154 | } 155 | return handlers.reduce((prev, next) => 156 | (req, res) => 157 | prev(req, res).then((req1, res1) => 158 | next(req1||req, res1||res) ) 159 | ) 160 | } 161 | 162 | 163 | function requestLogger() { 164 | return async (req, res) => { 165 | let end = res.end, writeHead = res.writeHead, statusCode 166 | 167 | res.writeHead = (code, headers) => { 168 | res.writeHead = writeHead 169 | res.writeHead(code, headers) 170 | res.__statusCode = statusCode = code 171 | res.__headers = headers || {} 172 | } 173 | 174 | res.end = (chunk, encoding) => { 175 | res.end = end 176 | res.end(chunk, encoding) 177 | let addr = ( 178 | req.socket && 179 | (req.socket.remoteAddress || (req.socket.socket && req.socket.socket.remoteAddress)) 180 | ) 181 | let time = (new Date).toUTCString() 182 | let httpv = req.httpVersionMajor + '.' + req.httpVersionMinor 183 | let status = statusCode || res.statusCode 184 | let ua = req.headers['user-agent'] || '-' 185 | console.log(`${addr} [${time}] "${req.method} ${req.url} HTTP/${httpv}" ${status} ${ua}`) 186 | } 187 | } 188 | } 189 | 190 | 191 | function endResponse(res, status, str) { 192 | let body = str ? Buffer.from(str.trim() + "\n", "utf8") : undefined 193 | res.writeHead(status, { 194 | "Content-Type": "text/plain", 195 | "Content-Length": body ? String(body.length) : "0", 196 | }) 197 | res.end(body) 198 | } 199 | 200 | 201 | function replyNotFound(req, res) { 202 | endResponse(res, 404, `404 not found ${req.pathname}`, "utf8") 203 | } 204 | 205 | 206 | function readStream(req) { 207 | return new Promise(resolve => { 208 | if (req.body && req.body instanceof Buffer) { 209 | return resolve(req.body) 210 | } 211 | let body = [] 212 | req.on('data', data => body.push(data)) 213 | req.on('end', () => { 214 | resolve(req.body = Buffer.concat(body)) 215 | }) 216 | }) 217 | } 218 | 219 | 220 | async function handleRequest(req, res) { 221 | req.pathname = decodeURIComponent(urlparse(req.url).pathname).replace(/^\.{2,}|\.{2,}$/g, "") 222 | 223 | // Only allow writing over files if the server can only accept local connections 224 | if (req.method === "POST" && server.localOnly) { 225 | return handlePOSTFileRequest(req, res) 226 | } 227 | 228 | if (req.method === "GET" || req.method === "HEAD") { 229 | if (WITH_LIVERELOAD && livereloadServer && req.pathname == "/livereload.js") { 230 | return handleGETLivereload(req, res) 231 | } 232 | return handleGETFileRequest(req, res) 233 | } 234 | 235 | endResponse(res, 500, `Unsupported method ${req.method}`) 236 | } 237 | 238 | 239 | let livereloadRes = null 240 | 241 | async function handleGETLivereload(req, res) { 242 | if (WITH_LIVERELOAD) { 243 | if (!livereloadRes) { 244 | let body = Buffer.from(livereloadJSBody, "utf8") 245 | livereloadRes = { 246 | body, 247 | header: { 248 | "Content-Type": "text/javascript", 249 | "Content-Length": String(body.length), 250 | } 251 | } 252 | } 253 | res.writeHead(200, livereloadRes.header) 254 | res.end(livereloadRes.body) 255 | } 256 | } 257 | 258 | 259 | async function handleGETFileRequest(req, res) { 260 | let filename = Path.join(pubdir, req.pathname) 261 | let st = await stat(filename) 262 | if (!st) { 263 | return replyNotFound(req, res) 264 | } 265 | if (st.isFile()) { 266 | return serveFile(req, res, filename, st) 267 | } 268 | if (st.isDirectory()) { 269 | return serveDir(req, res, filename, st) 270 | } 271 | endResponse(res, 404, `404 not found ${req.pathname} (unreadable file type)`, "utf8") 272 | } 273 | 274 | 275 | async function handlePOSTFileRequest(req, res) { 276 | // Write files with POST. Example: 277 | // curl -X POST -H "Content-Type: text/plain" -d "Hello" http://localhost:8090/hello.txt 278 | // 279 | let remoteAddr = req.socket.address().address 280 | if (remoteAddr != "127.0.0.1" && remoteAddr != "::1") { 281 | return endResponse(res, 403, "Forbidden") 282 | } 283 | 284 | let origin = req.headers.origin && urlparse(req.headers.origin).hostname 285 | if (origin) { 286 | // if (origin !== 'localhost' && origin !== '127.0.0.1' && origin !== "::1") { 287 | // return endResponse(res, 403, "Forbidden") 288 | // } 289 | res.setHeader('Access-Control-Allow-Origin', origin) 290 | } 291 | 292 | let filename = Path.join(pubdir, req.pathname) 293 | let [st, body] = await Promise.all([ 294 | stat(filename), 295 | readStream(req) 296 | ]) 297 | if (st && st.isDirectory()) { 298 | return endResponse(res, 409, "Conflict: Directory exists at path") 299 | } 300 | await writefile(filename, body) 301 | if (st) { 302 | endResponse(res, 200, "File replaced") 303 | } else { 304 | endResponse(res, 201, "File created") 305 | } 306 | } 307 | 308 | 309 | function isProbablyUTF8Text(buf) { 310 | for (let i = 0; i < Math.min(4096, buf.length); i++) { 311 | let b = buf[i] 312 | if (b <= 0x08) { return false } 313 | if (b >= 0x80 && ((b >> 5) != 0x6 || (b >> 4) != 0xe || (b >> 3) != 0x1e)) { 314 | return false // not UTF-8 315 | } 316 | } 317 | return true 318 | } 319 | 320 | 321 | async function serveFile(req, res, filename, st) { 322 | const opts = server.options 323 | res.statusCode = 200 324 | 325 | let body = await readfile(filename) 326 | 327 | const mimeType = ( 328 | extToMimeType[Path.extname(filename).substr(1)] || 329 | opts.defaultMimeType || 330 | (isProbablyUTF8Text(body) ? "text/plain; charset=utf-8" : "application/octet-stream") 331 | ) 332 | 333 | res.setHeader('Content-Type', mimeType) 334 | res.setHeader('Last-Modified', st.mtime.toUTCString()) 335 | res.setHeader('ETag', etag(st)) 336 | 337 | if (mimeType == "text/html" && !opts.nolivereload) { 338 | body = preprocessHtml(body) 339 | } 340 | 341 | res.setHeader('Content-Length', body.length) 342 | 343 | if (req.method == "HEAD") { 344 | res.end() 345 | return 346 | } 347 | 348 | if (opts.emulateSlowConnection) { 349 | let chunkTime = 1000 // Stream each file out over a second 350 | let chunkCount = 100 // The number of chunks to deliver the file in 351 | let chunkSize = Math.ceil(body.length / chunkCount) 352 | function next() { 353 | if (body.length > 0) { 354 | res.write(body.slice(0, chunkSize)) 355 | body = body.slice(chunkSize) 356 | setTimeout(next, chunkTime / chunkCount) 357 | } else { 358 | res.end() 359 | } 360 | } 361 | return next() 362 | } 363 | 364 | res.end(body) 365 | } 366 | 367 | 368 | async function serveDir(req, res, filename, st) { 369 | dlog("serveDir %r", req.pathname) 370 | const opts = server.options 371 | const indexFilename = opts.indexFilename || "index.html" 372 | 373 | let indexFile = Path.join(filename, indexFilename) 374 | let indexFileSt = await stat(indexFile) 375 | if (indexFileSt && indexFileSt.isFile()) { 376 | return serveFile(req, res, indexFile, indexFileSt) 377 | } 378 | 379 | if (opts.dirlist.disable) { 380 | return replyNotFound(req, res) 381 | } 382 | 383 | if (req.pathname[req.pathname.length - 1] != "/") { 384 | // redirect to "/" 385 | res.writeHead(303, { 386 | "Expires": "Sun, 11 Mar 1984 12:00:00 GMT", 387 | "Location": req.pathname + "/", 388 | "Content-Length": "0", 389 | }) 390 | res.end() 391 | return 392 | } 393 | 394 | let body = await createDirectoryListingHTML(filename, req.pathname, opts.dirlist) 395 | body = Buffer.from(body, "utf8") 396 | res.writeHead(200, { 397 | "Expires": "Sun, 11 Mar 1984 12:00:00 GMT", 398 | "Content-Type": "text/html", 399 | "Content-Length": String(body.length), 400 | }) 401 | res.end(req.method == "HEAD" ? undefined : body) 402 | } 403 | 404 | 405 | function etag(st) { 406 | return `"${st.mtimeMs.toString(36)}-${st.ino.toString(36)}-${st.size.toString(36)}"` 407 | } 408 | 409 | 410 | function preprocessHtml(body) { 411 | // add livereload script to html 412 | let s = body.toString("utf8") 413 | let i = s.indexOf("") 414 | if (i == -1) { i = s.indexOf("") } 415 | if (i != -1) { 416 | let port = livereloadServer.config.port 417 | let script = `` 418 | s = s.substr(0, i) + script + s.substr(i) 419 | body = Buffer.from(s, "utf8") 420 | } 421 | return body 422 | } 423 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | import { fmt } from "./fmt" 2 | 3 | const os = require("os") 4 | const fs = require("fs") 5 | 6 | 7 | export const dlog = ( 8 | DEBUG ? function() { 9 | if (typeof arguments[0] == "string") { 10 | console.log("D " + fmt.apply(null, arguments)) 11 | } else { 12 | console.log.apply(console, ["D"].concat([].slice.call(arguments))) 13 | } 14 | } : function(){} 15 | ) 16 | 17 | 18 | export function die(err) { 19 | console.error((!err || typeof err == "string") ? err : String(err.stack||err)) 20 | process.exit(1) 21 | } 22 | 23 | 24 | export function addrToURL(addr) { 25 | let host = addr.address 26 | if (host == "127.0.0.1" || host == "::1") { 27 | host = "localhost" 28 | } else if (host == "::" || host == "0.0.0.0" || host == "") { 29 | host = netInterfaceAddr(4) || "0.0.0.0" 30 | } 31 | return `http://${host}:${addr.port}` 32 | } 33 | 34 | 35 | export function netInterfaceAddr(ipVersionPreference /* :4|6|undefined */) { 36 | const ifaces = os.networkInterfaces() 37 | let bestAddr = null 38 | for (let ifname of Object.keys(ifaces)) { 39 | let alias = 0 40 | for (let iface of ifaces[ifname]) { 41 | if (!iface.internal) { 42 | if (iface.family == 'IPv4') { 43 | bestAddr = iface.address 44 | if (ipVersionPreference == 4 || !ipVersionPreference) { 45 | return bestAddr 46 | } 47 | } else if (iface.family == 'IPv6') { 48 | bestAddr = iface.address 49 | if (ipVersionPreference == 6 || !ipVersionPreference) { 50 | return bestAddr 51 | } 52 | } 53 | } 54 | } 55 | } 56 | return bestAddr 57 | } 58 | 59 | 60 | export function stat(path, options) { 61 | return new Promise((resolve, reject) => { 62 | fs.stat(path, options, (err, stats) => { 63 | if (!err) { 64 | resolve(stats) 65 | } else if (err.code == "ENOENT") { 66 | resolve(null) 67 | } else { 68 | reject(err) 69 | } 70 | }) 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /src/ws/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011 Einar Otto Stangvik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/ws/async-limiter.js: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | Copyright (c) 2017 Samuel Reed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | */ 23 | 'use strict'; 24 | 25 | export function Queue(options) { 26 | if (!(this instanceof Queue)) { 27 | return new Queue(options); 28 | } 29 | 30 | options = options || {}; 31 | this.concurrency = options.concurrency || Infinity; 32 | this.pending = 0; 33 | this.jobs = []; 34 | this.cbs = []; 35 | this._done = done.bind(this); 36 | } 37 | 38 | var arrayAddMethods = [ 39 | 'push', 40 | 'unshift', 41 | 'splice' 42 | ]; 43 | 44 | arrayAddMethods.forEach(function(method) { 45 | Queue.prototype[method] = function() { 46 | var methodResult = Array.prototype[method].apply(this.jobs, arguments); 47 | this._run(); 48 | return methodResult; 49 | }; 50 | }); 51 | 52 | Object.defineProperty(Queue.prototype, 'length', { 53 | get: function() { 54 | return this.pending + this.jobs.length; 55 | } 56 | }); 57 | 58 | Queue.prototype._run = function() { 59 | if (this.pending === this.concurrency) { 60 | return; 61 | } 62 | if (this.jobs.length) { 63 | var job = this.jobs.shift(); 64 | this.pending++; 65 | job(this._done); 66 | this._run(); 67 | } 68 | 69 | if (this.pending === 0) { 70 | while (this.cbs.length !== 0) { 71 | var cb = this.cbs.pop(); 72 | process.nextTick(cb); 73 | } 74 | } 75 | }; 76 | 77 | Queue.prototype.onDone = function(cb) { 78 | if (typeof cb === 'function') { 79 | this.cbs.push(cb); 80 | this._run(); 81 | } 82 | }; 83 | 84 | function done() { 85 | this.pending--; 86 | this._run(); 87 | } 88 | 89 | // module.exports = Queue; 90 | -------------------------------------------------------------------------------- /src/ws/buffer-util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { EMPTY_BUFFER } from './constants' 4 | 5 | /** 6 | * Merges an array of buffers into a new buffer. 7 | * 8 | * @param {Buffer[]} list The array of buffers to concat 9 | * @param {Number} totalLength The total length of buffers in the list 10 | * @return {Buffer} The resulting buffer 11 | * @public 12 | */ 13 | export function concat(list, totalLength) { 14 | if (list.length === 0) return EMPTY_BUFFER; 15 | if (list.length === 1) return list[0]; 16 | 17 | const target = Buffer.allocUnsafe(totalLength); 18 | let offset = 0; 19 | 20 | for (let i = 0; i < list.length; i++) { 21 | const buf = list[i]; 22 | target.set(buf, offset); 23 | offset += buf.length; 24 | } 25 | 26 | if (offset < totalLength) return target.slice(0, offset); 27 | 28 | return target; 29 | } 30 | 31 | /** 32 | * Masks a buffer using the given mask. 33 | * 34 | * @param {Buffer} source The buffer to mask 35 | * @param {Buffer} mask The mask to use 36 | * @param {Buffer} output The buffer where to store the result 37 | * @param {Number} offset The offset at which to start writing 38 | * @param {Number} length The number of bytes to mask. 39 | * @public 40 | */ 41 | function _mask(source, mask, output, offset, length) { 42 | for (let i = 0; i < length; i++) { 43 | output[offset + i] = source[i] ^ mask[i & 3]; 44 | } 45 | } 46 | 47 | /** 48 | * Unmasks a buffer using the given mask. 49 | * 50 | * @param {Buffer} buffer The buffer to unmask 51 | * @param {Buffer} mask The mask to use 52 | * @public 53 | */ 54 | function _unmask(buffer, mask) { 55 | // Required until https://github.com/nodejs/node/issues/9006 is resolved. 56 | const length = buffer.length; 57 | for (let i = 0; i < length; i++) { 58 | buffer[i] ^= mask[i & 3]; 59 | } 60 | } 61 | 62 | /** 63 | * Converts a buffer to an `ArrayBuffer`. 64 | * 65 | * @param {Buffer} buf The buffer to convert 66 | * @return {ArrayBuffer} Converted buffer 67 | * @public 68 | */ 69 | export function toArrayBuffer(buf) { 70 | if (buf.byteLength === buf.buffer.byteLength) { 71 | return buf.buffer; 72 | } 73 | 74 | return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); 75 | } 76 | 77 | /** 78 | * Converts `data` to a `Buffer`. 79 | * 80 | * @param {*} data The data to convert 81 | * @return {Buffer} The buffer 82 | * @throws {TypeError} 83 | * @public 84 | */ 85 | export function toBuffer(data) { 86 | toBuffer.readOnly = true; 87 | 88 | if (Buffer.isBuffer(data)) return data; 89 | 90 | let buf; 91 | 92 | if (data instanceof ArrayBuffer) { 93 | buf = Buffer.from(data); 94 | } else if (ArrayBuffer.isView(data)) { 95 | buf = viewToBuffer(data); 96 | } else { 97 | buf = Buffer.from(data); 98 | toBuffer.readOnly = false; 99 | } 100 | 101 | return buf; 102 | } 103 | 104 | /** 105 | * Converts an `ArrayBuffer` view into a buffer. 106 | * 107 | * @param {(DataView|TypedArray)} view The view to convert 108 | * @return {Buffer} Converted view 109 | * @private 110 | */ 111 | function viewToBuffer(view) { 112 | const buf = Buffer.from(view.buffer); 113 | 114 | if (view.byteLength !== view.buffer.byteLength) { 115 | return buf.slice(view.byteOffset, view.byteOffset + view.byteLength); 116 | } 117 | 118 | return buf; 119 | } 120 | 121 | // try { 122 | // const bufferUtil = require('bufferutil'); 123 | // const bu = bufferUtil.BufferUtil || bufferUtil; 124 | 125 | // module.exports = { 126 | // concat, 127 | // mask(source, mask, output, offset, length) { 128 | // if (length < 48) _mask(source, mask, output, offset, length); 129 | // else bu.mask(source, mask, output, offset, length); 130 | // }, 131 | // toArrayBuffer, 132 | // toBuffer, 133 | // unmask(buffer, mask) { 134 | // if (buffer.length < 32) _unmask(buffer, mask); 135 | // else bu.unmask(buffer, mask); 136 | // } 137 | // }; 138 | // } catch (e) /* istanbul ignore next */ { 139 | // module.exports = { 140 | // concat, 141 | // mask: _mask, 142 | // toArrayBuffer, 143 | // toBuffer, 144 | // unmask: _unmask 145 | // }; 146 | // } 147 | 148 | export const mask = _mask 149 | export const unmask = _unmask 150 | -------------------------------------------------------------------------------- /src/ws/constants.js: -------------------------------------------------------------------------------- 1 | export const BINARY_TYPES = ['nodebuffer', 'arraybuffer', 'fragments'] 2 | , GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' 3 | , kStatusCode = Symbol('status-code') 4 | , kWebSocket = Symbol('websocket') 5 | , EMPTY_BUFFER = Buffer.alloc(0) 6 | , NOOP = () => {} 7 | -------------------------------------------------------------------------------- /src/ws/event-target.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Class representing an event. 5 | * 6 | * @private 7 | */ 8 | class Event { 9 | /** 10 | * Create a new `Event`. 11 | * 12 | * @param {String} type The name of the event 13 | * @param {Object} target A reference to the target to which the event was dispatched 14 | */ 15 | constructor(type, target) { 16 | this.target = target; 17 | this.type = type; 18 | } 19 | } 20 | 21 | /** 22 | * Class representing a message event. 23 | * 24 | * @extends Event 25 | * @private 26 | */ 27 | class MessageEvent extends Event { 28 | /** 29 | * Create a new `MessageEvent`. 30 | * 31 | * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The received data 32 | * @param {WebSocket} target A reference to the target to which the event was dispatched 33 | */ 34 | constructor(data, target) { 35 | super('message', target); 36 | 37 | this.data = data; 38 | } 39 | } 40 | 41 | /** 42 | * Class representing a close event. 43 | * 44 | * @extends Event 45 | * @private 46 | */ 47 | class CloseEvent extends Event { 48 | /** 49 | * Create a new `CloseEvent`. 50 | * 51 | * @param {Number} code The status code explaining why the connection is being closed 52 | * @param {String} reason A human-readable string explaining why the connection is closing 53 | * @param {WebSocket} target A reference to the target to which the event was dispatched 54 | */ 55 | constructor(code, reason, target) { 56 | super('close', target); 57 | 58 | this.wasClean = target._closeFrameReceived && target._closeFrameSent; 59 | this.reason = reason; 60 | this.code = code; 61 | } 62 | } 63 | 64 | /** 65 | * Class representing an open event. 66 | * 67 | * @extends Event 68 | * @private 69 | */ 70 | class OpenEvent extends Event { 71 | /** 72 | * Create a new `OpenEvent`. 73 | * 74 | * @param {WebSocket} target A reference to the target to which the event was dispatched 75 | */ 76 | constructor(target) { 77 | super('open', target); 78 | } 79 | } 80 | 81 | /** 82 | * Class representing an error event. 83 | * 84 | * @extends Event 85 | * @private 86 | */ 87 | class ErrorEvent extends Event { 88 | /** 89 | * Create a new `ErrorEvent`. 90 | * 91 | * @param {Object} error The error that generated this event 92 | * @param {WebSocket} target A reference to the target to which the event was dispatched 93 | */ 94 | constructor(error, target) { 95 | super('error', target); 96 | 97 | this.message = error.message; 98 | this.error = error; 99 | } 100 | } 101 | 102 | /** 103 | * This provides methods for emulating the `EventTarget` interface. It's not 104 | * meant to be used directly. 105 | * 106 | * @mixin 107 | */ 108 | // export const EventTarget = { ... } 109 | 110 | /** 111 | * Register an event listener. 112 | * 113 | * @param {String} method A string representing the event type to listen for 114 | * @param {Function} listener The listener to add 115 | * @public 116 | */ 117 | export function addEventListener(method, listener) { 118 | if (typeof listener !== 'function') return; 119 | 120 | function onMessage(data) { 121 | listener.call(this, new MessageEvent(data, this)); 122 | } 123 | 124 | function onClose(code, message) { 125 | listener.call(this, new CloseEvent(code, message, this)); 126 | } 127 | 128 | function onError(error) { 129 | listener.call(this, new ErrorEvent(error, this)); 130 | } 131 | 132 | function onOpen() { 133 | listener.call(this, new OpenEvent(this)); 134 | } 135 | 136 | if (method === 'message') { 137 | onMessage._listener = listener; 138 | this.on(method, onMessage); 139 | } else if (method === 'close') { 140 | onClose._listener = listener; 141 | this.on(method, onClose); 142 | } else if (method === 'error') { 143 | onError._listener = listener; 144 | this.on(method, onError); 145 | } else if (method === 'open') { 146 | onOpen._listener = listener; 147 | this.on(method, onOpen); 148 | } else { 149 | this.on(method, listener); 150 | } 151 | } 152 | 153 | /** 154 | * Remove an event listener. 155 | * 156 | * @param {String} method A string representing the event type to remove 157 | * @param {Function} listener The listener to remove 158 | * @public 159 | */ 160 | export function removeEventListener(method, listener) { 161 | const listeners = this.listeners(method); 162 | 163 | for (let i = 0; i < listeners.length; i++) { 164 | if (listeners[i] === listener || listeners[i]._listener === listener) { 165 | this.removeListener(method, listeners[i]); 166 | } 167 | } 168 | } 169 | 170 | // module.exports = EventTarget; 171 | -------------------------------------------------------------------------------- /src/ws/extension.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // 4 | // Allowed token characters: 5 | // 6 | // '!', '#', '$', '%', '&', ''', '*', '+', '-', 7 | // '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~' 8 | // 9 | // tokenChars[32] === 0 // ' ' 10 | // tokenChars[33] === 1 // '!' 11 | // tokenChars[34] === 0 // '"' 12 | // ... 13 | // 14 | // prettier-ignore 15 | const tokenChars = [ 16 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 17 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 18 | 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47 19 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 20 | 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 21 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95 22 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 23 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127 24 | ]; 25 | 26 | /** 27 | * Adds an offer to the map of extension offers or a parameter to the map of 28 | * parameters. 29 | * 30 | * @param {Object} dest The map of extension offers or parameters 31 | * @param {String} name The extension or parameter name 32 | * @param {(Object|Boolean|String)} elem The extension parameters or the 33 | * parameter value 34 | * @private 35 | */ 36 | function push(dest, name, elem) { 37 | if (dest[name] === undefined) dest[name] = [elem]; 38 | else dest[name].push(elem); 39 | } 40 | 41 | /** 42 | * Parses the `Sec-WebSocket-Extensions` header into an object. 43 | * 44 | * @param {String} header The field value of the header 45 | * @return {Object} The parsed object 46 | * @public 47 | */ 48 | export function parse(header) { 49 | const offers = Object.create(null); 50 | 51 | if (header === undefined || header === '') return offers; 52 | 53 | let params = Object.create(null); 54 | let mustUnescape = false; 55 | let isEscaping = false; 56 | let inQuotes = false; 57 | let extensionName; 58 | let paramName; 59 | let start = -1; 60 | let end = -1; 61 | let i = 0; 62 | 63 | for (; i < header.length; i++) { 64 | const code = header.charCodeAt(i); 65 | 66 | if (extensionName === undefined) { 67 | if (end === -1 && tokenChars[code] === 1) { 68 | if (start === -1) start = i; 69 | } else if (code === 0x20 /* ' ' */ || code === 0x09 /* '\t' */) { 70 | if (end === -1 && start !== -1) end = i; 71 | } else if (code === 0x3b /* ';' */ || code === 0x2c /* ',' */) { 72 | if (start === -1) { 73 | throw new SyntaxError(`Unexpected character at index ${i}`); 74 | } 75 | 76 | if (end === -1) end = i; 77 | const name = header.slice(start, end); 78 | if (code === 0x2c) { 79 | push(offers, name, params); 80 | params = Object.create(null); 81 | } else { 82 | extensionName = name; 83 | } 84 | 85 | start = end = -1; 86 | } else { 87 | throw new SyntaxError(`Unexpected character at index ${i}`); 88 | } 89 | } else if (paramName === undefined) { 90 | if (end === -1 && tokenChars[code] === 1) { 91 | if (start === -1) start = i; 92 | } else if (code === 0x20 || code === 0x09) { 93 | if (end === -1 && start !== -1) end = i; 94 | } else if (code === 0x3b || code === 0x2c) { 95 | if (start === -1) { 96 | throw new SyntaxError(`Unexpected character at index ${i}`); 97 | } 98 | 99 | if (end === -1) end = i; 100 | push(params, header.slice(start, end), true); 101 | if (code === 0x2c) { 102 | push(offers, extensionName, params); 103 | params = Object.create(null); 104 | extensionName = undefined; 105 | } 106 | 107 | start = end = -1; 108 | } else if (code === 0x3d /* '=' */ && start !== -1 && end === -1) { 109 | paramName = header.slice(start, i); 110 | start = end = -1; 111 | } else { 112 | throw new SyntaxError(`Unexpected character at index ${i}`); 113 | } 114 | } else { 115 | // 116 | // The value of a quoted-string after unescaping must conform to the 117 | // token ABNF, so only token characters are valid. 118 | // Ref: https://tools.ietf.org/html/rfc6455#section-9.1 119 | // 120 | if (isEscaping) { 121 | if (tokenChars[code] !== 1) { 122 | throw new SyntaxError(`Unexpected character at index ${i}`); 123 | } 124 | if (start === -1) start = i; 125 | else if (!mustUnescape) mustUnescape = true; 126 | isEscaping = false; 127 | } else if (inQuotes) { 128 | if (tokenChars[code] === 1) { 129 | if (start === -1) start = i; 130 | } else if (code === 0x22 /* '"' */ && start !== -1) { 131 | inQuotes = false; 132 | end = i; 133 | } else if (code === 0x5c /* '\' */) { 134 | isEscaping = true; 135 | } else { 136 | throw new SyntaxError(`Unexpected character at index ${i}`); 137 | } 138 | } else if (code === 0x22 && header.charCodeAt(i - 1) === 0x3d) { 139 | inQuotes = true; 140 | } else if (end === -1 && tokenChars[code] === 1) { 141 | if (start === -1) start = i; 142 | } else if (start !== -1 && (code === 0x20 || code === 0x09)) { 143 | if (end === -1) end = i; 144 | } else if (code === 0x3b || code === 0x2c) { 145 | if (start === -1) { 146 | throw new SyntaxError(`Unexpected character at index ${i}`); 147 | } 148 | 149 | if (end === -1) end = i; 150 | let value = header.slice(start, end); 151 | if (mustUnescape) { 152 | value = value.replace(/\\/g, ''); 153 | mustUnescape = false; 154 | } 155 | push(params, paramName, value); 156 | if (code === 0x2c) { 157 | push(offers, extensionName, params); 158 | params = Object.create(null); 159 | extensionName = undefined; 160 | } 161 | 162 | paramName = undefined; 163 | start = end = -1; 164 | } else { 165 | throw new SyntaxError(`Unexpected character at index ${i}`); 166 | } 167 | } 168 | } 169 | 170 | if (start === -1 || inQuotes) { 171 | throw new SyntaxError('Unexpected end of input'); 172 | } 173 | 174 | if (end === -1) end = i; 175 | const token = header.slice(start, end); 176 | if (extensionName === undefined) { 177 | push(offers, token, params); 178 | } else { 179 | if (paramName === undefined) { 180 | push(params, token, true); 181 | } else if (mustUnescape) { 182 | push(params, paramName, token.replace(/\\/g, '')); 183 | } else { 184 | push(params, paramName, token); 185 | } 186 | push(offers, extensionName, params); 187 | } 188 | 189 | return offers; 190 | } 191 | 192 | /** 193 | * Builds the `Sec-WebSocket-Extensions` header field value. 194 | * 195 | * @param {Object} extensions The map of extensions and parameters to format 196 | * @return {String} A string representing the given object 197 | * @public 198 | */ 199 | export function format(extensions) { 200 | return Object.keys(extensions) 201 | .map((extension) => { 202 | let configurations = extensions[extension]; 203 | if (!Array.isArray(configurations)) configurations = [configurations]; 204 | return configurations 205 | .map((params) => { 206 | return [extension] 207 | .concat( 208 | Object.keys(params).map((k) => { 209 | let values = params[k]; 210 | if (!Array.isArray(values)) values = [values]; 211 | return values 212 | .map((v) => (v === true ? k : `${k}=${v}`)) 213 | .join('; '); 214 | }) 215 | ) 216 | .join('; '); 217 | }) 218 | .join(', '); 219 | }) 220 | .join(', '); 221 | } 222 | 223 | // module.exports = { format, parse }; 224 | -------------------------------------------------------------------------------- /src/ws/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const WebSocket = require('./websocket'); 4 | 5 | WebSocket.createWebSocketStream = require('./stream'); 6 | WebSocket.Server = require('./websocket-server'); 7 | WebSocket.Receiver = require('./receiver'); 8 | WebSocket.Sender = require('./sender'); 9 | 10 | module.exports = WebSocket; 11 | -------------------------------------------------------------------------------- /src/ws/permessage-deflate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const zlib = require('zlib'); 4 | 5 | import { Queue as Limiter } from './async-limiter' 6 | import * as bufferUtil from './buffer-util' 7 | import { kStatusCode, NOOP } from './constants' 8 | 9 | const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]); 10 | const EMPTY_BLOCK = Buffer.from([0x00]); 11 | 12 | const kPerMessageDeflate = Symbol('permessage-deflate'); 13 | const kTotalLength = Symbol('total-length'); 14 | const kCallback = Symbol('callback'); 15 | const kBuffers = Symbol('buffers'); 16 | const kError = Symbol('error'); 17 | 18 | // 19 | // We limit zlib concurrency, which prevents severe memory fragmentation 20 | // as documented in https://github.com/nodejs/node/issues/8871#issuecomment-250915913 21 | // and https://github.com/websockets/ws/issues/1202 22 | // 23 | // Intentionally global; it's the global thread pool that's an issue. 24 | // 25 | let zlibLimiter; 26 | 27 | /** 28 | * permessage-deflate implementation. 29 | */ 30 | export class PerMessageDeflate { 31 | /** 32 | * Creates a PerMessageDeflate instance. 33 | * 34 | * @param {Object} options Configuration options 35 | * @param {Boolean} options.serverNoContextTakeover Request/accept disabling 36 | * of server context takeover 37 | * @param {Boolean} options.clientNoContextTakeover Advertise/acknowledge 38 | * disabling of client context takeover 39 | * @param {(Boolean|Number)} options.serverMaxWindowBits Request/confirm the 40 | * use of a custom server window size 41 | * @param {(Boolean|Number)} options.clientMaxWindowBits Advertise support 42 | * for, or request, a custom client window size 43 | * @param {Object} options.zlibDeflateOptions Options to pass to zlib on deflate 44 | * @param {Object} options.zlibInflateOptions Options to pass to zlib on inflate 45 | * @param {Number} options.threshold Size (in bytes) below which messages 46 | * should not be compressed 47 | * @param {Number} options.concurrencyLimit The number of concurrent calls to 48 | * zlib 49 | * @param {Boolean} isServer Create the instance in either server or client 50 | * mode 51 | * @param {Number} maxPayload The maximum allowed message length 52 | */ 53 | constructor(options, isServer, maxPayload) { 54 | this._maxPayload = maxPayload | 0; 55 | this._options = options || {}; 56 | this._threshold = 57 | this._options.threshold !== undefined ? this._options.threshold : 1024; 58 | this._isServer = !!isServer; 59 | this._deflate = null; 60 | this._inflate = null; 61 | 62 | this.params = null; 63 | 64 | if (!zlibLimiter) { 65 | const concurrency = 66 | this._options.concurrencyLimit !== undefined 67 | ? this._options.concurrencyLimit 68 | : 10; 69 | zlibLimiter = new Limiter({ concurrency }); 70 | } 71 | } 72 | 73 | /** 74 | * @type {String} 75 | */ 76 | static get extensionName() { 77 | return 'permessage-deflate'; 78 | } 79 | 80 | /** 81 | * Create an extension negotiation offer. 82 | * 83 | * @return {Object} Extension parameters 84 | * @public 85 | */ 86 | offer() { 87 | const params = {}; 88 | 89 | if (this._options.serverNoContextTakeover) { 90 | params.server_no_context_takeover = true; 91 | } 92 | if (this._options.clientNoContextTakeover) { 93 | params.client_no_context_takeover = true; 94 | } 95 | if (this._options.serverMaxWindowBits) { 96 | params.server_max_window_bits = this._options.serverMaxWindowBits; 97 | } 98 | if (this._options.clientMaxWindowBits) { 99 | params.client_max_window_bits = this._options.clientMaxWindowBits; 100 | } else if (this._options.clientMaxWindowBits == null) { 101 | params.client_max_window_bits = true; 102 | } 103 | 104 | return params; 105 | } 106 | 107 | /** 108 | * Accept an extension negotiation offer/response. 109 | * 110 | * @param {Array} configurations The extension negotiation offers/reponse 111 | * @return {Object} Accepted configuration 112 | * @public 113 | */ 114 | accept(configurations) { 115 | configurations = this.normalizeParams(configurations); 116 | 117 | this.params = this._isServer 118 | ? this.acceptAsServer(configurations) 119 | : this.acceptAsClient(configurations); 120 | 121 | return this.params; 122 | } 123 | 124 | /** 125 | * Releases all resources used by the extension. 126 | * 127 | * @public 128 | */ 129 | cleanup() { 130 | if (this._inflate) { 131 | this._inflate.close(); 132 | this._inflate = null; 133 | } 134 | 135 | if (this._deflate) { 136 | if (this._deflate[kCallback]) { 137 | this._deflate[kCallback](); 138 | } 139 | 140 | this._deflate.close(); 141 | this._deflate = null; 142 | } 143 | } 144 | 145 | /** 146 | * Accept an extension negotiation offer. 147 | * 148 | * @param {Array} offers The extension negotiation offers 149 | * @return {Object} Accepted configuration 150 | * @private 151 | */ 152 | acceptAsServer(offers) { 153 | const opts = this._options; 154 | const accepted = offers.find((params) => { 155 | if ( 156 | (opts.serverNoContextTakeover === false && 157 | params.server_no_context_takeover) || 158 | (params.server_max_window_bits && 159 | (opts.serverMaxWindowBits === false || 160 | (typeof opts.serverMaxWindowBits === 'number' && 161 | opts.serverMaxWindowBits > params.server_max_window_bits))) || 162 | (typeof opts.clientMaxWindowBits === 'number' && 163 | !params.client_max_window_bits) 164 | ) { 165 | return false; 166 | } 167 | 168 | return true; 169 | }); 170 | 171 | if (!accepted) { 172 | throw new Error('None of the extension offers can be accepted'); 173 | } 174 | 175 | if (opts.serverNoContextTakeover) { 176 | accepted.server_no_context_takeover = true; 177 | } 178 | if (opts.clientNoContextTakeover) { 179 | accepted.client_no_context_takeover = true; 180 | } 181 | if (typeof opts.serverMaxWindowBits === 'number') { 182 | accepted.server_max_window_bits = opts.serverMaxWindowBits; 183 | } 184 | if (typeof opts.clientMaxWindowBits === 'number') { 185 | accepted.client_max_window_bits = opts.clientMaxWindowBits; 186 | } else if ( 187 | accepted.client_max_window_bits === true || 188 | opts.clientMaxWindowBits === false 189 | ) { 190 | delete accepted.client_max_window_bits; 191 | } 192 | 193 | return accepted; 194 | } 195 | 196 | /** 197 | * Accept the extension negotiation response. 198 | * 199 | * @param {Array} response The extension negotiation response 200 | * @return {Object} Accepted configuration 201 | * @private 202 | */ 203 | acceptAsClient(response) { 204 | const params = response[0]; 205 | 206 | if ( 207 | this._options.clientNoContextTakeover === false && 208 | params.client_no_context_takeover 209 | ) { 210 | throw new Error('Unexpected parameter "client_no_context_takeover"'); 211 | } 212 | 213 | if (!params.client_max_window_bits) { 214 | if (typeof this._options.clientMaxWindowBits === 'number') { 215 | params.client_max_window_bits = this._options.clientMaxWindowBits; 216 | } 217 | } else if ( 218 | this._options.clientMaxWindowBits === false || 219 | (typeof this._options.clientMaxWindowBits === 'number' && 220 | params.client_max_window_bits > this._options.clientMaxWindowBits) 221 | ) { 222 | throw new Error( 223 | 'Unexpected or invalid parameter "client_max_window_bits"' 224 | ); 225 | } 226 | 227 | return params; 228 | } 229 | 230 | /** 231 | * Normalize parameters. 232 | * 233 | * @param {Array} configurations The extension negotiation offers/reponse 234 | * @return {Array} The offers/response with normalized parameters 235 | * @private 236 | */ 237 | normalizeParams(configurations) { 238 | configurations.forEach((params) => { 239 | Object.keys(params).forEach((key) => { 240 | let value = params[key]; 241 | 242 | if (value.length > 1) { 243 | throw new Error(`Parameter "${key}" must have only a single value`); 244 | } 245 | 246 | value = value[0]; 247 | 248 | if (key === 'client_max_window_bits') { 249 | if (value !== true) { 250 | const num = +value; 251 | if (!Number.isInteger(num) || num < 8 || num > 15) { 252 | throw new TypeError( 253 | `Invalid value for parameter "${key}": ${value}` 254 | ); 255 | } 256 | value = num; 257 | } else if (!this._isServer) { 258 | throw new TypeError( 259 | `Invalid value for parameter "${key}": ${value}` 260 | ); 261 | } 262 | } else if (key === 'server_max_window_bits') { 263 | const num = +value; 264 | if (!Number.isInteger(num) || num < 8 || num > 15) { 265 | throw new TypeError( 266 | `Invalid value for parameter "${key}": ${value}` 267 | ); 268 | } 269 | value = num; 270 | } else if ( 271 | key === 'client_no_context_takeover' || 272 | key === 'server_no_context_takeover' 273 | ) { 274 | if (value !== true) { 275 | throw new TypeError( 276 | `Invalid value for parameter "${key}": ${value}` 277 | ); 278 | } 279 | } else { 280 | throw new Error(`Unknown parameter "${key}"`); 281 | } 282 | 283 | params[key] = value; 284 | }); 285 | }); 286 | 287 | return configurations; 288 | } 289 | 290 | /** 291 | * Decompress data. Concurrency limited by async-limiter. 292 | * 293 | * @param {Buffer} data Compressed data 294 | * @param {Boolean} fin Specifies whether or not this is the last fragment 295 | * @param {Function} callback Callback 296 | * @public 297 | */ 298 | decompress(data, fin, callback) { 299 | zlibLimiter.push((done) => { 300 | this._decompress(data, fin, (err, result) => { 301 | done(); 302 | callback(err, result); 303 | }); 304 | }); 305 | } 306 | 307 | /** 308 | * Compress data. Concurrency limited by async-limiter. 309 | * 310 | * @param {Buffer} data Data to compress 311 | * @param {Boolean} fin Specifies whether or not this is the last fragment 312 | * @param {Function} callback Callback 313 | * @public 314 | */ 315 | compress(data, fin, callback) { 316 | zlibLimiter.push((done) => { 317 | this._compress(data, fin, (err, result) => { 318 | done(); 319 | if (err || result) { 320 | callback(err, result); 321 | } 322 | }); 323 | }); 324 | } 325 | 326 | /** 327 | * Decompress data. 328 | * 329 | * @param {Buffer} data Compressed data 330 | * @param {Boolean} fin Specifies whether or not this is the last fragment 331 | * @param {Function} callback Callback 332 | * @private 333 | */ 334 | _decompress(data, fin, callback) { 335 | const endpoint = this._isServer ? 'client' : 'server'; 336 | 337 | if (!this._inflate) { 338 | const key = `${endpoint}_max_window_bits`; 339 | const windowBits = 340 | typeof this.params[key] !== 'number' 341 | ? zlib.Z_DEFAULT_WINDOWBITS 342 | : this.params[key]; 343 | 344 | this._inflate = zlib.createInflateRaw({ 345 | ...this._options.zlibInflateOptions, 346 | windowBits 347 | }); 348 | this._inflate[kPerMessageDeflate] = this; 349 | this._inflate[kTotalLength] = 0; 350 | this._inflate[kBuffers] = []; 351 | this._inflate.on('error', inflateOnError); 352 | this._inflate.on('data', inflateOnData); 353 | } 354 | 355 | this._inflate[kCallback] = callback; 356 | 357 | this._inflate.write(data); 358 | if (fin) this._inflate.write(TRAILER); 359 | 360 | this._inflate.flush(() => { 361 | const err = this._inflate[kError]; 362 | 363 | if (err) { 364 | this._inflate.close(); 365 | this._inflate = null; 366 | callback(err); 367 | return; 368 | } 369 | 370 | const data = bufferUtil.concat( 371 | this._inflate[kBuffers], 372 | this._inflate[kTotalLength] 373 | ); 374 | 375 | if (fin && this.params[`${endpoint}_no_context_takeover`]) { 376 | this._inflate.close(); 377 | this._inflate = null; 378 | } else { 379 | this._inflate[kTotalLength] = 0; 380 | this._inflate[kBuffers] = []; 381 | } 382 | 383 | callback(null, data); 384 | }); 385 | } 386 | 387 | /** 388 | * Compress data. 389 | * 390 | * @param {Buffer} data Data to compress 391 | * @param {Boolean} fin Specifies whether or not this is the last fragment 392 | * @param {Function} callback Callback 393 | * @private 394 | */ 395 | _compress(data, fin, callback) { 396 | if (!data || data.length === 0) { 397 | process.nextTick(callback, null, EMPTY_BLOCK); 398 | return; 399 | } 400 | 401 | const endpoint = this._isServer ? 'server' : 'client'; 402 | 403 | if (!this._deflate) { 404 | const key = `${endpoint}_max_window_bits`; 405 | const windowBits = 406 | typeof this.params[key] !== 'number' 407 | ? zlib.Z_DEFAULT_WINDOWBITS 408 | : this.params[key]; 409 | 410 | this._deflate = zlib.createDeflateRaw({ 411 | ...this._options.zlibDeflateOptions, 412 | windowBits 413 | }); 414 | 415 | this._deflate[kTotalLength] = 0; 416 | this._deflate[kBuffers] = []; 417 | 418 | // 419 | // An `'error'` event is emitted, only on Node.js < 10.0.0, if the 420 | // `zlib.DeflateRaw` instance is closed while data is being processed. 421 | // This can happen if `PerMessageDeflate#cleanup()` is called at the wrong 422 | // time due to an abnormal WebSocket closure. 423 | // 424 | this._deflate.on('error', NOOP); 425 | this._deflate.on('data', deflateOnData); 426 | } 427 | 428 | this._deflate[kCallback] = callback; 429 | 430 | this._deflate.write(data); 431 | this._deflate.flush(zlib.Z_SYNC_FLUSH, () => { 432 | if (!this._deflate) { 433 | // 434 | // This `if` statement is only needed for Node.js < 10.0.0 because as of 435 | // commit https://github.com/nodejs/node/commit/5e3f5164, the flush 436 | // callback is no longer called if the deflate stream is closed while 437 | // data is being processed. 438 | // 439 | return; 440 | } 441 | 442 | let data = bufferUtil.concat( 443 | this._deflate[kBuffers], 444 | this._deflate[kTotalLength] 445 | ); 446 | 447 | if (fin) data = data.slice(0, data.length - 4); 448 | 449 | // 450 | // Ensure that the callback will not be called again in 451 | // `PerMessageDeflate#cleanup()`. 452 | // 453 | this._deflate[kCallback] = null; 454 | 455 | if (fin && this.params[`${endpoint}_no_context_takeover`]) { 456 | this._deflate.close(); 457 | this._deflate = null; 458 | } else { 459 | this._deflate[kTotalLength] = 0; 460 | this._deflate[kBuffers] = []; 461 | } 462 | 463 | callback(null, data); 464 | }); 465 | } 466 | } 467 | 468 | module.exports = PerMessageDeflate; 469 | 470 | /** 471 | * The listener of the `zlib.DeflateRaw` stream `'data'` event. 472 | * 473 | * @param {Buffer} chunk A chunk of data 474 | * @private 475 | */ 476 | function deflateOnData(chunk) { 477 | this[kBuffers].push(chunk); 478 | this[kTotalLength] += chunk.length; 479 | } 480 | 481 | /** 482 | * The listener of the `zlib.InflateRaw` stream `'data'` event. 483 | * 484 | * @param {Buffer} chunk A chunk of data 485 | * @private 486 | */ 487 | function inflateOnData(chunk) { 488 | this[kTotalLength] += chunk.length; 489 | 490 | if ( 491 | this[kPerMessageDeflate]._maxPayload < 1 || 492 | this[kTotalLength] <= this[kPerMessageDeflate]._maxPayload 493 | ) { 494 | this[kBuffers].push(chunk); 495 | return; 496 | } 497 | 498 | this[kError] = new RangeError('Max payload size exceeded'); 499 | this[kError][kStatusCode] = 1009; 500 | this.removeListener('data', inflateOnData); 501 | this.reset(); 502 | } 503 | 504 | /** 505 | * The listener of the `zlib.InflateRaw` stream `'error'` event. 506 | * 507 | * @param {Error} err The emitted error 508 | * @private 509 | */ 510 | function inflateOnError(err) { 511 | // 512 | // There is no need to call `Zlib#close()` as the handle is automatically 513 | // closed when an error is emitted. 514 | // 515 | this[kPerMessageDeflate]._inflate = null; 516 | err[kStatusCode] = 1007; 517 | this[kCallback](err); 518 | } 519 | -------------------------------------------------------------------------------- /src/ws/receiver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Writable } = require('stream'); 4 | 5 | import {PerMessageDeflate} from './permessage-deflate' 6 | import { 7 | BINARY_TYPES, 8 | EMPTY_BUFFER, 9 | kStatusCode, 10 | kWebSocket 11 | } from './constants' 12 | import { concat, toArrayBuffer, unmask } from './buffer-util' 13 | import { isValidStatusCode, isValidUTF8 } from './validation' 14 | 15 | const GET_INFO = 0; 16 | const GET_PAYLOAD_LENGTH_16 = 1; 17 | const GET_PAYLOAD_LENGTH_64 = 2; 18 | const GET_MASK = 3; 19 | const GET_DATA = 4; 20 | const INFLATING = 5; 21 | 22 | /** 23 | * HyBi Receiver implementation. 24 | * 25 | * @extends stream.Writable 26 | */ 27 | export class Receiver extends Writable { 28 | /** 29 | * Creates a Receiver instance. 30 | * 31 | * @param {String} binaryType The type for binary data 32 | * @param {Object} extensions An object containing the negotiated extensions 33 | * @param {Number} maxPayload The maximum allowed message length 34 | */ 35 | constructor(binaryType, extensions, maxPayload) { 36 | super(); 37 | 38 | this._binaryType = binaryType || BINARY_TYPES[0]; 39 | this[kWebSocket] = undefined; 40 | this._extensions = extensions || {}; 41 | this._maxPayload = maxPayload | 0; 42 | 43 | this._bufferedBytes = 0; 44 | this._buffers = []; 45 | 46 | this._compressed = false; 47 | this._payloadLength = 0; 48 | this._mask = undefined; 49 | this._fragmented = 0; 50 | this._masked = false; 51 | this._fin = false; 52 | this._opcode = 0; 53 | 54 | this._totalPayloadLength = 0; 55 | this._messageLength = 0; 56 | this._fragments = []; 57 | 58 | this._state = GET_INFO; 59 | this._loop = false; 60 | } 61 | 62 | /** 63 | * Implements `Writable.prototype._write()`. 64 | * 65 | * @param {Buffer} chunk The chunk of data to write 66 | * @param {String} encoding The character encoding of `chunk` 67 | * @param {Function} cb Callback 68 | */ 69 | _write(chunk, encoding, cb) { 70 | if (this._opcode === 0x08 && this._state == GET_INFO) return cb(); 71 | 72 | this._bufferedBytes += chunk.length; 73 | this._buffers.push(chunk); 74 | this.startLoop(cb); 75 | } 76 | 77 | /** 78 | * Consumes `n` bytes from the buffered data. 79 | * 80 | * @param {Number} n The number of bytes to consume 81 | * @return {Buffer} The consumed bytes 82 | * @private 83 | */ 84 | consume(n) { 85 | this._bufferedBytes -= n; 86 | 87 | if (n === this._buffers[0].length) return this._buffers.shift(); 88 | 89 | if (n < this._buffers[0].length) { 90 | const buf = this._buffers[0]; 91 | this._buffers[0] = buf.slice(n); 92 | return buf.slice(0, n); 93 | } 94 | 95 | const dst = Buffer.allocUnsafe(n); 96 | 97 | do { 98 | const buf = this._buffers[0]; 99 | const offset = dst.length - n; 100 | 101 | if (n >= buf.length) { 102 | dst.set(this._buffers.shift(), offset); 103 | } else { 104 | dst.set(new Uint8Array(buf.buffer, buf.byteOffset, n), offset); 105 | this._buffers[0] = buf.slice(n); 106 | } 107 | 108 | n -= buf.length; 109 | } while (n > 0); 110 | 111 | return dst; 112 | } 113 | 114 | /** 115 | * Starts the parsing loop. 116 | * 117 | * @param {Function} cb Callback 118 | * @private 119 | */ 120 | startLoop(cb) { 121 | let err; 122 | this._loop = true; 123 | 124 | do { 125 | switch (this._state) { 126 | case GET_INFO: 127 | err = this.getInfo(); 128 | break; 129 | case GET_PAYLOAD_LENGTH_16: 130 | err = this.getPayloadLength16(); 131 | break; 132 | case GET_PAYLOAD_LENGTH_64: 133 | err = this.getPayloadLength64(); 134 | break; 135 | case GET_MASK: 136 | this.getMask(); 137 | break; 138 | case GET_DATA: 139 | err = this.getData(cb); 140 | break; 141 | default: 142 | // `INFLATING` 143 | this._loop = false; 144 | return; 145 | } 146 | } while (this._loop); 147 | 148 | cb(err); 149 | } 150 | 151 | /** 152 | * Reads the first two bytes of a frame. 153 | * 154 | * @return {(RangeError|undefined)} A possible error 155 | * @private 156 | */ 157 | getInfo() { 158 | if (this._bufferedBytes < 2) { 159 | this._loop = false; 160 | return; 161 | } 162 | 163 | const buf = this.consume(2); 164 | 165 | if ((buf[0] & 0x30) !== 0x00) { 166 | this._loop = false; 167 | return error(RangeError, 'RSV2 and RSV3 must be clear', true, 1002); 168 | } 169 | 170 | const compressed = (buf[0] & 0x40) === 0x40; 171 | 172 | if (compressed && !this._extensions[PerMessageDeflate.extensionName]) { 173 | this._loop = false; 174 | return error(RangeError, 'RSV1 must be clear', true, 1002); 175 | } 176 | 177 | this._fin = (buf[0] & 0x80) === 0x80; 178 | this._opcode = buf[0] & 0x0f; 179 | this._payloadLength = buf[1] & 0x7f; 180 | 181 | if (this._opcode === 0x00) { 182 | if (compressed) { 183 | this._loop = false; 184 | return error(RangeError, 'RSV1 must be clear', true, 1002); 185 | } 186 | 187 | if (!this._fragmented) { 188 | this._loop = false; 189 | return error(RangeError, 'invalid opcode 0', true, 1002); 190 | } 191 | 192 | this._opcode = this._fragmented; 193 | } else if (this._opcode === 0x01 || this._opcode === 0x02) { 194 | if (this._fragmented) { 195 | this._loop = false; 196 | return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002); 197 | } 198 | 199 | this._compressed = compressed; 200 | } else if (this._opcode > 0x07 && this._opcode < 0x0b) { 201 | if (!this._fin) { 202 | this._loop = false; 203 | return error(RangeError, 'FIN must be set', true, 1002); 204 | } 205 | 206 | if (compressed) { 207 | this._loop = false; 208 | return error(RangeError, 'RSV1 must be clear', true, 1002); 209 | } 210 | 211 | if (this._payloadLength > 0x7d) { 212 | this._loop = false; 213 | return error( 214 | RangeError, 215 | `invalid payload length ${this._payloadLength}`, 216 | true, 217 | 1002 218 | ); 219 | } 220 | } else { 221 | this._loop = false; 222 | return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002); 223 | } 224 | 225 | if (!this._fin && !this._fragmented) this._fragmented = this._opcode; 226 | this._masked = (buf[1] & 0x80) === 0x80; 227 | 228 | if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16; 229 | else if (this._payloadLength === 127) this._state = GET_PAYLOAD_LENGTH_64; 230 | else return this.haveLength(); 231 | } 232 | 233 | /** 234 | * Gets extended payload length (7+16). 235 | * 236 | * @return {(RangeError|undefined)} A possible error 237 | * @private 238 | */ 239 | getPayloadLength16() { 240 | if (this._bufferedBytes < 2) { 241 | this._loop = false; 242 | return; 243 | } 244 | 245 | this._payloadLength = this.consume(2).readUInt16BE(0); 246 | return this.haveLength(); 247 | } 248 | 249 | /** 250 | * Gets extended payload length (7+64). 251 | * 252 | * @return {(RangeError|undefined)} A possible error 253 | * @private 254 | */ 255 | getPayloadLength64() { 256 | if (this._bufferedBytes < 8) { 257 | this._loop = false; 258 | return; 259 | } 260 | 261 | const buf = this.consume(8); 262 | const num = buf.readUInt32BE(0); 263 | 264 | // 265 | // The maximum safe integer in JavaScript is 2^53 - 1. An error is returned 266 | // if payload length is greater than this number. 267 | // 268 | if (num > Math.pow(2, 53 - 32) - 1) { 269 | this._loop = false; 270 | return error( 271 | RangeError, 272 | 'Unsupported WebSocket frame: payload length > 2^53 - 1', 273 | false, 274 | 1009 275 | ); 276 | } 277 | 278 | this._payloadLength = num * Math.pow(2, 32) + buf.readUInt32BE(4); 279 | return this.haveLength(); 280 | } 281 | 282 | /** 283 | * Payload length has been read. 284 | * 285 | * @return {(RangeError|undefined)} A possible error 286 | * @private 287 | */ 288 | haveLength() { 289 | if (this._payloadLength && this._opcode < 0x08) { 290 | this._totalPayloadLength += this._payloadLength; 291 | if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) { 292 | this._loop = false; 293 | return error(RangeError, 'Max payload size exceeded', false, 1009); 294 | } 295 | } 296 | 297 | if (this._masked) this._state = GET_MASK; 298 | else this._state = GET_DATA; 299 | } 300 | 301 | /** 302 | * Reads mask bytes. 303 | * 304 | * @private 305 | */ 306 | getMask() { 307 | if (this._bufferedBytes < 4) { 308 | this._loop = false; 309 | return; 310 | } 311 | 312 | this._mask = this.consume(4); 313 | this._state = GET_DATA; 314 | } 315 | 316 | /** 317 | * Reads data bytes. 318 | * 319 | * @param {Function} cb Callback 320 | * @return {(Error|RangeError|undefined)} A possible error 321 | * @private 322 | */ 323 | getData(cb) { 324 | let data = EMPTY_BUFFER; 325 | 326 | if (this._payloadLength) { 327 | if (this._bufferedBytes < this._payloadLength) { 328 | this._loop = false; 329 | return; 330 | } 331 | 332 | data = this.consume(this._payloadLength); 333 | if (this._masked) unmask(data, this._mask); 334 | } 335 | 336 | if (this._opcode > 0x07) return this.controlMessage(data); 337 | 338 | if (this._compressed) { 339 | this._state = INFLATING; 340 | this.decompress(data, cb); 341 | return; 342 | } 343 | 344 | if (data.length) { 345 | // 346 | // This message is not compressed so its lenght is the sum of the payload 347 | // length of all fragments. 348 | // 349 | this._messageLength = this._totalPayloadLength; 350 | this._fragments.push(data); 351 | } 352 | 353 | return this.dataMessage(); 354 | } 355 | 356 | /** 357 | * Decompresses data. 358 | * 359 | * @param {Buffer} data Compressed data 360 | * @param {Function} cb Callback 361 | * @private 362 | */ 363 | decompress(data, cb) { 364 | const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; 365 | 366 | perMessageDeflate.decompress(data, this._fin, (err, buf) => { 367 | if (err) return cb(err); 368 | 369 | if (buf.length) { 370 | this._messageLength += buf.length; 371 | if (this._messageLength > this._maxPayload && this._maxPayload > 0) { 372 | return cb( 373 | error(RangeError, 'Max payload size exceeded', false, 1009) 374 | ); 375 | } 376 | 377 | this._fragments.push(buf); 378 | } 379 | 380 | const er = this.dataMessage(); 381 | if (er) return cb(er); 382 | 383 | this.startLoop(cb); 384 | }); 385 | } 386 | 387 | /** 388 | * Handles a data message. 389 | * 390 | * @return {(Error|undefined)} A possible error 391 | * @private 392 | */ 393 | dataMessage() { 394 | if (this._fin) { 395 | const messageLength = this._messageLength; 396 | const fragments = this._fragments; 397 | 398 | this._totalPayloadLength = 0; 399 | this._messageLength = 0; 400 | this._fragmented = 0; 401 | this._fragments = []; 402 | 403 | if (this._opcode === 2) { 404 | let data; 405 | 406 | if (this._binaryType === 'nodebuffer') { 407 | data = concat(fragments, messageLength); 408 | } else if (this._binaryType === 'arraybuffer') { 409 | data = toArrayBuffer(concat(fragments, messageLength)); 410 | } else { 411 | data = fragments; 412 | } 413 | 414 | this.emit('message', data); 415 | } else { 416 | const buf = concat(fragments, messageLength); 417 | 418 | if (!isValidUTF8(buf)) { 419 | this._loop = false; 420 | return error(Error, 'invalid UTF-8 sequence', true, 1007); 421 | } 422 | 423 | this.emit('message', buf.toString()); 424 | } 425 | } 426 | 427 | this._state = GET_INFO; 428 | } 429 | 430 | /** 431 | * Handles a control message. 432 | * 433 | * @param {Buffer} data Data to handle 434 | * @return {(Error|RangeError|undefined)} A possible error 435 | * @private 436 | */ 437 | controlMessage(data) { 438 | if (this._opcode === 0x08) { 439 | this._loop = false; 440 | 441 | if (data.length === 0) { 442 | this.emit('conclude', 1005, ''); 443 | this.end(); 444 | } else if (data.length === 1) { 445 | return error(RangeError, 'invalid payload length 1', true, 1002); 446 | } else { 447 | const code = data.readUInt16BE(0); 448 | 449 | if (!isValidStatusCode(code)) { 450 | return error(RangeError, `invalid status code ${code}`, true, 1002); 451 | } 452 | 453 | const buf = data.slice(2); 454 | 455 | if (!isValidUTF8(buf)) { 456 | return error(Error, 'invalid UTF-8 sequence', true, 1007); 457 | } 458 | 459 | this.emit('conclude', code, buf.toString()); 460 | this.end(); 461 | } 462 | } else if (this._opcode === 0x09) { 463 | this.emit('ping', data); 464 | } else { 465 | this.emit('pong', data); 466 | } 467 | 468 | this._state = GET_INFO; 469 | } 470 | } 471 | 472 | module.exports = Receiver; 473 | 474 | /** 475 | * Builds an error object. 476 | * 477 | * @param {(Error|RangeError)} ErrorCtor The error constructor 478 | * @param {String} message The error message 479 | * @param {Boolean} prefix Specifies whether or not to add a default prefix to 480 | * `message` 481 | * @param {Number} statusCode The status code 482 | * @return {(Error|RangeError)} The error 483 | * @private 484 | */ 485 | function error(ErrorCtor, message, prefix, statusCode) { 486 | const err = new ErrorCtor( 487 | prefix ? `Invalid WebSocket frame: ${message}` : message 488 | ); 489 | 490 | Error.captureStackTrace(err, error); 491 | err[kStatusCode] = statusCode; 492 | return err; 493 | } 494 | -------------------------------------------------------------------------------- /src/ws/sender.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { randomFillSync } = require('crypto'); 4 | 5 | import { PerMessageDeflate } from './permessage-deflate' 6 | import { EMPTY_BUFFER } from './constants' 7 | import { isValidStatusCode } from './validation' 8 | import { mask as applyMask, toBuffer } from './buffer-util' 9 | 10 | const mask = Buffer.alloc(4); 11 | 12 | /** 13 | * HyBi Sender implementation. 14 | */ 15 | export class Sender { 16 | /** 17 | * Creates a Sender instance. 18 | * 19 | * @param {net.Socket} socket The connection socket 20 | * @param {Object} extensions An object containing the negotiated extensions 21 | */ 22 | constructor(socket, extensions) { 23 | this._extensions = extensions || {}; 24 | this._socket = socket; 25 | 26 | this._firstFragment = true; 27 | this._compress = false; 28 | 29 | this._bufferedBytes = 0; 30 | this._deflating = false; 31 | this._queue = []; 32 | } 33 | 34 | /** 35 | * Frames a piece of data according to the HyBi WebSocket protocol. 36 | * 37 | * @param {Buffer} data The data to frame 38 | * @param {Object} options Options object 39 | * @param {Number} options.opcode The opcode 40 | * @param {Boolean} options.readOnly Specifies whether `data` can be modified 41 | * @param {Boolean} options.fin Specifies whether or not to set the FIN bit 42 | * @param {Boolean} options.mask Specifies whether or not to mask `data` 43 | * @param {Boolean} options.rsv1 Specifies whether or not to set the RSV1 bit 44 | * @return {Buffer[]} The framed data as a list of `Buffer` instances 45 | * @public 46 | */ 47 | static frame(data, options) { 48 | const merge = options.mask && options.readOnly; 49 | let offset = options.mask ? 6 : 2; 50 | let payloadLength = data.length; 51 | 52 | if (data.length >= 65536) { 53 | offset += 8; 54 | payloadLength = 127; 55 | } else if (data.length > 125) { 56 | offset += 2; 57 | payloadLength = 126; 58 | } 59 | 60 | const target = Buffer.allocUnsafe(merge ? data.length + offset : offset); 61 | 62 | target[0] = options.fin ? options.opcode | 0x80 : options.opcode; 63 | if (options.rsv1) target[0] |= 0x40; 64 | 65 | target[1] = payloadLength; 66 | 67 | if (payloadLength === 126) { 68 | target.writeUInt16BE(data.length, 2); 69 | } else if (payloadLength === 127) { 70 | target.writeUInt32BE(0, 2); 71 | target.writeUInt32BE(data.length, 6); 72 | } 73 | 74 | if (!options.mask) return [target, data]; 75 | 76 | randomFillSync(mask, 0, 4); 77 | 78 | target[1] |= 0x80; 79 | target[offset - 4] = mask[0]; 80 | target[offset - 3] = mask[1]; 81 | target[offset - 2] = mask[2]; 82 | target[offset - 1] = mask[3]; 83 | 84 | if (merge) { 85 | applyMask(data, mask, target, offset, data.length); 86 | return [target]; 87 | } 88 | 89 | applyMask(data, mask, data, 0, data.length); 90 | return [target, data]; 91 | } 92 | 93 | /** 94 | * Sends a close message to the other peer. 95 | * 96 | * @param {(Number|undefined)} code The status code component of the body 97 | * @param {String} data The message component of the body 98 | * @param {Boolean} mask Specifies whether or not to mask the message 99 | * @param {Function} cb Callback 100 | * @public 101 | */ 102 | close(code, data, mask, cb) { 103 | let buf; 104 | 105 | if (code === undefined) { 106 | buf = EMPTY_BUFFER; 107 | } else if (typeof code !== 'number' || !isValidStatusCode(code)) { 108 | throw new TypeError('First argument must be a valid error code number'); 109 | } else if (data === undefined || data === '') { 110 | buf = Buffer.allocUnsafe(2); 111 | buf.writeUInt16BE(code, 0); 112 | } else { 113 | buf = Buffer.allocUnsafe(2 + Buffer.byteLength(data)); 114 | buf.writeUInt16BE(code, 0); 115 | buf.write(data, 2); 116 | } 117 | 118 | if (this._deflating) { 119 | this.enqueue([this.doClose, buf, mask, cb]); 120 | } else { 121 | this.doClose(buf, mask, cb); 122 | } 123 | } 124 | 125 | /** 126 | * Frames and sends a close message. 127 | * 128 | * @param {Buffer} data The message to send 129 | * @param {Boolean} mask Specifies whether or not to mask `data` 130 | * @param {Function} cb Callback 131 | * @private 132 | */ 133 | doClose(data, mask, cb) { 134 | this.sendFrame( 135 | Sender.frame(data, { 136 | fin: true, 137 | rsv1: false, 138 | opcode: 0x08, 139 | mask, 140 | readOnly: false 141 | }), 142 | cb 143 | ); 144 | } 145 | 146 | /** 147 | * Sends a ping message to the other peer. 148 | * 149 | * @param {*} data The message to send 150 | * @param {Boolean} mask Specifies whether or not to mask `data` 151 | * @param {Function} cb Callback 152 | * @public 153 | */ 154 | ping(data, mask, cb) { 155 | const buf = toBuffer(data); 156 | 157 | if (this._deflating) { 158 | this.enqueue([this.doPing, buf, mask, toBuffer.readOnly, cb]); 159 | } else { 160 | this.doPing(buf, mask, toBuffer.readOnly, cb); 161 | } 162 | } 163 | 164 | /** 165 | * Frames and sends a ping message. 166 | * 167 | * @param {*} data The message to send 168 | * @param {Boolean} mask Specifies whether or not to mask `data` 169 | * @param {Boolean} readOnly Specifies whether `data` can be modified 170 | * @param {Function} cb Callback 171 | * @private 172 | */ 173 | doPing(data, mask, readOnly, cb) { 174 | this.sendFrame( 175 | Sender.frame(data, { 176 | fin: true, 177 | rsv1: false, 178 | opcode: 0x09, 179 | mask, 180 | readOnly 181 | }), 182 | cb 183 | ); 184 | } 185 | 186 | /** 187 | * Sends a pong message to the other peer. 188 | * 189 | * @param {*} data The message to send 190 | * @param {Boolean} mask Specifies whether or not to mask `data` 191 | * @param {Function} cb Callback 192 | * @public 193 | */ 194 | pong(data, mask, cb) { 195 | const buf = toBuffer(data); 196 | 197 | if (this._deflating) { 198 | this.enqueue([this.doPong, buf, mask, toBuffer.readOnly, cb]); 199 | } else { 200 | this.doPong(buf, mask, toBuffer.readOnly, cb); 201 | } 202 | } 203 | 204 | /** 205 | * Frames and sends a pong message. 206 | * 207 | * @param {*} data The message to send 208 | * @param {Boolean} mask Specifies whether or not to mask `data` 209 | * @param {Boolean} readOnly Specifies whether `data` can be modified 210 | * @param {Function} cb Callback 211 | * @private 212 | */ 213 | doPong(data, mask, readOnly, cb) { 214 | this.sendFrame( 215 | Sender.frame(data, { 216 | fin: true, 217 | rsv1: false, 218 | opcode: 0x0a, 219 | mask, 220 | readOnly 221 | }), 222 | cb 223 | ); 224 | } 225 | 226 | /** 227 | * Sends a data message to the other peer. 228 | * 229 | * @param {*} data The message to send 230 | * @param {Object} options Options object 231 | * @param {Boolean} options.compress Specifies whether or not to compress `data` 232 | * @param {Boolean} options.binary Specifies whether `data` is binary or text 233 | * @param {Boolean} options.fin Specifies whether the fragment is the last one 234 | * @param {Boolean} options.mask Specifies whether or not to mask `data` 235 | * @param {Function} cb Callback 236 | * @public 237 | */ 238 | send(data, options, cb) { 239 | const buf = toBuffer(data); 240 | const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; 241 | let opcode = options.binary ? 2 : 1; 242 | let rsv1 = options.compress; 243 | 244 | if (this._firstFragment) { 245 | this._firstFragment = false; 246 | if (rsv1 && perMessageDeflate) { 247 | rsv1 = buf.length >= perMessageDeflate._threshold; 248 | } 249 | this._compress = rsv1; 250 | } else { 251 | rsv1 = false; 252 | opcode = 0; 253 | } 254 | 255 | if (options.fin) this._firstFragment = true; 256 | 257 | if (perMessageDeflate) { 258 | const opts = { 259 | fin: options.fin, 260 | rsv1, 261 | opcode, 262 | mask: options.mask, 263 | readOnly: toBuffer.readOnly 264 | }; 265 | 266 | if (this._deflating) { 267 | this.enqueue([this.dispatch, buf, this._compress, opts, cb]); 268 | } else { 269 | this.dispatch(buf, this._compress, opts, cb); 270 | } 271 | } else { 272 | this.sendFrame( 273 | Sender.frame(buf, { 274 | fin: options.fin, 275 | rsv1: false, 276 | opcode, 277 | mask: options.mask, 278 | readOnly: toBuffer.readOnly 279 | }), 280 | cb 281 | ); 282 | } 283 | } 284 | 285 | /** 286 | * Dispatches a data message. 287 | * 288 | * @param {Buffer} data The message to send 289 | * @param {Boolean} compress Specifies whether or not to compress `data` 290 | * @param {Object} options Options object 291 | * @param {Number} options.opcode The opcode 292 | * @param {Boolean} options.readOnly Specifies whether `data` can be modified 293 | * @param {Boolean} options.fin Specifies whether or not to set the FIN bit 294 | * @param {Boolean} options.mask Specifies whether or not to mask `data` 295 | * @param {Boolean} options.rsv1 Specifies whether or not to set the RSV1 bit 296 | * @param {Function} cb Callback 297 | * @private 298 | */ 299 | dispatch(data, compress, options, cb) { 300 | if (!compress) { 301 | this.sendFrame(Sender.frame(data, options), cb); 302 | return; 303 | } 304 | 305 | const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; 306 | 307 | this._deflating = true; 308 | perMessageDeflate.compress(data, options.fin, (_, buf) => { 309 | this._deflating = false; 310 | options.readOnly = false; 311 | this.sendFrame(Sender.frame(buf, options), cb); 312 | this.dequeue(); 313 | }); 314 | } 315 | 316 | /** 317 | * Executes queued send operations. 318 | * 319 | * @private 320 | */ 321 | dequeue() { 322 | while (!this._deflating && this._queue.length) { 323 | const params = this._queue.shift(); 324 | 325 | this._bufferedBytes -= params[1].length; 326 | Reflect.apply(params[0], this, params.slice(1)); 327 | } 328 | } 329 | 330 | /** 331 | * Enqueues a send operation. 332 | * 333 | * @param {Array} params Send operation parameters. 334 | * @private 335 | */ 336 | enqueue(params) { 337 | this._bufferedBytes += params[1].length; 338 | this._queue.push(params); 339 | } 340 | 341 | /** 342 | * Sends a frame. 343 | * 344 | * @param {Buffer[]} list The frame to send 345 | * @param {Function} cb Callback 346 | * @private 347 | */ 348 | sendFrame(list, cb) { 349 | if (list.length === 2) { 350 | this._socket.cork(); 351 | this._socket.write(list[0]); 352 | this._socket.write(list[1], cb); 353 | this._socket.uncork(); 354 | } else { 355 | this._socket.write(list[0], cb); 356 | } 357 | } 358 | } 359 | 360 | module.exports = Sender; 361 | -------------------------------------------------------------------------------- /src/ws/stream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Duplex } = require('stream'); 4 | 5 | /** 6 | * Emits the `'close'` event on a stream. 7 | * 8 | * @param {stream.Duplex} The stream. 9 | * @private 10 | */ 11 | function emitClose(stream) { 12 | stream.emit('close'); 13 | } 14 | 15 | /** 16 | * The listener of the `'end'` event. 17 | * 18 | * @private 19 | */ 20 | function duplexOnEnd() { 21 | if (!this.destroyed && this._writableState.finished) { 22 | this.destroy(); 23 | } 24 | } 25 | 26 | /** 27 | * The listener of the `'error'` event. 28 | * 29 | * @private 30 | */ 31 | function duplexOnError(err) { 32 | this.removeListener('error', duplexOnError); 33 | this.destroy(); 34 | if (this.listenerCount('error') === 0) { 35 | // Do not suppress the throwing behavior. 36 | this.emit('error', err); 37 | } 38 | } 39 | 40 | /** 41 | * Wraps a `WebSocket` in a duplex stream. 42 | * 43 | * @param {WebSocket} ws The `WebSocket` to wrap 44 | * @param {Object} options The options for the `Duplex` constructor 45 | * @return {stream.Duplex} The duplex stream 46 | * @public 47 | */ 48 | function createWebSocketStream(ws, options) { 49 | let resumeOnReceiverDrain = true; 50 | 51 | function receiverOnDrain() { 52 | if (resumeOnReceiverDrain) ws._socket.resume(); 53 | } 54 | 55 | if (ws.readyState === ws.CONNECTING) { 56 | ws.once('open', function open() { 57 | ws._receiver.removeAllListeners('drain'); 58 | ws._receiver.on('drain', receiverOnDrain); 59 | }); 60 | } else { 61 | ws._receiver.removeAllListeners('drain'); 62 | ws._receiver.on('drain', receiverOnDrain); 63 | } 64 | 65 | const duplex = new Duplex({ 66 | ...options, 67 | autoDestroy: false, 68 | emitClose: false, 69 | objectMode: false, 70 | writableObjectMode: false 71 | }); 72 | 73 | ws.on('message', function message(msg) { 74 | if (!duplex.push(msg)) { 75 | resumeOnReceiverDrain = false; 76 | ws._socket.pause(); 77 | } 78 | }); 79 | 80 | ws.once('error', function error(err) { 81 | duplex.destroy(err); 82 | }); 83 | 84 | ws.once('close', function close() { 85 | if (duplex.destroyed) return; 86 | 87 | duplex.push(null); 88 | }); 89 | 90 | duplex._destroy = function(err, callback) { 91 | if (ws.readyState === ws.CLOSED) { 92 | callback(err); 93 | process.nextTick(emitClose, duplex); 94 | return; 95 | } 96 | 97 | ws.once('close', function close() { 98 | callback(err); 99 | process.nextTick(emitClose, duplex); 100 | }); 101 | ws.terminate(); 102 | }; 103 | 104 | duplex._final = function(callback) { 105 | if (ws.readyState === ws.CONNECTING) { 106 | ws.once('open', function open() { 107 | duplex._final(callback); 108 | }); 109 | return; 110 | } 111 | 112 | if (ws._socket._writableState.finished) { 113 | if (duplex._readableState.endEmitted) duplex.destroy(); 114 | callback(); 115 | } else { 116 | ws._socket.once('finish', function finish() { 117 | // `duplex` is not destroyed here because the `'end'` event will be 118 | // emitted on `duplex` after this `'finish'` event. The EOF signaling 119 | // `null` chunk is, in fact, pushed when the WebSocket emits `'close'`. 120 | callback(); 121 | }); 122 | ws.close(); 123 | } 124 | }; 125 | 126 | duplex._read = function() { 127 | if (ws.readyState === ws.OPEN && !resumeOnReceiverDrain) { 128 | resumeOnReceiverDrain = true; 129 | if (!ws._receiver._writableState.needDrain) ws._socket.resume(); 130 | } 131 | }; 132 | 133 | duplex._write = function(chunk, encoding, callback) { 134 | if (ws.readyState === ws.CONNECTING) { 135 | ws.once('open', function open() { 136 | duplex._write(chunk, encoding, callback); 137 | }); 138 | return; 139 | } 140 | 141 | ws.send(chunk, callback); 142 | }; 143 | 144 | duplex.on('end', duplexOnEnd); 145 | duplex.on('error', duplexOnError); 146 | return duplex; 147 | } 148 | 149 | module.exports = createWebSocketStream; 150 | -------------------------------------------------------------------------------- /src/ws/validation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // try { 4 | // const isValidUTF8 = require('utf-8-validate'); 5 | 6 | // exports.isValidUTF8 = 7 | // typeof isValidUTF8 === 'object' 8 | // ? isValidUTF8.Validation.isValidUTF8 // utf-8-validate@<3.0.0 9 | // : isValidUTF8; 10 | // } catch (e) /* istanbul ignore next */ { 11 | // exports.isValidUTF8 = () => true; 12 | // } 13 | 14 | export function isValidUTF8() { return true } 15 | 16 | /** 17 | * Checks if a status code is allowed in a close frame. 18 | * 19 | * @param {Number} code The status code 20 | * @return {Boolean} `true` if the status code is valid, else `false` 21 | * @public 22 | */ 23 | export function isValidStatusCode(code) { 24 | return ( 25 | (code >= 1000 && 26 | code <= 1013 && 27 | code !== 1004 && 28 | code !== 1005 && 29 | code !== 1006) || 30 | (code >= 3000 && code <= 4999) 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/ws/websocket-server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EventEmitter = require('events'); 4 | const { createHash } = require('crypto'); 5 | const { createServer, STATUS_CODES } = require('http'); 6 | 7 | import { PerMessageDeflate } from './permessage-deflate' 8 | import { WebSocket } from './websocket' 9 | import { format, parse } from './extension' 10 | import { GUID } from './constants' 11 | 12 | const keyRegex = /^[+/0-9A-Za-z]{22}==$/; 13 | const kUsedByWebSocketServer = Symbol('kUsedByWebSocketServer'); 14 | 15 | /** 16 | * Class representing a WebSocket server. 17 | * 18 | * @extends EventEmitter 19 | */ 20 | export class WebSocketServer extends EventEmitter { 21 | /** 22 | * Create a `WebSocketServer` instance. 23 | * 24 | * @param {Object} options Configuration options 25 | * @param {Number} options.backlog The maximum length of the queue of pending 26 | * connections 27 | * @param {Boolean} options.clientTracking Specifies whether or not to track 28 | * clients 29 | * @param {Function} options.handleProtocols A hook to handle protocols 30 | * @param {String} options.host The hostname where to bind the server 31 | * @param {Number} options.maxPayload The maximum allowed message size 32 | * @param {Boolean} options.noServer Enable no server mode 33 | * @param {String} options.path Accept only connections matching this path 34 | * @param {(Boolean|Object)} options.perMessageDeflate Enable/disable 35 | * permessage-deflate 36 | * @param {Number} options.port The port where to bind the server 37 | * @param {http.Server} options.server A pre-created HTTP/S server to use 38 | * @param {Function} options.verifyClient A hook to reject connections 39 | * @param {Function} callback A listener for the `listening` event 40 | */ 41 | constructor(options, callback) { 42 | super(); 43 | 44 | options = { 45 | maxPayload: 100 * 1024 * 1024, 46 | perMessageDeflate: false, 47 | handleProtocols: null, 48 | clientTracking: true, 49 | verifyClient: null, 50 | noServer: false, 51 | backlog: null, // use default (511 as implemented in net.js) 52 | server: null, 53 | host: null, 54 | path: null, 55 | port: null, 56 | ...options 57 | }; 58 | 59 | if (options.port == null && !options.server && !options.noServer) { 60 | throw new TypeError( 61 | 'One of the "port", "server", or "noServer" options must be specified' 62 | ); 63 | } 64 | 65 | if (options.port != null) { 66 | this._server = createServer((req, res) => { 67 | const body = STATUS_CODES[426]; 68 | 69 | res.writeHead(426, { 70 | 'Content-Length': body.length, 71 | 'Content-Type': 'text/plain' 72 | }); 73 | res.end(body); 74 | }); 75 | this._server.listen( 76 | options.port, 77 | options.host, 78 | options.backlog, 79 | callback 80 | ); 81 | } else if (options.server) { 82 | if (options.server[kUsedByWebSocketServer]) { 83 | throw new Error( 84 | 'The HTTP/S server is already being used by another WebSocket server' 85 | ); 86 | } 87 | 88 | options.server[kUsedByWebSocketServer] = true; 89 | this._server = options.server; 90 | } 91 | 92 | if (this._server) { 93 | this._removeListeners = addListeners(this._server, { 94 | listening: this.emit.bind(this, 'listening'), 95 | error: this.emit.bind(this, 'error'), 96 | upgrade: (req, socket, head) => { 97 | this.handleUpgrade(req, socket, head, (ws) => { 98 | this.emit('connection', ws, req); 99 | }); 100 | } 101 | }); 102 | } 103 | 104 | if (options.perMessageDeflate === true) options.perMessageDeflate = {}; 105 | if (options.clientTracking) this.clients = new Set(); 106 | this.options = options; 107 | } 108 | 109 | /** 110 | * Returns the bound address, the address family name, and port of the server 111 | * as reported by the operating system if listening on an IP socket. 112 | * If the server is listening on a pipe or UNIX domain socket, the name is 113 | * returned as a string. 114 | * 115 | * @return {(Object|String|null)} The address of the server 116 | * @public 117 | */ 118 | address() { 119 | if (this.options.noServer) { 120 | throw new Error('The server is operating in "noServer" mode'); 121 | } 122 | 123 | if (!this._server) return null; 124 | return this._server.address(); 125 | } 126 | 127 | /** 128 | * Close the server. 129 | * 130 | * @param {Function} cb Callback 131 | * @public 132 | */ 133 | close(cb) { 134 | if (cb) this.once('close', cb); 135 | 136 | // 137 | // Terminate all associated clients. 138 | // 139 | if (this.clients) { 140 | for (const client of this.clients) client.terminate(); 141 | } 142 | 143 | const server = this._server; 144 | 145 | if (server) { 146 | this._removeListeners(); 147 | this._removeListeners = this._server = null; 148 | 149 | // 150 | // Close the http server if it was internally created. 151 | // 152 | if (this.options.port != null) { 153 | server.close(() => this.emit('close')); 154 | return; 155 | } 156 | 157 | delete server[kUsedByWebSocketServer]; 158 | } 159 | 160 | process.nextTick(emitClose, this); 161 | } 162 | 163 | /** 164 | * See if a given request should be handled by this server instance. 165 | * 166 | * @param {http.IncomingMessage} req Request object to inspect 167 | * @return {Boolean} `true` if the request is valid, else `false` 168 | * @public 169 | */ 170 | shouldHandle(req) { 171 | if (this.options.path) { 172 | const index = req.url.indexOf('?'); 173 | const pathname = index !== -1 ? req.url.slice(0, index) : req.url; 174 | 175 | if (pathname !== this.options.path) return false; 176 | } 177 | 178 | return true; 179 | } 180 | 181 | /** 182 | * Handle a HTTP Upgrade request. 183 | * 184 | * @param {http.IncomingMessage} req The request object 185 | * @param {net.Socket} socket The network socket between the server and client 186 | * @param {Buffer} head The first packet of the upgraded stream 187 | * @param {Function} cb Callback 188 | * @public 189 | */ 190 | handleUpgrade(req, socket, head, cb) { 191 | socket.on('error', socketOnError); 192 | 193 | const key = 194 | req.headers['sec-websocket-key'] !== undefined 195 | ? req.headers['sec-websocket-key'].trim() 196 | : false; 197 | const version = +req.headers['sec-websocket-version']; 198 | const extensions = {}; 199 | 200 | if ( 201 | req.method !== 'GET' || 202 | req.headers.upgrade.toLowerCase() !== 'websocket' || 203 | !key || 204 | !keyRegex.test(key) || 205 | (version !== 8 && version !== 13) || 206 | !this.shouldHandle(req) 207 | ) { 208 | return abortHandshake(socket, 400); 209 | } 210 | 211 | if (this.options.perMessageDeflate) { 212 | const perMessageDeflate = new PerMessageDeflate( 213 | this.options.perMessageDeflate, 214 | true, 215 | this.options.maxPayload 216 | ); 217 | 218 | try { 219 | const offers = parse(req.headers['sec-websocket-extensions']); 220 | 221 | if (offers[PerMessageDeflate.extensionName]) { 222 | perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]); 223 | extensions[PerMessageDeflate.extensionName] = perMessageDeflate; 224 | } 225 | } catch (err) { 226 | return abortHandshake(socket, 400); 227 | } 228 | } 229 | 230 | // 231 | // Optionally call external client verification handler. 232 | // 233 | if (this.options.verifyClient) { 234 | const info = { 235 | origin: 236 | req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`], 237 | secure: !!(req.connection.authorized || req.connection.encrypted), 238 | req 239 | }; 240 | 241 | if (this.options.verifyClient.length === 2) { 242 | this.options.verifyClient(info, (verified, code, message, headers) => { 243 | if (!verified) { 244 | return abortHandshake(socket, code || 401, message, headers); 245 | } 246 | 247 | this.completeUpgrade(key, extensions, req, socket, head, cb); 248 | }); 249 | return; 250 | } 251 | 252 | if (!this.options.verifyClient(info)) return abortHandshake(socket, 401); 253 | } 254 | 255 | this.completeUpgrade(key, extensions, req, socket, head, cb); 256 | } 257 | 258 | /** 259 | * Upgrade the connection to WebSocket. 260 | * 261 | * @param {String} key The value of the `Sec-WebSocket-Key` header 262 | * @param {Object} extensions The accepted extensions 263 | * @param {http.IncomingMessage} req The request object 264 | * @param {net.Socket} socket The network socket between the server and client 265 | * @param {Buffer} head The first packet of the upgraded stream 266 | * @param {Function} cb Callback 267 | * @private 268 | */ 269 | completeUpgrade(key, extensions, req, socket, head, cb) { 270 | // 271 | // Destroy the socket if the client has already sent a FIN packet. 272 | // 273 | if (!socket.readable || !socket.writable) return socket.destroy(); 274 | 275 | const digest = createHash('sha1') 276 | .update(key + GUID) 277 | .digest('base64'); 278 | 279 | const headers = [ 280 | 'HTTP/1.1 101 Switching Protocols', 281 | 'Upgrade: websocket', 282 | 'Connection: Upgrade', 283 | `Sec-WebSocket-Accept: ${digest}` 284 | ]; 285 | 286 | const ws = new WebSocket(null); 287 | let protocol = req.headers['sec-websocket-protocol']; 288 | 289 | if (protocol) { 290 | protocol = protocol.trim().split(/ *, */); 291 | 292 | // 293 | // Optionally call external protocol selection handler. 294 | // 295 | if (this.options.handleProtocols) { 296 | protocol = this.options.handleProtocols(protocol, req); 297 | } else { 298 | protocol = protocol[0]; 299 | } 300 | 301 | if (protocol) { 302 | headers.push(`Sec-WebSocket-Protocol: ${protocol}`); 303 | ws.protocol = protocol; 304 | } 305 | } 306 | 307 | if (extensions[PerMessageDeflate.extensionName]) { 308 | const params = extensions[PerMessageDeflate.extensionName].params; 309 | const value = format({ 310 | [PerMessageDeflate.extensionName]: [params] 311 | }); 312 | headers.push(`Sec-WebSocket-Extensions: ${value}`); 313 | ws._extensions = extensions; 314 | } 315 | 316 | // 317 | // Allow external modification/inspection of handshake headers. 318 | // 319 | this.emit('headers', headers, req); 320 | 321 | socket.write(headers.concat('\r\n').join('\r\n')); 322 | socket.removeListener('error', socketOnError); 323 | 324 | ws.setSocket(socket, head, this.options.maxPayload); 325 | 326 | if (this.clients) { 327 | this.clients.add(ws); 328 | ws.on('close', () => this.clients.delete(ws)); 329 | } 330 | 331 | cb(ws); 332 | } 333 | } 334 | 335 | module.exports = WebSocketServer; 336 | 337 | /** 338 | * Add event listeners on an `EventEmitter` using a map of 339 | * pairs. 340 | * 341 | * @param {EventEmitter} server The event emitter 342 | * @param {Object.} map The listeners to add 343 | * @return {Function} A function that will remove the added listeners when called 344 | * @private 345 | */ 346 | function addListeners(server, map) { 347 | for (const event of Object.keys(map)) server.on(event, map[event]); 348 | 349 | return function removeListeners() { 350 | for (const event of Object.keys(map)) { 351 | server.removeListener(event, map[event]); 352 | } 353 | }; 354 | } 355 | 356 | /** 357 | * Emit a `'close'` event on an `EventEmitter`. 358 | * 359 | * @param {EventEmitter} server The event emitter 360 | * @private 361 | */ 362 | function emitClose(server) { 363 | server.emit('close'); 364 | } 365 | 366 | /** 367 | * Handle premature socket errors. 368 | * 369 | * @private 370 | */ 371 | function socketOnError() { 372 | this.destroy(); 373 | } 374 | 375 | /** 376 | * Close the connection when preconditions are not fulfilled. 377 | * 378 | * @param {net.Socket} socket The socket of the upgrade request 379 | * @param {Number} code The HTTP response status code 380 | * @param {String} [message] The HTTP response body 381 | * @param {Object} [headers] Additional HTTP response headers 382 | * @private 383 | */ 384 | function abortHandshake(socket, code, message, headers) { 385 | if (socket.writable) { 386 | message = message || STATUS_CODES[code]; 387 | headers = { 388 | Connection: 'close', 389 | 'Content-type': 'text/html', 390 | 'Content-Length': Buffer.byteLength(message), 391 | ...headers 392 | }; 393 | 394 | socket.write( 395 | `HTTP/1.1 ${code} ${STATUS_CODES[code]}\r\n` + 396 | Object.keys(headers) 397 | .map((h) => `${h}: ${headers[h]}`) 398 | .join('\r\n') + 399 | '\r\n\r\n' + 400 | message 401 | ); 402 | } 403 | 404 | socket.removeListener('error', socketOnError); 405 | socket.destroy(); 406 | } 407 | -------------------------------------------------------------------------------- /src/ws/websocket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EventEmitter = require('events'); 4 | const https = require('https'); 5 | const http = require('http'); 6 | const net = require('net'); 7 | const tls = require('tls'); 8 | const { randomBytes, createHash } = require('crypto'); 9 | const { URL } = require('url'); 10 | 11 | import {PerMessageDeflate} from './permessage-deflate' 12 | import {Receiver} from './receiver' 13 | import {Sender} from './sender' 14 | import { 15 | BINARY_TYPES, 16 | EMPTY_BUFFER, 17 | GUID, 18 | kStatusCode, 19 | kWebSocket, 20 | NOOP 21 | } from './constants' 22 | import { addEventListener, removeEventListener } from './event-target' 23 | import { format, parse } from './extension' 24 | import { toBuffer } from './buffer-util' 25 | 26 | const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']; 27 | const protocolVersions = [8, 13]; 28 | const closeTimeout = 30 * 1000; 29 | 30 | /** 31 | * Class representing a WebSocket. 32 | * 33 | * @extends EventEmitter 34 | */ 35 | export class WebSocket extends EventEmitter { 36 | /** 37 | * Create a new `WebSocket`. 38 | * 39 | * @param {(String|url.URL)} address The URL to which to connect 40 | * @param {(String|String[])} protocols The subprotocols 41 | * @param {Object} options Connection options 42 | */ 43 | constructor(address, protocols, options) { 44 | super(); 45 | 46 | this.readyState = WebSocket.CONNECTING; 47 | this.protocol = ''; 48 | 49 | this._binaryType = BINARY_TYPES[0]; 50 | this._closeFrameReceived = false; 51 | this._closeFrameSent = false; 52 | this._closeMessage = ''; 53 | this._closeTimer = null; 54 | this._closeCode = 1006; 55 | this._extensions = {}; 56 | this._receiver = null; 57 | this._sender = null; 58 | this._socket = null; 59 | 60 | if (address !== null) { 61 | this._bufferedAmount = 0; 62 | this._isServer = false; 63 | this._redirects = 0; 64 | 65 | if (Array.isArray(protocols)) { 66 | protocols = protocols.join(', '); 67 | } else if (typeof protocols === 'object' && protocols !== null) { 68 | options = protocols; 69 | protocols = undefined; 70 | } 71 | 72 | initAsClient(this, address, protocols, options); 73 | } else { 74 | this._isServer = true; 75 | } 76 | } 77 | 78 | get CONNECTING() { 79 | return WebSocket.CONNECTING; 80 | } 81 | get CLOSING() { 82 | return WebSocket.CLOSING; 83 | } 84 | get CLOSED() { 85 | return WebSocket.CLOSED; 86 | } 87 | get OPEN() { 88 | return WebSocket.OPEN; 89 | } 90 | 91 | /** 92 | * This deviates from the WHATWG interface since ws doesn't support the 93 | * required default "blob" type (instead we define a custom "nodebuffer" 94 | * type). 95 | * 96 | * @type {String} 97 | */ 98 | get binaryType() { 99 | return this._binaryType; 100 | } 101 | 102 | set binaryType(type) { 103 | if (!BINARY_TYPES.includes(type)) return; 104 | 105 | this._binaryType = type; 106 | 107 | // 108 | // Allow to change `binaryType` on the fly. 109 | // 110 | if (this._receiver) this._receiver._binaryType = type; 111 | } 112 | 113 | /** 114 | * @type {Number} 115 | */ 116 | get bufferedAmount() { 117 | if (!this._socket) return this._bufferedAmount; 118 | 119 | // 120 | // `socket.bufferSize` is `undefined` if the socket is closed. 121 | // 122 | return (this._socket.bufferSize || 0) + this._sender._bufferedBytes; 123 | } 124 | 125 | /** 126 | * @type {String} 127 | */ 128 | get extensions() { 129 | return Object.keys(this._extensions).join(); 130 | } 131 | 132 | /** 133 | * Set up the socket and the internal resources. 134 | * 135 | * @param {net.Socket} socket The network socket between the server and client 136 | * @param {Buffer} head The first packet of the upgraded stream 137 | * @param {Number} maxPayload The maximum allowed message size 138 | * @private 139 | */ 140 | setSocket(socket, head, maxPayload) { 141 | const receiver = new Receiver( 142 | this._binaryType, 143 | this._extensions, 144 | maxPayload 145 | ); 146 | 147 | this._sender = new Sender(socket, this._extensions); 148 | this._receiver = receiver; 149 | this._socket = socket; 150 | 151 | receiver[kWebSocket] = this; 152 | socket[kWebSocket] = this; 153 | 154 | receiver.on('conclude', receiverOnConclude); 155 | receiver.on('drain', receiverOnDrain); 156 | receiver.on('error', receiverOnError); 157 | receiver.on('message', receiverOnMessage); 158 | receiver.on('ping', receiverOnPing); 159 | receiver.on('pong', receiverOnPong); 160 | 161 | socket.setTimeout(0); 162 | socket.setNoDelay(); 163 | 164 | if (head.length > 0) socket.unshift(head); 165 | 166 | socket.on('close', socketOnClose); 167 | socket.on('data', socketOnData); 168 | socket.on('end', socketOnEnd); 169 | socket.on('error', socketOnError); 170 | 171 | this.readyState = WebSocket.OPEN; 172 | this.emit('open'); 173 | } 174 | 175 | /** 176 | * Emit the `'close'` event. 177 | * 178 | * @private 179 | */ 180 | emitClose() { 181 | this.readyState = WebSocket.CLOSED; 182 | 183 | if (!this._socket) { 184 | this.emit('close', this._closeCode, this._closeMessage); 185 | return; 186 | } 187 | 188 | if (this._extensions[PerMessageDeflate.extensionName]) { 189 | this._extensions[PerMessageDeflate.extensionName].cleanup(); 190 | } 191 | 192 | this._receiver.removeAllListeners(); 193 | this.emit('close', this._closeCode, this._closeMessage); 194 | } 195 | 196 | /** 197 | * Start a closing handshake. 198 | * 199 | * +----------+ +-----------+ +----------+ 200 | * - - -|ws.close()|-->|close frame|-->|ws.close()|- - - 201 | * | +----------+ +-----------+ +----------+ | 202 | * +----------+ +-----------+ | 203 | * CLOSING |ws.close()|<--|close frame|<--+-----+ CLOSING 204 | * +----------+ +-----------+ | 205 | * | | | +---+ | 206 | * +------------------------+-->|fin| - - - - 207 | * | +---+ | +---+ 208 | * - - - - -|fin|<---------------------+ 209 | * +---+ 210 | * 211 | * @param {Number} code Status code explaining why the connection is closing 212 | * @param {String} data A string explaining why the connection is closing 213 | * @public 214 | */ 215 | close(code, data) { 216 | if (this.readyState === WebSocket.CLOSED) return; 217 | if (this.readyState === WebSocket.CONNECTING) { 218 | const msg = 'WebSocket was closed before the connection was established'; 219 | return abortHandshake(this, this._req, msg); 220 | } 221 | 222 | if (this.readyState === WebSocket.CLOSING) { 223 | if (this._closeFrameSent && this._closeFrameReceived) this._socket.end(); 224 | return; 225 | } 226 | 227 | this.readyState = WebSocket.CLOSING; 228 | this._sender.close(code, data, !this._isServer, (err) => { 229 | // 230 | // This error is handled by the `'error'` listener on the socket. We only 231 | // want to know if the close frame has been sent here. 232 | // 233 | if (err) return; 234 | 235 | this._closeFrameSent = true; 236 | if (this._closeFrameReceived) this._socket.end(); 237 | }); 238 | 239 | // 240 | // Specify a timeout for the closing handshake to complete. 241 | // 242 | this._closeTimer = setTimeout( 243 | this._socket.destroy.bind(this._socket), 244 | closeTimeout 245 | ); 246 | } 247 | 248 | /** 249 | * Send a ping. 250 | * 251 | * @param {*} data The data to send 252 | * @param {Boolean} mask Indicates whether or not to mask `data` 253 | * @param {Function} cb Callback which is executed when the ping is sent 254 | * @public 255 | */ 256 | ping(data, mask, cb) { 257 | if (this.readyState === WebSocket.CONNECTING) { 258 | throw new Error('WebSocket is not open: readyState 0 (CONNECTING)'); 259 | } 260 | 261 | if (typeof data === 'function') { 262 | cb = data; 263 | data = mask = undefined; 264 | } else if (typeof mask === 'function') { 265 | cb = mask; 266 | mask = undefined; 267 | } 268 | 269 | if (typeof data === 'number') data = data.toString(); 270 | 271 | if (this.readyState !== WebSocket.OPEN) { 272 | sendAfterClose(this, data, cb); 273 | return; 274 | } 275 | 276 | if (mask === undefined) mask = !this._isServer; 277 | this._sender.ping(data || EMPTY_BUFFER, mask, cb); 278 | } 279 | 280 | /** 281 | * Send a pong. 282 | * 283 | * @param {*} data The data to send 284 | * @param {Boolean} mask Indicates whether or not to mask `data` 285 | * @param {Function} cb Callback which is executed when the pong is sent 286 | * @public 287 | */ 288 | pong(data, mask, cb) { 289 | if (this.readyState === WebSocket.CONNECTING) { 290 | throw new Error('WebSocket is not open: readyState 0 (CONNECTING)'); 291 | } 292 | 293 | if (typeof data === 'function') { 294 | cb = data; 295 | data = mask = undefined; 296 | } else if (typeof mask === 'function') { 297 | cb = mask; 298 | mask = undefined; 299 | } 300 | 301 | if (typeof data === 'number') data = data.toString(); 302 | 303 | if (this.readyState !== WebSocket.OPEN) { 304 | sendAfterClose(this, data, cb); 305 | return; 306 | } 307 | 308 | if (mask === undefined) mask = !this._isServer; 309 | this._sender.pong(data || EMPTY_BUFFER, mask, cb); 310 | } 311 | 312 | /** 313 | * Send a data message. 314 | * 315 | * @param {*} data The message to send 316 | * @param {Object} options Options object 317 | * @param {Boolean} options.compress Specifies whether or not to compress 318 | * `data` 319 | * @param {Boolean} options.binary Specifies whether `data` is binary or text 320 | * @param {Boolean} options.fin Specifies whether the fragment is the last one 321 | * @param {Boolean} options.mask Specifies whether or not to mask `data` 322 | * @param {Function} cb Callback which is executed when data is written out 323 | * @public 324 | */ 325 | send(data, options, cb) { 326 | if (this.readyState === WebSocket.CONNECTING) { 327 | throw new Error('WebSocket is not open: readyState 0 (CONNECTING)'); 328 | } 329 | 330 | if (typeof options === 'function') { 331 | cb = options; 332 | options = {}; 333 | } 334 | 335 | if (typeof data === 'number') data = data.toString(); 336 | 337 | if (this.readyState !== WebSocket.OPEN) { 338 | sendAfterClose(this, data, cb); 339 | return; 340 | } 341 | 342 | const opts = { 343 | binary: typeof data !== 'string', 344 | mask: !this._isServer, 345 | compress: true, 346 | fin: true, 347 | ...options 348 | }; 349 | 350 | if (!this._extensions[PerMessageDeflate.extensionName]) { 351 | opts.compress = false; 352 | } 353 | 354 | this._sender.send(data || EMPTY_BUFFER, opts, cb); 355 | } 356 | 357 | /** 358 | * Forcibly close the connection. 359 | * 360 | * @public 361 | */ 362 | terminate() { 363 | if (this.readyState === WebSocket.CLOSED) return; 364 | if (this.readyState === WebSocket.CONNECTING) { 365 | const msg = 'WebSocket was closed before the connection was established'; 366 | return abortHandshake(this, this._req, msg); 367 | } 368 | 369 | if (this._socket) { 370 | this.readyState = WebSocket.CLOSING; 371 | this._socket.destroy(); 372 | } 373 | } 374 | } 375 | 376 | readyStates.forEach((readyState, i) => { 377 | WebSocket[readyState] = i; 378 | }); 379 | 380 | // 381 | // Add the `onopen`, `onerror`, `onclose`, and `onmessage` attributes. 382 | // See https://html.spec.whatwg.org/multipage/comms.html#the-websocket-interface 383 | // 384 | ['open', 'error', 'close', 'message'].forEach((method) => { 385 | Object.defineProperty(WebSocket.prototype, `on${method}`, { 386 | /** 387 | * Return the listener of the event. 388 | * 389 | * @return {(Function|undefined)} The event listener or `undefined` 390 | * @public 391 | */ 392 | get() { 393 | const listeners = this.listeners(method); 394 | for (let i = 0; i < listeners.length; i++) { 395 | if (listeners[i]._listener) return listeners[i]._listener; 396 | } 397 | 398 | return undefined; 399 | }, 400 | /** 401 | * Add a listener for the event. 402 | * 403 | * @param {Function} listener The listener to add 404 | * @public 405 | */ 406 | set(listener) { 407 | const listeners = this.listeners(method); 408 | for (let i = 0; i < listeners.length; i++) { 409 | // 410 | // Remove only the listeners added via `addEventListener`. 411 | // 412 | if (listeners[i]._listener) this.removeListener(method, listeners[i]); 413 | } 414 | this.addEventListener(method, listener); 415 | } 416 | }); 417 | }); 418 | 419 | WebSocket.prototype.addEventListener = addEventListener; 420 | WebSocket.prototype.removeEventListener = removeEventListener; 421 | 422 | module.exports = WebSocket; 423 | 424 | /** 425 | * Initialize a WebSocket client. 426 | * 427 | * @param {WebSocket} websocket The client to initialize 428 | * @param {(String|url.URL)} address The URL to which to connect 429 | * @param {String} protocols The subprotocols 430 | * @param {Object} options Connection options 431 | * @param {(Boolean|Object)} options.perMessageDeflate Enable/disable 432 | * permessage-deflate 433 | * @param {Number} options.handshakeTimeout Timeout in milliseconds for the 434 | * handshake request 435 | * @param {Number} options.protocolVersion Value of the `Sec-WebSocket-Version` 436 | * header 437 | * @param {String} options.origin Value of the `Origin` or 438 | * `Sec-WebSocket-Origin` header 439 | * @param {Number} options.maxPayload The maximum allowed message size 440 | * @param {Boolean} options.followRedirects Whether or not to follow redirects 441 | * @param {Number} options.maxRedirects The maximum number of redirects allowed 442 | * @private 443 | */ 444 | function initAsClient(websocket, address, protocols, options) { 445 | const opts = { 446 | protocolVersion: protocolVersions[1], 447 | maxPayload: 100 * 1024 * 1024, 448 | perMessageDeflate: true, 449 | followRedirects: false, 450 | maxRedirects: 10, 451 | ...options, 452 | createConnection: undefined, 453 | socketPath: undefined, 454 | hostname: undefined, 455 | protocol: undefined, 456 | timeout: undefined, 457 | method: undefined, 458 | auth: undefined, 459 | host: undefined, 460 | path: undefined, 461 | port: undefined 462 | }; 463 | 464 | if (!protocolVersions.includes(opts.protocolVersion)) { 465 | throw new RangeError( 466 | `Unsupported protocol version: ${opts.protocolVersion} ` + 467 | `(supported versions: ${protocolVersions.join(', ')})` 468 | ); 469 | } 470 | 471 | let parsedUrl; 472 | 473 | if (address instanceof URL) { 474 | parsedUrl = address; 475 | websocket.url = address.href; 476 | } else { 477 | parsedUrl = new URL(address); 478 | websocket.url = address; 479 | } 480 | 481 | const isUnixSocket = parsedUrl.protocol === 'ws+unix:'; 482 | 483 | if (!parsedUrl.host && (!isUnixSocket || !parsedUrl.pathname)) { 484 | throw new Error(`Invalid URL: ${websocket.url}`); 485 | } 486 | 487 | const isSecure = 488 | parsedUrl.protocol === 'wss:' || parsedUrl.protocol === 'https:'; 489 | const defaultPort = isSecure ? 443 : 80; 490 | const key = randomBytes(16).toString('base64'); 491 | const get = isSecure ? https.get : http.get; 492 | let perMessageDeflate; 493 | 494 | opts.createConnection = isSecure ? tlsConnect : netConnect; 495 | opts.defaultPort = opts.defaultPort || defaultPort; 496 | opts.port = parsedUrl.port || defaultPort; 497 | opts.host = parsedUrl.hostname.startsWith('[') 498 | ? parsedUrl.hostname.slice(1, -1) 499 | : parsedUrl.hostname; 500 | opts.headers = { 501 | 'Sec-WebSocket-Version': opts.protocolVersion, 502 | 'Sec-WebSocket-Key': key, 503 | Connection: 'Upgrade', 504 | Upgrade: 'websocket', 505 | ...opts.headers 506 | }; 507 | opts.path = parsedUrl.pathname + parsedUrl.search; 508 | opts.timeout = opts.handshakeTimeout; 509 | 510 | if (opts.perMessageDeflate) { 511 | perMessageDeflate = new PerMessageDeflate( 512 | opts.perMessageDeflate !== true ? opts.perMessageDeflate : {}, 513 | false, 514 | opts.maxPayload 515 | ); 516 | opts.headers['Sec-WebSocket-Extensions'] = format({ 517 | [PerMessageDeflate.extensionName]: perMessageDeflate.offer() 518 | }); 519 | } 520 | if (protocols) { 521 | opts.headers['Sec-WebSocket-Protocol'] = protocols; 522 | } 523 | if (opts.origin) { 524 | if (opts.protocolVersion < 13) { 525 | opts.headers['Sec-WebSocket-Origin'] = opts.origin; 526 | } else { 527 | opts.headers.Origin = opts.origin; 528 | } 529 | } 530 | if (parsedUrl.username || parsedUrl.password) { 531 | opts.auth = `${parsedUrl.username}:${parsedUrl.password}`; 532 | } 533 | 534 | if (isUnixSocket) { 535 | const parts = opts.path.split(':'); 536 | 537 | opts.socketPath = parts[0]; 538 | opts.path = parts[1]; 539 | } 540 | 541 | let req = (websocket._req = get(opts)); 542 | 543 | if (opts.timeout) { 544 | req.on('timeout', () => { 545 | abortHandshake(websocket, req, 'Opening handshake has timed out'); 546 | }); 547 | } 548 | 549 | req.on('error', (err) => { 550 | if (websocket._req.aborted) return; 551 | 552 | req = websocket._req = null; 553 | websocket.readyState = WebSocket.CLOSING; 554 | websocket.emit('error', err); 555 | websocket.emitClose(); 556 | }); 557 | 558 | req.on('response', (res) => { 559 | const location = res.headers.location; 560 | const statusCode = res.statusCode; 561 | 562 | if ( 563 | location && 564 | opts.followRedirects && 565 | statusCode >= 300 && 566 | statusCode < 400 567 | ) { 568 | if (++websocket._redirects > opts.maxRedirects) { 569 | abortHandshake(websocket, req, 'Maximum redirects exceeded'); 570 | return; 571 | } 572 | 573 | req.abort(); 574 | 575 | const addr = new URL(location, address); 576 | 577 | initAsClient(websocket, addr, protocols, options); 578 | } else if (!websocket.emit('unexpected-response', req, res)) { 579 | abortHandshake( 580 | websocket, 581 | req, 582 | `Unexpected server response: ${res.statusCode}` 583 | ); 584 | } 585 | }); 586 | 587 | req.on('upgrade', (res, socket, head) => { 588 | websocket.emit('upgrade', res); 589 | 590 | // 591 | // The user may have closed the connection from a listener of the `upgrade` 592 | // event. 593 | // 594 | if (websocket.readyState !== WebSocket.CONNECTING) return; 595 | 596 | req = websocket._req = null; 597 | 598 | const digest = createHash('sha1') 599 | .update(key + GUID) 600 | .digest('base64'); 601 | 602 | if (res.headers['sec-websocket-accept'] !== digest) { 603 | abortHandshake(websocket, socket, 'Invalid Sec-WebSocket-Accept header'); 604 | return; 605 | } 606 | 607 | const serverProt = res.headers['sec-websocket-protocol']; 608 | const protList = (protocols || '').split(/, */); 609 | let protError; 610 | 611 | if (!protocols && serverProt) { 612 | protError = 'Server sent a subprotocol but none was requested'; 613 | } else if (protocols && !serverProt) { 614 | protError = 'Server sent no subprotocol'; 615 | } else if (serverProt && !protList.includes(serverProt)) { 616 | protError = 'Server sent an invalid subprotocol'; 617 | } 618 | 619 | if (protError) { 620 | abortHandshake(websocket, socket, protError); 621 | return; 622 | } 623 | 624 | if (serverProt) websocket.protocol = serverProt; 625 | 626 | if (perMessageDeflate) { 627 | try { 628 | const extensions = parse(res.headers['sec-websocket-extensions']); 629 | 630 | if (extensions[PerMessageDeflate.extensionName]) { 631 | perMessageDeflate.accept(extensions[PerMessageDeflate.extensionName]); 632 | websocket._extensions[ 633 | PerMessageDeflate.extensionName 634 | ] = perMessageDeflate; 635 | } 636 | } catch (err) { 637 | abortHandshake( 638 | websocket, 639 | socket, 640 | 'Invalid Sec-WebSocket-Extensions header' 641 | ); 642 | return; 643 | } 644 | } 645 | 646 | websocket.setSocket(socket, head, opts.maxPayload); 647 | }); 648 | } 649 | 650 | /** 651 | * Create a `net.Socket` and initiate a connection. 652 | * 653 | * @param {Object} options Connection options 654 | * @return {net.Socket} The newly created socket used to start the connection 655 | * @private 656 | */ 657 | function netConnect(options) { 658 | options.path = options.socketPath; 659 | return net.connect(options); 660 | } 661 | 662 | /** 663 | * Create a `tls.TLSSocket` and initiate a connection. 664 | * 665 | * @param {Object} options Connection options 666 | * @return {tls.TLSSocket} The newly created socket used to start the connection 667 | * @private 668 | */ 669 | function tlsConnect(options) { 670 | options.path = undefined; 671 | 672 | if (!options.servername && options.servername !== '') { 673 | options.servername = options.host; 674 | } 675 | 676 | return tls.connect(options); 677 | } 678 | 679 | /** 680 | * Abort the handshake and emit an error. 681 | * 682 | * @param {WebSocket} websocket The WebSocket instance 683 | * @param {(http.ClientRequest|net.Socket)} stream The request to abort or the 684 | * socket to destroy 685 | * @param {String} message The error message 686 | * @private 687 | */ 688 | function abortHandshake(websocket, stream, message) { 689 | websocket.readyState = WebSocket.CLOSING; 690 | 691 | const err = new Error(message); 692 | Error.captureStackTrace(err, abortHandshake); 693 | 694 | if (stream.setHeader) { 695 | stream.abort(); 696 | stream.once('abort', websocket.emitClose.bind(websocket)); 697 | websocket.emit('error', err); 698 | } else { 699 | stream.destroy(err); 700 | stream.once('error', websocket.emit.bind(websocket, 'error')); 701 | stream.once('close', websocket.emitClose.bind(websocket)); 702 | } 703 | } 704 | 705 | /** 706 | * Handle cases where the `ping()`, `pong()`, or `send()` methods are called 707 | * when the `readyState` attribute is `CLOSING` or `CLOSED`. 708 | * 709 | * @param {WebSocket} websocket The WebSocket instance 710 | * @param {*} data The data to send 711 | * @param {Function} cb Callback 712 | * @private 713 | */ 714 | function sendAfterClose(websocket, data, cb) { 715 | if (data) { 716 | const length = toBuffer(data).length; 717 | 718 | // 719 | // The `_bufferedAmount` property is used only when the peer is a client and 720 | // the opening handshake fails. Under these circumstances, in fact, the 721 | // `setSocket()` method is not called, so the `_socket` and `_sender` 722 | // properties are set to `null`. 723 | // 724 | if (websocket._socket) websocket._sender._bufferedBytes += length; 725 | else websocket._bufferedAmount += length; 726 | } 727 | 728 | if (cb) { 729 | const err = new Error( 730 | `WebSocket is not open: readyState ${websocket.readyState} ` + 731 | `(${readyStates[websocket.readyState]})` 732 | ); 733 | cb(err); 734 | } 735 | } 736 | 737 | /** 738 | * The listener of the `Receiver` `'conclude'` event. 739 | * 740 | * @param {Number} code The status code 741 | * @param {String} reason The reason for closing 742 | * @private 743 | */ 744 | function receiverOnConclude(code, reason) { 745 | const websocket = this[kWebSocket]; 746 | 747 | websocket._socket.removeListener('data', socketOnData); 748 | websocket._socket.resume(); 749 | 750 | websocket._closeFrameReceived = true; 751 | websocket._closeMessage = reason; 752 | websocket._closeCode = code; 753 | 754 | if (code === 1005) websocket.close(); 755 | else websocket.close(code, reason); 756 | } 757 | 758 | /** 759 | * The listener of the `Receiver` `'drain'` event. 760 | * 761 | * @private 762 | */ 763 | function receiverOnDrain() { 764 | this[kWebSocket]._socket.resume(); 765 | } 766 | 767 | /** 768 | * The listener of the `Receiver` `'error'` event. 769 | * 770 | * @param {(RangeError|Error)} err The emitted error 771 | * @private 772 | */ 773 | function receiverOnError(err) { 774 | const websocket = this[kWebSocket]; 775 | 776 | websocket._socket.removeListener('data', socketOnData); 777 | 778 | websocket.readyState = WebSocket.CLOSING; 779 | websocket._closeCode = err[kStatusCode]; 780 | websocket.emit('error', err); 781 | websocket._socket.destroy(); 782 | } 783 | 784 | /** 785 | * The listener of the `Receiver` `'finish'` event. 786 | * 787 | * @private 788 | */ 789 | function receiverOnFinish() { 790 | this[kWebSocket].emitClose(); 791 | } 792 | 793 | /** 794 | * The listener of the `Receiver` `'message'` event. 795 | * 796 | * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The message 797 | * @private 798 | */ 799 | function receiverOnMessage(data) { 800 | this[kWebSocket].emit('message', data); 801 | } 802 | 803 | /** 804 | * The listener of the `Receiver` `'ping'` event. 805 | * 806 | * @param {Buffer} data The data included in the ping frame 807 | * @private 808 | */ 809 | function receiverOnPing(data) { 810 | const websocket = this[kWebSocket]; 811 | 812 | websocket.pong(data, !websocket._isServer, NOOP); 813 | websocket.emit('ping', data); 814 | } 815 | 816 | /** 817 | * The listener of the `Receiver` `'pong'` event. 818 | * 819 | * @param {Buffer} data The data included in the pong frame 820 | * @private 821 | */ 822 | function receiverOnPong(data) { 823 | this[kWebSocket].emit('pong', data); 824 | } 825 | 826 | /** 827 | * The listener of the `net.Socket` `'close'` event. 828 | * 829 | * @private 830 | */ 831 | function socketOnClose() { 832 | const websocket = this[kWebSocket]; 833 | 834 | this.removeListener('close', socketOnClose); 835 | this.removeListener('end', socketOnEnd); 836 | 837 | websocket.readyState = WebSocket.CLOSING; 838 | 839 | // 840 | // The close frame might not have been received or the `'end'` event emitted, 841 | // for example, if the socket was destroyed due to an error. Ensure that the 842 | // `receiver` stream is closed after writing any remaining buffered data to 843 | // it. If the readable side of the socket is in flowing mode then there is no 844 | // buffered data as everything has been already written and `readable.read()` 845 | // will return `null`. If instead, the socket is paused, any possible buffered 846 | // data will be read as a single chunk and emitted synchronously in a single 847 | // `'data'` event. 848 | // 849 | websocket._socket.read(); 850 | websocket._receiver.end(); 851 | 852 | this.removeListener('data', socketOnData); 853 | this[kWebSocket] = undefined; 854 | 855 | clearTimeout(websocket._closeTimer); 856 | 857 | if ( 858 | websocket._receiver._writableState.finished || 859 | websocket._receiver._writableState.errorEmitted 860 | ) { 861 | websocket.emitClose(); 862 | } else { 863 | websocket._receiver.on('error', receiverOnFinish); 864 | websocket._receiver.on('finish', receiverOnFinish); 865 | } 866 | } 867 | 868 | /** 869 | * The listener of the `net.Socket` `'data'` event. 870 | * 871 | * @param {Buffer} chunk A chunk of data 872 | * @private 873 | */ 874 | function socketOnData(chunk) { 875 | if (!this[kWebSocket]._receiver.write(chunk)) { 876 | this.pause(); 877 | } 878 | } 879 | 880 | /** 881 | * The listener of the `net.Socket` `'end'` event. 882 | * 883 | * @private 884 | */ 885 | function socketOnEnd() { 886 | const websocket = this[kWebSocket]; 887 | 888 | websocket.readyState = WebSocket.CLOSING; 889 | websocket._receiver.end(); 890 | this.end(); 891 | } 892 | 893 | /** 894 | * The listener of the `net.Socket` `'error'` event. 895 | * 896 | * @private 897 | */ 898 | function socketOnError() { 899 | const websocket = this[kWebSocket]; 900 | 901 | this.removeListener('error', socketOnError); 902 | this.on('error', NOOP); 903 | 904 | if (websocket) { 905 | websocket.readyState = WebSocket.CLOSING; 906 | this.destroy(); 907 | } 908 | } 909 | --------------------------------------------------------------------------------