Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 21 additions & 13 deletions packages/safe-chain/src/registryProxy/mitmRequestHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,21 +67,29 @@ function createHttpsServer(hostname, port, interceptor) {
return;
}

const pathAndQuery = getRequestPathAndQuery(req.url);
const targetUrl = `https://${hostname}${pathAndQuery}`;

const requestInterceptor = await interceptor.handleRequest(targetUrl);
const blockResponse = requestInterceptor.blockResponse;
try {
const pathAndQuery = getRequestPathAndQuery(req.url);
const targetUrl = `https://${hostname}${pathAndQuery}`;

const requestInterceptor = await interceptor.handleRequest(targetUrl);
const blockResponse = requestInterceptor.blockResponse;

if (blockResponse) {
ui.writeVerbose(`Safe-chain: Blocking request to ${targetUrl}`);
res.writeHead(blockResponse.statusCode, blockResponse.message);
res.end(blockResponse.message);
return;
}

if (blockResponse) {
ui.writeVerbose(`Safe-chain: Blocking request to ${targetUrl}`);
res.writeHead(blockResponse.statusCode, blockResponse.message);
res.end(blockResponse.message);
return;
// Collect request body
forwardRequest(req, hostname, port, res, requestInterceptor);
} catch (/** @type {any} */ error) {
ui.writeError(
`Safe-chain: Error handling request for ${req.url}: ${error.message}`
);
res.writeHead(502, "Bad Gateway");
res.end("Bad Gateway: Error handling request");
}

// Collect request body
forwardRequest(req, hostname, port, res, requestInterceptor);
}

const server = https.createServer(
Expand Down
38 changes: 22 additions & 16 deletions packages/safe-chain/src/registryProxy/tunnelRequestHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,28 @@ let timedoutImdsEndpoints = [];
* @returns {void}
*/
export function tunnelRequest(req, clientSocket, head) {
const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy;

if (httpsProxy) {
// If an HTTPS proxy is set, tunnel the request via the proxy
// This is the system proxy, not the safe-chain proxy
// The package manager will run via the safe-chain proxy
// The safe-chain proxy will then send the request to the system proxy
// Typical flow: package manager -> safe-chain proxy -> system proxy -> destination

// There are 2 processes involved in this:
// 1. Safe-chain process: has HTTPS_PROXY set to system proxy
// 2. Package manager process: has HTTPS_PROXY set to safe-chain proxy

tunnelRequestViaProxy(req, clientSocket, head, httpsProxy);
} else {
tunnelRequestToDestination(req, clientSocket, head);
try {
const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy;

if (httpsProxy) {
// If an HTTPS proxy is set, tunnel the request via the proxy
// This is the system proxy, not the safe-chain proxy
// The package manager will run via the safe-chain proxy
// The safe-chain proxy will then send the request to the system proxy
// Typical flow: package manager -> safe-chain proxy -> system proxy -> destination

// There are 2 processes involved in this:
// 1. Safe-chain process: has HTTPS_PROXY set to system proxy
// 2. Package manager process: has HTTPS_PROXY set to safe-chain proxy

tunnelRequestViaProxy(req, clientSocket, head, httpsProxy);
} else {
tunnelRequestToDestination(req, clientSocket, head);
}
} catch (/** @type {any} */ err) {
ui.writeError(
`Safe-chain: tunnel request failed for ${req.url} : ${err.message}`
);
}
}

Expand Down
54 changes: 54 additions & 0 deletions test/e2e/dns-failure-resilience.e2e.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, it, before, beforeEach, afterEach } from "node:test";
import { DockerTestContainer } from "./DockerTestContainer.js";
import assert from "node:assert";

describe("E2E: DNS failure resilience", () => {
let container;

before(async () => {
DockerTestContainer.buildImage();
});

beforeEach(async () => {
container = new DockerTestContainer();
await container.start();

const installationShell = await container.openShell("zsh");
await installationShell.runCommand("safe-chain setup");
});

afterEach(async () => {
if (container) {
await container.stop();
container = null;
}
});

it("should not crash when the npm registry is unreachable", async () => {
const shell = await container.openShell("zsh");

// Make the npm registry domain unreachable.
// `npm install lodash` talks to https://registry.npmjs.org/ for both metadata and tarballs.
await shell.runCommand(
'echo "127.0.0.1 registry.npmjs.org" >> /etc/hosts'
);

const result = await shell.runCommand(
// Fail fast so the shell runner doesn't time out.
// Also disable extra network calls that could introduce noise.
"npm install lodash --no-audit --no-fund --fetch-retries=0 --fetch-timeout=2000 --safe-chain-logging=verbose"
);

assert.ok(
result.output.includes("registry.npmjs.org"),
`Output did not reference the npm registry host; /etc/hosts override may not have applied. Output was:\n${result.output}`
);

// Ensure it did NOT crash with Unhandled Promise Rejection
assert.strictEqual(
result.output.includes("Unhandled promise rejection"),
false,
`Output indicates process crash (Unhandled promise rejection). Output was:\n${result.output}`
);
});
});