Skip to content

Commit df50686

Browse files
committed
http2: add altsvc support
Add support for sending and receiving ALTSVC frames.
1 parent 231b116 commit df50686

File tree

8 files changed

+350
-0
lines changed

8 files changed

+350
-0
lines changed

doc/api/errors.md

+10
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,16 @@ that.
821821

822822
Occurs with multiple attempts to shutdown an HTTP/2 session.
823823

824+
<a id="ERR_HTTP2_ALTSVC_INVALID_ORIGIN"></a>
825+
### ERR_HTTP2_ALTSVC_INVALID_ORIGIN
826+
827+
HTTP/2 ALTSVC frames require a valid origin.
828+
829+
<a id="ERR_HTTP2_ALTSVC_LENGTH"></a>
830+
### ERR_HTTP2_ALTSVC_LENGTH
831+
832+
HTTP/2 ALTSVC frames are limited to a maximum of 16,382 payload bytes.
833+
824834
<a id="ERR_HTTP2_CONNECT_AUTHORITY"></a>
825835
### ERR_HTTP2_CONNECT_AUTHORITY
826836

doc/api/http2.md

+88
Original file line numberDiff line numberDiff line change
@@ -558,11 +558,97 @@ added: REPLACEME
558558
Calls [`unref()`][`net.Socket.prototype.unref`] on this `Http2Session`
559559
instance's underlying [`net.Socket`].
560560

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

631+
#### Event: 'altsvc'
632+
<!-- YAML
633+
added: REPLACEME
634+
-->
635+
636+
The `'altsvc'` event is emitted whenever an `ALTSVC` frame is received by
637+
the client. The event is emitted with the `ALTSVC` value, origin, and stream
638+
ID, if any. If no `origin` is provided in the `ALTSVC` frame, `origin` will
639+
be an empty string.
640+
641+
```js
642+
const http2 = require('http2');
643+
const client = http2.connect('https://example.org');
644+
645+
client.on('altsvc', (alt, origin, stream) => {
646+
console.log(alt);
647+
console.log(origin);
648+
console.log(stream);
649+
});
650+
```
651+
566652
#### clienthttp2session.request(headers[, options])
567653
<!-- YAML
568654
added: v8.4.0
@@ -2850,6 +2936,7 @@ following additional properties:
28502936

28512937

28522938
[ALPN negotiation]: #http2_alpn_negotiation
2939+
[ALPN Protocol ID]: https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids
28532940
[Compatibility API]: #http2_compatibility_api
28542941
[HTTP/1]: http.html
28552942
[HTTP/2]: https://tools.ietf.org/html/rfc7540
@@ -2858,6 +2945,7 @@ following additional properties:
28582945
[Http2Session and Sockets]: #http2_http2session_and_sockets
28592946
[Performance Observer]: perf_hooks.html
28602947
[Readable Stream]: stream.html#stream_class_stream_readable
2948+
[RFC 7838]: https://tools.ietf.org/html/rfc7838
28612949
[Settings Object]: #http2_settings_object
28622950
[Using options.selectPadding]: #http2_using_options_selectpadding
28632951
[Writable Stream]: stream.html#stream_writable_streams

lib/internal/errors.js

