Skip to content

Commit 644bd5f

Browse files
jasnellkjin
authored andcommitted
http2: add altsvc support
This commit also includes prerequisite error definitions from c75f87c and 1698c8e. Add support for sending and receiving ALTSVC frames. Backport-PR-URL: nodejs#18050 PR-URL: nodejs#17917 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 936337a commit 644bd5f

File tree

8 files changed

+382
-0
lines changed

8 files changed

+382
-0
lines changed

doc/api/errors.md

+10
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,16 @@ that.
643643

644644
Occurs with multiple attempts to shutdown an HTTP/2 session.
645645

646+
<a id="ERR_HTTP2_ALTSVC_INVALID_ORIGIN"></a>
647+
### ERR_HTTP2_ALTSVC_INVALID_ORIGIN
648+
649+
HTTP/2 ALTSVC frames require a valid origin.
650+
651+
<a id="ERR_HTTP2_ALTSVC_LENGTH"></a>
652+
### ERR_HTTP2_ALTSVC_LENGTH
653+
654+
HTTP/2 ALTSVC frames are limited to a maximum of 16,382 payload bytes.
655+
646656
<a id="ERR_HTTP2_CONNECT_AUTHORITY"></a>
647657
### ERR_HTTP2_CONNECT_AUTHORITY
648658

doc/api/http2.md

+94
Original file line numberDiff line numberDiff line change
@@ -570,11 +570,103 @@ added: REPLACEME
570570
Calls [`unref()`][`net.Socket.prototype.unref`] on this `Http2Session`
571571
instance's underlying [`net.Socket`].
572572

573+
### Class: ServerHttp2Session
574+
<!-- YAML
575+
added: v8.4.0
576+
-->
577+
578+
#### serverhttp2session.altsvc(alt, originOrStream)
579+
<!-- YAML
580+
added: REPLACEME
581+
-->
582+
583+
* `alt` {string} A description of the alternative service configuration as
584+
defined by [RFC 7838][].
585+
* `originOrStream` {number|string|URL|Object} Either a URL string specifying
586+
the origin (or an Object with an `origin` property) or the numeric identifier
587+
of an active `Http2Stream` as given by the `http2stream.id` property.
588+
589+
Submits an `ALTSVC` frame (as defined by [RFC 7838][]) to the connected client.
590+
591+
```js
592+
const http2 = require('http2');
593+
594+
const server = http2.createServer();
595+
server.on('session', (session) => {
596+
// Set altsvc for origin https://example.org:80
597+
session.altsvc('h2=":8000"', 'https://example.org:80');
598+
});
599+
600+
server.on('stream', (stream) => {
601+
// Set altsvc for a specific stream
602+
stream.session.altsvc('h2=":8000"', stream.id);
603+
});
604+
```
605+
606+
Sending an `ALTSVC` frame with a specific stream ID indicates that the alternate
607+
service is associated with the origin of the given `Http2Stream`.
608+
609+
The `alt` and origin string *must* contain only ASCII bytes and are
610+
strictly interpreted as a sequence of ASCII bytes. The special value `'clear'`
611+
may be passed to clear any previously set alternative service for a given
612+
domain.
613+
614+
When a string is passed for the `originOrStream` argument, it will be parsed as
615+
a URL and the origin will be derived. For insetance, the origin for the
616+
HTTP URL `'https://example.org/foo/bar'` is the ASCII string
617+
`'https://example.org'`. An error will be thrown if either the given string
618+
cannot be parsed as a URL or if a valid origin cannot be derived.
619+
620+
A `URL` object, or any object with an `origin` property, may be passed as
621+
`originOrStream`, in which case the value of the `origin` property will be
622+
used. The value of the `origin` property *must* be a properly serialized
623+
ASCII origin.
624+
625+
#### Specifying alternative services
626+
627+
The format of the `alt` parameter is strictly defined by [RFC 7838][] as an
628+
ASCII string containing a comma-delimited list of "alternative" protocols
629+
associated with a specific host and port.
630+
631+
For example, the value `'h2="example.org:81"'` indicates that the HTTP/2
632+
protocol is available on the host `'example.org'` on TCP/IP port 81. The
633+
host and port *must* be contained within the quote (`"`) characters.
634+
635+
Multiple alternatives may be specified, for instance: `'h2="example.org:81",
636+
h2=":82"'`
637+
638+
The protocol identifier (`'h2'` in the examples) may be any valid
639+
[ALPN Protocol ID][].
640+
641+
The syntax of these values is not validated by the Node.js implementation and
642+
are passed through as provided by the user or received from the peer.
643+
573644
### Class: ClientHttp2Session
574645
<!-- YAML
575646
added: v8.4.0
576647
-->
577648

