@verdaccio/proxy 6.0.0-6-next.18 → 6.0.0-6-next.21
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/CHANGELOG.md +96 -0
- package/build/agent.d.ts +17 -0
- package/build/agent.js +63 -0
- package/build/agent.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.js +4 -4
- package/build/index.js.map +1 -1
- package/build/proxy-utils.js.map +1 -1
- package/build/{up-storage.d.ts → proxy.d.ts} +29 -55
- package/build/proxy.js +565 -0
- package/build/proxy.js.map +1 -0
- package/jest.config.js +4 -5
- package/package.json +66 -68
- package/src/agent.ts +54 -0
- package/src/index.ts +1 -1
- package/src/{up-storage.ts → proxy.ts} +254 -357
- package/test/conf/proxy1.yaml +1 -1
- package/test/headers.auth.spec.ts +15 -18
- package/test/noProxy.spec.ts +215 -76
- package/test/partials/ssl-cert-snakeoil.key +15 -0
- package/test/partials/ssl-cert-snakeoil.pem +12 -0
- package/test/proxy.error.___.ts +126 -0
- package/test/proxy.metadata.spec.ts +354 -0
- package/test/proxy.search.spec.ts +7 -13
- package/test/proxy.tarball.spec.ts +142 -204
- package/tsconfig.json +0 -6
- package/build/up-storage.js +0 -693
- package/build/up-storage.js.map +0 -1
- package/test/proxy.error.spec.ts +0 -126
- package/test/proxy.metadata.ts +0 -133
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import nock from 'nock';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { setTimeout } from 'timers/promises';
|
|
4
|
+
|
|
5
|
+
import { Config, parseConfigFile } from '@verdaccio/config';
|
|
6
|
+
import { API_ERROR, errorUtils } from '@verdaccio/core';
|
|
7
|
+
|
|
8
|
+
import { ProxyStorage } from '../src';
|
|
9
|
+
|
|
10
|
+
const getConf = (name) => path.join(__dirname, '/conf', name);
|
|
11
|
+
|
|
12
|
+
const mockDebug = jest.fn();
|
|
13
|
+
const mockInfo = jest.fn();
|
|
14
|
+
const mockHttp = jest.fn();
|
|
15
|
+
const mockError = jest.fn();
|
|
16
|
+
const mockWarn = jest.fn();
|
|
17
|
+
|
|
18
|
+
// mock to get the headers fixed value
|
|
19
|
+
jest.mock('crypto', () => {
|
|
20
|
+
return {
|
|
21
|
+
randomBytes: (): { toString: () => string } => {
|
|
22
|
+
return {
|
|
23
|
+
toString: (): string => 'foo-random-bytes',
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
pseudoRandomBytes: (): { toString: () => string } => {
|
|
27
|
+
return {
|
|
28
|
+
toString: (): string => 'foo-phseudo-bytes',
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
jest.mock('@verdaccio/logger', () => {
|
|
35
|
+
const originalLogger = jest.requireActual('@verdaccio/logger');
|
|
36
|
+
return {
|
|
37
|
+
...originalLogger,
|
|
38
|
+
logger: {
|
|
39
|
+
child: () => ({
|
|
40
|
+
debug: (arg, msg) => mockDebug(arg, msg),
|
|
41
|
+
info: (arg, msg) => mockInfo(arg, msg),
|
|
42
|
+
http: (arg, msg) => mockHttp(arg, msg),
|
|
43
|
+
error: (arg, msg) => mockError(arg, msg),
|
|
44
|
+
warn: (arg, msg) => mockWarn(arg, msg),
|
|
45
|
+
}),
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const domain = 'https://registry.npmjs.org';
|
|
51
|
+
|
|
52
|
+
describe('proxy', () => {
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
nock.cleanAll();
|
|
55
|
+
});
|
|
56
|
+
const defaultRequestOptions = {
|
|
57
|
+
url: 'https://registry.npmjs.org',
|
|
58
|
+
};
|
|
59
|
+
const proxyPath = getConf('proxy1.yaml');
|
|
60
|
+
const conf = new Config(parseConfigFile(proxyPath));
|
|
61
|
+
|
|
62
|
+
describe('getRemoteMetadataNext', () => {
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
nock.cleanAll();
|
|
65
|
+
nock.abortPendingRequests();
|
|
66
|
+
jest.clearAllMocks();
|
|
67
|
+
});
|
|
68
|
+
describe('basic requests', () => {
|
|
69
|
+
test('success call to remote', async () => {
|
|
70
|
+
nock(domain, {
|
|
71
|
+
reqheaders: {
|
|
72
|
+
accept: 'application/json;',
|
|
73
|
+
'accept-encoding': 'gzip',
|
|
74
|
+
'x-forwarded-for': '127.0.0.1',
|
|
75
|
+
via: '1.1 foo-phseudo-bytes (Verdaccio)',
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
.get('/jquery')
|
|
79
|
+
.reply(200, { body: 'test' });
|
|
80
|
+
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
|
81
|
+
const [manifest] = await prox1.getRemoteMetadataNext('jquery', {
|
|
82
|
+
remoteAddress: '127.0.0.1',
|
|
83
|
+
});
|
|
84
|
+
expect(manifest).toEqual({ body: 'test' });
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('etag header', () => {
|
|
89
|
+
test('proxy call with etag', async () => {
|
|
90
|
+
nock(domain, {
|
|
91
|
+
reqheaders: {
|
|
92
|
+
accept: 'application/json;',
|
|
93
|
+
'accept-encoding': 'gzip',
|
|
94
|
+
'x-forwarded-for': '127.0.0.1',
|
|
95
|
+
via: '1.1 foo-phseudo-bytes (Verdaccio)',
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
.get('/jquery')
|
|
99
|
+
.reply(
|
|
100
|
+
200,
|
|
101
|
+
{ body: 'test' },
|
|
102
|
+
{
|
|
103
|
+
etag: () => `_ref_4444`,
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
|
107
|
+
const [manifest, etag] = await prox1.getRemoteMetadataNext('jquery', {
|
|
108
|
+
remoteAddress: '127.0.0.1',
|
|
109
|
+
});
|
|
110
|
+
expect(etag).toEqual('_ref_4444');
|
|
111
|
+
expect(manifest).toEqual({ body: 'test' });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('proxy call with etag as option', async () => {
|
|
115
|
+
nock(domain, {
|
|
116
|
+
reqheaders: {
|
|
117
|
+
accept: 'application/json;',
|
|
118
|
+
'accept-encoding': 'gzip',
|
|
119
|
+
'x-forwarded-for': '127.0.0.1',
|
|
120
|
+
via: '1.1 foo-phseudo-bytes (Verdaccio)',
|
|
121
|
+
// match only if etag is set as option
|
|
122
|
+
'if-none-match': 'foo',
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
.get('/jquery')
|
|
126
|
+
.reply(
|
|
127
|
+
200,
|
|
128
|
+
{ body: 'test' },
|
|
129
|
+
{
|
|
130
|
+
etag: () => `_ref_4444`,
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
|
134
|
+
const [manifest, etag] = await prox1.getRemoteMetadataNext('jquery', {
|
|
135
|
+
etag: 'foo',
|
|
136
|
+
remoteAddress: '127.0.0.1',
|
|
137
|
+
});
|
|
138
|
+
expect(etag).toEqual('_ref_4444');
|
|
139
|
+
expect(manifest).toEqual({ body: 'test' });
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('log activity', () => {
|
|
144
|
+
test('proxy call with etag', async () => {
|
|
145
|
+
nock(domain)
|
|
146
|
+
.get('/jquery')
|
|
147
|
+
.reply(200, { body: { name: 'foo', version: '1.0.0' } }, {});
|
|
148
|
+
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
|
149
|
+
await prox1.getRemoteMetadataNext('jquery', {
|
|
150
|
+
remoteAddress: '127.0.0.1',
|
|
151
|
+
});
|
|
152
|
+
expect(mockHttp).toHaveBeenCalledTimes(2);
|
|
153
|
+
expect(mockHttp).toHaveBeenCalledWith(
|
|
154
|
+
{
|
|
155
|
+
request: { method: 'GET', url: `${domain}/jquery` },
|
|
156
|
+
status: 200,
|
|
157
|
+
},
|
|
158
|
+
"@{!status}, req: '@{request.method} @{request.url}' (streaming)"
|
|
159
|
+
);
|
|
160
|
+
expect(mockHttp).toHaveBeenLastCalledWith(
|
|
161
|
+
{
|
|
162
|
+
request: { method: 'GET', url: `${domain}/jquery` },
|
|
163
|
+
status: 200,
|
|
164
|
+
bytes: {
|
|
165
|
+
in: 0,
|
|
166
|
+
out: 41,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
"@{!status}, req: '@{request.method} @{request.url}'"
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('error handling', () => {
|
|
175
|
+
test('proxy call with 304', async () => {
|
|
176
|
+
nock(domain).get('/jquery').reply(304);
|
|
177
|
+
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
|
178
|
+
await expect(prox1.getRemoteMetadataNext('jquery', { etag: 'rev_3333' })).rejects.toThrow(
|
|
179
|
+
'no data'
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('reply with error', async () => {
|
|
184
|
+
nock(domain).get('/jquery').replyWithError('something awful happened');
|
|
185
|
+
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
|
186
|
+
await expect(
|
|
187
|
+
prox1.getRemoteMetadataNext('jquery', {
|
|
188
|
+
remoteAddress: '127.0.0.1',
|
|
189
|
+
})
|
|
190
|
+
).rejects.toThrowError(new Error('something awful happened'));
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('reply with 409 error', async () => {
|
|
194
|
+
nock(domain).get('/jquery').reply(409);
|
|
195
|
+
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
|
196
|
+
await expect(prox1.getRemoteMetadataNext('jquery', { retry: 0 })).rejects.toThrow(
|
|
197
|
+
new Error('bad status code: 409')
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('reply with bad body json format', async () => {
|
|
202
|
+
nock(domain).get('/jquery').reply(200, 'some-text');
|
|
203
|
+
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
|
204
|
+
await expect(
|
|
205
|
+
prox1.getRemoteMetadataNext('jquery', {
|
|
206
|
+
remoteAddress: '127.0.0.1',
|
|
207
|
+
})
|
|
208
|
+
).rejects.toThrowError(
|
|
209
|
+
new Error(
|
|
210
|
+
'Unexpected token s in JSON at position 0 in "https://registry.npmjs.org/jquery"'
|
|
211
|
+
)
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('400 error proxy call', async () => {
|
|
216
|
+
nock(domain).get('/jquery').reply(409);
|
|
217
|
+
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
|
218
|
+
await expect(
|
|
219
|
+
prox1.getRemoteMetadataNext('jquery', {
|
|
220
|
+
remoteAddress: '127.0.0.1',
|
|
221
|
+
})
|
|
222
|
+
).rejects.toThrowError(
|
|
223
|
+
errorUtils.getInternalError(`${errorUtils.API_ERROR.BAD_STATUS_CODE}: 409`)
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('proxy not found', async () => {
|
|
228
|
+
nock(domain).get('/jquery').reply(404);
|
|
229
|
+
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
|
230
|
+
await expect(
|
|
231
|
+
prox1.getRemoteMetadataNext('jquery', {
|
|
232
|
+
remoteAddress: '127.0.0.1',
|
|
233
|
+
})
|
|
234
|
+
).rejects.toThrowError(errorUtils.getNotFound(API_ERROR.NOT_PACKAGE_UPLINK));
|
|
235
|
+
expect(mockHttp).toHaveBeenCalledTimes(1);
|
|
236
|
+
expect(mockHttp).toHaveBeenLastCalledWith(
|
|
237
|
+
{
|
|
238
|
+
request: { method: 'GET', url: `${domain}/jquery` },
|
|
239
|
+
status: 404,
|
|
240
|
+
},
|
|
241
|
+
"@{!status}, req: '@{request.method} @{request.url}' (streaming)"
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('retry', () => {
|
|
247
|
+
test('retry twice on 500 and return 200 logging offline activity', async () => {
|
|
248
|
+
nock(domain)
|
|
249
|
+
.get('/jquery')
|
|
250
|
+
.twice()
|
|
251
|
+
.reply(500, 'some-text')
|
|
252
|
+
.get('/jquery')
|
|
253
|
+
.once()
|
|
254
|
+
.reply(200, { body: { name: 'foo', version: '1.0.0' } });
|
|
255
|
+
|
|
256
|
+
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
|
257
|
+
const [manifest] = await prox1.getRemoteMetadataNext('jquery', {
|
|
258
|
+
retry: { limit: 2 },
|
|
259
|
+
});
|
|
260
|
+
expect(manifest).toEqual({ body: { name: 'foo', version: '1.0.0' } });
|
|
261
|
+
expect(mockInfo).toHaveBeenCalledTimes(2);
|
|
262
|
+
expect(mockInfo).toHaveBeenLastCalledWith(
|
|
263
|
+
{
|
|
264
|
+
error: 'Response code 500 (Internal Server Error)',
|
|
265
|
+
request: { method: 'GET', url: `${domain}/jquery` },
|
|
266
|
+
retryCount: 2,
|
|
267
|
+
},
|
|
268
|
+
"retry @{retryCount} req: '@{request.method} @{request.url}'"
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test('retry is exceded and uplink goes offline with logging activity', async () => {
|
|
273
|
+
nock(domain).get('/jquery').times(10).reply(500);
|
|
274
|
+
|
|
275
|
+
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
|
276
|
+
await expect(
|
|
277
|
+
prox1.getRemoteMetadataNext('jquery', {
|
|
278
|
+
remoteAddress: '127.0.0.1',
|
|
279
|
+
retry: { limit: 2 },
|
|
280
|
+
})
|
|
281
|
+
).rejects.toThrowError();
|
|
282
|
+
await expect(
|
|
283
|
+
prox1.getRemoteMetadataNext('jquery', {
|
|
284
|
+
remoteAddress: '127.0.0.1',
|
|
285
|
+
retry: { limit: 2 },
|
|
286
|
+
})
|
|
287
|
+
).rejects.toThrowError(errorUtils.getInternalError(errorUtils.API_ERROR.UPLINK_OFFLINE));
|
|
288
|
+
expect(mockWarn).toHaveBeenCalledTimes(1);
|
|
289
|
+
expect(mockWarn).toHaveBeenLastCalledWith(
|
|
290
|
+
{
|
|
291
|
+
host: 'registry.npmjs.org',
|
|
292
|
+
},
|
|
293
|
+
'host @{host} is now offline'
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('fails calls and recover with 200 with log online activity', async () => {
|
|
298
|
+
// This unit test is designed to verify if the uplink goes to offline
|
|
299
|
+
// and recover after the fail_timeout has expired.
|
|
300
|
+
nock(domain)
|
|
301
|
+
.get('/jquery')
|
|
302
|
+
.thrice()
|
|
303
|
+
.reply(500, 'some-text')
|
|
304
|
+
.get('/jquery')
|
|
305
|
+
.once()
|
|
306
|
+
.reply(200, { body: { name: 'foo', version: '1.0.0' } });
|
|
307
|
+
|
|
308
|
+
const prox1 = new ProxyStorage(
|
|
309
|
+
{ ...defaultRequestOptions, fail_timeout: '1s', max_fails: 1 },
|
|
310
|
+
conf
|
|
311
|
+
);
|
|
312
|
+
// force retry
|
|
313
|
+
await expect(
|
|
314
|
+
prox1.getRemoteMetadataNext('jquery', {
|
|
315
|
+
remoteAddress: '127.0.0.1',
|
|
316
|
+
retry: { limit: 2 },
|
|
317
|
+
})
|
|
318
|
+
).rejects.toThrowError();
|
|
319
|
+
// display offline error on exausted retry
|
|
320
|
+
await expect(
|
|
321
|
+
prox1.getRemoteMetadataNext('jquery', {
|
|
322
|
+
remoteAddress: '127.0.0.1',
|
|
323
|
+
retry: { limit: 2 },
|
|
324
|
+
})
|
|
325
|
+
).rejects.toThrowError(errorUtils.getInternalError(errorUtils.API_ERROR.UPLINK_OFFLINE));
|
|
326
|
+
expect(mockWarn).toHaveBeenCalledTimes(2);
|
|
327
|
+
expect(mockWarn).toHaveBeenLastCalledWith(
|
|
328
|
+
{
|
|
329
|
+
host: 'registry.npmjs.org',
|
|
330
|
+
},
|
|
331
|
+
'host @{host} is now offline'
|
|
332
|
+
);
|
|
333
|
+
expect(mockWarn).toHaveBeenLastCalledWith(
|
|
334
|
+
{
|
|
335
|
+
host: 'registry.npmjs.org',
|
|
336
|
+
},
|
|
337
|
+
'host @{host} is now offline'
|
|
338
|
+
);
|
|
339
|
+
// this is based on max_fails, if change that also change here acordingly
|
|
340
|
+
await setTimeout(3000);
|
|
341
|
+
const [manifest] = await prox1.getRemoteMetadataNext('jquery', {
|
|
342
|
+
retry: { limit: 2 },
|
|
343
|
+
});
|
|
344
|
+
expect(manifest).toEqual({ body: { name: 'foo', version: '1.0.0' } });
|
|
345
|
+
expect(mockWarn).toHaveBeenLastCalledWith(
|
|
346
|
+
{
|
|
347
|
+
host: 'registry.npmjs.org',
|
|
348
|
+
},
|
|
349
|
+
'host @{host} is now online'
|
|
350
|
+
);
|
|
351
|
+
}, 10000);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
});
|
|
@@ -3,20 +3,16 @@
|
|
|
3
3
|
/* global AbortController */
|
|
4
4
|
import getStream from 'get-stream';
|
|
5
5
|
import path from 'path';
|
|
6
|
-
import
|
|
6
|
+
import { MockAgent, setGlobalDispatcher } from 'undici';
|
|
7
7
|
|
|
8
8
|
import { Config, parseConfigFile } from '@verdaccio/config';
|
|
9
9
|
import { streamUtils } from '@verdaccio/core';
|
|
10
10
|
|
|
11
|
-
import { ProxyStorage } from '../src
|
|
12
|
-
|
|
13
|
-
// FUTURE: remove me when v15 is the min required version
|
|
14
|
-
if (semver.lte(process.version, 'v15.0.0')) {
|
|
15
|
-
global.AbortController = require('abortcontroller-polyfill/dist/cjs-ponyfill').AbortController;
|
|
16
|
-
}
|
|
11
|
+
import { ProxyStorage } from '../src';
|
|
17
12
|
|
|
18
13
|
const getConf = (name) => path.join(__dirname, '/conf', name);
|
|
19
14
|
|
|
15
|
+
// TODO: we can mock this globally maybe
|
|
20
16
|
const mockDebug = jest.fn();
|
|
21
17
|
const mockInfo = jest.fn();
|
|
22
18
|
const mockHttp = jest.fn();
|
|
@@ -38,8 +34,6 @@ jest.mock('@verdaccio/logger', () => {
|
|
|
38
34
|
};
|
|
39
35
|
});
|
|
40
36
|
|
|
41
|
-
const { MockAgent } = require('undici');
|
|
42
|
-
const { setGlobalDispatcher } = require('undici-fetch');
|
|
43
37
|
const domain = 'https://registry.npmjs.org';
|
|
44
38
|
|
|
45
39
|
describe('proxy', () => {
|
|
@@ -56,7 +50,7 @@ describe('proxy', () => {
|
|
|
56
50
|
};
|
|
57
51
|
|
|
58
52
|
describe('search', () => {
|
|
59
|
-
test('get response from
|
|
53
|
+
test('get response from endpoint', async () => {
|
|
60
54
|
const response = require('./partials/search-v1.json');
|
|
61
55
|
const mockAgent = new MockAgent({ connections: 1 });
|
|
62
56
|
mockAgent.disableNetConnect();
|
|
@@ -90,13 +84,13 @@ describe('proxy', () => {
|
|
|
90
84
|
).rejects.toThrow('bad status code 409 from uplink');
|
|
91
85
|
});
|
|
92
86
|
|
|
93
|
-
test.todo('abort search from
|
|
87
|
+
test.todo('abort search from endpoint');
|
|
94
88
|
|
|
95
89
|
// TODO: we should test the gzip deflate here, but is hard to test
|
|
96
90
|
// fix me if you can deal with Incorrect Header Check issue
|
|
97
|
-
test.todo('get file from
|
|
91
|
+
test.todo('get file from endpoint with gzip headers');
|
|
98
92
|
|
|
99
|
-
test('search
|
|
93
|
+
test('search endpoint fails', async () => {
|
|
100
94
|
const mockAgent = new MockAgent({ connections: 1 });
|
|
101
95
|
mockAgent.disableNetConnect();
|
|
102
96
|
setGlobalDispatcher(mockAgent);
|