// Copyright Joyent, Inc. and other Node contributors. // // 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 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'; const { Boolean, Int8Array, ObjectAssign, ObjectKeys, StringPrototypeCharCodeAt, decodeURIComponent, } = primordials; const { toASCII } = internalBinding('encoding_binding'); const { encodeStr, hexTable } = require('internal/querystring'); const querystring = require('querystring'); const { ERR_INVALID_ARG_TYPE, ERR_INVALID_URL, } = require('internal/errors').codes; const { validateString, validateObject, } = require('internal/validators'); // This ensures setURLConstructor() is called before the native // URL::ToObject() method is used. const { spliceOne } = require('internal/util'); const { isInsideNodeModules } = internalBinding('util'); // WHATWG URL implementation provided by internal/url const { URL, URLSearchParams, domainToASCII, domainToUnicode, fileURLToPath, pathToFileURL: _pathToFileURL, urlToHttpOptions, unsafeProtocol, hostlessProtocol, slashedProtocol, } = require('internal/url'); const bindingUrl = internalBinding('url'); // Original url.parse() API function Url() { this.protocol = null; this.slashes = null; this.auth = null; this.host = null; this.port = null; this.hostname = null; this.hash = null; this.search = null; this.query = null; this.pathname = null; this.path = null; this.href = null; } // Reference: RFC 3986, RFC 1808, RFC 2396 // define these here so at least they only have to be // compiled once on the first module load. 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; const { CHAR_SPACE, CHAR_TAB, CHAR_CARRIAGE_RETURN, CHAR_LINE_FEED, CHAR_NO_BREAK_SPACE, CHAR_ZERO_WIDTH_NOBREAK_SPACE, CHAR_HASH, CHAR_FORWARD_SLASH, CHAR_LEFT_SQUARE_BRACKET, CHAR_RIGHT_SQUARE_BRACKET, CHAR_LEFT_ANGLE_BRACKET, CHAR_RIGHT_ANGLE_BRACKET, CHAR_LEFT_CURLY_BRACKET, CHAR_RIGHT_CURLY_BRACKET, CHAR_QUESTION_MARK, CHAR_DOUBLE_QUOTE, CHAR_SINGLE_QUOTE, CHAR_PERCENT, CHAR_SEMICOLON, CHAR_BACKWARD_SLASH, CHAR_CIRCUMFLEX_ACCENT, CHAR_GRAVE_ACCENT, CHAR_VERTICAL_LINE, CHAR_AT, CHAR_COLON, } = require('internal/constants'); let urlParseWarned = false; function urlParse(url, parseQueryString, slashesDenoteHost) { if (!urlParseWarned && !isInsideNodeModules(100, true)) { urlParseWarned = true; process.emitWarning( '`url.parse()` behavior is not standardized and prone to ' + 'errors that have security implications. Use the WHATWG URL API ' + 'instead. CVEs are not issued for `url.parse()` vulnerabilities.', 'DeprecationWarning', 'DEP0169', ); } if (url instanceof Url) return url; const urlObject = new Url(); urlObject.parse(url, parseQueryString, slashesDenoteHost); return urlObject; } function isIpv6Hostname(hostname) { return ( StringPrototypeCharCodeAt(hostname, 0) === CHAR_LEFT_SQUARE_BRACKET && StringPrototypeCharCodeAt(hostname, hostname.length - 1) === CHAR_RIGHT_SQUARE_BRACKET ); } // This prevents some common spoofing bugs due to our use of IDNA toASCII. For // compatibility, the set of characters we use here is the *intersection* of // "forbidden host code point" in the WHATWG URL Standard [1] and the // characters in the host parsing loop in Url.prototype.parse, with the // following additions: // // - ':' since this could cause a "protocol spoofing" bug // - '@' since this could cause parts of the hostname to be confused with auth // - '[' and ']' since this could cause a non-IPv6 hostname to be interpreted // as IPv6 by isIpv6Hostname above // // [1]: https://url.spec.whatwg.org/#forbidden-host-code-point const forbiddenHostChars = /[\0\t\n\r #%/:<>?@[\\\]^|]/; // For IPv6, permit '[', ']', and ':'. const forbiddenHostCharsIpv6 = /[\0\t\n\r #%/<>?@\\^|]/; Url.prototype.parse = function parse(url, parseQueryString, slashesDenoteHost) { validateString(url, '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 let hasHash = false; let hasAt = false; let start = -1; let end = -1; let rest = ''; let lastPos = 0; for (let i = 0, 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 < 33 || code === CHAR_NO_BREAK_SPACE || code === CHAR_ZERO_WIDTH_NOBREAK_SPACE; 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; } // Only convert backslashes while we haven't seen a split character if (!split) { switch (code) { case CHAR_AT: hasAt = true; break; case CHAR_HASH: hasHash = true; // Fall through case CHAR_QUESTION_MARK: split = true; break; case CHAR_BACKWARD_SLASH: if (i - lastPos > 0) rest += url.slice(lastPos, i); rest += '/'; lastPos = i + 1; break; } } else if (!hasHash && code === CHAR_HASH) { hasHash = true; } } // 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 && !hasHash && !hasAt) { // Try fast path regexp const simplePath = simplePathPattern.exec(rest); if (simplePath) { this.path = rest; this.href = rest; this.pathname = simplePath[1]; if (simplePath[2]) { this.search = simplePath[2]; if (parseQueryString) { this.query = querystring.parse(this.search.slice(1)); } else { this.query = this.search.slice(1); } } else if (parseQueryString) { this.search = null; this.query = { __proto__: null }; } return this; } } let proto = protocolPattern.exec(rest); let lowerProto; if (proto) { proto = proto[0]; lowerProto = proto.toLowerCase(); this.protocol = lowerProto; 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. let slashes; if (slashesDenoteHost || proto || hostPattern.test(rest)) { slashes = rest.charCodeAt(0) === CHAR_FORWARD_SLASH && rest.charCodeAt(1) === CHAR_FORWARD_SLASH; if (slashes && !(proto && hostlessProtocol.has(lowerProto))) { rest = rest.slice(2); this.slashes = true; } } if (!hostlessProtocol.has(lowerProto) && (slashes || (proto && !slashedProtocol.has(proto)))) { // there's a hostname. // the first instance of /, ?, ;, or # ends the host. // // If there is an @ in the hostname, then non-host chars *are* allowed // to the left of the last @ sign, unless some host-ending character // comes *before* the @-sign. // URLs are obnoxious. // // ex: // http://a@b@c/ => user:a@b host:c // http://a@b?@c => user:a host:b path:/?@c let hostEnd = -1; let atSign = -1; let nonHost = -1; for (let i = 0; i < rest.length; ++i) { switch (rest.charCodeAt(i)) { case CHAR_TAB: case CHAR_LINE_FEED: case CHAR_CARRIAGE_RETURN: // WHATWG URL removes tabs, newlines, and carriage returns. Let's do that too. rest = rest.slice(0, i) + rest.slice(i + 1); i -= 1; break; case CHAR_SPACE: case CHAR_DOUBLE_QUOTE: case CHAR_PERCENT: case CHAR_SINGLE_QUOTE: case CHAR_SEMICOLON: case CHAR_LEFT_ANGLE_BRACKET: case CHAR_RIGHT_ANGLE_BRACKET: case CHAR_BACKWARD_SLASH: case CHAR_CIRCUMFLEX_ACCENT: case CHAR_GRAVE_ACCENT: case CHAR_LEFT_CURLY_BRACKET: case CHAR_VERTICAL_LINE: case CHAR_RIGHT_CURLY_BRACKET: // Characters that are never ever allowed in a hostname from RFC 2396 if (nonHost === -1) nonHost = i; break; case CHAR_HASH: case CHAR_FORWARD_SLASH: case CHAR_QUESTION_MARK: // Find the first instance of any host-ending characters if (nonHost === -1) nonHost = i; hostEnd = i; break; case CHAR_AT: // 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; } start = 0; if (atSign !== -1) { this.auth = decodeURIComponent(rest.slice(0, atSign)); start = atSign + 1; } if (nonHost === -1) { this.host = rest.slice(start); rest = ''; } else { this.host = rest.slice(start, nonHost); rest = rest.slice(nonHost); } // pull out port. this.parseHost(); // We've indicated that there is a hostname, // so even if it's empty, it has to be present. if (typeof this.hostname !== 'string') this.hostname = ''; const hostname = this.hostname; // If hostname begins with [ and ends with ] // assume that it's an IPv6 address. const ipv6Hostname = isIpv6Hostname(hostname); // validate a little. if (!ipv6Hostname) { rest = getHostname(this, rest, hostname, url); } if (this.hostname.length > hostnameMaxLen) { this.hostname = ''; } else { // Hostnames are always lower case. this.hostname = this.hostname.toLowerCase(); } if (this.hostname !== '') { if (ipv6Hostname) { if (forbiddenHostCharsIpv6.test(this.hostname)) { throw new ERR_INVALID_URL(url); } } else { // IDNA Support: Returns a punycoded representation of "domain". // 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 = toASCII(this.hostname); // Prevent two potential routes of hostname spoofing. // 1. If this.hostname is empty, it must have become empty due to toASCII // since we checked this.hostname above. // 2. If any of forbiddenHostChars appears in this.hostname, it must have // also gotten in due to toASCII. This is since getHostname would have // filtered them out otherwise. // Rather than trying to correct this by moving the non-host part into // the pathname as we've done in getHostname, throw an exception to // convey the severity of this issue. if (this.hostname === '' || forbiddenHostChars.test(this.hostname)) { throw new ERR_INVALID_URL(url); } } } const p = this.port ? ':' + this.port : ''; const h = this.hostname || ''; this.host = h + p; // strip [ and ] from the hostname // the host field still retains them, though if (ipv6Hostname) { this.hostname = this.hostname.slice(1, -1); if (rest[0] !== '/') { rest = '/' + rest; } } } // Now rest is set to the post-host stuff. // Chop off any delim chars. if (!unsafeProtocol.has(lowerProto)) { // First, make 100% sure that any "autoEscape" chars get // escaped, even if encodeURIComponent doesn't think they // need to be. rest = autoEscapeStr(rest); } let questionIdx = -1; let hashIdx = -1; for (let i = 0; i < rest.length; ++i) { const code = rest.charCodeAt(i); if (code === CHAR_HASH) { this.hash = rest.slice(i); hashIdx = i; break; } else if (code === CHAR_QUESTION_MARK && questionIdx === -1) { questionIdx = i; } } 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); } } else if (parseQueryString) { // No query string, but parseQueryString still requested this.search = null; this.query = { __proto__: null }; } const useQuestionIdx = questionIdx !== -1 && (hashIdx === -1 || questionIdx < hashIdx); const firstIdx = useQuestionIdx ? questionIdx : hashIdx; if (firstIdx === -1) { if (rest.length > 0) this.pathname = rest; } else if (firstIdx > 0) { this.pathname = rest.slice(0, firstIdx); } if (slashedProtocol.has(lowerProto) && this.hostname && !this.pathname) { this.pathname = '/'; } // To support http.request if (this.pathname || this.search) { const p = this.pathname || ''; const s = this.search || ''; this.path = p + s; } // Finally, reconstruct the href based on what has been validated. this.href = this.format(); return this; }; let warnInvalidPort = true; function getHostname(self, rest, hostname, url) { for (let i = 0; i < hostname.length; ++i) { const code = hostname.charCodeAt(i); const isValid = (code !== CHAR_FORWARD_SLASH && code !== CHAR_BACKWARD_SLASH && code !== CHAR_HASH && code !== CHAR_QUESTION_MARK && code !== CHAR_COLON); if (!isValid) { // If leftover starts with :, then it represents an invalid port. // But url.parse() is lenient about it for now. // Issue a warning and continue. if (warnInvalidPort && code === CHAR_COLON) { const detail = `The URL ${url} is invalid. Future versions of Node.js will throw an error.`; process.emitWarning(detail, 'DeprecationWarning', 'DEP0170'); warnInvalidPort = false; } self.hostname = hostname.slice(0, i); return `/${hostname.slice(i)}${rest}`; } } return rest; } // Escaped characters. Use empty strings to fill up unused entries. // Using Array is faster than Object/Map const escapedCodes = [ /* 0 - 9 */ '', '', '', '', '', '', '', '', '', '%09', /* 10 - 19 */ '%0A', '', '', '%0D', '', '', '', '', '', '', /* 20 - 29 */ '', '', '', '', '', '', '', '', '', '', /* 30 - 39 */ '', '', '%20', '', '%22', '', '', '', '', '%27', /* 40 - 49 */ '', '', '', '', '', '', '', '', '', '', /* 50 - 59 */ '', '', '', '', '', '', '', '', '', '', /* 60 - 69 */ '%3C', '', '%3E', '', '', '', '', '', '', '', /* 70 - 79 */ '', '', '', '', '', '', '', '', '', '', /* 80 - 89 */ '', '', '', '', '', '', '', '', '', '', /* 90 - 99 */ '', '', '%5C', '', '%5E', '', '%60', '', '', '', /* 100 - 109 */ '', '', '', '', '', '', '', '', '', '', /* 110 - 119 */ '', '', '', '', '', '', '', '', '', '', /* 120 - 125 */ '', '', '', '%7B', '%7C', '%7D', ]; // Automatically escape all delimiters and unwise characters from RFC 2396. // Also escape single quotes in case of an XSS attack. // Return the escaped string. function autoEscapeStr(rest) { let escaped = ''; let lastEscapedPos = 0; for (let i = 0; i < rest.length; ++i) { // `escaped` contains substring up to the last escaped character. const escapedChar = escapedCodes[rest.charCodeAt(i)]; if (escapedChar) { // Concat if there are ordinary characters in the middle. if (i > lastEscapedPos) escaped += rest.slice(lastEscapedPos, i); escaped += escapedChar; lastEscapedPos = i + 1; } } if (lastEscapedPos === 0) // Nothing has been escaped. return rest; // There are ordinary characters at the end. if (lastEscapedPos < rest.length) escaped += rest.slice(lastEscapedPos); return escaped; } // Format a parsed object into a url string function urlFormat(urlObject, options) { // Ensure it's an object, and not a string url. // If it's an object, this is a no-op. // this way, you can call urlParse() on strings // to clean up potentially wonky urls. if (typeof urlObject === 'string') { urlObject = urlParse(urlObject); } else if (typeof urlObject !== 'object' || urlObject === null) { throw new ERR_INVALID_ARG_TYPE('urlObject', ['Object', 'string'], urlObject); } else if (urlObject instanceof URL) { let fragment = true; let unicode = false; let search = true; let auth = true; if (options) { validateObject(options, 'options'); if (options.fragment != null) { fragment = Boolean(options.fragment); } if (options.unicode != null) { unicode = Boolean(options.unicode); } if (options.search != null) { search = Boolean(options.search); } if (options.auth != null) { auth = Boolean(options.auth); } } return bindingUrl.format(urlObject.href, fragment, unicode, search, auth); } return Url.prototype.format.call(urlObject); } // These characters do not need escaping: // ! - . _ ~ // ' ( ) * : // digits // alpha (uppercase) // alpha (lowercase) const noEscapeAuth = new Int8Array([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x00 - 0x0F 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x10 - 0x1F 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, // 0x20 - 0x2F 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, // 0x30 - 0x3F 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x40 - 0x4F 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, // 0x50 - 0x5F 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x60 - 0x6F 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, // 0x70 - 0x7F ]); Url.prototype.format = function format() { let auth = this.auth || ''; if (auth) { auth = encodeStr(auth, noEscapeAuth, hexTable); auth += '@'; } let protocol = this.protocol || ''; let pathname = this.pathname || ''; let hash = this.hash || ''; let host = ''; let query = ''; if (this.host) { host = auth + this.host; } else if (this.hostname) { host = auth + ( this.hostname.includes(':') && !isIpv6Hostname(this.hostname) ? '[' + this.hostname + ']' : this.hostname ); if (this.port) { host += ':' + this.port; } } if (this.query !== null && typeof this.query === 'object') { query = querystring.stringify(this.query); } let search = this.search || (query && ('?' + query)) || ''; if (protocol && protocol.charCodeAt(protocol.length - 1) !== 58/* : */) protocol += ':'; let newPathname = ''; let lastPos = 0; for (let i = 0; i < pathname.length; ++i) { switch (pathname.charCodeAt(i)) { case CHAR_HASH: if (i - lastPos > 0) newPathname += pathname.slice(lastPos, i); newPathname += '%23'; lastPos = i + 1; break; case CHAR_QUESTION_MARK: 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 || slashedProtocol.has(protocol)) { if (this.slashes || host) { if (pathname && pathname.charCodeAt(0) !== CHAR_FORWARD_SLASH) 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 = '//'; } } search = search.replace(/#/g, '%23'); if (hash && hash.charCodeAt(0) !== CHAR_HASH) hash = '#' + hash; if (search && search.charCodeAt(0) !== CHAR_QUESTION_MARK) search = '?' + search; return protocol + host + pathname + search + hash; }; function urlResolve(source, relative) { return urlParse(source, false, true).resolve(relative); } Url.prototype.resolve = function resolve(relative) { return this.resolveObject(urlParse(relative, false, true)).format(); }; function urlResolveObject(source, relative) { if (!source) return relative; return urlParse(source, false, true).resolveObject(relative); } Url.prototype.resolveObject = function resolveObject(relative) { if (typeof relative === 'string') { const rel = new Url(); rel.parse(relative, false, true); relative = rel; } const result = new Url(); ObjectAssign(result, this); // Hash is always overridden, no matter what. // even href="" will remove it. result.hash = relative.hash; // If the relative url is empty, then there's nothing left to do here. if (relative.href === '') { result.href = result.format(); return result; } // Hrefs like //foo/bar always cut to the protocol. if (relative.slashes && !relative.protocol) { // Take everything except the protocol from relative const relativeWithoutProtocol = ObjectKeys(relative).reduce((acc, key) => { if (key !== 'protocol') { acc[key] = relative[key]; } return acc; }, {}); ObjectAssign(result, relativeWithoutProtocol); // urlParse appends trailing / to urls like http://www.example.com if (slashedProtocol.has(result.protocol) && result.hostname && !result.pathname) { result.path = result.pathname = '/'; } result.href = result.format(); return result; } if (relative.protocol && relative.protocol !== result.protocol) { // If it's a known url protocol, then changing // the protocol does weird things // first, if it's not file:, then we MUST have a host, // and if there was a path // to begin with, then we MUST have a path. // if it is file:, then the host is dropped, // because that's known to be hostless. // anything else is assumed to be absolute. if (!slashedProtocol.has(relative.protocol)) { ObjectAssign(result, relative); result.href = result.format(); return result; } result.protocol = relative.protocol; if (!relative.host && !/^file:?$/.test(relative.protocol) && !hostlessProtocol.has(relative.protocol)) { const relPath = (relative.pathname || '').split('/'); while (relPath.length && !(relative.host = relPath.shift())); relative.host ||= ''; relative.hostname ||= ''; if (relPath[0] !== '') relPath.unshift(''); if (relPath.length < 2) relPath.unshift(''); result.pathname = relPath.join('/'); } else { result.pathname = relative.pathname; } result.search = relative.search; result.query = relative.query; result.host = relative.host || ''; result.auth = relative.auth; result.hostname = relative.hostname || relative.host; result.port = relative.port; // To support http.request if (result.pathname || result.search) { const p = result.pathname || ''; const s = result.search || ''; result.path = p + s; } result.slashes ||= relative.slashes; result.href = result.format(); return result; } const isSourceAbs = (result.pathname && result.pathname.charAt(0) === '/'); const isRelAbs = ( relative.host || (relative.pathname && relative.pathname.charAt(0) === '/') ); let mustEndAbs = (isRelAbs || isSourceAbs || (result.host && relative.pathname)); const removeAllDots = mustEndAbs; let srcPath = (result.pathname && result.pathname.split('/')) || []; const relPath = (relative.pathname && relative.pathname.split('/')) || []; const noLeadingSlashes = result.protocol && !slashedProtocol.has(result.protocol); // If the url is a non-slashed url, then relative // links like ../.. should be able // to crawl up to the hostname, as well. This is strange. // result.protocol has already been set by now. // Later on, put the first path part into the host field. if (noLeadingSlashes) { result.hostname = ''; result.port = null; if (result.host) { if (srcPath[0] === '') srcPath[0] = result.host; else srcPath.unshift(result.host); } result.host = ''; 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); } relative.host = null; } mustEndAbs &&= (relPath[0] === '' || srcPath[0] === ''); } if (isRelAbs) { // it's absolute. 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; // Fall through to the dot-handling below. } else if (relPath.length) { // it's relative // throw away the existing file, and take the new path instead. srcPath ||= []; srcPath.pop(); srcPath = srcPath.concat(relPath); result.search = relative.search; result.query = relative.query; } 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 (noLeadingSlashes) { result.hostname = result.host = srcPath.shift(); // Occasionally the auth can get stuck only in host. // This especially happens in cases like // url.resolveObject('mailto:local1@domain1', 'local2@domain2') const authInHost = result.host && result.host.indexOf('@') > 0 && result.host.split('@'); if (authInHost) { result.auth = authInHost.shift(); result.host = result.hostname = authInHost.shift(); } } result.search = relative.search; result.query = relative.query; // To support http.request if (result.pathname !== null || result.search !== null) { result.path = (result.pathname ? result.pathname : '') + (result.search ? result.search : ''); } result.href = result.format(); return result; } if (!srcPath.length) { // No path at all. All other things were already handled above. result.pathname = null; // To support http.request if (result.search) { result.path = '/' + result.search; } else { result.path = null; } result.href = result.format(); return result; } // If a url ENDs in . or .., then it must get a trailing slash. // however, if it ends in anything else non-slashy, // then it must NOT get a trailing slash. let last = srcPath.slice(-1)[0]; const hasTrailingSlash = ( ((result.host || relative.host || srcPath.length > 1) && (last === '.' || last === '..')) || last === ''); // Strip single dots, resolve double dots to parent dir // if the path tries to go above the root, `up` ends up > 0 let up = 0; for (let i = srcPath.length - 1; i >= 0; i--) { last = srcPath[i]; if (last === '.') { spliceOne(srcPath, i); } else if (last === '..') { spliceOne(srcPath, i); up++; } else if (up) { spliceOne(srcPath, i); up--; } } // If the path is allowed to go above the root, restore leading ..s if (!mustEndAbs && !removeAllDots) { while (up--) { srcPath.unshift('..'); } } if (mustEndAbs && srcPath[0] !== '' && (!srcPath[0] || srcPath[0].charAt(0) !== '/')) { srcPath.unshift(''); } if (hasTrailingSlash && (srcPath.join('/').slice(-1) !== '/')) { srcPath.push(''); } const isAbsolute = srcPath[0] === '' || (srcPath[0] && srcPath[0].charAt(0) === '/'); // put the host back if (noLeadingSlashes) { result.hostname = result.host = isAbsolute ? '' : srcPath.length ? srcPath.shift() : ''; // Occasionally the auth can get stuck only in host. // This especially happens in cases like // url.resolveObject('mailto:local1@domain1', 'local2@domain2') const authInHost = result.host && result.host.indexOf('@') > 0 ? result.host.split('@') : false; if (authInHost) { result.auth = authInHost.shift(); result.host = result.hostname = authInHost.shift(); } } mustEndAbs ||= (result.host && srcPath.length); if (mustEndAbs && !isAbsolute) { srcPath.unshift(''); } if (!srcPath.length) { result.pathname = null; result.path = null; } else { result.pathname = srcPath.join('/'); } // To support request.http if (result.pathname !== null || result.search !== null) { result.path = (result.pathname ? result.pathname : '') + (result.search ? result.search : ''); } result.auth = relative.auth || result.auth; result.slashes ||= relative.slashes; result.href = result.format(); return result; }; Url.prototype.parseHost = function parseHost() { let host = this.host; let port = portPattern.exec(host); if (port) { port = port[0]; if (port !== ':') { this.port = port.slice(1); } host = host.slice(0, host.length - port.length); } if (host) this.hostname = host; }; // When used internally, we are not obligated to associate TypeError with // this function, so non-strings can be rejected by underlying implementation. // Public API has to validate input and throw appropriate error. function pathToFileURL(path, options) { validateString(path, 'path'); return _pathToFileURL(path, options); } module.exports = { // Original API Url, parse: urlParse, resolve: urlResolve, resolveObject: urlResolveObject, format: urlFormat, // WHATWG API URL, URLSearchParams, domainToASCII, domainToUnicode, // Utilities pathToFileURL, fileURLToPath, urlToHttpOptions, };