maven-proxy 1.2.0 → 1.3.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 +47 -8
- package/bin/maven-proxy.js +175 -9
- package/package.json +2 -1
- package/src/cache/cache-cleanup-manager.js +416 -0
- package/src/cache/maven-affinity-index.js +6 -3
- package/src/config/config.js +63 -7
- package/src/index.js +106 -36
- package/src/proxy/proxy-connect-handler.js +15 -5
- package/src/proxy/proxy-http-handler.js +28 -9
- package/src/proxy/proxy-server.js +2 -0
- package/src/repo/repo-server.js +10 -3
package/README.md
CHANGED
|
@@ -256,6 +256,17 @@ curl.exe -k -sS -D - -o NUL -x http://127.0.0.1:8080 https://registry.npmjs.org/
|
|
|
256
256
|
### 8.4 Upstream Proxy Settings
|
|
257
257
|
|
|
258
258
|
Environment variables:
|
|
259
|
+
- CACHE_CLEANUP_ENABLED: enable automatic cache cleanup.
|
|
260
|
+
- CACHE_CLEANUP_DAILY_AT: daily cleanup check time in local timezone (HH:mm), for example 03:00.
|
|
261
|
+
- CACHE_CLEANUP_CHECK_MIN_INTERVAL: minimum interval between pressure checks (supports s/m/h/d), for example 10m.
|
|
262
|
+
- CACHE_TOUCH_ON_HIT: update file mtime when cache hit returns successfully.
|
|
263
|
+
- CACHE_TOUCH_MIN_INTERVAL: minimum interval between two touches for the same file (supports s/m/h/d), default 1d.
|
|
264
|
+
- CACHE_RETENTION_START: initial retention window for cleanup rounds (supports s/m/h/d), default 10d.
|
|
265
|
+
- CACHE_RETENTION_MIN: minimum retention window for cleanup rounds (supports s/m/h/d), default 1d.
|
|
266
|
+
- CACHE_DISK_FREE_TRIGGER: trigger cleanup when disk free bytes are below this value (supports K/M/G/T), for example 20G.
|
|
267
|
+
- CACHE_DISK_FREE_TARGET: stop cleanup when disk free bytes recover to this value (supports K/M/G/T), for example 25G.
|
|
268
|
+
- CACHE_MAX_SIZE: optional cache-size trigger threshold (supports K/M/G/T).
|
|
269
|
+
- CACHE_TARGET_SIZE: optional cache-size target to stop cleanup (supports K/M/G/T).
|
|
259
270
|
- UPSTREAM_PROXY_URL: generic upstream proxy URL.
|
|
260
271
|
- UPSTREAM_HTTP_PROXY_URL: upstream proxy for HTTP.
|
|
261
272
|
- UPSTREAM_HTTPS_PROXY_URL: upstream proxy for HTTPS.
|
|
@@ -266,12 +277,18 @@ Environment variables:
|
|
|
266
277
|
- MAVEN_REPO_DOMAINS: maven domains for ecosystem routing (wildcards supported).
|
|
267
278
|
- HTTPS_MITM_DOMAINS: MITM domain list (includes registry.npmjs.org by default, wildcards supported).
|
|
268
279
|
- DOWNLOAD_LOG_DIR: log directory.
|
|
269
|
-
-
|
|
270
|
-
- LOG_TO_STDOUT: whether to also print logs to stdout/stderr.
|
|
280
|
+
- LOG_RETENTION: log retention duration (supports s/m/h/d), for example 7d.
|
|
281
|
+
- LOG_TO_STDOUT: whether to also print runtime logs to stdout/stderr; startup logs are always printed.
|
|
282
|
+
- LOG_CONNECT_EVENTS: whether to print verbose CONNECT/MITM handshake logs. Default false.
|
|
271
283
|
- OUTBOUND_KEEP_ALIVE: enable outbound keep-alive connection pooling.
|
|
272
|
-
-
|
|
284
|
+
- OUTBOUND_KEEP_ALIVE_INTERVAL: keep-alive interval (supports s/m/h/d), for example 1s.
|
|
273
285
|
- OUTBOUND_MAX_SOCKETS: max outbound sockets per origin.
|
|
274
286
|
- OUTBOUND_MAX_FREE_SOCKETS: max idle outbound sockets per origin.
|
|
287
|
+
- MAVEN_AFFINITY_ENABLED: enable Maven affinity index.
|
|
288
|
+
- MAVEN_AFFINITY_INDEX_DIR: Maven affinity index directory. Default data/index.
|
|
289
|
+
- MAVEN_NEGATIVE_CACHE_TTL: negative cache TTL (supports s/m/h/d), for example 24h.
|
|
290
|
+
- MAVEN_AFFINITY_FLUSH_INTERVAL: flush interval for affinity event log (supports s/m/h/d), for example 5s.
|
|
291
|
+
- MAVEN_AFFINITY_EVENT_MAX_MB: max size threshold for affinity event log compaction in MB.
|
|
275
292
|
- MAVEN_PROXY_CONFIG_MODE: development or user.
|
|
276
293
|
- MAVEN_PROXY_CONFIG_FILE: explicit config file path.
|
|
277
294
|
- EXISTING_TRUST_STORE_PATH: optional existing truststore path. If present, truststore init prefers it as source.
|
|
@@ -291,23 +308,40 @@ Priority:
|
|
|
291
308
|
- `PROXY_PORT`: Proxy server port. Default `8080`.
|
|
292
309
|
- `REPO_PORT`: Local repository server port. Default `8081`.
|
|
293
310
|
- `CACHE_DIR`: Base cache directory. Default `data/cache`.
|
|
311
|
+
- `CACHE_CLEANUP_ENABLED`: Enable automatic cache cleanup. Default `true`.
|
|
312
|
+
- `CACHE_CLEANUP_DAILY_AT`: Daily cleanup check time in local timezone (`HH:mm`). Default `03:00`.
|
|
313
|
+
- `CACHE_CLEANUP_CHECK_MIN_INTERVAL`: Minimum interval between pressure checks (supports `s/m/h/d`). Default `10m`.
|
|
314
|
+
- `CACHE_TOUCH_ON_HIT`: Update file mtime when cache hit returns successfully. Default `true`.
|
|
315
|
+
- `CACHE_TOUCH_MIN_INTERVAL`: Minimum interval between two touches for the same file (supports `s/m/h/d`). Default `1d`.
|
|
316
|
+
- `CACHE_RETENTION_START`: Initial retention window for cleanup rounds (supports `s/m/h/d`). Default `10d`.
|
|
317
|
+
- `CACHE_RETENTION_MIN`: Minimum retention window for cleanup rounds (supports `s/m/h/d`). Default `1d`.
|
|
318
|
+
- `CACHE_DISK_FREE_TRIGGER`: Trigger cleanup when disk free bytes are below this value (supports `K/M/G/T`). Default `20G`.
|
|
319
|
+
- `CACHE_DISK_FREE_TARGET`: Stop cleanup when disk free bytes recover to this value (supports `K/M/G/T`). Default `25G`.
|
|
320
|
+
- `CACHE_MAX_SIZE`: Optional cache-size trigger threshold (supports `K/M/G/T`). Default empty (disabled).
|
|
321
|
+
- `CACHE_TARGET_SIZE`: Optional cache-size target to stop cleanup (supports `K/M/G/T`). Default empty (disabled).
|
|
294
322
|
- `REPO_FALLBACK_REPOS`: Comma-separated fallback repository URLs for cache misses.
|
|
295
323
|
- `ENABLE_HTTPS_PROXY`: Enable HTTPS proxy handling (`true/false`).
|
|
296
324
|
- `HTTPS_MITM_DOMAINS`: Comma-separated domains to apply MITM certificate issuance (wildcards supported).
|
|
297
|
-
- `HTTPS_PASSTHROUGH_FOR_UNMATCHED`: Whether unmatched HTTPS domains are tunneled directly.
|
|
325
|
+
- `HTTPS_PASSTHROUGH_FOR_UNMATCHED`: Whether unmatched HTTPS domains are tunneled directly. Default `false`.
|
|
298
326
|
- `NPM_REGISTRY_DOMAINS`: Domains treated as npm ecosystem for cache routing (wildcards supported).
|
|
299
327
|
- `MAVEN_REPO_DOMAINS`: Domains treated as Maven ecosystem for cache routing (wildcards supported).
|
|
300
328
|
- `MULTI_THREAD_DOMAINS`: Domains allowed to use multi-thread download (wildcards supported).
|
|
301
329
|
- `MULTI_THREAD_COUNT`: Number of download threads for ranged downloads.
|
|
302
330
|
- `MULTI_THREAD_MIN_SIZE_MB`: Minimum size threshold to trigger multi-thread download (MB).
|
|
303
|
-
- `
|
|
331
|
+
- `DOWNLOAD_TIMEOUT`: Upstream request timeout (supports `s/m/h/d`). Default `60s`.
|
|
304
332
|
- `DOWNLOAD_LOG_DIR`: Directory for unified app/error logs.
|
|
305
|
-
- `
|
|
306
|
-
- `LOG_TO_STDOUT`: Whether to also print logs to stdout/stderr. Default `true`.
|
|
333
|
+
- `LOG_RETENTION`: Log retention duration (supports `s/m/h/d`). Default `7d`.
|
|
334
|
+
- `LOG_TO_STDOUT`: Whether to also print runtime logs to stdout/stderr. Startup logs are always printed. Default `true`.
|
|
335
|
+
- `LOG_CONNECT_EVENTS`: Whether to print verbose CONNECT/MITM handshake logs. Default `false`.
|
|
307
336
|
- `OUTBOUND_KEEP_ALIVE`: Enable outbound keep-alive connection pooling. Default `true`.
|
|
308
|
-
- `
|
|
337
|
+
- `OUTBOUND_KEEP_ALIVE_INTERVAL`: Keep-alive interval (supports `s/m/h/d`). Default `1s`.
|
|
309
338
|
- `OUTBOUND_MAX_SOCKETS`: Max outbound sockets per origin. Default `64`.
|
|
310
339
|
- `OUTBOUND_MAX_FREE_SOCKETS`: Max idle outbound sockets per origin. Default `16`.
|
|
340
|
+
- `MAVEN_AFFINITY_ENABLED`: Enable Maven affinity cache index. Default `true`.
|
|
341
|
+
- `MAVEN_AFFINITY_INDEX_DIR`: Maven affinity index directory. Default `data/index`.
|
|
342
|
+
- `MAVEN_NEGATIVE_CACHE_TTL`: Negative cache TTL (supports `s/m/h/d`). Default `24h`.
|
|
343
|
+
- `MAVEN_AFFINITY_FLUSH_INTERVAL`: Flush interval for affinity event log (supports `s/m/h/d`). Default `5s`.
|
|
344
|
+
- `MAVEN_AFFINITY_EVENT_MAX_MB`: Max size threshold for affinity event log compaction in MB. Default `8`.
|
|
311
345
|
- `UPSTREAM_PROXY_URL`: Generic upstream proxy URL (fallback for HTTP/HTTPS).
|
|
312
346
|
- `UPSTREAM_HTTP_PROXY_URL`: Upstream proxy URL for HTTP requests.
|
|
313
347
|
- `UPSTREAM_HTTPS_PROXY_URL`: Upstream proxy URL for HTTPS requests.
|
|
@@ -404,12 +438,17 @@ Common commands:
|
|
|
404
438
|
- maven-proxy --config /path/to/config
|
|
405
439
|
- maven-proxy start --mode development
|
|
406
440
|
- maven-proxy start --mode user
|
|
441
|
+
- maven-proxy stop
|
|
407
442
|
- maven-proxy init-config --force
|
|
408
443
|
- maven-proxy truststore print
|
|
409
444
|
- maven-proxy truststore init
|
|
410
445
|
- maven-proxy truststore merge --source /path/source.jks --target /path/target.jks
|
|
411
446
|
- maven-proxy doctor
|
|
412
447
|
|
|
448
|
+
Start/stop behavior:
|
|
449
|
+
- `maven-proxy start` runs in background and returns immediately.
|
|
450
|
+
- `maven-proxy stop` stops the background process using PID file `~/maven-proxy/maven-proxy.pid`.
|
|
451
|
+
|
|
413
452
|
Doctor command:
|
|
414
453
|
- Checks config loading, port availability, keytool, JAVA_HOME, cert/truststore paths, and writable log/cache directories.
|
|
415
454
|
- Reports PASS/WARN/FAIL.
|
package/bin/maven-proxy.js
CHANGED
|
@@ -3,10 +3,14 @@ import fs from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import net from "node:net";
|
|
6
|
-
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
7
8
|
|
|
8
9
|
const defaultConfigDir = path.resolve(os.homedir(), "maven-proxy");
|
|
9
10
|
const defaultConfigFile = path.join(defaultConfigDir, "config.properties");
|
|
11
|
+
const daemonPidFile = path.join(defaultConfigDir, "maven-proxy.pid");
|
|
12
|
+
const internalRunCommand = "__run-server";
|
|
13
|
+
const cliFilePath = fileURLToPath(import.meta.url);
|
|
10
14
|
|
|
11
15
|
function normalizeMode(value) {
|
|
12
16
|
const normalized = String(value || "").trim().toLowerCase();
|
|
@@ -54,6 +58,7 @@ function printHelp() {
|
|
|
54
58
|
console.log("Usage:");
|
|
55
59
|
console.log(" maven-proxy");
|
|
56
60
|
console.log(" maven-proxy start [--mode <development|user>] [--config <file>]");
|
|
61
|
+
console.log(" maven-proxy stop");
|
|
57
62
|
console.log(" maven-proxy init-config [--force] [--config <file>]");
|
|
58
63
|
console.log(" maven-proxy truststore <print|init|merge> [options]");
|
|
59
64
|
console.log(" maven-proxy doctor [--mode <development|user>] [--config <file>]");
|
|
@@ -62,6 +67,7 @@ function printHelp() {
|
|
|
62
67
|
console.log(" npx maven-proxy");
|
|
63
68
|
console.log(" maven-proxy init-config");
|
|
64
69
|
console.log(" maven-proxy start --mode development");
|
|
70
|
+
console.log(" maven-proxy stop");
|
|
65
71
|
console.log(" maven-proxy --config ~/maven-proxy/config.properties");
|
|
66
72
|
console.log(" maven-proxy truststore print");
|
|
67
73
|
console.log(" maven-proxy truststore merge --source ./a.jks --target ./b.jks");
|
|
@@ -141,27 +147,38 @@ function getDefaultConfigTemplate() {
|
|
|
141
147
|
"PROXY_PORT=8080",
|
|
142
148
|
"REPO_PORT=8081",
|
|
143
149
|
"CACHE_DIR=data/cache",
|
|
150
|
+
"CACHE_CLEANUP_ENABLED=true",
|
|
151
|
+
"CACHE_CLEANUP_DAILY_AT=03:00",
|
|
152
|
+
"CACHE_CLEANUP_CHECK_MIN_INTERVAL=10m",
|
|
153
|
+
"CACHE_TOUCH_ON_HIT=true",
|
|
154
|
+
"CACHE_TOUCH_MIN_INTERVAL=1d",
|
|
155
|
+
"CACHE_RETENTION_START=10d",
|
|
156
|
+
"CACHE_RETENTION_MIN=1d",
|
|
157
|
+
"CACHE_DISK_FREE_TRIGGER=20G",
|
|
158
|
+
"CACHE_DISK_FREE_TARGET=25G",
|
|
159
|
+
"CACHE_MAX_SIZE=",
|
|
160
|
+
"CACHE_TARGET_SIZE=",
|
|
144
161
|
"REPO_FALLBACK_REPOS=https://repo1.maven.org/maven2,https://jitpack.io,https://plugins.gradle.org/m2,https://maven.google.com",
|
|
145
162
|
"ENABLE_HTTPS_PROXY=true",
|
|
146
163
|
"HTTPS_MITM_DOMAINS=repo1.maven.org,repo.maven.apache.org,registry.npmjs.org",
|
|
147
|
-
"HTTPS_PASSTHROUGH_FOR_UNMATCHED=
|
|
164
|
+
"HTTPS_PASSTHROUGH_FOR_UNMATCHED=false",
|
|
148
165
|
"NPM_REGISTRY_DOMAINS=registry.npmjs.org,registry.npmmirror.com,npm.pkg.github.com",
|
|
149
166
|
"MAVEN_REPO_DOMAINS=repo1.maven.org,repo.maven.apache.org,jitpack.io,plugins.gradle.org,maven.google.com",
|
|
150
167
|
"MULTI_THREAD_DOMAINS=repo1.maven.org",
|
|
151
168
|
"MULTI_THREAD_COUNT=8",
|
|
152
169
|
"MULTI_THREAD_MIN_SIZE_MB=1",
|
|
153
|
-
"
|
|
170
|
+
"DOWNLOAD_TIMEOUT=60s",
|
|
154
171
|
"OUTBOUND_KEEP_ALIVE=true",
|
|
155
|
-
"
|
|
172
|
+
"OUTBOUND_KEEP_ALIVE_INTERVAL=1s",
|
|
156
173
|
"OUTBOUND_MAX_SOCKETS=64",
|
|
157
174
|
"OUTBOUND_MAX_FREE_SOCKETS=16",
|
|
158
175
|
"MAVEN_AFFINITY_ENABLED=true",
|
|
159
|
-
"MAVEN_AFFINITY_INDEX_DIR
|
|
160
|
-
"
|
|
161
|
-
"
|
|
176
|
+
"MAVEN_AFFINITY_INDEX_DIR=data/index",
|
|
177
|
+
"MAVEN_NEGATIVE_CACHE_TTL=24h",
|
|
178
|
+
"MAVEN_AFFINITY_FLUSH_INTERVAL=5s",
|
|
162
179
|
"MAVEN_AFFINITY_EVENT_MAX_MB=8",
|
|
163
180
|
"DOWNLOAD_LOG_DIR=data/logs/downloads",
|
|
164
|
-
"
|
|
181
|
+
"LOG_RETENTION=7d",
|
|
165
182
|
"LOG_TO_STDOUT=false",
|
|
166
183
|
"UPSTREAM_PROXY_URL=",
|
|
167
184
|
"UPSTREAM_HTTP_PROXY_URL=",
|
|
@@ -217,12 +234,145 @@ async function ensureAutoConfigIfNeeded(options, command) {
|
|
|
217
234
|
await initConfigFile(defaultConfigFile, false);
|
|
218
235
|
}
|
|
219
236
|
|
|
220
|
-
async function
|
|
237
|
+
async function runServerInCurrentProcess(options) {
|
|
221
238
|
applyConfigOverrides(options);
|
|
222
239
|
|
|
223
240
|
await import("../src/index.js");
|
|
224
241
|
}
|
|
225
242
|
|
|
243
|
+
function parsePid(rawText) {
|
|
244
|
+
const pid = Number.parseInt(String(rawText || "").trim(), 10);
|
|
245
|
+
return Number.isFinite(pid) && pid > 0 ? pid : 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function readDaemonPid() {
|
|
249
|
+
if (!fs.existsSync(daemonPidFile)) {
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const text = fs.readFileSync(daemonPidFile, "utf8");
|
|
255
|
+
return parsePid(text);
|
|
256
|
+
} catch {
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function isProcessRunning(pid) {
|
|
262
|
+
if (!pid) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
process.kill(pid, 0);
|
|
268
|
+
return true;
|
|
269
|
+
} catch (error) {
|
|
270
|
+
if (error && error.code === "EPERM") {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function removeDaemonPidFile() {
|
|
278
|
+
await fs.promises.rm(daemonPidFile, { force: true });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function waitMs(ms) {
|
|
282
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function waitForProcessExit(pid, timeoutMs = 5000) {
|
|
286
|
+
const startedAt = Date.now();
|
|
287
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
288
|
+
if (!isProcessRunning(pid)) {
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
await waitMs(100);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return !isProcessRunning(pid);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function startServer(options) {
|
|
298
|
+
await fs.promises.mkdir(defaultConfigDir, { recursive: true });
|
|
299
|
+
|
|
300
|
+
const existingPid = readDaemonPid();
|
|
301
|
+
if (existingPid && isProcessRunning(existingPid)) {
|
|
302
|
+
console.log(`[maven-proxy] already running (pid=${existingPid})`);
|
|
303
|
+
console.log(`[maven-proxy] pid file: ${daemonPidFile}`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (existingPid && !isProcessRunning(existingPid)) {
|
|
308
|
+
await removeDaemonPidFile();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const childEnv = {
|
|
312
|
+
...process.env,
|
|
313
|
+
MAVEN_PROXY_CONFIG_MODE: resolveEffectiveMode(options),
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
if (options.configPath) {
|
|
317
|
+
childEnv.MAVEN_PROXY_CONFIG_FILE = resolvePath(options.configPath);
|
|
318
|
+
} else {
|
|
319
|
+
delete childEnv.MAVEN_PROXY_CONFIG_FILE;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const child = spawn(
|
|
323
|
+
process.execPath,
|
|
324
|
+
[cliFilePath, internalRunCommand, "--mode", resolveEffectiveMode(options)],
|
|
325
|
+
{
|
|
326
|
+
cwd: process.cwd(),
|
|
327
|
+
detached: true,
|
|
328
|
+
stdio: "ignore",
|
|
329
|
+
env: childEnv,
|
|
330
|
+
},
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
child.unref();
|
|
334
|
+
await fs.promises.writeFile(daemonPidFile, `${child.pid}\n`, "utf8");
|
|
335
|
+
|
|
336
|
+
await waitMs(300);
|
|
337
|
+
if (!isProcessRunning(child.pid)) {
|
|
338
|
+
await removeDaemonPidFile();
|
|
339
|
+
throw new Error("start failed: process exited immediately, check app/error logs");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
console.log(`[maven-proxy] started in background (pid=${child.pid})`);
|
|
343
|
+
console.log(`[maven-proxy] pid file: ${daemonPidFile}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function stopServer(options) {
|
|
347
|
+
const pid = readDaemonPid();
|
|
348
|
+
if (!pid) {
|
|
349
|
+
console.log("[maven-proxy] not running (pid file not found)");
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (!isProcessRunning(pid)) {
|
|
354
|
+
await removeDaemonPidFile();
|
|
355
|
+
console.log(`[maven-proxy] stale pid removed: ${pid}`);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
process.kill(pid, "SIGTERM");
|
|
360
|
+
const stopped = await waitForProcessExit(pid, 5000);
|
|
361
|
+
if (!stopped) {
|
|
362
|
+
process.kill(pid, "SIGKILL");
|
|
363
|
+
const forceStopped = await waitForProcessExit(pid, 2000);
|
|
364
|
+
if (!forceStopped) {
|
|
365
|
+
throw new Error(`stop failed: unable to terminate pid ${pid}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
await removeDaemonPidFile();
|
|
370
|
+
console.log(`[maven-proxy] stopped (pid=${pid})`);
|
|
371
|
+
if (options.configPath) {
|
|
372
|
+
console.log(`[maven-proxy] stop requested with config: ${resolvePath(options.configPath)}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
226
376
|
function applyConfigOverrides(options) {
|
|
227
377
|
process.env.MAVEN_PROXY_CONFIG_MODE = resolveEffectiveMode(options);
|
|
228
378
|
|
|
@@ -577,6 +727,14 @@ async function main() {
|
|
|
577
727
|
return;
|
|
578
728
|
}
|
|
579
729
|
|
|
730
|
+
if (command === "stop") {
|
|
731
|
+
if (options.commandArgs.length > 0) {
|
|
732
|
+
throw new Error(`Unknown argument for stop: ${options.commandArgs[0]}`);
|
|
733
|
+
}
|
|
734
|
+
await stopServer(options);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
|
|
580
738
|
if (command === "start") {
|
|
581
739
|
if (options.commandArgs.length > 0) {
|
|
582
740
|
throw new Error(`Unknown argument for start: ${options.commandArgs[0]}`);
|
|
@@ -585,6 +743,14 @@ async function main() {
|
|
|
585
743
|
return;
|
|
586
744
|
}
|
|
587
745
|
|
|
746
|
+
if (command === internalRunCommand) {
|
|
747
|
+
if (options.commandArgs.length > 0) {
|
|
748
|
+
throw new Error(`Unknown argument for ${internalRunCommand}: ${options.commandArgs[0]}`);
|
|
749
|
+
}
|
|
750
|
+
await runServerInCurrentProcess(options);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
|
|
588
754
|
throw new Error(`Unknown command: ${command}`);
|
|
589
755
|
}
|
|
590
756
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "maven-proxy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"start:cli": "node bin/maven-proxy.js",
|
|
18
18
|
"cli:help": "node bin/maven-proxy.js --help",
|
|
19
19
|
"cli:start": "node bin/maven-proxy.js start --mode development",
|
|
20
|
+
"cli:stop": "node bin/maven-proxy.js stop",
|
|
20
21
|
"cli:doctor": "node bin/maven-proxy.js doctor --mode development",
|
|
21
22
|
"cli:truststore:print": "node bin/maven-proxy.js truststore print --mode development",
|
|
22
23
|
"cli:truststore:init": "node bin/maven-proxy.js truststore init --mode development",
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const SECOND_MS = 1000;
|
|
5
|
+
const MINUTE_MS = 60 * SECOND_MS;
|
|
6
|
+
const HOUR_MS = 60 * MINUTE_MS;
|
|
7
|
+
const DAY_MS = 24 * HOUR_MS;
|
|
8
|
+
|
|
9
|
+
function toBool(value, fallback) {
|
|
10
|
+
if (value == null || value === "") {
|
|
11
|
+
return fallback;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return ["1", "true", "yes", "on"].includes(String(value).toLowerCase());
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseDurationToMs(raw, fallbackMs) {
|
|
18
|
+
const text = String(raw || "").trim();
|
|
19
|
+
if (!text) {
|
|
20
|
+
return fallbackMs;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const match = text.match(/^(\d+)([smhd])$/i);
|
|
24
|
+
if (!match) {
|
|
25
|
+
return fallbackMs;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const value = Number.parseInt(match[1], 10);
|
|
29
|
+
const unit = match[2].toLowerCase();
|
|
30
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
31
|
+
return fallbackMs;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (unit === "s") {
|
|
35
|
+
return value * SECOND_MS;
|
|
36
|
+
}
|
|
37
|
+
if (unit === "m") {
|
|
38
|
+
return value * MINUTE_MS;
|
|
39
|
+
}
|
|
40
|
+
if (unit === "h") {
|
|
41
|
+
return value * HOUR_MS;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return value * DAY_MS;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseSizeToBytes(raw, fallbackBytes = 0) {
|
|
48
|
+
const text = String(raw || "").trim();
|
|
49
|
+
if (!text) {
|
|
50
|
+
return fallbackBytes;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const match = text.match(/^(\d+)([KMGT]?)$/i);
|
|
54
|
+
if (!match) {
|
|
55
|
+
return fallbackBytes;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const value = Number.parseInt(match[1], 10);
|
|
59
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
60
|
+
return fallbackBytes;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const unit = String(match[2] || "").toUpperCase();
|
|
64
|
+
const unitPow = {
|
|
65
|
+
"": 0,
|
|
66
|
+
K: 1,
|
|
67
|
+
M: 2,
|
|
68
|
+
G: 3,
|
|
69
|
+
T: 4,
|
|
70
|
+
}[unit];
|
|
71
|
+
|
|
72
|
+
if (unitPow == null) {
|
|
73
|
+
return fallbackBytes;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return value * (1024 ** unitPow);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseDailyAt(text, fallback = { hour: 3, minute: 0 }) {
|
|
80
|
+
const raw = String(text || "").trim();
|
|
81
|
+
const match = raw.match(/^(\d{1,2}):(\d{2})$/);
|
|
82
|
+
if (!match) {
|
|
83
|
+
return fallback;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const hour = Number.parseInt(match[1], 10);
|
|
87
|
+
const minute = Number.parseInt(match[2], 10);
|
|
88
|
+
|
|
89
|
+
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
|
|
90
|
+
return fallback;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { hour, minute };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function statIfFile(filePath) {
|
|
97
|
+
try {
|
|
98
|
+
const stats = await fs.promises.stat(filePath);
|
|
99
|
+
return stats.isFile() ? stats : null;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (error.code === "ENOENT") {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function getDiskFreeBytes(targetDir) {
|
|
109
|
+
if (typeof fs.promises.statfs !== "function") {
|
|
110
|
+
return Number.POSITIVE_INFINITY;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const info = await fs.promises.statfs(targetDir);
|
|
114
|
+
return Number(info.bavail || 0) * Number(info.bsize || 0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function walkFiles(dirPath, onFile) {
|
|
118
|
+
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
119
|
+
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
122
|
+
|
|
123
|
+
if (entry.isDirectory()) {
|
|
124
|
+
await walkFiles(fullPath, onFile);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!entry.isFile()) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await onFile(fullPath);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function getDirSizeBytes(dirPath) {
|
|
137
|
+
let total = 0;
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
await walkFiles(dirPath, async (filePath) => {
|
|
141
|
+
const stats = await statIfFile(filePath);
|
|
142
|
+
if (stats) {
|
|
143
|
+
total += stats.size;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error.code !== "ENOENT") {
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return total;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function formatBytes(bytes) {
|
|
156
|
+
const value = Number(bytes || 0);
|
|
157
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
158
|
+
return "0B";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (value >= 1024 ** 4) {
|
|
162
|
+
return `${(value / (1024 ** 4)).toFixed(2)}TB`;
|
|
163
|
+
}
|
|
164
|
+
if (value >= 1024 ** 3) {
|
|
165
|
+
return `${(value / (1024 ** 3)).toFixed(2)}GB`;
|
|
166
|
+
}
|
|
167
|
+
if (value >= 1024 ** 2) {
|
|
168
|
+
return `${(value / (1024 ** 2)).toFixed(2)}MB`;
|
|
169
|
+
}
|
|
170
|
+
if (value >= 1024) {
|
|
171
|
+
return `${(value / 1024).toFixed(2)}KB`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return `${Math.floor(value)}B`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export class CacheCleanupManager {
|
|
178
|
+
constructor(config) {
|
|
179
|
+
this.config = config;
|
|
180
|
+
this.enabled = toBool(config.cacheCleanupEnabled, true);
|
|
181
|
+
this.touchOnHit = toBool(config.cacheTouchOnHit, true);
|
|
182
|
+
this.touchMinIntervalMs = Math.max(0, parseDurationToMs(config.cacheTouchMinInterval, DAY_MS));
|
|
183
|
+
this.retentionStartDays = Math.max(1, Math.floor(parseDurationToMs(config.cacheRetentionStart, 10 * DAY_MS) / DAY_MS));
|
|
184
|
+
this.retentionMinDays = Math.max(1, Math.floor(parseDurationToMs(config.cacheRetentionMin, DAY_MS) / DAY_MS));
|
|
185
|
+
this.diskFreeTriggerBytes = Math.max(0, parseSizeToBytes(config.cacheDiskFreeTrigger, 20 * 1024 ** 3));
|
|
186
|
+
this.diskFreeTargetBytes = Math.max(
|
|
187
|
+
this.diskFreeTriggerBytes,
|
|
188
|
+
parseSizeToBytes(config.cacheDiskFreeTarget, 25 * 1024 ** 3),
|
|
189
|
+
);
|
|
190
|
+
this.cacheMaxSizeBytes = Math.max(0, parseSizeToBytes(config.cacheMaxSize, 0));
|
|
191
|
+
this.cacheTargetSizeBytes = Math.max(
|
|
192
|
+
0,
|
|
193
|
+
parseSizeToBytes(config.cacheTargetSize, this.cacheMaxSizeBytes || 0),
|
|
194
|
+
);
|
|
195
|
+
this.checkMinIntervalMs = Math.max(MINUTE_MS, parseDurationToMs(config.cacheCleanupCheckMinInterval, 10 * MINUTE_MS));
|
|
196
|
+
this.dailyAt = parseDailyAt(config.cacheCleanupDailyAt, { hour: 3, minute: 0 });
|
|
197
|
+
|
|
198
|
+
this.lastTouchAt = new Map();
|
|
199
|
+
this.lastPressureCheckAt = 0;
|
|
200
|
+
this.cleanupRunning = false;
|
|
201
|
+
this.dailyTimer = null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async init() {
|
|
205
|
+
if (!this.enabled) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this.#scheduleDailyCheck();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async destroy() {
|
|
213
|
+
if (this.dailyTimer) {
|
|
214
|
+
clearTimeout(this.dailyTimer);
|
|
215
|
+
this.dailyTimer = null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
touchFileOnHit(filePath) {
|
|
220
|
+
if (!this.enabled || !this.touchOnHit || !filePath) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const now = Date.now();
|
|
225
|
+
const last = this.lastTouchAt.get(filePath) || 0;
|
|
226
|
+
if (now - last < this.touchMinIntervalMs) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
this.lastTouchAt.set(filePath, now);
|
|
231
|
+
|
|
232
|
+
fs.promises.utimes(filePath, new Date(now), new Date(now)).catch((error) => {
|
|
233
|
+
if (error.code !== "ENOENT") {
|
|
234
|
+
console.warn(`[cache-cleanup] touch failed path=${filePath} message=${error.message}`);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async checkAndCleanupIfNeeded(reason = "manual", force = false) {
|
|
240
|
+
if (!this.enabled) {
|
|
241
|
+
return { triggered: false, reason: "disabled" };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const now = Date.now();
|
|
245
|
+
if (!force && now - this.lastPressureCheckAt < this.checkMinIntervalMs) {
|
|
246
|
+
return { triggered: false, reason: "throttled" };
|
|
247
|
+
}
|
|
248
|
+
this.lastPressureCheckAt = now;
|
|
249
|
+
|
|
250
|
+
const metrics = await this.#collectMetrics();
|
|
251
|
+
const overLimit = this.#isOverLimit(metrics);
|
|
252
|
+
|
|
253
|
+
if (!overLimit) {
|
|
254
|
+
return {
|
|
255
|
+
triggered: false,
|
|
256
|
+
reason: "below-limit",
|
|
257
|
+
metrics,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return this.#runCleanup(reason, metrics);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async #runCleanup(reason, beforeMetrics) {
|
|
265
|
+
if (this.cleanupRunning) {
|
|
266
|
+
return { triggered: false, reason: "already-running" };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
this.cleanupRunning = true;
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
console.warn(
|
|
273
|
+
`[cache-cleanup] start reason=${reason} free=${formatBytes(beforeMetrics.diskFreeBytes)} cache=${formatBytes(beforeMetrics.cacheSizeBytes)}`,
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const rounds = [];
|
|
277
|
+
const startDay = Math.max(this.retentionStartDays, this.retentionMinDays);
|
|
278
|
+
const endDay = this.retentionMinDays;
|
|
279
|
+
|
|
280
|
+
for (let day = startDay; day >= endDay; day -= 1) {
|
|
281
|
+
const cutoffMs = Date.now() - (day * DAY_MS);
|
|
282
|
+
const result = await this.#deleteOlderThan(cutoffMs);
|
|
283
|
+
const metrics = await this.#collectMetrics();
|
|
284
|
+
|
|
285
|
+
rounds.push({
|
|
286
|
+
day,
|
|
287
|
+
deletedFiles: result.deletedFiles,
|
|
288
|
+
releasedBytes: result.releasedBytes,
|
|
289
|
+
diskFreeBytes: metrics.diskFreeBytes,
|
|
290
|
+
cacheSizeBytes: metrics.cacheSizeBytes,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
console.warn(
|
|
294
|
+
`[cache-cleanup] round day=${day} deleted=${result.deletedFiles} released=${formatBytes(result.releasedBytes)} free=${formatBytes(metrics.diskFreeBytes)} cache=${formatBytes(metrics.cacheSizeBytes)}`,
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
if (this.#meetsTarget(metrics)) {
|
|
298
|
+
console.warn(`[cache-cleanup] success reason=${reason} stop-day=${day}`);
|
|
299
|
+
return {
|
|
300
|
+
triggered: true,
|
|
301
|
+
success: true,
|
|
302
|
+
stopDay: day,
|
|
303
|
+
before: beforeMetrics,
|
|
304
|
+
after: metrics,
|
|
305
|
+
rounds,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const afterMetrics = await this.#collectMetrics();
|
|
311
|
+
console.error(
|
|
312
|
+
`[cache-cleanup] warn cannot meet target at min-day=${this.retentionMinDays} free=${formatBytes(afterMetrics.diskFreeBytes)} cache=${formatBytes(afterMetrics.cacheSizeBytes)}`,
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
triggered: true,
|
|
317
|
+
success: false,
|
|
318
|
+
stopDay: this.retentionMinDays,
|
|
319
|
+
before: beforeMetrics,
|
|
320
|
+
after: afterMetrics,
|
|
321
|
+
rounds,
|
|
322
|
+
};
|
|
323
|
+
} finally {
|
|
324
|
+
this.cleanupRunning = false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async #deleteOlderThan(cutoffMs) {
|
|
329
|
+
let deletedFiles = 0;
|
|
330
|
+
let releasedBytes = 0;
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
await walkFiles(this.config.cacheDir, async (filePath) => {
|
|
334
|
+
const stats = await statIfFile(filePath);
|
|
335
|
+
if (!stats) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (stats.mtimeMs >= cutoffMs) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
await fs.promises.unlink(filePath);
|
|
345
|
+
deletedFiles += 1;
|
|
346
|
+
releasedBytes += stats.size;
|
|
347
|
+
this.lastTouchAt.delete(filePath);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
if (error.code !== "ENOENT") {
|
|
350
|
+
console.warn(`[cache-cleanup] delete failed path=${filePath} message=${error.message}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
} catch (error) {
|
|
355
|
+
if (error.code !== "ENOENT") {
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return { deletedFiles, releasedBytes };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async #collectMetrics() {
|
|
364
|
+
const [diskFreeBytes, cacheSizeBytes] = await Promise.all([
|
|
365
|
+
getDiskFreeBytes(this.config.cacheDir),
|
|
366
|
+
getDirSizeBytes(this.config.cacheDir),
|
|
367
|
+
]);
|
|
368
|
+
|
|
369
|
+
return { diskFreeBytes, cacheSizeBytes };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
#isOverLimit(metrics) {
|
|
373
|
+
const diskLow = this.diskFreeTriggerBytes > 0 && metrics.diskFreeBytes <= this.diskFreeTriggerBytes;
|
|
374
|
+
const cacheOver = this.cacheMaxSizeBytes > 0 && metrics.cacheSizeBytes >= this.cacheMaxSizeBytes;
|
|
375
|
+
return diskLow || cacheOver;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
#meetsTarget(metrics) {
|
|
379
|
+
const diskOk =
|
|
380
|
+
this.diskFreeTriggerBytes <= 0 ||
|
|
381
|
+
metrics.diskFreeBytes >= this.diskFreeTargetBytes;
|
|
382
|
+
|
|
383
|
+
const cacheTarget = this.cacheTargetSizeBytes > 0 ? this.cacheTargetSizeBytes : this.cacheMaxSizeBytes;
|
|
384
|
+
const cacheOk =
|
|
385
|
+
this.cacheMaxSizeBytes <= 0 ||
|
|
386
|
+
(cacheTarget > 0 && metrics.cacheSizeBytes <= cacheTarget);
|
|
387
|
+
|
|
388
|
+
return diskOk && cacheOk;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
#scheduleDailyCheck() {
|
|
392
|
+
const now = new Date();
|
|
393
|
+
const next = new Date(now);
|
|
394
|
+
next.setHours(this.dailyAt.hour, this.dailyAt.minute, 0, 0);
|
|
395
|
+
|
|
396
|
+
if (next.getTime() <= now.getTime()) {
|
|
397
|
+
next.setDate(next.getDate() + 1);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const delay = Math.max(1000, next.getTime() - now.getTime());
|
|
401
|
+
|
|
402
|
+
this.dailyTimer = setTimeout(() => {
|
|
403
|
+
this.checkAndCleanupIfNeeded("daily-check", true)
|
|
404
|
+
.catch((error) => {
|
|
405
|
+
console.error(`[cache-cleanup] daily check failed: ${error.message}`);
|
|
406
|
+
})
|
|
407
|
+
.finally(() => {
|
|
408
|
+
this.#scheduleDailyCheck();
|
|
409
|
+
});
|
|
410
|
+
}, delay);
|
|
411
|
+
|
|
412
|
+
if (typeof this.dailyTimer.unref === "function") {
|
|
413
|
+
this.dailyTimer.unref();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
@@ -75,6 +75,8 @@ export class MavenAffinityIndex {
|
|
|
75
75
|
this.snapshotPath = path.join(this.indexDir, "maven-affinity.snapshot.json");
|
|
76
76
|
this.eventLogPath = path.join(this.indexDir, "maven-affinity.events.log");
|
|
77
77
|
|
|
78
|
+
// Positive entries are persistent and have no TTL. They are removed only
|
|
79
|
+
// when the cache file disappears or a conflict is detected.
|
|
78
80
|
this.positive = new Map();
|
|
79
81
|
this.negative = new Map();
|
|
80
82
|
this.conflicts = new Map();
|
|
@@ -269,7 +271,7 @@ export class MavenAffinityIndex {
|
|
|
269
271
|
return true;
|
|
270
272
|
}
|
|
271
273
|
|
|
272
|
-
recordSuccess({ canonicalKey, host, cachePath, fileName }) {
|
|
274
|
+
recordSuccess({ canonicalKey, host, cachePath, fileName, urlObj = null }) {
|
|
273
275
|
if (!this.enabled || !canonicalKey || !cachePath || !fileName) {
|
|
274
276
|
return;
|
|
275
277
|
}
|
|
@@ -309,8 +311,9 @@ export class MavenAffinityIndex {
|
|
|
309
311
|
},
|
|
310
312
|
});
|
|
311
313
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
+
const successScope = buildNegativeScope(urlObj);
|
|
315
|
+
if (successScope) {
|
|
316
|
+
const negativeKey = buildNegativeKey(successScope, canonicalKey);
|
|
314
317
|
if (this.negative.has(negativeKey)) {
|
|
315
318
|
this.#applyEvent({
|
|
316
319
|
type: "negative_remove",
|
package/src/config/config.js
CHANGED
|
@@ -99,6 +99,38 @@ function toInt(value, defaultValue) {
|
|
|
99
99
|
return Number.isFinite(parsed) ? parsed : defaultValue;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
function parseDurationToMs(value, fallbackMs) {
|
|
103
|
+
const text = String(value || "").trim();
|
|
104
|
+
if (!text) {
|
|
105
|
+
return fallbackMs;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const match = text.match(/^(\d+)([smhd])$/i);
|
|
109
|
+
if (!match) {
|
|
110
|
+
return fallbackMs;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const amount = Number.parseInt(match[1], 10);
|
|
114
|
+
const unit = String(match[2] || "").toLowerCase();
|
|
115
|
+
if (!Number.isFinite(amount) || amount < 0) {
|
|
116
|
+
return fallbackMs;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (unit === "s") {
|
|
120
|
+
return amount * 1000;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (unit === "m") {
|
|
124
|
+
return amount * 60 * 1000;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (unit === "h") {
|
|
128
|
+
return amount * 60 * 60 * 1000;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return amount * 24 * 60 * 60 * 1000;
|
|
132
|
+
}
|
|
133
|
+
|
|
102
134
|
function toList(value, defaultValue = []) {
|
|
103
135
|
if (!value) {
|
|
104
136
|
return defaultValue;
|
|
@@ -181,10 +213,17 @@ const defaultMavenRepoDomains = [
|
|
|
181
213
|
const cacheDir = path.resolve(configBaseDir, process.env.CACHE_DIR || "data/cache");
|
|
182
214
|
|
|
183
215
|
const multiThreadMinSizeBytes = Math.max(0, toInt(process.env.MULTI_THREAD_MIN_SIZE_MB, 1)) * 1024 * 1024;
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
const
|
|
216
|
+
const downloadTimeout = process.env.DOWNLOAD_TIMEOUT || "60s";
|
|
217
|
+
const outboundKeepAliveInterval = process.env.OUTBOUND_KEEP_ALIVE_INTERVAL || "1s";
|
|
218
|
+
const mavenNegativeCacheTtl = process.env.MAVEN_NEGATIVE_CACHE_TTL || "24h";
|
|
219
|
+
const mavenAffinityFlushInterval = process.env.MAVEN_AFFINITY_FLUSH_INTERVAL || "5s";
|
|
220
|
+
const logRetention = process.env.LOG_RETENTION || "7d";
|
|
221
|
+
|
|
222
|
+
const downloadTimeoutMs = Math.max(1, parseDurationToMs(downloadTimeout, 60 * 1000));
|
|
223
|
+
const outboundKeepAliveMsecs = Math.max(1, parseDurationToMs(outboundKeepAliveInterval, 1000));
|
|
224
|
+
const mavenNegativeCacheTtlMs = Math.max(1, parseDurationToMs(mavenNegativeCacheTtl, 24 * 60 * 60 * 1000));
|
|
225
|
+
const mavenAffinityFlushIntervalMs = Math.max(1, parseDurationToMs(mavenAffinityFlushInterval, 5 * 1000));
|
|
226
|
+
const logRetentionDays = Math.max(1, Math.ceil(parseDurationToMs(logRetention, 7 * 24 * 60 * 60 * 1000) / (24 * 60 * 60 * 1000)));
|
|
188
227
|
const mavenAffinityEventMaxBytes = Math.max(1, toInt(process.env.MAVEN_AFFINITY_EVENT_MAX_MB, 8)) * 1024 * 1024;
|
|
189
228
|
|
|
190
229
|
export const config = {
|
|
@@ -205,19 +244,36 @@ export const config = {
|
|
|
205
244
|
multiThreadDomains: toList(process.env.MULTI_THREAD_DOMAINS, ["repo1.maven.org"]),
|
|
206
245
|
multiThreadCount: Math.max(1, toInt(process.env.MULTI_THREAD_COUNT, 4)),
|
|
207
246
|
multiThreadMinSizeBytes,
|
|
247
|
+
downloadTimeout,
|
|
208
248
|
downloadTimeoutMs,
|
|
209
249
|
outboundKeepAlive: toBool(process.env.OUTBOUND_KEEP_ALIVE, true),
|
|
250
|
+
outboundKeepAliveInterval,
|
|
210
251
|
outboundKeepAliveMsecs,
|
|
211
252
|
outboundMaxSockets: Math.max(1, toInt(process.env.OUTBOUND_MAX_SOCKETS, 64)),
|
|
212
253
|
outboundMaxFreeSockets: Math.max(1, toInt(process.env.OUTBOUND_MAX_FREE_SOCKETS, 16)),
|
|
213
254
|
mavenAffinityEnabled: toBool(process.env.MAVEN_AFFINITY_ENABLED, true),
|
|
214
|
-
mavenAffinityIndexDir: path.resolve(
|
|
255
|
+
mavenAffinityIndexDir: path.resolve(configBaseDir, process.env.MAVEN_AFFINITY_INDEX_DIR || "data/index"),
|
|
256
|
+
mavenNegativeCacheTtl,
|
|
215
257
|
mavenNegativeCacheTtlMs,
|
|
258
|
+
mavenAffinityFlushInterval,
|
|
216
259
|
mavenAffinityFlushIntervalMs,
|
|
217
260
|
mavenAffinityEventMaxBytes,
|
|
261
|
+
cacheCleanupEnabled: toBool(process.env.CACHE_CLEANUP_ENABLED, true),
|
|
262
|
+
cacheCleanupDailyAt: process.env.CACHE_CLEANUP_DAILY_AT || "03:00",
|
|
263
|
+
cacheCleanupCheckMinInterval: process.env.CACHE_CLEANUP_CHECK_MIN_INTERVAL || "10m",
|
|
264
|
+
cacheTouchOnHit: toBool(process.env.CACHE_TOUCH_ON_HIT, true),
|
|
265
|
+
cacheTouchMinInterval: process.env.CACHE_TOUCH_MIN_INTERVAL || "1d",
|
|
266
|
+
cacheRetentionStart: process.env.CACHE_RETENTION_START || "10d",
|
|
267
|
+
cacheRetentionMin: process.env.CACHE_RETENTION_MIN || "1d",
|
|
268
|
+
cacheDiskFreeTrigger: process.env.CACHE_DISK_FREE_TRIGGER || "20G",
|
|
269
|
+
cacheDiskFreeTarget: process.env.CACHE_DISK_FREE_TARGET || "25G",
|
|
270
|
+
cacheMaxSize: process.env.CACHE_MAX_SIZE || "",
|
|
271
|
+
cacheTargetSize: process.env.CACHE_TARGET_SIZE || "",
|
|
218
272
|
downloadLogDir: path.resolve(configBaseDir, process.env.DOWNLOAD_LOG_DIR || "data/logs/downloads"),
|
|
219
|
-
|
|
273
|
+
logRetention,
|
|
274
|
+
logRetentionDays,
|
|
220
275
|
logToStdout: toBool(process.env.LOG_TO_STDOUT, true),
|
|
276
|
+
logConnectEvents: toBool(process.env.LOG_CONNECT_EVENTS, false),
|
|
221
277
|
certDir: path.resolve(configBaseDir, process.env.CERT_DIR || "data/certs"),
|
|
222
278
|
rootCertPath: path.resolve(configBaseDir, process.env.ROOT_CERT_PATH || "data/certs/root-ca.crt"),
|
|
223
279
|
rootKeyPath: path.resolve(configBaseDir, process.env.ROOT_KEY_PATH || "data/certs/root-ca.key.pem"),
|
|
@@ -230,7 +286,7 @@ export const config = {
|
|
|
230
286
|
javaHome: javaHomeResolution.javaHome,
|
|
231
287
|
javaHomeSource: javaHomeResolution.source,
|
|
232
288
|
javaHomeConfigured: javaHomeResolution.configuredJavaHome || "",
|
|
233
|
-
httpsPassthroughForUnmatched: toBool(process.env.HTTPS_PASSTHROUGH_FOR_UNMATCHED,
|
|
289
|
+
httpsPassthroughForUnmatched: toBool(process.env.HTTPS_PASSTHROUGH_FOR_UNMATCHED, false),
|
|
234
290
|
upstreamProxyUrl: normalizeProxyUrl(process.env.UPSTREAM_PROXY_URL || process.env.ALL_PROXY || process.env.all_proxy || ""),
|
|
235
291
|
upstreamHttpProxyUrl: normalizeProxyUrl(process.env.UPSTREAM_HTTP_PROXY_URL || process.env.HTTP_PROXY || process.env.http_proxy || ""),
|
|
236
292
|
upstreamHttpsProxyUrl: normalizeProxyUrl(process.env.UPSTREAM_HTTPS_PROXY_URL || process.env.HTTPS_PROXY || process.env.https_proxy || ""),
|
package/src/index.js
CHANGED
|
@@ -8,6 +8,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
10
|
import { MavenAffinityIndex } from "./cache/maven-affinity-index.js";
|
|
11
|
+
import { CacheCleanupManager } from "./cache/cache-cleanup-manager.js";
|
|
11
12
|
import { installConsoleLogFileMirror, installGlobalErrorLogging } from "./common/console-log-file.js";
|
|
12
13
|
|
|
13
14
|
installConsoleLogFileMirror({
|
|
@@ -17,6 +18,53 @@ installConsoleLogFileMirror({
|
|
|
17
18
|
});
|
|
18
19
|
installGlobalErrorLogging();
|
|
19
20
|
|
|
21
|
+
function startupInfo(message) {
|
|
22
|
+
if (!config.logToStdout) {
|
|
23
|
+
process.stdout.write(`${message}\n`);
|
|
24
|
+
}
|
|
25
|
+
console.log(message);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function startupError(message, error = null) {
|
|
29
|
+
if (!config.logToStdout) {
|
|
30
|
+
process.stderr.write(`${message}\n`);
|
|
31
|
+
if (error) {
|
|
32
|
+
process.stderr.write(`${error?.stack || error?.message || String(error)}\n`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (error) {
|
|
36
|
+
console.error(message, error);
|
|
37
|
+
} else {
|
|
38
|
+
console.error(message);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function waitForServerListening(server, name) {
|
|
43
|
+
if (server?.listening) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await new Promise((resolve, reject) => {
|
|
48
|
+
const onListening = () => {
|
|
49
|
+
cleanup();
|
|
50
|
+
resolve();
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const onError = (error) => {
|
|
54
|
+
cleanup();
|
|
55
|
+
reject(new Error(`${name} listen failed: ${error.message}`));
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const cleanup = () => {
|
|
59
|
+
server.off("listening", onListening);
|
|
60
|
+
server.off("error", onError);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
server.once("listening", onListening);
|
|
64
|
+
server.once("error", onError);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
20
68
|
async function main() {
|
|
21
69
|
await fs.promises.mkdir(config.cacheDir, { recursive: true });
|
|
22
70
|
await fs.promises.mkdir(config.mavenCacheDir, { recursive: true });
|
|
@@ -38,6 +86,8 @@ async function main() {
|
|
|
38
86
|
const upstreamProxyManager = new UpstreamProxyManager(config, matchesDomain);
|
|
39
87
|
const mavenAffinityIndex = new MavenAffinityIndex(config);
|
|
40
88
|
await mavenAffinityIndex.init();
|
|
89
|
+
const cacheCleanupManager = new CacheCleanupManager(config);
|
|
90
|
+
await cacheCleanupManager.init();
|
|
41
91
|
|
|
42
92
|
const downloader = new Downloader(config, matchesDomain, upstreamProxyManager);
|
|
43
93
|
|
|
@@ -48,55 +98,75 @@ async function main() {
|
|
|
48
98
|
matchesDomain,
|
|
49
99
|
upstreamProxyManager,
|
|
50
100
|
mavenAffinityIndex,
|
|
101
|
+
cacheCleanupManager,
|
|
51
102
|
);
|
|
52
|
-
const repoServer = startRepoServer(config, downloader);
|
|
103
|
+
const repoServer = startRepoServer(config, downloader, cacheCleanupManager);
|
|
104
|
+
|
|
105
|
+
await Promise.all([
|
|
106
|
+
waitForServerListening(proxyServer, "proxy server"),
|
|
107
|
+
waitForServerListening(repoServer, "repo server"),
|
|
108
|
+
]);
|
|
53
109
|
|
|
54
110
|
const trustCommands = getTrustStoreCommands(config);
|
|
55
111
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
112
|
+
startupInfo("[maven-proxy] started");
|
|
113
|
+
startupInfo(`[maven-proxy] config mode: ${config.configMode}`);
|
|
114
|
+
startupInfo(`[maven-proxy] config file: ${config.loadedConfigFile || "(none)"}`);
|
|
115
|
+
startupInfo(`[maven-proxy] config base: ${config.configBaseDir}`);
|
|
60
116
|
if (config.configMode === "user") {
|
|
61
|
-
|
|
117
|
+
startupInfo(`[maven-proxy] default user config: ${config.defaultUserConfigPath}`);
|
|
62
118
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
119
|
+
startupInfo(`[maven-proxy] proxy port: ${config.proxyPort}`);
|
|
120
|
+
startupInfo(`[maven-proxy] repo port: ${config.repoPort}`);
|
|
121
|
+
startupInfo(`[maven-proxy] cache dir : ${config.cacheDir}`);
|
|
122
|
+
startupInfo(`[maven-proxy] cache maven: ${config.mavenCacheDir}`);
|
|
123
|
+
startupInfo(`[maven-proxy] cache npm : ${config.npmCacheDir}`);
|
|
124
|
+
startupInfo(`[maven-proxy] cache other: ${config.genericCacheDir}`);
|
|
125
|
+
startupInfo(`[maven-proxy] log dir: ${config.downloadLogDir}`);
|
|
126
|
+
startupInfo(`[maven-proxy] log retention: ${config.logRetention} (${config.logRetentionDays}d)`);
|
|
127
|
+
startupInfo(`[maven-proxy] log to stdout: ${config.logToStdout}`);
|
|
128
|
+
startupInfo(`[maven-proxy] log connect events: ${config.logConnectEvents}`);
|
|
129
|
+
startupInfo(`[maven-proxy] outbound keep-alive: ${config.outboundKeepAlive}`);
|
|
130
|
+
startupInfo(`[maven-proxy] outbound keepAlive interval: ${config.outboundKeepAliveInterval}`);
|
|
131
|
+
startupInfo(`[maven-proxy] outbound maxSockets: ${config.outboundMaxSockets}`);
|
|
132
|
+
startupInfo(`[maven-proxy] outbound maxFreeSockets: ${config.outboundMaxFreeSockets}`);
|
|
133
|
+
startupInfo(`[maven-proxy] maven affinity enabled: ${config.mavenAffinityEnabled}`);
|
|
134
|
+
startupInfo(`[maven-proxy] maven affinity index dir: ${config.mavenAffinityIndexDir}`);
|
|
135
|
+
startupInfo(`[maven-proxy] maven negative cache ttl: ${config.mavenNegativeCacheTtl}`);
|
|
136
|
+
startupInfo(`[maven-proxy] maven affinity flush interval: ${config.mavenAffinityFlushInterval}`);
|
|
137
|
+
startupInfo(`[maven-proxy] download timeout: ${config.downloadTimeout}`);
|
|
138
|
+
startupInfo(`[maven-proxy] maven affinity event max(MB): ${config.mavenAffinityEventMaxBytes / (1024 * 1024)}`);
|
|
139
|
+
startupInfo(`[maven-proxy] cache cleanup enabled: ${config.cacheCleanupEnabled}`);
|
|
140
|
+
startupInfo(`[maven-proxy] cache cleanup daily at: ${config.cacheCleanupDailyAt}`);
|
|
141
|
+
startupInfo(`[maven-proxy] cache touch on hit: ${config.cacheTouchOnHit}`);
|
|
142
|
+
startupInfo(`[maven-proxy] cache touch min interval: ${config.cacheTouchMinInterval}`);
|
|
143
|
+
startupInfo(`[maven-proxy] cache retention start: ${config.cacheRetentionStart}`);
|
|
144
|
+
startupInfo(`[maven-proxy] cache retention min: ${config.cacheRetentionMin}`);
|
|
145
|
+
startupInfo(`[maven-proxy] cache disk free trigger: ${config.cacheDiskFreeTrigger}`);
|
|
146
|
+
startupInfo(`[maven-proxy] cache disk free target: ${config.cacheDiskFreeTarget}`);
|
|
147
|
+
startupInfo(`[maven-proxy] cache max size: ${config.cacheMaxSize || "(disabled)"}`);
|
|
148
|
+
startupInfo(`[maven-proxy] cache target size: ${config.cacheTargetSize || "(disabled)"}`);
|
|
149
|
+
startupInfo(`[maven-proxy] root cert : ${config.rootCertPath}`);
|
|
150
|
+
startupInfo(`[maven-proxy] repo fallback repos: ${(config.repoFallbackRepos || []).join(",") || "(none)"}`);
|
|
83
151
|
if (config.upstreamProxyUrl || config.upstreamHttpProxyUrl || config.upstreamHttpsProxyUrl) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
152
|
+
startupInfo(`[maven-proxy] upstream proxy (generic): ${config.upstreamProxyUrl || "(none)"}`);
|
|
153
|
+
startupInfo(`[maven-proxy] upstream proxy (http) : ${config.upstreamHttpProxyUrl || "(none)"}`);
|
|
154
|
+
startupInfo(`[maven-proxy] upstream proxy (https) : ${config.upstreamHttpsProxyUrl || "(none)"}`);
|
|
155
|
+
startupInfo(`[maven-proxy] upstream no-proxy : ${(config.upstreamNoProxyDomains || []).join(",") || "(none)"}`);
|
|
156
|
+
startupInfo(`[maven-proxy] upstream ignore-domains : ${(config.upstreamIgnoreDomains || []).join(",") || "(none)"}`);
|
|
89
157
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
158
|
+
startupInfo("[maven-proxy] trust store command (copy):");
|
|
159
|
+
startupInfo(trustCommands.copyCmd);
|
|
160
|
+
startupInfo("[maven-proxy] trust store command (import):");
|
|
161
|
+
startupInfo(trustCommands.importCmd);
|
|
162
|
+
startupInfo(`[maven-proxy] startup success: proxy=127.0.0.1:${config.proxyPort}, repo=127.0.0.1:${config.repoPort}`);
|
|
94
163
|
|
|
95
164
|
const shutdown = () => {
|
|
96
165
|
proxyServer.close();
|
|
97
166
|
mitmHttpServer.close();
|
|
98
167
|
repoServer.close();
|
|
99
168
|
upstreamProxyManager.destroy();
|
|
169
|
+
void cacheCleanupManager.destroy();
|
|
100
170
|
void mavenAffinityIndex.destroy();
|
|
101
171
|
};
|
|
102
172
|
|
|
@@ -105,6 +175,6 @@ async function main() {
|
|
|
105
175
|
}
|
|
106
176
|
|
|
107
177
|
main().catch((error) => {
|
|
108
|
-
|
|
178
|
+
startupError("[maven-proxy] fatal error:", error);
|
|
109
179
|
process.exit(1);
|
|
110
180
|
});
|
|
@@ -20,7 +20,9 @@ async function openConnectUpstreamSocket(targetHost, targetPort, timeoutMs, upst
|
|
|
20
20
|
upstreamProxyManager.hasProxyFor("https:", targetHost);
|
|
21
21
|
|
|
22
22
|
if (useUpstreamProxy) {
|
|
23
|
-
|
|
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
|
-
|
|
86
|
+
if (certManager?.config?.logConnectEvents) {
|
|
87
|
+
console.log(`[proxy] MITM prepare ${targetHost}`);
|
|
88
|
+
}
|
|
85
89
|
const leaf = await certManager.getOrCreateLeaf(targetHost);
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
|
@@ -80,6 +80,12 @@ function sendErrorText(res, statusCode, message, context = "proxy") {
|
|
|
80
80
|
sendText(res, statusCode, message);
|
|
81
81
|
}
|
|
82
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
|
+
|
|
83
89
|
function buildUrl(req, forcedProtocol = null) {
|
|
84
90
|
const raw = req.url || "/";
|
|
85
91
|
if (/^https?:\/\//i.test(raw)) {
|
|
@@ -95,13 +101,17 @@ function buildUrl(req, forcedProtocol = null) {
|
|
|
95
101
|
return new URL(`${protocol}//${host}${raw}`);
|
|
96
102
|
}
|
|
97
103
|
|
|
98
|
-
async function serveFile(res, req, filePath) {
|
|
104
|
+
async function serveFile(res, req, filePath, cacheCleanupManager = null) {
|
|
99
105
|
const stats = await statIfFile(filePath);
|
|
100
106
|
if (!stats) {
|
|
101
107
|
sendText(res, 404, "Not Found");
|
|
102
108
|
return;
|
|
103
109
|
}
|
|
104
110
|
|
|
111
|
+
if (cacheCleanupManager) {
|
|
112
|
+
cacheCleanupManager.touchFileOnHit(filePath);
|
|
113
|
+
}
|
|
114
|
+
|
|
105
115
|
res.setHeader("content-length", String(stats.size));
|
|
106
116
|
if (!res.hasHeader("x-cache")) {
|
|
107
117
|
res.setHeader("x-cache", "HIT");
|
|
@@ -165,6 +175,7 @@ export function createHttpRequestHandler({
|
|
|
165
175
|
upstreamProxyManager = null,
|
|
166
176
|
matchesDomain,
|
|
167
177
|
mavenAffinityIndex = null,
|
|
178
|
+
cacheCleanupManager = null,
|
|
168
179
|
}) {
|
|
169
180
|
return async function handleHttpRequestPath(req, res, forcedProtocol = null) {
|
|
170
181
|
let urlObj;
|
|
@@ -203,16 +214,19 @@ export function createHttpRequestHandler({
|
|
|
203
214
|
|
|
204
215
|
const existing = await statIfFile(cachePath);
|
|
205
216
|
if (existing) {
|
|
206
|
-
|
|
217
|
+
console.log(`[proxy] local cache hit host=${urlObj.hostname} path=${urlObj.pathname}`);
|
|
218
|
+
await serveFile(res, req, cachePath, cacheCleanupManager);
|
|
207
219
|
return;
|
|
208
220
|
}
|
|
209
221
|
|
|
210
222
|
if (canonical && mavenAffinityIndex) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
223
|
+
if (isPositiveAffinityEligible(canonical.fileName)) {
|
|
224
|
+
const preferredPath = await mavenAffinityIndex.resolvePreferredCachePath(canonical.canonicalKey);
|
|
225
|
+
if (preferredPath) {
|
|
226
|
+
console.log(`[proxy] affinity hit canonical=${canonical.canonicalKey} host=${urlObj.hostname}`);
|
|
227
|
+
await serveFile(res, req, preferredPath, cacheCleanupManager);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
216
230
|
}
|
|
217
231
|
|
|
218
232
|
if (mavenAffinityIndex.shouldSkipRequest(canonical.canonicalKey, urlObj)) {
|
|
@@ -223,20 +237,25 @@ export function createHttpRequestHandler({
|
|
|
223
237
|
}
|
|
224
238
|
|
|
225
239
|
try {
|
|
240
|
+
console.log(`[proxy] local cache miss host=${urlObj.hostname} path=${urlObj.pathname}`);
|
|
241
|
+
if (cacheCleanupManager) {
|
|
242
|
+
await cacheCleanupManager.checkAndCleanupIfNeeded("cache-miss");
|
|
243
|
+
}
|
|
226
244
|
await fs.promises.mkdir(path.dirname(cachePath), { recursive: true });
|
|
227
245
|
await downloader.ensureCached(urlObj, cachePath, req.headers);
|
|
228
246
|
|
|
229
|
-
if (canonical && mavenAffinityIndex) {
|
|
247
|
+
if (canonical && mavenAffinityIndex && isPositiveAffinityEligible(canonical.fileName)) {
|
|
230
248
|
mavenAffinityIndex.recordSuccess({
|
|
231
249
|
canonicalKey: canonical.canonicalKey,
|
|
232
250
|
host: urlObj.hostname,
|
|
233
251
|
cachePath,
|
|
234
252
|
fileName: canonical.fileName,
|
|
253
|
+
urlObj,
|
|
235
254
|
});
|
|
236
255
|
}
|
|
237
256
|
|
|
238
257
|
res.setHeader("x-cache", "MISS");
|
|
239
|
-
await serveFile(res, req, cachePath);
|
|
258
|
+
await serveFile(res, req, cachePath, cacheCleanupManager);
|
|
240
259
|
} catch (error) {
|
|
241
260
|
if (
|
|
242
261
|
canonical &&
|
|
@@ -19,6 +19,7 @@ export function startProxyServer(
|
|
|
19
19
|
matchesDomain,
|
|
20
20
|
upstreamProxyManager = null,
|
|
21
21
|
mavenAffinityIndex = null,
|
|
22
|
+
cacheCleanupManager = null,
|
|
22
23
|
) {
|
|
23
24
|
const handleHttpRequestPath = createHttpRequestHandler({
|
|
24
25
|
config,
|
|
@@ -26,6 +27,7 @@ export function startProxyServer(
|
|
|
26
27
|
upstreamProxyManager,
|
|
27
28
|
matchesDomain,
|
|
28
29
|
mavenAffinityIndex,
|
|
30
|
+
cacheCleanupManager,
|
|
29
31
|
});
|
|
30
32
|
const mitmHttpServer = createMitmHttpServer(handleHttpRequestPath);
|
|
31
33
|
|
package/src/repo/repo-server.js
CHANGED
|
@@ -41,7 +41,7 @@ function buildCandidateRelativePaths(relativePath) {
|
|
|
41
41
|
return [...new Set(candidates.filter(Boolean))];
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
async function ensureFromRemoteRepos(config, downloader, filePath, relativePath) {
|
|
44
|
+
async function ensureFromRemoteRepos(config, downloader, filePath, relativePath, cacheCleanupManager = null) {
|
|
45
45
|
if (!downloader) {
|
|
46
46
|
return null;
|
|
47
47
|
}
|
|
@@ -60,6 +60,9 @@ async function ensureFromRemoteRepos(config, downloader, filePath, relativePath)
|
|
|
60
60
|
const remoteUrl = buildRemoteUrl(repoBase, candidatePath);
|
|
61
61
|
|
|
62
62
|
try {
|
|
63
|
+
if (cacheCleanupManager) {
|
|
64
|
+
await cacheCleanupManager.checkAndCleanupIfNeeded("repo-cache-miss");
|
|
65
|
+
}
|
|
63
66
|
console.log(`[repo] cache miss, try remote ${remoteUrl.href}`);
|
|
64
67
|
await downloader.ensureCached(remoteUrl, filePath, {});
|
|
65
68
|
return await statIfExists(filePath);
|
|
@@ -79,7 +82,7 @@ async function ensureFromRemoteRepos(config, downloader, filePath, relativePath)
|
|
|
79
82
|
return null;
|
|
80
83
|
}
|
|
81
84
|
|
|
82
|
-
export function startRepoServer(config, downloader = null) {
|
|
85
|
+
export function startRepoServer(config, downloader = null, cacheCleanupManager = null) {
|
|
83
86
|
const server = http.createServer(async (req, res) => {
|
|
84
87
|
try {
|
|
85
88
|
const urlObj = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
|
|
@@ -88,7 +91,7 @@ export function startRepoServer(config, downloader = null) {
|
|
|
88
91
|
let stats = await statIfExists(filePath);
|
|
89
92
|
|
|
90
93
|
if (!stats || !stats.isFile()) {
|
|
91
|
-
stats = await ensureFromRemoteRepos(config, downloader, filePath, relativePath);
|
|
94
|
+
stats = await ensureFromRemoteRepos(config, downloader, filePath, relativePath, cacheCleanupManager);
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
if (!stats || !stats.isFile()) {
|
|
@@ -100,6 +103,10 @@ export function startRepoServer(config, downloader = null) {
|
|
|
100
103
|
res.setHeader("content-length", String(stats.size));
|
|
101
104
|
res.setHeader("cache-control", "public, max-age=3600");
|
|
102
105
|
|
|
106
|
+
if (cacheCleanupManager) {
|
|
107
|
+
cacheCleanupManager.touchFileOnHit(filePath);
|
|
108
|
+
}
|
|
109
|
+
|
|
103
110
|
if (req.method === "HEAD") {
|
|
104
111
|
res.writeHead(200);
|
|
105
112
|
res.end();
|