From 802a2e79e1adb22542ba12fba5e331e94277272d Mon Sep 17 00:00:00 2001 From: Shigeki Ohtsu Date: Thu, 23 Apr 2015 15:25:15 +0900 Subject: [PATCH] tls, crypto: add ALPN Support ALPN is added to tls according to RFC7301, which supersedes NPN. When the server receives both NPN and ALPN extensions from the client, ALPN takes precedence over NPN and the server does not send NPN extension to the client. alpnProtocol in TLSSocket always returns false when no selected protocol exists by ALPN. In https server, http/1.1 token is always set when no options.ALPNProtocols exists. PR-URL: https://github.com/nodejs/node/pull/2564 Reviewed-By: Fedor Indutny Reviewed-By: Ben Noordhuis --- doc/api/tls.markdown | 41 +- lib/_tls_legacy.js | 15 +- lib/_tls_wrap.js | 17 + lib/https.js | 7 + lib/tls.js | 51 +- src/env.h | 2 + src/node.cc | 7 + src/node_constants.cc | 5 + src/node_crypto.cc | 104 ++++ src/node_crypto.h | 13 +- test/parallel/test-tls-alpn-server-client.js | 540 +++++++++++++++++++ 11 files changed, 771 insertions(+), 31 deletions(-) create mode 100644 test/parallel/test-tls-alpn-server-client.js diff --git a/doc/api/tls.markdown b/doc/api/tls.markdown index 4907c65328cab0..ca891bb40fc5ce 100644 --- a/doc/api/tls.markdown +++ b/doc/api/tls.markdown @@ -66,14 +66,15 @@ and tap `R` (that's the letter `R` followed by a carriage return) a few times. -## NPN and SNI +## ALPN, NPN and SNI -NPN (Next Protocol Negotiation) and SNI (Server Name Indication) are TLS +ALPN (Application-Layer Protocol Negotiation Extension), NPN (Next +Protocol Negotiation) and SNI (Server Name Indication) are TLS handshake extensions allowing you: - * NPN - to use one TLS server for multiple protocols (HTTP, SPDY) + * ALPN/NPN - to use one TLS server for multiple protocols (HTTP, SPDY, HTTP/2) * SNI - to use one TLS server for multiple hostnames with different SSL certificates. @@ -249,6 +250,12 @@ automatically set as a listener for the [secureConnection][] event. The - `NPNProtocols`: An array or `Buffer` of possible NPN protocols. (Protocols should be ordered by their priority). + - `ALPNProtocols`: An array or `Buffer` of possible ALPN + protocols. (Protocols should be ordered by their priority). When + the server receives both NPN and ALPN extensions from the client, + ALPN takes precedence over NPN and the server does not send an NPN + extension to the client. + - `SNICallback(servername, cb)`: A function that will be called if client supports SNI TLS extension. Two argument will be passed to it: `servername`, and `cb`. `SNICallback` should invoke `cb(null, ctx)`, where `ctx` is a @@ -372,9 +379,16 @@ Creates a new client connection to the given `port` and `host` (old API) or fails; `err.code` contains the OpenSSL error code. Default: `true`. - `NPNProtocols`: An array of strings or `Buffer`s containing supported NPN - protocols. `Buffer`s should have following format: `0x05hello0x05world`, - where first byte is next protocol name's length. (Passing array should - usually be much simpler: `['hello', 'world']`.) + protocols. `Buffer`s should have the following format: + `0x05hello0x05world`, where first byte is next protocol name's + length. (Passing array should usually be much simpler: + `['hello', 'world']`.) + + - `ALPNProtocols`: An array of strings or `Buffer`s containing + supported ALPN protocols. `Buffer`s should have following format: + `0x05hello0x05world`, where the first byte is the next protocol + name's length. (Passing array should usually be much simpler: + `['hello', 'world']`.) - `servername`: Servername for SNI (Server Name Indication) TLS extension. @@ -476,6 +490,8 @@ Construct a new TLSSocket object from existing TCP socket. - `NPNProtocols`: Optional, see [tls.createServer][] + - `ALPNProtocols`: Optional, see [tls.createServer][] + - `SNICallback`: Optional, see [tls.createServer][] - `session`: Optional, a `Buffer` instance, containing TLS session @@ -571,7 +587,13 @@ server. If `socket.authorized` is false, then `socket.authorizationError` is set to describe how authorization failed. Implied but worth mentioning: depending on the settings of the TLS server, you unauthorized connections may be accepted. -`socket.npnProtocol` is a string containing selected NPN protocol. + +`socket.npnProtocol` is a string containing the selected NPN protocol +and `socket.alpnProtocol` is a string containing the selected ALPN +protocol, When both NPN and ALPN extensions are received, ALPN takes +precedence over NPN and the next protocol is selected by ALPN. When +ALPN has no selected protocol, this returns false. + `socket.servername` is a string containing servername requested with SNI. @@ -744,8 +766,9 @@ The listener will be called no matter if the server's certificate was authorized or not. It is up to the user to test `tlsSocket.authorized` to see if the server certificate was signed by one of the specified CAs. If `tlsSocket.authorized === false` then the error can be found in -`tlsSocket.authorizationError`. Also if NPN was used - you can check -`tlsSocket.npnProtocol` for negotiated protocol. +`tlsSocket.authorizationError`. Also if ALPN or NPN was used - you can +check `tlsSocket.alpnProtocol` or `tlsSocket.npnProtocol` for the +negotiated protocol. ### Event: 'OCSPResponse' diff --git a/lib/_tls_legacy.js b/lib/_tls_legacy.js index 7f7707d149dfa2..8c079e341b5b05 100644 --- a/lib/_tls_legacy.js +++ b/lib/_tls_legacy.js @@ -177,7 +177,7 @@ CryptoStream.prototype._write = function write(data, encoding, cb) { if (this.pair.encrypted._internallyPendingBytes()) this.pair.encrypted.read(0); - // Get NPN and Server name when ready + // Get ALPN, NPN and Server name when ready this.pair.maybeInitFinished(); // Whole buffer was written @@ -273,7 +273,7 @@ CryptoStream.prototype._read = function read(size) { bytesRead < size && this.pair.ssl !== null); - // Get NPN and Server name when ready + // Get ALPN, NPN and Server name when ready this.pair.maybeInitFinished(); // Create new buffer if previous was filled up @@ -726,6 +726,13 @@ function SecurePair(context, isServer, requestCert, rejectUnauthorized, this.npnProtocol = null; } + if (process.features.tls_alpn && options.ALPNProtocols) { + // keep reference in secureContext not to be GC-ed + this.ssl._secureContext.alpnBuffer = options.ALPNProtocols; + this.ssl.setALPNrotocols(this.ssl._secureContext.alpnBuffer); + this.alpnProtocol = null; + } + /* Acts as a r/w stream to the cleartext side of the stream. */ this.cleartext = new CleartextStream(this, options.cleartext); @@ -778,6 +785,10 @@ SecurePair.prototype.maybeInitFinished = function() { this.npnProtocol = this.ssl.getNegotiatedProtocol(); } + if (process.features.tls_alpn) { + this.alpnProtocol = this.ssl.getALPNNegotiatedProtocol(); + } + if (process.features.tls_sni) { this.servername = this.ssl.getServername(); } diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js index f0273471772137..d918656a360c7a 100644 --- a/lib/_tls_wrap.js +++ b/lib/_tls_wrap.js @@ -239,6 +239,7 @@ function TLSSocket(socket, options) { this._SNICallback = null; this.servername = null; this.npnProtocol = null; + this.alpnProtocol = null; this.authorized = false; this.authorizationError = null; @@ -453,6 +454,12 @@ TLSSocket.prototype._init = function(socket, wrap) { if (process.features.tls_npn && options.NPNProtocols) ssl.setNPNProtocols(options.NPNProtocols); + if (process.features.tls_alpn && options.ALPNProtocols) { + // keep reference in secureContext not to be GC-ed + ssl._secureContext.alpnBuffer = options.ALPNProtocols; + ssl.setALPNProtocols(ssl._secureContext.alpnBuffer); + } + if (options.handshakeTimeout > 0) this.setTimeout(options.handshakeTimeout, this._handleTimeout); @@ -559,6 +566,10 @@ TLSSocket.prototype._finishInit = function() { this.npnProtocol = this._handle.getNegotiatedProtocol(); } + if (process.features.tls_alpn) { + this.alpnProtocol = this.ssl.getALPNNegotiatedProtocol(); + } + if (process.features.tls_sni && this._tlsOptions.isServer) { this.servername = this._handle.getServername(); } @@ -766,6 +777,7 @@ function Server(/* [options], listener */) { rejectUnauthorized: self.rejectUnauthorized, handshakeTimeout: timeout, NPNProtocols: self.NPNProtocols, + ALPNProtocols: self.ALPNProtocols, SNICallback: options.SNICallback || SNICallback }); @@ -876,6 +888,8 @@ Server.prototype.setOptions = function(options) { this.honorCipherOrder = true; if (secureOptions) this.secureOptions = secureOptions; if (options.NPNProtocols) tls.convertNPNProtocols(options.NPNProtocols, this); + if (options.ALPNProtocols) + tls.convertALPNProtocols(options.ALPNProtocols, this); if (options.sessionIdContext) { this.sessionIdContext = options.sessionIdContext; } else { @@ -968,8 +982,10 @@ exports.connect = function(/* [port, host], options, cb */) { (options.socket && options.socket._host) || 'localhost', NPN = {}, + ALPN = {}, context = tls.createSecureContext(options); tls.convertNPNProtocols(options.NPNProtocols, NPN); + tls.convertALPNProtocols(options.ALPNProtocols, ALPN); var socket = new TLSSocket(options.socket, { pipe: options.path && !options.port, @@ -979,6 +995,7 @@ exports.connect = function(/* [port, host], options, cb */) { rejectUnauthorized: options.rejectUnauthorized, session: options.session, NPNProtocols: NPN.NPNProtocols, + ALPNProtocols: ALPN.ALPNProtocols, requestOCSP: options.requestOCSP }); diff --git a/lib/https.js b/lib/https.js index abe4a20907dfdd..edf0aa4432f82d 100644 --- a/lib/https.js +++ b/lib/https.js @@ -14,6 +14,13 @@ function Server(opts, requestListener) { opts.NPNProtocols = ['http/1.1', 'http/1.0']; } + if (process.features.tls_alpn && !opts.ALPNProtocols) { + // http/1.0 is not defined as Protocol IDs in IANA + // http://www.iana.org/assignments/tls-extensiontype-values + // /tls-extensiontype-values.xhtml#alpn-protocol-ids + opts.ALPNProtocols = ['http/1.1']; + } + tls.Server.call(this, opts, http._connectionListener); this.httpAllowHalfOpen = false; diff --git a/lib/tls.js b/lib/tls.js index 0d85a948dcc511..e269e800d31d1c 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -33,27 +33,42 @@ exports.getCiphers = function() { // Convert protocols array into valid OpenSSL protocols list // ("\x06spdy/2\x08http/1.1\x08http/1.0") -exports.convertNPNProtocols = function convertNPNProtocols(NPNProtocols, out) { - // If NPNProtocols is Array - translate it into buffer - if (Array.isArray(NPNProtocols)) { - var buff = new Buffer(NPNProtocols.reduce(function(p, c) { - return p + 1 + Buffer.byteLength(c); - }, 0)); - - NPNProtocols.reduce(function(offset, c) { - var clen = Buffer.byteLength(c); - buff[offset] = clen; - buff.write(c, offset + 1); - - return offset + 1 + clen; - }, 0); - - NPNProtocols = buff; +function convertProtocols(protocols) { + var buff = new Buffer(protocols.reduce(function(p, c) { + return p + 1 + Buffer.byteLength(c); + }, 0)); + + protocols.reduce(function(offset, c) { + var clen = Buffer.byteLength(c); + buff[offset] = clen; + buff.write(c, offset + 1); + + return offset + 1 + clen; + }, 0); + + return buff; +}; + +exports.convertNPNProtocols = function(protocols, out) { + // If protocols is Array - translate it into buffer + if (Array.isArray(protocols)) { + protocols = convertProtocols(protocols); } + // If it's already a Buffer - store it + if (protocols instanceof Buffer) { + out.NPNProtocols = protocols; + } +}; +exports.convertALPNProtocols = function(protocols, out) { + // If protocols is Array - translate it into buffer + if (Array.isArray(protocols)) { + protocols = convertProtocols(protocols); + } // If it's already a Buffer - store it - if (NPNProtocols instanceof Buffer) { - out.NPNProtocols = NPNProtocols; + if (protocols instanceof Buffer) { + // copy new buffer not to be modified by user + out.ALPNProtocols = new Buffer(protocols); } }; diff --git a/src/env.h b/src/env.h index b79ef4ae3e6587..b3f55c173a239e 100644 --- a/src/env.h +++ b/src/env.h @@ -42,6 +42,7 @@ namespace node { // for the sake of convenience. Strings should be ASCII-only. #define PER_ISOLATE_STRING_PROPERTIES(V) \ V(address_string, "address") \ + V(alpn_buffer_string, "alpnBuffer") \ V(args_string, "args") \ V(argv_string, "argv") \ V(arrow_message_string, "arrowMessage") \ @@ -205,6 +206,7 @@ namespace node { V(timestamp_string, "timestamp") \ V(title_string, "title") \ V(tls_npn_string, "tls_npn") \ + V(tls_alpn_string, "tls_alpn") \ V(tls_ocsp_string, "tls_ocsp") \ V(tls_sni_string, "tls_sni") \ V(tls_string, "tls") \ diff --git a/src/node.cc b/src/node.cc index 10e7da125d49a7..4de2f97491cf46 100644 --- a/src/node.cc +++ b/src/node.cc @@ -2582,6 +2582,13 @@ static Local GetFeatures(Environment* env) { #endif obj->Set(env->tls_npn_string(), tls_npn); +#ifdef TLSEXT_TYPE_application_layer_protocol_negotiation + Local tls_alpn = True(env->isolate()); +#else + Local tls_alpn = False(env->isolate()); +#endif + obj->Set(env->tls_alpn_string(), tls_alpn); + #ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB Local tls_sni = True(env->isolate()); #else diff --git a/src/node_constants.cc b/src/node_constants.cc index 51c2ee814ae504..1259f83697c015 100644 --- a/src/node_constants.cc +++ b/src/node_constants.cc @@ -935,6 +935,11 @@ void DefineOpenSSLConstants(Local target) { NODE_DEFINE_CONSTANT(target, NPN_ENABLED); #endif +#ifdef TLSEXT_TYPE_application_layer_protocol_negotiation +#define ALPN_ENABLED 1 + NODE_DEFINE_CONSTANT(target, ALPN_ENABLED); +#endif + #ifdef RSA_PKCS1_PADDING NODE_DEFINE_CONSTANT(target, RSA_PKCS1_PADDING); #endif diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 0185970c1cfcfa..1f50b643b5900f 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -167,6 +167,15 @@ template void SSLWrap::DestroySSL(); template int SSLWrap::SSLCertCallback(SSL* s, void* arg); template void SSLWrap::WaitForCertCb(CertCb cb, void* arg); +#ifdef TLSEXT_TYPE_application_layer_protocol_negotiation +template int SSLWrap::SelectALPNCallback( + SSL* s, + const unsigned char** out, + unsigned char* outlen, + const unsigned char* in, + unsigned int inlen, + void* arg); +#endif // TLSEXT_TYPE_application_layer_protocol_negotiation static void crypto_threadid_cb(CRYPTO_THREADID* tid) { static_assert(sizeof(uv_thread_t) <= sizeof(void*), // NOLINT(runtime/sizeof) @@ -1148,6 +1157,9 @@ void SSLWrap::AddMethods(Environment* env, Local t) { env->SetProtoMethod(t, "setNPNProtocols", SetNPNProtocols); #endif + env->SetProtoMethod(t, "getALPNNegotiatedProtocol", GetALPNNegotiatedProto); + env->SetProtoMethod(t, "setALPNProtocols", SetALPNProtocols); + t->PrototypeTemplate()->SetAccessor( FIXED_ONE_BYTE_STRING(env->isolate(), "_external"), SSLGetter, @@ -2010,6 +2022,98 @@ void SSLWrap::SetNPNProtocols(const FunctionCallbackInfo& args) { } #endif // OPENSSL_NPN_NEGOTIATED +#ifdef TLSEXT_TYPE_application_layer_protocol_negotiation +typedef struct tlsextalpnctx_st { + unsigned char* data; + unsigned short len; +} tlsextalpnctx; + +template +int SSLWrap::SelectALPNCallback(SSL* s, + const unsigned char** out, + unsigned char* outlen, + const unsigned char* in, + unsigned int inlen, + void* arg) { + Base* w = static_cast(SSL_get_app_data(s)); + Environment* env = w->env(); + HandleScope handle_scope(env->isolate()); + Context::Scope context_scope(env->context()); + + Local alpn_buffer = + w->object()->GetHiddenValue(env->alpn_buffer_string()); + CHECK(Buffer::HasInstance(alpn_buffer)); + const unsigned char* alpn_protos = + reinterpret_cast(Buffer::Data(alpn_buffer)); + unsigned alpn_protos_len = Buffer::Length(alpn_buffer); + int status = SSL_select_next_proto(const_cast(out), outlen, + alpn_protos, alpn_protos_len, in, inlen); + + switch (status) { + case OPENSSL_NPN_NO_OVERLAP: + // According to 3.2. Protocol Selection of RFC7301, + // fatal no_application_protocol alert shall be sent + // but current openssl does not support it yet. See + // https://rt.openssl.org/Ticket/Display.html?id=3463&user=guest&pass=guest + // Instead, we send a warning alert for now. + return SSL_TLSEXT_ERR_ALERT_WARNING; + case OPENSSL_NPN_NEGOTIATED: + return SSL_TLSEXT_ERR_OK; + default: + return SSL_TLSEXT_ERR_ALERT_FATAL; + } +} +#endif // TLSEXT_TYPE_application_layer_protocol_negotiation + + +template +void SSLWrap::GetALPNNegotiatedProto( + const FunctionCallbackInfo& args) { +#ifdef TLSEXT_TYPE_application_layer_protocol_negotiation + HandleScope scope(args.GetIsolate()); + Base* w = Unwrap(args.Holder()); + + const unsigned char* alpn_proto; + unsigned int alpn_proto_len; + + SSL_get0_alpn_selected(w->ssl_, &alpn_proto, &alpn_proto_len); + + if (!alpn_proto) + return args.GetReturnValue().Set(false); + + args.GetReturnValue().Set( + OneByteString(args.GetIsolate(), alpn_proto, alpn_proto_len)); +#endif // TLSEXT_TYPE_application_layer_protocol_negotiation +} + + +template +void SSLWrap::SetALPNProtocols( + const FunctionCallbackInfo& args) { +#ifdef TLSEXT_TYPE_application_layer_protocol_negotiation + HandleScope scope(args.GetIsolate()); + Base* w = Unwrap(args.Holder()); + Environment* env = w->env(); + if (args.Length() < 1 || !Buffer::HasInstance(args[0])) + return env->ThrowTypeError("Must give a Buffer as first argument"); + + if (w->is_client()) { + const unsigned char* alpn_protos = + reinterpret_cast(Buffer::Data(args[0])); + unsigned alpn_protos_len = Buffer::Length(args[0]); + int r = SSL_set_alpn_protos(w->ssl_, alpn_protos, alpn_protos_len); + CHECK_EQ(r, 0); + } else { + Local alpn_buffer = Local::New(env->isolate(), args[0]); + bool ret = w->object()->SetHiddenValue(env->alpn_buffer_string(), + alpn_buffer); + CHECK(ret); + // Server should select ALPN protocol from list of advertised by client + SSL_CTX_set_alpn_select_cb(w->ssl_->ctx, SelectALPNCallback, nullptr); + } +#endif // TLSEXT_TYPE_application_layer_protocol_negotiation +} + #ifdef NODE__HAVE_TLSEXT_STATUS_CB template diff --git a/src/node_crypto.h b/src/node_crypto.h index c276df04748942..4aceb41cb8ce19 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -5,9 +5,7 @@ #include "node_crypto_clienthello.h" // ClientHelloParser #include "node_crypto_clienthello-inl.h" -#ifdef OPENSSL_NPN_NEGOTIATED #include "node_buffer.h" -#endif #include "env.h" #include "async-wrap.h" @@ -187,6 +185,7 @@ class SSLWrap { #ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB sni_context_.Reset(); #endif + #ifdef NODE__HAVE_TLSEXT_STATUS_CB ocsp_response_.Reset(); #endif // NODE__HAVE_TLSEXT_STATUS_CB @@ -259,6 +258,16 @@ class SSLWrap { unsigned int inlen, void* arg); #endif // OPENSSL_NPN_NEGOTIATED + + static void GetALPNNegotiatedProto( + const v8::FunctionCallbackInfo& args); + static void SetALPNProtocols(const v8::FunctionCallbackInfo& args); + static int SelectALPNCallback(SSL* s, + const unsigned char** out, + unsigned char* outlen, + const unsigned char* in, + unsigned int inlen, + void* arg); static int TLSExtStatusCallback(SSL* s, void* arg); static int SSLCertCallback(SSL* s, void* arg); static void SSLGetter(v8::Local property, diff --git a/test/parallel/test-tls-alpn-server-client.js b/test/parallel/test-tls-alpn-server-client.js new file mode 100644 index 00000000000000..e5d809d1489aee --- /dev/null +++ b/test/parallel/test-tls-alpn-server-client.js @@ -0,0 +1,540 @@ +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) { + console.log('1..0 # Skipped: missing crypto'); + return; +} + +if (!process.features.tls_alpn) { + console.error('Skipping because node compiled without OpenSSL or ' + + 'with old OpenSSL version.'); + process.exit(0); +} + +const assert = require('assert'); +const fs = require('fs'); +const tls = require('tls'); + +function filenamePEM(n) { + return require('path').join(common.fixturesDir, 'keys', n + '.pem'); +} + +function loadPEM(n) { + return fs.readFileSync(filenamePEM(n)); +} + +var serverPort = common.PORT; +var serverIP = common.localhostIPv4; + +function checkResults(result, expected) { + assert.strictEqual(result.server.ALPN, expected.server.ALPN); + assert.strictEqual(result.server.NPN, expected.server.NPN); + assert.strictEqual(result.client.ALPN, expected.client.ALPN); + assert.strictEqual(result.client.NPN, expected.client.NPN); +} + +function runTest(clientsOptions, serverOptions, cb) { + serverOptions.key = loadPEM('agent2-key'); + serverOptions.cert = loadPEM('agent2-cert'); + var results = []; + var index = 0; + var server = tls.createServer(serverOptions, function(c) { + results[index].server = {ALPN: c.alpnProtocol, NPN: c.npnProtocol}; + }); + + server.listen(serverPort, serverIP, function() { + connectClient(clientsOptions); + }); + + function connectClient(options) { + var opt = options.shift(); + opt.port = serverPort; + opt.host = serverIP; + opt.rejectUnauthorized = false; + + results[index] = {}; + var client = tls.connect(opt, function() { + results[index].client = {ALPN: client.alpnProtocol, + NPN: client.npnProtocol}; + client.destroy(); + if (options.length) { + index++; + connectClient(options); + } else { + server.close(); + cb(results); + } + }); + }; + +} + +// Server: ALPN/NPN, Client: ALPN/NPN +function Test1() { + var serverOptions = { + ALPNProtocols: ['a', 'b', 'c'], + NPNProtocols: ['a', 'b', 'c'] + }; + + var clientsOptions = [{ + ALPNProtocols: ['a', 'b', 'c'], + NPNProtocols: ['a', 'b', 'c'] + }, { + ALPNProtocols: ['c', 'b', 'e'], + NPNProtocols: ['c', 'b', 'e'] + }, { + ALPNProtocols: ['first-priority-unsupported', 'x', 'y'], + NPNProtocols: ['first-priority-unsupported', 'x', 'y'] + }]; + + runTest(clientsOptions, serverOptions, function(results) { + // 'a' is selected by ALPN + checkResults(results[0], + {server: {ALPN: 'a', NPN: false}, + client: {ALPN: 'a', NPN: undefined}}); + // 'b' is selected by ALPN + checkResults(results[1], + {server: {ALPN: 'b', NPN: false}, + client: {ALPN: 'b', NPN: undefined}}); + // nothing is selected by ALPN + checkResults(results[2], + {server: {ALPN: false, NPN: false}, + client: {ALPN: false, NPN: undefined}}); + // execute next test + Test2(); + }); +} + +// Server: ALPN/NPN, Client: ALPN +function Test2() { + var serverOptions = { + ALPNProtocols: ['a', 'b', 'c'], + NPNProtocols: ['a', 'b', 'c'] + }; + + var clientsOptions = [{ + ALPNProtocols: ['a', 'b', 'c'] + }, { + ALPNProtocols: ['c', 'b', 'e'] + }, { + ALPNProtocols: ['first-priority-unsupported', 'x', 'y'] + }]; + + runTest(clientsOptions, serverOptions, function(results) { + // 'a' is selected by ALPN + checkResults(results[0], + {server: {ALPN: 'a', NPN: false}, + client: {ALPN: 'a', NPN: undefined}}); + // 'b' is selected by ALPN + checkResults(results[1], + {server: {ALPN: 'b', NPN: false}, + client: {ALPN: 'b', NPN: undefined}}); + // nothing is selected by ALPN + checkResults(results[2], + {server: {ALPN: false, NPN: false}, + client: {ALPN: false, NPN: undefined}}); + // execute next test + Test3(); + }); +} + +// Server: ALPN/NPN, Client: NPN +function Test3() { + var serverOptions = { + ALPNProtocols: ['a', 'b', 'c'], + NPNProtocols: ['a', 'b', 'c'] + }; + + var clientsOptions = [{ + NPNProtocols: ['a', 'b', 'c'] + }, { + NPPNProtocols: ['c', 'b', 'e'] + }, { + NPPNProtocols: ['first-priority-unsupported', 'x', 'y'] + }]; + + runTest(clientsOptions, serverOptions, function(results) { + // 'a' is selected by NPN + checkResults(results[0], + {server: {ALPN: false, NPN: 'a'}, + client: {ALPN: false, NPN: 'a'}}); + // nothing is selected by ALPN + checkResults(results[1], + {server: {ALPN: false, NPN: 'http/1.1'}, + client: {ALPN: false, NPN: false}}); + // nothing is selected by ALPN + checkResults(results[2], + {server: {ALPN: false, NPN: 'http/1.1'}, + client: {ALPN: false, NPN: false}}); + // execute next test + Test4(); + }); +} + +// Server: ALPN/NPN, Client: Nothing +function Test4() { + var serverOptions = { + ALPNProtocols: ['a', 'b', 'c'], + NPNProtocols: ['a', 'b', 'c'] + }; + + var clientsOptions = [{}, {}, {}]; + + runTest(clientsOptions, serverOptions, function(results) { + // nothing is selected by ALPN + checkResults(results[0], + {server: {ALPN: false, NPN: 'http/1.1'}, + client: {ALPN: false, NPN: false}}); + // nothing is selected by ALPN + checkResults(results[1], + {server: {ALPN: false, NPN: 'http/1.1'}, + client: {ALPN: false, NPN: false}}); + // nothing is selected by ALPN + checkResults(results[2], + {server: {ALPN: false, NPN: 'http/1.1'}, + client: {ALPN: false, NPN: false}}); + // execute next test + Test5(); + }); +} + +// Server: ALPN, Client: ALPN/NPN +function Test5() { + var serverOptions = { + ALPNProtocols: ['a', 'b', 'c'] + }; + + var clientsOptions = [{ + ALPNProtocols: ['a', 'b', 'c'], + NPNProtocols: ['a', 'b', 'c'] + }, { + ALPNProtocols: ['c', 'b', 'e'], + NPNProtocols: ['c', 'b', 'e'] + }, { + ALPNProtocols: ['first-priority-unsupported', 'x', 'y'], + NPNProtocols: ['first-priority-unsupported', 'x', 'y'] + }]; + + runTest(clientsOptions, serverOptions, function(results) { + // 'a' is selected by ALPN + checkResults(results[0], {server: {ALPN: 'a', NPN: false}, + client: {ALPN: 'a', NPN: undefined}}); + // 'b' is selected by ALPN + checkResults(results[1], {server: {ALPN: 'b', NPN: false}, + client: {ALPN: 'b', NPN: undefined}}); + // nothing is selected by ALPN + checkResults(results[2], {server: {ALPN: false, NPN: false}, + client: {ALPN: false, NPN: undefined}}); + // execute next test + Test6(); + }); +} + +// Server: ALPN, Client: ALPN +function Test6() { + var serverOptions = { + ALPNProtocols: ['a', 'b', 'c'] + }; + + var clientsOptions = [{ + ALPNProtocols: ['a', 'b', 'c'] + }, { + ALPNProtocols: ['c', 'b', 'e'] + }, { + ALPNProtocols: ['first-priority-unsupported', 'x', 'y'] + }]; + + runTest(clientsOptions, serverOptions, function(results) { + // 'a' is selected by ALPN + checkResults(results[0], {server: {ALPN: 'a', NPN: false}, + client: {ALPN: 'a', NPN: undefined}}); + // 'b' is selected by ALPN + checkResults(results[1], {server: {ALPN: 'b', NPN: false}, + client: {ALPN: 'b', NPN: undefined}}); + // nothing is selected by ALPN + checkResults(results[2], {server: {ALPN: false, NPN: false}, + client: {ALPN: false, NPN: undefined}}); + // execute next test + Test7(); + }); +} + +// Server: ALPN, Client: NPN +function Test7() { + var serverOptions = { + ALPNProtocols: ['a', 'b', 'c'] + }; + + var clientsOptions = [{ + NPNProtocols: ['a', 'b', 'c'] + }, { + NPNProtocols: ['c', 'b', 'e'] + }, { + NPNProtocols: ['first-priority-unsupported', 'x', 'y'] + }]; + + runTest(clientsOptions, serverOptions, function(results) { + // nothing is selected by ALPN + checkResults(results[0], {server: {ALPN: false, NPN: 'a'}, + client: {ALPN: false, NPN: false}}); + // nothing is selected by ALPN + checkResults(results[1], {server: {ALPN: false, NPN: 'c'}, + client: {ALPN: false, NPN: false}}); + // nothing is selected by ALPN + checkResults(results[2], + {server: {ALPN: false, NPN: 'first-priority-unsupported'}, + client: {ALPN: false, NPN: false}}); + // execute next test + Test8(); + }); +} + +// Server: ALPN, Client: Nothing +function Test8() { + var serverOptions = { + ALPNProtocols: ['a', 'b', 'c'] + }; + + var clientsOptions = [{}, {}, {}]; + + runTest(clientsOptions, serverOptions, function(results) { + // nothing is selected by ALPN + checkResults(results[0], {server: {ALPN: false, NPN: 'http/1.1'}, + client: {ALPN: false, NPN: false}}); + // nothing is selected by ALPN + checkResults(results[1], {server: {ALPN: false, NPN: 'http/1.1'}, + client: {ALPN: false, NPN: false}}); + // nothing is selected by ALPN + checkResults(results[2], + {server: {ALPN: false, NPN: 'http/1.1'}, + client: {ALPN: false, NPN: false}}); + // execute next test + Test9(); + }); +} + +// Server: NPN, Client: ALPN/NPN +function Test9() { + var serverOptions = { + NPNProtocols: ['a', 'b', 'c'] + }; + + var clientsOptions = [{ + ALPNrotocols: ['a', 'b', 'c'], + NPNProtocols: ['a', 'b', 'c'] + }, { + ALPNProtocols: ['c', 'b', 'e'], + NPNProtocols: ['c', 'b', 'e'] + }, { + ALPNProtocols: ['first-priority-unsupported', 'x', 'y'], + NPNProtocols: ['first-priority-unsupported', 'x', 'y'] + }]; + + runTest(clientsOptions, serverOptions, function(results) { + // 'a' is selected by NPN + checkResults(results[0], {server: {ALPN: false, NPN: 'a'}, + client: {ALPN: false, NPN: 'a'}}); + // 'b' is selected by NPN + checkResults(results[1], {server: {ALPN: false, NPN: 'b'}, + client: {ALPN: false, NPN: 'b'}}); + // nothing is selected + checkResults(results[2], + {server: {ALPN: false, NPN: 'first-priority-unsupported'}, + client: {ALPN: false, NPN: false}}); + // execute next test + Test10(); + }); +} + +// Server: NPN, Client: ALPN +function Test10() { + var serverOptions = { + NPNProtocols: ['a', 'b', 'c'] + }; + + var clientsOptions = [{ + ALPNProtocols: ['a', 'b', 'c'] + }, { + ALPNProtocols: ['c', 'b', 'e'] + }, { + ALPNProtocols: ['first-priority-unsupported', 'x', 'y'] + }]; + + runTest(clientsOptions, serverOptions, function(results) { + // nothing is selected + checkResults(results[0], {server: {ALPN: false, NPN: 'http/1.1'}, + client: {ALPN: false, NPN: false}}); + // nothing is selected + checkResults(results[1], {server: {ALPN: false, NPN: 'http/1.1'}, + client: {ALPN: false, NPN: false}}); + // nothing is selected + checkResults(results[2], {server: {ALPN: false, NPN: 'http/1.1'}, + client: {ALPN: false, NPN: false}}); + // execute next test + Test11(); + }); +} + +// Server: NPN, Client: NPN +function Test11() { + var serverOptions = { + NPNProtocols: ['a', 'b', 'c'] + }; + + var clientsOptions = [{ + NPNProtocols: ['a', 'b', 'c'] + }, { + NPNProtocols: ['c', 'b', 'e'] + }, { + NPNProtocols: ['first-priority-unsupported', 'x', 'y'] + }]; + + runTest(clientsOptions, serverOptions, function(results) { + // 'a' is selected by NPN + checkResults(results[0], {server: {ALPN: false, NPN: 'a'}, + client: {ALPN: false, NPN: 'a'}}); + // 'b' is selected by NPN + checkResults(results[1], {server: {ALPN: false, NPN: 'b'}, + client: {ALPN: false, NPN: 'b'}}); + // nothing is selected + checkResults(results[2], + {server: {ALPN: false, NPN: 'first-priority-unsupported'}, + client: {ALPN: false, NPN: false}}); + // execute next test + Test12(); + }); +} + +// Server: NPN, Client: Nothing +function Test12() { + var serverOptions = { + NPNProtocols: ['a', 'b', 'c'] + }; + + var clientsOptions = [{}, {}, {}]; + + runTest(clientsOptions, serverOptions, function(results) { + // nothing is selected + checkResults(results[0], {server: {ALPN: false, NPN: 'http/1.1'}, + client: {ALPN: false, NPN: false}}); + // nothing is selected + checkResults(results[1], {server: {ALPN: false, NPN: 'http/1.1'}, + client: {ALPN: false, NPN: false}}); + // nothing is selected + checkResults(results[2], + {server: {ALPN: false, NPN: 'http/1.1'}, + client: {ALPN: false, NPN: false}}); + // execute next test + Test13(); + }); +} + +// Server: Nothing, Client: ALPN/NPN +function Test13() { + var serverOptions = {}; + + var clientsOptions = [{ + ALPNrotocols: ['a', 'b', 'c'], + NPNProtocols: ['a', 'b', 'c'] + }, { + ALPNProtocols: ['c', 'b', 'e'], + NPNProtocols: ['c', 'b', 'e'] + }, { + ALPNProtocols: ['first-priority-unsupported', 'x', 'y'], + NPNProtocols: ['first-priority-unsupported', 'x', 'y'] + }]; + + runTest(clientsOptions, serverOptions, function(results) { + // nothing is selected + checkResults(results[0], {server: {ALPN: false, NPN: 'a'}, + client: {ALPN: false, NPN: false}}); + // nothing is selected + checkResults(results[1], {server: {ALPN: false, NPN: 'c'}, + client: {ALPN: false, NPN: false}}); + // nothing is selected + checkResults(results[2], + {server: {ALPN: false, NPN: 'first-priority-unsupported'}, + client: {ALPN: false, NPN: false}}); + // execute next test + Test14(); + }); +} + +// Server: Nothing, Client: ALPN +function Test14() { + var serverOptions = {}; + + var clientsOptions = [{ + ALPNrotocols: ['a', 'b', 'c'] + }, { + ALPNProtocols: ['c', 'b', 'e'] + }, { + ALPNProtocols: ['first-priority-unsupported', 'x', 'y'] + }]; + + runTest(clientsOptions, serverOptions, function(results) { + // nothing is selected + checkResults(results[0], {server: {ALPN: false, NPN: 'http/1.1'}, + client: {ALPN: false, NPN: false}}); + // nothing is selected + checkResults(results[1], {server: {ALPN: false, NPN: 'http/1.1'}, + client: {ALPN: false, NPN: false}}); + // nothing is selected + checkResults(results[2], + {server: {ALPN: false, NPN: 'http/1.1'}, + client: {ALPN: false, NPN: false}}); + // execute next test + Test15(); + }); +} + +// Server: Nothing, Client: NPN +function Test15() { + var serverOptions = {}; + + var clientsOptions = [{ + NPNProtocols: ['a', 'b', 'c'] + }, { + NPNProtocols: ['c', 'b', 'e'] + }, { + NPNProtocols: ['first-priority-unsupported', 'x', 'y'] + }]; + + runTest(clientsOptions, serverOptions, function(results) { + // nothing is selected + checkResults(results[0], {server: {ALPN: false, NPN: 'a'}, + client: {ALPN: false, NPN: false}}); + // nothing is selected + checkResults(results[1], {server: {ALPN: false, NPN: 'c'}, + client: {ALPN: false, NPN: false}}); + // nothing is selected + checkResults(results[2], + {server: {ALPN: false, NPN: 'first-priority-unsupported'}, + client: {ALPN: false, NPN: false}}); + // execute next test + Test16(); + }); +} + +// Server: Nothing, Client: Nothing +function Test16() { + var serverOptions = {}; + + var clientsOptions = [{}, {}, {}]; + + runTest(clientsOptions, serverOptions, function(results) { + // nothing is selected + checkResults(results[0], {server: {ALPN: false, NPN: 'http/1.1'}, + client: {ALPN: false, NPN: false}}); + // nothing is selected + checkResults(results[1], {server: {ALPN: false, NPN: 'http/1.1'}, + client: {ALPN: false, NPN: false}}); + // nothing is selected + checkResults(results[2], + {server: {ALPN: false, NPN: 'http/1.1'}, + client: {ALPN: false, NPN: false}}); + }); +} + +Test1();