From c674a7e9b4af2ca27c5866c22c5562d3a56613c0 Mon Sep 17 00:00:00 2001 From: Thomas Weber Date: Sat, 13 Jun 2020 15:15:57 -0500 Subject: [PATCH] Fix some issues - add LICENSE - switch from node-static to serve-static due to numerous vulnerabilities unfixed for years - update README - make type checking more strict - fix RoomList.get if room does not exist --- LICENSE | 20 ++++ README.md | 31 +++++- package-lock.json | 256 ++++++++++++++++++++++++++++++++++++++------- package.json | 8 +- protocol.md | 5 +- src/Client.js | 40 +++---- src/PingManager.js | 4 +- src/RateLimiter.js | 5 +- src/Room.js | 6 +- src/RoomList.js | 4 +- src/config.js | 2 +- src/index.js | 21 ++-- src/server.js | 5 +- tsconfig.json | 7 ++ 14 files changed, 320 insertions(+), 94 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..7dcd2a51 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2020 Thomas Weber + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index c4e824d3..991ba3c7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,27 @@ -This is a cloud data server for forkphorus. +# cloud-server - - this is not a reimplementation of the Scratch cloud data server - - this does not implement the same protocol as the Scratch cloud data server - - this is not a "permanent"/"long term" data server, all "rooms" are short lived and destructible - - this is not used by or part of Scratch, obviously +This is a cloud data server for [forkphorus](https://forkphorus.github.io/). + +## This is not + + - a reimplementation of the Scratch cloud data server (protocols have several differences) + - a "permanent" or "long term" data server, all "rooms" are removed when they are empty for a while + +## Setup + +Needs Node.js and npm. + +``` +git clone https://github.com/forkphorus/cloud-server +cd cloud-server +npm install +npm start +``` + +## Configuration + +HTTP requests are served static files from the public directory. + +Change the PORT environment variable (or PORT in src/config.js) to change the port. + +If you use a proxy such as nginx set the TRUST_PROXY environment variable (or TRUST_PROXY in src/config.js) to `true` to make log messages include the correct IP addresses. diff --git a/package-lock.json b/package-lock.json index 1ffeb77f..07ade47a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "forkphorus-cloud-server", - "version": "0.0.1", + "version": "0.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -672,6 +672,26 @@ "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", "dev": true }, + "@types/express-serve-static-core": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.7.tgz", + "integrity": "sha512-EMgTj/DF9qpgLXyc+Btimg+XoH7A2liE8uKul8qSmMTHCeNYzydDKFdsJskDvw42UsesCnhO63dO0Grbj8J4Dw==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/finalhandler": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/finalhandler/-/finalhandler-1.1.0.tgz", + "integrity": "sha512-iMD07b+UPNuqA6cm3yjbTJUfFfZNZpDc6MGQGK60jy5sdqX1W4wUC5RAi3FhaW72+RmVnzYxba8byPP9FWL8/w==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/graceful-fs": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.3.tgz", @@ -776,6 +796,12 @@ } } }, + "@types/mime": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.2.tgz", + "integrity": "sha512-4kPlzbljFcsttWEq6aBW0OZe6BDajAmyvr2xknBG92tejQnvdGtT9+kXSZ580DqpxY9qG2xeQVF9Dq0ymUTo5Q==", + "dev": true + }, "@types/node": { "version": "14.0.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.12.tgz", @@ -794,6 +820,28 @@ "integrity": "sha512-boy4xPNEtiw6N3abRhBi/e7hNvy3Tt8E9ZRAQrwAGzoCGZS/1wjo9KY7JHhnfnEsG5wSjDbymCozUM9a3ea7OQ==", "dev": true }, + "@types/qs": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.3.tgz", + "integrity": "sha512-7s9EQWupR1fTc2pSMtXRQ9w9gLOcrJn+h7HOXw4evxyvVqMi4f+q7d2tnFe3ng3SNHjtK+0EzGMGFUQX4/AQRA==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", + "dev": true + }, + "@types/serve-static": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.4.tgz", + "integrity": "sha512-jTDt0o/YbpNwZbQmE/+2e+lfjJEJJR0I3OFaKQKPWkASkCoW3i6fsUnqudSMcNAfbtmADGu8f4MV4q+GqULmug==", + "dev": true, + "requires": { + "@types/express-serve-static-core": "*", + "@types/mime": "*" + } + }, "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", @@ -1309,11 +1357,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" - }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1515,6 +1558,16 @@ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "dev": true }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -1554,12 +1607,22 @@ "safer-buffer": "^2.1.0" } }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -1578,6 +1641,11 @@ "is-arrayish": "^0.2.1" } }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -1615,6 +1683,11 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, "exec-sh": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz", @@ -1840,6 +1913,35 @@ "to-regex-range": "^5.0.1" } }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, "find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -1882,6 +1984,11 @@ "map-cache": "^0.2.2" } }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2065,6 +2172,18 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -2120,8 +2239,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ip-regex": { "version": "2.1.0", @@ -3214,16 +3332,6 @@ } } }, - "node-static": { - "version": "0.7.11", - "resolved": "https://registry.npmjs.org/node-static/-/node-static-0.7.11.tgz", - "integrity": "sha512-zfWC/gICcqb74D9ndyvxZWaI1jzcoHmf4UTHWQchBNuNMxdBLJMDiUgZ1tjGLEIe/BMhj2DxKD8HOuc2062pDQ==", - "requires": { - "colors": ">=0.6.0", - "mime": "^1.2.9", - "optimist": ">=0.3.4" - } - }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -3320,6 +3428,14 @@ "isobject": "^3.0.1" } }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3338,22 +3454,6 @@ "mimic-fn": "^2.1.0" } }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" - } - } - }, "optionator": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", @@ -3422,6 +3522,11 @@ "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", "dev": true }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", @@ -3544,6 +3649,11 @@ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "dev": true }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -3927,6 +4037,59 @@ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -3956,6 +4119,11 @@ } } }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -4261,6 +4429,11 @@ } } }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, "stealthy-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", @@ -4420,6 +4593,11 @@ "is-number": "^7.0.0" } }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, "tough-cookie": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", @@ -4503,6 +4681,11 @@ "set-value": "^2.0.1" } }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, "unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", @@ -4708,11 +4891,6 @@ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" - }, "wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", diff --git a/package.json b/package.json index 3cc543ba..4fa0fbc1 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,25 @@ { "name": "forkphorus-cloud-server", - "version": "0.0.1", + "version": "0.1.0", "description": "", "main": "src/index.js", "scripts": { + "start": "node src/index.js", "test": "jest", "check": "tsc" }, "author": "GarboMuffin", "license": "MIT", "dependencies": { - "node-static": "^0.7.11", + "finalhandler": "^1.1.2", + "serve-static": "^1.14.1", "ws": "^7.3.0" }, "devDependencies": { + "@types/finalhandler": "^1.1.0", "@types/jest": "^25.2.3", "@types/node": "^14.0.12", + "@types/serve-static": "^1.13.4", "@types/ws": "^7.2.5", "jest": "^26.0.1", "typescript": "^3.9.5" diff --git a/protocol.md b/protocol.md index f2c3afa7..bbdcb16b 100644 --- a/protocol.md +++ b/protocol.md @@ -81,10 +81,9 @@ interface SetMessage extends Message { 4000 Generic error When the client violates the protocol. - For example, if you send a 'set' before a 'handshake', or try to change the value of variables that don't exist. 4001 Incompatibility - When the connection is unable to continue due to a compatibility issue with the data provided. + When the connection is unable to continue due to an unfixable compatibility issue. For example, when the variables provided by the client do not match those in the room. 4002 Username error @@ -95,6 +94,6 @@ interface SetMessage extends Message { When the server is overloaded or full and refuses to accept new connections. 4004 Try Again Later - When the client has done an operation that is invalid at this time, but may have been valid if sent later. + When the client has done an operation that is invalid at this time, but would have been valid if sent later. For example, when the client sends too many messages in a given period of time. ``` diff --git a/src/Client.js b/src/Client.js index 7ab5b675..442d94d7 100644 --- a/src/Client.js +++ b/src/Client.js @@ -5,15 +5,19 @@ const logger = require('./logger'); /** * Get the remote IP address of a request. * This may be configured to trust a proxy and return the proxy's forwarded-for IP instead of the actual remote IP - * @param {import('http').IncomingMessage} req + * @param {?import('http').IncomingMessage} req * @returns {string} The IP address */ function getIP(req) { - const socketAddress = req.socket.remoteAddress || '???'; + if (req === null) { + return '(req missing)'; + } + + const socketAddress = req.socket.remoteAddress || '(remoteAddress missing)'; if (config.trustProxy) { const header = /** @type {string} */ (req.headers['x-forwarded-for']); - if (!header) { + if (!header || typeof header !== 'string') { return socketAddress; } // extract the first IP @@ -26,8 +30,8 @@ function getIP(req) { class Client { /** - * @param {import('ws')} ws The WebSocket connection - * @param {import('http').IncomingMessage} req The HTTP request + * @param {?import('ws')} ws The WebSocket connection + * @param {?import('http').IncomingMessage} req The HTTP request */ constructor(ws, req) { /** The WebSocket connection */ @@ -36,15 +40,15 @@ class Client { this.ip = getIP(req); /** * The Room this client is connected to. - * @type {Room} + * @type {?Room} */ this.room = null; /** * The username of the Client. - * This value is only valid if room != null + * This value is only valid if room !== null * @type {string} */ - this.username = null; + this.username = ''; } /** @@ -54,7 +58,7 @@ class Client { */ getLogPrefix() { let prefix = '[' + this.ip; - if (this.username !== null) { + if (this.username !== '') { prefix += ' "' + this.username + '"'; } if (this.room !== null) { @@ -88,22 +92,13 @@ class Client { logger.error(this.getLogPrefix(), ...args); } - /** - * Whether the client can receive messages. - * @private - * @returns {boolean} - */ - canSendMessage() { - return this.ws !== null && this.ws.readyState === this.ws.OPEN; - } - /** * Send a message to the client. * @param {object} data JSON object to send. * @private */ send(data) { - if (!this.canSendMessage()) { + if (this.ws === null || this.ws.readyState !== this.ws.OPEN) { this.log('Cannot send message'); return; } @@ -116,7 +111,7 @@ class Client { * @private */ sendMany(data) { - if (!this.canSendMessage()) { + if (this.ws === null || this.ws.readyState !== this.ws.OPEN) { this.log('Cannot send message'); return; } @@ -155,8 +150,13 @@ class Client { /** * Send a 'set variable' for each of the variables of the connected room. + * @throws Will throw if the client is not in a room. */ sendAllVariables() { + if (!this.room) { + throw new Error('Not in a room'); + } + /** @type {[string, string][]} */ const commands = []; this.room.getAllVariables().forEach((value, name) => { commands.push([name, value]); diff --git a/src/PingManager.js b/src/PingManager.js index 7256480f..aa0bfdbf 100644 --- a/src/PingManager.js +++ b/src/PingManager.js @@ -24,7 +24,7 @@ class PingManager { // @ts-ignore if (ws.isAlive === false) { // Socket has not responded to ping for a long time, and is probably dead. - // terminate will call the onclose handler to cleanup the used resources + // terminate will call the onclose handler to cleanup the connection ws.terminate(); /** @type {import('./Client')} */ @@ -43,7 +43,7 @@ class PingManager { // We will send a ping to the client. // When we receive a pong, isAlive will be set to true. // This gives the client until the next update to respond, this should be plenty long for any living connection. - + // @ts-ignore ws.isAlive = false; ws.ping(); diff --git a/src/RateLimiter.js b/src/RateLimiter.js index 368497fd..cdbe156c 100644 --- a/src/RateLimiter.js +++ b/src/RateLimiter.js @@ -1,7 +1,7 @@ class RateLimiter { /** * RateLimiter limits the rate of operations. - * + * * new RateLimiter(20, 1000) means that there cannot be more than 20 operations in any period of 1 seconds. * @param {number} maxOperations The maximum amount of operations that can be done in the given period, inclusive. * @param {number} timePeriod The time, in milliseconds, of each period. Cannot exceed maxOperations in this amount of time. @@ -28,7 +28,8 @@ class RateLimiter { // We record the time of the last maxOperations operations this.history.push(now); if (this.history.length > this.maxOperations) { - const period = this.history.shift(); + // history definitely will have something in it, so shift() will not return undefined. + const period = /** @type {number} */ (this.history.shift()); const timeSince = now - period; // If the last request of the last maxOperations operations was within the time period, this is a rate limiting violation. return timeSince < this.timePeriod; diff --git a/src/Room.js b/src/Room.js index d18a956c..8b18031b 100644 --- a/src/Room.js +++ b/src/Room.js @@ -117,15 +117,15 @@ class Room { * @throws Will throw if name or value are invalid, or the variable does not exist. */ set(name, value) { + if (!this.has(name)) { + throw new Error('Variable does not exist'); + } if (!validators.isValidVariableName(name)) { throw new Error('Invalid variable name'); } if (!validators.isValidVariableValue(value)) { throw new Error('Invalid value'); } - if (!this.has(name)) { - throw new Error('Variable does not exist'); - } this.variables.set(name, value); } diff --git a/src/RoomList.js b/src/RoomList.js index dfe62448..1e5a770c 100644 --- a/src/RoomList.js +++ b/src/RoomList.js @@ -40,13 +40,13 @@ class RoomList { /** * Get a Room - * @param {RoomID} id + * @param {RoomID} id * @returns {Room} * @throws Will throw if room does not exist */ get(id) { const room = this.rooms.get(id); - if (room === null) { + if (!room) { throw new Error('Room does not exist'); } return room; diff --git a/src/config.js b/src/config.js index 4da91aa5..70f33630 100644 --- a/src/config.js +++ b/src/config.js @@ -1,4 +1,4 @@ module.exports = { - port: +process.env.PORT || 9080, + port: process.env.PORT || 9080, trustProxy: process.env.TRUST_PROXY === 'true' || true, }; diff --git a/src/index.js b/src/index.js index 710addc0..a7b139e3 100644 --- a/src/index.js +++ b/src/index.js @@ -1,19 +1,16 @@ const http = require('http'); -const process = require('process'); -const static = require('node-static'); +const finalhandler = require('finalhandler'); +const serveStatic = require('serve-static'); const logger = require('./logger'); const config = require('./config'); - const wss = require('./server'); -const fileServer = new static.Server('./public', { - serverInfo: 'https://github.com/forkphorus/cloud-server', -}); -const server = http.createServer(function(req, res) { - // Serve static files over HTTP - req.addListener('end', function() { - fileServer.serve(req, res); - }).resume(); + +// We serve static files over HTTP +const serve = serveStatic('public'); +const server = http.createServer(function handler(req, res) { + // @ts-ignore + serve(req, res, finalhandler(req, res)); }); server.on('upgrade', function upgrade(request, socket, head) { @@ -28,7 +25,7 @@ server.on('close', function() { logger.info('Server closing'); wss.close(); }); - + server.listen(config.port, function() { logger.info('Server started on port: ' + config.port); }); diff --git a/src/server.js b/src/server.js index a40350bb..238b1a3b 100644 --- a/src/server.js +++ b/src/server.js @@ -1,13 +1,12 @@ const WebSocket = require('ws'); -const Room = require('./Room'); const Client = require('./Client'); const RoomList = require('./RoomList'); const ConnectionError = require('./ConnectionError'); const PingManager = require('./PingManager'); +const RateLimiter = require('./RateLimiter'); const validators = require('./validators'); const logger = require('./logger'); -const RateLimiter = require('./RateLimiter'); const wss = new WebSocket.Server({ noServer: true, @@ -19,7 +18,7 @@ const pingManager = new PingManager(wss); pingManager.start(1000 * 30); /** - * @param {unknown} data + * @param {unknown} data */ function isValidMessage(data) { // @ts-ignore diff --git a/tsconfig.json b/tsconfig.json index 14607121..1245e332 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,5 +8,12 @@ "noEmit": true, "allowJs": true, "checkJs": true, + // Make the checker much more strict without being too limiting + "strict": true, + "noImplicitAny": false, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowUnreachableCode": false, } } \ No newline at end of file