agent_first_http/sdk/
endpoint.rs1use std::str::FromStr;
13
14use crate::shared::error::{Error, ErrorCode};
15
16#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum Endpoint {
20 Ws {
21 host: String,
22 port: u16,
23 secure: bool,
24 },
25 Http {
26 host: String,
27 port: u16,
28 secure: bool,
29 },
30 #[cfg(unix)]
31 Unix { path: std::path::PathBuf },
32}
33
34impl Endpoint {
35 pub fn parse(input: &str) -> Result<Self, Error> {
36 if let Some(rest) = input.strip_prefix("ws://") {
37 let (host, port) = split_host_port(rest, 80)?;
38 Ok(Self::Ws {
39 host,
40 port,
41 secure: false,
42 })
43 } else if let Some(rest) = input.strip_prefix("wss://") {
44 let (host, port) = split_host_port(rest, 443)?;
45 Ok(Self::Ws {
46 host,
47 port,
48 secure: true,
49 })
50 } else if let Some(rest) = input.strip_prefix("http://") {
51 let (host, port) = split_host_port(rest, 80)?;
52 Ok(Self::Http {
53 host,
54 port,
55 secure: false,
56 })
57 } else if let Some(rest) = input.strip_prefix("https://") {
58 let (host, port) = split_host_port(rest, 443)?;
59 Ok(Self::Http {
60 host,
61 port,
62 secure: true,
63 })
64 } else if let Some(path) = input.strip_prefix("unix:") {
65 #[cfg(unix)]
66 {
67 Ok(Self::Unix {
68 path: std::path::PathBuf::from(path),
69 })
70 }
71 #[cfg(not(unix))]
72 {
73 let _ = path;
74 Err(Error::new(
75 ErrorCode::InvalidEndpoint,
76 "unix: endpoints are not supported on this platform; use tcp:127.0.0.1:<port>",
77 ))
78 }
79 } else {
80 Err(Error::new(
81 ErrorCode::InvalidEndpoint,
82 format!(
83 "endpoint must start with ws://, wss://, http://, https://, or unix:; got {input:?}"
84 ),
85 ))
86 }
87 }
88
89 #[must_use]
93 pub fn http_base(&self) -> String {
94 match self {
95 Self::Http { host, port, secure } | Self::Ws { host, port, secure } => {
96 let scheme = if *secure { "https" } else { "http" };
97 format!("{scheme}://{host}:{port}")
98 }
99 #[cfg(unix)]
100 Self::Unix { .. } => "http://unix-socket".to_string(),
101 }
102 }
103
104 #[must_use]
107 pub fn cdp_ws_url(&self) -> String {
108 match self {
109 Self::Ws { host, port, secure } | Self::Http { host, port, secure } => {
110 let scheme = if *secure { "wss" } else { "ws" };
111 format!("{scheme}://{host}:{port}/cdp")
112 }
113 #[cfg(unix)]
114 Self::Unix { .. } => "ws://unix-socket/cdp".to_string(),
115 }
116 }
117}
118
119impl FromStr for Endpoint {
120 type Err = Error;
121 fn from_str(s: &str) -> Result<Self, Self::Err> {
122 Self::parse(s)
123 }
124}
125
126fn split_host_port(rest: &str, default_port: u16) -> Result<(String, u16), Error> {
127 let rest = rest.split('/').next().unwrap_or(rest);
128 if rest.is_empty() {
129 return Err(Error::new(
130 ErrorCode::InvalidEndpoint,
131 "endpoint missing host",
132 ));
133 }
134 if let Some((host, port)) = rest.rsplit_once(':') {
135 let port: u16 = port.parse().map_err(|_| {
136 Error::new(
137 ErrorCode::InvalidEndpoint,
138 format!("endpoint port not a u16: {port:?}"),
139 )
140 })?;
141 Ok((host.to_string(), port))
142 } else {
143 Ok((rest.to_string(), default_port))
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn parses_ws_with_explicit_port() {
153 let e = Endpoint::parse("ws://127.0.0.1:9222").unwrap();
154 assert_eq!(
155 e,
156 Endpoint::Ws {
157 host: "127.0.0.1".into(),
158 port: 9222,
159 secure: false
160 }
161 );
162 }
163
164 #[test]
165 fn parses_wss_with_default_port() {
166 let e = Endpoint::parse("wss://host.example").unwrap();
167 assert_eq!(
168 e,
169 Endpoint::Ws {
170 host: "host.example".into(),
171 port: 443,
172 secure: true
173 }
174 );
175 }
176
177 #[test]
178 fn parses_http_endpoints() {
179 let e = Endpoint::parse("http://localhost:8080").unwrap();
180 assert!(matches!(e, Endpoint::Http { port: 8080, .. }));
181 }
182
183 #[cfg(unix)]
184 #[test]
185 fn parses_unix_endpoint() {
186 let e = Endpoint::parse("unix:/run/afhttp/work.sock").unwrap();
187 assert!(matches!(e, Endpoint::Unix { .. }));
188 }
189
190 #[test]
191 fn http_base_strips_path() {
192 let e = Endpoint::parse("ws://example:9222").unwrap();
193 assert_eq!(e.http_base(), "http://example:9222");
194 }
195
196 #[test]
197 fn cdp_ws_url_uses_secure_scheme_when_endpoint_is_secure() {
198 let e = Endpoint::parse("https://example:443").unwrap();
199 assert_eq!(e.cdp_ws_url(), "wss://example:443/cdp");
200 }
201
202 #[test]
203 fn rejects_unknown_scheme() {
204 let err = Endpoint::parse("ftp://example").err();
205 assert_eq!(err.map(|e| e.error_code), Some(ErrorCode::InvalidEndpoint),);
206 }
207
208 #[test]
209 fn rejects_bad_port() {
210 let err = Endpoint::parse("ws://host:notnum").err();
211 assert_eq!(err.map(|e| e.error_code), Some(ErrorCode::InvalidEndpoint),);
212 }
213}