From e5b7a5cca96d4476fd1137307511f301e980aad2 Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Sat, 15 Aug 2026 17:59:06 +0100 Subject: [PATCH 1/2] Add uWebSockets.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Requested upstream in uNetworking/uWebSockets.js#1298, where the maintainer asked to add uWS here and fix standards behaviour rather than chase benchmark lists. Implements the four probe endpoints with the idiomatic uWS API: any() routes with the wildcard registered last, req.forEach for /echo, and onData/onAborted with a cork()ed reply for the POST body echo. Header and cookie handling mirror the Node and Express servers so the three stay comparable. uWS is not on the npm registry, so the dependency is pinned to the v20.69.0 release tarball — release tags carry prebuilt .node binaries, so no build step and no git in the image. The base image is node:22-trixie-slim rather than the usual node:22-slim because those binaries need glibc 2.38 or newer and bookworm ships 2.36; on bookworm the module fails to load at startup. --- docs/content/servers/uwebsockets.md | 136 +++++++++++++++++++++ src/Servers/UWebSocketsServer/Dockerfile | 7 ++ src/Servers/UWebSocketsServer/package.json | 7 ++ src/Servers/UWebSocketsServer/probe.json | 1 + src/Servers/UWebSocketsServer/server.js | 61 +++++++++ 5 files changed, 212 insertions(+) create mode 100644 docs/content/servers/uwebsockets.md create mode 100644 src/Servers/UWebSocketsServer/Dockerfile create mode 100644 src/Servers/UWebSocketsServer/package.json create mode 100644 src/Servers/UWebSocketsServer/probe.json create mode 100644 src/Servers/UWebSocketsServer/server.js diff --git a/docs/content/servers/uwebsockets.md b/docs/content/servers/uwebsockets.md new file mode 100644 index 0000000..b18e5e6 --- /dev/null +++ b/docs/content/servers/uwebsockets.md @@ -0,0 +1,136 @@ +--- +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 . +RUN npm install --omit=dev +COPY src/Servers/UWebSocketsServer/server.js . +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. + +```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 = 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..371eec6 --- /dev/null +++ b/src/Servers/UWebSocketsServer/Dockerfile @@ -0,0 +1,7 @@ +# 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 . +RUN npm install --omit=dev +COPY src/Servers/UWebSocketsServer/server.js . +ENTRYPOINT ["node", "server.js", "8080"] 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..a379574 --- /dev/null +++ b/src/Servers/UWebSocketsServer/server.js @@ -0,0 +1,61 @@ +const uWS = require('uWebSockets.js'); + +const port = 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); + } +}); From 77706a9fa043007f5451e26735be4906ec5c798d Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Sat, 15 Aug 2026 18:08:51 +0100 Subject: [PATCH 2/2] Address SonarCloud findings on the uWS server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quality gate failed on Security Rating C for new code, from three major findings on the Dockerfile and one minor one. Add a generated package-lock.json and switch to npm ci, which pins the release tarball by sha512 integrity hash rather than trusting whatever the tag currently resolves to (docker:S8543, text:S8564). Pass --ignore-scripts; uWS declares no lifecycle scripts, so this costs nothing and closes the install-time execution path (docker:S6505). Run as the image's node user instead of root, matching Trillium, Effinitive, Horse and Swerver (docker:S6471) — port 8080 needs no privilege. .gitignore excluded every package-lock.json, which would have left the image building from an unpinned tarball in CI while working locally. Negate it for src/Servers so server locks are tracked; web/ is unaffected. Also Number.parseInt over the global (javascript:S7773). None of this touches HTTP behaviour: the suite scores 119/159 before and after, identical failure set. --- .gitignore | 2 ++ docs/content/servers/uwebsockets.md | 9 +++++---- src/Servers/UWebSocketsServer/Dockerfile | 5 +++-- .../UWebSocketsServer/package-lock.json | 19 +++++++++++++++++++ src/Servers/UWebSocketsServer/server.js | 2 +- 5 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 src/Servers/UWebSocketsServer/package-lock.json 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 index b18e5e6..9e0d7a2 100644 --- a/docs/content/servers/uwebsockets.md +++ b/docs/content/servers/uwebsockets.md @@ -13,15 +13,16 @@ breadcrumbs: false # 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 . -RUN npm install --omit=dev +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. +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 { @@ -38,7 +39,7 @@ uWebSockets.js is not published to the npm registry, so it is pinned to a releas ```javascript const uWS = require('uWebSockets.js'); -const port = parseInt(process.argv[2] || '8080', 10); +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. */ diff --git a/src/Servers/UWebSocketsServer/Dockerfile b/src/Servers/UWebSocketsServer/Dockerfile index 371eec6..a6ea591 100644 --- a/src/Servers/UWebSocketsServer/Dockerfile +++ b/src/Servers/UWebSocketsServer/Dockerfile @@ -1,7 +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 . -RUN npm install --omit=dev +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/server.js b/src/Servers/UWebSocketsServer/server.js index a379574..6868cee 100644 --- a/src/Servers/UWebSocketsServer/server.js +++ b/src/Servers/UWebSocketsServer/server.js @@ -1,6 +1,6 @@ const uWS = require('uWebSockets.js'); -const port = parseInt(process.argv[2] || '8080', 10); +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. */