refactor setup wizard (#1937)

* refactor setup backend

* rework setup wizard according to new scheme

* fix bug with partitions in SW and warning message in IW

* treat localhost as LAN for launching services

* misc backend fixes

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
This commit is contained in:
Aiden McClelland
2022-11-10 19:30:52 -07:00
parent 6cea0139d1
commit a29cd622c3
23 changed files with 721 additions and 753 deletions

View File

@@ -23,13 +23,14 @@ use crate::db::model::{PackageDataEntry, StaticFiles};
use crate::disk::mount::backup::{BackupMountGuard, PackageBackupMountGuard}; use crate::disk::mount::backup::{BackupMountGuard, PackageBackupMountGuard};
use crate::disk::mount::filesystem::ReadOnly; use crate::disk::mount::filesystem::ReadOnly;
use crate::disk::mount::guard::TmpMountGuard; use crate::disk::mount::guard::TmpMountGuard;
use crate::hostname::{get_hostname, Hostname};
use crate::install::progress::InstallProgress; use crate::install::progress::InstallProgress;
use crate::install::{download_install_s9pk, PKG_PUBLIC_DIR}; use crate::install::{download_install_s9pk, PKG_PUBLIC_DIR};
use crate::net::ssl::SslManager; use crate::net::ssl::SslManager;
use crate::notifications::NotificationLevel; use crate::notifications::NotificationLevel;
use crate::s9pk::manifest::{Manifest, PackageId}; use crate::s9pk::manifest::{Manifest, PackageId};
use crate::s9pk::reader::S9pkReader; use crate::s9pk::reader::S9pkReader;
use crate::setup::RecoveryStatus; use crate::setup::SetupStatus;
use crate::util::display_none; use crate::util::display_none;
use crate::util::io::dir_size; use crate::util::io::dir_size;
use crate::util::serde::IoFormat; use crate::util::serde::IoFormat;
@@ -140,7 +141,7 @@ async fn approximate_progress_loop(
tracing::error!("Failed to approximate restore progress: {}", e); tracing::error!("Failed to approximate restore progress: {}", e);
tracing::debug!("{:?}", e); tracing::debug!("{:?}", e);
} else { } 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; tokio::time::sleep(Duration::from_secs(1)).await;
} }
@@ -153,7 +154,7 @@ struct ProgressInfo {
target_volume_size: BTreeMap<PackageId, u64>, target_volume_size: BTreeMap<PackageId, u64>,
} }
impl ProgressInfo { impl ProgressInfo {
fn flatten(&self) -> RecoveryStatus { fn flatten(&self) -> SetupStatus {
let mut total_bytes = 0; let mut total_bytes = 0;
let mut bytes_transferred = 0; let mut bytes_transferred = 0;
@@ -176,7 +177,7 @@ impl ProgressInfo {
bytes_transferred = total_bytes; bytes_transferred = total_bytes;
} }
RecoveryStatus { SetupStatus {
total_bytes, total_bytes,
bytes_transferred, bytes_transferred,
complete: false, complete: false,
@@ -191,7 +192,7 @@ pub async fn recover_full_embassy(
embassy_password: String, embassy_password: String,
recovery_source: TmpMountGuard, recovery_source: TmpMountGuard,
recovery_password: Option<String>, recovery_password: Option<String>,
) -> Result<(OnionAddressV3, X509, BoxFuture<'static, Result<(), Error>>), Error> { ) -> Result<(Arc<String>, Hostname, OnionAddressV3, X509), Error> {
let backup_guard = BackupMountGuard::mount( let backup_guard = BackupMountGuard::mount(
recovery_source, recovery_source,
recovery_password.as_deref().unwrap_or_default(), recovery_password.as_deref().unwrap_or_default(),
@@ -232,11 +233,13 @@ pub async fn recover_full_embassy(
.await?; .await?;
secret_store.close().await; secret_store.close().await;
Ok(( let rpc_ctx = RpcContext::init(ctx.config_path.clone(), disk_guid.clone()).await?;
os_backup.tor_key.public().get_onion_address(), let mut db = rpc_ctx.db.handle();
os_backup.root_ca_cert,
async move { let receipts = crate::hostname::HostNameReceipt::new(&mut db).await?;
let rpc_ctx = RpcContext::init(ctx.config_path.clone(), disk_guid).await?; let hostname = get_hostname(&mut db, &receipts).await?;
drop(db);
let mut db = rpc_ctx.db.handle(); let mut db = rpc_ctx.db.handle();
let ids = backup_guard let ids = backup_guard
@@ -245,13 +248,8 @@ pub async fn recover_full_embassy(
.keys() .keys()
.cloned() .cloned()
.collect(); .collect();
let (backup_guard, tasks, progress_info) = restore_packages( let (backup_guard, tasks, progress_info) =
&rpc_ctx, restore_packages(&rpc_ctx, &mut db, backup_guard, ids).await?;
&mut db,
backup_guard,
ids,
)
.await?;
tokio::select! { tokio::select! {
res = futures::future::join_all(tasks) => { res = futures::future::join_all(tasks) => {
@@ -291,8 +289,13 @@ pub async fn recover_full_embassy(
} }
backup_guard.unmount().await?; backup_guard.unmount().await?;
rpc_ctx.shutdown().await rpc_ctx.shutdown().await?;
}.boxed()
Ok((
disk_guid,
hostname,
os_backup.tor_key.public().get_onion_address(),
os_backup.root_ca_cert,
)) ))
} }

View File

@@ -102,6 +102,15 @@ async fn setup_or_init(cfg_path: Option<PathBuf>) -> Result<(), Error> {
.await .await
.expect("context dropped"); .expect("context dropped");
setup_http_server.shutdown.send(()).unwrap(); 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 { } else {
let cfg = RpcContextConfig::load(cfg_path).await?; 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 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

View File

@@ -18,7 +18,7 @@ use crate::db::model::Database;
use crate::disk::OsPartitionInfo; use crate::disk::OsPartitionInfo;
use crate::init::{init_postgres, pgloader}; use crate::init::{init_postgres, pgloader};
use crate::net::tor::os_key; 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::util::config::load_config_from_paths;
use crate::{Error, ResultExt}; use crate::{Error, ResultExt};
@@ -76,7 +76,7 @@ pub struct SetupContextSeed {
pub current_secret: Arc<Jwk>, pub current_secret: Arc<Jwk>,
pub selected_v2_drive: RwLock<Option<PathBuf>>, pub selected_v2_drive: RwLock<Option<PathBuf>>,
pub cached_product_key: RwLock<Option<Arc<String>>>, pub cached_product_key: RwLock<Option<Arc<String>>>,
pub recovery_status: RwLock<Option<Result<RecoveryStatus, RpcError>>>, pub setup_status: RwLock<Option<Result<SetupStatus, RpcError>>>,
pub setup_result: RwLock<Option<(Arc<String>, SetupResult)>>, pub setup_result: RwLock<Option<(Arc<String>, SetupResult)>>,
} }
@@ -114,7 +114,7 @@ impl SetupContext {
), ),
selected_v2_drive: RwLock::new(None), selected_v2_drive: RwLock::new(None),
cached_product_key: RwLock::new(None), cached_product_key: RwLock::new(None),
recovery_status: RwLock::new(None), setup_status: RwLock::new(None),
setup_result: RwLock::new(None), setup_result: RwLock::new(None),
}))) })))
} }

View File

@@ -6,6 +6,7 @@ use std::time::Duration;
use color_eyre::eyre::eyre; use color_eyre::eyre::eyre;
use helpers::NonDetachingJoinHandle; use helpers::NonDetachingJoinHandle;
use patch_db::{DbHandle, LockReceipt, LockType}; use patch_db::{DbHandle, LockReceipt, LockType};
use sqlx::{Pool, Postgres};
use tokio::process::Command; use tokio::process::Command;
use crate::context::rpc::RpcContextConfig; use crate::context::rpc::RpcContextConfig;
@@ -190,6 +191,7 @@ pub async fn init_postgres(datadir: impl AsRef<Path>) -> Result<(), Error> {
} }
pub struct InitResult { pub struct InitResult {
pub secret_store: Pool<Postgres>,
pub db: patch_db::PatchDb, pub db: patch_db::PatchDb,
} }
@@ -360,5 +362,5 @@ pub async fn init(cfg: &RpcContextConfig) -> Result<InitResult, Error> {
tracing::info!("System initialized."); tracing::info!("System initialized.");
Ok(InitResult { db }) Ok(InitResult { secret_store, db })
} }

