GeezerVison is showing github.com — quic: support multiple ALPN negotiation · nodejs/node@abb881e
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/quic.md +17-3Lines changed: 17 additions & 3 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1141,12 +1141,26 @@ added: v23.8.0 | 1141 | 1141 | #### `sessionOptions.alpn` | 1142 | 1142 |
| 1143 | 1143 | <!-- YAML | 1144 | | -added: v23.8.0 | | 1144 | +added: REPLACEME | 1145 | 1145 | --> | 1146 | 1146 |
| 1147 | | -* Type: {string} | | 1147 | +* Type: {string} (client) | {string\[]} (server) | | 1148 | + | | 1149 | +The ALPN (Application-Layer Protocol Negotiation) identifier(s). | | 1150 | + | | 1151 | +For **client** sessions, this is a single string specifying the protocol | | 1152 | +the client wants to use (e.g. `'h3'`). | | 1153 | + | | 1154 | +For **server** sessions, this is an array of protocol names in preference | | 1155 | +order that the server supports (e.g. `['h3', 'h3-29']`). During the TLS | | 1156 | +handshake, the server selects the first protocol from its list that the | | 1157 | +client also supports. | | 1158 | + | | 1159 | +The negotiated ALPN determines which Application implementation is used | | 1160 | +for the session. `'h3'` and `'h3-*'` variants select the HTTP/3 | | 1161 | +application; all other values select the default application. | 1148 | 1162 |
| 1149 | | -The ALPN protocol identifier. | | 1163 | +Default: `'h3'` | 1150 | 1164 |
| 1151 | 1165 | #### `sessionOptions.ca` (client only) | 1152 | 1166 |
| |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/quic/quic.js +35-15Lines changed: 35 additions & 15 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -33,7 +33,6 @@ let debug = require('internal/util/debuglog').debuglog('quic', (fn) => { | 33 | 33 |
| 34 | 34 | const { | 35 | 35 | Endpoint: Endpoint_, | 36 | | - Http3Application: Http3, | 37 | 36 | setCallbacks, | 38 | 37 |
| 39 | 38 | // The constants to be exposed to end users for various options. | @@ -115,7 +114,6 @@ const { | 115 | 114 | const kEmptyObject = { __proto__: null }; | 116 | 115 |
| 117 | 116 | const { | 118 | | - kApplicationProvider, | 119 | 117 | kBlocked, | 120 | 118 | kConnect, | 121 | 119 | kDatagram, | @@ -1787,7 +1785,6 @@ class QuicEndpoint { | 1787 | 1785 | * @param {{replace?: boolean}} [options] | 1788 | 1786 | */ | 1789 | 1787 | setSNIContexts(entries, options = kEmptyObject) { | 1790 | | - QuicEndpoint.#assertIsQuicEndpoint(this); | 1791 | 1788 | if (this.#handle === undefined) { | 1792 | 1789 | throw new ERR_INVALID_STATE('Endpoint is destroyed'); | 1793 | 1790 | } | @@ -2053,7 +2050,7 @@ function processIdentityOptions(identity, label) { | 2053 | 2050 | function processTlsOptions(tls, forServer) { | 2054 | 2051 | const { | 2055 | 2052 | servername, | 2056 | | - protocol, | | 2053 | + alpn, | 2057 | 2054 | ciphers = DEFAULT_CIPHERS, | 2058 | 2055 | groups = DEFAULT_GROUPS, | 2059 | 2056 | keylog = false, | @@ -2071,9 +2068,6 @@ function processTlsOptions(tls, forServer) { | 2071 | 2068 | if (servername !== undefined) { | 2072 | 2069 | validateString(servername, 'options.servername'); | 2073 | 2070 | } | 2074 | | - if (protocol !== undefined) { | 2075 | | - validateString(protocol, 'options.protocol'); | 2076 | | - } | 2077 | 2071 | if (ciphers !== undefined) { | 2078 | 2072 | validateString(ciphers, 'options.ciphers'); | 2079 | 2073 | } | @@ -2084,11 +2078,42 @@ function processTlsOptions(tls, forServer) { | 2084 | 2078 | validateBoolean(verifyClient, 'options.verifyClient'); | 2085 | 2079 | validateBoolean(tlsTrace, 'options.tlsTrace'); | 2086 | 2080 |
| | 2081 | + // Encode the ALPN option to wire format (length-prefixed protocol names). | | 2082 | + // Server: array of protocol names. Client: single protocol name. | | 2083 | + // If not specified, the C++ default (h3) is used. | | 2084 | + let encodedAlpn; | | 2085 | + if (alpn !== undefined) { | | 2086 | + const protocols = forServer ? | | 2087 | + (ArrayIsArray(alpn) ? alpn : [alpn]) : | | 2088 | + [alpn]; | | 2089 | + if (!forServer) { | | 2090 | + validateString(alpn, 'options.alpn'); | | 2091 | + } | | 2092 | + let totalLen = 0; | | 2093 | + for (let i = 0; i < protocols.length; i++) { | | 2094 | + validateString(protocols[i], `options.alpn[${i}]`); | | 2095 | + if (protocols[i].length === 0 || protocols[i].length > 255) { | | 2096 | + throw new ERR_INVALID_ARG_VALUE(`options.alpn[${i}]`, protocols[i], | | 2097 | + 'must be between 1 and 255 characters'); | | 2098 | + } | | 2099 | + totalLen += 1 + protocols[i].length; | | 2100 | + } | | 2101 | + // Build wire format: [len1][name1][len2][name2]... | | 2102 | + const buf = Buffer.allocUnsafe(totalLen); | | 2103 | + let offset = 0; | | 2104 | + for (let i = 0; i < protocols.length; i++) { | | 2105 | + buf[offset++] = protocols[i].length; | | 2106 | + buf.write(protocols[i], offset, 'ascii'); | | 2107 | + offset += protocols[i].length; | | 2108 | + } | | 2109 | + encodedAlpn = buf.toString('latin1'); | | 2110 | + } | | 2111 | + | 2087 | 2112 | // Shared TLS options (same for all identities on the endpoint). | 2088 | 2113 | const shared = { | 2089 | 2114 | __proto__: null, | 2090 | 2115 | servername, | 2091 | | - protocol, | | 2116 | + alpn: encodedAlpn, | 2092 | 2117 | ciphers, | 2093 | 2118 | groups, | 2094 | 2119 | keylog, | @@ -2191,13 +2216,8 @@ function processSessionOptions(options, forServer = false) { | 2191 | 2216 | maxStreamWindow, | 2192 | 2217 | maxWindow, | 2193 | 2218 | cc, | 2194 | | - [kApplicationProvider]: provider, | 2195 | 2219 | } = options; | 2196 | 2220 |
| 2197 | | - if (provider !== undefined) { | 2198 | | - validateObject(provider, 'options[kApplicationProvider]'); | 2199 | | - } | 2200 | | - | 2201 | 2221 | if (cc !== undefined) { | 2202 | 2222 | validateString(cc, 'options.cc'); | 2203 | 2223 | if (cc !== 'reno' || cc !== 'bbr' || cc !== 'cubic') { | @@ -2226,7 +2246,6 @@ function processSessionOptions(options, forServer = false) { | 2226 | 2246 | maxStreamWindow, | 2227 | 2247 | maxWindow, | 2228 | 2248 | sessionTicket, | 2229 | | - provider, | 2230 | 2249 | cc, | 2231 | 2250 | }; | 2232 | 2251 | } | @@ -2328,7 +2347,8 @@ module.exports = { | 2328 | 2347 | QuicEndpoint, | 2329 | 2348 | QuicSession, | 2330 | 2349 | QuicStream, | 2331 | | - Http3, | | 2350 | + DEFAULT_CIPHERS, | | 2351 | + DEFAULT_GROUPS, | 2332 | 2352 | }; | 2333 | 2353 |
| 2334 | 2354 | ObjectDefineProperties(module.exports, { | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/quic/state.js +9Lines changed: 9 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -62,6 +62,7 @@ const { | 62 | 62 | IDX_STATE_SESSION_STREAM_OPEN_ALLOWED, | 63 | 63 | IDX_STATE_SESSION_PRIORITY_SUPPORTED, | 64 | 64 | IDX_STATE_SESSION_WRAPPED, | | 65 | + IDX_STATE_SESSION_APPLICATION_TYPE, | 65 | 66 | IDX_STATE_SESSION_LAST_DATAGRAM_ID, | 66 | 67 |
| 67 | 68 | IDX_STATE_ENDPOINT_BOUND, | @@ -99,6 +100,7 @@ assert(IDX_STATE_SESSION_HANDSHAKE_CONFIRMED !== undefined); | 99 | 100 | assert(IDX_STATE_SESSION_STREAM_OPEN_ALLOWED !== undefined); | 100 | 101 | assert(IDX_STATE_SESSION_PRIORITY_SUPPORTED !== undefined); | 101 | 102 | assert(IDX_STATE_SESSION_WRAPPED !== undefined); | | 103 | +assert(IDX_STATE_SESSION_APPLICATION_TYPE !== undefined); | 102 | 104 | assert(IDX_STATE_SESSION_LAST_DATAGRAM_ID !== undefined); | 103 | 105 | assert(IDX_STATE_ENDPOINT_BOUND !== undefined); | 104 | 106 | assert(IDX_STATE_ENDPOINT_RECEIVING !== undefined); | @@ -347,6 +349,12 @@ class QuicSessionState { | 347 | 349 | return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_WRAPPED); | 348 | 350 | } | 349 | 351 |
| | 352 | + /** @type {number} */ | | 353 | + get applicationType() { | | 354 | + if (this.#handle.byteLength === 0) return undefined; | | 355 | + return DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_APPLICATION_TYPE); | | 356 | + } | | 357 | + | 350 | 358 | /** @type {bigint} */ | 351 | 359 | get lastDatagramId() { | 352 | 360 | if (this.#handle.byteLength === 0) return undefined; | @@ -407,6 +415,7 @@ class QuicSessionState { | 407 | 415 | isStreamOpenAllowed: this.isStreamOpenAllowed, | 408 | 416 | isPrioritySupported: this.isPrioritySupported, | 409 | 417 | isWrapped: this.isWrapped, | | 418 | + applicationType: this.applicationType, | 410 | 419 | lastDatagramId: this.lastDatagramId, | 411 | 420 | }, opts)}`; | 412 | 421 | } | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/quic/symbols.js -2Lines changed: 0 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -23,7 +23,6 @@ const { | 23 | 23 | // Symbols used to hide various private properties and methods from the | 24 | 24 | // public API. | 25 | 25 |
| 26 | | -const kApplicationProvider = Symbol('kApplicationProvider'); | 27 | 26 | const kBlocked = Symbol('kBlocked'); | 28 | 27 | const kConnect = Symbol('kConnect'); | 29 | 28 | const kDatagram = Symbol('kDatagram'); | @@ -51,7 +50,6 @@ const kWantsHeaders = Symbol('kWantsHeaders'); | 51 | 50 | const kWantsTrailers = Symbol('kWantsTrailers'); | 52 | 51 |
| 53 | 52 | module.exports = { | 54 | | - kApplicationProvider, | 55 | 53 | kBlocked, | 56 | 54 | kConnect, | 57 | 55 | kDatagram, | |
Collapse file Copy file name to clipboardExpand all lines: src/quic/application.cc +7-8Lines changed: 7 additions & 8 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -464,6 +464,10 @@ class DefaultApplication final : public Session::Application { | 464 | 464 | // of the namespace. | 465 | 465 | using Application::Application; // NOLINT | 466 | 466 |
| | 467 | + Session::Application::Type type() const override { | | 468 | + return Session::Application::Type::DEFAULT; | | 469 | + } | | 470 | + | 467 | 471 | error_code GetNoErrorCode() const override { return 0; } | 468 | 472 |
| 469 | 473 | bool ReceiveStreamData(int64_t stream_id, | @@ -601,14 +605,9 @@ class DefaultApplication final : public Session::Application { | 601 | 605 | Stream::Queue stream_queue_; | 602 | 606 | }; | 603 | 607 |
| 604 | | -std::unique_ptr<Session::Application> Session::SelectApplication( | 605 | | - Session* session, const Config& config) { | 606 | | - if (config.options.application_provider) { | 607 | | - return config.options.application_provider->Create(session); | 608 | | - } | 609 | | - | 610 | | - return std::make_unique<DefaultApplication>(session, | 611 | | - Application_Options::kDefault); | | 608 | +std::unique_ptr<Session::Application> CreateDefaultApplication( | | 609 | + Session* session, const Session::Application_Options& options) { | | 610 | + return std::make_unique<DefaultApplication>(session, options); | 612 | 611 | } | 613 | 612 |
| 614 | 613 | } // namespace quic | |
Collapse file Copy file name to clipboardExpand all lines: src/quic/application.h +14Lines changed: 14 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -17,9 +17,19 @@ class Session::Application : public MemoryRetainer { | 17 | 17 | public: | 18 | 18 | using Options = Session::Application_Options; | 19 | 19 |
| | 20 | + // The type of Application, exposed via the session state so JS | | 21 | + // can observe which Application was selected after ALPN negotiation. | | 22 | + enum class Type : uint8_t { | | 23 | + NONE = 0, // Not yet selected (server pre-negotiation) | | 24 | + DEFAULT = 1, // DefaultApplication (non-h3 ALPN) | | 25 | + HTTP3 = 2, // Http3ApplicationImpl (h3 / h3-XX ALPN) | | 26 | + }; | | 27 | + | 20 | 28 | Application(Session* session, const Options& options); | 21 | 29 | DISALLOW_COPY_AND_MOVE(Application) | 22 | 30 |
| | 31 | + virtual Type type() const = 0; | | 32 | + | 23 | 33 | virtual bool Start(); | 24 | 34 |
| 25 | 35 | virtual error_code GetNoErrorCode() const = 0; | @@ -169,6 +179,10 @@ struct Session::Application::StreamData final { | 169 | 179 | std::string ToString() const; | 170 | 180 | }; | 171 | 181 |
| | 182 | +// Create a DefaultApplication for the given session. | | 183 | +std::unique_ptr<Session::Application> CreateDefaultApplication( | | 184 | + Session* session, const Session::Application_Options& options); | | 185 | + | 172 | 186 | } // namespace node::quic | 173 | 187 |
| 174 | 188 | #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS |
Collapse file Copy file name to clipboardExpand all lines: src/quic/bindingdata.h +2-4Lines changed: 2 additions & 4 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -24,7 +24,6 @@ class Packet; | 24 | 24 | // The FunctionTemplates the BindingData will store for us. | 25 | 25 | #define QUIC_CONSTRUCTORS(V) \ | 26 | 26 | V(endpoint) \ | 27 | | - V(http3application) \ | 28 | 27 | V(logstream) \ | 29 | 28 | V(session) \ | 30 | 29 | V(stream) \ | @@ -59,7 +58,7 @@ class Packet; | 59 | 58 | V(ack_delay_exponent, "ackDelayExponent") \ | 60 | 59 | V(active_connection_id_limit, "activeConnectionIDLimit") \ | 61 | 60 | V(address_lru_size, "addressLRUSize") \ | 62 | | - V(application_provider, "provider") \ | | 61 | + V(application, "application") \ | 63 | 62 | V(bbr, "bbr") \ | 64 | 63 | V(ca, "ca") \ | 65 | 64 | V(cc_algorithm, "cc") \ | @@ -78,7 +77,6 @@ class Packet; | 78 | 77 | V(groups, "groups") \ | 79 | 78 | V(handshake_timeout, "handshakeTimeout") \ | 80 | 79 | V(http3_alpn, &NGHTTP3_ALPN_H3[1]) \ | 81 | | - V(http3application, "Http3Application") \ | 82 | 80 | V(initial_max_data, "initialMaxData") \ | 83 | 81 | V(initial_max_stream_data_bidi_local, "initialMaxStreamDataBidiLocal") \ | 84 | 82 | V(initial_max_stream_data_bidi_remote, "initialMaxStreamDataBidiRemote") \ | @@ -105,7 +103,7 @@ class Packet; | 105 | 103 | V(max_window, "maxWindow") \ | 106 | 104 | V(min_version, "minVersion") \ | 107 | 105 | V(preferred_address_strategy, "preferredAddressPolicy") \ | 108 | | - V(protocol, "protocol") \ | | 106 | + V(alpn, "alpn") \ | 109 | 107 | V(qlog, "qlog") \ | 110 | 108 | V(qpack_blocked_streams, "qpackBlockedStreams") \ | 111 | 109 | V(qpack_encoder_max_dtable_capacity, "qpackEncoderMaxDTableCapacity") \ | |
Collapse file Copy file name to clipboardExpand all lines: src/quic/endpoint.cc +3-6Lines changed: 3 additions & 6 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -92,7 +92,7 @@ bool is_diagnostic_packet_loss(double probability) { | 92 | 92 | return (static_cast<double>(c) / 255) < probability; | 93 | 93 | } | 94 | 94 |
| 95 | | -template <typename Opt, double Opt::*member> | | 95 | +template <typename Opt, double Opt::* member> | 96 | 96 | bool SetOption(Environment* env, | 97 | 97 | Opt* options, | 98 | 98 | const Local<Object>& object, | @@ -113,7 +113,7 @@ bool SetOption(Environment* env, | 113 | 113 | } | 114 | 114 | #endif // DEBUG | 115 | 115 |
| 116 | | -template <typename Opt, uint8_t Opt::*member> | | 116 | +template <typename Opt, uint8_t Opt::* member> | 117 | 117 | bool SetOption(Environment* env, | 118 | 118 | Opt* options, | 119 | 119 | const Local<Object>& object, | @@ -140,7 +140,7 @@ bool SetOption(Environment* env, | 140 | 140 | return true; | 141 | 141 | } | 142 | 142 |
| 143 | | -template <typename Opt, TokenSecret Opt::*member> | | 143 | +template <typename Opt, TokenSecret Opt::* member> | 144 | 144 | bool SetOption(Environment* env, | 145 | 145 | Opt* options, | 146 | 146 | const Local<Object>& object, | @@ -511,7 +511,6 @@ JS_CONSTRUCTOR_IMPL(Endpoint, endpoint_constructor_template, { | 511 | 511 |
| 512 | 512 | void Endpoint::InitPerIsolate(IsolateData* data, Local<ObjectTemplate> target) { | 513 | 513 | // TODO(@jasnell): Implement the per-isolate state | 514 | | - Http3Application::InitPerIsolate(data, target); | 515 | 514 | } | 516 | 515 |
| 517 | 516 | void Endpoint::InitPerContext(Realm* realm, Local<Object> target) { | @@ -567,8 +566,6 @@ void Endpoint::InitPerContext(Realm* realm, Local<Object> target) { | 567 | 566 | NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_SEND_FAILURE); | 568 | 567 | NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_START_FAILURE); | 569 | 568 |
| 570 | | - Http3Application::InitPerContext(realm, target); | 571 | | - | 572 | 569 | SetConstructorFunction(realm->context(), | 573 | 570 | target, | 574 | 571 | "Endpoint", | |
|