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) profile: Option<String>,
39 pub(crate) http: reqwest::Client,
40 pub(crate) hostless: bool,
41 pub(crate) inline_host: Option<crate::sdk::inline::InlineHost>,
42 cdp: Mutex<Option<Arc<Connection>>>,
43 profile_info: Mutex<Option<Arc<crate::sdk::profile::info::ProfileInfo>>>,
44}
45
46impl Client {
47 pub fn connect(endpoint: &str) -> Result<Self, Error> {
50 ensure_rustls_provider();
51 let endpoint = Endpoint::parse(endpoint)?;
52 let mut http_builder = reqwest::Client::builder()
53 .user_agent(concat!("afhttp/", env!("CARGO_PKG_VERSION")))
54 .no_proxy();
57 #[cfg(unix)]
58 if let Endpoint::Unix { path } = &endpoint {
59 http_builder = http_builder.unix_socket(path.clone());
60 }
61 let http = http_builder.build().map_err(|e| {
62 Error::new(
63 ErrorCode::InternalError,
64 format!("reqwest client build failed: {e}"),
65 )
66 })?;
67 Ok(Self {
68 inner: Arc::new(ClientInner {
69 endpoint,
70 token: None,
71 profile: None,
72 http,
73 hostless: false,
74 inline_host: None,
75 cdp: Mutex::new(None),
76 profile_info: Mutex::new(None),
77 }),
78 })
79 }
80
81 pub fn http_only() -> Result<Self, Error> {
85 let mut client = Self::connect("ws://127.0.0.1:0")?;
86 if let Some(inner) = Arc::get_mut(&mut client.inner) {
87 inner.hostless = true;
88 }
89 Ok(client)
90 }
91
92 #[must_use]
95 pub fn with_token(mut self, token: impl Into<String>) -> Self {
96 let token = token.into();
97 if let Some(inner) = Arc::get_mut(&mut self.inner) {
98 inner.token = Some(token);
99 inner.cdp = Mutex::new(None);
100 inner.profile_info = Mutex::new(None);
101 } else {
102 let new = ClientInner {
104 endpoint: self.inner.endpoint.clone(),
105 token: Some(token),
106 profile: self.inner.profile.clone(),
107 http: self.inner.http.clone(),
108 hostless: self.inner.hostless,
109 inline_host: self.inner.inline_host.clone(),
110 cdp: Mutex::new(None),
111 profile_info: Mutex::new(None),
112 };
113 self.inner = Arc::new(new);
114 }
115 self
116 }
117
118 #[must_use]
121 pub fn with_profile(mut self, profile: impl Into<String>) -> Self {
122 let profile = profile.into();
123 if let Some(inner) = Arc::get_mut(&mut self.inner) {
124 inner.profile = Some(profile);
125 inner.cdp = Mutex::new(None);
126 } else {
127 let new = ClientInner {
128 endpoint: self.inner.endpoint.clone(),
129 token: self.inner.token.clone(),
130 profile: Some(profile),
131 http: self.inner.http.clone(),
132 hostless: self.inner.hostless,
133 inline_host: self.inner.inline_host.clone(),
134 cdp: Mutex::new(None),
135 profile_info: Mutex::new(None),
136 };
137 self.inner = Arc::new(new);
138 }
139 self
140 }
141
142 #[must_use]
144 pub fn profile(&self) -> Option<&str> {
145 self.inner.profile.as_deref()
146 }
147
148 #[must_use]
150 pub fn endpoint(&self) -> &Endpoint {
151 &self.inner.endpoint
152 }
153
154 #[must_use]
156 pub fn token(&self) -> Option<&str> {
157 self.inner.token.as_deref()
158 }
159
160 pub(crate) fn http(&self) -> &reqwest::Client {
161 &self.inner.http
162 }
163
164 pub(crate) fn is_hostless(&self) -> bool {
165 self.inner.hostless
166 }
167
168 pub(crate) fn has_inline_host(&self) -> bool {
169 self.inner.inline_host.is_some()
170 }
171
172 pub(crate) async fn inline_host_started(&self) -> bool {
173 match &self.inner.inline_host {
174 Some(inline) => inline.is_started().await,
175 None => false,
176 }
177 }
178
179 #[cfg(feature = "host")]
180 pub(crate) fn with_inline_host(mut self, inline_host: crate::sdk::inline::InlineHost) -> Self {
181 if let Some(inner) = Arc::get_mut(&mut self.inner) {
182 inner.inline_host = Some(inline_host);
183 inner.hostless = false;
184 inner.cdp = Mutex::new(None);
185 inner.profile_info = Mutex::new(None);
186 } else {
187 let new = ClientInner {
188 endpoint: self.inner.endpoint.clone(),
189 token: self.inner.token.clone(),
190 profile: self.inner.profile.clone(),
191 http: self.inner.http.clone(),
192 hostless: false,
193 inline_host: Some(inline_host),
194 cdp: Mutex::new(None),
195 profile_info: Mutex::new(None),
196 };
197 self.inner = Arc::new(new);
198 }
199 self
200 }
201
202 pub(crate) async fn effective_endpoint(&self) -> Result<Endpoint, Error> {
203 if self.inner.hostless {
204 return Err(Error::new(
205 ErrorCode::RenderUnavailable,
206 "this client has no afhttp host endpoint",
207 ));
208 }
209 if let Some(inline) = &self.inner.inline_host {
210 inline.endpoint().await
211 } else {
212 Ok(self.inner.endpoint.clone())
213 }
214 }
215
216 pub(crate) async fn cdp_connection(&self) -> Result<Arc<Connection>, Error> {
218 let mut guard = self.inner.cdp.lock().await;
219 if let Some(conn) = guard.as_ref() {
220 return Ok(conn.clone());
221 }
222 let endpoint = self.effective_endpoint().await?;
223 let conn =
224 Arc::new(Connection::connect_endpoint(&endpoint, self.token(), self.profile()).await?);
225 *guard = Some(conn.clone());
226 Ok(conn)
227 }
228
229 pub async fn close(&self) {
232 if let Some(conn) = self.inner.cdp.lock().await.take() {
233 conn.close();
234 }
235 }
236
237 #[must_use]
240 pub fn fetch(&self, url: impl Into<String>) -> crate::sdk::fetch::FetchBuilder {
241 crate::sdk::fetch::FetchBuilder::new(self.clone(), url.into())
242 }
243
244 pub async fn profile_info(&self) -> Result<Arc<crate::sdk::profile::info::ProfileInfo>, Error> {
251 let mut guard = self.inner.profile_info.lock().await;
252 if let Some(info) = guard.as_ref() {
253 return Ok(info.clone());
254 }
255 if self.inner.hostless {
256 return Err(Error::new(
257 ErrorCode::HostUnreachable,
258 "GET /profile: no afhttp host endpoint configured",
259 ));
260 }
261 let endpoint = self.effective_endpoint().await?;
262 let base = endpoint.http_base();
263 let url = profile_url(&base)?;
264 let mut req = self.http().get(&url);
265 if let Some(token) = self.token() {
266 req = req.bearer_auth(token);
267 }
268 let resp = req
269 .send()
270 .await
271 .map_err(|e| Error::new(ErrorCode::HostUnreachable, format!("GET {url}: {e}")))?;
272 let status = resp.status();
273 if !status.is_success() {
274 return Err(Error::new(
275 ErrorCode::HostUnreachable,
276 format!("GET {url}: status {status}"),
277 ));
278 }
279 let bytes = resp.bytes().await.map_err(|e| {
280 Error::new(
281 ErrorCode::InternalError,
282 format!("profile_info: read response: {e}"),
283 )
284 })?;
285 let info: crate::sdk::profile::info::ProfileInfo =
286 crate::shared::afdata::decode_result(&bytes)?;
287 let arc = Arc::new(info);
288 *guard = Some(arc.clone());
289 Ok(arc)
290 }
291}
292
293fn profile_url(base: &str) -> Result<String, Error> {
294 let url = url::Url::parse(&format!("{base}/profile")).map_err(|e| {
295 Error::new(
296 ErrorCode::InvalidEndpoint,
297 format!("profile URL from endpoint {base:?}: {e}"),
298 )
299 })?;
300 Ok(url.to_string())
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 #[test]
308 fn connect_rejects_bad_endpoint() {
309 let err = Client::connect("ftp://nope").err();
310 assert!(err.is_some());
311 }
312
313 #[test]
314 fn connect_accepts_ws() {
315 let c = Client::connect("ws://localhost:9222").unwrap();
316 assert!(matches!(c.endpoint(), Endpoint::Ws { .. }));
317 assert!(c.token().is_none());
318 }
319
320 #[test]
321 fn with_token_attaches() {
322 let c = Client::connect("ws://localhost:9222")
323 .unwrap()
324 .with_token("secret");
325 assert_eq!(c.token(), Some("secret"));
326 }
327}