Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 504d1d7

Browse filesBrowse files
pimterryRafaelGSS
authored andcommitted
tls: add ALPNCallback server option for dynamic ALPN negotiation
PR-URL: #45190 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Debadree Chatterjee <debadree333@gmail.com>
1 parent 8a611a3 commit 504d1d7
Copy full SHA for 504d1d7

File tree

Expand file treeCollapse file tree

8 files changed

+221
-3
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

8 files changed

+221
-3
lines changed
Open diff view settings
Collapse file

‎doc/api/errors.md‎

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 numberDiff line numberDiff line change
@@ -2729,6 +2729,20 @@ This error represents a failed test. Additional information about the failure
27292729
is available via the `cause` property. The `failureType` property specifies
27302730
what the test was doing when the failure occurred.
27312731

2732+
<a id="ERR_TLS_ALPN_CALLBACK_INVALID_RESULT"></a>
2733+
2734+
### `ERR_TLS_ALPN_CALLBACK_INVALID_RESULT`
2735+
2736+
This error is thrown when an `ALPNCallback` returns a value that is not in the
2737+
list of ALPN protocols offered by the client.
2738+
2739+
<a id="ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS"></a>
2740+
2741+
### `ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS`
2742+
2743+
This error is thrown when creating a `TLSServer` if the TLS options include
2744+
both `ALPNProtocols` and `ALPNCallback`. These options are mutually exclusive.
2745+
27322746
<a id="ERR_TLS_CERT_ALTNAME_FORMAT"></a>
27332747

27342748
### `ERR_TLS_CERT_ALTNAME_FORMAT`
Collapse file

‎doc/api/tls.md‎

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 numberDiff line numberDiff line change
@@ -2047,6 +2047,9 @@ where `secureSocket` has the same API as `pair.cleartext`.
20472047
<!-- YAML
20482048
added: v0.3.2
20492049
changes:
2050+
- version: REPLACEME
2051+
pr-url: https://github.com/nodejs/node/pull/45190
2052+
description: The `options` parameter can now include `ALPNCallback`.
20502053
- version: v19.0.0
20512054
pr-url: https://github.com/nodejs/node/pull/44031
20522055
description: If `ALPNProtocols` is set, incoming connections that send an
@@ -2077,6 +2080,17 @@ changes:
20772080
e.g. `0x05hello0x05world`, where the first byte is the length of the next
20782081
protocol name. Passing an array is usually much simpler, e.g.
20792082
`['hello', 'world']`. (Protocols should be ordered by their priority.)
2083+
* `ALPNCallback`: {Function} If set, this will be called when a
2084+
client opens a connection using the ALPN extension. One argument will
2085+
be passed to the callback: an object containing `servername` and
2086+
`protocols` fields, respectively containing the server name from
2087+
the SNI extension (if any) and an array of ALPN protocol name strings. The
2088+
callback must return either one of the strings listed in
2089+
`protocols`, which will be returned to the client as the selected
2090+
ALPN protocol, or `undefined`, to reject the connection with a fatal alert.
2091+
If a string is returned that does not match one of the client's ALPN
2092+
protocols, an error will be thrown. This option cannot be used with the
2093+
`ALPNProtocols` option, and setting both options will throw an error.
20802094
* `clientCertEngine` {string} Name of an OpenSSL engine which can provide the
20812095
client certificate.
20822096
* `enableTrace` {boolean} If `true`, [`tls.TLSSocket.enableTrace()`][] will be
Collapse file

‎lib/_tls_wrap.js‎

