From 362513b99693d76df8852787eafba79a3656ef2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konstantin=20Bl=C3=A4si?= Date: Tue, 1 Aug 2017 08:51:30 +0200 Subject: [PATCH 1/6] Make constructor throw on invalid ports. This makes the behaviour consistent with port(). --- src/URI.js | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/URI.js b/src/URI.js index 6f7fd572..09bd040f 100644 --- a/src/URI.js +++ b/src/URI.js @@ -575,6 +575,10 @@ string = '/' + string; } + if (parts.port) { + URI.ensureValidPort(parts.port); + } + return string.substring(pos) || '/'; }; URI.parseAuthority = function(string, parts) { @@ -1031,6 +1035,27 @@ } }; + URI.ensureValidPort = function(v) { + var valid = false; + + if (!v || !v.length) { + // no custom port specified + valid = true; + } else { + var port = Number(v); + + // verify type and range + if (Number.isInteger(port) && (port > 0) && (port < 65536) + ) { + valid = true; + } + } + + if (!valid) { + throw new TypeError('Port "' + v + '" is not a valid port'); + } + }; + // noConflict URI.noConflict = function(removeAll) { if (removeAll) { @@ -1288,9 +1313,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); From dd6846bddb65284dfec53aacc44de8d1af809461 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konstantin=20Bl=C3=A4si?= Date: Tue, 1 Aug 2017 14:20:59 +0200 Subject: [PATCH 2/6] Add tests for port validation in constructor. --- test/test.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/test.js b/test/test.js index 1cc2ce5d..e50f1523 100644 --- a/test/test.js +++ b/test/test.js @@ -124,6 +124,21 @@ 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('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'); From 7bfb89eea11170d263c2b20f28ec1121497ae70b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konstantin=20Bl=C3=A4si?= Date: Tue, 1 Aug 2017 16:23:56 +0200 Subject: [PATCH 3/6] Add some basic validation in the constructor. --- src/URI.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/URI.js b/src/URI.js index 09bd040f..746bc350 100644 --- a/src/URI.js +++ b/src/URI.js @@ -243,7 +243,7 @@ // 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', @@ -524,6 +524,8 @@ // what's left must be the path parts.path = string; + URI.basicValidation(parts); + // and we're done return parts; }; @@ -575,6 +577,10 @@ string = '/' + string; } + if (parts.hostname) { + URI.ensureValidHostname(parts.hostname); + } + if (parts.port) { URI.ensureValidPort(parts.port); } @@ -1028,9 +1034,8 @@ 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.:-]'); } } }; @@ -1056,6 +1061,16 @@ } }; + URI.basicValidation = function(parts) { + var hasProtocol = !!parts.protocol; // not null and not empty an empty string + var isHttpOrHttps = hasProtocol && (parts.protocol.indexOf('http') !== -1); + var hasHostname = !!parts.hostname; // not null and not an empty string + + if (isHttpOrHttps && !hasHostname) { + throw new TypeError('Hostname cannot be empty, if protocol is http(s)'); + } + }; + // noConflict URI.noConflict = function(removeAll) { if (removeAll) { From 520d29db1864f7c32c04b312ccb9fb3134985d00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konstantin=20Bl=C3=A4si?= Date: Wed, 2 Aug 2017 16:12:37 +0200 Subject: [PATCH 4/6] Fix tests. --- src/URI.js | 8 ++++++++ test/test.js | 10 +++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/URI.js b/src/URI.js index 746bc350..eabc504e 100644 --- a/src/URI.js +++ b/src/URI.js @@ -1464,6 +1464,10 @@ v += '.'; } + if (v.indexOf(':') !== -1) { + throw new TypeError('Domains cannot contain colons'); + } + if (v) { URI.ensureValidHostname(v); } @@ -1504,6 +1508,10 @@ throw new TypeError('cannot set domain empty'); } + if (v.indexOf(':') !== -1) { + throw new TypeError('Domains cannot contain colons'); + } + URI.ensureValidHostname(v); if (!this._parts.hostname || this.is('IP')) { diff --git a/test/test.js b/test/test.js index e50f1523..41ad48b0 100644 --- a/test/test.js +++ b/test/test.js @@ -139,6 +139,11 @@ 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'); @@ -1353,11 +1358,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', From a90bbec28f90fd6975fcee759a48b4c70158039c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konstantin=20Bl=C3=A4si?= Date: Wed, 2 Aug 2017 19:02:38 +0200 Subject: [PATCH 5/6] Simplify URI.ensureValidPort(). --- src/URI.js | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/URI.js b/src/URI.js index eabc504e..52bb18be 100644 --- a/src/URI.js +++ b/src/URI.js @@ -1040,25 +1040,17 @@ } }; - URI.ensureValidPort = function(v) { - var valid = false; - - if (!v || !v.length) { - // no custom port specified - valid = true; - } else { - var port = Number(v); - - // verify type and range - if (Number.isInteger(port) && (port > 0) && (port < 65536) - ) { - valid = true; - } + URI.ensureValidPort = function (v) { + if (!v) { + return; } - if (!valid) { - throw new TypeError('Port "' + v + '" is not a valid port'); + var port = Number(v); + if (Number.isInteger(port) && (port > 0) && (port < 65536)) { + return; } + + throw new TypeError('Port "' + v + '" is not a valid port'); }; URI.basicValidation = function(parts) { From dd40ca7c1c7b21b0272d13eb35cc6ea79711b31c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konstantin=20Bl=C3=A4si?= Date: Mon, 7 Aug 2017 17:15:20 +0200 Subject: [PATCH 6/6] Make hostname requirement configuration via URI.hostProtocols. Protocols listed in this array will require the user to specify a hostname. Oherwise URI.js' factory/constructor will throw a TypeError. --- src/URI.js | 41 ++++++++++++++++++++++------------------- test/test.js | 11 +++++++---- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/URI.js b/src/URI.js index 52bb18be..f63ad6c6 100644 --- a/src/URI.js +++ b/src/URI.js @@ -240,6 +240,12 @@ 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 . - @@ -524,8 +530,6 @@ // what's left must be the path parts.path = string; - URI.basicValidation(parts); - // and we're done return parts; }; @@ -577,9 +581,7 @@ string = '/' + string; } - if (parts.hostname) { - URI.ensureValidHostname(parts.hostname); - } + URI.ensureValidHostname(parts.hostname, parts.protocol); if (parts.port) { URI.ensureValidPort(parts.port); @@ -1025,11 +1027,21 @@ 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'); @@ -1053,16 +1065,6 @@ throw new TypeError('Port "' + v + '" is not a valid port'); }; - URI.basicValidation = function(parts) { - var hasProtocol = !!parts.protocol; // not null and not empty an empty string - var isHttpOrHttps = hasProtocol && (parts.protocol.indexOf('http') !== -1); - var hasHostname = !!parts.hostname; // not null and not an empty string - - if (isHttpOrHttps && !hasHostname) { - throw new TypeError('Hostname cannot be empty, if protocol is http(s)'); - } - }; - // noConflict URI.noConflict = function(removeAll) { if (removeAll) { @@ -1338,6 +1340,7 @@ } v = x.hostname; + URI.ensureValidHostname(v, this._parts.protocol); } return _hostname.call(this, v, build); }; @@ -1461,7 +1464,7 @@ } if (v) { - URI.ensureValidHostname(v); + URI.ensureValidHostname(v, this._parts.protocol); } this._parts.hostname = this._parts.hostname.replace(replace, v); @@ -1504,7 +1507,7 @@ throw new TypeError('Domains cannot contain colons'); } - URI.ensureValidHostname(v); + 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 41ad48b0..8ebf8623 100644 --- a/test/test.js +++ b/test/test.js @@ -243,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');