mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 20:20:49 +00:00
Cleanup formatting changes from PR
This commit is contained in:
parent
912f62f3b9
commit
e8a4fbcd76
19 changed files with 200 additions and 407 deletions
|
|
@ -71,20 +71,15 @@ export function modifyNpmInfoResponse(body, headers) {
|
|||
// Check if this package is excluded from minimum age filtering
|
||||
const packageName = bodyJson.name;
|
||||
const exclusions = getNpmMinimumPackageAgeExclusions();
|
||||
if (
|
||||
packageName &&
|
||||
exclusions.some((pattern) =>
|
||||
matchesExclusionPattern(packageName, pattern),
|
||||
)
|
||||
) {
|
||||
if (packageName && exclusions.some((pattern) => matchesExclusionPattern(packageName, pattern))) {
|
||||
ui.writeVerbose(
|
||||
`Safe-chain: ${packageName} is excluded from minimum package age filtering (minimumPackageAgeExclusions setting).`,
|
||||
`Safe-chain: ${packageName} is excluded from minimum package age filtering (minimumPackageAgeExclusions setting).`
|
||||
);
|
||||
return body;
|
||||
}
|
||||
|
||||
const cutOff = new Date(
|
||||
new Date().getTime() - getMinimumPackageAgeHours() * 3600 * 1000,
|
||||
new Date().getTime() - getMinimumPackageAgeHours() * 3600 * 1000
|
||||
);
|
||||
|
||||
const hasLatestTag = !!bodyJson["dist-tags"]["latest"];
|
||||
|
|
@ -121,7 +116,7 @@ export function modifyNpmInfoResponse(body, headers) {
|
|||
return Buffer.from(JSON.stringify(bodyJson));
|
||||
} catch (/** @type {any} */ err) {
|
||||
ui.writeVerbose(
|
||||
`Safe-chain: Package metadata not in expected format - bypassing modification. Error: ${err.message}`,
|
||||
`Safe-chain: Package metadata not in expected format - bypassing modification. Error: ${err.message}`
|
||||
);
|
||||
return body;
|
||||
}
|
||||
|
|
@ -137,7 +132,7 @@ function deleteVersionFromJson(json, version) {
|
|||
const packageName = typeof json?.name === "string" ? json.name : "(unknown)";
|
||||
|
||||
ui.writeVerbose(
|
||||
`Safe-chain: ${packageName}@${version} is newer than ${getMinimumPackageAgeHours()} hours and was removed (minimumPackageAgeInHours setting).`,
|
||||
`Safe-chain: ${packageName}@${version} is newer than ${getMinimumPackageAgeHours()} hours and was removed (minimumPackageAgeInHours setting).`
|
||||
);
|
||||
|
||||
delete json.time[version];
|
||||
|
|
@ -156,20 +151,18 @@ function deleteVersionFromJson(json, version) {
|
|||
*/
|
||||
function calculateLatestTag(tagList) {
|
||||
const entries = Object.entries(tagList).filter(
|
||||
([version, _]) => version !== "created" && version !== "modified",
|
||||
([version, _]) => version !== "created" && version !== "modified"
|
||||
);
|
||||
|
||||
const latestFullRelease = getMostRecentTag(
|
||||
Object.fromEntries(
|
||||
entries.filter(([version, _]) => !version.includes("-")),
|
||||
),
|
||||
Object.fromEntries(entries.filter(([version, _]) => !version.includes("-")))
|
||||
);
|
||||
if (latestFullRelease) {
|
||||
return latestFullRelease;
|
||||
}
|
||||
|
||||
const latestPrerelease = getMostRecentTag(
|
||||
Object.fromEntries(entries.filter(([version, _]) => version.includes("-"))),
|
||||
Object.fromEntries(entries.filter(([version, _]) => version.includes("-")))
|
||||
);
|
||||
return latestPrerelease;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const knownJsRegistries = [
|
|||
*/
|
||||
export function npmInterceptorForUrl(url) {
|
||||
const registry = [...knownJsRegistries, ...getNpmCustomRegistries()].find(
|
||||
(reg) => url.includes(reg),
|
||||
(reg) => url.includes(reg)
|
||||
);
|
||||
|
||||
if (registry) {
|
||||
|
|
@ -41,7 +41,7 @@ function buildNpmInterceptor(registry) {
|
|||
return interceptRequests(async (reqContext) => {
|
||||
const { packageName, version } = parseNpmPackageUrl(
|
||||
reqContext.targetUrl,
|
||||
registry,
|
||||
registry
|
||||
);
|
||||
|
||||
if (await isMalwarePackage(packageName, version)) {
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@ describe("npmInterceptor minimum package age", async () => {
|
|||
getMinimumPackageAgeHours: () => minimumPackageAgeSettings,
|
||||
skipMinimumPackageAge: () => skipMinimumPackageAgeSetting,
|
||||
getNpmCustomRegistries: () => [],
|
||||
getNpmMinimumPackageAgeExclusions: () =>
|
||||
minimumPackageAgeExclusionsSetting,
|
||||
getNpmMinimumPackageAgeExclusions: () => minimumPackageAgeExclusionsSetting,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -65,8 +64,9 @@ describe("npmInterceptor minimum package age", async () => {
|
|||
]) {
|
||||
it(`modifyResponse should be true for package info requests: ${packageInfoUrl}`, async () => {
|
||||
const interceptor = npmInterceptorForUrl(packageInfoUrl);
|
||||
const requestInterceptor =
|
||||
await interceptor.handleRequest(packageInfoUrl);
|
||||
const requestInterceptor = await interceptor.handleRequest(
|
||||
packageInfoUrl
|
||||
);
|
||||
|
||||
assert.equal(requestInterceptor.modifiesResponse(), true);
|
||||
});
|
||||
|
|
@ -120,8 +120,9 @@ describe("npmInterceptor minimum package age", async () => {
|
|||
]) {
|
||||
it(`modifyResponse should be false for special endpoints: ${specialEndpoint}`, async () => {
|
||||
const interceptor = npmInterceptorForUrl(specialEndpoint);
|
||||
const requestInterceptor =
|
||||
await interceptor.handleRequest(specialEndpoint);
|
||||
const requestInterceptor = await interceptor.handleRequest(
|
||||
specialEndpoint
|
||||
);
|
||||
|
||||
assert.equal(requestInterceptor.modifiesResponse(), false);
|
||||
});
|
||||
|
|
@ -151,7 +152,7 @@ describe("npmInterceptor minimum package age", async () => {
|
|||
["2.0.0"]: getDate(-4),
|
||||
["3.0.0"]: getDate(-3),
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
const modifiedJson = JSON.parse(modifiedBody);
|
||||
|
|
@ -192,7 +193,7 @@ describe("npmInterceptor minimum package age", async () => {
|
|||
["2.0.0"]: getDate(-4),
|
||||
["3.0.0"]: getDate(-3),
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
const modifiedJson = JSON.parse(modifiedBody);
|
||||
|
|
@ -224,7 +225,7 @@ describe("npmInterceptor minimum package age", async () => {
|
|||
// cutoff-date here
|
||||
["2.0.0-alpha"]: getDate(-4),
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
const modifiedJson = JSON.parse(modifiedBody);
|
||||
|
|
@ -260,7 +261,7 @@ describe("npmInterceptor minimum package age", async () => {
|
|||
|
||||
const modifiedBody = await runModifyNpmInfoRequest(
|
||||
packageUrl,
|
||||
originalBody,
|
||||
originalBody
|
||||
);
|
||||
|
||||
const modifiedJson = JSON.parse(modifiedBody);
|
||||
|
|
@ -302,7 +303,7 @@ describe("npmInterceptor minimum package age", async () => {
|
|||
["3.0.0"]: getDate(-40), // ~1.7 days old - should be removed
|
||||
["4.0.0"]: getDate(-24), // 1 day old - should be removed
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
const modifiedJson = JSON.parse(modifiedBody);
|
||||
|
|
@ -346,7 +347,7 @@ describe("npmInterceptor minimum package age", async () => {
|
|||
// 1-hour cutoff here
|
||||
["3.0.0"]: getDate(0), // just published - should be removed
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
const modifiedJson = JSON.parse(modifiedBody);
|
||||
|
|
@ -418,7 +419,7 @@ describe("npmInterceptor minimum package age", async () => {
|
|||
["1.0.0"]: getDate(-7),
|
||||
["3.0.0"]: getDate(-3),
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
const modifiedJson = JSON.parse(modifiedBody);
|
||||
|
|
@ -479,10 +480,7 @@ describe("npmInterceptor minimum package age", async () => {
|
|||
},
|
||||
});
|
||||
|
||||
const modifiedBody = await runModifyNpmInfoRequest(
|
||||
packageUrl,
|
||||
originalBody,
|
||||
);
|
||||
const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody);
|
||||
const modifiedJson = JSON.parse(modifiedBody);
|
||||
|
||||
// All versions should remain since lodash is in the exclusion list
|
||||
|
|
@ -508,10 +506,7 @@ describe("npmInterceptor minimum package age", async () => {
|
|||
},
|
||||
});
|
||||
|
||||
const modifiedBody = await runModifyNpmInfoRequest(
|
||||
packageUrl,
|
||||
originalBody,
|
||||
);
|
||||
const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody);
|
||||
const modifiedJson = JSON.parse(modifiedBody);
|
||||
|
||||
// All versions should remain since @aikidosec/* matches @aikidosec/safe-chain
|
||||
|
|
@ -539,10 +534,7 @@ describe("npmInterceptor minimum package age", async () => {
|
|||
},
|
||||
});
|
||||
|
||||
const modifiedBody = await runModifyNpmInfoRequest(
|
||||
packageUrl,
|
||||
originalBody,
|
||||
);
|
||||
const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody);
|
||||
const modifiedJson = JSON.parse(modifiedBody);
|
||||
|
||||
// Version 2.0.0 should be filtered since @other/package doesn't match @aikidosec/*
|
||||
|
|
@ -569,7 +561,7 @@ describe("npmInterceptor minimum package age", async () => {
|
|||
["1.0.0"]: getDate(-100),
|
||||
["2.0.0"]: getDate(-1),
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
const modifiedJson = JSON.parse(modifiedBody);
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ describe("npmInterceptor", async () => {
|
|||
const interceptor = npmInterceptorForUrl(url);
|
||||
assert.ok(
|
||||
interceptor,
|
||||
"Interceptor should be created for known npm registry",
|
||||
"Interceptor should be created for known npm registry"
|
||||
);
|
||||
|
||||
await interceptor.handleRequest(url);
|
||||
|
|
@ -153,7 +153,7 @@ describe("npmInterceptor", async () => {
|
|||
assert.equal(
|
||||
interceptor,
|
||||
undefined,
|
||||
"Interceptor should be undefined for unknown registry",
|
||||
"Interceptor should be undefined for unknown registry"
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -170,12 +170,12 @@ describe("npmInterceptor", async () => {
|
|||
assert.equal(
|
||||
result.blockResponse.statusCode,
|
||||
403,
|
||||
"Block response should have status code 403",
|
||||
"Block response should have status code 403"
|
||||
);
|
||||
assert.equal(
|
||||
result.blockResponse.message,
|
||||
"Forbidden - blocked by safe-chain",
|
||||
"Block response should have correct status message",
|
||||
"Block response should have correct status message"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -212,7 +212,7 @@ describe("npmInterceptor with custom registries", async () => {
|
|||
|
||||
assert.ok(
|
||||
interceptor,
|
||||
"Interceptor should be created for custom registry with scoped package",
|
||||
"Interceptor should be created for custom registry with scoped package"
|
||||
);
|
||||
|
||||
await interceptor.handleRequest(url);
|
||||
|
|
@ -262,7 +262,7 @@ describe("npmInterceptor with custom registries", async () => {
|
|||
assert.equal(
|
||||
interceptor,
|
||||
undefined,
|
||||
"Should not create interceptor for unknown registry",
|
||||
"Should not create interceptor for unknown registry"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -33,18 +33,16 @@ function buildPipInterceptor(registry) {
|
|||
return interceptRequests(async (reqContext) => {
|
||||
const { packageName, version } = parsePipPackageFromUrl(
|
||||
reqContext.targetUrl,
|
||||
registry,
|
||||
registry
|
||||
);
|
||||
|
||||
// Normalize underscores to hyphens for DB matching, as PyPI allows underscores in distribution names.
|
||||
// Per python, packages that differ only by hyphen vs underscore are considered the same.
|
||||
const hyphenName = packageName?.includes("_")
|
||||
? packageName.replace(/_/g, "-")
|
||||
: packageName;
|
||||
const hyphenName = packageName?.includes("_") ? packageName.replace(/_/g, "-") : packageName;
|
||||
|
||||
const isMalicious =
|
||||
(await isMalwarePackage(packageName, version)) ||
|
||||
(await isMalwarePackage(hyphenName, version));
|
||||
await isMalwarePackage(packageName, version)
|
||||
|| await isMalwarePackage(hyphenName, version);
|
||||
|
||||
if (isMalicious) {
|
||||
reqContext.blockMalware(packageName, version);
|
||||
|
|
@ -112,8 +110,7 @@ function parsePipPackageFromUrl(url, registry) {
|
|||
}
|
||||
|
||||
// Source dist (sdist) and potential metadata sidecars (e.g., .tar.gz.metadata)
|
||||
const sdistExtWithMetadataRe =
|
||||
/\.(tar\.gz|zip|tar\.bz2|tar\.xz)(\.metadata)?$/i;
|
||||
const sdistExtWithMetadataRe = /\.(tar\.gz|zip|tar\.bz2|tar\.xz)(\.metadata)?$/i;
|
||||
const sdistExtMatch = filename.match(sdistExtWithMetadataRe);
|
||||
if (sdistExtMatch) {
|
||||
const base = filename.replace(sdistExtWithMetadataRe, "");
|
||||
|
|
|
|||
|
|
@ -30,7 +30,10 @@ describe("pipInterceptor custom registries", async () => {
|
|||
|
||||
const interceptor = pipInterceptorForUrl(url);
|
||||
|
||||
assert.ok(interceptor, "Interceptor should be created for custom registry");
|
||||
assert.ok(
|
||||
interceptor,
|
||||
"Interceptor should be created for custom registry"
|
||||
);
|
||||
});
|
||||
|
||||
it("should parse package from custom registry URL", async () => {
|
||||
|
|
@ -66,7 +69,10 @@ describe("pipInterceptor custom registries", async () => {
|
|||
});
|
||||
|
||||
it("should handle multiple custom registries", async () => {
|
||||
customRegistries = ["registry-one.example.com", "registry-two.example.com"];
|
||||
customRegistries = [
|
||||
"registry-one.example.com",
|
||||
"registry-two.example.com",
|
||||
];
|
||||
|
||||
const url1 =
|
||||
"https://registry-one.example.com/packages/package1-1.0.0.tar.gz";
|
||||
|
|
@ -79,7 +85,7 @@ describe("pipInterceptor custom registries", async () => {
|
|||
assert.ok(interceptor1, "Interceptor should be created for first registry");
|
||||
assert.ok(
|
||||
interceptor2,
|
||||
"Interceptor should be created for second registry",
|
||||
"Interceptor should be created for second registry"
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -99,12 +105,12 @@ describe("pipInterceptor custom registries", async () => {
|
|||
assert.equal(
|
||||
result.blockResponse.statusCode,
|
||||
403,
|
||||
"Block response should have status code 403",
|
||||
"Block response should have status code 403"
|
||||
);
|
||||
assert.equal(
|
||||
result.blockResponse.message,
|
||||
"Forbidden - blocked by safe-chain",
|
||||
"Block response should have correct status message",
|
||||
"Block response should have correct status message"
|
||||
);
|
||||
|
||||
malwareResponse = false;
|
||||
|
|
@ -120,7 +126,7 @@ describe("pipInterceptor custom registries", async () => {
|
|||
|
||||
assert.ok(
|
||||
interceptor,
|
||||
"Interceptor should be created for known registry even with custom registries set",
|
||||
"Interceptor should be created for known registry even with custom registries set"
|
||||
);
|
||||
|
||||
await interceptor.handleRequest(url);
|
||||
|
|
@ -133,15 +139,14 @@ describe("pipInterceptor custom registries", async () => {
|
|||
|
||||
it("should not create interceptor for unknown registry when custom registries are set", () => {
|
||||
customRegistries = ["my-custom-registry.example.com"];
|
||||
const url =
|
||||
"https://unknown-registry.example.com/packages/foobar-1.0.0.tar.gz";
|
||||
const url = "https://unknown-registry.example.com/packages/foobar-1.0.0.tar.gz";
|
||||
|
||||
const interceptor = pipInterceptorForUrl(url);
|
||||
|
||||
assert.equal(
|
||||
interceptor,
|
||||
undefined,
|
||||
"Interceptor should be undefined for unknown registry",
|
||||
"Interceptor should be undefined for unknown registry"
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -155,7 +160,7 @@ describe("pipInterceptor custom registries", async () => {
|
|||
assert.equal(
|
||||
interceptor,
|
||||
undefined,
|
||||
"Interceptor should be undefined when no custom registries are configured",
|
||||
"Interceptor should be undefined when no custom registries are configured"
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ describe("pipInterceptor", async () => {
|
|||
const interceptor = pipInterceptorForUrl(url);
|
||||
assert.ok(
|
||||
interceptor,
|
||||
"Interceptor should be created for known npm registry",
|
||||
"Interceptor should be created for known npm registry"
|
||||
);
|
||||
|
||||
await interceptor.handleRequest(url);
|
||||
|
|
@ -117,7 +117,7 @@ describe("pipInterceptor", async () => {
|
|||
assert.equal(
|
||||
interceptor,
|
||||
undefined,
|
||||
"Interceptor should be undefined for unknown registry",
|
||||
"Interceptor should be undefined for unknown registry"
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -134,12 +134,12 @@ describe("pipInterceptor", async () => {
|
|||
assert.equal(
|
||||
result.blockResponse.statusCode,
|
||||
403,
|
||||
"Block response should have status code 403",
|
||||
"Block response should have status code 403"
|
||||
);
|
||||
assert.equal(
|
||||
result.blockResponse.message,
|
||||
"Forbidden - blocked by safe-chain",
|
||||
"Block response should have correct status message",
|
||||
"Block response should have correct status message"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export function mitmConnect(req, clientSocket, interceptor) {
|
|||
|
||||
clientSocket.on("error", (err) => {
|
||||
ui.writeVerbose(
|
||||
`Safe-chain: Client socket error for ${req.url}: ${err.message}`,
|
||||
`Safe-chain: Client socket error for ${req.url}: ${err.message}`
|
||||
);
|
||||
// NO-OP
|
||||
// This can happen if the client TCP socket sends RST instead of FIN.
|
||||
|
|
@ -89,7 +89,7 @@ function createHttpsServer(hostname, port, interceptor) {
|
|||
key: cert.privateKey,
|
||||
cert: cert.certificate,
|
||||
},
|
||||
handleRequest,
|
||||
handleRequest
|
||||
);
|
||||
|
||||
return server;
|
||||
|
|
@ -119,7 +119,7 @@ function forwardRequest(req, hostname, port, res, requestHandler) {
|
|||
|
||||
proxyReq.on("error", (err) => {
|
||||
ui.writeVerbose(
|
||||
`Safe-chain: Error occurred while proxying request to ${req.url} for ${hostname}: ${err.message}`,
|
||||
`Safe-chain: Error occurred while proxying request to ${req.url} for ${hostname}: ${err.message}`
|
||||
);
|
||||
res.writeHead(502);
|
||||
res.end("Bad Gateway");
|
||||
|
|
@ -127,7 +127,7 @@ function forwardRequest(req, hostname, port, res, requestHandler) {
|
|||
|
||||
req.on("error", (err) => {
|
||||
ui.writeError(
|
||||
`Safe-chain: Error reading client request to ${req.url} for ${hostname}: ${err.message}`,
|
||||
`Safe-chain: Error reading client request to ${req.url} for ${hostname}: ${err.message}`
|
||||
);
|
||||
proxyReq.destroy();
|
||||
});
|
||||
|
|
@ -138,7 +138,7 @@ function forwardRequest(req, hostname, port, res, requestHandler) {
|
|||
|
||||
req.on("end", () => {
|
||||
ui.writeVerbose(
|
||||
`Safe-chain: Finished proxying request to ${req.url} for ${hostname}`,
|
||||
`Safe-chain: Finished proxying request to ${req.url} for ${hostname}`
|
||||
);
|
||||
proxyReq.end();
|
||||
});
|
||||
|
|
@ -180,7 +180,7 @@ function createProxyRequest(hostname, port, req, res, requestHandler) {
|
|||
const proxyReq = https.request(options, (proxyRes) => {
|
||||
proxyRes.on("error", (err) => {
|
||||
ui.writeError(
|
||||
`Safe-chain: Error reading upstream response to ${req.url} for ${hostname}: ${err.message}`,
|
||||
`Safe-chain: Error reading upstream response to ${req.url} for ${hostname}: ${err.message}`
|
||||
);
|
||||
if (!res.headersSent) {
|
||||
res.writeHead(502);
|
||||
|
|
@ -190,7 +190,7 @@ function createProxyRequest(hostname, port, req, res, requestHandler) {
|
|||
|
||||
if (!proxyRes.statusCode) {
|
||||
ui.writeError(
|
||||
`Safe-chain: Proxy response missing status code to ${req.url} for ${hostname}`,
|
||||
`Safe-chain: Proxy response missing status code to ${req.url} for ${hostname}`
|
||||
);
|
||||
res.writeHead(500);
|
||||
res.end("Internal Server Error");
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export function handleHttpProxyRequest(req, res) {
|
|||
res.end();
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
)
|
||||
.on("error", (err) => {
|
||||
if (!res.headersSent) {
|
||||
|
|
|
|||
|
|
@ -49,11 +49,11 @@ function tunnelRequestToDestination(req, clientSocket, head) {
|
|||
clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
|
||||
if (isImds) {
|
||||
ui.writeVerbose(
|
||||
`Safe-chain: Closing connection because previously timedout connect to ${hostname}`,
|
||||
`Safe-chain: Closing connection because previously timedout connect to ${hostname}`
|
||||
);
|
||||
} else {
|
||||
ui.writeError(
|
||||
`Safe-chain: Closing connection because previously timedout connect to ${hostname}`,
|
||||
`Safe-chain: Closing connection because previously timedout connect to ${hostname}`
|
||||
);
|
||||
}
|
||||
return;
|
||||
|
|
@ -67,11 +67,11 @@ function tunnelRequestToDestination(req, clientSocket, head) {
|
|||
if (isImds) {
|
||||
timedoutImdsEndpoints.push(hostname);
|
||||
ui.writeVerbose(
|
||||
`Safe-chain: connect to ${hostname}:${targetPort} timed out after ${connectTimeout}ms`,
|
||||
`Safe-chain: connect to ${hostname}:${targetPort} timed out after ${connectTimeout}ms`
|
||||
);
|
||||
} else {
|
||||
ui.writeError(
|
||||
`Safe-chain: connect to ${hostname}:${targetPort} timed out after ${connectTimeout}ms`,
|
||||
`Safe-chain: connect to ${hostname}:${targetPort} timed out after ${connectTimeout}ms`
|
||||
);
|
||||
}
|
||||
serverSocket.destroy();
|
||||
|
|
@ -111,11 +111,11 @@ function tunnelRequestToDestination(req, clientSocket, head) {
|
|||
clearTimeout(connectTimer);
|
||||
if (isImds) {
|
||||
ui.writeVerbose(
|
||||
`Safe-chain: error connecting to ${hostname}:${targetPort} - ${err.message}`,
|
||||
`Safe-chain: error connecting to ${hostname}:${targetPort} - ${err.message}`
|
||||
);
|
||||
} else {
|
||||
ui.writeError(
|
||||
`Safe-chain: error connecting to ${hostname}:${targetPort} - ${err.message}`,
|
||||
`Safe-chain: error connecting to ${hostname}:${targetPort} - ${err.message}`
|
||||
);
|
||||
}
|
||||
if (clientSocket.writable) {
|
||||
|
|
@ -173,7 +173,7 @@ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) {
|
|||
clientSocket.pipe(proxySocket);
|
||||
} else {
|
||||
ui.writeError(
|
||||
`Safe-chain: proxy CONNECT failed: ${response.split("\r\n")[0]}`,
|
||||
`Safe-chain: proxy CONNECT failed: ${response.split("\r\n")[0]}`
|
||||
);
|
||||
if (clientSocket.writable) {
|
||||
clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
|
||||
|
|
@ -189,14 +189,14 @@ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) {
|
|||
ui.writeError(
|
||||
`Safe-chain: error connecting to proxy ${proxy.hostname}:${
|
||||
proxy.port || 8080
|
||||
} - ${err.message}`,
|
||||
} - ${err.message}`
|
||||
);
|
||||
if (clientSocket.writable) {
|
||||
clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
|
||||
}
|
||||
} else {
|
||||
ui.writeError(
|
||||
`Safe-chain: proxy socket error after connection - ${err.message}`,
|
||||
`Safe-chain: proxy socket error after connection - ${err.message}`
|
||||
);
|
||||
if (clientSocket.writable) {
|
||||
clientSocket.end();
|
||||
|
|
|
|||
|
|
@ -85,21 +85,14 @@ export function getCombinedCaBundlePath(proxyCaCert) {
|
|||
const userPem = readUserCertificateFile(userCertPath);
|
||||
if (userPem) {
|
||||
parts.push(userPem.trim());
|
||||
ui.writeVerbose(
|
||||
`Safe-chain: Merging user's NODE_EXTRA_CA_CERTS from ${userCertPath}`,
|
||||
);
|
||||
ui.writeVerbose(`Safe-chain: Merging user's NODE_EXTRA_CA_CERTS from ${userCertPath}`);
|
||||
} else {
|
||||
ui.writeWarning(
|
||||
`Safe-chain: Could not read or parse user's NODE_EXTRA_CA_CERTS from ${userCertPath}`,
|
||||
);
|
||||
ui.writeWarning(`Safe-chain: Could not read or parse user's NODE_EXTRA_CA_CERTS from ${userCertPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
const combined = parts.filter(Boolean).join("\n");
|
||||
const target = path.join(
|
||||
os.tmpdir(),
|
||||
`safe-chain-ca-bundle-${Date.now()}.pem`,
|
||||
);
|
||||
const target = path.join(os.tmpdir(), `safe-chain-ca-bundle-${Date.now()}.pem`);
|
||||
fs.writeFileSync(target, combined, { encoding: "utf8" });
|
||||
return target;
|
||||
}
|
||||
|
|
@ -177,3 +170,4 @@ function readUserCertificateFile(certPath) {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ describe("registryProxy.connectTunnel", () => {
|
|||
const tunnelResponse = await establishHttpsTunnel(
|
||||
socket,
|
||||
"postman-echo.com",
|
||||
443,
|
||||
443
|
||||
);
|
||||
|
||||
assert.ok(tunnelResponse.includes("HTTP/1.1 200 Connection Established"));
|
||||
|
|
@ -69,7 +69,7 @@ describe("registryProxy.connectTunnel", () => {
|
|||
const httpsResponse = await sendHttpsRequestThroughTunnel(
|
||||
socket,
|
||||
"GET",
|
||||
new URL("https://postman-echo.com/status/200"),
|
||||
new URL("https://postman-echo.com/status/200")
|
||||
);
|
||||
|
||||
assert.ok(httpsResponse.includes("HTTP/1.1 200 OK"));
|
||||
|
|
@ -85,25 +85,25 @@ describe("registryProxy.connectTunnel", () => {
|
|||
// without interception by the safe-chain MITM proxy.
|
||||
const certInfo = await getTlsCertificateInfo(
|
||||
socket,
|
||||
new URL("https://postman-echo.com"),
|
||||
new URL("https://postman-echo.com")
|
||||
);
|
||||
|
||||
// Verify the certificate is NOT issued by our safe-chain CA
|
||||
// Our self-signed CA would have issuer: "Safe-Chain Proxy CA"
|
||||
assert.ok(
|
||||
certInfo.issuer !== undefined,
|
||||
"Certificate should have an issuer",
|
||||
"Certificate should have an issuer"
|
||||
);
|
||||
assert.ok(
|
||||
!certInfo.issuer.includes("Safe-Chain"),
|
||||
`Tunnel should use destination's real certificate, not safe-chain CA. Issuer: ${certInfo.issuer}`,
|
||||
`Tunnel should use destination's real certificate, not safe-chain CA. Issuer: ${certInfo.issuer}`
|
||||
);
|
||||
|
||||
// Verify it's a real certificate with proper hostname
|
||||
assert.strictEqual(
|
||||
certInfo.subject.includes("postman-echo.com"),
|
||||
true,
|
||||
`Certificate subject should include postman-echo.com, got: ${certInfo.subject}`,
|
||||
`Certificate subject should include postman-echo.com, got: ${certInfo.subject}`
|
||||
);
|
||||
|
||||
socket.destroy();
|
||||
|
|
@ -232,13 +232,13 @@ describe("registryProxy.connectTunnel", () => {
|
|||
// Should return 502 immediately (cached timeout)
|
||||
assert.ok(
|
||||
responseData.includes("HTTP/1.1 502 Bad Gateway"),
|
||||
"Should return 502 for cached timeout",
|
||||
"Should return 502 for cached timeout"
|
||||
);
|
||||
|
||||
// Should be nearly instant (< 50ms) since it's cached
|
||||
assert.ok(
|
||||
duration < 50,
|
||||
`Cached IMDS timeout should be instant, got ${duration}ms`,
|
||||
`Cached IMDS timeout should be instant, got ${duration}ms`
|
||||
);
|
||||
|
||||
socket2.destroy();
|
||||
|
|
@ -283,14 +283,14 @@ describe("registryProxy.connectTunnel", () => {
|
|||
// Should return 504 Gateway Timeout (not 502 - 504 is for actual timeouts)
|
||||
assert.ok(
|
||||
responseData.includes("HTTP/1.1 504 Gateway Timeout"),
|
||||
"Should return 504 for timeout",
|
||||
"Should return 504 for timeout"
|
||||
);
|
||||
|
||||
// Should NOT be instant - it should retry the connection (taking ~500ms due to mock timeout)
|
||||
// If it was cached, it would return in < 50ms
|
||||
assert.ok(
|
||||
duration >= 400,
|
||||
`Non-IMDS timeout should NOT be cached, but got instant response in ${duration}ms`,
|
||||
`Non-IMDS timeout should NOT be cached, but got instant response in ${duration}ms`
|
||||
);
|
||||
|
||||
socket2.destroy();
|
||||
|
|
@ -343,7 +343,7 @@ function sendHttpsRequestThroughTunnel(
|
|||
socket,
|
||||
verb,
|
||||
url,
|
||||
rejectUnauthorized = false,
|
||||
rejectUnauthorized = false
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tlsSocket = tls.connect(
|
||||
|
|
@ -356,9 +356,9 @@ function sendHttpsRequestThroughTunnel(
|
|||
},
|
||||
() => {
|
||||
tlsSocket.write(
|
||||
`${verb} ${url.pathname} HTTP/1.1\r\nHost: ${url.hostname}\r\nConnection: close\r\n\r\n`,
|
||||
`${verb} ${url.pathname} HTTP/1.1\r\nHost: ${url.hostname}\r\nConnection: close\r\n\r\n`
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
let tlsData = "";
|
||||
|
|
@ -404,7 +404,7 @@ function getTlsCertificateInfo(socket, url) {
|
|||
|
||||
tlsSocket.end();
|
||||
resolve({ issuer, subject });
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
tlsSocket.on("error", (err) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue