bitwarden_core/client/
builder.rs1use std::sync::{Arc, OnceLock, RwLock};
2
3use bitwarden_crypto::KeyStore;
4use bitwarden_state::registry::StateRegistry;
5use reqwest::header::{self, HeaderValue};
6
7#[cfg(feature = "internal")]
8use crate::client::flags::Flags;
9use crate::{
10 auth::auth_tokens::{NoopTokenHandler, TokenHandler},
11 client::{
12 client::Client,
13 client_settings::{ClientName, ClientSettings},
14 internal::{ApiConfigurations, InternalClient},
15 },
16};
17
18pub struct ClientBuilder {
20 settings: Option<ClientSettings>,
21 token_handler: Arc<dyn TokenHandler>,
22 state_registry: Option<StateRegistry>,
23 middleware: Vec<Arc<dyn reqwest_middleware::Middleware>>,
24}
25
26impl ClientBuilder {
27 pub fn new() -> Self {
29 Self {
30 settings: None,
31 token_handler: Arc::new(NoopTokenHandler),
32 state_registry: None,
33 middleware: Vec::new(),
34 }
35 }
36
37 pub fn with_settings(mut self, settings: ClientSettings) -> Self {
39 self.settings = Some(settings);
40 self
41 }
42
43 pub fn with_token_handler(mut self, token_handler: Arc<dyn TokenHandler>) -> Self {
45 self.token_handler = token_handler;
46 self
47 }
48
49 pub fn with_middleware(
51 mut self,
52 middleware: Vec<Arc<dyn reqwest_middleware::Middleware>>,
53 ) -> Self {
54 self.middleware = middleware;
55 self
56 }
57
58 pub fn with_state(mut self, state_registry: StateRegistry) -> Self {
61 self.state_registry = Some(state_registry);
62 self
63 }
64
65 pub fn build(self) -> Client {
67 let settings = self.settings.unwrap_or_default();
68
69 let external_http_client = new_http_client_builder()
70 .build()
71 .expect("External HTTP Client build should not fail");
72
73 let headers = build_default_headers(&settings);
74
75 let login_method = Arc::new(RwLock::new(None));
76 let key_store = KeyStore::default();
77
78 let identity_http_client = new_http_client_builder()
80 .default_headers(headers.clone())
81 .build()
82 .expect("Bw HTTP Client build should not fail");
83 let identity = bitwarden_api_identity::Configuration {
84 base_path: settings.identity_url,
85 client: identity_http_client.into(),
86 };
87
88 let auth_middleware = self.token_handler.initialize_middleware(
90 login_method.clone(),
91 identity.clone(),
92 key_store.clone(),
93 );
94
95 #[cfg(not(target_arch = "wasm32"))]
100 let api_http_client = if self.middleware.is_empty() {
101 new_http_client_builder()
102 .default_headers(headers)
103 .build()
104 .expect("Bw HTTP Client build should not fail")
105 } else {
106 new_http_client_builder()
107 .default_headers(headers)
108 .redirect(reqwest::redirect::Policy::none())
109 .build()
110 .expect("Bw HTTP Client (no redirect) build should not fail")
111 };
112
113 #[cfg(target_arch = "wasm32")]
114 let api_http_client = new_http_client_builder()
115 .default_headers(headers)
116 .build()
117 .expect("Bw HTTP Client build should not fail");
118
119 let mut middleware_builder = reqwest_middleware::ClientBuilder::new(api_http_client);
121 for mw in self.middleware {
122 middleware_builder = middleware_builder.with_arc(mw);
123 }
124 let bw_http_client = middleware_builder.with_arc(auth_middleware).build();
125 let api = bitwarden_api_api::Configuration {
126 base_path: settings.api_url,
127 client: bw_http_client,
128 };
129
130 Client {
131 internal: Arc::new(InternalClient {
132 user_id: OnceLock::new(),
133 token_handler: self.token_handler,
134 login_method,
135 #[cfg(feature = "internal")]
136 flags: RwLock::new(Flags::default()),
137 api_configurations: ApiConfigurations::new(identity, api, settings.device_type),
138 external_http_client,
139 key_store,
140 #[cfg(feature = "internal")]
141 security_state: RwLock::new(None),
142 state_registry: self
143 .state_registry
144 .unwrap_or_else(StateRegistry::new_with_memory_db),
145 }),
146 }
147 }
148}
149
150impl Default for ClientBuilder {
151 fn default() -> Self {
152 Self::new()
153 }
154}
155
156fn new_http_client_builder() -> reqwest::ClientBuilder {
157 #[allow(unused_mut)]
158 let mut client_builder = reqwest::Client::builder();
159
160 #[cfg(not(target_arch = "wasm32"))]
161 {
162 use rustls::ClientConfig;
163 use rustls_platform_verifier::ConfigVerifierExt;
164 client_builder = client_builder.use_preconfigured_tls(
165 ClientConfig::with_platform_verifier().expect("Failed to create platform verifier"),
166 );
167
168 #[cfg(not(debug_assertions))]
170 {
171 client_builder = client_builder.https_only(true);
172 }
173 }
174
175 client_builder
176}
177
178fn build_default_headers(settings: &ClientSettings) -> header::HeaderMap {
180 let mut headers = header::HeaderMap::new();
181
182 if let Some(device_identifier) = &settings.device_identifier {
185 headers.append(
186 "Device-Identifier",
187 HeaderValue::from_str(device_identifier)
188 .expect("Device identifier should be a valid header value"),
189 );
190 }
191
192 if let Some(client_type) = Into::<Option<ClientName>>::into(settings.device_type) {
193 headers.append(
194 "Bitwarden-Client-Name",
195 HeaderValue::from_str(&client_type.to_string())
196 .expect("All ASCII strings are valid header values"),
197 );
198 }
199
200 if let Some(version) = &settings.bitwarden_client_version {
201 headers.append(
202 "Bitwarden-Client-Version",
203 HeaderValue::from_str(version).expect("Version should be a valid header value"),
204 );
205 }
206
207 if let Some(package_type) = &settings.bitwarden_package_type {
208 headers.append(
209 "Bitwarden-Package-Type",
210 HeaderValue::from_str(package_type)
211 .expect("Package type should be a valid header value"),
212 );
213 }
214
215 headers.append(
218 "Device-Type",
219 HeaderValue::from_str(&(settings.device_type as u8).to_string())
220 .expect("All numbers are valid ASCII"),
221 );
222
223 headers.append(
224 reqwest::header::USER_AGENT,
225 HeaderValue::from_str(&settings.user_agent)
226 .expect("User agent should be a valid header value"),
227 );
228
229 headers
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn test_client_builder_default_builds() {
238 let _client = ClientBuilder::new().build();
239 }
240
241 #[test]
242 fn test_client_builder_with_settings_builds() {
243 let settings = ClientSettings::default();
244 let _client = ClientBuilder::new().with_settings(settings).build();
245 }
246
247 #[test]
248 fn test_client_builder_with_token_handler_builds() {
249 let handler: Arc<dyn TokenHandler> = Arc::new(NoopTokenHandler);
250 let _client = ClientBuilder::new().with_token_handler(handler).build();
251 }
252
253 #[test]
254 fn test_client_builder_chain_order_independence() {
255 let _a = ClientBuilder::new()
256 .with_settings(ClientSettings::default())
257 .with_token_handler(Arc::new(NoopTokenHandler) as Arc<dyn TokenHandler>)
258 .build();
259 let _b = ClientBuilder::new()
260 .with_token_handler(Arc::new(NoopTokenHandler) as Arc<dyn TokenHandler>)
261 .with_settings(ClientSettings::default())
262 .build();
263 }
264
265 #[test]
266 fn test_client_builder_with_state_builds() {
267 use bitwarden_state::registry::StateRegistry;
268 let registry = StateRegistry::new_with_memory_db();
269 let _client = ClientBuilder::new().with_state(registry).build();
270 }
271
272 #[test]
273 fn test_client_builder_with_state_in_chain() {
274 use bitwarden_state::registry::StateRegistry;
275 let registry = StateRegistry::new_with_memory_db();
276 let _client = ClientBuilder::new()
277 .with_settings(ClientSettings::default())
278 .with_state(registry)
279 .build();
280 }
281
282 #[test]
283 fn test_client_builder_with_middleware_compiles() {
284 struct StubMiddleware;
285
286 #[async_trait::async_trait]
287 impl reqwest_middleware::Middleware for StubMiddleware {
288 async fn handle(
289 &self,
290 req: reqwest::Request,
291 extensions: &mut http::Extensions,
292 next: reqwest_middleware::Next<'_>,
293 ) -> reqwest_middleware::Result<reqwest::Response> {
294 next.run(req, extensions).await
295 }
296 }
297
298 let arc_middleware: Arc<dyn reqwest_middleware::Middleware> = Arc::new(StubMiddleware);
299 let _client = ClientBuilder::new()
300 .with_middleware(vec![arc_middleware])
301 .build();
302 }
303}