diff --git a/cli/js/web/url.ts b/cli/js/web/url.ts index e429fddbb9..544168d801 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 3e7ac62140..37d92089bf 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"); - // TODO(nayeemrmn): IPv4 parsing. - // assertEquals(new URL("https://260").hostname, "0.0.1.4"); - assertEquals(new URL("abcde://260").hostname, "260"); + // 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."); +}); + +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 5908fcf9a9..b1ffed96b4 100644 --- a/std/http/server.ts +++ b/std/http/server.ts @@ -248,7 +248,8 @@ export type HTTPOptions = Omit; 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), }; }