1'use strict';
2const common = require('../common');
3
4if (!common.hasCrypto)
5  common.skip('missing crypto');
6
7const assert = require('assert');
8const tls = require('tls');
9const fixtures = require('../common/fixtures');
10
11function loadPEM(n) {
12  return fixtures.readKey(`${n}.pem`);
13}
14
15const serverIP = common.localhostIPv4;
16
17function checkResults(result, expected) {
18  assert.strictEqual(result.server.ALPN, expected.server.ALPN);
19  assert.strictEqual(result.client.ALPN, expected.client.ALPN);
20}
21
22function runTest(clientsOptions, serverOptions, cb) {
23  serverOptions.key = loadPEM('agent2-key');
24  serverOptions.cert = loadPEM('agent2-cert');
25  const results = [];
26  let clientIndex = 0;
27  let serverIndex = 0;
28  const server = tls.createServer(serverOptions, function(c) {
29    results[serverIndex++].server = { ALPN: c.alpnProtocol };
30  });
31
32  server.listen(0, serverIP, function() {
33    connectClient(clientsOptions);
34  });
35
36  function connectClient(options) {
37    const opt = options.shift();
38    opt.port = server.address().port;
39    opt.host = serverIP;
40    opt.rejectUnauthorized = false;
41
42    results[clientIndex] = {};
43
44    function startNextClient() {
45      if (options.length) {
46        clientIndex++;
47        connectClient(options);
48      } else {
49        server.close();
50        server.on('close', () => {
51          cb(results);
52        });
53      }
54    }
55
56    const client = 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();
63    });
64  }
65
66}
67
68// Server: ALPN, Client: ALPN
69function Test1() {
70  const serverOptions = {
71    ALPNProtocols: ['a', 'b', 'c'],
72  };
73
74  const clientsOptions = [{
75    ALPNProtocols: ['a', 'b', 'c'],
76  }, {
77    ALPNProtocols: ['c', 'b', 'e'],
78  }, {
79    ALPNProtocols: ['first-priority-unsupported', 'x', 'y'],
80  }];
81
82  runTest(clientsOptions, serverOptions, function(results) {
83    // 'a' is selected by ALPN
84    checkResults(results[0],
85                 { server: { ALPN: 'a' },
86                   client: { ALPN: 'a' } });
87    // 'b' is selected by ALPN
88    checkResults(results[1],
89                 { server: { ALPN: 'b' },
90                   client: { ALPN: 'b' } });
91    // Nothing is selected by ALPN
92    checkResults(results[2],
93                 { server: { ALPN: false },
94                   client: { ALPN: false } });
95    // execute next test
96    Test2();
97  });
98}
99
100// Server: ALPN, Client: Nothing
101function Test2() {
102  const serverOptions = {
103    ALPNProtocols: ['a', 'b', 'c'],
104  };
105
106  const clientsOptions = [{}, {}, {}];
107
108  runTest(clientsOptions, serverOptions, function(results) {
109    // Nothing is selected by ALPN
110    checkResults(results[0],
111                 { server: { ALPN: false },
112                   client: { ALPN: false } });
113    // Nothing is selected by ALPN
114    checkResults(results[1],
115                 { server: { ALPN: false },
116                   client: { ALPN: false } });
117    // Nothing is selected by ALPN
118    checkResults(results[2],
119                 { server: { ALPN: false },
120                   client: { ALPN: false } });
121    // execute next test
122    Test3();
123  });
124}
125
126// Server: Nothing, Client: ALPN
127function Test3() {
128  const serverOptions = {};
129
130  const clientsOptions = [{
131    ALPNrotocols: ['a', 'b', 'c'],
132  }, {
133    ALPNProtocols: ['c', 'b', 'e'],
134  }, {
135    ALPNProtocols: ['first-priority-unsupported', 'x', 'y'],
136  }];
137
138  runTest(clientsOptions, serverOptions, function(results) {
139    // nothing is selected
140    checkResults(results[0], { server: { ALPN: false },
141                               client: { ALPN: false } });
142    // nothing is selected
143    checkResults(results[1], { server: { ALPN: false },
144                               client: { ALPN: false } });
145    // nothing is selected
146    checkResults(results[2],
147                 { server: { ALPN: false },
148                   client: { ALPN: false } });
149    // execute next test
150    Test4();
151  });
152}
153
154// Server: Nothing, Client: Nothing
155function Test4() {
156  const serverOptions = {};
157
158  const clientsOptions = [{}, {}, {}];
159
160  runTest(clientsOptions, serverOptions, function(results) {
161    // nothing is selected
162    checkResults(results[0], { server: { ALPN: false },
163                               client: { ALPN: false } });
164    // nothing is selected
165    checkResults(results[1], { server: { ALPN: false },
166                               client: { ALPN: false } });
167    // nothing is selected
168    checkResults(results[2],
169                 { server: { ALPN: false },
170                   client: { ALPN: false } });
171  });
172
173  TestALPNCallback();
174}
175
176function TestALPNCallback() {
177  // Server always selects the client's 2nd preference:
178  const serverOptions = {
179    ALPNCallback: common.mustCall(({ protocols }) => {
180      return protocols[1];
181    }, 2)
182  };
183
184  const clientsOptions = [{
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
204function TestBadALPNCallback() {
205  // Server always returns a fixed invalid value:
206  const serverOptions = {
207    ALPNCallback: common.mustCall(() => 'http/5')
208  };
209
210  const clientsOptions = [{
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
227function TestALPNOptionsCallback() {
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');
233}
234
235Test1();
236