├── .gitignore ├── README.md ├── package-lock.json ├── package.json └── src ├── index.html └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebSocket Server from Scratch 2 | 3 | This is a WebSocket server implemented from scratch in Node.js without using any third-party libraries. The server accepts messages from clients and broadcasts them to all other connected clients. 4 | 5 | ### How to run 6 | 7 | 1. Clone this repository 8 | 9 | 2. Start the server 10 | 11 | ```bash 12 | npm start 13 | ``` 14 | 15 | 3. Open your browser & go to `localhost:3000` 16 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ws-from-scratch", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "ws-from-scratch", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "nodemon": "^3.1.3" 13 | } 14 | }, 15 | "node_modules/anymatch": { 16 | "version": "3.1.3", 17 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 18 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 19 | "dev": true, 20 | "dependencies": { 21 | "normalize-path": "^3.0.0", 22 | "picomatch": "^2.0.4" 23 | }, 24 | "engines": { 25 | "node": ">= 8" 26 | } 27 | }, 28 | "node_modules/balanced-match": { 29 | "version": "1.0.2", 30 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 31 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 32 | "dev": true 33 | }, 34 | "node_modules/binary-extensions": { 35 | "version": "2.3.0", 36 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", 37 | "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", 38 | "dev": true, 39 | "engines": { 40 | "node": ">=8" 41 | }, 42 | "funding": { 43 | "url": "https://github.com/sponsors/sindresorhus" 44 | } 45 | }, 46 | "node_modules/brace-expansion": { 47 | "version": "1.1.11", 48 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 49 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 50 | "dev": true, 51 | "dependencies": { 52 | "balanced-match": "^1.0.0", 53 | "concat-map": "0.0.1" 54 | } 55 | }, 56 | "node_modules/braces": { 57 | "version": "3.0.3", 58 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", 59 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", 60 | "dev": true, 61 | "dependencies": { 62 | "fill-range": "^7.1.1" 63 | }, 64 | "engines": { 65 | "node": ">=8" 66 | } 67 | }, 68 | "node_modules/chokidar": { 69 | "version": "3.6.0", 70 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", 71 | "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", 72 | "dev": true, 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 | "funding": { 86 | "url": "https://paulmillr.com/funding/" 87 | }, 88 | "optionalDependencies": { 89 | "fsevents": "~2.3.2" 90 | } 91 | }, 92 | "node_modules/concat-map": { 93 | "version": "0.0.1", 94 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 95 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 96 | "dev": true 97 | }, 98 | "node_modules/debug": { 99 | "version": "4.3.5", 100 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", 101 | "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", 102 | "dev": true, 103 | "dependencies": { 104 | "ms": "2.1.2" 105 | }, 106 | "engines": { 107 | "node": ">=6.0" 108 | }, 109 | "peerDependenciesMeta": { 110 | "supports-color": { 111 | "optional": true 112 | } 113 | } 114 | }, 115 | "node_modules/fill-range": { 116 | "version": "7.1.1", 117 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", 118 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", 119 | "dev": true, 120 | "dependencies": { 121 | "to-regex-range": "^5.0.1" 122 | }, 123 | "engines": { 124 | "node": ">=8" 125 | } 126 | }, 127 | "node_modules/fsevents": { 128 | "version": "2.3.3", 129 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 130 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 131 | "dev": true, 132 | "hasInstallScript": true, 133 | "optional": true, 134 | "os": [ 135 | "darwin" 136 | ], 137 | "engines": { 138 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 139 | } 140 | }, 141 | "node_modules/glob-parent": { 142 | "version": "5.1.2", 143 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 144 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 145 | "dev": true, 146 | "dependencies": { 147 | "is-glob": "^4.0.1" 148 | }, 149 | "engines": { 150 | "node": ">= 6" 151 | } 152 | }, 153 | "node_modules/has-flag": { 154 | "version": "3.0.0", 155 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 156 | "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", 157 | "dev": true, 158 | "engines": { 159 | "node": ">=4" 160 | } 161 | }, 162 | "node_modules/ignore-by-default": { 163 | "version": "1.0.1", 164 | "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", 165 | "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", 166 | "dev": true 167 | }, 168 | "node_modules/is-binary-path": { 169 | "version": "2.1.0", 170 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 171 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 172 | "dev": true, 173 | "dependencies": { 174 | "binary-extensions": "^2.0.0" 175 | }, 176 | "engines": { 177 | "node": ">=8" 178 | } 179 | }, 180 | "node_modules/is-extglob": { 181 | "version": "2.1.1", 182 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 183 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 184 | "dev": true, 185 | "engines": { 186 | "node": ">=0.10.0" 187 | } 188 | }, 189 | "node_modules/is-glob": { 190 | "version": "4.0.3", 191 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 192 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 193 | "dev": true, 194 | "dependencies": { 195 | "is-extglob": "^2.1.1" 196 | }, 197 | "engines": { 198 | "node": ">=0.10.0" 199 | } 200 | }, 201 | "node_modules/is-number": { 202 | "version": "7.0.0", 203 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 204 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 205 | "dev": true, 206 | "engines": { 207 | "node": ">=0.12.0" 208 | } 209 | }, 210 | "node_modules/minimatch": { 211 | "version": "3.1.2", 212 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 213 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 214 | "dev": true, 215 | "dependencies": { 216 | "brace-expansion": "^1.1.7" 217 | }, 218 | "engines": { 219 | "node": "*" 220 | } 221 | }, 222 | "node_modules/ms": { 223 | "version": "2.1.2", 224 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 225 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 226 | "dev": true 227 | }, 228 | "node_modules/nodemon": { 229 | "version": "3.1.3", 230 | "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.3.tgz", 231 | "integrity": "sha512-m4Vqs+APdKzDFpuaL9F9EVOF85+h070FnkHVEoU4+rmT6Vw0bmNl7s61VEkY/cJkL7RCv1p4urnUDUMrS5rk2w==", 232 | "dev": true, 233 | "dependencies": { 234 | "chokidar": "^3.5.2", 235 | "debug": "^4", 236 | "ignore-by-default": "^1.0.1", 237 | "minimatch": "^3.1.2", 238 | "pstree.remy": "^1.1.8", 239 | "semver": "^7.5.3", 240 | "simple-update-notifier": "^2.0.0", 241 | "supports-color": "^5.5.0", 242 | "touch": "^3.1.0", 243 | "undefsafe": "^2.0.5" 244 | }, 245 | "bin": { 246 | "nodemon": "bin/nodemon.js" 247 | }, 248 | "engines": { 249 | "node": ">=10" 250 | }, 251 | "funding": { 252 | "type": "opencollective", 253 | "url": "https://opencollective.com/nodemon" 254 | } 255 | }, 256 | "node_modules/normalize-path": { 257 | "version": "3.0.0", 258 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 259 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 260 | "dev": true, 261 | "engines": { 262 | "node": ">=0.10.0" 263 | } 264 | }, 265 | "node_modules/picomatch": { 266 | "version": "2.3.1", 267 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 268 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 269 | "dev": true, 270 | "engines": { 271 | "node": ">=8.6" 272 | }, 273 | "funding": { 274 | "url": "https://github.com/sponsors/jonschlinkert" 275 | } 276 | }, 277 | "node_modules/pstree.remy": { 278 | "version": "1.1.8", 279 | "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", 280 | "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", 281 | "dev": true 282 | }, 283 | "node_modules/readdirp": { 284 | "version": "3.6.0", 285 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 286 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 287 | "dev": true, 288 | "dependencies": { 289 | "picomatch": "^2.2.1" 290 | }, 291 | "engines": { 292 | "node": ">=8.10.0" 293 | } 294 | }, 295 | "node_modules/semver": { 296 | "version": "7.6.2", 297 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", 298 | "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", 299 | "dev": true, 300 | "bin": { 301 | "semver": "bin/semver.js" 302 | }, 303 | "engines": { 304 | "node": ">=10" 305 | } 306 | }, 307 | "node_modules/simple-update-notifier": { 308 | "version": "2.0.0", 309 | "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", 310 | "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", 311 | "dev": true, 312 | "dependencies": { 313 | "semver": "^7.5.3" 314 | }, 315 | "engines": { 316 | "node": ">=10" 317 | } 318 | }, 319 | "node_modules/supports-color": { 320 | "version": "5.5.0", 321 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 322 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 323 | "dev": true, 324 | "dependencies": { 325 | "has-flag": "^3.0.0" 326 | }, 327 | "engines": { 328 | "node": ">=4" 329 | } 330 | }, 331 | "node_modules/to-regex-range": { 332 | "version": "5.0.1", 333 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 334 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 335 | "dev": true, 336 | "dependencies": { 337 | "is-number": "^7.0.0" 338 | }, 339 | "engines": { 340 | "node": ">=8.0" 341 | } 342 | }, 343 | "node_modules/touch": { 344 | "version": "3.1.1", 345 | "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", 346 | "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", 347 | "dev": true, 348 | "bin": { 349 | "nodetouch": "bin/nodetouch.js" 350 | } 351 | }, 352 | "node_modules/undefsafe": { 353 | "version": "2.0.5", 354 | "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", 355 | "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", 356 | "dev": true 357 | } 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ws-from-scratch", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "type": "module", 6 | "scripts": { 7 | "start": "node src/server.js", 8 | "dev": "nodemon src/server.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "description": "", 14 | "devDependencies": { 15 | "nodemon": "^3.1.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | WebSocket server from scratch 8 | 9 | 73 | 74 | 75 |
76 |

