Merge remote-tracking branch 'origin/main' into feature/pypi

This commit is contained in:
Reinier Criel 2025-11-03 06:49:53 -08:00
commit 548d416996
64 changed files with 1689 additions and 381 deletions

View file

@ -1,6 +1,7 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
// @ts-ignore - certifi has no type definitions
import certifi from "certifi";
import tls from "node:tls";
import { X509Certificate } from "node:crypto";
@ -8,6 +9,8 @@ import { getCaCertPath } from "./certUtils.js";
/**
* Check if a PEM string contains only parsable cert blocks.
* @param {string} pem - PEM-encoded certificate string
* @returns {boolean}
*/
function isParsable(pem) {
if (!pem || typeof pem !== "string") return false;
@ -38,6 +41,7 @@ function isParsable(pem) {
}
}
/** @type {string | null} */
let cachedPath = null;
/**

View file

@ -12,6 +12,10 @@ export function getCaCertPath() {
return path.join(certFolder, "ca-cert.pem");
}
/**
* @param {string} hostname
* @returns {{privateKey: string, certificate: string}}
*/
export function generateCertForHost(hostname) {
let existingCert = certCache.get(hostname);
if (existingCert) {

View file

@ -3,6 +3,11 @@ import { generateCertForHost } from "./certUtils.js";
import { HttpsProxyAgent } from "https-proxy-agent";
import { ui } from "../environment/userInteraction.js";
/**
* @param {import("http").IncomingMessage} req
* @param {import("net").Socket} clientSocket
* @param {(target: string) => Promise<boolean>} isAllowed
*/
export function mitmConnect(req, clientSocket, isAllowed) {
const { hostname } = new URL(`http://${req.url}`);
@ -16,6 +21,7 @@ export function mitmConnect(req, clientSocket, isAllowed) {
server.on("error", (err) => {
ui.writeError(`Safe-chain: HTTPS server error: ${err.message}`);
// @ts-expect-error Property 'headersSent' does not exist on type 'Socket'
if (!clientSocket.headersSent) {
clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
} else if (clientSocket.writable) {
@ -30,10 +36,22 @@ export function mitmConnect(req, clientSocket, isAllowed) {
server.emit("connection", clientSocket);
}
/**
* @param {string} hostname
* @param {(target: string) => Promise<boolean>} isAllowed
* @returns {import("https").Server}
*/
function createHttpsServer(hostname, isAllowed) {
const cert = generateCertForHost(hostname);
/**
* @param {import("http").IncomingMessage} req
* @param {import("http").ServerResponse} res
*
* @returns {Promise<void>}
*/
async function handleRequest(req, res) {
// @ts-expect-error req.url might be undefined
const pathAndQuery = getRequestPathAndQuery(req.url);
const targetUrl = `https://${hostname}${pathAndQuery}`;
@ -58,6 +76,10 @@ function createHttpsServer(hostname, isAllowed) {
return server;
}
/**
* @param {string} url
* @returns {string}
*/
function getRequestPathAndQuery(url) {
if (url.startsWith("http://") || url.startsWith("https://")) {
const parsedUrl = new URL(url);
@ -66,6 +88,11 @@ function getRequestPathAndQuery(url) {
return url;
}
/**
* @param {import("http").IncomingMessage} req
* @param {string} hostname
* @param {import("http").ServerResponse} res
*/
function forwardRequest(req, hostname, res) {
const proxyReq = createProxyRequest(hostname, req, res);
@ -88,7 +115,15 @@ function forwardRequest(req, hostname, res) {
});
}
/**
* @param {string} hostname
* @param {import("http").IncomingMessage} req
* @param {import("http").ServerResponse} res
*
* @returns {import("http").ClientRequest}
*/
function createProxyRequest(hostname, req, res) {
/** @type {import("http").RequestOptions} */
const options = {
hostname: hostname,
port: 443,
@ -97,7 +132,9 @@ function createProxyRequest(hostname, req, res) {
headers: { ...req.headers },
};
delete options.headers.host;
if (options.headers && "host" in options.headers) {
delete options.headers.host;
}
const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy;
if (httpsProxy) {
@ -115,6 +152,7 @@ function createProxyRequest(hostname, req, res) {
}
});
// @ts-expect-error statusCode might be undefined
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res);
});

View file

@ -3,6 +3,10 @@ import { getEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js"
export const knownJsRegistries = ["registry.npmjs.org","registry.yarnpkg.com"];
export const knownPipRegistries = ["files.pythonhosted.org", "pypi.org", "pypi.python.org", "pythonhosted.org"];
/**
* @param {string} url
* @returns {{packageName: string | undefined, version: string | undefined}}
*/
export function parsePackageFromUrl(url) {
const ecosystem = getEcoSystem();
let registry;

View file

@ -1,7 +1,14 @@
import * as http from "http";
import * as https from "https";
/**
* @param {import("http").IncomingMessage} req
* @param {import("http").ServerResponse} res
*
* @returns {void}
*/
export function handleHttpProxyRequest(req, res) {
// @ts-expect-error req.url might be undefined
const url = new URL(req.url);
// The protocol for the plainHttpProxy should usually only be http:
@ -20,9 +27,11 @@ export function handleHttpProxyRequest(req, res) {
const proxyRequest = protocol
.request(
// @ts-expect-error req.url might be undefined
req.url,
{ method: req.method, headers: req.headers },
(proxyRes) => {
// @ts-expect-error statusCode might be undefined
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res);

View file

@ -10,6 +10,9 @@ import { ui } from "../environment/userInteraction.js";
import chalk from "chalk";
const SERVER_STOP_TIMEOUT_MS = 1000;
/**
* @type {{port: number | null, blockedRequests: {packageName: string, version: string, url: string}[]}}
*/
const state = {
port: null,
blockedRequests: [],
@ -25,6 +28,9 @@ export function createSafeChainProxy() {
};
}
/**
* @returns {Record<string, string>}
*/
function getSafeChainProxyEnvironmentVariables() {
if (!state.port) {
return {};
@ -37,6 +43,11 @@ function getSafeChainProxyEnvironmentVariables() {
};
}
/**
* @param {Record<string, string>} env
*
* @returns {Record<string, string>}
*/
export function mergeSafeChainProxyEnvironmentVariables(env) {
const proxyEnv = getSafeChainProxyEnvironmentVariables();
@ -68,6 +79,11 @@ function createProxyServer() {
return server;
}
/**
* @param {import("http").Server} server
*
* @returns {Promise<void>}
*/
function startServer(server) {
return new Promise((resolve, reject) => {
// Passing port 0 makes the OS assign an available port
@ -87,6 +103,11 @@ function startServer(server) {
});
}
/**
* @param {import("http").Server} server
*
* @returns {Promise<void>}
*/
function stopServer(server) {
return new Promise((resolve) => {
try {
@ -100,6 +121,13 @@ function stopServer(server) {
});
}
/**
* @param {import("http").IncomingMessage} req
* @param {import("net").Socket} clientSocket
* @param {Buffer} head
*
* @returns {void}
*/
function handleConnect(req, clientSocket, head) {
// CONNECT method is used for HTTPS requests
// It establishes a tunnel to the server identified by the request URL
@ -122,6 +150,10 @@ function handleConnect(req, clientSocket, head) {
}
}
/**
* @param {string} url
* @returns {Promise<boolean>}
*/
async function isAllowedUrl(url) {
const { packageName, version } = parsePackageFromUrl(url);

View file

@ -1,6 +1,13 @@
import * as net from "net";
import { ui } from "../environment/userInteraction.js";
/**
* @param {import("http").IncomingMessage} req
* @param {import("net").Socket} clientSocket
* @param {Buffer} head
*
* @returns {void}
*/
export function tunnelRequest(req, clientSocket, head) {
const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy;
@ -21,9 +28,17 @@ export function tunnelRequest(req, clientSocket, head) {
}
}
/**
* @param {import("http").IncomingMessage} req
* @param {import("net").Socket} clientSocket
* @param {Buffer} head
*
* @returns {void}
*/
function tunnelRequestToDestination(req, clientSocket, head) {
const { port, hostname } = new URL(`http://${req.url}`);
// @ts-expect-error port from URL is a string but net.connect accepts number
const serverSocket = net.connect(port || 443, hostname, () => {
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
serverSocket.write(head);
@ -49,11 +64,18 @@ function tunnelRequestToDestination(req, clientSocket, head) {
});
}
/**
* @param {import("http").IncomingMessage} req
* @param {import("net").Socket} clientSocket
* @param {Buffer} head
* @param {string} proxyUrl
*/
function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) {
const { port, hostname } = new URL(`http://${req.url}`);
const proxy = new URL(proxyUrl);
// Connect to proxy server
// @ts-expect-error net.connect wants port as number but proxy.port is string
const proxySocket = net.connect({
host: proxy.hostname,
port: proxy.port,