1use std::future::Future;
30use std::pin::Pin;
31use std::sync::Arc;
32use std::task::Poll;
33use std::time::Duration;
34
35use anyhow::{Context as _, Result, bail, format_err};
36use futures_lite::FutureExt;
37use iroh::{Endpoint, RelayMode};
38use tokio::fs;
39use tokio::task::JoinHandle;
40use tokio_util::sync::CancellationToken;
41
42use crate::EventType;
43use crate::chat::add_device_msg;
44use crate::context::Context;
45use crate::imex::BlobDirContents;
46use crate::key;
47use crate::log::warn;
48use crate::message::Message;
49use crate::qr::Qr;
50use crate::stock_str::backup_transfer_msg_body;
51use crate::tools::{TempPathGuard, create_id, time};
52
53use super::{DBFILE_BACKUP_NAME, export_backup_stream, export_database, import_backup_stream};
54
55const BACKUP_ALPN: &[u8] = b"/deltachat/backup";
57
58#[derive(Debug)]
69pub struct BackupProvider {
70 _endpoint: Endpoint,
72
73 node_addr: iroh::NodeAddr,
75
76 auth_token: String,
79
80 handle: JoinHandle<Result<()>>,
82
83 _drop_guard: tokio_util::sync::DropGuard,
85}
86
87impl BackupProvider {
88 pub async fn prepare(context: &Context) -> Result<Self> {
99 let relay_mode = RelayMode::Disabled;
100 let endpoint = Endpoint::builder()
101 .tls_x509() .alpns(vec![BACKUP_ALPN.to_vec()])
103 .relay_mode(relay_mode)
104 .bind()
105 .await?;
106 let node_addr = endpoint.node_addr().await?;
107
108 let cancel_token = context.alloc_ongoing().await?;
110 let paused_guard = context.scheduler.pause(context).await?;
111 let context_dir = context
112 .get_blobdir()
113 .parent()
114 .context("Context dir not found")?;
115
116 key::ensure_secret_key_exists(context)
118 .await
119 .context("Cannot create private key or private key not available")?;
120
121 let dbfile = context_dir.join(DBFILE_BACKUP_NAME);
122 if fs::metadata(&dbfile).await.is_ok() {
123 fs::remove_file(&dbfile).await?;
124 warn!(context, "Previous database export deleted");
125 }
126 let dbfile = TempPathGuard::new(dbfile);
127
128 let auth_token = create_id();
130
131 let passphrase = String::new();
132
133 export_database(context, &dbfile, passphrase, time())
134 .await
135 .context("Database export failed")?;
136
137 let drop_token = CancellationToken::new();
138 let handle = {
139 let context = context.clone();
140 let drop_token = drop_token.clone();
141 let endpoint = endpoint.clone();
142 let auth_token = auth_token.clone();
143 tokio::spawn(async move {
144 Self::accept_loop(
145 context.clone(),
146 endpoint,
147 auth_token,
148 cancel_token,
149 drop_token,
150 dbfile,
151 )
152 .await;
153 info!(context, "Finished accept loop.");
154
155 context.free_ongoing().await;
156
157 drop(paused_guard);
159 Ok(())
160 })
161 };
162 Ok(Self {
163 _endpoint: endpoint,
164 node_addr,
165 auth_token,
166 handle,
167 _drop_guard: drop_token.drop_guard(),
168 })
169 }
170
171 async fn handle_connection(
172 context: Context,
173 conn: iroh::endpoint::Connecting,
174 auth_token: String,
175 dbfile: Arc<TempPathGuard>,
176 ) -> Result<()> {
177 let conn = conn.await?;
178 let (mut send_stream, mut recv_stream) = conn.accept_bi().await?;
179
180 let mut received_auth_token = vec![0u8; auth_token.len()];
182 recv_stream.read_exact(&mut received_auth_token).await?;
183 if received_auth_token.as_slice() != auth_token.as_bytes() {
184 warn!(context, "Received wrong backup authentication token.");
185 return Ok(());
186 }
187
188 info!(context, "Received valid backup authentication token.");
189 context.emit_event(EventType::ImexProgress(1));
191
192 let blobdir = BlobDirContents::new(&context).await?;
193
194 let mut file_size = dbfile.metadata()?.len();
195 for blob in blobdir.iter() {
196 file_size = file_size
197 .checked_add(blob.to_abs_path().metadata()?.len())
198 .context("File size overflow")?;
199 }
200
201 send_stream.write_all(&file_size.to_be_bytes()).await?;
202
203 export_backup_stream(&context, &dbfile, blobdir, send_stream, file_size)
204 .await
205 .context("Failed to write backup into QUIC stream")?;
206 info!(context, "Finished writing backup into QUIC stream.");
207 let mut buf = [0u8; 1];
208 info!(context, "Waiting for acknowledgment.");
209 recv_stream.read_exact(&mut buf).await?;
210 info!(context, "Received backup reception acknowledgement.");
211 context.emit_event(EventType::ImexProgress(1000));
212
213 let mut msg = Message::new_text(backup_transfer_msg_body(&context));
214 add_device_msg(&context, None, Some(&mut msg)).await?;
215
216 Ok(())
217 }
218
219 async fn accept_loop(
220 context: Context,
221 endpoint: Endpoint,
222 auth_token: String,
223 cancel_token: async_channel::Receiver<()>,
224 drop_token: CancellationToken,
225 dbfile: TempPathGuard,
226 ) {
227 let dbfile = Arc::new(dbfile);
228 loop {
229 tokio::select! {
230 biased;
231
232 conn = endpoint.accept() => {
233 if let Some(conn) = conn {
234 let conn = match conn.accept() {
235 Ok(conn) => conn,
236 Err(err) => {
237 warn!(context, "Failed to accept iroh connection: {err:#}.");
238 continue;
239 }
240 };
241 let context = context.clone();
243 let auth_token = auth_token.clone();
244 let dbfile = dbfile.clone();
245 if let Err(err) = Self::handle_connection(context.clone(), conn, auth_token, dbfile).race(
246 async {
247 cancel_token.recv().await.ok();
248 Err(format_err!("Backup transfer canceled"))
249 }
250 ).race(
251 async {
252 drop_token.cancelled().await;
253 Err(format_err!("Backup provider dropped"))
254 }
255 ).await {
256 error!(context, "Error while handling backup connection: {err:#}.");
257 context.emit_event(EventType::ImexProgress(0));
258 break;
259 } else {
260 info!(context, "Backup transfer finished successfully.");
261 break;
262 }
263 } else {
264 break;
265 }
266 },
267 _ = cancel_token.recv() => {
268 info!(context, "Backup transfer canceled by the user, stopping accept loop.");
269 context.emit_event(EventType::ImexProgress(0));
270 break;
271 }
272 _ = drop_token.cancelled() => {
273 info!(context, "Backup transfer canceled by dropping the provider, stopping accept loop.");
274 context.emit_event(EventType::ImexProgress(0));
275 break;
276 }
277 }
278 }
279 }
280
281 pub fn qr(&self) -> Qr {
285 Qr::Backup2 {
286 node_addr: self.node_addr.clone(),
287
288 auth_token: self.auth_token.clone(),
289 }
290 }
291}
292
293impl Future for BackupProvider {
294 type Output = Result<()>;
295
296 fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
298 Pin::new(&mut self.handle).poll(cx)?
299 }
300}
301
302pub async fn get_backup2(
303 context: &Context,
304 node_addr: iroh::NodeAddr,
305 auth_token: String,
306) -> Result<()> {
307 let relay_mode = RelayMode::Disabled;
308
309 let mut transport_config = iroh::endpoint::TransportConfig::default();
310 transport_config.max_idle_timeout(Some(Duration::from_secs(60).try_into()?));
311 let endpoint = Endpoint::builder()
312 .tls_x509() .relay_mode(relay_mode)
314 .transport_config(transport_config)
315 .bind()
316 .await?;
317
318 let conn = endpoint.connect(node_addr, BACKUP_ALPN).await?;
319 let (mut send_stream, mut recv_stream) = conn.open_bi().await?;
320 info!(context, "Sending backup authentication token.");
321 send_stream.write_all(auth_token.as_bytes()).await?;
322
323 let passphrase = String::new();
324 info!(context, "Starting to read backup from the stream.");
325
326 let mut file_size_buf = [0u8; 8];
327 recv_stream.read_exact(&mut file_size_buf).await?;
328 let file_size = u64::from_be_bytes(file_size_buf);
329 info!(context, "Received backup file size.");
330 context.emit_event(EventType::ImexProgress(1));
332
333 import_backup_stream(context, recv_stream, file_size, passphrase)
334 .await
335 .context("Failed to import backup from QUIC stream")?;
336 info!(context, "Finished importing backup from the stream.");
337 context.emit_event(EventType::ImexProgress(1000));
338
339 send_stream.write_all(b".").await.ok();
342 send_stream.finish().ok();
343 info!(context, "Sent backup reception acknowledgment.");
344
345 _ = send_stream.stopped().await;
348
349 Ok(())
350}
351
352pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
364 match qr {
365 Qr::Backup2 {
366 node_addr,
367 auth_token,
368 } => {
369 let cancel_token = context.alloc_ongoing().await?;
370 let res = get_backup2(context, node_addr, auth_token)
371 .race(async {
372 cancel_token.recv().await.ok();
373 Err(format_err!("Backup reception canceled"))
374 })
375 .await;
376 if let Err(ref res) = res {
377 error!(context, "{:#}", res);
378 context.emit_event(EventType::ImexProgress(0));
379 }
380 context.free_ongoing().await;
381 res?;
382 }
383 _ => bail!("QR code for backup must be of type DCBACKUP2"),
384 }
385 Ok(())
386}
387
388#[cfg(test)]
389mod tests {
390 use std::time::Duration;
391
392 use crate::chat::{ChatItem, get_chat_msgs, send_msg};
393 use crate::message::Viewtype;
394 use crate::test_utils::TestContextManager;
395
396 use super::*;
397
398 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
399 async fn test_send_receive() {
400 let mut tcm = TestContextManager::new();
401
402 let ctx0 = tcm.alice().await;
404
405 let self_chat = ctx0.get_self_chat().await;
407 let mut msg = Message::new_text("hi there".to_string());
408 send_msg(&ctx0, self_chat.id, &mut msg).await.unwrap();
409
410 let file = ctx0.get_blobdir().join("hello.txt");
412 fs::write(&file, "i am attachment").await.unwrap();
413 let mut msg = Message::new(Viewtype::File);
414 msg.set_file_and_deduplicate(&ctx0, &file, Some("hello.txt"), Some("text/plain"))
415 .unwrap();
416 send_msg(&ctx0, self_chat.id, &mut msg).await.unwrap();
417
418 let provider = BackupProvider::prepare(&ctx0).await.unwrap();
420
421 let ctx1 = tcm.unconfigured().await;
423 get_backup(&ctx1, provider.qr()).await.unwrap();
424
425 tokio::time::timeout(Duration::from_secs(30), provider)
427 .await
428 .expect("timed out")
429 .expect("error in provider");
430
431 let self_chat = ctx1.get_self_chat().await;
433 let msgs = get_chat_msgs(&ctx1, self_chat.id).await.unwrap();
434 assert_eq!(msgs.len(), 2);
435 let ChatItem::Message { msg_id } = msgs.first().unwrap() else {
436 panic!("wrong chat item");
437 };
438 let msg = Message::load_from_db(&ctx1, *msg_id).await.unwrap();
439 let text = msg.get_text();
440 assert_eq!(text, "hi there");
441 let ChatItem::Message { msg_id } = msgs.get(1).unwrap() else {
442 panic!("wrong chat item");
443 };
444 let msg = Message::load_from_db(&ctx1, *msg_id).await.unwrap();
445
446 let path = msg.get_file(&ctx1).unwrap();
447 assert_eq!(
448 path.with_file_name("ac1d2d284757656a8d41dc40aae4136.txt"),
450 path
451 );
452 assert_eq!("hello.txt", msg.get_filename().unwrap());
453 let text = fs::read_to_string(&path).await.unwrap();
454 assert_eq!(text, "i am attachment");
455
456 let path = path.with_file_name("saved.txt");
457 msg.save_file(&ctx1, &path).await.unwrap();
458 let text = fs::read_to_string(&path).await.unwrap();
459 assert_eq!(text, "i am attachment");
460 assert!(msg.save_file(&ctx1, &path).await.is_err());
461
462 for ctx in [&ctx0, &ctx1] {
464 ctx.evtracker
465 .get_matching(|ev| matches!(ev, EventType::ImexProgress(1)))
466 .await;
467 ctx.evtracker
468 .get_matching(|ev| matches!(ev, EventType::ImexProgress(1000)))
469 .await;
470 }
471 }
472
473 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
476 async fn test_cant_overwrite_profile_in_use() -> Result<()> {
477 let mut tcm = TestContextManager::new();
478 let ctx0 = &tcm.alice().await;
479 let ctx1 = &tcm.bob().await;
480
481 let provider = BackupProvider::prepare(ctx0).await?;
483
484 let err = get_backup(ctx1, provider.qr()).await.unwrap_err();
486 assert!(format!("{err:#}").contains("Cannot import backups to accounts in use"));
487
488 provider.await.unwrap();
490 ctx0.evtracker
491 .get_matching(|e| matches!(e, EventType::Error(_)))
492 .await;
493
494 assert_eq!(ctx1.get_primary_self_addr().await?, "bob@example.net");
495
496 Ok(())
497 }
498
499 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
500 async fn test_drop_provider() {
501 let mut tcm = TestContextManager::new();
502 let ctx = tcm.alice().await;
503
504 let provider = BackupProvider::prepare(&ctx).await.unwrap();
505 drop(provider);
506 ctx.evtracker
507 .get_matching(|ev| matches!(ev, EventType::ImexProgress(0)))
508 .await;
509 }
510}