+4
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,10 @@ E('ERR_FS_INVALID_SYMLINK_TYPE',
304304
'Symlink type must be one of "dir", "file", or "junction". Received "%s"');
305305
E('ERR_HTTP2_ALREADY_SHUTDOWN',
306306
'Http2Session is already shutdown or destroyed');
307+
E('ERR_HTTP2_ALTSVC_INVALID_ORIGIN',
308+
'HTTP/2 ALTSVC frames require a valid origin');
309+
E('ERR_HTTP2_ALTSVC_LENGTH',
310+
'HTTP/2 ALTSVC frames are limited to 16382 bytes');
307311
E('ERR_HTTP2_CONNECT_AUTHORITY',
308312
':authority header is required for CONNECT requests');
309313
E('ERR_HTTP2_CONNECT_PATH',

lib/internal/http2/core.js

+50
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,
@@ -364,6 +367,16 @@ function onFrameError(id, type, code) {
364367
process.nextTick(emit, emitter, 'frameError', type, code, id);
365368
}
366369

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

710724
if (typeof options.selectPadding === 'function')
711725
handle.ongetpadding = onSelectPadding(options.selectPadding);
@@ -1154,6 +1168,42 @@ class ServerHttp2Session extends Http2Session {
11541168
get server() {
11551169
return this[kServer];
11561170
}
1171+
1172+
// Submits an altsvc frame to be sent to the client. `stream` is a
1173+
// numeric Stream ID. origin is a URL string that will be used to get
1174+
// the origin. alt is a string containing the altsvc details. No fancy
1175+
// API is provided for that.
1176+
altsvc(alt, originOrStream) {
1177+
if (this.destroyed)
1178+
throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
1179+
1180+
let stream = 0;
1181+
let origin;
1182+
1183+
if (typeof originOrStream === 'string') {
1184+
origin = (new URL(originOrStream)).origin;
1185+
if (origin.length === 0 || origin === 'null')
1186+
throw new errors.TypeError('ERR_HTTP2_ALTSVC_INVALID_ORIGIN');
1187+
} else if (typeof originOrStream === 'number') {
1188+
if (originOrStream >>> 0 !== originOrStream || originOrStream === 0)
1189+
throw new errors.RangeError('ERR_OUT_OF_RANGE', 'originOrStream');
1190+
stream = originOrStream;
1191+
} else if (originOrStream !== undefined) {
1192+
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'originOrStream',
1193+
['string', 'number']);
1194+
}
1195+
1196+
if (typeof alt !== 'string')
1197+
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'alt', 'string');
1198+
if (!kQuotedString.test(alt))
1199+
throw new errors.TypeError('ERR_INVALID_CHAR', 'alt');
1200+
1201+
// Max length permitted for ALTSVC
1202+
if ((alt.length + (origin !== undefined ? origin.length : 0)) > 16382)
1203+
throw new errors.TypeError('ERR_HTTP2_ALTSVC_LENGTH');
1204+
1205+
this[kHandle].altsvc(stream, origin || '', alt);
1206+
}
11571207
}
11581208

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

src/env.h

+1
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ class ModuleWrap;
179179
V(netmask_string, "netmask") \
180180
V(nsname_string, "nsname") \
181181
V(ocsp_request_string, "OCSPRequest") \
182+
V(onaltsvc_string, "onaltsvc") \
182183
V(onchange_string, "onchange") \
183184
V(onclienthello_string, "onclienthello") \
184185
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];
@@ -830,6 +835,10 @@ inline int Http2Session::OnFrameReceive(nghttp2_session* handle,
830835
break;
831836
case NGHTTP2_PING:
832837
session->HandlePingFrame(frame);
838+
break;
839+
case NGHTTP2_ALTSVC:
840+
session->HandleAltSvcFrame(frame);
841+
break;
833842
default:
834843
break;
835844
}
@@ -1168,6 +1177,34 @@ inline void Http2Session::HandleGoawayFrame(const nghttp2_frame* frame) {
11681177
MakeCallback(env()->ongoawaydata_string(), arraysize(argv), argv);
11691178
}
11701179

1180+
// Called by OnFrameReceived when a complete ALTSVC frame has been received.
1181+
inline void Http2Session::HandleAltSvcFrame(const nghttp2_frame* frame) {
1182+
Isolate* isolate = env()->isolate();
1183+
HandleScope scope(isolate);
1184+
Local<Context> context = env()->context();
1185+
Context::Scope context_scope(context);
1186+
1187+
int32_t id = GetFrameID(frame);
1188+
1189+
nghttp2_extension ext = frame->ext;
1190+
nghttp2_ext_altsvc* altsvc = static_cast<nghttp2_ext_altsvc*>(ext.payload);
1191+
DEBUG_HTTP2SESSION(this, "handling altsvc frame");
1192+
1193+
Local<Value> argv[3] = {
1194+
Integer::New(isolate, id),
1195+
String::NewFromOneByte(isolate,
1196+
altsvc->origin,
1197+
v8::NewStringType::kNormal,
1198+
altsvc->origin_len).ToLocalChecked(),
1199+
String::NewFromOneByte(isolate,
1200+
altsvc->field_value,
1201+
v8::NewStringType::kNormal,
1202+
altsvc->field_value_len).ToLocalChecked(),
1203+
};
1204+
1205+
MakeCallback(env()->onaltsvc_string(), arraysize(argv), argv);
1206+
}
1207+
11711208
// Called by OnFrameReceived when a complete PING frame has been received.
11721209
inline void Http2Session::HandlePingFrame(const nghttp2_frame* frame) {
11731210
bool ack = frame->hd.flags & NGHTTP2_FLAG_ACK;
@@ -2477,6 +2514,44 @@ void Http2Stream::RefreshState(const FunctionCallbackInfo<Value>& args) {
24772514
}
24782515
}
24792516

