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