bitwarden_core/client/
builder.rs1use std::sync::{Arc, OnceLock, RwLock};
2
3use bitwarden_api_base::new_http_client_builder;
4use bitwarden_crypto::{CipherSuite, KeyStore};
5use bitwarden_managed_settings_types::ManagementProfile;
6use bitwarden_state::registry::StateRegistry;
7use reqwest::header::{self, HeaderValue};
8
9#[cfg(feature = "internal")]
10use crate::key_management::state_bridge::StateBridge;
11use crate::{
12 auth::auth_tokens::{NoopTokenHandler, TokenHandler},
13 client::{
14 client::Client,
15 client_settings::{ClientName, ClientSettings, HostPlatformInfo},
16 internal::{ApiConfigurations, InternalClient},
17 },
18};
19
20pub struct ClientBuilder {
22 settings: Option<ClientSettings>,
23 token_handler: Arc<dyn TokenHandler>,
24 state_registry: Option<StateRegistry>,
25 middleware: Vec<Arc<dyn reqwest_middleware::Middleware>>,
26 managed_profile: Option<Arc<RwLock<Option<ManagementProfile>>>>,
27 #[cfg(feature = "test-fixtures")]
28 api_configurations: Option<Arc<ApiConfigurations>>,
29}
30
31impl ClientBuilder {
32 pub fn new() -> Self {
34 Self {
35 settings: None,
36 token_handler: Arc::new(NoopTokenHandler),
37 state_registry: None,
38 middleware: Vec::new(),
39 managed_profile: None,
40 #[cfg(feature = "test-fixtures")]
41 api_configurations: None,
42 }
43 }
44
45 #[cfg(feature = "test-fixtures")]
49 pub fn with_api_configurations(mut self, api_configurations: Arc<ApiConfigurations>) -> Self {
50 self.api_configurations = Some(api_configurations);
51 self
52 }
53
54 pub fn with_settings(mut self, settings: ClientSettings) -> Self {
56 self.settings = Some(settings);
57 self
58 }
59
60 pub fn with_token_handler(mut self, token_handler: Arc<dyn TokenHandler>) -> Self {
62 self.token_handler = token_handler;
63 self
64 }
65
66 pub fn with_middleware(
68 mut self,
69 middleware: Vec<Arc<dyn reqwest_middleware::Middleware>>,
70 ) -> Self {
71 self.middleware = middleware;
72 self
73 }
74
75 pub fn with_state(mut self, state_registry: StateRegistry) -> Self {
78 self.state_registry = Some(state_registry);
79 self
80 }
81
82 pub fn with_managed_profile(mut self, cell: Arc<RwLock<Option<ManagementProfile>>>) -> Self {
88 self.managed_profile = Some(cell);
89 self
90 }
91
92 pub fn build(self) -> Client {
94 let settings = self.settings.unwrap_or_default();
95
96 let external_http_client = new_http_client_builder()
97 .build()
98 .expect("External HTTP Client build should not fail");
99
100 let headers = build_default_headers(&HostPlatformInfo::from(&settings));
101
102 let key_store = KeyStore::default();
103 let state_registry = self
104 .state_registry
105 .unwrap_or_else(StateRegistry::new_with_memory_db);
106
107 let identity_http_client = new_http_client_builder()
109 .default_headers(headers.clone())
110 .build()
111 .expect("Bw HTTP Client build should not fail");
112 let identity = bitwarden_api_identity::Configuration {
113 base_path: settings.identity_url,
114 client: identity_http_client.into(),
115 };
116
117 let auth_middleware = self.token_handler.initialize_middleware(
119 &state_registry,
120 identity.clone(),
121 key_store.clone(),
122 );
123
124 #[cfg(not(target_arch = "wasm32"))]
129 let api_http_client = if self.middleware.is_empty() {
130 new_http_client_builder()
131 .default_headers(headers)
132 .build()
133 .expect("Bw HTTP Client build should not fail")
134 } else {
135 new_http_client_builder()
136 .default_headers(headers)
137 .redirect(reqwest::redirect::Policy::none())
138 .build()
139 .expect("Bw HTTP Client (no redirect) build should not fail")
140 };
141
142 #[cfg(target_arch = "wasm32")]
143 let api_http_client = new_http_client_builder()
144 .default_headers(headers)
145 .build()
146 .expect("Bw HTTP Client build should not fail");
147
148 let mut middleware_builder = reqwest_middleware::ClientBuilder::new(api_http_client);
150 for mw in self.middleware {
151 middleware_builder = middleware_builder.with_arc(mw);
152 }
153 let bw_http_client = middleware_builder.with_arc(auth_middleware).build();
154 let api = bitwarden_api_api::Configuration {
155 base_path: settings.api_url,
156 client: bw_http_client,
157 };
158
159 #[cfg(feature = "test-fixtures")]
160 let api_configurations = self
161 .api_configurations
162 .unwrap_or_else(|| ApiConfigurations::new(identity, api, settings.device_type));
163 #[cfg(not(feature = "test-fixtures"))]
164 let api_configurations = ApiConfigurations::new(identity, api, settings.device_type);
165
166 let managed_profile = self
167 .managed_profile
168 .unwrap_or_else(|| Arc::new(RwLock::new(None)));
169
170 let client = Client {
171 internal: Arc::new(InternalClient {
172 user_id: OnceLock::new(),
173 token_handler: self.token_handler,
174 api_configurations,
175 external_http_client,
176 key_store,
177 #[cfg(feature = "internal")]
178 security_state: RwLock::new(None),
179 #[cfg(feature = "internal")]
180 state_bridge: StateBridge::new(),
181 state_registry,
182 managed_profile,
183 }),
184 };
185
186 client
189 .internal
190 .get_key_store()
191 .set_cipher_suite(CipherSuite::from_gov_mode(client.gov_mode()));
192
193 client
194 }
195}
196
197impl Default for ClientBuilder {
198 fn default() -> Self {
199 Self::new()
200 }
201}
202
203pub(crate) fn build_default_headers(info: &HostPlatformInfo) -> header::HeaderMap {
205 let mut headers = header::HeaderMap::new();
206
207 if let Some(device_identifier) = &info.device_identifier {
210 headers.append(
211 "Device-Identifier",
212 HeaderValue::from_str(device_identifier)
213 .expect("Device identifier should be a valid header value"),
214 );
215 }
216
217 if let Some(client_type) = Into::<Option<ClientName>>::into(info.device_type) {
218 headers.append(
219 "Bitwarden-Client-Name",
220 HeaderValue::from_str(&client_type.to_string())
221 .expect("All ASCII strings are valid header values"),
222 );
223 }
224
225 if let Some(version) = &info.bitwarden_client_version {
226 headers.append(
227 "Bitwarden-Client-Version",
228 HeaderValue::from_str(version).expect("Version should be a valid header value"),
229 );
230 }
231
232 if let Some(package_type) = &info.bitwarden_package_type {
233 headers.append(
234 "Bitwarden-Package-Type",
235 HeaderValue::from_str(package_type)
236 .expect("Package type should be a valid header value"),
237 );
238 }
239
240 headers.append(
243 "Device-Type",
244 HeaderValue::from_str(&(info.device_type as u8).to_string())
245 .expect("All numbers are valid ASCII"),
246 );
247
248 headers.append(
249 reqwest::header::USER_AGENT,
250 HeaderValue::from_str(&info.user_agent).expect("User agent should be a valid header value"),
251 );
252
253 headers
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn test_client_builder_default_builds() {
262 let _client = ClientBuilder::new().build();
263 }
264
265 #[test]
266 fn test_client_builder_with_settings_builds() {
267 let settings = ClientSettings::default();
268 let _client = ClientBuilder::new().with_settings(settings).build();
269 }
270
271 #[test]
272 fn test_client_builder_with_token_handler_builds() {
273 let handler: Arc<dyn TokenHandler> = Arc::new(NoopTokenHandler);
274 let _client = ClientBuilder::new().with_token_handler(handler).build();
275 }
276
277 #[test]
278 fn test_client_builder_chain_order_independence() {
279 let _a = ClientBuilder::new()
280 .with_settings(ClientSettings::default())
281 .with_token_handler(Arc::new(NoopTokenHandler) as Arc<dyn TokenHandler>)
282 .build();
283 let _b = ClientBuilder::new()
284 .with_token_handler(Arc::new(NoopTokenHandler) as Arc<dyn TokenHandler>)
285 .with_settings(ClientSettings::default())
286 .build();
287 }
288
289 #[test]
290 fn test_client_builder_with_state_builds() {
291 use bitwarden_state::registry::StateRegistry;
292 let registry = StateRegistry::new_with_memory_db();
293 let _client = ClientBuilder::new().with_state(registry).build();
294 }
295
296 #[test]
297 fn test_client_builder_with_state_in_chain() {
298 use bitwarden_state::registry::StateRegistry;
299 let registry = StateRegistry::new_with_memory_db();
300 let _client = ClientBuilder::new()
301 .with_settings(ClientSettings::default())
302 .with_state(registry)
303 .build();
304 }
305
306 #[test]
307 fn test_client_builder_defaults_to_an_empty_managed_profile() {
308 let client = ClientBuilder::new().build();
309
310 let handle = client.internal.managed_profile_handle();
311 assert!(
312 handle
313 .read()
314 .expect("cell should not be poisoned")
315 .is_none()
316 );
317 }
318
319 #[test]
320 fn test_client_builder_with_managed_profile_shares_the_cell() {
321 let cell = Arc::new(RwLock::new(Some(ManagementProfile::empty())));
322 let client = ClientBuilder::new()
323 .with_managed_profile(cell.clone())
324 .build();
325
326 *cell.write().expect("cell should not be poisoned") = None;
327
328 let handle = client.internal.managed_profile_handle();
329 assert!(
330 handle
331 .read()
332 .expect("cell should not be poisoned")
333 .is_none()
334 );
335 }
336
337 #[test]
338 fn test_client_builder_with_middleware_compiles() {
339 struct StubMiddleware;
340
341 #[async_trait::async_trait]
342 impl reqwest_middleware::Middleware for StubMiddleware {
343 async fn handle(
344 &self,
345 req: reqwest::Request,
346 extensions: &mut http::Extensions,
347 next: reqwest_middleware::Next<'_>,
348 ) -> reqwest_middleware::Result<reqwest::Response> {
349 next.run(req, extensions).await
350 }
351 }
352
353 let arc_middleware: Arc<dyn reqwest_middleware::Middleware> = Arc::new(StubMiddleware);
354 let _client = ClientBuilder::new()
355 .with_middleware(vec![arc_middleware])
356 .build();
357 }
358}