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,67 +233,69 @@ pub async fn recover_full_embassy(
.await?; .await?;
secret_store.close().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(( Ok((
disk_guid,
hostname,
os_backup.tor_key.public().get_onion_address(), os_backup.tor_key.public().get_onion_address(),
os_backup.root_ca_cert, 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()
)) ))
} }

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,73 +104,91 @@ 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 password: Option<String> = match password { let mut status = ctx.setup_status.write().await;
Some(a) => match a.decrypt(&*ctx) { if status.is_some() {
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( return Err(Error::new(
eyre!( eyre!("Setup already in progress"),
"Errors were corrected with your disk, but the Embassy must be restarted in order to proceed" ErrorKind::InvalidRequest,
),
ErrorKind::DiskManagement,
)); ));
} }
let (hostname, tor_addr, root_ca) = setup_init(&ctx, password).await?; *status = Some(Ok(SetupStatus {
init(&RpcContextConfig::load(ctx.config_path.clone()).await?).await?; bytes_transferred: 0,
let setup_result = SetupResult { total_bytes: 0,
tor_address: format!("http://{}", tor_addr), complete: false,
lan_address: hostname.lan_address(), }));
root_ca: String::from_utf8(root_ca.to_pem()?)?, drop(status);
}; tokio::task::spawn(async move {
*ctx.setup_result.write().await = Some((guid, setup_result.clone())); if let Err(e) = async {
Ok(setup_result) let password: Option<String> = match password {
} Some(a) => match a.decrypt(&*ctx) {
a @ Some(_) => a,
#[command(subcommands(recovery_status))] None => {
pub fn recovery() -> Result<(), Error> { 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(()) 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,29 +268,56 @@ pub async fn execute(
}, },
None => None, None => None,
}; };
match execute_inner( let mut status = ctx.setup_status.write().await;
ctx.clone(), if status.is_some() {
embassy_logicalname, return Err(Error::new(
embassy_password, eyre!("Setup already in progress"),
recovery_source, ErrorKind::InvalidRequest,
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)
}
} }
*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))] #[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,161 +369,20 @@ 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
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
} else if let Some(RecoverySource::Migrate { guid: old_guid }) = recovery_source { } else if let Some(RecoverySource::Migrate { guid: old_guid }) = recovery_source {
let _ = crate::disk::main::mount_fs( migrate(ctx, guid, &old_guid, embassy_password).await
&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
} else { } else {
let (tor_addr, root_ca) = fresh_setup(&ctx, &embassy_password).await?; let (hostname, tor_addr, root_ca) = fresh_setup(&ctx, &embassy_password).await?;
let db = init(&RpcContextConfig::load(ctx.config_path.clone()).await?) Ok((guid, hostname, tor_addr, root_ca))
.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)
} }
async fn fresh_setup( async fn fresh_setup(
ctx: &SetupContext, ctx: &SetupContext,
embassy_password: &str, embassy_password: &str,
) -> Result<(OnionAddressV3, X509), Error> { ) -> Result<(Hostname, OnionAddressV3, X509), Error> {
let password = argon2::hash_encoded( let password = argon2::hash_encoded(
embassy_password.as_bytes(), embassy_password.as_bytes(),
&rand::random::<[u8; 16]>()[..], &rand::random::<[u8; 16]>()[..],
@@ -521,13 +400,18 @@ async fn fresh_setup(
) )
.execute(&mut sqlite_pool.acquire().await?) .execute(&mut sqlite_pool.acquire().await?)
.await?; .await?;
let db = ctx.db(&sqlite_pool).await?; sqlite_pool.close().await;
let (_, root_ca) = SslManager::init(sqlite_pool.clone(), &mut db.handle()) 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? .await?
.export_root_ca() .export_root_ca()
.await?; .await?;
sqlite_pool.close().await; secret_store.close().await;
Ok((tor_key.public().get_onion_address(), root_ca)) Ok((hostname, tor_key.public().get_onion_address(), root_ca))
} }
#[instrument(skip(ctx, embassy_password, recovery_password))] #[instrument(skip(ctx, embassy_password, recovery_password))]
@@ -537,7 +421,7 @@ async fn recover(
embassy_password: String, embassy_password: String,
recovery_source: BackupTargetFS, recovery_source: BackupTargetFS,
recovery_password: Option<String>, recovery_password: Option<String>,
) -> Result<(OnionAddressV3, X509, BoxFuture<'static, Result<(), Error>>), Error> { ) -> Result<(Arc<String>, Hostname, OnionAddressV3, X509), Error> {
let recovery_source = TmpMountGuard::mount(&recovery_source, ReadOnly).await?; let recovery_source = TmpMountGuard::mount(&recovery_source, ReadOnly).await?;
recover_full_embassy( recover_full_embassy(
ctx.clone(), ctx.clone(),
@@ -548,3 +432,75 @@ async fn recover(
) )
.await .await
} }
#[instrument(skip(ctx, embassy_password))]
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(
&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))
}

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,245 +2,267 @@
<ion-grid> <ion-grid>
<ion-row> <ion-row>
<ion-col> <ion-col>
<ion-card color="dark"> <!-- kiosk mode -->
<ion-card-header class="ion-text-center" color="success"> <ng-container *ngIf="isKiosk; else notKiosk">
<ion-icon <ion-card color="dark">
style="font-size: 80px" <ion-card-header class="ion-text-center" color="success">
name="checkmark-circle-outline" <ion-icon
></ion-icon> style="font-size: 80px"
<ion-card-title>Setup Complete</ion-card-title> name="checkmark-circle-outline"
<ion-card-subtitle ></ion-icon>
><b <ion-card-title>Setup Complete</ion-card-title>
>You have successfully claimed your Embassy!</b <ion-card-subtitle
></ion-card-subtitle ><b>You will be redirected momentarily</b></ion-card-subtitle
>
<br />
</ion-card-header>
<ion-card-content>
<br />
<br />
<h2 *ngIf="recoverySource" class="ion-padding-bottom">
<span *ngIf="recoverySource.type === 'backup'"
>You can now safely unplug your backup drive.</span
> >
<span *ngIf="recoverySource.type === 'migrate'"
>You can now safely unplug your old drive.</span
>
</h2>
<h2 style="font-weight: bold">
Access your Embassy using the methods below. You should
<a (click)="download()" class="inline">
download this page <ion-icon name="download-outline"></ion-icon>
</a>
for your records.
</h2>
<div class="line"></div>
<!-- LAN Instructions -->
<h1><b>From Home (LAN)</b></h1>
<div class="ion-padding ion-text-start">
<p>
Visit the address below when you are connected to the same WiFi
or Local Area Network (LAN) as your Embassy:
</p>
<br /> <br />
</ion-card-header>
</ion-card>
</ng-container>
<p> <!-- not kiosk -->
<b>Note:</b> embassy.local was for setup purposes only, it will <ng-template #notKiosk>
no longer work. <ion-card color="dark">
</p> <ion-card-header class="ion-text-center" color="success">
<ion-icon
<ion-item style="font-size: 80px"
lines="none" name="checkmark-circle-outline"
color="dark" ></ion-icon>
class="ion-padding-top ion-padding-bottom" <ion-card-title>Setup Complete</ion-card-title>
<ion-card-subtitle
><b>See below for next steps</b></ion-card-subtitle
> >
<ion-label class="ion-text-wrap"> <br />
<code </ion-card-header>
><ion-text color="light" <ion-card-content>
><b>{{ lanAddress }}</b></ion-text <br />
></code <br />
> <h2 *ngIf="recoverySource" class="ion-padding-bottom">
</ion-label> <span *ngIf="recoverySource.type === 'backup'"
<ion-button >You can now safely unplug your backup drive.</span
color="light"
fill="clear"
[href]="lanAddress"
target="_blank"
> >
<ion-icon slot="icon-only" name="open-outline"></ion-icon> <span *ngIf="recoverySource.type === 'migrate'"
</ion-button> >You can now safely unplug your old drive.</span
<ion-button
color="light"
fill="clear"
(click)="copy(lanAddress)"
> >
<ion-icon slot="icon-only" name="copy-outline"></ion-icon> </h2>
</ion-button> <h2 style="font-weight: bold">
</ion-item> Access your Embassy using the methods below. You should
<a (click)="download()" class="inline">
<p> download this page
<b>Important!</b> <ion-icon name="download-outline"></ion-icon>
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
<a
href="https://docs.start9.com/latest/user-manual/connecting/connecting-lan"
target="_blank"
rel="noreferrer"
class="inline"
>
follow the instructions
<ion-icon name="open-outline"></ion-icon>
</a> </a>
to download and trust your Embassy's Root Certificate Authority. for your records.
</p> </h2>
<ion-button style="margin-top: 24px" (click)="installCert()"> <div class="line"></div>
Download Root CA
<ion-icon slot="end" name="download-outline"></ion-icon>
</ion-button>
</div>
<div class="line"></div> <!-- LAN Instructions -->
<h1><b>From Home (LAN)</b></h1>
<!-- Tor Instructions --> <div class="ion-padding ion-text-start">
<h1><b>On The Go (Tor)</b></h1> <p>
Visit the address below when you are connected to the same
WiFi or Local Area Network (LAN) as your Embassy:
</p>
<div class="ion-padding ion-text-start"> <br />
<p>Visit the address below when you are away from home:</p>
<ion-item <p>
lines="none" <b>Note:</b> embassy.local was for setup purposes only, it
color="dark" will no longer work.
class="ion-padding-top ion-padding-bottom" </p>
>
<ion-label class="ion-text-wrap"> <ion-item
<code lines="none"
><ion-text color="light" color="dark"
><b>{{ torAddress }}</b></ion-text class="ion-padding-top ion-padding-bottom"
></code >
<ion-label class="ion-text-wrap">
<code
><ion-text color="light"
><b>{{ lanAddress }}</b></ion-text
></code
>
</ion-label>
<ion-button
color="light"
fill="clear"
[href]="lanAddress"
target="_blank"
> >
</ion-label> <ion-icon slot="icon-only" name="open-outline"></ion-icon>
<ion-button </ion-button>
color="light" <ion-button
fill="clear" color="light"
(click)="copy(torAddress)" fill="clear"
> (click)="copy(lanAddress)"
<ion-icon slot="icon-only" name="copy-outline"></ion-icon> >
</ion-button> <ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-item> </ion-button>
</ion-item>
<p> <p>
<b>Important!</b> <b>Important!</b>
This address will only work from a Your browser will warn you that the website is untrusted. You
<a can bypass this warning on most browsers. The warning will go
href="https://docs.start9.com/latest/user-manual/connecting/connecting-tor" away after you
target="_blank" <a
rel="noreferrer" href="https://docs.start9.com/latest/user-manual/connecting/connecting-lan"
class="inline" target="_blank"
> rel="noreferrer"
Tor-enabled browser class="inline"
<ion-icon name="open-outline"></ion-icon> </a >
>. follow the instructions
</p> <ion-icon name="open-outline"></ion-icon>
</div> </a>
</ion-card-content> to download and trust your Embassy's Root Certificate
<div id="bottom-div"></div> Authority.
</ion-card> </p>
<!-- scroll down --> <ion-button style="margin-top: 24px" (click)="installCert()">
<div
[ngStyle]="{
position: 'fixed',
bottom: isOnBottom ? '-42px' : '24px',
transition: 'bottom 0.15s ease-out 0s',
right: '50%',
width: '120px',
'margin-right': '-60px',
'z-index': '1000'
}"
>
<ion-button color="warning" (click)="scrollToBottom()">
More
<ion-icon slot="end" name="chevron-down"></ion-icon>
</ion-button>
</div>
<!-- cert elem -->
<a hidden id="install-cert" download="embassy.crt"></a>
<!-- download elem -->
<div hidden id="downloadable">
<div style="padding: 0 24px; font-family: Courier">
<h1>Embassy Info</h1>
<section style="padding: 16px; border: solid 1px">
<h2>Tor Info</h2>
<p>
To use your Embassy over Tor, visit its unique Tor address from
any Tor-enabled browser.
</p>
<p>
For more detailed instructions, click
<a
href="https://docs.start9.com/latest/user-manual/connecting/connecting-tor"
target="_blank"
rel="noreferrer"
><b>here</b></a
>.
</p>
<p><b>Tor Address: </b><code id="tor-addr"></code></p>
</section>
<section style="padding: 16px; border: solid 1px; border-top: none">
<h2>LAN Info</h2>
<p>To use your Embassy locally, you must:</p>
<ol>
<li>
Currently be connected to the same Local Area Network (LAN) as
your Embassy.
</li>
<li>Download your Embassy's Root Certificate Authority.</li>
<li>
Trust your Embassy's Root CA on <i>both</i> your
computer/phone and in your browser settings.
</li>
</ol>
<p>
For step-by-step instructions, click
<a
href="https://docs.start9.com/latest/user-manual/connecting/connecting-lan"
target="_blank"
rel="noreferrer"
><b>here</b></a
>.
</p>
<div style="margin: 42px 0">
<a
id="cert"
download="embassy.crt"
style="
background: #25272b;
padding: 10px;
text-decoration: none;
text-align: center;
border-radius: 4px;
color: white;
"
>
Download Root CA Download Root CA
</a> <ion-icon slot="end" name="download-outline"></ion-icon>
</ion-button>
</div> </div>
<p><b>LAN Address: </b><code id="lan-addr"></code></p> <div class="line"></div>
</section>
<!-- Tor Instructions -->
<h1><b>On The Go (Tor)</b></h1>
<div class="ion-padding ion-text-start">
<p>Visit the address below when you are away from home:</p>
<ion-item
lines="none"
color="dark"
class="ion-padding-top ion-padding-bottom"
>
<ion-label class="ion-text-wrap">
<code
><ion-text color="light"
><b>{{ torAddress }}</b></ion-text
></code
>
</ion-label>
<ion-button
color="light"
fill="clear"
(click)="copy(torAddress)"
>
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-item>
<p>
<b>Important!</b>
This address will only work from a
<a
href="https://docs.start9.com/latest/user-manual/connecting/connecting-tor"
target="_blank"
rel="noreferrer"
class="inline"
>
Tor-enabled browser
<ion-icon name="open-outline"></ion-icon> </a
>.
</p>
</div>
</ion-card-content>
<div id="bottom-div"></div>
</ion-card>
<!-- scroll down -->
<div
[ngStyle]="{
position: 'fixed',
bottom: isOnBottom ? '-42px' : '24px',
transition: 'bottom 0.15s ease-out 0s',
right: '50%',
width: '120px',
'margin-right': '-60px',
'z-index': '1000'
}"
>
<ion-button color="warning" (click)="scrollToBottom()">
More
<ion-icon slot="end" name="chevron-down"></ion-icon>
</ion-button>
</div> </div>
</div>
<!-- cert elem -->
<a hidden id="install-cert" download="embassy.crt"></a>
<!-- download elem -->
<div hidden id="downloadable">
<div style="padding: 0 24px; font-family: Courier">
<h1>Embassy Info</h1>
<section style="padding: 16px; border: solid 1px">
<h2>Tor Info</h2>
<p>
To use your Embassy over Tor, visit its unique Tor address
from any Tor-enabled browser.
</p>
<p>
For more detailed instructions, click
<a
href="https://docs.start9.com/latest/user-manual/connecting/connecting-tor"
target="_blank"
rel="noreferrer"
><b>here</b></a
>.
</p>
<p><b>Tor Address: </b><code id="tor-addr"></code></p>
</section>
<section
style="padding: 16px; border: solid 1px; border-top: none"
>
<h2>LAN Info</h2>
<p>To use your Embassy locally, you must:</p>
<ol>
<li>
Currently be connected to the same Local Area Network (LAN)
as your Embassy.
</li>
<li>Download your Embassy's Root Certificate Authority.</li>
<li>
Trust your Embassy's Root CA on <i>both</i> your
computer/phone and in your browser settings.
</li>
</ol>
<p>
For step-by-step instructions, click
<a
href="https://docs.start9.com/latest/user-manual/connecting/connecting-lan"
target="_blank"
rel="noreferrer"
><b>here</b></a
>.
</p>
<div style="margin: 42px 0">
<a
id="cert"
download="embassy.crt"
style="
background: #25272b;
padding: 10px;
text-decoration: none;
text-align: center;
border-radius: 4px;
color: white;
"
>
Download Root CA
</a>
</div>
<p><b>LAN Address: </b><code id="lan-addr"></code></p>
</section>
</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() {
setTimeout(() => this.checkBottom(), 42)
try { try {
await this.stateService.completeEmbassy() const ret = await this.api.complete()
this.document if (!this.isKiosk) {
.getElementById('install-cert') setTimeout(() => this.checkBottom(), 42)
?.setAttribute(
'href', this.torAddress = ret['tor-address']
'data:application/x-x509-ca-cert;base64,' + this.lanAddress = ret['lan-address']
encodeURIComponent(this.stateService.cert), this.cert = ret['root-ca']
)
this.download() this.document
.getElementById('install-cert')
?.setAttribute(
'href',
'data:application/x-x509-ca-cert;base64,' +
encodeURIComponent(this.cert),
)
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]]