agent_first_http/sdk/
client.rs1use std::sync::Arc;
9use std::sync::OnceLock;
10
11use tokio::sync::Mutex;
12
13use crate::sdk::cdp::ws_client::Connection;
14use crate::sdk::endpoint::Endpoint;
15use crate::shared::error::{Error, ErrorCode};
16
17fn ensure_rustls_provider() {
21 static ONCE: OnceLock<()> = OnceLock::new();
22 ONCE.get_or_init(|| {
23 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
24 });
25}
26
27#[derive(Clone)]
29pub struct Client {
30 inner: Arc<ClientInner>,
31}
32
33pub(crate) struct ClientInner {
34 pub(crate) endpoint: Endpoint,
35 pub(crate) token: Option<String>,
36 pub(crate) http: reqwest::Client,
37 pub(crate) hostless: bool,
38 pub(crate) inline_host: Option<crate::sdk::inline::InlineHost>,
39 cdp: Mutex<Option<Arc<Connection>>>,
40 profile_info: Mutex<Option<Arc<crate::sdk::profile::info::ProfileInfo>>>,
41}
42
43impl Client {
44 pub fn connect(endpoint: &str) -> Result<Self, Error> {
47 ensure_rustls_provider();
48 let endpoint = Endpoint::parse(endpoint)?;
49 let mut http_builder = reqwest::Client::builder()
50 .user_agent(concat!("afhttp/", env!("CARGO_PKG_VERSION")))
51 .no_proxy();
54 #[cfg(unix)]
55 if let Endpoint::Unix { path } = &endpoint {
56 http_builder = http_builder.unix_socket(path.clone());
57 }
58 let http = http_builder.build().map_err(|e| {
59 Error::new(
60 ErrorCode::InternalError,
61 format!("reqwest client build failed: {e}"),
62 )
63 })?;
64 Ok(Self {
65 inner: Arc::new(ClientInner {
66 endpoint,
67 token: None,
68 http,
69 hostless: false,
70 inline_host: None,
71 cdp: Mutex::new(None),
72 profile_info: Mutex::new(None),
73 }),
74 })
75 }
76
77 pub fn http_only() -> Result<Self, Error> {
81 let mut client = Self::connect("ws://127.0.0.1:0")?;
82 if let Some(inner) = Arc::get_mut(&mut client.inner) {
83 inner.hostless = true;
84 }
85 Ok(client)
86 }
87
88 #[must_use]
91 pub fn with_token(mut self, token: impl Into<String>) -> Self {
92 let token = token.into();
93 if let Some(inner) = Arc::get_mut(&mut self.inner) {
94 inner.token = Some(token);
95 inner.cdp = Mutex::new(None);
96 inner.profile_info = Mutex::new(None);
97 } else {
98 let new = ClientInner {
100 endpoint: self.inner.endpoint.clone(),
101 token: Some(token),
102 http: self.inner.http.clone(),
103 hostless: self.inner.hostless,
104 inline_host: self.inner.inline_host.clone(),
105 cdp: Mutex::new(None),
106 profile_info: Mutex::new(None),
107 };
108 self.inner = Arc::new(new);
109 }
110 self
111 }
112
113 #[must_use]
115 pub fn endpoint(&self) -> &Endpoint {
116 &self.inner.endpoint
117 }
118
119 #[must_use]
121 pub fn token(&self) -> Option<&str> {
122 self.inner.token.as_deref()
123 }
124
125 pub(crate) fn http(&self) -> &reqwest::Client {
126 &self.inner.http
127 }
128
129 pub(crate) fn is_hostless(&self) -> bool {
130 self.inner.hostless
131 }
132
133 pub(crate) fn has_inline_host(&self) -> bool {
134 self.inner.inline_host.is_some()
135 }
136
137 pub(crate) async fn inline_host_started(&self) -> bool {
138 match &self.inner.inline_host {
139 Some(inline) => inline.is_started().await,
140 None => false,
141 }
142 }
143
144 #[cfg(feature = "host")]
145 pub(crate) fn with_inline_host(mut self, inline_host: crate::sdk::inline::InlineHost) -> Self {
146 if let Some(inner) = Arc::get_mut(&mut self.inner) {
147 inner.inline_host = Some(inline_host);
148 inner.hostless = false;
149 inner.cdp = Mutex::new(None);
150 inner.profile_info = Mutex::new(None);
151 } else {
152 let new = ClientInner {
153 endpoint: self.inner.endpoint.clone(),
154 token: self.inner.token.clone(),
155 http: self.inner.http.clone(),
156 hostless: false,
157 inline_host: Some(inline_host),
158 cdp: Mutex::new(None),
159 profile_info: Mutex::new(None),
160 };
161 self.inner = Arc::new(new);
162 }
163 self
164 }
165
166 pub(crate) async fn effective_endpoint(&self) -> Result<Endpoint, Error> {
167 if self.inner.hostless {
168 return Err(Error::new(
169 ErrorCode::RenderUnavailable,
170 "this client has no afhttp host endpoint",
171 ));
172 }
173 if let Some(inline) = &self.inner.inline_host {
174 inline.endpoint().await
175 } else {
176 Ok(self.inner.endpoint.clone())
177 }
178 }
179
180 pub(crate) async fn cdp_connection(&self) -> Result<Arc<Connection>, Error> {
182 let mut guard = self.inner.cdp.lock().await;
183 if let Some(conn) = guard.as_ref() {
184 return Ok(conn.clone());
185 }
186 let endpoint = self.effective_endpoint().await?;
187 let conn = Arc::new(Connection::connect_endpoint(&endpoint, self.token()).await?);
188 *guard = Some(conn.clone());
189 Ok(conn)
190 }
191
192 pub async fn close(&self) {
195 if let Some(conn) = self.inner.cdp.lock().await.take() {
196 conn.close();
197 }
198 }
199
200 #[must_use]
203 pub fn fetch(&self, url: impl Into<String>) -> crate::sdk::fetch::FetchBuilder {
204 crate::sdk::fetch::FetchBuilder::new(self.clone(), url.into())
205 }
206
207 pub async fn profile_info(&self) -> Result<Arc<crate::sdk::profile::info::ProfileInfo>, Error> {
214 let mut guard = self.inner.profile_info.lock().await;
215 if let Some(info) = guard.as_ref() {
216 return Ok(info.clone());
217 }
218 if self.inner.hostless {
219 return Err(Error::new(
220 ErrorCode::HostUnreachable,
221 "GET /profile: no afhttp host endpoint configured",
222 ));
223 }
224 let endpoint = self.effective_endpoint().await?;
225 let base = endpoint.http_base();
226 let url = profile_url(&base)?;
227 let mut req = self.http().get(&url);
228 if let Some(token) = self.token() {
229 req = req.bearer_auth(token);
230 }
231 let resp = req
232 .send()
233 .await
234 .map_err(|e| Error::new(ErrorCode::HostUnreachable, format!("GET {url}: {e}")))?;
235 let status = resp.status();
236 if !status.is_success() {
237 return Err(Error::new(
238 ErrorCode::HostUnreachable,
239 format!("GET {url}: status {status}"),
240 ));
241 }
242 let info: crate::sdk::profile::info::ProfileInfo = resp.json().await.map_err(|e| {
243 Error::new(
244 ErrorCode::InternalError,
245 format!("profile_info: decode response: {e}"),
246 )
247 })?;
248 let arc = Arc::new(info);
249 *guard = Some(arc.clone());
250 Ok(arc)
251 }
252}
253
254fn profile_url(base: &str) -> Result<String, Error> {
255 let url = url::Url::parse(&format!("{base}/profile")).map_err(|e| {
256 Error::new(
257 ErrorCode::InvalidEndpoint,
258 format!("profile URL from endpoint {base:?}: {e}"),
259 )
260 })?;
261 Ok(url.to_string())
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn connect_rejects_bad_endpoint() {
270 let err = Client::connect("ftp://nope").err();
271 assert!(err.is_some());
272 }
273
274 #[test]
275 fn connect_accepts_ws() {
276 let c = Client::connect("ws://localhost:9222").unwrap();
277 assert!(matches!(c.endpoint(), Endpoint::Ws { .. }));
278 assert!(c.token().is_none());
279 }
280
281 #[test]
282 fn with_token_attaches() {
283 let c = Client::connect("ws://localhost:9222")
284 .unwrap()
285 .with_token("secret");
286 assert_eq!(c.token(), Some("secret"));
287 }
288}