Skip to main content

bitwarden_core/client/
client_settings.rs

1use std::{fmt, sync::OnceLock};
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6/// Basic client behavior settings. These settings specify the various targets and behavior of the
7/// Bitwarden Client. They are optional and uneditable once the client is initialized.
8///
9/// Defaults to
10///
11/// ```
12/// # use bitwarden_core::{ClientSettings, DeviceType};
13/// let settings = ClientSettings {
14///     identity_url: "https://identity.bitwarden.com".to_string(),
15///     api_url: "https://api.bitwarden.com".to_string(),
16///     user_agent: "Bitwarden Rust-SDK".to_string(),
17///     device_type: DeviceType::SDK,
18///     bitwarden_client_version: None,
19///     bitwarden_package_type: None,
20///     device_identifier: None,
21/// };
22/// let default = ClientSettings::default();
23/// ```
24#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)]
25#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
26#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
27#[bitwarden_ffi::wasm_record]
28pub struct ClientSettings {
29    /// The identity url of the targeted Bitwarden instance. Defaults to `https://identity.bitwarden.com`
30    pub identity_url: String,
31    /// The api url of the targeted Bitwarden instance. Defaults to `https://api.bitwarden.com`
32    pub api_url: String,
33    /// The user_agent to sent to Bitwarden. Defaults to `Bitwarden Rust-SDK`
34    pub user_agent: String,
35    /// Device type to send to Bitwarden. Defaults to SDK
36    pub device_type: DeviceType,
37
38    // TODO: PM-29939 - Remove optionality when all clients pass these values
39    /// Device identifier to send to Bitwarden. Optional for now in transition period.
40    pub device_identifier: Option<String>,
41    /// Bitwarden Client Version to send to Bitwarden. Optional for now in transition period.
42    pub bitwarden_client_version: Option<String>,
43    /// Bitwarden Package Type to send to Bitwarden. We should evaluate this field to see if it
44    /// should be optional later.
45    pub bitwarden_package_type: Option<String>,
46}
47
48impl Default for ClientSettings {
49    fn default() -> Self {
50        Self {
51            identity_url: "https://identity.bitwarden.com".into(),
52            api_url: "https://api.bitwarden.com".into(),
53            user_agent: "Bitwarden Rust-SDK".into(),
54            device_type: DeviceType::SDK,
55            device_identifier: None,
56            bitwarden_client_version: None,
57            bitwarden_package_type: None,
58        }
59    }
60}
61
62#[allow(non_camel_case_types, missing_docs)]
63#[derive(Serialize, Deserialize, Copy, Clone, Debug, JsonSchema, PartialEq)]
64#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
65#[bitwarden_ffi::wasm_record]
66pub enum DeviceType {
67    Android = 0,
68    iOS = 1,
69    ChromeExtension = 2,
70    FirefoxExtension = 3,
71    OperaExtension = 4,
72    EdgeExtension = 5,
73    WindowsDesktop = 6,
74    MacOsDesktop = 7,
75    LinuxDesktop = 8,
76    ChromeBrowser = 9,
77    FirefoxBrowser = 10,
78    OperaBrowser = 11,
79    EdgeBrowser = 12,
80    IEBrowser = 13,
81    UnknownBrowser = 14,
82    AndroidAmazon = 15,
83    UWP = 16,
84    SafariBrowser = 17,
85    VivaldiBrowser = 18,
86    VivaldiExtension = 19,
87    SafariExtension = 20,
88    SDK = 21,
89    Server = 22,
90    WindowsCLI = 23,
91    MacOsCLI = 24,
92    LinuxCLI = 25,
93    DuckDuckGoBrowser = 26,
94}
95
96#[derive(Copy, Clone, Debug)]
97pub enum ClientName {
98    Web,
99    Browser,
100    Desktop,
101    Mobile,
102    Cli,
103}
104
105impl From<DeviceType> for Option<ClientName> {
106    fn from(device_type: DeviceType) -> Self {
107        match device_type {
108            DeviceType::Android | DeviceType::AndroidAmazon | DeviceType::iOS => {
109                Some(ClientName::Mobile)
110            }
111
112            DeviceType::ChromeBrowser
113            | DeviceType::FirefoxBrowser
114            | DeviceType::OperaBrowser
115            | DeviceType::EdgeBrowser
116            | DeviceType::IEBrowser
117            | DeviceType::SafariBrowser
118            | DeviceType::VivaldiBrowser
119            | DeviceType::DuckDuckGoBrowser
120            | DeviceType::UnknownBrowser => Some(ClientName::Web),
121
122            DeviceType::ChromeExtension
123            | DeviceType::FirefoxExtension
124            | DeviceType::OperaExtension
125            | DeviceType::EdgeExtension
126            | DeviceType::VivaldiExtension
127            | DeviceType::SafariExtension => Some(ClientName::Browser),
128
129            DeviceType::LinuxDesktop
130            | DeviceType::MacOsDesktop
131            | DeviceType::WindowsDesktop
132            | DeviceType::UWP => Some(ClientName::Desktop),
133
134            DeviceType::WindowsCLI | DeviceType::MacOsCLI | DeviceType::LinuxCLI => {
135                Some(ClientName::Cli)
136            }
137
138            DeviceType::SDK | DeviceType::Server => None,
139        }
140    }
141}
142
143impl fmt::Display for ClientName {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        let s = match self {
146            ClientName::Web => "web",
147            ClientName::Browser => "browser",
148            ClientName::Desktop => "desktop",
149            ClientName::Mobile => "mobile",
150            ClientName::Cli => "cli",
151        };
152        write!(f, "{}", s)
153    }
154}
155
156/// Process-wide, application-provided platform information.
157///
158/// Initialize exactly once at application startup via [`init_host_platform_info`];
159/// read via [`get_host_platform_info`]. Subsequent `init` calls are ignored.
160#[derive(Debug, Clone)]
161pub struct HostPlatformInfo {
162    pub user_agent: String,
163    pub device_type: DeviceType,
164    pub device_identifier: Option<String>,
165    pub bitwarden_client_version: Option<String>,
166    pub bitwarden_package_type: Option<String>,
167}
168
169impl From<&ClientSettings> for HostPlatformInfo {
170    fn from(settings: &ClientSettings) -> Self {
171        Self {
172            user_agent: settings.user_agent.clone(),
173            device_type: settings.device_type,
174            device_identifier: settings.device_identifier.clone(),
175            bitwarden_client_version: settings.bitwarden_client_version.clone(),
176            bitwarden_package_type: settings.bitwarden_package_type.clone(),
177        }
178    }
179}
180
181static HOST_PLATFORM_INFO: OnceLock<HostPlatformInfo> = OnceLock::new();
182
183/// Initialize the global [`HostPlatformInfo`].
184///
185/// Should be called once during application startup, before any
186/// `Client::load_from_state` calls. Subsequent calls are silently ignored.
187pub fn init_host_platform_info(info: HostPlatformInfo) {
188    let _ = HOST_PLATFORM_INFO.set(info);
189}
190
191/// Returns the globally-initialized [`HostPlatformInfo`].
192///
193/// # Panics
194/// Panics if [`init_host_platform_info`] has not yet been called.
195pub fn get_host_platform_info() -> &'static HostPlatformInfo {
196    HOST_PLATFORM_INFO
197        .get()
198        .expect("host platform info to be initialized")
199}
200
201impl HostPlatformInfo {
202    /// Derive a [`ClientSettings`] from this platform info, for use in client initialization.
203    pub fn to_client_settings(&self, api_url: String, identity_url: String) -> ClientSettings {
204        ClientSettings {
205            user_agent: self.user_agent.clone(),
206            device_type: self.device_type,
207            device_identifier: self.device_identifier.clone(),
208            bitwarden_client_version: self.bitwarden_client_version.clone(),
209            bitwarden_package_type: self.bitwarden_package_type.clone(),
210            api_url,
211            identity_url,
212        }
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    // `OnceLock` is process-global. Any test in this binary that calls
221    // `init_host_platform_info` may have run first, so we cannot assert what
222    // value won the first-init race — only that a *subsequent* init does not
223    // change the stored value.
224    #[test]
225    fn second_init_does_not_overwrite_first() {
226        let first = HostPlatformInfo {
227            user_agent: "first".into(),
228            device_type: DeviceType::SDK,
229            device_identifier: Some("dev-1".into()),
230            bitwarden_client_version: Some("1.0.0".into()),
231            bitwarden_package_type: Some("test".into()),
232        };
233        init_host_platform_info(first);
234
235        // Capture whatever value won the first-init race (ours or another
236        // test's).
237        let after_first_init = get_host_platform_info().clone();
238
239        let second = HostPlatformInfo {
240            user_agent: "second".into(),
241            device_type: DeviceType::SDK,
242            device_identifier: Some("dev-2".into()),
243            bitwarden_client_version: Some("2.0.0".into()),
244            bitwarden_package_type: Some("test".into()),
245        };
246        init_host_platform_info(second);
247
248        let after_second_init = get_host_platform_info();
249        assert_eq!(after_second_init.user_agent, after_first_init.user_agent);
250        assert_eq!(after_second_init.device_type, after_first_init.device_type);
251        assert_eq!(
252            after_second_init.device_identifier,
253            after_first_init.device_identifier
254        );
255        assert_eq!(
256            after_second_init.bitwarden_client_version,
257            after_first_init.bitwarden_client_version
258        );
259        assert_eq!(
260            after_second_init.bitwarden_package_type,
261            after_first_init.bitwarden_package_type
262        );
263    }
264}