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