mirror of
https://github.com/denoland/deno.git
synced 2025-01-18 03:44:05 -05:00
fix(cli/js/web/url): Implement IPv4 hostname parsing (#6707)
This commit is contained in:
parent
4aeac64ecd
commit
63edeb1c36
3 changed files with 125 additions and 41 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue