maven-proxy 1.3.0 → 1.3.2
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 +6 -0
- package/bin/maven-proxy.js +72 -54
- package/package.json +2 -1
- package/src/cache/cache-path.js +162 -1
- package/src/config/config.js +19 -2
- package/src/index.js +2 -0
- package/src/proxy/proxy-http-handler.js +11 -3
- package/src/repo/repo-server.js +142 -9
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | [中文](README-zh.md)
|
|
4
4
|
|
|
5
|
+
[English ChangeLog](ChangeLog.md) | [中文更新日志](ChangeLog-zh.md)
|
|
6
|
+
|
|
5
7
|
[](https://www.npmjs.com/package/maven-proxy)
|
|
6
8
|
|
|
7
9
|
## 1. Project Positioning
|
|
@@ -275,6 +277,8 @@ Environment variables:
|
|
|
275
277
|
- REPO_FALLBACK_REPOS: repository fallback list.
|
|
276
278
|
- NPM_REGISTRY_DOMAINS: npm domains for ecosystem routing (wildcards supported).
|
|
277
279
|
- MAVEN_REPO_DOMAINS: maven domains for ecosystem routing (wildcards supported).
|
|
280
|
+
- MAVEN_CACHE_USE_DOMAIN_DIR: whether Maven cache uses hostname as the first-level directory. Default false.
|
|
281
|
+
- MAVEN_CACHE_IGNORE_PATH_PREFIXES: Maven cache path-prefix ignore rules (comma-separated, supports host/path and host:port/path). Default: repo1.maven.org/maven2,repo.maven.apache.org/maven2,jitpack.io/,plugins.gradle.org/m2,dl.google.com/dl/android/maven2,dl.google.com/dl/google/maven.
|
|
278
282
|
- HTTPS_MITM_DOMAINS: MITM domain list (includes registry.npmjs.org by default, wildcards supported).
|
|
279
283
|
- DOWNLOAD_LOG_DIR: log directory.
|
|
280
284
|
- LOG_RETENTION: log retention duration (supports s/m/h/d), for example 7d.
|
|
@@ -325,6 +329,8 @@ Priority:
|
|
|
325
329
|
- `HTTPS_PASSTHROUGH_FOR_UNMATCHED`: Whether unmatched HTTPS domains are tunneled directly. Default `false`.
|
|
326
330
|
- `NPM_REGISTRY_DOMAINS`: Domains treated as npm ecosystem for cache routing (wildcards supported).
|
|
327
331
|
- `MAVEN_REPO_DOMAINS`: Domains treated as Maven ecosystem for cache routing (wildcards supported).
|
|
332
|
+
- `MAVEN_CACHE_USE_DOMAIN_DIR`: Whether Maven cache uses hostname as the first-level directory. Default `false`.
|
|
333
|
+
- `MAVEN_CACHE_IGNORE_PATH_PREFIXES`: Maven cache path-prefix ignore rules (comma-separated, supports `host/path` and `host:port/path`). Default `repo1.maven.org/maven2,repo.maven.apache.org/maven2,jitpack.io/,plugins.gradle.org/m2,dl.google.com/dl/android/maven2,dl.google.com/dl/google/maven`.
|
|
328
334
|
- `MULTI_THREAD_DOMAINS`: Domains allowed to use multi-thread download (wildcards supported).
|
|
329
335
|
- `MULTI_THREAD_COUNT`: Number of download threads for ranged downloads.
|
|
330
336
|
- `MULTI_THREAD_MIN_SIZE_MB`: Minimum size threshold to trigger multi-thread download (MB).
|
package/bin/maven-proxy.js
CHANGED
|
@@ -140,63 +140,81 @@ function resolvePath(inputPath) {
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
function getDefaultConfigTemplate() {
|
|
143
|
-
|
|
143
|
+
const lines = [
|
|
144
144
|
"# Maven Proxy default user config",
|
|
145
|
+
"# Maven Proxy 用户模式默认配置",
|
|
145
146
|
"# This file is loaded by default in user mode.",
|
|
146
147
|
"",
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
"
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
"
|
|
172
|
-
"
|
|
173
|
-
"
|
|
174
|
-
"
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
const appendEntry = (key, value, zhComment, enComment) => {
|
|
151
|
+
lines.push(`# 中文: ${zhComment}`);
|
|
152
|
+
lines.push(`# English: ${enComment}`);
|
|
153
|
+
lines.push(`${key}=${value}`);
|
|
154
|
+
lines.push("");
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
appendEntry("PROXY_PORT", "8080", "代理服务端口,默认 8080。", "Proxy service port. Default: 8080.");
|
|
158
|
+
appendEntry("REPO_PORT", "8081", "本地仓库服务端口,默认 8081。", "Local repository service port. Default: 8081.");
|
|
159
|
+
appendEntry("CACHE_DIR", "data/cache", "缓存根目录,默认 data/cache。", "Cache root directory. Default: data/cache.");
|
|
160
|
+
appendEntry("CACHE_CLEANUP_ENABLED", "true", "是否启用自动缓存清理,默认 true。", "Enable automatic cache cleanup. Default: true.");
|
|
161
|
+
appendEntry("CACHE_CLEANUP_DAILY_AT", "03:00", "每日固定检查时间(本地时区,HH:mm),默认 03:00。", "Fixed daily check time in local timezone (HH:mm). Default: 03:00.");
|
|
162
|
+
appendEntry("CACHE_CLEANUP_CHECK_MIN_INTERVAL", "10m", "压力检测最小间隔(支持 s/m/h/d),默认 10m。", "Minimum interval for pressure checks (supports s/m/h/d). Default: 10m.");
|
|
163
|
+
appendEntry("CACHE_TOUCH_ON_HIT", "true", "缓存命中返回时是否更新文件 mtime,默认 true。", "Update file mtime when serving cache hit. Default: true.");
|
|
164
|
+
appendEntry("CACHE_TOUCH_MIN_INTERVAL", "1d", "同一文件两次 touch 最小间隔(支持 s/m/h/d),默认 1d。", "Minimum interval between touches for the same file (supports s/m/h/d). Default: 1d.");
|
|
165
|
+
appendEntry("CACHE_RETENTION_START", "10d", "清理轮次初始保留窗口(支持 s/m/h/d),默认 10d。", "Initial retention window for cleanup cycle (supports s/m/h/d). Default: 10d.");
|
|
166
|
+
appendEntry("CACHE_RETENTION_MIN", "1d", "清理轮次最小保留窗口(支持 s/m/h/d),默认 1d。", "Minimum retention window for cleanup cycle (supports s/m/h/d). Default: 1d.");
|
|
167
|
+
appendEntry("CACHE_DISK_FREE_TRIGGER", "20G", "磁盘剩余空间低于该值触发清理(支持 K/M/G/T),默认 20G。", "Trigger cleanup when free disk space is below this value (supports K/M/G/T). Default: 20G.");
|
|
168
|
+
appendEntry("CACHE_DISK_FREE_TARGET", "25G", "磁盘剩余空间恢复到该值可停止清理(支持 K/M/G/T),默认 25G。", "Stop cleanup when free disk space recovers to this value (supports K/M/G/T). Default: 25G.");
|
|
169
|
+
appendEntry("CACHE_MAX_SIZE", "", "可选:缓存总大小触发阈值(支持 K/M/G/T),默认空(禁用)。", "Optional: cache total size trigger threshold (supports K/M/G/T). Default: empty (disabled).");
|
|
170
|
+
appendEntry("CACHE_TARGET_SIZE", "", "可选:缓存总大小回落目标(支持 K/M/G/T),默认空(禁用)。", "Optional: cache size recovery target (supports K/M/G/T). Default: empty (disabled).");
|
|
171
|
+
appendEntry(
|
|
172
|
+
"REPO_FALLBACK_REPOS",
|
|
173
|
+
"https://repo1.maven.org/maven2,https://jitpack.io,https://plugins.gradle.org/m2,https://dl.google.com",
|
|
174
|
+
"缓存未命中时回源仓库列表(逗号分隔)。",
|
|
175
|
+
"Fallback repository list when cache misses (comma-separated).",
|
|
176
|
+
);
|
|
177
|
+
appendEntry("ENABLE_HTTPS_PROXY", "true", "是否启用 HTTPS 代理处理(true/false),默认 true。", "Enable HTTPS proxy handling (true/false). Default: true.");
|
|
178
|
+
appendEntry("HTTPS_MITM_DOMAINS", "repo1.maven.org,repo.maven.apache.org,registry.npmjs.org", "执行 MITM 证书签发的域名列表(逗号分隔,支持通配符)。", "Domains for MITM certificate issuance (comma-separated, wildcard supported).");
|
|
179
|
+
appendEntry("HTTPS_PASSTHROUGH_FOR_UNMATCHED", "false", "未命中 MITM 域名时是否允许隧道透传,默认 false。", "Allow tunnel passthrough for unmatched MITM domains. Default: false.");
|
|
180
|
+
appendEntry("NPM_REGISTRY_DOMAINS", "registry.npmjs.org,registry.npmmirror.com,npm.pkg.github.com", "识别为 npm 生态并分流缓存的域名列表(支持通配符)。", "Domains recognized as npm ecosystem for cache routing (wildcard supported).");
|
|
181
|
+
appendEntry("MAVEN_REPO_DOMAINS", "repo1.maven.org,repo.maven.apache.org,jitpack.io,plugins.gradle.org,dl.google.com", "识别为 Maven 生态并分流缓存的域名列表(支持通配符)。", "Domains recognized as Maven ecosystem for cache routing (wildcard supported).");
|
|
182
|
+
appendEntry("MAVEN_CACHE_USE_DOMAIN_DIR", "false", "Maven 缓存是否按域名作为一级目录,默认 false。", "Whether Maven cache uses hostname as the first-level directory. Default: false.");
|
|
183
|
+
appendEntry("MAVEN_CACHE_IGNORE_PATH_PREFIXES", "repo1.maven.org/maven2,repo.maven.apache.org/maven2,jitpack.io/,plugins.gradle.org/m2,dl.google.com/dl/android/maven2,dl.google.com/dl/google/maven", "Maven 缓存应忽略的路径前缀规则(逗号分隔,支持 host/path 与 host:port/path)。", "Maven cache path-prefix ignore rules (comma-separated, supports host/path and host:port/path).");
|
|
184
|
+
appendEntry("MULTI_THREAD_DOMAINS", "repo1.maven.org", "启用多线程下载的域名列表(支持通配符)。", "Domains that enable multi-thread download (wildcard supported).");
|
|
185
|
+
appendEntry("MULTI_THREAD_COUNT", "8", "多线程下载线程数,默认 8。", "Thread count for multi-thread download. Default: 8.");
|
|
186
|
+
appendEntry("MULTI_THREAD_MIN_SIZE_MB", "1", "触发多线程下载的最小文件大小阈值(MB),默认 1。", "Minimum file size threshold in MB to trigger multi-thread download. Default: 1.");
|
|
187
|
+
appendEntry("DOWNLOAD_TIMEOUT", "60s", "上游请求超时时间(支持 s/m/h/d),默认 60s。", "Upstream request timeout (supports s/m/h/d). Default: 60s.");
|
|
188
|
+
appendEntry("DOWNLOAD_LOG_DIR", "data/logs/downloads", "统一主日志与错误日志目录。", "Unified main and error log directory.");
|
|
189
|
+
appendEntry("LOG_RETENTION", "7d", "日志保留时长(支持 s/m/h/d),默认 7d。", "Log retention duration (supports s/m/h/d). Default: 7d.");
|
|
190
|
+
appendEntry("LOG_TO_STDOUT", "false", "是否输出运行期日志到命令行,默认 false。", "Output runtime logs to stdout. Default: false.");
|
|
191
|
+
appendEntry("LOG_CONNECT_EVENTS", "false", "是否输出详细 CONNECT/MITM 握手日志,默认 false。", "Output detailed CONNECT/MITM handshake logs. Default: false.");
|
|
192
|
+
appendEntry("OUTBOUND_KEEP_ALIVE", "true", "是否启用出站 keep-alive 连接复用池,默认 true。", "Enable outbound keep-alive connection pool. Default: true.");
|
|
193
|
+
appendEntry("OUTBOUND_KEEP_ALIVE_INTERVAL", "1s", "keep-alive 间隔(支持 s/m/h/d),默认 1s。", "Keep-alive interval (supports s/m/h/d). Default: 1s.");
|
|
194
|
+
appendEntry("OUTBOUND_MAX_SOCKETS", "64", "每个源站最大出站连接数,默认 64。", "Maximum outbound sockets per upstream host. Default: 64.");
|
|
195
|
+
appendEntry("OUTBOUND_MAX_FREE_SOCKETS", "16", "每个源站可保留空闲连接上限,默认 16。", "Maximum free outbound sockets kept per upstream host. Default: 16.");
|
|
196
|
+
appendEntry("MAVEN_AFFINITY_ENABLED", "true", "是否启用 Maven affinity 缓存索引,默认 true。", "Enable Maven affinity index. Default: true.");
|
|
197
|
+
appendEntry("MAVEN_AFFINITY_INDEX_DIR", "data/index", "Maven affinity 索引目录,默认 data/index。", "Maven affinity index directory. Default: data/index.");
|
|
198
|
+
appendEntry("MAVEN_NEGATIVE_CACHE_TTL", "24h", "负缓存 TTL(支持 s/m/h/d),默认 24h。", "Negative cache TTL (supports s/m/h/d). Default: 24h.");
|
|
199
|
+
appendEntry("MAVEN_AFFINITY_FLUSH_INTERVAL", "5s", "affinity 事件日志 flush 周期(支持 s/m/h/d),默认 5s。", "Affinity event log flush interval (supports s/m/h/d). Default: 5s.");
|
|
200
|
+
appendEntry("MAVEN_AFFINITY_EVENT_MAX_MB", "8", "affinity 事件日志压缩阈值(MB),默认 8。", "Affinity event log compaction threshold in MB. Default: 8.");
|
|
201
|
+
appendEntry("UPSTREAM_PROXY_URL", "", "通用上级代理地址(HTTP/HTTPS 兜底)。", "Generic upstream proxy URL fallback for HTTP/HTTPS.");
|
|
202
|
+
appendEntry("UPSTREAM_HTTP_PROXY_URL", "", "HTTP 请求使用的上级代理地址。", "Upstream proxy URL for HTTP requests.");
|
|
203
|
+
appendEntry("UPSTREAM_HTTPS_PROXY_URL", "", "HTTPS 请求使用的上级代理地址。", "Upstream proxy URL for HTTPS requests.");
|
|
204
|
+
appendEntry("UPSTREAM_NO_PROXY", "127.0.0.1,localhost", "不走上级代理的域名列表(逗号分隔,支持通配符,* 表示全部直连)。", "Domains that bypass upstream proxy (comma-separated, wildcard supported, * means direct for all).");
|
|
205
|
+
appendEntry("UPSTREAM_IGNORE_DOMAINS", "", "额外忽略上级代理的域名列表(支持通配符)。", "Additional domains ignored by upstream proxy (wildcard supported).");
|
|
206
|
+
appendEntry("CERT_DIR", "data/certs", "证书根目录,默认 data/certs。", "Certificate root directory. Default: data/certs.");
|
|
207
|
+
appendEntry("ROOT_CERT_PATH", "data/certs/root-ca.crt", "Root CA 证书路径。", "Root CA certificate path.");
|
|
208
|
+
appendEntry("ROOT_KEY_PATH", "data/certs/root-ca.key.pem", "Root CA 私钥路径。", "Root CA private key path.");
|
|
209
|
+
appendEntry("LEAF_CERT_DIR", "data/certs/leaf", "站点叶子证书目录。", "Leaf certificate directory.");
|
|
210
|
+
appendEntry("TRUST_STORE_PATH", "data/certs/proxy-truststore.jks", "Java trust store 文件路径。", "Java trust store file path.");
|
|
211
|
+
appendEntry("TRUST_STORE_ALIAS", "maven-proxy-root-ca", "导入 Root CA 到 trust store 时使用的别名。", "Alias used to import Root CA into trust store.");
|
|
212
|
+
appendEntry("TRUST_STORE_PASSWORD", "changeit", "trust store 密码。", "Trust store password.");
|
|
213
|
+
appendEntry("EXISTING_TRUST_STORE_PATH", "", "已有 truststore 路径(可选),初始化时可作为源。", "Existing truststore path (optional), used as source during init.");
|
|
214
|
+
appendEntry("EXISTING_TRUST_STORE_PASSWORD", "", "已有 truststore 密码(可选),用于读取 EXISTING_TRUST_STORE_PATH。", "Existing truststore password (optional), used to read EXISTING_TRUST_STORE_PATH.");
|
|
215
|
+
appendEntry("JAVA_HOME", "", "Java 安装路径;为空或无效时会自动探测。", "Java installation path; auto-detected when empty or invalid.");
|
|
216
|
+
|
|
217
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
200
218
|
}
|
|
201
219
|
|
|
202
220
|
async function initConfigFile(configFile, force = false) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "maven-proxy",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
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": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"cli:start": "node bin/maven-proxy.js start --mode development",
|
|
20
20
|
"cli:stop": "node bin/maven-proxy.js stop",
|
|
21
21
|
"cli:doctor": "node bin/maven-proxy.js doctor --mode development",
|
|
22
|
+
"cli:doctor:user": "node bin/maven-proxy.js doctor --mode user",
|
|
22
23
|
"cli:truststore:print": "node bin/maven-proxy.js truststore print --mode development",
|
|
23
24
|
"cli:truststore:init": "node bin/maven-proxy.js truststore init --mode development",
|
|
24
25
|
"cli:truststore:merge": "node bin/maven-proxy.js truststore merge --mode development",
|
package/src/cache/cache-path.js
CHANGED
|
@@ -16,6 +16,162 @@ function looksLikeMavenVersionSegment(segment) {
|
|
|
16
16
|
return /^\d[0-9A-Za-z._-]*$/.test(String(segment || ""));
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function normalizeSlashPath(rawPath) {
|
|
20
|
+
const decoded = safeDecode(rawPath || "/").replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
21
|
+
if (!decoded) {
|
|
22
|
+
return "/";
|
|
23
|
+
}
|
|
24
|
+
return decoded.startsWith("/") ? decoded : `/${decoded}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function trimTrailingSlash(normalizedPath) {
|
|
28
|
+
if (normalizedPath.length > 1 && normalizedPath.endsWith("/")) {
|
|
29
|
+
return normalizedPath.slice(0, -1);
|
|
30
|
+
}
|
|
31
|
+
return normalizedPath;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeRulePrefixPath(rawPrefix) {
|
|
35
|
+
return trimTrailingSlash(normalizeSlashPath(rawPrefix || "/"));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getEffectivePort(urlObj) {
|
|
39
|
+
if (urlObj?.port) {
|
|
40
|
+
return String(urlObj.port);
|
|
41
|
+
}
|
|
42
|
+
return urlObj?.protocol === "https:" ? "443" : "80";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseMavenCacheIgnorePathRule(rawRule) {
|
|
46
|
+
const text = String(rawRule || "").trim();
|
|
47
|
+
if (!text) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const firstSlash = text.indexOf("/");
|
|
52
|
+
if (firstSlash <= 0) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const hostPortText = text.slice(0, firstSlash).trim().toLowerCase();
|
|
57
|
+
const prefixText = text.slice(firstSlash);
|
|
58
|
+
const hostMatch = hostPortText.match(/^([^:\/]+)(?::(\d+))?$/);
|
|
59
|
+
if (!hostMatch) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const hostname = hostMatch[1];
|
|
64
|
+
const port = hostMatch[2] || "";
|
|
65
|
+
const pathPrefix = normalizeRulePrefixPath(prefixText);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
hostname,
|
|
69
|
+
port,
|
|
70
|
+
pathPrefix,
|
|
71
|
+
raw: text,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function ruleMatchesUrl(rule, urlObj) {
|
|
76
|
+
if (!rule || !urlObj) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const hostname = String(urlObj.hostname || "").toLowerCase();
|
|
81
|
+
if (!hostname || hostname !== rule.hostname) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!rule.port) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return getEffectivePort(urlObj) === rule.port;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function parseMavenCacheIgnorePathPrefixes(rawRules) {
|
|
93
|
+
const input = Array.isArray(rawRules) ? rawRules.join(",") : String(rawRules || "");
|
|
94
|
+
const tokens = input
|
|
95
|
+
.split(",")
|
|
96
|
+
.map((item) => item.trim())
|
|
97
|
+
.filter(Boolean);
|
|
98
|
+
|
|
99
|
+
const parsed = [];
|
|
100
|
+
const dedupe = new Set();
|
|
101
|
+
|
|
102
|
+
for (const token of tokens) {
|
|
103
|
+
const rule = parseMavenCacheIgnorePathRule(token);
|
|
104
|
+
if (!rule) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const key = `${rule.hostname}:${rule.port}|${rule.pathPrefix}`;
|
|
109
|
+
if (dedupe.has(key)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
dedupe.add(key);
|
|
114
|
+
parsed.push(rule);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return parsed;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function stripMavenIgnoredPathPrefix(pathname, urlObj, rules = []) {
|
|
121
|
+
const normalizedPath = normalizeSlashPath(pathname || "/");
|
|
122
|
+
const validRules = Array.isArray(rules) ? rules : [];
|
|
123
|
+
if (validRules.length === 0 || !urlObj) {
|
|
124
|
+
return normalizedPath;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const matchedRules = validRules
|
|
128
|
+
.filter((rule) => ruleMatchesUrl(rule, urlObj))
|
|
129
|
+
.sort((a, b) => b.pathPrefix.length - a.pathPrefix.length);
|
|
130
|
+
|
|
131
|
+
for (const rule of matchedRules) {
|
|
132
|
+
const prefix = rule.pathPrefix;
|
|
133
|
+
if (prefix === "/") {
|
|
134
|
+
return normalizedPath;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (normalizedPath === prefix) {
|
|
138
|
+
return "/";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (normalizedPath.startsWith(`${prefix}/`)) {
|
|
142
|
+
const stripped = normalizedPath.slice(prefix.length);
|
|
143
|
+
return stripped || "/";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return normalizedPath;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function buildMavenHostlessPathCandidates(pathname, rules = []) {
|
|
151
|
+
const normalizedPath = normalizeSlashPath(pathname || "/");
|
|
152
|
+
const candidates = new Set([normalizedPath]);
|
|
153
|
+
const validRules = Array.isArray(rules) ? rules : [];
|
|
154
|
+
|
|
155
|
+
for (const rule of validRules) {
|
|
156
|
+
const prefix = rule.pathPrefix;
|
|
157
|
+
if (!prefix || prefix === "/") {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (normalizedPath === prefix) {
|
|
162
|
+
candidates.add("/");
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (normalizedPath.startsWith(`${prefix}/`)) {
|
|
167
|
+
const stripped = normalizedPath.slice(prefix.length);
|
|
168
|
+
candidates.add(stripped || "/");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return [...candidates];
|
|
173
|
+
}
|
|
174
|
+
|
|
19
175
|
function isLikelyMavenFilePath(parts, normalizedPath) {
|
|
20
176
|
if (normalizedPath.endsWith("/") || parts.length === 0) {
|
|
21
177
|
return false;
|
|
@@ -63,9 +219,14 @@ function isLikelyMavenFilePath(parts, normalizedPath) {
|
|
|
63
219
|
export function getCacheFilePath(cacheDir, urlObj, options = {}) {
|
|
64
220
|
const ecosystem = sanitizeSegment(String(options.ecosystem || "generic").toLowerCase());
|
|
65
221
|
const includeHost = options.includeHost ?? ecosystem !== "maven";
|
|
222
|
+
const mavenIgnoreRules = options.mavenCacheIgnorePathPrefixRules || [];
|
|
66
223
|
|
|
67
224
|
const rawPathname = safeDecode(urlObj.pathname || "/");
|
|
68
|
-
|
|
225
|
+
let normalized = rawPathname.replace(/\\/g, "/");
|
|
226
|
+
if (ecosystem === "maven") {
|
|
227
|
+
normalized = stripMavenIgnoredPathPrefix(normalized, urlObj, mavenIgnoreRules);
|
|
228
|
+
}
|
|
229
|
+
|
|
69
230
|
const lowerNormalized = normalized.toLowerCase();
|
|
70
231
|
const parts = normalized.split("/").filter(Boolean);
|
|
71
232
|
|
package/src/config/config.js
CHANGED
|
@@ -3,6 +3,7 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import dotenv from "dotenv";
|
|
5
5
|
import { detectJavaHome } from "../common/java-home.js";
|
|
6
|
+
import { parseMavenCacheIgnorePathPrefixes } from "../cache/cache-path.js";
|
|
6
7
|
|
|
7
8
|
const cwd = process.cwd();
|
|
8
9
|
const userConfigDir = path.resolve(os.homedir(), "maven-proxy");
|
|
@@ -193,7 +194,16 @@ const defaultRepoFallbackRepos = [
|
|
|
193
194
|
"https://repo1.maven.org/maven2",
|
|
194
195
|
"https://jitpack.io",
|
|
195
196
|
"https://plugins.gradle.org/m2",
|
|
196
|
-
"https://
|
|
197
|
+
"https://dl.google.com",
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
const defaultMavenCacheIgnorePathPrefixes = [
|
|
201
|
+
"repo1.maven.org/maven2",
|
|
202
|
+
"repo.maven.apache.org/maven2",
|
|
203
|
+
"jitpack.io/",
|
|
204
|
+
"plugins.gradle.org/m2",
|
|
205
|
+
"dl.google.com/dl/android/maven2",
|
|
206
|
+
"dl.google.com/dl/google/maven",
|
|
197
207
|
];
|
|
198
208
|
|
|
199
209
|
const repoFallbackRepos = normalizeRepoList(
|
|
@@ -201,12 +211,16 @@ const repoFallbackRepos = normalizeRepoList(
|
|
|
201
211
|
defaultRepoFallbackRepos,
|
|
202
212
|
);
|
|
203
213
|
|
|
214
|
+
const mavenCacheIgnorePathPrefixes = String(process.env.MAVEN_CACHE_IGNORE_PATH_PREFIXES || "").trim()
|
|
215
|
+
|| defaultMavenCacheIgnorePathPrefixes.join(",");
|
|
216
|
+
const mavenCacheIgnorePathPrefixRules = parseMavenCacheIgnorePathPrefixes(mavenCacheIgnorePathPrefixes);
|
|
217
|
+
|
|
204
218
|
const defaultMavenRepoDomains = [
|
|
205
219
|
"repo1.maven.org",
|
|
206
220
|
"repo.maven.apache.org",
|
|
207
221
|
"jitpack.io",
|
|
208
222
|
"plugins.gradle.org",
|
|
209
|
-
"
|
|
223
|
+
"dl.google.com",
|
|
210
224
|
...extractHostsFromUrls(repoFallbackRepos),
|
|
211
225
|
];
|
|
212
226
|
|
|
@@ -241,6 +255,9 @@ export const config = {
|
|
|
241
255
|
httpsMitmDomains: toList(process.env.HTTPS_MITM_DOMAINS, ["repo1.maven.org", "repo.maven.apache.org", "registry.npmjs.org"]),
|
|
242
256
|
npmRegistryDomains: toList(process.env.NPM_REGISTRY_DOMAINS, ["registry.npmjs.org", "registry.npmmirror.com", "npm.pkg.github.com"]),
|
|
243
257
|
mavenRepoDomains: toList(process.env.MAVEN_REPO_DOMAINS, [...new Set(defaultMavenRepoDomains)]),
|
|
258
|
+
mavenCacheUseDomainDir: toBool(process.env.MAVEN_CACHE_USE_DOMAIN_DIR, false),
|
|
259
|
+
mavenCacheIgnorePathPrefixes,
|
|
260
|
+
mavenCacheIgnorePathPrefixRules,
|
|
244
261
|
multiThreadDomains: toList(process.env.MULTI_THREAD_DOMAINS, ["repo1.maven.org"]),
|
|
245
262
|
multiThreadCount: Math.max(1, toInt(process.env.MULTI_THREAD_COUNT, 4)),
|
|
246
263
|
multiThreadMinSizeBytes,
|
package/src/index.js
CHANGED
|
@@ -120,6 +120,8 @@ async function main() {
|
|
|
120
120
|
startupInfo(`[maven-proxy] repo port: ${config.repoPort}`);
|
|
121
121
|
startupInfo(`[maven-proxy] cache dir : ${config.cacheDir}`);
|
|
122
122
|
startupInfo(`[maven-proxy] cache maven: ${config.mavenCacheDir}`);
|
|
123
|
+
startupInfo(`[maven-proxy] maven cache use domain dir: ${config.mavenCacheUseDomainDir}`);
|
|
124
|
+
startupInfo(`[maven-proxy] maven cache ignore path prefixes: ${config.mavenCacheIgnorePathPrefixes || "(none)"}`);
|
|
123
125
|
startupInfo(`[maven-proxy] cache npm : ${config.npmCacheDir}`);
|
|
124
126
|
startupInfo(`[maven-proxy] cache other: ${config.genericCacheDir}`);
|
|
125
127
|
startupInfo(`[maven-proxy] log dir: ${config.downloadLogDir}`);
|
|
@@ -200,7 +200,8 @@ export function createHttpRequestHandler({
|
|
|
200
200
|
ecosystem = detectPackageEcosystem(urlObj, config, matchesDomain);
|
|
201
201
|
cachePath = getCacheFilePath(config.cacheDir, urlObj, {
|
|
202
202
|
ecosystem,
|
|
203
|
-
includeHost: ecosystem !== "maven",
|
|
203
|
+
includeHost: ecosystem !== "maven" || config.mavenCacheUseDomainDir,
|
|
204
|
+
mavenCacheIgnorePathPrefixRules: config.mavenCacheIgnorePathPrefixRules,
|
|
204
205
|
});
|
|
205
206
|
|
|
206
207
|
if (ecosystem === "maven" && mavenAffinityIndex?.enabled) {
|
|
@@ -220,7 +221,9 @@ export function createHttpRequestHandler({
|
|
|
220
221
|
}
|
|
221
222
|
|
|
222
223
|
if (canonical && mavenAffinityIndex) {
|
|
223
|
-
|
|
224
|
+
const canUsePositiveAffinity = !config.mavenCacheUseDomainDir && isPositiveAffinityEligible(canonical.fileName);
|
|
225
|
+
|
|
226
|
+
if (canUsePositiveAffinity) {
|
|
224
227
|
const preferredPath = await mavenAffinityIndex.resolvePreferredCachePath(canonical.canonicalKey);
|
|
225
228
|
if (preferredPath) {
|
|
226
229
|
console.log(`[proxy] affinity hit canonical=${canonical.canonicalKey} host=${urlObj.hostname}`);
|
|
@@ -244,7 +247,12 @@ export function createHttpRequestHandler({
|
|
|
244
247
|
await fs.promises.mkdir(path.dirname(cachePath), { recursive: true });
|
|
245
248
|
await downloader.ensureCached(urlObj, cachePath, req.headers);
|
|
246
249
|
|
|
247
|
-
if (
|
|
250
|
+
if (
|
|
251
|
+
canonical &&
|
|
252
|
+
mavenAffinityIndex &&
|
|
253
|
+
!config.mavenCacheUseDomainDir &&
|
|
254
|
+
isPositiveAffinityEligible(canonical.fileName)
|
|
255
|
+
) {
|
|
248
256
|
mavenAffinityIndex.recordSuccess({
|
|
249
257
|
canonicalKey: canonical.canonicalKey,
|
|
250
258
|
host: urlObj.hostname,
|
package/src/repo/repo-server.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import http from "node:http";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
buildMavenHostlessPathCandidates,
|
|
6
|
+
stripMavenIgnoredPathPrefix,
|
|
7
|
+
} from "../cache/cache-path.js";
|
|
4
8
|
|
|
5
9
|
function safeJoin(baseDir, requestPath) {
|
|
6
10
|
const pathname = decodeURIComponent(requestPath || "/");
|
|
@@ -13,6 +17,58 @@ function safeJoin(baseDir, requestPath) {
|
|
|
13
17
|
return path.join(baseDir, normalized);
|
|
14
18
|
}
|
|
15
19
|
|
|
20
|
+
function sanitizeHostSegment(hostname) {
|
|
21
|
+
return String(hostname || "unknown").toLowerCase().replace(/[<>:\\"|?*]/g, "_");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function collectRepoHosts(repoBases = []) {
|
|
25
|
+
const hosts = new Set();
|
|
26
|
+
|
|
27
|
+
for (const repoBase of repoBases) {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = new URL(repoBase);
|
|
30
|
+
hosts.add(sanitizeHostSegment(parsed.hostname));
|
|
31
|
+
} catch {
|
|
32
|
+
// ignore invalid URL
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return [...hosts];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildDomainScopedPath(mavenCacheDir, hostname, relativePath) {
|
|
40
|
+
const hostDir = sanitizeHostSegment(hostname);
|
|
41
|
+
return safeJoin(path.join(mavenCacheDir, hostDir), relativePath);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeRelativePathForHost(relativePath, hostname, rules = []) {
|
|
45
|
+
const inputPath = `/${String(relativePath || "").replace(/^\/+/, "")}`;
|
|
46
|
+
const normalized = stripMavenIgnoredPathPrefix(
|
|
47
|
+
inputPath,
|
|
48
|
+
{ protocol: "https:", hostname, port: "" },
|
|
49
|
+
rules,
|
|
50
|
+
);
|
|
51
|
+
return normalized.replace(/^\/+/, "");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeRelativePathCandidates(relativePath, rules = []) {
|
|
55
|
+
const inputPath = `/${String(relativePath || "").replace(/^\/+/, "")}`;
|
|
56
|
+
const candidates = buildMavenHostlessPathCandidates(inputPath, rules);
|
|
57
|
+
return candidates.map((item) => item.replace(/^\/+/, ""));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildDefaultRepoFilePath(config, relativePath) {
|
|
61
|
+
if (!config.mavenCacheUseDomainDir) {
|
|
62
|
+
const candidates = normalizeRelativePathCandidates(relativePath, config.mavenCacheIgnorePathPrefixRules);
|
|
63
|
+
return safeJoin(config.mavenCacheDir, candidates[0] || relativePath);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const hosts = collectRepoHosts(config.repoFallbackRepos || []);
|
|
67
|
+
const host = hosts[0] || "unknown";
|
|
68
|
+
const normalized = normalizeRelativePathForHost(relativePath, host, config.mavenCacheIgnorePathPrefixRules);
|
|
69
|
+
return buildDomainScopedPath(config.mavenCacheDir, host, normalized || relativePath);
|
|
70
|
+
}
|
|
71
|
+
|
|
16
72
|
async function statIfExists(filePath) {
|
|
17
73
|
try {
|
|
18
74
|
return await fs.promises.stat(filePath);
|
|
@@ -24,6 +80,68 @@ async function statIfExists(filePath) {
|
|
|
24
80
|
}
|
|
25
81
|
}
|
|
26
82
|
|
|
83
|
+
async function findCachedMavenFile(config, relativePath) {
|
|
84
|
+
const ignoreRules = config.mavenCacheIgnorePathPrefixRules || [];
|
|
85
|
+
|
|
86
|
+
if (!config.mavenCacheUseDomainDir) {
|
|
87
|
+
const candidates = normalizeRelativePathCandidates(relativePath, ignoreRules);
|
|
88
|
+
for (const candidate of candidates) {
|
|
89
|
+
const filePath = safeJoin(config.mavenCacheDir, candidate);
|
|
90
|
+
const stats = await statIfExists(filePath);
|
|
91
|
+
if (stats && stats.isFile()) {
|
|
92
|
+
return { filePath, stats };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const fallbackPath = safeJoin(config.mavenCacheDir, candidates[0] || relativePath);
|
|
97
|
+
return { filePath: fallbackPath, stats: null };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const checkedHosts = new Set();
|
|
101
|
+
const preferredHosts = collectRepoHosts(config.repoFallbackRepos || []);
|
|
102
|
+
|
|
103
|
+
for (const host of preferredHosts) {
|
|
104
|
+
checkedHosts.add(host);
|
|
105
|
+
const normalized = normalizeRelativePathForHost(relativePath, host, ignoreRules);
|
|
106
|
+
const filePath = buildDomainScopedPath(config.mavenCacheDir, host, normalized || relativePath);
|
|
107
|
+
const stats = await statIfExists(filePath);
|
|
108
|
+
if (stats && stats.isFile()) {
|
|
109
|
+
return { filePath, stats };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let entries = [];
|
|
114
|
+
try {
|
|
115
|
+
entries = await fs.promises.readdir(config.mavenCacheDir, { withFileTypes: true });
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (error.code !== "ENOENT") {
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
if (!entry.isDirectory()) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (checkedHosts.has(entry.name)) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const normalized = normalizeRelativePathForHost(relativePath, entry.name, ignoreRules);
|
|
132
|
+
const filePath = safeJoin(path.join(config.mavenCacheDir, entry.name), normalized || relativePath);
|
|
133
|
+
const stats = await statIfExists(filePath);
|
|
134
|
+
if (stats && stats.isFile()) {
|
|
135
|
+
return { filePath, stats };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
filePath: buildDefaultRepoFilePath(config, relativePath),
|
|
141
|
+
stats: null,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
27
145
|
function buildRemoteUrl(repoBase, relativePath) {
|
|
28
146
|
const base = repoBase.endsWith("/") ? repoBase : `${repoBase}/`;
|
|
29
147
|
const relative = relativePath.replace(/^\/+/, "");
|
|
@@ -41,31 +159,45 @@ function buildCandidateRelativePaths(relativePath) {
|
|
|
41
159
|
return [...new Set(candidates.filter(Boolean))];
|
|
42
160
|
}
|
|
43
161
|
|
|
44
|
-
async function ensureFromRemoteRepos(config, downloader,
|
|
162
|
+
async function ensureFromRemoteRepos(config, downloader, relativePath, cacheCleanupManager = null) {
|
|
45
163
|
if (!downloader) {
|
|
46
|
-
return null;
|
|
164
|
+
return { filePath: buildDefaultRepoFilePath(config, relativePath), stats: null };
|
|
47
165
|
}
|
|
48
166
|
|
|
49
167
|
const repos = config.repoFallbackRepos || [];
|
|
50
168
|
if (repos.length === 0) {
|
|
51
|
-
return null;
|
|
169
|
+
return { filePath: buildDefaultRepoFilePath(config, relativePath), stats: null };
|
|
52
170
|
}
|
|
53
171
|
|
|
54
172
|
let hasNon404Error = false;
|
|
55
173
|
let lastError = null;
|
|
56
174
|
const candidatePaths = buildCandidateRelativePaths(relativePath);
|
|
175
|
+
const ignoreRules = config.mavenCacheIgnorePathPrefixRules || [];
|
|
57
176
|
|
|
58
177
|
for (const repoBase of repos) {
|
|
59
178
|
for (const candidatePath of candidatePaths) {
|
|
60
179
|
const remoteUrl = buildRemoteUrl(repoBase, candidatePath);
|
|
61
180
|
|
|
62
181
|
try {
|
|
182
|
+
const normalizedRelativePath = normalizeRelativePathForHost(
|
|
183
|
+
relativePath,
|
|
184
|
+
remoteUrl.hostname,
|
|
185
|
+
ignoreRules,
|
|
186
|
+
) || relativePath;
|
|
187
|
+
|
|
188
|
+
const targetPath = config.mavenCacheUseDomainDir
|
|
189
|
+
? buildDomainScopedPath(config.mavenCacheDir, remoteUrl.hostname, normalizedRelativePath)
|
|
190
|
+
: safeJoin(config.mavenCacheDir, normalizedRelativePath);
|
|
191
|
+
|
|
63
192
|
if (cacheCleanupManager) {
|
|
64
193
|
await cacheCleanupManager.checkAndCleanupIfNeeded("repo-cache-miss");
|
|
65
194
|
}
|
|
66
195
|
console.log(`[repo] cache miss, try remote ${remoteUrl.href}`);
|
|
67
|
-
await downloader.ensureCached(remoteUrl,
|
|
68
|
-
return
|
|
196
|
+
await downloader.ensureCached(remoteUrl, targetPath, {});
|
|
197
|
+
return {
|
|
198
|
+
filePath: targetPath,
|
|
199
|
+
stats: await statIfExists(targetPath),
|
|
200
|
+
};
|
|
69
201
|
} catch (error) {
|
|
70
202
|
lastError = error;
|
|
71
203
|
if (error.statusCode !== 404) {
|
|
@@ -79,7 +211,7 @@ async function ensureFromRemoteRepos(config, downloader, filePath, relativePath,
|
|
|
79
211
|
throw lastError;
|
|
80
212
|
}
|
|
81
213
|
|
|
82
|
-
return null;
|
|
214
|
+
return { filePath: buildDefaultRepoFilePath(config, relativePath), stats: null };
|
|
83
215
|
}
|
|
84
216
|
|
|
85
217
|
export function startRepoServer(config, downloader = null, cacheCleanupManager = null) {
|
|
@@ -87,11 +219,12 @@ export function startRepoServer(config, downloader = null, cacheCleanupManager =
|
|
|
87
219
|
try {
|
|
88
220
|
const urlObj = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
|
|
89
221
|
const relativePath = path.posix.normalize(urlObj.pathname || "/").replace(/^\/+/, "");
|
|
90
|
-
|
|
91
|
-
let stats = await statIfExists(filePath);
|
|
222
|
+
let { filePath, stats } = await findCachedMavenFile(config, relativePath);
|
|
92
223
|
|
|
93
224
|
if (!stats || !stats.isFile()) {
|
|
94
|
-
|
|
225
|
+
const fetched = await ensureFromRemoteRepos(config, downloader, relativePath, cacheCleanupManager);
|
|
226
|
+
filePath = fetched.filePath;
|
|
227
|
+
stats = fetched.stats;
|
|
95
228
|
}
|
|
96
229
|
|
|
97
230
|
if (!stats || !stats.isFile()) {
|