1#![doc = include_str!("../README.md")]
2
3uniffi::setup_scaffolding!();
4
5use std::sync::{Arc, Once};
6
7use auth::AuthClient;
8use bitwarden_core::{ClientSettings, auth::ClientManagedTokens};
9
10#[allow(missing_docs)]
11pub mod auth;
12#[allow(missing_docs)]
13pub mod crypto;
14#[allow(missing_docs)]
15pub mod error;
16mod log_callback;
17#[allow(missing_docs)]
18pub mod platform;
19#[allow(missing_docs)]
20pub mod tool;
21mod uniffi_support;
22#[allow(missing_docs)]
23pub mod vault;
24
25#[cfg(target_os = "android")]
26mod android_support;
27
28use crypto::CryptoClient;
29use error::{Error, Result};
30pub use log_callback::LogCallback;
31use platform::PlatformClient;
32pub use platform::{
33 AcquiredCookie, BootstrapConfig, ServerCommunicationConfig, ServerCommunicationConfigClient,
34 ServerCommunicationConfigRepository, SsoCookieVendorConfig,
35};
36use tool::{ExporterClient, GeneratorClients, SendClient, SshClient};
37use vault::VaultClient;
38
39#[allow(missing_docs)]
40#[derive(uniffi::Object)]
41pub struct Client(pub(crate) bitwarden_pm::PasswordManagerClient);
42
43#[uniffi::export(async_runtime = "tokio")]
44impl Client {
45 #[uniffi::constructor]
47 pub fn new(
48 token_provider: Arc<dyn ClientManagedTokens>,
49 settings: Option<ClientSettings>,
50 ) -> Self {
51 init_logger(None, None);
52 setup_error_converter();
53
54 #[cfg(target_os = "android")]
55 android_support::init();
56
57 Self(bitwarden_pm::PasswordManagerClient::new_with_client_tokens(
58 settings,
59 token_provider,
60 ))
61 }
62
63 pub fn crypto(&self) -> CryptoClient {
65 CryptoClient(self.0.crypto())
66 }
67
68 pub fn vault(&self) -> VaultClient {
70 VaultClient(self.0.vault())
71 }
72
73 #[allow(missing_docs)]
74 pub fn platform(&self) -> PlatformClient {
75 PlatformClient(self.0.0.clone())
76 }
77
78 pub fn generators(&self) -> GeneratorClients {
80 GeneratorClients(self.0.generator())
81 }
82
83 pub fn exporters(&self) -> ExporterClient {
85 ExporterClient(self.0.exporters())
86 }
87
88 pub fn sends(&self) -> SendClient {
90 SendClient(self.0.sends())
91 }
92
93 pub fn ssh(&self) -> SshClient {
95 SshClient()
96 }
97
98 pub fn auth(&self) -> AuthClient {
100 AuthClient(self.0.0.clone())
101 }
102
103 pub fn echo(&self, msg: String) -> String {
105 msg
106 }
107
108 pub async fn http_get(&self, url: String) -> Result<String> {
110 let client = self.0.0.internal.get_http_client();
111 let res = client
112 .get(&url)
113 .send()
114 .await
115 .map_err(|e| Error::Api(e.into()))?;
116
117 res.text().await.map_err(|e| Error::Api(e.into()))
118 }
119}
120
121static INIT: Once = Once::new();
122
123#[derive(uniffi::Enum)]
125pub enum LogLevel {
126 Trace,
128 Debug,
130 Info,
132 Warn,
134 Error,
136}
137
138impl LogLevel {
139 fn as_str(&self) -> &'static str {
140 match self {
141 LogLevel::Trace => "trace",
142 LogLevel::Debug => "debug",
143 LogLevel::Info => "info",
144 LogLevel::Warn => "warn",
145 LogLevel::Error => "error",
146 }
147 }
148}
149
150#[uniffi::export]
174pub fn init_logger(callback: Option<Arc<dyn LogCallback>>, level: Option<LogLevel>) {
175 use tracing_subscriber::{EnvFilter, layer::SubscriberExt as _, util::SubscriberInitExt as _};
176
177 INIT.call_once(|| {
178 let level = level.as_ref().map(|l| l.as_str()).unwrap_or("info");
184 let filter = EnvFilter::builder()
185 .with_default_directive(
186 option_env!("RUST_LOG")
187 .unwrap_or(level)
188 .parse()
189 .expect("should provide valid log level at compile time."),
190 )
191 .from_env_lossy();
192
193 let fmtlayer = tracing_subscriber::fmt::layer()
194 .with_ansi(true)
195 .with_file(true)
196 .with_line_number(true)
197 .with_target(true)
198 .pretty();
199
200 let registry = tracing_subscriber::registry().with(fmtlayer).with(filter);
202
203 let callback_layer = callback.map(log_callback::CallbackLayer::new);
206 let registry = registry.with(callback_layer);
207 #[cfg(target_os = "ios")]
208 {
209 const TAG: &str = "com.8bit.bitwarden";
210 registry
211 .with(tracing_oslog::OsLogger::new(TAG, "default"))
212 .init();
213 }
214
215 #[cfg(target_os = "android")]
216 {
217 const TAG: &str = "com.bitwarden.sdk";
218 registry
219 .with(
220 tracing_android::layer(TAG)
221 .expect("initialization of android logcat tracing layer"),
222 )
223 .init();
224 }
225
226 #[cfg(not(any(target_os = "android", target_os = "ios")))]
227 {
228 registry.init();
229 }
230 #[cfg(feature = "dangerous-crypto-debug")]
231 tracing::warn!(
232 "Dangerous crypto debug features are enabled. THIS MUST NOT BE USED IN PRODUCTION BUILDS!!"
233 );
234 });
235}
236
237fn setup_error_converter() {
240 bitwarden_uniffi_error::set_error_to_uniffi_error(|e| {
241 crate::error::BitwardenError::Conversion(e.to_string()).into()
242 });
243}
244#[cfg(test)]
245mod tests {
246 use std::sync::Mutex;
247
248 use super::*;
249 #[derive(Debug)]
251 struct MockTokenProvider;
252
253 #[async_trait::async_trait]
254 impl ClientManagedTokens for MockTokenProvider {
255 async fn get_access_token(&self) -> Option<String> {
256 Some("mock_token".to_string())
257 }
258 }
259 struct TestLogCallback {
261 logs: Arc<Mutex<Vec<(String, String, String)>>>,
262 }
263 impl LogCallback for TestLogCallback {
264 fn on_log(&self, level: String, target: String, message: String) -> Result<()> {
265 self.logs
266 .lock()
267 .expect("Failed to lock logs mutex")
268 .push((level, target, message));
269 Ok(())
270 }
271 }
272
273 #[test]
278 fn test_callback_receives_logs() {
279 let logs = Arc::new(Mutex::new(Vec::new()));
280 let callback = Arc::new(TestLogCallback { logs: logs.clone() });
281
282 init_logger(Some(callback), None);
284
285 let _client = Client::new(Arc::new(MockTokenProvider), None);
287
288 tracing::info!("test message from SDK");
290
291 let captured = logs.lock().expect("Failed to lock logs mutex");
293 assert!(!captured.is_empty(), "Callback should receive logs");
294
295 let test_log = captured
297 .iter()
298 .find(|(_, _, msg)| msg.contains("test message"))
299 .expect("Should find our test log message");
300
301 assert_eq!(test_log.0, "INFO");
302 assert!(test_log.2.contains("test message"));
303 }
304}