diff --git a/internal/url.js b/internal/url.js new file mode 100644 index 0000000..7ff89c7 --- /dev/null +++ b/internal/url.js @@ -0,0 +1,629 @@ +'use strict'; + +function getPunycode() { + try { + return process.binding('icu'); + } catch (err) { + return require('punycode'); + } +} +const punycode = getPunycode(); +const binding = require('../url'); +const context = Symbol('context'); +const cannotBeBase = Symbol('cannot-be-base'); +const special = Symbol('special'); +const searchParams = Symbol('query'); +const querystring = require('querystring'); + +const kScheme = Symbol('scheme'); +const kHost = Symbol('host'); +const kPort = Symbol('port'); +const kDomain = Symbol('domain'); + +function StorageObject() {} +StorageObject.prototype = Object.create(null); + +class OpaqueOrigin { + toString() { + return 'null'; + } + + get effectiveDomain() { + return this; + } +} + +class TupleOrigin { + constructor(scheme, host, port, domain) { + this[kScheme] = scheme; + this[kHost] = host; + this[kPort] = port; + this[kDomain] = domain; + } + + get scheme() { + return this[kScheme]; + } + + get host() { + return this[kHost]; + } + + get port() { + return this[kPort]; + } + + get domain() { + return this[kDomain]; + } + + get effectiveDomain() { + return this[kDomain] || this[kHost]; + } + + toString(unicode = false) { + var result = this.scheme; + result += '://'; + result += unicode ? URL.domainToUnicode(this.host) : this.host; + if (this.port !== undefined && this.port !== null) + result += `:${this.port}`; + return result; + } +} + +class URL { + constructor(input, base) { + if (base !== undefined && !(base instanceof URL)) + base = new URL(String(base)); + input = String(input); + const base_context = base ? base[context] : undefined; + this[context] = new StorageObject(); + binding.parse(input.trim(), -1, base_context, undefined, + (flags, protocol, username, password, + host, port, path, query, fragment) => { + if (flags & binding.URL_FLAGS_FAILED) + throw new TypeError('Invalid URL'); + this[context].flags = flags; + this[context].scheme = protocol; + this[context].username = username; + this[context].password = password; + this[context].port = port; + this[context].path = path; + this[context].query = query; + this[context].fragment = fragment; + this[context].host = host; + this[searchParams] = new URLSearchParams(this); + }); + } + + get origin() { + return URL.originFor(this).toString(true); + } + + get [special]() { + return (this[context].flags & binding.URL_FLAGS_SPECIAL) != 0; + } + + get [cannotBeBase]() { + return (this[context].flags & binding.URL_FLAGS_CANNOT_BE_BASE) != 0; + } + + get protocol() { + return this[context].scheme; + } + + get searchParams() { + return this[searchParams]; + } + + set protocol(scheme) { + scheme = String(scheme); + if (scheme.length === 0) + return; + binding.parse(scheme, + binding.kSchemeStart, + null, + this[context], + (flags, protocol, username, password, + host, port, path, query, fragment) => { + if (flags & binding.URL_FLAGS_FAILED) + return; + const newIsSpecial = (flags & binding.URL_FLAGS_SPECIAL) != 0; + if ((this[special] && !newIsSpecial) || + (!this[special] && newIsSpecial) || + (newIsSpecial && !this[special] && + this[context].host === undefined)) { + return; + } + if (newIsSpecial) { + this[context].flags |= binding.URL_FLAGS_SPECIAL; + } else { + this[context].flags &= ~binding.URL_FLAGS_SPECIAL; + } + if (protocol) { + this[context].scheme = protocol; + this[context].flags |= binding.URL_FLAGS_HAS_SCHEME; + } else { + this[context].flags &= ~binding.URL_FLAGS_HAS_SCHEME; + } + }); + } + + get username() { + return this[context].username || ''; + } + + set username(username) { + username = String(username); + if (!this.hostname) + return; + if (!username) { + this[context].username = null; + this[context].flags &= ~binding.URL_FLAGS_HAS_USERNAME; + return; + } + this[context].username = binding.encodeAuth(username); + this[context].flags |= binding.URL_FLAGS_HAS_USERNAME; + } + + get password() { + return this[context].password || ''; + } + + set password(password) { + password = String(password); + if (!this.hostname) + return; + if (!password) { + this[context].password = null; + this[context].flags &= ~binding.URL_FLAGS_HAS_PASSWORD; + return; + } + this[context].password = binding.encodeAuth(password); + this[context].flags |= binding.URL_FLAGS_HAS_PASSWORD; + } + + get host() { + var ret = this[context].host || ''; + if (this[context].port !== undefined) + ret += `:${this[context].port}`; + return ret; + } + + set host(host) { + host = String(host); + if (this[cannotBeBase] || + (this[special] && host.length === 0)) { + // Cannot set the host if cannot-be-base is set or + // scheme is special and host length is zero + return; + } + if (!host) { + this[context].host = null; + this[context].flags &= ~binding.URL_FLAGS_HAS_HOST; + return; + } + binding.parse(host, binding.kHost, null, this[context], + (flags, protocol, username, password, + host, port, path, query, fragment) => { + if (flags & binding.URL_FLAGS_FAILED) + return; + if (host) { + this[context].host = host; + this[context].flags |= binding.URL_FLAGS_HAS_HOST; + } else { + this[context].flags &= ~binding.URL_FLAGS_HAS_HOST; + } + if (port !== undefined) + this[context].port = port; + }); + } + + get hostname() { + return this[context].host || ''; + } + + set hostname(host) { + host = String(host); + if (this[cannotBeBase] || + (this[special] && host.length === 0)) { + // Cannot set the host if cannot-be-base is set or + // scheme is special and host length is zero + return; + } + if (!host) { + this[context].host = null; + this[context].flags &= ~binding.URL_FLAGS_HAS_HOST; + return; + } + binding.parse(host, + binding.kHostname, + null, + this[context], + (flags, protocol, username, password, + host, port, path, query, fragment) => { + if (flags & binding.URL_FLAGS_FAILED) + return; + if (host) { + this[context].host = host; + this[context].flags |= binding.URL_FLAGS_HAS_HOST; + } else { + this[context].flags &= ~binding.URL_FLAGS_HAS_HOST; + } + }); + } + + get port() { + const port = this[context].port; + return port === undefined ? '' : String(port); + } + + set port(port) { + if (!this[context].host || this[cannotBeBase] || this.protocol === 'file:') + return; + port = String(port); + if (port === '') { + // Currently, if port number is empty, left unchanged. + // TODO(jasnell): This might be changing in the spec + return; + } + binding.parse(port, binding.kPort, null, this[context], + (flags, protocol, username, password, + host, port, path, query, fragment) => { + if (flags & binding.URL_FLAGS_FAILED) + return; + this[context].port = port; + }); + } + + get pathname() { + if (this[cannotBeBase]) + return this[context].path[0]; + return this[context].path !== undefined ? + `/${this[context].path.join('/')}` : ''; + } + + set pathname(path) { + if (this[cannotBeBase]) + return; + path = String(path); + binding.parse(path, + binding.kPathStart, + null, + this[context], + (flags, protocol, username, password, + host, port, path, query, fragment) => { + if (flags & binding.URL_FLAGS_FAILED) + return; + if (path) { + this[context].path = path; + this[context].flags |= binding.URL_FLAGS_HAS_PATH; + } else { + this[context].flags &= ~binding.URL_FLAGS_HAS_PATH; + } + }); + } + + get search() { + return !this[context].query ? '' : `?${this[context].query}`; + } + + set search(search) { + update(this, search); + this[searchParams][searchParams] = querystring.parse(this.search); + } + + get hash() { + return !this[context].fragment ? '' : `#${this[context].fragment}`; + } + + set hash(hash) { + hash = String(hash); + if (this.protocol === 'javascript:') + return; + if (!hash) { + this[context].fragment = null; + this[context].flags &= ~binding.URL_FLAGS_HAS_FRAGMENT; + return; + } + if (hash[0] === '#') hash = hash.slice(1); + this[context].fragment = ''; + binding.parse(hash, + binding.kFragment, + null, + this[context], + (flags, protocol, username, password, + host, port, path, query, fragment) => { + if (flags & binding.URL_FLAGS_FAILED) + return; + if (fragment) { + this[context].fragment = fragment; + this[context].flags |= binding.URL_FLAGS_HAS_FRAGMENT; + } else { + this[context].flags &= ~binding.URL_FLAGS_HAS_FRAGMENT; + } + }); + } + + get href() { + return this.toString(); + } + + toString(options) { + options = options || {}; + const fragment = + options.fragment !== undefined ? + !!options.fragment : true; + const unicode = !!options.unicode; + var ret; + if (this.protocol) + ret = this.protocol; + if (this[context].host !== undefined) { + ret += '//'; + const has_username = typeof this[context].username === 'string'; + const has_password = typeof this[context].password === 'string'; + if (has_username || has_password) { + if (has_username) + ret += this[context].username; + if (has_password) + ret += `:${this[context].password}`; + ret += '@'; + } + if (unicode) { + ret += punycode.toUnicode(this.hostname); + if (this.port !== undefined) + ret += `:${this.port}`; + } else { + ret += this.host; + } + } else if (this[context].scheme === 'file:') { + ret += '//'; + } + if (this.pathname) + ret += this.pathname; + if (typeof this[context].query === 'string') + ret += `?${this[context].query}`; + if (fragment & typeof this[context].fragment === 'string') + ret += `#${this[context].fragment}`; + return ret; + } + + inspect(depth, opts) { + var ret = 'URL {\n'; + ret += ` href: ${this.href}\n`; + if (this[context].scheme !== undefined) + ret += ` protocol: ${this.protocol}\n`; + if (this[context].username !== undefined) + ret += ` username: ${this.username}\n`; + if (this[context].password !== undefined) { + const pwd = opts.showHidden ? this[context].password : '--------'; + ret += ` password: ${pwd}\n`; + } + if (this[context].host !== undefined) + ret += ` hostname: ${this.hostname}\n`; + if (this[context].port !== undefined) + ret += ` port: ${this.port}\n`; + if (this[context].path !== undefined) + ret += ` pathname: ${this.pathname}\n`; + if (this[context].query !== undefined) + ret += ` search: ${this.search}\n`; + if (this[context].fragment !== undefined) + ret += ` hash: ${this.hash}\n`; + if (opts.showHidden) { + ret += ` cannot-be-base: ${this[cannotBeBase]}\n`; + ret += ` special: ${this[special]}\n;`; + } + ret += '}'; + return ret; + } +} + +var hexTable = new Array(256); +for (var i = 0; i < 256; ++i) + hexTable[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase(); +function encodeAuth(str) { + // faster encodeURIComponent alternative for encoding auth uri components + var out = ''; + var lastPos = 0; + for (var i = 0; i < str.length; ++i) { + var c = str.charCodeAt(i); + + // These characters do not need escaping: + // ! - . _ ~ + // ' ( ) * : + // digits + // alpha (uppercase) + // alpha (lowercase) + if (c === 0x21 || c === 0x2D || c === 0x2E || c === 0x5F || c === 0x7E || + (c >= 0x27 && c <= 0x2A) || + (c >= 0x30 && c <= 0x3A) || + (c >= 0x41 && c <= 0x5A) || + (c >= 0x61 && c <= 0x7A)) { + continue; + } + + if (i - lastPos > 0) + out += str.slice(lastPos, i); + + lastPos = i + 1; + + // Other ASCII characters + if (c < 0x80) { + out += hexTable[c]; + continue; + } + + // Multi-byte characters ... + if (c < 0x800) { + out += hexTable[0xC0 | (c >> 6)] + hexTable[0x80 | (c & 0x3F)]; + continue; + } + if (c < 0xD800 || c >= 0xE000) { + out += hexTable[0xE0 | (c >> 12)] + + hexTable[0x80 | ((c >> 6) & 0x3F)] + + hexTable[0x80 | (c & 0x3F)]; + continue; + } + // Surrogate pair + ++i; + var c2; + if (i < str.length) + c2 = str.charCodeAt(i) & 0x3FF; + else + c2 = 0; + c = 0x10000 + (((c & 0x3FF) << 10) | c2); + out += hexTable[0xF0 | (c >> 18)] + + hexTable[0x80 | ((c >> 12) & 0x3F)] + + hexTable[0x80 | ((c >> 6) & 0x3F)] + + hexTable[0x80 | (c & 0x3F)]; + } + if (lastPos === 0) + return str; + if (lastPos < str.length) + return out + str.slice(lastPos); + return out; +} + +function update(url, search) { + search = String(search); + if (!search) { + url[context].query = null; + url[context].flags &= ~binding.URL_FLAGS_HAS_QUERY; + return; + } + if (search[0] === '?') search = search.slice(1); + url[context].query = ''; + binding.parse(search, + binding.kQuery, + null, + url[context], + (flags, protocol, username, password, + host, port, path, query, fragment) => { + if (flags & binding.URL_FLAGS_FAILED) + return; + if (query) { + url[context].query = query; + url[context].flags |= binding.URL_FLAGS_HAS_QUERY; + } else { + url[context].flags &= ~binding.URL_FLAGS_HAS_QUERY; + } + }); +} + +class URLSearchParams { + constructor(url) { + this[context] = url; + this[searchParams] = querystring.parse(url[context].search || ''); + } + + append(name, value) { + const obj = this[searchParams]; + name = String(name); + value = String(value); + var existing = obj[name]; + if (!existing) { + obj[name] = value; + } else if (Array.isArray(existing)) { + existing.push(value); + } else { + obj[name] = [existing, value]; + } + update(this[context], querystring.stringify(obj)); + } + + delete(name) { + const obj = this[searchParams]; + name = String(name); + delete obj[name]; + update(this[context], querystring.stringify(obj)); + } + + set(name, value) { + const obj = this[searchParams]; + name = String(name); + value = String(value); + obj[name] = value; + update(this[context], querystring.stringify(obj)); + } + + get(name) { + const obj = this[searchParams]; + name = String(name); + var value = obj[name]; + return Array.isArray(value) ? value[0] : value; + } + + getAll(name) { + const obj = this[searchParams]; + name = String(name); + var value = obj[name]; + return value === undefined ? [] : Array.isArray(value) ? value : [value]; + } + + has(name) { + const obj = this[searchParams]; + name = String(name); + return name in obj; + } + + *[Symbol.iterator]() { + const obj = this[searchParams]; + for (const name in obj) { + const value = obj[name]; + if (Array.isArray(value)) { + for (const item of value) + yield [name, item]; + } else { + yield [name, value]; + } + } + } + + toString() { + return querystring.stringify(this[searchParams]); + } +} + +URL.originFor = function(url) { + if (!(url instanceof URL)) + url = new URL(url); + var origin; + const protocol = url.protocol; + switch (protocol) { + case 'blob:': + if (url[context].path && url[context].path.length > 0) { + try { + return (new URL(url[context].path[0])).origin; + } catch (err) { + // fall through... do nothing + } + } + origin = new OpaqueOrigin(); + break; + case 'ftp:': + case 'gopher:': + case 'http:': + case 'https:': + case 'ws:': + case 'wss:': + case 'file': + origin = new TupleOrigin(protocol.slice(0, -1), + url[context].host, + url[context].port, + null); + break; + default: + origin = new OpaqueOrigin(); + } + return origin; +}; + +URL.domainToASCII = function(domain) { + return binding.domainToASCII(String(domain)); +}; +URL.domainToUnicode = function(domain) { + return binding.domainToUnicode(String(domain)); +}; + +exports.URL = URL; +exports.encodeAuth = encodeAuth; diff --git a/package.json b/package.json index e94743b..fb59234 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "devDependencies": { "assert": "1.1.1", "mocha": "1.18.2", + "node-util": "0.0.1", "zuul": "3.3.0" }, "scripts": { diff --git a/test.js b/test.js index 3b7d335..d9e5444 100644 --- a/test.js +++ b/test.js @@ -1,35 +1,38 @@ -// Copyright Joyent, Inc. and other Node contributors. +// Copyright Node.js contributors. All rights reserved. // -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: // -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. // -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. -var assert = require('assert'); +/* eslint-disable max-len */ +'use strict'; +const assert = require('assert'); +const inspect = require('node-util').inspect; -var url = require('./url'); +const url = require('./url'); // URLs to parse, and expected data // { url : parsed } +test('all the tests', function() { var parseTests = { - '//some_path' : { - 'href': '//some_path', - 'pathname': '//some_path', - 'path': '//some_path' + '//some_path': { + href: '//some_path', + pathname: '//some_path', + path: '//some_path' }, 'http:\\\\evil-phisher\\foo.html#h\\a\\s\\h': { @@ -78,507 +81,516 @@ var parseTests = { href: 'http://evil-phisher/foo.html' }, - 'HTTP://www.example.com/' : { - 'href': 'http://www.example.com/', - 'protocol': 'http:', - 'slashes': true, - 'host': 'www.example.com', - 'hostname': 'www.example.com', - 'pathname': '/', - 'path': '/' - }, - - 'HTTP://www.example.com' : { - 'href': 'http://www.example.com/', - 'protocol': 'http:', - 'slashes': true, - 'host': 'www.example.com', - 'hostname': 'www.example.com', - 'pathname': '/', - 'path': '/' - }, - - 'http://www.ExAmPlE.com/' : { - 'href': 'http://www.example.com/', - 'protocol': 'http:', - 'slashes': true, - 'host': 'www.example.com', - 'hostname': 'www.example.com', - 'pathname': '/', - 'path': '/' - }, - - 'http://user:pw@www.ExAmPlE.com/' : { - 'href': 'http://user:pw@www.example.com/', - 'protocol': 'http:', - 'slashes': true, - 'auth': 'user:pw', - 'host': 'www.example.com', - 'hostname': 'www.example.com', - 'pathname': '/', - 'path': '/' - }, - - 'http://USER:PW@www.ExAmPlE.com/' : { - 'href': 'http://USER:PW@www.example.com/', - 'protocol': 'http:', - 'slashes': true, - 'auth': 'USER:PW', - 'host': 'www.example.com', - 'hostname': 'www.example.com', - 'pathname': '/', - 'path': '/' - }, - - 'http://user@www.example.com/' : { - 'href': 'http://user@www.example.com/', - 'protocol': 'http:', - 'slashes': true, - 'auth': 'user', - 'host': 'www.example.com', - 'hostname': 'www.example.com', - 'pathname': '/', - 'path': '/' - }, - - 'http://user%3Apw@www.example.com/' : { - 'href': 'http://user:pw@www.example.com/', - 'protocol': 'http:', - 'slashes': true, - 'auth': 'user:pw', - 'host': 'www.example.com', - 'hostname': 'www.example.com', - 'pathname': '/', - 'path': '/' - }, - - 'http://x.com/path?that\'s#all, folks' : { - 'href': 'http://x.com/path?that%27s#all,%20folks', - 'protocol': 'http:', - 'slashes': true, - 'host': 'x.com', - 'hostname': 'x.com', - 'search': '?that%27s', - 'query': 'that%27s', - 'pathname': '/path', - 'hash': '#all,%20folks', - 'path': '/path?that%27s' - }, - - 'HTTP://X.COM/Y' : { - 'href': 'http://x.com/Y', - 'protocol': 'http:', - 'slashes': true, - 'host': 'x.com', - 'hostname': 'x.com', - 'pathname': '/Y', - 'path': '/Y' + 'HTTP://www.example.com/': { + href: 'http://www.example.com/', + protocol: 'http:', + slashes: true, + host: 'www.example.com', + hostname: 'www.example.com', + pathname: '/', + path: '/' + }, + + 'HTTP://www.example.com': { + href: 'http://www.example.com/', + protocol: 'http:', + slashes: true, + host: 'www.example.com', + hostname: 'www.example.com', + pathname: '/', + path: '/' + }, + + 'http://www.ExAmPlE.com/': { + href: 'http://www.example.com/', + protocol: 'http:', + slashes: true, + host: 'www.example.com', + hostname: 'www.example.com', + pathname: '/', + path: '/' + }, + + 'http://user:pw@www.ExAmPlE.com/': { + href: 'http://user:pw@www.example.com/', + protocol: 'http:', + slashes: true, + auth: 'user:pw', + host: 'www.example.com', + hostname: 'www.example.com', + pathname: '/', + path: '/' + }, + + 'http://USER:PW@www.ExAmPlE.com/': { + href: 'http://USER:PW@www.example.com/', + protocol: 'http:', + slashes: true, + auth: 'USER:PW', + host: 'www.example.com', + hostname: 'www.example.com', + pathname: '/', + path: '/' + }, + + 'http://user@www.example.com/': { + href: 'http://user@www.example.com/', + protocol: 'http:', + slashes: true, + auth: 'user', + host: 'www.example.com', + hostname: 'www.example.com', + pathname: '/', + path: '/' + }, + + 'http://user%3Apw@www.example.com/': { + href: 'http://user:pw@www.example.com/', + protocol: 'http:', + slashes: true, + auth: 'user:pw', + host: 'www.example.com', + hostname: 'www.example.com', + pathname: '/', + path: '/' + }, + + 'http://x.com/path?that\'s#all, folks': { + href: 'http://x.com/path?that%27s#all,%20folks', + protocol: 'http:', + slashes: true, + host: 'x.com', + hostname: 'x.com', + search: '?that%27s', + query: 'that%27s', + pathname: '/path', + hash: '#all,%20folks', + path: '/path?that%27s' + }, + + 'HTTP://X.COM/Y': { + href: 'http://x.com/Y', + protocol: 'http:', + slashes: true, + host: 'x.com', + hostname: 'x.com', + pathname: '/Y', + path: '/Y' + }, + + // whitespace in the front + ' http://www.example.com/': { + href: 'http://www.example.com/', + protocol: 'http:', + slashes: true, + host: 'www.example.com', + hostname: 'www.example.com', + pathname: '/', + path: '/' }, // + not an invalid host character // per https://url.spec.whatwg.org/#host-parsing - 'http://x.y.com+a/b/c' : { - 'href': 'http://x.y.com+a/b/c', - 'protocol': 'http:', - 'slashes': true, - 'host': 'x.y.com+a', - 'hostname': 'x.y.com+a', - 'pathname': '/b/c', - 'path': '/b/c' + 'http://x.y.com+a/b/c': { + href: 'http://x.y.com+a/b/c', + protocol: 'http:', + slashes: true, + host: 'x.y.com+a', + hostname: 'x.y.com+a', + pathname: '/b/c', + path: '/b/c' }, // an unexpected invalid char in the hostname. - 'HtTp://x.y.cOm;a/b/c?d=e#f gi' : { - 'href': 'http://x.y.com/;a/b/c?d=e#f%20g%3Ch%3Ei', - 'protocol': 'http:', - 'slashes': true, - 'host': 'x.y.com', - 'hostname': 'x.y.com', - 'pathname': ';a/b/c', - 'search': '?d=e', - 'query': 'd=e', - 'hash': '#f%20g%3Ch%3Ei', - 'path': ';a/b/c?d=e' + 'HtTp://x.y.cOm;a/b/c?d=e#f gi': { + href: 'http://x.y.com/;a/b/c?d=e#f%20g%3Ch%3Ei', + protocol: 'http:', + slashes: true, + host: 'x.y.com', + hostname: 'x.y.com', + pathname: ';a/b/c', + search: '?d=e', + query: 'd=e', + hash: '#f%20g%3Ch%3Ei', + path: ';a/b/c?d=e' }, // make sure that we don't accidentally lcast the path parts. - 'HtTp://x.y.cOm;A/b/c?d=e#f gi' : { - 'href': 'http://x.y.com/;A/b/c?d=e#f%20g%3Ch%3Ei', - 'protocol': 'http:', - 'slashes': true, - 'host': 'x.y.com', - 'hostname': 'x.y.com', - 'pathname': ';A/b/c', - 'search': '?d=e', - 'query': 'd=e', - 'hash': '#f%20g%3Ch%3Ei', - 'path': ';A/b/c?d=e' + 'HtTp://x.y.cOm;A/b/c?d=e#f gi': { + href: 'http://x.y.com/;A/b/c?d=e#f%20g%3Ch%3Ei', + protocol: 'http:', + slashes: true, + host: 'x.y.com', + hostname: 'x.y.com', + pathname: ';A/b/c', + search: '?d=e', + query: 'd=e', + hash: '#f%20g%3Ch%3Ei', + path: ';A/b/c?d=e' }, 'http://x...y...#p': { - 'href': 'http://x...y.../#p', - 'protocol': 'http:', - 'slashes': true, - 'host': 'x...y...', - 'hostname': 'x...y...', - 'hash': '#p', - 'pathname': '/', - 'path': '/' + href: 'http://x...y.../#p', + protocol: 'http:', + slashes: true, + host: 'x...y...', + hostname: 'x...y...', + hash: '#p', + pathname: '/', + path: '/' }, 'http://x/p/"quoted"': { - 'href': 'http://x/p/%22quoted%22', - 'protocol': 'http:', - 'slashes': true, - 'host': 'x', - 'hostname': 'x', - 'pathname': '/p/%22quoted%22', - 'path': '/p/%22quoted%22' + href: 'http://x/p/%22quoted%22', + protocol: 'http:', + slashes: true, + host: 'x', + hostname: 'x', + pathname: '/p/%22quoted%22', + path: '/p/%22quoted%22' }, ' Is a URL!': { - 'href': '%3Chttp://goo.corn/bread%3E%20Is%20a%20URL!', - 'pathname': '%3Chttp://goo.corn/bread%3E%20Is%20a%20URL!', - 'path': '%3Chttp://goo.corn/bread%3E%20Is%20a%20URL!' - }, - - 'http://www.narwhaljs.org/blog/categories?id=news' : { - 'href': 'http://www.narwhaljs.org/blog/categories?id=news', - 'protocol': 'http:', - 'slashes': true, - 'host': 'www.narwhaljs.org', - 'hostname': 'www.narwhaljs.org', - 'search': '?id=news', - 'query': 'id=news', - 'pathname': '/blog/categories', - 'path': '/blog/categories?id=news' - }, - - 'http://mt0.google.com/vt/lyrs=m@114&hl=en&src=api&x=2&y=2&z=3&s=' : { - 'href': 'http://mt0.google.com/vt/lyrs=m@114&hl=en&src=api&x=2&y=2&z=3&s=', - 'protocol': 'http:', - 'slashes': true, - 'host': 'mt0.google.com', - 'hostname': 'mt0.google.com', - 'pathname': '/vt/lyrs=m@114&hl=en&src=api&x=2&y=2&z=3&s=', - 'path': '/vt/lyrs=m@114&hl=en&src=api&x=2&y=2&z=3&s=' - }, - - 'http://mt0.google.com/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=' : { - 'href': 'http://mt0.google.com/vt/lyrs=m@114???&hl=en&src=api' + - '&x=2&y=2&z=3&s=', - 'protocol': 'http:', - 'slashes': true, - 'host': 'mt0.google.com', - 'hostname': 'mt0.google.com', - 'search': '???&hl=en&src=api&x=2&y=2&z=3&s=', - 'query': '??&hl=en&src=api&x=2&y=2&z=3&s=', - 'pathname': '/vt/lyrs=m@114', - 'path': '/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=' - }, - - 'http://user:pass@mt0.google.com/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=': - { - 'href': 'http://user:pass@mt0.google.com/vt/lyrs=m@114???' + - '&hl=en&src=api&x=2&y=2&z=3&s=', - 'protocol': 'http:', - 'slashes': true, - 'host': 'mt0.google.com', - 'auth': 'user:pass', - 'hostname': 'mt0.google.com', - 'search': '???&hl=en&src=api&x=2&y=2&z=3&s=', - 'query': '??&hl=en&src=api&x=2&y=2&z=3&s=', - 'pathname': '/vt/lyrs=m@114', - 'path': '/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=' - }, - - 'file:///etc/passwd' : { - 'href': 'file:///etc/passwd', - 'slashes': true, - 'protocol': 'file:', - 'pathname': '/etc/passwd', - 'hostname': '', - 'host': '', - 'path': '/etc/passwd' - }, - - 'file://localhost/etc/passwd' : { - 'href': 'file://localhost/etc/passwd', - 'protocol': 'file:', - 'slashes': true, - 'pathname': '/etc/passwd', - 'hostname': 'localhost', - 'host': 'localhost', - 'path': '/etc/passwd' - }, - - 'file://foo/etc/passwd' : { - 'href': 'file://foo/etc/passwd', - 'protocol': 'file:', - 'slashes': true, - 'pathname': '/etc/passwd', - 'hostname': 'foo', - 'host': 'foo', - 'path': '/etc/passwd' - }, - - 'file:///etc/node/' : { - 'href': 'file:///etc/node/', - 'slashes': true, - 'protocol': 'file:', - 'pathname': '/etc/node/', - 'hostname': '', - 'host': '', - 'path': '/etc/node/' - }, - - 'file://localhost/etc/node/' : { - 'href': 'file://localhost/etc/node/', - 'protocol': 'file:', - 'slashes': true, - 'pathname': '/etc/node/', - 'hostname': 'localhost', - 'host': 'localhost', - 'path': '/etc/node/' - }, - - 'file://foo/etc/node/' : { - 'href': 'file://foo/etc/node/', - 'protocol': 'file:', - 'slashes': true, - 'pathname': '/etc/node/', - 'hostname': 'foo', - 'host': 'foo', - 'path': '/etc/node/' - }, - - 'http:/baz/../foo/bar' : { - 'href': 'http:/baz/../foo/bar', - 'protocol': 'http:', - 'pathname': '/baz/../foo/bar', - 'path': '/baz/../foo/bar' - }, - - 'http://user:pass@example.com:8000/foo/bar?baz=quux#frag' : { - 'href': 'http://user:pass@example.com:8000/foo/bar?baz=quux#frag', - 'protocol': 'http:', - 'slashes': true, - 'host': 'example.com:8000', - 'auth': 'user:pass', - 'port': '8000', - 'hostname': 'example.com', - 'hash': '#frag', - 'search': '?baz=quux', - 'query': 'baz=quux', - 'pathname': '/foo/bar', - 'path': '/foo/bar?baz=quux' - }, - - '//user:pass@example.com:8000/foo/bar?baz=quux#frag' : { - 'href': '//user:pass@example.com:8000/foo/bar?baz=quux#frag', - 'slashes': true, - 'host': 'example.com:8000', - 'auth': 'user:pass', - 'port': '8000', - 'hostname': 'example.com', - 'hash': '#frag', - 'search': '?baz=quux', - 'query': 'baz=quux', - 'pathname': '/foo/bar', - 'path': '/foo/bar?baz=quux' - }, - - '/foo/bar?baz=quux#frag' : { - 'href': '/foo/bar?baz=quux#frag', - 'hash': '#frag', - 'search': '?baz=quux', - 'query': 'baz=quux', - 'pathname': '/foo/bar', - 'path': '/foo/bar?baz=quux' - }, - - 'http:/foo/bar?baz=quux#frag' : { - 'href': 'http:/foo/bar?baz=quux#frag', - 'protocol': 'http:', - 'hash': '#frag', - 'search': '?baz=quux', - 'query': 'baz=quux', - 'pathname': '/foo/bar', - 'path': '/foo/bar?baz=quux' - }, - - 'mailto:foo@bar.com?subject=hello' : { - 'href': 'mailto:foo@bar.com?subject=hello', - 'protocol': 'mailto:', - 'host': 'bar.com', - 'auth' : 'foo', - 'hostname' : 'bar.com', - 'search': '?subject=hello', - 'query': 'subject=hello', - 'path': '?subject=hello' - }, - - 'javascript:alert(\'hello\');' : { - 'href': 'javascript:alert(\'hello\');', - 'protocol': 'javascript:', - 'pathname': 'alert(\'hello\');', - 'path': 'alert(\'hello\');' - }, - - 'xmpp:isaacschlueter@jabber.org' : { - 'href': 'xmpp:isaacschlueter@jabber.org', - 'protocol': 'xmpp:', - 'host': 'jabber.org', - 'auth': 'isaacschlueter', - 'hostname': 'jabber.org' - }, - - 'http://atpass:foo%40bar@127.0.0.1:8080/path?search=foo#bar' : { - 'href' : 'http://atpass:foo%40bar@127.0.0.1:8080/path?search=foo#bar', - 'protocol' : 'http:', - 'slashes': true, - 'host' : '127.0.0.1:8080', - 'auth' : 'atpass:foo@bar', - 'hostname' : '127.0.0.1', - 'port' : '8080', - 'pathname': '/path', - 'search' : '?search=foo', - 'query' : 'search=foo', - 'hash' : '#bar', - 'path': '/path?search=foo' + href: '%3Chttp://goo.corn/bread%3E%20Is%20a%20URL!', + pathname: '%3Chttp://goo.corn/bread%3E%20Is%20a%20URL!', + path: '%3Chttp://goo.corn/bread%3E%20Is%20a%20URL!' + }, + + 'http://www.narwhaljs.org/blog/categories?id=news': { + href: 'http://www.narwhaljs.org/blog/categories?id=news', + protocol: 'http:', + slashes: true, + host: 'www.narwhaljs.org', + hostname: 'www.narwhaljs.org', + search: '?id=news', + query: 'id=news', + pathname: '/blog/categories', + path: '/blog/categories?id=news' + }, + + 'http://mt0.google.com/vt/lyrs=m@114&hl=en&src=api&x=2&y=2&z=3&s=': { + href: 'http://mt0.google.com/vt/lyrs=m@114&hl=en&src=api&x=2&y=2&z=3&s=', + protocol: 'http:', + slashes: true, + host: 'mt0.google.com', + hostname: 'mt0.google.com', + pathname: '/vt/lyrs=m@114&hl=en&src=api&x=2&y=2&z=3&s=', + path: '/vt/lyrs=m@114&hl=en&src=api&x=2&y=2&z=3&s=' + }, + + 'http://mt0.google.com/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=': { + href: 'http://mt0.google.com/vt/lyrs=m@114???&hl=en&src=api' + + '&x=2&y=2&z=3&s=', + protocol: 'http:', + slashes: true, + host: 'mt0.google.com', + hostname: 'mt0.google.com', + search: '???&hl=en&src=api&x=2&y=2&z=3&s=', + query: '??&hl=en&src=api&x=2&y=2&z=3&s=', + pathname: '/vt/lyrs=m@114', + path: '/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=' + }, + + 'http://user:pass@mt0.google.com/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=': { + href: 'http://user:pass@mt0.google.com/vt/lyrs=m@114???' + + '&hl=en&src=api&x=2&y=2&z=3&s=', + protocol: 'http:', + slashes: true, + host: 'mt0.google.com', + auth: 'user:pass', + hostname: 'mt0.google.com', + search: '???&hl=en&src=api&x=2&y=2&z=3&s=', + query: '??&hl=en&src=api&x=2&y=2&z=3&s=', + pathname: '/vt/lyrs=m@114', + path: '/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=' + }, + + 'file:///etc/passwd': { + href: 'file:///etc/passwd', + slashes: true, + protocol: 'file:', + pathname: '/etc/passwd', + hostname: '', + host: '', + path: '/etc/passwd' + }, + + 'file://localhost/etc/passwd': { + href: 'file://localhost/etc/passwd', + protocol: 'file:', + slashes: true, + pathname: '/etc/passwd', + hostname: 'localhost', + host: 'localhost', + path: '/etc/passwd' + }, + + 'file://foo/etc/passwd': { + href: 'file://foo/etc/passwd', + protocol: 'file:', + slashes: true, + pathname: '/etc/passwd', + hostname: 'foo', + host: 'foo', + path: '/etc/passwd' + }, + + 'file:///etc/node/': { + href: 'file:///etc/node/', + slashes: true, + protocol: 'file:', + pathname: '/etc/node/', + hostname: '', + host: '', + path: '/etc/node/' + }, + + 'file://localhost/etc/node/': { + href: 'file://localhost/etc/node/', + protocol: 'file:', + slashes: true, + pathname: '/etc/node/', + hostname: 'localhost', + host: 'localhost', + path: '/etc/node/' + }, + + 'file://foo/etc/node/': { + href: 'file://foo/etc/node/', + protocol: 'file:', + slashes: true, + pathname: '/etc/node/', + hostname: 'foo', + host: 'foo', + path: '/etc/node/' + }, + + 'http:/baz/../foo/bar': { + href: 'http:/baz/../foo/bar', + protocol: 'http:', + pathname: '/baz/../foo/bar', + path: '/baz/../foo/bar' + }, + + 'http://user:pass@example.com:8000/foo/bar?baz=quux#frag': { + href: 'http://user:pass@example.com:8000/foo/bar?baz=quux#frag', + protocol: 'http:', + slashes: true, + host: 'example.com:8000', + auth: 'user:pass', + port: '8000', + hostname: 'example.com', + hash: '#frag', + search: '?baz=quux', + query: 'baz=quux', + pathname: '/foo/bar', + path: '/foo/bar?baz=quux' + }, + + '//user:pass@example.com:8000/foo/bar?baz=quux#frag': { + href: '//user:pass@example.com:8000/foo/bar?baz=quux#frag', + slashes: true, + host: 'example.com:8000', + auth: 'user:pass', + port: '8000', + hostname: 'example.com', + hash: '#frag', + search: '?baz=quux', + query: 'baz=quux', + pathname: '/foo/bar', + path: '/foo/bar?baz=quux' + }, + + '/foo/bar?baz=quux#frag': { + href: '/foo/bar?baz=quux#frag', + hash: '#frag', + search: '?baz=quux', + query: 'baz=quux', + pathname: '/foo/bar', + path: '/foo/bar?baz=quux' + }, + + 'http:/foo/bar?baz=quux#frag': { + href: 'http:/foo/bar?baz=quux#frag', + protocol: 'http:', + hash: '#frag', + search: '?baz=quux', + query: 'baz=quux', + pathname: '/foo/bar', + path: '/foo/bar?baz=quux' + }, + + 'mailto:foo@bar.com?subject=hello': { + href: 'mailto:foo@bar.com?subject=hello', + protocol: 'mailto:', + host: 'bar.com', + auth: 'foo', + hostname: 'bar.com', + search: '?subject=hello', + query: 'subject=hello', + path: '?subject=hello' + }, + + 'javascript:alert(\'hello\');': { + href: 'javascript:alert(\'hello\');', + protocol: 'javascript:', + pathname: 'alert(\'hello\');', + path: 'alert(\'hello\');' + }, + + 'xmpp:isaacschlueter@jabber.org': { + href: 'xmpp:isaacschlueter@jabber.org', + protocol: 'xmpp:', + host: 'jabber.org', + auth: 'isaacschlueter', + hostname: 'jabber.org' + }, + + 'http://atpass:foo%40bar@127.0.0.1:8080/path?search=foo#bar': { + href: 'http://atpass:foo%40bar@127.0.0.1:8080/path?search=foo#bar', + protocol: 'http:', + slashes: true, + host: '127.0.0.1:8080', + auth: 'atpass:foo@bar', + hostname: '127.0.0.1', + port: '8080', + pathname: '/path', + search: '?search=foo', + query: 'search=foo', + hash: '#bar', + path: '/path?search=foo' }, 'svn+ssh://foo/bar': { - 'href': 'svn+ssh://foo/bar', - 'host': 'foo', - 'hostname': 'foo', - 'protocol': 'svn+ssh:', - 'pathname': '/bar', - 'path': '/bar', - 'slashes': true + href: 'svn+ssh://foo/bar', + host: 'foo', + hostname: 'foo', + protocol: 'svn+ssh:', + pathname: '/bar', + path: '/bar', + slashes: true }, 'dash-test://foo/bar': { - 'href': 'dash-test://foo/bar', - 'host': 'foo', - 'hostname': 'foo', - 'protocol': 'dash-test:', - 'pathname': '/bar', - 'path': '/bar', - 'slashes': true + href: 'dash-test://foo/bar', + host: 'foo', + hostname: 'foo', + protocol: 'dash-test:', + pathname: '/bar', + path: '/bar', + slashes: true }, 'dash-test:foo/bar': { - 'href': 'dash-test:foo/bar', - 'host': 'foo', - 'hostname': 'foo', - 'protocol': 'dash-test:', - 'pathname': '/bar', - 'path': '/bar' + href: 'dash-test:foo/bar', + host: 'foo', + hostname: 'foo', + protocol: 'dash-test:', + pathname: '/bar', + path: '/bar' }, 'dot.test://foo/bar': { - 'href': 'dot.test://foo/bar', - 'host': 'foo', - 'hostname': 'foo', - 'protocol': 'dot.test:', - 'pathname': '/bar', - 'path': '/bar', - 'slashes': true + href: 'dot.test://foo/bar', + host: 'foo', + hostname: 'foo', + protocol: 'dot.test:', + pathname: '/bar', + path: '/bar', + slashes: true }, 'dot.test:foo/bar': { - 'href': 'dot.test:foo/bar', - 'host': 'foo', - 'hostname': 'foo', - 'protocol': 'dot.test:', - 'pathname': '/bar', - 'path': '/bar' + href: 'dot.test:foo/bar', + host: 'foo', + hostname: 'foo', + protocol: 'dot.test:', + pathname: '/bar', + path: '/bar' }, // IDNA tests - 'http://www.日本語.com/' : { - 'href': 'http://www.xn--wgv71a119e.com/', - 'protocol': 'http:', - 'slashes': true, - 'host': 'www.xn--wgv71a119e.com', - 'hostname': 'www.xn--wgv71a119e.com', - 'pathname': '/', - 'path': '/' - }, - - 'http://example.Bücher.com/' : { - 'href': 'http://example.xn--bcher-kva.com/', - 'protocol': 'http:', - 'slashes': true, - 'host': 'example.xn--bcher-kva.com', - 'hostname': 'example.xn--bcher-kva.com', - 'pathname': '/', - 'path': '/' - }, - - 'http://www.Äffchen.com/' : { - 'href': 'http://www.xn--ffchen-9ta.com/', - 'protocol': 'http:', - 'slashes': true, - 'host': 'www.xn--ffchen-9ta.com', - 'hostname': 'www.xn--ffchen-9ta.com', - 'pathname': '/', - 'path': '/' - }, - - 'http://www.Äffchen.cOm;A/b/c?d=e#f gi' : { - 'href': 'http://www.xn--ffchen-9ta.com/;A/b/c?d=e#f%20g%3Ch%3Ei', - 'protocol': 'http:', - 'slashes': true, - 'host': 'www.xn--ffchen-9ta.com', - 'hostname': 'www.xn--ffchen-9ta.com', - 'pathname': ';A/b/c', - 'search': '?d=e', - 'query': 'd=e', - 'hash': '#f%20g%3Ch%3Ei', - 'path': ';A/b/c?d=e' - }, - - 'http://SÉLIER.COM/' : { - 'href': 'http://xn--slier-bsa.com/', - 'protocol': 'http:', - 'slashes': true, - 'host': 'xn--slier-bsa.com', - 'hostname': 'xn--slier-bsa.com', - 'pathname': '/', - 'path': '/' - }, - - 'http://ليهمابتكلموشعربي؟.ي؟/' : { - 'href': 'http://xn--egbpdaj6bu4bxfgehfvwxn.xn--egb9f/', - 'protocol': 'http:', - 'slashes': true, - 'host': 'xn--egbpdaj6bu4bxfgehfvwxn.xn--egb9f', - 'hostname': 'xn--egbpdaj6bu4bxfgehfvwxn.xn--egb9f', - 'pathname': '/', - 'path': '/' - }, - - 'http://➡.ws/➡' : { - 'href': 'http://xn--hgi.ws/➡', - 'protocol': 'http:', - 'slashes': true, - 'host': 'xn--hgi.ws', - 'hostname': 'xn--hgi.ws', - 'pathname': '/➡', - 'path': '/➡' + 'http://www.日本語.com/': { + href: 'http://www.xn--wgv71a119e.com/', + protocol: 'http:', + slashes: true, + host: 'www.xn--wgv71a119e.com', + hostname: 'www.xn--wgv71a119e.com', + pathname: '/', + path: '/' + }, + + 'http://example.Bücher.com/': { + href: 'http://example.xn--bcher-kva.com/', + protocol: 'http:', + slashes: true, + host: 'example.xn--bcher-kva.com', + hostname: 'example.xn--bcher-kva.com', + pathname: '/', + path: '/' + }, + + 'http://www.Äffchen.com/': { + href: 'http://www.xn--ffchen-9ta.com/', + protocol: 'http:', + slashes: true, + host: 'www.xn--ffchen-9ta.com', + hostname: 'www.xn--ffchen-9ta.com', + pathname: '/', + path: '/' + }, + + 'http://www.Äffchen.cOm;A/b/c?d=e#f gi': { + href: 'http://www.xn--ffchen-9ta.com/;A/b/c?d=e#f%20g%3Ch%3Ei', + protocol: 'http:', + slashes: true, + host: 'www.xn--ffchen-9ta.com', + hostname: 'www.xn--ffchen-9ta.com', + pathname: ';A/b/c', + search: '?d=e', + query: 'd=e', + hash: '#f%20g%3Ch%3Ei', + path: ';A/b/c?d=e' + }, + + 'http://SÉLIER.COM/': { + href: 'http://xn--slier-bsa.com/', + protocol: 'http:', + slashes: true, + host: 'xn--slier-bsa.com', + hostname: 'xn--slier-bsa.com', + pathname: '/', + path: '/' + }, + + 'http://ليهمابتكلموشعربي؟.ي؟/': { + href: 'http://xn--egbpdaj6bu4bxfgehfvwxn.xn--egb9f/', + protocol: 'http:', + slashes: true, + host: 'xn--egbpdaj6bu4bxfgehfvwxn.xn--egb9f', + hostname: 'xn--egbpdaj6bu4bxfgehfvwxn.xn--egb9f', + pathname: '/', + path: '/' + }, + + 'http://➡.ws/➡': { + href: 'http://xn--hgi.ws/➡', + protocol: 'http:', + slashes: true, + host: 'xn--hgi.ws', + hostname: 'xn--hgi.ws', + pathname: '/➡', + path: '/➡' }, 'http://bucket_name.s3.amazonaws.com/image.jpg': { protocol: 'http:', - 'slashes': true, slashes: true, host: 'bucket_name.s3.amazonaws.com', hostname: 'bucket_name.s3.amazonaws.com', pathname: '/image.jpg', href: 'http://bucket_name.s3.amazonaws.com/image.jpg', - 'path': '/image.jpg' + path: '/image.jpg' }, 'git+http://github.com/joyent/node.git': { @@ -595,216 +607,216 @@ var parseTests = { //be parse into auth@hostname, but here there is no //way to make it work in url.parse, I add the test to be explicit 'local1@domain1': { - 'pathname': 'local1@domain1', - 'path': 'local1@domain1', - 'href': 'local1@domain1' + pathname: 'local1@domain1', + path: 'local1@domain1', + href: 'local1@domain1' }, //While this may seem counter-intuitive, a browser will parse // as a path. - 'www.example.com' : { - 'href': 'www.example.com', - 'pathname': 'www.example.com', - 'path': 'www.example.com' + 'www.example.com': { + 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]' + 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': '/' + 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': '/' + 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': '/' + 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' + 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' }, // empty port 'http://example.com:': { - 'protocol': 'http:', - 'slashes': true, - 'host': 'example.com', - 'hostname': 'example.com', - 'href': 'http://example.com/', - 'pathname': '/', - 'path': '/' + protocol: 'http:', + slashes: true, + host: 'example.com', + hostname: 'example.com', + href: 'http://example.com/', + pathname: '/', + path: '/' }, 'http://example.com:/a/b.html': { - 'protocol': 'http:', - 'slashes': true, - 'host': 'example.com', - 'hostname': 'example.com', - 'href': 'http://example.com/a/b.html', - 'pathname': '/a/b.html', - 'path': '/a/b.html' + protocol: 'http:', + slashes: true, + host: 'example.com', + hostname: 'example.com', + href: 'http://example.com/a/b.html', + pathname: '/a/b.html', + path: '/a/b.html' }, 'http://example.com:?a=b': { - 'protocol': 'http:', - 'slashes': true, - 'host': 'example.com', - 'hostname': 'example.com', - 'href': 'http://example.com/?a=b', - 'search': '?a=b', - 'query': 'a=b', - 'pathname': '/', - 'path': '/?a=b' + protocol: 'http:', + slashes: true, + host: 'example.com', + hostname: 'example.com', + href: 'http://example.com/?a=b', + search: '?a=b', + query: 'a=b', + pathname: '/', + path: '/?a=b' }, 'http://example.com:#abc': { - 'protocol': 'http:', - 'slashes': true, - 'host': 'example.com', - 'hostname': 'example.com', - 'href': 'http://example.com/#abc', - 'hash': '#abc', - 'pathname': '/', - 'path': '/' + protocol: 'http:', + slashes: true, + host: 'example.com', + hostname: 'example.com', + href: 'http://example.com/#abc', + hash: '#abc', + pathname: '/', + path: '/' }, 'http://[fe80::1]:/a/b?a=b#abc': { - 'protocol': 'http:', - 'slashes': true, - 'host': '[fe80::1]', - 'hostname': 'fe80::1', - 'href': 'http://[fe80::1]/a/b?a=b#abc', - 'search': '?a=b', - 'query': 'a=b', - 'hash': '#abc', - 'pathname': '/a/b', - 'path': '/a/b?a=b' + protocol: 'http:', + slashes: true, + host: '[fe80::1]', + hostname: 'fe80::1', + href: 'http://[fe80::1]/a/b?a=b#abc', + search: '?a=b', + query: 'a=b', + hash: '#abc', + pathname: '/a/b', + path: '/a/b?a=b' }, 'http://-lovemonsterz.tumblr.com/rss': { - 'protocol': 'http:', - 'slashes': true, - 'host': '-lovemonsterz.tumblr.com', - 'hostname': '-lovemonsterz.tumblr.com', - 'href': 'http://-lovemonsterz.tumblr.com/rss', - 'pathname': '/rss', - 'path': '/rss', + protocol: 'http:', + slashes: true, + host: '-lovemonsterz.tumblr.com', + hostname: '-lovemonsterz.tumblr.com', + href: 'http://-lovemonsterz.tumblr.com/rss', + pathname: '/rss', + path: '/rss', }, 'http://-lovemonsterz.tumblr.com:80/rss': { - 'protocol': 'http:', - 'slashes': true, - 'port': '80', - 'host': '-lovemonsterz.tumblr.com:80', - 'hostname': '-lovemonsterz.tumblr.com', - 'href': 'http://-lovemonsterz.tumblr.com:80/rss', - 'pathname': '/rss', - 'path': '/rss', + protocol: 'http:', + slashes: true, + port: '80', + host: '-lovemonsterz.tumblr.com:80', + hostname: '-lovemonsterz.tumblr.com', + href: 'http://-lovemonsterz.tumblr.com:80/rss', + pathname: '/rss', + path: '/rss', }, 'http://user:pass@-lovemonsterz.tumblr.com/rss': { - 'protocol': 'http:', - 'slashes': true, - 'auth': 'user:pass', - 'host': '-lovemonsterz.tumblr.com', - 'hostname': '-lovemonsterz.tumblr.com', - 'href': 'http://user:pass@-lovemonsterz.tumblr.com/rss', - 'pathname': '/rss', - 'path': '/rss', + protocol: 'http:', + slashes: true, + auth: 'user:pass', + host: '-lovemonsterz.tumblr.com', + hostname: '-lovemonsterz.tumblr.com', + href: 'http://user:pass@-lovemonsterz.tumblr.com/rss', + pathname: '/rss', + path: '/rss', }, 'http://user:pass@-lovemonsterz.tumblr.com:80/rss': { - 'protocol': 'http:', - 'slashes': true, - 'auth': 'user:pass', - 'port': '80', - 'host': '-lovemonsterz.tumblr.com:80', - 'hostname': '-lovemonsterz.tumblr.com', - 'href': 'http://user:pass@-lovemonsterz.tumblr.com:80/rss', - 'pathname': '/rss', - 'path': '/rss', + protocol: 'http:', + slashes: true, + auth: 'user:pass', + port: '80', + host: '-lovemonsterz.tumblr.com:80', + hostname: '-lovemonsterz.tumblr.com', + href: 'http://user:pass@-lovemonsterz.tumblr.com:80/rss', + pathname: '/rss', + path: '/rss', }, 'http://_jabber._tcp.google.com/test': { - 'protocol': 'http:', - 'slashes': true, - 'host': '_jabber._tcp.google.com', - 'hostname': '_jabber._tcp.google.com', - 'href': 'http://_jabber._tcp.google.com/test', - 'pathname': '/test', - 'path': '/test', + protocol: 'http:', + slashes: true, + host: '_jabber._tcp.google.com', + hostname: '_jabber._tcp.google.com', + href: 'http://_jabber._tcp.google.com/test', + pathname: '/test', + path: '/test', }, 'http://user:pass@_jabber._tcp.google.com/test': { - 'protocol': 'http:', - 'slashes': true, - 'auth': 'user:pass', - 'host': '_jabber._tcp.google.com', - 'hostname': '_jabber._tcp.google.com', - 'href': 'http://user:pass@_jabber._tcp.google.com/test', - 'pathname': '/test', - 'path': '/test', + protocol: 'http:', + slashes: true, + auth: 'user:pass', + host: '_jabber._tcp.google.com', + hostname: '_jabber._tcp.google.com', + href: 'http://user:pass@_jabber._tcp.google.com/test', + pathname: '/test', + path: '/test', }, 'http://_jabber._tcp.google.com:80/test': { - 'protocol': 'http:', - 'slashes': true, - 'port': '80', - 'host': '_jabber._tcp.google.com:80', - 'hostname': '_jabber._tcp.google.com', - 'href': 'http://_jabber._tcp.google.com:80/test', - 'pathname': '/test', - 'path': '/test', + protocol: 'http:', + slashes: true, + port: '80', + host: '_jabber._tcp.google.com:80', + hostname: '_jabber._tcp.google.com', + href: 'http://_jabber._tcp.google.com:80/test', + pathname: '/test', + path: '/test', }, 'http://user:pass@_jabber._tcp.google.com:80/test': { - 'protocol': 'http:', - 'slashes': true, - 'auth': 'user:pass', - 'port': '80', - 'host': '_jabber._tcp.google.com:80', - 'hostname': '_jabber._tcp.google.com', - 'href': 'http://user:pass@_jabber._tcp.google.com:80/test', - 'pathname': '/test', - 'path': '/test', + protocol: 'http:', + slashes: true, + auth: 'user:pass', + port: '80', + host: '_jabber._tcp.google.com:80', + hostname: '_jabber._tcp.google.com', + href: 'http://user:pass@_jabber._tcp.google.com:80/test', + pathname: '/test', + path: '/test', }, 'http://x:1/\' <>"`/{}|\\^~`/': { @@ -842,7 +854,21 @@ var parseTests = { query: '@c' }, - 'http://a\r" \t\n<\'b:b@c\r\nd/e?f':{ + 'http://a.b/\tbc\ndr\ref g"hq\'j?mn\\op^q=r`99{st|uv}wz': { + protocol: 'http:', + slashes: true, + host: 'a.b', + port: null, + hostname: 'a.b', + hash: null, + pathname: '/%09bc%0Adr%0Def%20g%22hq%27j%3Ckl%3E', + path: '/%09bc%0Adr%0Def%20g%22hq%27j%3Ckl%3E?mn%5Cop%5Eq=r%6099%7Bst%7Cuv%7Dwz', + search: '?mn%5Cop%5Eq=r%6099%7Bst%7Cuv%7Dwz', + query: 'mn%5Cop%5Eq=r%6099%7Bst%7Cuv%7Dwz', + href: 'http://a.b/%09bc%0Adr%0Def%20g%22hq%27j%3Ckl%3E?mn%5Cop%5Eq=r%6099%7Bst%7Cuv%7Dwz' + }, + + 'http://a\r" \t\n<\'b:b@c\r\nd/e?f': { protocol: 'http:', slashes: true, auth: 'a\r" \t\n<\'b:b', @@ -871,243 +897,284 @@ var parseTests = { pathname: '/:npm/npm', path: '/:npm/npm', href: 'git+ssh://git@github.com/:npm/npm' + }, + + 'https://*': { + protocol: 'https:', + slashes: true, + auth: null, + host: '', + port: null, + hostname: '', + hash: null, + search: null, + query: null, + pathname: '/*', + path: '/*', + href: 'https:///*' } }; -Object.keys(parseTests).forEach(function(u) { - test('parse(' + u + ')', function() { - var actual = url.parse(u), - spaced = url.parse(' \t ' + u + '\n\t'); - expected = parseTests[u]; +for (const u in parseTests) { + let actual = url.parse(u); + const spaced = url.parse(` \t ${u}\n\t`); + let expected = Object.assign(new url.Url(), parseTests[u]); - Object.keys(actual).forEach(function (i) { - if (expected[i] === undefined && actual[i] === null) { - expected[i] = null; - } - }); + Object.keys(actual).forEach(function(i) { + if (expected[i] === undefined && actual[i] === null) { + expected[i] = null; + } + }); - assert.deepEqual(actual, expected); - assert.deepEqual(spaced, expected); + assert.deepStrictEqual( + actual, + expected, + `expected ${inspect(expected)}, got ${inspect(actual)}` + ); + assert.deepStrictEqual( + spaced, + expected, + `expected ${inspect(expected)}, got ${inspect(spaced)}` + ); + + expected = parseTests[u].href; + actual = url.format(parseTests[u]); + + assert.equal(actual, expected, + 'format(' + u + ') == ' + u + '\nactual:' + actual); +} - var expected = parseTests[u].href, - actual = url.format(parseTests[u]); +function createWithNoPrototype(properties = []) { + const noProto = Object.create(null); + properties.forEach((property) => { + noProto[property.key] = property.value; + }); + return noProto; +} - assert.equal(actual, expected, - 'format(' + u + ') == ' + u + '\nactual:' + actual); +function check(actual, expected) { + assert.notStrictEqual(Object.getPrototypeOf(actual), Object.prototype); + assert.deepStrictEqual(Object.keys(actual).sort(), + Object.keys(expected).sort()); + Object.keys(expected).forEach(function(key) { + assert.deepStrictEqual(actual[key], expected[key]); }); -}); +} var parseTestsWithQueryString = { - '/foo/bar?baz=quux#frag' : { - 'href': '/foo/bar?baz=quux#frag', - 'hash': '#frag', - 'search': '?baz=quux', - 'query': { - 'baz': 'quux' - }, - 'pathname': '/foo/bar', - 'path': '/foo/bar?baz=quux' - }, - 'http://example.com' : { - 'href': 'http://example.com/', - 'protocol': 'http:', - 'slashes': true, - 'host': 'example.com', - 'hostname': 'example.com', - 'query': {}, - 'search': '', - 'pathname': '/', - 'path': '/' + '/foo/bar?baz=quux#frag': { + href: '/foo/bar?baz=quux#frag', + hash: '#frag', + search: '?baz=quux', + query: createWithNoPrototype([{key: 'baz', value: 'quux'}]), + pathname: '/foo/bar', + path: '/foo/bar?baz=quux' + }, + 'http://example.com': { + href: 'http://example.com/', + protocol: 'http:', + slashes: true, + host: 'example.com', + hostname: 'example.com', + query: createWithNoPrototype(), + search: '', + pathname: '/', + path: '/' }, '/example': { protocol: null, slashes: null, - auth: null, + auth: undefined, host: null, port: null, hostname: null, hash: null, search: '', - query: {}, + query: createWithNoPrototype(), pathname: '/example', path: '/example', href: '/example' }, - '/example?query=value':{ + '/example?query=value': { protocol: null, slashes: null, - auth: null, + auth: undefined, host: null, port: null, hostname: null, hash: null, search: '?query=value', - query: { query: 'value' }, + query: createWithNoPrototype([{ key: 'query', value: 'value' }]), pathname: '/example', path: '/example?query=value', href: '/example?query=value' } }; - -Object.keys(parseTestsWithQueryString).forEach(function(u) { - test('parse(' + u + ')', function() { - var actual = url.parse(u, true); - var expected = parseTestsWithQueryString[u]; - for (var i in actual) { - if (actual[i] === null && expected[i] === undefined) { - expected[i] = null; - } +for (const u in parseTestsWithQueryString) { + const actual = url.parse(u, true); + const expected = Object.assign(new url.Url(), parseTestsWithQueryString[u]); + for (const i in actual) { + if (actual[i] === null && expected[i] === undefined) { + expected[i] = null; } + } - assert.deepEqual(actual, expected); + const properties = Object.keys(actual).sort(); + assert.deepStrictEqual(properties, Object.keys(expected).sort()); + properties.forEach((property) => { + if (property === 'query') { + check(actual[property], expected[property]); + } else { + assert.deepStrictEqual(actual[property], expected[property]); + } }); -}); +} // some extra formatting tests, just to verify // that it'll format slightly wonky content to a valid url. var formatTests = { - 'http://example.com?' : { - 'href': 'http://example.com/?', - 'protocol': 'http:', - 'slashes': true, - 'host': 'example.com', - 'hostname': 'example.com', - 'search': '?', - 'query': {}, - 'pathname': '/' - }, - 'http://example.com?foo=bar#frag' : { - 'href': 'http://example.com/?foo=bar#frag', - 'protocol': 'http:', - 'host': 'example.com', - 'hostname': 'example.com', - 'hash': '#frag', - 'search': '?foo=bar', - 'query': 'foo=bar', - 'pathname': '/' - }, - 'http://example.com?foo=@bar#frag' : { - 'href': 'http://example.com/?foo=@bar#frag', - 'protocol': 'http:', - 'host': 'example.com', - 'hostname': 'example.com', - 'hash': '#frag', - 'search': '?foo=@bar', - 'query': 'foo=@bar', - 'pathname': '/' - }, - 'http://example.com?foo=/bar/#frag' : { - 'href': 'http://example.com/?foo=/bar/#frag', - 'protocol': 'http:', - 'host': 'example.com', - 'hostname': 'example.com', - 'hash': '#frag', - 'search': '?foo=/bar/', - 'query': 'foo=/bar/', - 'pathname': '/' - }, - 'http://example.com?foo=?bar/#frag' : { - 'href': 'http://example.com/?foo=?bar/#frag', - 'protocol': 'http:', - 'host': 'example.com', - 'hostname': 'example.com', - 'hash': '#frag', - 'search': '?foo=?bar/', - 'query': 'foo=?bar/', - 'pathname': '/' - }, - 'http://example.com#frag=?bar/#frag' : { - 'href': 'http://example.com/#frag=?bar/#frag', - 'protocol': 'http:', - 'host': 'example.com', - 'hostname': 'example.com', - 'hash': '#frag=?bar/#frag', - 'pathname': '/' - }, - 'http://google.com" onload="alert(42)/' : { - 'href': 'http://google.com/%22%20onload=%22alert(42)/', - 'protocol': 'http:', - 'host': 'google.com', - 'pathname': '/%22%20onload=%22alert(42)/' - }, - 'http://a.com/a/b/c?s#h' : { - 'href': 'http://a.com/a/b/c?s#h', - 'protocol': 'http', - 'host': 'a.com', - 'pathname': 'a/b/c', - 'hash': 'h', - 'search': 's' - }, - 'xmpp:isaacschlueter@jabber.org' : { - 'href': 'xmpp:isaacschlueter@jabber.org', - 'protocol': 'xmpp:', - 'host': 'jabber.org', - 'auth': 'isaacschlueter', - 'hostname': 'jabber.org' - }, - 'http://atpass:foo%40bar@127.0.0.1/' : { - 'href': 'http://atpass:foo%40bar@127.0.0.1/', - 'auth': 'atpass:foo@bar', - 'hostname': '127.0.0.1', - 'protocol': 'http:', - 'pathname': '/' - }, - 'http://atslash%2F%40:%2F%40@foo/' : { - 'href': 'http://atslash%2F%40:%2F%40@foo/', - 'auth': 'atslash/@:/@', - 'hostname': 'foo', - 'protocol': 'http:', - 'pathname': '/' + 'http://example.com?': { + href: 'http://example.com/?', + protocol: 'http:', + slashes: true, + host: 'example.com', + hostname: 'example.com', + search: '?', + query: {}, + pathname: '/' + }, + 'http://example.com?foo=bar#frag': { + href: 'http://example.com/?foo=bar#frag', + protocol: 'http:', + host: 'example.com', + hostname: 'example.com', + hash: '#frag', + search: '?foo=bar', + query: 'foo=bar', + pathname: '/' + }, + 'http://example.com?foo=@bar#frag': { + href: 'http://example.com/?foo=@bar#frag', + protocol: 'http:', + host: 'example.com', + hostname: 'example.com', + hash: '#frag', + search: '?foo=@bar', + query: 'foo=@bar', + pathname: '/' + }, + 'http://example.com?foo=/bar/#frag': { + href: 'http://example.com/?foo=/bar/#frag', + protocol: 'http:', + host: 'example.com', + hostname: 'example.com', + hash: '#frag', + search: '?foo=/bar/', + query: 'foo=/bar/', + pathname: '/' + }, + 'http://example.com?foo=?bar/#frag': { + href: 'http://example.com/?foo=?bar/#frag', + protocol: 'http:', + host: 'example.com', + hostname: 'example.com', + hash: '#frag', + search: '?foo=?bar/', + query: 'foo=?bar/', + pathname: '/' + }, + 'http://example.com#frag=?bar/#frag': { + href: 'http://example.com/#frag=?bar/#frag', + protocol: 'http:', + host: 'example.com', + hostname: 'example.com', + hash: '#frag=?bar/#frag', + pathname: '/' + }, + 'http://google.com" onload="alert(42)/': { + href: 'http://google.com/%22%20onload=%22alert(42)/', + protocol: 'http:', + host: 'google.com', + pathname: '/%22%20onload=%22alert(42)/' + }, + 'http://a.com/a/b/c?s#h': { + href: 'http://a.com/a/b/c?s#h', + protocol: 'http', + host: 'a.com', + pathname: 'a/b/c', + hash: 'h', + search: 's' + }, + 'xmpp:isaacschlueter@jabber.org': { + href: 'xmpp:isaacschlueter@jabber.org', + protocol: 'xmpp:', + host: 'jabber.org', + auth: 'isaacschlueter', + hostname: 'jabber.org' + }, + 'http://atpass:foo%40bar@127.0.0.1/': { + href: 'http://atpass:foo%40bar@127.0.0.1/', + auth: 'atpass:foo@bar', + hostname: '127.0.0.1', + protocol: 'http:', + pathname: '/' + }, + 'http://atslash%2F%40:%2F%40@foo/': { + href: 'http://atslash%2F%40:%2F%40@foo/', + auth: 'atslash/@:/@', + hostname: 'foo', + protocol: 'http:', + pathname: '/' }, 'svn+ssh://foo/bar': { - 'href': 'svn+ssh://foo/bar', - 'hostname': 'foo', - 'protocol': 'svn+ssh:', - 'pathname': '/bar', - 'slashes': true + href: 'svn+ssh://foo/bar', + hostname: 'foo', + protocol: 'svn+ssh:', + pathname: '/bar', + slashes: true }, 'dash-test://foo/bar': { - 'href': 'dash-test://foo/bar', - 'hostname': 'foo', - 'protocol': 'dash-test:', - 'pathname': '/bar', - 'slashes': true + href: 'dash-test://foo/bar', + hostname: 'foo', + protocol: 'dash-test:', + pathname: '/bar', + slashes: true }, 'dash-test:foo/bar': { - 'href': 'dash-test:foo/bar', - 'hostname': 'foo', - 'protocol': 'dash-test:', - 'pathname': '/bar' + href: 'dash-test:foo/bar', + hostname: 'foo', + protocol: 'dash-test:', + pathname: '/bar' }, 'dot.test://foo/bar': { - 'href': 'dot.test://foo/bar', - 'hostname': 'foo', - 'protocol': 'dot.test:', - 'pathname': '/bar', - 'slashes': true + href: 'dot.test://foo/bar', + hostname: 'foo', + protocol: 'dot.test:', + pathname: '/bar', + slashes: true }, 'dot.test:foo/bar': { - 'href': 'dot.test:foo/bar', - 'hostname': 'foo', - 'protocol': 'dot.test:', - 'pathname': '/bar' + href: 'dot.test:foo/bar', + 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' + 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' + 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' }, // encode context-specific delimiters in path and query, but do not touch @@ -1115,23 +1182,23 @@ var formatTests = { // // `#`,`?` in path - '/path/to/%%23%3F+=&.txt?foo=theA1#bar' : { - href : '/path/to/%%23%3F+=&.txt?foo=theA1#bar', + '/path/to/%%23%3F+=&.txt?foo=theA1#bar': { + href: '/path/to/%%23%3F+=&.txt?foo=theA1#bar', pathname: '/path/to/%#?+=&.txt', query: { foo: 'theA1' }, - hash: "#bar" + hash: '#bar' }, // `#`,`?` in path + `#` in query - '/path/to/%%23%3F+=&.txt?foo=the%231#bar' : { - href : '/path/to/%%23%3F+=&.txt?foo=the%231#bar', + '/path/to/%%23%3F+=&.txt?foo=the%231#bar': { + href: '/path/to/%%23%3F+=&.txt?foo=the%231#bar', pathname: '/path/to/%#?+=&.txt', query: { foo: 'the#1' }, - hash: "#bar" + hash: '#bar' }, // `?` and `#` in path and search @@ -1152,24 +1219,53 @@ var formatTests = { hash: '#frag', search: '?abc=the#1?&foo=bar', pathname: '/fooA100%mBr', + }, + + // multiple `#` in search + 'http://example.com/?foo=bar%231%232%233&abc=%234%23%235#frag': { + href: 'http://example.com/?foo=bar%231%232%233&abc=%234%23%235#frag', + protocol: 'http:', + slashes: true, + host: 'example.com', + hostname: 'example.com', + hash: '#frag', + search: '?foo=bar#1#2#3&abc=#4##5', + query: {}, + pathname: '/' + }, + + // more than 255 characters in hostname which exceeds the limit + [`http://${'a'.repeat(255)}.com/node`]: { + href: 'http:///node', + protocol: 'http:', + slashes: true, + host: '', + hostname: '', + pathname: '/node', + path: '/node' + }, + + // https://github.com/nodejs/node/issues/3361 + 'file:///home/user': { + href: 'file:///home/user', + protocol: 'file', + pathname: '/home/user', + path: '/home/user' } }; - -Object.keys(formatTests).forEach(function(u) { - test('format(' + u + ')', function() { - var expect = formatTests[u].href; - delete formatTests[u].href; - var actual = url.format(u); - var actualObj = url.format(formatTests[u]); - assert.equal(actual, expect, - 'wonky format(' + u + ') == ' + expect + - '\nactual:' + actual); - assert.equal(actualObj, expect, - 'wonky format(' + JSON.stringify(formatTests[u]) + - ') == ' + expect + - '\nactual: ' + actualObj); - }); -}); +for (const u in formatTests) { + const expect = formatTests[u].href; + delete formatTests[u].href; + const actual = url.format(u); + const actualObj = url.format(formatTests[u]); + assert.strictEqual(actual, expect, + 'wonky format(' + u + ') == ' + expect + + '\nactual:' + actual); + assert.strictEqual(actualObj, expect, + 'wonky format(' + JSON.stringify(formatTests[u]) + + ') == ' + expect + + '\nactual: ' + actualObj); +} /* [from, path, expected] @@ -1216,17 +1312,16 @@ var relativeTests = [ ['http://example.com/b//c//d;p?q#blarg', 'http:/a/b/c/d', 'http://example.com/a/b/c/d'], - ['/foo/bar/baz', '/../etc/passwd', '/etc/passwd'] + ['/foo/bar/baz', '/../etc/passwd', '/etc/passwd'], + ['http://localhost', 'file:///Users/foo', 'file:///Users/foo'], + ['http://localhost', 'file://foo/Users', 'file://foo/Users'] ]; - relativeTests.forEach(function(relativeTest) { - test('resolve(' + [relativeTest[0], relativeTest[1]] + ')', function() { - var a = url.resolve(relativeTest[0], relativeTest[1]), - e = relativeTest[2]; - assert.equal(a, e, - 'resolve(' + [relativeTest[0], relativeTest[1]] + ') == ' + e + - '\n actual=' + a); - }); + const a = url.resolve(relativeTest[0], relativeTest[1]); + const e = relativeTest[2]; + assert.equal(a, e, + 'resolve(' + [relativeTest[0], relativeTest[1]] + ') == ' + e + + '\n actual=' + a); }); @@ -1241,9 +1336,7 @@ relativeTests.forEach(function(relativeTest) { [], {} ].forEach(function(val) { - test('parse(' + val + ')', function() { - assert.throws(function() { url.parse(val); }, TypeError); - }); + assert.throws(function() { url.parse(val); }, TypeError); }); @@ -1299,7 +1392,7 @@ var relativeTests2 = [ ['g?y#s', bases[0], 'http://a/b/c/g?y#s'], [';x', bases[0], 'http://a/b/c/;x'], ['g;x', bases[0], 'http://a/b/c/g;x'], - ['g;x?y#s' , bases[0], 'http://a/b/c/g;x?y#s'], + ['g;x?y#s', bases[0], 'http://a/b/c/g;x?y#s'], // changed with RFC 2396bis //('', bases[0], CURRENT_DOC_URI], ['', bases[0], 'http://a/b/c/d;p?q'], @@ -1310,7 +1403,7 @@ var relativeTests2 = [ ['../g', bases[0], 'http://a/b/g'], ['../..', bases[0], 'http://a/'], ['../../', bases[0], 'http://a/'], - ['../../g' , bases[0], 'http://a/g'], + ['../../g', bases[0], 'http://a/g'], ['../../../g', bases[0], ('http://a/../g', 'http://a/g')], ['../../../../g', bases[0], ('http://a/../../g', 'http://a/g')], // changed with RFC 2396bis @@ -1349,16 +1442,16 @@ var relativeTests2 = [ //('?y', bases[1], 'http://a/b/c/?y'], ['?y', bases[1], 'http://a/b/c/d;p?y'], ['g?y', bases[1], 'http://a/b/c/g?y'], - ['g?y/./x' , bases[1], 'http://a/b/c/g?y/./x'], + ['g?y/./x', bases[1], 'http://a/b/c/g?y/./x'], ['g?y/../x', bases[1], 'http://a/b/c/g?y/../x'], ['g#s', bases[1], 'http://a/b/c/g#s'], - ['g#s/./x' , bases[1], 'http://a/b/c/g#s/./x'], + ['g#s/./x', bases[1], 'http://a/b/c/g#s/./x'], ['g#s/../x', bases[1], 'http://a/b/c/g#s/../x'], ['./', bases[1], 'http://a/b/c/'], ['../', bases[1], 'http://a/b/'], ['../g', bases[1], 'http://a/b/g'], ['../../', bases[1], 'http://a/'], - ['../../g' , bases[1], 'http://a/g'], + ['../../g', bases[1], 'http://a/g'], // http://gbiv.com/protocols/uri/test/rel_examples3.html // slashes in path params @@ -1375,7 +1468,7 @@ var relativeTests2 = [ ['../', bases[2], 'http://a/b/c/'], ['../g', bases[2], 'http://a/b/c/g'], ['../../', bases[2], 'http://a/b/'], - ['../../g' , bases[2], 'http://a/b/g'], + ['../../g', bases[2], 'http://a/b/g'], // http://gbiv.com/protocols/uri/test/rel_examples4.html // double and triple slash, unknown scheme @@ -1392,7 +1485,7 @@ var relativeTests2 = [ ['../g', bases[3], 'fred:///s//a/g'], ['../../', bases[3], 'fred:///s//'], - ['../../g' , bases[3], 'fred:///s//g'], + ['../../g', bases[3], 'fred:///s//g'], ['../../../g', bases[3], 'fred:///s/g'], // may change to fred:///s//a/../../../g ['../../../../g', bases[3], 'fred:///g'], @@ -1411,7 +1504,7 @@ var relativeTests2 = [ ['../', bases[4], 'http:///s//a/'], ['../g', bases[4], 'http:///s//a/g'], ['../../', bases[4], 'http:///s//'], - ['../../g' , bases[4], 'http:///s//g'], + ['../../g', bases[4], 'http:///s//g'], // may change to http:///s//a/../../g ['../../../g', bases[4], 'http:///s/g'], // may change to http:///s//a/../../../g @@ -1531,40 +1624,54 @@ var relativeTests2 = [ //changeing auth ['http://diff:auth@www.example.com', 'http://asdf:qwer@www.example.com', - 'http://diff:auth@www.example.com/'] + 'http://diff:auth@www.example.com/'], + + // changing port + ['https://example.com:81/', + 'https://example.com:82/', + 'https://example.com:81/'], + + // https://github.com/nodejs/node/issues/1435 + ['https://another.host.com/', + 'https://user:password@example.org/', + 'https://another.host.com/'], + ['//another.host.com/', + 'https://user:password@example.org/', + 'https://another.host.com/'], + ['http://another.host.com/', + 'https://user:password@example.org/', + 'http://another.host.com/'], + ['mailto:another.host.com', + 'mailto:user@example.org', + 'mailto:another.host.com'], + ['https://example.com/foo', + 'https://user:password@example.com', + 'https://user:password@example.com/foo'], ]; - relativeTests2.forEach(function(relativeTest) { - test('resolve(' + [relativeTest[1], relativeTest[0]] + ')', function() { - var a = url.resolve(relativeTest[1], relativeTest[0]), - e = relativeTest[2]; - assert.equal(a, e, - 'resolve(' + [relativeTest[1], relativeTest[0]] + ') == ' + e + - '\n actual=' + a); - }); + const a = url.resolve(relativeTest[1], relativeTest[0]); + const e = url.format(relativeTest[2]); + assert.equal(a, e, + 'resolve(' + [relativeTest[1], relativeTest[0]] + ') == ' + e + + '\n actual=' + a); }); //if format and parse are inverse operations then //resolveObject(parse(x), y) == parse(resolve(x, y)) -//host and hostname are special, in this case a '' value is important -var emptyIsImportant = {'host': true, 'hostname': ''}; - //format: [from, path, expected] relativeTests.forEach(function(relativeTest) { -test('resolveObject(' + [relativeTest[0], relativeTest[1]] + ')', function() { - var actual = url.resolveObject(url.parse(relativeTest[0]), relativeTest[1]), - expected = url.parse(relativeTest[2]); + var actual = url.resolveObject(url.parse(relativeTest[0]), relativeTest[1]); + var expected = url.parse(relativeTest[2]); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); - expected = relativeTest[2]; - actual = url.format(actual); + expected = relativeTest[2]; + actual = url.format(actual); - assert.equal(actual, expected, - 'format(' + actual + ') == ' + expected + '\nactual:' + actual); - }); + assert.equal(actual, expected, + 'format(' + actual + ') == ' + expected + '\nactual:' + actual); }); //format: [to, from, result] @@ -1581,19 +1688,37 @@ if (relativeTests2[181][0] === './/g' && relativeTests2[181][2] === 'f://g') { relativeTests2.splice(181, 1); } - relativeTests2.forEach(function(relativeTest) { - test('resolveObject(' + [relativeTest[1], relativeTest[0]] + ')', function() { - var actual = url.resolveObject(url.parse(relativeTest[1]), relativeTest[0]), - expected = url.parse(relativeTest[2]); + var actual = url.resolveObject(url.parse(relativeTest[1]), relativeTest[0]); + var expected = url.parse(relativeTest[2]); - assert.deepEqual(actual, expected); + assert.deepStrictEqual( + actual, + expected, + `expected ${inspect(expected)} but got ${inspect(actual)}` + ); - var expected = relativeTest[2], - actual = url.format(actual); + expected = url.format(relativeTest[2]); + actual = url.format(actual); - assert.equal(actual, expected, - 'format(' + relativeTest[1] + ') == ' + expected + - '\nactual:' + actual); - }); + assert.equal(actual, expected, + 'format(' + relativeTest[1] + ') == ' + expected + + '\nactual:' + actual); +}); + + +// https://github.com/nodejs/node/pull/1036 +var throws = [ + undefined, + null, + true, + false, + 0, + function() {} +]; +for (let i = 0; i < throws.length; i++) { + assert.throws(function() { url.format(throws[i]); }, TypeError); +} +assert.strictEqual(url.format(''), ''); +assert.strictEqual(url.format({}), ''); }); diff --git a/url.js b/url.js index 23ac6f5..9cea2fa 100644 --- a/url.js +++ b/url.js @@ -1,33 +1,43 @@ -// Copyright Joyent, Inc. and other Node contributors. +// Copyright Node.js contributors. All rights reserved. // -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: // -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. // -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. 'use strict'; -var punycode = require('punycode'); -var util = require('./util'); +function importPunycode() { + try { + return process.binding('icu'); + } catch (e) { + return require('punycode'); + } +} +const { toASCII } = importPunycode(); + +const internalUrl = require('./internal/url'); +const encodeAuth = internalUrl.encodeAuth; exports.parse = urlParse; exports.resolve = urlResolve; exports.resolveObject = urlResolveObject; exports.format = urlFormat; +exports.URL = internalUrl.URL; + exports.Url = Url; @@ -50,88 +60,140 @@ function Url() { // define these here so at least they only have to be // compiled once on the first module load. -var protocolPattern = /^([a-z0-9.+-]+:)/i, - portPattern = /:[0-9]*$/, - - // Special case for a simple path URL - simplePathPattern = /^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/, - - // RFC 2396: characters reserved for delimiting URLs. - // We actually just auto-escape these. - delims = ['<', '>', '"', '`', ' ', '\r', '\n', '\t'], - - // RFC 2396: characters not allowed for various reasons. - unwise = ['{', '}', '|', '\\', '^', '`'].concat(delims), - - // Allowed by RFCs, but cause of XSS attacks. Always escape these. - autoEscape = ['\''].concat(unwise), - // Characters that are never ever allowed in a hostname. - // Note that any invalid chars are also handled, but these - // are the ones that are *expected* to be seen, so we fast-path - // them. - nonHostChars = ['%', '/', '?', ';', '#'].concat(autoEscape), - hostEndingChars = ['/', '?', '#'], - hostnameMaxLen = 255, - hostnamePartPattern = /^[+a-z0-9A-Z_-]{0,63}$/, - hostnamePartStart = /^([+a-z0-9A-Z_-]{0,63})(.*)$/, - // protocols that can allow "unsafe" and "unwise" chars. - unsafeProtocol = { - 'javascript': true, - 'javascript:': true - }, - // protocols that never have a hostname. - hostlessProtocol = { - 'javascript': true, - 'javascript:': true - }, - // protocols that always contain a // bit. - slashedProtocol = { - 'http': true, - 'https': true, - 'ftp': true, - 'gopher': true, - 'file': true, - 'http:': true, - 'https:': true, - 'ftp:': true, - 'gopher:': true, - 'file:': true - }, - querystring = require('querystring'); +const protocolPattern = /^([a-z0-9.+-]+:)/i; +const portPattern = /:[0-9]*$/; +const hostPattern = /^\/\/[^@/]+@[^@/]+/; + +// Special case for a simple path URL +const simplePathPattern = /^(\/\/?(?!\/)[^?\s]*)(\?[^\s]*)?$/; + +const hostnameMaxLen = 255; +// protocols that can allow "unsafe" and "unwise" chars. +const unsafeProtocol = { + 'javascript': true, + 'javascript:': true +}; +// protocols that never have a hostname. +const hostlessProtocol = { + 'javascript': true, + 'javascript:': true +}; +// protocols that always contain a // bit. +const slashedProtocol = { + 'http': true, + 'http:': true, + 'https': true, + 'https:': true, + 'ftp': true, + 'ftp:': true, + 'gopher': true, + 'gopher:': true, + 'file': true, + 'file:': true +}; +const querystring = require('querystring'); + +// This constructor is used to store parsed query string values. Instantiating +// this is faster than explicitly calling `Object.create(null)` to get a +// "clean" empty object (tested with v8 v4.9). +function ParsedQueryString() {} +ParsedQueryString.prototype = Object.create(null); function urlParse(url, parseQueryString, slashesDenoteHost) { - if (url && util.isObject(url) && url instanceof Url) return url; + if (url instanceof Url) return url; - var u = new Url; + var u = new Url(); u.parse(url, parseQueryString, slashesDenoteHost); return u; } Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) { - if (!util.isString(url)) { - throw new TypeError("Parameter 'url' must be a string, not " + typeof url); + if (typeof url !== 'string') { + throw new TypeError('Parameter "url" must be a string, not ' + typeof url); } // Copy chrome, IE, opera backslash-handling behavior. // Back slashes before the query string get converted to forward slashes // See: https://code.google.com/p/chromium/issues/detail?id=25916 - var queryIndex = url.indexOf('?'), - splitter = - (queryIndex !== -1 && queryIndex < url.indexOf('#')) ? '?' : '#', - uSplit = url.split(splitter), - slashRegex = /\\/g; - uSplit[0] = uSplit[0].replace(slashRegex, '/'); - url = uSplit.join(splitter); + var hasHash = false; + var start = -1; + var end = -1; + var rest = ''; + var lastPos = 0; + var i = 0; + for (var inWs = false, split = false; i < url.length; ++i) { + const code = url.charCodeAt(i); + + // Find first and last non-whitespace characters for trimming + const isWs = code === 32/* */ || + code === 9/*\t*/ || + code === 13/*\r*/ || + code === 10/*\n*/ || + code === 12/*\f*/ || + code === 160/*\u00A0*/ || + code === 65279/*\uFEFF*/; + if (start === -1) { + if (isWs) + continue; + lastPos = start = i; + } else { + if (inWs) { + if (!isWs) { + end = -1; + inWs = false; + } + } else if (isWs) { + end = i; + inWs = true; + } + } - var rest = url; + // Only convert backslashes while we haven't seen a split character + if (!split) { + switch (code) { + case 35: // '#' + hasHash = true; + // Fall through + case 63: // '?' + split = true; + break; + case 92: // '\\' + if (i - lastPos > 0) + rest += url.slice(lastPos, i); + rest += '/'; + lastPos = i + 1; + break; + } + } else if (!hasHash && code === 35/*#*/) { + hasHash = true; + } + } - // trim before proceeding. - // This is to support parse stuff like " http://foo.com \n" - rest = rest.trim(); + // Check if string was non-empty (including strings with only whitespace) + if (start !== -1) { + if (lastPos === start) { + // We didn't convert any backslashes + + if (end === -1) { + if (start === 0) + rest = url; + else + rest = url.slice(start); + } else { + rest = url.slice(start, end); + } + } else if (end === -1 && lastPos < url.length) { + // We converted some backslashes and have only part of the entire string + rest += url.slice(lastPos); + } else if (end !== -1 && lastPos < end) { + // We converted some backslashes and have only part of the entire string + rest += url.slice(lastPos, end); + } + } - if (!slashesDenoteHost && url.split('#').length === 1) { + if (!slashesDenoteHost && !hasHash) { // Try fast path regexp - var simplePath = simplePathPattern.exec(rest); + const simplePath = simplePathPattern.exec(rest); if (simplePath) { this.path = rest; this.href = rest; @@ -139,13 +201,13 @@ Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) { if (simplePath[2]) { this.search = simplePath[2]; if (parseQueryString) { - this.query = querystring.parse(this.search.substr(1)); + this.query = querystring.parse(this.search.slice(1)); } else { - this.query = this.search.substr(1); + this.query = this.search.slice(1); } } else if (parseQueryString) { this.search = ''; - this.query = {}; + this.query = new ParsedQueryString(); } return this; } @@ -156,17 +218,18 @@ Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) { proto = proto[0]; var lowerProto = proto.toLowerCase(); this.protocol = lowerProto; - rest = rest.substr(proto.length); + rest = rest.slice(proto.length); } // figure out if it's got a host // user@server is *always* interpreted as a hostname, and url // resolution will treat //foo/bar as host=foo,path=bar because that's // how the browser resolves relative URLs. - if (slashesDenoteHost || proto || rest.match(/^\/\/[^@\/]+@[^@\/]+/)) { - var slashes = rest.substr(0, 2) === '//'; + if (slashesDenoteHost || proto || hostPattern.test(rest)) { + var slashes = rest.charCodeAt(0) === 47/*/*/ && + rest.charCodeAt(1) === 47/*/*/; if (slashes && !(proto && hostlessProtocol[proto])) { - rest = rest.substr(2); + rest = rest.slice(2); this.slashes = true; } } @@ -184,100 +247,87 @@ Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) { // // ex: // http://a@b@c/ => user:a@b host:c - // http://a@b?@c => user:a host:c path:/?@c + // http://a@b?@c => user:a host:b path:/?@c // v0.12 TODO(isaacs): This is not quite how Chrome does things. // Review our test case against browsers more comprehensively. - // find the first instance of any hostEndingChars var hostEnd = -1; - for (var i = 0; i < hostEndingChars.length; i++) { - var hec = rest.indexOf(hostEndingChars[i]); - if (hec !== -1 && (hostEnd === -1 || hec < hostEnd)) - hostEnd = hec; - } - - // at this point, either we have an explicit point where the - // auth portion cannot go past, or the last @ char is the decider. - var auth, atSign; - if (hostEnd === -1) { - // atSign can be anywhere. - atSign = rest.lastIndexOf('@'); - } else { - // atSign must be in auth portion. - // http://a@b/c@d => host:b auth:a path:/c@d - atSign = rest.lastIndexOf('@', hostEnd); + var atSign = -1; + var nonHost = -1; + for (i = 0; i < rest.length; ++i) { + switch (rest.charCodeAt(i)) { + case 9: // '\t' + case 10: // '\n' + case 13: // '\r' + case 32: // ' ' + case 34: // '"' + case 37: // '%' + case 39: // '\'' + case 59: // ';' + case 60: // '<' + case 62: // '>' + case 92: // '\\' + case 94: // '^' + case 96: // '`' + case 123: // '{' + case 124: // '|' + case 125: // '}' + // Characters that are never ever allowed in a hostname from RFC 2396 + if (nonHost === -1) + nonHost = i; + break; + case 35: // '#' + case 47: // '/' + case 63: // '?' + // Find the first instance of any host-ending characters + if (nonHost === -1) + nonHost = i; + hostEnd = i; + break; + case 64: // '@' + // At this point, either we have an explicit point where the + // auth portion cannot go past, or the last @ char is the decider. + atSign = i; + nonHost = -1; + break; + } + if (hostEnd !== -1) + break; } - - // Now we have a portion which is definitely the auth. - // Pull that off. + start = 0; if (atSign !== -1) { - auth = rest.slice(0, atSign); - rest = rest.slice(atSign + 1); - this.auth = decodeURIComponent(auth); + this.auth = decodeURIComponent(rest.slice(0, atSign)); + start = atSign + 1; } - - // the host is the remaining to the left of the first non-host char - hostEnd = -1; - for (var i = 0; i < nonHostChars.length; i++) { - var hec = rest.indexOf(nonHostChars[i]); - if (hec !== -1 && (hostEnd === -1 || hec < hostEnd)) - hostEnd = hec; + if (nonHost === -1) { + this.host = rest.slice(start); + rest = ''; + } else { + this.host = rest.slice(start, nonHost); + rest = rest.slice(nonHost); } - // if we still have not hit it, then the entire thing is a host. - if (hostEnd === -1) - hostEnd = rest.length; - - this.host = rest.slice(0, hostEnd); - rest = rest.slice(hostEnd); // pull out port. this.parseHost(); // we've indicated that there is a hostname, // so even if it's empty, it has to be present. - this.hostname = this.hostname || ''; + if (typeof this.hostname !== 'string') + this.hostname = ''; + + var hostname = this.hostname; // if hostname begins with [ and ends with ] // assume that it's an IPv6 address. - var ipv6Hostname = this.hostname[0] === '[' && - this.hostname[this.hostname.length - 1] === ']'; + var ipv6Hostname = hostname.charCodeAt(0) === 91/*[*/ && + hostname.charCodeAt(hostname.length - 1) === 93/*]*/; // validate a little. if (!ipv6Hostname) { - var hostparts = this.hostname.split(/\./); - for (var i = 0, l = hostparts.length; i < l; i++) { - var part = hostparts[i]; - if (!part) continue; - if (!part.match(hostnamePartPattern)) { - var newpart = ''; - for (var j = 0, k = part.length; j < k; j++) { - if (part.charCodeAt(j) > 127) { - // we replace non-ASCII char with a temporary placeholder - // we need this to make sure size of hostname is not - // broken by replacing non-ASCII by nothing - newpart += 'x'; - } else { - newpart += part[j]; - } - } - // we test again with ASCII char only - if (!newpart.match(hostnamePartPattern)) { - var validParts = hostparts.slice(0, i); - var notHost = hostparts.slice(i + 1); - var bit = part.match(hostnamePartStart); - if (bit) { - validParts.push(bit[1]); - notHost.unshift(bit[2]); - } - if (notHost.length) { - rest = '/' + notHost.join('.') + rest; - } - this.hostname = validParts.join('.'); - break; - } - } - } + const result = validateHostname(this, rest, hostname); + if (result !== undefined) + rest = result; } if (this.hostname.length > hostnameMaxLen) { @@ -292,18 +342,17 @@ Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) { // It only converts parts of the domain name that // have non-ASCII characters, i.e. it doesn't matter if // you call it with a domain that already is ASCII-only. - this.hostname = punycode.toASCII(this.hostname); + this.hostname = toASCII(this.hostname); } var p = this.port ? ':' + this.port : ''; var h = this.hostname || ''; this.host = h + p; - this.href += this.host; // strip [ and ] from the hostname // the host field still retains them, though if (ipv6Hostname) { - this.hostname = this.hostname.substr(1, this.hostname.length - 2); + this.hostname = this.hostname.slice(1, -1); if (rest[0] !== '/') { rest = '/' + rest; } @@ -313,53 +362,63 @@ Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) { // now rest is set to the post-host stuff. // chop off any delim chars. if (!unsafeProtocol[lowerProto]) { - // First, make 100% sure that any "autoEscape" chars get // escaped, even if encodeURIComponent doesn't think they // need to be. - for (var i = 0, l = autoEscape.length; i < l; i++) { - var ae = autoEscape[i]; - if (rest.indexOf(ae) === -1) - continue; - var esc = encodeURIComponent(ae); - if (esc === ae) { - esc = escape(ae); - } - rest = rest.split(ae).join(esc); - } + const result = autoEscapeStr(rest); + if (result !== undefined) + rest = result; } - - // chop off from the tail first. - var hash = rest.indexOf('#'); - if (hash !== -1) { - // got a fragment string. - this.hash = rest.substr(hash); - rest = rest.slice(0, hash); + var questionIdx = -1; + var hashIdx = -1; + for (i = 0; i < rest.length; ++i) { + const code = rest.charCodeAt(i); + if (code === 35/*#*/) { + this.hash = rest.slice(i); + hashIdx = i; + break; + } else if (code === 63/*?*/ && questionIdx === -1) { + questionIdx = i; + } } - var qm = rest.indexOf('?'); - if (qm !== -1) { - this.search = rest.substr(qm); - this.query = rest.substr(qm + 1); + + if (questionIdx !== -1) { + if (hashIdx === -1) { + this.search = rest.slice(questionIdx); + this.query = rest.slice(questionIdx + 1); + } else { + this.search = rest.slice(questionIdx, hashIdx); + this.query = rest.slice(questionIdx + 1, hashIdx); + } if (parseQueryString) { this.query = querystring.parse(this.query); } - rest = rest.slice(0, qm); } else if (parseQueryString) { // no query string, but parseQueryString still requested this.search = ''; - this.query = {}; + this.query = new ParsedQueryString(); + } + + var firstIdx = (questionIdx !== -1 && + (hashIdx === -1 || questionIdx < hashIdx) + ? questionIdx + : hashIdx); + if (firstIdx === -1) { + if (rest.length > 0) + this.pathname = rest; + } else if (firstIdx > 0) { + this.pathname = rest.slice(0, firstIdx); } - if (rest) this.pathname = rest; if (slashedProtocol[lowerProto] && this.hostname && !this.pathname) { this.pathname = '/'; } - //to support http.request + // to support http.request if (this.pathname || this.search) { - var p = this.pathname || ''; - var s = this.search || ''; + const p = this.pathname || ''; + const s = this.search || ''; this.path = p + s; } @@ -368,30 +427,173 @@ Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) { return this; }; +function validateHostname(self, rest, hostname) { + for (var i = 0, lastPos; i <= hostname.length; ++i) { + var code; + if (i < hostname.length) + code = hostname.charCodeAt(i); + if (code === 46/*.*/ || i === hostname.length) { + if (i - lastPos > 0) { + if (i - lastPos > 63) { + self.hostname = hostname.slice(0, lastPos + 63); + return '/' + hostname.slice(lastPos + 63) + rest; + } + } + lastPos = i + 1; + continue; + } else if ((code >= 48/*0*/ && code <= 57/*9*/) || + (code >= 97/*a*/ && code <= 122/*z*/) || + code === 45/*-*/ || + (code >= 65/*A*/ && code <= 90/*Z*/) || + code === 43/*+*/ || + code === 95/*_*/ || + code > 127) { + continue; + } + // Invalid host character + self.hostname = hostname.slice(0, i); + if (i < hostname.length) + return '/' + hostname.slice(i) + rest; + break; + } +} + +// Automatically escape all delimiters and unwise characters from RFC 2396. +// Also escape single quotes in case of an XSS attack. +// Return undefined if the string doesn't need escaping, +// otherwise return the escaped string. +function autoEscapeStr(rest) { + var escaped = ''; + var lastEscapedPos = 0; + for (var i = 0; i < rest.length; ++i) { + // Manual switching is faster than using a Map/Object. + // `escaped` contains substring up to the last escaped cahracter. + switch (rest.charCodeAt(i)) { + case 9: // '\t' + // Concat if there are ordinary characters in the middle. + if (i > lastEscapedPos) + escaped += rest.slice(lastEscapedPos, i); + escaped += '%09'; + lastEscapedPos = i + 1; + break; + case 10: // '\n' + if (i > lastEscapedPos) + escaped += rest.slice(lastEscapedPos, i); + escaped += '%0A'; + lastEscapedPos = i + 1; + break; + case 13: // '\r' + if (i > lastEscapedPos) + escaped += rest.slice(lastEscapedPos, i); + escaped += '%0D'; + lastEscapedPos = i + 1; + break; + case 32: // ' ' + if (i > lastEscapedPos) + escaped += rest.slice(lastEscapedPos, i); + escaped += '%20'; + lastEscapedPos = i + 1; + break; + case 34: // '"' + if (i > lastEscapedPos) + escaped += rest.slice(lastEscapedPos, i); + escaped += '%22'; + lastEscapedPos = i + 1; + break; + case 39: // '\'' + if (i > lastEscapedPos) + escaped += rest.slice(lastEscapedPos, i); + escaped += '%27'; + lastEscapedPos = i + 1; + break; + case 60: // '<' + if (i > lastEscapedPos) + escaped += rest.slice(lastEscapedPos, i); + escaped += '%3C'; + lastEscapedPos = i + 1; + break; + case 62: // '>' + if (i > lastEscapedPos) + escaped += rest.slice(lastEscapedPos, i); + escaped += '%3E'; + lastEscapedPos = i + 1; + break; + case 92: // '\\' + if (i > lastEscapedPos) + escaped += rest.slice(lastEscapedPos, i); + escaped += '%5C'; + lastEscapedPos = i + 1; + break; + case 94: // '^' + if (i > lastEscapedPos) + escaped += rest.slice(lastEscapedPos, i); + escaped += '%5E'; + lastEscapedPos = i + 1; + break; + case 96: // '`' + if (i > lastEscapedPos) + escaped += rest.slice(lastEscapedPos, i); + escaped += '%60'; + lastEscapedPos = i + 1; + break; + case 123: // '{' + if (i > lastEscapedPos) + escaped += rest.slice(lastEscapedPos, i); + escaped += '%7B'; + lastEscapedPos = i + 1; + break; + case 124: // '|' + if (i > lastEscapedPos) + escaped += rest.slice(lastEscapedPos, i); + escaped += '%7C'; + lastEscapedPos = i + 1; + break; + case 125: // '}' + if (i > lastEscapedPos) + escaped += rest.slice(lastEscapedPos, i); + escaped += '%7D'; + lastEscapedPos = i + 1; + break; + } + } + if (lastEscapedPos === 0) // Nothing has been escaped. + return; + // There are ordinary characters at the end. + if (lastEscapedPos < rest.length) + return escaped + rest.slice(lastEscapedPos); + else // The last character is escaped. + return escaped; +} + // format a parsed object into a url string function urlFormat(obj) { // ensure it's an object, and not a string url. // If it's an obj, this is a no-op. // this way, you can call url_format() on strings // to clean up potentially wonky urls. - if (util.isString(obj)) obj = urlParse(obj); - if (!(obj instanceof Url)) return Url.prototype.format.call(obj); + if (typeof obj === 'string') obj = urlParse(obj); + + else if (typeof obj !== 'object' || obj === null) + throw new TypeError('Parameter "urlObj" must be an object, not ' + + obj === null ? 'null' : typeof obj); + + else if (!(obj instanceof Url)) return Url.prototype.format.call(obj); + return obj.format(); } Url.prototype.format = function() { var auth = this.auth || ''; if (auth) { - auth = encodeURIComponent(auth); - auth = auth.replace(/%3A/i, ':'); + auth = encodeAuth(auth); auth += '@'; } - var protocol = this.protocol || '', - pathname = this.pathname || '', - hash = this.hash || '', - host = false, - query = ''; + var protocol = this.protocol || ''; + var pathname = this.pathname || ''; + var hash = this.hash || ''; + var host = ''; + var query = ''; if (this.host) { host = auth + this.host; @@ -404,33 +606,59 @@ Url.prototype.format = function() { } } - if (this.query && - util.isObject(this.query) && - Object.keys(this.query).length) { + if (this.query !== null && typeof this.query === 'object') query = querystring.stringify(this.query); - } var search = this.search || (query && ('?' + query)) || ''; - if (protocol && protocol.substr(-1) !== ':') protocol += ':'; + if (protocol && protocol.charCodeAt(protocol.length - 1) !== 58/*:*/) + protocol += ':'; + + var newPathname = ''; + var lastPos = 0; + for (var i = 0; i < pathname.length; ++i) { + switch (pathname.charCodeAt(i)) { + case 35: // '#' + if (i - lastPos > 0) + newPathname += pathname.slice(lastPos, i); + newPathname += '%23'; + lastPos = i + 1; + break; + case 63: // '?' + if (i - lastPos > 0) + newPathname += pathname.slice(lastPos, i); + newPathname += '%3F'; + lastPos = i + 1; + break; + } + } + if (lastPos > 0) { + if (lastPos !== pathname.length) + pathname = newPathname + pathname.slice(lastPos); + else + pathname = newPathname; + } // only the slashedProtocols get the //. Not mailto:, xmpp:, etc. // unless they had them to begin with. - if (this.slashes || - (!protocol || slashedProtocol[protocol]) && host !== false) { - host = '//' + (host || ''); - if (pathname && pathname.charAt(0) !== '/') pathname = '/' + pathname; - } else if (!host) { - host = ''; + if (this.slashes || slashedProtocol[protocol]) { + if (this.slashes || host) { + if (pathname && pathname.charCodeAt(0) !== 47/*/*/) + pathname = '/' + pathname; + host = '//' + host; + } else if (protocol.length >= 4 && + protocol.charCodeAt(0) === 102/*f*/ && + protocol.charCodeAt(1) === 105/*i*/ && + protocol.charCodeAt(2) === 108/*l*/ && + protocol.charCodeAt(3) === 101/*e*/) { + host = '//'; + } } - if (hash && hash.charAt(0) !== '#') hash = '#' + hash; - if (search && search.charAt(0) !== '?') search = '?' + search; + search = search.replace(/#/g, '%23'); - pathname = pathname.replace(/[?#]/g, function(match) { - return encodeURIComponent(match); - }); - search = search.replace('#', '%23'); + if (hash && hash.charCodeAt(0) !== 35/*#*/) hash = '#' + hash; + if (search && search.charCodeAt(0) !== 63/*?*/) search = '?' + search; return protocol + host + pathname + search + hash; }; @@ -449,7 +677,7 @@ function urlResolveObject(source, relative) { } Url.prototype.resolveObject = function(relative) { - if (util.isString(relative)) { + if (typeof relative === 'string') { var rel = new Url(); rel.parse(relative, false, true); relative = rel; @@ -512,8 +740,10 @@ Url.prototype.resolveObject = function(relative) { } result.protocol = relative.protocol; - if (!relative.host && !hostlessProtocol[relative.protocol]) { - var relPath = (relative.pathname || '').split('/'); + if (!relative.host && + !/^file:?$/.test(relative.protocol) && + !hostlessProtocol[relative.protocol]) { + const relPath = (relative.pathname || '').split('/'); while (relPath.length && !(relative.host = relPath.shift())); if (!relative.host) relative.host = ''; if (!relative.hostname) relative.hostname = ''; @@ -540,17 +770,17 @@ Url.prototype.resolveObject = function(relative) { return result; } - var isSourceAbs = (result.pathname && result.pathname.charAt(0) === '/'), - isRelAbs = ( - relative.host || - relative.pathname && relative.pathname.charAt(0) === '/' - ), - mustEndAbs = (isRelAbs || isSourceAbs || - (result.host && relative.pathname)), - removeAllDots = mustEndAbs, - srcPath = result.pathname && result.pathname.split('/') || [], - relPath = relative.pathname && relative.pathname.split('/') || [], - psychotic = result.protocol && !slashedProtocol[result.protocol]; + var isSourceAbs = (result.pathname && result.pathname.charAt(0) === '/'); + var isRelAbs = ( + relative.host || + relative.pathname && relative.pathname.charAt(0) === '/' + ); + var mustEndAbs = (isRelAbs || isSourceAbs || + (result.host && relative.pathname)); + var removeAllDots = mustEndAbs; + var srcPath = result.pathname && result.pathname.split('/') || []; + var relPath = relative.pathname && relative.pathname.split('/') || []; + var psychotic = result.protocol && !slashedProtocol[result.protocol]; // if the url is a non-slashed url, then relative // links like ../.. should be able @@ -568,6 +798,7 @@ Url.prototype.resolveObject = function(relative) { if (relative.protocol) { relative.hostname = null; relative.port = null; + result.auth = null; if (relative.host) { if (relPath[0] === '') relPath[0] = relative.host; else relPath.unshift(relative.host); @@ -579,10 +810,15 @@ Url.prototype.resolveObject = function(relative) { if (isRelAbs) { // it's absolute. - result.host = (relative.host || relative.host === '') ? - relative.host : result.host; - result.hostname = (relative.hostname || relative.hostname === '') ? - relative.hostname : result.hostname; + if (relative.host || relative.host === '') { + if (result.host !== relative.host) result.auth = null; + result.host = relative.host; + result.port = relative.port; + } + if (relative.hostname || relative.hostname === '') { + if (result.hostname !== relative.hostname) result.auth = null; + result.hostname = relative.hostname; + } result.search = relative.search; result.query = relative.query; srcPath = relPath; @@ -595,16 +831,16 @@ Url.prototype.resolveObject = function(relative) { srcPath = srcPath.concat(relPath); result.search = relative.search; result.query = relative.query; - } else if (!util.isNullOrUndefined(relative.search)) { + } else if (relative.search !== null && relative.search !== undefined) { // just pull out the search. // like href='?foo'. // Put this after the other two cases because it simplifies the booleans if (psychotic) { result.hostname = result.host = srcPath.shift(); - //occationaly the auth can get stuck only in host + //occasionally the auth can get stuck only in host //this especially happens in cases like //url.resolveObject('mailto:local1@domain1', 'local2@domain2') - var authInHost = result.host && result.host.indexOf('@') > 0 ? + const authInHost = result.host && result.host.indexOf('@') > 0 ? result.host.split('@') : false; if (authInHost) { result.auth = authInHost.shift(); @@ -614,7 +850,7 @@ Url.prototype.resolveObject = function(relative) { result.search = relative.search; result.query = relative.query; //to support http.request - if (!util.isNull(result.pathname) || !util.isNull(result.search)) { + if (result.pathname !== null || result.search !== null) { result.path = (result.pathname ? result.pathname : '') + (result.search ? result.search : ''); } @@ -647,15 +883,15 @@ Url.prototype.resolveObject = function(relative) { // strip single dots, resolve double dots to parent dir // if the path tries to go above the root, `up` ends up > 0 var up = 0; - for (var i = srcPath.length; i >= 0; i--) { + for (var i = srcPath.length - 1; i >= 0; i--) { last = srcPath[i]; if (last === '.') { - srcPath.splice(i, 1); + spliceOne(srcPath, i); } else if (last === '..') { - srcPath.splice(i, 1); + spliceOne(srcPath, i); up++; } else if (up) { - srcPath.splice(i, 1); + spliceOne(srcPath, i); up--; } } @@ -683,10 +919,10 @@ Url.prototype.resolveObject = function(relative) { if (psychotic) { result.hostname = result.host = isAbsolute ? '' : srcPath.length ? srcPath.shift() : ''; - //occationaly the auth can get stuck only in host + //occasionally the auth can get stuck only in host //this especially happens in cases like //url.resolveObject('mailto:local1@domain1', 'local2@domain2') - var authInHost = result.host && result.host.indexOf('@') > 0 ? + const authInHost = result.host && result.host.indexOf('@') > 0 ? result.host.split('@') : false; if (authInHost) { result.auth = authInHost.shift(); @@ -708,7 +944,7 @@ Url.prototype.resolveObject = function(relative) { } //to support request.http - if (!util.isNull(result.pathname) || !util.isNull(result.search)) { + if (result.pathname !== null || result.search !== null) { result.path = (result.pathname ? result.pathname : '') + (result.search ? result.search : ''); } @@ -724,9 +960,16 @@ Url.prototype.parseHost = function() { if (port) { port = port[0]; if (port !== ':') { - this.port = port.substr(1); + this.port = port.slice(1); } - host = host.substr(0, host.length - port.length); + host = host.slice(0, host.length - port.length); } if (host) this.hostname = host; }; + +// About 1.5x faster than the two-arg version of Array#splice(). +function spliceOne(list, index) { + for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) + list[i] = list[k]; + list.pop(); +}