Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nodejs/node.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames M Snell <jasnell@gmail.com>2018-09-17 05:13:11 +0300
committerBeth Griggs <Bethany.Griggs@uk.ibm.com>2018-10-17 02:07:24 +0300
commit49f44f3b448de570747bf067735df7e43746498d (patch)
tree01eac23746bfff96a6757d289e2e8c57dde582e8
parent9f7934159eeb269bc12f163bd6e58e297a826a9d (diff)
http2: add origin frame support
v8.x Backport Note -- as V8 doesn't expose an overload of String::WriteOneByte in Node 8 that accepts an isolate argument, the Origins constructor has been changed to not accept an isolate. Backport-PR-URL: https://github.com/nodejs/node/pull/22850 PR-URL: https://github.com/nodejs/node/pull/22956 Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
-rwxr-xr-xdoc/api/errors.md10
-rw-r--r--doc/api/http2.md83
-rw-r--r--lib/internal/errors.js3
-rw-r--r--lib/internal/http2/core.js102
-rw-r--r--src/env.h1
-rw-r--r--src/node_http2.cc125
-rw-r--r--src/node_http2.h26
-rw-r--r--test/parallel/test-http2-origin.js184
8 files changed, 509 insertions, 25 deletions
diff --git a/doc/api/errors.md b/doc/api/errors.md
index d7c5da0ceb4..69cab1c524e 100755
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -737,6 +737,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
@@ -787,6 +792,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
diff --git a/doc/api/http2.md b/doc/api/http2.md
index 26ab8a461e0..349e2b9b710 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -423,6 +423,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
@@ -662,6 +664,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
@@ -692,6 +744,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
@@ -1903,6 +1979,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
@@ -1966,6 +2046,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}
@@ -3282,6 +3364,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
[Writable Stream]: stream.html#stream_writable_streams
[`'checkContinue'`]: #http2_event_checkcontinue
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index 0863f951830..48062b6d3d4 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -311,6 +311,7 @@ E('ERR_HTTP2_INVALID_CONNECTION_HEADERS',
E('ERR_HTTP2_INVALID_HEADER_VALUE', 'Invalid value "%s" for header "%s"');
E('ERR_HTTP2_INVALID_INFO_STATUS',
(code) => `Invalid informational status code: ${code}`);
+E('ERR_HTTP2_INVALID_ORIGIN', 'HTTP/2 ORIGIN frames require a valid origin');
E('ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH',
'Packed settings length must be a multiple of six');
E('ERR_HTTP2_INVALID_PSEUDOHEADER',
@@ -325,6 +326,8 @@ E('ERR_HTTP2_NESTED_PUSH',
'A push stream cannot initiate another push stream.', Error);
E('ERR_HTTP2_NO_SOCKET_MANIPULATION',
'HTTP/2 sockets should not be directly manipulated (e.g. read and written)');
+E('ERR_HTTP2_ORIGIN_LENGTH',
+ 'HTTP/2 ORIGIN frames are limited to 16382 bytes');
E('ERR_HTTP2_OUT_OF_STREAMS',
'No stream ID is available because maximum stream ID has been reached');
E('ERR_HTTP2_PAYLOAD_FORBIDDEN',
diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index dc98b05a0a6..69f498e3d0e 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -90,6 +90,7 @@ const kMaybeDestroy = Symbol('maybe-destroy');
const kLocalSettings = Symbol('local-settings');
const kOptions = Symbol('options');
const kOwner = Symbol('owner');
+const kOrigin = Symbol('origin');
const kProceed = Symbol('proceed');
const kProtocol = Symbol('protocol');
const kProxySocket = Symbol('proxy-socket');
@@ -152,6 +153,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
@@ -242,6 +244,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);
@@ -404,6 +411,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
@@ -766,6 +806,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);
@@ -792,6 +833,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);
}
@@ -930,23 +977,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
@@ -1324,6 +1355,41 @@ class ServerHttp2Session extends Http2Session {
this[kHandle].altsvc(stream, origin || '', alt);
}
+
+ // Submits an origin frame to be sent.
+ origin(...origins) {
+ if (this.destroyed)
+ throw new errors.Error('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 errors.Error('ERR_INVALID_ARG_TYPE', 'origin', 'string',
+ origin);
+ if (origin === 'null')
+ throw new errors.Error('ERR_HTTP2_INVALID_ORIGIN');
+
+ arr += `${origin}\0`;
+ len += origin.length;
+ }
+
+ if (len > 16382)
+ throw new errors.Error('ERR_HTTP2_ORIGIN_LENGTH');
+
+ this[kHandle].origin(arr, count);
+ }
+
}
// ClientHttp2Session instances have to wait for the socket to connect after
@@ -1394,6 +1460,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)
diff --git a/src/env.h b/src/env.h
index c098ca1d2b9..8afedb7044e 100644
--- a/src/env.h
+++ b/src/env.h
@@ -219,6 +219,7 @@ class ModuleWrap;
V(onnewsessiondone_string, "onnewsessiondone") \
V(onocspresponse_string, "onocspresponse") \
V(ongoawaydata_string, "ongoawaydata") \
+ V(onorigin_string, "onorigin") \
V(onpriority_string, "onpriority") \
V(onread_string, "onread") \
V(onreadstart_string, "onreadstart") \
diff --git a/src/node_http2.cc b/src/node_http2.cc
index 234504256b9..3dad804aa48 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -93,7 +93,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
@@ -104,10 +104,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, v8::Uint32Array>& buffer =
env->http2_state()->options_buffer;
@@ -446,6 +448,54 @@ Headers::Headers(Isolate* isolate,
}
}
+Origins::Origins(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(
+ 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.
@@ -581,7 +631,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();
@@ -985,6 +1035,9 @@ inline int Http2Session::OnFrameReceive(nghttp2_session* handle,
case NGHTTP2_ALTSVC:
session->HandleAltSvcFrame(frame);
break;
+ case NGHTTP2_ORIGIN:
+ session->HandleOriginFrame(frame);
+ break;
default:
break;
}
@@ -1378,6 +1431,41 @@ inline 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_HTTP2SESSION2(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.
inline void Http2Session::HandlePingFrame(const nghttp2_frame* frame) {
bool ack = frame->hd.flags & NGHTTP2_FLAG_ACK;
@@ -2809,7 +2897,12 @@ void Http2Session::AltSvc(int32_t id,
origin, origin_len, value, value_len), 0);
}
-// Submits an AltSvc frame to the sent to the connected peer.
+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);
Http2Session* session;
@@ -2837,6 +2930,23 @@ 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->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);
@@ -3063,6 +3173,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);
diff --git a/src/node_http2.h b/src/node_http2.h
index 219d1d02527..ca0dd55f97f 100644
--- a/src/node_http2.h
+++ b/src/node_http2.h
@@ -415,7 +415,7 @@ class Http2Scope {
// configured.
class Http2Options {
public:
- explicit Http2Options(Environment* env);
+ Http2Options(Environment* env, nghttp2_session_type type);
~Http2Options() {
nghttp2_option_del(options_);
@@ -761,6 +761,8 @@ class Http2Session : public AsyncWrap {
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);
@@ -856,6 +858,7 @@ class Http2Session : public AsyncWrap {
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);
@@ -933,6 +936,7 @@ class Http2Session : public AsyncWrap {
inline void HandleSettingsFrame(const nghttp2_frame* frame);
inline void HandlePingFrame(const nghttp2_frame* frame);
inline void HandleAltSvcFrame(const nghttp2_frame* frame);
+ inline void HandleOriginFrame(const nghttp2_frame* frame);
// nghttp2 callbacks
static inline int OnBeginHeadersCallback(
@@ -1287,6 +1291,26 @@ class Headers {
MaybeStackBuffer<char, 3000> buf_;
};
+class Origins {
+ public:
+ Origins(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
diff --git a/test/parallel/test-http2-origin.js b/test/parallel/test-http2-origin.js
new file mode 100644
index 00000000000..385d3827fc3
--- /dev/null
+++ b/test/parallel/test-http2-origin.js
@@ -0,0 +1,184 @@
+'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 { URL } = require('url');
+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 exceptionHasFields = ({ code, name }) => (err) => {
+ return err.code === code && err.name === name;
+};
+
+{
+ 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),
+ exceptionHasFields({
+ code: 'ERR_INVALID_ARG_TYPE',
+ name: 'Error [ERR_INVALID_ARG_TYPE]'
+ })
+ );
+ });
+
+ [new URL('foo://bar'), 'foo://bar'].forEach((input) => {
+ throws(
+ () => session.origin(input),
+ exceptionHasFields({
+ code: 'ERR_HTTP2_INVALID_ORIGIN',
+ name: 'Error [ERR_HTTP2_INVALID_ORIGIN]'
+ })
+ );
+ });
+
+ ['not a valid url'].forEach((input) => {
+ throws(
+ () => session.origin(input),
+ exceptionHasFields({
+ 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();
+ }));
+ }));
+}