diff --git a/backend/src/backup/restore.rs b/backend/src/backup/restore.rs index baee2304c..ff704f3b2 100644 --- a/backend/src/backup/restore.rs +++ b/backend/src/backup/restore.rs @@ -23,13 +23,14 @@ use crate::db::model::{PackageDataEntry, StaticFiles}; use crate::disk::mount::backup::{BackupMountGuard, PackageBackupMountGuard}; use crate::disk::mount::filesystem::ReadOnly; use crate::disk::mount::guard::TmpMountGuard; +use crate::hostname::{get_hostname, Hostname}; use crate::install::progress::InstallProgress; use crate::install::{download_install_s9pk, PKG_PUBLIC_DIR}; use crate::net::ssl::SslManager; use crate::notifications::NotificationLevel; use crate::s9pk::manifest::{Manifest, PackageId}; use crate::s9pk::reader::S9pkReader; -use crate::setup::RecoveryStatus; +use crate::setup::SetupStatus; use crate::util::display_none; use crate::util::io::dir_size; use crate::util::serde::IoFormat; @@ -140,7 +141,7 @@ async fn approximate_progress_loop( tracing::error!("Failed to approximate restore progress: {}", e); tracing::debug!("{:?}", e); } else { - *ctx.recovery_status.write().await = Some(Ok(starting_info.flatten())); + *ctx.setup_status.write().await = Some(Ok(starting_info.flatten())); } tokio::time::sleep(Duration::from_secs(1)).await; } @@ -153,7 +154,7 @@ struct ProgressInfo { target_volume_size: BTreeMap, } impl ProgressInfo { - fn flatten(&self) -> RecoveryStatus { + fn flatten(&self) -> SetupStatus { let mut total_bytes = 0; let mut bytes_transferred = 0; @@ -176,7 +177,7 @@ impl ProgressInfo { bytes_transferred = total_bytes; } - RecoveryStatus { + SetupStatus { total_bytes, bytes_transferred, complete: false, @@ -191,7 +192,7 @@ pub async fn recover_full_embassy( embassy_password: String, recovery_source: TmpMountGuard, recovery_password: Option, -) -> Result<(OnionAddressV3, X509, BoxFuture<'static, Result<(), Error>>), Error> { +) -> Result<(Arc, Hostname, OnionAddressV3, X509), Error> { let backup_guard = BackupMountGuard::mount( recovery_source, recovery_password.as_deref().unwrap_or_default(), @@ -232,67 +233,69 @@ pub async fn recover_full_embassy( .await?; secret_store.close().await; + let rpc_ctx = RpcContext::init(ctx.config_path.clone(), disk_guid.clone()).await?; + let mut db = rpc_ctx.db.handle(); + + let receipts = crate::hostname::HostNameReceipt::new(&mut db).await?; + let hostname = get_hostname(&mut db, &receipts).await?; + + drop(db); + let mut db = rpc_ctx.db.handle(); + + let ids = backup_guard + .metadata + .package_backups + .keys() + .cloned() + .collect(); + let (backup_guard, tasks, progress_info) = + restore_packages(&rpc_ctx, &mut db, backup_guard, ids).await?; + + tokio::select! { + res = futures::future::join_all(tasks) => { + for res in res { + match res.with_kind(crate::ErrorKind::Unknown) { + Ok((Ok(_), _)) => (), + Ok((Err(err), package_id)) => { + if let Err(err) = rpc_ctx.notification_manager.notify( + &mut db, + Some(package_id.clone()), + NotificationLevel::Error, + "Restoration Failure".to_string(), format!("Error restoring package {}: {}", package_id,err), (), None).await{ + tracing::error!("Failed to notify: {}", err); + tracing::debug!("{:?}", err); + }; + tracing::error!("Error restoring package {}: {}", package_id, err); + tracing::debug!("{:?}", err); + }, + Err(e) => { + if let Err(err) = rpc_ctx.notification_manager.notify( + &mut db, + None, + NotificationLevel::Error, + "Restoration Failure".to_string(), format!("Error during restoration: {}", e), (), None).await { + + tracing::error!("Failed to notify: {}", err); + tracing::debug!("{:?}", err); + } + tracing::error!("Error restoring packages: {}", e); + tracing::debug!("{:?}", e); + }, + + } + } + }, + _ = approximate_progress_loop(&ctx, &rpc_ctx, progress_info) => unreachable!(concat!(module_path!(), "::approximate_progress_loop should not terminate")), + } + + backup_guard.unmount().await?; + rpc_ctx.shutdown().await?; + Ok(( + disk_guid, + hostname, os_backup.tor_key.public().get_onion_address(), os_backup.root_ca_cert, - async move { - let rpc_ctx = RpcContext::init(ctx.config_path.clone(), disk_guid).await?; - let mut db = rpc_ctx.db.handle(); - - let ids = backup_guard - .metadata - .package_backups - .keys() - .cloned() - .collect(); - let (backup_guard, tasks, progress_info) = restore_packages( - &rpc_ctx, - &mut db, - backup_guard, - ids, - ) - .await?; - - tokio::select! { - res = futures::future::join_all(tasks) => { - for res in res { - match res.with_kind(crate::ErrorKind::Unknown) { - Ok((Ok(_), _)) => (), - Ok((Err(err), package_id)) => { - if let Err(err) = rpc_ctx.notification_manager.notify( - &mut db, - Some(package_id.clone()), - NotificationLevel::Error, - "Restoration Failure".to_string(), format!("Error restoring package {}: {}", package_id,err), (), None).await{ - tracing::error!("Failed to notify: {}", err); - tracing::debug!("{:?}", err); - }; - tracing::error!("Error restoring package {}: {}", package_id, err); - tracing::debug!("{:?}", err); - }, - Err(e) => { - if let Err(err) = rpc_ctx.notification_manager.notify( - &mut db, - None, - NotificationLevel::Error, - "Restoration Failure".to_string(), format!("Error during restoration: {}", e), (), None).await { - - tracing::error!("Failed to notify: {}", err); - tracing::debug!("{:?}", err); - } - tracing::error!("Error restoring packages: {}", e); - tracing::debug!("{:?}", e); - }, - - } - } - }, - _ = approximate_progress_loop(&ctx, &rpc_ctx, progress_info) => unreachable!(concat!(module_path!(), "::approximate_progress_loop should not terminate")), - } - - backup_guard.unmount().await?; - rpc_ctx.shutdown().await - }.boxed() )) } diff --git a/backend/src/bin/embassy-init.rs b/backend/src/bin/embassy-init.rs index 0b10b3a1e..507908881 100644 --- a/backend/src/bin/embassy-init.rs +++ b/backend/src/bin/embassy-init.rs @@ -102,6 +102,15 @@ async fn setup_or_init(cfg_path: Option) -> Result<(), Error> { .await .expect("context dropped"); setup_http_server.shutdown.send(()).unwrap(); + tokio::task::yield_now().await; + if let Err(e) = Command::new("killall") + .arg("firefox-esr") + .invoke(ErrorKind::NotFound) + .await + { + tracing::error!("Failed to kill kiosk: {}", e); + tracing::debug!("{:?}", e); + } } else { let cfg = RpcContextConfig::load(cfg_path).await?; let guid_string = tokio::fs::read_to_string("/media/embassy/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy diff --git a/backend/src/context/setup.rs b/backend/src/context/setup.rs index 8a66c72f5..cf64bd51c 100644 --- a/backend/src/context/setup.rs +++ b/backend/src/context/setup.rs @@ -18,7 +18,7 @@ use crate::db::model::Database; use crate::disk::OsPartitionInfo; use crate::init::{init_postgres, pgloader}; use crate::net::tor::os_key; -use crate::setup::{password_hash, RecoveryStatus}; +use crate::setup::{password_hash, SetupStatus}; use crate::util::config::load_config_from_paths; use crate::{Error, ResultExt}; @@ -76,7 +76,7 @@ pub struct SetupContextSeed { pub current_secret: Arc, pub selected_v2_drive: RwLock>, pub cached_product_key: RwLock>>, - pub recovery_status: RwLock>>, + pub setup_status: RwLock>>, pub setup_result: RwLock, SetupResult)>>, } @@ -114,7 +114,7 @@ impl SetupContext { ), selected_v2_drive: RwLock::new(None), cached_product_key: RwLock::new(None), - recovery_status: RwLock::new(None), + setup_status: RwLock::new(None), setup_result: RwLock::new(None), }))) } diff --git a/backend/src/init.rs b/backend/src/init.rs index 43e3bdd35..b6aed62ac 100644 --- a/backend/src/init.rs +++ b/backend/src/init.rs @@ -6,6 +6,7 @@ use std::time::Duration; use color_eyre::eyre::eyre; use helpers::NonDetachingJoinHandle; use patch_db::{DbHandle, LockReceipt, LockType}; +use sqlx::{Pool, Postgres}; use tokio::process::Command; use crate::context::rpc::RpcContextConfig; @@ -190,6 +191,7 @@ pub async fn init_postgres(datadir: impl AsRef) -> Result<(), Error> { } pub struct InitResult { + pub secret_store: Pool, pub db: patch_db::PatchDb, } @@ -360,5 +362,5 @@ pub async fn init(cfg: &RpcContextConfig) -> Result { tracing::info!("System initialized."); - Ok(InitResult { db }) + Ok(InitResult { secret_store, db }) } diff --git a/backend/src/setup.rs b/backend/src/setup.rs index e8373363a..64a670a8d 100644 --- a/backend/src/setup.rs +++ b/backend/src/setup.rs @@ -2,8 +2,7 @@ use std::path::PathBuf; use std::sync::Arc; use color_eyre::eyre::eyre; -use futures::future::BoxFuture; -use futures::{StreamExt, TryFutureExt}; +use futures::StreamExt; use helpers::{Rsync, RsyncOptions}; use josekit::jwk::Jwk; use openssl::x509::X509; @@ -30,10 +29,9 @@ use crate::disk::mount::guard::TmpMountGuard; use crate::disk::util::{pvscan, recovery_info, DiskInfo, EmbassyOsRecoveryInfo}; use crate::disk::REPAIR_DISK_PATH; use crate::hostname::{get_hostname, HostNameReceipt, Hostname}; -use crate::init::init; +use crate::init::{init, InitResult}; use crate::middleware::encrypt::EncryptedWire; use crate::net::ssl::SslManager; -use crate::sound::BEETHOVEN; use crate::{Error, ErrorKind, ResultExt}; #[instrument(skip(secrets))] @@ -49,24 +47,11 @@ where Ok(password) } -#[command(subcommands(status, disk, attach, execute, recovery, cifs, complete, get_pubkey))] +#[command(subcommands(status, disk, attach, execute, cifs, complete, get_pubkey, exit))] pub fn setup() -> Result<(), Error> { Ok(()) } -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct StatusRes { - migrating: bool, -} - -#[command(rpc_only, metadata(authenticated = false))] -pub async fn status(#[context] ctx: SetupContext) -> Result { - Ok(StatusRes { - migrating: ctx.recovery_status.read().await.is_some(), - }) -} - #[command(subcommands(list_disks))] pub fn disk() -> Result<(), Error> { Ok(()) @@ -81,9 +66,9 @@ async fn setup_init( ctx: &SetupContext, password: Option, ) -> Result<(Hostname, OnionAddressV3, X509), Error> { - let secrets = ctx.secret_store().await?; - let db = ctx.db(&secrets).await?; - let mut secrets_handle = secrets.acquire().await?; + let InitResult { secret_store, db } = + init(&RpcContextConfig::load(ctx.config_path.clone()).await?).await?; + let mut secrets_handle = secret_store.acquire().await?; let mut db_handle = db.handle(); let mut secrets_tx = secrets_handle.begin().await?; let mut db_tx = db_handle.begin().await?; @@ -107,7 +92,7 @@ async fn setup_init( let hostname_receipts = HostNameReceipt::new(&mut db_handle).await?; let hostname = get_hostname(&mut db_handle, &hostname_receipts).await?; - let (_, root_ca) = SslManager::init(secrets, &mut db_handle) + let (_, root_ca) = SslManager::init(secret_store, &mut db_handle) .await? .export_root_ca() .await?; @@ -119,73 +104,91 @@ pub async fn attach( #[context] ctx: SetupContext, #[arg] guid: Arc, #[arg(rename = "embassy-password")] password: Option, -) -> Result { - let password: Option = match password { - Some(a) => match a.decrypt(&*ctx) { - a @ Some(_) => a, - None => { - return Err(Error::new( - color_eyre::eyre::eyre!("Couldn't decode password"), - crate::ErrorKind::Unknown, - )); - } - }, - None => None, - }; - let requires_reboot = crate::disk::main::import( - &*guid, - &ctx.datadir, - if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() { - RepairStrategy::Aggressive - } else { - RepairStrategy::Preen - }, - DEFAULT_PASSWORD, - ) - .await?; - if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() { - tokio::fs::remove_file(REPAIR_DISK_PATH) - .await - .with_ctx(|_| (ErrorKind::Filesystem, REPAIR_DISK_PATH))?; - } - if requires_reboot.0 { - crate::disk::main::export(&*guid, &ctx.datadir).await?; +) -> Result<(), Error> { + let mut status = ctx.setup_status.write().await; + if status.is_some() { return Err(Error::new( - eyre!( - "Errors were corrected with your disk, but the Embassy must be restarted in order to proceed" - ), - ErrorKind::DiskManagement, + eyre!("Setup already in progress"), + ErrorKind::InvalidRequest, )); } - let (hostname, tor_addr, root_ca) = setup_init(&ctx, password).await?; - init(&RpcContextConfig::load(ctx.config_path.clone()).await?).await?; - let setup_result = SetupResult { - tor_address: format!("http://{}", tor_addr), - lan_address: hostname.lan_address(), - root_ca: String::from_utf8(root_ca.to_pem()?)?, - }; - *ctx.setup_result.write().await = Some((guid, setup_result.clone())); - Ok(setup_result) -} - -#[command(subcommands(recovery_status))] -pub fn recovery() -> Result<(), Error> { + *status = Some(Ok(SetupStatus { + bytes_transferred: 0, + total_bytes: 0, + complete: false, + })); + drop(status); + tokio::task::spawn(async move { + if let Err(e) = async { + let password: Option = match password { + Some(a) => match a.decrypt(&*ctx) { + a @ Some(_) => a, + None => { + return Err(Error::new( + color_eyre::eyre::eyre!("Couldn't decode password"), + crate::ErrorKind::Unknown, + )); + } + }, + None => None, + }; + let requires_reboot = crate::disk::main::import( + &*guid, + &ctx.datadir, + if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() { + RepairStrategy::Aggressive + } else { + RepairStrategy::Preen + }, + DEFAULT_PASSWORD, + ) + .await?; + if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() { + tokio::fs::remove_file(REPAIR_DISK_PATH) + .await + .with_ctx(|_| (ErrorKind::Filesystem, REPAIR_DISK_PATH))?; + } + if requires_reboot.0 { + crate::disk::main::export(&*guid, &ctx.datadir).await?; + return Err(Error::new( + eyre!( + "Errors were corrected with your disk, but the Embassy must be restarted in order to proceed" + ), + ErrorKind::DiskManagement, + )); + } + let (hostname, tor_addr, root_ca) = setup_init(&ctx, password).await?; + *ctx.setup_result.write().await = Some((guid, SetupResult { + tor_address: format!("http://{}", tor_addr), + lan_address: hostname.lan_address(), + root_ca: String::from_utf8(root_ca.to_pem()?)?, + })); + *ctx.setup_status.write().await = Some(Ok(SetupStatus { + bytes_transferred: 0, + total_bytes: 0, + complete: true, + })); + Ok(()) + }.await { + tracing::error!("Error Setting Up Embassy: {}", e); + tracing::debug!("{:?}", e); + *ctx.setup_status.write().await = Some(Err(e.into())); + } + }); Ok(()) } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] -pub struct RecoveryStatus { +pub struct SetupStatus { pub bytes_transferred: u64, pub total_bytes: u64, pub complete: bool, } -#[command(rename = "status", rpc_only, metadata(authenticated = false))] -pub async fn recovery_status( - #[context] ctx: SetupContext, -) -> Result, RpcError> { - ctx.recovery_status.read().await.clone().transpose() +#[command(rpc_only, metadata(authenticated = false))] +pub async fn status(#[context] ctx: SetupContext) -> Result, RpcError> { + ctx.setup_status.read().await.clone().transpose() } /// We want to be able to get a secret, a shared private key with the frontend @@ -243,7 +246,7 @@ pub async fn execute( #[arg(rename = "embassy-password")] embassy_password: EncryptedWire, #[arg(rename = "recovery-source")] recovery_source: Option, #[arg(rename = "recovery-password")] recovery_password: Option, -) -> Result { +) -> Result<(), Error> { let embassy_password = match embassy_password.decrypt(&*ctx) { Some(a) => a, None => { @@ -265,29 +268,56 @@ pub async fn execute( }, None => None, }; - match execute_inner( - ctx.clone(), - embassy_logicalname, - embassy_password, - recovery_source, - recovery_password, - ) - .await - { - Ok((hostname, tor_addr, root_ca)) => { - tracing::info!("Setup Successful! Tor Address: {}", tor_addr); - Ok(SetupResult { - tor_address: format!("http://{}", tor_addr), - lan_address: hostname.lan_address(), - root_ca: String::from_utf8(root_ca.to_pem()?)?, - }) - } - Err(e) => { - tracing::error!("Error Setting Up Embassy: {}", e); - tracing::debug!("{:?}", e); - Err(e) - } + let mut status = ctx.setup_status.write().await; + if status.is_some() { + return Err(Error::new( + eyre!("Setup already in progress"), + ErrorKind::InvalidRequest, + )); } + *status = Some(Ok(SetupStatus { + bytes_transferred: 0, + total_bytes: 0, + complete: false, + })); + drop(status); + tokio::task::spawn(async move { + match execute_inner( + ctx.clone(), + embassy_logicalname, + embassy_password, + recovery_source, + recovery_password, + ) + .await + { + Ok((guid, hostname, tor_addr, root_ca)) => { + tracing::info!("Setup Complete!"); + *ctx.setup_result.write().await = Some(( + guid, + SetupResult { + tor_address: format!("http://{}", tor_addr), + lan_address: hostname.lan_address(), + root_ca: String::from_utf8( + root_ca.to_pem().expect("failed to serialize root ca"), + ) + .expect("invalid pem string"), + }, + )); + *ctx.setup_status.write().await = Some(Ok(SetupStatus { + bytes_transferred: 0, + total_bytes: 0, + complete: true, + })); + } + Err(e) => { + tracing::error!("Error Setting Up Embassy: {}", e); + tracing::debug!("{:?}", e); + *ctx.setup_status.write().await = Some(Err(e.into())); + } + } + }); + Ok(()) } #[instrument(skip(ctx))] @@ -301,23 +331,19 @@ pub async fn complete(#[context] ctx: SetupContext) -> Result Result<(), Error> { + ctx.shutdown.send(()).expect("failed to shutdown"); + Ok(()) +} + #[instrument(skip(ctx, embassy_password, recovery_password))] pub async fn execute_inner( ctx: SetupContext, @@ -325,13 +351,7 @@ pub async fn execute_inner( embassy_password: String, recovery_source: Option, recovery_password: Option, -) -> Result<(Hostname, OnionAddressV3, X509), Error> { - if ctx.recovery_status.read().await.is_some() { - return Err(Error::new( - eyre!("Cannot execute setup while in recovery!"), - crate::ErrorKind::InvalidRequest, - )); - } +) -> Result<(Arc, Hostname, OnionAddressV3, X509), Error> { let guid = Arc::new( crate::disk::main::create( &[embassy_logicalname], @@ -349,161 +369,20 @@ pub async fn execute_inner( ) .await?; - let res = if let Some(RecoverySource::Backup { target }) = recovery_source { - let (tor_addr, root_ca, recover_fut) = recover( - ctx.clone(), - guid.clone(), - embassy_password, - target, - recovery_password, - ) - .await?; - let db = init(&RpcContextConfig::load(ctx.config_path.clone()).await?) - .await? - .db; - let hostname = { - let mut handle = db.handle(); - let receipts = crate::hostname::HostNameReceipt::new(&mut handle).await?; - get_hostname(&mut handle, &receipts).await? - }; - let res = (hostname.clone(), tor_addr, root_ca.clone()); - tokio::spawn(async move { - if let Err(e) = recover_fut - .and_then(|_| async { - *ctx.setup_result.write().await = Some(( - guid, - SetupResult { - tor_address: format!("http://{}", tor_addr), - lan_address: hostname.lan_address(), - root_ca: String::from_utf8(root_ca.to_pem()?)?, - }, - )); - if let Some(Ok(recovery_status)) = &mut *ctx.recovery_status.write().await { - recovery_status.complete = true; - } - Ok(()) - }) - .await - { - (&BEETHOVEN).play().await.unwrap_or_default(); // ignore error in playing the song - tracing::error!("Error recovering drive!: {}", e); - tracing::debug!("{:?}", e); - *ctx.recovery_status.write().await = Some(Err(e.into())); - } else { - tracing::info!("Recovery Complete!"); - } - }); - res + if let Some(RecoverySource::Backup { target }) = recovery_source { + recover(ctx, guid, embassy_password, target, recovery_password).await } else if let Some(RecoverySource::Migrate { guid: old_guid }) = recovery_source { - let _ = crate::disk::main::mount_fs( - &old_guid, - "/media/embassy/migrate", - "main", - RepairStrategy::Preen, - DEFAULT_PASSWORD, - ) - .await?; - Rsync::new( - "/media/embassy/migrate/main", - "/embassy-data/main", - RsyncOptions { - delete: true, - force: true, - ignore_existing: false, - exclude: Vec::new(), - }, - )? - .wait() - .await?; - let _ = crate::disk::main::mount_fs( - &old_guid, - "/media/embassy/migrate", - "package-data", - RepairStrategy::Preen, - DEFAULT_PASSWORD, - ) - .await?; - let mut package_data_transfer = Rsync::new( - "/media/embassy/migrate/package-data", - "/embassy-data/package-data", - RsyncOptions { - delete: true, - force: true, - ignore_existing: false, - exclude: vec!["tmp".to_owned()], - }, - )?; - *ctx.recovery_status.write().await = Some(Ok(RecoveryStatus { - bytes_transferred: 0, - total_bytes: 100, - complete: false, - })); - let (hostname, tor_addr, root_ca) = setup_init(&ctx, Some(embassy_password)).await?; - let res = (hostname.clone(), tor_addr.clone(), root_ca.clone()); - tokio::spawn(async move { - if let Err(e) = async { - while let Some(progress) = package_data_transfer.progress.next().await { - *ctx.recovery_status.write().await = Some(Ok(RecoveryStatus { - bytes_transferred: (progress * 100.0) as u64, - total_bytes: 100, - complete: false, - })); - } - package_data_transfer.wait().await?; - init(&RpcContextConfig::load(ctx.config_path.clone()).await?).await?; - Ok::<_, Error>(()) - } - .and_then(|_| async { - *ctx.setup_result.write().await = Some(( - guid, - SetupResult { - tor_address: format!("http://{}", tor_addr), - lan_address: hostname.lan_address(), - root_ca: String::from_utf8(root_ca.to_pem()?)?, - }, - )); - if let Some(Ok(recovery_status)) = &mut *ctx.recovery_status.write().await { - recovery_status.complete = true; - } - Ok(()) - }) - .await - { - (&BEETHOVEN).play().await.unwrap_or_default(); // ignore error in playing the song - tracing::error!("Error recovering drive!: {}", e); - tracing::debug!("{:?}", e); - *ctx.recovery_status.write().await = Some(Err(e.into())); - } else { - tracing::info!("Recovery Complete!"); - } - }); - res + migrate(ctx, guid, &old_guid, embassy_password).await } else { - let (tor_addr, root_ca) = fresh_setup(&ctx, &embassy_password).await?; - let db = init(&RpcContextConfig::load(ctx.config_path.clone()).await?) - .await? - .db; - let mut handle = db.handle(); - let receipts = crate::hostname::HostNameReceipt::new(&mut handle).await?; - *ctx.setup_result.write().await = Some(( - guid, - SetupResult { - tor_address: format!("http://{}", tor_addr), - lan_address: get_hostname(&mut handle, &receipts).await?.lan_address(), - root_ca: String::from_utf8(root_ca.to_pem()?)?, - }, - )); - let hostname = get_hostname(&mut handle, &receipts).await?; - (hostname, tor_addr, root_ca) - }; - - Ok(res) + let (hostname, tor_addr, root_ca) = fresh_setup(&ctx, &embassy_password).await?; + Ok((guid, hostname, tor_addr, root_ca)) + } } async fn fresh_setup( ctx: &SetupContext, embassy_password: &str, -) -> Result<(OnionAddressV3, X509), Error> { +) -> Result<(Hostname, OnionAddressV3, X509), Error> { let password = argon2::hash_encoded( embassy_password.as_bytes(), &rand::random::<[u8; 16]>()[..], @@ -521,13 +400,18 @@ async fn fresh_setup( ) .execute(&mut sqlite_pool.acquire().await?) .await?; - let db = ctx.db(&sqlite_pool).await?; - let (_, root_ca) = SslManager::init(sqlite_pool.clone(), &mut db.handle()) + sqlite_pool.close().await; + let InitResult { secret_store, db } = + init(&RpcContextConfig::load(ctx.config_path.clone()).await?).await?; + let mut handle = db.handle(); + let receipts = crate::hostname::HostNameReceipt::new(&mut handle).await?; + let hostname = get_hostname(&mut handle, &receipts).await?; + let (_, root_ca) = SslManager::init(secret_store.clone(), &mut handle) .await? .export_root_ca() .await?; - sqlite_pool.close().await; - Ok((tor_key.public().get_onion_address(), root_ca)) + secret_store.close().await; + Ok((hostname, tor_key.public().get_onion_address(), root_ca)) } #[instrument(skip(ctx, embassy_password, recovery_password))] @@ -537,7 +421,7 @@ async fn recover( embassy_password: String, recovery_source: BackupTargetFS, recovery_password: Option, -) -> Result<(OnionAddressV3, X509, BoxFuture<'static, Result<(), Error>>), Error> { +) -> Result<(Arc, Hostname, OnionAddressV3, X509), Error> { let recovery_source = TmpMountGuard::mount(&recovery_source, ReadOnly).await?; recover_full_embassy( ctx.clone(), @@ -548,3 +432,75 @@ async fn recover( ) .await } + +#[instrument(skip(ctx, embassy_password))] +async fn migrate( + ctx: SetupContext, + guid: Arc, + old_guid: &str, + embassy_password: String, +) -> Result<(Arc, Hostname, OnionAddressV3, X509), Error> { + *ctx.setup_status.write().await = Some(Ok(SetupStatus { + bytes_transferred: 0, + total_bytes: 110, + complete: false, + })); + let _ = crate::disk::main::mount_fs( + &old_guid, + "/media/embassy/migrate", + "main", + RepairStrategy::Preen, + DEFAULT_PASSWORD, + ) + .await?; + let mut main_transfer = Rsync::new( + "/media/embassy/migrate/main", + "/embassy-data/main", + RsyncOptions { + delete: true, + force: true, + ignore_existing: false, + exclude: Vec::new(), + }, + )?; + while let Some(progress) = main_transfer.progress.next().await { + *ctx.setup_status.write().await = Some(Ok(SetupStatus { + bytes_transferred: (progress * 10.0) as u64, + total_bytes: 110, + complete: false, + })); + } + main_transfer.wait().await?; + crate::disk::main::unmount_fs(&old_guid, "/media/embassy/migrate", "main").await?; + let _ = crate::disk::main::mount_fs( + &old_guid, + "/media/embassy/migrate", + "package-data", + RepairStrategy::Preen, + DEFAULT_PASSWORD, + ) + .await?; + let mut package_data_transfer = Rsync::new( + "/media/embassy/migrate/package-data", + "/embassy-data/package-data", + RsyncOptions { + delete: true, + force: true, + ignore_existing: false, + exclude: vec!["tmp".to_owned()], + }, + )?; + while let Some(progress) = package_data_transfer.progress.next().await { + *ctx.setup_status.write().await = Some(Ok(SetupStatus { + bytes_transferred: 10 + (progress * 100.0) as u64, + total_bytes: 110, + complete: false, + })); + } + package_data_transfer.wait().await?; + crate::disk::main::unmount_fs(&old_guid, "/media/embassy/migrate", "package-data").await?; + + let (hostname, tor_addr, root_ca) = setup_init(&ctx, Some(embassy_password)).await?; + + Ok((guid, hostname, tor_addr, root_ca)) +} diff --git a/build/lib/scripts/enable-kiosk b/build/lib/scripts/enable-kiosk index fef95bae4..140a2fc2d 100755 --- a/build/lib/scripts/enable-kiosk +++ b/build/lib/scripts/enable-kiosk @@ -10,10 +10,8 @@ apt install --no-install-recommends -y xserver-xorg x11-xserver-utils xinit fire cat > /home/start9/kiosk.sh << 'EOF' #!/bin/sh PROFILE=$(mktemp -d) -protocol=http if [ -f /usr/local/share/ca-certificates/embassy-root-ca.crt ]; then certutil -A -n "Embassy Local Root CA" -t "TCu,Cuw,Tuw" -i /usr/local/share/ca-certificates/embassy-root-ca.crt -d $PROFILE - protocol=https fi cat >> $PROFILE/prefs.js << EOT user_pref("network.proxy.autoconfig_url", "file:///usr/lib/embassy/proxy.pac"); @@ -22,12 +20,16 @@ user_pref("network.proxy.type", 2); user_pref("dom.securecontext.allowlist_onions", true); user_pref("dom.securecontext.whitelist_onions", true); user_pref("signon.rememberSignons", false); +user_pref("extensions.activeThemeID", "firefox-compact-dark@mozilla.org"); +user_pref("browser.theme.content-theme", 0); +user_pref("browser.theme.toolbar-theme", 0); +user_pref("datareporting.policy.firstRunURL", ""); EOT -while ! curl "${protocol}://$(hostname).local" > /dev/null; do +while ! curl "http://localhost" > /dev/null; do sleep 1 done -matchbox-window-manager -use_titlebar yes & -firefox-esr --kiosk ${protocol}://$(hostname).local --profile $PROFILE +matchbox-window-manager -use_titlebar no & +firefox-esr http://localhost --profile $PROFILE rm -rf $PROFILE EOF chmod +x /home/start9/kiosk.sh diff --git a/frontend/projects/install-wizard/src/app/pages/home/home.page.ts b/frontend/projects/install-wizard/src/app/pages/home/home.page.ts index 4d77bbcc4..d9c04cd21 100644 --- a/frontend/projects/install-wizard/src/app/pages/home/home.page.ts +++ b/frontend/projects/install-wizard/src/app/pages/home/home.page.ts @@ -47,27 +47,24 @@ export class HomePage { } async tryInstall(overwrite: boolean) { - if (!this.selectedDisk) return - - const { logicalname, guid } = this.selectedDisk - - const hasEmbassyData = !!guid - - if (hasEmbassyData && !overwrite) { - return this.install(logicalname, overwrite) + if (overwrite) { + return this.presentAlertDanger() } - await this.presentAlertDanger(logicalname, hasEmbassyData) + this.install(false) } - private async install(logicalname: string, overwrite: boolean) { + private async install(overwrite: boolean) { const loader = await this.loadingCtrl.create({ message: 'Installing embassyOS...', }) await loader.present() try { - await this.api.install({ logicalname, overwrite }) + await this.api.install({ + logicalname: this.selectedDisk!.logicalname, + overwrite, + }) this.presentAlertReboot() } catch (e: any) { this.error = e.message @@ -76,17 +73,14 @@ export class HomePage { } } - private async presentAlertDanger( - logicalname: string, - hasEmbassyData: boolean, - ) { - const message = hasEmbassyData - ? 'This action COMPLETELY erases your existing Embassy data' - : `This action COMPLETELY erases the disk ${logicalname} and installs embassyOS` + private async presentAlertDanger() { + const { vendor, model } = this.selectedDisk! const alert = await this.alertCtrl.create({ header: 'Warning', - message, + message: `This action will COMPLETELY erase the disk ${ + vendor || 'Unknown Vendor' + } - ${model || 'Unknown Model'} and install embassyOS in its place`, buttons: [ { text: 'Cancel', @@ -95,7 +89,7 @@ export class HomePage { { text: 'Continue', handler: () => { - this.install(logicalname, true) + this.install(true) }, }, ], diff --git a/frontend/projects/setup-wizard/src/app/app.component.ts b/frontend/projects/setup-wizard/src/app/app.component.ts index 5af215d99..e4bf41f5c 100644 --- a/frontend/projects/setup-wizard/src/app/app.component.ts +++ b/frontend/projects/setup-wizard/src/app/app.component.ts @@ -17,8 +17,14 @@ export class AppComponent { async ngOnInit() { try { - const { migrating } = await this.apiService.getStatus() - await this.navCtrl.navigateForward(migrating ? '/loading' : '/home') + const inProgress = await this.apiService.getStatus() + + let route = '/home' + if (inProgress) { + route = inProgress.complete ? '/success' : '/loading' + } + + await this.navCtrl.navigateForward(route) } catch (e: any) { this.errorToastService.present(e) } diff --git a/frontend/projects/setup-wizard/src/app/pages/attach/attach.page.ts b/frontend/projects/setup-wizard/src/app/pages/attach/attach.page.ts index a6fd69a09..b8e43a575 100644 --- a/frontend/projects/setup-wizard/src/app/pages/attach/attach.page.ts +++ b/frontend/projects/setup-wizard/src/app/pages/attach/attach.page.ts @@ -38,8 +38,7 @@ export class AttachPage { async getDrives() { try { - const drives = await this.apiService.getDrives() - this.drives = drives.filter(d => d.partitions.length) + this.drives = await this.apiService.getDrives() } catch (e: any) { this.errToastService.present(e) } finally { @@ -61,13 +60,11 @@ export class AttachPage { } private async attachDrive(guid: string, password: string) { - const loader = await this.loadingCtrl.create({ - message: 'Attaching Drive', - }) + const loader = await this.loadingCtrl.create() await loader.present() try { await this.stateService.importDrive(guid, password) - await this.navCtrl.navigateForward(`/success`) + await this.navCtrl.navigateForward(`/loading`) } catch (e: any) { this.errToastService.present(e) } finally { diff --git a/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts b/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts index 029787118..4d9aa9d2c 100644 --- a/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts +++ b/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts @@ -13,7 +13,6 @@ import { import { DiskInfo, ErrorToastService, GuidPipe } from '@start9labs/shared' import { StateService } from 'src/app/services/state.service' import { PasswordPage } from '../../modals/password/password.page' -import { ActivatedRoute } from '@angular/router' @Component({ selector: 'app-embassy', @@ -34,7 +33,6 @@ export class EmbassyPage { private readonly loadingCtrl: LoadingController, private readonly errorToastService: ErrorToastService, private readonly guidPipe: GuidPipe, - private route: ActivatedRoute, ) {} async ngOnInit() { @@ -133,21 +131,12 @@ export class EmbassyPage { logicalname: string, password: string, ): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Initializing data drive. This could take a while...', - }) - + const loader = await this.loadingCtrl.create() await loader.present() try { await this.stateService.setupEmbassy(logicalname, password) - if (!!this.stateService.recoverySource) { - await this.navCtrl.navigateForward(`/loading`, { - queryParams: { action: this.route.snapshot.paramMap.get('action') }, - }) - } else { - await this.navCtrl.navigateForward(`/success`) - } + await this.navCtrl.navigateForward(`/loading`) } catch (e: any) { this.errorToastService.present({ message: `${e.message}\n\nRestart Embassy to try again.`, diff --git a/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.html b/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.html index e94b8b329..5757244a6 100644 --- a/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.html +++ b/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.html @@ -8,11 +8,8 @@ - - Transferring - Recovering - - Initializing Embassy + Progress: {{ (stateService.dataProgress * 100).toFixed(0) }}% @@ -27,8 +24,10 @@ padding-bottom: 20px; margin-bottom: 40px; " + [type]="stateService.dataProgress ? 'determinate' : 'indeterminate'" [value]="stateService.dataProgress" > +

Setting up your Embassy. This can take a while.

diff --git a/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.scss b/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.scss index e69de29bb..87bfffa33 100644 --- a/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.scss +++ b/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.scss @@ -0,0 +1,3 @@ +ion-card-title { + font-size: 42px; +} \ No newline at end of file diff --git a/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.ts b/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.ts index e4d7e7cc0..658b9a55f 100644 --- a/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.ts +++ b/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.ts @@ -1,5 +1,4 @@ import { Component } from '@angular/core' -import { ActivatedRoute } from '@angular/router' import { NavController } from '@ionic/angular' import { StateService } from 'src/app/services/state.service' @@ -9,16 +8,12 @@ import { StateService } from 'src/app/services/state.service' styleUrls: ['loading.page.scss'], }) export class LoadingPage { - incomingAction!: string - constructor( public stateService: StateService, private navCtrl: NavController, - private route: ActivatedRoute, ) {} ngOnInit() { - this.incomingAction = this.route.snapshot.paramMap.get('action')! this.stateService.pollDataTransferProgress() const progSub = this.stateService.dataCompletionSubject.subscribe( async complete => { diff --git a/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts b/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts index c5ffe5e0a..7ae376ef2 100644 --- a/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts +++ b/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts @@ -118,9 +118,7 @@ export class RecoverPage { }, } this.stateService.recoveryPassword = password - this.navCtrl.navigateForward(`/embassy`, { - queryParams: { action: 'recover' }, - }) + this.navCtrl.navigateForward(`/embassy`) } } diff --git a/frontend/projects/setup-wizard/src/app/pages/success/success.page.html b/frontend/projects/setup-wizard/src/app/pages/success/success.page.html index 385935e6a..6adcabd03 100644 --- a/frontend/projects/setup-wizard/src/app/pages/success/success.page.html +++ b/frontend/projects/setup-wizard/src/app/pages/success/success.page.html @@ -2,245 +2,267 @@ - - - - Setup Complete - You have successfully claimed your Embassy! -
-
- -
-
-

- You can now safely unplug your backup drive. + + + + + Setup Complete + You will be redirected momentarily - You can now safely unplug your old drive. -

-

- Access your Embassy using the methods below. You should - - download this page - - for your records. -

- -
- - -

From Home (LAN)

- -
-

- Visit the address below when you are connected to the same WiFi - or Local Area Network (LAN) as your Embassy: -

-
+ + + -

- Note: embassy.local was for setup purposes only, it will - no longer work. -

- - + + + + + Setup Complete + See below for next steps - - {{ lanAddress }} - - + + +
+
+

+ You can now safely unplug your backup drive. - - - You can now safely unplug your old drive. - - - - -

- Important! - Your browser will warn you that the website is untrusted. You - can bypass this warning on most browsers. The warning will go - away after you - - follow the instructions - +

+

+ Access your Embassy using the methods below. You should + + download this page + - to download and trust your Embassy's Root Certificate Authority. -

+ for your records. +

- - Download Root CA - - -
+
-
+ +

From Home (LAN)

- -

On The Go (Tor)

+
+

+ Visit the address below when you are connected to the same + WiFi or Local Area Network (LAN) as your Embassy: +

-
-

Visit the address below when you are away from home:

+
- - - {{ torAddress }} + Note: embassy.local was for setup purposes only, it + will no longer work. +

+ + + + {{ lanAddress }} + + -
- - - -
+ + + + + + -

- Important! - This address will only work from a - - Tor-enabled browser - . -

-
- -
- +

+ Important! + Your browser will warn you that the website is untrusted. You + can bypass this warning on most browsers. The warning will go + away after you + + follow the instructions + + + to download and trust your Embassy's Root Certificate + Authority. +

- -
- - More - - -
- - - - - -