Skip to content

Commit 24675a4

Browse files
jasnelltargos
authored andcommitted
http2: add origin frame support
PR-URL: #22956 Reviewed-By: Matteo Collina <[email protected]>
1 parent 948dc71 commit 24675a4

File tree

8 files changed

+509
-24
lines changed

8 files changed

+509
-24
lines changed

doc/api/errors.md

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

928+
<a id="ERR_HTTP2_INVALID_ORIGIN"></a>
929+
### ERR_HTTP2_INVALID_ORIGIN
930+
931+
HTTP/2 `ORIGIN` frames require a valid origin.
932+
928933
<a id="ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH"></a>
929934
### ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH
930935

@@ -975,6 +980,11 @@ Nested push streams are not permitted.
975980
An attempt was made to directly manipulate (read, write, pause, resume, etc.) a
976981
socket attached to an `Http2Session`.
977982

983+
<a id="ERR_HTTP2_ORIGIN_LENGTH"></a>
984+
### ERR_HTTP2_ORIGIN_LENGTH
985+
986+
HTTP/2 `ORIGIN` frames are limited to a length of 16382 bytes.
987+
978988
<a id="ERR_HTTP2_OUT_OF_STREAMS"></a>
979989
### ERR_HTTP2_OUT_OF_STREAMS
980990

doc/api/http2.md

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

435+
The `originSet` property is only available when using a secure TLS connection.
436+
435437
#### http2session.pendingSettingsAck
436438
<!-- YAML
437439
added: v8.4.0
@@ -670,6 +672,56 @@ The protocol identifier (`'h2'` in the examples) may be any valid
670672
The syntax of these values is not validated by the Node.js implementation and
671673
are passed through as provided by the user or received from the peer.
672674

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

755+
#### Event: 'origin'
756+
<!-- YAML
757+
added: REPLACEME
758+
-->
759+
760+
* `origins` {string[]}
761+
762+
The `'origin'` event is emitted whenever an `ORIGIN` frame is received by
763+
the client. The event is emitted with an array of `origin` strings. The
764+
`http2session.originSet` will be updated to include the received
765+
origins.
766+
767+
```js
768+
const http2 = require('http2');
769+
const client = http2.connect('https://example.org');
770+
771+
client.on('origin', (origins) => {
772+
for (let n = 0; n < origins.length; n++)
773+
console.log(origins[n]);
774+
});
775+
```
776+
777+
The `'origin'` event is only emitted when using a secure TLS connection.
778+
703779
#### clienthttp2session.request(headers[, options])
704780
<!-- YAML
705781
added: v8.4.0
@@ -1914,6 +1990,10 @@ server.listen(80);
19141990
<!-- YAML
19151991
added: v8.4.0
19161992
changes:
1993+
- version: REPLACEME
1994+
pr-url: https://github.com/nodejs/node/pull/22956
1995+
description: Added the `origins` option to automatically send an `ORIGIN`
1996+
frame on `Http2Session` startup.
19171997
- version: v8.9.3
19181998
pr-url: https://github.com/nodejs/node/pull/17105
19191999
description: Added the `maxOutstandingPings` option with a default limit of
@@ -1977,6 +2057,8 @@ changes:
19772057
remote peer upon connection.
19782058
* ...: Any [`tls.createServer()`][] options can be provided. For
19792059
servers, the identity options (`pfx` or `key`/`cert`) are usually required.
2060+
* `origins` {string[]} An array of origin strings to send within an `ORIGIN`
2061+
frame immediately following creation of a new server `Http2Session`.
19802062
* `onRequestHandler` {Function} See [Compatibility API][]
19812063
* Returns: {Http2SecureServer}
19822064

@@ -3268,6 +3350,7 @@ following additional properties:
32683350
[Performance Observer]: perf_hooks.html
32693351
[Readable Stream]: stream.html#stream_class_stream_readable
32703352
[RFC 7838]: https://tools.ietf.org/html/rfc7838
3353+
[RFC 8336]: https://tools.ietf.org/html/rfc8336
32713354
[Using `options.selectPadding()`]: #http2_using_options_selectpadding
32723355
[`'checkContinue'`]: #http2_event_checkcontinue
32733356
[`'request'`]: #http2_event_request

lib/internal/errors.js

