diff --git a/packages/safe-chain/src/registryProxy/plainHttpProxy.js b/packages/safe-chain/src/registryProxy/plainHttpProxy.js new file mode 100644 index 0000000..e337b44 --- /dev/null +++ b/packages/safe-chain/src/registryProxy/plainHttpProxy.js @@ -0,0 +1,69 @@ +import * as http from "http"; +import * as https from "https"; + +export function handleHttpProxyRequest(req, res) { + const url = new URL(req.url); + + // The protocol for the plainHttpProxy should usually only be http: + // but when the client for some reason sends an https: request directly + // instead of using the CONNECT method, we should handle it gracefully. + let protocol; + if (url.protocol === "http:") { + protocol = http; + } else if (url.protocol === "https:") { + protocol = https; + } else { + res.writeHead(502); + res.end(`Bad Gateway: Unsupported protocol ${url.protocol}`); + return; + } + + const proxyRequest = protocol + .request( + req.url, + { method: req.method, headers: req.headers }, + (proxyRes) => { + res.writeHead(proxyRes.statusCode, proxyRes.headers); + proxyRes.pipe(res); + + proxyRes.on("error", () => { + // Proxy response stream error + // Clean up client response stream + if (res.writable) { + res.end(); + } + }); + + proxyRes.on("close", () => { + // Clean up if the proxy response stream closes + if (res.writable) { + res.end(); + } + }); + } + ) + .on("error", (err) => { + res.writeHead(502); + res.end(`Bad Gateway: ${err.message}`); + }); + + req.on("error", () => { + // Client request stream error + // Abort the proxy request + proxyRequest.destroy(); + }); + + res.on("error", () => { + // Client response stream error (client disconnected) + // Clean up proxy streams + proxyRequest.destroy(); + }); + + res.on("close", () => { + // Client disconnected + // Abort the proxy request to avoid unnecessary work + proxyRequest.destroy(); + }); + + req.pipe(proxyRequest); +} diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index 3558673..b0e8dd1 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -1,6 +1,7 @@ import * as http from "http"; import { tunnelRequest } from "./tunnelRequestHandler.js"; import { mitmConnect } from "./mitmRequestHandler.js"; +import { handleHttpProxyRequest } from "./plainHttpProxy.js"; import { getCaCertPath } from "./certUtils.js"; import { auditChanges } from "../scanning/audit/index.js"; import { knownRegistries, parsePackageFromUrl } from "./parsePackageFromUrl.js"; @@ -15,7 +16,6 @@ const state = { export function createSafeChainProxy() { const server = createProxyServer(); - server.on("connect", handleConnect); return { startServer: () => startServer(server), @@ -54,13 +54,15 @@ export function mergeSafeChainProxyEnvironmentVariables(env) { } function createProxyServer() { - const server = http.createServer((_, res) => { - res.writeHead(400, "Bad Request"); - res.write( - "Safe-chain proxy: Direct http not supported. Only CONNECT requests are allowed." - ); - res.end(); - }); + const server = http.createServer( + // This handles direct HTTP requests (non-CONNECT requests) + // This is normally http-only traffic, but we also handle + // https for clients that don't properly use CONNECT + handleHttpProxyRequest + ); + + // This handles HTTPS requests via the CONNECT method + server.on("connect", handleConnect); return server; } diff --git a/test/e2e/DockerTestContainer.js b/test/e2e/DockerTestContainer.js index 483f03a..ec1af3c 100644 --- a/test/e2e/DockerTestContainer.js +++ b/test/e2e/DockerTestContainer.js @@ -60,6 +60,26 @@ export class DockerTestContainer { } } + dockerExec(command, daemon = false) { + if (!this.isRunning) { + throw new Error("Container is not running"); + } + + try { + const dockerExecCommand = `docker exec ${daemon ? "-d " : " "}${ + this.containerName + } bash -c "${command}"`; + const output = execSync(dockerExecCommand, { + encoding: "utf-8", + stdio: "pipe", + timeout: 10000, + }); + return output; + } catch (error) { + throw new Error(`Failed to execute command: ${error.message}`); + } + } + async openShell(shell) { let ptyProcess = pty.spawn( "docker", @@ -96,9 +116,11 @@ export class DockerTestContainer { const timeout = setTimeout(() => { // Fallback in case the command doesn't finish in a reasonable time + // oxlint-disable-next-line no-console - having this log in CI helps diagnose issues + console.log("Command timeout reached"); resolve({ allData, output: parseShellOutput(allData), command }); ptyProcess.removeListener("data", handleInput); - }, 10000); + }, 15000); function handleInput(data) { allData.push(data);