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