maven-proxy 1.0.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 +420 -0
- package/bin/maven-proxy.js +573 -0
- package/package.json +54 -0
- package/scripts/truststore.js +96 -0
- package/src/cache/cache-path.js +50 -0
- package/src/cache/downloader.js +350 -0
- package/src/cert/cert-manager.js +194 -0
- package/src/cert/truststore-utils.js +289 -0
- package/src/common/console-log-file.js +62 -0
- package/src/common/daily-log-file.js +79 -0
- package/src/common/domain-match.js +39 -0
- package/src/common/download-log-writer.js +27 -0
- package/src/common/ecosystem.js +64 -0
- package/src/common/java-home.js +328 -0
- package/src/config/config.js +213 -0
- package/src/index.js +93 -0
- package/src/proxy/proxy-connect-handler.js +173 -0
- package/src/proxy/proxy-http-handler.js +187 -0
- package/src/proxy/proxy-server.js +35 -0
- package/src/proxy/upstream-proxy.js +236 -0
- package/src/repo/repo-server.js +120 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
const SUPPORTED_STORE_TYPES = new Set(["JKS", "PKCS12"]);
|
|
7
|
+
|
|
8
|
+
function runCommand(command, args) {
|
|
9
|
+
const result = spawnSync(command, args, {
|
|
10
|
+
stdio: "inherit",
|
|
11
|
+
shell: false,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
if (result.error) {
|
|
15
|
+
throw result.error;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (result.status !== 0) {
|
|
19
|
+
throw new Error(`Command failed: ${command} ${args.join(" ")}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function runCommandCapture(command, args) {
|
|
24
|
+
const result = spawnSync(command, args, {
|
|
25
|
+
shell: false,
|
|
26
|
+
encoding: "utf8",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (result.error) {
|
|
30
|
+
throw result.error;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (result.status !== 0) {
|
|
34
|
+
const stderr = (result.stderr || "").trim();
|
|
35
|
+
throw new Error(
|
|
36
|
+
stderr || `Command failed: ${command} ${args.join(" ")}`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return result.stdout || "";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function assertKeytoolAvailable() {
|
|
44
|
+
const result = spawnSync("keytool", ["-help"], {
|
|
45
|
+
shell: false,
|
|
46
|
+
encoding: "utf8",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (result.error) {
|
|
50
|
+
throw new Error(`keytool is not available: ${result.error.message}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (result.status !== 0) {
|
|
54
|
+
const stderr = (result.stderr || "").trim();
|
|
55
|
+
throw new Error(stderr || "keytool is not available.");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getDefaultCacertsPath(javaHome) {
|
|
60
|
+
return path.join(javaHome, "lib", "security", "cacerts");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseAliasesFromListOutput(output) {
|
|
64
|
+
const aliases = new Set();
|
|
65
|
+
const verboseMatches = output.matchAll(/Alias name:\s*(.+)\s*$/gm);
|
|
66
|
+
for (const match of verboseMatches) {
|
|
67
|
+
aliases.add(match[1].trim());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (aliases.size > 0) {
|
|
71
|
+
return aliases;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Fallback for non-verbose list format: "<alias>, <date>, <entryType>, ..."
|
|
75
|
+
const listMatches = output.matchAll(/^([^,\r\n]+),\s.+$/gm);
|
|
76
|
+
for (const match of listMatches) {
|
|
77
|
+
aliases.add(match[1].trim());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return aliases;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function listTrustStoreAliases({ storePath, storePass, storeType }) {
|
|
84
|
+
const output = runCommandCapture("keytool", [
|
|
85
|
+
"-list",
|
|
86
|
+
"-v",
|
|
87
|
+
"-keystore",
|
|
88
|
+
storePath,
|
|
89
|
+
"-storepass",
|
|
90
|
+
storePass,
|
|
91
|
+
"-storetype",
|
|
92
|
+
storeType,
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
return parseAliasesFromListOutput(output);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function validateMergeOptions(options) {
|
|
99
|
+
if (!options || typeof options !== "object") {
|
|
100
|
+
throw new Error("Merge options are required.");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const required = ["sourcePath", "targetPath", "sourcePassword", "targetPassword"];
|
|
104
|
+
for (const key of required) {
|
|
105
|
+
if (!options[key]) {
|
|
106
|
+
throw new Error(`Missing required option: ${key}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const sourceType = String(options.sourceType || "JKS").toUpperCase();
|
|
111
|
+
const targetType = String(options.targetType || "JKS").toUpperCase();
|
|
112
|
+
|
|
113
|
+
if (!SUPPORTED_STORE_TYPES.has(sourceType)) {
|
|
114
|
+
throw new Error(`Invalid sourceType: ${sourceType}. Use JKS or PKCS12.`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!SUPPORTED_STORE_TYPES.has(targetType)) {
|
|
118
|
+
throw new Error(`Invalid targetType: ${targetType}. Use JKS or PKCS12.`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const resolvedSourcePath = path.resolve(options.sourcePath);
|
|
122
|
+
const resolvedTargetPath = path.resolve(options.targetPath);
|
|
123
|
+
|
|
124
|
+
if (resolvedSourcePath === resolvedTargetPath) {
|
|
125
|
+
throw new Error("sourcePath and targetPath must be different.");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!fs.existsSync(options.sourcePath)) {
|
|
129
|
+
throw new Error(`Source truststore not found: ${options.sourcePath}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const sourceStats = fs.statSync(options.sourcePath);
|
|
133
|
+
if (!sourceStats.isFile()) {
|
|
134
|
+
throw new Error(`Source truststore is not a file: ${options.sourcePath}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (fs.existsSync(options.targetPath)) {
|
|
138
|
+
const targetStats = fs.statSync(options.targetPath);
|
|
139
|
+
if (!targetStats.isFile()) {
|
|
140
|
+
throw new Error(`Target truststore path is not a file: ${options.targetPath}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const mode = options.onConflict || "fail";
|
|
145
|
+
if (!["fail", "overwrite"].includes(mode)) {
|
|
146
|
+
throw new Error(`Invalid onConflict mode: ${mode}. Use \"fail\" or \"overwrite\".`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
onConflict: mode,
|
|
151
|
+
sourceType,
|
|
152
|
+
targetType,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function getTrustStoreCommands(runtimeConfig) {
|
|
157
|
+
const isWindows = os.platform() === "win32";
|
|
158
|
+
const javaHome = runtimeConfig.javaHome || (isWindows ? "%JAVA_HOME%" : "$JAVA_HOME");
|
|
159
|
+
const defaultCacerts = isWindows
|
|
160
|
+
? `${javaHome}\\lib\\security\\cacerts`
|
|
161
|
+
: `${javaHome}/lib/security/cacerts`;
|
|
162
|
+
|
|
163
|
+
const copyCmd = isWindows
|
|
164
|
+
? `Copy-Item "${defaultCacerts}" "${runtimeConfig.trustStorePath}"`
|
|
165
|
+
: `cp "${defaultCacerts}" "${runtimeConfig.trustStorePath}"`;
|
|
166
|
+
|
|
167
|
+
const importCmd = `keytool -importcert -noprompt -trustcacerts -alias "${runtimeConfig.trustStoreAlias}" -file "${runtimeConfig.rootCertPath}" -keystore "${runtimeConfig.trustStorePath}" -storepass "${runtimeConfig.trustStorePassword}"`;
|
|
168
|
+
const listCmd = `keytool -list -v -keystore "${runtimeConfig.trustStorePath}" -storepass "${runtimeConfig.trustStorePassword}" -alias "${runtimeConfig.trustStoreAlias}"`;
|
|
169
|
+
|
|
170
|
+
return { copyCmd, importCmd, listCmd };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function initTrustStore(runtimeConfig) {
|
|
174
|
+
assertKeytoolAvailable();
|
|
175
|
+
|
|
176
|
+
if (!runtimeConfig || !runtimeConfig.javaHome) {
|
|
177
|
+
throw new Error("JAVA_HOME is required to initialize trust store.");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const defaultCacerts = getDefaultCacertsPath(runtimeConfig.javaHome);
|
|
181
|
+
|
|
182
|
+
if (!fs.existsSync(defaultCacerts)) {
|
|
183
|
+
throw new Error(`JDK cacerts not found: ${defaultCacerts}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!fs.existsSync(runtimeConfig.rootCertPath)) {
|
|
187
|
+
throw new Error(`Root certificate not found: ${runtimeConfig.rootCertPath}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
fs.mkdirSync(path.dirname(runtimeConfig.trustStorePath), { recursive: true });
|
|
191
|
+
|
|
192
|
+
if (!fs.existsSync(runtimeConfig.trustStorePath)) {
|
|
193
|
+
fs.copyFileSync(defaultCacerts, runtimeConfig.trustStorePath);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
runCommand("keytool", [
|
|
197
|
+
"-importcert",
|
|
198
|
+
"-noprompt",
|
|
199
|
+
"-trustcacerts",
|
|
200
|
+
"-alias",
|
|
201
|
+
runtimeConfig.trustStoreAlias,
|
|
202
|
+
"-file",
|
|
203
|
+
runtimeConfig.rootCertPath,
|
|
204
|
+
"-keystore",
|
|
205
|
+
runtimeConfig.trustStorePath,
|
|
206
|
+
"-storepass",
|
|
207
|
+
runtimeConfig.trustStorePassword,
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
runCommand("keytool", [
|
|
211
|
+
"-list",
|
|
212
|
+
"-v",
|
|
213
|
+
"-keystore",
|
|
214
|
+
runtimeConfig.trustStorePath,
|
|
215
|
+
"-storepass",
|
|
216
|
+
runtimeConfig.trustStorePassword,
|
|
217
|
+
"-alias",
|
|
218
|
+
runtimeConfig.trustStoreAlias,
|
|
219
|
+
]);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function mergeTrustStores(options) {
|
|
223
|
+
assertKeytoolAvailable();
|
|
224
|
+
|
|
225
|
+
const validated = validateMergeOptions(options);
|
|
226
|
+
const onConflict = validated.onConflict;
|
|
227
|
+
const sourceType = validated.sourceType;
|
|
228
|
+
const targetType = validated.targetType;
|
|
229
|
+
const dryRun = Boolean(options.dryRun);
|
|
230
|
+
|
|
231
|
+
if (!dryRun) {
|
|
232
|
+
fs.mkdirSync(path.dirname(options.targetPath), { recursive: true });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (onConflict === "fail" && fs.existsSync(options.targetPath)) {
|
|
236
|
+
const sourceAliases = listTrustStoreAliases({
|
|
237
|
+
storePath: options.sourcePath,
|
|
238
|
+
storePass: options.sourcePassword,
|
|
239
|
+
storeType: sourceType,
|
|
240
|
+
});
|
|
241
|
+
const targetAliases = listTrustStoreAliases({
|
|
242
|
+
storePath: options.targetPath,
|
|
243
|
+
storePass: options.targetPassword,
|
|
244
|
+
storeType: targetType,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const conflicts = [...sourceAliases].filter((alias) => targetAliases.has(alias));
|
|
248
|
+
if (conflicts.length > 0) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
`Alias conflict detected: ${conflicts.join(", ")}. Use --on-conflict overwrite to continue.`,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (dryRun) {
|
|
256
|
+
return {
|
|
257
|
+
dryRun: true,
|
|
258
|
+
checkedSource: options.sourcePath,
|
|
259
|
+
checkedTarget: options.targetPath,
|
|
260
|
+
onConflict,
|
|
261
|
+
sourceType,
|
|
262
|
+
targetType,
|
|
263
|
+
targetExists: fs.existsSync(options.targetPath),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const args = [
|
|
268
|
+
"-importkeystore",
|
|
269
|
+
"-srckeystore",
|
|
270
|
+
options.sourcePath,
|
|
271
|
+
"-srcstoretype",
|
|
272
|
+
sourceType,
|
|
273
|
+
"-srcstorepass",
|
|
274
|
+
options.sourcePassword,
|
|
275
|
+
"-destkeystore",
|
|
276
|
+
options.targetPath,
|
|
277
|
+
"-deststoretype",
|
|
278
|
+
targetType,
|
|
279
|
+
"-deststorepass",
|
|
280
|
+
options.targetPassword,
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
if (onConflict === "overwrite") {
|
|
284
|
+
args.push("-noprompt");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
runCommand("keytool", args);
|
|
288
|
+
}
|
|
289
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import util from "node:util";
|
|
2
|
+
import { DailyLogFile } from "./daily-log-file.js";
|
|
3
|
+
|
|
4
|
+
const MIRROR_INSTALLED = Symbol.for("maven-proxy.console-log-file.installed");
|
|
5
|
+
const GLOBAL_ERROR_HOOK_INSTALLED = Symbol.for("maven-proxy.global-error-hook.installed");
|
|
6
|
+
|
|
7
|
+
function mirrorConsoleMethod({ level, originalMethod, logFile }) {
|
|
8
|
+
return (...args) => {
|
|
9
|
+
originalMethod(...args);
|
|
10
|
+
|
|
11
|
+
const line = `[${new Date().toISOString()}] [${level}] ${util.format(...args)}`;
|
|
12
|
+
logFile.appendLine(line).catch((error) => {
|
|
13
|
+
process.stderr.write(`[maven-proxy] write console log failed: ${error.message}\n`);
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function installConsoleLogFileMirror({ logDir, retentionDays = 7 }) {
|
|
19
|
+
if (globalThis[MIRROR_INSTALLED]) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
globalThis[MIRROR_INSTALLED] = true;
|
|
23
|
+
|
|
24
|
+
const logFile = new DailyLogFile({
|
|
25
|
+
logDir,
|
|
26
|
+
filePrefix: "console",
|
|
27
|
+
retentionDays,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
console.log = mirrorConsoleMethod({
|
|
31
|
+
level: "INFO",
|
|
32
|
+
originalMethod: console.log.bind(console),
|
|
33
|
+
logFile,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
console.warn = mirrorConsoleMethod({
|
|
37
|
+
level: "WARN",
|
|
38
|
+
originalMethod: console.warn.bind(console),
|
|
39
|
+
logFile,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
console.error = mirrorConsoleMethod({
|
|
43
|
+
level: "ERROR",
|
|
44
|
+
originalMethod: console.error.bind(console),
|
|
45
|
+
logFile,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function installGlobalErrorLogging() {
|
|
50
|
+
if (globalThis[GLOBAL_ERROR_HOOK_INSTALLED]) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
globalThis[GLOBAL_ERROR_HOOK_INSTALLED] = true;
|
|
54
|
+
|
|
55
|
+
process.on("uncaughtExceptionMonitor", (error, origin) => {
|
|
56
|
+
console.error(`[global-error] uncaughtException origin=${origin}`, error);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
process.on("unhandledRejection", (reason) => {
|
|
60
|
+
console.error("[global-error] unhandledRejection", reason);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
function toDateStampLocal(date = new Date()) {
|
|
5
|
+
const year = String(date.getFullYear());
|
|
6
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
7
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
8
|
+
return `${year}-${month}-${day}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function escapeRegExp(text) {
|
|
12
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class DailyLogFile {
|
|
16
|
+
constructor({ logDir, filePrefix, retentionDays = 7 }) {
|
|
17
|
+
this.logDir = logDir;
|
|
18
|
+
this.filePrefix = filePrefix;
|
|
19
|
+
this.retentionDays = Math.max(1, Number.parseInt(retentionDays, 10) || 7);
|
|
20
|
+
this.ensureDirPromise = null;
|
|
21
|
+
this.lastCleanupStamp = "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async ensureDir() {
|
|
25
|
+
if (!this.ensureDirPromise) {
|
|
26
|
+
this.ensureDirPromise = fs.promises.mkdir(this.logDir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
await this.ensureDirPromise;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getDailyLogPath(date = new Date()) {
|
|
32
|
+
return path.join(this.logDir, `${this.filePrefix}-${toDateStampLocal(date)}.log`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async cleanupOldLogsIfNeeded(date = new Date()) {
|
|
36
|
+
const todayStamp = toDateStampLocal(date);
|
|
37
|
+
if (this.lastCleanupStamp === todayStamp) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.lastCleanupStamp = todayStamp;
|
|
42
|
+
|
|
43
|
+
const cutoff = new Date(date);
|
|
44
|
+
cutoff.setHours(0, 0, 0, 0);
|
|
45
|
+
cutoff.setDate(cutoff.getDate() - (this.retentionDays - 1));
|
|
46
|
+
const cutoffStamp = toDateStampLocal(cutoff);
|
|
47
|
+
|
|
48
|
+
const pattern = new RegExp(`^${escapeRegExp(this.filePrefix)}-(\\d{4}-\\d{2}-\\d{2})\\.log$`);
|
|
49
|
+
const entries = await fs.promises.readdir(this.logDir, { withFileTypes: true });
|
|
50
|
+
|
|
51
|
+
const deleteTasks = [];
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
if (!entry.isFile()) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const match = entry.name.match(pattern);
|
|
58
|
+
if (!match) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const dateStamp = match[1];
|
|
63
|
+
if (dateStamp < cutoffStamp) {
|
|
64
|
+
deleteTasks.push(fs.promises.unlink(path.join(this.logDir, entry.name)));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (deleteTasks.length > 0) {
|
|
69
|
+
await Promise.all(deleteTasks);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async appendLine(line, date = new Date()) {
|
|
74
|
+
await this.ensureDir();
|
|
75
|
+
await this.cleanupOldLogsIfNeeded(date);
|
|
76
|
+
const content = line.endsWith("\n") ? line : `${line}\n`;
|
|
77
|
+
await fs.promises.appendFile(this.getDailyLogPath(date), content, "utf8");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
function escapeRegExp(value) {
|
|
2
|
+
return value.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&");
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function wildcardToRegExp(pattern) {
|
|
6
|
+
const escaped = pattern
|
|
7
|
+
.split("*")
|
|
8
|
+
.map((segment) => escapeRegExp(segment))
|
|
9
|
+
.join(".*");
|
|
10
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function matchesDomain(hostname, patterns) {
|
|
14
|
+
if (!hostname || !patterns || patterns.length === 0) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const host = hostname.toLowerCase();
|
|
19
|
+
|
|
20
|
+
for (const rawPattern of patterns) {
|
|
21
|
+
const pattern = rawPattern.trim().toLowerCase();
|
|
22
|
+
if (!pattern) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (pattern.includes("*")) {
|
|
27
|
+
if (wildcardToRegExp(pattern).test(host)) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (host === pattern || host.endsWith(`.${pattern}`)) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { DailyLogFile } from "./daily-log-file.js";
|
|
2
|
+
|
|
3
|
+
export class DownloadLogWriter {
|
|
4
|
+
constructor(logDir, retentionDays = 7) {
|
|
5
|
+
this.logFile = new DailyLogFile({
|
|
6
|
+
logDir,
|
|
7
|
+
filePrefix: "download",
|
|
8
|
+
retentionDays,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async append(event, url, details = {}) {
|
|
13
|
+
const record = {
|
|
14
|
+
time: new Date().toISOString(),
|
|
15
|
+
event,
|
|
16
|
+
url,
|
|
17
|
+
...details,
|
|
18
|
+
};
|
|
19
|
+
await this.logFile.appendLine(JSON.stringify(record));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
write(event, url, details = {}) {
|
|
23
|
+
this.append(event, url, details).catch((error) => {
|
|
24
|
+
console.warn(`[downloader] write download log failed: ${error.message}`);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const MAVEN_ARTIFACT_EXTENSIONS = new Set([
|
|
2
|
+
".pom",
|
|
3
|
+
".jar",
|
|
4
|
+
".aar",
|
|
5
|
+
".war",
|
|
6
|
+
".module",
|
|
7
|
+
".xml",
|
|
8
|
+
".sha1",
|
|
9
|
+
".md5",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
function safeDecode(pathname) {
|
|
13
|
+
try {
|
|
14
|
+
return decodeURIComponent(pathname || "/");
|
|
15
|
+
} catch {
|
|
16
|
+
return pathname || "/";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function hasExtension(pathname, extensions) {
|
|
21
|
+
const lower = String(pathname || "").toLowerCase();
|
|
22
|
+
for (const ext of extensions) {
|
|
23
|
+
if (lower.endsWith(ext)) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function detectPackageEcosystem(urlObj, config, matchesDomain) {
|
|
31
|
+
const hostname = String(urlObj.hostname || "").toLowerCase();
|
|
32
|
+
const pathname = safeDecode(urlObj.pathname || "/");
|
|
33
|
+
const lowerPath = pathname.toLowerCase();
|
|
34
|
+
|
|
35
|
+
if (matchesDomain(hostname, config.npmRegistryDomains || [])) {
|
|
36
|
+
return "npm";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (matchesDomain(hostname, config.mavenRepoDomains || [])) {
|
|
40
|
+
return "maven";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (lowerPath.startsWith("/maven2/") || hasExtension(lowerPath, MAVEN_ARTIFACT_EXTENSIONS)) {
|
|
44
|
+
return "maven";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (
|
|
48
|
+
lowerPath.startsWith("/-/v1/") ||
|
|
49
|
+
/\/\/-\/.+\.tgz$/i.test(lowerPath) ||
|
|
50
|
+
lowerPath.startsWith("/@")
|
|
51
|
+
) {
|
|
52
|
+
return "npm";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (hostname.includes("npm")) {
|
|
56
|
+
return "npm";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (hostname.includes("maven") || hostname.includes("jitpack") || hostname.includes("gradle")) {
|
|
60
|
+
return "maven";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return "generic";
|
|
64
|
+
}
|