com.jimuwd.xian.registry-proxy 1.0.34 → 1.0.35
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/dist/index.js +71 -98
- package/package.json +1 -1
- package/src/index.ts +95 -99
package/dist/index.js
CHANGED
|
@@ -139,6 +139,60 @@ async function loadProxyInfo(proxyConfigPath = './.registry-proxy.yml', localYar
|
|
|
139
139
|
const basePath = removeEndingSlashAndForceStartingSlash(proxyConfig.basePath);
|
|
140
140
|
return { registries, https, basePath };
|
|
141
141
|
}
|
|
142
|
+
async function fetchFromRegistry(registry, path, search, limiter) {
|
|
143
|
+
await limiter.acquire();
|
|
144
|
+
try {
|
|
145
|
+
const targetUrl = `${registry.normalizedRegistryUrl}${path}${search || ''}`;
|
|
146
|
+
console.log(`Fetching from: ${targetUrl}`);
|
|
147
|
+
const headers = registry.token ? { Authorization: `Bearer ${registry.token}` } : undefined;
|
|
148
|
+
const response = await fetch(targetUrl, { headers });
|
|
149
|
+
console.log(`Response from ${targetUrl}: ${response.status} ${response.statusText}`);
|
|
150
|
+
return response.ok ? response : null;
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
if (e instanceof Error) {
|
|
154
|
+
console.error(e.code === 'ECONNREFUSED'
|
|
155
|
+
? `Registry ${registry.normalizedRegistryUrl} unreachable [ECONNREFUSED]`
|
|
156
|
+
: `Error from ${registry.normalizedRegistryUrl}: ${e.message}`);
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
limiter.release();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function writeResponse(res, response, req, proxyInfo, proxyPort, registryInfos) {
|
|
165
|
+
const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
|
|
166
|
+
if (contentType.includes('application/json')) {
|
|
167
|
+
const data = await response.json();
|
|
168
|
+
if (data.versions) {
|
|
169
|
+
const host = req.headers.host || `localhost:${proxyPort}`;
|
|
170
|
+
const baseUrl = `${proxyInfo.https ? 'https' : 'http'}://${host}${proxyInfo.basePath === '/' ? '' : proxyInfo.basePath}`;
|
|
171
|
+
for (const version in data.versions) {
|
|
172
|
+
const tarball = data.versions[version]?.dist?.tarball;
|
|
173
|
+
if (tarball) {
|
|
174
|
+
const path = removeRegistryPrefix(tarball, registryInfos);
|
|
175
|
+
data.versions[version].dist.tarball = `${baseUrl}${path}${new URL(tarball).search || ''}`;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
180
|
+
res.end(JSON.stringify(data));
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
const safeHeaders = {
|
|
184
|
+
'content-type': contentType,
|
|
185
|
+
'connection': 'keep-alive',
|
|
186
|
+
};
|
|
187
|
+
['cache-control', 'etag', 'last-modified', 'content-encoding'].forEach(header => {
|
|
188
|
+
const value = response.headers.get(header);
|
|
189
|
+
if (value)
|
|
190
|
+
safeHeaders[header] = value;
|
|
191
|
+
});
|
|
192
|
+
res.writeHead(response.status, safeHeaders);
|
|
193
|
+
await pipeline(Readable.fromWeb(response.body), res);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
142
196
|
export async function startProxyServer(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath, port = 0) {
|
|
143
197
|
const proxyInfo = await loadProxyInfo(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath);
|
|
144
198
|
const registryInfos = proxyInfo.registries;
|
|
@@ -149,116 +203,35 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
149
203
|
let proxyPort;
|
|
150
204
|
const requestHandler = async (req, res) => {
|
|
151
205
|
if (!req.url || !req.headers.host) {
|
|
152
|
-
console.error('Invalid request: missing URL or host header');
|
|
153
206
|
res.writeHead(400).end('Invalid Request');
|
|
154
207
|
return;
|
|
155
208
|
}
|
|
156
209
|
const fullUrl = new URL(req.url, `${proxyInfo.https ? 'https' : 'http'}://${req.headers.host}`);
|
|
157
|
-
console.log(`Proxy server received request on ${fullUrl.toString()}`);
|
|
158
210
|
if (!fullUrl.pathname.startsWith(basePathPrefixedWithSlash)) {
|
|
159
|
-
console.error(`Path ${fullUrl.pathname} does not match basePath ${basePathPrefixedWithSlash}`);
|
|
160
211
|
res.writeHead(404).end('Not Found');
|
|
161
212
|
return;
|
|
162
213
|
}
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
214
|
+
const path = basePathPrefixedWithSlash === '/'
|
|
215
|
+
? fullUrl.pathname
|
|
216
|
+
: fullUrl.pathname.slice(basePathPrefixedWithSlash.length);
|
|
217
|
+
// 顺序尝试注册表,获取第一个成功响应
|
|
218
|
+
let successfulResponse = null;
|
|
219
|
+
for (const registry of registryInfos) {
|
|
167
220
|
if (req.destroyed)
|
|
168
221
|
break;
|
|
169
|
-
await limiter
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
console.log(`Fetching from: ${targetUrl}`);
|
|
174
|
-
const headers = token ? { Authorization: `Bearer ${token}` } : undefined;
|
|
175
|
-
response = await fetch(targetUrl, { headers });
|
|
176
|
-
console.log(`Response from ${targetUrl}: ${response.status} ${response.statusText}`);
|
|
177
|
-
if (response.ok) {
|
|
178
|
-
const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
|
|
179
|
-
if (contentType.includes('application/json')) {
|
|
180
|
-
// application/json 元数据
|
|
181
|
-
try {
|
|
182
|
-
const data = await response.json();
|
|
183
|
-
if (data.versions) {
|
|
184
|
-
const requestHeadersHostFromYarnClient = req.headers.host || 'localhost:' + proxyPort;
|
|
185
|
-
console.log("Request headers.host from yarn client is", requestHeadersHostFromYarnClient);
|
|
186
|
-
const proxyBaseUrlNoSuffixedWithSlash = `${proxyInfo.https ? 'https' : 'http'}://${requestHeadersHostFromYarnClient}${basePathPrefixedWithSlash === '/' ? '' : basePathPrefixedWithSlash}`;
|
|
187
|
-
console.log("proxyBaseUrlNoSuffixedWithSlash", proxyBaseUrlNoSuffixedWithSlash);
|
|
188
|
-
for (const version in data.versions) {
|
|
189
|
-
const dist = data.versions[version]?.dist;
|
|
190
|
-
if (dist?.tarball) {
|
|
191
|
-
const originalUrl = new URL(dist.tarball);
|
|
192
|
-
const originalSearchParamsStr = originalUrl.search || '';
|
|
193
|
-
const tarballPathPrefixedWithSlash = removeRegistryPrefix(dist.tarball, registryInfos);
|
|
194
|
-
dist.tarball = `${proxyBaseUrlNoSuffixedWithSlash}${tarballPathPrefixedWithSlash}${originalSearchParamsStr}`;
|
|
195
|
-
if (!tarballPathPrefixedWithSlash.startsWith("/"))
|
|
196
|
-
console.error("bad tarballPath, must be PrefixedWithSlash", tarballPathPrefixedWithSlash);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
201
|
-
res.end(JSON.stringify(data));
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
catch (e) {
|
|
205
|
-
console.error('Failed to parse JSON response:', e);
|
|
206
|
-
// 继续尝试下一个注册表
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
else {
|
|
210
|
-
// 二进制流处理
|
|
211
|
-
if (!response.body) {
|
|
212
|
-
console.error(`Empty response body from ${response.url}`);
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
// 头信息处理
|
|
216
|
-
const safeHeaders = {
|
|
217
|
-
'content-type': contentType,
|
|
218
|
-
'connection': 'keep-alive',
|
|
219
|
-
};
|
|
220
|
-
// 复制关键头信息
|
|
221
|
-
['cache-control', 'etag', 'last-modified', 'content-encoding'].forEach(header => {
|
|
222
|
-
const value = response?.headers.get(header);
|
|
223
|
-
if (value)
|
|
224
|
-
safeHeaders[header] = value;
|
|
225
|
-
});
|
|
226
|
-
res.writeHead(response.status, safeHeaders);
|
|
227
|
-
// 流转换与传输
|
|
228
|
-
const nodeStream = Readable.fromWeb(response.body);
|
|
229
|
-
let isComplete = false;
|
|
230
|
-
const cleanUp = () => {
|
|
231
|
-
if (!isComplete)
|
|
232
|
-
nodeStream.destroy();
|
|
233
|
-
};
|
|
234
|
-
try {
|
|
235
|
-
req.on('close', cleanUp);
|
|
236
|
-
res.on('close', cleanUp);
|
|
237
|
-
await pipeline(nodeStream, res);
|
|
238
|
-
isComplete = true;
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
finally {
|
|
242
|
-
req.off('close', cleanUp);
|
|
243
|
-
res.off('close', cleanUp);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
catch (e) {
|
|
249
|
-
// 增强错误日志
|
|
250
|
-
if (e instanceof Error) {
|
|
251
|
-
console.error(e.code === 'ECONNREFUSED'
|
|
252
|
-
? `Registry ${normalizedRegistryUrl} unreachable [ECONNREFUSED]`
|
|
253
|
-
: `Error from ${normalizedRegistryUrl}: ${e.message}`);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
finally {
|
|
257
|
-
limiter.release();
|
|
222
|
+
const response = await fetchFromRegistry(registry, path, fullUrl.search, limiter);
|
|
223
|
+
if (response) {
|
|
224
|
+
successfulResponse = response;
|
|
225
|
+
break;
|
|
258
226
|
}
|
|
259
227
|
}
|
|
260
|
-
//
|
|
261
|
-
|
|
228
|
+
// 统一回写响应
|
|
229
|
+
if (successfulResponse) {
|
|
230
|
+
await writeResponse(res, successfulResponse, req, proxyInfo, proxyPort, registryInfos);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
res.writeHead(404).end('All upstream registries failed');
|
|
234
|
+
}
|
|
262
235
|
};
|
|
263
236
|
let server;
|
|
264
237
|
if (proxyInfo.https) {
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -187,6 +187,79 @@ async function loadProxyInfo(
|
|
|
187
187
|
return {registries, https, basePath};
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
async function fetchFromRegistry(
|
|
191
|
+
registry: RegistryInfo,
|
|
192
|
+
path: string,
|
|
193
|
+
search: string,
|
|
194
|
+
limiter: ConcurrencyLimiter
|
|
195
|
+
): Promise<Response | null> {
|
|
196
|
+
await limiter.acquire();
|
|
197
|
+
try {
|
|
198
|
+
const targetUrl = `${registry.normalizedRegistryUrl}${path}${search || ''}`;
|
|
199
|
+
console.log(`Fetching from: ${targetUrl}`);
|
|
200
|
+
const headers = registry.token ? { Authorization: `Bearer ${registry.token}` } : undefined;
|
|
201
|
+
const response = await fetch(targetUrl, { headers });
|
|
202
|
+
console.log(`Response from ${targetUrl}: ${response.status} ${response.statusText}`);
|
|
203
|
+
return response.ok ? response : null;
|
|
204
|
+
} catch (e) {
|
|
205
|
+
if (e instanceof Error) {
|
|
206
|
+
console.error(
|
|
207
|
+
(e as any).code === 'ECONNREFUSED'
|
|
208
|
+
? `Registry ${registry.normalizedRegistryUrl} unreachable [ECONNREFUSED]`
|
|
209
|
+
: `Error from ${registry.normalizedRegistryUrl}: ${e.message}`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
} finally {
|
|
214
|
+
limiter.release();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function writeResponse(
|
|
219
|
+
res: ServerResponse,
|
|
220
|
+
response: Response,
|
|
221
|
+
req: IncomingMessage,
|
|
222
|
+
proxyInfo: ProxyInfo,
|
|
223
|
+
proxyPort: number,
|
|
224
|
+
registryInfos: RegistryInfo[]
|
|
225
|
+
): Promise<void> {
|
|
226
|
+
const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
|
|
227
|
+
|
|
228
|
+
if (contentType.includes('application/json')) {
|
|
229
|
+
const data = await response.json() as PackageData;
|
|
230
|
+
if (data.versions) {
|
|
231
|
+
const host = req.headers.host || `localhost:${proxyPort}`;
|
|
232
|
+
const baseUrl = `${proxyInfo.https ? 'https' : 'http'}://${host}${proxyInfo.basePath === '/' ? '' : proxyInfo.basePath}`;
|
|
233
|
+
|
|
234
|
+
for (const version in data.versions) {
|
|
235
|
+
const tarball = data.versions[version]?.dist?.tarball;
|
|
236
|
+
if (tarball) {
|
|
237
|
+
const path = removeRegistryPrefix(tarball, registryInfos);
|
|
238
|
+
data.versions[version]!.dist!.tarball = `${baseUrl}${path}${new URL(tarball).search || ''}`;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
243
|
+
res.end(JSON.stringify(data));
|
|
244
|
+
} else {
|
|
245
|
+
const safeHeaders: OutgoingHttpHeaders = {
|
|
246
|
+
'content-type': contentType,
|
|
247
|
+
'connection': 'keep-alive',
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
['cache-control', 'etag', 'last-modified', 'content-encoding'].forEach(header => {
|
|
251
|
+
const value = response.headers.get(header);
|
|
252
|
+
if (value) safeHeaders[header] = value;
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
res.writeHead(response.status, safeHeaders);
|
|
256
|
+
await pipeline(
|
|
257
|
+
Readable.fromWeb(response.body as any),
|
|
258
|
+
res
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
190
263
|
export async function startProxyServer(
|
|
191
264
|
proxyConfigPath?: string,
|
|
192
265
|
localYarnConfigPath?: string,
|
|
@@ -205,121 +278,44 @@ export async function startProxyServer(
|
|
|
205
278
|
|
|
206
279
|
const requestHandler = async (req: IncomingMessage, res: ServerResponse) => {
|
|
207
280
|
if (!req.url || !req.headers.host) {
|
|
208
|
-
console.error('Invalid request: missing URL or host header');
|
|
209
281
|
res.writeHead(400).end('Invalid Request');
|
|
210
282
|
return;
|
|
211
283
|
}
|
|
212
284
|
|
|
213
285
|
const fullUrl = new URL(req.url, `${proxyInfo.https ? 'https' : 'http'}://${req.headers.host}`);
|
|
214
|
-
console.log(`Proxy server received request on ${fullUrl.toString()}`)
|
|
215
286
|
if (!fullUrl.pathname.startsWith(basePathPrefixedWithSlash)) {
|
|
216
|
-
console.error(`Path ${fullUrl.pathname} does not match basePath ${basePathPrefixedWithSlash}`);
|
|
217
287
|
res.writeHead(404).end('Not Found');
|
|
218
288
|
return;
|
|
219
289
|
}
|
|
220
290
|
|
|
221
|
-
const
|
|
222
|
-
|
|
291
|
+
const path = basePathPrefixedWithSlash === '/'
|
|
292
|
+
? fullUrl.pathname
|
|
293
|
+
: fullUrl.pathname.slice(basePathPrefixedWithSlash.length);
|
|
223
294
|
|
|
224
|
-
//
|
|
225
|
-
|
|
295
|
+
// 顺序尝试注册表,获取第一个成功响应
|
|
296
|
+
let successfulResponse: Response | null = null;
|
|
297
|
+
for (const registry of registryInfos) {
|
|
226
298
|
if (req.destroyed) break;
|
|
227
|
-
await limiter.acquire();
|
|
228
|
-
let response: Response | null = null;
|
|
229
|
-
try {
|
|
230
|
-
const targetUrl = `${normalizedRegistryUrl}${relativePathPrefixedWithSlash}${fullUrl.search || ''}`;
|
|
231
|
-
console.log(`Fetching from: ${targetUrl}`);
|
|
232
|
-
const headers = token ? {Authorization: `Bearer ${token}`} : undefined;
|
|
233
|
-
response = await fetch(targetUrl, {headers});
|
|
234
|
-
console.log(`Response from ${targetUrl}: ${response.status} ${response.statusText}`);
|
|
235
|
-
|
|
236
|
-
if (response.ok) {
|
|
237
|
-
const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
|
|
238
|
-
|
|
239
|
-
if (contentType.includes('application/json')) {
|
|
240
|
-
// application/json 元数据
|
|
241
|
-
try {
|
|
242
|
-
const data = await response.json() as PackageData;
|
|
243
|
-
if (data.versions) {
|
|
244
|
-
const requestHeadersHostFromYarnClient = req.headers.host || 'localhost:' + proxyPort;
|
|
245
|
-
console.log("Request headers.host from yarn client is", requestHeadersHostFromYarnClient);
|
|
246
|
-
const proxyBaseUrlNoSuffixedWithSlash = `${proxyInfo.https ? 'https' : 'http'}://${requestHeadersHostFromYarnClient}${basePathPrefixedWithSlash === '/' ? '' : basePathPrefixedWithSlash}`;
|
|
247
|
-
console.log("proxyBaseUrlNoSuffixedWithSlash", proxyBaseUrlNoSuffixedWithSlash);
|
|
248
|
-
for (const version in data.versions) {
|
|
249
|
-
const dist = data.versions[version]?.dist;
|
|
250
|
-
if (dist?.tarball) {
|
|
251
|
-
const originalUrl = new URL(dist.tarball);
|
|
252
|
-
const originalSearchParamsStr = originalUrl.search || '';
|
|
253
|
-
const tarballPathPrefixedWithSlash = removeRegistryPrefix(dist.tarball, registryInfos);
|
|
254
|
-
dist.tarball = `${proxyBaseUrlNoSuffixedWithSlash}${tarballPathPrefixedWithSlash}${originalSearchParamsStr}`;
|
|
255
|
-
if (!tarballPathPrefixedWithSlash.startsWith("/")) console.error("bad tarballPath, must be PrefixedWithSlash", tarballPathPrefixedWithSlash);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
res.writeHead(200, {'Content-Type': 'application/json'});
|
|
260
|
-
res.end(JSON.stringify(data));
|
|
261
|
-
return;
|
|
262
|
-
} catch (e) {
|
|
263
|
-
console.error('Failed to parse JSON response:', e);
|
|
264
|
-
// 继续尝试下一个注册表
|
|
265
|
-
}
|
|
266
|
-
} else {
|
|
267
|
-
// 二进制流处理
|
|
268
|
-
if (!response.body) {
|
|
269
|
-
console.error(`Empty response body from ${response.url}`);
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
299
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
res.writeHead(response.status, safeHeaders);
|
|
286
|
-
|
|
287
|
-
// 流转换与传输
|
|
288
|
-
const nodeStream = Readable.fromWeb(response.body as any);
|
|
289
|
-
let isComplete = false;
|
|
290
|
-
const cleanUp = () => {
|
|
291
|
-
if (!isComplete) nodeStream.destroy();
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
try {
|
|
296
|
-
req.on('close', cleanUp);
|
|
297
|
-
res.on('close', cleanUp);
|
|
298
|
-
await pipeline(nodeStream, res);
|
|
299
|
-
isComplete = true;
|
|
300
|
-
return;
|
|
301
|
-
} finally {
|
|
302
|
-
req.off('close', cleanUp);
|
|
303
|
-
res.off('close', cleanUp);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
} catch (e) {
|
|
308
|
-
// 增强错误日志
|
|
309
|
-
if (e instanceof Error) {
|
|
310
|
-
console.error(
|
|
311
|
-
(e as any).code === 'ECONNREFUSED'
|
|
312
|
-
? `Registry ${normalizedRegistryUrl} unreachable [ECONNREFUSED]`
|
|
313
|
-
: `Error from ${normalizedRegistryUrl}: ${e.message}`
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
} finally {
|
|
317
|
-
limiter.release();
|
|
300
|
+
const response = await fetchFromRegistry(
|
|
301
|
+
registry,
|
|
302
|
+
path,
|
|
303
|
+
fullUrl.search,
|
|
304
|
+
limiter
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
if (response) {
|
|
308
|
+
successfulResponse = response;
|
|
309
|
+
break;
|
|
318
310
|
}
|
|
319
311
|
}
|
|
320
312
|
|
|
321
|
-
//
|
|
322
|
-
|
|
313
|
+
// 统一回写响应
|
|
314
|
+
if (successfulResponse) {
|
|
315
|
+
await writeResponse(res, successfulResponse, req, proxyInfo, proxyPort, registryInfos);
|
|
316
|
+
} else {
|
|
317
|
+
res.writeHead(404).end('All upstream registries failed');
|
|
318
|
+
}
|
|
323
319
|
};
|
|
324
320
|
|
|
325
321
|
let server: HttpServer | HttpsServer;
|