maven-proxy 1.1.1 → 1.2.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/src/index.js CHANGED
@@ -7,14 +7,63 @@ import { startProxyServer } from "./proxy/proxy-server.js";
7
7
  import { startRepoServer } from "./repo/repo-server.js";
8
8
  import { getTrustStoreCommands } from "./cert/truststore-utils.js";
9
9
  import { UpstreamProxyManager } from "./proxy/upstream-proxy.js";
10
+ import { MavenAffinityIndex } from "./cache/maven-affinity-index.js";
10
11
  import { installConsoleLogFileMirror, installGlobalErrorLogging } from "./common/console-log-file.js";
11
12
 
12
13
  installConsoleLogFileMirror({
13
14
  logDir: config.downloadLogDir,
14
15
  retentionDays: config.logRetentionDays,
16
+ outputToConsole: config.logToStdout,
15
17
  });
16
18
  installGlobalErrorLogging();
17
19
 
20
+ function startupInfo(message) {
21
+ if (!config.logToStdout) {
22
+ process.stdout.write(`${message}\n`);
23
+ }
24
+ console.log(message);
25
+ }
26
+
27
+ function startupError(message, error = null) {
28
+ if (!config.logToStdout) {
29
+ process.stderr.write(`${message}\n`);
30
+ if (error) {
31
+ process.stderr.write(`${error?.stack || error?.message || String(error)}\n`);
32
+ }
33
+ }
34
+ if (error) {
35
+ console.error(message, error);
36
+ } else {
37
+ console.error(message);
38
+ }
39
+ }
40
+
41
+ async function waitForServerListening(server, name) {
42
+ if (server?.listening) {
43
+ return;
44
+ }
45
+
46
+ await new Promise((resolve, reject) => {
47
+ const onListening = () => {
48
+ cleanup();
49
+ resolve();
50
+ };
51
+
52
+ const onError = (error) => {
53
+ cleanup();
54
+ reject(new Error(`${name} listen failed: ${error.message}`));
55
+ };
56
+
57
+ const cleanup = () => {
58
+ server.off("listening", onListening);
59
+ server.off("error", onError);
60
+ };
61
+
62
+ server.once("listening", onListening);
63
+ server.once("error", onError);
64
+ });
65
+ }
66
+
18
67
  async function main() {
19
68
  await fs.promises.mkdir(config.cacheDir, { recursive: true });
20
69
  await fs.promises.mkdir(config.mavenCacheDir, { recursive: true });
@@ -34,6 +83,8 @@ async function main() {
34
83
  }
35
84
 
36
85
  const upstreamProxyManager = new UpstreamProxyManager(config, matchesDomain);
86
+ const mavenAffinityIndex = new MavenAffinityIndex(config);
87
+ await mavenAffinityIndex.init();
37
88
 
38
89
  const downloader = new Downloader(config, matchesDomain, upstreamProxyManager);
39
90
 
@@ -43,44 +94,64 @@ async function main() {
43
94
  downloader,
44
95
  matchesDomain,
45
96
  upstreamProxyManager,
97
+ mavenAffinityIndex,
46
98
  );
47
99
  const repoServer = startRepoServer(config, downloader);
48
100
 
101
+ await Promise.all([
102
+ waitForServerListening(proxyServer, "proxy server"),
103
+ waitForServerListening(repoServer, "repo server"),
104
+ ]);
105
+
49
106
  const trustCommands = getTrustStoreCommands(config);
50
107
 
51
- console.log("[maven-proxy] started");
52
- console.log(`[maven-proxy] config mode: ${config.configMode}`);
53
- console.log(`[maven-proxy] config file: ${config.loadedConfigFile || "(none)"}`);
54
- console.log(`[maven-proxy] config base: ${config.configBaseDir}`);
108
+ startupInfo("[maven-proxy] started");
109
+ startupInfo(`[maven-proxy] config mode: ${config.configMode}`);
110
+ startupInfo(`[maven-proxy] config file: ${config.loadedConfigFile || "(none)"}`);
111
+ startupInfo(`[maven-proxy] config base: ${config.configBaseDir}`);
55
112
  if (config.configMode === "user") {
56
- console.log(`[maven-proxy] default user config: ${config.defaultUserConfigPath}`);
113
+ startupInfo(`[maven-proxy] default user config: ${config.defaultUserConfigPath}`);
57
114
  }
58
- console.log(`[maven-proxy] proxy port: ${config.proxyPort}`);
59
- console.log(`[maven-proxy] repo port: ${config.repoPort}`);
60
- console.log(`[maven-proxy] cache dir : ${config.cacheDir}`);
61
- console.log(`[maven-proxy] cache maven: ${config.mavenCacheDir}`);
62
- console.log(`[maven-proxy] cache npm : ${config.npmCacheDir}`);
63
- console.log(`[maven-proxy] cache other: ${config.genericCacheDir}`);
64
- console.log(`[maven-proxy] download log: ${config.downloadLogDir}`);
65
- console.log(`[maven-proxy] log retention days: ${config.logRetentionDays}`);
66
- console.log(`[maven-proxy] root cert : ${config.rootCertPath}`);
67
- console.log(`[maven-proxy] repo fallback repos: ${(config.repoFallbackRepos || []).join(",") || "(none)"}`);
115
+ startupInfo(`[maven-proxy] proxy port: ${config.proxyPort}`);
116
+ startupInfo(`[maven-proxy] repo port: ${config.repoPort}`);
117
+ startupInfo(`[maven-proxy] cache dir : ${config.cacheDir}`);
118
+ startupInfo(`[maven-proxy] cache maven: ${config.mavenCacheDir}`);
119
+ startupInfo(`[maven-proxy] cache npm : ${config.npmCacheDir}`);
120
+ startupInfo(`[maven-proxy] cache other: ${config.genericCacheDir}`);
121
+ startupInfo(`[maven-proxy] log dir: ${config.downloadLogDir}`);
122
+ startupInfo(`[maven-proxy] log retention days: ${config.logRetentionDays}`);
123
+ startupInfo(`[maven-proxy] log to stdout: ${config.logToStdout}`);
124
+ startupInfo(`[maven-proxy] log connect events: ${config.logConnectEvents}`);
125
+ startupInfo(`[maven-proxy] outbound keep-alive: ${config.outboundKeepAlive}`);
126
+ startupInfo(`[maven-proxy] outbound keepAlive(seconds): ${config.outboundKeepAliveMsecs / 1000}`);
127
+ startupInfo(`[maven-proxy] outbound maxSockets: ${config.outboundMaxSockets}`);
128
+ startupInfo(`[maven-proxy] outbound maxFreeSockets: ${config.outboundMaxFreeSockets}`);
129
+ startupInfo(`[maven-proxy] maven affinity enabled: ${config.mavenAffinityEnabled}`);
130
+ startupInfo(`[maven-proxy] maven affinity index dir: ${config.mavenAffinityIndexDir}`);
131
+ startupInfo(`[maven-proxy] maven negative cache ttl(hours): ${config.mavenNegativeCacheTtlMs / (60 * 60 * 1000)}`);
132
+ startupInfo(`[maven-proxy] maven affinity flush interval(seconds): ${config.mavenAffinityFlushIntervalMs / 1000}`);
133
+ startupInfo(`[maven-proxy] maven affinity event max(MB): ${config.mavenAffinityEventMaxBytes / (1024 * 1024)}`);
134
+ startupInfo(`[maven-proxy] root cert : ${config.rootCertPath}`);
135
+ startupInfo(`[maven-proxy] repo fallback repos: ${(config.repoFallbackRepos || []).join(",") || "(none)"}`);
68
136
  if (config.upstreamProxyUrl || config.upstreamHttpProxyUrl || config.upstreamHttpsProxyUrl) {
69
- console.log(`[maven-proxy] upstream proxy (generic): ${config.upstreamProxyUrl || "(none)"}`);
70
- console.log(`[maven-proxy] upstream proxy (http) : ${config.upstreamHttpProxyUrl || "(none)"}`);
71
- console.log(`[maven-proxy] upstream proxy (https) : ${config.upstreamHttpsProxyUrl || "(none)"}`);
72
- console.log(`[maven-proxy] upstream no-proxy : ${(config.upstreamNoProxyDomains || []).join(",") || "(none)"}`);
73
- console.log(`[maven-proxy] upstream ignore-domains : ${(config.upstreamIgnoreDomains || []).join(",") || "(none)"}`);
137
+ startupInfo(`[maven-proxy] upstream proxy (generic): ${config.upstreamProxyUrl || "(none)"}`);
138
+ startupInfo(`[maven-proxy] upstream proxy (http) : ${config.upstreamHttpProxyUrl || "(none)"}`);
139
+ startupInfo(`[maven-proxy] upstream proxy (https) : ${config.upstreamHttpsProxyUrl || "(none)"}`);
140
+ startupInfo(`[maven-proxy] upstream no-proxy : ${(config.upstreamNoProxyDomains || []).join(",") || "(none)"}`);
141
+ startupInfo(`[maven-proxy] upstream ignore-domains : ${(config.upstreamIgnoreDomains || []).join(",") || "(none)"}`);
74
142
  }
75
- console.log("[maven-proxy] trust store command (copy):");
76
- console.log(trustCommands.copyCmd);
77
- console.log("[maven-proxy] trust store command (import):");
78
- console.log(trustCommands.importCmd);
143
+ startupInfo("[maven-proxy] trust store command (copy):");
144
+ startupInfo(trustCommands.copyCmd);
145
+ startupInfo("[maven-proxy] trust store command (import):");
146
+ startupInfo(trustCommands.importCmd);
147
+ startupInfo(`[maven-proxy] startup success: proxy=127.0.0.1:${config.proxyPort}, repo=127.0.0.1:${config.repoPort}`);
79
148
 
80
149
  const shutdown = () => {
81
150
  proxyServer.close();
82
151
  mitmHttpServer.close();
83
152
  repoServer.close();
153
+ upstreamProxyManager.destroy();
154
+ void mavenAffinityIndex.destroy();
84
155
  };
85
156
 
86
157
  process.on("SIGINT", shutdown);
@@ -88,6 +159,6 @@ async function main() {
88
159
  }
89
160
 
90
161
  main().catch((error) => {
91
- console.error("[maven-proxy] fatal error:", error);
162
+ startupError("[maven-proxy] fatal error:", error);
92
163
  process.exit(1);
93
164
  });
@@ -20,7 +20,9 @@ async function openConnectUpstreamSocket(targetHost, targetPort, timeoutMs, upst
20
20
  upstreamProxyManager.hasProxyFor("https:", targetHost);
21
21
 
22
22
  if (useUpstreamProxy) {
23
- console.log(`[proxy] CONNECT via upstream target=${targetHost}:${targetPort}`);
23
+ if (upstreamProxyManager?.config?.logConnectEvents) {
24
+ console.log(`[proxy] CONNECT via upstream target=${targetHost}:${targetPort}`);
25
+ }
24
26
  const tunnel = await upstreamProxyManager.createConnectTunnel(targetHost, targetPort, timeoutMs);
25
27
  return {
26
28
  upstreamSocket: tunnel.socket,
@@ -81,9 +83,13 @@ async function handlePassThroughConnect(clientSocket, head, targetHost, targetPo
81
83
  }
82
84
 
83
85
  async function handleMitmConnect(clientSocket, head, targetHost, certManager, mitmHttpServer) {
84
- console.log(`[proxy] MITM prepare ${targetHost}`);
86
+ if (certManager?.config?.logConnectEvents) {
87
+ console.log(`[proxy] MITM prepare ${targetHost}`);
88
+ }
85
89
  const leaf = await certManager.getOrCreateLeaf(targetHost);
86
- console.log(`[proxy] MITM cert ready ${targetHost}`);
90
+ if (certManager?.config?.logConnectEvents) {
91
+ console.log(`[proxy] MITM cert ready ${targetHost}`);
92
+ }
87
93
 
88
94
  await new Promise((resolve, reject) => {
89
95
  writeTunnelResponse(clientSocket, "HTTP/1.1 200 Connection Established", (error) => {
@@ -94,7 +100,9 @@ async function handleMitmConnect(clientSocket, head, targetHost, certManager, mi
94
100
  resolve();
95
101
  });
96
102
  });
97
- console.log(`[proxy] MITM tunnel established ${targetHost}`);
103
+ if (certManager?.config?.logConnectEvents) {
104
+ console.log(`[proxy] MITM tunnel established ${targetHost}`);
105
+ }
98
106
 
99
107
  const tlsSocket = new tls.TLSSocket(clientSocket, {
100
108
  isServer: true,
@@ -138,7 +146,9 @@ export function attachConnectHandler(server, {
138
146
  config.enableHttpsProxy &&
139
147
  matchesDomain(host, config.httpsMitmDomains);
140
148
 
141
- console.log(`[proxy] CONNECT ${host}:${port} mitm=${mitmEnabled}`);
149
+ if (config.logConnectEvents) {
150
+ console.log(`[proxy] CONNECT ${host}:${port} mitm=${mitmEnabled}`);
151
+ }
142
152
 
143
153
  if (!mitmEnabled) {
144
154
  if (!config.httpsPassthroughForUnmatched) {
@@ -4,6 +4,7 @@ import https from "node:https";
4
4
  import path from "node:path";
5
5
  import { getCacheFilePath } from "../cache/cache-path.js";
6
6
  import { detectPackageEcosystem } from "../common/ecosystem.js";
7
+ import { parseMavenReleaseCanonical } from "../common/maven-canonical.js";
7
8
 
8
9
  const LOCAL_FS_ERROR_CODES = new Set([
9
10
  "EACCES",
@@ -79,6 +80,12 @@ function sendErrorText(res, statusCode, message, context = "proxy") {
79
80
  sendText(res, statusCode, message);
80
81
  }
81
82
 
83
+ export function isPositiveAffinityEligible(fileName) {
84
+ const lower = String(fileName || "").toLowerCase();
85
+ const base = lower.replace(/\.(sha1|sha256|sha512|md5|asc)$/i, "");
86
+ return /\.(jar|aar|war)$/i.test(base);
87
+ }
88
+
82
89
  function buildUrl(req, forcedProtocol = null) {
83
90
  const raw = req.url || "/";
84
91
  if (/^https?:\/\//i.test(raw)) {
@@ -158,7 +165,13 @@ function forwardDirectRequest(req, res, urlObj, timeoutMs, upstreamProxyManager
158
165
  req.pipe(upstreamReq);
159
166
  }
160
167
 
161
- export function createHttpRequestHandler({ config, downloader, upstreamProxyManager = null, matchesDomain }) {
168
+ export function createHttpRequestHandler({
169
+ config,
170
+ downloader,
171
+ upstreamProxyManager = null,
172
+ matchesDomain,
173
+ mavenAffinityIndex = null,
174
+ }) {
162
175
  return async function handleHttpRequestPath(req, res, forcedProtocol = null) {
163
176
  let urlObj;
164
177
  try {
@@ -176,12 +189,18 @@ export function createHttpRequestHandler({ config, downloader, upstreamProxyMana
176
189
  }
177
190
 
178
191
  let cachePath;
192
+ let ecosystem;
193
+ let canonical = null;
179
194
  try {
180
- const ecosystem = detectPackageEcosystem(urlObj, config, matchesDomain);
195
+ ecosystem = detectPackageEcosystem(urlObj, config, matchesDomain);
181
196
  cachePath = getCacheFilePath(config.cacheDir, urlObj, {
182
197
  ecosystem,
183
198
  includeHost: ecosystem !== "maven",
184
199
  });
200
+
201
+ if (ecosystem === "maven" && mavenAffinityIndex?.enabled) {
202
+ canonical = parseMavenReleaseCanonical(urlObj);
203
+ }
185
204
  } catch (error) {
186
205
  const message = `Invalid cache path: ${error.message}`;
187
206
  sendErrorText(res, 400, message, "proxy");
@@ -190,16 +209,58 @@ export function createHttpRequestHandler({ config, downloader, upstreamProxyMana
190
209
 
191
210
  const existing = await statIfFile(cachePath);
192
211
  if (existing) {
212
+ console.log(`[proxy] local cache hit host=${urlObj.hostname} path=${urlObj.pathname}`);
193
213
  await serveFile(res, req, cachePath);
194
214
  return;
195
215
  }
196
216
 
217
+ if (canonical && mavenAffinityIndex) {
218
+ if (isPositiveAffinityEligible(canonical.fileName)) {
219
+ const preferredPath = await mavenAffinityIndex.resolvePreferredCachePath(canonical.canonicalKey);
220
+ if (preferredPath) {
221
+ console.log(`[proxy] affinity hit canonical=${canonical.canonicalKey} host=${urlObj.hostname}`);
222
+ await serveFile(res, req, preferredPath);
223
+ return;
224
+ }
225
+ }
226
+
227
+ if (mavenAffinityIndex.shouldSkipRequest(canonical.canonicalKey, urlObj)) {
228
+ console.log(`[proxy] affinity negative skip canonical=${canonical.canonicalKey} host=${urlObj.hostname}`);
229
+ sendText(res, 404, "Not Found");
230
+ return;
231
+ }
232
+ }
233
+
197
234
  try {
235
+ console.log(`[proxy] local cache miss host=${urlObj.hostname} path=${urlObj.pathname}`);
198
236
  await fs.promises.mkdir(path.dirname(cachePath), { recursive: true });
199
237
  await downloader.ensureCached(urlObj, cachePath, req.headers);
238
+
239
+ if (canonical && mavenAffinityIndex && isPositiveAffinityEligible(canonical.fileName)) {
240
+ mavenAffinityIndex.recordSuccess({
241
+ canonicalKey: canonical.canonicalKey,
242
+ host: urlObj.hostname,
243
+ cachePath,
244
+ fileName: canonical.fileName,
245
+ urlObj,
246
+ });
247
+ }
248
+
200
249
  res.setHeader("x-cache", "MISS");
201
250
  await serveFile(res, req, cachePath);
202
251
  } catch (error) {
252
+ if (
253
+ canonical &&
254
+ mavenAffinityIndex &&
255
+ (error.statusCode === 404 || error.statusCode === 410)
256
+ ) {
257
+ mavenAffinityIndex.recordNegative({
258
+ canonicalKey: canonical.canonicalKey,
259
+ urlObj,
260
+ statusCode: error.statusCode,
261
+ });
262
+ }
263
+
203
264
  if (isLocalFsWriteError(error)) {
204
265
  if (!error.statusCode) {
205
266
  error.statusCode = 500;
@@ -12,12 +12,20 @@ function sendErrorText(res, statusCode, message) {
12
12
  sendText(res, statusCode, message);
13
13
  }
14
14
 
15
- export function startProxyServer(config, certManager, downloader, matchesDomain, upstreamProxyManager = null) {
15
+ export function startProxyServer(
16
+ config,
17
+ certManager,
18
+ downloader,
19
+ matchesDomain,
20
+ upstreamProxyManager = null,
21
+ mavenAffinityIndex = null,
22
+ ) {
16
23
  const handleHttpRequestPath = createHttpRequestHandler({
17
24
  config,
18
25
  downloader,
19
26
  upstreamProxyManager,
20
27
  matchesDomain,
28
+ mavenAffinityIndex,
21
29
  });
22
30
  const mitmHttpServer = createMitmHttpServer(handleHttpRequestPath);
23
31
 
@@ -1,7 +1,23 @@
1
+ import http from "node:http";
2
+ import https from "node:https";
1
3
  import net from "node:net";
2
4
  import tls from "node:tls";
3
5
  import { ProxyAgent } from "proxy-agent";
4
6
 
7
+ function toPositiveInt(value, fallback) {
8
+ const parsed = Number.parseInt(value, 10);
9
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
10
+ }
11
+
12
+ function buildAgentOptions(config) {
13
+ return {
14
+ keepAlive: Boolean(config.outboundKeepAlive),
15
+ keepAliveMsecs: toPositiveInt(config.outboundKeepAliveMsecs, 1000),
16
+ maxSockets: toPositiveInt(config.outboundMaxSockets, 64),
17
+ maxFreeSockets: toPositiveInt(config.outboundMaxFreeSockets, 16),
18
+ };
19
+ }
20
+
5
21
  function normalizeHostname(hostname) {
6
22
  return String(hostname || "")
7
23
  .trim()
@@ -64,6 +80,12 @@ export class UpstreamProxyManager {
64
80
  this.config = config;
65
81
  this.matchesDomain = matchesDomain;
66
82
  this.agentCache = new Map();
83
+ this.directHttpAgent = new http.Agent(buildAgentOptions(config));
84
+ this.directHttpsAgent = new https.Agent(buildAgentOptions(config));
85
+ }
86
+
87
+ getDirectAgentForProtocol(protocol) {
88
+ return protocol === "https:" ? this.directHttpsAgent : this.directHttpAgent;
67
89
  }
68
90
 
69
91
  shouldBypass(hostname) {
@@ -111,17 +133,21 @@ export class UpstreamProxyManager {
111
133
  }
112
134
 
113
135
  getAgentForUrl(urlObj) {
114
- const proxyUrl = this.getProxyUrlFor(urlObj.protocol, urlObj.hostname);
136
+ const protocol = urlObj?.protocol === "https:" ? "https:" : "http:";
137
+ const hostname = String(urlObj?.hostname || "");
138
+ const proxyUrl = this.getProxyUrlFor(protocol, hostname);
139
+
115
140
  if (!proxyUrl) {
116
- return undefined;
141
+ return this.getDirectAgentForProtocol(protocol);
117
142
  }
118
143
 
119
- const cacheKey = `${proxyUrl}`;
144
+ const cacheKey = `proxy:${proxyUrl}`;
120
145
  if (!this.agentCache.has(cacheKey)) {
121
146
  // proxy-agent v6 expects resolver-style options for deterministic proxy routing.
122
147
  this.agentCache.set(
123
148
  cacheKey,
124
149
  new ProxyAgent({
150
+ ...buildAgentOptions(this.config),
125
151
  getProxyForUrl: () => proxyUrl,
126
152
  }),
127
153
  );
@@ -134,6 +160,18 @@ export class UpstreamProxyManager {
134
160
  return Boolean(this.getProxyUrlFor(protocol, hostname));
135
161
  }
136
162
 
163
+ destroy() {
164
+ for (const agent of this.agentCache.values()) {
165
+ if (typeof agent?.destroy === "function") {
166
+ agent.destroy();
167
+ }
168
+ }
169
+
170
+ this.agentCache.clear();
171
+ this.directHttpAgent.destroy();
172
+ this.directHttpsAgent.destroy();
173
+ }
174
+
137
175
  async createConnectTunnel(targetHost, targetPort, timeoutMs) {
138
176
  const proxyUrlText = this.getProxyUrlFor("https:", targetHost);
139
177
  if (!proxyUrlText) {
@@ -1,27 +0,0 @@
1
- import { DailyLogFile } from "./daily-log-file.js";
2
-
3
- export class DownloadLogWriter {
4
- constructor(logDir, retentionDays = 7) {
5
- this.logFile = new DailyLogFile({
6
- logDir,
7
- filePrefix: "download",
8
- retentionDays,
9
- });
10
- }
11
-
12
- async append(event, url, details = {}) {
13
- const record = {
14
- time: new Date().toISOString(),
15
- event,
16
- url,
17
- ...details,
18
- };
19
- await this.logFile.appendLine(JSON.stringify(record));
20
- }
21
-
22
- write(event, url, details = {}) {
23
- this.append(event, url, details).catch((error) => {
24
- console.warn(`[downloader] write download log failed: ${error.message}`);
25
- });
26
- }
27
- }