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