Skip to main content

bitwarden_core/client/
builder.rs

1use 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
20/// Builder for constructing [`Client`] instances with custom configuration.
21pub 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    /// Creates a new [`ClientBuilder`] with default settings.
33    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    /// Overrides the [`ApiConfigurations`] used by the client being built, allowing tests to inject
46    /// a mocked [`bitwarden_api_api::apis::ApiClient`] via
47    /// [`ApiConfigurations::from_api_client`]. Only available for testing.
48    #[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    /// Sets the [`ClientSettings`] for the client being built.
55    pub fn with_settings(mut self, settings: ClientSettings) -> Self {
56        self.settings = Some(settings);
57        self
58    }
59
60    /// Sets a custom [`TokenHandler`] for managing authentication tokens.
61    pub fn with_token_handler(mut self, token_handler: Arc<dyn TokenHandler>) -> Self {
62        self.token_handler = token_handler;
63        self
64    }
65
66    /// Sets additional middleware to be chained outermost (before auth middleware).
67    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    /// Sets a custom [`StateRegistry`] for the client being built.
76    /// If not set, defaults to [`StateRegistry::new_with_memory_db`].
77    pub fn with_state(mut self, state_registry: StateRegistry) -> Self {
78        self.state_registry = Some(state_registry);
79        self
80    }
81
82    /// Low-level hook to store a managed-settings profile cell, shared with the host application
83    /// that pushes profiles into it. Defaults to a fresh empty cell when not set.
84    ///
85    /// The `ManagedSettingsClient` in `bitwarden-managed-settings` supplies the cell. Instantiate
86    /// the client through that crate instead of calling this directly.
87    pub fn with_managed_profile(mut self, cell: Arc<RwLock<Option<ManagementProfile>>>) -> Self {
88        self.managed_profile = Some(cell);
89        self
90    }
91
92    /// Consumes the builder and constructs a [`Client`].
93    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        // Create the HTTP client for the Identity service, without authentication middleware.
108        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        // Create the client for the API service, with authentication middleware.
118        let auth_middleware = self.token_handler.initialize_middleware(
119            &state_registry,
120            identity.clone(),
121            key_store.clone(),
122        );
123
124        // Build the API HTTP client conditionally: disable auto-redirect when additional
125        // middleware is present so the outermost middleware can observe raw 3xx responses.
126        // reqwest::redirect is not available on wasm32 targets; on WASM the middleware uses
127        // a proactive cookie strategy instead of reactive 302/307 detection.
128        #[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        // Chain additional middleware outermost, then auth middleware innermost.
149        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        // Configure the key store's cipher suite from the client's environment, so all crypto
187        // operations (e.g. the KDF for a new account) pick compliant algorithms.
188        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
203/// Build default headers for Bitwarden HttpClient
204pub(crate) fn build_default_headers(info: &HostPlatformInfo) -> header::HeaderMap {
205    let mut headers = header::HeaderMap::new();
206
207    // Handle optional headers
208
209    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    // Handle required headers
241
242    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}