mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Use CA bundle when using rama proxy
This commit is contained in:
parent
9a7c054a3f
commit
ba604eaeaa
12 changed files with 267 additions and 421 deletions
|
|
@ -1,7 +1,9 @@
|
||||||
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 {
|
||||||
import { getCombinedCaBundlePath } from "../../registryProxy/builtInProxy/certBundle.js";
|
getProxySettings,
|
||||||
|
mergeSafeChainProxyEnvironmentVariables,
|
||||||
|
} from "../../registryProxy/registryProxy.js";
|
||||||
import {
|
import {
|
||||||
PIP_COMMAND,
|
PIP_COMMAND,
|
||||||
PIP3_COMMAND,
|
PIP3_COMMAND,
|
||||||
|
|
@ -118,7 +120,7 @@ export async function runPip(command, args) {
|
||||||
// Always provide Python with a complete CA bundle (Safe Chain CA + Mozilla + Node built-in roots + user certs)
|
// Always provide Python with a complete CA bundle (Safe Chain CA + Mozilla + Node built-in roots + user certs)
|
||||||
// so that any network request made by pip, including those outside explicit CLI args,
|
// so that any network request made by pip, including those outside explicit CLI args,
|
||||||
// validates correctly under both MITM'd and tunneled HTTPS.
|
// validates correctly under both MITM'd and tunneled HTTPS.
|
||||||
const combinedCaPath = getCombinedCaBundlePath();
|
const combinedCaPath = getProxySettings().caCertBundlePath;
|
||||||
|
|
||||||
// Commands that need access to persistent config/cache/state files
|
// Commands that need access to persistent config/cache/state files
|
||||||
// These should not have PIP_CONFIG_FILE overridden as it would prevent them from
|
// These should not have PIP_CONFIG_FILE overridden as it would prevent them from
|
||||||
|
|
|
||||||
|
|
@ -48,11 +48,17 @@ describe("runPipCommand environment variable handling", () => {
|
||||||
HTTPS_PROXY: "http://localhost:8080",
|
HTTPS_PROXY: "http://localhost:8080",
|
||||||
HTTP_PROXY: "",
|
HTTP_PROXY: "",
|
||||||
}),
|
}),
|
||||||
|
getProxySettings: () => {
|
||||||
|
return {
|
||||||
|
proxyUrl: "http://localhost:8080",
|
||||||
|
caCertBundlePath: "/tmp/test-combined-ca.pem",
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mock certBundle to return a test combined bundle path
|
// Mock certBundle to return a test combined bundle path
|
||||||
mock.module("../../registryProxy/builtInProxy/certBundle.js", {
|
mock.module("../../registryProxy/certBundle.js", {
|
||||||
namedExports: {
|
namedExports: {
|
||||||
getCombinedCaBundlePath: () => "/tmp/test-combined-ca.pem",
|
getCombinedCaBundlePath: () => "/tmp/test-combined-ca.pem",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
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 {
|
||||||
import { getCombinedCaBundlePath } from "../../registryProxy/builtInProxy/certBundle.js";
|
getProxySettings,
|
||||||
|
mergeSafeChainProxyEnvironmentVariables,
|
||||||
|
} from "../../registryProxy/registryProxy.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets CA bundle environment variables used by Python libraries and pipx.
|
* Sets CA bundle environment variables used by Python libraries and pipx.
|
||||||
|
|
@ -47,7 +49,7 @@ export async function runPipX(command, args) {
|
||||||
try {
|
try {
|
||||||
const env = mergeSafeChainProxyEnvironmentVariables(process.env);
|
const env = mergeSafeChainProxyEnvironmentVariables(process.env);
|
||||||
|
|
||||||
const combinedCaPath = getCombinedCaBundlePath();
|
const combinedCaPath = getProxySettings().caCertBundlePath;
|
||||||
const modifiedEnv = getPipXCaBundleEnvironmentVariables(
|
const modifiedEnv = getPipXCaBundleEnvironmentVariables(
|
||||||
env,
|
env,
|
||||||
combinedCaPath,
|
combinedCaPath,
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,16 @@ describe("runPipXCommand", () => {
|
||||||
mergeCalls.push(env);
|
mergeCalls.push(env);
|
||||||
return { ...env, ...mergedEnvReturn };
|
return { ...env, ...mergedEnvReturn };
|
||||||
},
|
},
|
||||||
|
getProxySettings: () => {
|
||||||
|
return {
|
||||||
|
proxyUrl: "",
|
||||||
|
caCertBundlePath: "/tmp/test-combined-ca.pem",
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
mock.module("../../registryProxy/builtInProxy/certBundle.js", {
|
mock.module("../../registryProxy/certBundle.js", {
|
||||||
namedExports: {
|
namedExports: {
|
||||||
getCombinedCaBundlePath: () => "/tmp/test-combined-ca.pem",
|
getCombinedCaBundlePath: () => "/tmp/test-combined-ca.pem",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
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 {
|
||||||
import { getCombinedCaBundlePath } from "../../registryProxy/builtInProxy/certBundle.js";
|
getProxySettings,
|
||||||
|
mergeSafeChainProxyEnvironmentVariables,
|
||||||
|
} from "../../registryProxy/registryProxy.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {import("../currentPackageManager.js").PackageManager}
|
* @returns {import("../currentPackageManager.js").PackageManager}
|
||||||
|
|
@ -62,7 +64,7 @@ async function runPoetryCommand(args) {
|
||||||
try {
|
try {
|
||||||
const env = mergeSafeChainProxyEnvironmentVariables(process.env);
|
const env = mergeSafeChainProxyEnvironmentVariables(process.env);
|
||||||
|
|
||||||
const combinedCaPath = getCombinedCaBundlePath();
|
const combinedCaPath = getProxySettings().caCertBundlePath;
|
||||||
setPoetryCaBundleEnvironmentVariables(env, combinedCaPath);
|
setPoetryCaBundleEnvironmentVariables(env, combinedCaPath);
|
||||||
|
|
||||||
const result = await safeSpawn("poetry", args, {
|
const result = await safeSpawn("poetry", args, {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
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 {
|
||||||
import { getCombinedCaBundlePath } from "../../registryProxy/builtInProxy/certBundle.js";
|
getProxySettings,
|
||||||
|
mergeSafeChainProxyEnvironmentVariables,
|
||||||
|
} from "../../registryProxy/registryProxy.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets CA bundle environment variables used by Python libraries and uv.
|
* Sets CA bundle environment variables used by Python libraries and uv.
|
||||||
|
|
@ -53,7 +55,7 @@ export async function runUv(command, args) {
|
||||||
try {
|
try {
|
||||||
const env = mergeSafeChainProxyEnvironmentVariables(process.env);
|
const env = mergeSafeChainProxyEnvironmentVariables(process.env);
|
||||||
|
|
||||||
const combinedCaPath = getCombinedCaBundlePath();
|
const combinedCaPath = getProxySettings().caCertBundlePath;
|
||||||
setUvCaBundleEnvironmentVariables(env, combinedCaPath);
|
setUvCaBundleEnvironmentVariables(env, combinedCaPath);
|
||||||
|
|
||||||
// Note: uv uses HTTPS_PROXY and HTTP_PROXY environment variables for proxy configuration
|
// Note: uv uses HTTPS_PROXY and HTTP_PROXY environment variables for proxy configuration
|
||||||
|
|
|
||||||
|
|
@ -1,379 +0,0 @@
|
||||||
import { describe, it, beforeEach, mock } from "node:test";
|
|
||||||
import assert from "node:assert";
|
|
||||||
import fs from "node:fs";
|
|
||||||
import os from "node:os";
|
|
||||||
import path from "node:path";
|
|
||||||
import tls from "node:tls";
|
|
||||||
|
|
||||||
// Utility to remove the generated bundle so the module rebuilds it on demand
|
|
||||||
function removeBundleIfExists() {
|
|
||||||
const target = path.join(os.tmpdir(), "safe-chain-ca-bundle.pem");
|
|
||||||
try {
|
|
||||||
if (fs.existsSync(target)) fs.unlinkSync(target);
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility to get a valid PEM certificate for testing
|
|
||||||
function getValidCert() {
|
|
||||||
const cert = typeof tls.rootCertificates?.[0] === "string" ? tls.rootCertificates[0] : "";
|
|
||||||
assert.ok(cert.includes("BEGIN CERTIFICATE"), "Environment lacks Node root certificates for test");
|
|
||||||
return cert;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("certBundle.getCombinedCaBundlePath", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
mock.restoreAll();
|
|
||||||
removeBundleIfExists();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("includes Safe Chain CA when parsable and produces a PEM bundle", async () => {
|
|
||||||
// Prepare a temporary Safe Chain CA file with a recognizable marker and a valid cert block
|
|
||||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pipcabundle-"));
|
|
||||||
const safeChainPath = path.join(tmpDir, "safechain-ca.pem");
|
|
||||||
const marker = "# SAFE_CHAIN_TEST_MARKER";
|
|
||||||
const rootPem = typeof tls.rootCertificates?.[0] === "string" ? tls.rootCertificates[0] : "";
|
|
||||||
assert.ok(rootPem.includes("BEGIN CERTIFICATE"), "Environment lacks Node root certificates for test");
|
|
||||||
fs.writeFileSync(safeChainPath, `${marker}\n${rootPem}`, "utf8");
|
|
||||||
|
|
||||||
// Mock the certUtils.getCaCertPath to return our temp file
|
|
||||||
mock.module("./certUtils.js", {
|
|
||||||
namedExports: {
|
|
||||||
getCaCertPath: () => safeChainPath,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { getCombinedCaBundlePath } = await import("./certBundle.js");
|
|
||||||
const bundlePath = getCombinedCaBundlePath();
|
|
||||||
assert.ok(fs.existsSync(bundlePath), "Bundle path should exist");
|
|
||||||
const contents = fs.readFileSync(bundlePath, "utf8");
|
|
||||||
assert.match(contents, /-----BEGIN CERTIFICATE-----/);
|
|
||||||
assert.ok(contents.includes(marker), "Bundle should include Safe Chain CA content when parsable");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("ignores invalid Safe Chain CA but still builds from other sources", async () => {
|
|
||||||
// Write an invalid file (no cert blocks)
|
|
||||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pipcabundle-"));
|
|
||||||
const safeChainPath = path.join(tmpDir, "safechain-invalid.pem");
|
|
||||||
const invalidMarker = "INVALID_SAFE_CHAIN_CONTENT";
|
|
||||||
fs.writeFileSync(safeChainPath, invalidMarker, "utf8");
|
|
||||||
|
|
||||||
// Mock the certUtils.getCaCertPath to return our invalid file
|
|
||||||
mock.module("./certUtils.js", {
|
|
||||||
namedExports: {
|
|
||||||
getCaCertPath: () => safeChainPath,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ensure fresh build
|
|
||||||
removeBundleIfExists();
|
|
||||||
const { getCombinedCaBundlePath } = await import("./certBundle.js");
|
|
||||||
const bundlePath = getCombinedCaBundlePath();
|
|
||||||
assert.ok(fs.existsSync(bundlePath), "Bundle path should exist");
|
|
||||||
const contents = fs.readFileSync(bundlePath, "utf8");
|
|
||||||
assert.match(contents, /-----BEGIN CERTIFICATE-----/, "Bundle should contain certificate blocks from certifi/Node roots");
|
|
||||||
assert.ok(!contents.includes(invalidMarker), "Bundle should not include invalid Safe Chain content");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("certBundle.getCombinedCaBundlePath with user certs", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
mock.restoreAll();
|
|
||||||
delete process.env.NODE_EXTRA_CA_CERTS;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns a path with full CA bundle (Safe Chain + Mozilla + Node roots) when no user cert in env", async () => {
|
|
||||||
// Mock getCaCertPath to return valid cert
|
|
||||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-"));
|
|
||||||
const safeChainPath = path.join(tmpDir, "safechain.pem");
|
|
||||||
fs.writeFileSync(safeChainPath, getValidCert(), "utf8");
|
|
||||||
|
|
||||||
mock.module("./certUtils.js", {
|
|
||||||
namedExports: {
|
|
||||||
getCaCertPath: () => safeChainPath,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { getCombinedCaBundlePath } = await import("./certBundle.js");
|
|
||||||
const bundlePath = getCombinedCaBundlePath();
|
|
||||||
|
|
||||||
assert.ok(fs.existsSync(bundlePath), "Bundle path should exist");
|
|
||||||
const contents = fs.readFileSync(bundlePath, "utf8");
|
|
||||||
assert.match(contents, /-----BEGIN CERTIFICATE-----/, "Should contain certificate blocks");
|
|
||||||
// Should include base bundle (Safe Chain + Mozilla/Node roots)
|
|
||||||
assert.ok(contents.length > 1000, "Bundle should be substantial with Mozilla/Node roots included");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("merges user cert with full base bundle (Safe Chain CA + Mozilla + Node roots)", async () => {
|
|
||||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-"));
|
|
||||||
|
|
||||||
// Create Safe Chain CA
|
|
||||||
const safeChainPath = path.join(tmpDir, "safechain.pem");
|
|
||||||
const safeChainCert = getValidCert();
|
|
||||||
fs.writeFileSync(safeChainPath, safeChainCert, "utf8");
|
|
||||||
|
|
||||||
// Create user cert file
|
|
||||||
const userCertPath = path.join(tmpDir, "user-cert.pem");
|
|
||||||
const userCert = getValidCert();
|
|
||||||
fs.writeFileSync(userCertPath, userCert, "utf8");
|
|
||||||
process.env.NODE_EXTRA_CA_CERTS = userCertPath;
|
|
||||||
|
|
||||||
mock.module("./certUtils.js", {
|
|
||||||
namedExports: {
|
|
||||||
getCaCertPath: () => safeChainPath,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { getCombinedCaBundlePath } = await import("./certBundle.js");
|
|
||||||
const bundlePath = getCombinedCaBundlePath();
|
|
||||||
|
|
||||||
assert.ok(fs.existsSync(bundlePath), "Bundle path should exist");
|
|
||||||
const contents = fs.readFileSync(bundlePath, "utf8");
|
|
||||||
|
|
||||||
// Both certs should be in the bundle
|
|
||||||
const certCount = (contents.match(/-----BEGIN CERTIFICATE-----/g) || []).length;
|
|
||||||
assert.ok(certCount >= 2, "Bundle should contain both Safe Chain and user certificates");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("ignores non-existent user cert path", async () => {
|
|
||||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-"));
|
|
||||||
const safeChainPath = path.join(tmpDir, "safechain.pem");
|
|
||||||
fs.writeFileSync(safeChainPath, getValidCert(), "utf8");
|
|
||||||
process.env.NODE_EXTRA_CA_CERTS = "/nonexistent/path.pem";
|
|
||||||
|
|
||||||
mock.module("./certUtils.js", {
|
|
||||||
namedExports: {
|
|
||||||
getCaCertPath: () => safeChainPath,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { getCombinedCaBundlePath } = await import("./certBundle.js");
|
|
||||||
const bundlePath = getCombinedCaBundlePath();
|
|
||||||
|
|
||||||
assert.ok(fs.existsSync(bundlePath), "Bundle path should exist");
|
|
||||||
const contents = fs.readFileSync(bundlePath, "utf8");
|
|
||||||
// Should still have Safe Chain CA
|
|
||||||
assert.match(contents, /-----BEGIN CERTIFICATE-----/, "Should contain Safe Chain CA");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("ignores invalid PEM user cert", async () => {
|
|
||||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-"));
|
|
||||||
|
|
||||||
const safeChainPath = path.join(tmpDir, "safechain.pem");
|
|
||||||
fs.writeFileSync(safeChainPath, getValidCert(), "utf8");
|
|
||||||
|
|
||||||
const userCertPath = path.join(tmpDir, "invalid.pem");
|
|
||||||
fs.writeFileSync(userCertPath, "NOT A VALID PEM", "utf8");
|
|
||||||
process.env.NODE_EXTRA_CA_CERTS = userCertPath;
|
|
||||||
|
|
||||||
mock.module("./certUtils.js", {
|
|
||||||
namedExports: {
|
|
||||||
getCaCertPath: () => safeChainPath,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { getCombinedCaBundlePath } = await import("./certBundle.js");
|
|
||||||
const bundlePath = getCombinedCaBundlePath();
|
|
||||||
|
|
||||||
assert.ok(fs.existsSync(bundlePath), "Bundle path should exist");
|
|
||||||
const contents = fs.readFileSync(bundlePath, "utf8");
|
|
||||||
// Should still have Safe Chain CA only
|
|
||||||
assert.match(contents, /-----BEGIN CERTIFICATE-----/, "Should contain Safe Chain CA");
|
|
||||||
assert.ok(!contents.includes("NOT A VALID"), "Should not include invalid cert");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects user cert with path traversal attempts", async () => {
|
|
||||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-"));
|
|
||||||
const safeChainPath = path.join(tmpDir, "safechain.pem");
|
|
||||||
fs.writeFileSync(safeChainPath, getValidCert(), "utf8");
|
|
||||||
|
|
||||||
mock.module("./certUtils.js", {
|
|
||||||
namedExports: {
|
|
||||||
getCaCertPath: () => safeChainPath,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { getCombinedCaBundlePath } = await import("./certBundle.js");
|
|
||||||
process.env.NODE_EXTRA_CA_CERTS = "../../../etc/passwd";
|
|
||||||
const bundlePath = getCombinedCaBundlePath();
|
|
||||||
|
|
||||||
assert.ok(fs.existsSync(bundlePath), "Bundle path should exist");
|
|
||||||
const contents = fs.readFileSync(bundlePath, "utf8");
|
|
||||||
// Should only have Safe Chain CA, rejected the traversal path
|
|
||||||
assert.match(contents, /-----BEGIN CERTIFICATE-----/, "Should contain Safe Chain CA");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects user cert with symlink", async () => {
|
|
||||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-"));
|
|
||||||
|
|
||||||
const safeChainPath = path.join(tmpDir, "safechain.pem");
|
|
||||||
fs.writeFileSync(safeChainPath, getValidCert(), "utf8");
|
|
||||||
|
|
||||||
// Create a target file and a symlink to it
|
|
||||||
const targetCert = path.join(tmpDir, "target.pem");
|
|
||||||
fs.writeFileSync(targetCert, getValidCert(), "utf8");
|
|
||||||
|
|
||||||
const symlinkPath = path.join(tmpDir, "symlink.pem");
|
|
||||||
try {
|
|
||||||
fs.symlinkSync(targetCert, symlinkPath);
|
|
||||||
} catch {
|
|
||||||
// Skip test if symlinks are not supported (e.g., on Windows without admin)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mock.module("./certUtils.js", {
|
|
||||||
namedExports: {
|
|
||||||
getCaCertPath: () => safeChainPath,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { getCombinedCaBundlePath } = await import("./certBundle.js");
|
|
||||||
process.env.NODE_EXTRA_CA_CERTS = symlinkPath;
|
|
||||||
const bundlePath = getCombinedCaBundlePath();
|
|
||||||
|
|
||||||
assert.ok(fs.existsSync(bundlePath), "Bundle path should exist");
|
|
||||||
const contents = fs.readFileSync(bundlePath, "utf8");
|
|
||||||
// Should only have Safe Chain CA, symlinks are rejected
|
|
||||||
assert.match(contents, /-----BEGIN CERTIFICATE-----/, "Should contain Safe Chain CA");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects user cert that is a directory", async () => {
|
|
||||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-"));
|
|
||||||
|
|
||||||
const safeChainPath = path.join(tmpDir, "safechain.pem");
|
|
||||||
fs.writeFileSync(safeChainPath, getValidCert(), "utf8");
|
|
||||||
|
|
||||||
const certDir = path.join(tmpDir, "certs");
|
|
||||||
fs.mkdirSync(certDir);
|
|
||||||
|
|
||||||
mock.module("./certUtils.js", {
|
|
||||||
namedExports: {
|
|
||||||
getCaCertPath: () => safeChainPath,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { getCombinedCaBundlePath } = await import("./certBundle.js");
|
|
||||||
process.env.NODE_EXTRA_CA_CERTS = certDir;
|
|
||||||
const bundlePath = getCombinedCaBundlePath();
|
|
||||||
|
|
||||||
assert.ok(fs.existsSync(bundlePath), "Bundle path should exist");
|
|
||||||
const contents = fs.readFileSync(bundlePath, "utf8");
|
|
||||||
// Should only have Safe Chain CA
|
|
||||||
assert.match(contents, /-----BEGIN CERTIFICATE-----/, "Should contain Safe Chain CA");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles empty string user cert path", async () => {
|
|
||||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-"));
|
|
||||||
const safeChainPath = path.join(tmpDir, "safechain.pem");
|
|
||||||
fs.writeFileSync(safeChainPath, getValidCert(), "utf8");
|
|
||||||
|
|
||||||
mock.module("./certUtils.js", {
|
|
||||||
namedExports: {
|
|
||||||
getCaCertPath: () => safeChainPath,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { getCombinedCaBundlePath } = await import("./certBundle.js");
|
|
||||||
process.env.NODE_EXTRA_CA_CERTS = " ";
|
|
||||||
const bundlePath = getCombinedCaBundlePath();
|
|
||||||
|
|
||||||
assert.ok(fs.existsSync(bundlePath), "Bundle path should exist");
|
|
||||||
const contents = fs.readFileSync(bundlePath, "utf8");
|
|
||||||
assert.match(contents, /-----BEGIN CERTIFICATE-----/, "Should contain Safe Chain CA");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("accepts files with CRLF line endings (Windows-style)", async () => {
|
|
||||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-"));
|
|
||||||
const safeChainPath = path.join(tmpDir, "safechain.pem");
|
|
||||||
fs.writeFileSync(safeChainPath, getValidCert(), "utf8");
|
|
||||||
|
|
||||||
// Create a real file with CRLF content to test Windows line ending support
|
|
||||||
const userCertPath = path.join(tmpDir, "user-cert-crlf.pem");
|
|
||||||
const userCert = getValidCert();
|
|
||||||
const certWithCRLF = userCert.replace(/\n/g, "\r\n");
|
|
||||||
fs.writeFileSync(userCertPath, certWithCRLF, "utf8");
|
|
||||||
process.env.NODE_EXTRA_CA_CERTS = userCertPath;
|
|
||||||
|
|
||||||
mock.module("./certUtils.js", {
|
|
||||||
namedExports: {
|
|
||||||
getCaCertPath: () => safeChainPath,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { getCombinedCaBundlePath } = await import("./certBundle.js");
|
|
||||||
const bundlePath = getCombinedCaBundlePath();
|
|
||||||
assert.ok(fs.existsSync(bundlePath), "Bundle path should exist");
|
|
||||||
const contents = fs.readFileSync(bundlePath, "utf8");
|
|
||||||
const certCount = (contents.match(/-----BEGIN CERTIFICATE-----/g) || []).length;
|
|
||||||
assert.ok(certCount >= 2, "Bundle should contain Safe Chain and user certificates with CRLF");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("detects and handles Windows-style path syntax (drive letters and UNC)", async () => {
|
|
||||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-"));
|
|
||||||
const safeChainPath = path.join(tmpDir, "safechain.pem");
|
|
||||||
fs.writeFileSync(safeChainPath, getValidCert(), "utf8");
|
|
||||||
|
|
||||||
mock.module("./certUtils.js", {
|
|
||||||
namedExports: {
|
|
||||||
getCaCertPath: () => safeChainPath,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { getCombinedCaBundlePath } = await import("./certBundle.js");
|
|
||||||
|
|
||||||
// Test that Windows path syntax is recognized (even if files don't exist on macOS/Linux)
|
|
||||||
// These should gracefully fail (return Safe Chain CA only) rather than crash
|
|
||||||
const winPaths = [
|
|
||||||
"C:\\temp\\cert.pem",
|
|
||||||
"D:\\Users\\name\\certs\\ca.pem",
|
|
||||||
"\\\\server\\share\\cert.pem"
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const winPath of winPaths) {
|
|
||||||
process.env.NODE_EXTRA_CA_CERTS = winPath;
|
|
||||||
const bundlePath = getCombinedCaBundlePath();
|
|
||||||
assert.ok(fs.existsSync(bundlePath), `Bundle should exist for ${winPath}`);
|
|
||||||
const contents = fs.readFileSync(bundlePath, "utf8");
|
|
||||||
assert.match(contents, /-----BEGIN CERTIFICATE-----/, "Should contain Safe Chain CA");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects path traversal with Windows-style paths (C:\\temp\\..\\etc\\passwd)", async () => {
|
|
||||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-"));
|
|
||||||
const safeChainPath = path.join(tmpDir, "safechain.pem");
|
|
||||||
fs.writeFileSync(safeChainPath, getValidCert(), "utf8");
|
|
||||||
|
|
||||||
mock.module("./certUtils.js", {
|
|
||||||
namedExports: {
|
|
||||||
getCaCertPath: () => safeChainPath,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { getCombinedCaBundlePath } = await import("./certBundle.js");
|
|
||||||
|
|
||||||
// Test various Windows-style traversal attempts
|
|
||||||
const traversalPaths = [
|
|
||||||
"C:\\temp\\..\\etc\\passwd",
|
|
||||||
"D:\\Users\\..\\..\\Windows\\System32",
|
|
||||||
"\\\\server\\share\\..\\admin",
|
|
||||||
"../../../etc/passwd", // Unix-style for comparison
|
|
||||||
];
|
|
||||||
|
|
||||||
// First, get baseline bundle without user certs to know expected cert count
|
|
||||||
delete process.env.NODE_EXTRA_CA_CERTS;
|
|
||||||
const baselineBundlePath = getCombinedCaBundlePath();
|
|
||||||
const baselineContents = fs.readFileSync(baselineBundlePath, "utf8");
|
|
||||||
const baselineCertCount = (baselineContents.match(/-----BEGIN CERTIFICATE-----/g) || []).length;
|
|
||||||
|
|
||||||
for (const badPath of traversalPaths) {
|
|
||||||
process.env.NODE_EXTRA_CA_CERTS = badPath;
|
|
||||||
const bundlePath = getCombinedCaBundlePath();
|
|
||||||
assert.ok(fs.existsSync(bundlePath), "Bundle path should exist");
|
|
||||||
const contents = fs.readFileSync(bundlePath, "utf8");
|
|
||||||
// Should contain base bundle (Safe Chain + Mozilla + Node roots) but NOT user cert
|
|
||||||
const certCount = (contents.match(/-----BEGIN CERTIFICATE-----/g) || []).length;
|
|
||||||
assert.strictEqual(certCount, baselineCertCount, `Traversal path ${badPath} should be rejected; base bundle only (no user cert added)`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -2,11 +2,12 @@ import * as http from "http";
|
||||||
import { tunnelRequest } from "./tunnelRequestHandler.js";
|
import { tunnelRequest } from "./tunnelRequestHandler.js";
|
||||||
import { mitmConnect } from "./mitmRequestHandler.js";
|
import { mitmConnect } from "./mitmRequestHandler.js";
|
||||||
import { handleHttpProxyRequest } from "./plainHttpProxy.js";
|
import { handleHttpProxyRequest } from "./plainHttpProxy.js";
|
||||||
import { getCombinedCaBundlePath } from "./certBundle.js";
|
|
||||||
import { ui } from "../../environment/userInteraction.js";
|
import { ui } from "../../environment/userInteraction.js";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { createInterceptorForUrl } from "./interceptors/createInterceptorForEcoSystem.js";
|
import { createInterceptorForUrl } from "./interceptors/createInterceptorForEcoSystem.js";
|
||||||
import { getHasSuppressedVersions } from "./interceptors/npm/modifyNpmInfo.js";
|
import { getHasSuppressedVersions } from "./interceptors/npm/modifyNpmInfo.js";
|
||||||
|
import { getCaCertPath } from "./certUtils.js";
|
||||||
|
import { readFileSync } from "fs";
|
||||||
|
|
||||||
/** *
|
/** *
|
||||||
* @returns {import("../registryProxy.js").SafeChainProxy} */
|
* @returns {import("../registryProxy.js").SafeChainProxy} */
|
||||||
|
|
@ -36,7 +37,7 @@ export function createBuiltInProxyServer() {
|
||||||
verifyNoMaliciousPackages,
|
verifyNoMaliciousPackages,
|
||||||
hasSuppressedVersions: getHasSuppressedVersions,
|
hasSuppressedVersions: getHasSuppressedVersions,
|
||||||
getServerPort: () => state.port,
|
getServerPort: () => state.port,
|
||||||
getCombinedCaBundlePath,
|
getCaCert,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -147,4 +148,13 @@ export function createBuiltInProxyServer() {
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCaCert() {
|
||||||
|
try {
|
||||||
|
const safeChainPath = getCaCertPath();
|
||||||
|
return readFileSync(safeChainPath, "utf8");
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,7 @@ import path from "node:path";
|
||||||
import certifi from "certifi";
|
import certifi from "certifi";
|
||||||
import tls from "node:tls";
|
import tls from "node:tls";
|
||||||
import { X509Certificate } from "node:crypto";
|
import { X509Certificate } from "node:crypto";
|
||||||
import { getCaCertPath } from "./certUtils.js";
|
import { ui } from "../environment/userInteraction.js";
|
||||||
import { ui } from "../../environment/userInteraction.js";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a PEM string contains only parsable cert blocks.
|
* Check if a PEM string contains only parsable cert blocks.
|
||||||
|
|
@ -50,20 +49,14 @@ function isParsable(pem) {
|
||||||
* - Mozilla roots via certifi (for public HTTPS)
|
* - Mozilla roots via certifi (for public HTTPS)
|
||||||
* - Node's built-in root certificates (fallback)
|
* - Node's built-in root certificates (fallback)
|
||||||
* - User's custom certificates (if NODE_EXTRA_CA_CERTS environment variable is set)
|
* - User's custom certificates (if NODE_EXTRA_CA_CERTS environment variable is set)
|
||||||
|
* @param {string | null} proxyCaCert
|
||||||
*
|
*
|
||||||
* @returns {string} Path to the combined CA bundle PEM file
|
* @returns {string} Path to the combined CA bundle PEM file
|
||||||
*/
|
*/
|
||||||
export function getCombinedCaBundlePath() {
|
export function getCombinedCaBundlePath(proxyCaCert) {
|
||||||
const parts = [];
|
|
||||||
|
|
||||||
// 1) Safe Chain CA (for MITM'd registries)
|
// 1) Safe Chain CA (for MITM'd registries)
|
||||||
const safeChainPath = getCaCertPath();
|
const parts = [];
|
||||||
try {
|
if (proxyCaCert && isParsable(proxyCaCert)) parts.push(proxyCaCert.trim());
|
||||||
const safeChainPem = fs.readFileSync(safeChainPath, "utf8");
|
|
||||||
if (isParsable(safeChainPem)) parts.push(safeChainPem.trim());
|
|
||||||
} catch {
|
|
||||||
// Ignore if Safe Chain CA is not available
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) certifi (Mozilla CA bundle for all public HTTPS)
|
// 2) certifi (Mozilla CA bundle for all public HTTPS)
|
||||||
try {
|
try {
|
||||||
180
packages/safe-chain/src/registryProxy/certBundle.spec.js
Normal file
180
packages/safe-chain/src/registryProxy/certBundle.spec.js
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
import { describe, it, beforeEach, mock } from "node:test";
|
||||||
|
import assert from "node:assert";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import tls from "node:tls";
|
||||||
|
|
||||||
|
// Utility to remove the generated bundle so the module rebuilds it on demand
|
||||||
|
function removeBundleIfExists() {
|
||||||
|
const target = path.join(os.tmpdir(), "safe-chain-ca-bundle.pem");
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(target)) fs.unlinkSync(target);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility to get a valid PEM certificate for testing
|
||||||
|
function getValidCert() {
|
||||||
|
const cert =
|
||||||
|
typeof tls.rootCertificates?.[0] === "string"
|
||||||
|
? tls.rootCertificates[0]
|
||||||
|
: "";
|
||||||
|
assert.ok(
|
||||||
|
cert.includes("BEGIN CERTIFICATE"),
|
||||||
|
"Environment lacks Node root certificates for test",
|
||||||
|
);
|
||||||
|
return cert;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("certBundle.getCombinedCaBundlePath", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mock.restoreAll();
|
||||||
|
removeBundleIfExists();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes Safe Chain CA when parsable and produces a PEM bundle", async () => {
|
||||||
|
// Prepare a temporary Safe Chain CA file with a recognizable marker and a valid cert block
|
||||||
|
const marker = "# SAFE_CHAIN_TEST_MARKER";
|
||||||
|
const rootPem =
|
||||||
|
typeof tls.rootCertificates?.[0] === "string"
|
||||||
|
? tls.rootCertificates[0]
|
||||||
|
: "";
|
||||||
|
assert.ok(
|
||||||
|
rootPem.includes("BEGIN CERTIFICATE"),
|
||||||
|
"Environment lacks Node root certificates for test",
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getCombinedCaBundlePath } = await import("./certBundle.js");
|
||||||
|
const bundlePath = getCombinedCaBundlePath(`${marker}\n${rootPem}`);
|
||||||
|
assert.ok(fs.existsSync(bundlePath), "Bundle path should exist");
|
||||||
|
const contents = fs.readFileSync(bundlePath, "utf8");
|
||||||
|
assert.match(contents, /-----BEGIN CERTIFICATE-----/);
|
||||||
|
assert.ok(
|
||||||
|
contents.includes(marker),
|
||||||
|
"Bundle should include Safe Chain CA content when parsable",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores invalid Safe Chain CA but still builds from other sources", async () => {
|
||||||
|
// Write an invalid file (no cert blocks)
|
||||||
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pipcabundle-"));
|
||||||
|
const safeChainPath = path.join(tmpDir, "safechain-invalid.pem");
|
||||||
|
const invalidMarker = "INVALID_SAFE_CHAIN_CONTENT";
|
||||||
|
fs.writeFileSync(safeChainPath, invalidMarker, "utf8");
|
||||||
|
|
||||||
|
// Ensure fresh build
|
||||||
|
removeBundleIfExists();
|
||||||
|
const { getCombinedCaBundlePath } = await import("./certBundle.js");
|
||||||
|
const bundlePath = getCombinedCaBundlePath(invalidMarker);
|
||||||
|
assert.ok(fs.existsSync(bundlePath), "Bundle path should exist");
|
||||||
|
const contents = fs.readFileSync(bundlePath, "utf8");
|
||||||
|
assert.match(
|
||||||
|
contents,
|
||||||
|
/-----BEGIN CERTIFICATE-----/,
|
||||||
|
"Bundle should contain certificate blocks from certifi/Node roots",
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
!contents.includes(invalidMarker),
|
||||||
|
"Bundle should not include invalid Safe Chain content",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("certBundle.getCombinedCaBundlePath with user certs", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mock.restoreAll();
|
||||||
|
delete process.env.NODE_EXTRA_CA_CERTS;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a path with full CA bundle (Safe Chain + Mozilla + Node roots) when no user cert in env", async () => {
|
||||||
|
const { getCombinedCaBundlePath } = await import("./certBundle.js");
|
||||||
|
const bundlePath = getCombinedCaBundlePath(getValidCert());
|
||||||
|
|
||||||
|
assert.ok(fs.existsSync(bundlePath), "Bundle path should exist");
|
||||||
|
const contents = fs.readFileSync(bundlePath, "utf8");
|
||||||
|
assert.match(
|
||||||
|
contents,
|
||||||
|
/-----BEGIN CERTIFICATE-----/,
|
||||||
|
"Should contain certificate blocks",
|
||||||
|
);
|
||||||
|
// Should include base bundle (Safe Chain + Mozilla/Node roots)
|
||||||
|
assert.ok(
|
||||||
|
contents.length > 1000,
|
||||||
|
"Bundle should be substantial with Mozilla/Node roots included",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("merges user cert with full base bundle (Safe Chain CA + Mozilla + Node roots)", async () => {
|
||||||
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-"));
|
||||||
|
|
||||||
|
// Create Safe Chain CA
|
||||||
|
const safeChainCert = getValidCert();
|
||||||
|
|
||||||
|
// Create user cert file
|
||||||
|
const userCertPath = path.join(tmpDir, "user-cert.pem");
|
||||||
|
const userCert = getValidCert();
|
||||||
|
fs.writeFileSync(userCertPath, userCert, "utf8");
|
||||||
|
process.env.NODE_EXTRA_CA_CERTS = userCertPath;
|
||||||
|
|
||||||
|
const { getCombinedCaBundlePath } = await import("./certBundle.js");
|
||||||
|
const bundlePath = getCombinedCaBundlePath(safeChainCert);
|
||||||
|
|
||||||
|
assert.ok(fs.existsSync(bundlePath), "Bundle path should exist");
|
||||||
|
const contents = fs.readFileSync(bundlePath, "utf8");
|
||||||
|
|
||||||
|
// Both certs should be in the bundle
|
||||||
|
const certCount = (contents.match(/-----BEGIN CERTIFICATE-----/g) || [])
|
||||||
|
.length;
|
||||||
|
assert.ok(
|
||||||
|
certCount >= 2,
|
||||||
|
"Bundle should contain both Safe Chain and user certificates",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores invalid PEM user cert", async () => {
|
||||||
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-"));
|
||||||
|
|
||||||
|
const userCertPath = path.join(tmpDir, "invalid.pem");
|
||||||
|
fs.writeFileSync(userCertPath, "NOT A VALID PEM", "utf8");
|
||||||
|
process.env.NODE_EXTRA_CA_CERTS = userCertPath;
|
||||||
|
|
||||||
|
const { getCombinedCaBundlePath } = await import("./certBundle.js");
|
||||||
|
const bundlePath = getCombinedCaBundlePath(getValidCert());
|
||||||
|
|
||||||
|
assert.ok(fs.existsSync(bundlePath), "Bundle path should exist");
|
||||||
|
const contents = fs.readFileSync(bundlePath, "utf8");
|
||||||
|
// Should still have Safe Chain CA only
|
||||||
|
assert.match(
|
||||||
|
contents,
|
||||||
|
/-----BEGIN CERTIFICATE-----/,
|
||||||
|
"Should contain Safe Chain CA",
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
!contents.includes("NOT A VALID"),
|
||||||
|
"Should not include invalid cert",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts files with CRLF line endings (Windows-style)", async () => {
|
||||||
|
// Create a real file with CRLF content to test Windows line ending support
|
||||||
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-"));
|
||||||
|
const userCertPath = path.join(tmpDir, "user-cert-crlf.pem");
|
||||||
|
const userCert = getValidCert();
|
||||||
|
const certWithCRLF = userCert.replace(/\n/g, "\r\n");
|
||||||
|
fs.writeFileSync(userCertPath, certWithCRLF, "utf8");
|
||||||
|
process.env.NODE_EXTRA_CA_CERTS = userCertPath;
|
||||||
|
|
||||||
|
const { getCombinedCaBundlePath } = await import("./certBundle.js");
|
||||||
|
const bundlePath = getCombinedCaBundlePath(getValidCert());
|
||||||
|
assert.ok(fs.existsSync(bundlePath), "Bundle path should exist");
|
||||||
|
const contents = fs.readFileSync(bundlePath, "utf8");
|
||||||
|
const certCount = (contents.match(/-----BEGIN CERTIFICATE-----/g) || [])
|
||||||
|
.length;
|
||||||
|
assert.ok(
|
||||||
|
certCount >= 2,
|
||||||
|
"Bundle should contain Safe Chain and user certificates with CRLF",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import { mkdtempSync, readFile, writeFile } from "node:fs";
|
import { mkdtempSync, readFile } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
|
|
@ -8,14 +8,13 @@ import { ui } from "../../environment/userInteraction.js";
|
||||||
import { getLoggingLevel, LOGGING_VERBOSE } from "../../config/settings.js";
|
import { getLoggingLevel, LOGGING_VERBOSE } from "../../config/settings.js";
|
||||||
|
|
||||||
const readFilePromise = promisify(readFile);
|
const readFilePromise = promisify(readFile);
|
||||||
const writeFilePromise = promisify(writeFile);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} RamaProxyInstance
|
* @typedef {Object} RamaProxyInstance
|
||||||
* @property {import("node:child_process").ChildProcess} process
|
* @property {import("node:child_process").ChildProcess} process
|
||||||
* @property {string} proxyAddress
|
* @property {string} proxyAddress
|
||||||
* @property {string} metaAddress
|
* @property {string} metaAddress
|
||||||
* @property {string} certPath
|
* @property {string} caCert
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -61,7 +60,7 @@ export function createRamaProxy(ramaPath) {
|
||||||
const url = new URL(`http://${ramaInstance.proxyAddress}`);
|
const url = new URL(`http://${ramaInstance.proxyAddress}`);
|
||||||
return url.port ? parseInt(url.port, 10) : null;
|
return url.port ? parseInt(url.port, 10) : null;
|
||||||
},
|
},
|
||||||
getCombinedCaBundlePath: () => ramaInstance?.certPath ?? "",
|
getCaCert: () => ramaInstance?.caCert ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,14 +101,12 @@ async function startRama(ramaPath, dataFolder) {
|
||||||
);
|
);
|
||||||
|
|
||||||
const certResponse = await fetch(`http://${metaAddress}/ca`);
|
const certResponse = await fetch(`http://${metaAddress}/ca`);
|
||||||
const cert = await certResponse.text();
|
const caCert = await certResponse.text();
|
||||||
const certPath = join(dataFolder, "cert.ca");
|
|
||||||
await writeFilePromise(certPath, cert);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
process,
|
process,
|
||||||
proxyAddress,
|
proxyAddress,
|
||||||
metaAddress,
|
metaAddress,
|
||||||
certPath,
|
caCert,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { ui } from "../environment/userInteraction.js";
|
import { ui } from "../environment/userInteraction.js";
|
||||||
import { createRamaProxy, getRamaPath } from "./ramaProxy/createRamaProxy.js";
|
import { createRamaProxy, getRamaPath } from "./ramaProxy/createRamaProxy.js";
|
||||||
import { createBuiltInProxyServer } from "./builtInProxy/createBuiltInProxyServer.js";
|
import { createBuiltInProxyServer } from "./builtInProxy/createBuiltInProxyServer.js";
|
||||||
|
import { getCombinedCaBundlePath } from "./certBundle.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} SafeChainProxy
|
* @typedef {Object} SafeChainProxy
|
||||||
|
|
@ -9,7 +10,11 @@ import { createBuiltInProxyServer } from "./builtInProxy/createBuiltInProxyServe
|
||||||
* @prop {() => boolean} verifyNoMaliciousPackages
|
* @prop {() => boolean} verifyNoMaliciousPackages
|
||||||
* @prop {() => boolean} hasSuppressedVersions
|
* @prop {() => boolean} hasSuppressedVersions
|
||||||
* @prop {() => Number | null} getServerPort
|
* @prop {() => Number | null} getServerPort
|
||||||
* @prop {() => string} getCombinedCaBundlePath
|
* @prop {() => string | null} getCaCert
|
||||||
|
*
|
||||||
|
* @typedef {Object} ProxySettings
|
||||||
|
* @prop {string | null} proxyUrl
|
||||||
|
* @prop {string} caCertBundlePath
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** @type {SafeChainProxy} */
|
/** @type {SafeChainProxy} */
|
||||||
|
|
@ -31,6 +36,27 @@ export function createSafeChainProxy() {
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {ProxySettings}
|
||||||
|
*/
|
||||||
|
export function getProxySettings() {
|
||||||
|
if (!server || !server.getServerPort()) {
|
||||||
|
return {
|
||||||
|
proxyUrl: null,
|
||||||
|
caCertBundlePath: getCombinedCaBundlePath(null),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxyUrl = `http://localhost:${server.getServerPort()}`;
|
||||||
|
const caCert = server.getCaCert();
|
||||||
|
const caCertBundlePath = getCombinedCaBundlePath(caCert);
|
||||||
|
|
||||||
|
return {
|
||||||
|
proxyUrl,
|
||||||
|
caCertBundlePath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {Record<string, string>}
|
* @returns {Record<string, string>}
|
||||||
*/
|
*/
|
||||||
|
|
@ -39,13 +65,12 @@ function getSafeChainProxyEnvironmentVariables() {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const proxyUrl = `http://localhost:${server.getServerPort()}`;
|
const proxySettings = getProxySettings();
|
||||||
const caCertPath = server.getCombinedCaBundlePath();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
HTTPS_PROXY: proxyUrl,
|
HTTPS_PROXY: proxySettings.proxyUrl ?? "",
|
||||||
GLOBAL_AGENT_HTTP_PROXY: proxyUrl,
|
GLOBAL_AGENT_HTTP_PROXY: proxySettings.proxyUrl ?? "",
|
||||||
NODE_EXTRA_CA_CERTS: caCertPath,
|
NODE_EXTRA_CA_CERTS: proxySettings.caCertBundlePath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue