maven-proxy 1.0.2 → 1.1.0
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 +2 -0
- package/bin/maven-proxy.js +3 -4
- package/package.json +1 -1
- package/src/cache/cache-path.js +52 -0
- package/src/cache/downloader.js +69 -0
- package/src/proxy/proxy-http-handler.js +58 -5
- package/src/proxy/proxy-server.js +7 -1
- package/src/proxy/upstream-proxy.js +7 -1
- package/src/repo/repo-server.js +3 -1
package/README.md
CHANGED
|
@@ -433,6 +433,8 @@ org.gradle.jvmargs=-Djavax.net.ssl.trustStore=/Users/yize/projects/maven-proxy/d
|
|
|
433
433
|
./gradlew --refresh-dependencies dependencies
|
|
434
434
|
```
|
|
435
435
|
|
|
436
|
+
Note: always set `trustStorePassword` together with `trustStore`. If you use `systemProp.javax.net.ssl.trustStore`, also set `systemProp.javax.net.ssl.trustStorePassword`.
|
|
437
|
+
|
|
436
438
|
### 9.2 npm: Proxy + SSL behavior
|
|
437
439
|
|
|
438
440
|
For local troubleshooting only, you can disable strict SSL temporarily. Recommended long-term approach is to import Root CA and keep strict SSL enabled.
|
package/bin/maven-proxy.js
CHANGED
|
@@ -44,7 +44,8 @@ function resolveEffectiveMode(options) {
|
|
|
44
44
|
return forced;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
// CLI defaults to user mode to load ~/maven-proxy/config unless explicitly overridden.
|
|
48
|
+
return "user";
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
function printHelp() {
|
|
@@ -213,9 +214,7 @@ async function startServer(options) {
|
|
|
213
214
|
}
|
|
214
215
|
|
|
215
216
|
function applyConfigOverrides(options) {
|
|
216
|
-
|
|
217
|
-
process.env.MAVEN_PROXY_CONFIG_MODE = options.mode;
|
|
218
|
-
}
|
|
217
|
+
process.env.MAVEN_PROXY_CONFIG_MODE = resolveEffectiveMode(options);
|
|
219
218
|
|
|
220
219
|
if (options.configPath) {
|
|
221
220
|
process.env.MAVEN_PROXY_CONFIG_FILE = resolvePath(options.configPath);
|
package/package.json
CHANGED
package/src/cache/cache-path.js
CHANGED
|
@@ -12,6 +12,54 @@ function safeDecode(pathname) {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function looksLikeMavenVersionSegment(segment) {
|
|
16
|
+
return /^\d[0-9A-Za-z._-]*$/.test(String(segment || ""));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isLikelyMavenFilePath(parts, normalizedPath) {
|
|
20
|
+
if (normalizedPath.endsWith("/") || parts.length === 0) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const last = String(parts[parts.length - 1] || "").toLowerCase();
|
|
25
|
+
if (!last) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (last.startsWith("maven-metadata.")) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const knownSuffixes = [
|
|
34
|
+
".pom",
|
|
35
|
+
".jar",
|
|
36
|
+
".aar",
|
|
37
|
+
".war",
|
|
38
|
+
".zip",
|
|
39
|
+
".module",
|
|
40
|
+
".xml",
|
|
41
|
+
".sha1",
|
|
42
|
+
".md5",
|
|
43
|
+
".sha256",
|
|
44
|
+
".sha512",
|
|
45
|
+
".asc",
|
|
46
|
+
".json",
|
|
47
|
+
".toml",
|
|
48
|
+
".klib",
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
if (knownSuffixes.some((suffix) => last.endsWith(suffix))) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const secondLast = String(parts[parts.length - 2] || "").toLowerCase();
|
|
56
|
+
if (looksLikeMavenVersionSegment(secondLast)) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
15
63
|
export function getCacheFilePath(cacheDir, urlObj, options = {}) {
|
|
16
64
|
const ecosystem = sanitizeSegment(String(options.ecosystem || "generic").toLowerCase());
|
|
17
65
|
const includeHost = options.includeHost ?? ecosystem !== "maven";
|
|
@@ -35,6 +83,10 @@ export function getCacheFilePath(cacheDir, urlObj, options = {}) {
|
|
|
35
83
|
safeParts.unshift(sanitizeSegment(String(urlObj.hostname || "unknown").toLowerCase()));
|
|
36
84
|
}
|
|
37
85
|
|
|
86
|
+
if (ecosystem === "maven" && !isLikelyMavenFilePath(parts, normalized)) {
|
|
87
|
+
safeParts.push("__dir__.json");
|
|
88
|
+
}
|
|
89
|
+
|
|
38
90
|
const npmTarballPath = /\/-\/.+\.tgz$/i.test(lowerNormalized);
|
|
39
91
|
if (ecosystem === "npm" && !npmTarballPath) {
|
|
40
92
|
safeParts.push("__meta__.json");
|
package/src/cache/downloader.js
CHANGED
|
@@ -7,6 +7,31 @@ import { DownloadLogWriter } from "../common/download-log-writer.js";
|
|
|
7
7
|
|
|
8
8
|
const REDIRECT_STATUS = new Set([301, 302, 303, 307, 308]);
|
|
9
9
|
const MAX_REDIRECTS = 5;
|
|
10
|
+
const LOCAL_FS_ERROR_CODES = new Set([
|
|
11
|
+
"EACCES",
|
|
12
|
+
"EPERM",
|
|
13
|
+
"ENOSPC",
|
|
14
|
+
"EROFS",
|
|
15
|
+
"ENOTDIR",
|
|
16
|
+
"EISDIR",
|
|
17
|
+
"EINVAL",
|
|
18
|
+
"EMFILE",
|
|
19
|
+
"ENFILE",
|
|
20
|
+
"EEXIST",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
function isLocalFsWriteError(error) {
|
|
24
|
+
if (!error || typeof error !== "object") {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (LOCAL_FS_ERROR_CODES.has(error.code)) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const message = String(error.message || "").toLowerCase();
|
|
33
|
+
return message.includes("enotdir") || message.includes("read-only file system");
|
|
34
|
+
}
|
|
10
35
|
|
|
11
36
|
function pickClient(protocol) {
|
|
12
37
|
return protocol === "https:" ? https : http;
|
|
@@ -236,6 +261,27 @@ async function removeIfExists(filePath) {
|
|
|
236
261
|
}
|
|
237
262
|
}
|
|
238
263
|
|
|
264
|
+
function toHost(urlObj) {
|
|
265
|
+
if (!urlObj || typeof urlObj !== "object") {
|
|
266
|
+
return "";
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return String(urlObj.hostname || "");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function toBodyPreview(value, maxLength = 512) {
|
|
273
|
+
const text = String(value || "").replace(/\s+/g, " ").trim();
|
|
274
|
+
if (!text) {
|
|
275
|
+
return "";
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (text.length <= maxLength) {
|
|
279
|
+
return text;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return `${text.slice(0, maxLength)}...(truncated)`;
|
|
283
|
+
}
|
|
284
|
+
|
|
239
285
|
export class Downloader {
|
|
240
286
|
constructor(config, domainMatcher, upstreamProxyManager = null) {
|
|
241
287
|
this.config = config;
|
|
@@ -343,6 +389,29 @@ export class Downloader {
|
|
|
343
389
|
await verifyFileSize(tempPath, metadata.contentLength);
|
|
344
390
|
await fs.promises.rename(tempPath, finalPath);
|
|
345
391
|
} catch (error) {
|
|
392
|
+
if (isLocalFsWriteError(error)) {
|
|
393
|
+
if (!error.statusCode) {
|
|
394
|
+
error.statusCode = 500;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
this.logDownload("local cache write failed", urlObj, {
|
|
398
|
+
code: error.code || "UNKNOWN",
|
|
399
|
+
targetPath: finalPath,
|
|
400
|
+
tempPath,
|
|
401
|
+
message: error.message,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
this.logDownload("download failed", urlObj, {
|
|
406
|
+
host: toHost(urlObj),
|
|
407
|
+
code: error.code || "UNKNOWN",
|
|
408
|
+
statusCode: error.statusCode || 0,
|
|
409
|
+
targetPath: finalPath,
|
|
410
|
+
tempPath,
|
|
411
|
+
message: error.message,
|
|
412
|
+
upstreamBodyPreview: toBodyPreview(error.upstreamBody),
|
|
413
|
+
});
|
|
414
|
+
|
|
346
415
|
await removeIfExists(tempPath);
|
|
347
416
|
throw error;
|
|
348
417
|
}
|
|
@@ -5,6 +5,32 @@ import path from "node:path";
|
|
|
5
5
|
import { getCacheFilePath } from "../cache/cache-path.js";
|
|
6
6
|
import { detectPackageEcosystem } from "../common/ecosystem.js";
|
|
7
7
|
|
|
8
|
+
const LOCAL_FS_ERROR_CODES = new Set([
|
|
9
|
+
"EACCES",
|
|
10
|
+
"EPERM",
|
|
11
|
+
"ENOSPC",
|
|
12
|
+
"EROFS",
|
|
13
|
+
"ENOTDIR",
|
|
14
|
+
"EISDIR",
|
|
15
|
+
"EINVAL",
|
|
16
|
+
"EMFILE",
|
|
17
|
+
"ENFILE",
|
|
18
|
+
"EEXIST",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
function isLocalFsWriteError(error) {
|
|
22
|
+
if (!error || typeof error !== "object") {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (LOCAL_FS_ERROR_CODES.has(error.code)) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const message = String(error.message || "").toLowerCase();
|
|
31
|
+
return message.includes("enotdir") || message.includes("read-only file system");
|
|
32
|
+
}
|
|
33
|
+
|
|
8
34
|
function pickClient(protocol) {
|
|
9
35
|
return protocol === "https:" ? https : http;
|
|
10
36
|
}
|
|
@@ -48,6 +74,11 @@ function sendText(res, statusCode, message) {
|
|
|
48
74
|
res.end(message);
|
|
49
75
|
}
|
|
50
76
|
|
|
77
|
+
function sendErrorText(res, statusCode, message, context = "proxy") {
|
|
78
|
+
console.error(`[${context}] response error status=${statusCode} message=${message}`);
|
|
79
|
+
sendText(res, statusCode, message);
|
|
80
|
+
}
|
|
81
|
+
|
|
51
82
|
function buildUrl(req, forcedProtocol = null) {
|
|
52
83
|
const raw = req.url || "/";
|
|
53
84
|
if (/^https?:\/\//i.test(raw)) {
|
|
@@ -117,7 +148,8 @@ function forwardDirectRequest(req, res, urlObj, timeoutMs, upstreamProxyManager
|
|
|
117
148
|
|
|
118
149
|
upstreamReq.on("error", (error) => {
|
|
119
150
|
if (!res.headersSent) {
|
|
120
|
-
|
|
151
|
+
const message = `Proxy forward failed: ${error.message}`;
|
|
152
|
+
sendErrorText(res, 502, message, "proxy");
|
|
121
153
|
} else {
|
|
122
154
|
res.destroy(error);
|
|
123
155
|
}
|
|
@@ -132,7 +164,8 @@ export function createHttpRequestHandler({ config, downloader, upstreamProxyMana
|
|
|
132
164
|
try {
|
|
133
165
|
urlObj = buildUrl(req, forcedProtocol);
|
|
134
166
|
} catch (error) {
|
|
135
|
-
|
|
167
|
+
const message = `Bad request: ${error.message}`;
|
|
168
|
+
sendErrorText(res, 400, message, "proxy");
|
|
136
169
|
return;
|
|
137
170
|
}
|
|
138
171
|
|
|
@@ -150,7 +183,8 @@ export function createHttpRequestHandler({ config, downloader, upstreamProxyMana
|
|
|
150
183
|
includeHost: ecosystem !== "maven",
|
|
151
184
|
});
|
|
152
185
|
} catch (error) {
|
|
153
|
-
|
|
186
|
+
const message = `Invalid cache path: ${error.message}`;
|
|
187
|
+
sendErrorText(res, 400, message, "proxy");
|
|
154
188
|
return;
|
|
155
189
|
}
|
|
156
190
|
|
|
@@ -166,8 +200,26 @@ export function createHttpRequestHandler({ config, downloader, upstreamProxyMana
|
|
|
166
200
|
res.setHeader("x-cache", "MISS");
|
|
167
201
|
await serveFile(res, req, cachePath);
|
|
168
202
|
} catch (error) {
|
|
203
|
+
if (isLocalFsWriteError(error)) {
|
|
204
|
+
if (!error.statusCode) {
|
|
205
|
+
error.statusCode = 500;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (typeof downloader?.logDownload === "function") {
|
|
209
|
+
downloader.logDownload("local cache write failed", urlObj, {
|
|
210
|
+
code: error.code || "UNKNOWN",
|
|
211
|
+
cachePath,
|
|
212
|
+
message: error.message,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
console.error(`[proxy] local cache write failed cachePath=${cachePath} code=${error.code || "UNKNOWN"} message=${error.message}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
169
219
|
const statusCode = error.statusCode || 502;
|
|
170
|
-
|
|
220
|
+
const label = statusCode === 500 ? "Local cache write failed" : "Download failed";
|
|
221
|
+
const message = `${label}: ${error.message}`;
|
|
222
|
+
sendErrorText(res, statusCode, message, "proxy");
|
|
171
223
|
}
|
|
172
224
|
};
|
|
173
225
|
}
|
|
@@ -175,7 +227,8 @@ export function createHttpRequestHandler({ config, downloader, upstreamProxyMana
|
|
|
175
227
|
export function createMitmHttpServer(handleHttpRequestPath) {
|
|
176
228
|
const server = http.createServer((req, res) => {
|
|
177
229
|
handleHttpRequestPath(req, res, "https:").catch((error) => {
|
|
178
|
-
|
|
230
|
+
const message = `MITM request failed: ${error.message}`;
|
|
231
|
+
sendErrorText(res, 500, message, "proxy-mitm");
|
|
179
232
|
});
|
|
180
233
|
});
|
|
181
234
|
|
|
@@ -7,6 +7,11 @@ function sendText(res, statusCode, message) {
|
|
|
7
7
|
res.end(message);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
function sendErrorText(res, statusCode, message) {
|
|
11
|
+
console.error(`[proxy] response error status=${statusCode} message=${message}`);
|
|
12
|
+
sendText(res, statusCode, message);
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
export function startProxyServer(config, certManager, downloader, matchesDomain, upstreamProxyManager = null) {
|
|
11
16
|
const handleHttpRequestPath = createHttpRequestHandler({
|
|
12
17
|
config,
|
|
@@ -18,7 +23,8 @@ export function startProxyServer(config, certManager, downloader, matchesDomain,
|
|
|
18
23
|
|
|
19
24
|
const server = http.createServer((req, res) => {
|
|
20
25
|
handleHttpRequestPath(req, res, null).catch((error) => {
|
|
21
|
-
|
|
26
|
+
const message = `Proxy request failed: ${error.message}`;
|
|
27
|
+
sendErrorText(res, 500, message);
|
|
22
28
|
});
|
|
23
29
|
});
|
|
24
30
|
|
|
@@ -118,7 +118,13 @@ export class UpstreamProxyManager {
|
|
|
118
118
|
|
|
119
119
|
const cacheKey = `${proxyUrl}`;
|
|
120
120
|
if (!this.agentCache.has(cacheKey)) {
|
|
121
|
-
|
|
121
|
+
// proxy-agent v6 expects resolver-style options for deterministic proxy routing.
|
|
122
|
+
this.agentCache.set(
|
|
123
|
+
cacheKey,
|
|
124
|
+
new ProxyAgent({
|
|
125
|
+
getProxyForUrl: () => proxyUrl,
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
122
128
|
}
|
|
123
129
|
|
|
124
130
|
return this.agentCache.get(cacheKey);
|
package/src/repo/repo-server.js
CHANGED
|
@@ -110,8 +110,10 @@ export function startRepoServer(config, downloader = null) {
|
|
|
110
110
|
fs.createReadStream(filePath).pipe(res);
|
|
111
111
|
} catch (error) {
|
|
112
112
|
const statusCode = error.statusCode && error.statusCode >= 400 ? 502 : 500;
|
|
113
|
+
const message = `Repo server error: ${error.message}`;
|
|
114
|
+
console.error(`[repo] response error status=${statusCode} message=${message}`);
|
|
113
115
|
res.writeHead(statusCode, { "content-type": "text/plain; charset=utf-8" });
|
|
114
|
-
res.end(
|
|
116
|
+
res.end(message);
|
|
115
117
|
}
|
|
116
118
|
});
|
|
117
119
|
|