http2: add origin frame support
PR-URL: https://github.com/nodejs/node/pull/22956 Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
parent
c55ebd8502
commit
b92ce5165f
@ -925,6 +925,11 @@ An invalid HTTP/2 header value was specified.
|
|||||||
An invalid HTTP informational status code has been specified. Informational
|
An invalid HTTP informational status code has been specified. Informational
|
||||||
status codes must be an integer between `100` and `199` (inclusive).
|
status codes must be an integer between `100` and `199` (inclusive).
|
||||||
|
|
||||||
|
<a id="ERR_HTTP2_INVALID_ORIGIN"></a>
|
||||||
|
### ERR_HTTP2_INVALID_ORIGIN
|
||||||
|
|
||||||
|
HTTP/2 `ORIGIN` frames require a valid origin.
|
||||||
|
|
||||||
<a id="ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH"></a>
|
<a id="ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH"></a>
|
||||||
### ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH
|
### ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH
|
||||||
|
|
||||||
@ -975,6 +980,11 @@ Nested push streams are not permitted.
|
|||||||
An attempt was made to directly manipulate (read, write, pause, resume, etc.) a
|
An attempt was made to directly manipulate (read, write, pause, resume, etc.) a
|
||||||
socket attached to an `Http2Session`.
|
socket attached to an `Http2Session`.
|
||||||
|
|
||||||
|
<a id="ERR_HTTP2_ORIGIN_LENGTH"></a>
|
||||||
|
### ERR_HTTP2_ORIGIN_LENGTH
|
||||||
|
|
||||||
|
HTTP/2 `ORIGIN` frames are limited to a length of 16382 bytes.
|
||||||
|
|
||||||
<a id="ERR_HTTP2_OUT_OF_STREAMS"></a>
|
<a id="ERR_HTTP2_OUT_OF_STREAMS"></a>
|
||||||
### ERR_HTTP2_OUT_OF_STREAMS
|
### ERR_HTTP2_OUT_OF_STREAMS
|
||||||
|
|
||||||
|
@ -432,6 +432,8 @@ If the `Http2Session` is connected to a `TLSSocket`, the `originSet` property
|
|||||||
will return an `Array` of origins for which the `Http2Session` may be
|
will return an `Array` of origins for which the `Http2Session` may be
|
||||||
considered authoritative.
|
considered authoritative.
|
||||||
|
|
||||||
|
The `originSet` property is only available when using a secure TLS connection.
|
||||||
|
|
||||||
#### http2session.pendingSettingsAck
|
#### http2session.pendingSettingsAck
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
added: v8.4.0
|
added: v8.4.0
|
||||||
@ -670,6 +672,56 @@ The protocol identifier (`'h2'` in the examples) may be any valid
|
|||||||
The syntax of these values is not validated by the Node.js implementation and
|
The syntax of these values is not validated by the Node.js implementation and
|
||||||
are passed through as provided by the user or received from the peer.
|
are passed through as provided by the user or received from the peer.
|
||||||
|
|
||||||
|
#### serverhttp2session.origin(...origins)
|
||||||
|
<!-- YAML
|
||||||
|
added: REPLACEME
|
||||||
|
-->
|
||||||
|
|
||||||
|
* `origins` { string | URL | Object } One or more URL Strings passed as
|
||||||
|
separate arguments.
|
||||||
|
|
||||||
|
Submits an `ORIGIN` frame (as defined by [RFC 8336][]) to the connected client
|
||||||
|
to advertise the set of origins for which the server is capable of providing
|
||||||
|
authoritative responses.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const http2 = require('http2');
|
||||||
|
const options = getSecureOptionsSomehow();
|
||||||
|
const server = http2.createSecureServer(options);
|
||||||
|
server.on('stream', (stream) => {
|
||||||
|
stream.respond();
|
||||||
|
stream.end('ok');
|
||||||
|
});
|
||||||
|
server.on('session', (session) => {
|
||||||
|
session.origin('https://example.com', 'https://example.org');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
When a string is passed as an `origin`, it will be parsed as a URL and the
|
||||||
|
origin will be derived. For instance, the origin for the HTTP URL
|
||||||
|
`'https://example.org/foo/bar'` is the ASCII string
|
||||||
|
`'https://example.org'`. An error will be thrown if either the given string
|
||||||
|
cannot be parsed as a URL or if a valid origin cannot be derived.
|
||||||
|
|
||||||
|
A `URL` object, or any object with an `origin` property, may be passed as
|
||||||
|
an `origin`, in which case the value of the `origin` property will be
|
||||||
|
used. The value of the `origin` property *must* be a properly serialized
|
||||||
|
ASCII origin.
|
||||||
|
|
||||||
|
Alternatively, the `origins` option may be used when creating a new HTTP/2
|
||||||
|
server using the `http2.createSecureServer()` method:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const http2 = require('http2');
|
||||||
|
const options = getSecureOptionsSomehow();
|
||||||
|
options.origins = ['https://example.com', 'https://example.org'];
|
||||||
|
const server = http2.createSecureServer(options);
|
||||||
|
server.on('stream', (stream) => {
|
||||||
|
stream.respond();
|
||||||
|
stream.end('ok');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
### Class: ClientHttp2Session
|
### Class: ClientHttp2Session
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
added: v8.4.0
|
added: v8.4.0
|
||||||
@ -700,6 +752,30 @@ client.on('altsvc', (alt, origin, streamId) => {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Event: 'origin'
|
||||||
|
<!-- YAML
|
||||||
|
added: REPLACEME
|
||||||
|
-->
|
||||||
|
|
||||||
|
* `origins` {string[]}
|
||||||
|
|
||||||
|
The `'origin'` event is emitted whenever an `ORIGIN` frame is received by
|
||||||
|
the client. The event is emitted with an array of `origin` strings. The
|
||||||
|
`http2session.originSet` will be updated to include the received
|
||||||
|
origins.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const http2 = require('http2');
|
||||||
|
const client = http2.connect('https://example.org');
|
||||||
|
|
||||||
|
client.on('origin', (origins) => {
|
||||||
|
for (let n = 0; n < origins.length; n++)
|
||||||
|
console.log(origins[n]);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The `'origin'` event is only emitted when using a secure TLS connection.
|
||||||
|
|
||||||
#### clienthttp2session.request(headers[, options])
|
#### clienthttp2session.request(headers[, options])
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
added: v8.4.0
|
added: v8.4.0
|
||||||
@ -1914,6 +1990,10 @@ server.listen(80);
|
|||||||
<!-- YAML
|
<!-- YAML
|
||||||
added: v8.4.0
|
added: v8.4.0
|
||||||
changes:
|
changes:
|
||||||
|
- version: REPLACEME
|
||||||
|
pr-url: https://github.com/nodejs/node/pull/22956
|
||||||
|
description: Added the `origins` option to automatically send an `ORIGIN`
|
||||||
|
frame on `Http2Session` startup.
|
||||||
- version: v8.9.3
|
- version: v8.9.3
|
||||||
pr-url: https://github.com/nodejs/node/pull/17105
|
pr-url: https://github.com/nodejs/node/pull/17105
|
||||||
description: Added the `maxOutstandingPings` option with a default limit of
|
description: Added the `maxOutstandingPings` option with a default limit of
|
||||||
@ -1977,6 +2057,8 @@ changes:
|
|||||||
remote peer upon connection.
|
remote peer upon connection.
|
||||||
* ...: Any [`tls.createServer()`][] options can be provided. For
|
* ...: Any [`tls.createServer()`][] options can be provided. For
|
||||||
servers, the identity options (`pfx` or `key`/`cert`) are usually required.
|
servers, the identity options (`pfx` or `key`/`cert`) are usually required.
|
||||||
|
* `origins` {string[]} An array of origin strings to send within an `ORIGIN`
|
||||||
|
frame immediately following creation of a new server `Http2Session`.
|
||||||
* `onRequestHandler` {Function} See [Compatibility API][]
|
* `onRequestHandler` {Function} See [Compatibility API][]
|
||||||
* Returns: {Http2SecureServer}
|
* Returns: {Http2SecureServer}
|
||||||
|
|
||||||
@ -3268,6 +3350,7 @@ following additional properties:
|
|||||||
[Performance Observer]: perf_hooks.html
|
[Performance Observer]: perf_hooks.html
|
||||||
[Readable Stream]: stream.html#stream_class_stream_readable
|
[Readable Stream]: stream.html#stream_class_stream_readable
|
||||||
[RFC 7838]: https://tools.ietf.org/html/rfc7838
|
[RFC 7838]: https://tools.ietf.org/html/rfc7838
|
||||||
|
[RFC 8336]: https://tools.ietf.org/html/rfc8336
|
||||||
[Using `options.selectPadding()`]: #http2_using_options_selectpadding
|
[Using `options.selectPadding()`]: #http2_using_options_selectpadding
|
||||||
[`'checkContinue'`]: #http2_event_checkcontinue
|
[`'checkContinue'`]: #http2_event_checkcontinue
|
||||||
[`'request'`]: #http2_event_request
|
[`'request'`]: #http2_event_request
|
||||||
|
@ -567,6 +567,8 @@ E('ERR_HTTP2_INVALID_HEADER_VALUE',
|
|||||||
'Invalid value "%s" for header "%s"', TypeError);
|
'Invalid value "%s" for header "%s"', TypeError);
|
||||||
E('ERR_HTTP2_INVALID_INFO_STATUS',
|
E('ERR_HTTP2_INVALID_INFO_STATUS',
|
||||||
'Invalid informational status code: %s', RangeError);
|
'Invalid informational status code: %s', RangeError);
|
||||||
|
E('ERR_HTTP2_INVALID_ORIGIN',
|
||||||
|
'HTTP/2 ORIGIN frames require a valid origin', TypeError);
|
||||||
E('ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH',
|
E('ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH',
|
||||||
'Packed settings length must be a multiple of six', RangeError);
|
'Packed settings length must be a multiple of six', RangeError);
|
||||||
E('ERR_HTTP2_INVALID_PSEUDOHEADER',
|
E('ERR_HTTP2_INVALID_PSEUDOHEADER',
|
||||||
@ -582,6 +584,8 @@ E('ERR_HTTP2_NESTED_PUSH',
|
|||||||
E('ERR_HTTP2_NO_SOCKET_MANIPULATION',
|
E('ERR_HTTP2_NO_SOCKET_MANIPULATION',
|
||||||
'HTTP/2 sockets should not be directly manipulated (e.g. read and written)',
|
'HTTP/2 sockets should not be directly manipulated (e.g. read and written)',
|
||||||
Error);
|
Error);
|
||||||
|
E('ERR_HTTP2_ORIGIN_LENGTH',
|
||||||
|
'HTTP/2 ORIGIN frames are limited to 16382 bytes', TypeError);
|
||||||
E('ERR_HTTP2_OUT_OF_STREAMS',
|
E('ERR_HTTP2_OUT_OF_STREAMS',
|
||||||
'No stream ID is available because maximum stream ID has been reached',
|
'No stream ID is available because maximum stream ID has been reached',
|
||||||
Error);
|
Error);
|
||||||
|
@ -43,6 +43,7 @@ const {
|
|||||||
ERR_HTTP2_HEADERS_AFTER_RESPOND,
|
ERR_HTTP2_HEADERS_AFTER_RESPOND,
|
||||||
ERR_HTTP2_HEADERS_SENT,
|
ERR_HTTP2_HEADERS_SENT,
|
||||||
ERR_HTTP2_INVALID_INFO_STATUS,
|
ERR_HTTP2_INVALID_INFO_STATUS,
|
||||||
|
ERR_HTTP2_INVALID_ORIGIN,
|
||||||
ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH,
|
ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH,
|
||||||
ERR_HTTP2_INVALID_SESSION,
|
ERR_HTTP2_INVALID_SESSION,
|
||||||
ERR_HTTP2_INVALID_SETTING_VALUE,
|
ERR_HTTP2_INVALID_SETTING_VALUE,
|
||||||
@ -50,6 +51,7 @@ const {
|
|||||||
ERR_HTTP2_MAX_PENDING_SETTINGS_ACK,
|
ERR_HTTP2_MAX_PENDING_SETTINGS_ACK,
|
||||||
ERR_HTTP2_NESTED_PUSH,
|
ERR_HTTP2_NESTED_PUSH,
|
||||||
ERR_HTTP2_NO_SOCKET_MANIPULATION,
|
ERR_HTTP2_NO_SOCKET_MANIPULATION,
|
||||||
|
ERR_HTTP2_ORIGIN_LENGTH,
|
||||||
ERR_HTTP2_OUT_OF_STREAMS,
|
ERR_HTTP2_OUT_OF_STREAMS,
|
||||||
ERR_HTTP2_PAYLOAD_FORBIDDEN,
|
ERR_HTTP2_PAYLOAD_FORBIDDEN,
|
||||||
ERR_HTTP2_PING_CANCEL,
|
ERR_HTTP2_PING_CANCEL,
|
||||||
@ -148,6 +150,7 @@ const kInfoHeaders = Symbol('sent-info-headers');
|
|||||||
const kLocalSettings = Symbol('local-settings');
|
const kLocalSettings = Symbol('local-settings');
|
||||||
const kOptions = Symbol('options');
|
const kOptions = Symbol('options');
|
||||||
const kOwner = owner_symbol;
|
const kOwner = owner_symbol;
|
||||||
|
const kOrigin = Symbol('origin');
|
||||||
const kProceed = Symbol('proceed');
|
const kProceed = Symbol('proceed');
|
||||||
const kProtocol = Symbol('protocol');
|
const kProtocol = Symbol('protocol');
|
||||||
const kProxySocket = Symbol('proxy-socket');
|
const kProxySocket = Symbol('proxy-socket');
|
||||||
@ -209,6 +212,7 @@ const {
|
|||||||
HTTP_STATUS_NO_CONTENT,
|
HTTP_STATUS_NO_CONTENT,
|
||||||
HTTP_STATUS_NOT_MODIFIED,
|
HTTP_STATUS_NOT_MODIFIED,
|
||||||
HTTP_STATUS_SWITCHING_PROTOCOLS,
|
HTTP_STATUS_SWITCHING_PROTOCOLS,
|
||||||
|
HTTP_STATUS_MISDIRECTED_REQUEST,
|
||||||
|
|
||||||
STREAM_OPTION_EMPTY_PAYLOAD,
|
STREAM_OPTION_EMPTY_PAYLOAD,
|
||||||
STREAM_OPTION_GET_TRAILERS
|
STREAM_OPTION_GET_TRAILERS
|
||||||
@ -299,6 +303,11 @@ function onSessionHeaders(handle, id, cat, flags, headers) {
|
|||||||
} else {
|
} else {
|
||||||
event = endOfStream ? 'trailers' : 'headers';
|
event = endOfStream ? 'trailers' : 'headers';
|
||||||
}
|
}
|
||||||
|
const session = stream.session;
|
||||||
|
if (status === HTTP_STATUS_MISDIRECTED_REQUEST) {
|
||||||
|
const originSet = session[kState].originSet = initOriginSet(session);
|
||||||
|
originSet.delete(stream[kOrigin]);
|
||||||
|
}
|
||||||
debug(`Http2Stream ${id} [Http2Session ` +
|
debug(`Http2Stream ${id} [Http2Session ` +
|
||||||
`${sessionName(type)}]: emitting stream '${event}' event`);
|
`${sessionName(type)}]: emitting stream '${event}' event`);
|
||||||
process.nextTick(emit, stream, event, obj, flags, headers);
|
process.nextTick(emit, stream, event, obj, flags, headers);
|
||||||
@ -429,6 +438,39 @@ function onAltSvc(stream, origin, alt) {
|
|||||||
session.emit('altsvc', alt, origin, stream);
|
session.emit('altsvc', alt, origin, stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initOriginSet(session) {
|
||||||
|
let originSet = session[kState].originSet;
|
||||||
|
if (originSet === undefined) {
|
||||||
|
const socket = session[kSocket];
|
||||||
|
session[kState].originSet = originSet = new Set();
|
||||||
|
if (socket.servername != null) {
|
||||||
|
let originString = `https://${socket.servername}`;
|
||||||
|
if (socket.remotePort != null)
|
||||||
|
originString += `:${socket.remotePort}`;
|
||||||
|
// We have to ensure that it is a properly serialized
|
||||||
|
// ASCII origin string. The socket.servername might not
|
||||||
|
// be properly ASCII encoded.
|
||||||
|
originSet.add((new URL(originString)).origin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return originSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOrigin(origins) {
|
||||||
|
const session = this[kOwner];
|
||||||
|
if (session.destroyed)
|
||||||
|
return;
|
||||||
|
debug(`Http2Session ${sessionName(session[kType])}: origin received: ` +
|
||||||
|
`${origins.join(', ')}`);
|
||||||
|
session[kUpdateTimer]();
|
||||||
|
if (!session.encrypted || session.destroyed)
|
||||||
|
return undefined;
|
||||||
|
const originSet = initOriginSet(session);
|
||||||
|
for (var n = 0; n < origins.length; n++)
|
||||||
|
originSet.add(origins[n]);
|
||||||
|
session.emit('origin', origins);
|
||||||
|
}
|
||||||
|
|
||||||
// Receiving a GOAWAY frame from the connected peer is a signal that no
|
// Receiving a GOAWAY frame from the connected peer is a signal that no
|
||||||
// new streams should be created. If the code === NGHTTP2_NO_ERROR, we
|
// new streams should be created. If the code === NGHTTP2_NO_ERROR, we
|
||||||
// are going to send our close, but allow existing frames to close
|
// are going to send our close, but allow existing frames to close
|
||||||
@ -782,6 +824,7 @@ function setupHandle(socket, type, options) {
|
|||||||
handle.onframeerror = onFrameError;
|
handle.onframeerror = onFrameError;
|
||||||
handle.ongoawaydata = onGoawayData;
|
handle.ongoawaydata = onGoawayData;
|
||||||
handle.onaltsvc = onAltSvc;
|
handle.onaltsvc = onAltSvc;
|
||||||
|
handle.onorigin = onOrigin;
|
||||||
|
|
||||||
if (typeof options.selectPadding === 'function')
|
if (typeof options.selectPadding === 'function')
|
||||||
handle.ongetpadding = onSelectPadding(options.selectPadding);
|
handle.ongetpadding = onSelectPadding(options.selectPadding);
|
||||||
@ -808,6 +851,12 @@ function setupHandle(socket, type, options) {
|
|||||||
options.settings : {};
|
options.settings : {};
|
||||||
|
|
||||||
this.settings(settings);
|
this.settings(settings);
|
||||||
|
|
||||||
|
if (type === NGHTTP2_SESSION_SERVER &&
|
||||||
|
Array.isArray(options.origins)) {
|
||||||
|
this.origin(...options.origins);
|
||||||
|
}
|
||||||
|
|
||||||
process.nextTick(emit, this, 'connect', this, socket);
|
process.nextTick(emit, this, 'connect', this, socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -947,23 +996,7 @@ class Http2Session extends EventEmitter {
|
|||||||
get originSet() {
|
get originSet() {
|
||||||
if (!this.encrypted || this.destroyed)
|
if (!this.encrypted || this.destroyed)
|
||||||
return undefined;
|
return undefined;
|
||||||
|
return Array.from(initOriginSet(this));
|
||||||
let originSet = this[kState].originSet;
|
|
||||||
if (originSet === undefined) {
|
|
||||||
const socket = this[kSocket];
|
|
||||||
this[kState].originSet = originSet = new Set();
|
|
||||||
if (socket.servername != null) {
|
|
||||||
let originString = `https://${socket.servername}`;
|
|
||||||
if (socket.remotePort != null)
|
|
||||||
originString += `:${socket.remotePort}`;
|
|
||||||
// We have to ensure that it is a properly serialized
|
|
||||||
// ASCII origin string. The socket.servername might not
|
|
||||||
// be properly ASCII encoded.
|
|
||||||
originSet.add((new URL(originString)).origin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(originSet);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// True if the Http2Session is still waiting for the socket to connect
|
// True if the Http2Session is still waiting for the socket to connect
|
||||||
@ -1338,6 +1371,40 @@ class ServerHttp2Session extends Http2Session {
|
|||||||
|
|
||||||
this[kHandle].altsvc(stream, origin || '', alt);
|
this[kHandle].altsvc(stream, origin || '', alt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Submits an origin frame to be sent.
|
||||||
|
origin(...origins) {
|
||||||
|
if (this.destroyed)
|
||||||
|
throw new ERR_HTTP2_INVALID_SESSION();
|
||||||
|
|
||||||
|
if (origins.length === 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
let arr = '';
|
||||||
|
let len = 0;
|
||||||
|
const count = origins.length;
|
||||||
|
for (var i = 0; i < count; i++) {
|
||||||
|
let origin = origins[i];
|
||||||
|
if (typeof origin === 'string') {
|
||||||
|
origin = (new URL(origin)).origin;
|
||||||
|
} else if (origin != null && typeof origin === 'object') {
|
||||||
|
origin = origin.origin;
|
||||||
|
}
|
||||||
|
if (typeof origin !== 'string')
|
||||||
|
throw new ERR_INVALID_ARG_TYPE('origin', 'string', origin);
|
||||||
|
if (origin === 'null')
|
||||||
|
throw new ERR_HTTP2_INVALID_ORIGIN();
|
||||||
|
|
||||||
|
arr += `${origin}\0`;
|
||||||
|
len += origin.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len > 16382)
|
||||||
|
throw new ERR_HTTP2_ORIGIN_LENGTH();
|
||||||
|
|
||||||
|
this[kHandle].origin(arr, count);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClientHttp2Session instances have to wait for the socket to connect after
|
// ClientHttp2Session instances have to wait for the socket to connect after
|
||||||
@ -1406,6 +1473,8 @@ class ClientHttp2Session extends Http2Session {
|
|||||||
|
|
||||||
const stream = new ClientHttp2Stream(this, undefined, undefined, {});
|
const stream = new ClientHttp2Stream(this, undefined, undefined, {});
|
||||||
stream[kSentHeaders] = headers;
|
stream[kSentHeaders] = headers;
|
||||||
|
stream[kOrigin] = `${headers[HTTP2_HEADER_SCHEME]}://` +
|
||||||
|
`${headers[HTTP2_HEADER_AUTHORITY]}`;
|
||||||
|
|
||||||
// Close the writable side of the stream if options.endStream is set.
|
// Close the writable side of the stream if options.endStream is set.
|
||||||
if (options.endStream)
|
if (options.endStream)
|
||||||
|
@ -224,6 +224,7 @@ struct PackageConfig {
|
|||||||
V(onnewsession_string, "onnewsession") \
|
V(onnewsession_string, "onnewsession") \
|
||||||
V(onocspresponse_string, "onocspresponse") \
|
V(onocspresponse_string, "onocspresponse") \
|
||||||
V(ongoawaydata_string, "ongoawaydata") \
|
V(ongoawaydata_string, "ongoawaydata") \
|
||||||
|
V(onorigin_string, "onorigin") \
|
||||||
V(onpriority_string, "onpriority") \
|
V(onpriority_string, "onpriority") \
|
||||||
V(onread_string, "onread") \
|
V(onread_string, "onread") \
|
||||||
V(onreadstart_string, "onreadstart") \
|
V(onreadstart_string, "onreadstart") \
|
||||||
|
@ -95,7 +95,7 @@ Http2Scope::~Http2Scope() {
|
|||||||
// instances to configure an appropriate nghttp2_options struct. The class
|
// instances to configure an appropriate nghttp2_options struct. The class
|
||||||
// uses a single TypedArray instance that is shared with the JavaScript side
|
// uses a single TypedArray instance that is shared with the JavaScript side
|
||||||
// to more efficiently pass values back and forth.
|
// to more efficiently pass values back and forth.
|
||||||
Http2Options::Http2Options(Environment* env) {
|
Http2Options::Http2Options(Environment* env, nghttp2_session_type type) {
|
||||||
nghttp2_option_new(&options_);
|
nghttp2_option_new(&options_);
|
||||||
|
|
||||||
// We manually handle flow control within a session in order to
|
// We manually handle flow control within a session in order to
|
||||||
@ -106,10 +106,12 @@ Http2Options::Http2Options(Environment* env) {
|
|||||||
// are required to buffer.
|
// are required to buffer.
|
||||||
nghttp2_option_set_no_auto_window_update(options_, 1);
|
nghttp2_option_set_no_auto_window_update(options_, 1);
|
||||||
|
|
||||||
// Enable built in support for ALTSVC frames. Once we add support for
|
// Enable built in support for receiving ALTSVC and ORIGIN frames (but
|
||||||
// other non-built in extension frames, this will need to be handled
|
// only on client side sessions
|
||||||
// a bit differently. For now, let's let nghttp2 take care of it.
|
if (type == NGHTTP2_SESSION_CLIENT) {
|
||||||
nghttp2_option_set_builtin_recv_extension_type(options_, NGHTTP2_ALTSVC);
|
nghttp2_option_set_builtin_recv_extension_type(options_, NGHTTP2_ALTSVC);
|
||||||
|
nghttp2_option_set_builtin_recv_extension_type(options_, NGHTTP2_ORIGIN);
|
||||||
|
}
|
||||||
|
|
||||||
AliasedBuffer<uint32_t, Uint32Array>& buffer =
|
AliasedBuffer<uint32_t, Uint32Array>& buffer =
|
||||||
env->http2_state()->options_buffer;
|
env->http2_state()->options_buffer;
|
||||||
@ -413,6 +415,56 @@ Headers::Headers(Isolate* isolate,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Origins::Origins(Isolate* isolate,
|
||||||
|
Local<Context> context,
|
||||||
|
Local<String> origin_string,
|
||||||
|
size_t origin_count) : count_(origin_count) {
|
||||||
|
int origin_string_len = origin_string->Length();
|
||||||
|
if (count_ == 0) {
|
||||||
|
CHECK_EQ(origin_string_len, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate a single buffer with count_ nghttp2_nv structs, followed
|
||||||
|
// by the raw header data as passed from JS. This looks like:
|
||||||
|
// | possible padding | nghttp2_nv | nghttp2_nv | ... | header contents |
|
||||||
|
buf_.AllocateSufficientStorage((alignof(nghttp2_origin_entry) - 1) +
|
||||||
|
count_ * sizeof(nghttp2_origin_entry) +
|
||||||
|
origin_string_len);
|
||||||
|
|
||||||
|
// Make sure the start address is aligned appropriately for an nghttp2_nv*.
|
||||||
|
char* start = reinterpret_cast<char*>(
|
||||||
|
ROUND_UP(reinterpret_cast<uintptr_t>(*buf_),
|
||||||
|
alignof(nghttp2_origin_entry)));
|
||||||
|
char* origin_contents = start + (count_ * sizeof(nghttp2_origin_entry));
|
||||||
|
nghttp2_origin_entry* const nva =
|
||||||
|
reinterpret_cast<nghttp2_origin_entry*>(start);
|
||||||
|
|
||||||
|
CHECK_LE(origin_contents + origin_string_len, *buf_ + buf_.length());
|
||||||
|
CHECK_EQ(origin_string->WriteOneByte(
|
||||||
|
isolate,
|
||||||
|
reinterpret_cast<uint8_t*>(origin_contents),
|
||||||
|
0,
|
||||||
|
origin_string_len,
|
||||||
|
String::NO_NULL_TERMINATION),
|
||||||
|
origin_string_len);
|
||||||
|
|
||||||
|
size_t n = 0;
|
||||||
|
char* p;
|
||||||
|
for (p = origin_contents; p < origin_contents + origin_string_len; n++) {
|
||||||
|
if (n >= count_) {
|
||||||
|
static uint8_t zero = '\0';
|
||||||
|
nva[0].origin = &zero;
|
||||||
|
nva[0].origin_len = 1;
|
||||||
|
count_ = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nva[n].origin = reinterpret_cast<uint8_t*>(p);
|
||||||
|
nva[n].origin_len = strlen(p);
|
||||||
|
p += nva[n].origin_len + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Sets the various callback functions that nghttp2 will use to notify us
|
// Sets the various callback functions that nghttp2 will use to notify us
|
||||||
// about significant events while processing http2 stuff.
|
// about significant events while processing http2 stuff.
|
||||||
@ -548,7 +600,7 @@ Http2Session::Http2Session(Environment* env,
|
|||||||
statistics_.start_time = uv_hrtime();
|
statistics_.start_time = uv_hrtime();
|
||||||
|
|
||||||
// Capture the configuration options for this session
|
// Capture the configuration options for this session
|
||||||
Http2Options opts(env);
|
Http2Options opts(env, type);
|
||||||
|
|
||||||
max_session_memory_ = opts.GetMaxSessionMemory();
|
max_session_memory_ = opts.GetMaxSessionMemory();
|
||||||
|
|
||||||
@ -933,6 +985,9 @@ int Http2Session::OnFrameReceive(nghttp2_session* handle,
|
|||||||
case NGHTTP2_ALTSVC:
|
case NGHTTP2_ALTSVC:
|
||||||
session->HandleAltSvcFrame(frame);
|
session->HandleAltSvcFrame(frame);
|
||||||
break;
|
break;
|
||||||
|
case NGHTTP2_ORIGIN:
|
||||||
|
session->HandleOriginFrame(frame);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -1365,6 +1420,41 @@ void Http2Session::HandleAltSvcFrame(const nghttp2_frame* frame) {
|
|||||||
MakeCallback(env()->onaltsvc_string(), arraysize(argv), argv);
|
MakeCallback(env()->onaltsvc_string(), arraysize(argv), argv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Http2Session::HandleOriginFrame(const nghttp2_frame* frame) {
|
||||||
|
Isolate* isolate = env()->isolate();
|
||||||
|
HandleScope scope(isolate);
|
||||||
|
Local<Context> context = env()->context();
|
||||||
|
Context::Scope context_scope(context);
|
||||||
|
|
||||||
|
Debug(this, "handling origin frame");
|
||||||
|
|
||||||
|
nghttp2_extension ext = frame->ext;
|
||||||
|
nghttp2_ext_origin* origin = static_cast<nghttp2_ext_origin*>(ext.payload);
|
||||||
|
|
||||||
|
Local<Array> holder = Array::New(isolate);
|
||||||
|
Local<Function> fn = env()->push_values_to_array_function();
|
||||||
|
Local<Value> argv[NODE_PUSH_VAL_TO_ARRAY_MAX];
|
||||||
|
|
||||||
|
size_t n = 0;
|
||||||
|
while (n < origin->nov) {
|
||||||
|
size_t j = 0;
|
||||||
|
while (n < origin->nov && j < arraysize(argv)) {
|
||||||
|
auto entry = origin->ov[n++];
|
||||||
|
argv[j++] =
|
||||||
|
String::NewFromOneByte(isolate,
|
||||||
|
entry.origin,
|
||||||
|
v8::NewStringType::kNormal,
|
||||||
|
entry.origin_len).ToLocalChecked();
|
||||||
|
}
|
||||||
|
if (j > 0)
|
||||||
|
fn->Call(context, holder, j, argv).ToLocalChecked();
|
||||||
|
}
|
||||||
|
|
||||||
|
Local<Value> args[1] = { holder };
|
||||||
|
|
||||||
|
MakeCallback(env()->onorigin_string(), arraysize(args), args);
|
||||||
|
}
|
||||||
|
|
||||||
// Called by OnFrameReceived when a complete PING frame has been received.
|
// Called by OnFrameReceived when a complete PING frame has been received.
|
||||||
void Http2Session::HandlePingFrame(const nghttp2_frame* frame) {
|
void Http2Session::HandlePingFrame(const nghttp2_frame* frame) {
|
||||||
bool ack = frame->hd.flags & NGHTTP2_FLAG_ACK;
|
bool ack = frame->hd.flags & NGHTTP2_FLAG_ACK;
|
||||||
@ -2613,6 +2703,11 @@ void Http2Session::AltSvc(int32_t id,
|
|||||||
origin, origin_len, value, value_len), 0);
|
origin, origin_len, value, value_len), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Http2Session::Origin(nghttp2_origin_entry* ov, size_t count) {
|
||||||
|
Http2Scope h2scope(this);
|
||||||
|
CHECK_EQ(nghttp2_submit_origin(session_, NGHTTP2_FLAG_NONE, ov, count), 0);
|
||||||
|
}
|
||||||
|
|
||||||
// Submits an AltSvc frame to be sent to the connected peer.
|
// Submits an AltSvc frame to be sent to the connected peer.
|
||||||
void Http2Session::AltSvc(const FunctionCallbackInfo<Value>& args) {
|
void Http2Session::AltSvc(const FunctionCallbackInfo<Value>& args) {
|
||||||
Environment* env = Environment::GetCurrent(args);
|
Environment* env = Environment::GetCurrent(args);
|
||||||
@ -2641,6 +2736,24 @@ void Http2Session::AltSvc(const FunctionCallbackInfo<Value>& args) {
|
|||||||
session->AltSvc(id, *origin, origin_len, *value, value_len);
|
session->AltSvc(id, *origin, origin_len, *value, value_len);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Http2Session::Origin(const FunctionCallbackInfo<Value>& args) {
|
||||||
|
Environment* env = Environment::GetCurrent(args);
|
||||||
|
Local<Context> context = env->context();
|
||||||
|
Http2Session* session;
|
||||||
|
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
|
||||||
|
|
||||||
|
Local<String> origin_string = args[0].As<String>();
|
||||||
|
int count = args[1]->IntegerValue(context).ToChecked();
|
||||||
|
|
||||||
|
|
||||||
|
Origins origins(env->isolate(),
|
||||||
|
env->context(),
|
||||||
|
origin_string,
|
||||||
|
count);
|
||||||
|
|
||||||
|
session->Origin(*origins, origins.length());
|
||||||
|
}
|
||||||
|
|
||||||
// Submits a PING frame to be sent to the connected peer.
|
// Submits a PING frame to be sent to the connected peer.
|
||||||
void Http2Session::Ping(const FunctionCallbackInfo<Value>& args) {
|
void Http2Session::Ping(const FunctionCallbackInfo<Value>& args) {
|
||||||
Environment* env = Environment::GetCurrent(args);
|
Environment* env = Environment::GetCurrent(args);
|
||||||
@ -2874,6 +2987,7 @@ void Initialize(Local<Object> target,
|
|||||||
session->SetClassName(http2SessionClassName);
|
session->SetClassName(http2SessionClassName);
|
||||||
session->InstanceTemplate()->SetInternalFieldCount(1);
|
session->InstanceTemplate()->SetInternalFieldCount(1);
|
||||||
AsyncWrap::AddWrapMethods(env, session);
|
AsyncWrap::AddWrapMethods(env, session);
|
||||||
|
env->SetProtoMethod(session, "origin", Http2Session::Origin);
|
||||||
env->SetProtoMethod(session, "altsvc", Http2Session::AltSvc);
|
env->SetProtoMethod(session, "altsvc", Http2Session::AltSvc);
|
||||||
env->SetProtoMethod(session, "ping", Http2Session::Ping);
|
env->SetProtoMethod(session, "ping", Http2Session::Ping);
|
||||||
env->SetProtoMethod(session, "consume", Http2Session::Consume);
|
env->SetProtoMethod(session, "consume", Http2Session::Consume);
|
||||||
|
@ -364,7 +364,7 @@ class Http2Scope {
|
|||||||
// configured.
|
// configured.
|
||||||
class Http2Options {
|
class Http2Options {
|
||||||
public:
|
public:
|
||||||
explicit Http2Options(Environment* env);
|
Http2Options(Environment* env, nghttp2_session_type type);
|
||||||
|
|
||||||
~Http2Options() {
|
~Http2Options() {
|
||||||
nghttp2_option_del(options_);
|
nghttp2_option_del(options_);
|
||||||
@ -700,6 +700,8 @@ class Http2Session : public AsyncWrap, public StreamListener {
|
|||||||
size_t origin_len,
|
size_t origin_len,
|
||||||
uint8_t* value,
|
uint8_t* value,
|
||||||
size_t value_len);
|
size_t value_len);
|
||||||
|
void Origin(nghttp2_origin_entry* ov, size_t count);
|
||||||
|
|
||||||
|
|
||||||
bool Ping(v8::Local<v8::Function> function);
|
bool Ping(v8::Local<v8::Function> function);
|
||||||
|
|
||||||
@ -796,6 +798,7 @@ class Http2Session : public AsyncWrap, public StreamListener {
|
|||||||
static void RefreshState(const FunctionCallbackInfo<Value>& args);
|
static void RefreshState(const FunctionCallbackInfo<Value>& args);
|
||||||
static void Ping(const FunctionCallbackInfo<Value>& args);
|
static void Ping(const FunctionCallbackInfo<Value>& args);
|
||||||
static void AltSvc(const FunctionCallbackInfo<Value>& args);
|
static void AltSvc(const FunctionCallbackInfo<Value>& args);
|
||||||
|
static void Origin(const FunctionCallbackInfo<Value>& args);
|
||||||
|
|
||||||
template <get_setting fn>
|
template <get_setting fn>
|
||||||
static void RefreshSettings(const FunctionCallbackInfo<Value>& args);
|
static void RefreshSettings(const FunctionCallbackInfo<Value>& args);
|
||||||
@ -871,6 +874,7 @@ class Http2Session : public AsyncWrap, public StreamListener {
|
|||||||
void HandleSettingsFrame(const nghttp2_frame* frame);
|
void HandleSettingsFrame(const nghttp2_frame* frame);
|
||||||
void HandlePingFrame(const nghttp2_frame* frame);
|
void HandlePingFrame(const nghttp2_frame* frame);
|
||||||
void HandleAltSvcFrame(const nghttp2_frame* frame);
|
void HandleAltSvcFrame(const nghttp2_frame* frame);
|
||||||
|
void HandleOriginFrame(const nghttp2_frame* frame);
|
||||||
|
|
||||||
// nghttp2 callbacks
|
// nghttp2 callbacks
|
||||||
static int OnBeginHeadersCallback(
|
static int OnBeginHeadersCallback(
|
||||||
@ -1224,6 +1228,27 @@ class Headers {
|
|||||||
MaybeStackBuffer<char, 3000> buf_;
|
MaybeStackBuffer<char, 3000> buf_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class Origins {
|
||||||
|
public:
|
||||||
|
Origins(Isolate* isolate,
|
||||||
|
Local<Context> context,
|
||||||
|
Local<v8::String> origin_string,
|
||||||
|
size_t origin_count);
|
||||||
|
~Origins() {}
|
||||||
|
|
||||||
|
nghttp2_origin_entry* operator*() {
|
||||||
|
return reinterpret_cast<nghttp2_origin_entry*>(*buf_);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t length() const {
|
||||||
|
return count_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
size_t count_;
|
||||||
|
MaybeStackBuffer<char, 512> buf_;
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace http2
|
} // namespace http2
|
||||||
} // namespace node
|
} // namespace node
|
||||||
|
|
||||||
|
179
test/parallel/test-http2-origin.js
Normal file
179
test/parallel/test-http2-origin.js
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const {
|
||||||
|
hasCrypto,
|
||||||
|
mustCall,
|
||||||
|
mustNotCall,
|
||||||
|
skip
|
||||||
|
} = require('../common');
|
||||||
|
if (!hasCrypto)
|
||||||
|
skip('missing crypto');
|
||||||
|
|
||||||
|
const {
|
||||||
|
deepStrictEqual,
|
||||||
|
strictEqual,
|
||||||
|
throws
|
||||||
|
} = require('assert');
|
||||||
|
const {
|
||||||
|
createSecureServer,
|
||||||
|
createServer,
|
||||||
|
connect
|
||||||
|
} = require('http2');
|
||||||
|
const Countdown = require('../common/countdown');
|
||||||
|
|
||||||
|
const { readKey } = require('../common/fixtures');
|
||||||
|
|
||||||
|
const key = readKey('agent8-key.pem', 'binary');
|
||||||
|
const cert = readKey('agent8-cert.pem', 'binary');
|
||||||
|
const ca = readKey('fake-startcom-root-cert.pem', 'binary');
|
||||||
|
|
||||||
|
{
|
||||||
|
const server = createSecureServer({ key, cert });
|
||||||
|
server.on('stream', mustCall((stream) => {
|
||||||
|
stream.session.origin('https://example.org/a/b/c',
|
||||||
|
new URL('https://example.com'));
|
||||||
|
stream.respond();
|
||||||
|
stream.end('ok');
|
||||||
|
}));
|
||||||
|
server.on('session', mustCall((session) => {
|
||||||
|
session.origin('https://foo.org/a/b/c', new URL('https://bar.org'));
|
||||||
|
|
||||||
|
// Won't error, but won't send anything
|
||||||
|
session.origin();
|
||||||
|
|
||||||
|
[0, true, {}, []].forEach((input) => {
|
||||||
|
throws(
|
||||||
|
() => session.origin(input),
|
||||||
|
{
|
||||||
|
code: 'ERR_INVALID_ARG_TYPE',
|
||||||
|
name: 'TypeError [ERR_INVALID_ARG_TYPE]'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
[new URL('foo://bar'), 'foo://bar'].forEach((input) => {
|
||||||
|
throws(
|
||||||
|
() => session.origin(input),
|
||||||
|
{
|
||||||
|
code: 'ERR_HTTP2_INVALID_ORIGIN',
|
||||||
|
name: 'TypeError [ERR_HTTP2_INVALID_ORIGIN]'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
['not a valid url'].forEach((input) => {
|
||||||
|
throws(
|
||||||
|
() => session.origin(input),
|
||||||
|
{
|
||||||
|
code: 'ERR_INVALID_URL',
|
||||||
|
name: 'TypeError [ERR_INVALID_URL]'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
server.listen(0, mustCall(() => {
|
||||||
|
const originSet = [`https://localhost:${server.address().port}`];
|
||||||
|
const client = connect(originSet[0], { ca });
|
||||||
|
const checks = [
|
||||||
|
['https://foo.org', 'https://bar.org'],
|
||||||
|
['https://example.org', 'https://example.com']
|
||||||
|
];
|
||||||
|
|
||||||
|
const countdown = new Countdown(2, () => {
|
||||||
|
client.close();
|
||||||
|
server.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('origin', mustCall((origins) => {
|
||||||
|
const check = checks.shift();
|
||||||
|
originSet.push(...check);
|
||||||
|
deepStrictEqual(originSet, client.originSet);
|
||||||
|
deepStrictEqual(origins, check);
|
||||||
|
countdown.dec();
|
||||||
|
}, 2));
|
||||||
|
|
||||||
|
client.request().on('close', mustCall()).resume();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test automatically sending origin on connection start
|
||||||
|
{
|
||||||
|
const origins = [ 'https://foo.org/a/b/c', 'https://bar.org' ];
|
||||||
|
const server = createSecureServer({ key, cert, origins });
|
||||||
|
server.on('stream', mustCall((stream) => {
|
||||||
|
stream.respond();
|
||||||
|
stream.end('ok');
|
||||||
|
}));
|
||||||
|
|
||||||
|
server.listen(0, mustCall(() => {
|
||||||
|
const check = ['https://foo.org', 'https://bar.org'];
|
||||||
|
const originSet = [`https://localhost:${server.address().port}`];
|
||||||
|
const client = connect(originSet[0], { ca });
|
||||||
|
|
||||||
|
client.on('origin', mustCall((origins) => {
|
||||||
|
originSet.push(...check);
|
||||||
|
deepStrictEqual(originSet, client.originSet);
|
||||||
|
deepStrictEqual(origins, check);
|
||||||
|
client.close();
|
||||||
|
server.close();
|
||||||
|
}));
|
||||||
|
|
||||||
|
client.request().on('close', mustCall()).resume();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If return status is 421, the request origin must be removed from the
|
||||||
|
// originSet
|
||||||
|
{
|
||||||
|
const server = createSecureServer({ key, cert });
|
||||||
|
server.on('stream', mustCall((stream) => {
|
||||||
|
stream.respond({ ':status': 421 });
|
||||||
|
stream.end();
|
||||||
|
}));
|
||||||
|
server.on('session', mustCall((session) => {
|
||||||
|
session.origin('https://foo.org');
|
||||||
|
}));
|
||||||
|
|
||||||
|
server.listen(0, mustCall(() => {
|
||||||
|
const origin = `https://localhost:${server.address().port}`;
|
||||||
|
const client = connect(origin, { ca });
|
||||||
|
|
||||||
|
client.on('origin', mustCall((origins) => {
|
||||||
|
deepStrictEqual([origin, 'https://foo.org'], client.originSet);
|
||||||
|
const req = client.request({ ':authority': 'foo.org' });
|
||||||
|
req.on('response', mustCall((headers) => {
|
||||||
|
strictEqual(421, headers[':status']);
|
||||||
|
deepStrictEqual([origin], client.originSet);
|
||||||
|
}));
|
||||||
|
req.resume();
|
||||||
|
req.on('close', mustCall(() => {
|
||||||
|
client.close();
|
||||||
|
server.close();
|
||||||
|
}));
|
||||||
|
}, 1));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Origin is ignored on plain text HTTP/2 connections... server will still
|
||||||
|
// send them, but client will ignore them.
|
||||||
|
{
|
||||||
|
const server = createServer();
|
||||||
|
server.on('stream', mustCall((stream) => {
|
||||||
|
stream.session.origin('https://example.org',
|
||||||
|
new URL('https://example.com'));
|
||||||
|
stream.respond();
|
||||||
|
stream.end('ok');
|
||||||
|
}));
|
||||||
|
server.listen(0, mustCall(() => {
|
||||||
|
const client = connect(`http://localhost:${server.address().port}`);
|
||||||
|
client.on('origin', mustNotCall());
|
||||||
|
strictEqual(client.originSet, undefined);
|
||||||
|
const req = client.request();
|
||||||
|
req.resume();
|
||||||
|
req.on('close', mustCall(() => {
|
||||||
|
client.close();
|
||||||
|
server.close();
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user