Skip to content

Commit b5d16cd

Browse files
pimterrytargos
authored andcommitted
tls: add ALPNCallback server option for dynamic ALPN negotiation
PR-URL: #45190 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Debadree Chatterjee <[email protected]>
1 parent ae49f31 commit b5d16cd

File tree

8 files changed

+221
-3
lines changed

8 files changed

+221
-3
lines changed

‎doc/api/errors.md‎

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

27132727
### `ERR_TLS_CERT_ALTNAME_FORMAT`

‎doc/api/tls.md‎

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

‎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;
@@ -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(typeofoptions.ALPNCallback!=='function'){
835+
thrownewERR_INVALID_ARG_TYPE('options.ALPNCallback','Function',options.ALPNCallback);
836+
}
837+
assert(typeofoptions.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+
thrownewERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS();
1324+
}
1325+
12671326
if(options.sessionTimeout)
12681327
this.sessionTimeout=options.sessionTimeout;
12691328

‎lib/internal/errors.js‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1622,6 +1622,16 @@ E('ERR_TEST_FAILURE', function(error, failureType){
16221622
this.cause=error;
16231623
returnmsg;
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){

‎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+
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+
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+
return SSL_TLSEXT_ERR_OK;
263+
}
264+
227265
const std::vector<unsignedchar>& 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+
voidTLSWrap::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
voidTLSWrap::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);

‎src/crypto/crypto_tls.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ class TLSWrap : public AsyncWrap,
175175
staticvoidCertCbDone(const v8::FunctionCallbackInfo<v8::Value>& args);
176176
staticvoidDestroySSL(const v8::FunctionCallbackInfo<v8::Value>& args);
177177
staticvoidEnableCertCb(const v8::FunctionCallbackInfo<v8::Value>& args);
178+
staticvoidEnableALPNCb(const v8::FunctionCallbackInfo<v8::Value>& args);
178179
staticvoidEnableKeylogCallback(
179180
const v8::FunctionCallbackInfo<v8::Value>& args);
180181
staticvoidEnableSessionCallbacks(
@@ -292,6 +293,7 @@ class TLSWrap : public AsyncWrap,
292293

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

297299
} // namespace crypto

‎src/env_properties.h‎

Lines changed: 1 addition & 0 deletions
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") \

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

Lines changed: 72 additions & 3 deletions
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-
constclient=tls.connect(opt,function(){
44-
results[clientIndex].client={ALPN: client.alpnProtocol};
45-
client.end();
43+
44+
functionstartNextClient(){
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+
constclient=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+
functionTestALPNCallback(){
177+
// Server always selects the client's 2nd preference:
178+
constserverOptions={
179+
ALPNCallback: common.mustCall(({ protocols })=>{
180+
returnprotocols[1];
181+
},2)
182+
};
183+
184+
constclientsOptions=[{
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+
functionTestBadALPNCallback(){
205+
// Server always returns a fixed invalid value:
206+
constserverOptions={
207+
ALPNCallback: common.mustCall(()=>'http/5')
208+
};
209+
210+
constclientsOptions=[{
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+
functionTestALPNOptionsCallback(){
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)