View File

@@ -2,8 +2,7 @@ use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use color_eyre::eyre::eyre; use color_eyre::eyre::eyre;
use futures::future::BoxFuture; use futures::StreamExt;
use futures::{StreamExt, TryFutureExt};
use helpers::{Rsync, RsyncOptions}; use helpers::{Rsync, RsyncOptions};
use josekit::jwk::Jwk; use josekit::jwk::Jwk;
use openssl::x509::X509; 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::util::{pvscan, recovery_info, DiskInfo, EmbassyOsRecoveryInfo};
use crate::disk::REPAIR_DISK_PATH; use crate::disk::REPAIR_DISK_PATH;
use crate::hostname::{get_hostname, HostNameReceipt, Hostname}; use crate::hostname::{get_hostname, HostNameReceipt, Hostname};
use crate::init::init; use crate::init::{init, InitResult};
use crate::middleware::encrypt::EncryptedWire; use crate::middleware::encrypt::EncryptedWire;
use crate::net::ssl::SslManager; use crate::net::ssl::SslManager;
use crate::sound::BEETHOVEN;
use crate::{Error, ErrorKind, ResultExt}; use crate::{Error, ErrorKind, ResultExt};
#[instrument(skip(secrets))] #[instrument(skip(secrets))]
@@ -49,24 +47,11 @@ where
Ok(password) 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> { pub fn setup() -> Result<(), Error> {
Ok(()) 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<StatusRes, Error> {
Ok(StatusRes {
migrating: ctx.recovery_status.read().await.is_some(),
})
}
#[command(subcommands(list_disks))] #[command(subcommands(list_disks))]
pub fn disk() -> Result<(), Error> { pub fn disk() -> Result<(), Error> {
Ok(()) Ok(())
@@ -81,9 +66,9 @@ async fn setup_init(
ctx: &SetupContext, ctx: &SetupContext,
password: Option<String>, password: Option<String>,
) -> Result<(Hostname, OnionAddressV3, X509), Error> { ) -> Result<(Hostname, OnionAddressV3, X509), Error> {
let secrets = ctx.secret_store().await?; let InitResult { secret_store, db } =
let db = ctx.db(&secrets).await?; init(&RpcContextConfig::load(ctx.config_path.clone()).await?).await?;
let mut secrets_handle = secrets.acquire().await?; let mut secrets_handle = secret_store.acquire().await?;
let mut db_handle = db.handle(); let mut db_handle = db.handle();
let mut secrets_tx = secrets_handle.begin().await?; let mut secrets_tx = secrets_handle.begin().await?;
let mut db_tx = db_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_receipts = HostNameReceipt::new(&mut db_handle).await?;
let hostname = get_hostname(&mut db_handle, &hostname_receipts).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? .await?
.export_root_ca() .export_root_ca()
.await?; .await?;
@@ -119,7 +104,22 @@ pub async fn attach(
#[context] ctx: SetupContext, #[context] ctx: SetupContext,
#[arg] guid: Arc<String>, #[arg] guid: Arc<String>,
#[arg(rename = "embassy-password")] password: Option<EncryptedWire>, #[arg(rename = "embassy-password")] password: Option<EncryptedWire>,
) -> Result<SetupResult, Error> { ) -> Result<(), Error> {
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 {
if let Err(e) = async {
let password: Option<String> = match password { let password: Option<String> = match password {
Some(a) => match a.decrypt(&*ctx) { Some(a) => match a.decrypt(&*ctx) {
a @ Some(_) => a, a @ Some(_) => a,
@@ -158,34 +158,37 @@ pub async fn attach(
)); ));
} }
let (hostname, tor_addr, root_ca) = setup_init(&ctx, password).await?; let (hostname, tor_addr, root_ca) = setup_init(&ctx, password).await?;
init(&RpcContextConfig::load(ctx.config_path.clone()).await?).await?; *ctx.setup_result.write().await = Some((guid, SetupResult {
let setup_result = SetupResult {
tor_address: format!("http://{}", tor_addr), tor_address: format!("http://{}", tor_addr),
lan_address: hostname.lan_address(), lan_address: hostname.lan_address(),
root_ca: String::from_utf8(root_ca.to_pem()?)?, root_ca: String::from_utf8(root_ca.to_pem()?)?,
}; }));
*ctx.setup_result.write().await = Some((guid, setup_result.clone())); *ctx.setup_status.write().await = Some(Ok(SetupStatus {
Ok(setup_result) 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()));
} }
});
#[command(subcommands(recovery_status))]
pub fn recovery() -> Result<(), Error> {
Ok(()) Ok(())
} }
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct RecoveryStatus { pub struct SetupStatus {
pub bytes_transferred: u64, pub bytes_transferred: u64,
pub total_bytes: u64, pub total_bytes: u64,
pub complete: bool, pub complete: bool,
} }
#[command(rename = "status", rpc_only, metadata(authenticated = false))] #[command(rpc_only, metadata(authenticated = false))]
pub async fn recovery_status( pub async fn status(#[context] ctx: SetupContext) -> Result<Option<SetupStatus>, RpcError> {
#[context] ctx: SetupContext, ctx.setup_status.read().await.clone().transpose()
) -> Result<Option<RecoveryStatus>, RpcError> {
ctx.recovery_status.read().await.clone().transpose()
} }
/// We want to be able to get a secret, a shared private key with the frontend /// 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 = "embassy-password")] embassy_password: EncryptedWire,
#[arg(rename = "recovery-source")] recovery_source: Option<RecoverySource>, #[arg(rename = "recovery-source")] recovery_source: Option<RecoverySource>,
#[arg(rename = "recovery-password")] recovery_password: Option<EncryptedWire>, #[arg(rename = "recovery-password")] recovery_password: Option<EncryptedWire>,
) -> Result<SetupResult, Error> { ) -> Result<(), Error> {
let embassy_password = match embassy_password.decrypt(&*ctx) { let embassy_password = match embassy_password.decrypt(&*ctx) {
Some(a) => a, Some(a) => a,
None => { None => {
@@ -265,6 +268,20 @@ pub async fn execute(
}, },
None => None, None => None,
}; };
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( match execute_inner(
ctx.clone(), ctx.clone(),
embassy_logicalname, embassy_logicalname,
@@ -274,20 +291,33 @@ pub async fn execute(
) )
.await .await
{ {
Ok((hostname, tor_addr, root_ca)) => { Ok((guid, hostname, tor_addr, root_ca)) => {
tracing::info!("Setup Successful! Tor Address: {}", tor_addr); tracing::info!("Setup Complete!");
Ok(SetupResult { *ctx.setup_result.write().await = Some((
guid,
SetupResult {
tor_address: format!("http://{}", tor_addr), tor_address: format!("http://{}", tor_addr),
lan_address: hostname.lan_address(), lan_address: hostname.lan_address(),
root_ca: String::from_utf8(root_ca.to_pem()?)?, 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) => { Err(e) => {
tracing::error!("Error Setting Up Embassy: {}", e); tracing::error!("Error Setting Up Embassy: {}", e);
tracing::debug!("{:?}", e); tracing::debug!("{:?}", e);
Err(e) *ctx.setup_status.write().await = Some(Err(e.into()));
} }
} }
});
Ok(())
} }
#[instrument(skip(ctx))] #[instrument(skip(ctx))]
@@ -301,23 +331,19 @@ pub async fn complete(#[context] ctx: SetupContext) -> Result<SetupResult, Error
crate::ErrorKind::InvalidRequest, crate::ErrorKind::InvalidRequest,
)); ));
}; };
let secrets = ctx.secret_store().await?;
let mut db = ctx.db(&secrets).await?.handle();
let receipts = crate::hostname::HostNameReceipt::new(&mut db).await?;
let hostname = crate::hostname::get_hostname(&mut db, &receipts).await?;
let si = crate::db::DatabaseModel::new().server_info();
let id = crate::hostname::get_id(&mut db, &receipts).await?;
si.clone().id().put(&mut db, &id).await?;
si.lan_address()
.put(&mut db, &hostname.lan_address().parse().unwrap())
.await?;
let mut guid_file = File::create("/media/embassy/config/disk.guid").await?; let mut guid_file = File::create("/media/embassy/config/disk.guid").await?;
guid_file.write_all(guid.as_bytes()).await?; guid_file.write_all(guid.as_bytes()).await?;
guid_file.sync_all().await?; guid_file.sync_all().await?;
ctx.shutdown.send(()).expect("failed to shutdown");
Ok(setup_result) Ok(setup_result)
} }
#[instrument(skip(ctx))]
#[command(rpc_only)]
pub async fn exit(#[context] ctx: SetupContext) -> Result<(), Error> {
ctx.shutdown.send(()).expect("failed to shutdown");
Ok(())
}
#[instrument(skip(ctx, embassy_password, recovery_password))] #[instrument(skip(ctx, embassy_password, recovery_password))]
pub async fn execute_inner( pub async fn execute_inner(
ctx: SetupContext, ctx: SetupContext,
@@ -325,13 +351,7 @@ pub async fn execute_inner(
embassy_password: String, embassy_password: String,
recovery_source: Option<RecoverySource>, recovery_source: Option<RecoverySource>,
recovery_password: Option<String>, recovery_password: Option<String>,
) -> Result<(Hostname, OnionAddressV3, X509), Error> { ) -> Result<(Arc<String>, 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,
));
}
let guid = Arc::new( let guid = Arc::new(
crate::disk::main::create( crate::disk::main::create(
&[embassy_logicalname], &[embassy_logicalname],
@@ -349,52 +369,82 @@ pub async fn execute_inner(
) )
.await?; .await?;
let res = if let Some(RecoverySource::Backup { target }) = recovery_source { if let Some(RecoverySource::Backup { target }) = recovery_source {
let (tor_addr, root_ca, recover_fut) = recover( recover(ctx, guid, embassy_password, target, recovery_password).await
} else if let Some(RecoverySource::Migrate { guid: old_guid }) = recovery_source {
migrate(ctx, guid, &old_guid, embassy_password).await
} else {
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<(Hostname, OnionAddressV3, X509), Error> {
let password = argon2::hash_encoded(
embassy_password.as_bytes(),
&rand::random::<[u8; 16]>()[..],
&argon2::Config::default(),
)
.with_kind(crate::ErrorKind::PasswordHashGeneration)?;
let tor_key = TorSecretKeyV3::generate();
let key_vec = tor_key.as_bytes().to_vec();
let sqlite_pool = ctx.secret_store().await?;
sqlx::query!(
"INSERT INTO account (id, password, tor_key) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET password = $2, tor_key = $3",
0,
password,
key_vec,
)
.execute(&mut sqlite_pool.acquire().await?)
.await?;
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?;
secret_store.close().await;
Ok((hostname, tor_key.public().get_onion_address(), root_ca))
}
#[instrument(skip(ctx, embassy_password, recovery_password))]
async fn recover(
ctx: SetupContext,
guid: Arc<String>,
embassy_password: String,
recovery_source: BackupTargetFS,
recovery_password: Option<String>,
) -> Result<(Arc<String>, Hostname, OnionAddressV3, X509), Error> {
let recovery_source = TmpMountGuard::mount(&recovery_source, ReadOnly).await?;
recover_full_embassy(
ctx.clone(), ctx.clone(),
guid.clone(), guid.clone(),
embassy_password, embassy_password,
target, recovery_source,
recovery_password, 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 .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 #[instrument(skip(ctx, embassy_password))]
} else if let Some(RecoverySource::Migrate { guid: old_guid }) = recovery_source { async fn migrate(
ctx: SetupContext,
guid: Arc<String>,
old_guid: &str,
embassy_password: String,
) -> Result<(Arc<String>, 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( let _ = crate::disk::main::mount_fs(
&old_guid, &old_guid,
"/media/embassy/migrate", "/media/embassy/migrate",
@@ -403,7 +453,7 @@ pub async fn execute_inner(
DEFAULT_PASSWORD, DEFAULT_PASSWORD,
) )
.await?; .await?;
Rsync::new( let mut main_transfer = Rsync::new(
"/media/embassy/migrate/main", "/media/embassy/migrate/main",
"/embassy-data/main", "/embassy-data/main",
RsyncOptions { RsyncOptions {
@@ -412,9 +462,16 @@ pub async fn execute_inner(
ignore_existing: false, ignore_existing: false,
exclude: Vec::new(), exclude: Vec::new(),
}, },
)? )?;
.wait() while let Some(progress) = main_transfer.progress.next().await {
.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( let _ = crate::disk::main::mount_fs(
&old_guid, &old_guid,
"/media/embassy/migrate", "/media/embassy/migrate",
@@ -433,118 +490,17 @@ pub async fn execute_inner(
exclude: vec!["tmp".to_owned()], 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 { while let Some(progress) = package_data_transfer.progress.next().await {
*ctx.recovery_status.write().await = Some(Ok(RecoveryStatus { *ctx.setup_status.write().await = Some(Ok(SetupStatus {
bytes_transferred: (progress * 100.0) as u64, bytes_transferred: 10 + (progress * 100.0) as u64,
total_bytes: 100, total_bytes: 110,
complete: false, complete: false,
})); }));
} }
package_data_transfer.wait().await?; package_data_transfer.wait().await?;
init(&RpcContextConfig::load(ctx.config_path.clone()).await?).await?; crate::disk::main::unmount_fs(&old_guid, "/media/embassy/migrate", "package-data").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
} 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) = setup_init(&ctx, Some(embassy_password)).await?;
}
async fn fresh_setup( Ok((guid, hostname, tor_addr, root_ca))
ctx: &SetupContext,
embassy_password: &str,
) -> Result<(OnionAddressV3, X509), Error> {
let password = argon2::hash_encoded(
embassy_password.as_bytes(),
&rand::random::<[u8; 16]>()[..],
&argon2::Config::default(),
)
.with_kind(crate::ErrorKind::PasswordHashGeneration)?;
let tor_key = TorSecretKeyV3::generate();
let key_vec = tor_key.as_bytes().to_vec();
let sqlite_pool = ctx.secret_store().await?;
sqlx::query!(
"INSERT INTO account (id, password, tor_key) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET password = $2, tor_key = $3",
0,
password,
key_vec,
)
.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())
.await?
.export_root_ca()
.await?;
sqlite_pool.close().await;
Ok((tor_key.public().get_onion_address(), root_ca))
}
#[instrument(skip(ctx, embassy_password, recovery_password))]
async fn recover(
ctx: SetupContext,
guid: Arc<String>,
embassy_password: String,
recovery_source: BackupTargetFS,
recovery_password: Option<String>,
) -> Result<(OnionAddressV3, X509, BoxFuture<'static, Result<(), Error>>), Error> {
let recovery_source = TmpMountGuard::mount(&recovery_source, ReadOnly).await?;
recover_full_embassy(
ctx.clone(),
guid.clone(),
embassy_password,
recovery_source,
recovery_password,
)
.await
} }

View File

@@ -10,10 +10,8 @@ apt install --no-install-recommends -y xserver-xorg x11-xserver-utils xinit fire
cat > /home/start9/kiosk.sh << 'EOF' cat > /home/start9/kiosk.sh << 'EOF'
#!/bin/sh #!/bin/sh
PROFILE=$(mktemp -d) PROFILE=$(mktemp -d)
protocol=http
if [ -f /usr/local/share/ca-certificates/embassy-root-ca.crt ]; then 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 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 fi
cat >> $PROFILE/prefs.js << EOT cat >> $PROFILE/prefs.js << EOT
user_pref("network.proxy.autoconfig_url", "file:///usr/lib/embassy/proxy.pac"); 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.allowlist_onions", true);
user_pref("dom.securecontext.whitelist_onions", true); user_pref("dom.securecontext.whitelist_onions", true);
user_pref("signon.rememberSignons", false); 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 EOT
while ! curl "${protocol}://$(hostname).local" > /dev/null; do while ! curl "http://localhost" > /dev/null; do
sleep 1 sleep 1
done done
matchbox-window-manager -use_titlebar yes & matchbox-window-manager -use_titlebar no &
firefox-esr --kiosk ${protocol}://$(hostname).local --profile $PROFILE firefox-esr http://localhost --profile $PROFILE
rm -rf $PROFILE rm -rf $PROFILE
EOF EOF
chmod +x /home/start9/kiosk.sh chmod +x /home/start9/kiosk.sh

View File

@@ -47,27 +47,24 @@ export class HomePage {
} }
async tryInstall(overwrite: boolean) { async tryInstall(overwrite: boolean) {
if (!this.selectedDisk) return if (overwrite) {
return this.presentAlertDanger()
const { logicalname, guid } = this.selectedDisk
const hasEmbassyData = !!guid
if (hasEmbassyData && !overwrite) {
return this.install(logicalname, overwrite)
} }
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({ const loader = await this.loadingCtrl.create({
message: 'Installing embassyOS...', message: 'Installing embassyOS...',
}) })
await loader.present() await loader.present()
try { try {
await this.api.install({ logicalname, overwrite }) await this.api.install({
logicalname: this.selectedDisk!.logicalname,
overwrite,
})
this.presentAlertReboot() this.presentAlertReboot()
} catch (e: any) { } catch (e: any) {
this.error = e.message this.error = e.message
@@ -76,17 +73,14 @@ export class HomePage {
} }
} }
private async presentAlertDanger( private async presentAlertDanger() {
logicalname: string, const { vendor, model } = this.selectedDisk!
hasEmbassyData: boolean,
) {
const message = hasEmbassyData
? 'This action COMPLETELY erases your existing Embassy data'
: `This action COMPLETELY erases the disk ${logicalname} and installs embassyOS`
const alert = await this.alertCtrl.create({ const alert = await this.alertCtrl.create({
header: 'Warning', header: 'Warning',
message, message: `This action will COMPLETELY erase the disk ${
vendor || 'Unknown Vendor'
} - ${model || 'Unknown Model'} and install embassyOS in its place`,
buttons: [ buttons: [
{ {
text: 'Cancel', text: 'Cancel',
@@ -95,7 +89,7 @@ export class HomePage {
{ {
text: 'Continue', text: 'Continue',
handler: () => { handler: () => {
this.install(logicalname, true) this.install(true)
}, },
}, },
], ],

View File

@@ -17,8 +17,14 @@ export class AppComponent {
async ngOnInit() { async ngOnInit() {
try { try {
const { migrating } = await this.apiService.getStatus() const inProgress = await this.apiService.getStatus()
await this.navCtrl.navigateForward(migrating ? '/loading' : '/home')
let route = '/home'
if (inProgress) {
route = inProgress.complete ? '/success' : '/loading'
}
await this.navCtrl.navigateForward(route)
} catch (e: any) { } catch (e: any) {
this.errorToastService.present(e) this.errorToastService.present(e)
} }

View File

@@ -38,8 +38,7 @@ export class AttachPage {
async getDrives() { async getDrives() {
try { try {
const drives = await this.apiService.getDrives() this.drives = await this.apiService.getDrives()
this.drives = drives.filter(d => d.partitions.length)
} catch (e: any) { } catch (e: any) {
this.errToastService.present(e) this.errToastService.present(e)
} finally { } finally {
@@ -61,13 +60,11 @@ export class AttachPage {
} }
private async attachDrive(guid: string, password: string) { private async attachDrive(guid: string, password: string) {
const loader = await this.loadingCtrl.create({ const loader = await this.loadingCtrl.create()
message: 'Attaching Drive',
})
await loader.present() await loader.present()
try { try {
await this.stateService.importDrive(guid, password) await this.stateService.importDrive(guid, password)
await this.navCtrl.navigateForward(`/success`) await this.navCtrl.navigateForward(`/loading`)
} catch (e: any) { } catch (e: any) {
this.errToastService.present(e) this.errToastService.present(e)
} finally { } finally {

View File

@@ -13,7 +13,6 @@ import {
import { DiskInfo, ErrorToastService, GuidPipe } from '@start9labs/shared' import { DiskInfo, ErrorToastService, GuidPipe } from '@start9labs/shared'
import { StateService } from 'src/app/services/state.service' import { StateService } from 'src/app/services/state.service'
import { PasswordPage } from '../../modals/password/password.page' import { PasswordPage } from '../../modals/password/password.page'
import { ActivatedRoute } from '@angular/router'
@Component({ @Component({
selector: 'app-embassy', selector: 'app-embassy',
@@ -34,7 +33,6 @@ export class EmbassyPage {
private readonly loadingCtrl: LoadingController, private readonly loadingCtrl: LoadingController,
private readonly errorToastService: ErrorToastService, private readonly errorToastService: ErrorToastService,
private readonly guidPipe: GuidPipe, private readonly guidPipe: GuidPipe,
private route: ActivatedRoute,
) {} ) {}
async ngOnInit() { async ngOnInit() {
@@ -133,21 +131,12 @@ export class EmbassyPage {
logicalname: string, logicalname: string,
password: string, password: string,
): Promise<void> { ): Promise<void> {
const loader = await this.loadingCtrl.create({ const loader = await this.loadingCtrl.create()
message: 'Initializing data drive. This could take a while...',
})
await loader.present() await loader.present()
try { try {
await this.stateService.setupEmbassy(logicalname, password) await this.stateService.setupEmbassy(logicalname, password)
if (!!this.stateService.recoverySource) { await this.navCtrl.navigateForward(`/loading`)
await this.navCtrl.navigateForward(`/loading`, {
queryParams: { action: this.route.snapshot.paramMap.get('action') },
})
} else {
await this.navCtrl.navigateForward(`/success`)
}
} catch (e: any) { } catch (e: any) {
this.errorToastService.present({ this.errorToastService.present({
message: `${e.message}\n\nRestart Embassy to try again.`, message: `${e.message}\n\nRestart Embassy to try again.`,

View File

@@ -8,11 +8,8 @@
<ion-card color="dark"> <ion-card color="dark">
<ion-card-header> <ion-card-header>
<ion-card-title style="font-size: 40px"> <ion-card-title>Initializing Embassy</ion-card-title>
<span *ngIf="incomingAction === 'transfer'">Transferring</span> <ion-card-subtitle *ngIf="stateService.dataProgress"
<span *ngIf="incomingAction === 'recover'">Recovering</span>
</ion-card-title>
<ion-card-subtitle
>Progress: {{ (stateService.dataProgress * 100).toFixed(0) >Progress: {{ (stateService.dataProgress * 100).toFixed(0)
}}%</ion-card-subtitle }}%</ion-card-subtitle
> >
@@ -27,8 +24,10 @@
padding-bottom: 20px; padding-bottom: 20px;
margin-bottom: 40px; margin-bottom: 40px;
" "
[type]="stateService.dataProgress ? 'determinate' : 'indeterminate'"
[value]="stateService.dataProgress" [value]="stateService.dataProgress"
></ion-progress-bar> ></ion-progress-bar>
<p>Setting up your Embassy. This can take a while.</p>
</ion-card-content> </ion-card-content>
</ion-card> </ion-card>
</ion-col> </ion-col>

View File

@@ -0,0 +1,3 @@
ion-card-title {
font-size: 42px;
}

View File

@@ -1,5 +1,4 @@
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { NavController } from '@ionic/angular' import { NavController } from '@ionic/angular'
import { StateService } from 'src/app/services/state.service' import { StateService } from 'src/app/services/state.service'
@@ -9,16 +8,12 @@ import { StateService } from 'src/app/services/state.service'
styleUrls: ['loading.page.scss'], styleUrls: ['loading.page.scss'],
}) })
export class LoadingPage { export class LoadingPage {
incomingAction!: string
constructor( constructor(
public stateService: StateService, public stateService: StateService,
private navCtrl: NavController, private navCtrl: NavController,
private route: ActivatedRoute,
) {} ) {}
ngOnInit() { ngOnInit() {
this.incomingAction = this.route.snapshot.paramMap.get('action')!
this.stateService.pollDataTransferProgress() this.stateService.pollDataTransferProgress()
const progSub = this.stateService.dataCompletionSubject.subscribe( const progSub = this.stateService.dataCompletionSubject.subscribe(
async complete => { async complete => {

View File

@@ -118,9 +118,7 @@ export class RecoverPage {
}, },
} }
this.stateService.recoveryPassword = password this.stateService.recoveryPassword = password
this.navCtrl.navigateForward(`/embassy`, { this.navCtrl.navigateForward(`/embassy`)
queryParams: { action: 'recover' },
})
} }
} }

View File

@@ -2,6 +2,8 @@
<ion-grid> <ion-grid>
<ion-row> <ion-row>
<ion-col> <ion-col>
<!-- kiosk mode -->
<ng-container *ngIf="isKiosk; else notKiosk">
<ion-card color="dark"> <ion-card color="dark">
<ion-card-header class="ion-text-center" color="success"> <ion-card-header class="ion-text-center" color="success">
<ion-icon <ion-icon
@@ -10,9 +12,24 @@
></ion-icon> ></ion-icon>
<ion-card-title>Setup Complete</ion-card-title> <ion-card-title>Setup Complete</ion-card-title>
<ion-card-subtitle <ion-card-subtitle
><b ><b>You will be redirected momentarily</b></ion-card-subtitle
>You have successfully claimed your Embassy!</b >
></ion-card-subtitle <br />
</ion-card-header>
</ion-card>
</ng-container>
<!-- not kiosk -->
<ng-template #notKiosk>
<ion-card color="dark">
<ion-card-header class="ion-text-center" color="success">
<ion-icon
style="font-size: 80px"
name="checkmark-circle-outline"
></ion-icon>
<ion-card-title>Setup Complete</ion-card-title>
<ion-card-subtitle
><b>See below for next steps</b></ion-card-subtitle
> >
<br /> <br />
</ion-card-header> </ion-card-header>
@@ -30,7 +47,8 @@
<h2 style="font-weight: bold"> <h2 style="font-weight: bold">
Access your Embassy using the methods below. You should Access your Embassy using the methods below. You should
<a (click)="download()" class="inline"> <a (click)="download()" class="inline">
download this page <ion-icon name="download-outline"></ion-icon> download this page
<ion-icon name="download-outline"></ion-icon>
</a> </a>
for your records. for your records.
</h2> </h2>
@@ -42,15 +60,15 @@
<div class="ion-padding ion-text-start"> <div class="ion-padding ion-text-start">
<p> <p>
Visit the address below when you are connected to the same WiFi Visit the address below when you are connected to the same
or Local Area Network (LAN) as your Embassy: WiFi or Local Area Network (LAN) as your Embassy:
</p> </p>
<br /> <br />
<p> <p>
<b>Note:</b> embassy.local was for setup purposes only, it will <b>Note:</b> embassy.local was for setup purposes only, it
no longer work. will no longer work.
</p> </p>
<ion-item <ion-item
@@ -96,7 +114,8 @@
follow the instructions follow the instructions
<ion-icon name="open-outline"></ion-icon> <ion-icon name="open-outline"></ion-icon>
</a> </a>
to download and trust your Embassy's Root Certificate Authority. to download and trust your Embassy's Root Certificate
Authority.
</p> </p>
<ion-button style="margin-top: 24px" (click)="installCert()"> <ion-button style="margin-top: 24px" (click)="installCert()">
@@ -181,8 +200,8 @@
<section style="padding: 16px; border: solid 1px"> <section style="padding: 16px; border: solid 1px">
<h2>Tor Info</h2> <h2>Tor Info</h2>
<p> <p>
To use your Embassy over Tor, visit its unique Tor address from To use your Embassy over Tor, visit its unique Tor address
any Tor-enabled browser. from any Tor-enabled browser.
</p> </p>
<p> <p>
For more detailed instructions, click For more detailed instructions, click
@@ -196,13 +215,15 @@
<p><b>Tor Address: </b><code id="tor-addr"></code></p> <p><b>Tor Address: </b><code id="tor-addr"></code></p>
</section> </section>
<section style="padding: 16px; border: solid 1px; border-top: none"> <section
style="padding: 16px; border: solid 1px; border-top: none"
>
<h2>LAN Info</h2> <h2>LAN Info</h2>
<p>To use your Embassy locally, you must:</p> <p>To use your Embassy locally, you must:</p>
<ol> <ol>
<li> <li>
Currently be connected to the same Local Area Network (LAN) as Currently be connected to the same Local Area Network (LAN)
your Embassy. as your Embassy.
</li> </li>
<li>Download your Embassy's Root Certificate Authority.</li> <li>Download your Embassy's Root Certificate Authority.</li>
<li> <li>
@@ -241,6 +262,7 @@
</section> </section>
</div> </div>
</div> </div>
</ng-template>
</ion-col> </ion-col>
</ion-row> </ion-row>
</ion-grid> </ion-grid>

View File

@@ -12,6 +12,7 @@ import {
DownloadHTMLService, DownloadHTMLService,
ErrorToastService, ErrorToastService,
} from '@start9labs/shared' } from '@start9labs/shared'
import { ApiService } from 'src/app/services/api/api.service'
import { StateService } from 'src/app/services/state.service' import { StateService } from 'src/app/services/state.service'
@Component({ @Component({
@@ -26,6 +27,10 @@ export class SuccessPage {
@Output() onDownload = new EventEmitter() @Output() onDownload = new EventEmitter()
torAddress = ''
lanAddress = ''
cert = ''
isOnBottom = true isOnBottom = true
constructor( constructor(
@@ -33,6 +38,7 @@ export class SuccessPage {
private readonly toastCtrl: ToastController, private readonly toastCtrl: ToastController,
private readonly errCtrl: ErrorToastService, private readonly errCtrl: ErrorToastService,
private readonly stateService: StateService, private readonly stateService: StateService,
private api: ApiService,
private readonly downloadHtml: DownloadHTMLService, private readonly downloadHtml: DownloadHTMLService,
) {} ) {}
@@ -40,27 +46,30 @@ export class SuccessPage {
return this.stateService.recoverySource return this.stateService.recoverySource
} }
get torAddress() { get isKiosk() {
return this.stateService.torAddress return ['localhost', '127.0.0.1'].includes(this.document.location.hostname)
}
get lanAddress() {
return this.stateService.lanAddress
} }
async ngAfterViewInit() { async ngAfterViewInit() {
try {
const ret = await this.api.complete()
if (!this.isKiosk) {
setTimeout(() => this.checkBottom(), 42) setTimeout(() => this.checkBottom(), 42)
try { this.torAddress = ret['tor-address']
await this.stateService.completeEmbassy() this.lanAddress = ret['lan-address']
this.cert = ret['root-ca']
this.document this.document
.getElementById('install-cert') .getElementById('install-cert')
?.setAttribute( ?.setAttribute(
'href', 'href',
'data:application/x-x509-ca-cert;base64,' + 'data:application/x-x509-ca-cert;base64,' +
encodeURIComponent(this.stateService.cert), encodeURIComponent(this.cert),
) )
this.download() this.download()
}
await this.api.exit()
} catch (e: any) { } catch (e: any) {
await this.errCtrl.present(e) await this.errCtrl.present(e)
} }
@@ -88,15 +97,15 @@ export class SuccessPage {
const torAddress = this.document.getElementById('tor-addr') const torAddress = this.document.getElementById('tor-addr')
const lanAddress = this.document.getElementById('lan-addr') const lanAddress = this.document.getElementById('lan-addr')
if (torAddress) torAddress.innerHTML = this.stateService.torAddress if (torAddress) torAddress.innerHTML = this.torAddress
if (lanAddress) lanAddress.innerHTML = this.stateService.lanAddress if (lanAddress) lanAddress.innerHTML = this.lanAddress
this.document this.document
.getElementById('cert') .getElementById('cert')
?.setAttribute( ?.setAttribute(
'href', 'href',
'data:application/x-x509-ca-cert;base64,' + 'data:application/x-x509-ca-cert;base64,' +
encodeURIComponent(this.stateService.cert), encodeURIComponent(this.cert),
) )
let html = this.document.getElementById('downloadable')?.innerHTML || '' let html = this.document.getElementById('downloadable')?.innerHTML || ''
this.downloadHtml.download('embassy-info.html', html) this.downloadHtml.download('embassy-info.html', html)

View File

@@ -32,8 +32,7 @@ export class TransferPage {
async getDrives() { async getDrives() {
try { try {
const drives = await this.apiService.getDrives() this.drives = await this.apiService.getDrives()
this.drives = drives.filter(d => d.partitions.length)
} catch (e: any) { } catch (e: any) {
this.errToastService.present(e) this.errToastService.present(e)
} finally { } finally {
@@ -58,9 +57,7 @@ export class TransferPage {
type: 'migrate', type: 'migrate',
guid, guid,
} }
this.navCtrl.navigateForward(`/embassy`, { this.navCtrl.navigateForward(`/embassy`)
queryParams: { action: 'transfer' },
})
}, },
}, },
], ],

View File

@@ -3,14 +3,14 @@ import { DiskListResponse, EmbassyOSDiskInfo } from '@start9labs/shared'
export abstract class ApiService { export abstract class ApiService {
pubkey?: jose.JWK.Key pubkey?: jose.JWK.Key
abstract getStatus(): Promise<GetStatusRes> // setup.status abstract getStatus(): Promise<StatusRes> // setup.status
abstract getPubKey(): Promise<void> // setup.get-pubkey abstract getPubKey(): Promise<void> // setup.get-pubkey
abstract getDrives(): Promise<DiskListResponse> // setup.disk.list abstract getDrives(): Promise<DiskListResponse> // setup.disk.list
abstract getRecoveryStatus(): Promise<RecoveryStatusRes> // setup.recovery.status
abstract verifyCifs(cifs: CifsRecoverySource): Promise<EmbassyOSDiskInfo> // setup.cifs.verify abstract verifyCifs(cifs: CifsRecoverySource): Promise<EmbassyOSDiskInfo> // setup.cifs.verify
abstract importDrive(importInfo: ImportDriveReq): Promise<SetupEmbassyRes> // setup.attach abstract attach(importInfo: AttachReq): Promise<void> // setup.attach
abstract setupEmbassy(setupInfo: SetupEmbassyReq): Promise<SetupEmbassyRes> // setup.execute abstract execute(setupInfo: ExecuteReq): Promise<void> // setup.execute
abstract setupComplete(): Promise<SetupEmbassyRes> // setup.complete abstract complete(): Promise<CompleteRes> // setup.complete
abstract exit(): Promise<void> // setup.exit
async encrypt(toEncrypt: string): Promise<Encrypted> { async encrypt(toEncrypt: string): Promise<Encrypted> {
if (!this.pubkey) throw new Error('No pubkey found!') if (!this.pubkey) throw new Error('No pubkey found!')
@@ -27,23 +27,25 @@ type Encrypted = {
encrypted: string encrypted: string
} }
export type GetStatusRes = { export type StatusRes = {
migrating: boolean 'bytes-transferred': number
} 'total-bytes': number
complete: boolean
} | null
export type ImportDriveReq = { export type AttachReq = {
guid: string guid: string
'embassy-password': Encrypted 'embassy-password': Encrypted
} }
export type SetupEmbassyReq = { export type ExecuteReq = {
'embassy-logicalname': string 'embassy-logicalname': string
'embassy-password': Encrypted 'embassy-password': Encrypted
'recovery-source': RecoverySource | null 'recovery-source': RecoverySource | null
'recovery-password': Encrypted | null 'recovery-password': Encrypted | null
} }
export type SetupEmbassyRes = { export type CompleteRes = {
'tor-address': string 'tor-address': string
'lan-address': string 'lan-address': string
'root-ca': string 'root-ca': string
@@ -90,9 +92,3 @@ export type CifsRecoverySource = {
username: string username: string
password: Encrypted | null password: Encrypted | null
} }
export type RecoveryStatusRes = {
'bytes-transferred': number
'total-bytes': number
complete: boolean
}

View File

@@ -12,11 +12,10 @@ import {
ApiService, ApiService,
CifsRecoverySource, CifsRecoverySource,
DiskRecoverySource, DiskRecoverySource,
GetStatusRes, StatusRes,
ImportDriveReq, AttachReq,
RecoveryStatusRes, ExecuteReq,
SetupEmbassyReq, CompleteRes,
SetupEmbassyRes,
} from './api.service' } from './api.service'
import * as jose from 'node-jose' import * as jose from 'node-jose'
@@ -29,7 +28,7 @@ export class LiveApiService extends ApiService {
} }
async getStatus() { async getStatus() {
return this.rpcRequest<GetStatusRes>({ return this.rpcRequest<StatusRes>({
method: 'setup.status', method: 'setup.status',
params: {}, params: {},
}) })
@@ -58,13 +57,6 @@ export class LiveApiService extends ApiService {
}) })
} }
async getRecoveryStatus() {
return this.rpcRequest<RecoveryStatusRes>({
method: 'setup.recovery.status',
params: {},
})
}
async verifyCifs(source: CifsRecoverySource) { async verifyCifs(source: CifsRecoverySource) {
source.path = source.path.replace('/\\/g', '/') source.path = source.path.replace('/\\/g', '/')
return this.rpcRequest<EmbassyOSDiskInfo>({ return this.rpcRequest<EmbassyOSDiskInfo>({
@@ -73,19 +65,14 @@ export class LiveApiService extends ApiService {
}) })
} }
async importDrive(params: ImportDriveReq) { async attach(params: AttachReq) {
const res = await this.rpcRequest<SetupEmbassyRes>({ await this.rpcRequest<void>({
method: 'setup.attach', method: 'setup.attach',
params, params,
}) })
return {
...res,
'root-ca': encodeBase64(res['root-ca']),
}
} }
async setupEmbassy(setupInfo: SetupEmbassyReq) { async execute(setupInfo: ExecuteReq) {
if (setupInfo['recovery-source']?.type === 'backup') { if (setupInfo['recovery-source']?.type === 'backup') {
if (isCifsSource(setupInfo['recovery-source'].target)) { if (isCifsSource(setupInfo['recovery-source'].target)) {
setupInfo['recovery-source'].target.path = setupInfo[ setupInfo['recovery-source'].target.path = setupInfo[
@@ -94,19 +81,14 @@ export class LiveApiService extends ApiService {
} }
} }
const res = await this.rpcRequest<SetupEmbassyRes>({ await this.rpcRequest<void>({
method: 'setup.execute', method: 'setup.execute',
params: setupInfo, params: setupInfo,
}) })
return {
...res,
'root-ca': encodeBase64(res['root-ca']),
}
} }
async setupComplete() { async complete() {
const res = await this.rpcRequest<SetupEmbassyRes>({ const res = await this.rpcRequest<CompleteRes>({
method: 'setup.complete', method: 'setup.complete',
params: {}, params: {},
}) })
@@ -117,6 +99,13 @@ export class LiveApiService extends ApiService {
} }
} }
async exit() {
await this.rpcRequest<void>({
method: 'setup.exit',
params: {},
})
}
private async rpcRequest<T>(opts: RPCOptions): Promise<T> { private async rpcRequest<T>(opts: RPCOptions): Promise<T> {
const res = await this.http.rpcRequest<T>(opts) const res = await this.http.rpcRequest<T>(opts)

View File

@@ -3,21 +3,36 @@ import { encodeBase64, pauseFor } from '@start9labs/shared'
import { import {
ApiService, ApiService,
CifsRecoverySource, CifsRecoverySource,
ImportDriveReq, AttachReq,
SetupEmbassyReq, ExecuteReq,
CompleteRes,
} from './api.service' } from './api.service'
import * as jose from 'node-jose' import * as jose from 'node-jose'
let tries = 0 let tries: number
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class MockApiService extends ApiService { export class MockApiService extends ApiService {
async getStatus() { async getStatus() {
const restoreOrMigrate = true
const total = 4
await pauseFor(1000) await pauseFor(1000)
if (tries === undefined) {
tries = 0
return null
}
tries++
const progress = tries - 1
return { return {
migrating: false, 'bytes-transferred': restoreOrMigrate ? progress : 0,
'total-bytes': restoreOrMigrate ? total : 0,
complete: progress === total,
} }
} }
@@ -112,15 +127,6 @@ export class MockApiService extends ApiService {
] ]
} }
async getRecoveryStatus() {
tries = Math.min(tries + 1, 4)
return {
'bytes-transferred': tries,
'total-bytes': 4,
complete: tries === 4,
}
}
async verifyCifs(params: CifsRecoverySource) { async verifyCifs(params: CifsRecoverySource) {
await pauseFor(1000) await pauseFor(1000)
return { return {
@@ -132,19 +138,25 @@ export class MockApiService extends ApiService {
} }
} }
async importDrive(params: ImportDriveReq) { async attach(params: AttachReq) {
await pauseFor(3000) await pauseFor(1000)
return setupRes }
}
async execute(setupInfo: ExecuteReq) {
async setupEmbassy(setupInfo: SetupEmbassyReq) { await pauseFor(1000)
await pauseFor(3000) }
return setupRes
} async complete(): Promise<CompleteRes> {
await pauseFor(1000)
async setupComplete() { return {
'tor-address': 'http://asdafsadasdasasdasdfasdfasdf.onion',
'lan-address': 'https://embassy-abcdefgh.local',
'root-ca': encodeBase64(rootCA),
}
}
async exit() {
await pauseFor(1000) await pauseFor(1000)
return setupRes
} }
} }
@@ -170,9 +182,3 @@ Rf3ZOPm9QP92YpWyYDkfAU04xdDo1vR0MYjKPkl4LjRqSU/tcCJnPMbJiwq+bWpX
2WJoEBXB/p15Kn6JxjI0ze2SnSI48JZ8it4fvxrhOo0VoLNIuCuNXJOwU17Rdl1W 2WJoEBXB/p15Kn6JxjI0ze2SnSI48JZ8it4fvxrhOo0VoLNIuCuNXJOwU17Rdl1W
YJidaq7je6k18AdgPA0Kh8y1XtfUH3fTaVw4 YJidaq7je6k18AdgPA0Kh8y1XtfUH3fTaVw4
-----END CERTIFICATE-----` -----END CERTIFICATE-----`
const setupRes = {
'tor-address': 'http://asdafsadasdasasdasdfasdfasdf.onion',
'lan-address': 'https://embassy-abcdefgh.local',
'root-ca': encodeBase64(rootCA),
}

View File

@@ -7,9 +7,6 @@ import { pauseFor, ErrorToastService } from '@start9labs/shared'
providedIn: 'root', providedIn: 'root',
}) })
export class StateService { export class StateService {
polling = false
embassyLoaded = false
recoverySource?: RecoverySource recoverySource?: RecoverySource
recoveryPassword?: string recoveryPassword?: string
@@ -21,17 +18,12 @@ export class StateService {
dataProgress = 0 dataProgress = 0
dataCompletionSubject = new BehaviorSubject(false) dataCompletionSubject = new BehaviorSubject(false)
torAddress = ''
lanAddress = ''
cert = ''
constructor( constructor(
private readonly api: ApiService, private readonly api: ApiService,
private readonly errorToastService: ErrorToastService, private readonly errorToastService: ErrorToastService,
) {} ) {}
async pollDataTransferProgress() { async pollDataTransferProgress() {
this.polling = true
await pauseFor(500) await pauseFor(500)
if (this.dataTransferProgress?.complete) { if (this.dataTransferProgress?.complete) {
@@ -39,15 +31,10 @@ export class StateService {
return return
} }
let progress
try { try {
progress = await this.api.getRecoveryStatus() const progress = await this.api.getStatus()
} catch (e: any) { if (!progress) return
this.errorToastService.present({
message: `${e.message}\n\nRestart Embassy to try again.`,
})
}
if (progress) {
this.dataTransferProgress = { this.dataTransferProgress = {
bytesTransferred: progress['bytes-transferred'], bytesTransferred: progress['bytes-transferred'],
totalBytes: progress['total-bytes'], totalBytes: progress['total-bytes'],
@@ -58,25 +45,26 @@ export class StateService {
this.dataTransferProgress.bytesTransferred / this.dataTransferProgress.bytesTransferred /
this.dataTransferProgress.totalBytes this.dataTransferProgress.totalBytes
} }
} catch (e: any) {
this.errorToastService.present({
message: `${e.message}\n\nRestart Embassy to try again.`,
})
} }
setTimeout(() => this.pollDataTransferProgress(), 0) // prevent call stack from growing setTimeout(() => this.pollDataTransferProgress(), 0) // prevent call stack from growing
} }
async importDrive(guid: string, password: string): Promise<void> { async importDrive(guid: string, password: string): Promise<void> {
const ret = await this.api.importDrive({ await this.api.attach({
guid, guid,
'embassy-password': await this.api.encrypt(password), 'embassy-password': await this.api.encrypt(password),
}) })
this.torAddress = ret['tor-address']
this.lanAddress = ret['lan-address']
this.cert = ret['root-ca']
} }
async setupEmbassy( async setupEmbassy(
storageLogicalname: string, storageLogicalname: string,
password: string, password: string,
): Promise<void> { ): Promise<void> {
const ret = await this.api.setupEmbassy({ await this.api.execute({
'embassy-logicalname': storageLogicalname, 'embassy-logicalname': storageLogicalname,
'embassy-password': await this.api.encrypt(password), 'embassy-password': await this.api.encrypt(password),
'recovery-source': this.recoverySource || null, 'recovery-source': this.recoverySource || null,
@@ -84,15 +72,5 @@ export class StateService {
? await this.api.encrypt(this.recoveryPassword) ? await this.api.encrypt(this.recoveryPassword)
: null, : null,
}) })
this.torAddress = ret['tor-address']
this.lanAddress = ret['lan-address']
this.cert = ret['root-ca']
}
async completeEmbassy(): Promise<void> {
const ret = await this.api.setupComplete()
this.torAddress = ret['tor-address']
this.lanAddress = ret['lan-address']
this.cert = ret['root-ca']
} }
} }

View File

@@ -1,4 +1,5 @@
import { Injectable } from '@angular/core' import { DOCUMENT } from '@angular/common'
import { Inject, Injectable } from '@angular/core'
import { WorkspaceConfig } from '@start9labs/shared' import { WorkspaceConfig } from '@start9labs/shared'
import { import {
InterfaceDef, InterfaceDef,
@@ -18,7 +19,9 @@ const {
providedIn: 'root', providedIn: 'root',
}) })
export class ConfigService { export class ConfigService {
origin = removePort(removeProtocol(window.origin)) constructor(@Inject(DOCUMENT) private readonly document: Document) {}
hostname = this.document.location.hostname
version = require('../../../../../package.json').version as string version = require('../../../../../package.json').version as string
useMocks = useMocks useMocks = useMocks
mocks = mocks mocks = mocks
@@ -31,13 +34,15 @@ export class ConfigService {
isTor(): boolean { isTor(): boolean {
return ( return (
(useMocks && mocks.maskAs === 'tor') || this.origin.endsWith('.onion') this.hostname.endsWith('.onion') || (useMocks && mocks.maskAs === 'tor')
) )
} }
isLan(): boolean { isLan(): boolean {
return ( return (
(useMocks && mocks.maskAs === 'lan') || this.origin.endsWith('.local') this.hostname === 'localhost' ||
this.hostname.endsWith('.local') ||
(useMocks && mocks.maskAs === 'lan')
) )
} }

View File

@@ -1486,7 +1486,10 @@ dependencies = [
"futures", "futures",
"models", "models",
"pin-project", "pin-project",
"serde",
"tokio", "tokio",
"tokio-stream",
"tracing",
] ]
[[package]] [[package]]
@@ -2038,13 +2041,23 @@ dependencies = [
name = "models" name = "models"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bollard",
"color-eyre",
"ed25519-dalek",
"embassy_container_init", "embassy_container_init",
"emver", "emver",
"mbrman",
"openssl",
"patch-db", "patch-db",
"rand 0.8.5", "rand 0.8.5",
"rpc-toolkit",
"serde", "serde",
"serde_json",
"sqlx",
"thiserror", "thiserror",
"tokio", "tokio",
"torut",
"tracing",
] ]
[[package]] [[package]]