@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.
- package/LICENSE +674 -0
- package/README.md +257 -0
- package/bin/aikido-bun.js +14 -0
- package/bin/aikido-bunx.js +14 -0
- package/bin/aikido-npm.js +14 -0
- package/bin/aikido-npx.js +14 -0
- package/bin/aikido-pip.js +20 -0
- package/bin/aikido-pip3.js +21 -0
- package/bin/aikido-pnpm.js +14 -0
- package/bin/aikido-pnpx.js +14 -0
- package/bin/aikido-python.js +30 -0
- package/bin/aikido-python3.js +30 -0
- package/bin/aikido-uv.js +16 -0
- package/bin/aikido-yarn.js +14 -0
- package/bin/safe-chain.js +190 -0
- package/docs/banner.svg +151 -0
- package/docs/npm-to-binary-migration.md +89 -0
- package/docs/safe-package-manager-demo.gif +0 -0
- package/docs/safe-package-manager-demo.png +0 -0
- package/docs/shell-integration.md +149 -0
- package/package.json +68 -0
- package/src/api/aikido.js +54 -0
- package/src/api/npmApi.js +71 -0
- package/src/config/cliArguments.js +138 -0
- package/src/config/configFile.js +192 -0
- package/src/config/environmentVariables.js +7 -0
- package/src/config/settings.js +100 -0
- package/src/environment/environment.js +14 -0
- package/src/environment/userInteraction.js +122 -0
- package/src/main.js +104 -0
- package/src/packagemanager/_shared/matchesCommand.js +18 -0
- package/src/packagemanager/bun/createBunPackageManager.js +53 -0
- package/src/packagemanager/currentPackageManager.js +72 -0
- package/src/packagemanager/npm/createPackageManager.js +72 -0
- package/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js +74 -0
- package/src/packagemanager/npm/dependencyScanner/nullScanner.js +9 -0
- package/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js +144 -0
- package/src/packagemanager/npm/runNpmCommand.js +25 -0
- package/src/packagemanager/npm/utils/abbrevs-generated.js +359 -0
- package/src/packagemanager/npm/utils/cmd-list.js +174 -0
- package/src/packagemanager/npm/utils/npmCommands.js +34 -0
- package/src/packagemanager/npx/createPackageManager.js +15 -0
- package/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js +43 -0
- package/src/packagemanager/npx/parsing/parsePackagesFromArguments.js +130 -0
- package/src/packagemanager/npx/runNpxCommand.js +25 -0
- package/src/packagemanager/pip/createPackageManager.js +21 -0
- package/src/packagemanager/pip/pipSettings.js +30 -0
- package/src/packagemanager/pip/runPipCommand.js +175 -0
- package/src/packagemanager/pnpm/createPackageManager.js +57 -0
- package/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js +35 -0
- package/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js +109 -0
- package/src/packagemanager/pnpm/runPnpmCommand.js +36 -0
- package/src/packagemanager/uv/createUvPackageManager.js +18 -0
- package/src/packagemanager/uv/runUvCommand.js +71 -0
- package/src/packagemanager/yarn/createPackageManager.js +41 -0
- package/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js +35 -0
- package/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js +128 -0
- package/src/packagemanager/yarn/runYarnCommand.js +41 -0
- package/src/registryProxy/certBundle.js +95 -0
- package/src/registryProxy/certUtils.js +128 -0
- package/src/registryProxy/http-utils.js +17 -0
- package/src/registryProxy/interceptors/createInterceptorForEcoSystem.js +25 -0
- package/src/registryProxy/interceptors/interceptorBuilder.js +140 -0
- package/src/registryProxy/interceptors/npm/modifyNpmInfo.js +177 -0
- package/src/registryProxy/interceptors/npm/npmInterceptor.js +47 -0
- package/src/registryProxy/interceptors/npm/parseNpmPackageUrl.js +43 -0
- package/src/registryProxy/interceptors/pipInterceptor.js +115 -0
- package/src/registryProxy/mitmRequestHandler.js +231 -0
- package/src/registryProxy/plainHttpProxy.js +95 -0
- package/src/registryProxy/registryProxy.js +184 -0
- package/src/registryProxy/tunnelRequestHandler.js +180 -0
- package/src/scanning/audit/index.js +129 -0
- package/src/scanning/index.js +82 -0
- package/src/scanning/malwareDatabase.js +131 -0
- package/src/shell-integration/helpers.js +213 -0
- package/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +22 -0
- package/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +24 -0
- package/src/shell-integration/setup-ci.js +170 -0
- package/src/shell-integration/setup.js +127 -0
- package/src/shell-integration/shellDetection.js +37 -0
- package/src/shell-integration/startup-scripts/include-python/init-fish.fish +94 -0
- package/src/shell-integration/startup-scripts/include-python/init-posix.sh +81 -0
- package/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 +115 -0
- package/src/shell-integration/startup-scripts/init-fish.fish +71 -0
- package/src/shell-integration/startup-scripts/init-posix.sh +58 -0
- package/src/shell-integration/startup-scripts/init-pwsh.ps1 +92 -0
- package/src/shell-integration/supported-shells/bash.js +134 -0
- package/src/shell-integration/supported-shells/fish.js +77 -0
- package/src/shell-integration/supported-shells/powershell.js +73 -0
- package/src/shell-integration/supported-shells/windowsPowershell.js +73 -0
- package/src/shell-integration/supported-shells/zsh.js +74 -0
- package/src/shell-integration/teardown.js +64 -0
- package/src/utils/safeSpawn.js +137 -0
- 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
|
+
}
|