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/README.md CHANGED
@@ -185,8 +185,8 @@ npm install
185
185
  2. Configure environment variables as needed.
186
186
 
187
187
  Notes:
188
- - Development mode (default): npm start loads .env first, then .evn as fallback alias.
189
- - User mode (CLI default): npx maven-proxy or global command uses ~/maven-proxy/config.
188
+ - Development mode (default): npm start loads config.properties in the project root.
189
+ - User mode (CLI default): npx maven-proxy or global command uses ~/maven-proxy/config.properties.
190
190
  - Override mode with MAVEN_PROXY_CONFIG_MODE as development or user.
191
191
  - Override config file path with MAVEN_PROXY_CONFIG_FILE.
192
192
  - JAVA_HOME supports auto-detection:
@@ -267,6 +267,12 @@ Environment variables:
267
267
  - HTTPS_MITM_DOMAINS: MITM domain list (includes registry.npmjs.org by default, wildcards supported).
268
268
  - DOWNLOAD_LOG_DIR: log directory.
269
269
  - LOG_RETENTION_DAYS: number of days to retain logs.
270
+ - LOG_TO_STDOUT: whether to also print runtime logs to stdout/stderr; startup logs are always printed.
271
+ - LOG_CONNECT_EVENTS: whether to print verbose CONNECT/MITM handshake logs. Default false.
272
+ - OUTBOUND_KEEP_ALIVE: enable outbound keep-alive connection pooling.
273
+ - OUTBOUND_KEEP_ALIVE_SECONDS: keep-alive interval in seconds.
274
+ - OUTBOUND_MAX_SOCKETS: max outbound sockets per origin.
275
+ - OUTBOUND_MAX_FREE_SOCKETS: max idle outbound sockets per origin.
270
276
  - MAVEN_PROXY_CONFIG_MODE: development or user.
271
277
  - MAVEN_PROXY_CONFIG_FILE: explicit config file path.
272
278
  - EXISTING_TRUST_STORE_PATH: optional existing truststore path. If present, truststore init prefers it as source.
@@ -289,15 +295,21 @@ Priority:
289
295
  - `REPO_FALLBACK_REPOS`: Comma-separated fallback repository URLs for cache misses.
290
296
  - `ENABLE_HTTPS_PROXY`: Enable HTTPS proxy handling (`true/false`).
291
297
  - `HTTPS_MITM_DOMAINS`: Comma-separated domains to apply MITM certificate issuance (wildcards supported).
292
- - `HTTPS_PASSTHROUGH_FOR_UNMATCHED`: Whether unmatched HTTPS domains are tunneled directly.
298
+ - `HTTPS_PASSTHROUGH_FOR_UNMATCHED`: Whether unmatched HTTPS domains are tunneled directly. Default `false`.
293
299
  - `NPM_REGISTRY_DOMAINS`: Domains treated as npm ecosystem for cache routing (wildcards supported).
294
300
  - `MAVEN_REPO_DOMAINS`: Domains treated as Maven ecosystem for cache routing (wildcards supported).
295
301
  - `MULTI_THREAD_DOMAINS`: Domains allowed to use multi-thread download (wildcards supported).
296
302
  - `MULTI_THREAD_COUNT`: Number of download threads for ranged downloads.
297
- - `MULTI_THREAD_MIN_SIZE_BYTES`: Minimum size threshold to trigger multi-thread download.
298
- - `DOWNLOAD_TIMEOUT_MS`: Upstream request timeout in milliseconds.
299
- - `DOWNLOAD_LOG_DIR`: Directory for download/console logs.
303
+ - `MULTI_THREAD_MIN_SIZE_MB`: Minimum size threshold to trigger multi-thread download (MB).
304
+ - `DOWNLOAD_TIMEOUT_SECONDS`: Upstream request timeout in seconds.
305
+ - `DOWNLOAD_LOG_DIR`: Directory for unified app/error logs.
300
306
  - `LOG_RETENTION_DAYS`: Number of days to keep log files.
307
+ - `LOG_TO_STDOUT`: Whether to also print runtime logs to stdout/stderr. Startup logs are always printed. Default `true`.
308
+ - `LOG_CONNECT_EVENTS`: Whether to print verbose CONNECT/MITM handshake logs. Default `false`.
309
+ - `OUTBOUND_KEEP_ALIVE`: Enable outbound keep-alive connection pooling. Default `true`.
310
+ - `OUTBOUND_KEEP_ALIVE_SECONDS`: Keep-alive interval in seconds. Default `1`.
311
+ - `OUTBOUND_MAX_SOCKETS`: Max outbound sockets per origin. Default `64`.
312
+ - `OUTBOUND_MAX_FREE_SOCKETS`: Max idle outbound sockets per origin. Default `16`.
301
313
  - `UPSTREAM_PROXY_URL`: Generic upstream proxy URL (fallback for HTTP/HTTPS).