2517+
void Http2Session::AltSvc(int32_t id,
2518+
uint8_t* origin,
2519+
size_t origin_len,
2520+
uint8_t* value,
2521+
size_t value_len) {
2522+
Http2Scope h2scope(this);
2523+
CHECK_EQ(nghttp2_submit_altsvc(session_, NGHTTP2_FLAG_NONE, id,
2524+
origin, origin_len, value, value_len), 0);
2525+
}
2526+
2527+
// Submits an AltSvc frame to the sent to the connected peer.
2528+
void Http2Session::AltSvc(const FunctionCallbackInfo<Value>& args) {
2529+
Environment* env = Environment::GetCurrent(args);
2530+
Http2Session* session;
2531+
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
2532+
2533+
int32_t id = args[0]->Int32Value(env->context()).ToChecked();
2534+
2535+
// origin and value are both required to be ASCII, handle them as such.
2536+
Local<String> origin_str = args[1]->ToString(env->context()).ToLocalChecked();
2537+
Local<String> value_str = args[2]->ToString(env->context()).ToLocalChecked();
2538+
2539+
size_t origin_len = origin_str->Length();
2540+
size_t value_len = value_str->Length();
2541+
2542+
CHECK_LE(origin_len + value_len, 16382); // Max permitted for ALTSVC
2543+
// Verify that origin len != 0 if stream id == 0, or
2544+
// that origin len == 0 if stream id != 0
2545+
CHECK((origin_len != 0 && id == 0) || (origin_len == 0 && id != 0));
2546+
2547+
MaybeStackBuffer<uint8_t> origin(origin_len);
2548+
MaybeStackBuffer<uint8_t> value(value_len);
2549+
origin_str->WriteOneByte(*origin);
2550+
value_str->WriteOneByte(*value);
2551+
2552+
session->AltSvc(id, *origin, origin_len, *value, value_len);
2553+
}
2554+
24802555
// Submits a PING frame to be sent to the connected peer.
24812556
void Http2Session::Ping(const FunctionCallbackInfo<Value>& args) {
24822557
Environment* env = Environment::GetCurrent(args);
@@ -2694,6 +2769,7 @@ void Initialize(Local<Object> target,
26942769
session->SetClassName(http2SessionClassName);
26952770
session->InstanceTemplate()->SetInternalFieldCount(1);
26962771
AsyncWrap::AddWrapMethods(env, session);
2772+
env->SetProtoMethod(session, "altsvc", Http2Session::AltSvc);
26972773
env->SetProtoMethod(session, "ping", Http2Session::Ping);
26982774
env->SetProtoMethod(session, "consume", Http2Session::Consume);
26992775
env->SetProtoMethod(session, "destroy", Http2Session::Destroy);

src/node_http2.h

+7
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,11 @@ class Http2Session : public AsyncWrap {
800800
void Consume(Local<External> external);
801801
void Unconsume();
802802
void Goaway(uint32_t code, int32_t lastStreamID, uint8_t* data, size_t len);
803+
void AltSvc(int32_t id,
804+
uint8_t* origin,
805+
size_t origin_len,
806+
uint8_t* value,
807+
size_t value_len);
803808

804809
bool Ping(v8::Local<v8::Function> function);
805810

@@ -877,6 +882,7 @@ class Http2Session : public AsyncWrap {
877882
static void UpdateChunksSent(const FunctionCallbackInfo<Value>& args);
878883
static void RefreshState(const FunctionCallbackInfo<Value>& args);
879884
static void Ping(const FunctionCallbackInfo<Value>& args);
885+
static void AltSvc(const FunctionCallbackInfo<Value>& args);
880886

881887
template <get_setting fn>
882888
static void RefreshSettings(const FunctionCallbackInfo<Value>& args);
@@ -921,6 +927,7 @@ class Http2Session : public AsyncWrap {
921927
inline void HandlePriorityFrame(const nghttp2_frame* frame);
922928
inline void HandleSettingsFrame(const nghttp2_frame* frame);
923929
inline void HandlePingFrame(const nghttp2_frame* frame);
930+
inline void HandleAltSvcFrame(const nghttp2_frame* frame);
924931

925932
// nghttp2 callbacks
926933
static inline int OnBeginHeadersCallback(

0 commit comments

Comments
 (0)