Skip to content

Commit 49f44f3

Browse files
jasnellBethGriggs
authored andcommittedOct 16, 2018
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: #22850 PR-URL: #22956 Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 9f79341 commit 49f44f3

File tree

8 files changed

+509
-25
lines changed

8 files changed

+509
-25
lines changed
 

‎doc/api/errors.md

+10
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,11 @@ An invalid HTTP/2 header value was specified.
737737
An invalid HTTP informational status code has been specified. Informational
738738
status codes must be an integer between `100` and `199` (inclusive).
739739

740+
<a id="ERR_HTTP2_INVALID_ORIGIN"></a>
741+
### ERR_HTTP2_INVALID_ORIGIN
742+
743+
HTTP/2 `ORIGIN` frames require a valid origin.
744+
740745
<a id="ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH"></a>
741746
### ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH
742747

@@ -787,6 +792,11 @@ Nested push streams are not permitted.
787792
An attempt was made to directly manipulate (read, write, pause, resume, etc.) a
788793
socket attached to an `Http2Session`.
789794

795+
<a id="ERR_HTTP2_ORIGIN_LENGTH"></a>
796+
### ERR_HTTP2_ORIGIN_LENGTH
797+
798+
HTTP/2 `ORIGIN` frames are limited to a length of 16382 bytes.
799+
790800
<a id="ERR_HTTP2_OUT_OF_STREAMS"></a>
791801
### ERR_HTTP2_OUT_OF_STREAMS
792802

‎doc/api/http2.md

+83
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,8 @@ If the `Http2Session` is connected to a `TLSSocket`, the `originSet` property
423423
will return an Array of origins for which the `Http2Session` may be
424424
considered authoritative.
425425

426+
The `originSet` property is only available when using a secure TLS connection.
427+
426428
#### http2session.pendingSettingsAck
427429
<!-- YAML
428430
added: v8.4.0
@@ -662,6 +664,56 @@ The protocol identifier (`'h2'` in the examples) may be any valid
662664
The syntax of these values is not validated by the Node.js implementation and
663665
are passed through as provided by the user or received from the peer.
664666

