@asiainfo-sdd/multica-cli 0.2.1 → 0.3.7
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 +129 -98
- package/bin/multica-uninstall.js +5 -0
- package/bin/multica.js +49 -52
- package/lib/dist-config.json +2 -1
- package/lib/http.js +174 -174
- package/lib/install.js +337 -333
- package/lib/platform.js +45 -45
- package/lib/server-config.js +102 -102
- package/lib/uninstall.js +263 -0
- package/package.json +7 -4
- package/bin/aido.js +0 -52
package/lib/http.js
CHANGED
|
@@ -1,174 +1,174 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// 零依赖 HTTP 客户端:Node 自带 fetch(undici)不认 http_proxy/https_proxy
|
|
4
|
-
// 环境变量,内网代理场景必须自己处理。支持:
|
|
5
|
-
// - 直连 http/https
|
|
6
|
-
// - http_proxy / https_proxy / HTTP_PROXY / HTTPS_PROXY
|
|
7
|
-
// - NO_PROXY(逗号分隔主机名,支持后缀匹配)
|
|
8
|
-
// - https over proxy:CONNECT 隧道
|
|
9
|
-
// 流式响应,供下载侧边收边算 sha256。
|
|
10
|
-
|
|
11
|
-
const http = require('http');
|
|
12
|
-
const https = require('https');
|
|
13
|
-
const { URL } = require('url');
|
|
14
|
-
|
|
15
|
-
const KEEP_ALIVE_AGENT_OPTIONS = { keepAlive: false };
|
|
16
|
-
|
|
17
|
-
function proxyFromEnv(targetURL) {
|
|
18
|
-
const url = new URL(targetURL);
|
|
19
|
-
if (matchesNoProxy(url.hostname)) return null;
|
|
20
|
-
const proto = url.protocol === 'https:' ? 'https' : 'http';
|
|
21
|
-
const raw =
|
|
22
|
-
process.env[`${proto}_proxy`] || process.env[`${proto}_PROXY`] || '';
|
|
23
|
-
if (!raw) return null;
|
|
24
|
-
try {
|
|
25
|
-
const proxy = new URL(raw);
|
|
26
|
-
if (!/^https?:$/.test(proxy.protocol)) return null;
|
|
27
|
-
return proxy;
|
|
28
|
-
} catch {
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function matchesNoProxy(hostname) {
|
|
34
|
-
const list = (process.env.NO_PROXY || process.env.NO_PROXY === '' ? process.env.NO_PROXY : process.env.no_proxy) || '';
|
|
35
|
-
if (!list) return false;
|
|
36
|
-
return list
|
|
37
|
-
.split(',')
|
|
38
|
-
.map((s) => s.trim().toLowerCase())
|
|
39
|
-
.filter(Boolean)
|
|
40
|
-
.some((entry) => {
|
|
41
|
-
if (entry === '*') return true;
|
|
42
|
-
if (entry.startsWith('.')) return hostname.endsWith(entry);
|
|
43
|
-
return hostname === entry || hostname.endsWith(`.${entry}`);
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// http 目标 + http 代理:向代理发绝对 URI 的 GET。
|
|
48
|
-
// https 目标 + 任意代理:先 CONNECT 隧道再在隧道上发 GET。
|
|
49
|
-
function request(targetURL, { headers = {}, timeoutMs = 120000 } = {}) {
|
|
50
|
-
return new Promise((resolve, reject) => {
|
|
51
|
-
let url;
|
|
52
|
-
try {
|
|
53
|
-
url = new URL(targetURL);
|
|
54
|
-
} catch (err) {
|
|
55
|
-
reject(new Error(`invalid URL: ${targetURL}`));
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
const proxy = proxyFromEnv(targetURL);
|
|
59
|
-
|
|
60
|
-
const onConnectError = (err) =>
|
|
61
|
-
reject(new Error(`proxy connect failed: ${err.message}`));
|
|
62
|
-
|
|
63
|
-
if (url.protocol === 'https:' && proxy) {
|
|
64
|
-
const proxyPort = Number(proxy.port) || (proxy.protocol === 'https:' ? 443 : 80);
|
|
65
|
-
const connectReq = http.request({
|
|
66
|
-
host: proxy.hostname,
|
|
67
|
-
port: proxyPort,
|
|
68
|
-
method: 'CONNECT',
|
|
69
|
-
path: `${url.hostname}:${url.port || 443}`,
|
|
70
|
-
headers: { Host: `${url.hostname}:${url.port || 443}` },
|
|
71
|
-
timeout: timeoutMs,
|
|
72
|
-
});
|
|
73
|
-
connectReq.on('connect', (res, socket) => {
|
|
74
|
-
if (res.statusCode !== 200) {
|
|
75
|
-
socket.destroy();
|
|
76
|
-
reject(new Error(`proxy CONNECT failed: ${res.statusCode}`));
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
const agent = new https.Agent({ ...KEEP_ALIVE_AGENT_OPTIONS, socket });
|
|
80
|
-
const req = https.get(
|
|
81
|
-
{
|
|
82
|
-
host: url.hostname,
|
|
83
|
-
port: url.port || 443,
|
|
84
|
-
path: url.pathname + url.search,
|
|
85
|
-
headers,
|
|
86
|
-
agent,
|
|
87
|
-
timeout: timeoutMs,
|
|
88
|
-
},
|
|
89
|
-
resolve,
|
|
90
|
-
);
|
|
91
|
-
req.on('timeout', () => {
|
|
92
|
-
req.destroy(new Error(`request timed out after ${timeoutMs}ms`));
|
|
93
|
-
});
|
|
94
|
-
req.on('error', reject);
|
|
95
|
-
});
|
|
96
|
-
connectReq.on('error', onConnectError);
|
|
97
|
-
connectReq.on('timeout', () => {
|
|
98
|
-
connectReq.destroy(new Error(`proxy connect timed out after ${timeoutMs}ms`));
|
|
99
|
-
});
|
|
100
|
-
connectReq.end();
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const isHttps = url.protocol === 'https:';
|
|
105
|
-
const mod = isHttps ? https : http;
|
|
106
|
-
const options = {
|
|
107
|
-
host: proxy ? proxy.hostname : url.hostname,
|
|
108
|
-
port: proxy
|
|
109
|
-
? Number(proxy.port) || (proxy.protocol === 'https:' ? 443 : 80)
|
|
110
|
-
: Number(url.port) || (isHttps ? 443 : 80),
|
|
111
|
-
method: 'GET',
|
|
112
|
-
path: proxy ? targetURL : url.pathname + url.search,
|
|
113
|
-
headers,
|
|
114
|
-
timeout: timeoutMs,
|
|
115
|
-
};
|
|
116
|
-
// https 代理入口(少见于内网)不支持时直接报错,不做二次降级。
|
|
117
|
-
if (proxy && proxy.protocol === 'https:') {
|
|
118
|
-
reject(new Error('https proxy entry is not supported'));
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
const req = mod.get(options, resolve);
|
|
122
|
-
req.on('timeout', () => {
|
|
123
|
-
req.destroy(new Error(`request timed out after ${timeoutMs}ms`));
|
|
124
|
-
});
|
|
125
|
-
req.on('error', reject);
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function checkStatus(res, what) {
|
|
130
|
-
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
131
|
-
res.resume();
|
|
132
|
-
throw new Error(`${what} failed: HTTP ${res.statusCode} ${res.statusMessage || ''}`.trim());
|
|
133
|
-
}
|
|
134
|
-
return res;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async function fetchText(url, { timeoutMs = 10000 } = {}) {
|
|
138
|
-
const res = checkStatus(await request(url, { timeoutMs }), `fetch ${url}`);
|
|
139
|
-
const chunks = [];
|
|
140
|
-
for await (const chunk of res) chunks.push(chunk);
|
|
141
|
-
return Buffer.concat(chunks).toString('utf8');
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// 流式下载到可写流,返回 { statusCode 后已校验的响应流 }。
|
|
145
|
-
// onProgress(bytesDone, totalOrNull) 仅在有内容时回调。
|
|
146
|
-
function downloadToStream(url, dest, { timeoutMs = 300000, onProgress } = {}) {
|
|
147
|
-
return new Promise(async (resolve, reject) => {
|
|
148
|
-
let res;
|
|
149
|
-
try {
|
|
150
|
-
res = await request(url, { timeoutMs });
|
|
151
|
-
checkStatus(res, `download ${url}`);
|
|
152
|
-
} catch (err) {
|
|
153
|
-
reject(err);
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
const total = Number(res.headers['content-length']) || null;
|
|
157
|
-
let done = 0;
|
|
158
|
-
let lastTick = 0;
|
|
159
|
-
res.on('data', (chunk) => {
|
|
160
|
-
done += chunk.length;
|
|
161
|
-
const now = Date.now();
|
|
162
|
-
if (onProgress && (now - lastTick > 200 || done === total)) {
|
|
163
|
-
lastTick = now;
|
|
164
|
-
onProgress(done, total);
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
res.on('error', reject);
|
|
168
|
-
dest.on('error', reject);
|
|
169
|
-
res.pipe(dest);
|
|
170
|
-
dest.on('finish', () => resolve());
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
module.exports = { request, fetchText, downloadToStream };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// 零依赖 HTTP 客户端:Node 自带 fetch(undici)不认 http_proxy/https_proxy
|
|
4
|
+
// 环境变量,内网代理场景必须自己处理。支持:
|
|
5
|
+
// - 直连 http/https
|
|
6
|
+
// - http_proxy / https_proxy / HTTP_PROXY / HTTPS_PROXY
|
|
7
|
+
// - NO_PROXY(逗号分隔主机名,支持后缀匹配)
|
|
8
|
+
// - https over proxy:CONNECT 隧道
|
|
9
|
+
// 流式响应,供下载侧边收边算 sha256。
|
|
10
|
+
|
|
11
|
+
const http = require('http');
|
|
12
|
+
const https = require('https');
|
|
13
|
+
const { URL } = require('url');
|
|
14
|
+
|
|
15
|
+
const KEEP_ALIVE_AGENT_OPTIONS = { keepAlive: false };
|
|
16
|
+
|
|
17
|
+
function proxyFromEnv(targetURL) {
|
|
18
|
+
const url = new URL(targetURL);
|
|
19
|
+
if (matchesNoProxy(url.hostname)) return null;
|
|
20
|
+
const proto = url.protocol === 'https:' ? 'https' : 'http';
|
|
21
|
+
const raw =
|
|
22
|
+
process.env[`${proto}_proxy`] || process.env[`${proto}_PROXY`] || '';
|
|
23
|
+
if (!raw) return null;
|
|
24
|
+
try {
|
|
25
|
+
const proxy = new URL(raw);
|
|
26
|
+
if (!/^https?:$/.test(proxy.protocol)) return null;
|
|
27
|
+
return proxy;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function matchesNoProxy(hostname) {
|
|
34
|
+
const list = (process.env.NO_PROXY || process.env.NO_PROXY === '' ? process.env.NO_PROXY : process.env.no_proxy) || '';
|
|
35
|
+
if (!list) return false;
|
|
36
|
+
return list
|
|
37
|
+
.split(',')
|
|
38
|
+
.map((s) => s.trim().toLowerCase())
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.some((entry) => {
|
|
41
|
+
if (entry === '*') return true;
|
|
42
|
+
if (entry.startsWith('.')) return hostname.endsWith(entry);
|
|
43
|
+
return hostname === entry || hostname.endsWith(`.${entry}`);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// http 目标 + http 代理:向代理发绝对 URI 的 GET。
|
|
48
|
+
// https 目标 + 任意代理:先 CONNECT 隧道再在隧道上发 GET。
|
|
49
|
+
function request(targetURL, { headers = {}, timeoutMs = 120000 } = {}) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
let url;
|
|
52
|
+
try {
|
|
53
|
+
url = new URL(targetURL);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
reject(new Error(`invalid URL: ${targetURL}`));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const proxy = proxyFromEnv(targetURL);
|
|
59
|
+
|
|
60
|
+
const onConnectError = (err) =>
|
|
61
|
+
reject(new Error(`proxy connect failed: ${err.message}`));
|
|
62
|
+
|
|
63
|
+
if (url.protocol === 'https:' && proxy) {
|
|
64
|
+
const proxyPort = Number(proxy.port) || (proxy.protocol === 'https:' ? 443 : 80);
|
|
65
|
+
const connectReq = http.request({
|
|
66
|
+
host: proxy.hostname,
|
|
67
|
+
port: proxyPort,
|
|
68
|
+
method: 'CONNECT',
|
|
69
|
+
path: `${url.hostname}:${url.port || 443}`,
|
|
70
|
+
headers: { Host: `${url.hostname}:${url.port || 443}` },
|
|
71
|
+
timeout: timeoutMs,
|
|
72
|
+
});
|
|
73
|
+
connectReq.on('connect', (res, socket) => {
|
|
74
|
+
if (res.statusCode !== 200) {
|
|
75
|
+
socket.destroy();
|
|
76
|
+
reject(new Error(`proxy CONNECT failed: ${res.statusCode}`));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const agent = new https.Agent({ ...KEEP_ALIVE_AGENT_OPTIONS, socket });
|
|
80
|
+
const req = https.get(
|
|
81
|
+
{
|
|
82
|
+
host: url.hostname,
|
|
83
|
+
port: url.port || 443,
|
|
84
|
+
path: url.pathname + url.search,
|
|
85
|
+
headers,
|
|
86
|
+
agent,
|
|
87
|
+
timeout: timeoutMs,
|
|
88
|
+
},
|
|
89
|
+
resolve,
|
|
90
|
+
);
|
|
91
|
+
req.on('timeout', () => {
|
|
92
|
+
req.destroy(new Error(`request timed out after ${timeoutMs}ms`));
|
|
93
|
+
});
|
|
94
|
+
req.on('error', reject);
|
|
95
|
+
});
|
|
96
|
+
connectReq.on('error', onConnectError);
|
|
97
|
+
connectReq.on('timeout', () => {
|
|
98
|
+
connectReq.destroy(new Error(`proxy connect timed out after ${timeoutMs}ms`));
|
|
99
|
+
});
|
|
100
|
+
connectReq.end();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const isHttps = url.protocol === 'https:';
|
|
105
|
+
const mod = isHttps ? https : http;
|
|
106
|
+
const options = {
|
|
107
|
+
host: proxy ? proxy.hostname : url.hostname,
|
|
108
|
+
port: proxy
|
|
109
|
+
? Number(proxy.port) || (proxy.protocol === 'https:' ? 443 : 80)
|
|
110
|
+
: Number(url.port) || (isHttps ? 443 : 80),
|
|
111
|
+
method: 'GET',
|
|
112
|
+
path: proxy ? targetURL : url.pathname + url.search,
|
|
113
|
+
headers,
|
|
114
|
+
timeout: timeoutMs,
|
|
115
|
+
};
|
|
116
|
+
// https 代理入口(少见于内网)不支持时直接报错,不做二次降级。
|
|
117
|
+
if (proxy && proxy.protocol === 'https:') {
|
|
118
|
+
reject(new Error('https proxy entry is not supported'));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const req = mod.get(options, resolve);
|
|
122
|
+
req.on('timeout', () => {
|
|
123
|
+
req.destroy(new Error(`request timed out after ${timeoutMs}ms`));
|
|
124
|
+
});
|
|
125
|
+
req.on('error', reject);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function checkStatus(res, what) {
|
|
130
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
131
|
+
res.resume();
|
|
132
|
+
throw new Error(`${what} failed: HTTP ${res.statusCode} ${res.statusMessage || ''}`.trim());
|
|
133
|
+
}
|
|
134
|
+
return res;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function fetchText(url, { timeoutMs = 10000 } = {}) {
|
|
138
|
+
const res = checkStatus(await request(url, { timeoutMs }), `fetch ${url}`);
|
|
139
|
+
const chunks = [];
|
|
140
|
+
for await (const chunk of res) chunks.push(chunk);
|
|
141
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 流式下载到可写流,返回 { statusCode 后已校验的响应流 }。
|
|
145
|
+
// onProgress(bytesDone, totalOrNull) 仅在有内容时回调。
|
|
146
|
+
function downloadToStream(url, dest, { timeoutMs = 300000, onProgress } = {}) {
|
|
147
|
+
return new Promise(async (resolve, reject) => {
|
|
148
|
+
let res;
|
|
149
|
+
try {
|
|
150
|
+
res = await request(url, { timeoutMs });
|
|
151
|
+
checkStatus(res, `download ${url}`);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
reject(err);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const total = Number(res.headers['content-length']) || null;
|
|
157
|
+
let done = 0;
|
|
158
|
+
let lastTick = 0;
|
|
159
|
+
res.on('data', (chunk) => {
|
|
160
|
+
done += chunk.length;
|
|
161
|
+
const now = Date.now();
|
|
162
|
+
if (onProgress && (now - lastTick > 200 || done === total)) {
|
|
163
|
+
lastTick = now;
|
|
164
|
+
onProgress(done, total);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
res.on('error', reject);
|
|
168
|
+
dest.on('error', reject);
|
|
169
|
+
res.pipe(dest);
|
|
170
|
+
dest.on('finish', () => resolve());
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = { request, fetchText, downloadToStream };
|