649+
#### Event: 'altsvc'
650+
<!-- YAML
651+
added: REPLACEME
652+
-->
653+
654+
The `'altsvc'` event is emitted whenever an `ALTSVC` frame is received by
655+
the client. The event is emitted with the `ALTSVC` value, origin, and stream
656+
ID, if any. If no `origin` is provided in the `ALTSVC` frame, `origin` will
657+
be an empty string.
658+
659+
```js
660+
const http2 = require('http2');
661+
const client = http2.connect('https://example.org');
662+
663+
client.on('altsvc', (alt, origin, stream) => {
664+
console.log(alt);
665+
console.log(origin);
666+
console.log(stream);
667+
});
668+
```
669+
578670
#### clienthttp2session.request(headers[, options])
579671
<!-- YAML
580672
added: v8.4.0
@@ -2869,6 +2961,7 @@ following additional properties:
28692961

28702962

28712963
[ALPN negotiation]: #http2_alpn_negotiation
2964+
[ALPN Protocol ID]: https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids
28722965
[Compatibility API]: #http2_compatibility_api
28732966
[HTTP/1]: http.html
28742967
[HTTP/2]: https://tools.ietf.org/html/rfc7540
@@ -2877,6 +2970,7 @@ following additional properties:
28772970
[Http2Session and Sockets]: #http2_http2session_and_sockets
28782971
[Performance Observer]: perf_hooks.html
28792972
[Readable Stream]: stream.html#stream_class_stream_readable
2973+
[RFC 7838]: https://tools.ietf.org/html/rfc7838
28802974
[Settings Object]: #http2_settings_object
28812975
[Using options.selectPadding]: #http2_using_options_selectpadding
28822976
[Writable Stream]: stream.html#stream_writable_streams

lib/internal/errors.js

