Skip to content
Permalink
Browse files

http2: add origin frame support

PR-URL: #22956
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
  • Loading branch information...
jasnell authored and targos committed Sep 17, 2018
1 parent 948dc71 commit 24675a43064c9c321a0a8aa39ea08ce8e15e1ae6
Showing with 509 additions and 24 deletions.
  1. +10 −0 doc/api/errors.md
  2. +83 −0 doc/api/http2.md
  3. +4 −0 lib/internal/errors.js
  4. +86 −17 lib/internal/http2/core.js
  5. +1 −0 src/env.h
  6. +120 −6 src/node_http2.cc
  7. +26 −1 src/node_http2.h
  8. +179 −0 test/parallel/test-http2-origin.js
@@ -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
@@ -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);
@@ -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)
@@ -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") \
Oops, something went wrong.

0 comments on commit 24675a4

Please sign in to comment.
You can’t perform that action at this time.