From bd5617487048180fbb0b502de6c99d9f92353748 Mon Sep 17 00:00:00 2001 From: Hampus Date: Thu, 8 Oct 2026 17:26:41 +0200 Subject: [PATCH] fix(desktop): frame streamed request bodies for every method (#3282) --- .../src/main/DesktopOutboundHTTP.test.mjs | 52 +++++++++++++++++++ .../src/main/DesktopOutboundHTTP.ts | 3 ++ 2 files changed, 55 insertions(+) diff --git a/fluxer_desktop/src/main/DesktopOutboundHTTP.test.mjs b/fluxer_desktop/src/main/DesktopOutboundHTTP.test.mjs index 97d651030..34af111c5 100644 --- a/fluxer_desktop/src/main/DesktopOutboundHTTP.test.mjs +++ b/fluxer_desktop/src/main/DesktopOutboundHTTP.test.mjs @@ -302,6 +302,58 @@ describe('DesktopOutboundHTTP origin anchoring', () => { } }); + test('a streamed request body reaches the server for every method that carries one', async () => { + const received = []; + const {port, server} = await startServer({ + '/api/v1/oauth2/applications/1': (request, response) => { + let body = ''; + request.setEncoding('utf8'); + request.on('data', (chunk) => { + body += chunk; + }); + request.on('end', () => { + received.push({body, method: request.method, sudo: request.headers['x-fluxer-sudo-mode-jwt'] ?? null}); + response.writeHead(204, {'x-fluxer-sudo-mode-jwt': 'granted'}); + response.end(); + }); + }, + }); + try { + const outboundHTTP = new DesktopOutboundHTTP({ + resolveHostAddresses: stubResolver({'lan.example': ['127.0.0.1']}), + }); + await outboundHTTP.registerAnchoredOrigins({anchorOrigin: `http://lan.example:${port}`, origins: []}); + const payload = JSON.stringify({mfa_method: 'totp', mfa_code: '123456'}); + for (const method of ['DELETE', 'POST', 'PATCH', 'PUT']) { + const message = await outboundHTTP.request({ + body: Readable.toWeb(Readable.from([Buffer.from(payload)])), + expectedOrigin: `http://lan.example:${port}`, + headers: {'content-type': 'application/json', 'x-fluxer-sudo-mode-jwt': 'previous'}, + method, + originTrust: DesktopOriginTrust.REGISTERED, + serviceName: 'test', + signal: null, + timeoutMs: 2000, + url: `http://lan.example:${port}/api/v1/oauth2/applications/1`, + }); + assert.equal(message.status, 204); + assert.equal(message.headers['x-fluxer-sudo-mode-jwt'], 'granted'); + message.message.resume(); + } + assert.deepEqual( + received.map((entry) => entry.method), + ['DELETE', 'POST', 'PATCH', 'PUT'], + ); + for (const entry of received) { + assert.equal(entry.body, payload); + assert.equal(entry.sudo, 'previous'); + } + outboundHTTP.cleanup(); + } finally { + server.close(); + } + }); + test('a private literal anchor binds and registers without a public requirement', async () => { const outboundHTTP = new DesktopOutboundHTTP({resolveHostAddresses: stubResolver({})}); await outboundHTTP.registerAnchoredOrigins({anchorOrigin: 'http://192.168.1.10', origins: []}); diff --git a/fluxer_desktop/src/main/DesktopOutboundHTTP.ts b/fluxer_desktop/src/main/DesktopOutboundHTTP.ts index bf50f63b6..bf50bafb2 100644 --- a/fluxer_desktop/src/main/DesktopOutboundHTTP.ts +++ b/fluxer_desktop/src/main/DesktopOutboundHTTP.ts @@ -1356,6 +1356,9 @@ export class DesktopOutboundHTTP { clientRequest.end(Buffer.from(body)); return; } + if (!clientRequest.hasHeader('content-length') && !clientRequest.hasHeader('transfer-encoding')) { + clientRequest.setHeader('Transfer-Encoding', 'chunked'); + } const source = Readable.fromWeb(body as unknown as NodeReadableStream); const limit = createRequestBodyLimit( request.maximumRequestBodyBytes ?? DESKTOP_OUTBOUND_HTTP_MAX_REQUEST_BODY_BYTES,