302
314
  - `UPSTREAM_HTTP_PROXY_URL`: Upstream proxy URL for HTTP requests.
303
315
  - `UPSTREAM_HTTPS_PROXY_URL`: Upstream proxy URL for HTTPS requests.
@@ -388,18 +400,23 @@ maven-proxy
388
400
  ```
389
401
 
390
402
  Default CLI config path:
391
- - ~/maven-proxy/config
403
+ - ~/maven-proxy/config.properties
392
404
 
393
405
  Common commands:
394
406
  - maven-proxy --config /path/to/config
395
407
  - maven-proxy start --mode development
396
408
  - maven-proxy start --mode user
409
+ - maven-proxy stop
397
410
  - maven-proxy init-config --force
398
411
  - maven-proxy truststore print
399
412
  - maven-proxy truststore init
400
413
  - maven-proxy truststore merge --source /path/source.jks --target /path/target.jks
401
414
  - maven-proxy doctor
402
415
 
416
+ Start/stop behavior:
417
+ - `maven-proxy start` runs in background and returns immediately.
418
+ - `maven-proxy stop` stops the background process using PID file `~/maven-proxy/maven-proxy.pid`.
419
+
403
420
  Doctor command:
404
421
  - Checks config loading, port availability, keytool, JAVA_HOME, cert/truststore paths, and writable log/cache directories.
405
422
  - Reports PASS/WARN/FAIL.
@@ -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
- const defaultConfigFile = path.join(defaultConfigDir, "config");
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();
@@ -44,7 +48,7 @@ function resolveEffectiveMode(options) {
44
48
  return forced;
45
49
  }
46
50
 
47
- // CLI defaults to user mode to load ~/maven-proxy/config unless explicitly overridden.
51
+ // CLI defaults to user mode to load ~/maven-proxy/config.properties unless explicitly overridden.
48
52
  return "user";
49
53
  }
50
54
 
@@ -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,7 +67,8 @@ 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");
65
- console.log(" maven-proxy --config ~/maven-proxy/config");
70
+ console.log(" maven-proxy stop");
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");
68
74
  console.log(" maven-proxy doctor");
@@ -144,15 +150,25 @@ function getDefaultConfigTemplate() {
144
150
  "REPO_FALLBACK_REPOS=https://repo1.maven.org/maven2,https://jitpack.io,https://plugins.gradle.org/m2,https://maven.google.com",
145
151
  "ENABLE_HTTPS_PROXY=true",
146
152
  "HTTPS_MITM_DOMAINS=repo1.maven.org,repo.maven.apache.org,registry.npmjs.org",
147
- "HTTPS_PASSTHROUGH_FOR_UNMATCHED=true",
153
+ "HTTPS_PASSTHROUGH_FOR_UNMATCHED=false",
148
154
  "NPM_REGISTRY_DOMAINS=registry.npmjs.org,registry.npmmirror.com,npm.pkg.github.com",
149
155
  "MAVEN_REPO_DOMAINS=repo1.maven.org,repo.maven.apache.org,jitpack.io,plugins.gradle.org,maven.google.com",
150
156
  "MULTI_THREAD_DOMAINS=repo1.maven.org",
151
157
  "MULTI_THREAD_COUNT=8",
152
- "MULTI_THREAD_MIN_SIZE_BYTES=1048576",
153
- "DOWNLOAD_TIMEOUT_MS=60000",
158
+ "MULTI_THREAD_MIN_SIZE_MB=1",
159
+ "DOWNLOAD_TIMEOUT_SECONDS=60",
160
+ "OUTBOUND_KEEP_ALIVE=true",
161
+ "OUTBOUND_KEEP_ALIVE_SECONDS=1",
162
+ "OUTBOUND_MAX_SOCKETS=64",
163
+ "OUTBOUND_MAX_FREE_SOCKETS=16",
164
+ "MAVEN_AFFINITY_ENABLED=true",
165
+ "MAVEN_AFFINITY_INDEX_DIR=.index",
166
+ "MAVEN_NEGATIVE_CACHE_TTL_HOURS=24",
167
+ "MAVEN_AFFINITY_FLUSH_INTERVAL_SECONDS=5",
168
+ "MAVEN_AFFINITY_EVENT_MAX_MB=8",
154
169
  "DOWNLOAD_LOG_DIR=data/logs/downloads",
155
170
  "LOG_RETENTION_DAYS=7",
171
+ "LOG_TO_STDOUT=false",
156
172
  "UPSTREAM_PROXY_URL=",
157
173
  "UPSTREAM_HTTP_PROXY_URL=",
158
174
  "UPSTREAM_HTTPS_PROXY_URL=",
@@ -207,12 +223,145 @@ async function ensureAutoConfigIfNeeded(options, command) {
207
223
  await initConfigFile(defaultConfigFile, false);
208
224
  }
209
225
 
210
- async function startServer(options) {
226
+ async function runServerInCurrentProcess(options) {
211
227
  applyConfigOverrides(options);
212
228
 
213
229
  await import("../src/index.js");
214
230
  }
215
231
 
232
+ function parsePid(rawText) {
233
+ const pid = Number.parseInt(String(rawText || "").trim(), 10);
234
+ return Number.isFinite(pid) && pid > 0 ? pid : 0;
235
+ }
236
+
237
+ function readDaemonPid() {
238
+ if (!fs.existsSync(daemonPidFile)) {
239
+ return 0;
240
+ }
241
+
242
+ try {
243
+ const text = fs.readFileSync(daemonPidFile, "utf8");
244
+ return parsePid(text);
245
+ } catch {
246
+ return 0;
247
+ }
248
+ }
249
+
250
+ function isProcessRunning(pid) {
251
+ if (!pid) {
252
+ return false;
253
+ }
254
+
255
+ try {
256
+ process.kill(pid, 0);
257
+ return true;
258
+ } catch (error) {
259
+ if (error && error.code === "EPERM") {
260
+ return true;
261
+ }
262
+ return false;
263
+ }
264
+ }
265
+
266
+ async function removeDaemonPidFile() {
267
+ await fs.promises.rm(daemonPidFile, { force: true });
268
+ }
269
+
270
+ function waitMs(ms) {
271
+ return new Promise((resolve) => setTimeout(resolve, ms));
272
+ }
273
+
274
+ async function waitForProcessExit(pid, timeoutMs = 5000) {
275
+ const startedAt = Date.now();
276
+ while (Date.now() - startedAt < timeoutMs) {
277
+ if (!isProcessRunning(pid)) {
278
+ return true;
279
+ }
280
+ await waitMs(100);
281
+ }
282
+
283
+ return !isProcessRunning(pid);
284
+ }
285
+
286
+ async function startServer(options) {
287
+ await fs.promises.mkdir(defaultConfigDir, { recursive: true });
288
+
289
+ const existingPid = readDaemonPid();
290
+ if (existingPid && isProcessRunning(existingPid)) {
291
+ console.log(`[maven-proxy] already running (pid=${existingPid})`);
292
+ console.log(`[maven-proxy] pid file: ${daemonPidFile}`);
293
+ return;
294
+ }
295
+
296
+ if (existingPid && !isProcessRunning(existingPid)) {
297
+ await removeDaemonPidFile();
298
+ }
299
+
300
+ const childEnv = {
301
+ ...process.env,
302
+ MAVEN_PROXY_CONFIG_MODE: resolveEffectiveMode(options),
303
+ };
304
+
305
+ if (options.configPath) {
306
+ childEnv.MAVEN_PROXY_CONFIG_FILE = resolvePath(options.configPath);
307
+ } else {
308
+ delete childEnv.MAVEN_PROXY_CONFIG_FILE;
309
+ }
310
+
311
+ const child = spawn(
312
+ process.execPath,
313
+ [cliFilePath, internalRunCommand, "--mode", resolveEffectiveMode(options)],
314
+ {
315
+ cwd: process.cwd(),
316
+ detached: true,
317
+ stdio: "ignore",
318
+ env: childEnv,
319
+ },
320
+ );
321
+
322
+ child.unref();
323
+ await fs.promises.writeFile(daemonPidFile, `${child.pid}\n`, "utf8");
324
+
325
+ await waitMs(300);
326
+ if (!isProcessRunning(child.pid)) {
327
+ await removeDaemonPidFile();
328
+ throw new Error("start failed: process exited immediately, check app/error logs");
329
+ }
330
+
331
+ console.log(`[maven-proxy] started in background (pid=${child.pid})`);
332
+ console.log(`[maven-proxy] pid file: ${daemonPidFile}`);
333
+ }
334
+
335
+ async function stopServer(options) {
336
+ const pid = readDaemonPid();
337
+ if (!pid) {
338
+ console.log("[maven-proxy] not running (pid file not found)");
339
+ return;
340
+ }
341
+
342
+ if (!isProcessRunning(pid)) {
343
+ await removeDaemonPidFile();
344
+ console.log(`[maven-proxy] stale pid removed: ${pid}`);
345
+ return;
346
+ }
347
+
348
+ process.kill(pid, "SIGTERM");
349
+ const stopped = await waitForProcessExit(pid, 5000);
350
+ if (!stopped) {
351
+ process.kill(pid, "SIGKILL");
352
+ const forceStopped = await waitForProcessExit(pid, 2000);
353
+ if (!forceStopped) {
354
+ throw new Error(`stop failed: unable to terminate pid ${pid}`);
355
+ }
356
+ }
357
+
358
+ await removeDaemonPidFile();
359
+ console.log(`[maven-proxy] stopped (pid=${pid})`);
360
+ if (options.configPath) {
361
+ console.log(`[maven-proxy] stop requested with config: ${resolvePath(options.configPath)}`);
362
+ }
363
+ }
364
+
216
365
  function applyConfigOverrides(options) {
217
366
  process.env.MAVEN_PROXY_CONFIG_MODE = resolveEffectiveMode(options);
218
367
 
@@ -304,7 +453,7 @@ async function runDoctor(options) {
304
453
  printDoctorLine("WARN", "config file", `not found, expected ${config.defaultUserConfigPath}`);
305
454
  } else {
306
455
  warnings.push("development config file missing");
307
- printDoctorLine("WARN", "config file", "no .env/.evn loaded, using defaults");
456
+ printDoctorLine("WARN", "config file", "no config.properties loaded, using defaults");
308
457
  }
309
458
 
310
459
  const keytool = checkKeytool();
@@ -567,6 +716,14 @@ async function main() {
567
716
  return;
568
717
  }
569
718
 
719
+ if (command === "stop") {
720
+ if (options.commandArgs.length > 0) {
721
+ throw new Error(`Unknown argument for stop: ${options.commandArgs[0]}`);
722
+ }
723
+ await stopServer(options);
724
+ return;
725
+ }
726
+
570
727
  if (command === "start") {
571
728
  if (options.commandArgs.length > 0) {
572
729
  throw new Error(`Unknown argument for start: ${options.commandArgs[0]}`);
@@ -575,6 +732,14 @@ async function main() {
575
732
  return;
576
733
  }
577
734
 
735
+ if (command === internalRunCommand) {
736
+ if (options.commandArgs.length > 0) {
737
+ throw new Error(`Unknown argument for ${internalRunCommand}: ${options.commandArgs[0]}`);
738
+ }
739
+ await runServerInCurrentProcess(options);
740
+ return;
741
+ }
742
+
578
743
  throw new Error(`Unknown command: ${command}`);
579
744
  }
580
745
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maven-proxy",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
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",
@@ -33,7 +34,9 @@
33
34
  "npm:version:patch": "npm version patch",
34
35
  "npm:view:version": "npm view maven-proxy version",
35
36
  "npm:view:dist-tags": "npm view maven-proxy dist-tags",
36
- "test": "node -e \"console.log('No tests yet')\""
37
+ "replay:affinity": "node --test test/replay-affinity.test.js",
38
+ "test:replay": "node --test test/replay-affinity.test.js",
39
+ "test": "node --test test/*.test.js"
37
40
  },
38
41
  "keywords": [],
39
42
  "author": "",
@@ -3,7 +3,6 @@ import http from "node:http";
3
3
  import https from "node:https";
4
4
  import path from "node:path";
5
5
  import { pipeline } from "node:stream/promises";
6
- import { DownloadLogWriter } from "../common/download-log-writer.js";
7
6
 
8
7
  const REDIRECT_STATUS = new Set([301, 302, 303, 307, 308]);
9
8
  const MAX_REDIRECTS = 5;
@@ -288,7 +287,6 @@ export class Downloader {
288
287
  this.domainMatcher = domainMatcher;
289
288
  this.upstreamProxyManager = upstreamProxyManager;
290
289
  this.inflight = new Map();
291
- this.downloadLogWriter = new DownloadLogWriter(config.downloadLogDir, config.logRetentionDays);
292
290
  }
293
291
 
294
292
  logDownload(event, urlObj, details = {}) {
@@ -299,7 +297,6 @@ export class Downloader {
299
297
  .join(" ");
300
298
 
301
299
  console.log(`[downloader] ${event} url=${url}${detailText ? ` ${detailText}` : ""}`);
302
- this.downloadLogWriter.write(event, url, details);
303
300
  }
304
301
 
305
302
  async ensureCached(urlObj, finalPath, requestHeaders = {}) {