@aikidosec/safe-chain 0.0.4-connect-timeout-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +257 -0
  3. package/bin/aikido-bun.js +14 -0
  4. package/bin/aikido-bunx.js +14 -0
  5. package/bin/aikido-npm.js +14 -0
  6. package/bin/aikido-npx.js +14 -0
  7. package/bin/aikido-pip.js +20 -0
  8. package/bin/aikido-pip3.js +21 -0
  9. package/bin/aikido-pnpm.js +14 -0
  10. package/bin/aikido-pnpx.js +14 -0
  11. package/bin/aikido-python.js +30 -0
  12. package/bin/aikido-python3.js +30 -0
  13. package/bin/aikido-uv.js +16 -0
  14. package/bin/aikido-yarn.js +14 -0
  15. package/bin/safe-chain.js +190 -0
  16. package/docs/banner.svg +151 -0
  17. package/docs/npm-to-binary-migration.md +89 -0
  18. package/docs/safe-package-manager-demo.gif +0 -0
  19. package/docs/safe-package-manager-demo.png +0 -0
  20. package/docs/shell-integration.md +149 -0
  21. package/package.json +68 -0
  22. package/src/api/aikido.js +54 -0
  23. package/src/api/npmApi.js +71 -0
  24. package/src/config/cliArguments.js +138 -0
  25. package/src/config/configFile.js +192 -0
  26. package/src/config/environmentVariables.js +7 -0
  27. package/src/config/settings.js +100 -0
  28. package/src/environment/environment.js +14 -0
  29. package/src/environment/userInteraction.js +122 -0
  30. package/src/main.js +104 -0
  31. package/src/packagemanager/_shared/matchesCommand.js +18 -0
  32. package/src/packagemanager/bun/createBunPackageManager.js +53 -0
  33. package/src/packagemanager/currentPackageManager.js +72 -0
  34. package/src/packagemanager/npm/createPackageManager.js +72 -0
  35. package/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js +74 -0
  36. package/src/packagemanager/npm/dependencyScanner/nullScanner.js +9 -0
  37. package/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js +144 -0
  38. package/src/packagemanager/npm/runNpmCommand.js +25 -0
  39. package/src/packagemanager/npm/utils/abbrevs-generated.js +359 -0
  40. package/src/packagemanager/npm/utils/cmd-list.js +174 -0
  41. package/src/packagemanager/npm/utils/npmCommands.js +34 -0
  42. package/src/packagemanager/npx/createPackageManager.js +15 -0
  43. package/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js +43 -0
  44. package/src/packagemanager/npx/parsing/parsePackagesFromArguments.js +130 -0
  45. package/src/packagemanager/npx/runNpxCommand.js +25 -0
  46. package/src/packagemanager/pip/createPackageManager.js +21 -0
  47. package/src/packagemanager/pip/pipSettings.js +30 -0
  48. package/src/packagemanager/pip/runPipCommand.js +175 -0
  49. package/src/packagemanager/pnpm/createPackageManager.js +57 -0
  50. package/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js +35 -0
  51. package/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js +109 -0
  52. package/src/packagemanager/pnpm/runPnpmCommand.js +36 -0
  53. package/src/packagemanager/uv/createUvPackageManager.js +18 -0
  54. package/src/packagemanager/uv/runUvCommand.js +71 -0
  55. package/src/packagemanager/yarn/createPackageManager.js +41 -0
  56. package/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js +35 -0
  57. package/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js +128 -0
  58. package/src/packagemanager/yarn/runYarnCommand.js +41 -0
  59. package/src/registryProxy/certBundle.js +95 -0
  60. package/src/registryProxy/certUtils.js +128 -0
  61. package/src/registryProxy/http-utils.js +17 -0
  62. package/src/registryProxy/interceptors/createInterceptorForEcoSystem.js +25 -0
  63. package/src/registryProxy/interceptors/interceptorBuilder.js +140 -0
  64. package/src/registryProxy/interceptors/npm/modifyNpmInfo.js +177 -0
  65. package/src/registryProxy/interceptors/npm/npmInterceptor.js +47 -0
  66. package/src/registryProxy/interceptors/npm/parseNpmPackageUrl.js +43 -0
  67. package/src/registryProxy/interceptors/pipInterceptor.js +115 -0
  68. package/src/registryProxy/mitmRequestHandler.js +231 -0
  69. package/src/registryProxy/plainHttpProxy.js +95 -0
  70. package/src/registryProxy/registryProxy.js +184 -0
  71. package/src/registryProxy/tunnelRequestHandler.js +180 -0
  72. package/src/scanning/audit/index.js +129 -0
  73. package/src/scanning/index.js +82 -0
  74. package/src/scanning/malwareDatabase.js +131 -0
  75. package/src/shell-integration/helpers.js +213 -0
  76. package/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +22 -0
  77. package/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +24 -0
  78. package/src/shell-integration/setup-ci.js +170 -0
  79. package/src/shell-integration/setup.js +127 -0
  80. package/src/shell-integration/shellDetection.js +37 -0
  81. package/src/shell-integration/startup-scripts/include-python/init-fish.fish +94 -0
  82. package/src/shell-integration/startup-scripts/include-python/init-posix.sh +81 -0
  83. package/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 +115 -0
  84. package/src/shell-integration/startup-scripts/init-fish.fish +71 -0
  85. package/src/shell-integration/startup-scripts/init-posix.sh +58 -0
  86. package/src/shell-integration/startup-scripts/init-pwsh.ps1 +92 -0
  87. package/src/shell-integration/supported-shells/bash.js +134 -0
  88. package/src/shell-integration/supported-shells/fish.js +77 -0
  89. package/src/shell-integration/supported-shells/powershell.js +73 -0
  90. package/src/shell-integration/supported-shells/windowsPowershell.js +73 -0
  91. package/src/shell-integration/supported-shells/zsh.js +74 -0
  92. package/src/shell-integration/teardown.js +64 -0
  93. package/src/utils/safeSpawn.js +137 -0
  94. package/tsconfig.json +21 -0
