///| Pure-logic Origin header policy for DNS rebinding prevention
/// (spec basic/transports/streamable-http#security-endpoint: servers must
/// validate Origin on all incoming connections). All-target like
/// jsonrpc_logic.mbt: the policy itself has no I/O, so js (and wasm-gc) keep
/// and test it even though the server transport is gated.
///|
/// Strip a leading `http://` or `https://` scheme. Any other shape — missing
/// scheme, other schemes such as `file:`/`ftp:`, or the bare `null` some
/// sandboxed browsers send — is malformed and fails closed (`None`).
fn strip_origin_scheme(origin : String) -> String? {
if origin is [.. "https://", .. rest] {
Some(rest.to_owned())
} else if origin is [.. "http://", .. rest] {
Some(rest.to_owned())
} else {
None
}
}
///|
/// Extract the host of an Origin value shaped `scheme://host[:port]`,
/// keeping the brackets on IPv6 literals (`[::1]`). Fail-closed: `None` on a
/// path/query/fragment suffix (Origin carries only an authority), an
/// unterminated IPv6 literal, an empty host, or a missing/non-numeric port.
fn origin_host(origin : String) -> String? {
let authority = match strip_origin_scheme(origin) {
Some(a) => a
None => return None
}
let host = StringBuilder()
let mut in_brackets = false
let mut in_port = false
let mut port_digits = 0
for c in authority {
if in_port {
match c {
'0'..='9' => port_digits += 1
_ => return None
}
} else {
match c {
'/' | '?' | '#' => return None
'[' =>
if in_brackets {
return None
} else {
in_brackets = true
host.write_char('[')
}
']' =>
if in_brackets {
in_brackets = false
host.write_char(']')
} else {
return None
}
':' =>
if in_brackets {
// Colon inside an IPv6 literal is part of the address.
host.write_char(':')
} else {
in_port = true
}
_ => host.write_char(c)
}
}
}
if in_brackets || (in_port && port_digits == 0) || host.to_string() is "" {
None
} else {
Some(host.to_string())
}
}
///|
/// Decide whether a request `Origin` value may reach the MCP endpoint.
///
/// - `allowed_origins = Some(list)` (auth configured with an allowlist): the
/// value must match one entry exactly; the allowlist replaces, not extends,
/// the default policy.
/// - `allowed_origins = None` (no auth, or auth without an allowlist): the
/// default policy admits only loopback origins — `http`/`https` with host
/// `127.0.0.1`, `localhost`, or `[::1]` on any port — so a page served
/// from a rebound public DNS name cannot pass. Malformed values and every
/// other host fail closed.
///
/// Requests carrying no Origin header are non-browser clients; the caller
/// allows them without consulting this function.
pub fn origin_allowed(
origin : String,
allowed_origins : Array[String]?,
) -> Bool {
match allowed_origins {
Some(list) => list.exists(fn(o) { o == origin })
None =>
match origin_host(origin) {
Some(host) => {
let h = host.to_lower()
h == "127.0.0.1" || h == "localhost" || h == "[::1]"
}
None => false
}
}
}