summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNayeem Rahman <nayeemrmn99@gmail.com>2020-07-13 05:56:45 +0100
committerGitHub <noreply@github.com>2020-07-13 00:56:45 -0400
commit63edeb1c36bfcea278c248e8be92f7dbb75f7671 (patch)
treedbf47efb616fbe896e2c7b559f186af8150ab5e7
parent4aeac64ecd82d5953bdb92916248f5f7352be4f4 (diff)
fix(cli/js/web/url): Implement IPv4 hostname parsing (#6707)
-rw-r--r--cli/js/web/url.ts85
-rw-r--r--cli/tests/unit/url_test.ts76
-rw-r--r--std/http/server.ts5
3 files changed, 125 insertions, 41 deletions
diff --git a/cli/js/web/url.ts b/cli/js/web/url.ts
index e429fddbb..544168d80 100644
--- a/cli/js/web/url.ts
+++ b/cli/js/web/url.ts
@@ -73,12 +73,14 @@ function parse(url: string, isBase = true): URLParts | undefined {
// equivalent to: `new URL("file://localhost/foo/bar")`.
[parts.hostname, restUrl] = takePattern(restUrl, /^[/\\]{2,}([^/\\?#]*)/);
}
- } else if (isSpecial) {
- parts.slashes = "//";
+ } else {
let restAuthority;
- [restAuthority, restUrl] = takePattern(restUrl, /^[/\\]{2,}([^/\\?#]+)/);
- if (isBase && restAuthority == "") {
- return undefined;
+ if (isSpecial) {
+ parts.slashes = "//";
+ [restAuthority, restUrl] = takePattern(restUrl, /^[/\\]{2,}([^/\\?#]*)/);
+ } else {
+ parts.slashes = restUrl.match(/^[/\\]{2}/) ? "//" : "";
+ [restAuthority, restUrl] = takePattern(restUrl, /^[/\\]{2}([^/\\?#]*)/);
}
let restAuthentication;
[restAuthentication, restAuthority] = takePattern(restAuthority, /^(.*)@/);
@@ -97,16 +99,9 @@ function parse(url: string, isBase = true): URLParts | undefined {
if (!isValidPort(parts.port)) {
return undefined;
}
- } else {
- [parts.slashes, restUrl] = takePattern(restUrl, /^([/\\]{2})/);
- parts.username = "";
- parts.password = "";
- if (parts.slashes) {
- [parts.hostname, restUrl] = takePattern(restUrl, /^([^/\\?#]*)/);
- } else {
- parts.hostname = "";
+ if (parts.hostname == "" && isSpecial && isBase) {
+ return undefined;
}
- parts.port = "";
}
try {
parts.hostname = encodeHostname(parts.hostname, isSpecial);
@@ -315,9 +310,13 @@ export class URLImpl implements URL {
this.username || this.password
? `${this.username}${this.password ? ":" + this.password : ""}@`
: "";
- return `${this.protocol}${parts.get(this)!.slashes}${authentication}${
- this.host
- }${this.pathname}${this.search}${this.hash}`;
+ const host = this.host;
+ const slashes = host ? "//" : parts.get(this)!.slashes;
+ let pathname = this.pathname;
+ if (pathname.charAt(0) != "/" && pathname != "" && host != "") {
+ pathname = `/${pathname}`;
+ }
+ return `${this.protocol}${slashes}${authentication}${host}${pathname}${this.search}${this.hash}`;
}
set href(value: string) {
@@ -346,16 +345,17 @@ export class URLImpl implements URL {
}
get pathname(): string {
- return parts.get(this)?.path || "/";
+ let path = parts.get(this)!.path;
+ if (specialSchemes.includes(parts.get(this)!.protocol)) {
+ if (path.charAt(0) != "/") {
+ path = `/${path}`;
+ }
+ }
+ return path;
}
set pathname(value: string) {
- value = unescape(String(value));
- if (!value || value.charAt(0) !== "/") {
- value = `/${value}`;
- }
- // paths can contain % unescaped
- parts.get(this)!.path = encodePathname(value);
+ parts.get(this)!.path = encodePathname(String(value));
}
get port(): string {
@@ -485,6 +485,38 @@ export class URLImpl implements URL {
}
}
+function parseIpv4Number(s: string): number {
+ if (s.match(/^(0[Xx])[0-9A-Za-z]+$/)) {
+ return Number(s);
+ }
+ if (s.match(/^[0-9]+$/)) {
+ return Number(s.startsWith("0") ? `0o${s}` : s);
+ }
+ return NaN;
+}
+
+function parseIpv4(s: string): string {
+ const parts = s.split(".");
+ if (parts[parts.length - 1] == "" && parts.length > 1) {
+ parts.pop();
+ }
+ if (parts.includes("") || parts.length > 4) {
+ return s;
+ }
+ const numbers = parts.map(parseIpv4Number);
+ if (numbers.includes(NaN)) {
+ return s;
+ }
+ const last = numbers.pop()!;
+ if (last >= 256 ** (4 - numbers.length) || numbers.find((n) => n >= 256)) {
+ throw new TypeError("Invalid hostname.");
+ }
+ const ipv4 = numbers.reduce((sum, n, i) => sum + n * 256 ** (3 - i), last);
+ const ipv4Hex = ipv4.toString(16).padStart(8, "0");
+ const ipv4HexParts = ipv4Hex.match(/(..)(..)(..)(..)$/)!.slice(1);
+ return ipv4HexParts.map((s) => String(Number(`0x${s}`))).join(".");
+}
+
function charInC0ControlSet(c: string): boolean {
return (c >= "\u0000" && c <= "\u001F") || c > "\u007E";
}
@@ -573,7 +605,10 @@ function encodeHostname(s: string, isSpecial = true): string {
}
}
- // TODO(nayeemrmn): IPv4 parsing.
+ // IPv4 parsing.
+ if (isSpecial) {
+ result = parseIpv4(result);
+ }
return result;
}
diff --git a/cli/tests/unit/url_test.ts b/cli/tests/unit/url_test.ts
index 3e7ac6214..37d92089b 100644
--- a/cli/tests/unit/url_test.ts
+++ b/cli/tests/unit/url_test.ts
@@ -27,23 +27,65 @@ unitTest(function urlParsing(): void {
);
});
-unitTest(function urlHostParsing(): void {
+unitTest(function urlAuthenticationParsing(): void {
+ const specialUrl = new URL("http://foo:bar@baz");
+ assertEquals(specialUrl.username, "foo");
+ assertEquals(specialUrl.password, "bar");
+ assertEquals(specialUrl.hostname, "baz");
+ assertThrows(() => new URL("file://foo:bar@baz"), TypeError, "Invalid URL.");
+ const nonSpecialUrl = new URL("abcd://foo:bar@baz");
+ assertEquals(nonSpecialUrl.username, "foo");
+ assertEquals(nonSpecialUrl.password, "bar");
+ assertEquals(nonSpecialUrl.hostname, "baz");
+});
+
+unitTest(function urlHostnameParsing(): void {
// IPv6.
- assertEquals(new URL("https://foo:bar@[::1]:8000").hostname, "[::1]");
+ assertEquals(new URL("http://[::1]").hostname, "[::1]");
+ assertEquals(new URL("file://[::1]").hostname, "[::1]");
+ assertEquals(new URL("abcd://[::1]").hostname, "[::1]");
// Forbidden host code point.
- assertThrows(() => new URL("https:// a"), TypeError, "Invalid URL.");
- assertThrows(() => new URL("abcde:// a"), TypeError, "Invalid URL.");
- assertThrows(() => new URL("https://%"), TypeError, "Invalid URL.");
- assertEquals(new URL("abcde://%").hostname, "%");
+ assertThrows(() => new URL("http:// a"), TypeError, "Invalid URL.");
+ assertThrows(() => new URL("file:// a"), TypeError, "Invalid URL.");
+ assertThrows(() => new URL("abcd:// a"), TypeError, "Invalid URL.");
+ assertThrows(() => new URL("http://%"), TypeError, "Invalid URL.");
+ assertThrows(() => new URL("file://%"), TypeError, "Invalid URL.");
+ assertEquals(new URL("abcd://%").hostname, "%");
// Percent-decode.
- assertEquals(new URL("https://%21").hostname, "!");
- assertEquals(new URL("abcde://%21").hostname, "%21");
+ assertEquals(new URL("http://%21").hostname, "!");
+ assertEquals(new URL("file://%21").hostname, "!");
+ assertEquals(new URL("abcd://%21").hostname, "%21");
+
+ // IPv4 parsing.
+ assertEquals(new URL("http://260").hostname, "0.0.1.4");
+ assertEquals(new URL("file://260").hostname, "0.0.1.4");
+ assertEquals(new URL("abcd://260").hostname, "260");
+ assertEquals(new URL("http://255.0.0.0").hostname, "255.0.0.0");
+ assertThrows(() => new URL("http://256.0.0.0"), TypeError, "Invalid URL.");
+ assertEquals(new URL("http://0.255.0.0").hostname, "0.255.0.0");
+ assertThrows(() => new URL("http://0.256.0.0"), TypeError, "Invalid URL.");
+ assertEquals(new URL("http://0.0.255.0").hostname, "0.0.255.0");
+ assertThrows(() => new URL("http://0.0.256.0"), TypeError, "Invalid URL.");
+ assertEquals(new URL("http://0.0.0.255").hostname, "0.0.0.255");
+ assertThrows(() => new URL("http://0.0.0.256"), TypeError, "Invalid URL.");
+ assertEquals(new URL("http://0.0.65535").hostname, "0.0.255.255");
+ assertThrows(() => new URL("http://0.0.65536"), TypeError, "Invalid URL.");
+ assertEquals(new URL("http://0.16777215").hostname, "0.255.255.255");
+ assertThrows(() => new URL("http://0.16777216"), TypeError, "Invalid URL.");
+ assertEquals(new URL("http://4294967295").hostname, "255.255.255.255");
+ assertThrows(() => new URL("http://4294967296"), TypeError, "Invalid URL.");
+});
- // TODO(nayeemrmn): IPv4 parsing.
- // assertEquals(new URL("https://260").hostname, "0.0.1.4");
- assertEquals(new URL("abcde://260").hostname, "260");
+unitTest(function urlPortParsing(): void {
+ const specialUrl = new URL("http://foo:8000");
+ assertEquals(specialUrl.hostname, "foo");
+ assertEquals(specialUrl.port, "8000");
+ assertThrows(() => new URL("file://foo:8000"), TypeError, "Invalid URL.");
+ const nonSpecialUrl = new URL("abcd://foo:8000");
+ assertEquals(nonSpecialUrl.hostname, "foo");
+ assertEquals(nonSpecialUrl.port, "8000");
});
unitTest(function urlModifications(): void {
@@ -200,12 +242,18 @@ unitTest(function urlUncHostname() {
});
unitTest(function urlHostnameUpperCase() {
- assertEquals(new URL("https://EXAMPLE.COM").href, "https://example.com/");
- assertEquals(new URL("abcde://EXAMPLE.COM").href, "abcde://EXAMPLE.COM/");
+ assertEquals(new URL("http://EXAMPLE.COM").href, "http://example.com/");
+ assertEquals(new URL("abcd://EXAMPLE.COM").href, "abcd://EXAMPLE.COM");
+});
+
+unitTest(function urlEmptyPath() {
+ assertEquals(new URL("http://foo").pathname, "/");
+ assertEquals(new URL("file://foo").pathname, "/");
+ assertEquals(new URL("abcd://foo").pathname, "");
});
unitTest(function urlTrim() {
- assertEquals(new URL(" https://example.com ").href, "https://example.com/");
+ assertEquals(new URL(" http://example.com ").href, "http://example.com/");
});
unitTest(function urlEncoding() {
diff --git a/std/http/server.ts b/std/http/server.ts
index 5908fcf9a..b1ffed96b 100644
--- a/std/http/server.ts
+++ b/std/http/server.ts
@@ -248,7 +248,8 @@ export type HTTPOptions = Omit<Deno.ListenOptions, "transport">;
export function _parseAddrFromStr(addr: string): HTTPOptions {
let url: URL;
try {
- url = new URL(`http://${addr}`);
+ const host = addr.startsWith(":") ? `0.0.0.0${addr}` : addr;
+ url = new URL(`http://${host}`);
} catch {
throw new TypeError("Invalid address.");
}
@@ -263,7 +264,7 @@ export function _parseAddrFromStr(addr: string): HTTPOptions {
}
return {
- hostname: url.hostname == "" ? "0.0.0.0" : url.hostname,
+ hostname: url.hostname,
port: url.port === "" ? 80 : Number(url.port),
};
}