Skip to content

Commit 49f44f3

Browse files
jasnellBethGriggs
authored andcommitted
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 <[email protected]>
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") \

0 commit comments

Comments
 (0)