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
|
||||
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>
|
||||
### 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
|
||||
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>
|
||||
### 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
|
||||
considered authoritative.
|
||||
|
||||
The `originSet` property is only available when using a secure TLS connection.
|
||||
|
||||
#### http2session.pendingSettingsAck
|
||||
<!-- YAML
|
||||
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
|
||||
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
|
||||
<!-- YAML
|
||||
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])
|
||||
<!-- YAML
|
||||
added: v8.4.0
|
||||
@ -1914,6 +1990,10 @@ server.listen(80);
|
||||
<!-- YAML
|
||||
added: v8.4.0
|
||||
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
|
||||
pr-url: https://github.com/nodejs/node/pull/17105
|
||||
description: Added the `maxOutstandingPings` option with a default limit of
|
||||
@ -1977,6 +2057,8 @@ changes:
|
||||
remote peer upon connection.
|
||||
* ...: Any [`tls.createServer()`][] options can be provided. For
|
||||
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][]
|
||||
* Returns: {Http2SecureServer}
|
||||
|
||||
@ -3268,6 +3350,7 @@ following additional properties:
|
||||
[Performance Observer]: perf_hooks.html
|
||||
[Readable Stream]: stream.html#stream_class_stream_readable
|
||||
[RFC 7838]: https://tools.ietf.org/html/rfc7838
|
||||
[RFC 8336]: https://tools.ietf.org/html/rfc8336
|
||||
[Using `options.selectPadding()`]: #http2_using_options_selectpadding
|
||||
[`'checkContinue'`]: #http2_event_checkcontinue
|
||||
[`'request'`]: #http2_event_request
|
||||
|
@ -567,6 +567,8 @@ E('ERR_HTTP2_INVALID_HEADER_VALUE',
|
||||
'Invalid value "%s" for header "%s"', TypeError);
|
||||
E('ERR_HTTP2_INVALID_INFO_STATUS',
|
||||
'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',
|
||||
'Packed settings length must be a multiple of six', RangeError);
|
||||
E('ERR_HTTP2_INVALID_PSEUDOHEADER',
|
||||
@ -582,6 +584,8 @@ E('ERR_HTTP2_NESTED_PUSH',
|
||||
E('ERR_HTTP2_NO_SOCKET_MANIPULATION',
|
||||
'HTTP/2 sockets should not be directly manipulated (e.g. read and written)',
|
||||
Error);
|
||||
E('ERR_HTTP2_ORIGIN_LENGTH',
|
||||
'HTTP/2 ORIGIN frames are limited to 16382 bytes', TypeError);
|
||||
E('ERR_HTTP2_OUT_OF_STREAMS',
|
||||
'No stream ID is available because maximum stream ID has been reached',
|
||||
Error);
|
||||
|
@ -43,6 +43,7 @@ const {
|
||||
ERR_HTTP2_HEADERS_AFTER_RESPOND,
|
||||
ERR_HTTP2_HEADERS_SENT,
|
||||
ERR_HTTP2_INVALID_INFO_STATUS,
|
||||
ERR_HTTP2_INVALID_ORIGIN,
|
||||
ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH,
|
||||
ERR_HTTP2_INVALID_SESSION,
|
||||
ERR_HTTP2_INVALID_SETTING_VALUE,
|
||||
@ -50,6 +51,7 @@ const {
|
||||
ERR_HTTP2_MAX_PENDING_SETTINGS_ACK,
|
||||
ERR_HTTP2_NESTED_PUSH,
|
||||
ERR_HTTP2_NO_SOCKET_MANIPULATION,
|
||||
ERR_HTTP2_ORIGIN_LENGTH,
|
||||
ERR_HTTP2_OUT_OF_STREAMS,
|
||||
ERR_HTTP2_PAYLOAD_FORBIDDEN,
|
||||
ERR_HTTP2_PING_CANCEL,
|
||||
@ -148,6 +150,7 @@ const kInfoHeaders = Symbol('sent-info-headers');
|
||||
const kLocalSettings = Symbol('local-settings');
|
||||
const kOptions = Symbol('options');
|
||||
const kOwner = owner_symbol;
|
||||
const kOrigin = Symbol('origin');
|
||||
const kProceed = Symbol('proceed');
|
||||
const kProtocol = Symbol('protocol');
|
||||
const kProxySocket = Symbol('proxy-socket');
|
||||
@ -209,6 +212,7 @@ const {
|
||||
HTTP_STATUS_NO_CONTENT,
|
||||
HTTP_STATUS_NOT_MODIFIED,
|
||||
HTTP_STATUS_SWITCHING_PROTOCOLS,
|
||||
HTTP_STATUS_MISDIRECTED_REQUEST,
|
||||
|
||||
STREAM_OPTION_EMPTY_PAYLOAD,
|
||||
STREAM_OPTION_GET_TRAILERS
|
||||
@ -299,6 +303,11 @@ function onSessionHeaders(handle, id, cat, flags, headers) {
|
||||
} else {
|
||||
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 ` +
|
||||
`${sessionName(type)}]: emitting stream '${event}' event`);
|
||||
process.nextTick(emit, stream, event, obj, flags, headers);
|
||||
@ -429,6 +438,39 @@ function onAltSvc(stream, origin, alt) {
|
||||
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
|
||||
// new streams should be created. If the code === NGHTTP2_NO_ERROR, we
|
||||
// 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.ongoawaydata = onGoawayData;
|
||||
handle.onaltsvc = onAltSvc;
|
||||
handle.onorigin = onOrigin;
|
||||
|
||||
if (typeof options.selectPadding === 'function')
|
||||
handle.ongetpadding = onSelectPadding(options.selectPadding);
|
||||
@ -808,6 +851,12 @@ function setupHandle(socket, type, options) {
|
||||
options.settings : {};
|
||||
|
||||
this.settings(settings);
|
||||
|
||||
if (type === NGHTTP2_SESSION_SERVER &&
|
||||
Array.isArray(options.origins)) {
|
||||
this.origin(...options.origins);
|
||||
}
|
||||
|
||||
process.nextTick(emit, this, 'connect', this, socket);
|
||||
}
|
||||
|
||||
@ -947,23 +996,7 @@ class Http2Session extends EventEmitter {
|
||||
get originSet() {
|
||||
if (!this.encrypted || this.destroyed)
|
||||
return undefined;
|
||||
|
||||
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);
|
||||
return Array.from(initOriginSet(this));
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -1406,6 +1473,8 @@ class ClientHttp2Session extends Http2Session {
|
||||
|
||||
const stream = new ClientHttp2Stream(this, undefined, undefined, {});
|
||||
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.
|
||||
if (options.endStream)
|
||||
|
@ -224,6 +224,7 @@ struct PackageConfig {
|
||||
V(onnewsession_string, "onnewsession") \
|
||||
V(onocspresponse_string, "onocspresponse") \
|
||||
V(ongoawaydata_string, "ongoawaydata") \
|
||||
V(onorigin_string, "onorigin") \
|
||||
V(onpriority_string, "onpriority") \
|
||||
V(onread_string, "onread") \
|
||||
V(onreadstart_string, "onreadstart") \
|
||||
|
@ -95,7 +95,7 @@ Http2Scope::~Http2Scope() {
|
||||
// instances to configure an appropriate nghttp2_options struct. The class
|
||||
// uses a single TypedArray instance that is shared with the JavaScript side
|
||||
// to more efficiently pass values back and forth.
|
||||
Http2Options::Http2Options(Environment* env) {
|
||||
Http2Options::Http2Options(Environment* env, nghttp2_session_type type) {
|
||||
nghttp2_option_new(&options_);
|
||||
|
||||
// We manually handle flow control within a session in order to
|
||||
@ -106,10 +106,12 @@ Http2Options::Http2Options(Environment* env) {
|
||||
// are required to buffer.
|
||||
nghttp2_option_set_no_auto_window_update(options_, 1);
|
||||
|
||||
// Enable built in support for ALTSVC frames. Once we add support for
|
||||
// other non-built in extension frames, this will need to be handled
|
||||
// a bit differently. For now, let's let nghttp2 take care of it.
|
||||
nghttp2_option_set_builtin_recv_extension_type(options_, NGHTTP2_ALTSVC);
|
||||
// Enable built in support for receiving ALTSVC and ORIGIN frames (but
|
||||
// only on client side sessions
|
||||
if (type == NGHTTP2_SESSION_CLIENT) {
|
||||
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 =
|
||||
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
|
||||
// about significant events while processing http2 stuff.
|
||||
@ -548,7 +600,7 @@ Http2Session::Http2Session(Environment* env,
|
||||
statistics_.start_time = uv_hrtime();
|
||||
|
||||
// Capture the configuration options for this session
|
||||
Http2Options opts(env);
|
||||
Http2Options opts(env, type);
|
||||
|
||||
max_session_memory_ = opts.GetMaxSessionMemory();
|
||||
|
||||
@ -933,6 +985,9 @@ int Http2Session::OnFrameReceive(nghttp2_session* handle,
|
||||
case NGHTTP2_ALTSVC:
|
||||
session->HandleAltSvcFrame(frame);
|
||||
break;
|
||||
case NGHTTP2_ORIGIN:
|
||||
session->HandleOriginFrame(frame);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@ -1365,6 +1420,41 @@ void Http2Session::HandleAltSvcFrame(const nghttp2_frame* frame) {
|
||||
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.
|
||||
void Http2Session::HandlePingFrame(const nghttp2_frame* frame) {
|
||||
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);
|
||||
}
|
||||
|
||||
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.
|
||||
void Http2Session::AltSvc(const FunctionCallbackInfo<Value>& 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);
|
||||
}
|
||||
|
||||
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.
|
||||
void Http2Session::Ping(const FunctionCallbackInfo<Value>& args) {
|
||||
Environment* env = Environment::GetCurrent(args);
|
||||
@ -2874,6 +2987,7 @@ void Initialize(Local<Object> target,
|
||||
session->SetClassName(http2SessionClassName);
|
||||
session->InstanceTemplate()->SetInternalFieldCount(1);
|
||||
AsyncWrap::AddWrapMethods(env, session);
|
||||
env->SetProtoMethod(session, "origin", Http2Session::Origin);
|
||||
env->SetProtoMethod(session, "altsvc", Http2Session::AltSvc);
|
||||
env->SetProtoMethod(session, "ping", Http2Session::Ping);
|
||||
env->SetProtoMethod(session, "consume", Http2Session::Consume);
|
||||
|
@ -364,7 +364,7 @@ class Http2Scope {
|
||||
// configured.
|
||||
class Http2Options {
|
||||
public:
|
||||
explicit Http2Options(Environment* env);
|
||||
Http2Options(Environment* env, nghttp2_session_type type);
|
||||
|
||||
~Http2Options() {
|
||||
nghttp2_option_del(options_);
|
||||
@ -700,6 +700,8 @@ class Http2Session : public AsyncWrap, public StreamListener {
|
||||
size_t origin_len,
|
||||
uint8_t* value,
|
||||
size_t value_len);
|
||||
void Origin(nghttp2_origin_entry* ov, size_t count);
|
||||
|
||||
|
||||
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 Ping(const FunctionCallbackInfo<Value>& args);
|
||||
static void AltSvc(const FunctionCallbackInfo<Value>& args);
|
||||
static void Origin(const FunctionCallbackInfo<Value>& args);
|
||||
|
||||
template <get_setting fn>
|
||||
static void RefreshSettings(const FunctionCallbackInfo<Value>& args);
|
||||
@ -871,6 +874,7 @@ class Http2Session : public AsyncWrap, public StreamListener {
|
||||
void HandleSettingsFrame(const nghttp2_frame* frame);
|
||||
void HandlePingFrame(const nghttp2_frame* frame);
|
||||
void HandleAltSvcFrame(const nghttp2_frame* frame);
|
||||
void HandleOriginFrame(const nghttp2_frame* frame);
|
||||
|
||||
// nghttp2 callbacks
|
||||
static int OnBeginHeadersCallback(
|
||||
@ -1224,6 +1228,27 @@ class Headers {
|
||||
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 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