diff --git a/.github/workflows/npm-publish-secure-ws.yml b/.github/workflows/npm-publish-secure-ws.yml new file mode 100644 index 0000000..b00a029 --- /dev/null +++ b/.github/workflows/npm-publish-secure-ws.yml @@ -0,0 +1,89 @@ +name: Release and Publish secure-ws + +on: + push: + paths: + - "secure-ws/**" + +env: + BRANCH_TAG: "${{ github.ref_name == 'main' && 'latest' || github.ref_name }}" + BRANCH: ${{ github.ref_name }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + publish_and_release: + name: Publish secure-ws package + environment: "${{ github.ref_name == 'main' && 'Prod' || 'Dev' }}" + runs-on: ubuntu-latest + defaults: + run: + working-directory: "secure-ws" + steps: + - name: Generate token + if: ${{ github.ref_name == 'main'}} + id: generate_token + uses: tibdex/github-app-token@v1 + with: + app_id: ${{ vars.FUNDABOT_APP_ID }} + private_key: ${{ secrets.FUNDABOT_PRIVATE_KEY }} + + - uses: actions/setup-node@v3 + with: + node-version: 18 + + - uses: actions/checkout@v3 + with: + token: ${{ github.ref_name == 'main' && steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }} + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Version bump + id: version + uses: phips28/gh-action-bump-version@v9.1.0 + with: + major-wording: ${{ env.BRANCH == 'main' && '[bump major]' || '[bump major --force]' }} + minor-wording: ${{ env.BRANCH == 'main' && '[bump minor]' || '[bump minor --force]' }} + patch-wording: ${{ null }} + rc-wording: ${{ null }} + default: "${{ env.BRANCH == 'main' && 'patch' || 'prerelease' }}" + PACKAGEJSON_DIR: "secure-ws" + preid: "${{ env.BRANCH }}" + skip-tag: "true" + skip-push: "true" + skip-commit: "true" + bump-policy: "ignore" + + - name: Commit changes + env: + VERSION: ${{ steps.version.outputs.newTag }} + run: | + git config user.email "fundabot@fundwave.com" + git config user.name "fundabot" + git commit -a -m "CI: bumps secure-ws to $VERSION" -m "[skip ci]" + + - name: Publish package to npm + run: | + echo //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN} > ~/.npmrc + echo registry=https://registry.npmjs.org/ >> ~/.npmrc + npm publish --tag $BRANCH_TAG --access public + env: + NODE_AUTH_TOKEN: ${{secrets.NPMJS_TOKEN}} + + - name: Push changes + uses: ad-m/github-push-action@master + with: + github_token: ${{ env.GITHUB_TOKEN }} + branch: ${{ github.ref }} + + - name: Release + if: ${{ github.ref_name == 'main'}} + env: + VERSION: ${{ steps.version.outputs.newTag }} + run: | + if [ "${BRANCH}" != "main" ]; then PRERELEASE="-p"; fi + echo "Releasing version ${VERSION} on branch ${BRANCH}" + gh release create ${VERSION} --target ${BRANCH} --generate-notes ${PRERELEASE} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b4a4e49 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +**/dist/** +**/node_modules/** \ No newline at end of file diff --git a/.talismanrc b/.talismanrc index 7bea482..1d1b710 100644 --- a/.talismanrc +++ b/.talismanrc @@ -1,19 +1,23 @@ -threshold: medium fileignoreconfig: - - filename: aws-credentials-utils/store-credentials.sh - checksum: 784aec6e80314be796af73887ba630fbaf0e116cc4d11bd5cba787a20f9c4bbb - - filename: jwks-slim/README.md - checksum: a1954df51e49fc6a09d7fe04bac198fe60a4fb39b2569180475a749a72df472d - - filename: jwks-slim/package-lock.json - checksum: d014d548f9f2997cbe1d7c4f7ec312f8c634e88788bc453ccc606ea70e081a77 - - filename: jwks-slim/tests/cache.test.js - checksum: 716f25000789d7ca18adfe07f968bae6eb2fbfa1c3d05e9e92897b9597ac5f17 - - filename: jwks-slim/tests/mock.js - checksum: 0d2fd2ec4847acda384c398e04e8aab9a615a8599b0a06d685e7fb100e71537c - - filename: jwks-slim/tests/keys.js - checksum: 207c0d07dea4c069883822b2b268d3b34c8b18bdfd645b8aa3b2be20f280c270 - - filename: jwks-slim/tests/index.test.js - checksum: 992a5c1f6c254fd344ae52955aa0c1c22a59e0472fe836b0450ff499255ef6f8 - - filename: aws-ssm/package-lock.json - checksum: 4cf91061e42ed9b1aafb17bdf68c9b4b24a7f86b191133ab4095fa635f8f01a0 -version: "" +- filename: aws-credentials-utils/store-credentials.sh + checksum: 784aec6e80314be796af73887ba630fbaf0e116cc4d11bd5cba787a20f9c4bbb +- filename: aws-ssm/package-lock.json + checksum: 4cf91061e42ed9b1aafb17bdf68c9b4b24a7f86b191133ab4095fa635f8f01a0 +- filename: jwks-slim/README.md + checksum: a1954df51e49fc6a09d7fe04bac198fe60a4fb39b2569180475a749a72df472d +- filename: jwks-slim/package-lock.json + checksum: d014d548f9f2997cbe1d7c4f7ec312f8c634e88788bc453ccc606ea70e081a77 +- filename: jwks-slim/tests/cache.test.js + checksum: 716f25000789d7ca18adfe07f968bae6eb2fbfa1c3d05e9e92897b9597ac5f17 +- filename: jwks-slim/tests/index.test.js + checksum: 992a5c1f6c254fd344ae52955aa0c1c22a59e0472fe836b0450ff499255ef6f8 +- filename: jwks-slim/tests/keys.js + checksum: 207c0d07dea4c069883822b2b268d3b34c8b18bdfd645b8aa3b2be20f280c270 +- filename: jwks-slim/tests/mock.js + checksum: 0d2fd2ec4847acda384c398e04e8aab9a615a8599b0a06d685e7fb100e71537c +- filename: secure-ws/mocks/express-response.ts + checksum: 7774e72616894119c419f7ad86a3da579036b0391fd208766bcdcb96401b8d2c +- filename: websocket-provider/package-lock.json + checksum: 605a2d92b91f08ee577d3b1e7ed38d27eef43573a044608c22466f05ab6eaff7 +threshold: medium +version: "1.0" diff --git a/secure-ws/.npmignore b/secure-ws/.npmignore new file mode 100644 index 0000000..de53100 --- /dev/null +++ b/secure-ws/.npmignore @@ -0,0 +1,20 @@ +# Exclude TypeScript source files and configs +src/ +*.ts +*.tsx +tsconfig.json +# Exclude node_modules +node_modules/ +# Exclude test files and folders +test/ +*.spec.* +*.test.* +# Exclude build scripts and configs +*.log +*.env +# Exclude editor and OS files +.DS_Store +.vscode/ +.idea/ +# Exclude other common files +coverage/ diff --git a/secure-ws/README.md b/secure-ws/README.md new file mode 100644 index 0000000..491b393 --- /dev/null +++ b/secure-ws/README.md @@ -0,0 +1,46 @@ +# secure-ws + +secure-ws is a TypeScript library for secure WebSocket servers that lets you run Express-style middleware during the upgrade handshake, ensuring unauthenticated connections are never left open. + +## Why use secure-ws + +- Run middleware during the WebSocket upgrade handshake +- Reuse Express Middlewares for authentication, validation and more +- Abstract away connection, upgrade, and messaging with Express-style routes, middleware, and controllers. + +## Installation + +``` +npm install --save secure-ws +``` + +## Usage + +```typescript +import express from "express"; +import { WebSocketProvider } from 'secure-ws'; + +const wsApp = new WebSocketProvider(); +const app = express(); + +// app.get... +// app.post... + +wsApp.addRoute( + "/extract/metrics", + { + onConnect: [authMiddleware1, authMiddleware2], + onMessage: [controller] + } +); + +const httpServer = app.listen(PORT, () => { + console.info(`Service running at port: ${PORT}`); +}); + +httpServer.on('upgrade', wsApp.handleUpgrade); +``` + +## License + +MIT diff --git a/secure-ws/core/web-socket-provider.ts b/secure-ws/core/web-socket-provider.ts new file mode 100644 index 0000000..ac943ae --- /dev/null +++ b/secure-ws/core/web-socket-provider.ts @@ -0,0 +1,99 @@ +import { WebSocketServer } from 'ws'; +import { IncomingMessage } from 'http'; +import { decode } from "@msgpack/msgpack"; +import { randomUUID } from "crypto"; + +import { WSProtocolCodec } from './ws-protocol-codec'; +import { runExpressMiddleware } from '../utils/middleware-adapter'; +import { injectHttpRequest } from '../utils/inject-http-request'; + +import { WSController } from '../types/ws-controller'; +import { AddRouteParams } from '../types/add-route-params'; +import { ClientSocket } from '../types/client-socket'; +import { Duplex } from 'stream'; + +export class WebSocketProvider { + public server: WebSocketServer; + public clientSockets: Map; + private routes: Record; + + constructor() { + this.server = new WebSocketServer({ noServer: true }); + this.server.on('connection', this.handleConnection); + this.routes = {}; + this.clientSockets = new Map(); + } + + public addRoute = ( + path: string, + { + onConnect, + onMessage, + } : AddRouteParams + ) => { + this.routes[path] = { + onConnect, + onMessage + } + } + public handleUpgrade = (request: IncomingMessage, socket: Duplex, head: Buffer) => { + const { pathname } = new URL(request.url!, 'wss://base.url'); + + if (this.routes[pathname]) { + this.server.handleUpgrade(request, socket, head, (ws: ClientSocket) => { + + ws.id = `${Date.now()}-${randomUUID()}`; + this.clientSockets.set(ws.id, ws); + ws.on('close', () => { + this.clientSockets.delete(ws.id); + }); + + this.server.emit('connection', ws, request); + }); + } else { + console.log('No route for path:', pathname); + socket.end(); + } + }; + + private handleConnection = async (ws: ClientSocket, request: IncomingMessage) => { + const { pathname } = new URL(request.url!, 'wss://base.url'); + + let injectedRequest = {} as IncomingMessage; + + if (typeof ws.protocol === 'string' && ws.protocol.trim() !== '') { + const httpRequest = WSProtocolCodec.decode(ws.protocol); + injectedRequest = injectHttpRequest(request, httpRequest); + } + + if(!this.routes[pathname]) { + ws.close(1000, 'Unknown path'); + return; + } + + const { onConnect, onMessage } = this.routes[pathname]; + + for (const middleware of onConnect) { + const res = await runExpressMiddleware(middleware, injectedRequest, ws); + if (!res.success) { + console.error("Middleware rejected connection:", res.error); + ws.send(JSON.stringify({ error: res.error })); + ws.close(1000, res.error); + return; + } + } + + ws.on('message', this.handleMessage(ws, onMessage)); + ws.send(JSON.stringify({ type: "connection:ack" })); + }; + + private handleMessage = (socket: ClientSocket, onMessage: WSController[]) => { + return async (message) => { + const request = decode(message) as IncomingMessage; + + for (const controller of onMessage) { + await controller(request, socket); + } + }; + } +} \ No newline at end of file diff --git a/secure-ws/core/ws-protocol-codec.ts b/secure-ws/core/ws-protocol-codec.ts new file mode 100644 index 0000000..c710dcb --- /dev/null +++ b/secure-ws/core/ws-protocol-codec.ts @@ -0,0 +1,16 @@ +export class WSProtocolCodec { + static encode(data: unknown): string { + const json = JSON.stringify(data); + return Buffer.from(json).toString('base64') + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/, ""); + } + + static decode(encoded: string): T { + let str = encoded.replace(/-/g, "+").replace(/_/g, "/"); + while (str.length % 4) str += "="; + const json = Buffer.from(str, 'base64').toString(); + return JSON.parse(json) as T; + } +} diff --git a/secure-ws/index.ts b/secure-ws/index.ts new file mode 100644 index 0000000..800b0de --- /dev/null +++ b/secure-ws/index.ts @@ -0,0 +1,2 @@ +export * from './core/web-socket-provider'; +export * from './core/ws-protocol-codec'; \ No newline at end of file diff --git a/secure-ws/mocks/express-response.ts b/secure-ws/mocks/express-response.ts new file mode 100644 index 0000000..569f321 --- /dev/null +++ b/secure-ws/mocks/express-response.ts @@ -0,0 +1,50 @@ +import { WebSocket } from "ws"; +import { MockResponse as MockResponseType } from "../types/mock-response"; + +export class MockResponse implements MockResponseType { + private socket: WebSocket; + public statusCode: number = 200; + + constructor(socket: WebSocket) { + this.socket = socket; + } + + status(code: number) { + this.statusCode = code; + return this; + } + + send(data) { + const responseType = this.statusCode >= 400 ? "error" : "success"; + this.socket.send(JSON.stringify({ + type: responseType, + status: this.statusCode, + data: data + })); + this.socket.close(); + return this; + } + + sendStatus(code: number) { + this.statusCode = code; + const responseType = code >= 400 ? "error" : "success"; + this.socket.send(JSON.stringify({ + status: code, + statusText: this.getStatusText(), + type: responseType + })); + this.socket.close(); + return this; + } + + private getStatusText(): string { + const statusTexts: { [key: number]: string } = { + 200: 'OK', + 401: 'Unauthorized', + 403: 'Forbidden', + 404: 'Not Found', + 500: 'Internal Server Error' + }; + return statusTexts[this.statusCode] || 'Unknown'; + } +}; \ No newline at end of file diff --git a/secure-ws/package-lock.json b/secure-ws/package-lock.json new file mode 100644 index 0000000..28f1b27 --- /dev/null +++ b/secure-ws/package-lock.json @@ -0,0 +1,175 @@ +{ + "name": "secure-ws", + "version": "0.0.2-websocket-provider.14", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "secure-ws", + "version": "0.0.2-websocket-provider.14", + "license": "ISC", + "dependencies": { + "@msgpack/msgpack": "^3.1.2", + "ws": "^8.18.3" + }, + "devDependencies": { + "@types/express": "^5.0.3", + "@types/ws": "^8.18.1" + } + }, + "node_modules/@msgpack/msgpack": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.2.tgz", + "integrity": "sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ==", + "license": "ISC", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", + "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.0.tgz", + "integrity": "sha512-y1dMvuvJspJiPSDZUQ+WMBvF7dpnEqN4x9DDC9ie5Fs/HUZJA3wFp7EhHoVaKX/iI0cRoECV8X2jL8zi0xrHCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.12.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/undici-types": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/secure-ws/package.json b/secure-ws/package.json new file mode 100644 index 0000000..be5c1e6 --- /dev/null +++ b/secure-ws/package.json @@ -0,0 +1,25 @@ +{ + "name": "secure-ws", + "version": "0.0.2-websocket-provider.14", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "tsc" + }, + "author": "The Fundwave Authors", + "license": "ISC", + "description": "", + "devDependencies": { + "@types/express": "^5.0.3", + "@types/ws": "^8.18.1" + }, + "dependencies": { + "@msgpack/msgpack": "^3.1.2", + "ws": "^8.18.3" + }, + "files": [ + "dist", + "README.md" + ] +} diff --git a/secure-ws/tsconfig.json b/secure-ws/tsconfig.json new file mode 100644 index 0000000..d57db00 --- /dev/null +++ b/secure-ws/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "./dist" + } +} diff --git a/secure-ws/types/add-route-params.ts b/secure-ws/types/add-route-params.ts new file mode 100644 index 0000000..0359b8e --- /dev/null +++ b/secure-ws/types/add-route-params.ts @@ -0,0 +1,7 @@ +import { ExpressMiddleware } from "./express-middleware"; +import { WSController } from "./ws-controller"; + +export type AddRouteParams = { + onConnect: ExpressMiddleware[], + onMessage: WSController[], +} \ No newline at end of file diff --git a/secure-ws/types/client-socket.ts b/secure-ws/types/client-socket.ts new file mode 100644 index 0000000..a6d5169 --- /dev/null +++ b/secure-ws/types/client-socket.ts @@ -0,0 +1,5 @@ +import { WebSocket } from 'ws'; + +export interface ClientSocket extends WebSocket { + id: string; +} \ No newline at end of file diff --git a/secure-ws/types/express-middleware.ts b/secure-ws/types/express-middleware.ts new file mode 100644 index 0000000..ca8875a --- /dev/null +++ b/secure-ws/types/express-middleware.ts @@ -0,0 +1,3 @@ +import { Request, Response, NextFunction } from 'express'; + +export type ExpressMiddleware = (req: Request, res: Response, next: NextFunction) => any; \ No newline at end of file diff --git a/secure-ws/types/mock-response.ts b/secure-ws/types/mock-response.ts new file mode 100644 index 0000000..22fbabc --- /dev/null +++ b/secure-ws/types/mock-response.ts @@ -0,0 +1,6 @@ +// types/mock-response.ts +export interface MockResponse { + status(code: number): this; + send(data: any): this; + sendStatus(code: number): this; +} diff --git a/secure-ws/types/ws-controller.ts b/secure-ws/types/ws-controller.ts new file mode 100644 index 0000000..1b86c85 --- /dev/null +++ b/secure-ws/types/ws-controller.ts @@ -0,0 +1,3 @@ +import { WebSocket } from 'ws'; + +export type WSController = (message: any, socket: WebSocket) => any; \ No newline at end of file diff --git a/secure-ws/utils/inject-http-request.ts b/secure-ws/utils/inject-http-request.ts new file mode 100644 index 0000000..64ec4c2 --- /dev/null +++ b/secure-ws/utils/inject-http-request.ts @@ -0,0 +1,26 @@ +import { IncomingMessage } from "http"; + +type HttpRequest = { + headers?: Record; + body?: unknown; +}; + +interface IncomingMessageWithBody extends IncomingMessage { + body?: unknown; +} + +export function injectHttpRequest(request: IncomingMessageWithBody, httpRequest: HttpRequest) { + // Merge headers + if (httpRequest.headers) { + for (const [key, value] of Object.entries(httpRequest.headers)) { + request.headers[key.toLowerCase()] = value; + } + } + + // Attach body as a custom property + if (httpRequest.body) { + request.body = httpRequest.body; + } + + return request; +} diff --git a/secure-ws/utils/middleware-adapter.ts b/secure-ws/utils/middleware-adapter.ts new file mode 100644 index 0000000..fc627dd --- /dev/null +++ b/secure-ws/utils/middleware-adapter.ts @@ -0,0 +1,37 @@ +import { IncomingMessage } from 'http'; +import { WebSocket } from 'ws'; +import { MockResponse } from '../mocks/express-response'; +import { MockResponse as MockResponseType } from '../types/mock-response'; +import { NextFunction, Request, Response } from 'express'; + +export function runExpressMiddleware( + middleware: (req: Request, res: MockResponseType, next: NextFunction) => void, + request: IncomingMessage, + socket: WebSocket +): Promise<{ success: boolean; error?: string }> { + return new Promise((resolve) => { + const mockRes = new MockResponse(socket); + let nextCalled = false; + + const next = (error?: any) => { + if (nextCalled) return; + nextCalled = true; + + if (error) { + resolve({ success: false, error: error.message || 'Middleware error' }); + } else { + resolve({ success: true }); + } + }; + + try { + middleware(request as Request, mockRes as MockResponseType, next); + } catch (error) { + console.error("Middleware exception:", error); + if (!nextCalled) { + nextCalled = true; + resolve({ success: false, error: error.message || 'Middleware exception' }); + } + } + }); +} \ No newline at end of file