From d6d461003f1b58e65d4d5869008abc538ebe2ddf Mon Sep 17 00:00:00 2001 From: Mattias Holmlund Date: Thu, 10 Aug 2017 15:11:19 +0200 Subject: [PATCH] http, tls: better support for IPv6 addresses - Properly handle IPv6 in Host header when setting servername. - When comparing IP addresses against addresses in the subjectAltName field of a certificate, format the address correctly before doing the string comparison. PR-URL: https://github.com/nodejs/node/pull/14772 Fixes: https://github.com/nodejs/node/issues/14736 Reviewed-By: Ben Noordhuis Reviewed-By: Anatoli Papirovski Reviewed-By: James M Snell --- lib/_http_agent.js | 41 +++++++++++++++++--------- lib/tls.js | 5 ++-- src/cares_wrap.cc | 22 ++++++++++++++ test/parallel/test-tls-canonical-ip.js | 31 +++++++++++++++++++ 4 files changed, 83 insertions(+), 16 deletions(-) create mode 100644 test/parallel/test-tls-canonical-ip.js diff --git a/lib/_http_agent.js b/lib/_http_agent.js index e3f9a58437330f..564eab9254387b 100644 --- a/lib/_http_agent.js +++ b/lib/_http_agent.js @@ -153,13 +153,8 @@ Agent.prototype.addRequest = function addRequest(req, options, port/*legacy*/, if (options.socketPath) options.path = options.socketPath; - if (!options.servername) { - options.servername = options.host; - const hostHeader = req.getHeader('host'); - if (hostHeader) { - options.servername = hostHeader.replace(/:.*$/, ''); - } - } + if (!options.servername) + options.servername = calculateServerName(options, req); var name = this.getName(options); if (!this.sockets[name]) { @@ -207,13 +202,8 @@ Agent.prototype.createSocket = function createSocket(req, options, cb) { if (options.socketPath) options.path = options.socketPath; - if (!options.servername) { - options.servername = options.host; - const hostHeader = req.getHeader('host'); - if (hostHeader) { - options.servername = hostHeader.replace(/:.*$/, ''); - } - } + if (!options.servername) + options.servername = calculateServerName(options, req); var name = self.getName(options); options._agentKey = name; @@ -241,6 +231,29 @@ Agent.prototype.createSocket = function createSocket(req, options, cb) { } }; +function calculateServerName(options, req) { + let servername = options.host; + const hostHeader = req.getHeader('host'); + if (hostHeader) { + // abc => abc + // abc:123 => abc + // [::1] => ::1 + // [::1]:123 => ::1 + if (hostHeader.startsWith('[')) { + const index = hostHeader.indexOf(']'); + if (index === -1) { + // Leading '[', but no ']'. Need to do something... + servername = hostHeader; + } else { + servername = hostHeader.substr(1, index - 1); + } + } else { + servername = hostHeader.split(':', 1)[0]; + } + } + return servername; +} + function installListeners(agent, s, options) { function onFree() { debug('CLIENT socket onFree'); diff --git a/lib/tls.js b/lib/tls.js index 2d1c539532f9bf..a82535df618f99 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -31,6 +31,7 @@ const net = require('net'); const url = require('url'); const binding = process.binding('crypto'); const Buffer = require('buffer').Buffer; +const canonicalizeIP = process.binding('cares_wrap').canonicalizeIP; // Allow {CLIENT_RENEG_LIMIT} client-initiated session renegotiations // every {CLIENT_RENEG_WINDOW} seconds. An error event is emitted if more @@ -181,7 +182,7 @@ exports.checkServerIdentity = function checkServerIdentity(host, cert) { const uri = url.parse(name.slice(4)); uriNames.push(uri.hostname); // TODO(bnoordhuis) Also use scheme. } else if (name.startsWith('IP Address:')) { - ips.push(name.slice(11)); + ips.push(canonicalizeIP(name.slice(11))); } } } @@ -190,7 +191,7 @@ exports.checkServerIdentity = function checkServerIdentity(host, cert) { let reason = 'Unknown reason'; if (net.isIP(host)) { - valid = ips.includes(host); + valid = ips.includes(canonicalizeIP(host)); if (!valid) reason = `IP: ${host} is not in the cert's list: ${ips.join(', ')}`; // TODO(bnoordhuis) Also check URI SANs that are IP addresses. diff --git a/src/cares_wrap.cc b/src/cares_wrap.cc index 2cd494fca81a13..7c09b01d80c570 100644 --- a/src/cares_wrap.cc +++ b/src/cares_wrap.cc @@ -1945,6 +1945,27 @@ void IsIPv6(const FunctionCallbackInfo& args) { } } +void CanonicalizeIP(const FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + node::Utf8Value ip(isolate, args[0]); + char address_buffer[sizeof(struct in6_addr)]; + char canonical_ip[INET6_ADDRSTRLEN]; + + int af; + if (uv_inet_pton(AF_INET, *ip, &address_buffer) == 0) + af = AF_INET; + else if (uv_inet_pton(AF_INET6, *ip, &address_buffer) == 0) + af = AF_INET6; + else + return; + + int err = uv_inet_ntop(af, address_buffer, canonical_ip, + sizeof(canonical_ip)); + CHECK_EQ(err, 0); + + args.GetReturnValue().Set(String::NewFromUtf8(isolate, canonical_ip)); +} + void GetAddrInfo(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); @@ -2165,6 +2186,7 @@ void Initialize(Local target, env->SetMethod(target, "isIP", IsIP); env->SetMethod(target, "isIPv4", IsIPv4); env->SetMethod(target, "isIPv6", IsIPv6); + env->SetMethod(target, "canonicalizeIP", CanonicalizeIP); env->SetMethod(target, "strerror", StrError); diff --git a/test/parallel/test-tls-canonical-ip.js b/test/parallel/test-tls-canonical-ip.js new file mode 100644 index 00000000000000..408948119c3501 --- /dev/null +++ b/test/parallel/test-tls-canonical-ip.js @@ -0,0 +1,31 @@ +'use strict'; +require('../common'); + +// Test conversion of IP addresses to the format returned +// for addresses in Subject Alternative Name section +// of a TLS certificate + +const assert = require('assert'); +const { canonicalizeIP } = process.binding('cares_wrap'); + +assert.strictEqual(canonicalizeIP('127.0.0.1'), '127.0.0.1'); +assert.strictEqual(canonicalizeIP('10.1.0.1'), '10.1.0.1'); +assert.strictEqual(canonicalizeIP('::1'), '::1'); +assert.strictEqual(canonicalizeIP('fe80:0:0:0:0:0:0:1'), 'fe80::1'); +assert.strictEqual(canonicalizeIP('fe80:0:0:0:0:0:0:0'), 'fe80::'); +assert.strictEqual(canonicalizeIP('fe80::0000:0010:0001'), 'fe80::10:1'); +assert.strictEqual(canonicalizeIP('0001:2222:3333:4444:5555:6666:7777:0088'), + '1:2222:3333:4444:5555:6666:7777:88'); + +assert.strictEqual(canonicalizeIP('0001:2222:3333:4444:5555:6666::'), + '1:2222:3333:4444:5555:6666::'); + +assert.strictEqual(canonicalizeIP('a002:B12:00Ba:4444:5555:6666:0:0'), + 'a002:b12:ba:4444:5555:6666::'); + +// IPv4 address represented in IPv6 +assert.strictEqual(canonicalizeIP('0:0:0:0:0:ffff:c0a8:101'), + '::ffff:192.168.1.1'); + +assert.strictEqual(canonicalizeIP('::ffff:192.168.1.1'), + '::ffff:192.168.1.1');