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_crypto::KeyStore;
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}
27
28impl ClientBuilder {
29    /// Creates a new [`ClientBuilder`] with default settings.
30    pub fn new() -> Self {
31        Self {
32            settings: None,
33            token_handler: Arc::new(NoopTokenHandler),
34            state_registry: None,
35            middleware: Vec::new(),
36        }
37    }
38
39    /// Sets the [`ClientSettings`] for the client being built.
40    pub fn with_settings(mut self, settings: ClientSettings) -> Self {
41        self.settings = Some(settings);
42        self
43    }
44
45    /// Sets a custom [`TokenHandler`] for managing authentication tokens.
46    pub fn with_token_handler(mut self, token_handler: Arc<dyn TokenHandler>) -> Self {
47        self.token_handler = token_handler;
48        self
49    }
50
51    /// Sets additional middleware to be chained outermost (before auth middleware).
52    pub fn with_middleware(
53        mut self,
54        middleware: Vec<Arc<dyn reqwest_middleware::Middleware>>,
55    ) -> Self {
56        self.middleware = middleware;
57        self
58    }
59
60    /// Sets a custom [`StateRegistry`] for the client being built.
61    /// If not set, defaults to [`StateRegistry::new_with_memory_db`].
62    pub fn with_state(mut self, state_registry: StateRegistry) -> Self {
63        self.state_registry = Some(state_registry);
64        self
65    }
66
67    /// Consumes the builder and constructs a [`Client`].
68    pub fn build(self) -> Client {
69        let settings = self.settings.unwrap_or_default();
70
71        let external_http_client = new_http_client_builder()
72            .build()
73            .expect("External HTTP Client build should not fail");
74
75        let headers = build_default_headers(&HostPlatformInfo::from(&settings));
76
77        let key_store = KeyStore::default();
78        let state_registry = self
79            .state_registry
80            .unwrap_or_else(StateRegistry::new_with_memory_db);
81
82        // Create the HTTP client for the Identity service, without authentication middleware.
83        let identity_http_client = new_http_client_builder()
84            .default_headers(headers.clone())
85            .build()
86            .expect("Bw HTTP Client build should not fail");
87        let identity = bitwarden_api_identity::Configuration {
88            base_path: settings.identity_url,
89            client: identity_http_client.into(),
90        };
91
92        // Create the client for the API service, with authentication middleware.
93        let auth_middleware = self.token_handler.initialize_middleware(
94            &state_registry,
95            identity.clone(),
96            key_store.clone(),
97        );
98
99        // Build the API HTTP client conditionally: disable auto-redirect when additional
100        // middleware is present so the outermost middleware can observe raw 3xx responses.
101        // reqwest::redirect is not available on wasm32 targets; on WASM the middleware uses
102        // a proactive cookie strategy instead of reactive 302/307 detection.
103        #[cfg(not(target_arch = "wasm32"))]
104        let api_http_client = if self.middleware.is_empty() {
105            new_http_client_builder()
106                .default_headers(headers)
107                .build()
108                .expect("Bw HTTP Client build should not fail")
109        } else {
110            new_http_client_builder()
111                .default_headers(headers)
112                .redirect(reqwest::redirect::Policy::none())
113                .build()
114                .expect("Bw HTTP Client (no redirect) build should not fail")
115        };
116
117        #[cfg(target_arch = "wasm32")]
118        let api_http_client = new_http_client_builder()
119            .default_headers(headers)
120            .build()
121            .expect("Bw HTTP Client build should not fail");
122
123        // Chain additional middleware outermost, then auth middleware innermost.
124        let mut middleware_builder = reqwest_middleware::ClientBuilder::new(api_http_client);
125        for mw in self.middleware {
126            middleware_builder = middleware_builder.with_arc(mw);
127        }
128        let bw_http_client = middleware_builder.with_arc(auth_middleware).build();
129        let api = bitwarden_api_api::Configuration {
130            base_path: settings.api_url,
131            client: bw_http_client,
132        };
133
134        Client {
135            internal: Arc::new(InternalClient {
136                user_id: OnceLock::new(),
137                token_handler: self.token_handler,
138                api_configurations: ApiConfigurations::new(identity, api, settings.device_type),
139                external_http_client,
140                key_store,
141                #[cfg(feature = "internal")]
142                security_state: RwLock::new(None),
143                #[cfg(feature = "internal")]
144                state_bridge: StateBridge::new(),
145                state_registry,
146            }),
147        }
148    }
149}
150
151impl Default for ClientBuilder {
152    fn default() -> Self {
153        Self::new()
154    }
155}
156
157pub(crate) fn new_http_client_builder() -> reqwest::ClientBuilder {
158    #[allow(unused_mut)]
159    let mut client_builder = reqwest::Client::builder();
160
161    #[cfg(not(target_arch = "wasm32"))]
162    {
163        use rustls::ClientConfig;
164        use rustls_platform_verifier::ConfigVerifierExt;
165        client_builder = client_builder.use_preconfigured_tls(
166            ClientConfig::with_platform_verifier().expect("Failed to create platform verifier"),
167        );
168
169        // Enforce HTTPS for all requests in non-debug builds
170        #[cfg(not(debug_assertions))]
171        {
172            client_builder = client_builder.https_only(true);
173        }
174    }
175
176    client_builder
177}
178
179/// Build default headers for Bitwarden HttpClient
180pub(crate) fn build_default_headers(info: &HostPlatformInfo) -> header::HeaderMap {
181    let mut headers = header::HeaderMap::new();
182
183    // Handle optional headers
184
185    if let Some(device_identifier) = &info.device_identifier {
186        headers.append(
187            "Device-Identifier",
188            HeaderValue::from_str(device_identifier)
189                .expect("Device identifier should be a valid header value"),
190        );
191    }
192
193    if let Some(client_type) = Into::<Option<ClientName>>::into(info.device_type) {
194        headers.append(
195            "Bitwarden-Client-Name",
196            HeaderValue::from_str(&client_type.to_string())
197                .expect("All ASCII strings are valid header values"),
198        );
199    }
200
201    if let Some(version) = &info.bitwarden_client_version {
202        headers.append(
203            "Bitwarden-Client-Version",
204            HeaderValue::from_str(version).expect("Version should be a valid header value"),
205        );
206    }
207
208    if let Some(package_type) = &info.bitwarden_package_type {
209        headers.append(
210            "Bitwarden-Package-Type",
211            HeaderValue::from_str(package_type)
212                .expect("Package type should be a valid header value"),
213        );
214    }
215
216    // Handle required headers
217
218    headers.append(
219        "Device-Type",
220        HeaderValue::from_str(&(info.device_type as u8).to_string())
221            .expect("All numbers are valid ASCII"),
222    );
223
224    headers.append(
225        reqwest::header::USER_AGENT,
226        HeaderValue::from_str(&info.user_agent).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}