current commit
251124.3 · test: ensure assertions are reached on more tests
'use strict';

const common = require('../common');

if (!common.hasCrypto) {
  common.skip('missing crypto');
}

const { parseDNSPacket, writeDNSPacket } = require('../common/dns');
const fixtures = require('../common/fixtures');

const assert = require('assert');
const dgram = require('dgram');
const { Resolver } = require('dns');
const { request, createServer } = require('https');

if (!common.hasCrypto)
  common.skip('missing crypto');

const options = {
  key: fixtures.readKey('agent1-key.pem'),
  cert: fixtures.readKey('agent1-cert.pem')
};

// Test that happy eyeballs algorithm is properly implemented when using HTTP.

function _lookup(resolver, hostname, options, cb) {
  resolver.resolve(hostname, 'ANY', common.mustCall((err, replies) => {
    assert.notStrictEqual(options.family, 4);

    if (err) {
      return cb(err);
    }

    const hosts = replies
      .map((r) => ({ address: r.address, family: r.type === 'AAAA' ? 6 : 4 }))
      .sort((a, b) => b.family - a.family);

    if (options.all === true) {
      return cb(null, hosts);
    }

    return cb(null, hosts[0].address, hosts[0].family);
  }));
}

function createDnsServer(ipv6Addr, ipv4Addr, cb) {
  // Create a DNS server which replies with a AAAA and a A record for the same host
  const socket = dgram.createSocket('udp4');

  socket.on('message', common.mustCall((msg, { address, port }) => {
    const parsed = parseDNSPacket(msg);
    const domain = parsed.questions[0].domain;
    assert.strictEqual(domain, 'example.org');

    socket.send(writeDNSPacket({
      id: parsed.id,
      questions: parsed.questions,
      answers: [
        { type: 'AAAA', address: ipv6Addr, ttl: 123, domain: 'example.org' },
        { type: 'A', address: ipv4Addr, ttl: 123, domain: 'example.org' },
      ]
    }), port, address);
  }));

  socket.bind(0, () => {
    const resolver = new Resolver();
    resolver.setServers([`127.0.0.1:${socket.address().port}`]);

    cb({ dnsServer: socket, lookup: _lookup.bind(null, resolver) });
  });
}

// Test that IPV4 is reached if IPV6 is not reachable
{
  createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
    const ipv4Server = createServer(options, common.mustCall((req, res) => {
      assert.strictEqual(req.socket.servername, 'example.org');
      res.writeHead(200, { Connection: 'close' });
      res.end('response-ipv4');
    }));

    ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
      request(
        `https://example.org:${ipv4Server.address().port}/`,
        {
          lookup,
          rejectUnauthorized: false,
          autoSelectFamily: true,
          servername: 'example.org',
        },
        common.mustCall((res) => {
          assert.strictEqual(res.statusCode, 200);
          res.setEncoding('utf-8');

          let response = '';

          res.on('data', (chunk) => {
            response += chunk;
          });

          res.on('end', common.mustCall(() => {
            assert.strictEqual(response, 'response-ipv4');
            ipv4Server.close();
            dnsServer.close();
          }));
        })
      ).end();
    }));
  }));
}

// Test that IPV4 is NOT reached if IPV6 is reachable
if (common.hasIPv6) {
  createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
    const ipv4Server = createServer(options, common.mustNotCall());

    const ipv6Server = createServer(options, common.mustCall((req, res) => {
      assert.strictEqual(req.socket.servername, 'example.org');
      res.writeHead(200, { Connection: 'close' });
      res.end('response-ipv6');
    }));

    ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
      const port = ipv4Server.address().port;

      ipv6Server.listen(port, '::1', common.mustCall(() => {
        request(
          `https://example.org:${ipv4Server.address().port}/`,
          {
            lookup,
            rejectUnauthorized: false,
            autoSelectFamily: true,
            servername: 'example.org',
          },
          common.mustCall((res) => {
            assert.strictEqual(res.statusCode, 200);
            res.setEncoding('utf-8');

            let response = '';

            res.on('data', (chunk) => {
              response += chunk;
            });

            res.on('end', common.mustCall(() => {
              assert.strictEqual(response, 'response-ipv6');
              ipv4Server.close();
              ipv6Server.close();
              dnsServer.close();
            }));
          })
        ).end();
      }));
    }));
  }));
}