Copy file name to clipboardExpand all lines: lib/_tls_wrap.js
+59Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ const {
7272
ERR_INVALID_ARG_VALUE,
7373
ERR_MULTIPLE_CALLBACK,
7474
ERR_SOCKET_CLOSED,
75+
ERR_TLS_ALPN_CALLBACK_INVALID_RESULT,
76+
ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS,
7577
ERR_TLS_DH_PARAM_SIZE,
7678
ERR_TLS_HANDSHAKE_TIMEOUT,
7779
ERR_TLS_INVALID_CONTEXT,
@@ -108,6 +110,7 @@ const kErrorEmitted = Symbol('error-emitted');
108110
const kHandshakeTimeout = Symbol('handshake-timeout');
109111
const kRes = Symbol('res');
110112
const kSNICallback = Symbol('snicallback');
113+
const kALPNCallback = Symbol('alpncallback');
111114
const kEnableTrace = Symbol('enableTrace');
112115
const kPskCallback = Symbol('pskcallback');
113116
const kPskIdentityHint = Symbol('pskidentityhint');
@@ -234,6 +237,45 @@ function loadSNI(info) {
234237
}
235238

236239

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+
237279
function requestOCSP(socket, info) {
238280
if (!info.OCSPRequest || !socket.server)
239281
return requestOCSPDone(socket);
@@ -493,6 +535,7 @@ function TLSSocket(socket, opts) {
493535
this._controlReleased = false;
494536
this.secureConnecting = true;
495537
this._SNICallback = null;
538+
this[kALPNCallback] = null;
496539
this.servername = null;
497540
this.alpnProtocol = null;
498541
this.authorized = false;
@@ -755,6 +798,16 @@ TLSSocket.prototype._init = function(socket, wrap) {
755798
ssl.lastHandshakeTime = 0;
756799
ssl.handshakes = 0;
757800

801+
if (options.ALPNCallback) {
802+
if (typeof options.ALPNCallback !== 'function') {
803+
throw new ERR_INVALID_ARG_TYPE('options.ALPNCallback', 'Function', options.ALPNCallback);
804+
}
805+
assert(typeof options.ALPNCallback === 'function');
806+
this[kALPNCallback] = options.ALPNCallback;
807+
ssl.ALPNCallback = callALPNCallback;
808+
ssl.enableALPNCb();
809+
}
810+
758811
if (this.server) {
759812
if (this.server.listenerCount('resumeSession') > 0 ||
760813
this.server.listenerCount('newSession') > 0) {
@@ -1133,6 +1186,7 @@ function tlsConnectionListener(rawSocket) {
11331186
rejectUnauthorized: this.rejectUnauthorized,
11341187
handshakeTimeout: this[kHandshakeTimeout],
11351188
ALPNProtocols: this.ALPNProtocols,
1189+
ALPNCallback: this.ALPNCallback,
11361190
SNICallback: this[kSNICallback] || SNICallback,
11371191
enableTrace: this[kEnableTrace],
11381192
pauseOnConnect: this.pauseOnConnect,
@@ -1232,6 +1286,11 @@ function Server(options, listener) {
12321286
this.requestCert = options.requestCert === true;
12331287
this.rejectUnauthorized = options.rejectUnauthorized !== false;
12341288

1289+
this.ALPNCallback = options.ALPNCallback;
1290+
if (this.ALPNCallback && options.ALPNProtocols) {
1291+
throw new ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS();
1292+
}
1293+
12351294
if (options.sessionTimeout)
12361295
this.sessionTimeout = options.sessionTimeout;
12371296

Collapse file

‎lib/internal/errors.js‎

Copy file name to clipboardExpand all lines: lib/internal/errors.js
+10Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1623,6 +1623,16 @@ E('ERR_TEST_FAILURE', function(error, failureType) {
16231623
this.cause = error;
16241624
return msg;
16251625
}, Error);
1626+
E('ERR_TLS_ALPN_CALLBACK_INVALID_RESULT', (value, protocols) => {
1627+
return `ALPN callback returned a value (${
1628+
value
1629+
}) that did not match any of the client's offered protocols (${
1630+
protocols.join(', ')
1631+
})`;
1632+
}, TypeError);
1633+
E('ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS',
1634+
'The ALPNCallback and ALPNProtocols TLS options are mutually exclusive',
1635+
TypeError);
16261636
E('ERR_TLS_CERT_ALTNAME_FORMAT', 'Invalid subject alternative name string',
16271637
SyntaxError);
16281638
E('ERR_TLS_CERT_ALTNAME_INVALID', function(reason, host, cert) {
Collapse file

‎src/crypto/crypto_tls.cc‎

Copy file name to clipboardExpand all lines: src/crypto/crypto_tls.cc
+49Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,44 @@ int SelectALPNCallback(
224224
unsigned int inlen,
225225
void* arg) {
226226
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+
227265
const std::vector<unsigned char>& alpn_protos = w->alpn_protos_;
228266

229267
if (alpn_protos.empty()) return SSL_TLSEXT_ERR_NOACK;
@@ -1224,6 +1262,15 @@ void TLSWrap::OnClientHelloParseEnd(void* arg) {
12241262
c->Cycle();
12251263
}
12261264

1265+
void TLSWrap::EnableALPNCb(const FunctionCallbackInfo<Value>& args) {
1266+
TLSWrap* wrap;
1267+
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());
1268+
wrap->alpn_callback_enabled_ = true;
1269+
1270+
SSL* ssl = wrap->ssl_.get();
1271+
SSL_CTX_set_alpn_select_cb(SSL_get_SSL_CTX(ssl), SelectALPNCallback, wrap);
1272+
}
1273+
12271274
void TLSWrap::GetServername(const FunctionCallbackInfo<Value>& args) {
12281275
Environment* env = Environment::GetCurrent(args);
12291276

@@ -2034,6 +2081,7 @@ void TLSWrap::Initialize(
20342081
SetProtoMethod(isolate, t, "certCbDone", CertCbDone);
20352082
SetProtoMethod(isolate, t, "destroySSL", DestroySSL);
20362083
SetProtoMethod(isolate, t, "enableCertCb", EnableCertCb);
2084+
SetProtoMethod(isolate, t, "enableALPNCb", EnableALPNCb);
20372085
SetProtoMethod(isolate, t, "endParser", EndParser);
20382086
SetProtoMethod(isolate, t, "enableKeylogCallback", EnableKeylogCallback);
20392087
SetProtoMethod(isolate, t, "enableSessionCallbacks", EnableSessionCallbacks);
@@ -2099,6 +2147,7 @@ void TLSWrap::RegisterExternalReferences(ExternalReferenceRegistry* registry) {
20992147
registry->Register(CertCbDone);
21002148
registry->Register(DestroySSL);
21012149
registry->Register(EnableCertCb);
2150+
registry->Register(EnableALPNCb);
21022151
registry->Register(EndParser);
21032152
registry->Register(EnableKeylogCallback);
21042153
registry->Register(EnableSessionCallbacks);
Collapse file

‎src/crypto/crypto_tls.h‎

Copy file name to clipboardExpand all lines: src/crypto/crypto_tls.h
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ class TLSWrap : public AsyncWrap,
172172
static void CertCbDone(const v8::FunctionCallbackInfo<v8::Value>& args);
173173
static void DestroySSL(const v8::FunctionCallbackInfo<v8::Value>& args);
174174
static void EnableCertCb(const v8::FunctionCallbackInfo<v8::Value>& args);
175+
static void EnableALPNCb(const v8::FunctionCallbackInfo<v8::Value>& args);
175176
static void EnableKeylogCallback(
176177
const v8::FunctionCallbackInfo<v8::Value>& args);
177178
static void EnableSessionCallbacks(
@@ -285,6 +286,7 @@ class TLSWrap : public AsyncWrap,
285286

286287
public:
287288
std::vector<unsigned char> alpn_protos_; // Accessed by SelectALPNCallback.
289+
bool alpn_callback_enabled_ = false; // Accessed by SelectALPNCallback.
288290
};
289291

290292
} // namespace crypto
Collapse file

‎src/env_properties.h‎

Copy file name to clipboardExpand all lines: src/env_properties.h
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
V(ack_string, "ack") \
5151
V(address_string, "address") \
5252
V(aliases_string, "aliases") \
53+
V(alpn_callback_string, "ALPNCallback") \
5354
V(args_string, "args") \
5455
V(asn1curve_string, "asn1Curve") \
5556
V(async_ids_stack_string, "async_ids_stack") \
Collapse file

‎test/parallel/test-tls-alpn-server-client.js‎

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 numberDiff line numberDiff line change
@@ -41,9 +41,8 @@ function runTest(clientsOptions, serverOptions, cb) {
4141
opt.rejectUnauthorized = false;
4242

4343
results[clientIndex] = {};
44-
const client = tls.connect(opt, function() {
45-
results[clientIndex].client = { ALPN: client.alpnProtocol };
46-
client.end();
44+
45+
function startNextClient() {
4746
if (options.length) {
4847
clientIndex++;
4948
connectClient(options);
@@ -53,6 +52,15 @@ function runTest(clientsOptions, serverOptions, cb) {
5352
cb(results);
5453
});
5554
}
55+
}
56+
57+
const client = tls.connect(opt, function() {
58+
results[clientIndex].client = { ALPN: client.alpnProtocol };
59+
client.end();
60+
startNextClient();
61+
}).on('error', function(err) {
62+
results[clientIndex].client = { error: err };
63+
startNextClient();
5664
});
5765
}
5866

@@ -200,12 +208,73 @@ function TestFatalAlert() {
200208
.on('close', common.mustCall(() => {
201209
assert.match(stderr, /SSL alert number 120/);
202210
server.close();
211+
TestALPNCallback();
203212
}));
204213
} else {
205214
server.close();
215+
TestALPNCallback();
206216
}
207217
}));
208218
}));
209219
}
210220

221+
function TestALPNCallback() {
222+
// Server always selects the client's 2nd preference:
223+
const serverOptions = {
224+
ALPNCallback: common.mustCall(({ protocols }) => {
225+
return protocols[1];
226+
}, 2)
227+
};
228+
229+
const clientsOptions = [{
230+
ALPNProtocols: ['a', 'b', 'c'],
231+
}, {
232+
ALPNProtocols: ['a'],
233+
}];
234+
235+
runTest(clientsOptions, serverOptions, function(results) {
236+
// Callback picks 2nd preference => picks 'b'
237+
checkResults(results[0],
238+
{ server: { ALPN: 'b' },
239+
client: { ALPN: 'b' } });
240+
241+
// Callback picks 2nd preference => undefined => ALPN rejected:
242+
assert.strictEqual(results[1].server, undefined);
243+
assert.strictEqual(results[1].client.error.code, 'ECONNRESET');
244+
245+
TestBadALPNCallback();
246+
});
247+
}
248+
249+
function TestBadALPNCallback() {
250+
// Server always returns a fixed invalid value:
251+
const serverOptions = {
252+
ALPNCallback: common.mustCall(() => 'http/5')
253+
};
254+
255+
const clientsOptions = [{
256+
ALPNProtocols: ['http/1', 'h2'],
257+
}];
258+
259+
process.once('uncaughtException', common.mustCall((error) => {
260+
assert.strictEqual(error.code, 'ERR_TLS_ALPN_CALLBACK_INVALID_RESULT');
261+
}));
262+
263+
runTest(clientsOptions, serverOptions, function(results) {
264+
// Callback returns 'http/5' => doesn't match client ALPN => error & reset
265+
assert.strictEqual(results[0].server, undefined);
266+
assert.strictEqual(results[0].client.error.code, 'ECONNRESET');
267+
268+
TestALPNOptionsCallback();
269+
});
270+
}
271+
272+
function TestALPNOptionsCallback() {
273+
// Server sets two incompatible ALPN options:
274+
assert.throws(() => tls.createServer({
275+
ALPNCallback: () => 'a',
276+
ALPNProtocols: ['b', 'c']
277+
}), (error) => error.code === 'ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS');
278+
}
279+
211280
Test1();

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.