+4
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,8 @@ E('ERR_HTTP2_INVALID_HEADER_VALUE',
569569
'Invalid value "%s" for header "%s"', TypeError);
570570
E('ERR_HTTP2_INVALID_INFO_STATUS',
571571
'Invalid informational status code: %s', RangeError);
572+
E('ERR_HTTP2_INVALID_ORIGIN',
573+
'HTTP/2 ORIGIN frames require a valid origin', TypeError);
572574
E('ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH',
573575
'Packed settings length must be a multiple of six', RangeError);
574576
E('ERR_HTTP2_INVALID_PSEUDOHEADER',
@@ -584,6 +586,8 @@ E('ERR_HTTP2_NESTED_PUSH',
584586
E('ERR_HTTP2_NO_SOCKET_MANIPULATION',
585587
'HTTP/2 sockets should not be directly manipulated (e.g. read and written)',
586588
Error);
589+
E('ERR_HTTP2_ORIGIN_LENGTH',
590+
'HTTP/2 ORIGIN frames are limited to 16382 bytes', TypeError);
587591
E('ERR_HTTP2_OUT_OF_STREAMS',
588592
'No stream ID is available because maximum stream ID has been reached',
589593
Error);

lib/internal/http2/core.js

+86-17
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,15 @@ const {
4343
ERR_HTTP2_HEADERS_AFTER_RESPOND,
4444
ERR_HTTP2_HEADERS_SENT,
4545
ERR_HTTP2_INVALID_INFO_STATUS,
46+
ERR_HTTP2_INVALID_ORIGIN,
4647
ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH,
4748
ERR_HTTP2_INVALID_SESSION,
4849
ERR_HTTP2_INVALID_SETTING_VALUE,
4950
ERR_HTTP2_INVALID_STREAM,
5051
ERR_HTTP2_MAX_PENDING_SETTINGS_ACK,
5152
ERR_HTTP2_NESTED_PUSH,
5253
ERR_HTTP2_NO_SOCKET_MANIPULATION,
54+
ERR_HTTP2_ORIGIN_LENGTH,
5355
ERR_HTTP2_OUT_OF_STREAMS,
5456
ERR_HTTP2_PAYLOAD_FORBIDDEN,
5557
ERR_HTTP2_PING_CANCEL,
@@ -148,6 +150,7 @@ const kInfoHeaders = Symbol('sent-info-headers');
148150
const kLocalSettings = Symbol('local-settings');
149151
const kOptions = Symbol('options');
150152
const kOwner = owner_symbol;
153+
const kOrigin = Symbol('origin');
151154
const kProceed = Symbol('proceed');
152155
const kProtocol = Symbol('protocol');
153156
const kProxySocket = Symbol('proxy-socket');
@@ -209,6 +212,7 @@ const {
209212
HTTP_STATUS_NO_CONTENT,
210213
HTTP_STATUS_NOT_MODIFIED,
211214
HTTP_STATUS_SWITCHING_PROTOCOLS,
215+
HTTP_STATUS_MISDIRECTED_REQUEST,
212216

213217
STREAM_OPTION_EMPTY_PAYLOAD,
214218
STREAM_OPTION_GET_TRAILERS
@@ -299,6 +303,11 @@ function onSessionHeaders(handle, id, cat, flags, headers) {
299303
} else {
300304
event = endOfStream ? 'trailers' : 'headers';
301305
}
306+
const session = stream.session;
307+
if (status === HTTP_STATUS_MISDIRECTED_REQUEST) {
308+
const originSet = session[kState].originSet = initOriginSet(session);
309+
originSet.delete(stream[kOrigin]);
310+
}
302311
debug(`Http2Stream ${id} [Http2Session ` +
303312
`${sessionName(type)}]: emitting stream '${event}' event`);
304313
process.nextTick(emit, stream, event, obj, flags, headers);
@@ -429,6 +438,39 @@ function onAltSvc(stream, origin, alt) {
429438
session.emit('altsvc', alt, origin, stream);
430439
}
431440

441+
function initOriginSet(session) {
442+
let originSet = session[kState].originSet;
443+
if (originSet === undefined) {
444+
const socket = session[kSocket];
445+
session[kState].originSet = originSet = new Set();
446+
if (socket.servername != null) {
447+
let originString = `https://${socket.servername}`;
448+
if (socket.remotePort != null)
449+
originString += `:${socket.remotePort}`;
450+
// We have to ensure that it is a properly serialized
451+
// ASCII origin string. The socket.servername might not
452+
// be properly ASCII encoded.
453+
originSet.add((new URL(originString)).origin);
454+
}
455+
}
456+
return originSet;
457+
}
458+
459+
function onOrigin(origins) {
460+
const session = this[kOwner];
461+
if (session.destroyed)
462+
return;
463+
debug(`Http2Session ${sessionName(session[kType])}: origin received: ` +
464+
`${origins.join(', ')}`);
465+
session[kUpdateTimer]();
466+
if (!session.encrypted || session.destroyed)
467+
return undefined;
468+
const originSet = initOriginSet(session);
469+
for (var n = 0; n < origins.length; n++)
470+
originSet.add(origins[n]);
471+
session.emit('origin', origins);
472+
}
473+
432474
// Receiving a GOAWAY frame from the connected peer is a signal that no
433475
// new streams should be created. If the code === NGHTTP2_NO_ERROR, we
434476
// are going to send our close, but allow existing frames to close
@@ -782,6 +824,7 @@ function setupHandle(socket, type, options) {
782824
handle.onframeerror = onFrameError;
783825
handle.ongoawaydata = onGoawayData;
784826
handle.onaltsvc = onAltSvc;
827+
handle.onorigin = onOrigin;
785828

786829
if (typeof options.selectPadding === 'function')
787830
handle.ongetpadding = onSelectPadding(options.selectPadding);
@@ -808,6 +851,12 @@ function setupHandle(socket, type, options) {
808851
options.settings : {};
809852

810853
this.settings(settings);
854+
855+
if (type === NGHTTP2_SESSION_SERVER &&
856+
Array.isArray(options.origins)) {
857+
this.origin(...options.origins);
858+
}
859+
811860
process.nextTick(emit, this, 'connect', this, socket);
812861
}
813862

@@ -947,23 +996,7 @@ class Http2Session extends EventEmitter {
947996
get originSet() {
948997
if (!this.encrypted || this.destroyed)
949998
return undefined;
950-
951-
let originSet = this[kState].originSet;
952-
if (originSet === undefined) {
953-
const socket = this[kSocket];
954-
this[kState].originSet = originSet = new Set();
955-
if (socket.servername != null) {
956-
let originString = `https://${socket.servername}`;
957-
if (socket.remotePort != null)
958-
originString += `:${socket.remotePort}`;
959-
// We have to ensure that it is a properly serialized
960-
// ASCII origin string. The socket.servername might not
961-
// be properly ASCII encoded.
962-
originSet.add((new URL(originString)).origin);
963-
}
964-
}
965-
966-
return Array.from(originSet);
999+
return Array.from(initOriginSet(this));
9671000
}
9681001

9691002
// True if the Http2Session is still waiting for the socket to connect
@@ -1338,6 +1371,40 @@ class ServerHttp2Session extends Http2Session {
13381371

13391372
this[kHandle].altsvc(stream, origin || '', alt);
13401373
}
1374+
1375+
// Submits an origin frame to be sent.
1376+
origin(...origins) {
1377+
if (this.destroyed)
1378+
throw new ERR_HTTP2_INVALID_SESSION();
1379+
1380+
if (origins.length === 0)
1381+
return;
1382+
1383+
let arr = '';
1384+
let len = 0;
1385+
const count = origins.length;
1386+
for (var i = 0; i < count; i++) {
1387+
let origin = origins[i];
1388+
if (typeof origin === 'string') {
1389+
origin = (new URL(origin)).origin;
1390+
} else if (origin != null && typeof origin === 'object') {
1391+
origin = origin.origin;
1392+
}
1393+
if (typeof origin !== 'string')
1394+
throw new ERR_INVALID_ARG_TYPE('origin', 'string', origin);
1395+
if (origin === 'null')
1396+
throw new ERR_HTTP2_INVALID_ORIGIN();
1397+
1398+
arr += `${origin}\0`;
1399+
len += origin.length;
1400+
}
1401+
1402+
if (len > 16382)
1403+
throw new ERR_HTTP2_ORIGIN_LENGTH();
1404+
1405+
this[kHandle].origin(arr, count);
1406+
}
1407+
13411408
}
13421409

13431410
// ClientHttp2Session instances have to wait for the socket to connect after
@@ -1406,6 +1473,8 @@ class ClientHttp2Session extends Http2Session {
14061473

14071474
const stream = new ClientHttp2Stream(this, undefined, undefined, {});
14081475
stream[kSentHeaders] = headers;
1476+
stream[kOrigin] = `${headers[HTTP2_HEADER_SCHEME]}://` +
1477+
`${headers[HTTP2_HEADER_AUTHORITY]}`;
14091478

14101479
// Close the writable side of the stream if options.endStream is set.
14111480
if (options.endStream)

src/env.h

+1
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ struct PackageConfig {
224224
V(onnewsession_string, "onnewsession") \
225225
V(onocspresponse_string, "onocspresponse") \
226226
V(ongoawaydata_string, "ongoawaydata") \
227+
V(onorigin_string, "onorigin") \
227228
V(onpriority_string, "onpriority") \
228229
V(onread_string, "onread") \
229230
V(onreadstart_string, "onreadstart") \

0 commit comments

Comments
 (0)