synd_client/client/
mod.rs1use std::time::Duration;
2#[cfg(unix)]
3use std::{path::Path, path::PathBuf};
4
5use url::Url;
6
7use crate::SyndApiError;
8
9mod authentication;
10mod daemon;
11mod feed_events;
12mod graphql;
13mod session;
14mod tls;
15
16pub use authentication::ApiCredential;
17use authentication::ClientAuthentication;
18pub use feed_events::FeedEventWatch;
19use tls::ClientTls;
20
21#[derive(Debug, Clone)]
22pub struct ClientOptions {
23 timeout: Duration,
24 user_agent: String,
25 tls: ClientTls,
26}
27
28impl ClientOptions {
29 pub fn new(timeout: Duration, user_agent: impl Into<String>) -> Self {
30 Self {
31 timeout,
32 user_agent: user_agent.into(),
33 tls: ClientTls::default(),
34 }
35 }
36
37 pub fn with_root_certificate_pem(mut self, pem: &[u8]) -> Result<Self, SyndApiError> {
38 self.tls.add_root_certificate_pem(pem)?;
39 Ok(self)
40 }
41
42 #[must_use]
43 pub fn danger_accept_invalid_certs(mut self, accept: bool) -> Self {
44 self.tls.accept_invalid_certificates(accept);
45 self
46 }
47}
48
49#[derive(Clone)]
51pub struct Client {
52 #[expect(clippy::struct_field_names)]
53 client: reqwest::Client,
54 authentication: ClientAuthentication,
55 endpoint: Url,
56 feed_event_transport: FeedEventTransport,
57}
58
59#[derive(Clone)]
60enum FeedEventTransport {
61 Tcp {
62 connector: Option<tokio_tungstenite::Connector>,
63 },
64 #[cfg(unix)]
65 Unix { socket_path: PathBuf },
66}
67
68impl Client {
69 pub fn new(endpoint: Url, options: ClientOptions) -> Result<Self, SyndApiError> {
70 endpoint.join("/")?;
71 let connector = options.tls.websocket_connector()?;
72 let client = Self::builder(options)?
73 .build()
74 .map_err(SyndApiError::BuildRequest)?;
75
76 Ok(Self {
77 client,
78 endpoint,
79 authentication: ClientAuthentication::Required,
80 feed_event_transport: FeedEventTransport::Tcp { connector },
81 })
82 }
83
84 #[cfg(unix)]
85 pub fn new_unix(
86 socket_path: impl AsRef<Path>,
87 options: ClientOptions,
88 ) -> Result<Self, SyndApiError> {
89 let socket_path = socket_path.as_ref().to_path_buf();
90 let client = Self::builder(options)?
91 .unix_socket(socket_path.clone())
92 .build()
93 .map_err(SyndApiError::BuildRequest)?;
94
95 Ok(Self {
96 client,
97 endpoint: Url::parse("http://localhost")?,
98 authentication: ClientAuthentication::TransportTrusted,
99 feed_event_transport: FeedEventTransport::Unix { socket_path },
100 })
101 }
102
103 fn builder(options: ClientOptions) -> Result<reqwest::ClientBuilder, SyndApiError> {
104 let ClientOptions {
105 timeout,
106 user_agent,
107 tls,
108 } = options;
109 let builder = reqwest::ClientBuilder::new()
110 .user_agent(user_agent)
111 .timeout(timeout)
112 .connect_timeout(Duration::from_secs(10));
113 tls.configure_http(builder)
114 }
115
116 pub fn set_credential(&mut self, credential: ApiCredential) -> Result<(), SyndApiError> {
117 self.authentication.configure(credential)
118 }
119
120 pub fn set_local_token(&mut self, token: &str) -> Result<(), SyndApiError> {
121 self.set_credential(ApiCredential::LocalBearer {
122 token: token.to_owned(),
123 })
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use core::assert_matches;
130 use std::time::Duration;
131
132 use reqwest::header;
133
134 use super::{ApiCredential, Client, ClientOptions};
135 use crate::SyndApiError;
136
137 #[test]
138 fn tcp_client_requires_credential_before_authorized_request() {
139 let client =
140 Client::new(url::Url::parse("http://127.0.0.1:8080").unwrap(), options()).unwrap();
141 let mut headers = header::HeaderMap::new();
142
143 let error = client
144 .authentication
145 .apply_authorization_header(&mut headers)
146 .unwrap_err();
147
148 assert_matches!(error, SyndApiError::MissingCredential);
149 assert!(!headers.contains_key(header::AUTHORIZATION));
150 }
151
152 #[test]
153 fn configured_credential_is_written_to_authorization_header() {
154 let mut client =
155 Client::new(url::Url::parse("http://127.0.0.1:8080").unwrap(), options()).unwrap();
156 client
157 .set_credential(ApiCredential::LocalBearer {
158 token: "secret".to_owned(),
159 })
160 .unwrap();
161 let mut headers = header::HeaderMap::new();
162
163 client
164 .authentication
165 .apply_authorization_header(&mut headers)
166 .unwrap();
167
168 assert_eq!(headers.get(header::AUTHORIZATION).unwrap(), "Bearer secret");
169 }
170
171 #[cfg(unix)]
172 #[test]
173 fn unix_client_uses_transport_trust_without_authorization_header() {
174 let client = Client::new_unix("/tmp/synd-client-test.sock", options()).unwrap();
175 let mut headers = header::HeaderMap::new();
176
177 client
178 .authentication
179 .apply_authorization_header(&mut headers)
180 .unwrap();
181
182 assert!(!headers.contains_key(header::AUTHORIZATION));
183 }
184
185 fn options() -> ClientOptions {
186 ClientOptions::new(Duration::from_secs(1), "synd-client-test")
187 }
188}