diff --git a/examples/cactus-example-carbon-accounting-backend/src/test/typescript/integration/admin-enroll-v1-endpoint.test.ts b/examples/cactus-example-carbon-accounting-backend/src/test/typescript/integration/admin-enroll-v1-endpoint.test.ts index 96771c981ad..a7a8cc4b5cc 100644 --- a/examples/cactus-example-carbon-accounting-backend/src/test/typescript/integration/admin-enroll-v1-endpoint.test.ts +++ b/examples/cactus-example-carbon-accounting-backend/src/test/typescript/integration/admin-enroll-v1-endpoint.test.ts @@ -46,13 +46,14 @@ test("BEFORE " + testCase, async (t: Test) => { test(testCase, async (t: Test) => { const jwtKeyPair = await JWK.generate("RSA", 4096); const jwtPublicKey = jwtKeyPair.toPEM(false); - const middlewareOptions: expressJwt.Options = { + const expressJwtOptions: expressJwt.Options = { algorithms: ["RS256"], secret: jwtPublicKey, audience: "carbon-accounting-tool-servers-hostname-here", issuer: uuidv4(), }; - t.ok(middlewareOptions, "Express JWT config truthy OK"); + t.ok(expressJwtOptions, "Express JWT config truthy OK"); + const socketIoJwtOptions = { secret: jwtPublicKey }; const httpGui = await Servers.startOnPreferredPort(3000); t.true(httpGui.listening, `httpGui.listening === true`); @@ -67,7 +68,8 @@ test(testCase, async (t: Test) => { const authorizationConfig: IAuthorizationConfig = { unprotectedEndpointExemptions: [], - middlewareOptions, + expressJwtOptions, + socketIoJwtOptions, }; const configService = new ConfigService(); @@ -109,8 +111,8 @@ test(testCase, async (t: Test) => { }; const jwtSignOptions: JWT.SignOptions = { algorithm: "RS256", - issuer: middlewareOptions.issuer, - audience: middlewareOptions.audience, + issuer: expressJwtOptions.issuer, + audience: expressJwtOptions.audience, }; const tokenWithScope = JWT.sign(jwtPayload, jwtKeyPair, jwtSignOptions); const verification = JWT.verify(tokenWithScope, jwtKeyPair, jwtSignOptions); diff --git a/packages/cactus-cmd-api-server/package-lock.json b/packages/cactus-cmd-api-server/package-lock.json index 88530b521e2..d8c53161624 100644 --- a/packages/cactus-cmd-api-server/package-lock.json +++ b/packages/cactus-cmd-api-server/package-lock.json @@ -24,6 +24,14 @@ "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==" }, + "@thream/socketio-jwt": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@thream/socketio-jwt/-/socketio-jwt-2.1.0.tgz", + "integrity": "sha512-sBXgVh1VbT9RcjXtYZCbFZXUZVWzUVHcXY5f/AIQqJ0DwMvTAhPmh9IuNs4JufvaUpCYinbbke+IOS5hwTJ0nA==", + "requires": { + "jsonwebtoken": "8.5.1" + } + }, "@types/body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", @@ -34,6 +42,11 @@ "@types/node": "*" } }, + "@types/component-emitter": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz", + "integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==" + }, "@types/compression": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.0.tgz", @@ -61,6 +74,11 @@ "@types/node": "*" } }, + "@types/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg==" + }, "@types/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.6.tgz", @@ -121,6 +139,15 @@ "@types/express": "*" } }, + "@types/jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-rNAPdomlIUX0i0cg2+I+Q1wOUr531zHBQ+cV/28PJ39bSPKjahatZZ2LMuhiguETkCgLVzfruw/ZvNMNkKoSzw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/mime": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", @@ -139,8 +166,7 @@ "@types/node": { "version": "13.9.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.1.tgz", - "integrity": "sha512-E6M6N0blf/jiZx8Q3nb0vNaswQeEyn0XlupO+xN6DtJ6r6IT4nXrTry7zhIfYvFCl3/8Cu6WIysmUBKiqV0bqQ==", - "dev": true + "integrity": "sha512-E6M6N0blf/jiZx8Q3nb0vNaswQeEyn0XlupO+xN6DtJ6r6IT4nXrTry7zhIfYvFCl3/8Cu6WIysmUBKiqV0bqQ==" }, "@types/node-forge": { "version": "0.9.3", @@ -306,6 +332,21 @@ "follow-redirects": "^1.10.0" } }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, + "base64-arraybuffer": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", + "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=" + }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, "bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -371,6 +412,11 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, "compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -554,6 +600,79 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, + "engine.io": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-5.0.0.tgz", + "integrity": "sha512-BATIdDV3H1SrE9/u2BAotvsmjJg0t1P4+vGedImSs1lkFAtQdvk4Ev1y4LDiPF7BPWgXWEG+NDY+nLvW3UrMWw==", + "requires": { + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~4.0.0", + "ws": "~7.4.2" + }, + "dependencies": { + "cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "engine.io-client": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-5.0.1.tgz", + "integrity": "sha512-CQtGN3YwfvbxVwpPugcsHe5rHT4KgT49CEcQppNtu9N7WxbPN0MAG27lGaem7bvtCFtGNLSL+GEqXsFSz36jTg==", + "requires": { + "base64-arraybuffer": "0.1.4", + "component-emitter": "~1.3.0", + "debug": "~4.3.1", + "engine.io-parser": "~4.0.1", + "has-cors": "1.1.0", + "parseqs": "0.0.6", + "parseuri": "0.0.6", + "ws": "~7.4.2", + "yeast": "0.1.2" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "engine.io-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz", + "integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==", + "requires": { + "base64-arraybuffer": "0.1.4" + } + }, "es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", @@ -733,6 +852,11 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + }, "http-errors": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", @@ -2784,6 +2908,16 @@ "@jsdevtools/ono": "7.1.1" } }, + "parseqs": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", + "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==" + }, + "parseuri": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", + "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==" + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2911,6 +3045,101 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, + "socket.io": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.0.1.tgz", + "integrity": "sha512-g8eZB9lV0f4X4gndG0k7YZAywOg1VxYgCUspS4V+sDqsgI/duqd0AW84pKkbGj/wQwxrqrEq+VZrspRfTbHTAQ==", + "requires": { + "@types/cookie": "^0.4.0", + "@types/cors": "^2.8.8", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.1", + "engine.io": "~5.0.0", + "socket.io-adapter": "~2.2.0", + "socket.io-parser": "~4.0.3" + }, + "dependencies": { + "@types/cors": { + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.10.tgz", + "integrity": "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ==" + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "socket.io-adapter": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.2.0.tgz", + "integrity": "sha512-rG49L+FwaVEwuAdeBRq49M97YI3ElVabJPzvHT9S6a2CWhDKnjSFasvwAwSYPRhQzfn4NtDIbCaGYgOCOU/rlg==" + }, + "socket.io-client": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.0.1.tgz", + "integrity": "sha512-6AkaEG5zrVuSVW294cH1chioag9i1OqnCYjKwTc3EBGXbnyb98Lw7yMa40ifLjFj3y6fsFKsd0llbUZUCRf3Qw==", + "requires": { + "@types/component-emitter": "^1.2.10", + "backo2": "~1.0.2", + "component-emitter": "~1.3.0", + "debug": "~4.3.1", + "engine.io-client": "~5.0.0", + "parseuri": "0.0.6", + "socket.io-parser": "~4.0.4" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "socket.io-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", + "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", + "requires": { + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.3.1" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -3001,6 +3230,11 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, + "ws": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", + "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==" + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -3014,6 +3248,11 @@ "camelcase": "^5.0.0", "decamelize": "^1.2.0" } + }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" } } } diff --git a/packages/cactus-cmd-api-server/package.json b/packages/cactus-cmd-api-server/package.json index 638bd1ab51c..b222cf4af04 100644 --- a/packages/cactus-cmd-api-server/package.json +++ b/packages/cactus-cmd-api-server/package.json @@ -89,6 +89,7 @@ "@hyperledger/cactus-common": "0.5.0", "@hyperledger/cactus-core": "0.5.0", "@hyperledger/cactus-core-api": "0.5.0", + "@thream/socketio-jwt": "2.1.0", "axios": "0.21.1", "body-parser": "1.19.0", "compression": "1.7.4", @@ -106,6 +107,8 @@ "npm": "7.8.0", "prom-client": "13.1.0", "semver": "7.3.2", + "socket.io": "4.0.1", + "socket.io-client": "4.0.1", "typescript-optional": "2.0.1", "uuid": "7.0.2" }, @@ -119,6 +122,7 @@ "@types/express": "4.17.8", "@types/express-http-proxy": "1.6.1", "@types/express-jwt": "6.0.1", + "@types/jsonwebtoken": "8.5.1", "@types/multer": "1.4.5", "@types/node-forge": "0.9.3", "@types/npm": "2.0.31", diff --git a/packages/cactus-cmd-api-server/src/main/json/openapi.json b/packages/cactus-cmd-api-server/src/main/json/openapi.json index c9b40dc2a04..092c63ba97b 100644 --- a/packages/cactus-cmd-api-server/src/main/json/openapi.json +++ b/packages/cactus-cmd-api-server/src/main/json/openapi.json @@ -31,6 +31,23 @@ ], "components": { "schemas": { + "WatchHealthcheckV1": { + "type": "string", + "enum": [ + "org.hyperledger.cactus.api.async.besu.WatchHealthcheckV1.Subscribe", + "org.hyperledger.cactus.api.async.besu.WatchHealthcheckV1.Next", + "org.hyperledger.cactus.api.async.besu.WatchHealthcheckV1.Unsubscribe", + "org.hyperledger.cactus.api.async.besu.WatchHealthcheckV1.Error", + "org.hyperledger.cactus.api.async.besu.WatchHealthcheckV1.Complete" + ], + "x-enum-varnames": [ + "Subscribe", + "Next", + "Unsubscribe", + "Error", + "Complete" + ] + }, "MemoryUsage": { "type": "object", "properties": { diff --git a/packages/cactus-cmd-api-server/src/main/typescript/api-client/api-server-api-client.ts b/packages/cactus-cmd-api-server/src/main/typescript/api-client/api-server-api-client.ts new file mode 100644 index 00000000000..40671440673 --- /dev/null +++ b/packages/cactus-cmd-api-server/src/main/typescript/api-client/api-server-api-client.ts @@ -0,0 +1,140 @@ +import { Observable, ReplaySubject } from "rxjs"; +import { finalize } from "rxjs/operators"; +import { Socket, io, SocketOptions } from "socket.io-client"; +import { Logger, Checks, IAsyncProvider } from "@hyperledger/cactus-common"; +import { LogLevelDesc, LoggerProvider } from "@hyperledger/cactus-common"; +import { Constants } from "@hyperledger/cactus-core-api"; +import { + DefaultApi, + WatchHealthcheckV1, + HealthCheckResponse, +} from "../generated/openapi/typescript-axios"; +import { Configuration } from "../generated/openapi/typescript-axios/configuration"; +import { Optional } from "typescript-optional"; + +export interface IApiServerApiClientOptions extends Configuration { + readonly logLevel?: LogLevelDesc; + readonly wsApiHost?: string; + readonly wsApiPath?: string; + readonly tokenProvider?: IAsyncProvider; +} + +export class ApiServerApiClientConfiguration extends Configuration { + public static readonly CLASS_NAME = "ApiServerApiClientConfiguration"; + + public readonly logLevel: LogLevelDesc; + public readonly wsApiHost: string; + public readonly wsApiPath: string; + public readonly tokenProvider: Optional>; + + public get className(): string { + return ApiServerApiClient.CLASS_NAME; + } + + constructor(public readonly options: IApiServerApiClientOptions) { + super(options); + const fnTag = `${this.className}#constructor()`; + this.logLevel = options.logLevel || "INFO"; + this.wsApiHost = options.wsApiHost || options.basePath || location.host; + this.wsApiPath = options.wsApiPath || Constants.SocketIoConnectionPathV1; + this.tokenProvider = Optional.ofNullable(options.tokenProvider); + Checks.nonBlankString(this.logLevel, `${fnTag}:logLevel`); + Checks.nonBlankString(this.wsApiHost, `${fnTag}:wsApiHost`); + Checks.nonBlankString(this.wsApiPath, `${fnTag}:wsApiPath`); + Checks.truthy(this.tokenProvider, `${fnTag}:tokenProvider`); + } +} + +export class ApiServerApiClient extends DefaultApi { + public static readonly CLASS_NAME = "ApiServerApiClient"; + + public readonly log: Logger; + public readonly wsApiHost: string; + public readonly wsApiPath: string; + public readonly tokenProvider: Optional>; + + public get className(): string { + return ApiServerApiClient.CLASS_NAME; + } + + constructor(public readonly options: ApiServerApiClientConfiguration) { + super(options); + const fnTag = `${this.className}#constructor()`; + Checks.truthy(options, `${fnTag} arg options`); + + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level: options.logLevel, label }); + + this.wsApiHost = options.wsApiHost; + this.wsApiPath = options.wsApiPath; + this.tokenProvider = options.tokenProvider; + this.log.debug(`Created ${this.className} OK.`); + this.log.debug(`wsApiHost=${this.wsApiHost}`); + this.log.debug(`wsApiPath=${this.wsApiPath}`); + this.log.debug(`basePath=${this.options.basePath}`); + } + + public async watchHealthcheckV1(): Promise> { + const { log, tokenProvider } = this; + + const socketOptions: SocketOptions = { + auth: { token: this.configuration?.baseOptions.headers.Authorization }, + path: this.wsApiPath, + } as any; // TODO + + const socket: Socket = io(this.wsApiHost, socketOptions); + const subject = new ReplaySubject(1); + + socket.on("error", (ex: Error) => { + log.error("[SocketIOClient] ERROR: %o", ex); + socket.disconnect(); + subject.error(ex); + }); + + socket.on("connect_error", async (err) => { + log.debug("[SocketIOClient] CONNECT_ERROR: %o", err); + if (tokenProvider.isPresent()) { + const theProvider = tokenProvider.get(); // unwrap the Optional + const token = await theProvider.get(); // get the actual token + socket.auth = { token }; + this.options.baseOptions = { headers: { Authorization: token } }; + log.debug("Received fresh token from token provider OK"); + } else { + socket.disconnect(); + subject.error(err); + log.debug("Disconnected socket, send error to RxJS subject."); + } + }); + + socket.on(WatchHealthcheckV1.Next, (data: HealthCheckResponse) => { + subject.next(data); + }); + + socket.on("connect", () => { + const transport = socket.io.engine.transport.name; // in most cases, "polling" + + socket.io.engine.on("upgrade", () => { + const upgradedTransport = socket.io.engine.transport.name; // in most cases, "websocket" + log.debug("[SocketIOClient] Upgraded transport=%o", upgradedTransport); + }); + log.debug("[SocketIOClient] Connected OK"); + log.debug("[SocketIOClient] initial transport=%o", transport); + socket.emit(WatchHealthcheckV1.Subscribe); + }); + + return subject.pipe( + finalize(() => { + log.debug("Emitting unsubscribe, disconnecting socket..."); + socket.emit(WatchHealthcheckV1.Unsubscribe); + socket.disconnect(); + log.debug("Completing RxJS subject..."); + subject.complete(); + log.debug("Finalized RxJS subject OK"); + }), + // TODO: Investigate if we need these below - in theory without these + // it could happen that only the fist subscriber gets the last emitted value + // publishReplay(1), + // refCount(), + ); + } +} diff --git a/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts b/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts index 02b42fbf292..138e538dae3 100644 --- a/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts +++ b/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts @@ -14,6 +14,11 @@ import compression from "compression"; import bodyParser from "body-parser"; import cors from "cors"; +import { Server as SocketIoServer } from "socket.io"; +import type { ServerOptions as SocketIoServerOptions } from "socket.io"; +import type { Socket as SocketIoSocket } from "socket.io"; +import { authorize as authorizeSocket } from "@thream/socketio-jwt"; + import { ICactusPlugin, isIPluginWebService, @@ -21,6 +26,7 @@ import { IPluginFactoryOptions, PluginFactoryFactory, PluginImport, + Constants, } from "@hyperledger/cactus-core-api"; import { PluginRegistry } from "@hyperledger/cactus-core"; @@ -33,9 +39,13 @@ import { OpenAPIV3 } from "express-openapi-validator/dist/framework/types"; import { PrometheusExporter } from "./prometheus-exporter/prometheus-exporter"; import { AuthorizerFactory } from "./authzn/authorizer-factory"; +import { WatchHealthcheckV1 } from "./generated/openapi/typescript-axios"; +import { WatchHealthcheckV1Endpoint } from "./web-services/watch-healthcheck-v1-endpoint"; export interface IApiServerConstructorOptions { pluginRegistry?: PluginRegistry; httpServerApi?: Server | SecureServer; + wsServerApi?: SocketIoServer; + wsOptions?: SocketIoServerOptions; httpServerCockpit?: Server | SecureServer; config: ICactusApiServerOptions; prometheusExporter?: PrometheusExporter; @@ -58,6 +68,9 @@ export class ApiServer { private pluginRegistry: PluginRegistry | undefined; private readonly httpServerApi: Server | SecureServer; private readonly httpServerCockpit: Server | SecureServer; + private readonly wsApi: SocketIoServer; + private readonly expressApi: Application; + private readonly expressCockpit: Application; public prometheusExporter: PrometheusExporter; public get className(): string { @@ -96,6 +109,10 @@ export class ApiServer { this.httpServerCockpit = createServer(); } + this.wsApi = new SocketIoServer(); + this.expressApi = express(); + this.expressCockpit = express(); + if (this.options.prometheusExporter) { this.prometheusExporter = this.options.prometheusExporter; } else { @@ -334,6 +351,7 @@ export class ApiServer { } async startCockpitFileServer(): Promise { + const { expressCockpit: app } = this; const cockpitWwwRoot = this.options.config.cockpitWwwRoot; this.log.info(`wwwRoot: ${cockpitWwwRoot}`); @@ -372,7 +390,6 @@ export class ApiServer { }, }); - const app: Application = express(); app.use("/api/v*", apiProxyMiddleware); app.use(compression()); app.use(corsMiddleware); @@ -406,6 +423,9 @@ export class ApiServer { * @param app */ async getOrCreateWebServices(app: express.Application): Promise { + const { log } = this; + const { logLevel } = this.options.config; + const healthcheckHandler = (req: Request, res: Response) => { res.json({ success: true, @@ -419,6 +439,25 @@ export class ApiServer { const { path: httpPath, verbLowerCase: httpVerb } = http; (app as any)[httpVerb](httpPath, healthcheckHandler); + this.wsApi.on("connection", (socket: SocketIoSocket) => { + const { id } = socket; + const transport = socket.conn.transport.name; // in most cases, "polling" + log.debug(`Socket connected. ID=${id} transport=%o`, transport); + + socket.conn.on("upgrade", () => { + const upgradedTransport = socket.conn.transport.name; // in most cases, "websocket" + log.debug(`Socket upgraded ID=${id} transport=%o`, upgradedTransport); + }); + + socket.on(WatchHealthcheckV1.Subscribe, () => + new WatchHealthcheckV1Endpoint({ + process, + socket, + logLevel, + }).subscribe(), + ); + }); + const prometheusExporterHandler = (req: Request, res: Response) => { this.getPrometheusExporterMetrics().then((resBody) => { res.status(200); @@ -443,7 +482,7 @@ export class ApiServer { } async startApiServer(): Promise { - const { options } = this; + const { options, expressApi: app, wsApi } = this; const { config } = options; const { authorizationConfigJson: authzConf, @@ -454,7 +493,6 @@ export class ApiServer { const pluginRegistry = await this.getOrInitPluginRegistry(); - const app: Application = express(); app.use(compression()); const apiCorsDomainCsv = this.options.config.apiCorsDomainCsv; @@ -490,7 +528,7 @@ export class ApiServer { .map(async (plugin: ICactusPlugin) => { const p = plugin as IPluginWebService; await p.getOrCreateWebServices(); - const webSvcs = await p.registerWebServices(app, null as any); + const webSvcs = await p.registerWebServices(app, wsApi); return webSvcs; }); @@ -518,6 +556,23 @@ export class ApiServer { const addressInfo = this.httpServerApi.address() as AddressInfo; this.log.info(`Cactus API net.AddressInfo`, addressInfo); + const wsOptions = { + path: Constants.SocketIoConnectionPathV1, + serveClient: false, + ...this.options.wsOptions, + } as SocketIoServerOptions; + + this.wsApi.attach(this.httpServerApi, wsOptions); + + const socketIoAuthorizer = authorizeSocket({ + ...authzConf.socketIoJwtOptions, + onAuthentication: (decodedToken) => { + this.log.debug("Socket authorized OK: %o", decodedToken); + }, + }); + + this.wsApi.use(socketIoAuthorizer); + return addressInfo; } diff --git a/packages/cactus-cmd-api-server/src/main/typescript/authzn/authorizer-factory.ts b/packages/cactus-cmd-api-server/src/main/typescript/authzn/authorizer-factory.ts index e9e98d04ebf..5731b62368e 100644 --- a/packages/cactus-cmd-api-server/src/main/typescript/authzn/authorizer-factory.ts +++ b/packages/cactus-cmd-api-server/src/main/typescript/authzn/authorizer-factory.ts @@ -76,20 +76,29 @@ export class AuthorizerFactory { authzConf: IAuthorizationConfig, ): Promise { const fnTag = `${this.className}#createExpressJwtMiddleware()`; + const { log } = this; const { E_BAD_EXPRESS_JWT_OPTIONS } = AuthorizerFactory; - const { middlewareOptions } = authzConf; - if (!isExpressJwtOptions(middlewareOptions)) { + const { expressJwtOptions, socketIoPath } = authzConf; + if (!isExpressJwtOptions(expressJwtOptions)) { throw new Error(`${fnTag}: ${E_BAD_EXPRESS_JWT_OPTIONS}`); } const options: expressJwt.Options = { audience: "org.hyperledger.cactus", // default that can be overridden - ...middlewareOptions, + ...expressJwtOptions, }; const unprotectedEndpoints = this.unprotectedEndpoints.map((e) => { // type pathFilter = string | RegExp | { url: string | RegExp, methods?: string[], method?: string | string[] }; return { url: e.getPath(), method: e.getVerbLowerCase() }; }); + if (socketIoPath) { + log.info("SocketIO path configuration detected: %o", socketIoPath); + // const exemption = { url: new RegExp(`${socketIoPath}.*`), method: "get" }; + // unprotectedEndpoints.push(exemption as any); + log.info( + "Exempted SocketIO path from express-jwt authorization. Using @thream/socketio-jwt instead)", + ); + } return expressJwt(options).unless({ path: unprotectedEndpoints }); } diff --git a/packages/cactus-cmd-api-server/src/main/typescript/authzn/i-authorization-config.ts b/packages/cactus-cmd-api-server/src/main/typescript/authzn/i-authorization-config.ts index 65e3e1e6636..ed6ca101fc9 100644 --- a/packages/cactus-cmd-api-server/src/main/typescript/authzn/i-authorization-config.ts +++ b/packages/cactus-cmd-api-server/src/main/typescript/authzn/i-authorization-config.ts @@ -1,4 +1,9 @@ +import type { Options as ExpressJwtOptions } from "express-jwt"; +import type { AuthorizeOptions as SocketIoJwtOptions } from "@thream/socketio-jwt"; + export interface IAuthorizationConfig { - middlewareOptions: Record; + expressJwtOptions: ExpressJwtOptions; + socketIoJwtOptions: SocketIoJwtOptions; unprotectedEndpointExemptions: Array; + socketIoPath?: string; } diff --git a/packages/cactus-cmd-api-server/src/main/typescript/config/config-service.ts b/packages/cactus-cmd-api-server/src/main/typescript/config/config-service.ts index 3a18e653acb..5fa49f47713 100644 --- a/packages/cactus-cmd-api-server/src/main/typescript/config/config-service.ts +++ b/packages/cactus-cmd-api-server/src/main/typescript/config/config-service.ts @@ -12,6 +12,7 @@ import { } from "@hyperledger/cactus-common"; import { ConsortiumDatabase, + Constants, PluginImport, PluginImportType, } from "@hyperledger/cactus-core-api"; @@ -510,12 +511,19 @@ export class ConfigService { }, ]; + const jwtSecret = uuidV4(); + return { authorizationProtocol: AuthorizationProtocol.JSON_WEB_TOKEN, authorizationConfigJson: { + socketIoPath: Constants.SocketIoConnectionPathV1, unprotectedEndpointExemptions: [], - middlewareOptions: { - secret: uuidV4(), + socketIoJwtOptions: { + secret: jwtSecret, + }, + expressJwtOptions: { + secret: jwtSecret, + algorithms: ["RS256"], audience: "org.hyperledger.cactus.jwt.audience", issuer: "org.hyperledger.cactus.jwt.issuer", }, diff --git a/packages/cactus-cmd-api-server/src/main/typescript/generated/openapi/typescript-axios/api.ts b/packages/cactus-cmd-api-server/src/main/typescript/generated/openapi/typescript-axios/api.ts index 682251644e8..1f6414ed049 100644 --- a/packages/cactus-cmd-api-server/src/main/typescript/generated/openapi/typescript-axios/api.ts +++ b/packages/cactus-cmd-api-server/src/main/typescript/generated/openapi/typescript-axios/api.ts @@ -81,6 +81,19 @@ export interface MemoryUsage { */ arrayBuffers?: number; } +/** + * + * @export + * @enum {string} + */ +export enum WatchHealthcheckV1 { + Subscribe = 'org.hyperledger.cactus.api.async.besu.WatchHealthcheckV1.Subscribe', + Next = 'org.hyperledger.cactus.api.async.besu.WatchHealthcheckV1.Next', + Unsubscribe = 'org.hyperledger.cactus.api.async.besu.WatchHealthcheckV1.Unsubscribe', + Error = 'org.hyperledger.cactus.api.async.besu.WatchHealthcheckV1.Error', + Complete = 'org.hyperledger.cactus.api.async.besu.WatchHealthcheckV1.Complete' +} + /** * DefaultApi - axios parameter creator diff --git a/packages/cactus-cmd-api-server/src/main/typescript/public-api.ts b/packages/cactus-cmd-api-server/src/main/typescript/public-api.ts index 77f01985d1d..f2fdad000b1 100755 --- a/packages/cactus-cmd-api-server/src/main/typescript/public-api.ts +++ b/packages/cactus-cmd-api-server/src/main/typescript/public-api.ts @@ -18,6 +18,10 @@ export { export * from "./generated/openapi/typescript-axios/index"; +export { ApiServerApiClient } from "./api-client/api-server-api-client"; +export { ApiServerApiClientConfiguration } from "./api-client/api-server-api-client"; +export { IApiServerApiClientOptions } from "./api-client/api-server-api-client"; + export { isHealthcheckResponse } from "./model/is-healthcheck-response-type-guard"; export { isExpressJwtOptions } from "./authzn/is-express-jwt-options-type-guard"; diff --git a/packages/cactus-cmd-api-server/src/main/typescript/web-services/watch-healthcheck-v1-endpoint.ts b/packages/cactus-cmd-api-server/src/main/typescript/web-services/watch-healthcheck-v1-endpoint.ts new file mode 100644 index 00000000000..c6db12eea01 --- /dev/null +++ b/packages/cactus-cmd-api-server/src/main/typescript/web-services/watch-healthcheck-v1-endpoint.ts @@ -0,0 +1,70 @@ +import { Socket as SocketIoSocket } from "socket.io"; + +import { Logger, Checks } from "@hyperledger/cactus-common"; +import { LogLevelDesc, LoggerProvider } from "@hyperledger/cactus-common"; +import { HealthCheckResponse } from "../generated/openapi/typescript-axios"; +import { WatchHealthcheckV1 } from "../generated/openapi/typescript-axios"; + +export interface IWatchHealthcheckV1EndpointOptions { + logLevel?: LogLevelDesc; + socket: SocketIoSocket; + process: NodeJS.Process; +} + +export class WatchHealthcheckV1Endpoint { + public static readonly CLASS_NAME = "WatchHealthcheckV1Endpoint"; + + private readonly log: Logger; + private readonly socket: SocketIoSocket< + Record void>, + Record void> + >; + private readonly process: NodeJS.Process; + + public get className(): string { + return WatchHealthcheckV1Endpoint.CLASS_NAME; + } + + constructor(public readonly options: IWatchHealthcheckV1EndpointOptions) { + const fnTag = `${this.className}#constructor()`; + Checks.truthy(options, `${fnTag} arg options`); + Checks.truthy(options.process, `${fnTag} arg options.process`); + Checks.truthy(options.socket, `${fnTag} arg options.socket`); + + this.process = options.process; + this.socket = options.socket; + + const level = this.options.logLevel || "INFO"; + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level, label }); + } + + public async subscribe(): Promise { + const { socket, log } = this; + log.debug(`${WatchHealthcheckV1.Subscribe} => ${socket.id}`); + + const timerId = setInterval(() => { + try { + const next: HealthCheckResponse = { + createdAt: new Date().toJSON(), + memoryUsage: this.process.memoryUsage(), + success: true, + }; + socket.emit(WatchHealthcheckV1.Next, next); + } catch (ex) { + log.error(`Failed to construct health check response:`, ex); + socket.emit(WatchHealthcheckV1.Error, ex); + clearInterval(timerId); + } + }, 1000); + + socket.on("disconnect", async (reason: string) => { + log.debug("WebSocket:disconnect reason=%o", reason); + clearInterval(timerId); + }); + + socket.on(WatchHealthcheckV1.Unsubscribe, () => { + clearInterval(timerId); + }); + } +} diff --git a/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-endpoint-authorization.test.ts b/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-endpoint-authorization.test.ts index 03e6566a298..5eb65021f2f 100644 --- a/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-endpoint-authorization.test.ts +++ b/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-endpoint-authorization.test.ts @@ -38,26 +38,29 @@ test(testCase, async (t: Test) => { const jwtKeyPair = await JWK.generate("RSA", 4096); const jwtPublicKey = jwtKeyPair.toPEM(false); - const middlewareOptions: expressJwt.Options = { + const expressJwtOptions: expressJwt.Options = { algorithms: ["RS256"], secret: jwtPublicKey, audience: uuidv4(), issuer: uuidv4(), }; - t.ok(middlewareOptions, "Express JWT config truthy OK"); + t.ok(expressJwtOptions, "Express JWT config truthy OK"); const jwtPayload = { name: "Peter", location: "London" }; const jwtSignOptions: JWT.SignOptions = { algorithm: "RS256", - issuer: middlewareOptions.issuer, - audience: middlewareOptions.audience, + issuer: expressJwtOptions.issuer, + audience: expressJwtOptions.audience, }; const tokenGood = JWT.sign(jwtPayload, jwtKeyPair, jwtSignOptions); // const tokenBad = JWT.sign(jwtPayload, jwtKeyPair); const authorizationConfig: IAuthorizationConfig = { unprotectedEndpointExemptions: [], - middlewareOptions, + expressJwtOptions, + socketIoJwtOptions: { + secret: jwtPublicKey, + }, }; const configService = new ConfigService(); diff --git a/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-endpoint-authz-scope-enforcement.test.ts b/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-endpoint-authz-scope-enforcement.test.ts index fd682996e9d..539e143765e 100644 --- a/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-endpoint-authz-scope-enforcement.test.ts +++ b/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-endpoint-authz-scope-enforcement.test.ts @@ -32,13 +32,13 @@ test(testCase, async (t: Test) => { try { const jwtKeyPair = await JWK.generate("RSA", 4096); const jwtPublicKey = jwtKeyPair.toPEM(false); - const middlewareOptions: expressJwt.Options = { + const expressJwtOptions: expressJwt.Options = { algorithms: ["RS256"], secret: jwtPublicKey, audience: uuidv4(), issuer: uuidv4(), }; - t.ok(middlewareOptions, "Express JWT config truthy OK"); + t.ok(expressJwtOptions, "Express JWT config truthy OK"); const unprotectedActionEp = new UnprotectedActionEndpoint({ connector: {} as PluginLedgerConnectorStub, @@ -47,7 +47,8 @@ test(testCase, async (t: Test) => { const authorizationConfig: IAuthorizationConfig = { unprotectedEndpointExemptions: [unprotectedActionEp.getPath()], - middlewareOptions, + expressJwtOptions, + socketIoJwtOptions: { secret: jwtPublicKey }, }; const pluginRegistry = new PluginRegistry(); @@ -95,8 +96,8 @@ test(testCase, async (t: Test) => { }; const jwtSignOptions: JWT.SignOptions = { algorithm: "RS256", - issuer: middlewareOptions.issuer, - audience: middlewareOptions.audience, + issuer: expressJwtOptions.issuer, + audience: expressJwtOptions.audience, }; const token = JWT.sign(jwtPayload, jwtKeyPair, jwtSignOptions); diff --git a/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-socketio-endpoint-authorization.test.ts b/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-socketio-endpoint-authorization.test.ts new file mode 100644 index 00000000000..f43c73b987c --- /dev/null +++ b/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-socketio-endpoint-authorization.test.ts @@ -0,0 +1,188 @@ +import test, { Test } from "tape-promise/tape"; +import { v4 as uuidv4 } from "uuid"; +import { JWK, JWT } from "jose"; +import type { Options as ExpressJwtOptions } from "express-jwt"; +import type { AuthorizeOptions as SocketIoJwtOptions } from "@thream/socketio-jwt"; + +import { Constants } from "@hyperledger/cactus-core-api"; +import { + ApiServer, + ConfigService, + HealthCheckResponse, + isHealthcheckResponse, +} from "../../../main/typescript/public-api"; +import { ApiServerApiClient } from "../../../main/typescript/public-api"; +import { ApiServerApiClientConfiguration } from "../../../main/typescript/public-api"; +import { LoggerProvider, LogLevelDesc } from "@hyperledger/cactus-common"; +import { AuthorizationProtocol } from "../../../main/typescript/config/authorization-protocol"; +import { IAuthorizationConfig } from "../../../main/typescript/authzn/i-authorization-config"; + +const testCase = "API server enforces authorization for SocketIO endpoints"; +const logLevel: LogLevelDesc = "TRACE"; +const log = LoggerProvider.getOrCreate({ + level: logLevel, + label: __filename, +}); + +test(testCase, async (t: Test) => { + try { + const jwtKeyPair = await JWK.generate("RSA", 4096); + const jwtPublicKey = jwtKeyPair.toPEM(false); + const expressJwtOptions: ExpressJwtOptions = { + algorithms: ["RS256"], + secret: jwtPublicKey, + audience: uuidv4(), + issuer: uuidv4(), + }; + const socketIoJwtOptions: SocketIoJwtOptions = { + secret: jwtPublicKey, + algorithms: ["RS256"], + }; + t.ok(expressJwtOptions, "Express JWT config truthy OK"); + + const authorizationConfig: IAuthorizationConfig = { + unprotectedEndpointExemptions: [], + expressJwtOptions, + socketIoJwtOptions, + socketIoPath: Constants.SocketIoConnectionPathV1, + }; + + const configService = new ConfigService(); + const apiSrvOpts = configService.newExampleConfig(); + apiSrvOpts.authorizationProtocol = AuthorizationProtocol.JSON_WEB_TOKEN; + apiSrvOpts.authorizationConfigJson = authorizationConfig; + apiSrvOpts.configFile = ""; + apiSrvOpts.apiCorsDomainCsv = "*"; + apiSrvOpts.apiPort = 0; + apiSrvOpts.cockpitPort = 0; + apiSrvOpts.apiTlsEnabled = false; + apiSrvOpts.plugins = []; + const config = configService.newExampleConfigConvict(apiSrvOpts); + + const apiServer = new ApiServer({ + config: config.getProperties(), + }); + test.onFinish(async () => await apiServer.shutdown()); + + const startResponse = apiServer.start(); + await t.doesNotReject(startResponse, "API server started OK"); + t.ok(startResponse, "API server start response truthy OK"); + + const addressInfoApi = (await startResponse).addressInfoApi; + const protocol = apiSrvOpts.apiTlsEnabled ? "https" : "http"; + const { address, port } = addressInfoApi; + const apiHost = `${protocol}://${address}:${port}`; + + const jwtPayload = { name: "Peter", location: "Albertirsa" }; + const jwtSignOptions: JWT.SignOptions = { + algorithm: "RS256", + issuer: expressJwtOptions.issuer, + audience: expressJwtOptions.audience, + }; + const validJwt = JWT.sign(jwtPayload, jwtKeyPair, jwtSignOptions); + t.ok(validJwt, "JWT signed truthy OK"); + + const validBearerToken = `Bearer ${validJwt}`; + t.ok(validBearerToken, "validBearerToken truthy OK"); + + const apiClientBad = new ApiServerApiClient( + new ApiServerApiClientConfiguration({ + basePath: apiHost, + baseOptions: { headers: { Authorization: "Mr. Invalid Token" } }, + logLevel: "TRACE", + }), + ); + + const apiClientFixable = new ApiServerApiClient( + new ApiServerApiClientConfiguration({ + basePath: apiHost, + baseOptions: { headers: { Authorization: "Mr. Invalid Token" } }, + logLevel: "TRACE", + tokenProvider: { + get: () => Promise.resolve(validBearerToken), + }, + }), + ); + + const apiClientGood = new ApiServerApiClient( + new ApiServerApiClientConfiguration({ + basePath: apiHost, + baseOptions: { headers: { Authorization: validBearerToken } }, + logLevel: "TRACE", + tokenProvider: { + get: () => Promise.resolve(validBearerToken), + }, + }), + ); + + { + const healthchecks = await apiClientBad.watchHealthcheckV1(); + + const watchHealthcheckV1WithBadToken = new Promise((resolve, reject) => { + healthchecks.subscribe({ + next: () => { + resolve(new Error("Was authorized with an invalid token, bad.")); + }, + error: (ex: Error) => { + reject(ex); + }, + complete: () => { + resolve(new Error("Was authorized with an invalid token, bad.")); + }, + }); + }); + + await t.rejects( + watchHealthcheckV1WithBadToken, + /Format is Authorization: Bearer \[token\]/, + "SocketIO connection rejected when JWT is invalid OK", + ); + + const resHc = await apiClientGood.getHealthCheck(); + t.ok(resHc, "healthcheck response truthy OK"); + t.equal(resHc.status, 200, "healthcheck response status === 200 OK"); + t.equal(typeof resHc.data, "object", "typeof resHc.data is 'object' OK"); + t.ok(resHc.data.createdAt, "resHc.data.createdAt truthy OK"); + t.ok(resHc.data.memoryUsage, "resHc.data.memoryUsage truthy OK"); + t.ok(resHc.data.memoryUsage.rss, "resHc.data.memoryUsage.rss truthy OK"); + t.ok(resHc.data.success, "resHc.data.success truthy OK"); + t.true(isHealthcheckResponse(resHc.data), "isHealthcheckResponse OK"); + } + + { + let idx = 0; + const healthchecks = await apiClientFixable.watchHealthcheckV1(); + const sub = healthchecks.subscribe((next: HealthCheckResponse) => { + idx++; + t.ok(next, idx + " next healthcheck truthy OK"); + t.equal(typeof next, "object", idx + "typeof next is 'object' OK"); + t.ok(next.createdAt, idx + " next.createdAt truthy OK"); + t.ok(next.memoryUsage, idx + " next.memoryUsage truthy OK"); + t.ok(next.memoryUsage.rss, idx + " next.memoryUsage.rss truthy OK"); + t.ok(next.success, idx + " next.success truthy OK"); + t.true(isHealthcheckResponse(next), idx + " isHealthcheckResponse OK"); + if (idx > 2) { + sub.unsubscribe(); + } + }); + + const all = await healthchecks.toPromise(); + t.comment("all=" + JSON.stringify(all)); + + const resHc = await apiClientFixable.getHealthCheck(); + t.ok(resHc, "healthcheck response truthy OK"); + t.equal(resHc.status, 200, "healthcheck response status === 200 OK"); + t.equal(typeof resHc.data, "object", "typeof resHc.data is 'object' OK"); + t.ok(resHc.data.createdAt, "resHc.data.createdAt truthy OK"); + t.ok(resHc.data.memoryUsage, "resHc.data.memoryUsage truthy OK"); + t.ok(resHc.data.memoryUsage.rss, "resHc.data.memoryUsage.rss truthy OK"); + t.ok(resHc.data.success, "resHc.data.success truthy OK"); + t.true(isHealthcheckResponse(resHc.data), "isHealthcheckResponse OK"); + } + t.end(); + } catch (ex) { + log.error(ex); + t.fail("Exception thrown during test execution, see above for details!"); + throw ex; + } +}); diff --git a/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-unprotected-endpoint-authz-ops-confirm.test.ts b/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-unprotected-endpoint-authz-ops-confirm.test.ts index a7388d8dfff..1bebde1a90c 100644 --- a/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-unprotected-endpoint-authz-ops-confirm.test.ts +++ b/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-unprotected-endpoint-authz-ops-confirm.test.ts @@ -27,17 +27,18 @@ test(testCase, async (t: Test) => { try { const jwtKeyPair = await JWK.generate("RSA", 4096); const jwtPublicKey = jwtKeyPair.toPEM(false); - const middlewareOptions: expressJwt.Options = { + const expressJwtOptions: expressJwt.Options = { algorithms: ["RS256"], secret: jwtPublicKey, audience: uuidv4(), issuer: uuidv4(), }; - t.ok(middlewareOptions, "Express JWT config truthy OK"); + t.ok(expressJwtOptions, "Express JWT config truthy OK"); const authorizationConfig: IAuthorizationConfig = { unprotectedEndpointExemptions: [], - middlewareOptions, + expressJwtOptions, + socketIoJwtOptions: { secret: jwtPublicKey }, }; const pluginRegistry = new PluginRegistry(); diff --git a/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-unprotected-endpoint-authz.test.ts b/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-unprotected-endpoint-authz.test.ts index 76882b8856b..190d44bf407 100644 --- a/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-unprotected-endpoint-authz.test.ts +++ b/packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-unprotected-endpoint-authz.test.ts @@ -30,13 +30,13 @@ test(testCase, async (t: Test) => { try { const jwtKeyPair = await JWK.generate("RSA", 4096); const jwtPublicKey = jwtKeyPair.toPEM(false); - const middlewareOptions: expressJwt.Options = { + const expressJwtOptions: expressJwt.Options = { algorithms: ["RS256"], secret: jwtPublicKey, audience: uuidv4(), issuer: uuidv4(), }; - t.ok(middlewareOptions, "Express JWT config truthy OK"); + t.ok(expressJwtOptions, "Express JWT config truthy OK"); const ep = new UnprotectedActionEndpoint({ connector: {} as PluginLedgerConnectorStub, @@ -45,7 +45,8 @@ test(testCase, async (t: Test) => { const authorizationConfig: IAuthorizationConfig = { unprotectedEndpointExemptions: [ep.getPath()], - middlewareOptions, + expressJwtOptions, + socketIoJwtOptions: { secret: jwtPublicKey }, }; const pluginRegistry = new PluginRegistry();