Merge pull request #108 from AikidoSec/proxy-http-requests

Allow the safe-chain to act as a regular http proxy too (besides the CONNECT tunneling implementation)
This commit is contained in:
bitterpanda 2025-10-15 12:02:40 +02:00 committed by GitHub
commit 9cec5e4bc9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 102 additions and 9 deletions

View file

@ -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);
}

View file

@ -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."
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
);
res.end();
});
// This handles HTTPS requests via the CONNECT method
server.on("connect", handleConnect);
return server;
}

View file

@ -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);