| 'use strict'; |
| const common = require('../common'); |
| |
| if (!common.hasCrypto) |
| common.skip('missing crypto'); |
| |
| const assert = require('assert'); |
| const { spawn } = require('child_process'); |
| const tls = require('tls'); |
| const fixtures = require('../common/fixtures'); |
| |
| function loadPEM(n) { |
| return fixtures.readKey(`${n}.pem`); |
| } |
| |
| const serverIP = common.localhostIPv4; |
| |
| function checkResults(result, expected) { |
| assert.strictEqual(result.server.ALPN, expected.server.ALPN); |
| assert.strictEqual(result.client.ALPN, expected.client.ALPN); |
| } |
| |
| function runTest(clientsOptions, serverOptions, cb) { |
| serverOptions.key = loadPEM('agent2-key'); |
| serverOptions.cert = loadPEM('agent2-cert'); |
| const results = []; |
| let clientIndex = 0; |
| let serverIndex = 0; |
| const server = tls.createServer(serverOptions, function(c) { |
| results[serverIndex++].server = { ALPN: c.alpnProtocol }; |
| }); |
| |
| server.listen(0, serverIP, function() { |
| connectClient(clientsOptions); |
| }); |
| |
| function connectClient(options) { |
| const opt = options.shift(); |
| opt.port = server.address().port; |
| opt.host = serverIP; |
| opt.rejectUnauthorized = false; |
| |
| results[clientIndex] = {}; |
| |
| function startNextClient() { |
| if (options.length) { |
| clientIndex++; |
| connectClient(options); |
| } else { |
| server.close(); |
| server.on('close', () => { |
| cb(results); |
| }); |
| } |
| } |
| |
| const client = tls.connect(opt, function() { |
| results[clientIndex].client = { ALPN: client.alpnProtocol }; |
| client.end(); |
| startNextClient(); |
| }).on('error', function(err) { |
| results[clientIndex].client = { error: err }; |
| startNextClient(); |
| }); |
| } |
| |
| } |
| |
| // Server: ALPN, Client: ALPN |
| function Test1() { |
| const serverOptions = { |
| ALPNProtocols: ['a', 'b', 'c'], |
| }; |
| |
| const clientsOptions = [{ |
| ALPNProtocols: ['a', 'b', 'c'], |
| }, { |
| ALPNProtocols: ['c', 'b', 'e'], |
| }, { |
| ALPNProtocols: ['x', 'y', 'c'], |
| }]; |
| |
| runTest(clientsOptions, serverOptions, function(results) { |
| // 'a' is selected by ALPN |
| checkResults(results[0], |
| { server: { ALPN: 'a' }, |
| client: { ALPN: 'a' } }); |
| // 'b' is selected by ALPN |
| checkResults(results[1], |
| { server: { ALPN: 'b' }, |
| client: { ALPN: 'b' } }); |
| // Nothing is selected by ALPN |
| checkResults(results[2], |
| { server: { ALPN: 'c' }, |
| client: { ALPN: 'c' } }); |
| // execute next test |
| Test2(); |
| }); |
| } |
| |
| // Server: ALPN, Client: Nothing |
| function Test2() { |
| const serverOptions = { |
| ALPNProtocols: ['a', 'b', 'c'], |
| }; |
| |
| const clientsOptions = [{}, {}, {}]; |
| |
| runTest(clientsOptions, serverOptions, function(results) { |
| // Nothing is selected by ALPN |
| checkResults(results[0], |
| { server: { ALPN: false }, |
| client: { ALPN: false } }); |
| // Nothing is selected by ALPN |
| checkResults(results[1], |
| { server: { ALPN: false }, |
| client: { ALPN: false } }); |
| // Nothing is selected by ALPN |
| checkResults(results[2], |
| { server: { ALPN: false }, |
| client: { ALPN: false } }); |
| // execute next test |
| Test3(); |
| }); |
| } |
| |
| // Server: Nothing, Client: ALPN |
| function Test3() { |
| const serverOptions = {}; |
| |
| const 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 }, |
| client: { ALPN: false } }); |
| // nothing is selected |
| checkResults(results[1], { server: { ALPN: false }, |
| client: { ALPN: false } }); |
| // nothing is selected |
| checkResults(results[2], |
| { server: { ALPN: false }, |
| client: { ALPN: false } }); |
| // execute next test |
| Test4(); |
| }); |
| } |
| |
| // Server: Nothing, Client: Nothing |
| function Test4() { |
| const serverOptions = {}; |
| |
| const clientsOptions = [{}, {}, {}]; |
| |
| runTest(clientsOptions, serverOptions, function(results) { |
| // nothing is selected |
| checkResults(results[0], { server: { ALPN: false }, |
| client: { ALPN: false } }); |
| // nothing is selected |
| checkResults(results[1], { server: { ALPN: false }, |
| client: { ALPN: false } }); |
| // nothing is selected |
| checkResults(results[2], |
| { server: { ALPN: false }, |
| client: { ALPN: false } }); |
| }); |
| |
| TestFatalAlert(); |
| } |
| |
| function TestFatalAlert() { |
| const server = tls.createServer({ |
| ALPNProtocols: ['foo'], |
| key: loadPEM('agent2-key'), |
| cert: loadPEM('agent2-cert') |
| }, common.mustNotCall()); |
| |
| server.listen(0, serverIP, common.mustCall(() => { |
| const { port } = server.address(); |
| |
| // The Node.js client will just report ECONNRESET (older OpenSSL) or |
| // ERR_SSL_TLSV1_ALERT_NO_APPLICATION_PROTOCOL because the connection |
| // is severed before the TLS handshake completes. |
| tls.connect({ |
| host: serverIP, |
| port, |
| rejectUnauthorized: false, |
| ALPNProtocols: ['bar'] |
| }, common.mustNotCall()).on('error', common.mustCall((err) => { |
| const allowedErrors = ['ECONNRESET', 'ERR_SSL_TLSV1_ALERT_NO_APPLICATION_PROTOCOL']; |
| assert.ok(allowedErrors.includes(err.code), `'${err.code}' was not one of ${allowedErrors}.`); |
| |
| // OpenSSL's s_client should output the TLS alert number, which is 120 |
| // for the 'no_application_protocol' alert. |
| const { opensslCli } = common; |
| if (opensslCli) { |
| const addr = `${serverIP}:${port}`; |
| let stderr = ''; |
| spawn(opensslCli, ['s_client', '--alpn', 'bar', addr], { |
| stdio: ['ignore', 'ignore', 'pipe'] |
| }).stderr |
| .setEncoding('utf8') |
| .on('data', (chunk) => stderr += chunk) |
| .on('close', common.mustCall(() => { |
| assert.match(stderr, /SSL alert number 120/); |
| server.close(); |
| TestALPNCallback(); |
| })); |
| } else { |
| server.close(); |
| TestALPNCallback(); |
| } |
| })); |
| })); |
| } |
| |
| function TestALPNCallback() { |
| // Server always selects the client's 2nd preference: |
| const serverOptions = { |
| ALPNCallback: common.mustCall(({ protocols }) => { |
| return protocols[1]; |
| }, 2) |
| }; |
| |
| const clientsOptions = [{ |
| ALPNProtocols: ['a', 'b', 'c'], |
| }, { |
| ALPNProtocols: ['a'], |
| }]; |
| |
| runTest(clientsOptions, serverOptions, function(results) { |
| // Callback picks 2nd preference => picks 'b' |
| checkResults(results[0], |
| { server: { ALPN: 'b' }, |
| client: { ALPN: 'b' } }); |
| |
| // Callback picks 2nd preference => undefined => ALPN rejected: |
| assert.strictEqual(results[1].server, undefined); |
| const allowedErrors = ['ECONNRESET', 'ERR_SSL_TLSV1_ALERT_NO_APPLICATION_PROTOCOL']; |
| assert.ok(allowedErrors.includes(results[1].client.error.code), `'${results[1].client.error.code}' was not one of ${allowedErrors}.`); |
| |
| TestBadALPNCallback(); |
| }); |
| } |
| |
| function TestBadALPNCallback() { |
| // Server always returns a fixed invalid value: |
| const serverOptions = { |
| ALPNCallback: common.mustCall(() => 'http/5') |
| }; |
| |
| const clientsOptions = [{ |
| ALPNProtocols: ['http/1', 'h2'], |
| }]; |
| |
| process.once('uncaughtException', common.mustCall((error) => { |
| assert.strictEqual(error.code, 'ERR_TLS_ALPN_CALLBACK_INVALID_RESULT'); |
| })); |
| |
| runTest(clientsOptions, serverOptions, function(results) { |
| // Callback returns 'http/5' => doesn't match client ALPN => error & reset |
| assert.strictEqual(results[0].server, undefined); |
| const allowedErrors = ['ECONNRESET', 'ERR_SSL_TLSV1_ALERT_NO_APPLICATION_PROTOCOL']; |
| assert.ok(allowedErrors.includes(results[0].client.error.code), `'${results[0].client.error.code}' was not one of ${allowedErrors}.`); |
| |
| TestALPNOptionsCallback(); |
| }); |
| } |
| |
| function TestALPNOptionsCallback() { |
| // Server sets two incompatible ALPN options: |
| assert.throws(() => tls.createServer({ |
| ALPNCallback: () => 'a', |
| ALPNProtocols: ['b', 'c'] |
| }), (error) => error.code === 'ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS'); |
| } |
| |
| Test1(); |