GeezerVison is showing github.com — tls: add ALPNCallback server option for dynamic ALPN negotiation · nodejs/node@b5d16cd
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 +14Lines changed: 14 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -2708,6 +2708,20 @@ This error represents a failed test. Additional information about the failure | 2708 | 2708 | is available via the `cause` property. The `failureType` property specifies | 2709 | 2709 | what the test was doing when the failure occurred. | 2710 | 2710 |
| | 2711 | +<a id="ERR_TLS_ALPN_CALLBACK_INVALID_RESULT"></a> | | 2712 | + | | 2713 | +### `ERR_TLS_ALPN_CALLBACK_INVALID_RESULT` | | 2714 | + | | 2715 | +This error is thrown when an `ALPNCallback` returns a value that is not in the | | 2716 | +list of ALPN protocols offered by the client. | | 2717 | + | | 2718 | +<a id="ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS"></a> | | 2719 | + | | 2720 | +### `ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS` | | 2721 | + | | 2722 | +This error is thrown when creating a `TLSServer` if the TLS options include | | 2723 | +both `ALPNProtocols` and `ALPNCallback`. These options are mutually exclusive. | | 2724 | + | 2711 | 2725 | <a id="ERR_TLS_CERT_ALTNAME_FORMAT"></a> | 2712 | 2726 |
| 2713 | 2727 | ### `ERR_TLS_CERT_ALTNAME_FORMAT` | |
Collapse file Copy file name to clipboardExpand all lines: doc/api/tls.md +14Lines changed: 14 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -2045,6 +2045,9 @@ where `secureSocket` has the same API as `pair.cleartext`. | 2045 | 2045 | <!-- YAML | 2046 | 2046 | added: v0.3.2 | 2047 | 2047 | changes: | | 2048 | + - version: REPLACEME | | 2049 | + pr-url: https://github.com/nodejs/node/pull/45190 | | 2050 | + description: The `options` parameter can now include `ALPNCallback`. | 2048 | 2051 | - version: v12.3.0 | 2049 | 2052 | pr-url: https://github.com/nodejs/node/pull/27665 | 2050 | 2053 | description: The `options` parameter now supports `net.createServer()` | @@ -2070,6 +2073,17 @@ changes: | 2070 | 2073 | e.g. `0x05hello0x05world`, where the first byte is the length of the next | 2071 | 2074 | protocol name. Passing an array is usually much simpler, e.g. | 2072 | 2075 | `['hello', 'world']`. (Protocols should be ordered by their priority.) | | 2076 | + * `ALPNCallback`: {Function} If set, this will be called when a | | 2077 | + client opens a connection using the ALPN extension. One argument will | | 2078 | + be passed to the callback: an object containing `servername` and | | 2079 | + `protocols` fields, respectively containing the server name from | | 2080 | + the SNI extension (if any) and an array of ALPN protocol name strings. The | | 2081 | + callback must return either one of the strings listed in | | 2082 | + `protocols`, which will be returned to the client as the selected | | 2083 | + ALPN protocol, or `undefined`, to reject the connection with a fatal alert. | | 2084 | + If a string is returned that does not match one of the client's ALPN | | 2085 | + protocols, an error will be thrown. This option cannot be used with the | | 2086 | + `ALPNProtocols` option, and setting both options will throw an error. | 2073 | 2087 | * `clientCertEngine` {string} Name of an OpenSSL engine which can provide the | 2074 | 2088 | client certificate. | 2075 | 2089 | * `enableTrace` {boolean} If `true`, [`tls.TLSSocket.enableTrace()`][] will be | |
Collapse file Copy file name to clipboardExpand all lines: lib/_tls_wrap.js +59Lines changed: 59 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -72,6 +72,8 @@ const { | 72 | 72 | ERR_INVALID_ARG_VALUE, | 73 | 73 | ERR_MULTIPLE_CALLBACK, | 74 | 74 | ERR_SOCKET_CLOSED, | | 75 | + ERR_TLS_ALPN_CALLBACK_INVALID_RESULT, | | 76 | + ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS, | 75 | 77 | ERR_TLS_DH_PARAM_SIZE, | 76 | 78 | ERR_TLS_HANDSHAKE_TIMEOUT, | 77 | 79 | ERR_TLS_INVALID_CONTEXT, | @@ -108,6 +110,7 @@ const kErrorEmitted = Symbol('error-emitted'); | 108 | 110 | const kHandshakeTimeout = Symbol('handshake-timeout'); | 109 | 111 | const kRes = Symbol('res'); | 110 | 112 | const kSNICallback = Symbol('snicallback'); | | 113 | +const kALPNCallback = Symbol('alpncallback'); | 111 | 114 | const kEnableTrace = Symbol('enableTrace'); | 112 | 115 | const kPskCallback = Symbol('pskcallback'); | 113 | 116 | const kPskIdentityHint = Symbol('pskidentityhint'); | @@ -234,6 +237,45 @@ function loadSNI(info) { | 234 | 237 | } | 235 | 238 |
| 236 | 239 |
| | 240 | +function callALPNCallback(protocolsBuffer) { | | 241 | + const handle = this; | | 242 | + const socket = handle[owner_symbol]; | | 243 | + | | 244 | + const servername = handle.getServername(); | | 245 | + | | 246 | + // Collect all the protocols from the given buffer: | | 247 | + const protocols = []; | | 248 | + let offset = 0; | | 249 | + while (offset < protocolsBuffer.length) { | | 250 | + const protocolLen = protocolsBuffer[offset]; | | 251 | + offset += 1; | | 252 | + | | 253 | + const protocol = protocolsBuffer.slice(offset, offset + protocolLen); | | 254 | + offset += protocolLen; | | 255 | + | | 256 | + protocols.push(protocol.toString('ascii')); | | 257 | + } | | 258 | + | | 259 | + const selectedProtocol = socket[kALPNCallback]({ | | 260 | + servername, | | 261 | + protocols, | | 262 | + }); | | 263 | + | | 264 | + // Undefined -> all proposed protocols rejected | | 265 | + if (selectedProtocol === undefined) return undefined; | | 266 | + | | 267 | + const protocolIndex = protocols.indexOf(selectedProtocol); | | 268 | + if (protocolIndex === -1) { | | 269 | + throw new ERR_TLS_ALPN_CALLBACK_INVALID_RESULT(selectedProtocol, protocols); | | 270 | + } | | 271 | + let protocolOffset = 0; | | 272 | + for (let i = 0; i < protocolIndex; i++) { | | 273 | + protocolOffset += 1 + protocols[i].length; | | 274 | + } | | 275 | + | | 276 | + return protocolOffset; | | 277 | +} | | 278 | + | 237 | 279 | function requestOCSP(socket, info) { | 238 | 280 | if (!info.OCSPRequest || !socket.server) | 239 | 281 | return requestOCSPDone(socket); | @@ -493,6 +535,7 @@ function TLSSocket(socket, opts) { | 493 | 535 | this._controlReleased = false; | 494 | 536 | this.secureConnecting = true; | 495 | 537 | this._SNICallback = null; | | 538 | + this[kALPNCallback] = null; | 496 | 539 | this.servername = null; | 497 | 540 | this.alpnProtocol = null; | 498 | 541 | this.authorized = false; | @@ -787,6 +830,16 @@ TLSSocket.prototype._init = function(socket, wrap) { | 787 | 830 | ssl.lastHandshakeTime = 0; | 788 | 831 | ssl.handshakes = 0; | 789 | 832 |
| | 833 | + if (options.ALPNCallback) { | | 834 | + if (typeof options.ALPNCallback !== 'function') { | | 835 | + throw new ERR_INVALID_ARG_TYPE('options.ALPNCallback', 'Function', options.ALPNCallback); | | 836 | + } | | 837 | + assert(typeof options.ALPNCallback === 'function'); | | 838 | + this[kALPNCallback] = options.ALPNCallback; | | 839 | + ssl.ALPNCallback = callALPNCallback; | | 840 | + ssl.enableALPNCb(); | | 841 | + } | | 842 | + | 790 | 843 | if (this.server) { | 791 | 844 | if (this.server.listenerCount('resumeSession') > 0 || | 792 | 845 | this.server.listenerCount('newSession') > 0) { | @@ -1165,6 +1218,7 @@ function tlsConnectionListener(rawSocket) { | 1165 | 1218 | rejectUnauthorized: this.rejectUnauthorized, | 1166 | 1219 | handshakeTimeout: this[kHandshakeTimeout], | 1167 | 1220 | ALPNProtocols: this.ALPNProtocols, | | 1221 | + ALPNCallback: this.ALPNCallback, | 1168 | 1222 | SNICallback: this[kSNICallback] || SNICallback, | 1169 | 1223 | enableTrace: this[kEnableTrace], | 1170 | 1224 | pauseOnConnect: this.pauseOnConnect, | @@ -1264,6 +1318,11 @@ function Server(options, listener) { | 1264 | 1318 | this.requestCert = options.requestCert === true; | 1265 | 1319 | this.rejectUnauthorized = options.rejectUnauthorized !== false; | 1266 | 1320 |
| | 1321 | + this.ALPNCallback = options.ALPNCallback; | | 1322 | + if (this.ALPNCallback && options.ALPNProtocols) { | | 1323 | + throw new ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS(); | | 1324 | + } | | 1325 | + | 1267 | 1326 | if (options.sessionTimeout) | 1268 | 1327 | this.sessionTimeout = options.sessionTimeout; | 1269 | 1328 |
| |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/errors.js +10Lines changed: 10 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -1622,6 +1622,16 @@ E('ERR_TEST_FAILURE', function(error, failureType) { | 1622 | 1622 | this.cause = error; | 1623 | 1623 | return msg; | 1624 | 1624 | }, Error); | | 1625 | +E('ERR_TLS_ALPN_CALLBACK_INVALID_RESULT', (value, protocols) => { | | 1626 | + return `ALPN callback returned a value (${ | | 1627 | + value | | 1628 | + }) that did not match any of the client's offered protocols (${ | | 1629 | + protocols.join(', ') | | 1630 | + })`; | | 1631 | +}, TypeError); | | 1632 | +E('ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS', | | 1633 | + 'The ALPNCallback and ALPNProtocols TLS options are mutually exclusive', | | 1634 | + TypeError); | 1625 | 1635 | E('ERR_TLS_CERT_ALTNAME_FORMAT', 'Invalid subject alternative name string', | 1626 | 1636 | SyntaxError); | 1627 | 1637 | E('ERR_TLS_CERT_ALTNAME_INVALID', function(reason, host, cert) { | |
Collapse file Copy file name to clipboardExpand all lines: src/crypto/crypto_tls.cc +49Lines changed: 49 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -224,6 +224,44 @@ int SelectALPNCallback( | 224 | 224 | unsigned int inlen, | 225 | 225 | void* arg) { | 226 | 226 | TLSWrap* w = static_cast<TLSWrap*>(arg); | | 227 | + if (w->alpn_callback_enabled_) { | | 228 | + Environment* env = w->env(); | | 229 | + HandleScope handle_scope(env->isolate()); | | 230 | + | | 231 | + Local<Value> callback_arg = | | 232 | + Buffer::Copy(env, reinterpret_cast<const char*>(in), inlen) | | 233 | + .ToLocalChecked(); | | 234 | + | | 235 | + MaybeLocal<Value> maybe_callback_result = | | 236 | + w->MakeCallback(env->alpn_callback_string(), 1, &callback_arg); | | 237 | + | | 238 | + if (UNLIKELY(maybe_callback_result.IsEmpty())) { | | 239 | + // Implies the callback didn't return, because some exception was thrown | | 240 | + // during processing, e.g. if callback returned an invalid ALPN value. | | 241 | + return SSL_TLSEXT_ERR_ALERT_FATAL; | | 242 | + } | | 243 | + | | 244 | + Local<Value> callback_result = maybe_callback_result.ToLocalChecked(); | | 245 | + | | 246 | + if (callback_result->IsUndefined()) { | | 247 | + // If you set an ALPN callback, but you return undefined for an ALPN | | 248 | + // request, you're rejecting all proposed ALPN protocols, and so we send | | 249 | + // a fatal alert: | | 250 | + return SSL_TLSEXT_ERR_ALERT_FATAL; | | 251 | + } | | 252 | + | | 253 | + CHECK(callback_result->IsNumber()); | | 254 | + unsigned int result_int = callback_result.As<v8::Number>()->Value(); | | 255 | + | | 256 | + // The callback returns an offset into the given buffer, for the selected | | 257 | + // protocol that should be returned. We then set outlen & out to point | | 258 | + // to the selected input length & value directly: | | 259 | + *outlen = *(in + result_int); | | 260 | + *out = (in + result_int + 1); | | 261 | + | | 262 | + return SSL_TLSEXT_ERR_OK; | | 263 | + } | | 264 | + | 227 | 265 | const std::vector<unsigned char>& alpn_protos = w->alpn_protos_; | 228 | 266 |
| 229 | 267 | if (alpn_protos.empty()) return SSL_TLSEXT_ERR_NOACK; | @@ -1249,6 +1287,15 @@ void TLSWrap::OnClientHelloParseEnd(void* arg) { | 1249 | 1287 | c->Cycle(); | 1250 | 1288 | } | 1251 | 1289 |
| | 1290 | +void TLSWrap::EnableALPNCb(const FunctionCallbackInfo<Value>& args) { | | 1291 | + TLSWrap* wrap; | | 1292 | + ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); | | 1293 | + wrap->alpn_callback_enabled_ = true; | | 1294 | + | | 1295 | + SSL* ssl = wrap->ssl_.get(); | | 1296 | + SSL_CTX_set_alpn_select_cb(SSL_get_SSL_CTX(ssl), SelectALPNCallback, wrap); | | 1297 | +} | | 1298 | + | 1252 | 1299 | void TLSWrap::GetServername(const FunctionCallbackInfo<Value>& args) { | 1253 | 1300 | Environment* env = Environment::GetCurrent(args); | 1254 | 1301 |
| @@ -2069,6 +2116,7 @@ void TLSWrap::Initialize( | 2069 | 2116 | SetProtoMethod(isolate, t, "certCbDone", CertCbDone); | 2070 | 2117 | SetProtoMethod(isolate, t, "destroySSL", DestroySSL); | 2071 | 2118 | SetProtoMethod(isolate, t, "enableCertCb", EnableCertCb); | | 2119 | + SetProtoMethod(isolate, t, "enableALPNCb", EnableALPNCb); | 2072 | 2120 | SetProtoMethod(isolate, t, "endParser", EndParser); | 2073 | 2121 | SetProtoMethod(isolate, t, "enableKeylogCallback", EnableKeylogCallback); | 2074 | 2122 | SetProtoMethod(isolate, t, "enableSessionCallbacks", EnableSessionCallbacks); | @@ -2138,6 +2186,7 @@ void TLSWrap::RegisterExternalReferences(ExternalReferenceRegistry* registry) { | 2138 | 2186 | registry->Register(CertCbDone); | 2139 | 2187 | registry->Register(DestroySSL); | 2140 | 2188 | registry->Register(EnableCertCb); | | 2189 | + registry->Register(EnableALPNCb); | 2141 | 2190 | registry->Register(EndParser); | 2142 | 2191 | registry->Register(EnableKeylogCallback); | 2143 | 2192 | registry->Register(EnableSessionCallbacks); | |
Collapse file Copy file name to clipboardExpand all lines: src/crypto/crypto_tls.h +2Lines changed: 2 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -175,6 +175,7 @@ class TLSWrap : public AsyncWrap, | 175 | 175 | static void CertCbDone(const v8::FunctionCallbackInfo<v8::Value>& args); | 176 | 176 | static void DestroySSL(const v8::FunctionCallbackInfo<v8::Value>& args); | 177 | 177 | static void EnableCertCb(const v8::FunctionCallbackInfo<v8::Value>& args); | | 178 | + static void EnableALPNCb(const v8::FunctionCallbackInfo<v8::Value>& args); | 178 | 179 | static void EnableKeylogCallback( | 179 | 180 | const v8::FunctionCallbackInfo<v8::Value>& args); | 180 | 181 | static void EnableSessionCallbacks( | @@ -292,6 +293,7 @@ class TLSWrap : public AsyncWrap, | 292 | 293 |
| 293 | 294 | public: | 294 | 295 | std::vector<unsigned char> alpn_protos_; // Accessed by SelectALPNCallback. | | 296 | + bool alpn_callback_enabled_ = false; // Accessed by SelectALPNCallback. | 295 | 297 | }; | 296 | 298 |
| 297 | 299 | } // namespace crypto | |
Collapse file Copy file name to clipboardExpand all lines: src/env_properties.h +1Lines changed: 1 addition & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 49 | 49 | V(ack_string, "ack") \ | 50 | 50 | V(address_string, "address") \ | 51 | 51 | V(aliases_string, "aliases") \ | | 52 | + V(alpn_callback_string, "ALPNCallback") \ | 52 | 53 | V(args_string, "args") \ | 53 | 54 | V(asn1curve_string, "asn1Curve") \ | 54 | 55 | V(async_ids_stack_string, "async_ids_stack") \ | |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-tls-alpn-server-client.js +72-3Lines changed: 72 additions & 3 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -40,9 +40,8 @@ function runTest(clientsOptions, serverOptions, cb) { | 40 | 40 | opt.rejectUnauthorized = false; | 41 | 41 |
| 42 | 42 | results[clientIndex] = {}; | 43 | | - const client = tls.connect(opt, function() { | 44 | | - results[clientIndex].client = { ALPN: client.alpnProtocol }; | 45 | | - client.end(); | | 43 | + | | 44 | + function startNextClient() { | 46 | 45 | if (options.length) { | 47 | 46 | clientIndex++; | 48 | 47 | connectClient(options); | @@ -52,6 +51,15 @@ function runTest(clientsOptions, serverOptions, cb) { | 52 | 51 | cb(results); | 53 | 52 | }); | 54 | 53 | } | | 54 | + } | | 55 | + | | 56 | + const client = tls.connect(opt, function() { | | 57 | + results[clientIndex].client = { ALPN: client.alpnProtocol }; | | 58 | + client.end(); | | 59 | + startNextClient(); | | 60 | + }).on('error', function(err) { | | 61 | + results[clientIndex].client = { error: err }; | | 62 | + startNextClient(); | 55 | 63 | }); | 56 | 64 | } | 57 | 65 |
| @@ -161,6 +169,67 @@ function Test4() { | 161 | 169 | { server: { ALPN: false }, | 162 | 170 | client: { ALPN: false } }); | 163 | 171 | }); | | 172 | + | | 173 | + TestALPNCallback(); | | 174 | +} | | 175 | + | | 176 | +function TestALPNCallback() { | | 177 | + // Server always selects the client's 2nd preference: | | 178 | + const serverOptions = { | | 179 | + ALPNCallback: common.mustCall(({ protocols }) => { | | 180 | + return protocols[1]; | | 181 | + }, 2) | | 182 | + }; | | 183 | + | | 184 | + const clientsOptions = [{ | | 185 | + ALPNProtocols: ['a', 'b', 'c'], | | 186 | + }, { | | 187 | + ALPNProtocols: ['a'], | | 188 | + }]; | | 189 | + | | 190 | + runTest(clientsOptions, serverOptions, function(results) { | | 191 | + // Callback picks 2nd preference => picks 'b' | | 192 | + checkResults(results[0], | | 193 | + { server: { ALPN: 'b' }, | | 194 | + client: { ALPN: 'b' } }); | | 195 | + | | 196 | + // Callback picks 2nd preference => undefined => ALPN rejected: | | 197 | + assert.strictEqual(results[1].server, undefined); | | 198 | + assert.strictEqual(results[1].client.error.code, 'ECONNRESET'); | | 199 | + | | 200 | + TestBadALPNCallback(); | | 201 | + }); | | 202 | +} | | 203 | + | | 204 | +function TestBadALPNCallback() { | | 205 | + // Server always returns a fixed invalid value: | | 206 | + const serverOptions = { | | 207 | + ALPNCallback: common.mustCall(() => 'http/5') | | 208 | + }; | | 209 | + | | 210 | + const clientsOptions = [{ | | 211 | + ALPNProtocols: ['http/1', 'h2'], | | 212 | + }]; | | 213 | + | | 214 | + process.once('uncaughtException', common.mustCall((error) => { | | 215 | + assert.strictEqual(error.code, 'ERR_TLS_ALPN_CALLBACK_INVALID_RESULT'); | | 216 | + })); | | 217 | + | | 218 | + runTest(clientsOptions, serverOptions, function(results) { | | 219 | + // Callback returns 'http/5' => doesn't match client ALPN => error & reset | | 220 | + assert.strictEqual(results[0].server, undefined); | | 221 | + assert.strictEqual(results[0].client.error.code, 'ECONNRESET'); | | 222 | + | | 223 | + TestALPNOptionsCallback(); | | 224 | + }); | | 225 | +} | | 226 | + | | 227 | +function TestALPNOptionsCallback() { | | 228 | + // Server sets two incompatible ALPN options: | | 229 | + assert.throws(() => tls.createServer({ | | 230 | + ALPNCallback: () => 'a', | | 231 | + ALPNProtocols: ['b', 'c'] | | 232 | + }), (error) => error.code === 'ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS'); | 164 | 233 | } | 165 | 234 |
| 166 | 235 | Test1(); |
|