Skip to main content

bitwarden_core/client/
builder.rs

1#[cfg(feature = "internal")]
2use std::sync::RwLock;
3use std::sync::{Arc, OnceLock};
4
5use bitwarden_api_base::new_http_client_builder;
6use bitwarden_crypto::{CipherSuite, KeyStore};
7use bitwarden_state::registry::StateRegistry;
8use reqwest::header::{self, HeaderValue};
9
10#[cfg(feature = "internal")]
11use crate::key_management::state_bridge::StateBridge;
12use crate::{
13    auth::auth_tokens::{NoopTokenHandler, TokenHandler},
14    client::{
15        client::Client,
16        client_settings::{ClientName, ClientSettings, HostPlatformInfo},
17        internal::{ApiConfigurations, InternalClient},
18    },
19};
20
21/// Builder for constructing [`Client`] instances with custom configuration.
22pub struct ClientBuilder {
23    settings: Option<ClientSettings>,
24    token_handler: Arc<dyn TokenHandler>,
25    state_registry: Option<StateRegistry>,
26    middleware: Vec<Arc<dyn reqwest_middleware::Middleware>>,
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            #[cfg(feature = "test-fixtures")]
40            api_configurations: None,
41        }
42    }
43
44    /// Overrides the [`ApiConfigurations`] used by the client being built, allowing tests to inject
45    /// a mocked [`bitwarden_api_api::apis::ApiClient`] via
46    /// [`ApiConfigurations::from_api_client`]. Only available for testing.
47    #[cfg(feature = "test-fixtures")]
48    pub fn with_api_configurations(mut self, api_configurations: Arc<ApiConfigurations>) -> Self {
49        self.api_configurations = Some(api_configurations);
50        self
51    }
52
53    /// Sets the [`ClientSettings`] for the client being built.
54    pub fn with_settings(mut self, settings: ClientSettings) -> Self {
55        self.settings = Some(settings);
56        self
57    }
58
59    /// Sets a custom [`TokenHandler`] for managing authentication tokens.
60    pub fn with_token_handler(mut self, token_handler: Arc<dyn TokenHandler>) -> Self {
61        self.token_handler = token_handler;
62        self
63    }
64
65    /// Sets additional middleware to be chained outermost (before auth middleware).
66    pub fn with_middleware(
67        mut self,
68        middleware: Vec<Arc<dyn reqwest_middleware::Middleware>>,
69    ) -> Self {
70        self.middleware = middleware;
71        self
72    }
73
74    /// Sets a custom [`StateRegistry`] for the client being built.
75    /// If not set, defaults to [`StateRegistry::new_with_memory_db`].
76    pub fn with_state(mut self, state_registry: StateRegistry) -> Self {
77        self.state_registry = Some(state_registry);
78        self
79    }
80
81    /// Consumes the builder and constructs a [`Client`].
82    pub fn build(self) -> Client {
83        let settings = self.settings.unwrap_or_default();
84
85        let external_http_client = new_http_client_builder()
86            .build()
87            .expect("External HTTP Client build should not fail");
88
89        let headers = build_default_headers(&HostPlatformInfo::from(&settings));
90
91        let key_store = KeyStore::default();
92        let state_registry = self
93            .state_registry
94            .unwrap_or_else(StateRegistry::new_with_memory_db);
95
96        // Create the HTTP client for the Identity service, without authentication middleware.
97        let identity_http_client = new_http_client_builder()
98            .default_headers(headers.clone())
99            .build()
100            .expect("Bw HTTP Client build should not fail");
101        let identity = bitwarden_api_identity::Configuration {
102            base_path: settings.identity_url,
103            client: identity_http_client.into(),
104        };
105
106        // Create the client for the API service, with authentication middleware.
107        let auth_middleware = self.token_handler.initialize_middleware(
108            &state_registry,
109            identity.clone(),
110            key_store.clone(),
111        );
112
113        // Build the API HTTP client conditionally: disable auto-redirect when additional
114        // middleware is present so the outermost middleware can observe raw 3xx responses.
115        // reqwest::redirect is not available on wasm32 targets; on WASM the middleware uses
116        // a proactive cookie strategy instead of reactive 302/307 detection.
117        #[cfg(not(target_arch = "wasm32"))]
118        let api_http_client = if self.middleware.is_empty() {
119            new_http_client_builder()
120                .default_headers(headers)
121                .build()
122                .expect("Bw HTTP Client build should not fail")
123        } else {
124            new_http_client_builder()
125                .default_headers(headers)
126                .redirect(reqwest::redirect::Policy::none())
127                .build()
128                .expect("Bw HTTP Client (no redirect) build should not fail")
129        };
130
131        #[cfg(target_arch = "wasm32")]
132        let api_http_client = new_http_client_builder()
133            .default_headers(headers)
134            .build()
135            .expect("Bw HTTP Client build should not fail");
136
137        // Chain additional middleware outermost, then auth middleware innermost.
138        let mut middleware_builder = reqwest_middleware::ClientBuilder::new(api_http_client);
139        for mw in self.middleware {
140            middleware_builder = middleware_builder.with_arc(mw);
141        }
142        let bw_http_client = middleware_builder.with_arc(auth_middleware).build();
143        let api = bitwarden_api_api::Configuration {
144            base_path: settings.api_url,
145            client: bw_http_client,
146        };
147
148        #[cfg(feature = "test-fixtures")]
149        let api_configurations = self
150            .api_configurations
151            .unwrap_or_else(|| ApiConfigurations::new(identity, api, settings.device_type));
152        #[cfg(not(feature = "test-fixtures"))]
153        let api_configurations = ApiConfigurations::new(identity, api, settings.device_type);
154
155        let client = Client {
156            internal: Arc::new(InternalClient {
157                user_id: OnceLock::new(),
158                token_handler: self.token_handler,
159                api_configurations,
160                external_http_client,
161                key_store,
162                #[cfg(feature = "internal")]
163                security_state: RwLock::new(None),
164                #[cfg(feature = "internal")]
165                state_bridge: StateBridge::new(),
166                state_registry,
167            }),
168        };
169
170        // Configure the key store's cipher suite from the client's environment, so all crypto
171        // operations (e.g. the KDF for a new account) pick compliant algorithms.
172        client
173            .internal
174            .get_key_store()
175            .set_cipher_suite(CipherSuite::from_gov_mode(client.gov_mode()));
176
177        client
178    }
179}
180
181impl Default for ClientBuilder {
182    fn default() -> Self {
183        Self::new()
184    }
185}
186
187/// Build default headers for Bitwarden HttpClient
188pub(crate) fn build_default_headers(info: &HostPlatformInfo) -> header::HeaderMap {
189    let mut headers = header::HeaderMap::new();
190
191    // Handle optional headers
192
193    if let Some(device_identifier) = &info.device_identifier {
194        headers.append(
195            "Device-Identifier",
196            HeaderValue::from_str(device_identifier)
197                .expect("Device identifier should be a valid header value"),
198        );
199    }
200
201    if let Some(client_type) = Into::<Option<ClientName>>::into(info.device_type) {
202        headers.append(
203            "Bitwarden-Client-Name",
204            HeaderValue::from_str(&client_type.to_string())
205                .expect("All ASCII strings are valid header values"),
206        );
207    }
208
209    if let Some(version) = &info.bitwarden_client_version {
210        headers.append(
211            "Bitwarden-Client-Version",
212            HeaderValue::from_str(version).expect("Version should be a valid header value"),
213        );
214    }
215
216    if let Some(package_type) = &info.bitwarden_package_type {
217        headers.append(
218            "Bitwarden-Package-Type",
219            HeaderValue::from_str(package_type)
220                .expect("Package type should be a valid header value"),
221        );
222    }
223
224    // Handle required headers
225
226    headers.append(
227        "Device-Type",
228        HeaderValue::from_str(&(info.device_type as u8).to_string())
229            .expect("All numbers are valid ASCII"),
230    );
231
232    headers.append(
233        reqwest::header::USER_AGENT,
234        HeaderValue::from_str(&info.user_agent).expect("User agent should be a valid header value"),
235    );
236
237    headers
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    #[test]
245    fn test_client_builder_default_builds() {
246        let _client = ClientBuilder::new().build();
247    }
248
249    #[test]
250    fn test_client_builder_with_settings_builds() {
251        let settings = ClientSettings::default();
252        let _client = ClientBuilder::new().with_settings(settings).build();
253    }
254
255    #[test]
256    fn test_client_builder_with_token_handler_builds() {
257        let handler: Arc<dyn TokenHandler> = Arc::new(NoopTokenHandler);
258        let _client = ClientBuilder::new().with_token_handler(handler).build();
259    }
260
261    #[test]
262    fn test_client_builder_chain_order_independence() {
263        let _a = ClientBuilder::new()
264            .with_settings(ClientSettings::default())
265            .with_token_handler(Arc::new(NoopTokenHandler) as Arc<dyn TokenHandler>)
266            .build();
267        let _b = ClientBuilder::new()
268            .with_token_handler(Arc::new(NoopTokenHandler) as Arc<dyn TokenHandler>)
269            .with_settings(ClientSettings::default())
270            .build();
271    }
272
273    #[test]
274    fn test_client_builder_with_state_builds() {
275        use bitwarden_state::registry::StateRegistry;
276        let registry = StateRegistry::new_with_memory_db();
277        let _client = ClientBuilder::new().with_state(registry).build();
278    }
279
280    #[test]
281    fn test_client_builder_with_state_in_chain() {
282        use bitwarden_state::registry::StateRegistry;
283        let registry = StateRegistry::new_with_memory_db();
284        let _client = ClientBuilder::new()
285            .with_settings(ClientSettings::default())
286            .with_state(registry)
287            .build();
288    }
289
290    #[test]
291    fn test_client_builder_with_middleware_compiles() {
292        struct StubMiddleware;
293
294        #[async_trait::async_trait]
295        impl reqwest_middleware::Middleware for StubMiddleware {
296            async fn handle(
297                &self,
298                req: reqwest::Request,
299                extensions: &mut http::Extensions,
300                next: reqwest_middleware::Next<'_>,
301            ) -> reqwest_middleware::Result<reqwest::Response> {
302                next.run(req, extensions).await
303            }
304        }
305
306        let arc_middleware: Arc<dyn reqwest_middleware::Middleware> = Arc::new(StubMiddleware);
307        let _client = ClientBuilder::new()
308            .with_middleware(vec![arc_middleware])
309            .build();
310    }
311}