http2wrap 2.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.
@@ -0,0 +1,563 @@
1
+ 'use strict';
2
+ // See https://github.com/facebook/jest/issues/2549
3
+ // eslint-disable-next-line node/prefer-global/url
4
+ const {URL, urlToHttpOptions} = require('url');
5
+ const http2 = require('http2');
6
+ const {Writable} = require('stream');
7
+ const {Agent, globalAgent} = require('./agent.js');
8
+ const IncomingMessage = require('./incoming-message.js');
9
+ const proxyEvents = require('./utils/proxy-events.js');
10
+ const {
11
+ ERR_INVALID_ARG_TYPE,
12
+ ERR_INVALID_PROTOCOL,
13
+ ERR_HTTP_HEADERS_SENT
14
+ } = require('./utils/errors.js');
15
+ const validateHeaderName = require('./utils/validate-header-name.js');
16
+ const validateHeaderValue = require('./utils/validate-header-value.js');
17
+ const proxySocketHandler = require('./utils/proxy-socket-handler.js');
18
+
19
+ const {
20
+ HTTP2_HEADER_STATUS,
21
+ HTTP2_HEADER_METHOD,
22
+ HTTP2_HEADER_PATH,
23
+ HTTP2_HEADER_AUTHORITY,
24
+ HTTP2_METHOD_CONNECT
25
+ } = http2.constants;
26
+
27
+ const kHeaders = Symbol('headers');
28
+ const kOrigin = Symbol('origin');
29
+ const kSession = Symbol('session');
30
+ const kOptions = Symbol('options');
31
+ const kFlushedHeaders = Symbol('flushedHeaders');
32
+ const kJobs = Symbol('jobs');
33
+ const kPendingAgentPromise = Symbol('pendingAgentPromise');
34
+
35
+ class ClientRequest extends Writable {
36
+ constructor(input, options, callback) {
37
+ super({
38
+ autoDestroy: false,
39
+ emitClose: false
40
+ });
41
+
42
+ if (typeof input === 'string') {
43
+ input = urlToHttpOptions(new URL(input));
44
+ } else if (input instanceof URL) {
45
+ input = urlToHttpOptions(input);
46
+ } else {
47
+ input = {...input};
48
+ }
49
+
50
+ if (typeof options === 'function' || options === undefined) {
51
+ // (options, callback)
52
+ callback = options;
53
+ options = input;
54
+ } else {
55
+ // (input, options, callback)
56
+ options = Object.assign(input, options);
57
+ }
58
+
59
+ if (options.h2session) {
60
+ this[kSession] = options.h2session;
61
+
62
+ if (this[kSession].destroyed) {
63
+ throw new Error('The session has been closed already');
64
+ }
65
+
66
+ this.protocol = this[kSession].socket.encrypted ? 'https:' : 'http:';
67
+ } else if (options.agent === false) {
68
+ this.agent = new Agent({maxEmptySessions: 0});
69
+ } else if (typeof options.agent === 'undefined' || options.agent === null) {
70
+ this.agent = globalAgent;
71
+ } else if (typeof options.agent.request === 'function') {
72
+ this.agent = options.agent;
73
+ } else {
74
+ throw new ERR_INVALID_ARG_TYPE('options.agent', ['http2wrapper.Agent-like Object', 'undefined', 'false'], options.agent);
75
+ }
76
+
77
+ if (this.agent) {
78
+ this.protocol = this.agent.protocol;
79
+ }
80
+
81
+ if (options.protocol && options.protocol !== this.protocol) {
82
+ throw new ERR_INVALID_PROTOCOL(options.protocol, this.protocol);
83
+ }
84
+
85
+ if (!options.port) {
86
+ options.port = options.defaultPort || (this.agent && this.agent.defaultPort) || 443;
87
+ }
88
+
89
+ options.host = options.hostname || options.host || 'localhost';
90
+
91
+ // Unused
92
+ delete options.hostname;
93
+
94
+ const {timeout} = options;
95
+ options.timeout = undefined;
96
+
97
+ this[kHeaders] = Object.create(null);
98
+ this[kJobs] = [];
99
+
100
+ this[kPendingAgentPromise] = undefined;
101
+
102
+ this.socket = null;
103
+ this.connection = null;
104
+
105
+ this.method = options.method || 'GET';
106
+
107
+ if (!(this.method === 'CONNECT' && (options.path === '/' || options.path === undefined))) {
108
+ this.path = options.path;
109
+ }
110
+
111
+ this.res = null;
112
+ this.aborted = false;
113
+ this.reusedSocket = false;
114
+
115
+ const {headers} = options;
116
+ if (headers) {
117
+ // eslint-disable-next-line guard-for-in
118
+ for (const header in headers) {
119
+ this.setHeader(header, headers[header]);
120
+ }
121
+ }
122
+
123
+ if (options.auth && !('authorization' in this[kHeaders])) {
124
+ this[kHeaders].authorization = 'Basic ' + Buffer.from(options.auth).toString('base64');
125
+ }
126
+
127
+ options.session = options.tlsSession;
128
+ options.path = options.socketPath;
129
+
130
+ this[kOptions] = options;
131
+
132
+ // Clients that generate HTTP/2 requests directly SHOULD use the :authority pseudo-header field instead of the Host header field.
133
+ this[kOrigin] = new URL(`${this.protocol}//${options.servername || options.host}:${options.port}`);
134
+
135
+ // A socket is being reused
136
+ const reuseSocket = options._reuseSocket;
137
+ if (reuseSocket) {
138
+ options.createConnection = (...args) => {
139
+ if (reuseSocket.destroyed) {
140
+ return this.agent.createConnection(...args);
141
+ }
142
+
143
+ return reuseSocket;
144
+ };
145
+
146
+ // eslint-disable-next-line promise/prefer-await-to-then
147
+ this.agent.getSession(this[kOrigin], this[kOptions]).catch(() => {});
148
+ }
149
+
150
+ if (timeout) {
151
+ this.setTimeout(timeout);
152
+ }
153
+
154
+ if (callback) {
155
+ this.once('response', callback);
156
+ }
157
+
158
+ this[kFlushedHeaders] = false;
159
+ }
160
+
161
+ get method() {
162
+ return this[kHeaders][HTTP2_HEADER_METHOD];
163
+ }
164
+
165
+ set method(value) {
166
+ if (value) {
167
+ this[kHeaders][HTTP2_HEADER_METHOD] = value.toUpperCase();
168
+ }
169
+ }
170
+
171
+ get path() {
172
+ const header = this.method === 'CONNECT' ? HTTP2_HEADER_AUTHORITY : HTTP2_HEADER_PATH;
173
+
174
+ return this[kHeaders][header];
175
+ }
176
+
177
+ set path(value) {
178
+ if (value) {
179
+ const header = this.method === 'CONNECT' ? HTTP2_HEADER_AUTHORITY : HTTP2_HEADER_PATH;
180
+
181
+ this[kHeaders][header] = value;
182
+ }
183
+ }
184
+
185
+ get host() {
186
+ return this[kOrigin].hostname;
187
+ }
188
+
189
+ set host(_value) {
190
+ // Do nothing as this is read only.
191
+ }
192
+
193
+ get _mustNotHaveABody() {
194
+ return this.method === 'GET' || this.method === 'HEAD' || this.method === 'DELETE';
195
+ }
196
+
197
+ _write(chunk, encoding, callback) {
198
+ // https://github.com/nodejs/node/blob/654df09ae0c5e17d1b52a900a545f0664d8c7627/lib/internal/http2/util.js#L148-L156
199
+ if (this._mustNotHaveABody) {
200
+ callback(new Error('The GET, HEAD and DELETE methods must NOT have a body'));
201
+ /* istanbul ignore next: Node.js 12 throws directly */
202
+ return;
203
+ }
204
+
205
+ this.flushHeaders();
206
+
207
+ const callWrite = () => this._request.write(chunk, encoding, callback);
208
+ if (this._request) {
209
+ callWrite();
210
+ } else {
211
+ this[kJobs].push(callWrite);
212
+ }
213
+ }
214
+
215
+ _final(callback) {
216
+ this.flushHeaders();
217
+
218
+ const callEnd = () => {
219
+ // For GET, HEAD and DELETE and CONNECT
220
+ if (this._mustNotHaveABody || this.method === 'CONNECT') {
221
+ callback();
222
+ return;
223
+ }
224
+
225
+ this._request.end(callback);
226
+ };
227
+
228
+ if (this._request) {
229
+ callEnd();
230
+ } else {
231
+ this[kJobs].push(callEnd);
232
+ }
233
+ }
234
+
235
+ abort() {
236
+ if (this.res && this.res.complete) {
237
+ return;
238
+ }
239
+
240
+ if (!this.aborted) {
241
+ process.nextTick(() => this.emit('abort'));
242
+ }
243
+
244
+ this.aborted = true;
245
+
246
+ this.destroy();
247
+ }
248
+
249
+ async _destroy(error, callback) {
250
+ if (this.res) {
251
+ this.res._dump();
252
+ }
253
+
254
+ if (this._request) {
255
+ this._request.destroy();
256
+ } else {
257
+ process.nextTick(() => {
258
+ this.emit('close');
259
+ });
260
+ }
261
+
262
+ try {
263
+ await this[kPendingAgentPromise];
264
+ } catch (internalError) {
265
+ if (this.aborted) {
266
+ error = internalError;
267
+ }
268
+ }
269
+
270
+ callback(error);
271
+ }
272
+
273
+ async flushHeaders() {
274
+ if (this[kFlushedHeaders] || this.destroyed) {
275
+ return;
276
+ }
277
+
278
+ this[kFlushedHeaders] = true;
279
+
280
+ const isConnectMethod = this.method === HTTP2_METHOD_CONNECT;
281
+
282
+ // The real magic is here
283
+ const onStream = stream => {
284
+ this._request = stream;
285
+
286
+ if (this.destroyed) {
287
+ stream.destroy();
288
+ return;
289
+ }
290
+
291
+ // Forwards `timeout`, `continue`, `close` and `error` events to this instance.
292
+ if (!isConnectMethod) {
293
+ // TODO: Should we proxy `close` here?
294
+ proxyEvents(stream, this, ['timeout', 'continue']);
295
+ }
296
+
297
+ stream.once('error', error => {
298
+ this.destroy(error);
299
+ });
300
+
301
+ stream.once('aborted', () => {
302
+ const {res} = this;
303
+ if (res) {
304
+ res.aborted = true;
305
+ res.emit('aborted');
306
+ res.destroy();
307
+ } else {
308
+ this.destroy(new Error('The server aborted the HTTP/2 stream'));
309
+ }
310
+ });
311
+
312
+ const onResponse = (headers, flags, rawHeaders) => {
313
+ // If we were to emit raw request stream, it would be as fast as the native approach.
314
+ // Note that wrapping the raw stream in a Proxy instance won't improve the performance (already tested it).
315
+ const response = new IncomingMessage(this.socket, stream.readableHighWaterMark);
316
+ this.res = response;
317
+
318
+ // Undocumented, but it is used by `cacheable-request`
319
+ response.url = `${this[kOrigin].origin}${this.path}`;
320
+
321
+ response.req = this;
322
+ response.statusCode = headers[HTTP2_HEADER_STATUS];
323
+ response.headers = headers;
324
+ response.rawHeaders = rawHeaders;
325
+
326
+ response.once('end', () => {
327
+ response.complete = true;
328
+
329
+ // Has no effect, just be consistent with the Node.js behavior
330
+ response.socket = null;
331
+ response.connection = null;
332
+ });
333
+
334
+ if (isConnectMethod) {
335
+ response.upgrade = true;
336
+
337
+ // The HTTP1 API says the socket is detached here,
338
+ // but we can't do that so we pass the original HTTP2 request.
339
+ if (this.emit('connect', response, stream, Buffer.alloc(0))) {
340
+ this.emit('close');
341
+ } else {
342
+ // No listeners attached, destroy the original request.
343
+ stream.destroy();
344
+ }
345
+ } else {
346
+ // Forwards data
347
+ stream.on('data', chunk => {
348
+ if (!response._dumped && !response.push(chunk)) {
349
+ stream.pause();
350
+ }
351
+ });
352
+
353
+ stream.once('end', () => {
354
+ if (!this.aborted) {
355
+ response.push(null);
356
+ }
357
+ });
358
+
359
+ if (!this.emit('response', response)) {
360
+ // No listeners attached, dump the response.
361
+ response._dump();
362
+ }
363
+ }
364
+ };
365
+
366
+ // This event tells we are ready to listen for the data.
367
+ stream.once('response', onResponse);
368
+
369
+ // Emits `information` event
370
+ stream.once('headers', headers => this.emit('information', {statusCode: headers[HTTP2_HEADER_STATUS]}));
371
+
372
+ stream.once('trailers', (trailers, flags, rawTrailers) => {
373
+ const {res} = this;
374
+
375
+ // https://github.com/nodejs/node/issues/41251
376
+ if (res === null) {
377
+ onResponse(trailers, flags, rawTrailers);
378
+ return;
379
+ }
380
+
381
+ // Assigns trailers to the response object.
382
+ res.trailers = trailers;
383
+ res.rawTrailers = rawTrailers;
384
+ });
385
+
386
+ stream.once('close', () => {
387
+ const {aborted, res} = this;
388
+ if (res) {
389
+ if (aborted) {
390
+ res.aborted = true;
391
+ res.emit('aborted');
392
+ res.destroy();
393
+ }
394
+
395
+ const finish = () => {
396
+ res.emit('close');
397
+
398
+ this.destroy();
399
+ this.emit('close');
400
+ };
401
+
402
+ if (res.readable) {
403
+ res.once('end', finish);
404
+ } else {
405
+ finish();
406
+ }
407
+
408
+ return;
409
+ }
410
+
411
+ if (!this.destroyed) {
412
+ this.destroy(new Error('The HTTP/2 stream has been early terminated'));
413
+ this.emit('close');
414
+ return;
415
+ }
416
+
417
+ this.destroy();
418
+ this.emit('close');
419
+ });
420
+
421
+ this.socket = new Proxy(stream, proxySocketHandler);
422
+
423
+ for (const job of this[kJobs]) {
424
+ job();
425
+ }
426
+
427
+ this[kJobs].length = 0;
428
+
429
+ this.emit('socket', this.socket);
430
+ };
431
+
432
+ if (!(HTTP2_HEADER_AUTHORITY in this[kHeaders]) && !isConnectMethod) {
433
+ this[kHeaders][HTTP2_HEADER_AUTHORITY] = this[kOrigin].host;
434
+ }
435
+
436
+ // Makes a HTTP2 request
437
+ if (this[kSession]) {
438
+ try {
439
+ onStream(this[kSession].request(this[kHeaders]));
440
+ } catch (error) {
441
+ this.destroy(error);
442
+ }
443
+ } else {
444
+ this.reusedSocket = true;
445
+
446
+ try {
447
+ const promise = this.agent.request(this[kOrigin], this[kOptions], this[kHeaders]);
448
+ this[kPendingAgentPromise] = promise;
449
+
450
+ onStream(await promise);
451
+
452
+ this[kPendingAgentPromise] = false;
453
+ } catch (error) {
454
+ this[kPendingAgentPromise] = false;
455
+
456
+ this.destroy(error);
457
+ }
458
+ }
459
+ }
460
+
461
+ get connection() {
462
+ return this.socket;
463
+ }
464
+
465
+ set connection(value) {
466
+ this.socket = value;
467
+ }
468
+
469
+ getHeaderNames() {
470
+ return Object.keys(this[kHeaders]);
471
+ }
472
+
473
+ hasHeader(name) {
474
+ if (typeof name !== 'string') {
475
+ throw new ERR_INVALID_ARG_TYPE('name', 'string', name);
476
+ }
477
+
478
+ return Boolean(this[kHeaders][name.toLowerCase()]);
479
+ }
480
+
481
+ getHeader(name) {
482
+ if (typeof name !== 'string') {
483
+ throw new ERR_INVALID_ARG_TYPE('name', 'string', name);
484
+ }
485
+
486
+ return this[kHeaders][name.toLowerCase()];
487
+ }
488
+
489
+ get headersSent() {
490
+ return this[kFlushedHeaders];
491
+ }
492
+
493
+ removeHeader(name) {
494
+ if (typeof name !== 'string') {
495
+ throw new ERR_INVALID_ARG_TYPE('name', 'string', name);
496
+ }
497
+
498
+ if (this.headersSent) {
499
+ throw new ERR_HTTP_HEADERS_SENT('remove');
500
+ }
501
+
502
+ delete this[kHeaders][name.toLowerCase()];
503
+ }
504
+
505
+ setHeader(name, value) {
506
+ if (this.headersSent) {
507
+ throw new ERR_HTTP_HEADERS_SENT('set');
508
+ }
509
+
510
+ validateHeaderName(name);
511
+ validateHeaderValue(name, value);
512
+
513
+ const lowercased = name.toLowerCase();
514
+
515
+ if (lowercased === 'connection') {
516
+ if (value.toLowerCase() === 'keep-alive') {
517
+ return;
518
+ }
519
+
520
+ throw new Error(`Invalid 'connection' header: ${value}`);
521
+ }
522
+
523
+ if (lowercased === 'host' && this.method === 'CONNECT') {
524
+ this[kHeaders][HTTP2_HEADER_AUTHORITY] = value;
525
+ } else {
526
+ this[kHeaders][lowercased] = value;
527
+ }
528
+ }
529
+
530
+ setNoDelay() {
531
+ // HTTP2 sockets cannot be malformed, do nothing.
532
+ }
533
+
534
+ setSocketKeepAlive() {
535
+ // HTTP2 sockets cannot be malformed, do nothing.
536
+ }
537
+
538
+ setTimeout(ms, callback) {
539
+ const applyTimeout = () => this._request.setTimeout(ms, callback);
540
+
541
+ if (this._request) {
542
+ applyTimeout();
543
+ } else {
544
+ this[kJobs].push(applyTimeout);
545
+ }
546
+
547
+ return this;
548
+ }
549
+
550
+ get maxHeadersCount() {
551
+ if (!this.destroyed && this._request) {
552
+ return this._request.session.localSettings.maxHeaderListSize;
553
+ }
554
+
555
+ return undefined;
556
+ }
557
+
558
+ set maxHeadersCount(_value) {
559
+ // Updating HTTP2 settings would affect all requests, do nothing.
560
+ }
561
+ }
562
+
563
+ module.exports = ClientRequest;
@@ -0,0 +1,73 @@
1
+ 'use strict';
2
+ const {Readable} = require('stream');
3
+
4
+ class IncomingMessage extends Readable {
5
+ constructor(socket, highWaterMark) {
6
+ super({
7
+ emitClose: false,
8
+ autoDestroy: true,
9
+ highWaterMark
10
+ });
11
+
12
+ this.statusCode = null;
13
+ this.statusMessage = '';
14
+ this.httpVersion = '2.0';
15
+ this.httpVersionMajor = 2;
16
+ this.httpVersionMinor = 0;
17
+ this.headers = {};
18
+ this.trailers = {};
19
+ this.req = null;
20
+
21
+ this.aborted = false;
22
+ this.complete = false;
23
+ this.upgrade = null;
24
+
25
+ this.rawHeaders = [];
26
+ this.rawTrailers = [];
27
+
28
+ this.socket = socket;
29
+
30
+ this._dumped = false;
31
+ }
32
+
33
+ get connection() {
34
+ return this.socket;
35
+ }
36
+
37
+ set connection(value) {
38
+ this.socket = value;
39
+ }
40
+
41
+ _destroy(error, callback) {
42
+ if (!this.readableEnded) {
43
+ this.aborted = true;
44
+ }
45
+
46
+ // See https://github.com/nodejs/node/issues/35303
47
+ callback();
48
+
49
+ this.req._request.destroy(error);
50
+ }
51
+
52
+ setTimeout(ms, callback) {
53
+ this.req.setTimeout(ms, callback);
54
+ return this;
55
+ }
56
+
57
+ _dump() {
58
+ if (!this._dumped) {
59
+ this._dumped = true;
60
+
61
+ this.removeAllListeners('data');
62
+ this.resume();
63
+ }
64
+ }
65
+
66
+ _read() {
67
+ if (this.req) {
68
+ this.req._request.resume();
69
+ }
70
+ }
71
+ }
72
+
73
+ module.exports = IncomingMessage;
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+ const http2 = require('http2');
3
+ const {
4
+ Agent,
5
+ globalAgent
6
+ } = require('./agent.js');
7
+ const ClientRequest = require('./client-request.js');
8
+ const IncomingMessage = require('./incoming-message.js');
9
+ const auto = require('./auto.js');
10
+ const {
11
+ HttpOverHttp2,
12
+ HttpsOverHttp2
13
+ } = require('./proxies/h1-over-h2.js');
14
+ const Http2OverHttp2 = require('./proxies/h2-over-h2.js');
15
+ const {
16
+ Http2OverHttp,
17
+ Http2OverHttps
18
+ } = require('./proxies/h2-over-h1.js');
19
+ const validateHeaderName = require('./utils/validate-header-name.js');
20
+ const validateHeaderValue = require('./utils/validate-header-value.js');
21
+
22
+ const request = (url, options, callback) => new ClientRequest(url, options, callback);
23
+
24
+ const get = (url, options, callback) => {
25
+ // eslint-disable-next-line unicorn/prevent-abbreviations
26
+ const req = new ClientRequest(url, options, callback);
27
+ req.end();
28
+
29
+ return req;
30
+ };
31
+
32
+ module.exports = {
33
+ ...http2,
34
+ ClientRequest,
35
+ IncomingMessage,
36
+ Agent,
37
+ globalAgent,
38
+ request,
39
+ get,
40
+ auto,
41
+ proxies: {
42
+ HttpOverHttp2,
43
+ HttpsOverHttp2,
44
+ Http2OverHttp2,
45
+ Http2OverHttp,
46
+ Http2OverHttps
47
+ },
48
+ validateHeaderName,
49
+ validateHeaderValue
50
+ };
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ module.exports = self => {
4
+ const {username, password} = self.proxyOptions.url;
5
+
6
+ if (username || password) {
7
+ const data = `${username}:${password}`;
8
+ const authorization = `Basic ${Buffer.from(data).toString('base64')}`;
9
+
10
+ return {
11
+ 'proxy-authorization': authorization,
12
+ authorization
13
+ };
14
+ }
15
+
16
+ return {};
17
+ };