node/test/parallel/test-tls-alpn-server-client.js
Richard Lau 14863e8058
test: update tests for OpenSSL 3.0.14
Starting from OpenSSL 3.0.14, 3.1.6, 3.2.2, and 3.3.1, OpenSSL was fixed
to return an error reason string for bad/unknown application protocols.

Update tests to handle both the old `ECONNRESET` error on older versions
of OpenSSL and the new `ERR_SSL_TLSV1_ALERT_NO_APPLICATION_PROTOCOL` on
newer versions of OpenSSL.

Refs: https://github.com/openssl/openssl/pull/24338
PR-URL: https://github.com/nodejs/node/pull/53373
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Michael Dawson <midawson@redhat.com>
2024-06-10 17:43:36 +00:00

285 lines
8.2 KiB
JavaScript

'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();