@@ -0,0 +1,231 @@
1
+ import https from "https";
2
+ import { generateCertForHost } from "./certUtils.js";
3
+ import { HttpsProxyAgent } from "https-proxy-agent";
4
+ import { ui } from "../environment/userInteraction.js";
5
+ import { gunzipSync, gzipSync } from "zlib";
6
+
7
+ /**
8
+ * @typedef {import("./interceptors/interceptorBuilder.js").Interceptor} Interceptor
9
+ */
10
+
11
+ /**
12
+ * @param {import("http").IncomingMessage} req
13
+ * @param {import("http").ServerResponse} clientSocket
14
+ * @param {Interceptor} interceptor
15
+ */
16
+ export function mitmConnect(req, clientSocket, interceptor) {
17
+ ui.writeVerbose(`Safe-chain: Set up MITM tunnel for ${req.url}`);
18
+ const { hostname } = new URL(`http://${req.url}`);
19
+
20
+ clientSocket.on("error", (err) => {
21
+ ui.writeVerbose(
22
+ `Safe-chain: Client socket error for ${req.url}: ${err.message}`
23
+ );
24
+ // NO-OP
25
+ // This can happen if the client TCP socket sends RST instead of FIN.
26
+ // Not subscribing to 'close' event will cause node to throw and crash.
27
+ });
28
+
29
+ const server = createHttpsServer(hostname, interceptor);
30
+
31
+ server.on("error", (err) => {
32
+ ui.writeError(`Safe-chain: HTTPS server error: ${err.message}`);
33
+ if (!clientSocket.headersSent) {
34
+ clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
35
+ } else if (clientSocket.writable) {
36
+ clientSocket.end();
37
+ }
38
+ });
39
+
40
+ // Establish the connection
41
+ clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
42
+
43
+ // Hand off the socket to the HTTPS server
44
+ server.emit("connection", clientSocket);
45
+ }
46
+
47
+ /**
48
+ * @param {string} hostname
49
+ * @param {Interceptor} interceptor
50
+ * @returns {import("https").Server}
51
+ */
52
+ function createHttpsServer(hostname, interceptor) {
53
+ const cert = generateCertForHost(hostname);
54
+
55
+ /**
56
+ * @param {import("http").IncomingMessage} req
57
+ * @param {import("http").ServerResponse} res
58
+ *
59
+ * @returns {Promise<void>}
60
+ */
61
+ async function handleRequest(req, res) {
62
+ if (!req.url) {
63
+ ui.writeError("Safe-chain: Request missing URL");
64
+ res.writeHead(400, "Bad Request");
65
+ res.end("Bad Request: Missing URL");
66
+ return;
67
+ }
68
+
69
+ const pathAndQuery = getRequestPathAndQuery(req.url);
70
+ const targetUrl = `https://${hostname}${pathAndQuery}`;
71
+
72
+ const requestInterceptor = await interceptor.handleRequest(targetUrl);
73
+ const blockResponse = requestInterceptor.blockResponse;
74
+
75
+ if (blockResponse) {
76
+ ui.writeVerbose(`Safe-chain: Blocking request to ${targetUrl}`);
77
+ res.writeHead(blockResponse.statusCode, blockResponse.message);
78
+ res.end(blockResponse.message);
79
+ return;
80
+ }
81
+
82
+ // Collect request body
83
+ forwardRequest(req, hostname, res, requestInterceptor);
84
+ }
85
+
86
+ const server = https.createServer(
87
+ {
88
+ key: cert.privateKey,
89
+ cert: cert.certificate,
90
+ },
91
+ handleRequest
92
+ );
93
+
94
+ return server;
95
+ }
96
+
97
+ /**
98
+ * @param {string} url
99
+ * @returns {string}
100
+ */
101
+ function getRequestPathAndQuery(url) {
102
+ if (url.startsWith("http://") || url.startsWith("https://")) {
103
+ const parsedUrl = new URL(url);
104
+ return parsedUrl.pathname + parsedUrl.search + parsedUrl.hash;
105
+ }
106
+ return url;
107
+ }
108
+
109
+ /**
110
+ * @param {import("http").IncomingMessage} req
111
+ * @param {string} hostname
112
+ * @param {import("http").ServerResponse} res
113
+ * @param {import("./interceptors/interceptorBuilder.js").RequestInterceptionHandler} requestHandler
114
+ */
115
+ function forwardRequest(req, hostname, res, requestHandler) {
116
+ const proxyReq = createProxyRequest(hostname, req, res, requestHandler);
117
+
118
+ proxyReq.on("error", (err) => {
119
+ ui.writeVerbose(
120
+ `Safe-chain: Error occurred while proxying request to ${req.url} for ${hostname}: ${err.message}`
121
+ );
122
+ res.writeHead(502);
123
+ res.end("Bad Gateway");
124
+ });
125
+
126
+ req.on("error", (err) => {
127
+ ui.writeError(
128
+ `Safe-chain: Error reading client request to ${req.url} for ${hostname}: ${err.message}`
129
+ );
130
+ proxyReq.destroy();
131
+ });
132
+
133
+ req.on("data", (chunk) => {
134
+ proxyReq.write(chunk);
135
+ });
136
+
137
+ req.on("end", () => {
138
+ ui.writeVerbose(
139
+ `Safe-chain: Finished proxying request to ${req.url} for ${hostname}`
140
+ );
141
+ proxyReq.end();
142
+ });
143
+ }
144
+
145
+ /**
146
+ * @param {string} hostname
147
+ * @param {import("http").IncomingMessage} req
148
+ * @param {import("http").ServerResponse} res
149
+ * @param {import("./interceptors/interceptorBuilder.js").RequestInterceptionHandler} requestHandler
150
+ *
151
+ * @returns {import("http").ClientRequest}
152
+ */
153
+ function createProxyRequest(hostname, req, res, requestHandler) {
154
+ /** @type {NodeJS.Dict<string | string[]> | undefined} */
155
+ let headers = { ...req.headers };
156
+ // Remove the host header from the incoming request before forwarding.
157
+ // Node's http module sets the correct host header for the target hostname automatically.
158
+ if (headers.host) {
159
+ delete headers.host;
160
+ }
161
+ headers = requestHandler.modifyRequestHeaders(headers);
162
+
163
+ /** @type {import("http").RequestOptions} */
164
+ const options = {
165
+ hostname: hostname,
166
+ port: 443,
167
+ path: req.url,
168
+ method: req.method,
169
+ headers: { ...headers },
170
+ };
171
+
172
+ const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy;
173
+ if (httpsProxy) {
174
+ options.agent = new HttpsProxyAgent(httpsProxy);
175
+ }
176
+
177
+ const proxyReq = https.request(options, (proxyRes) => {
178
+ proxyRes.on("error", (err) => {
179
+ ui.writeError(
180
+ `Safe-chain: Error reading upstream response to ${req.url} for ${hostname}: ${err.message}`
181
+ );
182
+ if (!res.headersSent) {
183
+ res.writeHead(502);
184
+ res.end("Bad Gateway");
185
+ }
186
+ });
187
+
188
+ if (!proxyRes.statusCode) {
189
+ ui.writeError(
190
+ `Safe-chain: Proxy response missing status code to ${req.url} for ${hostname}`
191
+ );
192
+ res.writeHead(500);
193
+ res.end("Internal Server Error");
194
+ return;
195
+ }
196
+
197
+ const { statusCode, headers } = proxyRes;
198
+
199
+ if (requestHandler.modifiesResponse()) {
200
+ /** @type {Array<any>} */
201
+ let chunks = [];
202
+
203
+ proxyRes.on("data", (chunk) => chunks.push(chunk));
204
+
205
+ proxyRes.on("end", () => {
206
+ /** @type {Buffer} */
207
+ let buffer = Buffer.concat(chunks);
208
+
209
+ if (proxyRes.headers["content-encoding"] === "gzip") {
210
+ buffer = gunzipSync(buffer);
211
+ }
212
+
213
+ buffer = requestHandler.modifyBody(buffer, headers);
214
+
215
+ if (proxyRes.headers["content-encoding"] === "gzip") {
216
+ buffer = gzipSync(buffer);
217
+ }
218
+
219
+ res.writeHead(statusCode, headers);
220
+ res.end(buffer);
221
+ });
222
+ } else {
223
+ // If the response is not being modified, we can
224
+ // just pipe without the need for buffering the output
225
+ res.writeHead(statusCode, headers);
226
+ proxyRes.pipe(res);
227
+ }
228
+ });
229
+
230
+ return proxyReq;
231
+ }
@@ -0,0 +1,95 @@
1
+ import * as http from "http";
2
+ import * as https from "https";
3
+ import { ui } from "../environment/userInteraction.js";
4
+
5
+ /**
6
+ * @param {import("http").IncomingMessage} req
7
+ * @param {import("http").ServerResponse} res
8
+ *
9
+ * @returns {void}
10
+ */
11
+ export function handleHttpProxyRequest(req, res) {
12
+ if (!req.url) {
13
+ ui.writeError("Safe-chain: Request missing URL");
14
+ res.writeHead(400, "Bad Request");
15
+ res.end("Bad Request: Missing URL");
16
+ return;
17
+ }
18
+
19
+ const url = new URL(req.url);
20
+
21
+ // The protocol for the plainHttpProxy should usually only be http:
22
+ // but when the client for some reason sends an https: request directly
23
+ // instead of using the CONNECT method, we should handle it gracefully.
24
+ let protocol;
25
+ if (url.protocol === "http:") {
26
+ protocol = http;
27
+ } else if (url.protocol === "https:") {
28
+ protocol = https;
29
+ } else {
30
+ res.writeHead(502);
31
+ res.end(`Bad Gateway: Unsupported protocol ${url.protocol}`);
32
+ return;
33
+ }
34
+
35
+ const proxyRequest = protocol
36
+ .request(
37
+ req.url,
38
+ { method: req.method, headers: req.headers },
39
+ (proxyRes) => {
40
+ if (!proxyRes.statusCode) {
41
+ ui.writeError("Safe-chain: Proxy response missing status code");
42
+ res.writeHead(500);
43
+ res.end("Internal Server Error");
44
+ return;
45
+ }
46
+
47
+ res.writeHead(proxyRes.statusCode, proxyRes.headers);
48
+ proxyRes.pipe(res);
49
+
50
+ proxyRes.on("error", () => {
51
+ // Proxy response stream error
52
+ // Clean up client response stream
53
+ if (res.writable) {
54
+ res.end();
55
+ }
56
+ });
57
+
58
+ proxyRes.on("close", () => {
59
+ // Clean up if the proxy response stream closes
60
+ if (res.writable) {
61
+ res.end();
62
+ }
63
+ });
64
+ }
65
+ )
66
+ .on("error", (err) => {
67
+ if (!res.headersSent) {
68
+ res.writeHead(502);
69
+ res.end(`Bad Gateway: ${err.message}`);
70
+ } else {
71
+ // Headers already sent, just destroy the response
72
+ res.destroy();
73
+ }
74
+ });
75
+
76
+ req.on("error", () => {
77
+ // Client request stream error
78
+ // Abort the proxy request
79
+ proxyRequest.destroy();
80
+ });
81
+
82
+ res.on("error", () => {
83
+ // Client response stream error (client disconnected)
84
+ // Clean up proxy streams
85
+ proxyRequest.destroy();
86
+ });
87
+
88
+ res.on("close", () => {
89
+ // Client disconnected
90
+ // Abort the proxy request to avoid unnecessary work
91
+ proxyRequest.destroy();
92
+ });
93
+
94
+ req.pipe(proxyRequest);
95
+ }
@@ -0,0 +1,184 @@
1
+ import * as http from "http";
2
+ import { tunnelRequest } from "./tunnelRequestHandler.js";
3
+ import { mitmConnect } from "./mitmRequestHandler.js";
4
+ import { handleHttpProxyRequest } from "./plainHttpProxy.js";
5
+ import { getCaCertPath } from "./certUtils.js";
6
+ import { ui } from "../environment/userInteraction.js";
7
+ import chalk from "chalk";
8
+ import { createInterceptorForUrl } from "./interceptors/createInterceptorForEcoSystem.js";
9
+ import { getHasSuppressedVersions } from "./interceptors/npm/modifyNpmInfo.js";
10
+
11
+ const SERVER_STOP_TIMEOUT_MS = 1000;
12
+ /**
13
+ * @type {{port: number | null, blockedRequests: {packageName: string, version: string, url: string}[]}}
14
+ */
15
+ const state = {
16
+ port: null,
17
+ blockedRequests: [],
18
+ };
19
+
20
+ export function createSafeChainProxy() {
21
+ const server = createProxyServer();
22
+
23
+ return {
24
+ startServer: () => startServer(server),
25
+ stopServer: () => stopServer(server),
26
+ verifyNoMaliciousPackages,
27
+ hasSuppressedVersions: getHasSuppressedVersions,
28
+ };
29
+ }
30
+
31
+ /**
32
+ * @returns {Record<string, string>}
33
+ */
34
+ function getSafeChainProxyEnvironmentVariables() {
35
+ if (!state.port) {
36
+ return {};
37
+ }
38
+
39
+ return {
40
+ HTTPS_PROXY: `http://localhost:${state.port}`,
41
+ GLOBAL_AGENT_HTTP_PROXY: `http://localhost:${state.port}`,
42
+ NODE_EXTRA_CA_CERTS: getCaCertPath(),
43
+ };
44
+ }
45
+
46
+ /**
47
+ * @param {Record<string, string | undefined>} env
48
+ *
49
+ * @returns {Record<string, string>}
50
+ */
51
+ export function mergeSafeChainProxyEnvironmentVariables(env) {
52
+ const proxyEnv = getSafeChainProxyEnvironmentVariables();
53
+
54
+ for (const key of Object.keys(env)) {
55
+ // If we were to simply copy all env variables, we might overwrite
56
+ // the proxy settings set by safe-chain when casing varies (e.g. http_proxy vs HTTP_PROXY)
57
+ // So we only copy the variable if it's not already set in a different case
58
+ const upperKey = key.toUpperCase();
59
+
60
+ if (!proxyEnv[upperKey] && env[key]) {
61
+ proxyEnv[key] = env[key];
62
+ }
63
+ }
64
+
65
+ return proxyEnv;
66
+ }
67
+
68
+ function createProxyServer() {
69
+ const server = http.createServer(
70
+ // This handles direct HTTP requests (non-CONNECT requests)
71
+ // This is normally http-only traffic, but we also handle
72
+ // https for clients that don't properly use CONNECT
73
+ handleHttpProxyRequest
74
+ );
75
+
76
+ // This handles HTTPS requests via the CONNECT method
77
+ server.on("connect", handleConnect);
78
+
79
+ return server;
80
+ }
81
+
82
+ /**
83
+ * @param {import("http").Server} server
84
+ *
85
+ * @returns {Promise<void>}
86
+ */
87
+ function startServer(server) {
88
+ return new Promise((resolve, reject) => {
89
+ // Passing port 0 makes the OS assign an available port
90
+ server.listen(0, () => {
91
+ const address = server.address();
92
+ if (address && typeof address === "object") {
93
+ state.port = address.port;
94
+ resolve();
95
+ } else {
96
+ reject(new Error("Failed to start proxy server"));
97
+ }
98
+ });
99
+
100
+ server.on("error", (err) => {
101
+ reject(err);
102
+ });
103
+ });
104
+ }
105
+
106
+ /**
107
+ * @param {import("http").Server} server
108
+ *
109
+ * @returns {Promise<void>}
110
+ */
111
+ function stopServer(server) {
112
+ return new Promise((resolve) => {
113
+ try {
114
+ server.close(() => {
115
+ resolve();
116
+ });
117
+ } catch {
118
+ resolve();
119
+ }
120
+ setTimeout(() => resolve(), SERVER_STOP_TIMEOUT_MS);
121
+ });
122
+ }
123
+
124
+ /**
125
+ * @param {import("http").IncomingMessage} req
126
+ * @param {import("http").ServerResponse} clientSocket
127
+ * @param {Buffer} head
128
+ *
129
+ * @returns {void}
130
+ */
131
+ function handleConnect(req, clientSocket, head) {
132
+ // CONNECT method is used for HTTPS requests
133
+ // It establishes a tunnel to the server identified by the request URL
134
+
135
+ const interceptor = createInterceptorForUrl(req.url || "");
136
+
137
+ if (interceptor) {
138
+ // Subscribe to malware blocked events
139
+ interceptor.on("malwareBlocked", (event) => {
140
+ onMalwareBlocked(event.packageName, event.version, event.url);
141
+ });
142
+
143
+ mitmConnect(req, clientSocket, interceptor);
144
+ } else {
145
+ // For other hosts, just tunnel the request to the destination tcp socket
146
+ ui.writeVerbose(`Safe-chain: Tunneling request to ${req.url}`);
147
+ tunnelRequest(req, clientSocket, head);
148
+ }
149
+ }
150
+
151
+ /**
152
+ *
153
+ * @param {string} packageName
154
+ * @param {string} version
155
+ * @param {string} url
156
+ */
157
+ function onMalwareBlocked(packageName, version, url) {
158
+ state.blockedRequests.push({ packageName, version, url });
159
+ }
160
+
161
+ function verifyNoMaliciousPackages() {
162
+ if (state.blockedRequests.length === 0) {
163
+ // No malicious packages were blocked, so nothing to block
164
+ return true;
165
+ }
166
+
167
+ ui.emptyLine();
168
+
169
+ ui.writeInformation(
170
+ `Safe-chain: ${chalk.bold(
171
+ `blocked ${state.blockedRequests.length} malicious package downloads`
172
+ )}:`
173
+ );
174
+
175
+ for (const req of state.blockedRequests) {
176
+ ui.writeInformation(` - ${req.packageName}@${req.version} (${req.url})`);
177
+ }
178
+
179
+ ui.emptyLine();
180
+ ui.writeExitWithoutInstallingMaliciousPackages();
181
+ ui.emptyLine();
182
+
183
+ return false;
184
+ }
@@ -0,0 +1,180 @@
1
+ import * as net from "net";
2
+ import { ui } from "../environment/userInteraction.js";
3
+
4
+ /** @type {string[]} */
5
+ let timedoutEndpoints = [];
6
+
7
+ /**
8
+ * @param {import("http").IncomingMessage} req
9
+ * @param {import("http").ServerResponse} clientSocket
10
+ * @param {Buffer} head
11
+ *
12
+ * @returns {void}
13
+ */
14
+ export function tunnelRequest(req, clientSocket, head) {
15
+ const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy;
16
+
17
+ if (httpsProxy) {
18
+ // If an HTTPS proxy is set, tunnel the request via the proxy
19
+ // This is the system proxy, not the safe-chain proxy
20
+ // The package manager will run via the safe-chain proxy
21
+ // The safe-chain proxy will then send the request to the system proxy
22
+ // Typical flow: package manager -> safe-chain proxy -> system proxy -> destination
23
+
24
+ // There are 2 processes involved in this:
25
+ // 1. Safe-chain process: has HTTPS_PROXY set to system proxy
26
+ // 2. Package manager process: has HTTPS_PROXY set to safe-chain proxy
27
+
28
+ tunnelRequestViaProxy(req, clientSocket, head, httpsProxy);
29
+ } else {
30
+ tunnelRequestToDestination(req, clientSocket, head);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * @param {import("http").IncomingMessage} req
36
+ * @param {import("http").ServerResponse} clientSocket
37
+ * @param {Buffer} head
38
+ *
39
+ * @returns {void}
40
+ */
41
+ function tunnelRequestToDestination(req, clientSocket, head) {
42
+ const { port, hostname } = new URL(`http://${req.url}`);
43
+
44
+ if (timedoutEndpoints.includes(hostname)) {
45
+ clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
46
+ ui.writeError(
47
+ `Safe-chain: Closing connection because previously timedout connect to ${hostname}`
48
+ );
49
+ return;
50
+ }
51
+
52
+ const serverSocket = net.connect(
53
+ Number.parseInt(port) || 443,
54
+ hostname,
55
+ () => {
56
+ clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
57
+ serverSocket.write(head);
58
+ serverSocket.pipe(clientSocket);
59
+ clientSocket.pipe(serverSocket);
60
+ }
61
+ );
62
+
63
+ const connectTimeout = getConnectTimeout(hostname);
64
+ serverSocket.setTimeout(connectTimeout);
65
+ serverSocket.on("timeout", () => {
66
+ timedoutEndpoints.push(hostname);
67
+ ui.writeError(
68
+ `Safe-chain: connect to ${hostname}:${port} timed out after ${connectTimeout}ms`
69
+ );
70
+ clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
71
+ });
72
+
73
+ clientSocket.on("error", () => {
74
+ // This can happen if the client TCP socket sends RST instead of FIN.
75
+ // Not subscribing to 'error' event will cause node to throw and crash.
76
+ if (serverSocket.writable) {
77
+ serverSocket.end();
78
+ }
79
+ });
80
+
81
+ serverSocket.on("error", (err) => {
82
+ ui.writeError(
83
+ `Safe-chain: error connecting to ${hostname}:${port} - ${err.message}`
84
+ );
85
+ if (clientSocket.writable) {
86
+ clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
87
+ }
88
+ });
89
+ }
90
+
91
+ /**
92
+ * @param {import("http").IncomingMessage} req
93
+ * @param {import("http").ServerResponse} clientSocket
94
+ * @param {Buffer} head
95
+ * @param {string} proxyUrl
96
+ */
97
+ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) {
98
+ const { port, hostname } = new URL(`http://${req.url}`);
99
+ const proxy = new URL(proxyUrl);
100
+
101
+ // Connect to proxy server
102
+ const proxySocket = net.connect({
103
+ host: proxy.hostname,
104
+ port: Number.parseInt(proxy.port) || 80,
105
+ });
106
+
107
+ proxySocket.on("connect", () => {
108
+ // Send CONNECT request to proxy
109
+ const connectRequest = [
110
+ `CONNECT ${hostname}:${port || 443} HTTP/1.1`,
111
+ `Host: ${hostname}:${port || 443}`,
112
+ "",
113
+ "",
114
+ ].join("\r\n");
115
+
116
+ proxySocket.write(connectRequest);
117
+ });
118
+
119
+ let isConnected = false;
120
+ proxySocket.once("data", (data) => {
121
+ const response = data.toString();
122
+
123
+ // Check if CONNECT succeeded (HTTP/1.1 200)
124
+ if (response.startsWith("HTTP/1.1 200")) {
125
+ isConnected = true;
126
+ clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
127
+ proxySocket.write(head);
128
+ proxySocket.pipe(clientSocket);
129
+ clientSocket.pipe(proxySocket);
130
+ } else {
131
+ ui.writeError(
132
+ `Safe-chain: proxy CONNECT failed: ${response.split("\r\n")[0]}`
133
+ );
134
+ if (clientSocket.writable) {
135
+ clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
136
+ }
137
+ if (proxySocket.writable) {
138
+ proxySocket.end();
139
+ }
140
+ }
141
+ });
142
+
143
+ proxySocket.on("error", (err) => {
144
+ if (!isConnected) {
145
+ ui.writeError(
146
+ `Safe-chain: error connecting to proxy ${proxy.hostname}:${
147
+ proxy.port || 8080
148
+ } - ${err.message}`
149
+ );
150
+ if (clientSocket.writable) {
151
+ clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
152
+ }
153
+ } else {
154
+ ui.writeError(
155
+ `Safe-chain: proxy socket error after connection - ${err.message}`
156
+ );
157
+ if (clientSocket.writable) {
158
+ clientSocket.end();
159
+ }
160
+ }
161
+ });
162
+
163
+ clientSocket.on("error", () => {
164
+ if (proxySocket.writable) {
165
+ proxySocket.end();
166
+ }
167
+ });
168
+ }
169
+
170
+ const imdsEndpoints = [
171
+ "metadata.google.internal",
172
+ "metadata.goog",
173
+ "169.254.169.254",
174
+ ];
175
+ function getConnectTimeout(/** @type {string} */ host) {
176
+ if (imdsEndpoints.includes(host)) {
177
+ return 3000;
178
+ }
179
+ return 30000;
180
+ }