mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
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:
@@ -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,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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.`,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ion-card-title {
|
||||||
|
font-size: 42px;
|
||||||
|
}
|
||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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' },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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' },
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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']
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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')
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
13
system-images/compat/Cargo.lock
generated
13
system-images/compat/Cargo.lock
generated
@@ -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]]
|
||||||
|
|||||||
Reference in New Issue
Block a user