mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
111 lines
4.2 KiB
JavaScript
111 lines
4.2 KiB
JavaScript
import { ui } from "../../environment/userInteraction.js";
|
|
import { safeSpawn } from "../../utils/safeSpawn.js";
|
|
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
|
import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js";
|
|
import fs from "node:fs/promises";
|
|
import fsSync from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import ini from "ini";
|
|
|
|
/**
|
|
* @param {string} command
|
|
* @param {string[]} args
|
|
*
|
|
* @returns {Promise<{status: number}>}
|
|
*/
|
|
export async function runPip(command, args) {
|
|
try {
|
|
const env = mergeSafeChainProxyEnvironmentVariables(process.env);
|
|
|
|
// Always provide Python with a complete CA bundle (Safe Chain CA + Mozilla + Node built-in roots)
|
|
// so that any network request made by pip, including those outside explicit CLI args,
|
|
// validates correctly under both MITM'd and tunneled HTTPS.
|
|
const combinedCaPath = getCombinedCaBundlePath();
|
|
|
|
// https://pip.pypa.io/en/stable/topics/https-certificates/ explains that the 'cert' param (which we're providing via INI file)
|
|
// will tell pip to use the provided CA bundle for HTTPS verification.
|
|
|
|
// Proxy settings: prefer GLOBAL_AGENT_HTTP_PROXY, then HTTPS_PROXY, then HTTP_PROXY
|
|
const proxy = env.GLOBAL_AGENT_HTTP_PROXY || env.HTTPS_PROXY || env.HTTP_PROXY || '';
|
|
|
|
const tmpDir = os.tmpdir();
|
|
const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`);
|
|
|
|
if (!env.PIP_CONFIG_FILE) { // Build pip config INI
|
|
/** @type {{ global: { cert: string, proxy?: string } }} */
|
|
const configObj = { global: { cert: combinedCaPath } };
|
|
if (proxy) {
|
|
configObj.global.proxy = proxy;
|
|
}
|
|
const pipConfig = ini.stringify(configObj);
|
|
await fs.writeFile(pipConfigPath, pipConfig);
|
|
env.PIP_CONFIG_FILE = pipConfigPath;
|
|
|
|
} else if (fsSync.existsSync(env.PIP_CONFIG_FILE)) { // Merge pip config INI
|
|
ui.writeVerbose("Safe-chain: Merging user provided PIP_CONFIG_FILE with safe-chain certificate and proxy settings.");
|
|
const userConfig = env.PIP_CONFIG_FILE;
|
|
|
|
// Read the existing config without modifying it
|
|
let content = await fs.readFile(userConfig, "utf-8");
|
|
const parsed = ini.parse(content);
|
|
|
|
// Ensure [global] section exists
|
|
parsed.global = parsed.global || {};
|
|
|
|
// Cert
|
|
if (typeof parsed.global.cert !== "undefined") {
|
|
ui.writeWarning("Safe-chain: User defined cert found in PIP_CONFIG_FILE. It will be overwritten in the temporary config.");
|
|
}
|
|
parsed.global.cert = combinedCaPath;
|
|
|
|
// Proxy
|
|
if (typeof parsed.global.proxy !== "undefined") {
|
|
ui.writeWarning("Safe-chain: User defined proxy found in PIP_CONFIG_FILE. It will be overwritten in the temporary config.");
|
|
}
|
|
if (proxy) {
|
|
parsed.global.proxy = proxy;
|
|
}
|
|
|
|
const updated = ini.stringify(parsed);
|
|
|
|
// Save to a new temp file to avoid overwriting user's original config
|
|
await fs.writeFile(pipConfigPath, updated, "utf-8");
|
|
env.PIP_CONFIG_FILE = pipConfigPath;
|
|
|
|
} else {
|
|
// The user provided PIP_CONFIG_FILE does not exist on disk
|
|
// PIP will handle this as an error and inform the user
|
|
}
|
|
|
|
// REQUESTS_CA_BUNDLE, SSL_CERT_FILE and PIP_CERT as extra safety nets.
|
|
if (env.REQUESTS_CA_BUNDLE) {
|
|
ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.");
|
|
}
|
|
env.REQUESTS_CA_BUNDLE = combinedCaPath;
|
|
|
|
if (env.SSL_CERT_FILE) {
|
|
ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.");
|
|
}
|
|
env.SSL_CERT_FILE = combinedCaPath;
|
|
|
|
if (env.PIP_CERT) {
|
|
ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.");
|
|
}
|
|
env.PIP_CERT = combinedCaPath;
|
|
|
|
const result = await safeSpawn(command, args, {
|
|
stdio: "inherit",
|
|
env,
|
|
});
|
|
return { status: result.status };
|
|
} catch (/** @type any */ error) {
|
|
if (error.status) {
|
|
return { status: error.status };
|
|
} else {
|
|
ui.writeError(`Error executing command: ${error.message}`);
|
|
ui.writeError(`Is '${command}' installed and available on your system?`);
|
|
return { status: 1 };
|
|
}
|
|
}
|
|
}
|