Skip to content

Commit c97532c

Browse files
committed
tls: add ALPNCallback server option for dynamic ALPN
PR-URL: nodejs/node#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 3fdce64 commit c97532c

File tree

8 files changed

+221
-3
lines changed

8 files changed

+221
-3
lines changed

graal-nodejs/doc/api/errors.md

+14
Original file line numberDiff line numberDiff line change
@@ -2708,6 +2708,20 @@ This error represents a failed test. Additional information about the failure
27082708
is available via the `cause` property. The `failureType` property specifies
27092709
what the test was doing when the failure occurred.
27102710

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+
27112725
<a id="ERR_TLS_CERT_ALTNAME_FORMAT"></a>
27122726

27132727
### `ERR_TLS_CERT_ALTNAME_FORMAT`

graal-nodejs/doc/api/tls.md

+14
Original file line numberDiff line numberDiff line change
@@ -2045,6 +2045,9 @@ where `secureSocket` has the same API as `pair.cleartext`.
20452045
<!-- YAML
20462046
added: v0.3.2
20472047
changes:
2048+
- version: REPLACEME
2049+
pr-url: https://github.com/nodejs/node/pull/45190
2050+
description: The `options` parameter can now include `ALPNCallback`.
20482051
- version: v12.3.0
20492052
pr-url: https://github.com/nodejs/node/pull/27665
20502053
description: The `options` parameter now supports `net.createServer()`
@@ -2070,6 +2073,17 @@ changes:
20702073
e.g. `0x05hello0x05world`, where the first byte is the length of the next
20712074
protocol name. Passing an array is usually much simpler, e.g.
20722075
`['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.
20732087
* `clientCertEngine` {string} Name of an OpenSSL engine which can provide the
20742088
client certificate.
20752089
* `enableTrace` {boolean} If `true`, [`tls.TLSSocket.enableTrace()`][] will be

graal-nodejs/lib/_tls_wrap.js

+59
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;
@@ -787,6 +830,16 @@ TLSSocket.prototype._init = function(socket, wrap) {
787830
ssl.lastHandshakeTime = 0;
788831
ssl.handshakes = 0;
789832

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+
790843
if (this.server) {
791844
if (this.server.listenerCount('resumeSession') > 0 ||
792845
this.server.listenerCount('newSession') > 0) {
@@ -1165,6 +1218,7 @@ function tlsConnectionListener(rawSocket) {
11651218
rejectUnauthorized: this.rejectUnauthorized,
11661219
handshakeTimeout: this[kHandshakeTimeout],
11671220
ALPNProtocols: this.ALPNProtocols,
1221+
ALPNCallback: this.ALPNCallback,
11681222
SNICallback: this[kSNICallback] || SNICallback,
11691223
enableTrace: this[kEnableTrace],
11701224
pauseOnConnect: this.pauseOnConnect,
@@ -1264,6 +1318,11 @@ function Server(options, listener) {
12641318
this.requestCert = options.requestCert === true;
12651319
this.rejectUnauthorized = options.rejectUnauthorized !== false;
12661320

1321+
this.ALPNCallback = options.ALPNCallback;
1322+
if (this.ALPNCallback && options.ALPNProtocols) {
1323+
throw new ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS();
1324+
}
1325+
12671326
if (options.sessionTimeout)
12681327
this.sessionTimeout = options.sessionTimeout;
12691328

graal-nodejs/lib/internal/errors.js

+10
Original file line numberDiff line numberDiff line change
@@ -1622,6 +1622,16 @@ E('ERR_TEST_FAILURE', function(error, failureType) {
16221622
this.cause = error;
16231623
return msg;
16241624
}, 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);
16251635
E('ERR_TLS_CERT_ALTNAME_FORMAT', 'Invalid subject alternative name string',
16261636
SyntaxError);
16271637
E('ERR_TLS_CERT_ALTNAME_INVALID', function(reason, host, cert) {

graal-nodejs/src/crypto/crypto_tls.cc

+49
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;
@@ -1249,6 +1287,15 @@ void TLSWrap::OnClientHelloParseEnd(void* arg) {
12491287
c->Cycle();
12501288
}
12511289

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+
12521299
void TLSWrap::GetServername(const FunctionCallbackInfo<Value>& args) {
12531300
Environment* env = Environment::GetCurrent(args);
12541301

@@ -2069,6 +2116,7 @@ void TLSWrap::Initialize(
20692116
SetProtoMethod(isolate, t, "certCbDone", CertCbDone);
20702117
SetProtoMethod(isolate, t, "destroySSL", DestroySSL);
20712118
SetProtoMethod(isolate, t, "enableCertCb", EnableCertCb);
2119+
SetProtoMethod(isolate, t, "enableALPNCb", EnableALPNCb);
20722120
SetProtoMethod(isolate, t, "endParser", EndParser);
20732121
SetProtoMethod(isolate, t, "enableKeylogCallback", EnableKeylogCallback);
20742122
SetProtoMethod(isolate, t, "enableSessionCallbacks", EnableSessionCallbacks);
@@ -2138,6 +2186,7 @@ void TLSWrap::RegisterExternalReferences(ExternalReferenceRegistry* registry) {
21382186
registry->Register(CertCbDone);
21392187
registry->Register(DestroySSL);
21402188
registry->Register(EnableCertCb);
2189+
registry->Register(EnableALPNCb);
21412190
registry->Register(EndParser);
21422191
registry->Register(EnableKeylogCallback);
21432192
registry->Register(EnableSessionCallbacks);

graal-nodejs/src/crypto/crypto_tls.h

+2
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ class TLSWrap : public AsyncWrap,
175175
static void CertCbDone(const v8::FunctionCallbackInfo<v8::Value>& args);
176176
static void DestroySSL(const v8::FunctionCallbackInfo<v8::Value>& args);
177177
static void EnableCertCb(const v8::FunctionCallbackInfo<v8::Value>& args);
178+
static void EnableALPNCb(const v8::FunctionCallbackInfo<v8::Value>& args);
178179
static void EnableKeylogCallback(
179180
const v8::FunctionCallbackInfo<v8::Value>& args);
180181
static void EnableSessionCallbacks(
@@ -292,6 +293,7 @@ class TLSWrap : public AsyncWrap,
292293

293294
public:
294295
std::vector<unsigned char> alpn_protos_; // Accessed by SelectALPNCallback.
296+
bool alpn_callback_enabled_ = false; // Accessed by SelectALPNCallback.
295297
};
296298

297299
} // namespace crypto

graal-nodejs/src/env_properties.h

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
V(ack_string, "ack") \
5050
V(address_string, "address") \
5151
V(aliases_string, "aliases") \
52+
V(alpn_callback_string, "ALPNCallback") \
5253
V(args_string, "args") \
5354
V(asn1curve_string, "asn1Curve") \
5455
V(async_ids_stack_string, "async_ids_stack") \

graal-nodejs/test/parallel/test-tls-alpn-server-client.js

+72-3
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,8 @@ function runTest(clientsOptions, serverOptions, cb) {
4040
opt.rejectUnauthorized = false;
4141

4242
results[clientIndex] = {};
43-
const client = tls.connect(opt, function() {
44-
results[clientIndex].client = { ALPN: client.alpnProtocol };
45-
client.end();
43+
44+
function startNextClient() {
4645
if (options.length) {
4746
clientIndex++;
4847
connectClient(options);
@@ -52,6 +51,15 @@ function runTest(clientsOptions, serverOptions, cb) {
5251
cb(results);
5352
});
5453
}
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();
5563
});
5664
}
5765

@@ -161,6 +169,67 @@ function Test4() {
161169
{ server: { ALPN: false },
162170
client: { ALPN: false } });
163171
});
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');
164233
}
165234

166235
Test1();

0 commit comments

Comments
 (0)