-
Notifications
You must be signed in to change notification settings - Fork 30k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
http2: add origin frame support #22956
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -920,6 +920,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 | ||
|
||
|
@@ -970,6 +975,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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ditto. |
||
|
||
<a id="ERR_HTTP2_OUT_OF_STREAMS"></a> | ||
### ERR_HTTP2_OUT_OF_STREAMS | ||
|
||
|
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 } One or more URL Strings passed as separate arguments. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
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 either the `http2.createServer()` or `http2.createSecureServer()` | ||
methods: | ||
|
||
```js | ||
const http2 = require('http2'); | ||
const options = getSecureOptionsSomehow(); | ||
options.origins = ['http://example.com', 'http://example.org']; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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 | ||
|
@@ -1977,6 +2053,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` | ||
vsemozhetbyt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
frame immediately following creation of a new server `Http2Session`. | ||
* `onRequestHandler` {Function} See [Compatibility API][] | ||
* Returns: {Http2SecureServer} | ||
|
||
|
@@ -3268,6 +3346,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 | ||
|
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') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just a quick question. And an error is thrown below even though this assignment goes through. I just came here by chance and was wondering about it :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the object does not have an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks 👍 |
||
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) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The format is inconsistent through the docs, but
`ORIGIN` frames
seems more readable?