+6
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,10 @@ E('ERR_ENCODING_NOT_SUPPORTED',
152152
E('ERR_FALSY_VALUE_REJECTION', 'Promise was rejected with falsy value');
153153
E('ERR_HTTP2_ALREADY_SHUTDOWN',
154154
'Http2Session is already shutdown or destroyed');
155+
E('ERR_HTTP2_ALTSVC_INVALID_ORIGIN',
156+
'HTTP/2 ALTSVC frames require a valid origin');
157+
E('ERR_HTTP2_ALTSVC_LENGTH',
158+
'HTTP/2 ALTSVC frames are limited to 16382 bytes');
155159
E('ERR_HTTP2_CONNECT_AUTHORITY',
156160
':authority header is required for CONNECT requests');
157161
E('ERR_HTTP2_CONNECT_PATH',
@@ -233,6 +237,7 @@ E('ERR_INVALID_ARRAY_LENGTH',
233237
});
234238
E('ERR_INVALID_ASYNC_ID', (type, id) => `Invalid ${type} value: ${id}`);
235239
E('ERR_INVALID_CALLBACK', 'callback must be a function');
240+
E('ERR_INVALID_CHAR', 'Invalid character in %s');
236241
E('ERR_INVALID_FD', (fd) => `"fd" must be a positive integer: ${fd}`);
237242
E('ERR_INVALID_FILE_URL_HOST', 'File URL host %s');
238243
E('ERR_INVALID_FILE_URL_PATH', 'File URL path %s');
@@ -270,6 +275,7 @@ E('ERR_NAPI_CONS_FUNCTION', 'Constructor must be a function');
270275
E('ERR_NAPI_CONS_PROTOTYPE_OBJECT', 'Constructor.prototype must be an object');
271276
E('ERR_NO_CRYPTO', 'Node.js is not compiled with OpenSSL crypto support');
272277
E('ERR_NO_ICU', '%s is not supported on Node.js compiled without ICU');
278+
E('ERR_OUT_OF_RANGE', 'The "%s" argument is out of range');
273279
E('ERR_PARSE_HISTORY_DATA', 'Could not parse history data in %s');
274280
E('ERR_REQUIRE_ESM', 'Must use import to load ES Module: %s');
275281
E('ERR_SOCKET_ALREADY_BOUND', 'Socket is already bound');

lib/internal/http2/core.js

+62
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ const kMaxFrameSize = (2 ** 24) - 1;
3232
const kMaxInt = (2 ** 32) - 1;
3333
const kMaxStreams = (2 ** 31) - 1;
3434

35+
// eslint-disable-next-line no-control-regex
36+
const kQuotedString = /^[\x09\x20-\x5b\x5d-\x7e\x80-\xff]*$/;
37+
3538
const {
3639
assertIsObject,
3740
assertValidPseudoHeaderResponse,
@@ -362,6 +365,16 @@ function onFrameError(id, type, code) {
362365
process.nextTick(emit, emitter, 'frameError', type, code, id);
363366
}
364367

368+
function onAltSvc(stream, origin, alt) {
369+
const session = this[kOwner];
370+
if (session.destroyed)
371+
return;
372+
debug(`Http2Session ${sessionName(session[kType])}: altsvc received: ` +
373+
`stream: ${stream}, origin: ${origin}, alt: ${alt}`);
374+
session[kUpdateTimer]();
375+
process.nextTick(emit, session, 'altsvc', alt, origin, stream);
376+
}
377+
365378
// Receiving a GOAWAY frame from the connected peer is a signal that no
366379
// new streams should be created. If the code === NGHTTP2_NO_ERROR, we
367380
// are going to send our our close, but allow existing frames to close
@@ -704,6 +717,7 @@ function setupHandle(socket, type, options) {
704717
handle.onheaders = onSessionHeaders;
705718
handle.onframeerror = onFrameError;
706719
handle.ongoawaydata = onGoawayData;
720+
handle.onaltsvc = onAltSvc;
707721

708722
if (typeof options.selectPadding === 'function')
709723
handle.ongetpadding = onSelectPadding(options.selectPadding);
@@ -1150,6 +1164,54 @@ class ServerHttp2Session extends Http2Session {
11501164
get server() {
11511165
return this[kServer];
11521166
}
1167+
1168+
// Submits an altsvc frame to be sent to the client. `stream` is a
1169+
// numeric Stream ID. origin is a URL string that will be used to get
1170+
// the origin. alt is a string containing the altsvc details. No fancy
1171+
// API is provided for that.
1172+
altsvc(alt, originOrStream) {
1173+
if (this.destroyed)
1174+
throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
1175+
1176+
let stream = 0;
1177+
let origin;
1178+
1179+
if (typeof originOrStream === 'string') {
1180+
origin = (new URL(originOrStream)).origin;
1181+
if (origin === 'null')
1182+
throw new errors.TypeError('ERR_HTTP2_ALTSVC_INVALID_ORIGIN');
1183+
} else if (typeof originOrStream === 'number') {
1184+
if (originOrStream >>> 0 !== originOrStream || originOrStream === 0)
1185+
throw new errors.RangeError('ERR_OUT_OF_RANGE', 'originOrStream');
1186+
stream = originOrStream;
1187+
} else if (originOrStream !== undefined) {
1188+
// Allow origin to be passed a URL or object with origin property
1189+
if (originOrStream !== null && typeof originOrStream === 'object')
1190+
origin = originOrStream.origin;
1191+
// Note: if originOrStream is an object with an origin property other
1192+
// than a URL, then it is possible that origin will be malformed.
1193+
// We do not verify that here. Users who go that route need to
1194+
// ensure they are doing the right thing or the payload data will
1195+
// be invalid.
1196+
if (typeof origin !== 'string') {
1197+
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'originOrStream',
1198+
['string', 'number', 'URL', 'object']);
1199+
} else if (origin === 'null' || origin.length === 0) {
1200+
throw new errors.TypeError('ERR_HTTP2_ALTSVC_INVALID_ORIGIN');
1201+
}
1202+
}
1203+
1204+
if (typeof alt !== 'string')
1205+
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'alt', 'string');
1206+
if (!kQuotedString.test(alt))
1207+
throw new errors.TypeError('ERR_INVALID_CHAR', 'alt');
1208+
1209+
// Max length permitted for ALTSVC
1210+
if ((alt.length + (origin !== undefined ? origin.length : 0)) > 16382)
1211+
throw new errors.TypeError('ERR_HTTP2_ALTSVC_LENGTH');
1212+
1213+
this[kHandle].altsvc(stream, origin || '', alt);
1214+
}
11531215
}
11541216

11551217
// ClientHttp2Session instances have to wait for the socket to connect after

src/env.h

+1
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ class ModuleWrap;
199199
V(nsname_string, "nsname") \
200200
V(nexttick_string, "nextTick") \
201201
V(ocsp_request_string, "OCSPRequest") \
202+
V(onaltsvc_string, "onaltsvc") \
202203
V(onchange_string, "onchange") \
203204
V(onclienthello_string, "onclienthello") \
204205
V(oncomplete_string, "oncomplete") \

src/node_http2.cc

+76
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ Http2Options::Http2Options(Environment* env) {
103103
// are required to buffer.
104104
nghttp2_option_set_no_auto_window_update(options_, 1);
105105

106+
// Enable built in support for ALTSVC frames. Once we add support for
107+
// other non-built in extension frames, this will need to be handled
108+
// a bit differently. For now, let's let nghttp2 take care of it.
109+
nghttp2_option_set_builtin_recv_extension_type(options_, NGHTTP2_ALTSVC);
110+
106111
AliasedBuffer<uint32_t, v8::Uint32Array>& buffer =
107112
env->http2_state()->options_buffer;
108113
uint32_t flags = buffer[IDX_OPTIONS_FLAGS];
@@ -848,6 +853,10 @@ inline int Http2Session::OnFrameReceive(nghttp2_session* handle,
848853
break;
849854
case NGHTTP2_PING:
850855
session->HandlePingFrame(frame);
856+
break;
857+
case NGHTTP2_ALTSVC:
858+
session->HandleAltSvcFrame(frame);
859+
break;
851860
default:
852861
break;
853862
}
@@ -1186,6 +1195,34 @@ inline void Http2Session::HandleGoawayFrame(const nghttp2_frame* frame) {
11861195
MakeCallback(env()->ongoawaydata_string(), arraysize(argv), argv);
11871196
}
11881197

1198+
// Called by OnFrameReceived when a complete ALTSVC frame has been received.
1199+
inline void Http2Session::HandleAltSvcFrame(const nghttp2_frame* frame) {
1200+
Isolate* isolate = env()->isolate();
1201+
HandleScope scope(isolate);
1202+
Local<Context> context = env()->context();
1203+
Context::Scope context_scope(context);
1204+
1205+
int32_t id = GetFrameID(frame);
1206+
1207+
nghttp2_extension ext = frame->ext;
1208+
nghttp2_ext_altsvc* altsvc = static_cast<nghttp2_ext_altsvc*>(ext.payload);
1209+
DEBUG_HTTP2SESSION(this, "handling altsvc frame");
1210+
1211+
Local<Value> argv[3] = {
1212+
Integer::New(isolate, id),
1213+
String::NewFromOneByte(isolate,
1214+
altsvc->origin,
1215+
v8::NewStringType::kNormal,
1216+
altsvc->origin_len).ToLocalChecked(),
1217+
String::NewFromOneByte(isolate,
1218+
altsvc->field_value,
1219+
v8::NewStringType::kNormal,
1220+
altsvc->field_value_len).ToLocalChecked(),
1221+
};
1222+
1223+
MakeCallback(env()->onaltsvc_string(), arraysize(argv), argv);
1224+
}
1225+
11891226
// Called by OnFrameReceived when a complete PING frame has been received.
11901227
inline void Http2Session::HandlePingFrame(const nghttp2_frame* frame) {
11911228
bool ack = frame->hd.flags & NGHTTP2_FLAG_ACK;
@@ -2495,6 +2532,44 @@ void Http2Stream::RefreshState(const FunctionCallbackInfo<Value>& args) {
24952532
}
24962533
}
24972534

2535+
void Http2Session::AltSvc(int32_t id,
2536+
uint8_t* origin,
2537+
size_t origin_len,
2538+
uint8_t* value,
2539+
size_t value_len) {
2540+
Http2Scope h2scope(this);
2541+
CHECK_EQ(nghttp2_submit_altsvc(session_, NGHTTP2_FLAG_NONE, id,
2542+
origin, origin_len, value, value_len), 0);
2543+
}
2544+
2545+
// Submits an AltSvc frame to the sent to the connected peer.
2546+
void Http2Session::AltSvc(const FunctionCallbackInfo<Value>& args) {
2547+
Environment* env = Environment::GetCurrent(args);
2548+
Http2Session* session;
2549+
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
2550+
2551+
int32_t id = args[0]->Int32Value(env->context()).ToChecked();
2552+
2553+
// origin and value are both required to be ASCII, handle them as such.
2554+
Local<String> origin_str = args[1]->ToString(env->context()).ToLocalChecked();
2555+
Local<String> value_str = args[2]->ToString(env->context()).ToLocalChecked();
2556+
2557+
size_t origin_len = origin_str->Length();
2558+
size_t value_len = value_str->Length();
2559+
2560+
CHECK_LE(origin_len + value_len, 16382); // Max permitted for ALTSVC
2561+
// Verify that origin len != 0 if stream id == 0, or
2562+
// that origin len == 0 if stream id != 0
2563+
CHECK((origin_len != 0 && id == 0) || (origin_len == 0 && id != 0));
2564+
2565+
MaybeStackBuffer<uint8_t> origin(origin_len);
2566+
MaybeStackBuffer<uint8_t> value(value_len);
2567+
origin_str->WriteOneByte(*origin);
2568+
value_str->WriteOneByte(*value);
2569+
2570+
session->AltSvc(id, *origin, origin_len, *value, value_len);
2571+
}
2572+
24982573
// Submits a PING frame to be sent to the connected peer.
24992574
void Http2Session::Ping(const FunctionCallbackInfo<Value>& args) {
25002575
Environment* env = Environment::GetCurrent(args);
@@ -2712,6 +2787,7 @@ void Initialize(Local<Object> target,
27122787
session->SetClassName(http2SessionClassName);
27132788
session->InstanceTemplate()->SetInternalFieldCount(1);
27142789
AsyncWrap::AddWrapMethods(env, session);
2790+
env->SetProtoMethod(session, "altsvc", Http2Session::AltSvc);
27152791
env->SetProtoMethod(session, "ping", Http2Session::Ping);
27162792
env->SetProtoMethod(session, "consume", Http2Session::Consume);
27172793
env->SetProtoMethod(session, "destroy", Http2Session::Destroy);

0 commit comments

Comments
 (0)