GeezerVison is showing github.com — http2: add altsvc support · nodejs/node@1a24fec
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/errors.md +10Lines changed: 10 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -643,6 +643,16 @@ that. | 643 | 643 |
| 644 | 644 | Occurs with multiple attempts to shutdown an HTTP/2 session. | 645 | 645 |
| | 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 | + | 646 | 656 | <a id="ERR_HTTP2_CONNECT_AUTHORITY"></a> | 647 | 657 | ### ERR_HTTP2_CONNECT_AUTHORITY | 648 | 658 |
| |
Collapse file Copy file name to clipboardExpand all lines: doc/api/http2.md +94Lines changed: 94 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -570,11 +570,103 @@ added: REPLACEME | 570 | 570 | Calls [`unref()`][`net.Socket.prototype.unref`] on this `Http2Session` | 571 | 571 | instance's underlying [`net.Socket`]. | 572 | 572 |
| | 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 | + | 573 | 644 | ### Class: ClientHttp2Session | 574 | 645 | <!-- YAML | 575 | 646 | added: v8.4.0 | 576 | 647 | --> | 577 | 648 |
| | 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 | + | 578 | 670 | #### clienthttp2session.request(headers[, options]) | 579 | 671 | <!-- YAML | 580 | 672 | added: v8.4.0 | @@ -2869,6 +2961,7 @@ following additional properties: | 2869 | 2961 |
| 2870 | 2962 |
| 2871 | 2963 | [ALPN negotiation]: #http2_alpn_negotiation | | 2964 | +[ALPN Protocol ID]: https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids | 2872 | 2965 | [Compatibility API]: #http2_compatibility_api | 2873 | 2966 | [HTTP/1]: http.html | 2874 | 2967 | [HTTP/2]: https://tools.ietf.org/html/rfc7540 | @@ -2877,6 +2970,7 @@ following additional properties: | 2877 | 2970 | [Http2Session and Sockets]: #http2_http2session_and_sockets | 2878 | 2971 | [Performance Observer]: perf_hooks.html | 2879 | 2972 | [Readable Stream]: stream.html#stream_class_stream_readable | | 2973 | +[RFC 7838]: https://tools.ietf.org/html/rfc7838 | 2880 | 2974 | [Settings Object]: #http2_settings_object | 2881 | 2975 | [Using options.selectPadding]: #http2_using_options_selectpadding | 2882 | 2976 | [Writable Stream]: stream.html#stream_writable_streams | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/errors.js +6Lines changed: 6 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -152,6 +152,10 @@ E('ERR_ENCODING_NOT_SUPPORTED', | 152 | 152 | E('ERR_FALSY_VALUE_REJECTION', 'Promise was rejected with falsy value'); | 153 | 153 | E('ERR_HTTP2_ALREADY_SHUTDOWN', | 154 | 154 | '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'); | 155 | 159 | E('ERR_HTTP2_CONNECT_AUTHORITY', | 156 | 160 | ':authority header is required for CONNECT requests'); | 157 | 161 | E('ERR_HTTP2_CONNECT_PATH', | @@ -233,6 +237,7 @@ E('ERR_INVALID_ARRAY_LENGTH', | 233 | 237 | }); | 234 | 238 | E('ERR_INVALID_ASYNC_ID', (type, id) => `Invalid ${type} value: ${id}`); | 235 | 239 | E('ERR_INVALID_CALLBACK', 'callback must be a function'); | | 240 | +E('ERR_INVALID_CHAR', 'Invalid character in %s'); | 236 | 241 | E('ERR_INVALID_FD', (fd) => `"fd" must be a positive integer: ${fd}`); | 237 | 242 | E('ERR_INVALID_FILE_URL_HOST', 'File URL host %s'); | 238 | 243 | E('ERR_INVALID_FILE_URL_PATH', 'File URL path %s'); | @@ -276,6 +281,7 @@ E('ERR_NAPI_INVALID_TYPEDARRAY_ALIGNMENT', 'start offset of %s should be a ' + | 276 | 281 | E('ERR_NAPI_INVALID_TYPEDARRAY_LENGTH', 'Invalid typed array length'); | 277 | 282 | E('ERR_NO_CRYPTO', 'Node.js is not compiled with OpenSSL crypto support'); | 278 | 283 | E('ERR_NO_ICU', '%s is not supported on Node.js compiled without ICU'); | | 284 | +E('ERR_OUT_OF_RANGE', 'The "%s" argument is out of range'); | 279 | 285 | E('ERR_PARSE_HISTORY_DATA', 'Could not parse history data in %s'); | 280 | 286 | E('ERR_REQUIRE_ESM', 'Must use import to load ES Module: %s'); | 281 | 287 | E('ERR_SOCKET_ALREADY_BOUND', 'Socket is already bound'); | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/http2/core.js +62Lines changed: 62 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -32,6 +32,9 @@ const kMaxFrameSize = (2 ** 24) - 1; | 32 | 32 | const kMaxInt = (2 ** 32) - 1; | 33 | 33 | const kMaxStreams = (2 ** 31) - 1; | 34 | 34 |
| | 35 | +// eslint-disable-next-line no-control-regex | | 36 | +const kQuotedString = /^[\x09\x20-\x5b\x5d-\x7e\x80-\xff]*$/; | | 37 | + | 35 | 38 | const { | 36 | 39 | assertIsObject, | 37 | 40 | assertValidPseudoHeaderResponse, | @@ -362,6 +365,16 @@ function onFrameError(id, type, code) { | 362 | 365 | process.nextTick(emit, emitter, 'frameError', type, code, id); | 363 | 366 | } | 364 | 367 |
| | 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 | + | 365 | 378 | // Receiving a GOAWAY frame from the connected peer is a signal that no | 366 | 379 | // new streams should be created. If the code === NGHTTP2_NO_ERROR, we | 367 | 380 | // are going to send our our close, but allow existing frames to close | @@ -704,6 +717,7 @@ function setupHandle(socket, type, options) { | 704 | 717 | handle.onheaders = onSessionHeaders; | 705 | 718 | handle.onframeerror = onFrameError; | 706 | 719 | handle.ongoawaydata = onGoawayData; | | 720 | + handle.onaltsvc = onAltSvc; | 707 | 721 |
| 708 | 722 | if (typeof options.selectPadding === 'function') | 709 | 723 | handle.ongetpadding = onSelectPadding(options.selectPadding); | @@ -1150,6 +1164,54 @@ class ServerHttp2Session extends Http2Session { | 1150 | 1164 | get server() { | 1151 | 1165 | return this[kServer]; | 1152 | 1166 | } | | 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 | + } | 1153 | 1215 | } | 1154 | 1216 |
| 1155 | 1217 | // ClientHttp2Session instances have to wait for the socket to connect after | |
Collapse file Copy file name to clipboardExpand all lines: src/env.h +1Lines changed: 1 addition & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -201,6 +201,7 @@ class ModuleWrap; | 201 | 201 | V(nsname_string, "nsname") \ | 202 | 202 | V(nexttick_string, "nextTick") \ | 203 | 203 | V(ocsp_request_string, "OCSPRequest") \ | | 204 | + V(onaltsvc_string, "onaltsvc") \ | 204 | 205 | V(onchange_string, "onchange") \ | 205 | 206 | V(onclienthello_string, "onclienthello") \ | 206 | 207 | V(oncomplete_string, "oncomplete") \ | |
Collapse file Copy file name to clipboardExpand all lines: src/node_http2.cc +76Lines changed: 76 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -103,6 +103,11 @@ Http2Options::Http2Options(Environment* env) { | 103 | 103 | // are required to buffer. | 104 | 104 | nghttp2_option_set_no_auto_window_update(options_, 1); | 105 | 105 |
| | 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 | + | 106 | 111 | AliasedBuffer<uint32_t, v8::Uint32Array>& buffer = | 107 | 112 | env->http2_state()->options_buffer; | 108 | 113 | uint32_t flags = buffer[IDX_OPTIONS_FLAGS]; | @@ -848,6 +853,10 @@ inline int Http2Session::OnFrameReceive(nghttp2_session* handle, | 848 | 853 | break; | 849 | 854 | case NGHTTP2_PING: | 850 | 855 | session->HandlePingFrame(frame); | | 856 | + break; | | 857 | + case NGHTTP2_ALTSVC: | | 858 | + session->HandleAltSvcFrame(frame); | | 859 | + break; | 851 | 860 | default: | 852 | 861 | break; | 853 | 862 | } | @@ -1186,6 +1195,34 @@ inline void Http2Session::HandleGoawayFrame(const nghttp2_frame* frame) { | 1186 | 1195 | MakeCallback(env()->ongoawaydata_string(), arraysize(argv), argv); | 1187 | 1196 | } | 1188 | 1197 |
| | 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 | + | 1189 | 1226 | // Called by OnFrameReceived when a complete PING frame has been received. | 1190 | 1227 | inline void Http2Session::HandlePingFrame(const nghttp2_frame* frame) { | 1191 | 1228 | bool ack = frame->hd.flags & NGHTTP2_FLAG_ACK; | @@ -2495,6 +2532,44 @@ void Http2Stream::RefreshState(const FunctionCallbackInfo<Value>& args) { | 2495 | 2532 | } | 2496 | 2533 | } | 2497 | 2534 |
| | 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 | + | 2498 | 2573 | // Submits a PING frame to be sent to the connected peer. | 2499 | 2574 | void Http2Session::Ping(const FunctionCallbackInfo<Value>& args) { | 2500 | 2575 | Environment* env = Environment::GetCurrent(args); | @@ -2712,6 +2787,7 @@ void Initialize(Local<Object> target, | 2712 | 2787 | session->SetClassName(http2SessionClassName); | 2713 | 2788 | session->InstanceTemplate()->SetInternalFieldCount(1); | 2714 | 2789 | AsyncWrap::AddWrapMethods(env, session); | | 2790 | + env->SetProtoMethod(session, "altsvc", Http2Session::AltSvc); | 2715 | 2791 | env->SetProtoMethod(session, "ping", Http2Session::Ping); | 2716 | 2792 | env->SetProtoMethod(session, "consume", Http2Session::Consume); | 2717 | 2793 | env->SetProtoMethod(session, "destroy", Http2Session::Destroy); | |
|