Skip to content

Commit 2a30400

Browse files
committed
- fixed edge-cases in .protocol(), .port(), .subdomain(), .domain(), .tld(), .filename()
- fixed parsing of hostname in .hostname()
1 parent 639c9f8 commit 2a30400

4 files changed

Lines changed: 186 additions & 46 deletions

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,11 @@ URI.js is published under the [MIT license](http://www.opensource.org/licenses/m
165165

166166
## Changelog ##
167167

168+
* Updated Punycode.js to version 0.3.0
169+
* added edge-case tests ("jim")
170+
* fixed edge-cases in .protocol(), .port(), .subdomain(), .domain(), .tld(), .filename()
171+
* fixed parsing of hostname in .hostname()
172+
168173
### 1.3.0 ###
169174

170175
* added .subdomain() convenience accessor

docs.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ <h3 id="accessors-protocol">protocol()</h3>
178178
uri.protocol(); // returns string "http"
179179
// set protocol
180180
uri.protocol("ftp"); // returns the URI instance for chaining</pre>
181+
<p>NOTE: Throws a <code>TypeError</code> on illegal input</p>
181182

182183
<h3 id="accessors-username">username()</h3>
183184
<pre class="prettyprint lang-js">var uri = new URI("http://user:pass@example.org/foo/hello.html");
@@ -208,6 +209,7 @@ <h3 id="accessors-port">port()</h3>
208209
// set port
209210
uri.port("80"); // returns the URI instance for chaining</pre>
210211
<p>NOTE: although the port may be considered an integer, within URI it is a string.</p>
212+
<p>NOTE: Throws a <code>TypeError</code> on illegal input</p>
211213

212214
<h3 id="accessors-host">host()</h3>
213215
<pre class="prettyprint lang-js">var uri = new URI("http://example.org:80/foo/hello.html");
@@ -234,6 +236,7 @@ <h3 id="accessors-domain">domain()</h3>
234236
// set domain
235237
uri.domain("otherdomain.com"); // returns the URI instance for chaining</pre>
236238
<p>NOTE: .domain() will throw an error if you pass it an empty string.</p>
239+
<p>NOTE: Throws a <code>TypeError</code> on illegal input</p>
237240

238241
<h3 id="accessors-subdomain">subdomain()</h3>
239242
<p>.subdomain() is a convenience method that returns <code>www</code> from the hostname <code>www.example.org</code>.</p>
@@ -242,6 +245,7 @@ <h3 id="accessors-subdomain">subdomain()</h3>
242245
uri.subdomain(); // returns string "www"
243246
// set subdomain
244247
uri.subdomain("other.subdomain"); // returns the URI instance for chaining</pre>
248+
<p>NOTE: Throws a <code>TypeError</code> on illegal input</p>
245249

246250
<h3 id="accessors-tld">tld()</h3>
247251
<p>.tld() is a convenience method that returns <code>org</code> from the hostname <code>www.example.org</code>.</p>
@@ -250,7 +254,7 @@ <h3 id="accessors-tld">tld()</h3>
250254
uri.tld(); // returns string "org"
251255
// set tld
252256
uri.tld("com"); // returns the URI instance for chaining</pre>
253-
<p>NOTE: .tld() will throw an error if you pass it an empty string or use it on an IP-host.</p>
257+
<p>NOTE: Throws an <code>Error</code> if you pass it an empty string or use it on an IP-host.</p>
254258

255259
<h3 id="accessors-pathname">pathname(), path()</h3>
256260
<p>.path() is an alias of .pathname()</p>
@@ -295,6 +299,7 @@ <h3 id="accessors-filename">filename()</h3>
295299
uri.filename() === 'hello%20world.html';
296300
// will decode for you
297301
uri.filename(true) === 'hello world.html';</pre>
302+
<p>NOTE: If you pass <code>../file.html</code>, the directory will be changed accordingly</p>
298303

299304
<h3 id="accessors-suffix">suffix()</h3>
300305
<p>.suffix() is an convenience method for mutating the filename part of a path</p>

src/URI.js

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,10 @@ URI.defaultPorts = {
8686
https: "443",
8787
ftp: "21"
8888
};
89-
89+
// allowed hostname characters according to RFC 3986
90+
// ALPHA DIGIT "-" "." "_" "~" "!" "$" "&" "'" "(" ")" "*" "+" "," ";" "=" %encoded
91+
// I've never seen a (non-IDN) hostname other than: ALPHA DIGIT . -
92+
URI.invalid_hostname_characters = /[^a-zA-Z0-9\.-]/;
9093
// encoding / decoding according to RFC3986
9194
URI.encode = encodeURIComponent;
9295
URI.decode = decodeURIComponent;
@@ -206,6 +209,7 @@ URI.parseHost = function(string, parts) {
206209
parts.port = string.substring(bracketPos+2, pos) || null;
207210
} else if (string.indexOf(':') !== string.lastIndexOf(':')) {
208211
// IPv6 host contains multiple colons - but no port
212+
// this notation is actually not allowed by RFC 3986, but we're a liberal parser
209213
parts.hostname = string.substring(0, pos) || null;
210214
parts.port = null;
211215
} else {
@@ -457,6 +461,22 @@ URI.withinString = function(string, callback) {
457461
return string.replace(URI.find_uri_expression, callback);
458462
};
459463

464+
URI.ensureValidHostname = function(v) {
465+
// Theoretically URIs allow percent-encoding in Hostnames (according to RFC 3986)
466+
// they are not part of DNS and therefore ignored by URI.js
467+
468+
if (v.match(URI.invalid_hostname_characters)) {
469+
// test punycode
470+
if (!window.punycode) {
471+
throw new TypeError("Hostname '" + v + "' contains characters other than [A-Z0-9.-] and Punycode.js is not available");
472+
}
473+
474+
if (punycode.toASCII(v).match(URI.invalid_hostname_characters)) {
475+
throw new TypeError("Hostname '" + v + "' contains characters other than [A-Z0-9.-]");
476+
}
477+
}
478+
};
479+
460480
p.build = function(deferBuild) {
461481
if (deferBuild === true) {
462482
this._deferred_build = true;
@@ -627,6 +647,52 @@ p.is = function(what) {
627647
return null;
628648
};
629649

650+
// component specific input validation
651+
var _protocol = p.protocol,
652+
_port = p.port,
653+
_hostname = p.hostname;
654+
655+
p.protocol = function(v, build) {
656+
if (v !== undefined) {
657+
// accept trailing :
658+
if (v) {
659+
if (v[v.length - 1] === ":") {
660+
v = v.substring(0, v.length - 1);
661+
} else if (v.match(/[^a-zA-z0-9\.+-]/)) {
662+
throw new TypeError("Protocol '" + v + "' contains characters other than [A-Z0-9.+-]");
663+
}
664+
}
665+
}
666+
return _protocol.call(this, v, build);
667+
};
668+
p.port = function(v, build) {
669+
if (v !== undefined) {
670+
if (v === 0) {
671+
v = null;
672+
}
673+
674+
if (v) {
675+
v += "";
676+
if (v[0] === ":") {
677+
v = v.substring(1);
678+
}
679+
680+
if (v.match(/[^0-9]/)) {
681+
throw new TypeError("Port '" + v + "' contains characters other than [0-9]");
682+
}
683+
}
684+
}
685+
return _port.call(this, v, build);
686+
};
687+
p.hostname = function(v, build) {
688+
if (v !== undefined) {
689+
var x = {};
690+
URI.parseHost(v, x);
691+
v = x.hostname;
692+
}
693+
return _hostname.call(this, v, build);
694+
};
695+
630696
// combination accessors
631697
p.host = function(v, build) {
632698
if (v === undefined) {
@@ -666,6 +732,10 @@ p.subdomain = function(v, build) {
666732
v += ".";
667733
}
668734

735+
if (v) {
736+
URI.ensureValidHostname(v);
737+
}
738+
669739
this._parts.hostname = this._parts.hostname.replace(replace, v);
670740
this.build(!build);
671741
return this;
@@ -683,7 +753,11 @@ p.domain = function(v, build) {
683753
} else {
684754
if (!v) {
685755
throw new TypeError("cannot set domain empty");
686-
} else if (!this._parts.hostname || this.is('IP')) {
756+
}
757+
758+
URI.ensureValidHostname(v);
759+
760+
if (!this._parts.hostname || this.is('IP')) {
687761
this._parts.hostname = v;
688762
} else {
689763
var replace = new RegExp(escapeRegEx(this.domain()) + "$");
@@ -706,6 +780,8 @@ p.tld = function(v, build) {
706780
} else {
707781
if (!v) {
708782
throw new TypeError("cannot set TLD empty");
783+
} else if (v.match(/[^a-zA-Z0-9-]/)) {
784+
throw new TypeError("TLD '" + v + "' contains characters other than [A-Z0-9]");
709785
} else if (!this._parts.hostname || this.is('IP')) {
710786
throw new ReferenceError("cannot set TLD on non-domain host");
711787
} else {
@@ -766,15 +842,25 @@ p.filename = function(v, build) {
766842

767843
return v ? URI.decodePathSegment(res) : res;
768844
} else {
769-
845+
var mutatedDirectory = false;
770846
if (v[0] === '/') {
771847
v = v.substring(1);
772848
}
773849

850+
if (v.match(/\.?\//)) {
851+
mutatedDirectory = true;
852+
}
853+
774854
var replace = new RegExp(escapeRegEx(this.filename()) + "$");
775855
v = URI.recodePath(v);
776856
this._parts.path = this._parts.path.replace(replace, v);
777-
this.build(!build);
857+
858+
if (mutatedDirectory) {
859+
this.normalizePath(build);
860+
} else {
861+
this.build(!build);
862+
}
863+
778864
return this;
779865
}
780866
};

test/test_jim.js

Lines changed: 85 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,112 @@
11
/*
2-
What would jim do?
2+
* What would jim do?
3+
* more tests for border-edge cases
4+
* Christian Harms.
5+
*
6+
* Note: I have no clue who or what jim is supposed to be. It might be something like the German DAU (dumbest possible user)
7+
*/
38

4-
more tests for border-edge cases
5-
6-
Christian Harms.
7-
8-
*/
9-
10-
// try to set one part - modify the next part
119
module("injection");
1210
test("protocol", function() {
1311
var u = new URI("http://example.com/dir1/dir2/?query1=value1&query2=value2#hash");
14-
u.protocol("ftp://example.org");
12+
raises(function() {
13+
u.protocol("ftp://example.org");
14+
}, TypeError, "Failing invalid characters");
15+
16+
u.protocol("ftp:");
1517
equal(u.protocol(), "ftp", "protocol() has set invalid protocoll!");
1618
equal(u.hostname(), "example.com", "protocol() has changed the hostname");
17-
});
18-
19+
});
1920
test("port", function() {
2021
var u = new URI("http://example.com/dir1/dir2/?query1=value1&query2=value2#hash");
21-
u.port("99:example.org");
22+
raises(function() {
23+
u.port("99:example.org");
24+
}, TypeError, "Failing invalid characters");
25+
26+
u.port(":99");
2227
equal(u.hostname(), "example.com", "port() has modified hostname");
2328
equal(u.port(), 99, "port() has set an invalid port");
24-
});
25-
test("domain", function() {
26-
var u = new URI("http://example.com/dir1/dir2/?query1=value1&query2=value2#hash");
27-
u.domain("example.org/dir0/");
28-
equal(u.hostname(), "example.com", "domain() has set invalid domain");
29-
equal(u.path(), "/dir1/dir2/", "domain() has inflicted path with part of domainname");
3029

31-
});
30+
u.port(false);
31+
equal(u.port(), "", "port() has set an invalid port");
32+
33+
// RFC 3986 says nothing about "16-bit unsigned" http://tools.ietf.org/html/rfc3986#section-3.2.3
34+
// u.href(new URI("http://example.com/"))
35+
// u.port(65536);
36+
// notEqual(u.port(), "65536", "port() has set to an non-valid value (A port number is a 16-bit unsigned integer)");
3237

38+
raises(function() {
39+
u.port("-99");
40+
}, TypeError, "Failing invalid characters");
41+
});
42+
test("domain", function() {
43+
var u = new URI("http://example.com/dir1/dir2/?query1=value1&query2=value2#hash");
44+
45+
raises(function() {
46+
u.domain("example.org/dir0/");
47+
}, TypeError, "Failing invalid characters");
48+
49+
raises(function() {
50+
u.domain("example.org:80");
51+
}, TypeError, "Failing invalid characters");
52+
53+
raises(function() {
54+
u.domain("foo@example.org");
55+
}, TypeError, "Failing invalid characters");
56+
});
57+
test("subdomain", function() {
58+
var u = new URI("http://example.com/dir1/dir2/?query1=value1&query2=value2#hash");
59+
60+
raises(function() {
61+
u.subdomain("example.org/dir0/");
62+
}, TypeError, "Failing invalid characters");
63+
64+
raises(function() {
65+
u.subdomain("example.org:80");
66+
}, TypeError, "Failing invalid characters");
67+
68+
raises(function() {
69+
u.subdomain("foo@example.org");
70+
}, TypeError, "Failing invalid characters");
71+
});
72+
test("tld", function() {
73+
var u = new URI("http://example.com/dir1/dir2/?query1=value1&query2=value2#hash");
74+
75+
raises(function() {
76+
u.tld("foo/bar.html");
77+
}, TypeError, "Failing invalid characters");
78+
});
3379
test("path", function() {
3480
var u = new URI("http://example.com/dir1/dir2/?query1=value1&query2=value2#hash");
35-
u.path("/dir3/?query3=value3");
81+
u.path("/dir3/?query3=value3#fragment");
3682
equal(u.hostname(), "example.com", "path() has modified hostname");
37-
equal(u.path(), "/dir3/%3Fquery3=value3", "path() has set invalid path");
83+
equal(u.path(), "/dir3/%3Fquery3=value3%23fragment", "path() has set invalid path");
3884
equal(u.query(), "query1=value1&query2=value2", "path() has modified query");
39-
});
40-
85+
equal(u.fragment(), "hash", "path() has modified fragment");
86+
});
4187
test("filename", function() {
4288
var u = new URI("http://example.com/dir1/dir2/?query1=value1&query2=value2#hash");
43-
u.filename("../name.html?query");
44-
equal(u.hostname(), "example.com", "filename() has modified hostname");
45-
equal(u.path(), "/dir1/dir2/", "filename() has modified path");
46-
equal(u.filename(), "name.html%3Fquery", "filename() has set invalid filename")
89+
90+
u.filename("name.html?query");
91+
equal(u.filename(), "name.html%3Fquery", "filename() has set invalid filename");
4792
equal(u.query(), "query1=value1&query2=value2", "filename() has modified query");
48-
});
49-
93+
94+
// allowed!
95+
u.filename("../name.html?query");
96+
equal(u.filename(), "name.html%3Fquery", "filename() has set invalid filename");
97+
equal(u.directory(), "/dir1", "filename() has not altered directory properly");
98+
});
5099
test("addQuery", function() {
51100
var u = new URI("http://example.com/dir1/dir2/?query1=value1&query2=value2#hash");
52101
u.addQuery("query3", "value3#got");
53102
equal(u.query(), "query1=value1&query2=value2&query3=value3%23got", "addQuery() has set invalid query");
54103
equal(u.fragment(), "hash", "addQuery() has modified fragment");
55-
});
104+
});
56105

57-
58-
// try to set not-valid values - check the rfc-allowed parts
106+
107+
/*
108+
// RFC 3986 says "…and should limit these names to no more than 255 characters in length."
109+
// SHOULD is not MUST therefore not the responsibility of URI.js
59110
60111
module("validation");
61112
test("domain", function() {
@@ -88,12 +139,5 @@ test("domain", function() {
88139
}
89140
u.domain(domain);
90141
equals(u.hostname() == domain, true, "set domain() with 70-character subdomain not valid domainname");
91-
});
92-
93-
test("port", function() {
94-
var u = new URI("http://example.com/");
95-
u.port(65536);
96-
equal(u.port() === 65536, false, "port() has set to an non-valid value (A port number is a 16-bit unsigned integer)");
97-
u.port(-1);
98-
equal(u.port() === -1, false, "port() has set to an non-valid value (A port number is a 16-bit unsigned integer)");
99-
});
142+
});
143+
*/

0 commit comments

Comments
 (0)