Projekt

Obecné

Profil

Stáhnout (22.8 KB) Statistiky
| Větev: | Revize:
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

    
24
var punycode = require('punycode');
25
var util = require('./util');
26

    
27
exports.parse = urlParse;
28
exports.resolve = urlResolve;
29
exports.resolveObject = urlResolveObject;
30
exports.format = urlFormat;
31

    
32
exports.Url = Url;
33

    
34
function Url() {
35
  this.protocol = null;
36
  this.slashes = null;
37
  this.auth = null;
38
  this.host = null;
39
  this.port = null;
40
  this.hostname = null;
41
  this.hash = null;
42
  this.search = null;
43
  this.query = null;
44
  this.pathname = null;
45
  this.path = null;
46
  this.href = null;
47
}
48

    
49
// Reference: RFC 3986, RFC 1808, RFC 2396
50

    
51
// define these here so at least they only have to be
52
// compiled once on the first module load.
53
var protocolPattern = /^([a-z0-9.+-]+:)/i,
54
    portPattern = /:[0-9]*$/,
55

    
56
    // Special case for a simple path URL
57
    simplePathPattern = /^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,
58

    
59
    // RFC 2396: characters reserved for delimiting URLs.
60
    // We actually just auto-escape these.
61
    delims = ['<', '>', '"', '`', ' ', '\r', '\n', '\t'],
62

    
63
    // RFC 2396: characters not allowed for various reasons.
64
    unwise = ['{', '}', '|', '\\', '^', '`'].concat(delims),
65

    
66
    // Allowed by RFCs, but cause of XSS attacks.  Always escape these.
67
    autoEscape = ['\''].concat(unwise),
68
    // Characters that are never ever allowed in a hostname.
69
    // Note that any invalid chars are also handled, but these
70
    // are the ones that are *expected* to be seen, so we fast-path
71
    // them.
72
    nonHostChars = ['%', '/', '?', ';', '#'].concat(autoEscape),
73
    hostEndingChars = ['/', '?', '#'],
74
    hostnameMaxLen = 255,
75
    hostnamePartPattern = /^[+a-z0-9A-Z_-]{0,63}$/,
76
    hostnamePartStart = /^([+a-z0-9A-Z_-]{0,63})(.*)$/,
77
    // protocols that can allow "unsafe" and "unwise" chars.
78
    unsafeProtocol = {
79
      'javascript': true,
80
      'javascript:': true
81
    },
82
    // protocols that never have a hostname.
83
    hostlessProtocol = {
84
      'javascript': true,
85
      'javascript:': true
86
    },
87
    // protocols that always contain a // bit.
88
    slashedProtocol = {
89
      'http': true,
90
      'https': true,
91
      'ftp': true,
92
      'gopher': true,
93
      'file': true,
94
      'http:': true,
95
      'https:': true,
96
      'ftp:': true,
97
      'gopher:': true,
98
      'file:': true
99
    },
100
    querystring = require('querystring');
