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 +24 -7
- package/bin/maven-proxy.js +174 -9
- package/package.json +5 -2
- package/src/cache/downloader.js +0 -3
- package/src/cache/maven-affinity-index.js +396 -0
- package/src/common/console-log-file.js +39 -9
- package/src/common/maven-canonical.js +151 -0
- package/src/config/config.js +23 -6
- package/src/index.js +96 -25
- package/src/proxy/proxy-connect-handler.js +15 -5
- package/src/proxy/proxy-http-handler.js +63 -2
- package/src/proxy/proxy-server.js +9 -1
- package/src/proxy/upstream-proxy.js +41 -3
- package/src/common/download-log-writer.js +0 -27
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
function toPositiveInt(value, fallback) {
|
|
5
|
+
const parsed = Number.parseInt(value, 10);
|
|
6
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
7
|
+
}
|
|
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 nowMs() {
|
|
18
|
+
return Date.now();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readJsonFile(filePath, fallback) {
|
|
22
|
+
if (!fs.existsSync(filePath)) {
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const text = fs.readFileSync(filePath, "utf8");
|
|
28
|
+
return JSON.parse(text);
|
|
29
|
+
} catch {
|
|
30
|
+
return fallback;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function serializeSnapshot(positiveMap, negativeMap, conflictMap) {
|
|
35
|
+
return {
|
|
36
|
+
version: 1,
|
|
37
|
+
generatedAt: new Date().toISOString(),
|
|
38
|
+
positive: [...positiveMap.entries()],
|
|
39
|
+
negative: [...negativeMap.entries()],
|
|
40
|
+
conflicts: [...conflictMap.entries()],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeRequestPath(pathname) {
|
|
45
|
+
return String(pathname || "")
|
|
46
|
+
.replace(/\\/g, "/")
|
|
47
|
+
.replace(/\/+/g, "/")
|
|
48
|
+
.replace(/^\/+/, "")
|
|
49
|
+
.trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildNegativeScope(urlObj) {
|
|
53
|
+
if (!urlObj || typeof urlObj !== "object") {
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const protocol = urlObj.protocol === "https:" ? "https:" : "http:";
|
|
58
|
+
const host = String(urlObj.host || "").toLowerCase();
|
|
59
|
+
const pathname = normalizeRequestPath(urlObj.pathname || "");
|
|
60
|
+
return `${protocol}//${host}/${pathname}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildNegativeKey(scope, canonicalKey) {
|
|
64
|
+
return `${String(scope || "").toLowerCase()}|${canonicalKey}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class MavenAffinityIndex {
|
|
68
|
+
constructor(config) {
|
|
69
|
+
this.enabled = toBool(config.mavenAffinityEnabled, true);
|
|
70
|
+
this.indexDir = config.mavenAffinityIndexDir;
|
|
71
|
+
this.negativeTtlMs = toPositiveInt(config.mavenNegativeCacheTtlMs, 24 * 60 * 60 * 1000);
|
|
72
|
+
this.flushIntervalMs = toPositiveInt(config.mavenAffinityFlushIntervalMs, 5000);
|
|
73
|
+
this.maxEventBytes = toPositiveInt(config.mavenAffinityEventMaxBytes, 8 * 1024 * 1024);
|
|
74
|
+
|
|
75
|
+
this.snapshotPath = path.join(this.indexDir, "maven-affinity.snapshot.json");
|
|
76
|
+
this.eventLogPath = path.join(this.indexDir, "maven-affinity.events.log");
|
|
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.
|
|
80
|
+
this.positive = new Map();
|
|
81
|
+
this.negative = new Map();
|
|
82
|
+
this.conflicts = new Map();
|
|
83
|
+
|
|
84
|
+
this.pendingEvents = [];
|
|
85
|
+
this.flushTimer = null;
|
|
86
|
+
this.flushing = false;
|
|
87
|
+
this.dirtySinceSnapshot = false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async init() {
|
|
91
|
+
if (!this.enabled) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await fs.promises.mkdir(this.indexDir, { recursive: true });
|
|
96
|
+
this.#loadSnapshot();
|
|
97
|
+
this.#replayEventLog();
|
|
98
|
+
|
|
99
|
+
this.flushTimer = setInterval(() => {
|
|
100
|
+
this.flush().catch((error) => {
|
|
101
|
+
console.error(`[affinity] flush failed: ${error.message}`);
|
|
102
|
+
});
|
|
103
|
+
}, this.flushIntervalMs);
|
|
104
|
+
|
|
105
|
+
if (typeof this.flushTimer.unref === "function") {
|
|
106
|
+
this.flushTimer.unref();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#loadSnapshot() {
|
|
111
|
+
const snapshot = readJsonFile(this.snapshotPath, null);
|
|
112
|
+
if (!snapshot || typeof snapshot !== "object") {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const [key, value] of snapshot.positive || []) {
|
|
117
|
+
this.positive.set(key, value);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const currentTime = nowMs();
|
|
121
|
+
for (const [key, value] of snapshot.negative || []) {
|
|
122
|
+
if (value?.expireAt && value.expireAt > currentTime) {
|
|
123
|
+
this.negative.set(key, value);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const [key, value] of snapshot.conflicts || []) {
|
|
128
|
+
this.conflicts.set(key, value);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#replayEventLog() {
|
|
133
|
+
if (!fs.existsSync(this.eventLogPath)) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let raw = "";
|
|
138
|
+
try {
|
|
139
|
+
raw = fs.readFileSync(this.eventLogPath, "utf8");
|
|
140
|
+
} catch {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const lines = raw.split(/\r?\n/).filter(Boolean);
|
|
145
|
+
for (const line of lines) {
|
|
146
|
+
try {
|
|
147
|
+
const event = JSON.parse(line);
|
|
148
|
+
this.#applyEvent(event, false);
|
|
149
|
+
} catch {
|
|
150
|
+
// ignore invalid line
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#enqueueEvent(type, payload) {
|
|
156
|
+
this.pendingEvents.push(JSON.stringify({ t: nowMs(), type, payload }));
|
|
157
|
+
this.dirtySinceSnapshot = true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#applyEvent(event, append = true) {
|
|
161
|
+
if (!event || typeof event !== "object") {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const { type, payload } = event;
|
|
166
|
+
if (!type || !payload || typeof payload !== "object") {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (type === "positive_upsert") {
|
|
171
|
+
this.positive.set(payload.key, payload.value);
|
|
172
|
+
if (append) {
|
|
173
|
+
this.#enqueueEvent(type, payload);
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (type === "positive_remove") {
|
|
179
|
+
this.positive.delete(payload.key);
|
|
180
|
+
if (append) {
|
|
181
|
+
this.#enqueueEvent(type, payload);
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (type === "negative_upsert") {
|
|
187
|
+
if (payload.value?.expireAt > nowMs()) {
|
|
188
|
+
this.negative.set(payload.key, payload.value);
|
|
189
|
+
} else {
|
|
190
|
+
this.negative.delete(payload.key);
|
|
191
|
+
}
|
|
192
|
+
if (append) {
|
|
193
|
+
this.#enqueueEvent(type, payload);
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (type === "negative_remove") {
|
|
199
|
+
this.negative.delete(payload.key);
|
|
200
|
+
if (append) {
|
|
201
|
+
this.#enqueueEvent(type, payload);
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (type === "conflict_set") {
|
|
207
|
+
this.conflicts.set(payload.key, payload.value);
|
|
208
|
+
if (append) {
|
|
209
|
+
this.#enqueueEvent(type, payload);
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (type === "conflict_clear") {
|
|
215
|
+
this.conflicts.delete(payload.key);
|
|
216
|
+
if (append) {
|
|
217
|
+
this.#enqueueEvent(type, payload);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async resolvePreferredCachePath(canonicalKey) {
|
|
223
|
+
if (!this.enabled || !canonicalKey) {
|
|
224
|
+
return "";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (this.conflicts.has(canonicalKey)) {
|
|
228
|
+
return "";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const existing = this.positive.get(canonicalKey);
|
|
232
|
+
if (!existing?.cachePath) {
|
|
233
|
+
return "";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const stats = await fs.promises.stat(existing.cachePath);
|
|
238
|
+
if (!stats.isFile()) {
|
|
239
|
+
throw new Error("not-file");
|
|
240
|
+
}
|
|
241
|
+
return existing.cachePath;
|
|
242
|
+
} catch {
|
|
243
|
+
this.#applyEvent({
|
|
244
|
+
type: "positive_remove",
|
|
245
|
+
payload: { key: canonicalKey },
|
|
246
|
+
});
|
|
247
|
+
return "";
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
shouldSkipRequest(canonicalKey, urlObj) {
|
|
252
|
+
const scope = buildNegativeScope(urlObj);
|
|
253
|
+
if (!this.enabled || !canonicalKey || !scope) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const key = buildNegativeKey(scope, canonicalKey);
|
|
258
|
+
const entry = this.negative.get(key);
|
|
259
|
+
if (!entry) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (entry.expireAt <= nowMs()) {
|
|
264
|
+
this.#applyEvent({
|
|
265
|
+
type: "negative_remove",
|
|
266
|
+
payload: { key },
|
|
267
|
+
});
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
recordSuccess({ canonicalKey, host, cachePath, fileName, urlObj = null }) {
|
|
275
|
+
if (!this.enabled || !canonicalKey || !cachePath || !fileName) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const previous = this.positive.get(canonicalKey);
|
|
280
|
+
if (previous && previous.fileName !== fileName) {
|
|
281
|
+
this.#applyEvent({
|
|
282
|
+
type: "conflict_set",
|
|
283
|
+
payload: {
|
|
284
|
+
key: canonicalKey,
|
|
285
|
+
value: {
|
|
286
|
+
reason: "file-name-mismatch",
|
|
287
|
+
updatedAt: nowMs(),
|
|
288
|
+
previousFileName: previous.fileName,
|
|
289
|
+
currentFileName: fileName,
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
this.#applyEvent({
|
|
295
|
+
type: "positive_remove",
|
|
296
|
+
payload: { key: canonicalKey },
|
|
297
|
+
});
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
this.#applyEvent({
|
|
302
|
+
type: "positive_upsert",
|
|
303
|
+
payload: {
|
|
304
|
+
key: canonicalKey,
|
|
305
|
+
value: {
|
|
306
|
+
cachePath,
|
|
307
|
+
fileName,
|
|
308
|
+
host: String(host || "").toLowerCase(),
|
|
309
|
+
updatedAt: nowMs(),
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const successScope = buildNegativeScope(urlObj);
|
|
315
|
+
if (successScope) {
|
|
316
|
+
const negativeKey = buildNegativeKey(successScope, canonicalKey);
|
|
317
|
+
if (this.negative.has(negativeKey)) {
|
|
318
|
+
this.#applyEvent({
|
|
319
|
+
type: "negative_remove",
|
|
320
|
+
payload: { key: negativeKey },
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
recordNegative({ canonicalKey, urlObj, statusCode = 404, ttlMs = this.negativeTtlMs }) {
|
|
327
|
+
const scope = buildNegativeScope(urlObj);
|
|
328
|
+
if (!this.enabled || !canonicalKey || !scope) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const expireAt = nowMs() + toPositiveInt(ttlMs, this.negativeTtlMs);
|
|
333
|
+
const key = buildNegativeKey(scope, canonicalKey);
|
|
334
|
+
this.#applyEvent({
|
|
335
|
+
type: "negative_upsert",
|
|
336
|
+
payload: {
|
|
337
|
+
key,
|
|
338
|
+
value: {
|
|
339
|
+
scope,
|
|
340
|
+
statusCode,
|
|
341
|
+
expireAt,
|
|
342
|
+
updatedAt: nowMs(),
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async flush() {
|
|
349
|
+
if (!this.enabled || this.flushing) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
this.flushing = true;
|
|
354
|
+
try {
|
|
355
|
+
if (this.pendingEvents.length > 0) {
|
|
356
|
+
const text = `${this.pendingEvents.join("\n")}\n`;
|
|
357
|
+
this.pendingEvents = [];
|
|
358
|
+
await fs.promises.appendFile(this.eventLogPath, text, "utf8");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const stats = await fs.promises.stat(this.eventLogPath).catch(() => null);
|
|
362
|
+
const needsSnapshot = this.dirtySinceSnapshot && (!stats || stats.size >= this.maxEventBytes);
|
|
363
|
+
|
|
364
|
+
if (needsSnapshot) {
|
|
365
|
+
await this.#writeSnapshotAndResetEventLog();
|
|
366
|
+
}
|
|
367
|
+
} finally {
|
|
368
|
+
this.flushing = false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async #writeSnapshotAndResetEventLog() {
|
|
373
|
+
const snapshot = serializeSnapshot(this.positive, this.negative, this.conflicts);
|
|
374
|
+
const tempPath = `${this.snapshotPath}.tmp`;
|
|
375
|
+
await fs.promises.writeFile(tempPath, `${JSON.stringify(snapshot, null, 2)}\n`, "utf8");
|
|
376
|
+
await fs.promises.rename(tempPath, this.snapshotPath);
|
|
377
|
+
await fs.promises.writeFile(this.eventLogPath, "", "utf8");
|
|
378
|
+
this.dirtySinceSnapshot = false;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async destroy() {
|
|
382
|
+
if (!this.enabled) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (this.flushTimer) {
|
|
387
|
+
clearInterval(this.flushTimer);
|
|
388
|
+
this.flushTimer = null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
await this.flush();
|
|
392
|
+
if (this.dirtySinceSnapshot) {
|
|
393
|
+
await this.#writeSnapshotAndResetEventLog();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
@@ -4,45 +4,75 @@ import { DailyLogFile } from "./daily-log-file.js";
|
|
|
4
4
|
const MIRROR_INSTALLED = Symbol.for("maven-proxy.console-log-file.installed");
|
|
5
5
|
const GLOBAL_ERROR_HOOK_INSTALLED = Symbol.for("maven-proxy.global-error-hook.installed");
|
|
6
6
|
|
|
7
|
-
function mirrorConsoleMethod({
|
|
7
|
+
function mirrorConsoleMethod({
|
|
8
|
+
level,
|
|
9
|
+
originalMethod,
|
|
10
|
+
appLogFile,
|
|
11
|
+
errorLogFile,
|
|
12
|
+
outputToConsole,
|
|
13
|
+
}) {
|
|
8
14
|
return (...args) => {
|
|
9
|
-
|
|
15
|
+
if (outputToConsole) {
|
|
16
|
+
originalMethod(...args);
|
|
17
|
+
}
|
|
10
18
|
|
|
11
19
|
const line = `[${new Date().toISOString()}] [${level}] ${util.format(...args)}`;
|
|
12
|
-
|
|
20
|
+
appLogFile.appendLine(line).catch((error) => {
|
|
13
21
|
process.stderr.write(`[maven-proxy] write console log failed: ${error.message}\n`);
|
|
14
22
|
});
|
|
23
|
+
|
|
24
|
+
if (level === "ERROR") {
|
|
25
|
+
errorLogFile.appendLine(line).catch((error) => {
|
|
26
|
+
process.stderr.write(`[maven-proxy] write error log failed: ${error.message}\n`);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
15
29
|
};
|
|
16
30
|
}
|
|
17
31
|
|
|
18
|
-
export function installConsoleLogFileMirror({
|
|
32
|
+
export function installConsoleLogFileMirror({
|
|
33
|
+
logDir,
|
|
34
|
+
retentionDays = 7,
|
|
35
|
+
outputToConsole = true,
|
|
36
|
+
}) {
|
|
19
37
|
if (globalThis[MIRROR_INSTALLED]) {
|
|
20
38
|
return;
|
|
21
39
|
}
|
|
22
40
|
globalThis[MIRROR_INSTALLED] = true;
|
|
23
41
|
|
|
24
|
-
const
|
|
42
|
+
const appLogFile = new DailyLogFile({
|
|
43
|
+
logDir,
|
|
44
|
+
filePrefix: "app",
|
|
45
|
+
retentionDays,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const errorLogFile = new DailyLogFile({
|
|
25
49
|
logDir,
|
|
26
|
-
filePrefix: "
|
|
50
|
+
filePrefix: "error",
|
|
27
51
|
retentionDays,
|
|
28
52
|
});
|
|
29
53
|
|
|
30
54
|
console.log = mirrorConsoleMethod({
|
|
31
55
|
level: "INFO",
|
|
32
56
|
originalMethod: console.log.bind(console),
|
|
33
|
-
|
|
57
|
+
appLogFile,
|
|
58
|
+
errorLogFile,
|
|
59
|
+
outputToConsole,
|
|
34
60
|
});
|
|
35
61
|
|
|
36
62
|
console.warn = mirrorConsoleMethod({
|
|
37
63
|
level: "WARN",
|
|
38
64
|
originalMethod: console.warn.bind(console),
|
|
39
|
-
|
|
65
|
+
appLogFile,
|
|
66
|
+
errorLogFile,
|
|
67
|
+
outputToConsole,
|
|
40
68
|
});
|
|
41
69
|
|
|
42
70
|
console.error = mirrorConsoleMethod({
|
|
43
71
|
level: "ERROR",
|
|
44
72
|
originalMethod: console.error.bind(console),
|
|
45
|
-
|
|
73
|
+
appLogFile,
|
|
74
|
+
errorLogFile,
|
|
75
|
+
outputToConsole,
|
|
46
76
|
});
|
|
47
77
|
}
|
|
48
78
|
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
function safeDecode(pathname) {
|
|
2
|
+
try {
|
|
3
|
+
return decodeURIComponent(pathname || "/");
|
|
4
|
+
} catch {
|
|
5
|
+
return pathname || "/";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function escapeRegex(value) {
|
|
10
|
+
return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizePathname(pathname) {
|
|
14
|
+
return String(pathname || "")
|
|
15
|
+
.replace(/\\/g, "/")
|
|
16
|
+
.replace(/^\/+/, "")
|
|
17
|
+
.replace(/\/+/g, "/")
|
|
18
|
+
.trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function stripKnownPrefixes(relativePath) {
|
|
22
|
+
const raw = normalizePathname(relativePath);
|
|
23
|
+
if (!raw) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const candidates = new Set([raw]);
|
|
28
|
+
|
|
29
|
+
if (raw.toLowerCase().startsWith("maven2/")) {
|
|
30
|
+
candidates.add(raw.slice("maven2/".length));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (raw.toLowerCase().startsWith("m2/")) {
|
|
34
|
+
candidates.add(raw.slice("m2/".length));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const maven2Marker = raw.toLowerCase().indexOf("/maven2/");
|
|
38
|
+
if (maven2Marker >= 0) {
|
|
39
|
+
candidates.add(raw.slice(maven2Marker + "/maven2/".length));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const m2Marker = raw.toLowerCase().indexOf("/m2/");
|
|
43
|
+
if (m2Marker >= 0) {
|
|
44
|
+
candidates.add(raw.slice(m2Marker + "/m2/".length));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const patterns = [
|
|
48
|
+
/^repository\/[^/]+\/(.+)$/i,
|
|
49
|
+
/^artifactory\/[^/]+\/(.+)$/i,
|
|
50
|
+
/^nexus\/content\/repositories\/[^/]+\/(.+)$/i,
|
|
51
|
+
/^repositories\/[^/]+\/(.+)$/i,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
for (const pattern of patterns) {
|
|
55
|
+
const match = raw.match(pattern);
|
|
56
|
+
if (match?.[1]) {
|
|
57
|
+
candidates.add(match[1]);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const normalizedCandidates = [...candidates].map((item) => normalizePathname(item)).filter(Boolean);
|
|
62
|
+
normalizedCandidates.sort((left, right) => left.split("/").length - right.split("/").length);
|
|
63
|
+
return normalizedCandidates;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isSafePathSegment(segment) {
|
|
67
|
+
return /^[A-Za-z0-9_.+-]+$/.test(String(segment || ""));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isReleaseVersion(version) {
|
|
71
|
+
return !String(version || "").toUpperCase().endsWith("-SNAPSHOT");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function matchReleaseFileName(artifact, version, fileName) {
|
|
75
|
+
if (/-SNAPSHOT(?=\.|-)/i.test(fileName)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const escapedArtifact = escapeRegex(artifact);
|
|
80
|
+
const escapedVersion = escapeRegex(version);
|
|
81
|
+
const pattern = new RegExp(
|
|
82
|
+
`^${escapedArtifact}-${escapedVersion}(?:-[A-Za-z0-9_.+:-]+)?\\.(pom|jar|module|aar|war)(?:\\.(sha1|sha256|sha512|md5|asc))?$`,
|
|
83
|
+
"i",
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return pattern.test(fileName);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function tryParseCandidate(relativePath) {
|
|
90
|
+
const normalized = normalizePathname(relativePath);
|
|
91
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
92
|
+
if (parts.length < 4) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const fileName = parts[parts.length - 1];
|
|
97
|
+
const version = parts[parts.length - 2];
|
|
98
|
+
const artifact = parts[parts.length - 3];
|
|
99
|
+
const groupParts = parts.slice(0, -3);
|
|
100
|
+
|
|
101
|
+
if (!artifact || !version || !fileName || groupParts.length === 0) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!isSafePathSegment(artifact) || !isSafePathSegment(version) || !isSafePathSegment(fileName)) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!groupParts.every((segment) => isSafePathSegment(segment))) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!isReleaseVersion(version)) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!matchReleaseFileName(artifact, version, fileName)) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const groupPath = groupParts.join("/");
|
|
122
|
+
const canonicalPath = `${groupPath}/${artifact}/${version}/${fileName}`;
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
canonicalPath,
|
|
126
|
+
canonicalKey: canonicalPath,
|
|
127
|
+
groupPath,
|
|
128
|
+
artifact,
|
|
129
|
+
version,
|
|
130
|
+
fileName,
|
|
131
|
+
isRelease: true,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function parseMavenReleaseCanonical(urlObj) {
|
|
136
|
+
if (!urlObj || typeof urlObj !== "object") {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const decodedPath = safeDecode(urlObj.pathname || "/");
|
|
141
|
+
const candidates = stripKnownPrefixes(decodedPath);
|
|
142
|
+
|
|
143
|
+
for (const candidate of candidates) {
|
|
144
|
+
const parsed = tryParseCandidate(candidate);
|
|
145
|
+
if (parsed) {
|
|
146
|
+
return parsed;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return null;
|
|
151
|
+
}
|
package/src/config/config.js
CHANGED
|
@@ -6,7 +6,7 @@ import { detectJavaHome } from "../common/java-home.js";
|
|
|
6
6
|
|
|
7
7
|
const cwd = process.cwd();
|
|
8
8
|
const userConfigDir = path.resolve(os.homedir(), "maven-proxy");
|
|
9
|
-
const defaultUserConfigPath = path.join(userConfigDir, "config");
|
|
9
|
+
const defaultUserConfigPath = path.join(userConfigDir, "config.properties");
|
|
10
10
|
|
|
11
11
|
function normalizeConfigMode(value) {
|
|
12
12
|
const normalized = String(value || "").trim().toLowerCase();
|
|
@@ -55,8 +55,7 @@ function resolveConfigFilePath(configMode) {
|
|
|
55
55
|
|
|
56
56
|
if (configMode === "development") {
|
|
57
57
|
const devCandidates = [
|
|
58
|
-
path.resolve(cwd, ".
|
|
59
|
-
path.resolve(cwd, ".evn"),
|
|
58
|
+
path.resolve(cwd, "config.properties"),
|
|
60
59
|
];
|
|
61
60
|
|
|
62
61
|
for (const candidate of devCandidates) {
|
|
@@ -181,6 +180,13 @@ const defaultMavenRepoDomains = [
|
|
|
181
180
|
|
|
182
181
|
const cacheDir = path.resolve(configBaseDir, process.env.CACHE_DIR || "data/cache");
|
|
183
182
|
|
|
183
|
+
const multiThreadMinSizeBytes = Math.max(0, toInt(process.env.MULTI_THREAD_MIN_SIZE_MB, 1)) * 1024 * 1024;
|
|
184
|
+
const downloadTimeoutMs = Math.max(1, toInt(process.env.DOWNLOAD_TIMEOUT_SECONDS, 60)) * 1000;
|
|
185
|
+
const outboundKeepAliveMsecs = Math.max(1, toInt(process.env.OUTBOUND_KEEP_ALIVE_SECONDS, 1)) * 1000;
|
|
186
|
+
const mavenNegativeCacheTtlMs = Math.max(1, toInt(process.env.MAVEN_NEGATIVE_CACHE_TTL_HOURS, 24)) * 60 * 60 * 1000;
|
|
187
|
+
const mavenAffinityFlushIntervalMs = Math.max(1, toInt(process.env.MAVEN_AFFINITY_FLUSH_INTERVAL_SECONDS, 5)) * 1000;
|
|
188
|
+
const mavenAffinityEventMaxBytes = Math.max(1, toInt(process.env.MAVEN_AFFINITY_EVENT_MAX_MB, 8)) * 1024 * 1024;
|
|
189
|
+
|
|
184
190
|
export const config = {
|
|
185
191
|
configMode,
|
|
186
192
|
configBaseDir,
|
|
@@ -198,10 +204,21 @@ export const config = {
|
|
|
198
204
|
mavenRepoDomains: toList(process.env.MAVEN_REPO_DOMAINS, [...new Set(defaultMavenRepoDomains)]),
|
|
199
205
|
multiThreadDomains: toList(process.env.MULTI_THREAD_DOMAINS, ["repo1.maven.org"]),
|
|
200
206
|
multiThreadCount: Math.max(1, toInt(process.env.MULTI_THREAD_COUNT, 4)),
|
|
201
|
-
multiThreadMinSizeBytes
|
|
202
|
-
downloadTimeoutMs
|
|
207
|
+
multiThreadMinSizeBytes,
|
|
208
|
+
downloadTimeoutMs,
|
|
209
|
+
outboundKeepAlive: toBool(process.env.OUTBOUND_KEEP_ALIVE, true),
|
|
210
|
+
outboundKeepAliveMsecs,
|
|
211
|
+
outboundMaxSockets: Math.max(1, toInt(process.env.OUTBOUND_MAX_SOCKETS, 64)),
|
|
212
|
+
outboundMaxFreeSockets: Math.max(1, toInt(process.env.OUTBOUND_MAX_FREE_SOCKETS, 16)),
|
|
213
|
+
mavenAffinityEnabled: toBool(process.env.MAVEN_AFFINITY_ENABLED, true),
|
|
214
|
+
mavenAffinityIndexDir: path.resolve(cacheDir, process.env.MAVEN_AFFINITY_INDEX_DIR || ".index"),
|
|
215
|
+
mavenNegativeCacheTtlMs,
|
|
216
|
+
mavenAffinityFlushIntervalMs,
|
|
217
|
+
mavenAffinityEventMaxBytes,
|
|
203
218
|
downloadLogDir: path.resolve(configBaseDir, process.env.DOWNLOAD_LOG_DIR || "data/logs/downloads"),
|
|
204
219
|
logRetentionDays: Math.max(1, toInt(process.env.LOG_RETENTION_DAYS, 7)),
|
|
220
|
+
logToStdout: toBool(process.env.LOG_TO_STDOUT, true),
|
|
221
|
+
logConnectEvents: toBool(process.env.LOG_CONNECT_EVENTS, false),
|
|
205
222
|
certDir: path.resolve(configBaseDir, process.env.CERT_DIR || "data/certs"),
|
|
206
223
|
rootCertPath: path.resolve(configBaseDir, process.env.ROOT_CERT_PATH || "data/certs/root-ca.crt"),
|
|
207
224
|
rootKeyPath: path.resolve(configBaseDir, process.env.ROOT_KEY_PATH || "data/certs/root-ca.key.pem"),
|
|
@@ -214,7 +231,7 @@ export const config = {
|
|
|
214
231
|
javaHome: javaHomeResolution.javaHome,
|
|
215
232
|
javaHomeSource: javaHomeResolution.source,
|
|
216
233
|
javaHomeConfigured: javaHomeResolution.configuredJavaHome || "",
|
|
217
|
-
httpsPassthroughForUnmatched: toBool(process.env.HTTPS_PASSTHROUGH_FOR_UNMATCHED,
|
|
234
|
+
httpsPassthroughForUnmatched: toBool(process.env.HTTPS_PASSTHROUGH_FOR_UNMATCHED, false),
|
|
218
235
|
upstreamProxyUrl: normalizeProxyUrl(process.env.UPSTREAM_PROXY_URL || process.env.ALL_PROXY || process.env.all_proxy || ""),
|
|
219
236
|
upstreamHttpProxyUrl: normalizeProxyUrl(process.env.UPSTREAM_HTTP_PROXY_URL || process.env.HTTP_PROXY || process.env.http_proxy || ""),
|
|
220
237
|
upstreamHttpsProxyUrl: normalizeProxyUrl(process.env.UPSTREAM_HTTPS_PROXY_URL || process.env.HTTPS_PROXY || process.env.https_proxy || ""),
|