Cleanup formatting changes from PR

This commit is contained in:
Sander Declerck 2026-03-02 15:54:41 +01:00
parent 912f62f3b9
commit e8a4fbcd76
No known key found for this signature in database
19 changed files with 200 additions and 407 deletions

View file

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

View file

@ -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)) {

View file

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

View file

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

View file

@ -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, "");

View file

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

View file

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

View file

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

View file

@ -61,7 +61,7 @@ export function handleHttpProxyRequest(req, res) {
res.end();
}
});
},
}
)
.on("error", (err) => {
if (!res.headersSent) {

View file

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

View file

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

View file

@ -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) => {