101

    
102
function urlParse(url, parseQueryString, slashesDenoteHost) {
103
  if (url && util.isObject(url) && url instanceof Url) return url;
104

    
105
  var u = new Url;
106
  u.parse(url, parseQueryString, slashesDenoteHost);
107
  return u;
108
}
109

    
110
Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) {
111
  if (!util.isString(url)) {
112
    throw new TypeError("Parameter 'url' must be a string, not " + typeof url);
113
  }
114

    
115
  // Copy chrome, IE, opera backslash-handling behavior.
116
  // Back slashes before the query string get converted to forward slashes
117
  // See: https://code.google.com/p/chromium/issues/detail?id=25916
118
  var queryIndex = url.indexOf('?'),
119
      splitter =
120
          (queryIndex !== -1 && queryIndex < url.indexOf('#')) ? '?' : '#',
121
      uSplit = url.split(splitter),
122
      slashRegex = /\\/g;
123
  uSplit[0] = uSplit[0].replace(slashRegex, '/');
124
  url = uSplit.join(splitter);
125

    
126
  var rest = url;
127

    
128
  // trim before proceeding.
129
  // This is to support parse stuff like "  http://foo.com  \n"
130
  rest = rest.trim();
131

    
132
  if (!slashesDenoteHost && url.split('#').length === 1) {
133
    // Try fast path regexp
134
    var simplePath = simplePathPattern.exec(rest);
135
    if (simplePath) {
136
      this.path = rest;
137
      this.href = rest;
138
      this.pathname = simplePath[1];
139
      if (simplePath[2]) {
140
        this.search = simplePath[2];
141
        if (parseQueryString) {
142
          this.query = querystring.parse(this.search.substr(1));
143
        } else {
144
          this.query = this.search.substr(1);
145
        }
146
      } else if (parseQueryString) {
147
        this.search = '';
148
        this.query = {};
149
      }
150
      return this;
151
    }
152
  }
153

    
154
  var proto = protocolPattern.exec(rest);
155
  if (proto) {
156
    proto = proto[0];
157
    var lowerProto = proto.toLowerCase();
158
    this.protocol = lowerProto;
159
    rest = rest.substr(proto.length);
160
  }
161

    
162
  // figure out if it's got a host
163
  // user@server is *always* interpreted as a hostname, and url
164
  // resolution will treat //foo/bar as host=foo,path=bar because that's
165
  // how the browser resolves relative URLs.
166
  if (slashesDenoteHost || proto || rest.match(/^\/\/[^@\/]+@[^@\/]+/)) {
167
    var slashes = rest.substr(0, 2) === '//';
168
    if (slashes && !(proto && hostlessProtocol[proto])) {
169
      rest = rest.substr(2);
170
      this.slashes = true;
171
    }
172
  }
173

    
174
  if (!hostlessProtocol[proto] &&
175
      (slashes || (proto && !slashedProtocol[proto]))) {
176

    
177
    // there's a hostname.
178
    // the first instance of /, ?, ;, or # ends the host.
179
    //
180
    // If there is an @ in the hostname, then non-host chars *are* allowed
181
    // to the left of the last @ sign, unless some host-ending character
182
    // comes *before* the @-sign.
183
    // URLs are obnoxious.
184
    //
185
    // ex:
186
    // http://a@b@c/ => user:a@b host:c
187
    // http://a@b?@c => user:a host:c path:/?@c
188

    
189
    // v0.12 TODO(isaacs): This is not quite how Chrome does things.
190
    // Review our test case against browsers more comprehensively.
191

    
192
    // find the first instance of any hostEndingChars
193
    var hostEnd = -1;
194
    for (var i = 0; i < hostEndingChars.length; i++) {
195
      var hec = rest.indexOf(hostEndingChars[i]);
196
      if (hec !== -1 && (hostEnd === -1 || hec < hostEnd))
197
        hostEnd = hec;
198
    }
199

    
200
    // at this point, either we have an explicit point where the
201
    // auth portion cannot go past, or the last @ char is the decider.
202
    var auth, atSign;
203
    if (hostEnd === -1) {
204
      // atSign can be anywhere.
205
      atSign = rest.lastIndexOf('@');
206
    } else {
207
      // atSign must be in auth portion.
208
      // http://a@b/c@d => host:b auth:a path:/c@d
209
      atSign = rest.lastIndexOf('@', hostEnd);
210
    }
211

    
212
    // Now we have a portion which is definitely the auth.
213
    // Pull that off.
214
    if (atSign !== -1) {
215
      auth = rest.slice(0, atSign);
216
      rest = rest.slice(atSign + 1);
217
      this.auth = decodeURIComponent(auth);
218
    }
219

    
220
    // the host is the remaining to the left of the first non-host char
221
    hostEnd = -1;
222
    for (var i = 0; i < nonHostChars.length; i++) {
223
      var hec = rest.indexOf(nonHostChars[i]);
224
      if (hec !== -1 && (hostEnd === -1 || hec < hostEnd))
225
        hostEnd = hec;
226
    }
227
    // if we still have not hit it, then the entire thing is a host.
228
    if (hostEnd === -1)
229
      hostEnd = rest.length;
230

    
231
    this.host = rest.slice(0, hostEnd);
232
    rest = rest.slice(hostEnd);
233

    
234
    // pull out port.
235
    this.parseHost();
236

    
237
    // we've indicated that there is a hostname,
238
    // so even if it's empty, it has to be present.
239
    this.hostname = this.hostname || '';
240

    
241
    // if hostname begins with [ and ends with ]
242
    // assume that it's an IPv6 address.
243
    var ipv6Hostname = this.hostname[0] === '[' &&
244
        this.hostname[this.hostname.length - 1] === ']';
245

    
246
    // validate a little.
247
    if (!ipv6Hostname) {
248
      var hostparts = this.hostname.split(/\./);
249
      for (var i = 0, l = hostparts.length; i < l; i++) {
250
        var part = hostparts[i];
251
        if (!part) continue;
252
        if (!part.match(hostnamePartPattern)) {
253
          var newpart = '';
254
          for (var j = 0, k = part.length; j < k; j++) {
255
            if (part.charCodeAt(j) > 127) {
256
              // we replace non-ASCII char with a temporary placeholder
257
              // we need this to make sure size of hostname is not
258
              // broken by replacing non-ASCII by nothing
259
              newpart += 'x';
260
            } else {
261
              newpart += part[j];
262
            }
263
          }
264
          // we test again with ASCII char only
265
          if (!newpart.match(hostnamePartPattern)) {
266
            var validParts = hostparts.slice(0, i);
267
            var notHost = hostparts.slice(i + 1);
268
            var bit = part.match(hostnamePartStart);
269
            if (bit) {
270
              validParts.push(bit[1]);
271
              notHost.unshift(bit[2]);
272
            }
273
            if (notHost.length) {
274
              rest = '/' + notHost.join('.') + rest;
275
            }
276
            this.hostname = validParts.join('.');
277
            break;
278
          }
279
        }
280
      }
281
    }
282

    
283
    if (this.hostname.length > hostnameMaxLen) {
284
      this.hostname = '';
285
    } else {
286
      // hostnames are always lower case.
287
      this.hostname = this.hostname.toLowerCase();
288
    }
289

    
290
    if (!ipv6Hostname) {
291
      // IDNA Support: Returns a punycoded representation of "domain".
292
      // It only converts parts of the domain name that
293
      // have non-ASCII characters, i.e. it doesn't matter if
294
      // you call it with a domain that already is ASCII-only.
295
      this.hostname = punycode.toASCII(this.hostname);
296
    }
297

    
298
    var p = this.port ? ':' + this.port : '';
299
    var h = this.hostname || '';
300
    this.host = h + p;
301
    this.href += this.host;
302

    
303
    // strip [ and ] from the hostname
304
    // the host field still retains them, though
305
    if (ipv6Hostname) {
306
      this.hostname = this.hostname.substr(1, this.hostname.length - 2);
307
      if (rest[0] !== '/') {
308
        rest = '/' + rest;
309
      }
310
    }
311
  }
312

    
313
  // now rest is set to the post-host stuff.
314
  // chop off any delim chars.
315
  if (!unsafeProtocol[lowerProto]) {
316

    
317
    // First, make 100% sure that any "autoEscape" chars get
318
    // escaped, even if encodeURIComponent doesn't think they
319
    // need to be.
320
    for (var i = 0, l = autoEscape.length; i < l; i++) {
321
      var ae = autoEscape[i];
322
      if (rest.indexOf(ae) === -1)
323
        continue;
324
      var esc = encodeURIComponent(ae);
325
      if (esc === ae) {
326
        esc = escape(ae);
327
      }
328
      rest = rest.split(ae).join(esc);
329
    }
330
  }
331

    
332

    
333
  // chop off from the tail first.
334
  var hash = rest.indexOf('#');
335
  if (hash !== -1) {
336
    // got a fragment string.
337
    this.hash = rest.substr(hash);
338
    rest = rest.slice(0, hash);
339
  }
340
  var qm = rest.indexOf('?');
341
  if (qm !== -1) {
342
    this.search = rest.substr(qm);
343
    this.query = rest.substr(qm + 1);
344
    if (parseQueryString) {
345
      this.query = querystring.parse(this.query);
346
    }
347
    rest = rest.slice(0, qm);
348
  } else if (parseQueryString) {
349
    // no query string, but parseQueryString still requested
350
    this.search = '';
351
    this.query = {};
352
  }
353
  if (rest) this.pathname = rest;
354
  if (slashedProtocol[lowerProto] &&
355
      this.hostname && !this.pathname) {
356
    this.pathname = '/';
357
  }
358

    
359
  //to support http.request
360
  if (this.pathname || this.search) {
361
    var p = this.pathname || '';
362
    var s = this.search || '';
363
    this.path = p + s;
364
  }
365

    
366
  // finally, reconstruct the href based on what has been validated.
367
  this.href = this.format();
368
  return this;
369
};
370

    
371
// format a parsed object into a url string
372
function urlFormat(obj) {
373
  // ensure it's an object, and not a string url.
374
  // If it's an obj, this is a no-op.
375
  // this way, you can call url_format() on strings
376
  // to clean up potentially wonky urls.
377
  if (util.isString(obj)) obj = urlParse(obj);
378
  if (!(obj instanceof Url)) return Url.prototype.format.call(obj);
379
  return obj.format();
380
}
381

    
382
Url.prototype.format = function() {
383
  var auth = this.auth || '';
384
  if (auth) {
385
    auth = encodeURIComponent(auth);
386
    auth = auth.replace(/%3A/i, ':');
387
    auth += '@';
388
  }
389

    
390
  var protocol = this.protocol || '',
391
      pathname = this.pathname || '',
392
      hash = this.hash || '',
393
      host = false,
394
      query = '';
395

    
396
  if (this.host) {
397
    host = auth + this.host;
398
  } else if (this.hostname) {
399
    host = auth + (this.hostname.indexOf(':') === -1 ?
400
        this.hostname :
401
        '[' + this.hostname + ']');
402
    if (this.port) {
403
      host += ':' + this.port;
404
    }
405
  }
406

    
407
  if (this.query &&
408
      util.isObject(this.query) &&
409
      Object.keys(this.query).length) {
410
    query = querystring.stringify(this.query);
411
  }
412

    
413
  var search = this.search || (query && ('?' + query)) || '';
414

    
415
  if (protocol && protocol.substr(-1) !== ':') protocol += ':';
416

    
417
  // only the slashedProtocols get the //.  Not mailto:, xmpp:, etc.
418
  // unless they had them to begin with.
419
  if (this.slashes ||
420
      (!protocol || slashedProtocol[protocol]) && host !== false) {
421
    host = '//' + (host || '');
422
    if (pathname && pathname.charAt(0) !== '/') pathname = '/' + pathname;
423
  } else if (!host) {
424
    host = '';
425
  }
426

    
427
  if (hash && hash.charAt(0) !== '#') hash = '#' + hash;
428
  if (search && search.charAt(0) !== '?') search = '?' + search;
429

    
430
  pathname = pathname.replace(/[?#]/g, function(match) {
431
    return encodeURIComponent(match);
432
  });
433
  search = search.replace('#', '%23');
434

    
435
  return protocol + host + pathname + search + hash;
436
};
437

    
438
function urlResolve(source, relative) {
439
  return urlParse(source, false, true).resolve(relative);
440
}
441

    
442
Url.prototype.resolve = function(relative) {
443
  return this.resolveObject(urlParse(relative, false, true)).format();
444
};
445

    
446
function urlResolveObject(source, relative) {
447
  if (!source) return relative;
448
  return urlParse(source, false, true).resolveObject(relative);
449
}
450

    
451
Url.prototype.resolveObject = function(relative) {
452
  if (util.isString(relative)) {
453
    var rel = new Url();
454
    rel.parse(relative, false, true);
455
    relative = rel;
456
  }
457

    
458
  var result = new Url();
459
  var tkeys = Object.keys(this);
460
  for (var tk = 0; tk < tkeys.length; tk++) {
461
    var tkey = tkeys[tk];
462
    result[tkey] = this[tkey];
463
  }
464

    
465
  // hash is always overridden, no matter what.
466
  // even href="" will remove it.
467
  result.hash = relative.hash;
468

    
469
  // if the relative url is empty, then there's nothing left to do here.
470
  if (relative.href === '') {
471
    result.href = result.format();
472
    return result;
473
  }
474

    
475
  // hrefs like //foo/bar always cut to the protocol.
476
  if (relative.slashes && !relative.protocol) {
477
    // take everything except the protocol from relative
478
    var rkeys = Object.keys(relative);
479
    for (var rk = 0; rk < rkeys.length; rk++) {
480
      var rkey = rkeys[rk];
481
      if (rkey !== 'protocol')
482
        result[rkey] = relative[rkey];
483
    }
484

    
485
    //urlParse appends trailing / to urls like http://www.example.com
486
    if (slashedProtocol[result.protocol] &&
487
        result.hostname && !result.pathname) {
488
      result.path = result.pathname = '/';
489
    }
490

    
491
    result.href = result.format();
492
    return result;
493
  }
494

    
495
  if (relative.protocol && relative.protocol !== result.protocol) {
496
    // if it's a known url protocol, then changing
497
    // the protocol does weird things
498
    // first, if it's not file:, then we MUST have a host,
499
    // and if there was a path
500
    // to begin with, then we MUST have a path.
501
    // if it is file:, then the host is dropped,
502
    // because that's known to be hostless.
503
    // anything else is assumed to be absolute.
504
    if (!slashedProtocol[relative.protocol]) {
505
      var keys = Object.keys(relative);
506
      for (var v = 0; v < keys.length; v++) {
507
        var k = keys[v];
508
        result[k] = relative[k];
509
      }
510
      result.href = result.format();
511
      return result;
512
    }
513

    
514
    result.protocol = relative.protocol;
515
    if (!relative.host && !hostlessProtocol[relative.protocol]) {
516
      var relPath = (relative.pathname || '').split('/');
517
      while (relPath.length && !(relative.host = relPath.shift()));
518
      if (!relative.host) relative.host = '';
519
      if (!relative.hostname) relative.hostname = '';
520
      if (relPath[0] !== '') relPath.unshift('');
521
      if (relPath.length < 2) relPath.unshift('');
522
      result.pathname = relPath.join('/');
523
    } else {
524
      result.pathname = relative.pathname;
525
    }
526
    result.search = relative.search;
527
    result.query = relative.query;
528
    result.host = relative.host || '';
529
    result.auth = relative.auth;
530
    result.hostname = relative.hostname || relative.host;
531
    result.port = relative.port;
532
    // to support http.request
533
    if (result.pathname || result.search) {
534
      var p = result.pathname || '';
535
      var s = result.search || '';
536
      result.path = p + s;
537
    }
538
    result.slashes = result.slashes || relative.slashes;
539
    result.href = result.format();
540
    return result;
541
  }
542

    
543
  var isSourceAbs = (result.pathname && result.pathname.charAt(0) === '/'),
544
      isRelAbs = (
545
          relative.host ||
546
          relative.pathname && relative.pathname.charAt(0) === '/'
547
      ),
548
      mustEndAbs = (isRelAbs || isSourceAbs ||
549
                    (result.host && relative.pathname)),
550
      removeAllDots = mustEndAbs,
551
      srcPath = result.pathname && result.pathname.split('/') || [],
552
      relPath = relative.pathname && relative.pathname.split('/') || [],
553
      psychotic = result.protocol && !slashedProtocol[result.protocol];
554

    
555
  // if the url is a non-slashed url, then relative
556
  // links like ../.. should be able
557
  // to crawl up to the hostname, as well.  This is strange.
558
  // result.protocol has already been set by now.
559
  // Later on, put the first path part into the host field.
560
  if (psychotic) {
561
    result.hostname = '';
562
    result.port = null;
563
    if (result.host) {
564
      if (srcPath[0] === '') srcPath[0] = result.host;
565
      else srcPath.unshift(result.host);
566
    }
567
    result.host = '';
568
    if (relative.protocol) {
569
      relative.hostname = null;
570
      relative.port = null;
571
      if (relative.host) {
572
        if (relPath[0] === '') relPath[0] = relative.host;
573
        else relPath.unshift(relative.host);
574
      }
575
      relative.host = null;
576
    }
577
    mustEndAbs = mustEndAbs && (relPath[0] === '' || srcPath[0] === '');
578
  }
579

    
580
  if (isRelAbs) {
581
    // it's absolute.
582
    result.host = (relative.host || relative.host === '') ?
583
                  relative.host : result.host;
584
    result.hostname = (relative.hostname || relative.hostname === '') ?
585
                      relative.hostname : result.hostname;
586
    result.search = relative.search;
587
    result.query = relative.query;
588
    srcPath = relPath;
589
    // fall through to the dot-handling below.
590
  } else if (relPath.length) {
591
    // it's relative
592
    // throw away the existing file, and take the new path instead.
593
    if (!srcPath) srcPath = [];
594
    srcPath.pop();
595
    srcPath = srcPath.concat(relPath);
596
    result.search = relative.search;
597
    result.query = relative.query;
598
  } else if (!util.isNullOrUndefined(relative.search)) {
599
    // just pull out the search.
600
    // like href='?foo'.
601
    // Put this after the other two cases because it simplifies the booleans
602
    if (psychotic) {
603
      result.hostname = result.host = srcPath.shift();
604
      //occationaly the auth can get stuck only in host
605
      //this especially happens in cases like
606
      //url.resolveObject('mailto:local1@domain1', 'local2@domain2')
607
      var authInHost = result.host && result.host.indexOf('@') > 0 ?
608
                       result.host.split('@') : false;
609
      if (authInHost) {
610
        result.auth = authInHost.shift();
611
        result.host = result.hostname = authInHost.shift();
612
      }
613
    }
614
    result.search = relative.search;
615
    result.query = relative.query;
616
    //to support http.request
617
    if (!util.isNull(result.pathname) || !util.isNull(result.search)) {
618
      result.path = (result.pathname ? result.pathname : '') +
619
                    (result.search ? result.search : '');
620
    }
621
    result.href = result.format();
622
    return result;
623
  }
624

    
625
  if (!srcPath.length) {
626
    // no path at all.  easy.
627
    // we've already handled the other stuff above.
628
    result.pathname = null;
629
    //to support http.request
630
    if (result.search) {
631
      result.path = '/' + result.search;
632
    } else {
633
      result.path = null;
634
    }
635
    result.href = result.format();
636
    return result;
637
  }
638

    
639
  // if a url ENDs in . or .., then it must get a trailing slash.
640
  // however, if it ends in anything else non-slashy,
641
  // then it must NOT get a trailing slash.
642
  var last = srcPath.slice(-1)[0];
643
  var hasTrailingSlash = (
644
      (result.host || relative.host || srcPath.length > 1) &&
645
      (last === '.' || last === '..') || last === '');
646

    
647
  // strip single dots, resolve double dots to parent dir
648
  // if the path tries to go above the root, `up` ends up > 0
649
  var up = 0;
650
  for (var i = srcPath.length; i >= 0; i--) {
651
    last = srcPath[i];
652
    if (last === '.') {
653
      srcPath.splice(i, 1);
654
    } else if (last === '..') {
655
      srcPath.splice(i, 1);
656
      up++;
657
    } else if (up) {
658
      srcPath.splice(i, 1);
659
      up--;
660
    }
661
  }
662

    
663
  // if the path is allowed to go above the root, restore leading ..s
664
  if (!mustEndAbs && !removeAllDots) {
665
    for (; up--; up) {
666
      srcPath.unshift('..');
667
    }
668
  }
669

    
670
  if (mustEndAbs && srcPath[0] !== '' &&
671
      (!srcPath[0] || srcPath[0].charAt(0) !== '/')) {
672
    srcPath.unshift('');
673
  }
674

    
675
  if (hasTrailingSlash && (srcPath.join('/').substr(-1) !== '/')) {
676
    srcPath.push('');
677
  }
678

    
679
  var isAbsolute = srcPath[0] === '' ||
680
      (srcPath[0] && srcPath[0].charAt(0) === '/');
681

    
682
  // put the host back
683
  if (psychotic) {
684
    result.hostname = result.host = isAbsolute ? '' :
685
                                    srcPath.length ? srcPath.shift() : '';
686
    //occationaly the auth can get stuck only in host
687
    //this especially happens in cases like
688
    //url.resolveObject('mailto:local1@domain1', 'local2@domain2')
689
    var authInHost = result.host && result.host.indexOf('@') > 0 ?
690
                     result.host.split('@') : false;
691
    if (authInHost) {
692
      result.auth = authInHost.shift();
693
      result.host = result.hostname = authInHost.shift();
694
    }
695
  }
696

    
697
  mustEndAbs = mustEndAbs || (result.host && srcPath.length);
698

    
699
  if (mustEndAbs && !isAbsolute) {
700
    srcPath.unshift('');
701
  }
702

    
703
  if (!srcPath.length) {
704
    result.pathname = null;
705
    result.path = null;
706
  } else {
707
    result.pathname = srcPath.join('/');
708
  }
709

    
710
  //to support request.http
711
  if (!util.isNull(result.pathname) || !util.isNull(result.search)) {
712
    result.path = (result.pathname ? result.pathname : '') +
713
                  (result.search ? result.search : '');
714
  }
715
  result.auth = relative.auth || result.auth;
716
  result.slashes = result.slashes || relative.slashes;
717
  result.href = result.format();
718
  return result;
719
};
720

    
721
Url.prototype.parseHost = function() {
722
  var host = this.host;
723
  var port = portPattern.exec(host);
724
  if (port) {
725
    port = port[0];
726
    if (port !== ':') {
727
      this.port = port.substr(1);
728
    }
729
    host = host.substr(0, host.length - port.length);
730
  }
731
  if (host) this.hostname = host;
732
};
(8-8/9)