url.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776
  1. /*
  2. * Copyright Joyent, Inc. and other Node contributors.
  3. *
  4. * Permission is hereby granted, free of charge, to any person obtaining a
  5. * copy of this software and associated documentation files (the
  6. * "Software"), to deal in the Software without restriction, including
  7. * without limitation the rights to use, copy, modify, merge, publish,
  8. * distribute, sublicense, and/or sell copies of the Software, and to permit
  9. * persons to whom the Software is furnished to do so, subject to the
  10. * following conditions:
  11. *
  12. * The above copyright notice and this permission notice shall be included
  13. * in all copies or substantial portions of the Software.
  14. *
  15. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
  16. * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  17. * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
  18. * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
  19. * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
  20. * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
  21. * USE OR OTHER DEALINGS IN THE SOFTWARE.
  22. */
  23. 'use strict';
  24. var punycode = require('punycode');
  25. function Url() {
  26. this.protocol = null;
  27. this.slashes = null;
  28. this.auth = null;
  29. this.host = null;
  30. this.port = null;
  31. this.hostname = null;
  32. this.hash = null;
  33. this.search = null;
  34. this.query = null;
  35. this.pathname = null;
  36. this.path = null;
  37. this.href = null;
  38. }
  39. // Reference: RFC 3986, RFC 1808, RFC 2396
  40. /*
  41. * define these here so at least they only have to be
  42. * compiled once on the first module load.
  43. */
  44. var protocolPattern = /^([a-z0-9.+-]+:)/i,
  45. portPattern = /:[0-9]*$/,
  46. // Special case for a simple path URL
  47. simplePathPattern = /^(\/\/?(?!\/)[^?\s]*)(\?[^\s]*)?$/,
  48. /*
  49. * RFC 2396: characters reserved for delimiting URLs.
  50. * We actually just auto-escape these.
  51. */
  52. delims = [
  53. '<', '>', '"', '`', ' ', '\r', '\n', '\t'
  54. ],
  55. // RFC 2396: characters not allowed for various reasons.
  56. unwise = [
  57. '{', '}', '|', '\\', '^', '`'
  58. ].concat(delims),
  59. // Allowed by RFCs, but cause of XSS attacks. Always escape these.
  60. autoEscape = ['\''].concat(unwise),
  61. /*
  62. * Characters that are never ever allowed in a hostname.
  63. * Note that any invalid chars are also handled, but these
  64. * are the ones that are *expected* to be seen, so we fast-path
  65. * them.
  66. */
  67. nonHostChars = [
  68. '%', '/', '?', ';', '#'
  69. ].concat(autoEscape),
  70. hostEndingChars = [
  71. '/', '?', '#'
  72. ],
  73. hostnameMaxLen = 255,
  74. hostnamePartPattern = /^[+a-z0-9A-Z_-]{0,63}$/,
  75. hostnamePartStart = /^([+a-z0-9A-Z_-]{0,63})(.*)$/,
  76. // protocols that can allow "unsafe" and "unwise" chars.
  77. unsafeProtocol = {
  78. javascript: true,
  79. 'javascript:': true
  80. },
  81. // protocols that never have a hostname.
  82. hostlessProtocol = {
  83. javascript: true,
  84. 'javascript:': true
  85. },
  86. // protocols that always contain a // bit.
  87. slashedProtocol = {
  88. http: true,
  89. https: true,
  90. ftp: true,
  91. gopher: true,
  92. file: true,
  93. 'http:': true,
  94. 'https:': true,
  95. 'ftp:': true,
  96. 'gopher:': true,
  97. 'file:': true
  98. },
  99. querystring = require('qs');
  100. function urlParse(url, parseQueryString, slashesDenoteHost) {
  101. if (url && typeof url === 'object' && url instanceof Url) { return url; }
  102. var u = new Url();
  103. u.parse(url, parseQueryString, slashesDenoteHost);
  104. return u;
  105. }
  106. Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) {
  107. if (typeof url !== 'string') {
  108. throw new TypeError("Parameter 'url' must be a string, not " + typeof url);
  109. }
  110. /*
  111. * Copy chrome, IE, opera backslash-handling behavior.
  112. * Back slashes before the query string get converted to forward slashes
  113. * See: https://code.google.com/p/chromium/issues/detail?id=25916
  114. */
  115. var queryIndex = url.indexOf('?'),
  116. splitter = queryIndex !== -1 && queryIndex < url.indexOf('#') ? '?' : '#',
  117. uSplit = url.split(splitter),
  118. slashRegex = /\\/g;
  119. uSplit[0] = uSplit[0].replace(slashRegex, '/');
  120. url = uSplit.join(splitter);
  121. var rest = url;
  122. /*
  123. * trim before proceeding.
  124. * This is to support parse stuff like " http://foo.com \n"
  125. */
  126. rest = rest.trim();
  127. if (!slashesDenoteHost && url.split('#').length === 1) {
  128. // Try fast path regexp
  129. var simplePath = simplePathPattern.exec(rest);
  130. if (simplePath) {
  131. this.path = rest;
  132. this.href = rest;
  133. this.pathname = simplePath[1];
  134. if (simplePath[2]) {
  135. this.search = simplePath[2];
  136. if (parseQueryString) {
  137. this.query = querystring.parse(this.search.substr(1));
  138. } else {
  139. this.query = this.search.substr(1);
  140. }
  141. } else if (parseQueryString) {
  142. this.search = '';
  143. this.query = {};
  144. }
  145. return this;
  146. }
  147. }
  148. var proto = protocolPattern.exec(rest);
  149. if (proto) {
  150. proto = proto[0];
  151. var lowerProto = proto.toLowerCase();
  152. this.protocol = lowerProto;
  153. rest = rest.substr(proto.length);
  154. }
  155. /*
  156. * figure out if it's got a host
  157. * user@server is *always* interpreted as a hostname, and url
  158. * resolution will treat //foo/bar as host=foo,path=bar because that's
  159. * how the browser resolves relative URLs.
  160. */
  161. if (slashesDenoteHost || proto || rest.match(/^\/\/[^@/]+@[^@/]+/)) {
  162. var slashes = rest.substr(0, 2) === '//';
  163. if (slashes && !(proto && hostlessProtocol[proto])) {
  164. rest = rest.substr(2);
  165. this.slashes = true;
  166. }
  167. }
  168. if (!hostlessProtocol[proto] && (slashes || (proto && !slashedProtocol[proto]))) {
  169. /*
  170. * there's a hostname.
  171. * the first instance of /, ?, ;, or # ends the host.
  172. *
  173. * If there is an @ in the hostname, then non-host chars *are* allowed
  174. * to the left of the last @ sign, unless some host-ending character
  175. * comes *before* the @-sign.
  176. * URLs are obnoxious.
  177. *
  178. * ex:
  179. * http://a@b@c/ => user:a@b host:c
  180. * http://a@b?@c => user:a host:c path:/?@c
  181. */
  182. /*
  183. * v0.12 TODO(isaacs): This is not quite how Chrome does things.
  184. * Review our test case against browsers more comprehensively.
  185. */
  186. // find the first instance of any hostEndingChars
  187. var hostEnd = -1;
  188. for (var i = 0; i < hostEndingChars.length; i++) {
  189. var hec = rest.indexOf(hostEndingChars[i]);
  190. if (hec !== -1 && (hostEnd === -1 || hec < hostEnd)) { hostEnd = hec; }
  191. }
  192. /*
  193. * at this point, either we have an explicit point where the
  194. * auth portion cannot go past, or the last @ char is the decider.
  195. */
  196. var auth, atSign;
  197. if (hostEnd === -1) {
  198. // atSign can be anywhere.
  199. atSign = rest.lastIndexOf('@');
  200. } else {
  201. /*
  202. * atSign must be in auth portion.
  203. * http://a@b/c@d => host:b auth:a path:/c@d
  204. */
  205. atSign = rest.lastIndexOf('@', hostEnd);
  206. }
  207. /*
  208. * Now we have a portion which is definitely the auth.
  209. * Pull that off.
  210. */
  211. if (atSign !== -1) {
  212. auth = rest.slice(0, atSign);
  213. rest = rest.slice(atSign + 1);
  214. this.auth = decodeURIComponent(auth);
  215. }
  216. // the host is the remaining to the left of the first non-host char
  217. hostEnd = -1;
  218. for (var i = 0; i < nonHostChars.length; i++) {
  219. var hec = rest.indexOf(nonHostChars[i]);
  220. if (hec !== -1 && (hostEnd === -1 || hec < hostEnd)) { hostEnd = hec; }
  221. }
  222. // if we still have not hit it, then the entire thing is a host.
  223. if (hostEnd === -1) { hostEnd = rest.length; }
  224. this.host = rest.slice(0, hostEnd);
  225. rest = rest.slice(hostEnd);
  226. // pull out port.
  227. this.parseHost();
  228. /*
  229. * we've indicated that there is a hostname,
  230. * so even if it's empty, it has to be present.
  231. */
  232. this.hostname = this.hostname || '';
  233. /*
  234. * if hostname begins with [ and ends with ]
  235. * assume that it's an IPv6 address.
  236. */
  237. var ipv6Hostname = this.hostname[0] === '[' && this.hostname[this.hostname.length - 1] === ']';
  238. // validate a little.
  239. if (!ipv6Hostname) {
  240. var hostparts = this.hostname.split(/\./);
  241. for (var i = 0, l = hostparts.length; i < l; i++) {
  242. var part = hostparts[i];
  243. if (!part) { continue; }
  244. if (!part.match(hostnamePartPattern)) {
  245. var newpart = '';
  246. for (var j = 0, k = part.length; j < k; j++) {
  247. if (part.charCodeAt(j) > 127) {
  248. /*
  249. * we replace non-ASCII char with a temporary placeholder
  250. * we need this to make sure size of hostname is not
  251. * broken by replacing non-ASCII by nothing
  252. */
  253. newpart += 'x';
  254. } else {
  255. newpart += part[j];
  256. }
  257. }
  258. // we test again with ASCII char only
  259. if (!newpart.match(hostnamePartPattern)) {
  260. var validParts = hostparts.slice(0, i);
  261. var notHost = hostparts.slice(i + 1);
  262. var bit = part.match(hostnamePartStart);
  263. if (bit) {
  264. validParts.push(bit[1]);
  265. notHost.unshift(bit[2]);
  266. }
  267. if (notHost.length) {
  268. rest = '/' + notHost.join('.') + rest;
  269. }
  270. this.hostname = validParts.join('.');
  271. break;
  272. }
  273. }
  274. }
  275. }
  276. if (this.hostname.length > hostnameMaxLen) {
  277. this.hostname = '';
  278. } else {
  279. // hostnames are always lower case.
  280. this.hostname = this.hostname.toLowerCase();
  281. }
  282. if (!ipv6Hostname) {
  283. /*
  284. * IDNA Support: Returns a punycoded representation of "domain".
  285. * It only converts parts of the domain name that
  286. * have non-ASCII characters, i.e. it doesn't matter if
  287. * you call it with a domain that already is ASCII-only.
  288. */
  289. this.hostname = punycode.toASCII(this.hostname);
  290. }
  291. var p = this.port ? ':' + this.port : '';
  292. var h = this.hostname || '';
  293. this.host = h + p;
  294. this.href += this.host;
  295. /*
  296. * strip [ and ] from the hostname
  297. * the host field still retains them, though
  298. */
  299. if (ipv6Hostname) {
  300. this.hostname = this.hostname.substr(1, this.hostname.length - 2);
  301. if (rest[0] !== '/') {
  302. rest = '/' + rest;
  303. }
  304. }
  305. }
  306. /*
  307. * now rest is set to the post-host stuff.
  308. * chop off any delim chars.
  309. */
  310. if (!unsafeProtocol[lowerProto]) {
  311. /*
  312. * First, make 100% sure that any "autoEscape" chars get
  313. * escaped, even if encodeURIComponent doesn't think they
  314. * need to be.
  315. */
  316. for (var i = 0, l = autoEscape.length; i < l; i++) {
  317. var ae = autoEscape[i];
  318. if (rest.indexOf(ae) === -1) { continue; }
  319. var esc = encodeURIComponent(ae);
  320. if (esc === ae) {
  321. esc = escape(ae);
  322. }
  323. rest = rest.split(ae).join(esc);
  324. }
  325. }
  326. // chop off from the tail first.
  327. var hash = rest.indexOf('#');
  328. if (hash !== -1) {
  329. // got a fragment string.
  330. this.hash = rest.substr(hash);
  331. rest = rest.slice(0, hash);
  332. }
  333. var qm = rest.indexOf('?');
  334. if (qm !== -1) {
  335. this.search = rest.substr(qm);
  336. this.query = rest.substr(qm + 1);
  337. if (parseQueryString) {
  338. this.query = querystring.parse(this.query);
  339. }
  340. rest = rest.slice(0, qm);
  341. } else if (parseQueryString) {
  342. // no query string, but parseQueryString still requested
  343. this.search = '';
  344. this.query = {};
  345. }
  346. if (rest) { this.pathname = rest; }
  347. if (slashedProtocol[lowerProto] && this.hostname && !this.pathname) {
  348. this.pathname = '/';
  349. }
  350. // to support http.request
  351. if (this.pathname || this.search) {
  352. var p = this.pathname || '';
  353. var s = this.search || '';
  354. this.path = p + s;
  355. }
  356. // finally, reconstruct the href based on what has been validated.
  357. this.href = this.format();
  358. return this;
  359. };
  360. // format a parsed object into a url string
  361. function urlFormat(obj) {
  362. /*
  363. * ensure it's an object, and not a string url.
  364. * If it's an obj, this is a no-op.
  365. * this way, you can call url_format() on strings
  366. * to clean up potentially wonky urls.
  367. */
  368. if (typeof obj === 'string') { obj = urlParse(obj); }
  369. if (!(obj instanceof Url)) { return Url.prototype.format.call(obj); }
  370. return obj.format();
  371. }
  372. Url.prototype.format = function () {
  373. var auth = this.auth || '';
  374. if (auth) {
  375. auth = encodeURIComponent(auth);
  376. auth = auth.replace(/%3A/i, ':');
  377. auth += '@';
  378. }
  379. var protocol = this.protocol || '',
  380. pathname = this.pathname || '',
  381. hash = this.hash || '',
  382. host = false,
  383. query = '';
  384. if (this.host) {
  385. host = auth + this.host;
  386. } else if (this.hostname) {
  387. host = auth + (this.hostname.indexOf(':') === -1 ? this.hostname : '[' + this.hostname + ']');
  388. if (this.port) {
  389. host += ':' + this.port;
  390. }
  391. }
  392. if (this.query && typeof this.query === 'object' && Object.keys(this.query).length) {
  393. query = querystring.stringify(this.query, {
  394. arrayFormat: 'repeat',
  395. addQueryPrefix: false
  396. });
  397. }
  398. var search = this.search || (query && ('?' + query)) || '';
  399. if (protocol && protocol.substr(-1) !== ':') { protocol += ':'; }
  400. /*
  401. * only the slashedProtocols get the //. Not mailto:, xmpp:, etc.
  402. * unless they had them to begin with.
  403. */
  404. if (this.slashes || (!protocol || slashedProtocol[protocol]) && host !== false) {
  405. host = '//' + (host || '');
  406. if (pathname && pathname.charAt(0) !== '/') { pathname = '/' + pathname; }
  407. } else if (!host) {
  408. host = '';
  409. }
  410. if (hash && hash.charAt(0) !== '#') { hash = '#' + hash; }
  411. if (search && search.charAt(0) !== '?') { search = '?' + search; }
  412. pathname = pathname.replace(/[?#]/g, function (match) {
  413. return encodeURIComponent(match);
  414. });
  415. search = search.replace('#', '%23');
  416. return protocol + host + pathname + search + hash;
  417. };
  418. function urlResolve(source, relative) {
  419. return urlParse(source, false, true).resolve(relative);
  420. }
  421. Url.prototype.resolve = function (relative) {
  422. return this.resolveObject(urlParse(relative, false, true)).format();
  423. };
  424. function urlResolveObject(source, relative) {
  425. if (!source) { return relative; }
  426. return urlParse(source, false, true).resolveObject(relative);
  427. }
  428. Url.prototype.resolveObject = function (relative) {
  429. if (typeof relative === 'string') {
  430. var rel = new Url();
  431. rel.parse(relative, false, true);
  432. relative = rel;
  433. }
  434. var result = new Url();
  435. var tkeys = Object.keys(this);
  436. for (var tk = 0; tk < tkeys.length; tk++) {
  437. var tkey = tkeys[tk];
  438. result[tkey] = this[tkey];
  439. }
  440. /*
  441. * hash is always overridden, no matter what.
  442. * even href="" will remove it.
  443. */
  444. result.hash = relative.hash;
  445. // if the relative url is empty, then there's nothing left to do here.
  446. if (relative.href === '') {
  447. result.href = result.format();
  448. return result;
  449. }
  450. // hrefs like //foo/bar always cut to the protocol.
  451. if (relative.slashes && !relative.protocol) {
  452. // take everything except the protocol from relative
  453. var rkeys = Object.keys(relative);
  454. for (var rk = 0; rk < rkeys.length; rk++) {
  455. var rkey = rkeys[rk];
  456. if (rkey !== 'protocol') { result[rkey] = relative[rkey]; }
  457. }
  458. // urlParse appends trailing / to urls like http://www.example.com
  459. if (slashedProtocol[result.protocol] && result.hostname && !result.pathname) {
  460. result.pathname = '/';
  461. result.path = result.pathname;
  462. }
  463. result.href = result.format();
  464. return result;
  465. }
  466. if (relative.protocol && relative.protocol !== result.protocol) {
  467. /*
  468. * if it's a known url protocol, then changing
  469. * the protocol does weird things
  470. * first, if it's not file:, then we MUST have a host,
  471. * and if there was a path
  472. * to begin with, then we MUST have a path.
  473. * if it is file:, then the host is dropped,
  474. * because that's known to be hostless.
  475. * anything else is assumed to be absolute.
  476. */
  477. if (!slashedProtocol[relative.protocol]) {
  478. var keys = Object.keys(relative);
  479. for (var v = 0; v < keys.length; v++) {
  480. var k = keys[v];
  481. result[k] = relative[k];
  482. }
  483. result.href = result.format();
  484. return result;
  485. }
  486. result.protocol = relative.protocol;
  487. if (!relative.host && !hostlessProtocol[relative.protocol]) {
  488. var relPath = (relative.pathname || '').split('/');
  489. while (relPath.length && !(relative.host = relPath.shift())) { }
  490. if (!relative.host) { relative.host = ''; }
  491. if (!relative.hostname) { relative.hostname = ''; }
  492. if (relPath[0] !== '') { relPath.unshift(''); }
  493. if (relPath.length < 2) { relPath.unshift(''); }
  494. result.pathname = relPath.join('/');
  495. } else {
  496. result.pathname = relative.pathname;
  497. }
  498. result.search = relative.search;
  499. result.query = relative.query;
  500. result.host = relative.host || '';
  501. result.auth = relative.auth;
  502. result.hostname = relative.hostname || relative.host;
  503. result.port = relative.port;
  504. // to support http.request
  505. if (result.pathname || result.search) {
  506. var p = result.pathname || '';
  507. var s = result.search || '';
  508. result.path = p + s;
  509. }
  510. result.slashes = result.slashes || relative.slashes;
  511. result.href = result.format();
  512. return result;
  513. }
  514. var isSourceAbs = result.pathname && result.pathname.charAt(0) === '/',
  515. isRelAbs = relative.host || relative.pathname && relative.pathname.charAt(0) === '/',
  516. mustEndAbs = isRelAbs || isSourceAbs || (result.host && relative.pathname),
  517. removeAllDots = mustEndAbs,
  518. srcPath = result.pathname && result.pathname.split('/') || [],
  519. relPath = relative.pathname && relative.pathname.split('/') || [],
  520. psychotic = result.protocol && !slashedProtocol[result.protocol];
  521. /*
  522. * if the url is a non-slashed url, then relative
  523. * links like ../.. should be able
  524. * to crawl up to the hostname, as well. This is strange.
  525. * result.protocol has already been set by now.
  526. * Later on, put the first path part into the host field.
  527. */
  528. if (psychotic) {
  529. result.hostname = '';
  530. result.port = null;
  531. if (result.host) {
  532. if (srcPath[0] === '') { srcPath[0] = result.host; } else { srcPath.unshift(result.host); }
  533. }
  534. result.host = '';
  535. if (relative.protocol) {
  536. relative.hostname = null;
  537. relative.port = null;
  538. if (relative.host) {
  539. if (relPath[0] === '') { relPath[0] = relative.host; } else { relPath.unshift(relative.host); }
  540. }
  541. relative.host = null;
  542. }
  543. mustEndAbs = mustEndAbs && (relPath[0] === '' || srcPath[0] === '');
  544. }
  545. if (isRelAbs) {
  546. // it's absolute.
  547. result.host = relative.host || relative.host === '' ? relative.host : result.host;
  548. result.hostname = relative.hostname || relative.hostname === '' ? relative.hostname : result.hostname;
  549. result.search = relative.search;
  550. result.query = relative.query;
  551. srcPath = relPath;
  552. // fall through to the dot-handling below.
  553. } else if (relPath.length) {
  554. /*
  555. * it's relative
  556. * throw away the existing file, and take the new path instead.
  557. */
  558. if (!srcPath) { srcPath = []; }
  559. srcPath.pop();
  560. srcPath = srcPath.concat(relPath);
  561. result.search = relative.search;
  562. result.query = relative.query;
  563. } else if (relative.search != null) {
  564. /*
  565. * just pull out the search.
  566. * like href='?foo'.
  567. * Put this after the other two cases because it simplifies the booleans
  568. */
  569. if (psychotic) {
  570. result.host = srcPath.shift();
  571. result.hostname = result.host;
  572. /*
  573. * occationaly the auth can get stuck only in host
  574. * this especially happens in cases like
  575. * url.resolveObject('mailto:local1@domain1', 'local2@domain2')
  576. */
  577. var authInHost = result.host && result.host.indexOf('@') > 0 ? result.host.split('@') : false;
  578. if (authInHost) {
  579. result.auth = authInHost.shift();
  580. result.hostname = authInHost.shift();
  581. result.host = result.hostname;
  582. }
  583. }
  584. result.search = relative.search;
  585. result.query = relative.query;
  586. // to support http.request
  587. if (result.pathname !== null || result.search !== null) {
  588. result.path = (result.pathname ? result.pathname : '') + (result.search ? result.search : '');
  589. }
  590. result.href = result.format();
  591. return result;
  592. }
  593. if (!srcPath.length) {
  594. /*
  595. * no path at all. easy.
  596. * we've already handled the other stuff above.
  597. */
  598. result.pathname = null;
  599. // to support http.request
  600. if (result.search) {
  601. result.path = '/' + result.search;
  602. } else {
  603. result.path = null;
  604. }
  605. result.href = result.format();
  606. return result;
  607. }
  608. /*
  609. * if a url ENDs in . or .., then it must get a trailing slash.
  610. * however, if it ends in anything else non-slashy,
  611. * then it must NOT get a trailing slash.
  612. */
  613. var last = srcPath.slice(-1)[0];
  614. var hasTrailingSlash = (result.host || relative.host || srcPath.length > 1) && (last === '.' || last === '..') || last === '';
  615. /*
  616. * strip single dots, resolve double dots to parent dir
  617. * if the path tries to go above the root, `up` ends up > 0
  618. */
  619. var up = 0;
  620. for (var i = srcPath.length; i >= 0; i--) {
  621. last = srcPath[i];
  622. if (last === '.') {
  623. srcPath.splice(i, 1);
  624. } else if (last === '..') {
  625. srcPath.splice(i, 1);
  626. up++;
  627. } else if (up) {
  628. srcPath.splice(i, 1);
  629. up--;
  630. }
  631. }
  632. // if the path is allowed to go above the root, restore leading ..s
  633. if (!mustEndAbs && !removeAllDots) {
  634. for (; up--; up) {
  635. srcPath.unshift('..');
  636. }
  637. }
  638. if (mustEndAbs && srcPath[0] !== '' && (!srcPath[0] || srcPath[0].charAt(0) !== '/')) {
  639. srcPath.unshift('');
  640. }
  641. if (hasTrailingSlash && (srcPath.join('/').substr(-1) !== '/')) {
  642. srcPath.push('');
  643. }
  644. var isAbsolute = srcPath[0] === '' || (srcPath[0] && srcPath[0].charAt(0) === '/');
  645. // put the host back
  646. if (psychotic) {
  647. result.hostname = isAbsolute ? '' : srcPath.length ? srcPath.shift() : '';
  648. result.host = result.hostname;
  649. /*
  650. * occationaly the auth can get stuck only in host
  651. * this especially happens in cases like
  652. * url.resolveObject('mailto:local1@domain1', 'local2@domain2')
  653. */
  654. var authInHost = result.host && result.host.indexOf('@') > 0 ? result.host.split('@') : false;
  655. if (authInHost) {
  656. result.auth = authInHost.shift();
  657. result.hostname = authInHost.shift();
  658. result.host = result.hostname;
  659. }
  660. }
  661. mustEndAbs = mustEndAbs || (result.host && srcPath.length);
  662. if (mustEndAbs && !isAbsolute) {
  663. srcPath.unshift('');
  664. }
  665. if (srcPath.length > 0) {
  666. result.pathname = srcPath.join('/');
  667. } else {
  668. result.pathname = null;
  669. result.path = null;
  670. }
  671. // to support request.http
  672. if (result.pathname !== null || result.search !== null) {
  673. result.path = (result.pathname ? result.pathname : '') + (result.search ? result.search : '');
  674. }
  675. result.auth = relative.auth || result.auth;
  676. result.slashes = result.slashes || relative.slashes;
  677. result.href = result.format();
  678. return result;
  679. };
  680. Url.prototype.parseHost = function () {
  681. var host = this.host;
  682. var port = portPattern.exec(host);
  683. if (port) {
  684. port = port[0];
  685. if (port !== ':') {
  686. this.port = port.substr(1);
  687. }
  688. host = host.substr(0, host.length - port.length);
  689. }
  690. if (host) { this.hostname = host; }
  691. };
  692. exports.parse = urlParse;
  693. exports.resolve = urlResolve;
  694. exports.resolveObject = urlResolveObject;
  695. exports.format = urlFormat;
  696. exports.Url = Url;