From 3e18f65d3bafdc7fa6c11335af8b5b3bd9f69e54 Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Mon, 18 Mar 2024 19:04:59 +0100 Subject: [PATCH] Added basic app. --- pnpm-lock.yaml | 248 ++++++++++----- src/node/hooks/express.ts | 390 +++++++++++++----------- src/node/hooks/express/admin.ts | 28 +- src/node/hooks/express/apicalls.ts | 53 ++-- src/node/hooks/express/errorhandling.ts | 10 +- src/node/hooks/express/importexport.ts | 117 ++++--- src/node/hooks/express/openapi.ts | 2 +- src/node/hooks/express/socketio.ts | 8 + src/node/hooks/express/specialpages.ts | 5 +- src/node/hooks/express/static.ts | 30 +- src/node/hooks/express/webaccess.ts | 43 +-- src/node/types/ArgsExpressType.ts | 6 +- src/node/types/WebAccessTypes.ts | 2 +- src/node/utils/sanitizePathname.ts | 1 - src/package.json | 6 +- src/static/js/pad.js | 7 +- src/static/js/pad_utils.js | 7 +- 17 files changed, 581 insertions(+), 382 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 21276ae4d..795061641 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,9 +124,21 @@ importers: src: dependencies: + '@fastify/compress': + specifier: ^7.0.0 + version: 7.0.0 + '@fastify/cookie': + specifier: ^9.3.1 + version: 9.3.1 '@fastify/express': specifier: ^2.3.0 version: 2.3.0 + '@fastify/rate-limit': + specifier: ^9.1.0 + version: 9.1.0 + '@fastify/session': + specifier: ^10.7.0 + version: 10.7.0 '@fastify/static': specifier: ^7.0.1 version: 7.0.1 @@ -154,12 +166,9 @@ importers: etherpad-yajsml: specifier: 0.0.12 version: 0.0.12 - express: - specifier: 4.18.3 - version: 4.18.3 express-rate-limit: specifier: ^7.2.0 - version: 7.2.0(express@4.18.3) + version: 7.2.0 express-session: specifier: npm:@etherpad/express-session@^1.18.2 version: /@etherpad/express-session@1.18.2 @@ -281,9 +290,6 @@ importers: '@types/async': specifier: ^3.2.24 version: 3.2.24 - '@types/express': - specifier: ^4.17.21 - version: 4.17.21 '@types/http-errors': specifier: ^2.0.4 version: 2.0.4 @@ -819,6 +825,26 @@ packages: fast-uri: 2.3.0 dev: false + /@fastify/compress@7.0.0: + resolution: {integrity: sha512-jo/NaBVHP1OXIf8Kmr3bZyYQB0gAIgcy5c8rRKTPjhklHO7lRs/6ZFckUVT0NtbKSvrTuIcmSkxYpjyv3FNHXA==} + dependencies: + '@fastify/accept-negotiator': 1.1.0 + fastify-plugin: 4.5.1 + into-stream: 6.0.0 + mime-db: 1.52.0 + minipass: 7.0.4 + peek-stream: 1.1.3 + pump: 3.0.0 + pumpify: 2.0.1 + dev: false + + /@fastify/cookie@9.3.1: + resolution: {integrity: sha512-h1NAEhB266+ZbZ0e9qUE6NnNR07i7DnNXWG9VbbZ8uC6O/hxHpl+Zoe5sw1yfdZ2U6XhToUGDnzQtWJdCaPwfg==} + dependencies: + cookie-signature: 1.2.1 + fastify-plugin: 4.5.1 + dev: false + /@fastify/error@3.4.1: resolution: {integrity: sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==} dev: false @@ -844,6 +870,14 @@ packages: fast-deep-equal: 3.1.3 dev: false + /@fastify/rate-limit@9.1.0: + resolution: {integrity: sha512-h5dZWCkuZXN0PxwqaFQLxeln8/LNwQwH9popywmDCFdKfgpi4b/HoMH1lluy6P+30CG9yzzpSpwTCIPNB9T1JA==} + dependencies: + '@lukeed/ms': 2.0.2 + fastify-plugin: 4.5.1 + toad-cache: 3.7.0 + dev: false + /@fastify/send@2.1.0: resolution: {integrity: sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==} dependencies: @@ -854,6 +888,13 @@ packages: mime: 3.0.0 dev: false + /@fastify/session@10.7.0: + resolution: {integrity: sha512-ECA75gnyaxcyIukgyO2NGT3XdbLReNl/pTKrrkRfDc6pVqNtdptwwfx9KXrIMOfsO4B3m84eF3wZ9GgnebiZ4w==} + dependencies: + fastify-plugin: 4.5.1 + safe-stable-stringify: 2.4.3 + dev: false + /@fastify/static@7.0.1: resolution: {integrity: sha512-i1p/nELMknAisNfnjo7yhfoUOdKzA+n92QaMirv2NkZrJ1Wl12v2nyTYlDwPN8XoStMBAnRK/Kx6zKmfrXUPXw==} dependencies: @@ -1767,19 +1808,6 @@ packages: resolution: {integrity: sha512-8iHVLHsCCOBKjCF2KwFe0p9Z3rfM9mL+sSP8btyR5vTjJRAqpBYD28/ZLgXPf0pjG1VxOvtCV/BgXkQbpSe8Hw==} dev: true - /@types/body-parser@1.19.5: - resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} - dependencies: - '@types/connect': 3.4.38 - '@types/node': 20.11.28 - dev: true - - /@types/connect@3.4.38: - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - dependencies: - '@types/node': 20.11.28 - dev: true - /@types/cookie@0.4.1: resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} dev: false @@ -1804,24 +1832,6 @@ packages: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} dev: true - /@types/express-serve-static-core@4.17.43: - resolution: {integrity: sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==} - dependencies: - '@types/node': 20.11.28 - '@types/qs': 6.9.11 - '@types/range-parser': 1.2.7 - '@types/send': 0.17.4 - dev: true - - /@types/express@4.17.21: - resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} - dependencies: - '@types/body-parser': 1.19.5 - '@types/express-serve-static-core': 4.17.43 - '@types/qs': 6.9.11 - '@types/serve-static': 1.15.5 - dev: true - /@types/fs-extra@9.0.13: resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} dependencies: @@ -1877,14 +1887,6 @@ packages: resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} dev: true - /@types/mime@1.3.5: - resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - dev: true - - /@types/mime@3.0.4: - resolution: {integrity: sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==} - dev: true - /@types/mocha@10.0.6: resolution: {integrity: sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==} dev: true @@ -1915,14 +1917,6 @@ packages: resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} dev: true - /@types/qs@6.9.11: - resolution: {integrity: sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==} - dev: true - - /@types/range-parser@1.2.7: - resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - dev: true - /@types/react-dom@18.2.21: resolution: {integrity: sha512-gnvBA/21SA4xxqNXEwNiVcP0xSGHh/gi1VhWv9Bl46a0ItbTT5nFY+G9VSQpaG/8N/qdJpJ+vftQ4zflTtnjLw==} dependencies: @@ -1944,21 +1938,6 @@ packages: /@types/semver@7.5.8: resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} - /@types/send@0.17.4: - resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} - dependencies: - '@types/mime': 1.3.5 - '@types/node': 20.11.28 - dev: true - - /@types/serve-static@1.15.5: - resolution: {integrity: sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==} - dependencies: - '@types/http-errors': 2.0.4 - '@types/mime': 3.0.4 - '@types/node': 20.11.28 - dev: true - /@types/sinon@17.0.3: resolution: {integrity: sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==} dependencies: @@ -2794,6 +2773,11 @@ packages: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} dev: false + /cookie-signature@1.2.1: + resolution: {integrity: sha512-78KWk9T26NhzXtuL26cIJ8/qNHANyJ/ZYrmEXFzUmhZdjpBv+DlWlOANRTGBt48YcyslsLrj0bMLFTmXvLRCOw==} + engines: {node: '>=6.6.0'} + dev: false + /cookie@0.4.1: resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==} engines: {node: '>= 0.6'} @@ -2817,6 +2801,10 @@ packages: /cookiejar@2.1.4: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + /core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + dev: false + /cors@2.8.5: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} @@ -3018,6 +3006,24 @@ packages: tslib: 2.6.2 dev: true + /duplexify@3.7.1: + resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==} + dependencies: + end-of-stream: 1.4.4 + inherits: 2.0.4 + readable-stream: 2.3.8 + stream-shift: 1.0.3 + dev: false + + /duplexify@4.1.3: + resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + dependencies: + end-of-stream: 1.4.4 + inherits: 2.0.4 + readable-stream: 3.6.2 + stream-shift: 1.0.3 + dev: false + /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: false @@ -3656,13 +3662,11 @@ packages: engines: {node: '>=0.8.x'} dev: false - /express-rate-limit@7.2.0(express@4.18.3): + /express-rate-limit@7.2.0: resolution: {integrity: sha512-T7nul1t4TNyfZMJ7pKRKkdeVJWa2CqB8NA1P8BwYaoDI5QSBZARv5oMS43J7b7I5P+4asjVXjb7ONuwDKucahg==} engines: {node: '>= 16'} peerDependencies: express: 4 || 5 || ^5.0.0-beta.1 - dependencies: - express: 4.18.3 dev: false /express@4.18.3: @@ -3946,6 +3950,13 @@ packages: engines: {node: '>= 0.6'} dev: false + /from2@2.3.0: + resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.8 + dev: false + /fs-extra@10.1.0: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} @@ -4419,6 +4430,14 @@ packages: side-channel: 1.0.5 dev: true + /into-stream@6.0.0: + resolution: {integrity: sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==} + engines: {node: '>=10'} + dependencies: + from2: 2.3.0 + p-is-promise: 3.0.0 + dev: false + /invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} dependencies: @@ -4594,6 +4613,10 @@ packages: call-bind: 1.0.7 dev: true + /isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + dev: false + /isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} dev: true @@ -5042,6 +5065,11 @@ packages: engines: {node: '>=8'} dev: false + /minipass@7.0.4: + resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} + engines: {node: '>=16 || 14 >=14.17'} + dev: false + /minizlib@2.1.2: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} engines: {node: '>= 8'} @@ -5301,6 +5329,11 @@ packages: type-check: 0.4.0 dev: true + /p-is-promise@3.0.0: + resolution: {integrity: sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==} + engines: {node: '>=8'} + dev: false + /p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -5380,6 +5413,14 @@ packages: engines: {node: '>=8'} dev: true + /peek-stream@1.1.3: + resolution: {integrity: sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==} + dependencies: + buffer-from: 1.1.2 + duplexify: 3.7.1 + through2: 2.0.5 + dev: false + /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} dev: true @@ -5472,6 +5513,10 @@ packages: engines: {node: '>= 0.8.0'} dev: true + /process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + dev: false + /process-warning@3.0.0: resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} dev: false @@ -5514,6 +5559,14 @@ packages: once: 1.4.0 dev: false + /pumpify@2.0.1: + resolution: {integrity: sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==} + dependencies: + duplexify: 4.1.3 + inherits: 2.0.4 + pump: 3.0.0 + dev: false + /punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -5698,6 +5751,27 @@ packages: loose-envify: 1.4.0 dev: true + /readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + dev: false + + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: false + /readable-stream@4.5.2: resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -5868,6 +5942,10 @@ packages: isarray: 2.0.5 dev: true + /safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + dev: false + /safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -6151,6 +6229,10 @@ packages: engines: {node: '>= 0.8'} dev: false + /stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + dev: false + /streamroller@3.1.5: resolution: {integrity: sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==} engines: {node: '>=8.0'} @@ -6204,6 +6286,12 @@ packages: es-abstract: 1.22.4 dev: true + /string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + dependencies: + safe-buffer: 5.1.2 + dev: false + /string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: @@ -6352,6 +6440,13 @@ packages: - supports-color dev: false + /through2@2.0.5: + resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + dependencies: + readable-stream: 2.3.8 + xtend: 4.0.2 + dev: false + /tiny-worker@2.3.0: resolution: {integrity: sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==} requiresBuild: true @@ -6694,6 +6789,10 @@ packages: react: 18.2.0 dev: true + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: false + /utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} @@ -6936,6 +7035,11 @@ packages: resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==} engines: {node: '>=0.4.0'} + /xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + dev: false + /y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} diff --git a/src/node/hooks/express.ts b/src/node/hooks/express.ts index 093eedab2..0baa9f265 100644 --- a/src/node/hooks/express.ts +++ b/src/node/hooks/express.ts @@ -10,180 +10,173 @@ import events from 'events'; // @ts-ignore import expressSession from 'express-session'; import fs from 'fs'; + const hooks = require('../../static/js/pluginfw/hooks'); import log4js from 'log4js'; + const SessionStore = require('../db/SessionStore'); const settings = require('../utils/Settings'); const stats = require('../stats') import util from 'util'; + const webaccess = require('./express/webaccess'); import Fastify from 'fastify'; import SecretRotator from '../security/SecretRotator'; +import fastifyCookie from '@fastify/cookie'; +import fastifySession from "@fastify/session"; -let secretRotator: SecretRotator|null = null; +let secretRotator: SecretRotator | null = null; const logger = log4js.getLogger('http'); -let serverName:string; -let sessionStore: { shutdown: () => void; } | null; -const sockets:Set = new Set(); +let serverName: string; +let sessionStore: typeof SessionStore | null; +const sockets: Set = new Set(); const socketsEvents = new events.EventEmitter(); const startTime = stats.settableGauge('httpStartTime'); exports.server = null; const closeServer = async () => { - if (exports.server != null) { - logger.info('Closing HTTP server...'); - // Call exports.server.close() to reject new connections but don't await just yet because the - // Promise won't resolve until all preexisting connections are closed. - const p = util.promisify(exports.server.close.bind(exports.server))(); - await hooks.aCallAll('expressCloseServer'); - // Give existing connections some time to close on their own before forcibly terminating. The - // time should be long enough to avoid interrupting most preexisting transmissions but short - // enough to avoid a noticeable outage. - const timeout = setTimeout(async () => { - logger.info(`Forcibly terminating remaining ${sockets.size} HTTP connections...`); - for (const socket of sockets) socket.destroy(new Error('HTTP server is closing')); - }, 5000); - let lastLogged = 0; - while (sockets.size > 0 && !settings.enableAdminUITests) { - if (Date.now() - lastLogged > 1000) { // Rate limit to avoid filling logs. - logger.info(`Waiting for ${sockets.size} HTTP clients to disconnect...`); - lastLogged = Date.now(); - } - await events.once(socketsEvents, 'updated'); + if (exports.server != null) { + logger.info('Closing HTTP server...'); + // Call exports.server.close() to reject new connections but don't await just yet because the + // Promise won't resolve until all preexisting connections are closed. + const p = util.promisify(exports.server.close.bind(exports.server))(); + await hooks.aCallAll('expressCloseServer'); + // Give existing connections some time to close on their own before forcibly terminating. The + // time should be long enough to avoid interrupting most preexisting transmissions but short + // enough to avoid a noticeable outage. + const timeout = setTimeout(async () => { + logger.info(`Forcibly terminating remaining ${sockets.size} HTTP connections...`); + for (const socket of sockets) socket.destroy(new Error('HTTP server is closing')); + }, 5000); + let lastLogged = 0; + while (sockets.size > 0 && !settings.enableAdminUITests) { + if (Date.now() - lastLogged > 1000) { // Rate limit to avoid filling logs. + logger.info(`Waiting for ${sockets.size} HTTP clients to disconnect...`); + lastLogged = Date.now(); + } + await events.once(socketsEvents, 'updated'); + } + await p; + clearTimeout(timeout); + exports.server = null; + startTime.setValue(0); + logger.info('HTTP server closed'); } - await p; - clearTimeout(timeout); - exports.server = null; - startTime.setValue(0); - logger.info('HTTP server closed'); - } - if (sessionStore) sessionStore.shutdown(); - sessionStore = null; - if (secretRotator) secretRotator.stop(); - secretRotator = null; + if (sessionStore) sessionStore.shutdown(); + sessionStore = null; + if (secretRotator) secretRotator.stop(); + secretRotator = null; }; exports.createServer = async () => { - console.log('Report bugs at https://github.com/ether/etherpad-lite/issues'); + console.log('Report bugs at https://github.com/ether/etherpad-lite/issues'); - serverName = `Etherpad ${settings.getGitCommit()} (https://etherpad.org)`; + serverName = `Etherpad ${settings.getGitCommit()} (https://etherpad.org)`; - console.log(`Your Etherpad version is ${settings.getEpVersion()} (${settings.getGitCommit()})`); + console.log(`Your Etherpad version is ${settings.getEpVersion()} (${settings.getGitCommit()})`); - await exports.restartServer(); + await exports.restartServer(); - if (settings.ip === '') { - // using Unix socket for connectivity - console.log(`You can access your Etherpad instance using the Unix socket at ${settings.port}`); - } else { - console.log(`You can access your Etherpad instance at http://${settings.ip}:${settings.port}/`); - } + if (settings.ip === '') { + // using Unix socket for connectivity + console.log(`You can access your Etherpad instance using the Unix socket at ${settings.port}`); + } else { + console.log(`You can access your Etherpad instance at http://${settings.ip}:${settings.port}/`); + } - if (!_.isEmpty(settings.users)) { - console.log(`The plugin admin page is at http://${settings.ip}:${settings.port}/admin/plugins`); - } else { - console.warn('Admin username and password not set in settings.json. ' + - 'To access admin please uncomment and edit "users" in settings.json'); - } + if (!_.isEmpty(settings.users)) { + console.log(`The plugin admin page is at http://${settings.ip}:${settings.port}/admin/plugins`); + } else { + console.warn('Admin username and password not set in settings.json. ' + + 'To access admin please uncomment and edit "users" in settings.json'); + } - const env = process.env.NODE_ENV || 'development'; + const env = process.env.NODE_ENV || 'development'; - if (env !== 'production') { - console.warn('Etherpad is running in Development mode. This mode is slower for users and ' + - 'less secure than production mode. You should set the NODE_ENV environment ' + - 'variable to production by using: export NODE_ENV=production'); - } + if (env !== 'production') { + console.warn('Etherpad is running in Development mode. This mode is slower for users and ' + + 'less secure than production mode. You should set the NODE_ENV environment ' + + 'variable to production by using: export NODE_ENV=production'); + } }; exports.restartServer = async () => { - await closeServer(); + await closeServer(); - console.log('Starting Etherpad...'); - const fastify = Fastify({ - logger: { - level: 'error', - transport: { - target: 'pino-pretty', - options: { - translateTime: 'HH:MM:ss Z', - ignore: 'pid,hostname', - }, - } + console.log('Starting Etherpad...'); + const fastify = Fastify({ + logger: { + level: 'error', + transport: { + target: 'pino-pretty', + options: { + translateTime: 'HH:MM:ss Z', + ignore: 'pid,hostname', + }, + } + } + }); + //await fastify.register(require('@fastify/express')) + + + if (settings.ssl) { + console.log('SSL -- enabled'); + console.log(`SSL -- server key file: ${settings.ssl.key}`); + console.log(`SSL -- Certificate Authority's certificate file: ${settings.ssl.cert}`); + + const options: MapArrayType = { + key: fs.readFileSync(settings.ssl.key), + cert: fs.readFileSync(settings.ssl.cert), + }; + + if (settings.ssl.ca) { + options.ca = []; + for (let i = 0; i < settings.ssl.ca.length; i++) { + const caFileName = settings.ssl.ca[i]; + options.ca.push(fs.readFileSync(caFileName)); + } + } } - }); - await fastify.register(require('@fastify/express')) - - - if (settings.ssl) { - console.log('SSL -- enabled'); - console.log(`SSL -- server key file: ${settings.ssl.key}`); - console.log(`SSL -- Certificate Authority's certificate file: ${settings.ssl.cert}`); - - const options: MapArrayType = { - key: fs.readFileSync(settings.ssl.key), - cert: fs.readFileSync(settings.ssl.cert), - }; - - if (settings.ssl.ca) { - options.ca = []; - for (let i = 0; i < settings.ssl.ca.length; i++) { - const caFileName = settings.ssl.ca[i]; - options.ca.push(fs.readFileSync(caFileName)); - } - } - } exports.server = fastify.server - fastify.use((req, res, next) => { - // res.header("X-Frame-Options", "deny"); // breaks embedded pads - if (settings.ssl) { - // we use SSL - res.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); - } + fastify.addHook('onRequest', async (req, res) => { + // res.header("X-Frame-Options", "deny"); // breaks embedded pads + if (settings.ssl) { + // we use SSL + res.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); + } - // Stop IE going into compatability mode - // https://github.com/ether/etherpad-lite/issues/2547 - res.header('X-UA-Compatible', 'IE=Edge,chrome=1'); + // Stop IE going into compatability mode + // https://github.com/ether/etherpad-lite/issues/2547 + res.header('X-UA-Compatible', 'IE=Edge,chrome=1'); - // Enable a strong referrer policy. Same-origin won't drop Referers when - // loading local resources, but it will drop them when loading foreign resources. - // It's still a last bastion of referrer security. External URLs should be - // already marked with rel="noreferer" and user-generated content pages are already - // marked with - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy - // https://github.com/ether/etherpad-lite/pull/3636 - res.header('Referrer-Policy', 'same-origin'); + // Enable a strong referrer policy. Same-origin won't drop Referers when + // loading local resources, but it will drop them when loading foreign resources. + // It's still a last bastion of referrer security. External URLs should be + // already marked with rel="noreferer" and user-generated content pages are already + // marked with + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy + // https://github.com/ether/etherpad-lite/pull/3636 + res.header('Referrer-Policy', 'same-origin'); - // send git version in the Server response header if exposeVersion is true. - if (settings.exposeVersion) { - res.header('Server', serverName); - } - - next(); + // send git version in the Server response header if exposeVersion is true. + if (settings.exposeVersion) { + res.header('Server', serverName); + } }); if (settings.trustProxy) { - /* - * If 'trust proxy' === true, the client’s IP address in req.ip will be the - * left-most entry in the X-Forwarded-* header. - * - * Source: https://expressjs.com/en/guide/behind-proxies.html - */ - fastify.enable('trust proxy'); + /* + * If 'trust proxy' === true, the client’s IP address in req.ip will be the + * left-most entry in the X-Forwarded-* header. + * + * Source: https://expressjs.com/en/guide/behind-proxies.html + */ + fastify.enable('trust proxy'); } - // Measure response time - fastify.use((req, res, next) => { - const stopWatch = stats.timer('httpRequests').start(); - const sendFn = res.send.bind(res); - res.send = (...args) => { - stopWatch.end(); - return sendFn(...args); - }; - next(); - }); // If the log level specified in the config file is WARN or ERROR the application server never // starts listening to requests as reported in issue #158. Not installing the log4js connect @@ -195,78 +188,109 @@ exports.restartServer = async () => { const {keyRotationInterval, sessionLifetime} = settings.cookie; let secret = settings.sessionKey; if (keyRotationInterval && sessionLifetime) { - secretRotator = new SecretRotator( - 'expressSessionSecrets', keyRotationInterval, sessionLifetime, settings.sessionKey); - await secretRotator.start(); - secret = secretRotator.secrets; + secretRotator = new SecretRotator( + 'expressSessionSecrets', keyRotationInterval, sessionLifetime, settings.sessionKey); + await secretRotator.start(); + secret = secretRotator.secrets; } if (!secret) throw new Error('missing cookie signing secret'); - - fastify.use(cookieParser(secret, {})); - sessionStore = new SessionStore(settings.cookie.sessionRefreshInterval); exports.sessionMiddleware = expressSession({ - propagateTouch: true, - rolling: true, - secret, - store: sessionStore, - resave: false, - saveUninitialized: false, - // Set the cookie name to a javascript identifier compatible string. Makes code handling it - // cleaner :) - name: 'express_sid', - cookie: { - maxAge: sessionLifetime || null, // Convert 0 to null. - sameSite: settings.cookie.sameSite, + propagateTouch: true, + rolling: true, + secret, + store: sessionStore, + resave: false, + saveUninitialized: false, + // Set the cookie name to a javascript identifier compatible string. Makes code handling it + // cleaner :) + name: 'express_sid', + cookie: { + maxAge: sessionLifetime || null, // Convert 0 to null. + sameSite: settings.cookie.sameSite, - // The automatic express-session mechanism for determining if the application is being served - // over ssl is similar to the one used for setting the language cookie, which check if one of - // these conditions is true: - // - // 1. we are directly serving the nodejs application over SSL, using the "ssl" options in - // settings.json - // - // 2. we are serving the nodejs application in plaintext, but we are using a reverse proxy - // that terminates SSL for us. In this case, the user has to set trustProxy = true in - // settings.json, and the information wheter the application is over SSL or not will be - // extracted from the X-Forwarded-Proto HTTP header - // - // Please note that this will not be compatible with applications being served over http and - // https at the same time. - // - // reference: https://github.com/expressjs/session/blob/v1.17.0/README.md#cookiesecure - secure: 'auto', - }, + // The automatic express-session mechanism for determining if the application is being served + // over ssl is similar to the one used for setting the language cookie, which check if one of + // these conditions is true: + // + // 1. we are directly serving the nodejs application over SSL, using the "ssl" options in + // settings.json + // + // 2. we are serving the nodejs application in plaintext, but we are using a reverse proxy + // that terminates SSL for us. In this case, the user has to set trustProxy = true in + // settings.json, and the information wheter the application is over SSL or not will be + // extracted from the X-Forwarded-Proto HTTP header + // + // Please note that this will not be compatible with applications being served over http and + // https at the same time. + // + // reference: https://github.com/expressjs/session/blob/v1.17.0/README.md#cookiesecure + secure: 'auto', + }, }); + // Give plugins an opportunity to install handlers/middleware before the express-session // middleware. This allows plugins to avoid creating an express-session record in the database // when it is not needed (e.g., public static content). - await hooks.aCallAll('expressPreSession', {app:fastify}); - fastify.use(exports.sessionMiddleware); + await hooks.aCallAll('expressPreSession', {app: fastify}); - fastify.use(webaccess.checkAccess); + /*fastify.addHook('preHandler', (req, res, next) => { + cookieParser(secret, {})(req,res,next) + })*/ + + + fastify.register(fastifyCookie, { + hook: 'onRequest', + secret + }) + fastify.register(fastifySession, { + secret, + cookie: { + secure: 'auto', + maxAge: sessionLifetime || null, + sameSite: settings.cookie.sameSite, + }, + saveUninitialized: false, + prefix: 's:', + rolling: true, cookieName: 'express_sid', + }) + + fastify.addHook('preHandler', (request, reply, next) => { + request.session.user = "max"; + next() + }) + + /*fastify.addHook('preHandler', (req, res, next) => { + exports.sessionMiddleware(req, res, next); + })*/ + + fastify.addHook('preHandler', (req, res, next) => { + webaccess.checkAccess(req, res, next); + }) await Promise.all([ - hooks.aCallAll('expressConfigure', {app: fastify}), - hooks.aCallAll('expressCreateServer', {app:fastify, server: fastify}), + hooks.aCallAll('expressConfigure', {app: fastify}), + hooks.aCallAll('expressCreateServer', {app: fastify, server: fastify}), ]); exports.server.on('connection', (socket: Socket) => { - sockets.add(socket); - socketsEvents.emit('updated'); - socket.on('close', () => { - sockets.delete(socket); + sockets.add(socket); socketsEvents.emit('updated'); - }); + socket.on('close', () => { + sockets.delete(socket); + socketsEvents.emit('updated'); + }); }); + + console.log('Listening on port: ' + settings.port) // Run the server! - await fastify.listen({ - port: settings.port, - }) - startTime.setValue(Date.now()); + await fastify.listen({ + port: settings.port, + }) + startTime.setValue(Date.now()); logger.info('HTTP server listening for connections'); } -exports.shutdown = async (hookName:string, context: any) => { - await closeServer(); +exports.shutdown = async (hookName: string, context: any) => { + await closeServer(); }; diff --git a/src/node/hooks/express/admin.ts b/src/node/hooks/express/admin.ts index 5a88379f2..f2c1aedf4 100644 --- a/src/node/hooks/express/admin.ts +++ b/src/node/hooks/express/admin.ts @@ -2,7 +2,7 @@ import {ArgsExpressType} from "../../types/ArgsExpressType"; import path from "path"; import fs from "fs"; -import express from "express"; + const settings = require('ep_etherpad-lite/node/utils/Settings'); const ADMIN_PATH = path.join(settings.root, 'src', 'templates', 'admin'); @@ -14,13 +14,21 @@ const ADMIN_PATH = path.join(settings.root, 'src', 'templates', 'admin'); * @param {Function} cb the callback function * @return {*} */ -exports.expressCreateServer = (hookName:string, args: ArgsExpressType, cb:Function): any => { - args.app.use('/admin/', express.static(path.join(__dirname, '../../../templates/admin'), {maxAge: 1000 * 60 * 60 * 24})); - args.app.get('/admin/*', (_request:any, response:any)=>{ - response.sendFile(path.resolve(__dirname,'../../../templates/admin', 'index.html')); - } ) - args.app.get('/admin', (req:any, res:any, next:Function) => { - if ('/' !== req.path[req.path.length - 1]) return res.redirect('./admin/'); - }) - return cb(); +exports.expressCreateServer = (hookName: string, args: ArgsExpressType, cb: Function): any => { + args.app.register((instance, opts, next) => { + instance.register(require('@fastify/static'), { + root: ADMIN_PATH, + prefix: '/', // optional: default '/' + constraints: {} // optional: default {} + }) + instance.setNotFoundHandler((req, res) => { + const index = path.join(ADMIN_PATH, 'index.html'); + const file = fs.readFileSync(index, 'utf8') + res.type('text/html').send(file); + }) + next() + }, { + prefix: '/admin' + }) + return cb(); }; diff --git a/src/node/hooks/express/apicalls.ts b/src/node/hooks/express/apicalls.ts index 91c44e389..7b4e02834 100644 --- a/src/node/hooks/express/apicalls.ts +++ b/src/node/hooks/express/apicalls.ts @@ -1,40 +1,45 @@ 'use strict'; +import {FastifyInstance} from "fastify"; + const log4js = require('log4js'); const clientLogger = log4js.getLogger('client'); -const {Formidable} = require('formidable'); const apiHandler = require('../../handler/APIHandler'); const util = require('util'); -exports.expressPreSession = async (hookName:string, {app}:any) => { +exports.expressPreSession = async (hookName:string, {app}:{ + app: FastifyInstance +}) => { // The Etherpad client side sends information about how a disconnect happened app.post('/ep/pad/connection-diagnostic-info', async (req:any, res:any) => { - const [fields, files] = await (new Formidable({})).parse(req); + /*const [fields, files] = await (new Formidable({})).parse(req); clientLogger.info(`DIAGNOSTIC-INFO: ${fields.diagnosticInfo}`); - res.end('OK'); + */ + res.send('ok'); }); - const parseJserrorForm = async (req:any) => { - const form = new Formidable({ - maxFileSize: 1, // Files are not expected. Not sure if 0 means unlimited, so 1 is used. - }); - const [fields, files] = await form.parse(req); - return fields.errorInfo; - }; - // The Etherpad client side sends information about client side javscript errors - app.post('/jserror', (req:any, res:any, next:Function) => { - (async () => { - const data = JSON.parse(await parseJserrorForm(req)); - clientLogger.warn(`${data.msg} --`, { - [util.inspect.custom]: (depth: number, options:any) => { - // Depth is forced to infinity to ensure that all of the provided data is logged. - options = Object.assign({}, options, {depth: Infinity, colors: true}); - return util.inspect(data, options); - }, - }); - res.end('OK'); - })().catch((err) => next(err || new Error(err))); + app.post<{ + Body: { + errorId: string, + type: string, + msg: string, + url: string, + source: string, + linenumber: number, + userAgent: string, + stack: string, + } + }>('/jserror', async (req, res) => { + + clientLogger.warn(`${req.body.msg} --`, { + [util.inspect.custom]: (depth: number, options: any) => { + // Depth is forced to infinity to ensure that all of the provided data is logged. + options = Object.assign({}, options, {depth: Infinity, colors: true}); + return util.inspect(req.body, options); + }, + }); + res.send('ok'); }); // Provide a possibility to query the latest available API version diff --git a/src/node/hooks/express/errorhandling.ts b/src/node/hooks/express/errorhandling.ts index 2de819b0e..bd5d25672 100644 --- a/src/node/hooks/express/errorhandling.ts +++ b/src/node/hooks/express/errorhandling.ts @@ -9,14 +9,16 @@ exports.expressCreateServer = (hook_name:string, args: ArgsExpressType, cb:Funct exports.app = args.app; // Handle errors - args.app.use((err:ErrorCaused, req:any, res:any, next:Function) => { + args.app.setErrorHandler((error, request, reply) => { // if an error occurs Connect will pass it down // through these "error-handling" middleware // allowing you to respond however you like - res.status(500).send({error: 'Sorry, something bad happened!'}); - console.error(err.stack ? err.stack : err.toString()); + console.log('Error:', error); + console.log('Request:', request.url); + reply.status(500).send({error: 'Sorry, something bad happened!'}); + console.error(error.stack ? error.stack : error.toString()); stats.meter('http500').mark(); - }); + }) return cb(); }; diff --git a/src/node/hooks/express/importexport.ts b/src/node/hooks/express/importexport.ts index 1cc6bcdf4..740ae2758 100644 --- a/src/node/hooks/express/importexport.ts +++ b/src/node/hooks/express/importexport.ts @@ -11,11 +11,18 @@ const readOnlyManager = require('../../db/ReadOnlyManager'); const rateLimit = require('express-rate-limit'); const securityManager = require('../../db/SecurityManager'); const webaccess = require('./webaccess'); +import fLimiter from '@fastify/rate-limit' -exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Function) => { +exports.expressCreateServer = async (hookName: string, args: ArgsExpressType, cb: Function) => { + + + await args.app.register(fLimiter, { + global: false, + ...settings.importExportRateLimiting + }) const limiter = rateLimit({ ...settings.importExportRateLimiting, - handler: (request:any) => { + handler: (request: any) => { if (request.rateLimit.current === request.rateLimit.limit + 1) { // when the rate limiter triggers, write a warning in the logs console.warn('Import/Export rate limiter triggered on ' + @@ -25,60 +32,78 @@ exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Functio }); - const handleImport = (req:any, res:any, next:Function) => { - (async () => { - const types = ['pdf', 'doc', 'txt', 'html', 'odt', 'etherpad']; - // send a 404 if we don't support this filetype - if (types.indexOf(req.params.type) === -1) { - return next(); + const handleImport = async (req: any, res: any) => { + const types = ['pdf', 'doc', 'txt', 'html', 'odt', 'etherpad']; + // send a 404 if we don't support this filetype + if (types.indexOf(req.params.type) === -1) { + return res.send(409); + } + + // if abiword is disabled, and this is a format we only support with abiword, output a message + if (settings.exportAvailable() === 'no' && + ['odt', 'pdf', 'doc'].indexOf(req.params.type) !== -1) { + console.error(`Impossible to export pad "${req.params.pad}" in ${req.params.type} format.` + + ' There is no converter configured'); + + // ACHTUNG: do not include req.params.type in res.send() because there is + // no HTML escaping and it would lead to an XSS + res.send('This export is not enabled at this Etherpad instance. Set the path to Abiword' + + ' or soffice (LibreOffice) in settings.json to enable this feature'); + return; + } + + res.header('Access-Control-Allow-Origin', '*'); + + if (await hasPadAccess(req, res)) { + let padId = req.params.pad; + + let readOnlyId = null; + if (readOnlyManager.isReadOnlyId(padId)) { + readOnlyId = padId; + padId = await readOnlyManager.getPadId(readOnlyId); } - // if abiword is disabled, and this is a format we only support with abiword, output a message - if (settings.exportAvailable() === 'no' && - ['odt', 'pdf', 'doc'].indexOf(req.params.type) !== -1) { - console.error(`Impossible to export pad "${req.params.pad}" in ${req.params.type} format.` + - ' There is no converter configured'); - - // ACHTUNG: do not include req.params.type in res.send() because there is - // no HTML escaping and it would lead to an XSS - res.send('This export is not enabled at this Etherpad instance. Set the path to Abiword' + - ' or soffice (LibreOffice) in settings.json to enable this feature'); - return; + const exists = await padManager.doesPadExists(padId); + if (!exists) { + console.warn(`Someone tried to export a pad that doesn't exist (${padId})`); + return { + code: 404, + message: 'notfound', + }; } - res.header('Access-Control-Allow-Origin', '*'); - - if (await hasPadAccess(req, res)) { - let padId = req.params.pad; - - let readOnlyId = null; - if (readOnlyManager.isReadOnlyId(padId)) { - readOnlyId = padId; - padId = await readOnlyManager.getPadId(readOnlyId); - } - - const exists = await padManager.doesPadExists(padId); - if (!exists) { - console.warn(`Someone tried to export a pad that doesn't exist (${padId})`); - return next(); - } - - console.log(`Exporting pad "${req.params.pad}" in ${req.params.type} format`); - await exportHandler.doExport(req, res, padId, readOnlyId, req.params.type); - } - })().catch((err) => next(err || new Error(err))); + console.log(`Exporting pad "${req.params.pad}" in ${req.params.type} format`); + await exportHandler.doExport(req, res, padId, readOnlyId, req.params.type); + } }; // handle export requests - args.app.use('/p/:pad/export/:type', limiter); - args.app.use('/p/:pad/:rev/export/:type', limiter); - args.app.get('/p/:pad/:rev/export/:type', handleImport); - args.app.get('/p/:pad/export/:type', handleImport); + args.app.get('/p/:pad/:rev/export/:type',{ + config: { + rateLimit: { + ...settings.importExportRateLimiting + } + } + }, handleImport); + args.app.get('/p/:pad/export/:type',{ + config: { + rateLimit: { + ...settings.importExportRateLimiting + } + } + }, handleImport); // handle import requests - args.app.use('/p/:pad/import', limiter); - args.app.post('/p/:pad/import', async (req: any, res: any, next: Function) => { + args.app.get('/p/:pad/import',{ + config: { + rateLimit: { + ...settings.importExportRateLimiting + } + } + }, limiter); + args.app.post('/p/:pad/import', async (req: any, res: any) => { + // @ts-ignore const {session: {user} = {}} = req; const {accessStatus, authorID: authorId} = await securityManager.checkAccess( req.params.pad, req.cookies.sessionID, req.cookies.token, user); diff --git a/src/node/hooks/express/openapi.ts b/src/node/hooks/express/openapi.ts index aa2f1e483..65ba2d2fb 100644 --- a/src/node/hooks/express/openapi.ts +++ b/src/node/hooks/express/openapi.ts @@ -658,7 +658,7 @@ exports.expressPreSession = async (hookName:string, {app}:any) => { // start and bind to express api.init(); - app.use(apiRoot, async (req:any, res:any) => { + app.get(apiRoot, async (req:any, res:any) => { let response = null; try { if (style === APIPathStyle.REST) { diff --git a/src/node/hooks/express/socketio.ts b/src/node/hooks/express/socketio.ts index c7f59b038..7e94efacf 100644 --- a/src/node/hooks/express/socketio.ts +++ b/src/node/hooks/express/socketio.ts @@ -8,6 +8,8 @@ import log4js from 'log4js'; const proxyaddr = require('proxy-addr'); const settings = require('../../utils/Settings'); import {Server, Socket} from 'socket.io' +import path from "path"; +import {readFileSync} from "fs"; const socketIORouter = require('../../handler/SocketIORouter'); const hooks = require('../../../static/js/pluginfw/hooks'); const padMessageHandler = require('../../handler/PadMessageHandler'); @@ -76,6 +78,12 @@ export const expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Fu maxHttpBufferSize: settings.socketIo.maxHttpBufferSize, }) + args.app.get('/socket.io/socket.io.js', (_req,res:any) => { + res.header('Content-Type', 'application/javascript; charset=utf-8'); + const socketIo = readFileSync(path.join(settings.root, 'src', 'node_modules', 'socket.io-client', 'dist', 'socket.io.min.js'), 'utf8'); + res.header('Cache-Control', `public, max-age=${settings.maxAge}`); + res.send(socketIo); + }) const handleConnection = (socket:Socket) => { sockets.add(socket); diff --git a/src/node/hooks/express/specialpages.ts b/src/node/hooks/express/specialpages.ts index 9812a682f..d744b703a 100644 --- a/src/node/hooks/express/specialpages.ts +++ b/src/node/hooks/express/specialpages.ts @@ -73,12 +73,13 @@ exports.expressPreSession = async (hookName:string, {app}:any) => { exports.expressCreateServer = (hookName:string, args:any, cb:Function) => { // serve index.html under / args.app.get('/', (req:any, res:any) => { + console.log('GET /') res.type('text/html').send(eejs.require('ep_etherpad-lite/templates/index.html', {req})) //res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req})); }); // serve pad.html under /p - args.app.get('/p/:pad', (req:any, res:any, next:Function) => { + args.app.get('/p/:pad', (req:any, res:any) => { // The below might break for pads being rewritten const isReadOnly = !webaccess.userCanModify(req.params.pad, req); @@ -97,7 +98,7 @@ exports.expressCreateServer = (hookName:string, args:any, cb:Function) => { }); // serve timeslider.html under /p/$padname/timeslider - args.app.get('/p/:pad/timeslider', (req:any, res:any, next:Function) => { + args.app.get('/p/:pad/timeslider', (req:any, res:any) => { hooks.callAll('padInitToolbar', { toolbar, }); diff --git a/src/node/hooks/express/static.ts b/src/node/hooks/express/static.ts index fc581ef46..8014faacf 100644 --- a/src/node/hooks/express/static.ts +++ b/src/node/hooks/express/static.ts @@ -9,6 +9,7 @@ const path = require('path'); const plugins = require('../../../static/js/pluginfw/plugin_defs'); const settings = require('../../utils/Settings'); import CachingMiddleware from '../../utils/caching_middleware'; +import {FastifyInstance} from "fastify"; const Yajsml = require('etherpad-yajsml'); // Rewrite tar to include modules with no extensions and proper rooted paths. @@ -32,7 +33,9 @@ const getTar = async () => { return tar; }; -exports.expressPreSession = async (hookName:string, {app}:any) => { +exports.expressPreSession = async (hookName:string, {app}: { + app: FastifyInstance +}) => { // Cache both minified and static. const assetCache = new CachingMiddleware(); // Cache static assets @@ -40,9 +43,10 @@ exports.expressPreSession = async (hookName:string, {app}:any) => { app.get('/static/js/require-kernel.js', (req:any, res:any) => { res.header('Content-Type', 'application/javascript; charset=utf-8'); + const RequireKernel = require('etherpad-require-kernel'); + const requireDefinition = () => `var require = ${RequireKernel.kernelSource};\n`; res.header('Cache-Control', `public, max-age=${settings.maxAge}`); - const file = fs.readFile(path.join(settings.root, 'src/static/js/require-kernel.js'), 'utf8'); - res.send(); + res.send(requireDefinition()); }) app.route({ @@ -57,19 +61,18 @@ exports.expressPreSession = async (hookName:string, {app}:any) => { handler: assetCache.handle.bind(assetCache) }); - /*app.register(require('@fastify/static'), { + app.register(require('@fastify/static'), { root: path.join(settings.root, 'src', 'static'), prefix: '/static/', // optional: default '/' - constraints: {} // optional: default {} - })*/ + }) // Minify will serve static files compressed (minify enabled). It also has // file-specific hacks for ace/require-kernel/etc. - app.route({ + /*app.route({ method: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'], url: '/static/*', handler: minify.minify - }); + });*/ // Setup middleware that will package JavaScript files served by minify for // CommonJS loader on the client-side. @@ -87,12 +90,19 @@ exports.expressPreSession = async (hookName:string, {app}:any) => { const associator = new StaticAssociator(associations); jsServer.setAssociator(associator); - app.use(jsServer.handle.bind(jsServer)); + app.addHook('onRequest', (req, res, done) => { + if(req.url.startsWith('/javascripts')){ + console.log('GET /javascripts in handler', req.url) + res.header('Content-Type', 'application/javascript; charset=utf-8'); + return jsServer.handle(req, res); + } + done() + }) // serve plugin definitions // not very static, but served here so that client can do // require("pluginfw/static/js/plugin-definitions.js"); - app.get('/pluginfw/plugin-definitions.json', (req: any, res:any, next:Function) => { + app.get('/pluginfw/plugin-definitions.json', (req: any, res:any) => { const clientParts = plugins.parts.filter((part: PartType) => part.client_hooks != null); const clientPlugins:MapArrayType = {}; for (const name of new Set(clientParts.map((part: PartType) => part.plugin))) { diff --git a/src/node/hooks/express/webaccess.ts b/src/node/hooks/express/webaccess.ts index 0034f87c8..706b35c8b 100644 --- a/src/node/hooks/express/webaccess.ts +++ b/src/node/hooks/express/webaccess.ts @@ -5,6 +5,7 @@ import log4js from 'log4js'; import {SocketClientRequest} from "../../types/SocketClientRequest"; import {WebAccessTypes} from "../../types/WebAccessTypes"; import {SettingsUser} from "../../types/SettingsUser"; +import {FastifyReply, FastifyRequest} from "fastify"; const httpLogger = log4js.getLogger('http'); const settings = require('../../utils/Settings'); const hooks = require('../../../static/js/pluginfw/hooks'); @@ -14,6 +15,7 @@ hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure ho // Promisified wrapper around hooks.aCallFirst. const aCallFirst = (hookName: string, context:any, pred = null) => new Promise((resolve, reject) => { + console.log(hookName) hooks.aCallFirst(hookName, context, (err:any, r: unknown) => err != null ? reject(err) : resolve(r), pred); }); @@ -49,8 +51,8 @@ exports.userCanModify = (padId: string, req: SocketClientRequest) => { // Exported so that tests can set this to 0 to avoid unnecessary test slowness. exports.authnFailureDelayMs = 1000; -const checkAccess = async (req:any, res:any, next: Function) => { - const requireAdmin = req.path.toLowerCase().startsWith('/admin-auth'); +const checkAccess = async (req:FastifyRequest, res: FastifyReply, next: Function) => { + const requireAdmin = req.url.toLowerCase().startsWith('/admin-auth'); // /////////////////////////////////////////////////////////////////////////////////////////////// // Step 1: Check the preAuthorize hook for early permit/deny (permit is only allowed for non-admin @@ -94,10 +96,12 @@ const checkAccess = async (req:any, res:any, next: Function) => { const authorize = async () => { const grant = async (level: string|false) => { level = exports.normalizeAuthzLevel(level); + console.log("Level is", level) if (!level) return false; const user = req.session.user; + console.log("User is", user) if (user == null) return true; // This will happen if authentication is not required. - const encodedPadId = (req.path.match(/^\/p\/([^/]*)/) || [])[1]; + const encodedPadId = (req.url.match(/^\/p\/([^/]*)/) || [])[1]; if (encodedPadId == null) return true; let padId = decodeURIComponent(encodedPadId); if (readOnlyManager.isReadOnlyId(padId)) { @@ -118,15 +122,17 @@ const checkAccess = async (req:any, res:any, next: Function) => { if (!isAuthenticated) return await grant(false); if (requireAdmin && !req.session.user.is_admin) return await grant(false); if (!settings.requireAuthorization) return await grant('create'); - return await grant(await aCallFirst0('authorize', {req, res, next, resource: req.path})); + const level = await aCallFirst0('authorize', {req, res, next, resource: req.url}) + return await grant(level); }; // /////////////////////////////////////////////////////////////////////////////////////////////// // Step 2: Try to just access the thing. If access fails (perhaps authentication has not yet // completed, or maybe different credentials are required), go to the next step. // /////////////////////////////////////////////////////////////////////////////////////////////// - + console.log("Authorize") if (await authorize()) { + console.log("Authorize2") if(requireAdmin) { res.status(200).send('Authorized') return @@ -149,7 +155,7 @@ const checkAccess = async (req:any, res:any, next: Function) => { const httpBasicAuth = req.headers.authorization && req.headers.authorization.startsWith('Basic '); if (httpBasicAuth) { const userpass = - Buffer.from(req.headers.authorization.split(' ')[1], 'base64').toString().split(':'); + Buffer.from(req.headers.authorization!.split(' ')[1], 'base64').toString().split(':'); ctx.username = userpass.shift(); // Prevent prototype pollution vulnerabilities in plugins. This also silences a prototype // pollution warning below (when setting settings.users[ctx.username]) that isn't actually a @@ -161,7 +167,6 @@ const checkAccess = async (req:any, res:any, next: Function) => { // Fall back to HTTP basic auth. // @ts-ignore const {[ctx.username]: {password} = {}} = settings.users as SettingsUser; - if (!httpBasicAuth || !ctx.username || password == null || password.toString() !== ctx.password) { @@ -172,8 +177,8 @@ const checkAccess = async (req:any, res:any, next: Function) => { //res.header('WWW-Authenticate', 'Basic realm="Protected Area"'); // Delay the error response for 1s to slow down brute force attacks. await new Promise((resolve) => setTimeout(resolve, exports.authnFailureDelayMs)); - res.status(401).send('Authentication Required'); - return; + await res.status(401).send('Authentication Required'); + throw new Error('Authentication Required'); } settings.users[ctx.username].username = ctx.username; // Make a shallow copy so that the password property can be deleted (to prevent it from @@ -181,9 +186,10 @@ const checkAccess = async (req:any, res:any, next: Function) => { req.session.user = {...settings.users[ctx.username]}; delete req.session.user.password; } + console.log("Session is", req.session) if (req.session.user == null) { httpLogger.error('authenticate hook failed to add user settings to session'); - return res.status(500).send('Internal Server Error'); + await res.status(500).send('Internal Server Error'); } const {username = ''} = req.session.user; httpLogger.info(`Successful authentication from IP ${req.ip} for user ${username}`); @@ -195,22 +201,25 @@ const checkAccess = async (req:any, res:any, next: Function) => { // /////////////////////////////////////////////////////////////////////////////////////////////// const auth = await authorize() - if (auth && !requireAdmin) return next(); - if(auth && requireAdmin) { - res.status(200).send('Authorized') - return + if (auth && !requireAdmin) { + return next(); } + if(auth && requireAdmin) { + console.log(auth+"-------Require admin") + res.code(200).send('Authorized') + } + console.log('authzFailure') if (await aCallFirst0('authzFailure', {req, res})) return; if (await aCallFirst0('authFailure', {req, res, next})) return; // No plugin handled the authorization failure. - res.status(403).send('Forbidden'); + await res.code(403).send('Forbidden'); }; /** * Express middleware to authenticate the user and check authorization. Must be installed after the * express-session middleware. */ -exports.checkAccess = (req:any, res:any, next:Function) => { - checkAccess(req, res, next).catch((err) => next(err || new Error(err))); +exports.checkAccess = async (req: any, res: any, next: Function) => { + return await checkAccess(req, res, next).catch((err) => next(err || new Error(err))); }; diff --git a/src/node/types/ArgsExpressType.ts b/src/node/types/ArgsExpressType.ts index 5c0675b97..c792f06e6 100644 --- a/src/node/types/ArgsExpressType.ts +++ b/src/node/types/ArgsExpressType.ts @@ -1,5 +1,7 @@ +import {FastifyInstance} from "fastify"; + export type ArgsExpressType = { - app:any, + app:FastifyInstance, io: any, server:any -} \ No newline at end of file +} diff --git a/src/node/types/WebAccessTypes.ts b/src/node/types/WebAccessTypes.ts index b351f059f..16484a24a 100644 --- a/src/node/types/WebAccessTypes.ts +++ b/src/node/types/WebAccessTypes.ts @@ -5,6 +5,6 @@ export type WebAccessTypes = { password?: string; req:any; res:any; - next:any; + next:Function; users: SettingsUser; } diff --git a/src/node/utils/sanitizePathname.ts b/src/node/utils/sanitizePathname.ts index e7d2cd417..82dc461cf 100644 --- a/src/node/utils/sanitizePathname.ts +++ b/src/node/utils/sanitizePathname.ts @@ -5,7 +5,6 @@ import path from 'path'; // Normalizes p and ensures that it is a relative path that does not reach outside. See // https://nvd.nist.gov/vuln/detail/CVE-2015-3297 for additional context. module.exports = (p: string, pathApi = path) => { - console.log('p:', p); // The documentation for path.normalize() says that it resolves '..' and '.' segments. The word // "resolve" implies that it examines the filesystem to resolve symbolic links, so 'a/../b' might // not be the same thing as 'b'. Most path normalization functions from other libraries (e.g., diff --git a/src/package.json b/src/package.json index 7811fcc4d..3f87907d4 100644 --- a/src/package.json +++ b/src/package.json @@ -30,7 +30,11 @@ } ], "dependencies": { + "@fastify/compress": "^7.0.0", + "@fastify/cookie": "^9.3.1", "@fastify/express": "^2.3.0", + "@fastify/rate-limit": "^9.1.0", + "@fastify/session": "^10.7.0", "@fastify/static": "^7.0.1", "async": "^3.2.5", "axios": "^1.6.8", @@ -40,7 +44,6 @@ "ejs": "^3.1.9", "etherpad-require-kernel": "^1.0.16", "etherpad-yajsml": "0.0.12", - "express": "4.18.3", "express-rate-limit": "^7.2.0", "express-session": "npm:@etherpad/express-session@^1.18.2", "fast-deep-equal": "^3.1.3", @@ -88,7 +91,6 @@ "devDependencies": { "@playwright/test": "^1.42.1", "@types/async": "^3.2.24", - "@types/express": "^4.17.21", "@types/http-errors": "^2.0.4", "@types/jsdom": "^21.1.6", "@types/mocha": "^10.0.6", diff --git a/src/static/js/pad.js b/src/static/js/pad.js index b9f08ba6a..df409372a 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -23,13 +23,12 @@ */ let socket; -const requireFromUrl = require('require-from-url/sync'); // These jQuery things should create local references, but for now `require()` // assigns to the global `$` and augments it with plugins. -requireFromUrl('./vendors/jquery'); -requireFromUrl('./vendors/farbtastic'); -requireFromUrl('./vendors/gritter'); +require('./vendors/jquery'); +require('./vendors/farbtastic'); +require('./vendors/gritter'); const Cookies = require('./pad_utils').Cookies; const chat = require('./chat').chat; diff --git a/src/static/js/pad_utils.js b/src/static/js/pad_utils.js index 58105d23c..64bd56092 100644 --- a/src/static/js/pad_utils.js +++ b/src/static/js/pad_utils.js @@ -357,7 +357,7 @@ let globalExceptionHandler = null; padutils.setupGlobalExceptionHandler = () => { if (globalExceptionHandler == null) { require('./vendors/gritter'); - globalExceptionHandler = (e) => { + globalExceptionHandler = async (e) => { let type; let err; let msg, url, linenumber; @@ -410,8 +410,9 @@ padutils.setupGlobalExceptionHandler = () => { } // send javascript errors to the server - $.post('../jserror', { - errorInfo: JSON.stringify({ + await fetch('../jserror', { + method: 'POST', + body: JSON.stringify({ errorId, type, msg,