From 048f53570a87a6ed92aa5b3d62f9c0fb721f5f55 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Dec 2021 14:58:11 -0800 Subject: [PATCH] Bump actionhero from 27.2.0 to 28.0.0 (#163) * Bump actionhero from 27.2.0 to 28.0.0 Bumps [actionhero](https://github.com/actionhero/actionhero) from 27.2.0 to 28.0.0. - [Release notes](https://github.com/actionhero/actionhero/releases) - [Commits](https://github.com/actionhero/actionhero/compare/v27.2.0...v28.0.0) --- updated-dependencies: - dependency-name: actionhero dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * Actionhero v28 * remove action test * readme Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Evan Tahler --- README.md | 50 ++++---- __tests__/servers/socket.ts | 2 +- package-lock.json | 217 +++++++++++++++++++++----------- package.json | 4 +- src/config/api.ts | 26 ++-- src/config/errors.ts | 61 +++++---- src/config/logger.ts | 113 +++++++++++++++++ src/config/plugins.ts | 37 ++++++ src/config/redis.ts | 112 +++++++++++++++++ src/config/routes.ts | 44 +++++++ src/config/servers/socket.ts | 40 ------ src/config/servers/web.ts | 9 -- src/config/servers/websocket.ts | 9 -- src/config/socket.ts | 42 +++++++ src/config/tasks.ts | 84 +++++++++++++ src/config/web.ts | 134 ++++++++++++++++++++ src/config/websocket.ts | 62 +++++++++ src/servers/socket.ts | 38 +++--- 18 files changed, 885 insertions(+), 199 deletions(-) create mode 100644 src/config/logger.ts create mode 100644 src/config/plugins.ts create mode 100644 src/config/redis.ts create mode 100644 src/config/routes.ts delete mode 100644 src/config/servers/socket.ts delete mode 100644 src/config/servers/web.ts delete mode 100644 src/config/servers/websocket.ts create mode 100644 src/config/socket.ts create mode 100644 src/config/tasks.ts create mode 100644 src/config/web.ts create mode 100644 src/config/websocket.ts diff --git a/README.md b/README.md index 724f95a..133f439 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ As of Actionhero v21, the socket server is not included with Actionhero by default. You can add it (this package) via `npm install actionhero-socket-server`. +As of version `3.0.0` of this package, Actionhero v28+ is required. + ```shell ❯ telnet localhost 5000 Trying 127.0.0.1... @@ -20,7 +22,7 @@ Connection closed by foreign host. ## Installation 1. Add the package to your actionhero project: `npm install actionhero-socket-server --save` -2. Copy the config file into your project `cp ./node_modules/actionhero-socket-server/src/config/servers/socket.ts src/config/servers/socket.ts` +2. Copy the config file into your project `cp ./node_modules/actionhero-socket-server/src/config/socket.ts src/config/socket.ts` 3. Enable the plugin: ```ts @@ -64,27 +66,33 @@ socket: error => { All options are exposed via the config file: ```ts +const namespace = "socket"; + +declare module "actionhero" { + export interface ActionheroConfigInterface { + [namespace]: ReturnType; + } +} + export const DEFAULT = { - servers: { - socket: (config) => { - return { - enabled: true, - // TCP or TLS? - secure: false, - // Passed to tls.createServer if secure=true. Should contain SSL certificates - serverOptions: {}, - // Port or Socket - port: 5000, - // Which IP to listen on (use 0.0.0.0 for all) - bindIP: "0.0.0.0", - // Enable TCP KeepAlive pings on each connection? - setKeepAlive: false, - // Delimiter string for incoming messages - delimiter: "\n", - // Maximum incoming message string length in Bytes (use 0 for Infinite) - maxDataLength: 0, - }; - }, + [namespace]: () => { + return { + enabled: true, + // TCP or TLS? + secure: false, + // Passed to tls.createServer if secure=true. Should contain SSL certificates + serverOptions: {}, + // Port or Socket + port: 5000, + // Which IP to listen on (use 0.0.0.0 for all) + bindIP: "0.0.0.0", + // Enable TCP KeepAlive pings on each connection? + setKeepAlive: false, + // Delimiter string for incoming messages + delimiter: "\n", + // Maximum incoming message string length in Bytes (use 0 for Infinite) + maxDataLength: 0, + }; }, }; ``` diff --git a/__tests__/servers/socket.ts b/__tests__/servers/socket.ts index b4dff3f..49ba49f 100644 --- a/__tests__/servers/socket.ts +++ b/__tests__/servers/socket.ts @@ -48,7 +48,7 @@ const makeSocketRequest = async ( const buildClient = (): any => { return new Promise((resolve) => { - const conn = net.connect(config.servers.socket.port); + const conn = net.connect(config.socket.port?.toString()); // conn.data = ""; conn.on("connect", () => { conn.setEncoding("utf8"); diff --git a/package-lock.json b/package-lock.json index 2af6d27..44413dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "actionhero-socket-server", "version": "2.1.3", "license": "Apache-2.0", "dependencies": { @@ -13,7 +14,7 @@ "devDependencies": { "@types/jest": "^27.0.1", "@types/node": "^16.7.10", - "actionhero": "^27.0.3", + "actionhero": "^28.0.0", "jest": "^27.1.0", "prettier": "^2.3.2", "ts-jest": "^27.0.5", @@ -970,9 +971,9 @@ } }, "node_modules/@types/ioredis": { - "version": "4.27.8", - "resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.27.8.tgz", - "integrity": "sha512-THsyghYuFI6h/UzwEaeSeagRxiDG1P/NIiL5uTjN7bcbQHwDP6nMWJfEmY0iuu3pOzl1j0FgRpAYz7WWX2eW0Q==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.28.1.tgz", + "integrity": "sha512-raYHPqRWrfnEoym94BY28mG1+tcZqh3dsp2q7x5IyMAAEvIdu+H0X8diASMpncIm+oHyH9dalOeOnGOL/YnuOA==", "dev": true, "dependencies": { "@types/node": "*" @@ -1141,28 +1142,27 @@ } }, "node_modules/actionhero": { - "version": "27.2.0", - "resolved": "https://registry.npmjs.org/actionhero/-/actionhero-27.2.0.tgz", - "integrity": "sha512-CX0u8tFdvsv6hYxN3xiSV5tpQn768f0aOBEJcFN4CqECMkgY1zg8y7goUZS3u9rX0m2kL6XSX3Q3kYXDfeXUUA==", + "version": "28.0.0", + "resolved": "https://registry.npmjs.org/actionhero/-/actionhero-28.0.0.tgz", + "integrity": "sha512-hDCwnr5bnXXffhJXLEAgZ7Ur7IPBQg15ViIwzj/mztW4fJHD+pnkfLxdQprfLVreYsKCq048oPr6JYpA0keQaA==", "dev": true, "hasInstallScript": true, "dependencies": { - "@types/ioredis": "^4.27.5", + "@types/ioredis": "^4.28.1", "browser_fingerprint": "^2.0.3", - "commander": "^8.2.0", + "commander": "^8.3.0", "dot-prop": "^6.0.1", "etag": "^1.8.1", - "formidable": "^1.2.2", + "formidable": "^2.0.1", "glob": "^7.2.0", - "ioredis": "^4.27.9", - "is-running": "^2.1.0", - "mime": "^2.5.2", - "node-resque": "^9.1.1", + "ioredis": "^4.28.0", + "mime": "^3.0.0", + "node-resque": "^9.1.2", "primus": "^8.0.5", "qs": "^6.10.1", "uuid": "^8.3.2", "winston": "^3.3.3", - "ws": "^8.2.2", + "ws": "^8.2.3", "yargs": "^17.2.1" }, "bin": { @@ -1294,6 +1294,12 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", + "dev": true + }, "node_modules/async": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", @@ -1900,9 +1906,9 @@ } }, "node_modules/denque": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz", - "integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz", + "integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==", "dev": true, "engines": { "node": ">=0.10" @@ -1917,6 +1923,16 @@ "node": ">=8" } }, + "node_modules/dezalgo": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", + "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=", + "dev": true, + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/diagnostics": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-2.0.2.tgz", @@ -2263,10 +2279,31 @@ } }, "node_modules/formidable": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", - "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==", - "dev": true + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz", + "integrity": "sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ==", + "dev": true, + "dependencies": { + "dezalgo": "1.0.3", + "hexoid": "1.0.0", + "once": "1.4.0", + "qs": "6.9.3" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/formidable/node_modules/qs": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz", + "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==", + "dev": true, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/forwarded-for": { "version": "1.1.0", @@ -2451,6 +2488,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/html-encoding-sniffer": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", @@ -2559,9 +2605,9 @@ "dev": true }, "node_modules/ioredis": { - "version": "4.27.9", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.27.9.tgz", - "integrity": "sha512-hAwrx9F+OQ0uIvaJefuS3UTqW+ByOLyLIV+j0EH8ClNVxvFyH9Vmb08hCL4yje6mDYT5zMquShhypkd50RRzkg==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.1.tgz", + "integrity": "sha512-7gcrUJEcPHWy+eEyq6wIZpXtfHt8crhbc5+z0sqrnHUkwBblXinygfamj+/jx83Qo+2LW3q87Nj2VsuH6BF2BA==", "dev": true, "dependencies": { "cluster-key-slot": "^1.1.0", @@ -2691,12 +2737,6 @@ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true }, - "node_modules/is-running": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-running/-/is-running-2.1.0.tgz", - "integrity": "sha1-MKc/9cw4VOT8JUkICen1q/jeCeA=", - "dev": true - }, "node_modules/is-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", @@ -3690,15 +3730,15 @@ "dev": true }, "node_modules/mime": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", - "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", "dev": true, "bin": { "mime": "cli.js" }, "engines": { - "node": ">=4.0.0" + "node": ">=10.0.0" } }, "node_modules/mime-db": { @@ -3840,9 +3880,9 @@ "dev": true }, "node_modules/node-resque": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/node-resque/-/node-resque-9.1.1.tgz", - "integrity": "sha512-+S5sB9247x8bsTKGzpANux9K2zBBdwlMzINtXU59JOca8D7AVdGfSwbJWt42yFkfu8klX5Z7xwM0oiWbjnYL2A==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/node-resque/-/node-resque-9.1.2.tgz", + "integrity": "sha512-BWBEf7NL7i8eZ6uPsBgDFIKVhbZwLkAGWN5maRltjYkIkuoNE70FQsL3BCu2CxPXmRSPGDJDsOz17p3ostOQGA==", "dev": true, "dependencies": { "ioredis": "^4.27.6" @@ -6017,9 +6057,9 @@ } }, "@types/ioredis": { - "version": "4.27.8", - "resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.27.8.tgz", - "integrity": "sha512-THsyghYuFI6h/UzwEaeSeagRxiDG1P/NIiL5uTjN7bcbQHwDP6nMWJfEmY0iuu3pOzl1j0FgRpAYz7WWX2eW0Q==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.28.1.tgz", + "integrity": "sha512-raYHPqRWrfnEoym94BY28mG1+tcZqh3dsp2q7x5IyMAAEvIdu+H0X8diASMpncIm+oHyH9dalOeOnGOL/YnuOA==", "dev": true, "requires": { "@types/node": "*" @@ -6175,27 +6215,26 @@ "dev": true }, "actionhero": { - "version": "27.2.0", - "resolved": "https://registry.npmjs.org/actionhero/-/actionhero-27.2.0.tgz", - "integrity": "sha512-CX0u8tFdvsv6hYxN3xiSV5tpQn768f0aOBEJcFN4CqECMkgY1zg8y7goUZS3u9rX0m2kL6XSX3Q3kYXDfeXUUA==", + "version": "28.0.0", + "resolved": "https://registry.npmjs.org/actionhero/-/actionhero-28.0.0.tgz", + "integrity": "sha512-hDCwnr5bnXXffhJXLEAgZ7Ur7IPBQg15ViIwzj/mztW4fJHD+pnkfLxdQprfLVreYsKCq048oPr6JYpA0keQaA==", "dev": true, "requires": { - "@types/ioredis": "^4.27.5", + "@types/ioredis": "^4.28.1", "browser_fingerprint": "^2.0.3", - "commander": "^8.2.0", + "commander": "^8.3.0", "dot-prop": "^6.0.1", "etag": "^1.8.1", - "formidable": "^1.2.2", + "formidable": "^2.0.1", "glob": "^7.2.0", - "ioredis": "^4.27.9", - "is-running": "^2.1.0", - "mime": "^2.5.2", - "node-resque": "^9.1.1", + "ioredis": "^4.28.0", + "mime": "^3.0.0", + "node-resque": "^9.1.2", "primus": "^8.0.5", "qs": "^6.10.1", "uuid": "^8.3.2", "winston": "^3.3.3", - "ws": "^8.2.2", + "ws": "^8.2.3", "yargs": "^17.2.1" }, "dependencies": { @@ -6290,6 +6329,12 @@ } } }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", + "dev": true + }, "async": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", @@ -6791,9 +6836,9 @@ "dev": true }, "denque": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz", - "integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz", + "integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==", "dev": true }, "detect-newline": { @@ -6802,6 +6847,16 @@ "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "dev": true }, + "dezalgo": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", + "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=", + "dev": true, + "requires": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "diagnostics": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-2.0.2.tgz", @@ -7073,10 +7128,24 @@ } }, "formidable": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", - "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==", - "dev": true + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz", + "integrity": "sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ==", + "dev": true, + "requires": { + "dezalgo": "1.0.3", + "hexoid": "1.0.0", + "once": "1.4.0", + "qs": "6.9.3" + }, + "dependencies": { + "qs": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz", + "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==", + "dev": true + } + } }, "forwarded-for": { "version": "1.1.0", @@ -7212,6 +7281,12 @@ "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", "dev": true }, + "hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", + "dev": true + }, "html-encoding-sniffer": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", @@ -7296,9 +7371,9 @@ "dev": true }, "ioredis": { - "version": "4.27.9", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.27.9.tgz", - "integrity": "sha512-hAwrx9F+OQ0uIvaJefuS3UTqW+ByOLyLIV+j0EH8ClNVxvFyH9Vmb08hCL4yje6mDYT5zMquShhypkd50RRzkg==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.1.tgz", + "integrity": "sha512-7gcrUJEcPHWy+eEyq6wIZpXtfHt8crhbc5+z0sqrnHUkwBblXinygfamj+/jx83Qo+2LW3q87Nj2VsuH6BF2BA==", "dev": true, "requires": { "cluster-key-slot": "^1.1.0", @@ -7391,12 +7466,6 @@ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true }, - "is-running": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-running/-/is-running-2.1.0.tgz", - "integrity": "sha1-MKc/9cw4VOT8JUkICen1q/jeCeA=", - "dev": true - }, "is-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", @@ -8181,9 +8250,9 @@ "dev": true }, "mime": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", - "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", "dev": true }, "mime-db": { @@ -8294,9 +8363,9 @@ "dev": true }, "node-resque": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/node-resque/-/node-resque-9.1.1.tgz", - "integrity": "sha512-+S5sB9247x8bsTKGzpANux9K2zBBdwlMzINtXU59JOca8D7AVdGfSwbJWt42yFkfu8klX5Z7xwM0oiWbjnYL2A==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/node-resque/-/node-resque-9.1.2.tgz", + "integrity": "sha512-BWBEf7NL7i8eZ6uPsBgDFIKVhbZwLkAGWN5maRltjYkIkuoNE70FQsL3BCu2CxPXmRSPGDJDsOz17p3ostOQGA==", "dev": true, "requires": { "ioredis": "^4.27.6" diff --git a/package.json b/package.json index c68ef3d..457f2d0 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Evan Tahler ", "name": "actionhero-socket-server", "description": "A TCP and JSON server for actionhero", - "version": "2.1.3", + "version": "3.0.0", "homepage": "http://www.actionherojs.com", "license": "Apache-2.0", "repository": { @@ -34,7 +34,7 @@ "devDependencies": { "@types/jest": "^27.0.1", "@types/node": "^16.7.10", - "actionhero": "^27.0.3", + "actionhero": "^28.0.0", "jest": "^27.1.0", "prettier": "^2.3.2", "ts-jest": "^27.0.5", diff --git a/src/config/api.ts b/src/config/api.ts index d272452..9f0493a 100644 --- a/src/config/api.ts +++ b/src/config/api.ts @@ -1,9 +1,19 @@ import * as path from "path"; import * as fs from "fs"; +import { PackageJson } from "type-fest"; +import { ActionheroLogLevel } from "actionhero"; + +const namespace = "general"; + +declare module "actionhero" { + export interface ActionheroConfigInterface { + [namespace]: ReturnType; + } +} export const DEFAULT = { - general: (config) => { - const packageJSON = JSON.parse( + [namespace]: () => { + const packageJSON: PackageJson = JSON.parse( fs .readFileSync(path.join(__dirname, "..", "..", "package.json")) .toString() @@ -12,6 +22,8 @@ export const DEFAULT = { return { apiVersion: packageJSON.version, serverName: packageJSON.name, + // you can manually set the server id (not recommended) + id: undefined as string, welcomeMessage: `Welcome to the ${packageJSON.name} api`, // A unique token to your application that servers will use to authenticate to each other serverToken: "change-me", @@ -30,15 +42,15 @@ export const DEFAULT = { // enable action response to logger enableResponseLogging: false, // params you would like hidden from any logs. Can be an array of strings or a method that returns an array of strings. - filteredParams: [], + filteredParams: [] as string[] | (() => string[]), // responses you would like hidden from any logs. Can be an array of strings or a method that returns an array of strings. - filteredResponse: [], + filteredResponse: [] as string[] | (() => string[]), // values that signify missing params missingParamChecks: [null, "", undefined], // The default filetype to server when a user requests a directory directoryFileType: "index.html", // What log-level should we use for file requests? - fileRequestLogLevel: "info", + fileRequestLogLevel: "info" as ActionheroLogLevel, // The default priority level given to middleware of all types (action, connection, say, and task) defaultMiddlewarePriority: 100, // Which channel to use on redis pub/sub for RPC communication @@ -74,7 +86,7 @@ export const DEFAULT = { }; export const test = { - general: (config) => { + [namespace]: () => { return { serverToken: `serverToken-${process.env.JEST_WORKER_ID || 0}`, startingChatRooms: { @@ -87,7 +99,7 @@ export const test = { }; export const production = { - general: (config) => { + [namespace]: () => { return { fileRequestLogLevel: "debug", }; diff --git a/src/config/errors.ts b/src/config/errors.ts index 8433046..350826e 100644 --- a/src/config/errors.ts +++ b/src/config/errors.ts @@ -1,5 +1,15 @@ +import { ActionProcessor, Connection } from "actionhero"; + +const namespace = "errors"; + +declare module "actionhero" { + export interface ActionheroConfigInterface { + [namespace]: ReturnType; + } +} + export const DEFAULT = { - errors: (config) => { + [namespace]: () => { return { _toExpand: false, @@ -12,28 +22,28 @@ export const DEFAULT = { serializers: { servers: { - web: (error) => { + web: (error: NodeJS.ErrnoException) => { if (error.message) { return String(error.message); } else { return error; } }, - websocket: (error) => { + websocket: (error: NodeJS.ErrnoException) => { if (error.message) { return String(error.message); } else { return error; } }, - socket: (error) => { + socket: (error: NodeJS.ErrnoException) => { if (error.message) { return String(error.message); } else { return error; } }, - specHelper: (error) => { + specHelper: (error: NodeJS.ErrnoException) => { if (error.message) { return "Error: " + String(error.message); } else { @@ -42,7 +52,8 @@ export const DEFAULT = { }, }, // See ActionProcessor#applyDefaultErrorLogLineFormat to see an example of how to customize - actionProcessor: null, + actionProcessor: + null as ActionProcessor["applyDefaultErrorLogLineFormat"], }, // /////////// @@ -50,41 +61,47 @@ export const DEFAULT = { // /////////// // When a params for an action is invalid - invalidParams: (data, validationErrors: string[]) => { + invalidParams: ( + data: ActionProcessor, + validationErrors: Array + ) => { if (validationErrors.length >= 0) return validationErrors[0]; return "validation error"; }, // When a required param for an action is not provided - missingParams: (data, missingParams: string[]) => { + missingParams: (data: ActionProcessor, missingParams: string[]) => { return `${missingParams[0]} is a required parameter for this action`; }, // user requested an unknown action - unknownAction: (data) => { + unknownAction: (data: ActionProcessor) => { return `unknown action or invalid apiVersion`; }, // action not useable by this client/server type - unsupportedServerType: (data) => { + unsupportedServerType: (data: ActionProcessor) => { return `this action does not support the ${data.connection.type} connection type`; }, // action failed because server is mid-shutdown - serverShuttingDown: (data) => { + serverShuttingDown: (data: ActionProcessor) => { return `the server is shutting down`; }, // action failed because this client already has too many pending actions // limit defined in api.config.general.simultaneousActions - tooManyPendingActions: (data) => { + tooManyPendingActions: (data: ActionProcessor) => { return `you have too many pending requests`; }, // Decorate your response based on Error here. // Any action that throws an Error will pass through this method before returning // an error to the client. Response can be edited here, status codes changed, etc. - async genericError(data, error) { + async genericError( + data: ActionProcessor, + error: NodeJS.ErrnoException + ) { return error; }, @@ -94,17 +111,17 @@ export const DEFAULT = { // The body message to accompany 404 (file not found) errors regarding flat files // You may want to load in the content of 404.html or similar - fileNotFound: (connection) => { + fileNotFound: (connection: Connection) => { return `that file is not found`; }, // user didn't request a file - fileNotProvided: (connection) => { + fileNotProvided: (connection: Connection) => { return `file is a required param to send a file`; }, // something went wrong trying to read the file - fileReadError: (connection, error: Error) => { + fileReadError: (connection: Connection, error: NodeJS.ErrnoException) => { return `error reading file: ${error?.message ?? error}`; }, @@ -112,23 +129,23 @@ export const DEFAULT = { // CONNECTIONS // // /////////////// - verbNotFound: (connection, verb: string) => { + verbNotFound: (connection: Connection, verb: string) => { return `verb not found or not allowed (${verb})`; }, - verbNotAllowed: (connection, verb: string) => { + verbNotAllowed: (connection: Connection, verb: string) => { return `verb not found or not allowed (${verb})`; }, - connectionRoomAndMessage: (connection) => { + connectionRoomAndMessage: (connection: Connection) => { return `both room and message are required`; }, - connectionNotInRoom: (connection, room: string) => { + connectionNotInRoom: (connection: Connection, room: string) => { return `connection not in this room (${room})`; }, - connectionAlreadyInRoom: (connection, room: string) => { + connectionAlreadyInRoom: (connection: Connection, room: string) => { return `connection already in this room (${room})`; }, @@ -144,7 +161,7 @@ export const DEFAULT = { return "room exists"; }, - connectionRoomRequired: (room: string) => { + connectionRoomRequired: () => { return "a room is required"; }, }; diff --git a/src/config/logger.ts b/src/config/logger.ts new file mode 100644 index 0000000..4efc735 --- /dev/null +++ b/src/config/logger.ts @@ -0,0 +1,113 @@ +import * as winston from "winston"; +import { ActionheroConfigInterface } from "actionhero"; + +const namespace = "logger"; + +declare module "actionhero" { + export interface ActionheroConfigInterface { + [namespace]: ReturnType; + } +} + +/* +The loggers defined here will eventually be available via `import { loggers } from "actionhero"` + +You may want to customize how Actionhero sets the log level. By default, you can use `process.env.LOG_LEVEL` to change each logger's level (default: 'info') + +learn more about winston v3 loggers @ + - https://github.com/winstonjs/winston + - https://github.com/winstonjs/winston/blob/master/docs/transports.md +*/ + +type ActionheroConfigLoggerBuilderArray = Array< + (config: any) => winston.Logger +>; + +export const DEFAULT = { + [namespace]: (config: ActionheroConfigInterface) => { + const loggers: ActionheroConfigLoggerBuilderArray = []; + loggers.push(buildConsoleLogger(process.env.LOG_LEVEL)); + config.general.paths.log.forEach((p: string) => { + loggers.push(buildFileLogger(p, process.env.LOG_LEVEL)); + }); + + return { + loggers, + maxLogStringLength: 100, // the maximum length of param to log (we will truncate) + maxLogArrayLength: 10, // the maximum number of items in an array to log before collapsing into one message + }; + }, +}; + +export const test = { + [namespace]: (config: ActionheroConfigInterface) => { + const loggers: ActionheroConfigLoggerBuilderArray = []; + loggers.push(buildConsoleLogger(process.env.LOG_LEVEL ?? "crit")); + config.general.paths.log.forEach((path: string) => { + loggers.push(buildFileLogger(path, "debug", 1)); + }); + + return { loggers }; + }, +}; + +// helpers for building the winston loggers + +function buildConsoleLogger(level = "info") { + return function () { + return winston.createLogger({ + format: winston.format.combine( + winston.format.timestamp(), + winston.format.colorize(), + winston.format.printf((info) => { + return `${info.timestamp} - ${info.level}: ${ + info.message + } ${stringifyExtraMessagePropertiesForConsole(info)}`; + }) + ), + level, + levels: winston.config.syslog.levels, + transports: [new winston.transports.Console()], + }); + }; +} + +function buildFileLogger(path: string, level = "info", maxFiles?: number) { + return function (config: ActionheroConfigInterface) { + const filename = `${path}/${config.process.id}-${config.process.env}.log`; + return winston.createLogger({ + format: winston.format.combine( + winston.format.timestamp(), + winston.format.json() + ), + level, + levels: winston.config.syslog.levels, + transports: [ + new winston.transports.File({ + filename, + maxFiles, + }), + ], + }); + }; +} + +function stringifyExtraMessagePropertiesForConsole( + info: winston.Logform.TransformableInfo +) { + const skippedProperties = ["message", "timestamp", "level"]; + let response = ""; + + for (const key in info) { + const value = info[key]; + if (skippedProperties.includes(key)) { + continue; + } + if (value === undefined || value === null || value === "") { + continue; + } + response += `${key}=${value} `; + } + + return response; +} diff --git a/src/config/plugins.ts b/src/config/plugins.ts new file mode 100644 index 0000000..d70c399 --- /dev/null +++ b/src/config/plugins.ts @@ -0,0 +1,37 @@ +import { PluginConfig } from "actionhero"; + +const namespace = "plugins"; + +declare module "actionhero" { + export interface ActionheroConfigInterface { + [namespace]: ReturnType; + } +} + +export const DEFAULT: { [namespace]: () => PluginConfig } = { + [namespace]: () => { + /* + If you want to use plugins in your application, include them here: + + return { + 'myPlugin': { path: __dirname + '/../node_modules/myPlugin' } + } + + You can also toggle on or off sections of a plugin to include (default true for all sections): + + return { + 'myPlugin': { + path: __dirname + '/../node_modules/myPlugin', + actions: true, + tasks: true, + initializers: true, + servers: true, + public: true, + cli: true + } + } + */ + + return {}; + }, +}; diff --git a/src/config/redis.ts b/src/config/redis.ts new file mode 100644 index 0000000..8a812e3 --- /dev/null +++ b/src/config/redis.ts @@ -0,0 +1,112 @@ +import { URL } from "url"; + +const namespace = "redis"; + +declare module "actionhero" { + export interface ActionheroConfigInterface { + [namespace]: ReturnType; + } +} + +/** + * This is the standard redis config for Actionhero. + * This will use a redis server to persist cache, share chat message between processes, etc. + */ + +export const DEFAULT = { + [namespace]: () => { + const konstructor = require("ioredis"); + let protocol = process.env.REDIS_SSL ? "rediss" : "redis"; + let host = process.env.REDIS_HOST || "127.0.0.1"; + let port = process.env.REDIS_PORT || 6379; + let db = process.env.REDIS_DB || process.env.JEST_WORKER_ID || "0"; + let password = process.env.REDIS_PASSWORD || null; + + if (process.env.REDIS_URL) { + const parsed = new URL(process.env.REDIS_URL); + if (parsed.protocol) protocol = parsed.protocol.split(":")[0]; + if (parsed.password) password = parsed.password; + if (parsed.hostname) host = parsed.hostname; + if (parsed.port) port = parsed.port; + if (parsed.pathname) db = parsed.pathname.substring(1); + } + + const maxBackoff = 1000; + const commonArgs = { + port, + host, + password, + db: parseInt(db), + // ssl options + tls: protocol === "rediss" ? { rejectUnauthorized: false } : undefined, + // you can learn more about retryStrategy @ https://github.com/luin/ioredis#auto-reconnect + retryStrategy: (times: number) => { + if (times === 1) { + console.error( + "Unable to connect to Redis - please check your Redis config!" + ); + return 5000; + } + return Math.min(times * 50, maxBackoff); + }, + }; + + return { + // how many items should be fetched in a batch at once? + scanCount: 1000, + // how many ms should we wait when disconnecting after sending server-side disconnect message to the cluster + stopTimeout: 100, + + _toExpand: false, + client: { + konstructor, + args: [commonArgs], + buildNew: true, + }, + subscriber: { + konstructor, + args: [commonArgs], + buildNew: true, + }, + tasks: { + konstructor, + args: [commonArgs], + buildNew: true, + }, + }; + }, +}; + +/** + * If you do not want to connect to a real redis server, and want to emulate the functionally of redis in-memory, you can use `MockIORedis` + * Note that large data sets will be stored in RAM, and not persisted to disk. Multiple Actionhero processes cannot share cache, chat messages, etc. + * Redis Pub/Sub works with this configuration. + */ + +// export const DEFAULT = { +// [namespace]: (config) => { +// const MockIORedis = require("ioredis-mock"); +// const baseRedis = new MockIORedis(); + +// return { +// scanCount: 1000, + +// _toExpand: false, +// client: { +// konstructor: () => baseRedis, +// args: [], +// buildNew: false, +// }, +// subscriber: { +// konstructor: () => baseRedis.createConnectedClient(), +// args: [], +// buildNew: false, +// }, +// tasks: { +// konstructor: () => baseRedis.createConnectedClient(), +// args: [], +// buildNew: false, +// }, +// }; +// }, +// }; diff --git a/src/config/routes.ts b/src/config/routes.ts new file mode 100644 index 0000000..8b6d231 --- /dev/null +++ b/src/config/routes.ts @@ -0,0 +1,44 @@ +import { RoutesConfig } from "actionhero"; + +const namespace = "routes"; + +declare module "actionhero" { + export interface ActionheroConfigInterface { + [namespace]: ReturnType; + } +} + +export const DEFAULT: { [namespace]: () => RoutesConfig } = { + [namespace]: () => { + return { + get: [ + { path: "/status", action: "status" }, + { path: "/swagger", action: "swagger" }, + { path: "/createChatRoom", action: "createChatRoom" }, + ], + + /* --------------------- + For web clients (http and https) you can define an optional RESTful mapping to help route requests to actions. + If the client doesn't specify and action in a param, and the base route isn't a named action, the action will attempt to be discerned from this routes.js file. + + Learn more here: https://www.actionherojs.com/tutorials/web-server#Routes + + examples: + + get: [ + { path: '/users', action: 'usersList' }, // (GET) /api/users + { path: '/search/:term/limit/:limit/offset/:offset', action: 'search' }, // (GET) /api/search/car/limit/10/offset/100 + ], + + post: [ + { path: '/login/:userID(^\\d{3}$)', action: 'login' } // (POST) /api/login/123 + ], + + all: [ + { path: '/user/:userID', action: 'user', matchTrailingPathParts: true } // (*) /api/user/123, api/user/123/stuff + ] + + ---------------------- */ + }; + }, +}; diff --git a/src/config/servers/socket.ts b/src/config/servers/socket.ts deleted file mode 100644 index 87c4828..0000000 --- a/src/config/servers/socket.ts +++ /dev/null @@ -1,40 +0,0 @@ -export const DEFAULT = { - servers: { - socket: (config) => { - return { - enabled: true, - // TCP or TLS? - secure: false, - // Passed to tls.createServer if secure=true. Should contain SSL certificates - serverOptions: {}, - // Port or Socket - port: 5000, - // Which IP to listen on (use 0.0.0.0 for all) - bindIP: "0.0.0.0", - // Enable TCP KeepAlive pings on each connection? - setKeepAlive: false, - // Delimiter string for incoming messages - delimiter: "\n", - // Maximum incoming message string length in Bytes (use 0 for Infinite) - maxDataLength: 0, - }; - }, - }, -}; - -export const test = { - servers: { - socket: (config) => { - return { - enabled: true, - port: - 15000 + - (process.env.JEST_WORKER_ID - ? parseInt(process.env.JEST_WORKER_ID) - : 0), - secure: false, - maxDataLength: 999, - }; - }, - }, -}; diff --git a/src/config/servers/web.ts b/src/config/servers/web.ts deleted file mode 100644 index 7646159..0000000 --- a/src/config/servers/web.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const DEFAULT = { - servers: { - web: (config) => { - return { - enabled: false, - }; - }, - }, -}; diff --git a/src/config/servers/websocket.ts b/src/config/servers/websocket.ts deleted file mode 100644 index 6a72ba2..0000000 --- a/src/config/servers/websocket.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const DEFAULT = { - servers: { - websocket: (config) => { - return { - enabled: false, - }; - }, - }, -}; diff --git a/src/config/socket.ts b/src/config/socket.ts new file mode 100644 index 0000000..b47f824 --- /dev/null +++ b/src/config/socket.ts @@ -0,0 +1,42 @@ +const namespace = "socket"; + +declare module "actionhero" { + export interface ActionheroConfigInterface { + [namespace]: ReturnType; + } +} + +export const DEFAULT = { + [namespace]: () => { + return { + enabled: true, + // TCP or TLS? + secure: false, + // Passed to tls.createServer if secure=true. Should contain SSL certificates + serverOptions: {}, + // Port or Socket + port: 5000, + // Which IP to listen on (use 0.0.0.0 for all) + bindIP: "0.0.0.0", + // Enable TCP KeepAlive pings on each connection? + setKeepAlive: false, + // Delimiter string for incoming messages + delimiter: "\n", + // Maximum incoming message string length in Bytes (use 0 for Infinite) + maxDataLength: 0, + }; + }, +}; + +export const test = { + [namespace]: () => { + return { + enabled: true, + port: + 15000 + + (process.env.JEST_WORKER_ID ? parseInt(process.env.JEST_WORKER_ID) : 0), + secure: false, + maxDataLength: 999, + }; + }, +}; diff --git a/src/config/tasks.ts b/src/config/tasks.ts new file mode 100644 index 0000000..75ae03d --- /dev/null +++ b/src/config/tasks.ts @@ -0,0 +1,84 @@ +import { ActionheroLogLevel } from "actionhero"; +import { MultiWorker, Queue, Scheduler } from "node-resque"; + +const namespace = "tasks"; + +declare module "actionhero" { + export interface ActionheroConfigInterface { + [namespace]: ReturnType; + } +} + +export const DEFAULT = { + [namespace]: () => { + return { + _toExpand: false, + + // Should this node run a scheduler to promote delayed tasks? + scheduler: false, + + // what queues should the taskProcessors work? + queues: ["*"] as string[] | (() => Promise), + // Or, rather than providing a static list of `queues`, you can define a method that returns the list of queues. + // queues: async () => { return ["queueA", "queueB"]; } as string[] | (() => Promise)>, + + // Logging levels of task workers + workerLogging: { + failure: "error" as ActionheroLogLevel, // task failure + success: "info" as ActionheroLogLevel, // task success + start: "info" as ActionheroLogLevel, + end: "info" as ActionheroLogLevel, + cleaning_worker: "info" as ActionheroLogLevel, + poll: "debug" as ActionheroLogLevel, + job: "debug" as ActionheroLogLevel, + pause: "debug" as ActionheroLogLevel, + reEnqueue: "debug" as ActionheroLogLevel, + internalError: "error" as ActionheroLogLevel, + multiWorkerAction: "debug" as ActionheroLogLevel, + }, + // Logging levels of the task scheduler + schedulerLogging: { + start: "info" as ActionheroLogLevel, + end: "info" as ActionheroLogLevel, + poll: "debug" as ActionheroLogLevel, + enqueue: "debug" as ActionheroLogLevel, + working_timestamp: "debug" as ActionheroLogLevel, + reEnqueue: "debug" as ActionheroLogLevel, + transferred_job: "debug" as ActionheroLogLevel, + }, + // how long to sleep between jobs / scheduler checks + timeout: 5000, + // at minimum, how many parallel taskProcessors should this node spawn? + // (have number > 0 to enable, and < 1 to disable) + minTaskProcessors: 0, + // at maximum, how many parallel taskProcessors should this node spawn? + maxTaskProcessors: 0, + // how often should we check the event loop to spawn more taskProcessors? + checkTimeout: 500, + // how many ms would constitute an event loop delay to halt taskProcessors spawning? + maxEventLoopDelay: 5, + // how long before we mark a resque worker / task processor as stuck/dead? + stuckWorkerTimeout: 1000 * 60 * 60, + // should the scheduler automatically try to retry failed tasks which were failed due to being 'stuck'? + retryStuckJobs: false, + // Customize Resque primitives, replace null with required replacement. + resque_overrides: { + queue: null as Queue, + multiWorker: null as MultiWorker, + scheduler: null as Scheduler, + }, + connectionOptions: { + tasks: {}, + }, + }; + }, +}; + +export const test = { + [namespace]: () => { + return { + timeout: 100, + checkTimeout: 50, + }; + }, +}; diff --git a/src/config/web.ts b/src/config/web.ts new file mode 100644 index 0000000..fd4f5e4 --- /dev/null +++ b/src/config/web.ts @@ -0,0 +1,134 @@ +import * as os from "os"; +import { ActionheroConfigInterface } from "actionhero"; + +const namespace = "web"; + +declare module "actionhero" { + export interface ActionheroConfigInterface { + [namespace]: ReturnType; + } +} + +export const DEFAULT = { + [namespace]: (config: ActionheroConfigInterface) => { + return { + enabled: true, + // HTTP or HTTPS? This setting is to enable SSL termination directly in the actionhero app, not set redirection host headers + secure: false, + // Passed to https.createServer if secure=true. Should contain SSL certificates + serverOptions: {}, + // Should we redirect all traffic to the first host in this array if hte request header doesn't match? + // i.e.: [ 'https://www.site.com' ] + allowedRequestHosts: process.env.ALLOWED_HOSTS + ? process.env.ALLOWED_HOSTS.split(",") + : [], + // Port or Socket Path + port: process.env.PORT || 8080, + // Which IP to listen on (use '0.0.0.0' for all; '::' for all on ipv4 and ipv6) + // Set to `null` when listening to socket + bindIP: "0.0.0.0", + // Any additional headers you want actionhero to respond with + httpHeaders: { + "X-Powered-By": config.general.serverName, + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": + "HEAD, GET, POST, PUT, PATCH, DELETE, OPTIONS, TRACE", + "Access-Control-Allow-Headers": "Content-Type", + "Strict-Transport-Security": "max-age=31536000; includeSubDomains", + }, + // Route that actions will be served from; secondary route against this route will be treated as actions, + // IE: /api/?action=test == /api/test/ + urlPathForActions: "api", + // Route that static files will be served from; + // path (relative to your project root) to serve static content from + // set to `null` to disable the file server entirely + urlPathForFiles: "public", + // When visiting the root URL, should visitors see 'api' or 'file'? + // Visitors can always visit /api and /public as normal + rootEndpointType: "file", + // In addition to what's defined in config/routes.ts, should we make a route for every action? Useful for debugging or simple APIs. + // automaticRoutes should an array of strings - HTTP verbs, ie: [] (default), ['get'], ['post'], ['get','put'], ['get','post','put'], etc. + automaticRoutes: process.env.AUTOMATIC_ROUTES + ? process.env.AUTOMATIC_ROUTES.split(",") + .map((v) => v.trim()) + .map((v) => v.toLowerCase()) + : [], + // Default HTTP status code for errors thrown in an action + defaultErrorStatusCode: 500, + // The cache or (if etags are enabled) next-revalidation time to be returned for all flat files served from /public; defined in seconds + flatFileCacheDuration: 60, + // Add an etag header to requested flat files which acts as fingerprint that changes when the file is updated; + // Client will revalidate the fingerprint at latest after flatFileCacheDuration and reload it if the etag (and therefore the file) changed + // or continue to use the cached file if it's still valid + enableEtag: true, + // should we save the un-parsed HTTP POST/PUT payload to connection.rawConnection.params.rawBody? + saveRawBody: false, + // How many times should we try to boot the server? + // This might happen if the port is in use by another process or the socket file is claimed + bootAttempts: 1, + // Settings for determining the id of an http(s) request (browser-fingerprint) + fingerprintOptions: { + cookieKey: "sessionID", + toSetCookie: true, + onlyStaticElements: false, + settings: { + path: "/", + expires: 3600000, + }, + }, + // Options to be applied to incoming file uploads. + // More options and details at https://github.com/felixge/node-formidable + formOptions: { + uploadDir: os.tmpdir(), + keepExtensions: false, + maxFieldsSize: 1024 * 1024 * 20, + maxFileSize: 1024 * 1024 * 200, + }, + // Should we pad JSON responses with whitespace to make them more human-readable? + // set to null to disable + padding: 2, + // Options to configure metadata in responses + metadataOptions: { + serverInformation: true, + requesterInformation: true, + }, + // When true, returnErrorCodes will modify the response header for http(s) clients if connection.error is not null. + // You can also set connection.rawConnection.responseHttpCode to specify a code per request. + returnErrorCodes: true, + // should this node server attempt to gzip responses if the client can accept them? + // this will slow down the performance of actionhero, and if you need this functionality, it is recommended that you do this upstream with nginx or your load balancer + compress: false, + // options to pass to the query parser + // learn more about the options @ https://github.com/hapijs/qs + queryParseOptions: {}, + }; + }, +}; + +export const production = { + [namespace]: () => { + return { + padding: null as number, + metadataOptions: { + serverInformation: false, + requesterInformation: false, + }, + }; + }, +}; + +export const test = { + [namespace]: () => { + return { + secure: false, + port: process.env.PORT + ? process.env.PORT + : 18080 + parseInt(process.env.JEST_WORKER_ID || "0"), + matchExtensionMime: true, + metadataOptions: { + serverInformation: true, + requesterInformation: true, + }, + }; + }, +}; diff --git a/src/config/websocket.ts b/src/config/websocket.ts new file mode 100644 index 0000000..e84540c --- /dev/null +++ b/src/config/websocket.ts @@ -0,0 +1,62 @@ +// Note that to use the websocket server, you also need the web server enabled + +import { ActionheroConfigInterface } from "actionhero"; + +const namespace = "websocket"; + +declare module "actionhero" { + export interface ActionheroConfigInterface { + [namespace]: ReturnType; + } +} + +export const DEFAULT = { + [namespace]: (config: ActionheroConfigInterface) => { + return { + enabled: true, + // you can pass a FQDN (like https://company.com) here or 'window.location.origin' + clientUrl: "window.location.origin", + // Directory to render client-side JS. + // Path should start with "/" and will be built starting from api.config..general.paths.public + clientJsPath: "javascript/", + // the name of the client-side JS file to render. Both `.js` and `.min.js` versions will be created + // do not include the file extension + // set to `undefined` to not render the client-side JS on boot + clientJsName: "ActionheroWebsocketClient", + // should the server signal clients to not reconnect when the server is shutdown/reboot + destroyClientsOnShutdown: false, + + // websocket Server Options: + server: { + // authorization: null, + // pathname: '/primus', + // parser: 'JSON', + // transformer: 'websockets', + // plugin: {}, + // timeout: 35000, + // origins: '*', + // methods: ['GET','HEAD','PUT','POST','DELETE','OPTIONS'], + // credentials: true, + // maxAge: '30 days', + // exposed: false, + }, + + // websocket Client Options: + client: { + apiPath: "/api", // the api base endpoint on your actionhero server + // the cookie name we should use for shared authentication between WS and web connections + cookieKey: config.web.fingerprintOptions.cookieKey, + // reconnect: {}, + // timeout: 10000, + // ping: 25000, + // pong: 10000, + // strategy: "online", + // manual: false, + // websockets: true, + // network: true, + // transport: {}, + // queueSize: Infinity, + }, + }; + }, +}; diff --git a/src/servers/socket.ts b/src/servers/socket.ts index 7ac9289..b84540c 100644 --- a/src/servers/socket.ts +++ b/src/servers/socket.ts @@ -1,7 +1,9 @@ import * as net from "net"; import * as tls from "tls"; import * as uuid from "uuid"; -import { config, Server } from "actionhero"; +import { ActionProcessor, config, Connection, Server } from "actionhero"; + +type RawConnection = net.Socket | (net.Socket & { socketDataString: string }); export class SocketServer extends Server { constructor() { @@ -53,7 +55,7 @@ export class SocketServer extends Server { ); } - this.server.on("error", (error) => { + this.server.on("error", (error: NodeJS.ErrnoException) => { throw new Error( `Cannot start socket server @ ${this.config.bindIP}:${this.config.port} => ${error.message}` ); @@ -63,11 +65,11 @@ export class SocketServer extends Server { this.server.listen(this.config.port, this.config.bindIP, resolve); }); - this.on("connection", async (connection) => { + this.on("connection", async (connection: Connection) => { await this.onConnection(connection); }); - this.on("actionComplete", (data) => { + this.on("actionComplete", (data: ActionProcessor) => { if (data.toRender === true) { data.response.context = "response"; this.sendMessage(data.connection, data.response, data.messageId); @@ -79,7 +81,11 @@ export class SocketServer extends Server { await this.gracefulShutdown(); } - async sendMessage(connection, message, messageId) { + async sendMessage( + connection: Connection, + message: Record, + messageId: string | number + ) { if (message.error) { message.error = config.errors.serializers.servers ? await config.errors.serializers.servers.socket(message.error) @@ -102,7 +108,7 @@ export class SocketServer extends Server { } // @ts-ignore - async goodbye(connection) { + async goodbye(connection: Connection) { try { connection.rawConnection.end( JSON.stringify({ status: "Bye", context: "api" }) + "\r\n" @@ -112,7 +118,11 @@ export class SocketServer extends Server { } } - async sendFile(connection, error, fileStream) { + async sendFile( + connection: Connection, + error: NodeJS.ErrnoException, + fileStream: any + ) { if (error) { this.sendMessage(connection, error, connection.messageId); } else { @@ -120,11 +130,11 @@ export class SocketServer extends Server { } } - handleConnection(rawConnection) { + handleConnection(rawConnection: RawConnection) { if (this.config.setKeepAlive === true) { rawConnection.setKeepAlive(true); } - rawConnection.socketDataString = ""; + rawConnection["socketDataString"] = ""; const id = uuid.v4(); this.buildConnection({ id, @@ -135,7 +145,7 @@ export class SocketServer extends Server { }); } - async onConnection(connection) { + async onConnection(connection: Connection) { connection.params = {}; connection.rawConnection.on("data", async (chunk) => { @@ -172,7 +182,7 @@ export class SocketServer extends Server { } }); - connection.rawConnection.on("error", (e) => { + connection.rawConnection.on("error", (e: NodeJS.ErrnoException) => { if (connection.destroyed !== true) { this.log("socket error: " + e, "error"); try { @@ -183,7 +193,7 @@ export class SocketServer extends Server { }); } - async parseLine(connection, line) { + async parseLine(connection: Connection, line: string) { if (this.config.maxDataLength > 0) { const blen = Buffer.byteLength(line, "utf8"); if (blen > this.config.maxDataLength) { @@ -206,7 +216,7 @@ export class SocketServer extends Server { } } - async parseRequest(connection, line) { + async parseRequest(connection: Connection, line: string) { const words = line.split(" "); const verb = words.shift(); connection.messageId = connection.params.messageId || uuid.v4(); @@ -257,7 +267,7 @@ export class SocketServer extends Server { return this.processAction(connection); } - checkBreakChars(chunk) { + checkBreakChars(chunk: Buffer) { let found = false; const hexChunk = chunk.toString("hex", 0, chunk.length); if (hexChunk === "fff4fffd06") {