| Safe Haskell | None |
|---|---|
| Language | GHC2021 |
Ecluse.Core.Security.Authority
Description
Textual extraction of the host[:port] authority an outbound request dials.
Pragmatic, comparison-oriented extractors over a URI or bare host[:port] value:
hostAddress recovers the bare host, hostPortAddress recovers the host together
with its effective port as a HostPort (443 when none is written), and
splitHostPort is the bracket-aware host[:port] split both build on (also shared
with the SQS endpoint parser). These are not a full RFC 3986 parser: a value
with no recognisable authority yields the empty string or Nothing, which every
guard treats as not-allowed. The SSRF policy gates in Ecluse.Core.Security.Host
consume HostPort; the parsing here carries no policy of its own.
The dialled authority
The authority an outbound fetch actually dials: a bare host together with its effective port.
Registry egress is https-only (Ecluse.Core.Security.Egress), so a URL that writes
no port dials 443; hostPortAddress bakes that default in, and an explicit :443
is therefore the same authority as no port at all. Carrying the port beside the
host is what lets the egress gate authorise the pair the dial targets rather than
the host alone: a dist.tarball naming an allowlisted host on an attacker-chosen
port must not inherit that host's authorisation.
Constructors
| HostPort | |
Fields
| |
Authority extraction
hostAddress :: Text -> Text Source #
Extract the bare host from a URI or host[:port] authority.
A convenience for the checks that classify the host alone: isBlockedTarget
tests the bare literal (an address is internal regardless of port), and the
same-host http-upgrade decision in Ecluse.Core.Security.Egress compares bare
hosts. This strips a scheme:// prefix, any userinfo@, any :port suffix,
and any /path/?query/#fragment tail, lower-casing the result. It is a
pragmatic extractor for comparison, not a full RFC 3986 parser; a value with
no recognisable host yields the empty string, which the guards treat as
not-allowed. IPv6 literals in brackets ([::1]:443) are returned without the
brackets -- the bracket-aware host[:port] split is splitHostPort, shared with
the SQS endpoint parser so the two cannot drift on an authority edge case; a
malformed authority (an opening bracket with no close) yields the empty string,
the same fail-safe the guards apply to it. The authorisation clauses compare the
host with its effective port instead: extract those with hostPortAddress.
hostPortAddress :: Text -> Maybe HostPort Source #
Extract the host and the effective port a URI or host[:port] authority
dials, or Nothing when no dialable authority can be recovered.
The authorisation-comparison companion to hostAddress: the same pragmatic
scheme/userinfo/path stripping, but the :port suffix is __parsed rather than
discarded__, so the egress gate compares the pair the fetch dials. A missing port
defaults to 443 (registry egress is https-only), and an explicit :443 therefore
yields the same HostPort as no port at all. The port is strict: a canonical
run of decimal digits, no leading zero, whose value fits 1..65535. Anything else
yields Nothing, which every authorisation clause treats as refused:
- a non-numeric, signed, out-of-range, or leading-zero port (
parsePort); - a written-but-empty port (
host:or[::1]:): http-client refuses any URL that writes a colon with no port digits, so the gate refuses it too rather than authorise an authority that can never be dialled. The two spellings are treated identically here even thoughsplitHostPortcollapses the unbracketedhost:into an empty remainder (recognised by the authority's trailing colon) while it carries the bracketed[::1]:through as a":"remainder; - junk after a bracketed IPv6 literal, or an unbracketed IPv6 literal (whose colons leave no unambiguous host/port split, so it is refused whole rather than mangled into a truncated host).
>>>hostPortAddress "https://registry.npmjs.org/thing/-/thing-1.0.0.tgz"Just (HostPort {hpHost = "registry.npmjs.org", hpPort = 443})
>>>hostPortAddress "https://registry.npmjs.org:9443/thing"Just (HostPort {hpHost = "registry.npmjs.org", hpPort = 9443})
>>>hostPortAddress "https://[2606:4700::1111]:8443/thing"Just (HostPort {hpHost = "2606:4700::1111", hpPort = 8443})
splitHostPort :: Text -> Maybe (Text, Text) Source #
Split a host[:port] authority into its bare host and the raw ":port"
remainder (empty when no port is present), bracket-aware so an IPv6 literal's
inner colons are never mistaken for the port separator.
The single canonical authority split feeding both the data-plane host extractor
(hostAddress) and the SQS endpoint parser (parseEndpointUrl),
so the two re-implementations the [::1]:port edge cases tripped on cannot drift
again. A […] IPv6 literal is split on its closing bracket -- the host is returned
without the brackets and the remainder is whatever follows (a ":port" or empty) --
so an inner :: is never read as the port separator; a bare authority is split on
its first . An opening bracket with no close is a malformed authority and
yields :Nothing, which hostAddress folds to the empty (not-allowed) host and the
endpoint parser surfaces as a malformed-URL boot error.