Simplify setting certificates

This commit is contained in:
Reinier Criel 2025-10-28 13:56:27 -07:00
parent b886bb1cfe
commit 70dc89c3e8
4 changed files with 77 additions and 16 deletions

View file

@ -1,13 +1,20 @@
import { ui } from "../../environment/userInteraction.js"; import { ui } from "../../environment/userInteraction.js";
import { safeSpawn } from "../../utils/safeSpawn.js"; import { safeSpawn } from "../../utils/safeSpawn.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
import { getCaCertPath } from "../../registryProxy/certUtils.js";
export async function runPip(command, args) { export async function runPip(command, args) {
try { try {
const result = await safeSpawn(command, args, { const env = mergeSafeChainProxyEnvironmentVariables(process.env);
// Pass --cert with our CA to pip so it trusts our MITM for known registries.
// pip will append this to its default CA bundle, so it still validates
// non-registry HTTPS (GitHub, custom mirrors) against system CAs.
const finalArgs = [...args, "--cert", getCaCertPath()];
const result = await safeSpawn(command, finalArgs, {
stdio: "inherit", stdio: "inherit",
env: mergeSafeChainProxyEnvironmentVariables(process.env), env,
}); });
return { status: result.status }; return { status: result.status };
} catch (error) { } catch (error) {

View file

@ -0,0 +1,63 @@
import { describe, it, beforeEach, afterEach, mock } from "node:test";
import assert from "node:assert";
describe("runPipCommand --cert handling", () => {
let runPip;
let capturedArgs = null;
beforeEach(async () => {
capturedArgs = null;
// Mock safeSpawn to capture args
mock.module("../../utils/safeSpawn.js", {
namedExports: {
safeSpawn: async (command, args, options) => {
capturedArgs = { command, args, options };
return { status: 0 };
},
},
});
// Mock proxy env merge
mock.module("../../registryProxy/registryProxy.js", {
namedExports: {
mergeSafeChainProxyEnvironmentVariables: (env) => ({
...env,
HTTPS_PROXY: "http://localhost:8080",
}),
},
});
// Mock certUtils to point to a test CA path
mock.module("../../registryProxy/certUtils.js", {
namedExports: {
getCaCertPath: () => "/tmp/test-ca.pem",
},
});
const mod = await import("./runPipCommand.js");
runPip = mod.runPip;
});
afterEach(() => {
mock.reset();
});
it("should append --cert with our CA path to pip args", async () => {
const res = await runPip("pip3", ["install", "requests"]);
assert.strictEqual(res.status, 0);
// safeSpawn should be called with --cert flag
assert.ok(capturedArgs, "safeSpawn should have been called");
const idx = capturedArgs.args.indexOf("--cert");
assert.ok(idx >= 0, "--cert flag should be present in pip args");
const certPath = capturedArgs.args[idx + 1];
assert.strictEqual(certPath, "/tmp/test-ca.pem", "CA path should match getCaCertPath()");
// Original args should be preserved before --cert
assert.strictEqual(capturedArgs.args[0], "install");
assert.strictEqual(capturedArgs.args[1], "requests");
});
});

View file

@ -34,14 +34,6 @@ function getSafeChainProxyEnvironmentVariables() {
HTTPS_PROXY: `http://localhost:${state.port}`, HTTPS_PROXY: `http://localhost:${state.port}`,
GLOBAL_AGENT_HTTP_PROXY: `http://localhost:${state.port}`, GLOBAL_AGENT_HTTP_PROXY: `http://localhost:${state.port}`,
NODE_EXTRA_CA_CERTS: getCaCertPath(), NODE_EXTRA_CA_CERTS: getCaCertPath(),
// Following env vars point pip and Python's requests/urllib at a CA Cert file.
// pip checks PIP_CERT first
// If pip uses requests library internally, it needs REQUESTS_CA_BUNDLE
// Other Python packages or pip's fallback SSL code may use SSL_CERT_FILE
PIP_CERT: getCaCertPath(),
REQUESTS_CA_BUNDLE: getCaCertPath(),
SSL_CERT_FILE: getCaCertPath(),
}; };
} }

View file

@ -145,12 +145,11 @@ describe("registryProxy.mitm", () => {
}); });
// --- Pip registry MITM and env var tests --- // --- Pip registry MITM and env var tests ---
it("should set pip CA trust environment variables", () => { it("should NOT set global Python CA environment variables", () => {
const envVars = mergeSafeChainProxyEnvironmentVariables([]); const envVars = mergeSafeChainProxyEnvironmentVariables([]);
const caPath = getCaCertPath(); assert.strictEqual(envVars.PIP_CERT, undefined);
assert.strictEqual(envVars.PIP_CERT, caPath); assert.strictEqual(envVars.REQUESTS_CA_BUNDLE, undefined);
assert.strictEqual(envVars.REQUESTS_CA_BUNDLE, caPath); assert.strictEqual(envVars.SSL_CERT_FILE, undefined);
assert.strictEqual(envVars.SSL_CERT_FILE, caPath);
}); });
it("should intercept HTTPS requests to pypi.org for pip package", async () => { it("should intercept HTTPS requests to pypi.org for pip package", async () => {