// 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. // // Changes from joyent/node: // // 1. No leading slash in paths, // e.g. in `url.parse('http://foo?bar')` pathname is ``, not `/` // // 2. Backslashes are not replaced with slashes, // so `http:\\example.org\` is treated like a relative path // // 3. Trailing colon is treated like a part of the path, // i.e. in `http://example.org:foo` pathname is `:foo` // // 4. Nothing is URL-encoded in the resulting object, // (in joyent/node some chars in auth and paths are encoded) // // 5. `url.parse()` does not have `parseQueryString` argument // // 6. Removed extraneous result properties: `host`, `path`, `query`, etc., // which can be constructed using other parts of the url. // function Url () { this.protocol = null this.slashes = null this.auth = null this.port = null this.hostname = null this.hash = null this.search = null this.pathname = 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]*$/ // Special case for a simple path URL /* eslint-disable-next-line no-useless-escape */ const simplePathPattern = /^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/ // RFC 2396: characters reserved for delimiting URLs. // We actually just auto-escape these. const delims = ['<', '>', '"', '`', ' ', '\r', '\n', '\t'] // RFC 2396: characters not allowed for various reasons. const unwise = ['{', '}', '|', '\\', '^', '`'].concat(delims) // Allowed by RFCs, but cause of XSS attacks. Always escape these. const 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. const nonHostChars = ['%', '/', '?', ';', '#'].concat(autoEscape) const hostEndingChars = ['/', '?', '#'] const hostnameMaxLen = 255 const hostnamePartPattern = /^[+a-z0-9A-Z_-]{0,63}$/ const hostnamePartStart = /^([+a-z0-9A-Z_-]{0,63})(.*)$/ // protocols that can allow "unsafe" and "unwise" chars. // protocols that never have a hostname. const hostlessProtocol = { javascript: true, 'javascript:': true } // protocols that always contain a // bit. const slashedProtocol = { http: true, https: true, ftp: true, gopher: true, file: true, 'http:': true, 'https:': true, 'ftp:': true, 'gopher:': true, 'file:': true } function urlParse (url, slashesDenoteHost) { if (url && url instanceof Url) return url const u = new Url() u.parse(url, slashesDenoteHost) return u } Url.prototype.parse = function (url, slashesDenoteHost) { let lowerProto, hec, slashes let rest = url // trim before proceeding. // This is to support parse stuff like " http://foo.com \n" rest = rest.trim() if (!slashesDenoteHost && url.split('#').length === 1) { // Try fast path regexp const simplePath = simplePathPattern.exec(rest) if (simplePath) { this.pathname = simplePath[1] if (simplePath[2]) { this.search = simplePath[2] } return this } } let proto = protocolPattern.exec(rest) if (proto) { proto = proto[0] lowerProto = proto.toLowerCase() this.protocol = proto rest = rest.substr(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. /* eslint-disable-next-line no-useless-escape */ if (slashesDenoteHost || proto || rest.match(/^\/\/[^@\/]+@[^@\/]+/)) { slashes = rest.substr(0, 2) === '//' if (slashes && !(proto && hostlessProtocol[proto])) { rest = rest.substr(2) this.slashes = true } } if (!hostlessProtocol[proto] && (slashes || (proto && !slashedProtocol[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:c 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 let hostEnd = -1 for (let i = 0; i < hostEndingChars.length; i++) { 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. let 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) } // Now we have a portion which is definitely the auth. // Pull that off. if (atSign !== -1) { auth = rest.slice(0, atSign) rest = rest.slice(atSign + 1) this.auth = auth } // the host is the remaining to the left of the first non-host char hostEnd = -1 for (let i = 0; i < nonHostChars.length; i++) { hec = rest.indexOf(nonHostChars[i]) if (hec !== -1 && (hostEnd === -1 || hec < hostEnd)) { hostEnd = hec } } // if we still have not hit it, then the entire thing is a host. if (hostEnd === -1) { hostEnd = rest.length } if (rest[hostEnd - 1] === ':') { hostEnd-- } const host = rest.slice(0, hostEnd) rest = rest.slice(hostEnd) // pull out port. this.parseHost(host) // we've indicated that there is a hostname, // so even if it's empty, it has to be present. this.hostname = this.hostname || '' // if hostname begins with [ and ends with ] // assume that it's an IPv6 address. const ipv6Hostname = this.hostname[0] === '[' && this.hostname[this.hostname.length - 1] === ']' // validate a little. if (!ipv6Hostname) { const hostparts = this.hostname.split(/\./) for (let i = 0, l = hostparts.length; i < l; i++) { const part = hostparts[i] if (!part) { continue } if (!part.match(hostnamePartPattern)) { let newpart = '' for (let 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)) { const validParts = hostparts.slice(0, i) const notHost = hostparts.slice(i + 1) const 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 } } } } if (this.hostname.length > hostnameMaxLen) { this.hostname = '' } // strip [ and ] from the hostname // the host field still retains them, though if (ipv6Hostname) { this.hostname = this.hostname.substr(1, this.hostname.length - 2) } } // chop off from the tail first. const hash = rest.indexOf('#') if (hash !== -1) { // got a fragment string. this.hash = rest.substr(hash) rest = rest.slice(0, hash) } const qm = rest.indexOf('?') if (qm !== -1) { this.search = rest.substr(qm) rest = rest.slice(0, qm) } if (rest) { this.pathname = rest } if (slashedProtocol[lowerProto] && this.hostname && !this.pathname) { this.pathname = '' } return this } Url.prototype.parseHost = function (host) { let port = portPattern.exec(host) if (port) { port = port[0] if (port !== ':') { this.port = port.substr(1) } host = host.substr(0, host.length - port.length) } if (host) { this.hostname = host } } export default urlParse