• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright Joyent, Inc. and other Node contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a
4// copy of this software and associated documentation files (the
5// "Software"), to deal in the Software without restriction, including
6// without limitation the rights to use, copy, modify, merge, publish,
7// distribute, sublicense, and/or sell copies of the Software, and to permit
8// persons to whom the Software is furnished to do so, subject to the
9// following conditions:
10//
11// The above copyright notice and this permission notice shall be included
12// in all copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
17// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
20// USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22'use strict';
23
24const {
25  ObjectCreate,
26  ObjectKeys,
27  SafeSet,
28} = primordials;
29
30const { toASCII } = require('internal/idna');
31const { encodeStr, hexTable } = require('internal/querystring');
32
33const {
34  ERR_INVALID_ARG_TYPE
35} = require('internal/errors').codes;
36const { validateString } = require('internal/validators');
37
38// This ensures setURLConstructor() is called before the native
39// URL::ToObject() method is used.
40const { spliceOne } = require('internal/util');
41
42// WHATWG URL implementation provided by internal/url
43const {
44  URL,
45  URLSearchParams,
46  domainToASCII,
47  domainToUnicode,
48  formatSymbol,
49  pathToFileURL,
50  fileURLToPath
51} = require('internal/url');
52
53// Original url.parse() API
54
55function Url() {
56  this.protocol = null;
57  this.slashes = null;
58  this.auth = null;
59  this.host = null;
60  this.port = null;
61  this.hostname = null;
62  this.hash = null;
63  this.search = null;
64  this.query = null;
65  this.pathname = null;
66  this.path = null;
67  this.href = null;
68}
69
70// Reference: RFC 3986, RFC 1808, RFC 2396
71
72// define these here so at least they only have to be
73// compiled once on the first module load.
74const protocolPattern = /^[a-z0-9.+-]+:/i;
75const portPattern = /:[0-9]*$/;
76const hostPattern = /^\/\/[^@/]+@[^@/]+/;
77
78// Special case for a simple path URL
79const simplePathPattern = /^(\/\/?(?!\/)[^?\s]*)(\?[^\s]*)?$/;
80
81const hostnameMaxLen = 255;
82// Protocols that can allow "unsafe" and "unwise" chars.
83const unsafeProtocol = new SafeSet([
84  'javascript',
85  'javascript:'
86]);
87// Protocols that never have a hostname.
88const hostlessProtocol = new SafeSet([
89  'javascript',
90  'javascript:'
91]);
92// Protocols that always contain a // bit.
93const slashedProtocol = new SafeSet([
94  'http',
95  'http:',
96  'https',
97  'https:',
98  'ftp',
99  'ftp:',
100  'gopher',
101  'gopher:',
102  'file',
103  'file:',
104  'ws',
105  'ws:',
106  'wss',
107  'wss:'
108]);
109const {
110  CHAR_SPACE,
111  CHAR_TAB,
112  CHAR_CARRIAGE_RETURN,
113  CHAR_LINE_FEED,
114  CHAR_FORM_FEED,
115  CHAR_NO_BREAK_SPACE,
116  CHAR_ZERO_WIDTH_NOBREAK_SPACE,
117  CHAR_HASH,
118  CHAR_FORWARD_SLASH,
119  CHAR_LEFT_SQUARE_BRACKET,
120  CHAR_RIGHT_SQUARE_BRACKET,
121  CHAR_LEFT_ANGLE_BRACKET,
122  CHAR_RIGHT_ANGLE_BRACKET,
123  CHAR_LEFT_CURLY_BRACKET,
124  CHAR_RIGHT_CURLY_BRACKET,
125  CHAR_QUESTION_MARK,
126  CHAR_LOWERCASE_A,
127  CHAR_LOWERCASE_Z,
128  CHAR_UPPERCASE_A,
129  CHAR_UPPERCASE_Z,
130  CHAR_DOT,
131  CHAR_0,
132  CHAR_9,
133  CHAR_HYPHEN_MINUS,
134  CHAR_PLUS,
135  CHAR_UNDERSCORE,
136  CHAR_DOUBLE_QUOTE,
137  CHAR_SINGLE_QUOTE,
138  CHAR_PERCENT,
139  CHAR_SEMICOLON,
140  CHAR_BACKWARD_SLASH,
141  CHAR_CIRCUMFLEX_ACCENT,
142  CHAR_GRAVE_ACCENT,
143  CHAR_VERTICAL_LINE,
144  CHAR_AT,
145} = require('internal/constants');
146
147// Lazy loaded for startup performance.
148let querystring;
149
150function urlParse(url, parseQueryString, slashesDenoteHost) {
151  if (url instanceof Url) return url;
152
153  const urlObject = new Url();
154  urlObject.parse(url, parseQueryString, slashesDenoteHost);
155  return urlObject;
156}
157
158Url.prototype.parse = function parse(url, parseQueryString, slashesDenoteHost) {
159  validateString(url, 'url');
160
161  // Copy chrome, IE, opera backslash-handling behavior.
162  // Back slashes before the query string get converted to forward slashes
163  // See: https://code.google.com/p/chromium/issues/detail?id=25916
164  let hasHash = false;
165  let start = -1;
166  let end = -1;
167  let rest = '';
168  let lastPos = 0;
169  for (let i = 0, inWs = false, split = false; i < url.length; ++i) {
170    const code = url.charCodeAt(i);
171
172    // Find first and last non-whitespace characters for trimming
173    const isWs = code === CHAR_SPACE ||
174                 code === CHAR_TAB ||
175                 code === CHAR_CARRIAGE_RETURN ||
176                 code === CHAR_LINE_FEED ||
177                 code === CHAR_FORM_FEED ||
178                 code === CHAR_NO_BREAK_SPACE ||
179                 code === CHAR_ZERO_WIDTH_NOBREAK_SPACE;
180    if (start === -1) {
181      if (isWs)
182        continue;
183      lastPos = start = i;
184    } else if (inWs) {
185      if (!isWs) {
186        end = -1;
187        inWs = false;
188      }
189    } else if (isWs) {
190      end = i;
191      inWs = true;
192    }
193
194    // Only convert backslashes while we haven't seen a split character
195    if (!split) {
196      switch (code) {
197        case CHAR_HASH:
198          hasHash = true;
199        // Fall through
200        case CHAR_QUESTION_MARK:
201          split = true;
202          break;
203        case CHAR_BACKWARD_SLASH:
204          if (i - lastPos > 0)
205            rest += url.slice(lastPos, i);
206          rest += '/';
207          lastPos = i + 1;
208          break;
209      }
210    } else if (!hasHash && code === CHAR_HASH) {
211      hasHash = true;
212    }
213  }
214
215  // Check if string was non-empty (including strings with only whitespace)
216  if (start !== -1) {
217    if (lastPos === start) {
218      // We didn't convert any backslashes
219
220      if (end === -1) {
221        if (start === 0)
222          rest = url;
223        else
224          rest = url.slice(start);
225      } else {
226        rest = url.slice(start, end);
227      }
228    } else if (end === -1 && lastPos < url.length) {
229      // We converted some backslashes and have only part of the entire string
230      rest += url.slice(lastPos);
231    } else if (end !== -1 && lastPos < end) {
232      // We converted some backslashes and have only part of the entire string
233      rest += url.slice(lastPos, end);
234    }
235  }
236
237  if (!slashesDenoteHost && !hasHash) {
238    // Try fast path regexp
239    const simplePath = simplePathPattern.exec(rest);
240    if (simplePath) {
241      this.path = rest;
242      this.href = rest;
243      this.pathname = simplePath[1];
244      if (simplePath[2]) {
245        this.search = simplePath[2];
246        if (parseQueryString) {
247          if (querystring === undefined) querystring = require('querystring');
248          this.query = querystring.parse(this.search.slice(1));
249        } else {
250          this.query = this.search.slice(1);
251        }
252      } else if (parseQueryString) {
253        this.search = null;
254        this.query = ObjectCreate(null);
255      }
256      return this;
257    }
258  }
259
260  let proto = protocolPattern.exec(rest);
261  let lowerProto;
262  if (proto) {
263    proto = proto[0];
264    lowerProto = proto.toLowerCase();
265    this.protocol = lowerProto;
266    rest = rest.slice(proto.length);
267  }
268
269  // Figure out if it's got a host
270  // user@server is *always* interpreted as a hostname, and url
271  // resolution will treat //foo/bar as host=foo,path=bar because that's
272  // how the browser resolves relative URLs.
273  let slashes;
274  if (slashesDenoteHost || proto || hostPattern.test(rest)) {
275    slashes = rest.charCodeAt(0) === CHAR_FORWARD_SLASH &&
276              rest.charCodeAt(1) === CHAR_FORWARD_SLASH;
277    if (slashes && !(proto && hostlessProtocol.has(lowerProto))) {
278      rest = rest.slice(2);
279      this.slashes = true;
280    }
281  }
282
283  if (!hostlessProtocol.has(lowerProto) &&
284      (slashes || (proto && !slashedProtocol.has(proto)))) {
285
286    // there's a hostname.
287    // the first instance of /, ?, ;, or # ends the host.
288    //
289    // If there is an @ in the hostname, then non-host chars *are* allowed
290    // to the left of the last @ sign, unless some host-ending character
291    // comes *before* the @-sign.
292    // URLs are obnoxious.
293    //
294    // ex:
295    // http://a@b@c/ => user:a@b host:c
296    // http://a@b?@c => user:a host:b path:/?@c
297
298    let hostEnd = -1;
299    let atSign = -1;
300    let nonHost = -1;
301    for (let i = 0; i < rest.length; ++i) {
302      switch (rest.charCodeAt(i)) {
303        case CHAR_TAB:
304        case CHAR_LINE_FEED:
305        case CHAR_CARRIAGE_RETURN:
306        case CHAR_SPACE:
307        case CHAR_DOUBLE_QUOTE:
308        case CHAR_PERCENT:
309        case CHAR_SINGLE_QUOTE:
310        case CHAR_SEMICOLON:
311        case CHAR_LEFT_ANGLE_BRACKET:
312        case CHAR_RIGHT_ANGLE_BRACKET:
313        case CHAR_BACKWARD_SLASH:
314        case CHAR_CIRCUMFLEX_ACCENT:
315        case CHAR_GRAVE_ACCENT:
316        case CHAR_LEFT_CURLY_BRACKET:
317        case CHAR_VERTICAL_LINE:
318        case CHAR_RIGHT_CURLY_BRACKET:
319          // Characters that are never ever allowed in a hostname from RFC 2396
320          if (nonHost === -1)
321            nonHost = i;
322          break;
323        case CHAR_HASH:
324        case CHAR_FORWARD_SLASH:
325        case CHAR_QUESTION_MARK:
326          // Find the first instance of any host-ending characters
327          if (nonHost === -1)
328            nonHost = i;
329          hostEnd = i;
330          break;
331        case CHAR_AT:
332          // At this point, either we have an explicit point where the
333          // auth portion cannot go past, or the last @ char is the decider.
334          atSign = i;
335          nonHost = -1;
336          break;
337      }
338      if (hostEnd !== -1)
339        break;
340    }
341    start = 0;
342    if (atSign !== -1) {
343      this.auth = decodeURIComponent(rest.slice(0, atSign));
344      start = atSign + 1;
345    }
346    if (nonHost === -1) {
347      this.host = rest.slice(start);
348      rest = '';
349    } else {
350      this.host = rest.slice(start, nonHost);
351      rest = rest.slice(nonHost);
352    }
353
354    // pull out port.
355    this.parseHost();
356
357    // We've indicated that there is a hostname,
358    // so even if it's empty, it has to be present.
359    if (typeof this.hostname !== 'string')
360      this.hostname = '';
361
362    const hostname = this.hostname;
363
364    // If hostname begins with [ and ends with ]
365    // assume that it's an IPv6 address.
366    const ipv6Hostname = hostname.charCodeAt(0) === CHAR_LEFT_SQUARE_BRACKET &&
367      hostname.charCodeAt(hostname.length - 1) === CHAR_RIGHT_SQUARE_BRACKET;
368
369    // validate a little.
370    if (!ipv6Hostname) {
371      rest = getHostname(this, rest, hostname);
372    }
373
374    if (this.hostname.length > hostnameMaxLen) {
375      this.hostname = '';
376    } else {
377      // Hostnames are always lower case.
378      this.hostname = this.hostname.toLowerCase();
379    }
380
381    if (!ipv6Hostname) {
382      // IDNA Support: Returns a punycoded representation of "domain".
383      // It only converts parts of the domain name that
384      // have non-ASCII characters, i.e. it doesn't matter if
385      // you call it with a domain that already is ASCII-only.
386
387      // Use lenient mode (`true`) to try to support even non-compliant
388      // URLs.
389      this.hostname = toASCII(this.hostname, true);
390    }
391
392    const p = this.port ? ':' + this.port : '';
393    const h = this.hostname || '';
394    this.host = h + p;
395
396    // strip [ and ] from the hostname
397    // the host field still retains them, though
398    if (ipv6Hostname) {
399      this.hostname = this.hostname.slice(1, -1);
400      if (rest[0] !== '/') {
401        rest = '/' + rest;
402      }
403    }
404  }
405
406  // Now rest is set to the post-host stuff.
407  // Chop off any delim chars.
408  if (!unsafeProtocol.has(lowerProto)) {
409    // First, make 100% sure that any "autoEscape" chars get
410    // escaped, even if encodeURIComponent doesn't think they
411    // need to be.
412    rest = autoEscapeStr(rest);
413  }
414
415  let questionIdx = -1;
416  let hashIdx = -1;
417  for (let i = 0; i < rest.length; ++i) {
418    const code = rest.charCodeAt(i);
419    if (code === CHAR_HASH) {
420      this.hash = rest.slice(i);
421      hashIdx = i;
422      break;
423    } else if (code === CHAR_QUESTION_MARK && questionIdx === -1) {
424      questionIdx = i;
425    }
426  }
427
428  if (questionIdx !== -1) {
429    if (hashIdx === -1) {
430      this.search = rest.slice(questionIdx);
431      this.query = rest.slice(questionIdx + 1);
432    } else {
433      this.search = rest.slice(questionIdx, hashIdx);
434      this.query = rest.slice(questionIdx + 1, hashIdx);
435    }
436    if (parseQueryString) {
437      if (querystring === undefined) querystring = require('querystring');
438      this.query = querystring.parse(this.query);
439    }
440  } else if (parseQueryString) {
441    // No query string, but parseQueryString still requested
442    this.search = null;
443    this.query = ObjectCreate(null);
444  }
445
446  const useQuestionIdx =
447    questionIdx !== -1 && (hashIdx === -1 || questionIdx < hashIdx);
448  const firstIdx = useQuestionIdx ? questionIdx : hashIdx;
449  if (firstIdx === -1) {
450    if (rest.length > 0)
451      this.pathname = rest;
452  } else if (firstIdx > 0) {
453    this.pathname = rest.slice(0, firstIdx);
454  }
455  if (slashedProtocol.has(lowerProto) &&
456      this.hostname && !this.pathname) {
457    this.pathname = '/';
458  }
459
460  // To support http.request
461  if (this.pathname || this.search) {
462    const p = this.pathname || '';
463    const s = this.search || '';
464    this.path = p + s;
465  }
466
467  // Finally, reconstruct the href based on what has been validated.
468  this.href = this.format();
469  return this;
470};
471
472function getHostname(self, rest, hostname) {
473  for (let i = 0; i < hostname.length; ++i) {
474    const code = hostname.charCodeAt(i);
475    const isValid = (code >= CHAR_LOWERCASE_A && code <= CHAR_LOWERCASE_Z) ||
476                    code === CHAR_DOT ||
477                    (code >= CHAR_UPPERCASE_A && code <= CHAR_UPPERCASE_Z) ||
478                    (code >= CHAR_0 && code <= CHAR_9) ||
479                    code === CHAR_HYPHEN_MINUS ||
480                    code === CHAR_PLUS ||
481                    code === CHAR_UNDERSCORE ||
482                    code > 127;
483
484    // Invalid host character
485    if (!isValid) {
486      self.hostname = hostname.slice(0, i);
487      return `/${hostname.slice(i)}${rest}`;
488    }
489  }
490  return rest;
491}
492
493// Escaped characters. Use empty strings to fill up unused entries.
494// Using Array is faster than Object/Map
495const escapedCodes = [
496  /* 0 - 9 */ '', '', '', '', '', '', '', '', '', '%09',
497  /* 10 - 19 */ '%0A', '', '', '%0D', '', '', '', '', '', '',
498  /* 20 - 29 */ '', '', '', '', '', '', '', '', '', '',
499  /* 30 - 39 */ '', '', '%20', '', '%22', '', '', '', '', '%27',
500  /* 40 - 49 */ '', '', '', '', '', '', '', '', '', '',
501  /* 50 - 59 */ '', '', '', '', '', '', '', '', '', '',
502  /* 60 - 69 */ '%3C', '', '%3E', '', '', '', '', '', '', '',
503  /* 70 - 79 */ '', '', '', '', '', '', '', '', '', '',
504  /* 80 - 89 */ '', '', '', '', '', '', '', '', '', '',
505  /* 90 - 99 */ '', '', '%5C', '', '%5E', '', '%60', '', '', '',
506  /* 100 - 109 */ '', '', '', '', '', '', '', '', '', '',
507  /* 110 - 119 */ '', '', '', '', '', '', '', '', '', '',
508  /* 120 - 125 */ '', '', '', '%7B', '%7C', '%7D'
509];
510
511// Automatically escape all delimiters and unwise characters from RFC 2396.
512// Also escape single quotes in case of an XSS attack.
513// Return the escaped string.
514function autoEscapeStr(rest) {
515  let escaped = '';
516  let lastEscapedPos = 0;
517  for (let i = 0; i < rest.length; ++i) {
518    // `escaped` contains substring up to the last escaped character.
519    const escapedChar = escapedCodes[rest.charCodeAt(i)];
520    if (escapedChar) {
521      // Concat if there are ordinary characters in the middle.
522      if (i > lastEscapedPos)
523        escaped += rest.slice(lastEscapedPos, i);
524      escaped += escapedChar;
525      lastEscapedPos = i + 1;
526    }
527  }
528  if (lastEscapedPos === 0)  // Nothing has been escaped.
529    return rest;
530
531  // There are ordinary characters at the end.
532  if (lastEscapedPos < rest.length)
533    escaped += rest.slice(lastEscapedPos);
534
535  return escaped;
536}
537
538// Format a parsed object into a url string
539function urlFormat(urlObject, options) {
540  // Ensure it's an object, and not a string url.
541  // If it's an object, this is a no-op.
542  // this way, you can call urlParse() on strings
543  // to clean up potentially wonky urls.
544  if (typeof urlObject === 'string') {
545    urlObject = urlParse(urlObject);
546  } else if (typeof urlObject !== 'object' || urlObject === null) {
547    throw new ERR_INVALID_ARG_TYPE('urlObject',
548                                   ['Object', 'string'], urlObject);
549  } else if (!(urlObject instanceof Url)) {
550    const format = urlObject[formatSymbol];
551    return format ?
552      format.call(urlObject, options) :
553      Url.prototype.format.call(urlObject);
554  }
555  return urlObject.format();
556}
557
558// These characters do not need escaping:
559// ! - . _ ~
560// ' ( ) * :
561// digits
562// alpha (uppercase)
563// alpha (lowercase)
564const noEscapeAuth = [
565  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x00 - 0x0F
566  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x10 - 0x1F
567  0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, // 0x20 - 0x2F
568  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, // 0x30 - 0x3F
569  0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x40 - 0x4F
570  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, // 0x50 - 0x5F
571  0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x60 - 0x6F
572  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0  // 0x70 - 0x7F
573];
574
575Url.prototype.format = function format() {
576  let auth = this.auth || '';
577  if (auth) {
578    auth = encodeStr(auth, noEscapeAuth, hexTable);
579    auth += '@';
580  }
581
582  let protocol = this.protocol || '';
583  let pathname = this.pathname || '';
584  let hash = this.hash || '';
585  let host = '';
586  let query = '';
587
588  if (this.host) {
589    host = auth + this.host;
590  } else if (this.hostname) {
591    host = auth + (
592      this.hostname.includes(':') ?
593        '[' + this.hostname + ']' :
594        this.hostname
595    );
596    if (this.port) {
597      host += ':' + this.port;
598    }
599  }
600
601  if (this.query !== null && typeof this.query === 'object') {
602    if (querystring === undefined) querystring = require('querystring');
603    query = querystring.stringify(this.query);
604  }
605
606  let search = this.search || (query && ('?' + query)) || '';
607
608  if (protocol && protocol.charCodeAt(protocol.length - 1) !== 58/* : */)
609    protocol += ':';
610
611  let newPathname = '';
612  let lastPos = 0;
613  for (let i = 0; i < pathname.length; ++i) {
614    switch (pathname.charCodeAt(i)) {
615      case CHAR_HASH:
616        if (i - lastPos > 0)
617          newPathname += pathname.slice(lastPos, i);
618        newPathname += '%23';
619        lastPos = i + 1;
620        break;
621      case CHAR_QUESTION_MARK:
622        if (i - lastPos > 0)
623          newPathname += pathname.slice(lastPos, i);
624        newPathname += '%3F';
625        lastPos = i + 1;
626        break;
627    }
628  }
629  if (lastPos > 0) {
630    if (lastPos !== pathname.length)
631      pathname = newPathname + pathname.slice(lastPos);
632    else
633      pathname = newPathname;
634  }
635
636  // Only the slashedProtocols get the //.  Not mailto:, xmpp:, etc.
637  // unless they had them to begin with.
638  if (this.slashes || slashedProtocol.has(protocol)) {
639    if (this.slashes || host) {
640      if (pathname && pathname.charCodeAt(0) !== CHAR_FORWARD_SLASH)
641        pathname = '/' + pathname;
642      host = '//' + host;
643    } else if (protocol.length >= 4 &&
644               protocol.charCodeAt(0) === 102/* f */ &&
645               protocol.charCodeAt(1) === 105/* i */ &&
646               protocol.charCodeAt(2) === 108/* l */ &&
647               protocol.charCodeAt(3) === 101/* e */) {
648      host = '//';
649    }
650  }
651
652  search = search.replace(/#/g, '%23');
653
654  if (hash && hash.charCodeAt(0) !== CHAR_HASH)
655    hash = '#' + hash;
656  if (search && search.charCodeAt(0) !== CHAR_QUESTION_MARK)
657    search = '?' + search;
658
659  return protocol + host + pathname + search + hash;
660};
661
662function urlResolve(source, relative) {
663  return urlParse(source, false, true).resolve(relative);
664}
665
666Url.prototype.resolve = function resolve(relative) {
667  return this.resolveObject(urlParse(relative, false, true)).format();
668};
669
670function urlResolveObject(source, relative) {
671  if (!source) return relative;
672  return urlParse(source, false, true).resolveObject(relative);
673}
674
675Url.prototype.resolveObject = function resolveObject(relative) {
676  if (typeof relative === 'string') {
677    const rel = new Url();
678    rel.parse(relative, false, true);
679    relative = rel;
680  }
681
682  const result = new Url();
683  const tkeys = ObjectKeys(this);
684  for (let tk = 0; tk < tkeys.length; tk++) {
685    const tkey = tkeys[tk];
686    result[tkey] = this[tkey];
687  }
688
689  // Hash is always overridden, no matter what.
690  // even href="" will remove it.
691  result.hash = relative.hash;
692
693  // If the relative url is empty, then there's nothing left to do here.
694  if (relative.href === '') {
695    result.href = result.format();
696    return result;
697  }
698
699  // Hrefs like //foo/bar always cut to the protocol.
700  if (relative.slashes && !relative.protocol) {
701    // Take everything except the protocol from relative
702    const rkeys = ObjectKeys(relative);
703    for (let rk = 0; rk < rkeys.length; rk++) {
704      const rkey = rkeys[rk];
705      if (rkey !== 'protocol')
706        result[rkey] = relative[rkey];
707    }
708
709    // urlParse appends trailing / to urls like http://www.example.com
710    if (slashedProtocol.has(result.protocol) &&
711        result.hostname && !result.pathname) {
712      result.path = result.pathname = '/';
713    }
714
715    result.href = result.format();
716    return result;
717  }
718
719  if (relative.protocol && relative.protocol !== result.protocol) {
720    // If it's a known url protocol, then changing
721    // the protocol does weird things
722    // first, if it's not file:, then we MUST have a host,
723    // and if there was a path
724    // to begin with, then we MUST have a path.
725    // if it is file:, then the host is dropped,
726    // because that's known to be hostless.
727    // anything else is assumed to be absolute.
728    if (!slashedProtocol.has(relative.protocol)) {
729      const keys = ObjectKeys(relative);
730      for (let v = 0; v < keys.length; v++) {
731        const k = keys[v];
732        result[k] = relative[k];
733      }
734      result.href = result.format();
735      return result;
736    }
737
738    result.protocol = relative.protocol;
739    if (!relative.host &&
740        !/^file:?$/.test(relative.protocol) &&
741        !hostlessProtocol.has(relative.protocol)) {
742      const relPath = (relative.pathname || '').split('/');
743      while (relPath.length && !(relative.host = relPath.shift()));
744      if (!relative.host) relative.host = '';
745      if (!relative.hostname) relative.hostname = '';
746      if (relPath[0] !== '') relPath.unshift('');
747      if (relPath.length < 2) relPath.unshift('');
748      result.pathname = relPath.join('/');
749    } else {
750      result.pathname = relative.pathname;
751    }
752    result.search = relative.search;
753    result.query = relative.query;
754    result.host = relative.host || '';
755    result.auth = relative.auth;
756    result.hostname = relative.hostname || relative.host;
757    result.port = relative.port;
758    // To support http.request
759    if (result.pathname || result.search) {
760      const p = result.pathname || '';
761      const s = result.search || '';
762      result.path = p + s;
763    }
764    result.slashes = result.slashes || relative.slashes;
765    result.href = result.format();
766    return result;
767  }
768
769  const isSourceAbs = (result.pathname && result.pathname.charAt(0) === '/');
770  const isRelAbs = (
771    relative.host || (relative.pathname && relative.pathname.charAt(0) === '/')
772  );
773  let mustEndAbs = (isRelAbs || isSourceAbs ||
774                    (result.host && relative.pathname));
775  const removeAllDots = mustEndAbs;
776  let srcPath = (result.pathname && result.pathname.split('/')) || [];
777  const relPath = (relative.pathname && relative.pathname.split('/')) || [];
778  const noLeadingSlashes = result.protocol &&
779      !slashedProtocol.has(result.protocol);
780
781  // If the url is a non-slashed url, then relative
782  // links like ../.. should be able
783  // to crawl up to the hostname, as well.  This is strange.
784  // result.protocol has already been set by now.
785  // Later on, put the first path part into the host field.
786  if (noLeadingSlashes) {
787    result.hostname = '';
788    result.port = null;
789    if (result.host) {
790      if (srcPath[0] === '') srcPath[0] = result.host;
791      else srcPath.unshift(result.host);
792    }
793    result.host = '';
794    if (relative.protocol) {
795      relative.hostname = null;
796      relative.port = null;
797      result.auth = null;
798      if (relative.host) {
799        if (relPath[0] === '') relPath[0] = relative.host;
800        else relPath.unshift(relative.host);
801      }
802      relative.host = null;
803    }
804    mustEndAbs = mustEndAbs && (relPath[0] === '' || srcPath[0] === '');
805  }
806
807  if (isRelAbs) {
808    // it's absolute.
809    if (relative.host || relative.host === '') {
810      if (result.host !== relative.host) result.auth = null;
811      result.host = relative.host;
812      result.port = relative.port;
813    }
814    if (relative.hostname || relative.hostname === '') {
815      if (result.hostname !== relative.hostname) result.auth = null;
816      result.hostname = relative.hostname;
817    }
818    result.search = relative.search;
819    result.query = relative.query;
820    srcPath = relPath;
821    // Fall through to the dot-handling below.
822  } else if (relPath.length) {
823    // it's relative
824    // throw away the existing file, and take the new path instead.
825    if (!srcPath) srcPath = [];
826    srcPath.pop();
827    srcPath = srcPath.concat(relPath);
828    result.search = relative.search;
829    result.query = relative.query;
830  } else if (relative.search !== null && relative.search !== undefined) {
831    // Just pull out the search.
832    // like href='?foo'.
833    // Put this after the other two cases because it simplifies the booleans
834    if (noLeadingSlashes) {
835      result.hostname = result.host = srcPath.shift();
836      // Occasionally the auth can get stuck only in host.
837      // This especially happens in cases like
838      // url.resolveObject('mailto:local1@domain1', 'local2@domain2')
839      const authInHost =
840        result.host && result.host.indexOf('@') > 0 && result.host.split('@');
841      if (authInHost) {
842        result.auth = authInHost.shift();
843        result.host = result.hostname = authInHost.shift();
844      }
845    }
846    result.search = relative.search;
847    result.query = relative.query;
848    // To support http.request
849    if (result.pathname !== null || result.search !== null) {
850      result.path = (result.pathname ? result.pathname : '') +
851                    (result.search ? result.search : '');
852    }
853    result.href = result.format();
854    return result;
855  }
856
857  if (!srcPath.length) {
858    // No path at all. All other things were already handled above.
859    result.pathname = null;
860    // To support http.request
861    if (result.search) {
862      result.path = '/' + result.search;
863    } else {
864      result.path = null;
865    }
866    result.href = result.format();
867    return result;
868  }
869
870  // If a url ENDs in . or .., then it must get a trailing slash.
871  // however, if it ends in anything else non-slashy,
872  // then it must NOT get a trailing slash.
873  let last = srcPath.slice(-1)[0];
874  const hasTrailingSlash = (
875    ((result.host || relative.host || srcPath.length > 1) &&
876    (last === '.' || last === '..')) || last === '');
877
878  // Strip single dots, resolve double dots to parent dir
879  // if the path tries to go above the root, `up` ends up > 0
880  let up = 0;
881  for (let i = srcPath.length - 1; i >= 0; i--) {
882    last = srcPath[i];
883    if (last === '.') {
884      spliceOne(srcPath, i);
885    } else if (last === '..') {
886      spliceOne(srcPath, i);
887      up++;
888    } else if (up) {
889      spliceOne(srcPath, i);
890      up--;
891    }
892  }
893
894  // If the path is allowed to go above the root, restore leading ..s
895  if (!mustEndAbs && !removeAllDots) {
896    while (up--) {
897      srcPath.unshift('..');
898    }
899  }
900
901  if (mustEndAbs && srcPath[0] !== '' &&
902      (!srcPath[0] || srcPath[0].charAt(0) !== '/')) {
903    srcPath.unshift('');
904  }
905
906  if (hasTrailingSlash && (srcPath.join('/').substr(-1) !== '/')) {
907    srcPath.push('');
908  }
909
910  const isAbsolute = srcPath[0] === '' ||
911      (srcPath[0] && srcPath[0].charAt(0) === '/');
912
913  // put the host back
914  if (noLeadingSlashes) {
915    result.hostname =
916      result.host = isAbsolute ? '' : srcPath.length ? srcPath.shift() : '';
917    // Occasionally the auth can get stuck only in host.
918    // This especially happens in cases like
919    // url.resolveObject('mailto:local1@domain1', 'local2@domain2')
920    const authInHost = result.host && result.host.indexOf('@') > 0 ?
921      result.host.split('@') : false;
922    if (authInHost) {
923      result.auth = authInHost.shift();
924      result.host = result.hostname = authInHost.shift();
925    }
926  }
927
928  mustEndAbs = mustEndAbs || (result.host && srcPath.length);
929
930  if (mustEndAbs && !isAbsolute) {
931    srcPath.unshift('');
932  }
933
934  if (!srcPath.length) {
935    result.pathname = null;
936    result.path = null;
937  } else {
938    result.pathname = srcPath.join('/');
939  }
940
941  // To support request.http
942  if (result.pathname !== null || result.search !== null) {
943    result.path = (result.pathname ? result.pathname : '') +
944                  (result.search ? result.search : '');
945  }
946  result.auth = relative.auth || result.auth;
947  result.slashes = result.slashes || relative.slashes;
948  result.href = result.format();
949  return result;
950};
951
952Url.prototype.parseHost = function parseHost() {
953  let host = this.host;
954  let port = portPattern.exec(host);
955  if (port) {
956    port = port[0];
957    if (port !== ':') {
958      this.port = port.slice(1);
959    }
960    host = host.slice(0, host.length - port.length);
961  }
962  if (host) this.hostname = host;
963};
964
965module.exports = {
966  // Original API
967  Url,
968  parse: urlParse,
969  resolve: urlResolve,
970  resolveObject: urlResolveObject,
971  format: urlFormat,
972
973  // WHATWG API
974  URL,
975  URLSearchParams,
976  domainToASCII,
977  domainToUnicode,
978
979  // Utilities
980  pathToFileURL,
981  fileURLToPath
982};
983