-
-
Notifications
You must be signed in to change notification settings - Fork 33.3k
http2: add altsvc support #17917
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 altsvc support #17917
Changes from all 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 |
---|---|---|
|
@@ -558,11 +558,103 @@ added: REPLACEME | |
Calls [`unref()`][`net.Socket.prototype.unref`] on this `Http2Session` | ||
instance's underlying [`net.Socket`]. | ||
|
||
### Class: ServerHttp2Session | ||
<!-- YAML | ||
added: v8.4.0 | ||
--> | ||
|
||
#### serverhttp2session.altsvc(alt, originOrStream) | ||
<!-- YAML | ||
added: REPLACEME | ||
--> | ||
|
||
* `alt` {string} A description of the alternative service configuration as | ||
defined by [RFC 7838][]. | ||
* `originOrStream` {number|string|URL|Object} Either a URL string specifying | ||
the origin (or an Object with an `origin` property) or the numeric identifier | ||
of an active `Http2Stream` as given by the `http2stream.id` property. | ||
|
||
Submits an `ALTSVC` frame (as defined by [RFC 7838][]) to the connected client. | ||
|
||
```js | ||
const http2 = require('http2'); | ||
|
||
const server = http2.createServer(); | ||
server.on('session', (session) => { | ||
// Set altsvc for origin https://example.org:80 | ||
session.altsvc('h2=":8000"', 'https://example.org:80'); | ||
}); | ||
|
||
server.on('stream', (stream) => { | ||
// Set altsvc for a specific stream | ||
stream.session.altsvc('h2=":8000"', stream.id); | ||
}); | ||
``` | ||
|
||
Sending an `ALTSVC` frame with a specific stream ID indicates that the alternate | ||
service is associated with the origin of the given `Http2Stream`. | ||
|
||
The `alt` and origin string *must* contain only ASCII bytes and are | ||
strictly interpreted as a sequence of ASCII bytes. The special value `'clear'` | ||
may be passed to clear any previously set alternative service for a given | ||
domain. | ||
|
||
When a string is passed for the `originOrStream` argument, it will be parsed as | ||
a URL and the origin will be derived. For insetance, 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 | ||
`originOrStream`, 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. | ||
|
||
#### Specifying alternative services | ||
|
||
The format of the `alt` parameter is strictly defined by [RFC 7838][] as an | ||
ASCII string containing a comma-delimited list of "alternative" protocols | ||
associated with a specific host and port. | ||
|
||
For example, the value `'h2="example.org:81"'` indicates that the HTTP/2 | ||
protocol is available on the host `'example.org'` on TCP/IP port 81. The | ||
host and port *must* be contained within the quote (`"`) characters. | ||
|
||
Multiple alternatives may be specified, for instance: `'h2="example.org:81", | ||
h2=":82"'` | ||
|
||
The protocol identifier (`'h2'` in the examples) may be any valid | ||
[ALPN Protocol ID][]. | ||
|
||
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. | ||
|
||
### Class: ClientHttp2Session | ||
<!-- YAML | ||
added: v8.4.0 | ||
--> | ||
|
||
#### Event: 'altsvc' | ||
|
||
<!-- YAML | ||
added: REPLACEME | ||
--> | ||
|
||
The `'altsvc'` event is emitted whenever an `ALTSVC` frame is received by | ||
the client. The event is emitted with the `ALTSVC` value, origin, and stream | ||
ID, if any. If no `origin` is provided in the `ALTSVC` frame, `origin` will | ||
be an empty string. | ||
|
||
```js | ||
const http2 = require('http2'); | ||
const client = http2.connect('https://example.org'); | ||
|
||
client.on('altsvc', (alt, origin, stream) => { | ||
console.log(alt); | ||
console.log(origin); | ||
console.log(stream); | ||
}); | ||
``` | ||
|
||
#### clienthttp2session.request(headers[, options]) | ||
<!-- YAML | ||
added: v8.4.0 | ||
|
@@ -2850,6 +2942,7 @@ following additional properties: | |
|
||
|
||
[ALPN negotiation]: #http2_alpn_negotiation | ||
[ALPN Protocol ID]: https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids | ||
[Compatibility API]: #http2_compatibility_api | ||
[HTTP/1]: http.html | ||
[HTTP/2]: https://tools.ietf.org/html/rfc7540 | ||
|
@@ -2858,6 +2951,7 @@ following additional properties: | |
[Http2Session and Sockets]: #http2_http2session_and_sockets | ||
[Performance Observer]: perf_hooks.html | ||
[Readable Stream]: stream.html#stream_class_stream_readable | ||
[RFC 7838]: https://tools.ietf.org/html/rfc7838 | ||
[Settings Object]: #http2_settings_object | ||
[Using options.selectPadding]: #http2_using_options_selectpadding | ||
[Writable Stream]: stream.html#stream_writable_streams | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,6 +32,9 @@ const kMaxFrameSize = (2 ** 24) - 1; | |
const kMaxInt = (2 ** 32) - 1; | ||
const kMaxStreams = (2 ** 31) - 1; | ||
|
||
// eslint-disable-next-line no-control-regex | ||
const kQuotedString = /^[\x09\x20-\x5b\x5d-\x7e\x80-\xff]*$/; | ||
|
||
const { | ||
assertIsObject, | ||
assertValidPseudoHeaderResponse, | ||
|
@@ -364,6 +367,16 @@ function onFrameError(id, type, code) { | |
process.nextTick(emit, emitter, 'frameError', type, code, id); | ||
} | ||
|
||
function onAltSvc(stream, origin, alt) { | ||
const session = this[kOwner]; | ||
if (session.destroyed) | ||
return; | ||
debug(`Http2Session ${sessionName(session[kType])}: altsvc received: ` + | ||
`stream: ${stream}, origin: ${origin}, alt: ${alt}`); | ||
session[kUpdateTimer](); | ||
process.nextTick(emit, session, 'altsvc', alt, origin, stream); | ||
} | ||
|
||
// 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 | ||
|
@@ -706,6 +719,7 @@ function setupHandle(socket, type, options) { | |
handle.onheaders = onSessionHeaders; | ||
handle.onframeerror = onFrameError; | ||
handle.ongoawaydata = onGoawayData; | ||
handle.onaltsvc = onAltSvc; | ||
|
||
if (typeof options.selectPadding === 'function') | ||
handle.ongetpadding = onSelectPadding(options.selectPadding); | ||
|
@@ -1154,6 +1168,54 @@ class ServerHttp2Session extends Http2Session { | |
get server() { | ||
return this[kServer]; | ||
} | ||
|
||
// Submits an altsvc frame to be sent to the client. `stream` is a | ||
// numeric Stream ID. origin is a URL string that will be used to get | ||
// the origin. alt is a string containing the altsvc details. No fancy | ||
// API is provided for that. | ||
altsvc(alt, originOrStream) { | ||
if (this.destroyed) | ||
throw new errors.Error('ERR_HTTP2_INVALID_SESSION'); | ||
|
||
let stream = 0; | ||
let origin; | ||
|
||
if (typeof originOrStream === 'string') { | ||
origin = (new URL(originOrStream)).origin; | ||
|
||
if (origin === 'null') | ||
throw new errors.TypeError('ERR_HTTP2_ALTSVC_INVALID_ORIGIN'); | ||
} else if (typeof originOrStream === 'number') { | ||
if (originOrStream >>> 0 !== originOrStream || originOrStream === 0) | ||
throw new errors.RangeError('ERR_OUT_OF_RANGE', 'originOrStream'); | ||
stream = originOrStream; | ||
} else if (originOrStream !== undefined) { | ||
// Allow origin to be passed a URL or object with origin property | ||
if (originOrStream !== null && typeof originOrStream === 'object') | ||
origin = originOrStream.origin; | ||
// Note: if originOrStream is an object with an origin property other | ||
// than a URL, then it is possible that origin will be malformed. | ||
// We do not verify that here. Users who go that route need to | ||
// ensure they are doing the right thing or the payload data will | ||
// be invalid. | ||
if (typeof origin !== 'string') { | ||
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'originOrStream', | ||
['string', 'number', 'URL', 'object']); | ||
} else if (origin === 'null' || origin.length === 0) { | ||
throw new errors.TypeError('ERR_HTTP2_ALTSVC_INVALID_ORIGIN'); | ||
} | ||
} | ||
|
||
if (typeof alt !== 'string') | ||
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'alt', 'string'); | ||
if (!kQuotedString.test(alt)) | ||
throw new errors.TypeError('ERR_INVALID_CHAR', 'alt'); | ||
|
||
// Max length permitted for ALTSVC | ||
if ((alt.length + (origin !== undefined ? origin.length : 0)) > 16382) | ||
throw new errors.TypeError('ERR_HTTP2_ALTSVC_LENGTH'); | ||
|
||
this[kHandle].altsvc(stream, origin || '', alt); | ||
} | ||
} | ||
|
||
// ClientHttp2Session instances have to wait for the socket to connect after | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -103,6 +103,11 @@ Http2Options::Http2Options(Environment* env) { | |
// are required to buffer. | ||
nghttp2_option_set_no_auto_window_update(options_, 1); | ||
|
||
// Enable built in support for ALTSVC frames. Once we add support for | ||
// other non-built in extension frames, this will need to be handled | ||
// a bit differently. For now, let's let nghttp2 take care of it. | ||
nghttp2_option_set_builtin_recv_extension_type(options_, NGHTTP2_ALTSVC); | ||
|
||
AliasedBuffer<uint32_t, v8::Uint32Array>& buffer = | ||
env->http2_state()->options_buffer; | ||
uint32_t flags = buffer[IDX_OPTIONS_FLAGS]; | ||
|
@@ -830,6 +835,10 @@ inline int Http2Session::OnFrameReceive(nghttp2_session* handle, | |
break; | ||
case NGHTTP2_PING: | ||
session->HandlePingFrame(frame); | ||
break; | ||
case NGHTTP2_ALTSVC: | ||
session->HandleAltSvcFrame(frame); | ||
break; | ||
default: | ||
break; | ||
} | ||
|
@@ -1168,6 +1177,34 @@ inline void Http2Session::HandleGoawayFrame(const nghttp2_frame* frame) { | |
MakeCallback(env()->ongoawaydata_string(), arraysize(argv), argv); | ||
} | ||
|
||
// Called by OnFrameReceived when a complete ALTSVC frame has been received. | ||
inline void Http2Session::HandleAltSvcFrame(const nghttp2_frame* frame) { | ||
Isolate* isolate = env()->isolate(); | ||
HandleScope scope(isolate); | ||
Local<Context> context = env()->context(); | ||
Context::Scope context_scope(context); | ||
|
||
int32_t id = GetFrameID(frame); | ||
|
||
nghttp2_extension ext = frame->ext; | ||
nghttp2_ext_altsvc* altsvc = static_cast<nghttp2_ext_altsvc*>(ext.payload); | ||
DEBUG_HTTP2SESSION(this, "handling altsvc frame"); | ||
|
||
Local<Value> argv[3] = { | ||
Integer::New(isolate, id), | ||
String::NewFromOneByte(isolate, | ||
altsvc->origin, | ||
v8::NewStringType::kNormal, | ||
altsvc->origin_len).ToLocalChecked(), | ||
String::NewFromOneByte(isolate, | ||
altsvc->field_value, | ||
v8::NewStringType::kNormal, | ||
altsvc->field_value_len).ToLocalChecked(), | ||
}; | ||
|
||
MakeCallback(env()->onaltsvc_string(), arraysize(argv), argv); | ||
} | ||
|
||
// Called by OnFrameReceived when a complete PING frame has been received. | ||
inline void Http2Session::HandlePingFrame(const nghttp2_frame* frame) { | ||
bool ack = frame->hd.flags & NGHTTP2_FLAG_ACK; | ||
|
@@ -2477,6 +2514,44 @@ void Http2Stream::RefreshState(const FunctionCallbackInfo<Value>& args) { | |
} | ||
} | ||
|
||
void Http2Session::AltSvc(int32_t id, | ||
uint8_t* origin, | ||
size_t origin_len, | ||
uint8_t* value, | ||
size_t value_len) { | ||
Http2Scope h2scope(this); | ||
CHECK_EQ(nghttp2_submit_altsvc(session_, NGHTTP2_FLAG_NONE, id, | ||
origin, origin_len, value, value_len), 0); | ||
} | ||
|
||
// Submits an AltSvc frame to the sent to the connected peer. | ||
void Http2Session::AltSvc(const FunctionCallbackInfo<Value>& args) { | ||
Environment* env = Environment::GetCurrent(args); | ||
Http2Session* session; | ||
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); | ||
|
||
int32_t id = args[0]->Int32Value(env->context()).ToChecked(); | ||
|
||
// origin and value are both required to be ASCII, handle them as such. | ||
|
||
Local<String> origin_str = args[1]->ToString(env->context()).ToLocalChecked(); | ||
Local<String> value_str = args[2]->ToString(env->context()).ToLocalChecked(); | ||
|
||
size_t origin_len = origin_str->Length(); | ||
size_t value_len = value_str->Length(); | ||
|
||
CHECK_LE(origin_len + value_len, 16382); // Max permitted for ALTSVC | ||
// Verify that origin len != 0 if stream id == 0, or | ||
// that origin len == 0 if stream id != 0 | ||
CHECK((origin_len != 0 && id == 0) || (origin_len == 0 && id != 0)); | ||
|
||
MaybeStackBuffer<uint8_t> origin(origin_len); | ||
MaybeStackBuffer<uint8_t> value(value_len); | ||
origin_str->WriteOneByte(*origin); | ||
value_str->WriteOneByte(*value); | ||
|
||
session->AltSvc(id, *origin, origin_len, *value, value_len); | ||
} | ||
|
||
// Submits a PING frame to be sent to the connected peer. | ||
void Http2Session::Ping(const FunctionCallbackInfo<Value>& args) { | ||
Environment* env = Environment::GetCurrent(args); | ||
|
@@ -2694,6 +2769,7 @@ void Initialize(Local<Object> target, | |
session->SetClassName(http2SessionClassName); | ||
session->InstanceTemplate()->SetInternalFieldCount(1); | ||
AsyncWrap::AddWrapMethods(env, session); | ||
env->SetProtoMethod(session, "altsvc", Http2Session::AltSvc); | ||
env->SetProtoMethod(session, "ping", Http2Session::Ping); | ||
env->SetProtoMethod(session, "consume", Http2Session::Consume); | ||
env->SetProtoMethod(session, "destroy", Http2Session::Destroy); | ||
|
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.
Maybe this could mention the
clear
special value as well.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.
Yeah, good idea