diff --git a/.gitignore b/.gitignore index 090cd6e..98775e4 100644 --- a/.gitignore +++ b/.gitignore @@ -73,6 +73,8 @@ __pycache__/ # Node node_modules/ package-lock.json +# ...except for servers, where the lock pins the image's dependencies by hash +!src/Servers/**/package-lock.json # Probe results (local testing) probe-*.json diff --git a/docs/content/servers/uwebsockets.md b/docs/content/servers/uwebsockets.md new file mode 100644 index 0000000..9e0d7a2 --- /dev/null +++ b/docs/content/servers/uwebsockets.md @@ -0,0 +1,137 @@ +--- +title: "uWebSockets.js" +description: "uWebSockets.js (JavaScript) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** JavaScript · [View source on GitHub](https://github.com/MDA2AV/Http11Probe/tree/main/src/Servers/UWebSocketsServer) + +## Dockerfile + +```dockerfile +# trixie, not the default bookworm: uWS ships prebuilt binaries needing glibc >= 2.38 +FROM node:22-trixie-slim +WORKDIR /app +COPY src/Servers/UWebSocketsServer/package.json src/Servers/UWebSocketsServer/package-lock.json ./ +RUN npm ci --omit=dev --ignore-scripts +COPY src/Servers/UWebSocketsServer/server.js . +USER node +ENTRYPOINT ["node", "server.js", "8080"] +``` + +## Source — `package.json` + +uWebSockets.js is not published to the npm registry, so it is pinned to a release tarball. Release tags carry prebuilt `.node` binaries, so there is no build step and no lifecycle scripts to run. A generated `package-lock.json` sits alongside this file, pinning the tarball by sha512 integrity hash so `npm ci` gets identical bytes on every build. + +```json +{ + "name": "uwebsockets-server", + "private": true, + "dependencies": { + "uWebSockets.js": "https://github.com/uNetworking/uWebSockets.js/archive/refs/tags/v20.69.0.tar.gz" + } +} +``` + +## Source — `server.js` + +```javascript +const uWS = require('uWebSockets.js'); + +const port = Number.parseInt(process.argv[2] || '8080', 10); + +/* uWS invalidates `req` the moment the handler returns, so everything needed + * later has to be read out synchronously. Only the body echo is async here. */ + +function readBody(res, onDone) { + const chunks = []; + res.onAborted(() => { res.aborted = true; }); + res.onData((ab, isLast) => { + /* The ArrayBuffer is only valid inside this callback — slice(0) copies it. */ + chunks.push(Buffer.from(ab.slice(0))); + if (isLast) onDone(Buffer.concat(chunks)); + }); +} + +const app = uWS.App(); + +app.any('/cookie', (res, req) => { + let body = ''; + const raw = req.getHeader('cookie'); + for (const pair of raw.split(';')) { + const trimmed = pair.trimStart(); + const eq = trimmed.indexOf('='); + if (eq > 0) body += trimmed.substring(0, eq) + '=' + trimmed.substring(eq + 1) + '\n'; + } + res.writeHeader('Content-Type', 'text/plain'); + res.end(body); +}); + +app.any('/echo', (res, req) => { + let body = ''; + req.forEach((name, value) => { body += name + ': ' + value + '\n'; }); + res.writeHeader('Content-Type', 'text/plain'); + res.end(body); +}); + +/* Wildcards must be registered last. */ +app.any('/*', (res, req) => { + if (req.getMethod() === 'post') { + readBody(res, (body) => { + if (res.aborted) return; + /* Cork when responding from an async callback. */ + res.cork(() => { + res.writeHeader('Content-Type', 'text/plain'); + res.end(body); + }); + }); + return; + } + res.writeHeader('Content-Type', 'text/plain'); + res.end('OK'); +}); + +app.listen('0.0.0.0', port, (token) => { + if (!token) { + console.error('Failed to listen on port ' + port); + process.exit(1); + } +}); +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/src/Servers/UWebSocketsServer/Dockerfile b/src/Servers/UWebSocketsServer/Dockerfile new file mode 100644 index 0000000..a6ea591 --- /dev/null +++ b/src/Servers/UWebSocketsServer/Dockerfile @@ -0,0 +1,8 @@ +# trixie, not the default bookworm: uWS ships prebuilt binaries needing glibc >= 2.38 +FROM node:22-trixie-slim +WORKDIR /app +COPY src/Servers/UWebSocketsServer/package.json src/Servers/UWebSocketsServer/package-lock.json ./ +RUN npm ci --omit=dev --ignore-scripts +COPY src/Servers/UWebSocketsServer/server.js . +USER node +ENTRYPOINT ["node", "server.js", "8080"] diff --git a/src/Servers/UWebSocketsServer/package-lock.json b/src/Servers/UWebSocketsServer/package-lock.json new file mode 100644 index 0000000..53c1add --- /dev/null +++ b/src/Servers/UWebSocketsServer/package-lock.json @@ -0,0 +1,19 @@ +{ + "name": "uwebsockets-server", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "uwebsockets-server", + "dependencies": { + "uWebSockets.js": "https://github.com/uNetworking/uWebSockets.js/archive/refs/tags/v20.69.0.tar.gz" + } + }, + "node_modules/uWebSockets.js": { + "version": "20.69.0", + "resolved": "https://github.com/uNetworking/uWebSockets.js/archive/refs/tags/v20.69.0.tar.gz", + "integrity": "sha512-vSARYbg98BfI8G4hB305Oa4iU3Ocon3o+SKzy1iza7hQGECgZSxJJ0/yp7LvPdTdQlY3bVQs+A4Ir7tCGQ2hnA==", + "license": "Apache-2.0" + } + } +} diff --git a/src/Servers/UWebSocketsServer/package.json b/src/Servers/UWebSocketsServer/package.json new file mode 100644 index 0000000..caa1807 --- /dev/null +++ b/src/Servers/UWebSocketsServer/package.json @@ -0,0 +1,7 @@ +{ + "name": "uwebsockets-server", + "private": true, + "dependencies": { + "uWebSockets.js": "https://github.com/uNetworking/uWebSockets.js/archive/refs/tags/v20.69.0.tar.gz" + } +} diff --git a/src/Servers/UWebSocketsServer/probe.json b/src/Servers/UWebSocketsServer/probe.json new file mode 100644 index 0000000..ee071da --- /dev/null +++ b/src/Servers/UWebSocketsServer/probe.json @@ -0,0 +1 @@ +{"name": "uWebSockets.js", "language": "JavaScript", "repository": "https://github.com/uNetworking/uWebSockets.js"} diff --git a/src/Servers/UWebSocketsServer/server.js b/src/Servers/UWebSocketsServer/server.js new file mode 100644 index 0000000..6868cee --- /dev/null +++ b/src/Servers/UWebSocketsServer/server.js @@ -0,0 +1,61 @@ +const uWS = require('uWebSockets.js'); + +const port = Number.parseInt(process.argv[2] || '8080', 10); + +/* uWS invalidates `req` the moment the handler returns, so everything needed + * later has to be read out synchronously. Only the body echo is async here. */ + +function readBody(res, onDone) { + const chunks = []; + res.onAborted(() => { res.aborted = true; }); + res.onData((ab, isLast) => { + /* The ArrayBuffer is only valid inside this callback — slice(0) copies it. */ + chunks.push(Buffer.from(ab.slice(0))); + if (isLast) onDone(Buffer.concat(chunks)); + }); +} + +const app = uWS.App(); + +app.any('/cookie', (res, req) => { + let body = ''; + const raw = req.getHeader('cookie'); + for (const pair of raw.split(';')) { + const trimmed = pair.trimStart(); + const eq = trimmed.indexOf('='); + if (eq > 0) body += trimmed.substring(0, eq) + '=' + trimmed.substring(eq + 1) + '\n'; + } + res.writeHeader('Content-Type', 'text/plain'); + res.end(body); +}); + +app.any('/echo', (res, req) => { + let body = ''; + req.forEach((name, value) => { body += name + ': ' + value + '\n'; }); + res.writeHeader('Content-Type', 'text/plain'); + res.end(body); +}); + +/* Wildcards must be registered last. */ +app.any('/*', (res, req) => { + if (req.getMethod() === 'post') { + readBody(res, (body) => { + if (res.aborted) return; + /* Cork when responding from an async callback. */ + res.cork(() => { + res.writeHeader('Content-Type', 'text/plain'); + res.end(body); + }); + }); + return; + } + res.writeHeader('Content-Type', 'text/plain'); + res.end('OK'); +}); + +app.listen('0.0.0.0', port, (token) => { + if (!token) { + console.error('Failed to listen on port ' + port); + process.exit(1); + } +});