Skip to content

Commit

Permalink
http2: add origin frame support
Browse files Browse the repository at this point in the history
PR-URL: #22956
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
jasnell authored and targos committed Sep 23, 2018

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 948dc71 commit 24675a4
Showing 8 changed files with 509 additions and 24 deletions.
10 changes: 10 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
@@ -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

83 changes: 83 additions & 0 deletions doc/api/http2.md
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
@@ -569,6 +569,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',
@@ -584,6 +586,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);
103 changes: 86 additions & 17 deletions lib/internal/http2/core.js
Original file line number Diff line number Diff line change
@@ -43,13 +43,15 @@ 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,
ERR_HTTP2_INVALID_STREAM,
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)
1 change: 1 addition & 0 deletions src/env.h
Original file line number Diff line number Diff line change
@@ -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") \
126 changes: 120 additions & 6 deletions src/node_http2.cc
Original file line number Diff line number Diff line change
@@ -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);
27 changes: 26 additions & 1 deletion src/node_http2.h
Original file line number Diff line number Diff line change
@@ -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 changes: 179 additions & 0 deletions test/parallel/test-http2-origin.js
Original file line number Diff line number Diff line change
@@ -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();
}));
}));
}

0 comments on commit 24675a4

Please sign in to comment.