cookie_agent_server/
websocket_url.rs1use std::net::IpAddr;
2
3use thiserror::Error;
4use url::{Host, Url};
5
6#[derive(Debug, Error)]
8pub enum WebSocketUrlError {
9 #[error("parse daemon WebSocket URL: {0}")]
11 Parse(#[source] url::ParseError),
12 #[error("daemon WebSocket URL scheme must be ws or wss")]
14 InvalidScheme,
15 #[error("daemon WebSocket URL must not contain credentials")]
17 Credentials,
18 #[error("daemon WebSocket URL requires a host")]
20 MissingHost,
21 #[error("daemon WebSocket URL host must be loopback")]
23 NonLoopbackHost,
24 #[error("daemon WebSocket URL path must be exactly /ws without query or fragment")]
26 InvalidEndpoint,
27}
28
29pub 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}