maven-proxy 1.0.1
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/README.md +420 -0
- package/bin/maven-proxy.js +573 -0
- package/package.json +54 -0
- package/scripts/truststore.js +96 -0
- package/src/cache/cache-path.js +50 -0
- package/src/cache/downloader.js +350 -0
- package/src/cert/cert-manager.js +194 -0
- package/src/cert/truststore-utils.js +289 -0
- package/src/common/console-log-file.js +62 -0
- package/src/common/daily-log-file.js +79 -0
- package/src/common/domain-match.js +39 -0
- package/src/common/download-log-writer.js +27 -0
- package/src/common/ecosystem.js +64 -0
- package/src/common/java-home.js +328 -0
- package/src/config/config.js +213 -0
- package/src/index.js +93 -0
- package/src/proxy/proxy-connect-handler.js +173 -0
- package/src/proxy/proxy-http-handler.js +187 -0
- package/src/proxy/proxy-server.js +35 -0
- package/src/proxy/upstream-proxy.js +236 -0
- package/src/repo/repo-server.js +120 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import tls from "node:tls";
|
|
3
|
+
|
|
4
|
+
function parseConnectTarget(rawUrl) {
|
|
5
|
+
const [host, portText] = String(rawUrl || "").split(":");
|
|
6
|
+
const port = Number.parseInt(portText || "443", 10);
|
|
7
|
+
return {
|
|
8
|
+
host,
|
|
9
|
+
port: Number.isFinite(port) ? port : 443,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function writeTunnelResponse(socket, statusLine, callback) {
|
|
14
|
+
socket.write(`${statusLine}\r\n\r\n`, callback);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function openConnectUpstreamSocket(targetHost, targetPort, timeoutMs, upstreamProxyManager = null) {
|
|
18
|
+
const useUpstreamProxy =
|
|
19
|
+
upstreamProxyManager &&
|
|
20
|
+
upstreamProxyManager.hasProxyFor("https:", targetHost);
|
|
21
|
+
|
|
22
|
+
if (useUpstreamProxy) {
|
|
23
|
+
console.log(`[proxy] CONNECT via upstream target=${targetHost}:${targetPort}`);
|
|
24
|
+
const tunnel = await upstreamProxyManager.createConnectTunnel(targetHost, targetPort, timeoutMs);
|
|
25
|
+
return {
|
|
26
|
+
upstreamSocket: tunnel.socket,
|
|
27
|
+
bufferedData: tunnel.bufferedData || Buffer.alloc(0),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const upstreamSocket = await new Promise((resolve, reject) => {
|
|
32
|
+
const socket = net.connect(targetPort, targetHost, () => resolve(socket));
|
|
33
|
+
socket.once("error", reject);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
upstreamSocket,
|
|
38
|
+
bufferedData: Buffer.alloc(0),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function handlePassThroughConnect(clientSocket, head, targetHost, targetPort, timeoutMs, upstreamProxyManager = null) {
|
|
43
|
+
const { upstreamSocket, bufferedData } = await openConnectUpstreamSocket(
|
|
44
|
+
targetHost,
|
|
45
|
+
targetPort,
|
|
46
|
+
timeoutMs,
|
|
47
|
+
upstreamProxyManager,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
await new Promise((resolve, reject) => {
|
|
51
|
+
writeTunnelResponse(clientSocket, "HTTP/1.1 200 Connection Established", (error) => {
|
|
52
|
+
if (error) {
|
|
53
|
+
reject(error);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
resolve();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (head && head.length > 0) {
|
|
61
|
+
upstreamSocket.write(head);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (bufferedData.length > 0) {
|
|
65
|
+
clientSocket.write(bufferedData);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
upstreamSocket.pipe(clientSocket);
|
|
69
|
+
clientSocket.pipe(upstreamSocket);
|
|
70
|
+
|
|
71
|
+
upstreamSocket.on("error", (error) => {
|
|
72
|
+
if (!clientSocket.destroyed) {
|
|
73
|
+
writeTunnelResponse(clientSocket, "HTTP/1.1 502 Bad Gateway");
|
|
74
|
+
clientSocket.destroy(error);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
clientSocket.on("error", () => {
|
|
79
|
+
upstreamSocket.destroy();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function handleMitmConnect(clientSocket, head, targetHost, certManager, mitmHttpServer) {
|
|
84
|
+
console.log(`[proxy] MITM prepare ${targetHost}`);
|
|
85
|
+
const leaf = await certManager.getOrCreateLeaf(targetHost);
|
|
86
|
+
console.log(`[proxy] MITM cert ready ${targetHost}`);
|
|
87
|
+
|
|
88
|
+
await new Promise((resolve, reject) => {
|
|
89
|
+
writeTunnelResponse(clientSocket, "HTTP/1.1 200 Connection Established", (error) => {
|
|
90
|
+
if (error) {
|
|
91
|
+
reject(error);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
resolve();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
console.log(`[proxy] MITM tunnel established ${targetHost}`);
|
|
98
|
+
|
|
99
|
+
const tlsSocket = new tls.TLSSocket(clientSocket, {
|
|
100
|
+
isServer: true,
|
|
101
|
+
secureContext: tls.createSecureContext({
|
|
102
|
+
key: leaf.keyPem,
|
|
103
|
+
cert: leaf.certPem,
|
|
104
|
+
}),
|
|
105
|
+
ALPNProtocols: ["http/1.1"],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
tlsSocket.__mitmHost = targetHost;
|
|
109
|
+
|
|
110
|
+
if (head && head.length > 0) {
|
|
111
|
+
tlsSocket.unshift(head);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
tlsSocket.on("error", () => {
|
|
115
|
+
tlsSocket.destroy();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
mitmHttpServer.emit("connection", tlsSocket);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function attachConnectHandler(server, {
|
|
122
|
+
config,
|
|
123
|
+
certManager,
|
|
124
|
+
matchesDomain,
|
|
125
|
+
upstreamProxyManager = null,
|
|
126
|
+
mitmHttpServer,
|
|
127
|
+
}) {
|
|
128
|
+
server.on("connect", (req, clientSocket, head) => {
|
|
129
|
+
const { host, port } = parseConnectTarget(req.url);
|
|
130
|
+
|
|
131
|
+
if (!host) {
|
|
132
|
+
writeTunnelResponse(clientSocket, "HTTP/1.1 400 Bad Request");
|
|
133
|
+
clientSocket.destroy();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const mitmEnabled =
|
|
138
|
+
config.enableHttpsProxy &&
|
|
139
|
+
matchesDomain(host, config.httpsMitmDomains);
|
|
140
|
+
|
|
141
|
+
console.log(`[proxy] CONNECT ${host}:${port} mitm=${mitmEnabled}`);
|
|
142
|
+
|
|
143
|
+
if (!mitmEnabled) {
|
|
144
|
+
if (!config.httpsPassthroughForUnmatched) {
|
|
145
|
+
writeTunnelResponse(clientSocket, "HTTP/1.1 403 Forbidden");
|
|
146
|
+
clientSocket.destroy();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
handlePassThroughConnect(
|
|
151
|
+
clientSocket,
|
|
152
|
+
head,
|
|
153
|
+
host,
|
|
154
|
+
port,
|
|
155
|
+
config.downloadTimeoutMs,
|
|
156
|
+
upstreamProxyManager,
|
|
157
|
+
).catch((error) => {
|
|
158
|
+
if (!clientSocket.destroyed) {
|
|
159
|
+
writeTunnelResponse(clientSocket, "HTTP/1.1 502 Bad Gateway");
|
|
160
|
+
clientSocket.destroy(error);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
handleMitmConnect(clientSocket, head, host, certManager, mitmHttpServer).catch((error) => {
|
|
167
|
+
if (!clientSocket.destroyed) {
|
|
168
|
+
writeTunnelResponse(clientSocket, "HTTP/1.1 502 Bad Gateway");
|
|
169
|
+
}
|
|
170
|
+
clientSocket.destroy(error);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import https from "node:https";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { getCacheFilePath } from "../cache/cache-path.js";
|
|
6
|
+
import { detectPackageEcosystem } from "../common/ecosystem.js";
|
|
7
|
+
|
|
8
|
+
function pickClient(protocol) {
|
|
9
|
+
return protocol === "https:" ? https : http;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function sanitizeHeaders(headers = {}) {
|
|
13
|
+
const result = { ...headers };
|
|
14
|
+
const blocked = [
|
|
15
|
+
"proxy-connection",
|
|
16
|
+
"proxy-authorization",
|
|
17
|
+
"proxy-authenticate",
|
|
18
|
+
"connection",
|
|
19
|
+
"keep-alive",
|
|
20
|
+
"transfer-encoding",
|
|
21
|
+
"upgrade",
|
|
22
|
+
"te",
|
|
23
|
+
"trailer",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
for (const key of blocked) {
|
|
27
|
+
delete result[key];
|
|
28
|
+
delete result[key.toLowerCase()];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function statIfFile(filePath) {
|
|
35
|
+
try {
|
|
36
|
+
const stats = await fs.promises.stat(filePath);
|
|
37
|
+
return stats.isFile() ? stats : null;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error.code === "ENOENT") {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function sendText(res, statusCode, message) {
|
|
47
|
+
res.writeHead(statusCode, { "content-type": "text/plain; charset=utf-8" });
|
|
48
|
+
res.end(message);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildUrl(req, forcedProtocol = null) {
|
|
52
|
+
const raw = req.url || "/";
|
|
53
|
+
if (/^https?:\/\//i.test(raw)) {
|
|
54
|
+
return new URL(raw);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const host = req.headers.host || req.socket.__mitmHost;
|
|
58
|
+
if (!host) {
|
|
59
|
+
throw new Error("Missing host header");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const protocol = forcedProtocol || "http:";
|
|
63
|
+
return new URL(`${protocol}//${host}${raw}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function serveFile(res, req, filePath) {
|
|
67
|
+
const stats = await statIfFile(filePath);
|
|
68
|
+
if (!stats) {
|
|
69
|
+
sendText(res, 404, "Not Found");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
res.setHeader("content-length", String(stats.size));
|
|
74
|
+
if (!res.hasHeader("x-cache")) {
|
|
75
|
+
res.setHeader("x-cache", "HIT");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (req.method === "HEAD") {
|
|
79
|
+
res.writeHead(200);
|
|
80
|
+
res.end();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
res.writeHead(200);
|
|
85
|
+
fs.createReadStream(filePath).pipe(res);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function forwardDirectRequest(req, res, urlObj, timeoutMs, upstreamProxyManager = null) {
|
|
89
|
+
const client = pickClient(urlObj.protocol);
|
|
90
|
+
const headers = sanitizeHeaders(req.headers);
|
|
91
|
+
headers.host = urlObj.host;
|
|
92
|
+
const agent = upstreamProxyManager ? upstreamProxyManager.getAgentForUrl(urlObj) : undefined;
|
|
93
|
+
|
|
94
|
+
if (agent) {
|
|
95
|
+
console.log(`[proxy] direct forward via upstream host=${urlObj.hostname} protocol=${urlObj.protocol}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const upstreamReq = client.request(
|
|
99
|
+
{
|
|
100
|
+
protocol: urlObj.protocol,
|
|
101
|
+
hostname: urlObj.hostname,
|
|
102
|
+
port: urlObj.port || (urlObj.protocol === "https:" ? 443 : 80),
|
|
103
|
+
method: req.method,
|
|
104
|
+
path: `${urlObj.pathname}${urlObj.search}`,
|
|
105
|
+
headers,
|
|
106
|
+
agent,
|
|
107
|
+
},
|
|
108
|
+
(upstreamRes) => {
|
|
109
|
+
res.writeHead(upstreamRes.statusCode || 502, upstreamRes.headers);
|
|
110
|
+
upstreamRes.pipe(res);
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
upstreamReq.setTimeout(timeoutMs, () => {
|
|
115
|
+
upstreamReq.destroy(new Error(`Upstream timeout after ${timeoutMs}ms`));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
upstreamReq.on("error", (error) => {
|
|
119
|
+
if (!res.headersSent) {
|
|
120
|
+
sendText(res, 502, `Proxy forward failed: ${error.message}`);
|
|
121
|
+
} else {
|
|
122
|
+
res.destroy(error);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
req.pipe(upstreamReq);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function createHttpRequestHandler({ config, downloader, upstreamProxyManager = null, matchesDomain }) {
|
|
130
|
+
return async function handleHttpRequestPath(req, res, forcedProtocol = null) {
|
|
131
|
+
let urlObj;
|
|
132
|
+
try {
|
|
133
|
+
urlObj = buildUrl(req, forcedProtocol);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
sendText(res, 400, `Bad request: ${error.message}`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const method = (req.method || "GET").toUpperCase();
|
|
140
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
141
|
+
forwardDirectRequest(req, res, urlObj, config.downloadTimeoutMs, upstreamProxyManager);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let cachePath;
|
|
146
|
+
try {
|
|
147
|
+
const ecosystem = detectPackageEcosystem(urlObj, config, matchesDomain);
|
|
148
|
+
cachePath = getCacheFilePath(config.cacheDir, urlObj, {
|
|
149
|
+
ecosystem,
|
|
150
|
+
includeHost: ecosystem !== "maven",
|
|
151
|
+
});
|
|
152
|
+
} catch (error) {
|
|
153
|
+
sendText(res, 400, `Invalid cache path: ${error.message}`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const existing = await statIfFile(cachePath);
|
|
158
|
+
if (existing) {
|
|
159
|
+
await serveFile(res, req, cachePath);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
await fs.promises.mkdir(path.dirname(cachePath), { recursive: true });
|
|
165
|
+
await downloader.ensureCached(urlObj, cachePath, req.headers);
|
|
166
|
+
res.setHeader("x-cache", "MISS");
|
|
167
|
+
await serveFile(res, req, cachePath);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
const statusCode = error.statusCode || 502;
|
|
170
|
+
sendText(res, statusCode, `Download failed: ${error.message}`);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function createMitmHttpServer(handleHttpRequestPath) {
|
|
176
|
+
const server = http.createServer((req, res) => {
|
|
177
|
+
handleHttpRequestPath(req, res, "https:").catch((error) => {
|
|
178
|
+
sendText(res, 500, `MITM request failed: ${error.message}`);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
server.on("clientError", (error, socket) => {
|
|
183
|
+
socket.destroy(error);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return server;
|
|
187
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import { createHttpRequestHandler, createMitmHttpServer } from "./proxy-http-handler.js";
|
|
3
|
+
import { attachConnectHandler } from "./proxy-connect-handler.js";
|
|
4
|
+
|
|
5
|
+
function sendText(res, statusCode, message) {
|
|
6
|
+
res.writeHead(statusCode, { "content-type": "text/plain; charset=utf-8" });
|
|
7
|
+
res.end(message);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function startProxyServer(config, certManager, downloader, matchesDomain, upstreamProxyManager = null) {
|
|
11
|
+
const handleHttpRequestPath = createHttpRequestHandler({
|
|
12
|
+
config,
|
|
13
|
+
downloader,
|
|
14
|
+
upstreamProxyManager,
|
|
15
|
+
matchesDomain,
|
|
16
|
+
});
|
|
17
|
+
const mitmHttpServer = createMitmHttpServer(handleHttpRequestPath);
|
|
18
|
+
|
|
19
|
+
const server = http.createServer((req, res) => {
|
|
20
|
+
handleHttpRequestPath(req, res, null).catch((error) => {
|
|
21
|
+
sendText(res, 500, `Proxy request failed: ${error.message}`);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
attachConnectHandler(server, {
|
|
26
|
+
config,
|
|
27
|
+
certManager,
|
|
28
|
+
matchesDomain,
|
|
29
|
+
upstreamProxyManager,
|
|
30
|
+
mitmHttpServer,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
server.listen(config.proxyPort);
|
|
34
|
+
return { proxyServer: server, mitmHttpServer };
|
|
35
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import tls from "node:tls";
|
|
3
|
+
import { ProxyAgent } from "proxy-agent";
|
|
4
|
+
|
|
5
|
+
function normalizeHostname(hostname) {
|
|
6
|
+
return String(hostname || "")
|
|
7
|
+
.trim()
|
|
8
|
+
.replace(/^\[/, "")
|
|
9
|
+
.replace(/\]$/, "")
|
|
10
|
+
.toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function safeDecode(value) {
|
|
14
|
+
try {
|
|
15
|
+
return decodeURIComponent(value);
|
|
16
|
+
} catch {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function buildProxyAuthHeader(proxyUrl) {
|
|
22
|
+
if (!proxyUrl.username && !proxyUrl.password) {
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const username = safeDecode(proxyUrl.username || "");
|
|
27
|
+
const password = safeDecode(proxyUrl.password || "");
|
|
28
|
+
const token = Buffer.from(`${username}:${password}`).toString("base64");
|
|
29
|
+
return `Basic ${token}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseStatusCode(responseHeader) {
|
|
33
|
+
const statusLine = responseHeader.split("\r\n")[0] || "";
|
|
34
|
+
const match = statusLine.match(/^HTTP\/\d\.\d\s+(\d{3})/i);
|
|
35
|
+
const code = match ? Number.parseInt(match[1], 10) : 0;
|
|
36
|
+
return { code, statusLine };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createSocketToProxy(proxyUrl, timeoutMs) {
|
|
40
|
+
const port = proxyUrl.port
|
|
41
|
+
? Number.parseInt(proxyUrl.port, 10)
|
|
42
|
+
: proxyUrl.protocol === "https:"
|
|
43
|
+
? 443
|
|
44
|
+
: 80;
|
|
45
|
+
|
|
46
|
+
if (proxyUrl.protocol === "https:") {
|
|
47
|
+
return tls.connect({
|
|
48
|
+
host: proxyUrl.hostname,
|
|
49
|
+
port,
|
|
50
|
+
servername: proxyUrl.hostname,
|
|
51
|
+
timeout: timeoutMs,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return net.connect({
|
|
56
|
+
host: proxyUrl.hostname,
|
|
57
|
+
port,
|
|
58
|
+
timeout: timeoutMs,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class UpstreamProxyManager {
|
|
63
|
+
constructor(config, matchesDomain) {
|
|
64
|
+
this.config = config;
|
|
65
|
+
this.matchesDomain = matchesDomain;
|
|
66
|
+
this.agentCache = new Map();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
shouldBypass(hostname) {
|
|
70
|
+
const host = normalizeHostname(hostname);
|
|
71
|
+
if (!host) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const patterns = [
|
|
76
|
+
...(this.config.upstreamNoProxyDomains || []),
|
|
77
|
+
...(this.config.upstreamIgnoreDomains || []),
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const uniquePatterns = [...new Set(patterns.map((item) => String(item).trim()).filter(Boolean))];
|
|
81
|
+
if (uniquePatterns.length === 0) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (uniquePatterns.includes("*")) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return this.matchesDomain(host, uniquePatterns);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getProxyUrlFor(protocol, hostname) {
|
|
93
|
+
if (this.shouldBypass(hostname)) {
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (protocol === "https:") {
|
|
98
|
+
return (
|
|
99
|
+
this.config.upstreamHttpsProxyUrl ||
|
|
100
|
+
this.config.upstreamProxyUrl ||
|
|
101
|
+
this.config.upstreamHttpProxyUrl ||
|
|
102
|
+
""
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
this.config.upstreamHttpProxyUrl ||
|
|
108
|
+
this.config.upstreamProxyUrl ||
|
|
109
|
+
""
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
getAgentForUrl(urlObj) {
|
|
114
|
+
const proxyUrl = this.getProxyUrlFor(urlObj.protocol, urlObj.hostname);
|
|
115
|
+
if (!proxyUrl) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const cacheKey = `${proxyUrl}`;
|
|
120
|
+
if (!this.agentCache.has(cacheKey)) {
|
|
121
|
+
this.agentCache.set(cacheKey, new ProxyAgent(proxyUrl));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return this.agentCache.get(cacheKey);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
hasProxyFor(protocol, hostname) {
|
|
128
|
+
return Boolean(this.getProxyUrlFor(protocol, hostname));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async createConnectTunnel(targetHost, targetPort, timeoutMs) {
|
|
132
|
+
const proxyUrlText = this.getProxyUrlFor("https:", targetHost);
|
|
133
|
+
if (!proxyUrlText) {
|
|
134
|
+
throw new Error("Upstream proxy is not configured for CONNECT");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const proxyUrl = new URL(proxyUrlText);
|
|
138
|
+
if (proxyUrl.protocol !== "http:" && proxyUrl.protocol !== "https:") {
|
|
139
|
+
throw new Error(`Unsupported upstream proxy protocol for CONNECT: ${proxyUrl.protocol}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const authHeader = buildProxyAuthHeader(proxyUrl);
|
|
143
|
+
|
|
144
|
+
const socket = createSocketToProxy(proxyUrl, timeoutMs);
|
|
145
|
+
const connectEvent = proxyUrl.protocol === "https:" ? "secureConnect" : "connect";
|
|
146
|
+
|
|
147
|
+
return new Promise((resolve, reject) => {
|
|
148
|
+
let settled = false;
|
|
149
|
+
let bytes = 0;
|
|
150
|
+
const chunks = [];
|
|
151
|
+
|
|
152
|
+
const cleanup = () => {
|
|
153
|
+
socket.removeListener(connectEvent, onConnectReady);
|
|
154
|
+
socket.removeListener("data", onData);
|
|
155
|
+
socket.removeListener("timeout", onTimeout);
|
|
156
|
+
socket.removeListener("error", onError);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const fail = (error) => {
|
|
160
|
+
if (settled) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
settled = true;
|
|
164
|
+
cleanup();
|
|
165
|
+
socket.destroy();
|
|
166
|
+
reject(error);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const succeed = (bufferedData) => {
|
|
170
|
+
if (settled) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
settled = true;
|
|
174
|
+
cleanup();
|
|
175
|
+
socket.setTimeout(0);
|
|
176
|
+
resolve({ socket, bufferedData });
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const onConnectReady = () => {
|
|
180
|
+
const headers = [
|
|
181
|
+
`CONNECT ${targetHost}:${targetPort} HTTP/1.1`,
|
|
182
|
+
`Host: ${targetHost}:${targetPort}`,
|
|
183
|
+
"Proxy-Connection: Keep-Alive",
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
if (authHeader) {
|
|
187
|
+
headers.push(`Proxy-Authorization: ${authHeader}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const payload = `${headers.join("\r\n")}\r\n\r\n`;
|
|
191
|
+
socket.write(payload);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const onData = (chunk) => {
|
|
195
|
+
chunks.push(chunk);
|
|
196
|
+
bytes += chunk.length;
|
|
197
|
+
|
|
198
|
+
if (bytes > 128 * 1024) {
|
|
199
|
+
fail(new Error("Upstream proxy CONNECT response is too large"));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const merged = Buffer.concat(chunks, bytes);
|
|
204
|
+
const boundary = merged.indexOf("\r\n\r\n");
|
|
205
|
+
if (boundary === -1) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const headerBuffer = merged.slice(0, boundary + 4);
|
|
210
|
+
const headerText = headerBuffer.toString("latin1");
|
|
211
|
+
const { code, statusLine } = parseStatusCode(headerText);
|
|
212
|
+
|
|
213
|
+
if (code !== 200) {
|
|
214
|
+
fail(new Error(`Upstream proxy CONNECT failed: ${statusLine || "unknown response"}`));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const rest = merged.slice(boundary + 4);
|
|
219
|
+
succeed(rest);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const onTimeout = () => {
|
|
223
|
+
fail(new Error(`Upstream proxy CONNECT timeout after ${timeoutMs}ms`));
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const onError = (error) => {
|
|
227
|
+
fail(error);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
socket.on(connectEvent, onConnectReady);
|
|
231
|
+
socket.on("data", onData);
|
|
232
|
+
socket.on("timeout", onTimeout);
|
|
233
|
+
socket.on("error", onError);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|