mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Parse IPv4 hosts in special-scheme URLs like WHATWG URL
The URL host parser kept a host whose last label is a number as a
domain, so new URL("http://127.1").hostname was "127.1" where Node
answers 127.0.0.1, and hosts such as 1.2.3.256, 0x100000000 and
1.2.3.4.5 parsed where Node throws Invalid URL.
A special-scheme host (file: included) that ends in a number now goes
through the WHATWG IPv4 parser: up to four labels after one trailing
empty label, each a decimal, 0x hexadecimal or leading-zero octal
number, every label but the last at most 255 and the last below
256^(5 - labels). The address serializes as dotted decimal, and any
failure makes the URL invalid. The constructor, URL.parse, URL.canParse,
the host and hostname setters and domainToASCII/domainToUnicode share
the parser. Non-special schemes keep their opaque host as written.
Corpus 5860 covers each form.
This commit is contained in:
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5860-url-ipv4-hosts.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -322,8 +322,80 @@ static ScrStr *parse_rooted_path(const char *raw, size_t len, bool special) {
|
||||
return parse_rooted_path_mode(raw, len, special, false);
|
||||
}
|
||||
|
||||
/* The WHATWG IPv4 number parser: "0x"/"0X" makes the rest hexadecimal
|
||||
* and a leading "0" octal; an empty rest is 0. Values past 2^32 saturate
|
||||
* there, since every caller rejects them. */
|
||||
static bool url_ipv4_number(const char *s, size_t n, uint64_t *out) {
|
||||
if (n == 0) return false;
|
||||
unsigned radix = 10;
|
||||
if (n >= 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) {
|
||||
radix = 16;
|
||||
s += 2;
|
||||
n -= 2;
|
||||
} else if (n >= 2 && s[0] == '0') {
|
||||
radix = 8;
|
||||
s += 1;
|
||||
n -= 1;
|
||||
}
|
||||
uint64_t v = 0;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
unsigned char c = (unsigned char)s[i];
|
||||
unsigned d = c >= '0' && c <= '9' ? (unsigned)(c - '0')
|
||||
: c >= 'a' && c <= 'f' ? (unsigned)(c - 'a' + 10)
|
||||
: c >= 'A' && c <= 'F' ? (unsigned)(c - 'A' + 10)
|
||||
: 16;
|
||||
if (d >= radix) return false;
|
||||
v = v > 0xFFFFFFFFull ? v : v * radix + d;
|
||||
}
|
||||
*out = v;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* The WHATWG "ends in a number" checker: the last dot-separated label
|
||||
* (one trailing empty label dropped) is all digits or an IPv4 number. */
|
||||
static bool url_ends_in_number(const char *s, size_t n) {
|
||||
if (n > 0 && s[n - 1] == '.') n--;
|
||||
size_t start = n;
|
||||
while (start > 0 && s[start - 1] != '.') start--;
|
||||
const char *last = s + start;
|
||||
size_t len = n - start;
|
||||
if (len == 0) return false;
|
||||
bool digits = true;
|
||||
for (size_t i = 0; i < len && digits; i++) digits = last[i] >= '0' && last[i] <= '9';
|
||||
uint64_t ignored;
|
||||
return digits || url_ipv4_number(last, len, &ignored);
|
||||
}
|
||||
|
||||
/* The WHATWG IPv4 parser: up to four labels (one trailing empty label
|
||||
* dropped); every label but the last at most 255, and the last below
|
||||
* 256^(5 - labels). Writes the dotted-decimal serialization. */
|
||||
static bool url_ipv4_parse(const char *s, size_t n, UrlBuf *out) {
|
||||
if (n > 0 && s[n - 1] == '.') n--;
|
||||
uint64_t parts[4];
|
||||
size_t count = 0, start = 0;
|
||||
for (size_t i = 0; i <= n; i++) {
|
||||
if (i < n && s[i] != '.') continue;
|
||||
if (count == 4 || !url_ipv4_number(s + start, i - start, &parts[count])) return false;
|
||||
count++;
|
||||
start = i + 1;
|
||||
}
|
||||
for (size_t i = 0; i + 1 < count; i++)
|
||||
if (parts[i] > 255) return false;
|
||||
uint64_t limit = 1ull << (8 * (5 - count));
|
||||
if (parts[count - 1] >= limit) return false;
|
||||
uint64_t v = parts[count - 1];
|
||||
for (size_t i = 0; i + 1 < count; i++) v += parts[i] << (8 * (3 - i));
|
||||
char text[16];
|
||||
int len = snprintf(text, sizeof text, "%u.%u.%u.%u", (unsigned)(v >> 24) & 255, (unsigned)(v >> 16) & 255,
|
||||
(unsigned)(v >> 8) & 255, (unsigned)v & 255);
|
||||
ub_append(out, text, (size_t)len);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Shared constructor/setter host parsing keeps the existing ASCII-host
|
||||
* boundary and canonical IPv6 serialization in both execution tiers. */
|
||||
* boundary and canonical IPv6 serialization in both execution tiers. A
|
||||
* special host that ends in a number is an IPv4 address: it serializes
|
||||
* as dotted decimal, or the URL is invalid. */
|
||||
static bool parse_host(const char *hp, size_t host_len, bool special, bool is_file,
|
||||
ScrStr **host) {
|
||||
bool ipv6 = host_len > 0 && hp[0] == '[';
|
||||
@@ -353,6 +425,17 @@ static bool parse_host(const char *hp, size_t host_len, bool special, bool is_fi
|
||||
ub_push(&hb, (char)c);
|
||||
}
|
||||
}
|
||||
if (!ipv6 && special && url_ends_in_number(hb.data, hb.len)) {
|
||||
UrlBuf ip;
|
||||
ub_init(&ip);
|
||||
bool ok = url_ipv4_parse(hb.data, hb.len, &ip);
|
||||
free(hb.data);
|
||||
if (!ok) {
|
||||
free(ip.data);
|
||||
return false;
|
||||
}
|
||||
hb = ip;
|
||||
}
|
||||
/* file: "localhost" normalizes to "" at parse time (Node). */
|
||||
if (is_file && hb.len == 9 && memcmp(hb.data, "localhost", 9) == 0) hb.len = 0;
|
||||
if (hb.len == 0 && special && !is_file) {
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// WHATWG IPv4 host parsing in special-scheme URLs: a host whose last label
|
||||
// is a number is an IPv4 address in dotted, short, hexadecimal or octal
|
||||
// form and serializes as dotted decimal; out-of-range parts and more than
|
||||
// four labels make the URL invalid. Hosts that do not end in a number
|
||||
// stay domains, and non-special schemes keep their host as written.
|
||||
import * as nodeUrl from "node:url";
|
||||
|
||||
const hosts = [
|
||||
"1.2.3.4",
|
||||
"127.1",
|
||||
"127.0.1",
|
||||
"0x7f.0.0.1",
|
||||
"0X7F.1",
|
||||
"0177.0.0.1",
|
||||
"00177.0.0.1",
|
||||
"2130706433",
|
||||
"0x7f000001",
|
||||
"017700000001",
|
||||
"0",
|
||||
"0x",
|
||||
"1.2.3.4.",
|
||||
"1.0x.0.",
|
||||
"1.2.3.256",
|
||||
"1.256.3",
|
||||
"1.2.65536",
|
||||
"4294967295",
|
||||
"4294967296",
|
||||
"0x100000000",
|
||||
"1.2.3.4.5",
|
||||
"1.2.3.09",
|
||||
"1.2.3.0xg",
|
||||
"foo.0x",
|
||||
"foo.1",
|
||||
"1.foo",
|
||||
"example.com",
|
||||
"0x1.example",
|
||||
"999999999999999999999",
|
||||
];
|
||||
for (const host of hosts) {
|
||||
const special = URL.parse(`http://${host}:8080/p`);
|
||||
const file = URL.parse(`file://${host}/p`);
|
||||
const opaque = URL.parse(`git://${host}/p`);
|
||||
console.log(
|
||||
JSON.stringify(host),
|
||||
special ? special.host : "invalid",
|
||||
file ? JSON.stringify(file.hostname) : "invalid",
|
||||
opaque ? opaque.hostname : "invalid",
|
||||
);
|
||||
}
|
||||
try {
|
||||
new URL("https://1.2.3.256/");
|
||||
} catch (err) {
|
||||
console.log((err as Error).name, (err as NodeJS.ErrnoException).code, (err as Error).message);
|
||||
}
|
||||
const url = new URL("http://example.com/");
|
||||
url.hostname = "0x7f.1";
|
||||
console.log(url.href);
|
||||
url.host = "1.2.3.999:81";
|
||||
console.log(url.href);
|
||||
url.host = "10.1:82";
|
||||
console.log(url.href);
|
||||
console.log(new URL("ws://192.168.1/").href, new URL("https://0300.0250.1.1/").origin);
|
||||
// The same host parser behind domainToASCII, domainToUnicode and URL.canParse.
|
||||
console.log(nodeUrl.domainToASCII("127.1"), nodeUrl.domainToASCII("0x7f.1"), JSON.stringify(nodeUrl.domainToASCII("1.2.3.256")));
|
||||
console.log(nodeUrl.domainToUnicode("127.1"), JSON.stringify(nodeUrl.domainToUnicode("1.2.3.256")));
|
||||
console.log(URL.canParse("http://127.1"), URL.canParse("http://1.2.3.256"), URL.canParse("git://1.2.3.256"));
|
||||
console.log(nodeUrl.format(new URL("http://0x7f.1/a")));
|
||||
Reference in New Issue
Block a user