Skip to content

Commit 504d1d7

Browse 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

8 files changed

Lines changed: 221 additions & 3 deletions

File tree

‎doc/api/errors.md‎

Lines changed: 14 additions & 0 deletions
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+
<aid="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+
<aid="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
<aid="ERR_TLS_CERT_ALTNAME_FORMAT"></a>
27332747

27342748
### `ERR_TLS_CERT_ALTNAME_FORMAT`

‎doc/api/tls.md‎

Lines changed: 14 additions & 0 deletions
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

‎lib/_tls_wrap.js‎

Lines 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
constkHandshakeTimeout=Symbol('handshake-timeout');
109111
constkRes=Symbol('res');
110112
constkSNICallback=Symbol('snicallback');
113+
constkALPNCallback=Symbol('alpncallback');
111114
constkEnableTrace=Symbol('enableTrace');
112115
constkPskCallback=Symbol('pskcallback');
113116
constkPskIdentityHint=Symbol('pskidentityhint');
@@ -234,6 +237,45 @@ function loadSNI(info) {
234237
}
235238

236239

240+
functioncallALPNCallback(protocolsBuffer){
241+
consthandle=this;
242+
constsocket=handle[owner_symbol];
243+
244+
constservername=handle.getServername();
245+
246+
// Collect all the protocols from the given buffer:
247+
constprotocols=[];
248+
letoffset=0;
249+
while(offset<protocolsBuffer.length){
250+
constprotocolLen=protocolsBuffer[offset];
251+
offset+=1;
252+
253+
constprotocol=protocolsBuffer.slice(offset,offset+protocolLen);
254+
offset+=protocolLen;
255+
256+
protocols.push(protocol.toString('ascii'));
257+
}
258+
259+
constselectedProtocol=socket[kALPNCallback]({
260+
servername,
261+
protocols,
262+
});
263+
264+
// Undefined -> all proposed protocols rejected
265+
if(selectedProtocol===undefined)returnundefined;
266+
267+
constprotocolIndex=protocols.indexOf(selectedProtocol);
268+
if(protocolIndex===-1){
269+
thrownewERR_TLS_ALPN_CALLBACK_INVALID_RESULT(selectedProtocol,protocols);
270+
}
271+
letprotocolOffset=0;
272+
for(leti=0;i<protocolIndex;i++){
273+
protocolOffset+=1+protocols[i].length;
274+
}
275+
276+
returnprotocolOffset;
277+
}
278+
237279
functionrequestOCSP(socket,info){
238280
if(!info.OCSPRequest||!socket.server)
239281
returnrequestOCSPDone(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(typeofoptions.ALPNCallback!=='function'){
803+
thrownewERR_INVALID_ARG_TYPE('options.ALPNCallback','Function',options.ALPNCallback);
804+
}
805+
assert(typeofoptions.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+
thrownewERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS();
1292+
}
1293+
12351294
if(options.sessionTimeout)
12361295
this.sessionTimeout=options.sessionTimeout;
12371296

‎lib/internal/errors.js‎

Lines 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
returnmsg;
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){

‎src/crypto/crypto_tls.cc‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,44 @@ int SelectALPNCallback(
224224
unsignedint 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<constchar*>(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+
returnSSL_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+
returnSSL_TLSEXT_ERR_ALERT_FATAL;
251+
}
252+
253+
CHECK(callback_result->IsNumber());
254+
unsignedint 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+
returnSSL_TLSEXT_ERR_OK;
263+
}
264+
227265
const std::vector<unsignedchar>& alpn_protos = w->alpn_protos_;
228266

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

1265+
voidTLSWrap::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
voidTLSWrap::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);

‎src/crypto/crypto_tls.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ class TLSWrap : public AsyncWrap,
172172
staticvoidCertCbDone(const v8::FunctionCallbackInfo<v8::Value>& args);
173173
staticvoidDestroySSL(const v8::FunctionCallbackInfo<v8::Value>& args);
174174
staticvoidEnableCertCb(const v8::FunctionCallbackInfo<v8::Value>& args);
175+
staticvoidEnableALPNCb(const v8::FunctionCallbackInfo<v8::Value>& args);
175176
staticvoidEnableKeylogCallback(
176177
const v8::FunctionCallbackInfo<v8::Value>& args);
177178
staticvoidEnableSessionCallbacks(
@@ -285,6 +286,7 @@ class TLSWrap : public AsyncWrap,
285286

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

290292
} // namespace crypto

‎src/env_properties.h‎

Lines 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") \

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

Lines 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-
constclient=tls.connect(opt,function(){
45-
results[clientIndex].client={ALPN: client.alpnProtocol};
46-
client.end();
44+
45+
functionstartNextClient(){
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+
constclient=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,/SSLalertnumber120/);
202210
server.close();
211+
TestALPNCallback();
203212
}));
204213
}else{
205214
server.close();
215+
TestALPNCallback();
206216
}
207217
}));
208218
}));
209219
}
210220

221+
functionTestALPNCallback(){
222+
// Server always selects the client's 2nd preference:
223+
constserverOptions={
224+
ALPNCallback: common.mustCall(({ protocols })=>{
225+
returnprotocols[1];
226+
},2)
227+
};
228+
229+
constclientsOptions=[{
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+
functionTestBadALPNCallback(){
250+
// Server always returns a fixed invalid value:
251+
constserverOptions={
252+
ALPNCallback: common.mustCall(()=>'http/5')
253+
};
254+
255+
constclientsOptions=[{
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+
functionTestALPNOptionsCallback(){
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)