Documentation
¶
Overview ¶
Package urlform classifies the structural form of a raw, untrusted URL string — specifically the forms where a BROWSER's reading (the WHATWG URL parser) diverges from net/url's: whitespace-smuggled URLs, backslash authorities, slash-count fixups after a special scheme, protocol-relative and schemeless-host forms, hidden-host parses, userinfo spoofing.
The parser of record is the divide that places this package. Validating a URL the PROCESS will fetch is a different concern (there net/url and the dialer are authoritative; use an SSRF guard). urlform models classify-for-PUBLISH: the string is destined for a human whose browser will read it, and the quirk classes exist precisely where that reading and Go's disagree.
The covered divergence set is bounded and enumerated — urlform is a classifier over net/url with the WHATWG readings layered on, not a conformant WHATWG parser. What it models, pinned by the conformance fixtures in testdata/whatwg-fixtures.json (WPT-derived plus hand-derived address-bar rows):
- Input preprocessing: leading/trailing C0-control-or-space trimming (widened to all Unicode whitespace, a documented fail-safe superset) and embedded ASCII tab/newline removal, recorded by HasTabOrNewline — so "https://anime\tbytes.tv/x" classifies with its real host.
- Backslash-as-slash for special schemes (http, https, ws, wss, ftp, file) and schemeless forms, ahead of the query/fragment; a non-special scheme's backslashes stay ordinary characters.
- Slash-count fixups: after an authority-carrying special scheme the browser reads an authority through ANY run of slashes, so "https:/host/x" and "https:host/x" expose their hidden host evidence (ClassHiddenHost with recovered facts).
- Address-bar forms outside the URL spec: "host/x" navigates to host (ClassSchemelessHost), "//host/x" resolves against the ambient scheme (ClassProtocolRelative).
Deliberately NOT modeled (the boundary of the contract): IDNA/UTS46 host mapping and punycode (non-ASCII host evidence survives raw for the fail-closed gates — see IsASCIIHost), percent-encoding normalization, port range checking (the fact is reported, the publisher validates), full host validation (net/url's acceptance stands in for it), interior non-tab C0 controls (net/url rejects them; where they sit in a host the browser rejects them too), and the file scheme's drive-letter quirks. Future WHATWG changes land here only when enumerated — the fixtures name the supported set, and drift against them fails the build.
Classify never errors: every input lands in exactly one Class with the extractable semantic facts (Host, Scheme, Port, HasUserInfo, HasBackslash, HasTabOrNewline) alongside. The classification is deliberately judgment-free; each consumer applies its own fail direction over the same facts — a publisher drops what it cannot vouch for, an evidence gate hides what it cannot classify.
Host evidence folds ASCII-only (a full-Unicode fold would launder homograph bytes such as U+0130 or U+212A into ASCII), and IsASCIIHost is the fail-closed companion gate for consumers matching hosts against known ASCII domains.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func IsASCIIHost ¶
IsASCIIHost reports whether every byte of host is ASCII (below utf8.RuneSelf). It is the fail-closed companion of Form.Host's ASCII-only fold: a consumer matching host evidence against known ASCII domains gates on it first, so a homograph host (Cyrillic lookalikes, a fold-laundering U+0130 or U+212A) never string-matches a canonical domain. Callers that must ACCEPT international hosts convert punycode explicitly instead of relaxing this predicate.
Types ¶
type Class ¶
type Class int
Class names the structural form of a raw, untrusted URL string - specifically the browser-vs-net/url parse quirks that decide whether the string really carries a host. It is the single home of that quirk vocabulary; see Form.
const ( // ClassEmpty is a string that is empty after the input preprocessing // (edge trimming plus tab/newline removal; see Classify). ClassEmpty Class = iota // ClassMalformed is a string the canonicalized parse rejected; no // structural facts (and no host evidence) can be extracted from it. ClassMalformed // ClassAbsolute is a scheme-and-host absolute URL ("https://host/x"); // Host carries the parsed hostname. ClassAbsolute // ClassHiddenHost is a scheme-bearing parse with no hostname, where // net/url sees no host but a browser may navigate to one. For the // authority-carrying special schemes (http, https, ws, wss, ftp) the // WHATWG parser skips ANY run of slashes after the scheme - zero // ("https:host/x"), one ("https:/host/x"), or many - and reads the // authority, so the classifier runs the same authority reparse it uses // for schemeless forms and recovers the browser's reading into // Host/Port/HasUserInfo (HostUnrecoverable marks a failed recovery; a // port-only authority such as "https://:443/x" recovers no host, which // matches the browser - the WHATWG parser fails on an empty special // host). For every other scheme ("host:443/x" parsing the host as an // opaque scheme, "javascript:alert(1)", "mailto:x") the browser reads an // opaque path with no authority, so the facts stay empty - there the // host evidence, if any, is genuinely hidden. ClassHiddenHost // ClassProtocolRelative is a network-path reference: "//host/x" (Host // carries the parsed host a browser would resolve against the ambient // scheme) or a leading-"//" form with no extractable host evidence - a // three-or-more-slash form ("///x": Go parses a rooted path while // browsers read an authority) or an empty-authority form ("//", "//?q"). // Host is the discriminator between the two sub-forms: consumers that // need host evidence treat an empty Host here as ambiguous and fail // closed. ClassProtocolRelative // ClassSchemelessHost is a scheme-free, non-rooted form ("host/x"): // net/url parses a bare path, but a browser address bar navigates to the // first segment as a host. Host carries that authority-reparse evidence // (empty for a query- or fragment-only form such as "?x:y"); // HostUnrecoverable marks a failed reparse. ClassSchemelessHost // ClassRelative is a rooted, host-free relative path ("/x"). ClassRelative )
type Form ¶
type Form struct {
// Trimmed is the preprocessed raw string the classification read: edges
// trimmed and embedded ASCII tab/newline removed (the WHATWG input
// preprocessing, recorded by HasTabOrNewline), with backslashes NOT
// canonicalized. It is what a publisher emits or prefixes - already free
// of the whitespace-smuggling bytes a browser would silently drop, and
// never otherwise rewritten.
Trimmed string
// Host is the lowercased host evidence a browser would navigate to, when
// extractable: the parsed hostname of an absolute or protocol-relative
// form, or the authority reparse of a schemeless-host or recoverable
// hidden-host form. Empty when the string carries none (or the form
// hides it; see Class). The fold is ASCII-only by design (see
// asciiLower): a full-Unicode fold would launder homograph bytes
// (U+0130 -> 'i', U+212A -> 'k') into ASCII, so non-ASCII host evidence
// survives here unfolded for a consumer's fail-closed ASCII-only host
// gates.
Host string
// Scheme is the canonicalized parse's scheme, which url.Parse folds to
// lowercase (an "HTTPS://" source reads "https", RFC 3986 canonical
// form), so the value is already case-folded; empty when the string
// carries none or did not parse. Case-insensitive comparison by consumers
// remains correct as defense in depth.
Scheme string
// Port is the canonicalized parse's port string; empty when none is
// present or the string did not parse. net/url only accepts an
// all-digit port, but it does not range-check it - consumers that need
// a valid 16-bit port (a link publisher) validate the range. (The WHATWG
// parser rejects an out-of-range port outright; reporting the fact and
// leaving the fail direction to the consumer is this package's model.)
Port string
// Class is the structural form.
Class Class
// HasBackslash records a '\' anywhere in the trimmed string. Browsers
// (WHATWG URL parser) treat '\' as '/' for the special schemes (http,
// https, ws, wss, ftp, file) and for schemeless forms (where the address
// bar's ambient scheme is special), so for those the parsed facts
// (Scheme/Host/Port/Class) describe the canonicalized reading - a
// `/\host/x` form classifies protocol-relative, not as a host-less
// rooted path. For a non-special scheme a backslash is an ordinary
// character and is NOT canonicalized (a browser reads an opaque path).
// Either way the flag lets a publisher that must emit the raw string
// reject it outright.
HasBackslash bool
// HasTabOrNewline records that the edge-trimmed string contained
// embedded ASCII tab or newline (U+0009, U+000A, U+000D), which the
// WHATWG parser - and therefore this classification - removes wherever
// they appear ("https://anime\tbytes.tv/x" navigates to animebytes.tv).
// Trimmed already has them removed, so emitting Trimmed is safe; the
// flag records the smuggling attempt for publishers that treat the
// ORIGINAL string as emittable or want to reject de-smuggled input
// outright.
HasTabOrNewline bool
// HostUnrecoverable marks a ClassSchemelessHost or recoverable
// ClassHiddenHost whose authority reparse failed (e.g. a space before an
// "@"): browser-visible host evidence may exist but cannot be extracted,
// so evidence-driven consumers treat the form like a parse failure.
HostUnrecoverable bool
// HasUserInfo records a userinfo authority component ("user@host") in
// the canonicalized parse - a visual-spoofing vector
// ("https://trusted@evil/") a link publisher typically rejects. For a
// ClassSchemelessHost or recovered ClassHiddenHost the fact comes from
// the same authority reparse that supplies Host (so "user@host/x"
// reports it alongside the recovered host). Always false when the
// string did not parse.
HasUserInfo bool
// contains filtered or unexported fields
}
Form is the structural classification of one raw, untrusted URL string (an upstream API field, a scraped link, operator input). It names the browser-vs-net/url parse-quirk classes ONCE - backslash authorities, protocol-relative and schemeless-host forms, hidden-host parses - so every consumer branches on the same facts while keeping its own fail direction as policy: a publisher drops what it cannot vouch for (publish-or-drop), while an evidence gate hides what it cannot classify (extract-evidence-or-hide). Fields are ordered for govet fieldalignment.
func Classify ¶
Classify classifies a raw URL string into its structural Form. It never errors: every input lands in exactly one class, and unparseable input is ClassMalformed. Consumers apply their own policy over the returned facts (see Form).
Classification starts with the WHATWG basic parser's input preprocessing, so string-level whitespace smuggling cannot hide a URL from the facts: leading/trailing C0 controls and whitespace are trimmed (trimEdges), and embedded ASCII tab/newline are removed everywhere (recorded by HasTabOrNewline). This is the same hardening CPython adopted for urllib.parse (CVE-2022-0391).