From a94ffdaec57d9999485b11183f8da10aec15ff8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Walukiewicz?= Date: Thu, 26 Jan 2012 00:12:00 +0100 Subject: [PATCH] url: Support for IPv6 addresses in URLs. Fixes #1138, #2610. --- lib/url.js | 75 +++++++++++++++++++++++++++-------------- test/simple/test-url.js | 65 +++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 26 deletions(-) diff --git a/lib/url.js b/lib/url.js index 7b4e1b397f6..4f1c0e0e9c4 100644 --- a/lib/url.js +++ b/lib/url.js @@ -35,7 +35,7 @@ var protocolPattern = /^([a-z0-9.+-]+:)/i, // RFC 2396: characters reserved for delimiting URLs. delims = ['<', '>', '"', '`', ' ', '\r', '\n', '\t'], // RFC 2396: characters not allowed for various reasons. - unwise = ['{', '}', '|', '\\', '^', '~', '[', ']', '`'].concat(delims), + unwise = ['{', '}', '|', '\\', '^', '~', '`'].concat(delims), // Allowed by RFCs, but cause of XSS attacks. Always escape these. autoEscape = ['\''], // Characters that are never ever allowed in a hostname. @@ -179,10 +179,15 @@ function urlParse(url, parseQueryString, slashesDenoteHost) { // so even if it's empty, it has to be present. out.hostname = out.hostname || ''; + // if hostname begins with [ and ends with ] + // assume that it's an IPv6 address. + var ipv6Hostname = out.hostname[0] === '[' && + out.hostname[out.hostname.length - 1] === ']'; + // validate a little. if (out.hostname.length > hostnameMaxLen) { out.hostname = ''; - } else { + } else if (!ipv6Hostname) { var hostparts = out.hostname.split(/\./); for (var i = 0, l = hostparts.length; i < l; i++) { var part = hostparts[i]; @@ -221,22 +226,32 @@ function urlParse(url, parseQueryString, slashesDenoteHost) { // hostnames are always lower case. out.hostname = out.hostname.toLowerCase(); - // IDNA Support: Returns a puny coded representation of "domain". - // It only converts the part of the domain name that - // has non ASCII characters. I.e. it dosent matter if - // you call it with a domain that already is in ASCII. - var domainArray = out.hostname.split('.'); - var newOut = []; - for (var i = 0; i < domainArray.length; ++i) { - var s = domainArray[i]; - newOut.push(s.match(/[^A-Za-z0-9_-]/) ? - 'xn--' + punycode.encode(s) : s); + if (!ipv6Hostname) { + // IDNA Support: Returns a puny coded representation of "domain". + // It only converts the part of the domain name that + // has non ASCII characters. I.e. it dosent matter if + // you call it with a domain that already is in ASCII. + var domainArray = out.hostname.split('.'); + var newOut = []; + for (var i = 0; i < domainArray.length; ++i) { + var s = domainArray[i]; + newOut.push(s.match(/[^A-Za-z0-9_-]/) ? + 'xn--' + punycode.encode(s) : s); + } + out.hostname = newOut.join('.'); } - out.hostname = newOut.join('.'); out.host = (out.hostname || '') + ((out.port) ? ':' + out.port : ''); out.href += out.host; + + // strip [ and ] from the hostname + if (ipv6Hostname) { + out.hostname = out.hostname.substr(1, out.hostname.length - 2); + if (rest[0] !== '/') { + rest = '/' + rest; + } + } } // now rest is set to the post-host stuff. @@ -323,20 +338,28 @@ function urlFormat(obj) { } var protocol = obj.protocol || '', - host = (obj.host !== undefined) ? auth + obj.host : - obj.hostname !== undefined ? ( - auth + obj.hostname + - (obj.port ? ':' + obj.port : '') - ) : - false, pathname = obj.pathname || '', - query = obj.query && - ((typeof obj.query === 'object' && - Object.keys(obj.query).length) ? - querystring.stringify(obj.query) : - '') || '', - search = obj.search || (query && ('?' + query)) || '', - hash = obj.hash || ''; + hash = obj.hash || '', + host = false, + query = ''; + + if (obj.host !== undefined) { + host = auth + obj.host; + } else if (obj.hostname !== undefined) { + host = auth + (obj.hostname.indexOf(':') === -1 ? + obj.hostname : + '[' + obj.hostname + ']'); + if (obj.port) { + host += ':' + obj.port; + } + } + + if (obj.query && typeof obj.query === 'object' && + Object.keys(obj.query).length) { + query = querystring.stringify(obj.query); + } + + var search = obj.search || (query && ('?' + query)) || ''; if (protocol && protocol.substr(-1) !== ':') protocol += ':'; diff --git a/test/simple/test-url.js b/test/simple/test-url.js index 6a847b246da..fbbdbbbf478 100644 --- a/test/simple/test-url.js +++ b/test/simple/test-url.js @@ -476,6 +476,55 @@ var parseTests = { 'href': 'www.example.com', 'pathname': 'www.example.com', 'path': 'www.example.com' + }, + // ipv6 support + '[fe80::1]': { + 'href': '[fe80::1]', + 'pathname': '[fe80::1]', + 'path': '[fe80::1]' + }, + 'coap://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]': { + 'protocol': 'coap:', + 'slashes': true, + 'host': '[fedc:ba98:7654:3210:fedc:ba98:7654:3210]', + 'hostname': 'fedc:ba98:7654:3210:fedc:ba98:7654:3210', + 'href': 'coap://[fedc:ba98:7654:3210:fedc:ba98:7654:3210]/', + 'pathname': '/', + 'path': '/' + }, + 'coap://[1080:0:0:0:8:800:200C:417A]:61616/': { + 'protocol': 'coap:', + 'slashes': true, + 'host': '[1080:0:0:0:8:800:200c:417a]:61616', + 'port': '61616', + 'hostname': '1080:0:0:0:8:800:200c:417a', + 'href': 'coap://[1080:0:0:0:8:800:200c:417a]:61616/', + 'pathname': '/', + 'path': '/' + }, + 'http://user:password@[3ffe:2a00:100:7031::1]:8080': { + 'protocol': 'http:', + 'slashes': true, + 'auth': 'user:password', + 'host': '[3ffe:2a00:100:7031::1]:8080', + 'port': '8080', + 'hostname': '3ffe:2a00:100:7031::1', + 'href': 'http://user:password@[3ffe:2a00:100:7031::1]:8080/', + 'pathname': '/', + 'path': '/' + }, + 'coap://u:p@[::192.9.5.5]:61616/.well-known/r?n=Temperature': { + 'protocol': 'coap:', + 'slashes': true, + 'auth': 'u:p', + 'host': '[::192.9.5.5]:61616', + 'port': '61616', + 'hostname': '::192.9.5.5', + 'href': 'coap://u:p@[::192.9.5.5]:61616/.well-known/r?n=Temperature', + 'search': '?n=Temperature', + 'query': 'n=Temperature', + 'pathname': '/.well-known/r', + 'path': '/.well-known/r?n=Temperature' } }; @@ -650,6 +699,22 @@ var formatTests = { 'hostname': 'foo', 'protocol': 'dot.test:', 'pathname': '/bar' + }, + // ipv6 support + 'coap:u:p@[::1]:61616/.well-known/r?n=Temperature': { + 'href': 'coap:u:p@[::1]:61616/.well-known/r?n=Temperature', + 'protocol': 'coap:', + 'auth': 'u:p', + 'hostname': '::1', + 'port': '61616', + 'pathname': '/.well-known/r', + 'search': 'n=Temperature' + }, + 'coap:[fedc:ba98:7654:3210:fedc:ba98:7654:3210]:61616/s/stopButton': { + 'href': 'coap:[fedc:ba98:7654:3210:fedc:ba98:7654:3210]:61616/s/stopButton', + 'protocol': 'coap', + 'host': '[fedc:ba98:7654:3210:fedc:ba98:7654:3210]:61616', + 'pathname': '/s/stopButton' } }; for (var u in formatTests) {