More cleanup

This commit is contained in:
Reinier Criel 2026-04-01 14:57:24 -07:00
parent e29c11546c
commit 1a811edc95
5 changed files with 201 additions and 78 deletions

View file

@ -18,6 +18,8 @@ export function getHeaderValueAsString(headers, headerName) {
/** /**
* Remove headers that become stale when the response body is modified. * Remove headers that become stale when the response body is modified.
* Mutates the provided headers object in place.
*
* @param {NodeJS.Dict<string | string[]> | undefined} headers * @param {NodeJS.Dict<string | string[]> | undefined} headers
* @returns {void} * @returns {void}
*/ */

View file

@ -3,15 +3,8 @@ import { clearCachingHeaders } from "../../http-utils.js";
import { normalizePipPackageName } from "../../../scanning/packageNameVariants.js"; import { normalizePipPackageName } from "../../../scanning/packageNameVariants.js";
import { parsePipPackageFromUrl } from "./parsePipPackageUrl.js"; import { parsePipPackageFromUrl } from "./parsePipPackageUrl.js";
export { parsePipMetadataUrl, isPipPackageInfoUrl } from "./parsePipPackageUrl.js"; export { parsePipMetadataUrl, isPipPackageInfoUrl } from "./parsePipPackageUrl.js";
import { import { getPipMetadataContentType, logSuppressedVersion } from "./pipMetadataResponseUtils.js";
calculateLatestVersion, import { modifyPipJsonResponse } from "./modifyPipJsonResponse.js";
getAvailableVersionsFromJson,
getPackageVersionFromMetadataFile,
} from "./pipMetadataVersionUtils.js";
import {
getPipMetadataContentType,
logSuppressedVersion,
} from "./pipMetadataResponseUtils.js";
/** /**
* @param {Buffer} body * @param {Buffer} body
@ -134,63 +127,12 @@ function modifyJsonResponse(
packageName packageName
) { ) {
const json = JSON.parse(body.toString("utf8")); const json = JSON.parse(body.toString("utf8"));
let modified = false; const modified = modifyPipJsonResponse(
json,
if (Array.isArray(json.files)) { metadataUrl,
const filteredFiles = json.files.filter((/** @type {any} */ file) => { isNewlyReleasedPackage,
const version = getPackageVersionFromMetadataFile(file, metadataUrl); packageName
);
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;
}
}
if (!modified) return body; if (!modified) return body;
const modifiedBuffer = Buffer.from(JSON.stringify(json)); const modifiedBuffer = Buffer.from(JSON.stringify(json));

View file

@ -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);
}

View file

@ -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} * @returns {void}
*/ */
export function recordSuppressedVersion() { export function recordSuppressedVersion() {

View file

@ -2,7 +2,7 @@ import https from "https";
import { generateCertForHost } from "./certUtils.js"; import { generateCertForHost } from "./certUtils.js";
import { HttpsProxyAgent } from "https-proxy-agent"; import { HttpsProxyAgent } from "https-proxy-agent";
import { ui } from "../environment/userInteraction.js"; import { ui } from "../environment/userInteraction.js";
import { gunzipSync, gzipSync } from "zlib"; import { gunzipSync } from "zlib";
/** /**
* @typedef {import("./interceptors/interceptorBuilder.js").Interceptor} Interceptor * @typedef {import("./interceptors/interceptorBuilder.js").Interceptor} Interceptor
@ -107,6 +107,23 @@ function getRequestPathAndQuery(url) {
return url; return url;
} }
/**
* @param {NodeJS.Dict<string | string[]>} 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 {import("http").IncomingMessage} req
* @param {string} hostname * @param {string} hostname
@ -218,17 +235,7 @@ function createProxyRequest(hostname, port, req, res, requestHandler) {
// For rewritten responses, send the final body uncompressed. // For rewritten responses, send the final body uncompressed.
// This avoids mismatches between upstream compression metadata and the // This avoids mismatches between upstream compression metadata and the
// rewritten payload on the wire. // rewritten payload on the wire.
for (const headerName of Object.keys(headers)) { normalizeRewrittenResponseHeaders(headers);
const lowerHeaderName = headerName.toLowerCase();
if (
lowerHeaderName === "content-length" ||
lowerHeaderName === "transfer-encoding" ||
lowerHeaderName === "content-encoding"
) {
delete headers[headerName];
}
}
headers["content-length"] = String(buffer.byteLength); headers["content-length"] = String(buffer.byteLength);
res.writeHead(statusCode, headers); res.writeHead(statusCode, headers);
res.end(buffer); res.end(buffer);