Skip to content

Commit

Permalink
http2: add origin frame support
Browse files Browse the repository at this point in the history
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.

PR-URL: nodejs#22956
Reviewed-By: Matteo Collina <[email protected]>
  • Loading branch information
jasnell authored and kjin committed Oct 3, 2018
1 parent 57ac6a5 commit c719f74
Show file tree
Hide file tree
Showing 8 changed files with 514 additions and 25 deletions.
10 changes: 10 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
88 changes: 88 additions & 0 deletions doc/api/http2.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}

Expand Down Expand Up @@ -3282,8 +3364,14 @@ following additional properties:
[Performance Observer]: perf_hooks.html
[Readable Stream]: stream.html#stream_class_stream_readable
[RFC 7838]: https://tools.ietf.org/html/rfc7838
<<<<<<< HEAD
[RFC 8336]: https://tools.ietf.org/html/rfc8336
[Using options.selectPadding]: #http2_using_options_selectpadding
[Writable Stream]: stream.html#stream_writable_streams
=======
[RFC 8336]: https://tools.ietf.org/html/rfc8336
[Using `options.selectPadding()`]: #http2_using_options_selectpadding
>>>>>>> 24675a4306... http2: add origin frame support
[`'checkContinue'`]: #http2_event_checkcontinue
[`'request'`]: #http2_event_request
[`'unknownProtocol'`]: #http2_event_unknownprotocol
Expand Down
3 changes: 3 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand Down
102 changes: 85 additions & 17 deletions lib/internal/http2/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -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") \
Expand Down
Loading

0 comments on commit c719f74

Please sign in to comment.