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 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.
@@ -44,7 +44,8 @@ function resolveEffectiveMode(options) {
44
44
  return forced;
45
45
  }
46
46
 
47
- return isProjectWorkspace(process.cwd()) ? "development" : "user";
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
- if (options.mode) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maven-proxy",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Maven proxy with cache, HTTPS MITM for selected domains, and local repo publishing",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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");
@@ -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
- sendText(res, 502, `Proxy forward failed: ${error.message}`);
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
- sendText(res, 400, `Bad request: ${error.message}`);
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
- sendText(res, 400, `Invalid cache path: ${error.message}`);
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
- sendText(res, statusCode, `Download failed: ${error.message}`);
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
- sendText(res, 500, `MITM request failed: ${error.message}`);
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
- sendText(res, 500, `Proxy request failed: ${error.message}`);
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
- this.agentCache.set(cacheKey, new ProxyAgent(proxyUrl));
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);
@@ -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(`Repo server error: ${error.message}`);
116
+ res.end(message);
115
117
  }
116
118
  });
117
119