Skip to main content

bitwarden_core/client/
builder.rs

1use 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
18/// Builder for constructing [`Client`] instances with custom configuration.
19pub 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    /// Creates a new [`ClientBuilder`] with default settings.
28    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    /// Sets the [`ClientSettings`] for the client being built.
38    pub fn with_settings(mut self, settings: ClientSettings) -> Self {
39        self.settings = Some(settings);
40        self
41    }
42
43    /// Sets a custom [`TokenHandler`] for managing authentication tokens.
44    pub fn with_token_handler(mut self, token_handler: Arc<dyn TokenHandler>) -> Self {
45        self.token_handler = token_handler;
46        self
47    }
48
49    /// Sets additional middleware to be chained outermost (before auth middleware).
50    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    /// Sets a custom [`StateRegistry`] for the client being built.
59    /// If not set, defaults to [`StateRegistry::new_with_memory_db`].
60    pub fn with_state(mut self, state_registry: StateRegistry) -> Self {
61        self.state_registry = Some(state_registry);
62        self
63    }
64
65    /// Consumes the builder and constructs a [`Client`].
66    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        // Create the HTTP client for the Identity service, without authentication middleware.
79        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        // Create the client for the API service, with authentication middleware.
89        let auth_middleware = self.token_handler.initialize_middleware(
90            login_method.clone(),
91            identity.clone(),
92            key_store.clone(),
93        );
94
95        // Build the API HTTP client conditionally: disable auto-redirect when additional
96        // middleware is present so the outermost middleware can observe raw 3xx responses.
97        // reqwest::redirect is not available on wasm32 targets; on WASM the middleware uses
98        // a proactive cookie strategy instead of reactive 302/307 detection.
99        #[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        // Chain additional middleware outermost, then auth middleware innermost.
120        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        // Enforce HTTPS for all requests in non-debug builds
169        #[cfg(not(debug_assertions))]
170        {
171            client_builder = client_builder.https_only(true);
172        }
173    }
174
175    client_builder
176}
177
178/// Build default headers for Bitwarden HttpClient
179fn build_default_headers(settings: &ClientSettings) -> header::HeaderMap {
180    let mut headers = header::HeaderMap::new();
181
182    // Handle optional headers
183
184    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    // Handle required headers
216
217    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}