From 1a811edc95002c3fe10873a3600301e4b9a589a9 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 1 Apr 2026 14:57:24 -0700 Subject: [PATCH] More cleanup --- .../src/registryProxy/http-utils.js | 2 + .../interceptors/pip/modifyPipInfo.js | 74 +------- .../interceptors/pip/modifyPipJsonResponse.js | 168 ++++++++++++++++++ .../interceptors/suppressedVersionsState.js | 4 + .../src/registryProxy/mitmRequestHandler.js | 31 ++-- 5 files changed, 201 insertions(+), 78 deletions(-) create mode 100644 packages/safe-chain/src/registryProxy/interceptors/pip/modifyPipJsonResponse.js diff --git a/packages/safe-chain/src/registryProxy/http-utils.js b/packages/safe-chain/src/registryProxy/http-utils.js index f44e1d6..967aec8 100644 --- a/packages/safe-chain/src/registryProxy/http-utils.js +++ b/packages/safe-chain/src/registryProxy/http-utils.js @@ -18,6 +18,8 @@ export function getHeaderValueAsString(headers, headerName) { /** * Remove headers that become stale when the response body is modified. + * Mutates the provided headers object in place. + * * @param {NodeJS.Dict | undefined} headers * @returns {void} */ diff --git a/packages/safe-chain/src/registryProxy/interceptors/pip/modifyPipInfo.js b/packages/safe-chain/src/registryProxy/interceptors/pip/modifyPipInfo.js index de4cae8..d3d10fe 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/pip/modifyPipInfo.js +++ b/packages/safe-chain/src/registryProxy/interceptors/pip/modifyPipInfo.js @@ -3,15 +3,8 @@ import { clearCachingHeaders } from "../../http-utils.js"; import { normalizePipPackageName } from "../../../scanning/packageNameVariants.js"; import { parsePipPackageFromUrl } from "./parsePipPackageUrl.js"; export { parsePipMetadataUrl, isPipPackageInfoUrl } from "./parsePipPackageUrl.js"; -import { - calculateLatestVersion, - getAvailableVersionsFromJson, - getPackageVersionFromMetadataFile, -} from "./pipMetadataVersionUtils.js"; -import { - getPipMetadataContentType, - logSuppressedVersion, -} from "./pipMetadataResponseUtils.js"; +import { getPipMetadataContentType, logSuppressedVersion } from "./pipMetadataResponseUtils.js"; +import { modifyPipJsonResponse } from "./modifyPipJsonResponse.js"; /** * @param {Buffer} body @@ -134,63 +127,12 @@ function modifyJsonResponse( packageName ) { const json = JSON.parse(body.toString("utf8")); - let modified = false; - - if (Array.isArray(json.files)) { - const filteredFiles = json.files.filter((/** @type {any} */ file) => { - const version = getPackageVersionFromMetadataFile(file, metadataUrl); - - if (version && isNewlyReleasedPackage(packageName, version)) { - modified = true; - logSuppressedVersion(packageName, version); - return false; - } - - return true; - }); - - json.files = filteredFiles; - } - - if (json.releases && typeof json.releases === "object") { - for (const [version, files] of Object.entries(json.releases)) { - if ( - Array.isArray(/** @type {unknown[]} */ (files)) && - isNewlyReleasedPackage(packageName, version) - ) { - delete json.releases[version]; - modified = true; - logSuppressedVersion(packageName, version); - } - } - } - - if (Array.isArray(json.urls)) { - json.urls = json.urls.filter((/** @type {any} */ file) => { - const version = getPackageVersionFromMetadataFile(file, metadataUrl); - - if (version && isNewlyReleasedPackage(packageName, version)) { - modified = true; - logSuppressedVersion(packageName, version); - return false; - } - return true; - }); - } - - if (json.info && typeof json.info === "object") { - const candidateVersions = getAvailableVersionsFromJson(json, metadataUrl); - const replacementVersion = calculateLatestVersion(candidateVersions); - - if ( - typeof json.info.version === "string" && - replacementVersion && - json.info.version !== replacementVersion - ) { - json.info.version = replacementVersion; - modified = true; - } - } + const modified = modifyPipJsonResponse( + json, + metadataUrl, + isNewlyReleasedPackage, + packageName + ); if (!modified) return body; const modifiedBuffer = Buffer.from(JSON.stringify(json)); diff --git a/packages/safe-chain/src/registryProxy/interceptors/pip/modifyPipJsonResponse.js b/packages/safe-chain/src/registryProxy/interceptors/pip/modifyPipJsonResponse.js new file mode 100644 index 0000000..869a516 --- /dev/null +++ b/packages/safe-chain/src/registryProxy/interceptors/pip/modifyPipJsonResponse.js @@ -0,0 +1,168 @@ +import { + calculateLatestVersion, + getAvailableVersionsFromJson, + getPackageVersionFromMetadataFile, +} from "./pipMetadataVersionUtils.js"; +import { logSuppressedVersion } from "./pipMetadataResponseUtils.js"; + +/** + * @param {any} json + * @param {string} metadataUrl + * @param {(packageName: string | undefined, version: string | undefined) => boolean} isNewlyReleasedPackage + * @param {string} packageName + * @returns {boolean} + */ +export function modifyPipJsonResponse( + json, + metadataUrl, + isNewlyReleasedPackage, + packageName +) { + const filesModified = filterJsonMetadataFiles( + json, + metadataUrl, + isNewlyReleasedPackage, + packageName + ); + const releasesModified = removeJsonMetadataReleases( + json, + isNewlyReleasedPackage, + packageName + ); + const urlsModified = filterJsonMetadataUrls( + json, + metadataUrl, + isNewlyReleasedPackage, + packageName + ); + const versionModified = updateJsonInfoVersion(json, metadataUrl); + + return filesModified || releasesModified || urlsModified || versionModified; +} + +/** + * @param {any} json + * @param {string} metadataUrl + * @param {(packageName: string | undefined, version: string | undefined) => boolean} isNewlyReleasedPackage + * @param {string} packageName + * @returns {boolean} + */ +function filterJsonMetadataFiles( + json, + metadataUrl, + isNewlyReleasedPackage, + packageName +) { + if (!Array.isArray(json.files)) { + return false; + } + + let modified = false; + json.files = json.files.filter((/** @type {any} */ file) => { + const version = getPackageVersionFromMetadataFile(file, metadataUrl); + + if (version && isNewlyReleasedPackage(packageName, version)) { + modified = true; + logSuppressedVersion(packageName, version); + return false; + } + + return true; + }); + + return modified; +} + +/** + * @param {any} json + * @param {(packageName: string | undefined, version: string | undefined) => boolean} isNewlyReleasedPackage + * @param {string} packageName + * @returns {boolean} + */ +function removeJsonMetadataReleases(json, isNewlyReleasedPackage, packageName) { + if (!json.releases || typeof json.releases !== "object") { + return false; + } + + let modified = false; + + for (const [version, files] of Object.entries(json.releases)) { + if ( + Array.isArray(/** @type {unknown[]} */ (files)) && + isNewlyReleasedPackage(packageName, version) + ) { + delete json.releases[version]; + modified = true; + logSuppressedVersion(packageName, version); + } + } + + return modified; +} + +/** + * @param {any} json + * @param {string} metadataUrl + * @param {(packageName: string | undefined, version: string | undefined) => boolean} isNewlyReleasedPackage + * @param {string} packageName + * @returns {boolean} + */ +function filterJsonMetadataUrls( + json, + metadataUrl, + isNewlyReleasedPackage, + packageName +) { + if (!Array.isArray(json.urls)) { + return false; + } + + let modified = false; + json.urls = json.urls.filter((/** @type {any} */ file) => { + const version = getPackageVersionFromMetadataFile(file, metadataUrl); + + if (version && isNewlyReleasedPackage(packageName, version)) { + modified = true; + logSuppressedVersion(packageName, version); + return false; + } + + return true; + }); + + return modified; +} + +/** + * @param {any} json + * @param {string} metadataUrl + * @returns {boolean} + */ +function updateJsonInfoVersion(json, metadataUrl) { + if (!json.info || typeof json.info !== "object") { + return false; + } + + const replacementVersion = computeReplacementVersion(json, metadataUrl); + + if ( + typeof json.info.version !== "string" || + !replacementVersion || + json.info.version === replacementVersion + ) { + return false; + } + + json.info.version = replacementVersion; + return true; +} + +/** + * @param {any} json + * @param {string} metadataUrl + * @returns {string | undefined} + */ +function computeReplacementVersion(json, metadataUrl) { + const candidateVersions = getAvailableVersionsFromJson(json, metadataUrl); + return calculateLatestVersion(candidateVersions); +} diff --git a/packages/safe-chain/src/registryProxy/interceptors/suppressedVersionsState.js b/packages/safe-chain/src/registryProxy/interceptors/suppressedVersionsState.js index a3b1055..26c0559 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/suppressedVersionsState.js +++ b/packages/safe-chain/src/registryProxy/interceptors/suppressedVersionsState.js @@ -3,6 +3,10 @@ const state = { }; /** + * Tracks whether any rewritten metadata response suppressed versions during the + * current process lifetime. This is intentional shared state used only for the + * end-of-run summary message exposed through the proxy API. + * * @returns {void} */ export function recordSuppressedVersion() { diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index 7220370..1b76c81 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -2,7 +2,7 @@ import https from "https"; import { generateCertForHost } from "./certUtils.js"; import { HttpsProxyAgent } from "https-proxy-agent"; import { ui } from "../environment/userInteraction.js"; -import { gunzipSync, gzipSync } from "zlib"; +import { gunzipSync } from "zlib"; /** * @typedef {import("./interceptors/interceptorBuilder.js").Interceptor} Interceptor @@ -107,6 +107,23 @@ function getRequestPathAndQuery(url) { return url; } +/** + * @param {NodeJS.Dict} headers + * @returns {void} + */ +function normalizeRewrittenResponseHeaders(headers) { + for (const headerName of Object.keys(headers)) { + const lowerHeaderName = headerName.toLowerCase(); + if ( + lowerHeaderName === "content-length" || + lowerHeaderName === "transfer-encoding" || + lowerHeaderName === "content-encoding" + ) { + delete headers[headerName]; + } + } +} + /** * @param {import("http").IncomingMessage} req * @param {string} hostname @@ -218,17 +235,7 @@ function createProxyRequest(hostname, port, req, res, requestHandler) { // For rewritten responses, send the final body uncompressed. // This avoids mismatches between upstream compression metadata and the // rewritten payload on the wire. - for (const headerName of Object.keys(headers)) { - const lowerHeaderName = headerName.toLowerCase(); - if ( - lowerHeaderName === "content-length" || - lowerHeaderName === "transfer-encoding" || - lowerHeaderName === "content-encoding" - ) { - delete headers[headerName]; - } - } - + normalizeRewrittenResponseHeaders(headers); headers["content-length"] = String(buffer.byteLength); res.writeHead(statusCode, headers); res.end(buffer);