WebSocket server from scratch

77 | 78 |
79 | 80 |
81 | 82 | 83 |
84 |
85 | 86 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import crypto from "node:crypto"; 2 | import fs from "node:fs"; 3 | import http from "node:http"; 4 | import path from "node:path"; 5 | 6 | const server = http.createServer((req, res) => { 7 | if (req.method === "GET" && req.url === "/") { 8 | fs.createReadStream(path.resolve("src", "index.html")).pipe(res); 9 | return; 10 | } 11 | }); 12 | 13 | /** 14 | * connected sockets 15 | */ 16 | const connectedSockets = new Set(); 17 | 18 | /** 19 | * source: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#the_websocket_handshake 20 | */ 21 | const CONN_UPGRADE_MAGIC_STRING = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 22 | 23 | server.on("upgrade", (req, socket, head) => { 24 | // ========== WebSocket: handshake ========== // 25 | /* 26 | the client will start the handshake process by contacting the server & requesting 27 | a WebSocket connection. 28 | 29 | example request from client: 30 | ``` 31 | GET / HTTP/1.1 32 | Host: localhost:3000 33 | Origin: http://localhost:3000 34 | Upgrade: websocket 35 | Connection: Upgrade 36 | Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== 37 | Sec-WebSocket-Version: 13 38 | ``` 39 | 40 | the server will receive an "upgrade" event, in which it will 41 | - concatenate Sec-WebSocket-Key request header & CONN_UPGRADE_MAGIC_STRING (defined above) 42 | - take the SHA-1 hash of the concatenation result 43 | - encode it to base64 & include it in the response header like this 44 | ``` 45 | HTTP/1.1 101 Switching Protocols 46 | Upgrade: websocket 47 | Connection: Upgrade 48 | Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= 49 | ``` 50 | */ 51 | const secWebSocketKey = req.headers["sec-websocket-key"]; 52 | if (!secWebSocketKey) { 53 | socket.write("HTTP/1.1 400 Bad Request\r\n\r\n"); 54 | socket.destroy(); 55 | return; 56 | } 57 | 58 | // read https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#the_websocket_handshake 59 | // for WebSocket handshake 60 | const secWebSocketAccept = crypto 61 | .createHash("sha1") 62 | .update(secWebSocketKey + CONN_UPGRADE_MAGIC_STRING) 63 | .digest("base64"); 64 | 65 | socket.write( 66 | "HTTP/1.1 101 Switching Protocols\r\n" + 67 | "Upgrade: WebSocket\r\n" + 68 | "Connection: Upgrade\r\n" + 69 | `Sec-WebSocket-Accept: ${secWebSocketAccept}\r\n` + 70 | "\r\n", 71 | ); 72 | 73 | // add this socket to our set 74 | connectedSockets.add(socket); 75 | 76 | socket.on("end", () => { 77 | console.log("connection ended"); 78 | connectedSockets.delete(socket); 79 | }); 80 | 81 | socket.on("error", (error) => { 82 | console.error("socket error:", error); 83 | connectedSockets.delete(socket); // Remove the socket on error 84 | }); 85 | 86 | // ========== WebSocket: reading data frames ========== // 87 | 88 | // https://nodejs.org/api/stream.html#event-readable 89 | socket.on("readable", () => { 90 | processDataFrame(socket); 91 | }); 92 | }); 93 | 94 | const PORT = 3000; 95 | server.listen(PORT, () => console.log(`server running on port ${PORT}`)); 96 | 97 | // error handling to prevent the server from crashing 98 | ["uncaughtException", "unhandledRejection"].forEach((event) => 99 | process.on(event, (err) => { 100 | console.error(`an unhandled error(${event}):`, err.stack || err); 101 | }), 102 | ); 103 | 104 | // ========== WebSocket: porcessing data frames ========== // 105 | // https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#exchanging_data_frames 106 | 107 | // bitmasks 108 | const BM_FIN = 0b1000_0000; 109 | const BM_OPCODE = 0b0000_1111; 110 | const BM_MASKED = 0b1000_0000; 111 | const BM_EXP_LEN = 0b0111_1111; // for expected payload length 112 | 113 | // opcodes 114 | const OP_CONT = 0x1; 115 | const OP_TEXT = 0x1; 116 | const OP_BIN = 0x2; 117 | 118 | // expected payload length 119 | const LEN_7_BITS = 125; 120 | const LEN_16_BITS = 126; 121 | const LEN_64_BITS = 127; 122 | 123 | /** 124 | * reads data from the given socket (duplex stream). 125 | * 126 | * @param {import('stream').Duplex} socket - the socket (duplex stream) to read data from 127 | */ 128 | function processDataFrame(socket) { 129 | // 1st byte 130 | const [finAndOpcode] = socket.read(1); 131 | const fin = finAndOpcode & BM_FIN; 132 | const opcode = finAndOpcode & BM_OPCODE; 133 | 134 | // 2nd byte 135 | const [maskAndPayloadLen] = socket.read(1); 136 | const isMasked = maskAndPayloadLen & BM_MASKED; 137 | 138 | // it's written in MDN: Messages from the client must be masked, so your server 139 | // must expect this to be 1. (In fact, section 5.1 of the spec says that your 140 | // server must disconnect from a client if that client sends an unmasked message.) 141 | if (!isMasked) { 142 | socket.destroy(); 143 | return; 144 | } 145 | 146 | const expectedPayloadLen = maskAndPayloadLen & BM_EXP_LEN; // 7 bits 147 | 148 | // decoding payload length 149 | let payloadLen = 0; 150 | if (expectedPayloadLen <= LEN_7_BITS) { 151 | // <= 125 - done! this is the payload length 152 | payloadLen = expectedPayloadLen; 153 | } else if (expectedPayloadLen === LEN_16_BITS) { 154 | // == 126 - read the next 16 bits (2 bytes) to get the payload length 155 | const buffer = socket.read(2); 156 | payloadLen = buffer.readUInt16BE(); 157 | } else if (expectedPayloadLen === LEN_64_BITS) { 158 | // == 127 - read the next 64 bits (8 bytes) to get the payload length 159 | const buffer = socket.read(8); 160 | payloadLen = buffer.readBigUint64BE(); 161 | } else { 162 | throw new Erorr( 163 | `error: readDataFrame() -> expectedPayloadLen is ${expectedPayloadLen}... how??`, 164 | ); 165 | } 166 | 167 | // read the mask 168 | const mask = socket.read(4); 169 | 170 | // read the payload (it will be masked) 171 | const maskedPayload = socket.read(payloadLen); 172 | 173 | // unmask the payload & convert it to string 174 | let payload = maskedPayload.map((e, i) => e ^ mask[i % 4]); 175 | 176 | // if (opcode === OP_TEXT) { 177 | // const text = new TextDecoder().decode(payload); 178 | // console.log("_DEBUG_ text:", text); 179 | // } 180 | 181 | if (connectedSockets.size <= 1) { 182 | return; 183 | } 184 | 185 | // create WebSocket frame to broadcast message to connected sockets 186 | const resFrame = createResponseFrame(payload); 187 | 188 | // broadcast message to all sockets except this one 189 | for (const s of connectedSockets) { 190 | if (s === socket) { 191 | continue; 192 | } 193 | 194 | s.write(resFrame); 195 | } 196 | } 197 | 198 | /** 199 | * @param payload {Buffer} the payload to create frame with 200 | */ 201 | function createResponseFrame(payload) { 202 | let payloadLenBytes = 0; 203 | let payloadLenBuffer; 204 | 205 | if (payload.length <= 125) { 206 | payloadLenBytes = 1; 207 | payloadLenBuffer = Buffer.alloc(1); 208 | payloadLenBuffer.writeUInt8(payload.length); 209 | } else if (payload.length <= 2 ** 16 - 1) { 210 | payloadLenBytes = 2; 211 | payloadLenBuffer = Buffer.alloc(2); 212 | payloadLenBuffer.writeUint16BE(payload.length); 213 | } else if (payload.length <= 2 ** 64 - 1) { 214 | payloadLenBytes = 8; 215 | payloadLenBuffer = Buffer.alloc(8); 216 | payloadLenBuffer.writeBigUInt64BE(payload.length); 217 | } else { 218 | throw new Erorr( 219 | `error: createResponseFrame() -> payload.length is ${payload.length}... how??`, 220 | ); 221 | } 222 | 223 | const frame = Buffer.alloc(1 + payloadLenBytes + payload.length); 224 | frame[0] = 0x81; // FIN and opcode for text frame 225 | payloadLenBuffer.copy(frame, 1); 226 | payload.copy(frame, 1 + payloadLenBytes); 227 | 228 | return frame; 229 | } 230 | --------------------------------------------------------------------------------