diff --git a/src/URI.js b/src/URI.js index 6f7fd572..f63ad6c6 100644 --- a/src/URI.js +++ b/src/URI.js @@ -240,10 +240,16 @@ ws: '80', wss: '443' }; + // list of protocols which always require a hostname + URI.hostProtocols = [ + 'http', + 'https' + ]; + // allowed hostname characters according to RFC 3986 // ALPHA DIGIT "-" "." "_" "~" "!" "$" "&" "'" "(" ")" "*" "+" "," ";" "=" %encoded // I've never seen a (non-IDN) hostname other than: ALPHA DIGIT . - - URI.invalid_hostname_characters = /[^a-zA-Z0-9\.-]/; + URI.invalid_hostname_characters = /[^a-zA-Z0-9\.\-:]/; // map DOM Elements to their URI attribute URI.domAttributes = { 'a': 'href', @@ -575,6 +581,12 @@ string = '/' + string; } + URI.ensureValidHostname(parts.hostname, parts.protocol); + + if (parts.port) { + URI.ensureValidPort(parts.port); + } + return string.substring(pos) || '/'; }; URI.parseAuthority = function(string, parts) { @@ -1015,22 +1027,44 @@ return string; }; - URI.ensureValidHostname = function(v) { + URI.ensureValidHostname = function(v, protocol) { // Theoretically URIs allow percent-encoding in Hostnames (according to RFC 3986) // they are not part of DNS and therefore ignored by URI.js - if (v.match(URI.invalid_hostname_characters)) { + var hasHostname = !!v; // not null and not an empty string + var hasProtocol = !!protocol; + var rejectEmptyHostname = false; + + if (hasProtocol) { + rejectEmptyHostname = arrayContains(URI.hostProtocols, protocol); + } + + if (rejectEmptyHostname && !hasHostname) { + throw new TypeError('Hostname cannot be empty, if protocol is ' + protocol); + } else if (v && v.match(URI.invalid_hostname_characters)) { // test punycode if (!punycode) { throw new TypeError('Hostname "' + v + '" contains characters other than [A-Z0-9.-] and Punycode.js is not available'); } - if (punycode.toASCII(v).match(URI.invalid_hostname_characters)) { - throw new TypeError('Hostname "' + v + '" contains characters other than [A-Z0-9.-]'); + throw new TypeError('Hostname "' + v + '" contains characters other than [A-Z0-9.:-]'); } } }; + URI.ensureValidPort = function (v) { + if (!v) { + return; + } + + var port = Number(v); + if (Number.isInteger(port) && (port > 0) && (port < 65536)) { + return; + } + + throw new TypeError('Port "' + v + '" is not a valid port'); + }; + // noConflict URI.noConflict = function(removeAll) { if (removeAll) { @@ -1288,9 +1322,7 @@ v = v.substring(1); } - if (v.match(/[^0-9]/)) { - throw new TypeError('Port "' + v + '" contains characters other than [0-9]'); - } + URI.ensureValidPort(v); } } return _port.call(this, v, build); @@ -1308,6 +1340,7 @@ } v = x.hostname; + URI.ensureValidHostname(v, this._parts.protocol); } return _hostname.call(this, v, build); }; @@ -1426,8 +1459,12 @@ v += '.'; } + if (v.indexOf(':') !== -1) { + throw new TypeError('Domains cannot contain colons'); + } + if (v) { - URI.ensureValidHostname(v); + URI.ensureValidHostname(v, this._parts.protocol); } this._parts.hostname = this._parts.hostname.replace(replace, v); @@ -1466,7 +1503,11 @@ throw new TypeError('cannot set domain empty'); } - URI.ensureValidHostname(v); + if (v.indexOf(':') !== -1) { + throw new TypeError('Domains cannot contain colons'); + } + + URI.ensureValidHostname(v, this._parts.protocol); if (!this._parts.hostname || this.is('IP')) { this._parts.hostname = v; diff --git a/test/test.js b/test/test.js index 1cc2ce5d..8ebf8623 100644 --- a/test/test.js +++ b/test/test.js @@ -124,6 +124,26 @@ ok(u instanceof URI, 'instanceof URI'); ok(u._parts.hostname !== undefined, 'host undefined'); }); + test('function URI(string) with invalid port "port" throws', function () { + raises(function () { + new URI('http://example.org:port'); + }, TypeError, "throws TypeError"); + }); + test('function URI(string) with invalid port "0" throws', function () { + raises(function () { + new URI('http://example.org:0'); + }, TypeError, "throws TypeError"); + }); + test('function URI(string) with invalid port "65536" throws', function () { + raises(function () { + new URI('http://example.org:65536'); + }, TypeError, "throws TypeError"); + }); + test('function URI(string) with protocol and without hostname should throw', function () { + raises(function () { + new URI('http://'); + }, TypeError, "throws TypeError"); + }); test('new URI(string, string)', function() { // see http://dvcs.w3.org/hg/url/raw-file/tip/Overview.html#constructor var u = new URI('../foobar.html', 'http://example.org/hello/world.html'); @@ -223,13 +243,16 @@ equal(u.hostname(), 'abc.foobar.lala', 'hostname changed'); equal(u+'', 'http://abc.foobar.lala/foo.html', 'hostname changed url'); - u.hostname(''); - equal(u.hostname(), '', 'hostname removed'); - equal(u+'', 'http:///foo.html', 'hostname removed url'); - raises(function() { u.hostname('foo\\bar.com'); }, TypeError, 'Failing backslash detection in hostname'); + + raises(function() { + u.hostname(''); + }, TypeError, "Trying to set an empty hostname with http(s) protocol throws a TypeError"); + raises(function() { + u.hostname(null); + }, TypeError, "Trying to set hostname to null with http(s) protocol throws a TypeError"); }); test('port', function() { var u = new URI('http://example.org/foo.html'); @@ -1338,11 +1361,6 @@ url: 'file:///C:/skyclan/snipkit', base: 'http://example.com/foo/bar', result: 'file:///C:/skyclan/snipkit' - }, { - name: 'absolute passthru - generic empty-hostname - urljoin (#328)', - url: 'http:///foo', - base: 'http://example.com/foo/bar', - result: 'http:///foo' }, { name: 'file paths - urljoin', url: 'anotherFile',