667+
#### serverhttp2session.origin(...origins)
668+
<!-- YAML
669+
added: REPLACEME
670+
-->
671+
672+
* `origins` { string | URL | Object } One or more URL Strings passed as
673+
separate arguments.
674+
675+
Submits an `ORIGIN` frame (as defined by [RFC 8336][]) to the connected client
676+
to advertise the set of origins for which the server is capable of providing
677+
authoritative responses.
678+
679+
```js
680+
const http2 = require('http2');
681+
const options = getSecureOptionsSomehow();
682+
const server = http2.createSecureServer(options);
683+
server.on('stream', (stream) => {
684+
stream.respond();
685+
stream.end('ok');
686+
});
687+
server.on('session', (session) => {
688+
session.origin('https://example.com', 'https://example.org');
689+
});
690+
```
691+
692+
When a string is passed as an `origin`, it will be parsed as a URL and the
693+
origin will be derived. For instance, the origin for the HTTP URL
694+
`'https://example.org/foo/bar'` is the ASCII string
695+
`'https://example.org'`. An error will be thrown if either the given string
696+
cannot be parsed as a URL or if a valid origin cannot be derived.
697+
698+
A `URL` object, or any object with an `origin` property, may be passed as
699+
an `origin`, in which case the value of the `origin` property will be
700+
used. The value of the `origin` property *must* be a properly serialized
701+
ASCII origin.
702+
703+
Alternatively, the `origins` option may be used when creating a new HTTP/2
704+
server using the `http2.createSecureServer()` method:
705+
706+
```js
707+
const http2 = require('http2');
708+
const options = getSecureOptionsSomehow();
709+
options.origins = ['https://example.com', 'https://example.org'];
710+
const server = http2.createSecureServer(options);
711+
server.on('stream', (stream) => {
712+
stream.respond();
713+
stream.end('ok');
714+
});
715+
```
716+
665717
### Class: ClientHttp2Session
666718
<!-- YAML
667719
added: v8.4.0
@@ -692,6 +744,30 @@ client.on('altsvc', (alt, origin, streamId) => {
692744
});
693745
```
694746

747+
#### Event: 'origin'
748+
<!-- YAML
749+
added: REPLACEME
750+
-->
751+
752+
* `origins` {string[]}
753+
754+
The `'origin'` event is emitted whenever an `ORIGIN` frame is received by
755+
the client. The event is emitted with an array of `origin` strings. The
756+
`http2session.originSet` will be updated to include the received
757+
origins.
758+
759+
```js
760+
const http2 = require('http2');
761+
const client = http2.connect('https://example.org');
762+
763+
client.on('origin', (origins) => {
764+
for (let n = 0; n < origins.length; n++)
765+
console.log(origins[n]);
766+
});
767+
```
768+
769+
The `'origin'` event is only emitted when using a secure TLS connection.
770+
695771
#### clienthttp2session.request(headers[, options])
696772
<!-- YAML
697773
added: v8.4.0
@@ -1903,6 +1979,10 @@ server.listen(80);
19031979
<!-- YAML
19041980
added: v8.4.0
19051981
changes:
1982+
- version: REPLACEME
1983+
pr-url: https://github.com/nodejs/node/pull/22956
1984+
description: Added the `origins` option to automatically send an `ORIGIN`
1985+
frame on `Http2Session` startup.
19061986
- version: v8.9.3
19071987
pr-url: https://github.com/nodejs/node/pull/17105
19081988
description: Added the `maxOutstandingPings` option with a default limit of
@@ -1966,6 +2046,8 @@ changes:
19662046
remote peer upon connection.
19672047
* ...: Any [`tls.createServer()`][] options can be provided. For
19682048
servers, the identity options (`pfx` or `key`/`cert`) are usually required.
2049+
* `origins` {string[]} An array of origin strings to send within an `ORIGIN`
2050+
frame immediately following creation of a new server `Http2Session`.
19692051
* `onRequestHandler` {Function} See [Compatibility API][]
19702052
* Returns: {Http2SecureServer}
19712053

@@ -3282,6 +3364,7 @@ following additional properties:
32823364
[Performance Observer]: perf_hooks.html
32833365
[Readable Stream]: stream.html#stream_class_stream_readable
32843366
[RFC 7838]: https://tools.ietf.org/html/rfc7838
3367+
[RFC 8336]: https://tools.ietf.org/html/rfc8336
32853368
[Using options.selectPadding]: #http2_using_options_selectpadding
32863369
[Writable Stream]: stream.html#stream_writable_streams
32873370
[`'checkContinue'`]: #http2_event_checkcontinue

‎lib/internal/errors.js

+3
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ E('ERR_HTTP2_INVALID_CONNECTION_HEADERS',
311311
E('ERR_HTTP2_INVALID_HEADER_VALUE', 'Invalid value "%s" for header "%s"');
312312
E('ERR_HTTP2_INVALID_INFO_STATUS',
313313
(code) => `Invalid informational status code: ${code}`);
314+
E('ERR_HTTP2_INVALID_ORIGIN', 'HTTP/2 ORIGIN frames require a valid origin');
314315
E('ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH',
315316
'Packed settings length must be a multiple of six');
316317
E('ERR_HTTP2_INVALID_PSEUDOHEADER',
@@ -325,6 +326,8 @@ E('ERR_HTTP2_NESTED_PUSH',
325326
'A push stream cannot initiate another push stream.', Error);
326327
E('ERR_HTTP2_NO_SOCKET_MANIPULATION',
327328
'HTTP/2 sockets should not be directly manipulated (e.g. read and written)');
329+
E('ERR_HTTP2_ORIGIN_LENGTH',
330+
'HTTP/2 ORIGIN frames are limited to 16382 bytes');
328331
E('ERR_HTTP2_OUT_OF_STREAMS',
329332
'No stream ID is available because maximum stream ID has been reached');
330333
E('ERR_HTTP2_PAYLOAD_FORBIDDEN',

‎lib/internal/http2/core.js

+85-17
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ const kMaybeDestroy = Symbol('maybe-destroy');
9090
const kLocalSettings = Symbol('local-settings');
9191
const kOptions = Symbol('options');
9292
const kOwner = Symbol('owner');
93+
const kOrigin = Symbol('origin');
9394
const kProceed = Symbol('proceed');
9495
const kProtocol = Symbol('protocol');
9596
const kProxySocket = Symbol('proxy-socket');
@@ -152,6 +153,7 @@ const {
152153
HTTP_STATUS_NO_CONTENT,
153154
HTTP_STATUS_NOT_MODIFIED,
154155
HTTP_STATUS_SWITCHING_PROTOCOLS,
156+
HTTP_STATUS_MISDIRECTED_REQUEST,
155157

156158
STREAM_OPTION_EMPTY_PAYLOAD,
157159
STREAM_OPTION_GET_TRAILERS
@@ -242,6 +244,11 @@ function onSessionHeaders(handle, id, cat, flags, headers) {
242244
} else {
243245
event = endOfStream ? 'trailers' : 'headers';
244246
}
247+
const session = stream.session;
248+
if (status === HTTP_STATUS_MISDIRECTED_REQUEST) {
249+
const originSet = session[kState].originSet = initOriginSet(session);
250+
originSet.delete(stream[kOrigin]);
251+
}
245252
debug(`Http2Stream ${id} [Http2Session ` +
246253
`${sessionName(type)}]: emitting stream '${event}' event`);
247254
process.nextTick(emit, stream, event, obj, flags, headers);
@@ -404,6 +411,39 @@ function onAltSvc(stream, origin, alt) {
404411
session.emit('altsvc', alt, origin, stream);
405412
}
406413

414+
function initOriginSet(session) {
415+
let originSet = session[kState].originSet;
416+
if (originSet === undefined) {
417+
const socket = session[kSocket];
418+
session[kState].originSet = originSet = new Set();
419+
if (socket.servername != null) {
420+
let originString = `https://${socket.servername}`;
421+
if (socket.remotePort != null)
422+
originString += `:${socket.remotePort}`;
423+
// We have to ensure that it is a properly serialized
424+
// ASCII origin string. The socket.servername might not
425+
// be properly ASCII encoded.
426+
originSet.add((new URL(originString)).origin);
427+
}
428+
}
429+
return originSet;
430+
}
431+
432+
function onOrigin(origins) {
433+
const session = this[kOwner];
434+
if (session.destroyed)
435+
return;
436+
debug(`Http2Session ${sessionName(session[kType])}: origin received: ` +
437+
`${origins.join(', ')}`);
438+
session[kUpdateTimer]();
439+
if (!session.encrypted || session.destroyed)
440+
return undefined;
441+
const originSet = initOriginSet(session);
442+
for (var n = 0; n < origins.length; n++)
443+
originSet.add(origins[n]);
444+
session.emit('origin', origins);
445+
}
446+
407447
// Receiving a GOAWAY frame from the connected peer is a signal that no
408448
// new streams should be created. If the code === NGHTTP2_NO_ERROR, we
409449
// are going to send our close, but allow existing frames to close
@@ -766,6 +806,7 @@ function setupHandle(socket, type, options) {
766806
handle.onframeerror = onFrameError;
767807
handle.ongoawaydata = onGoawayData;
768808
handle.onaltsvc = onAltSvc;
809+
handle.onorigin = onOrigin;
769810

770811
if (typeof options.selectPadding === 'function')
771812
handle.ongetpadding = onSelectPadding(options.selectPadding);
@@ -792,6 +833,12 @@ function setupHandle(socket, type, options) {
792833
options.settings : {};
793834

794835
this.settings(settings);
836+
837+
if (type === NGHTTP2_SESSION_SERVER &&
838+
Array.isArray(options.origins)) {
839+
this.origin(...options.origins);
840+
}
841+
795842
process.nextTick(emit, this, 'connect', this, socket);
796843
}
797844

@@ -930,23 +977,7 @@ class Http2Session extends EventEmitter {
930977
get originSet() {
931978
if (!this.encrypted || this.destroyed)
932979
return undefined;
933-
934-
let originSet = this[kState].originSet;
935-
if (originSet === undefined) {
936-
const socket = this[kSocket];
937-
this[kState].originSet = originSet = new Set();
938-
if (socket.servername != null) {
939-
let originString = `https://${socket.servername}`;
940-
if (socket.remotePort != null)
941-
originString += `:${socket.remotePort}`;
942-
// We have to ensure that it is a properly serialized
943-
// ASCII origin string. The socket.servername might not
944-
// be properly ASCII encoded.
945-
originSet.add((new URL(originString)).origin);
946-
}
947-
}
948-
949-
return Array.from(originSet);
980+
return Array.from(initOriginSet(this));
950981
}
951982

952983
// True if the Http2Session is still waiting for the socket to connect
@@ -1324,6 +1355,41 @@ class ServerHttp2Session extends Http2Session {
13241355

13251356
this[kHandle].altsvc(stream, origin || '', alt);
13261357
}
1358+
1359+
// Submits an origin frame to be sent.
1360+
origin(...origins) {
1361+
if (this.destroyed)
1362+
throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
1363+
1364+
if (origins.length === 0)
1365+
return;
1366+
1367+
let arr = '';
1368+
let len = 0;
1369+
const count = origins.length;
1370+
for (var i = 0; i < count; i++) {
1371+
let origin = origins[i];
1372+
if (typeof origin === 'string') {
1373+
origin = (new URL(origin)).origin;
1374+
} else if (origin != null && typeof origin === 'object') {
1375+
origin = origin.origin;
1376+
}
1377+
if (typeof origin !== 'string')
1378+
throw new errors.Error('ERR_INVALID_ARG_TYPE', 'origin', 'string',
1379+
origin);
1380+
if (origin === 'null')
1381+
throw new errors.Error('ERR_HTTP2_INVALID_ORIGIN');
1382+
1383+
arr += `${origin}\0`;
1384+
len += origin.length;
1385+
}
1386+
1387+
if (len > 16382)
1388+
throw new errors.Error('ERR_HTTP2_ORIGIN_LENGTH');
1389+
1390+
this[kHandle].origin(arr, count);
1391+
}
1392+
13271393
}
13281394

13291395
// ClientHttp2Session instances have to wait for the socket to connect after
@@ -1394,6 +1460,8 @@ class ClientHttp2Session extends Http2Session {
13941460

13951461
const stream = new ClientHttp2Stream(this, undefined, undefined, {});
13961462
stream[kSentHeaders] = headers;
1463+
stream[kOrigin] = `${headers[HTTP2_HEADER_SCHEME]}://` +
1464+
`${headers[HTTP2_HEADER_AUTHORITY]}`;
13971465

13981466
// Close the writable side of the stream if options.endStream is set.
13991467
if (options.endStream)

‎src/env.h

+1
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ class ModuleWrap;
219219
V(onnewsessiondone_string, "onnewsessiondone") \
220220
V(onocspresponse_string, "onocspresponse") \
221221
V(ongoawaydata_string, "ongoawaydata") \
222+
V(onorigin_string, "onorigin") \
222223
V(onpriority_string, "onpriority") \
223224
V(onread_string, "onread") \
224225
V(onreadstart_string, "onreadstart") \

‎src/node_http2.cc

+118-7
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ Http2Scope::~Http2Scope() {
9393
// instances to configure an appropriate nghttp2_options struct. The class
9494
// uses a single TypedArray instance that is shared with the JavaScript side
9595
// to more efficiently pass values back and forth.
96-
Http2Options::Http2Options(Environment* env) {
96+
Http2Options::Http2Options(Environment* env, nghttp2_session_type type) {
9797
nghttp2_option_new(&options_);
9898

9999
// We manually handle flow control within a session in order to
@@ -104,10 +104,12 @@ Http2Options::Http2Options(Environment* env) {
104104
// are required to buffer.
105105
nghttp2_option_set_no_auto_window_update(options_, 1);
106106

107-
// Enable built in support for ALTSVC frames. Once we add support for
108-
// other non-built in extension frames, this will need to be handled
109-
// a bit differently. For now, let's let nghttp2 take care of it.
110-
nghttp2_option_set_builtin_recv_extension_type(options_, NGHTTP2_ALTSVC);
107+
// Enable built in support for receiving ALTSVC and ORIGIN frames (but
108+
// only on client side sessions
109+
if (type == NGHTTP2_SESSION_CLIENT) {
110+
nghttp2_option_set_builtin_recv_extension_type(options_, NGHTTP2_ALTSVC);
111+
nghttp2_option_set_builtin_recv_extension_type(options_, NGHTTP2_ORIGIN);
112+
}
111113

112114
AliasedBuffer<uint32_t, v8::Uint32Array>& buffer =
113115
env->http2_state()->options_buffer;
@@ -446,6 +448,54 @@ Headers::Headers(Isolate* isolate,
446448
}
447449
}
448450

451+
Origins::Origins(Local<Context> context,
452+
Local<String> origin_string,
453+
size_t origin_count) : count_(origin_count) {
454+
int origin_string_len = origin_string->Length();
455+
if (count_ == 0) {
456+
CHECK_EQ(origin_string_len, 0);
457+
return;
458+
}
459+
460+
// Allocate a single buffer with count_ nghttp2_nv structs, followed
461+
// by the raw header data as passed from JS. This looks like:
462+
// | possible padding | nghttp2_nv | nghttp2_nv | ... | header contents |
463+
buf_.AllocateSufficientStorage((alignof(nghttp2_origin_entry) - 1) +
464+
count_ * sizeof(nghttp2_origin_entry) +
465+
origin_string_len);
466+
467+
// Make sure the start address is aligned appropriately for an nghttp2_nv*.
468+
char* start = reinterpret_cast<char*>(
469+
ROUND_UP(reinterpret_cast<uintptr_t>(*buf_),
470+
alignof(nghttp2_origin_entry)));
471+
char* origin_contents = start + (count_ * sizeof(nghttp2_origin_entry));
472+
nghttp2_origin_entry* const nva =
473+
reinterpret_cast<nghttp2_origin_entry*>(start);
474+
475+
CHECK_LE(origin_contents + origin_string_len, *buf_ + buf_.length());
476+
CHECK_EQ(origin_string->WriteOneByte(
477+
reinterpret_cast<uint8_t*>(origin_contents),
478+
0,
479+
origin_string_len,
480+
String::NO_NULL_TERMINATION),
481+
origin_string_len);
482+
483+
size_t n = 0;
484+
char* p;
485+
for (p = origin_contents; p < origin_contents + origin_string_len; n++) {
486+
if (n >= count_) {
487+
static uint8_t zero = '\0';
488+
nva[0].origin = &zero;
489+
nva[0].origin_len = 1;
490+
count_ = 1;
491+
return;
492+
}
493+
494+
nva[n].origin = reinterpret_cast<uint8_t*>(p);
495+
nva[n].origin_len = strlen(p);
496+
p += nva[n].origin_len + 1;
497+
}
498+
}
449499

450500
// Sets the various callback functions that nghttp2 will use to notify us
451501
// about significant events while processing http2 stuff.
@@ -581,7 +631,7 @@ Http2Session::Http2Session(Environment* env,
581631
statistics_.start_time = uv_hrtime();
582632

583633
// Capture the configuration options for this session
584-
Http2Options opts(env);
634+
Http2Options opts(env, type);
585635

586636
max_session_memory_ = opts.GetMaxSessionMemory();
587637

@@ -985,6 +1035,9 @@ inline int Http2Session::OnFrameReceive(nghttp2_session* handle,
9851035
case NGHTTP2_ALTSVC:
9861036
session->HandleAltSvcFrame(frame);
9871037
break;
1038+
case NGHTTP2_ORIGIN:
1039+
session->HandleOriginFrame(frame);
1040+
break;
9881041
default:
9891042
break;
9901043
}
@@ -1378,6 +1431,41 @@ inline void Http2Session::HandleAltSvcFrame(const nghttp2_frame* frame) {
13781431
MakeCallback(env()->onaltsvc_string(), arraysize(argv), argv);
13791432
}
13801433

1434+
void Http2Session::HandleOriginFrame(const nghttp2_frame* frame) {
1435+
Isolate* isolate = env()->isolate();
1436+
HandleScope scope(isolate);
1437+
Local<Context> context = env()->context();
1438+
Context::Scope context_scope(context);
1439+
1440+
DEBUG_HTTP2SESSION2(this, "handling origin frame");
1441+
1442+
nghttp2_extension ext = frame->ext;
1443+
nghttp2_ext_origin* origin = static_cast<nghttp2_ext_origin*>(ext.payload);
1444+
1445+
Local<Array> holder = Array::New(isolate);
1446+
Local<Function> fn = env()->push_values_to_array_function();
1447+
Local<Value> argv[NODE_PUSH_VAL_TO_ARRAY_MAX];
1448+
1449+
size_t n = 0;
1450+
while (n < origin->nov) {
1451+
size_t j = 0;
1452+
while (n < origin->nov && j < arraysize(argv)) {
1453+
auto entry = origin->ov[n++];
1454+
argv[j++] =
1455+
String::NewFromOneByte(isolate,
1456+
entry.origin,
1457+
v8::NewStringType::kNormal,
1458+
entry.origin_len).ToLocalChecked();
1459+
}
1460+
if (j > 0)
1461+
fn->Call(context, holder, j, argv).ToLocalChecked();
1462+
}
1463+
1464+
Local<Value> args[1] = { holder };
1465+
1466+
MakeCallback(env()->onorigin_string(), arraysize(args), args);
1467+
}
1468+
13811469
// Called by OnFrameReceived when a complete PING frame has been received.
13821470
inline void Http2Session::HandlePingFrame(const nghttp2_frame* frame) {
13831471
bool ack = frame->hd.flags & NGHTTP2_FLAG_ACK;
@@ -2809,7 +2897,12 @@ void Http2Session::AltSvc(int32_t id,
28092897
origin, origin_len, value, value_len), 0);
28102898
}
28112899

2812-
// Submits an AltSvc frame to the sent to the connected peer.
2900+
void Http2Session::Origin(nghttp2_origin_entry* ov, size_t count) {
2901+
Http2Scope h2scope(this);
2902+
CHECK_EQ(nghttp2_submit_origin(session_, NGHTTP2_FLAG_NONE, ov, count), 0);
2903+
}
2904+
2905+
// Submits an AltSvc frame to be sent to the connected peer.
28132906
void Http2Session::AltSvc(const FunctionCallbackInfo<Value>& args) {
28142907
Environment* env = Environment::GetCurrent(args);
28152908
Http2Session* session;
@@ -2837,6 +2930,23 @@ void Http2Session::AltSvc(const FunctionCallbackInfo<Value>& args) {
28372930
session->AltSvc(id, *origin, origin_len, *value, value_len);
28382931
}
28392932

2933+
void Http2Session::Origin(const FunctionCallbackInfo<Value>& args) {
2934+
Environment* env = Environment::GetCurrent(args);
2935+
Local<Context> context = env->context();
2936+
Http2Session* session;
2937+
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
2938+
2939+
Local<String> origin_string = args[0].As<String>();
2940+
int count = args[1]->IntegerValue(context).ToChecked();
2941+
2942+
2943+
Origins origins(env->context(),
2944+
origin_string,
2945+
count);
2946+
2947+
session->Origin(*origins, origins.length());
2948+
}
2949+
28402950
// Submits a PING frame to be sent to the connected peer.
28412951
void Http2Session::Ping(const FunctionCallbackInfo<Value>& args) {
28422952
Environment* env = Environment::GetCurrent(args);
@@ -3063,6 +3173,7 @@ void Initialize(Local<Object> target,
30633173
session->SetClassName(http2SessionClassName);
30643174
session->InstanceTemplate()->SetInternalFieldCount(1);
30653175
AsyncWrap::AddWrapMethods(env, session);
3176+
env->SetProtoMethod(session, "origin", Http2Session::Origin);
30663177
env->SetProtoMethod(session, "altsvc", Http2Session::AltSvc);
30673178
env->SetProtoMethod(session, "ping", Http2Session::Ping);
30683179
env->SetProtoMethod(session, "consume", Http2Session::Consume);

‎src/node_http2.h

+25-1
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ class Http2Scope {
415415
// configured.
416416
class Http2Options {
417417
public:
418-
explicit Http2Options(Environment* env);
418+
Http2Options(Environment* env, nghttp2_session_type type);
419419

420420
~Http2Options() {
421421
nghttp2_option_del(options_);
@@ -761,6 +761,8 @@ class Http2Session : public AsyncWrap {
761761
size_t origin_len,
762762
uint8_t* value,
763763
size_t value_len);
764+
void Origin(nghttp2_origin_entry* ov, size_t count);
765+
764766

765767
bool Ping(v8::Local<v8::Function> function);
766768

@@ -856,6 +858,7 @@ class Http2Session : public AsyncWrap {
856858
static void RefreshState(const FunctionCallbackInfo<Value>& args);
857859
static void Ping(const FunctionCallbackInfo<Value>& args);
858860
static void AltSvc(const FunctionCallbackInfo<Value>& args);
861+
static void Origin(const FunctionCallbackInfo<Value>& args);
859862

860863
template <get_setting fn>
861864
static void RefreshSettings(const FunctionCallbackInfo<Value>& args);
@@ -933,6 +936,7 @@ class Http2Session : public AsyncWrap {
933936
inline void HandleSettingsFrame(const nghttp2_frame* frame);
934937
inline void HandlePingFrame(const nghttp2_frame* frame);
935938
inline void HandleAltSvcFrame(const nghttp2_frame* frame);
939+
inline void HandleOriginFrame(const nghttp2_frame* frame);
936940

937941
// nghttp2 callbacks
938942
static inline int OnBeginHeadersCallback(
@@ -1287,6 +1291,26 @@ class Headers {
12871291
MaybeStackBuffer<char, 3000> buf_;
12881292
};
12891293

1294+
class Origins {
1295+
public:
1296+
Origins(Local<Context> context,
1297+
Local<v8::String> origin_string,
1298+
size_t origin_count);
1299+
~Origins() {}
1300+
1301+
nghttp2_origin_entry* operator*() {
1302+
return reinterpret_cast<nghttp2_origin_entry*>(*buf_);
1303+
}
1304+
1305+
size_t length() const {
1306+
return count_;
1307+
}
1308+
1309+
private:
1310+
size_t count_;
1311+
MaybeStackBuffer<char, 512> buf_;
1312+
};
1313+
12901314
} // namespace http2
12911315
} // namespace node
12921316

‎test/parallel/test-http2-origin.js

+184
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
'use strict';
2+
3+
const {
4+
hasCrypto,
5+
mustCall,
6+
mustNotCall,
7+
skip
8+
} = require('../common');
9+
if (!hasCrypto)
10+
skip('missing crypto');
11+
12+
const {
13+
deepStrictEqual,
14+
strictEqual,
15+
throws
16+
} = require('assert');
17+
const {
18+
createSecureServer,
19+
createServer,
20+
connect
21+
} = require('http2');
22+
const { URL } = require('url');
23+
const Countdown = require('../common/countdown');
24+
25+
const { readKey } = require('../common/fixtures');
26+
27+
const key = readKey('agent8-key.pem', 'binary');
28+
const cert = readKey('agent8-cert.pem', 'binary');
29+
const ca = readKey('fake-startcom-root-cert.pem', 'binary');
30+
31+
const exceptionHasFields = ({ code, name }) => (err) => {
32+
return err.code === code && err.name === name;
33+
};
34+
35+
{
36+
const server = createSecureServer({ key, cert });
37+
server.on('stream', mustCall((stream) => {
38+
stream.session.origin('https://example.org/a/b/c',
39+
new URL('https://example.com'));
40+
stream.respond();
41+
stream.end('ok');
42+
}));
43+
server.on('session', mustCall((session) => {
44+
session.origin('https://foo.org/a/b/c', new URL('https://bar.org'));
45+
46+
// Won't error, but won't send anything
47+
session.origin();
48+
49+
[0, true, {}, []].forEach((input) => {
50+
throws(
51+
() => session.origin(input),
52+
exceptionHasFields({
53+
code: 'ERR_INVALID_ARG_TYPE',
54+
name: 'Error [ERR_INVALID_ARG_TYPE]'
55+
})
56+
);
57+
});
58+
59+
[new URL('foo://bar'), 'foo://bar'].forEach((input) => {
60+
throws(
61+
() => session.origin(input),
62+
exceptionHasFields({
63+
code: 'ERR_HTTP2_INVALID_ORIGIN',
64+
name: 'Error [ERR_HTTP2_INVALID_ORIGIN]'
65+
})
66+
);
67+
});
68+
69+
['not a valid url'].forEach((input) => {
70+
throws(
71+
() => session.origin(input),
72+
exceptionHasFields({
73+
code: 'ERR_INVALID_URL',
74+
name: 'TypeError [ERR_INVALID_URL]'
75+
})
76+
);
77+
});
78+
}));
79+
80+
server.listen(0, mustCall(() => {
81+
const originSet = [`https://localhost:${server.address().port}`];
82+
const client = connect(originSet[0], { ca });
83+
const checks = [
84+
['https://foo.org', 'https://bar.org'],
85+
['https://example.org', 'https://example.com']
86+
];
87+
88+
const countdown = new Countdown(2, () => {
89+
client.close();
90+
server.close();
91+
});
92+
93+
client.on('origin', mustCall((origins) => {
94+
const check = checks.shift();
95+
originSet.push(...check);
96+
deepStrictEqual(originSet, client.originSet);
97+
deepStrictEqual(origins, check);
98+
countdown.dec();
99+
}, 2));
100+
101+
client.request().on('close', mustCall()).resume();
102+
}));
103+
}
104+
105+
// Test automatically sending origin on connection start
106+
{
107+
const origins = [ 'https://foo.org/a/b/c', 'https://bar.org' ];
108+
const server = createSecureServer({ key, cert, origins });
109+
server.on('stream', mustCall((stream) => {
110+
stream.respond();
111+
stream.end('ok');
112+
}));
113+
114+
server.listen(0, mustCall(() => {
115+
const check = ['https://foo.org', 'https://bar.org'];
116+
const originSet = [`https://localhost:${server.address().port}`];
117+
const client = connect(originSet[0], { ca });
118+
119+
client.on('origin', mustCall((origins) => {
120+
originSet.push(...check);
121+
deepStrictEqual(originSet, client.originSet);
122+
deepStrictEqual(origins, check);
123+
client.close();
124+
server.close();
125+
}));
126+
127+
client.request().on('close', mustCall()).resume();
128+
}));
129+
}
130+
131+
// If return status is 421, the request origin must be removed from the
132+
// originSet
133+
{
134+
const server = createSecureServer({ key, cert });
135+
server.on('stream', mustCall((stream) => {
136+
stream.respond({ ':status': 421 });
137+
stream.end();
138+
}));
139+
server.on('session', mustCall((session) => {
140+
session.origin('https://foo.org');
141+
}));
142+
143+
server.listen(0, mustCall(() => {
144+
const origin = `https://localhost:${server.address().port}`;
145+
const client = connect(origin, { ca });
146+
147+
client.on('origin', mustCall((origins) => {
148+
deepStrictEqual([origin, 'https://foo.org'], client.originSet);
149+
const req = client.request({ ':authority': 'foo.org' });
150+
req.on('response', mustCall((headers) => {
151+
strictEqual(421, headers[':status']);
152+
deepStrictEqual([origin], client.originSet);
153+
}));
154+
req.resume();
155+
req.on('close', mustCall(() => {
156+
client.close();
157+
server.close();
158+
}));
159+
}, 1));
160+
}));
161+
}
162+
163+
// Origin is ignored on plain text HTTP/2 connections... server will still
164+
// send them, but client will ignore them.
165+
{
166+
const server = createServer();
167+
server.on('stream', mustCall((stream) => {
168+
stream.session.origin('https://example.org',
169+
new URL('https://example.com'));
170+
stream.respond();
171+
stream.end('ok');
172+
}));
173+
server.listen(0, mustCall(() => {
174+
const client = connect(`http://localhost:${server.address().port}`);
175+
client.on('origin', mustNotCall());
176+
strictEqual(client.originSet, undefined);
177+
const req = client.request();
178+
req.resume();
179+
req.on('close', mustCall(() => {
180+
client.close();
181+
server.close();
182+
}));
183+
}));
184+
}

0 commit comments

Comments
 (0)
Please sign in to comment.