Skip to main content

cookie_agent_server/
websocket_url.rs

1use std::net::IpAddr;
2
3use thiserror::Error;
4use url::{Host, Url};
5
6/// Errors returned when a daemon WebSocket URL is not a safe attach endpoint.
7#[derive(Debug, Error)]
8pub enum WebSocketUrlError {
9    /// The URL could not be parsed.
10    #[error("parse daemon WebSocket URL: {0}")]
11    Parse(#[source] url::ParseError),
12    /// The URL does not use WebSocket transport.
13    #[error("daemon WebSocket URL scheme must be ws or wss")]
14    InvalidScheme,
15    /// The URL embeds credentials.
16    #[error("daemon WebSocket URL must not contain credentials")]
17    Credentials,
18    /// The URL has no host.
19    #[error("daemon WebSocket URL requires a host")]
20    MissingHost,
21    /// The URL host is not loopback.
22    #[error("daemon WebSocket URL host must be loopback")]
23    NonLoopbackHost,
24    /// The URL does not target the exact daemon endpoint.
25    #[error("daemon WebSocket URL path must be exactly /ws without query or fragment")]
26    InvalidEndpoint,
27}
28
29/// Validates that a URL targets the daemon's exact loopback WebSocket endpoint.
30pub fn validate_websocket_url(value: &str) -> Result<(), WebSocketUrlError> {
31    let url = Url::parse(value).map_err(WebSocketUrlError::Parse)?;
32    if !matches!(url.scheme(), "ws" | "wss") {
33        return Err(WebSocketUrlError::InvalidScheme);
34    }
35    if !url.username().is_empty() || url.password().is_some() {
36        return Err(WebSocketUrlError::Credentials);
37    }
38    let loopback = match url.host().ok_or(WebSocketUrlError::MissingHost)? {
39        Host::Domain(host) => host.eq_ignore_ascii_case("localhost"),
40        Host::Ipv4(address) => IpAddr::V4(address).is_loopback(),
41        Host::Ipv6(address) => IpAddr::V6(address).is_loopback(),
42    };
43    if !loopback {
44        return Err(WebSocketUrlError::NonLoopbackHost);
45    }
46    if url.path() != "/ws" || url.query().is_some() || url.fragment().is_some() {
47        return Err(WebSocketUrlError::InvalidEndpoint);
48    }
49    Ok(())
50}