remove product key from setup flow (#1750)

* remove product key flow from setup

* feat: backend turned off encryption + new Id + no package id

* implement new encryption scheme in FE

* decode response string

* crypto not working

* update setup wizard closes #1762

* feat: Get the encryption key

* fix: Get to recovery

* remove old code

* fix build

* fix: Install works for now

* fix bug in config for adding new list items

* dismiss action modal on success

* clear button in config

* wip: Currently broken in avahi mdns

* include headers with req/res and refactor patchDB init and usage

* fix: Can now run in the main

* flatline on failed init

* update patch DB

* add last-wifi-region to data model even though not used by FE

* chore: Fix the start.

* wip: Fix wrong order for getting hostname before sql has been
created

* fix edge case where union keys displayed as new when not new

* fix: Can start

* last backup color, markdown links always new tab, fix bug with login

* refactor to remove WithRevision

* resolve circular dep issue

* update submodule

* fix patch-db

* update patchDB

* update patch again

* escape error

* decodeuricomponent

* increase proxy buffer size

* increase proxy buffer size

* fix nginx

Co-authored-by: BluJ <mogulslayer@gmail.com>
Co-authored-by: BluJ <dragondef@gmail.com>
Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
Matt Hill
2022-09-07 09:25:01 -06:00
committed by GitHub
parent 76682ebef0
commit 50111e37da
175 changed files with 11436 additions and 2906 deletions

669
backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -80,6 +80,7 @@ imbl = "2.0.0"
indexmap = { version = "1.9.1", features = ["serde"] }
isocountry = "0.3.2"
itertools = "0.10.3"
josekit = "0.8.1"
js_engine = { path = '../libs/js_engine', optional = true }
jsonpath_lib = "0.3.0"
lazy_static = "1.4.0"
@@ -141,6 +142,7 @@ tracing-subscriber = { version = "0.3.14", features = ["env-filter"] }
trust-dns-server = "0.21.2"
typed-builder = "0.10.0"
url = { version = "2.2.2", features = ["serde"] }
uuid = { version = "1.1.2", features = ["v4"] }
[profile.test]
opt-level = 3

View File

@@ -6,7 +6,8 @@ Wants=avahi-daemon.service nginx.service tor.service
[Service]
Type=oneshot
Environment=RUST_LOG=embassy_init=debug,embassy=debug,js_engine=debug
Environment=RUST_LOG=embassy_init=debug,embassy=debug,js_engine=debug,patch_db=trace
Environment=RUST_LIB_BACKTRACE=full
ExecStart=/usr/local/bin/embassy-init
RemainAfterExit=true
StandardOutput=file:/var/log/embassy-init.out.log

File diff suppressed because it is too large Load Diff

7776
backend/src/assets/nouns.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,6 @@ use crate::auth::check_password_against_db;
use crate::backup::{BackupReport, ServerBackupReport};
use crate::context::RpcContext;
use crate::db::model::BackupProgress;
use crate::db::util::WithRevision;
use crate::disk::mount::backup::BackupMountGuard;
use crate::disk::mount::filesystem::ReadWrite;
use crate::disk::mount::guard::TmpMountGuard;
@@ -135,7 +134,7 @@ pub async fn backup_all(
)]
package_ids: Option<BTreeSet<PackageId>>,
#[arg] password: String,
) -> Result<WithRevision<()>, Error> {
) -> Result<(), Error> {
let mut db = ctx.db.handle();
check_password_against_db(&mut ctx.secret_store.acquire().await?, &password).await?;
let fs = target_id
@@ -159,7 +158,7 @@ pub async fn backup_all(
if old_password.is_some() {
backup_guard.change_password(&password)?;
}
let revision = assure_backing_up(&mut db, &package_ids).await?;
assure_backing_up(&mut db, &package_ids).await?;
tokio::task::spawn(async move {
let backup_res = perform_backup(&ctx, &mut db, backup_guard, &package_ids).await;
let backup_progress = crate::db::DatabaseModel::new()
@@ -238,17 +237,14 @@ pub async fn backup_all(
.await
.expect("failed to change server status");
});
Ok(WithRevision {
response: (),
revision,
})
Ok(())
}
#[instrument(skip(db, packages))]
async fn assure_backing_up(
db: &mut PatchDbHandle,
packages: impl IntoIterator<Item = &PackageId>,
) -> Result<Option<Arc<Revision>>, Error> {
) -> Result<(), Error> {
let mut tx = db.begin().await?;
let mut backing_up = crate::db::DatabaseModel::new()
.server_info()
@@ -279,7 +275,8 @@ async fn assure_backing_up(
.collect(),
);
backing_up.save(&mut tx).await?;
Ok(tx.commit(None).await?)
tx.commit().await?;
Ok(())
}
#[instrument(skip(ctx, db, backup_guard))]

View File

@@ -20,7 +20,6 @@ use super::target::BackupTargetId;
use crate::backup::backup_bulk::OsBackup;
use crate::context::{RpcContext, SetupContext};
use crate::db::model::{PackageDataEntry, StaticFiles};
use crate::db::util::WithRevision;
use crate::disk::mount::backup::{BackupMountGuard, PackageBackupMountGuard};
use crate::disk::mount::filesystem::ReadOnly;
use crate::disk::mount::guard::TmpMountGuard;
@@ -50,7 +49,7 @@ pub async fn restore_packages_rpc(
#[arg(parse(parse_comma_separated))] ids: Vec<PackageId>,
#[arg(rename = "target-id")] target_id: BackupTargetId,
#[arg] password: String,
) -> Result<WithRevision<()>, Error> {
) -> Result<(), Error> {
let mut db = ctx.db.handle();
let fs = target_id
.load(&mut ctx.secret_store.acquire().await?)
@@ -114,10 +113,7 @@ pub async fn restore_packages_rpc(
}
});
Ok(WithRevision {
response: (),
revision,
})
Ok(())
}
async fn approximate_progress(
@@ -418,7 +414,7 @@ async fn assure_restoring(
guards.push((manifest, guard));
}
Ok((tx.commit(None).await?, guards))
Ok((tx.commit().await?, guards))
}
#[instrument(skip(ctx, guard))]

View File

@@ -7,11 +7,9 @@ use embassy::context::{DiagnosticContext, SetupContext};
use embassy::disk::fsck::RepairStrategy;
use embassy::disk::main::DEFAULT_PASSWORD;
use embassy::disk::REPAIR_DISK_PATH;
use embassy::hostname::get_product_key;
use embassy::init::STANDBY_MODE_PATH;
use embassy::middleware::cors::cors;
use embassy::middleware::diagnostic::diagnostic;
use embassy::middleware::encrypt::encrypt;
#[cfg(feature = "avahi")]
use embassy::net::mdns::MdnsController;
use embassy::shutdown::Shutdown;
@@ -50,12 +48,7 @@ async fn setup_or_init(cfg_path: Option<&str>) -> Result<(), Error> {
.invoke(embassy::ErrorKind::Nginx)
.await?;
let ctx = SetupContext::init(cfg_path).await?;
let keysource_ctx = ctx.clone();
let keysource = move || {
let ctx = keysource_ctx.clone();
async move { ctx.product_key().await }
};
let encrypt = encrypt(keysource);
let encrypt = embassy::middleware::encrypt::encrypt(ctx.clone());
tokio::time::sleep(Duration::from_secs(1)).await; // let the record state that I hate this
CHIME.play().await?;
rpc_server!({
@@ -103,7 +96,7 @@ async fn setup_or_init(cfg_path: Option<&str>) -> Result<(), Error> {
.await?;
}
tracing::info!("Loaded Disk");
embassy::init::init(&cfg, &get_product_key().await?).await?;
embassy::init::init(&cfg).await?;
}
Ok(())

View File

@@ -7,6 +7,7 @@ use embassy::core::rpc_continuations::RequestGuid;
use embassy::db::subscribe;
use embassy::middleware::auth::auth;
use embassy::middleware::cors::cors;
use embassy::middleware::db::db as db_middleware;
use embassy::middleware::diagnostic::diagnostic;
#[cfg(feature = "avahi")]
use embassy::net::mdns::MdnsController;
@@ -40,7 +41,6 @@ fn err_to_500(e: Error) -> Response<Body> {
#[instrument]
async fn inner_main(cfg_path: Option<&str>) -> Result<Option<Shutdown>, Error> {
let (rpc_ctx, shutdown) = {
embassy::hostname::sync_hostname().await?;
let rpc_ctx = RpcContext::init(
cfg_path,
Arc::new(
@@ -82,11 +82,13 @@ async fn inner_main(cfg_path: Option<&str>) -> Result<Option<Shutdown>, Error> {
});
let mut db = rpc_ctx.db.handle();
embassy::hostname::sync_hostname(&mut db).await?;
let receipts = embassy::context::rpc::RpcSetNginxReceipts::new(&mut db).await?;
rpc_ctx.set_nginx_conf(&mut db, receipts).await?;
drop(db);
let auth = auth(rpc_ctx.clone());
let db_middleware = db_middleware(rpc_ctx.clone());
let ctx = rpc_ctx.clone();
let server = rpc_server!({
command: embassy::main_api,
@@ -95,6 +97,7 @@ async fn inner_main(cfg_path: Option<&str>) -> Result<Option<Shutdown>, Error> {
middleware: [
cors,
auth,
db_middleware,
]
})
.with_graceful_shutdown({
@@ -112,29 +115,6 @@ async fn inner_main(cfg_path: Option<&str>) -> Result<Option<Shutdown>, Error> {
.await
});
let rev_cache_ctx = rpc_ctx.clone();
let revision_cache_task = tokio::spawn(async move {
let mut sub = rev_cache_ctx.db.subscribe();
let mut shutdown = rev_cache_ctx.shutdown.subscribe();
loop {
let rev = match tokio::select! {
a = sub.recv() => a,
_ = shutdown.recv() => break,
} {
Ok(a) => a,
Err(_) => {
rev_cache_ctx.revision_cache.write().await.truncate(0);
continue;
}
}; // TODO: handle falling behind
let mut cache = rev_cache_ctx.revision_cache.write().await;
cache.push_back(rev);
if cache.len() > rev_cache_ctx.revision_cache_size {
cache.pop_front();
}
}
});
let ws_ctx = rpc_ctx.clone();
let ws_server = {
let builder = Server::bind(&ws_ctx.bind_ws);
@@ -268,12 +248,6 @@ async fn inner_main(cfg_path: Option<&str>) -> Result<Option<Shutdown>, Error> {
ErrorKind::Unknown
))
.map_ok(|_| tracing::debug!("Metrics daemon Shutdown")),
revision_cache_task
.map_err(|e| Error::new(
eyre!("{}", e).wrap_err("Revision Cache daemon panicked!"),
ErrorKind::Unknown
))
.map_ok(|_| tracing::debug!("Revision Cache daemon Shutdown")),
ws_server
.map_err(|e| Error::new(e, ErrorKind::Network))
.map_ok(|_| tracing::debug!("WebSocket Server Shutdown")),

View File

@@ -15,7 +15,6 @@ use tracing::instrument;
use crate::context::RpcContext;
use crate::db::model::{CurrentDependencies, CurrentDependencyInfo, CurrentDependents};
use crate::db::util::WithRevision;
use crate::dependencies::{
add_dependent_to_current_dependents_lists, break_transitive, heal_all_dependents_transitive,
BreakTransitiveReceipts, BreakageRes, Dependencies, DependencyConfig, DependencyError,
@@ -237,7 +236,8 @@ pub async fn get(
#[command(
subcommands(self(set_impl(async, context(RpcContext))), set_dry),
display(display_none)
display(display_none),
metadata(sync_db = true)
)]
#[instrument]
pub fn set(
@@ -247,9 +247,8 @@ pub fn set(
format: Option<IoFormat>,
#[arg(long = "timeout")] timeout: Option<crate::util::serde::Duration>,
#[arg(stdin, parse(parse_stdin_deserializable))] config: Option<Config>,
#[arg(rename = "expire-id", long = "expire-id")] expire_id: Option<String>,
) -> Result<(PackageId, Option<Config>, Option<Duration>, Option<String>), Error> {
Ok((id, config, timeout.map(|d| *d), expire_id))
) -> Result<(PackageId, Option<Config>, Option<Duration>), Error> {
Ok((id, config, timeout.map(|d| *d)))
}
/// So, the new locking finds all the possible locks and lifts them up into a bundle of locks.
@@ -407,12 +406,7 @@ impl ConfigReceipts {
#[instrument(skip(ctx))]
pub async fn set_dry(
#[context] ctx: RpcContext,
#[parent_data] (id, config, timeout, _): (
PackageId,
Option<Config>,
Option<Duration>,
Option<String>,
),
#[parent_data] (id, config, timeout): (PackageId, Option<Config>, Option<Duration>),
) -> Result<BreakageRes, Error> {
let mut db = ctx.db.handle();
let mut tx = db.begin().await?;
@@ -439,8 +433,8 @@ pub async fn set_dry(
#[instrument(skip(ctx))]
pub async fn set_impl(
ctx: RpcContext,
(id, config, timeout, expire_id): (PackageId, Option<Config>, Option<Duration>, Option<String>),
) -> Result<WithRevision<()>, Error> {
(id, config, timeout): (PackageId, Option<Config>, Option<Duration>),
) -> Result<(), Error> {
let mut db = ctx.db.handle();
let mut tx = db.begin().await?;
let mut breakages = BTreeMap::new();
@@ -457,10 +451,8 @@ pub async fn set_impl(
&locks,
)
.await?;
Ok(WithRevision {
response: (),
revision: tx.commit(expire_id).await?,
})
tx.commit().await?;
Ok(())
}
#[instrument(skip(ctx, db, receipts))]

View File

@@ -22,7 +22,6 @@ use tracing::instrument;
use crate::core::rpc_continuations::{RequestGuid, RestHandler, RpcContinuation};
use crate::db::model::{Database, InstalledPackageDataEntry, PackageDataEntry};
use crate::hostname::{derive_hostname, derive_id, get_product_key};
use crate::init::{init_postgres, pgloader};
use crate::install::cleanup::{cleanup_failed, uninstall, CleanupFailedReceipts};
use crate::manager::ManagerMap;
@@ -71,23 +70,18 @@ impl RpcContextConfig {
.as_deref()
.unwrap_or_else(|| Path::new("/embassy-data"))
}
pub async fn db(&self, secret_store: &PgPool, product_key: &str) -> Result<PatchDb, Error> {
let sid = derive_id(product_key);
let hostname = derive_hostname(&sid);
pub async fn db(&self, secret_store: &PgPool) -> Result<PatchDb, Error> {
let db_path = self.datadir().join("main").join("embassy.db");
let db = PatchDb::open(&db_path)
.await
.with_ctx(|_| (crate::ErrorKind::Filesystem, db_path.display().to_string()))?;
if !db.exists(&<JsonPointer>::default()).await? {
if !db.exists(&<JsonPointer>::default()).await {
db.put(
&<JsonPointer>::default(),
&Database::init(
sid,
&hostname,
&os_key(&mut secret_store.acquire().await?).await?,
password_hash(&mut secret_store.acquire().await?).await?,
),
None,
)
.await?;
}
@@ -216,7 +210,7 @@ impl RpcContext {
let (shutdown, _) = tokio::sync::broadcast::channel(1);
let secret_store = base.secret_store().await?;
tracing::info!("Opened Pg DB");
let db = base.db(&secret_store, &get_product_key().await?).await?;
let db = base.db(&secret_store).await?;
tracing::info!("Opened PatchDB");
let docker = Docker::connect_with_unix_defaults()?;
tracing::info!("Connected to Docker");
@@ -231,6 +225,7 @@ impl RpcContext {
.unwrap_or(&[SocketAddr::from(([127, 0, 0, 1], 53))]),
secret_store.clone(),
None,
&mut db.handle(),
)
.await?;
tracing::info!("Initialized Net Controller");

View File

@@ -2,29 +2,28 @@ use std::net::{IpAddr, SocketAddr};
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use patch_db::json_ptr::JsonPointer;
use patch_db::PatchDb;
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use rpc_toolkit::yajrc::RpcError;
use rpc_toolkit::Context;
use serde::{Deserialize, Serialize};
use sqlx::postgres::PgConnectOptions;
use sqlx::PgPool;
use tokio::fs::File;
use tokio::process::Command;
use tokio::sync::broadcast::Sender;
use tokio::sync::RwLock;
use tracing::instrument;
use url::Host;
use crate::db::model::Database;
use crate::hostname::{derive_hostname, derive_id, get_product_key};
use crate::init::{init_postgres, pgloader};
use crate::net::tor::os_key;
use crate::setup::{password_hash, RecoveryStatus};
use crate::util::io::from_yaml_async_reader;
use crate::util::{AsyncFileExt, Invoke};
use crate::util::AsyncFileExt;
use crate::{Error, ResultExt};
#[derive(Clone, Serialize, Deserialize)]
@@ -69,6 +68,9 @@ pub struct SetupContextSeed {
pub bind_rpc: SocketAddr,
pub shutdown: Sender<()>,
pub datadir: PathBuf,
/// Used to encrypt for hidding from snoopers for setups create password
/// Set via path
pub current_secret: RwLock<Option<String>>,
pub selected_v2_drive: RwLock<Option<PathBuf>>,
pub cached_product_key: RwLock<Option<Arc<String>>>,
pub recovery_status: RwLock<Option<Result<RecoveryStatus, RpcError>>>,
@@ -88,6 +90,7 @@ impl SetupContext {
bind_rpc: cfg.bind_rpc.unwrap_or(([127, 0, 0, 1], 5959).into()),
shutdown,
datadir,
current_secret: RwLock::new(None),
selected_v2_drive: RwLock::new(None),
cached_product_key: RwLock::new(None),
recovery_status: RwLock::new(None),
@@ -100,19 +103,13 @@ impl SetupContext {
let db = PatchDb::open(&db_path)
.await
.with_ctx(|_| (crate::ErrorKind::Filesystem, db_path.display().to_string()))?;
if !db.exists(&<JsonPointer>::default()).await? {
let pkey = self.product_key().await?;
let sid = derive_id(&*pkey);
let hostname = derive_hostname(&sid);
if !db.exists(&<JsonPointer>::default()).await {
db.put(
&<JsonPointer>::default(),
&Database::init(
sid,
&hostname,
&os_key(&mut secret_store.acquire().await?).await?,
password_hash(&mut secret_store.acquire().await?).await?,
),
None,
)
.await?;
}
@@ -134,22 +131,17 @@ impl SetupContext {
}
Ok(secret_store)
}
#[instrument(skip(self))]
pub async fn product_key(&self) -> Result<Arc<String>, Error> {
Ok(
if let Some(k) = {
let guard = self.cached_product_key.read().await;
let res = guard.clone();
drop(guard);
res
} {
k
} else {
let k = Arc::new(get_product_key().await?);
*self.cached_product_key.write().await = Some(k.clone());
k
},
)
/// So we assume that there will only be one client that will ask for a secret,
/// And during that time do we upsert to a new key
pub async fn update_secret(&self) -> Result<String, Error> {
let new_secret: String = thread_rng()
.sample_iter(&Alphanumeric)
.take(30)
.map(char::from)
.collect();
*self.current_secret.write().await = Some(new_secret.clone());
Ok(new_secret)
}
}

View File

@@ -6,7 +6,6 @@ use rpc_toolkit::command;
use tracing::instrument;
use crate::context::RpcContext;
use crate::db::util::WithRevision;
use crate::dependencies::{
break_all_dependents_transitive, heal_all_dependents_transitive, BreakageRes, DependencyError,
DependencyReceipt, TaggedDependencyError,
@@ -61,12 +60,9 @@ impl StartReceipts {
}
}
#[command(display(display_none))]
#[command(display(display_none), metadata(sync_db = true))]
#[instrument(skip(ctx))]
pub async fn start(
#[context] ctx: RpcContext,
#[arg] id: PackageId,
) -> Result<WithRevision<()>, Error> {
pub async fn start(#[context] ctx: RpcContext, #[arg] id: PackageId) -> Result<(), Error> {
let mut db = ctx.db.handle();
let mut tx = db.begin().await?;
let receipts = StartReceipts::new(&mut tx, &id).await?;
@@ -77,7 +73,7 @@ pub async fn start(
.await?;
heal_all_dependents_transitive(&ctx, &mut tx, &id, &receipts.dependency_receipt).await?;
let revision = tx.commit(None).await?;
tx.commit().await?;
drop(receipts);
ctx.managers
@@ -87,10 +83,7 @@ pub async fn start(
.synchronize()
.await;
Ok(WithRevision {
revision,
response: (),
})
Ok(())
}
#[derive(Clone)]
pub struct StopReceipts {
@@ -150,7 +143,11 @@ async fn stop_common<Db: DbHandle>(
Ok(())
}
#[command(subcommands(self(stop_impl(async)), stop_dry), display(display_none))]
#[command(
subcommands(self(stop_impl(async)), stop_dry),
display(display_none),
metadata(sync_db = true)
)]
pub fn stop(#[arg] id: PackageId) -> Result<PackageId, Error> {
Ok(id)
}
@@ -173,23 +170,19 @@ pub async fn stop_dry(
}
#[instrument(skip(ctx))]
pub async fn stop_impl(ctx: RpcContext, id: PackageId) -> Result<WithRevision<()>, Error> {
pub async fn stop_impl(ctx: RpcContext, id: PackageId) -> Result<(), Error> {
let mut db = ctx.db.handle();
let mut tx = db.begin().await?;
stop_common(&mut tx, &id, &mut BTreeMap::new()).await?;
Ok(WithRevision {
revision: tx.commit(None).await?,
response: (),
})
tx.commit().await?;
Ok(())
}
#[command(display(display_none))]
pub async fn restart(
#[context] ctx: RpcContext,
#[arg] id: PackageId,
) -> Result<WithRevision<()>, Error> {
#[command(display(display_none), metadata(sync_db = true))]
pub async fn restart(#[context] ctx: RpcContext, #[arg] id: PackageId) -> Result<(), Error> {
let mut db = ctx.db.handle();
let mut tx = db.begin().await?;
@@ -208,9 +201,7 @@ pub async fn restart(
}
*status = Some(MainStatus::Restarting);
status.save(&mut tx).await?;
tx.commit().await?;
Ok(WithRevision {
revision: tx.commit(None).await?,
response: (),
})
Ok(())
}

View File

@@ -1,6 +1,5 @@
pub mod model;
pub mod package;
pub mod util;
use std::future::Future;
use std::sync::Arc;
@@ -14,7 +13,7 @@ use rpc_toolkit::hyper::{Body, Error as HyperError, Request, Response};
use rpc_toolkit::yajrc::RpcError;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::sync::{broadcast, oneshot};
use tokio::sync::oneshot;
use tokio::task::JoinError;
use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
use tokio_tungstenite::tungstenite::protocol::CloseFrame;
@@ -23,7 +22,6 @@ use tokio_tungstenite::WebSocketStream;
use tracing::instrument;
pub use self::model::DatabaseModel;
use self::util::WithRevision;
use crate::context::RpcContext;
use crate::middleware::auth::{HasValidSession, HashSessionToken};
use crate::util::serde::{display_serializable, IoFormat};
@@ -37,7 +35,7 @@ async fn ws_handler<
session: Option<(HasValidSession, HashSessionToken)>,
ws_fut: WSFut,
) -> Result<(), Error> {
let (dump, sub) = ctx.db.dump_and_sub().await;
let (dump, sub) = ctx.db.dump_and_sub().await?;
let mut stream = ws_fut
.await
.with_kind(crate::ErrorKind::Network)?
@@ -79,7 +77,7 @@ async fn subscribe_to_session_kill(
async fn deal_with_messages(
_has_valid_authentication: HasValidSession,
mut kill: oneshot::Receiver<()>,
mut sub: broadcast::Receiver<Arc<Revision>>,
mut sub: patch_db::Subscriber,
mut stream: WebSocketStream<Upgraded>,
) -> Result<(), Error> {
loop {
@@ -95,8 +93,8 @@ async fn deal_with_messages(
.with_kind(crate::ErrorKind::Network)?;
return Ok(())
}
new_rev = sub.recv().fuse() => {
let rev = new_rev.with_kind(crate::ErrorKind::Database)?;
new_rev = sub.recv_async().fuse() => {
let rev = new_rev.expect("UNREACHABLE: patch-db is dropped");
stream
.send(Message::Text(serde_json::to_string(&rev).with_kind(crate::ErrorKind::Serialization)?))
.await
@@ -184,24 +182,11 @@ pub async fn revisions(
#[allow(unused_variables)]
#[arg(long = "format")]
format: Option<IoFormat>,
) -> Result<RevisionsRes, RpcError> {
let cache = ctx.revision_cache.read().await;
if cache
.front()
.map(|rev| rev.id <= since + 1)
.unwrap_or(false)
{
Ok(RevisionsRes::Revisions(
cache
.iter()
.skip_while(|rev| rev.id < since + 1)
.cloned()
.collect(),
))
} else {
drop(cache);
Ok(RevisionsRes::Dump(ctx.db.dump().await))
}
) -> Result<RevisionsRes, Error> {
Ok(match ctx.db.sync(since).await? {
Ok(revs) => RevisionsRes::Revisions(revs),
Err(dump) => RevisionsRes::Dump(dump),
})
}
#[command(display(display_serializable))]
@@ -210,8 +195,8 @@ pub async fn dump(
#[allow(unused_variables)]
#[arg(long = "format")]
format: Option<IoFormat>,
) -> Result<Dump, RpcError> {
Ok(ctx.db.dump().await)
) -> Result<Dump, Error> {
Ok(ctx.db.dump().await?)
}
#[command(subcommands(ui))]
@@ -228,13 +213,11 @@ pub async fn ui(
#[allow(unused_variables)]
#[arg(long = "format")]
format: Option<IoFormat>,
) -> Result<WithRevision<()>, Error> {
) -> Result<(), Error> {
let ptr = "/ui"
.parse::<JsonPointer>()
.with_kind(crate::ErrorKind::Database)?
+ &pointer;
Ok(WithRevision {
response: (),
revision: ctx.db.put(&ptr, &value, None).await?,
})
ctx.db.put(&ptr, &value).await?;
Ok(())
}

View File

@@ -12,6 +12,7 @@ use serde_json::Value;
use torut::onion::TorSecretKeyV3;
use crate::config::spec::{PackagePointerSpec, SystemPointerSpec};
use crate::hostname::{generate_hostname, generate_id};
use crate::install::progress::InstallProgress;
use crate::net::interface::InterfaceId;
use crate::s9pk::manifest::{Manifest, ManifestModel, PackageId};
@@ -32,21 +33,20 @@ pub struct Database {
pub ui: Value,
}
impl Database {
pub fn init(
id: String,
hostname: &str,
tor_key: &TorSecretKeyV3,
password_hash: String,
) -> Self {
pub fn init(tor_key: &TorSecretKeyV3, password_hash: String) -> Self {
let id = generate_id();
let my_hostname = generate_hostname();
let lan_address = my_hostname.lan_address().parse().unwrap();
// TODO
Database {
server_info: ServerInfo {
id,
version: Current::new().semver().into(),
hostname: Some(my_hostname.0),
last_backup: None,
last_wifi_region: None,
eos_version_compat: Current::new().compat().clone(),
lan_address: format!("https://{}.local", hostname).parse().unwrap(),
lan_address,
tor_address: format!("http://{}", tor_key.public().get_onion_address())
.parse()
.unwrap(),
@@ -83,6 +83,7 @@ impl DatabaseModel {
#[serde(rename_all = "kebab-case")]
pub struct ServerInfo {
pub id: String,
pub hostname: Option<String>,
pub version: Version,
pub last_backup: Option<DateTime<Utc>>,
/// Used in the wifi to determine the region to set the system to

View File

@@ -1,10 +0,0 @@
use std::sync::Arc;
use patch_db::Revision;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct WithRevision<T> {
pub response: T,
pub revision: Option<Arc<Revision>>,
}

View File

@@ -1,34 +1,53 @@
use digest::Digest;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
use patch_db::DbHandle;
use rand::{thread_rng, Rng};
use tokio::process::Command;
use tracing::instrument;
use crate::util::Invoke;
use crate::{Error, ErrorKind, ResultExt};
use crate::{Error, ErrorKind};
#[derive(Clone, serde::Deserialize, serde::Serialize, Debug)]
pub struct Hostname(pub String);
pub const PRODUCT_KEY_PATH: &'static str = "/embassy-os/product_key.txt";
#[instrument]
pub async fn get_hostname() -> Result<String, Error> {
Ok(derive_hostname(&get_id().await?))
lazy_static::lazy_static! {
static ref ADJECTIVES: Vec<String> = include_str!("./assets/adjectives.txt").lines().map(|x| x.to_string()).collect();
static ref NOUNS: Vec<String> = include_str!("./assets/nouns.txt").lines().map(|x| x.to_string()).collect();
}
impl AsRef<str> for Hostname {
fn as_ref(&self) -> &str {
&self.0
}
}
pub fn derive_hostname(id: &str) -> String {
format!("embassy-{}", id)
impl Hostname {
pub fn lan_address(&self) -> String {
format!("https://{}.local", self.0)
}
}
pub fn generate_hostname() -> Hostname {
let mut rng = thread_rng();
let adjective = &ADJECTIVES[rng.gen_range(0..ADJECTIVES.len())];
let noun = &NOUNS[rng.gen_range(0..NOUNS.len())];
Hostname(format!("{adjective}-{noun}"))
}
pub fn generate_id() -> String {
let id = uuid::Uuid::new_v4();
id.to_string()
}
#[instrument]
pub async fn get_current_hostname() -> Result<String, Error> {
pub async fn get_current_hostname() -> Result<Hostname, Error> {
let out = Command::new("hostname")
.invoke(ErrorKind::ParseSysInfo)
.await?;
let out_string = String::from_utf8(out)?;
Ok(out_string.trim().to_owned())
Ok(Hostname(out_string.trim().to_owned()))
}
#[instrument]
pub async fn set_hostname(hostname: &str) -> Result<(), Error> {
pub async fn set_hostname(hostname: &Hostname) -> Result<(), Error> {
let hostname: &String = &hostname.0;
let _out = Command::new("hostnamectl")
.arg("set-hostname")
.arg(hostname)
@@ -37,38 +56,36 @@ pub async fn set_hostname(hostname: &str) -> Result<(), Error> {
Ok(())
}
#[instrument]
pub async fn get_product_key() -> Result<String, Error> {
let out = tokio::fs::read_to_string(PRODUCT_KEY_PATH)
#[instrument(skip(handle))]
pub async fn get_id<Db: DbHandle>(handle: &mut Db) -> Result<String, Error> {
let id = crate::db::DatabaseModel::new()
.server_info()
.id()
.get(handle, false)
.await?;
Ok(id.to_string())
}
pub async fn get_hostname<Db: DbHandle>(handle: &mut Db) -> Result<Hostname, Error> {
if let Ok(hostname) = crate::db::DatabaseModel::new()
.server_info()
.hostname()
.get(handle, false)
.await
.with_ctx(|_| (crate::ErrorKind::Filesystem, PRODUCT_KEY_PATH))?;
Ok(out.trim().to_owned())
{
if let Some(hostname) = hostname.to_owned() {
return Ok(Hostname(hostname));
}
#[instrument]
pub async fn set_product_key(key: &str) -> Result<(), Error> {
let mut pkey_file = File::create(PRODUCT_KEY_PATH).await?;
pkey_file.write_all(key.as_bytes()).await?;
Ok(())
}
pub fn derive_id(key: &str) -> String {
let mut hasher = sha2::Sha256::new();
hasher.update(key.as_bytes());
let res = hasher.finalize();
hex::encode(&res[0..4])
let id = get_id(handle).await?;
if id.len() != 8 {
return Ok(generate_hostname());
}
#[instrument]
pub async fn get_id() -> Result<String, Error> {
let key = get_product_key().await?;
Ok(derive_id(&key))
return Ok(Hostname(format!("embassy-{}", id)));
}
// cat /embassy-os/product_key.txt | shasum -a 256 | head -c 8 | awk '{print "embassy-"$1}' | xargs hostnamectl set-hostname && systemctl restart avahi-daemon
#[instrument]
pub async fn sync_hostname() -> Result<(), Error> {
set_hostname(&format!("embassy-{}", get_id().await?)).await?;
#[instrument(skip(handle))]
pub async fn sync_hostname<Db: DbHandle>(handle: &mut Db) -> Result<(), Error> {
set_hostname(&get_hostname(handle).await?).await?;
Command::new("systemctl")
.arg("restart")
.arg("avahi-daemon")

View File

@@ -8,7 +8,6 @@ use tokio::process::Command;
use crate::context::rpc::RpcContextConfig;
use crate::db::model::ServerStatus;
use crate::disk::mount::util::unmount;
use crate::install::PKG_DOCKER_DIR;
use crate::util::Invoke;
use crate::Error;
@@ -152,7 +151,11 @@ pub async fn init_postgres(datadir: impl AsRef<Path>) -> Result<(), Error> {
Ok(())
}
pub async fn init(cfg: &RpcContextConfig, product_key: &str) -> Result<(), Error> {
pub struct InitResult {
pub db: patch_db::PatchDb,
}
pub async fn init(cfg: &RpcContextConfig) -> Result<InitResult, Error> {
let should_rebuild = tokio::fs::metadata(SYSTEM_REBUILD_PATH).await.is_ok();
let secret_store = cfg.secret_store().await?;
let log_dir = cfg.datadir().join("main/logs");
@@ -213,9 +216,14 @@ pub async fn init(cfg: &RpcContextConfig, product_key: &str) -> Result<(), Error
crate::ssh::sync_keys_from_db(&secret_store, "/home/start9/.ssh/authorized_keys").await?;
tracing::info!("Synced SSH Keys");
let db = cfg.db(&secret_store, product_key).await?;
let db = cfg.db(&secret_store).await?;
let mut handle = db.handle();
crate::db::DatabaseModel::new()
.server_info()
.lock(&mut handle, LockType::Write)
.await?;
let receipts = InitReceipts::new(&mut handle).await?;
crate::net::wifi::synchronize_wpa_supplicant_conf(
@@ -258,5 +266,5 @@ pub async fn init(cfg: &RpcContextConfig, product_key: &str) -> Result<(), Error
tracing::info!("System initialized.");
Ok(())
Ok(InitResult { db })
}

View File

@@ -375,7 +375,7 @@ where
if tokio::fs::metadata(&volumes).await.is_ok() {
tokio::fs::remove_dir_all(&volumes).await?;
}
tx.commit(None).await?;
tx.commit().await?;
remove_tor_keys(secrets, &entry.manifest.id).await?;
Ok(())
}

View File

@@ -32,7 +32,6 @@ use crate::db::model::{
CurrentDependencies, CurrentDependencyInfo, CurrentDependents, InstalledPackageDataEntry,
PackageDataEntry, RecoveredPackageInfo, StaticDependencyInfo, StaticFiles,
};
use crate::db::util::WithRevision;
use crate::dependencies::{
add_dependent_to_current_dependents_lists, break_all_dependents_transitive,
reconfigure_dependents_with_live_pointers, BreakTransitiveReceipts, BreakageRes,
@@ -115,7 +114,8 @@ impl std::fmt::Display for MinMax {
#[command(
custom_cli(cli_install(async, context(CliContext))),
display(display_none)
display(display_none),
metadata(sync_db = true)
)]
#[instrument(skip(ctx))]
pub async fn install(
@@ -127,7 +127,7 @@ pub async fn install(
String,
>,
#[arg(long = "version-priority", rename = "version-priority")] version_priority: Option<MinMax>,
) -> Result<WithRevision<()>, Error> {
) -> Result<(), Error> {
let version_str = match &version_spec {
None => "*",
Some(v) => &*v,
@@ -287,7 +287,7 @@ pub async fn install(
}
}
pde.save(&mut tx).await?;
let res = tx.commit(None).await?;
tx.commit().await?;
drop(db_handle);
tokio::spawn(async move {
@@ -323,10 +323,7 @@ pub async fn install(
}
});
Ok(WithRevision {
revision: res,
response: (),
})
Ok(())
}
#[command(rpc_only, display(display_none))]
@@ -427,7 +424,7 @@ pub async fn sideload(
}
}
pde.save(&mut tx).await?;
tx.commit(None).await?;
tx.commit().await?;
if let Err(e) = download_install_s9pk(
&new_ctx,
@@ -559,7 +556,7 @@ async fn cli_install(
ctx,
"package.install",
params,
PhantomData::<WithRevision<()>>,
PhantomData::<()>,
)
.await?
.result?;
@@ -570,7 +567,8 @@ async fn cli_install(
#[command(
subcommands(self(uninstall_impl(async)), uninstall_dry),
display(display_none)
display(display_none),
metadata(sync_db = true)
)]
pub async fn uninstall(#[arg] id: PackageId) -> Result<PackageId, Error> {
Ok(id)
@@ -601,7 +599,7 @@ pub async fn uninstall_dry(
}
#[instrument(skip(ctx))]
pub async fn uninstall_impl(ctx: RpcContext, id: PackageId) -> Result<WithRevision<()>, Error> {
pub async fn uninstall_impl(ctx: RpcContext, id: PackageId) -> Result<(), Error> {
let mut handle = ctx.db.handle();
let mut tx = handle.begin().await?;
@@ -629,7 +627,7 @@ pub async fn uninstall_impl(ctx: RpcContext, id: PackageId) -> Result<WithRevisi
removing: installed,
});
pde.save(&mut tx).await?;
let res = tx.commit(None).await?;
tx.commit().await?;
drop(handle);
tokio::spawn(async move {
@@ -666,17 +664,18 @@ pub async fn uninstall_impl(ctx: RpcContext, id: PackageId) -> Result<WithRevisi
}
});
Ok(WithRevision {
revision: res,
response: (),
})
Ok(())
}
#[command(rename = "delete-recovered", display(display_none))]
#[command(
rename = "delete-recovered",
display(display_none),
metadata(sync_db = true)
)]
pub async fn delete_recovered(
#[context] ctx: RpcContext,
#[arg] id: PackageId,
) -> Result<WithRevision<()>, Error> {
) -> Result<(), Error> {
let mut handle = ctx.db.handle();
let mut tx = handle.begin().await?;
let mut sql_tx = ctx.secret_store.begin().await?;
@@ -699,13 +698,10 @@ pub async fn delete_recovered(
}
cleanup::remove_tor_keys(&mut sql_tx, &id).await?;
let res = tx.commit(None).await?;
tx.commit().await?;
sql_tx.commit().await?;
Ok(WithRevision {
revision: res,
response: (),
})
Ok(())
}
pub struct DownloadInstallReceipts {
@@ -858,7 +854,7 @@ pub async fn download_install_s9pk(
tracing::error!("Failed to clean up {}@{}: {}", pkg_id, version, e);
tracing::debug!("{:?}", e);
} else {
tx.commit(None).await?;
tx.commit().await?;
}
Err(e)
} else {
@@ -1147,7 +1143,7 @@ pub async fn install_s9pk<R: AsyncRead + AsyncSeek + Unpin>(
if let Some(mut hdl) = rdr.scripts().await? {
tokio::io::copy(
&mut hdl,
&mut File::create(dbg!(script_dir.join("embassy.js"))).await?,
&mut File::create(script_dir.join("embassy.js")).await?,
)
.await?;
}
@@ -1505,7 +1501,7 @@ pub async fn install_s9pk<R: AsyncRead + AsyncSeek + Unpin>(
}
sql_tx.commit().await?;
tx.commit(None).await?;
tx.commit().await?;
tracing::info!("Install {}@{}: Complete", pkg_id, version);

View File

@@ -120,7 +120,7 @@ pub struct LogResponse {
end_cursor: Option<String>,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case", tag = "type")]
#[serde(rename_all = "kebab-case")]
pub struct LogFollowResponse {
start_cursor: Option<String>,
guid: RequestGuid,

View File

@@ -128,8 +128,7 @@ pub async fn check<Db: DbHandle>(
let status = receipts.status.get(&mut checkpoint).await?;
match status {
MainStatus::Running { health, started } => {
if let MainStatus::Running { health: _, started } = status {
receipts
.status
.set(
@@ -141,8 +140,6 @@ pub async fn check<Db: DbHandle>(
)
.await?;
}
_ => (),
}
let current_dependents = receipts.current_dependents.get(&mut checkpoint).await?;
checkpoint.save().await?;

View File

@@ -0,0 +1,84 @@
use color_eyre::eyre::eyre;
use futures::future::BoxFuture;
use futures::FutureExt;
use http::HeaderValue;
use rpc_toolkit::hyper::http::Error as HttpError;
use rpc_toolkit::hyper::{Body, Request, Response};
use rpc_toolkit::rpc_server_helpers::{
noop4, DynMiddleware, DynMiddlewareStage2, DynMiddlewareStage3,
};
use rpc_toolkit::yajrc::RpcMethod;
use rpc_toolkit::Metadata;
use crate::context::RpcContext;
use crate::{Error, ResultExt};
pub fn db<M: Metadata>(ctx: RpcContext) -> DynMiddleware<M> {
Box::new(
move |_: &mut Request<Body>,
metadata: M|
-> BoxFuture<Result<Result<DynMiddlewareStage2, Response<Body>>, HttpError>> {
let ctx = ctx.clone();
async move {
let m2: DynMiddlewareStage2 = Box::new(move |req, rpc_req| {
async move {
let seq = req.headers.remove("x-patch-sequence");
let sync_db = metadata
.get(rpc_req.method.as_str(), "sync_db")
.unwrap_or(false);
let m3: DynMiddlewareStage3 = Box::new(move |res, _| {
async move {
if sync_db && seq.is_some() {
match async {
let seq = seq
.ok_or_else(|| {
Error::new(
eyre!("Missing X-Patch-Sequence"),
crate::ErrorKind::InvalidRequest,
)
})?
.to_str()
.with_kind(crate::ErrorKind::InvalidRequest)?
.parse()?;
let res = ctx.db.sync(seq).await?;
let json = match res {
Ok(revs) => serde_json::to_vec(&revs),
Err(dump) => serde_json::to_vec(&[dump]),
}
.with_kind(crate::ErrorKind::Serialization)?;
Ok::<_, Error>(
url::form_urlencoded::byte_serialize(&json)
.collect::<String>(),
)
}
.await
{
Ok(a) => res
.headers
.append("X-Patch-Updates", HeaderValue::from_str(&a)?),
Err(e) => res.headers.append(
"X-Patch-Error",
HeaderValue::from_str(
&url::form_urlencoded::byte_serialize(
e.to_string().as_bytes(),
)
.collect::<String>(),
)?,
),
};
}
Ok(Ok(noop4()))
}
.boxed()
});
Ok(Ok(m3))
}
.boxed()
});
Ok(Ok(m2))
}
.boxed()
},
)
}

View File

@@ -1,4 +1,3 @@
use std::future::Future;
use std::sync::Arc;
use aes::cipher::{CipherKey, NewCipher, Nonce, StreamCipher};
@@ -17,6 +16,7 @@ use rpc_toolkit::yajrc::RpcMethod;
use rpc_toolkit::Metadata;
use sha2::Sha256;
use crate::context::SetupContext;
use crate::util::Apply;
use crate::Error;
@@ -35,7 +35,7 @@ pub fn encrypt_slice(input: impl AsRef<[u8]>, password: impl AsRef<[u8]>) -> Vec
let prefix: [u8; 32] = rand::random();
let aeskey = pbkdf2(password.as_ref(), &prefix[16..]);
let ctr = Nonce::<Aes256Ctr>::from_slice(&prefix[..16]);
let mut aes = Aes256Ctr::new(&aeskey, &ctr);
let mut aes = Aes256Ctr::new(&aeskey, ctr);
let mut res = Vec::with_capacity(32 + input.as_ref().len());
res.extend_from_slice(&prefix[..]);
res.extend_from_slice(input.as_ref());
@@ -50,7 +50,7 @@ pub fn decrypt_slice(input: impl AsRef<[u8]>, password: impl AsRef<[u8]>) -> Vec
let (prefix, rest) = input.as_ref().split_at(32);
let aeskey = pbkdf2(password.as_ref(), &prefix[16..]);
let ctr = Nonce::<Aes256Ctr>::from_slice(&prefix[..16]);
let mut aes = Aes256Ctr::new(&aeskey, &ctr);
let mut aes = Aes256Ctr::new(&aeskey, ctr);
let mut res = rest.to_vec();
aes.apply_keystream(&mut res);
res
@@ -92,20 +92,20 @@ impl Stream for DecryptStream {
aes.apply_keystream(&mut res);
res.into()
} else {
if this.ctr.len() < 16 && buf.len() > 0 {
if this.ctr.len() < 16 && !buf.is_empty() {
let to_read = std::cmp::min(16 - this.ctr.len(), buf.len());
this.ctr.extend_from_slice(&buf[0..to_read]);
buf = &buf[to_read..];
}
if this.salt.len() < 16 && buf.len() > 0 {
if this.salt.len() < 16 && !buf.is_empty() {
let to_read = std::cmp::min(16 - this.salt.len(), buf.len());
this.salt.extend_from_slice(&buf[0..to_read]);
buf = &buf[to_read..];
}
if this.ctr.len() == 16 && this.salt.len() == 16 {
let aeskey = pbkdf2(this.key.as_bytes(), &this.salt);
let ctr = Nonce::<Aes256Ctr>::from_slice(&this.ctr);
let mut aes = Aes256Ctr::new(&aeskey, &ctr);
let ctr = Nonce::<Aes256Ctr>::from_slice(this.ctr);
let mut aes = Aes256Ctr::new(&aeskey, ctr);
let mut res = buf.to_vec();
aes.apply_keystream(&mut res);
*this.aes = Some(aes);
@@ -132,7 +132,7 @@ impl EncryptStream {
let prefix: [u8; 32] = rand::random();
let aeskey = pbkdf2(key.as_bytes(), &prefix[16..]);
let ctr = Nonce::<Aes256Ctr>::from_slice(&prefix[..16]);
let aes = Aes256Ctr::new(&aeskey, &ctr);
let aes = Aes256Ctr::new(&aeskey, ctr);
EncryptStream {
body,
aes,
@@ -169,42 +169,42 @@ fn encrypted(headers: &HeaderMap) -> bool {
.and_then(|h| {
h.to_str()
.ok()?
.split(",")
.split(',')
.any(|s| s == "aesctr256")
.apply(Some)
})
.unwrap_or_default()
}
pub fn encrypt<
F: Fn() -> Fut + Send + Sync + Clone + 'static,
Fut: Future<Output = Result<Arc<String>, Error>> + Send + Sync + 'static,
M: Metadata,
>(
keysource: F,
) -> DynMiddleware<M> {
pub fn encrypt<M: Metadata>(ctx: SetupContext) -> DynMiddleware<M> {
Box::new(
move |req: &mut Request<Body>,
metadata: M|
-> BoxFuture<Result<Result<DynMiddlewareStage2, Response<Body>>, HttpError>> {
let keysource = keysource.clone();
let keysource = ctx.clone();
async move {
let encrypted = encrypted(req.headers());
let current_secret: Option<String> = keysource.current_secret.read().await.clone();
let key = if encrypted {
let key = match keysource().await {
Ok(s) => s,
Err(e) => {
let key = match current_secret {
Some(s) => s,
None => {
let (res_parts, _) = Response::new(()).into_parts();
return Ok(Err(to_response(
req.headers(),
res_parts,
Err(e.into()),
Err(Error::new(
eyre!("No Secret has been set"),
crate::ErrorKind::RateLimited,
)
.into()),
|_| StatusCode::OK,
)?));
}
};
let body = std::mem::take(req.body_mut());
*req.body_mut() = Body::wrap_stream(DecryptStream::new(key.clone(), body));
*req.body_mut() =
Body::wrap_stream(DecryptStream::new(Arc::new(key.clone()), body));
Some(key)
} else {
None
@@ -213,7 +213,7 @@ pub fn encrypt<
async move {
if !encrypted
&& metadata
.get(&rpc_req.method.as_str(), "authenticated")
.get(rpc_req.method.as_str(), "authenticated")
.unwrap_or(true)
{
let (res_parts, _) = Response::new(()).into_parts();

View File

@@ -1,4 +1,5 @@
pub mod auth;
pub mod cors;
pub mod db;
pub mod diagnostic;
pub mod encrypt;

View File

@@ -3,14 +3,14 @@ use std::net::Ipv4Addr;
use avahi_sys::{
self, avahi_client_errno, avahi_entry_group_add_service, avahi_entry_group_commit,
avahi_entry_group_free, avahi_entry_group_reset, avahi_free, avahi_strerror, AvahiClient,
AvahiEntryGroup,
avahi_entry_group_free, avahi_free, avahi_strerror, AvahiClient, AvahiEntryGroup,
};
use color_eyre::eyre::eyre;
use libc::c_void;
use tokio::process::Command;
use tokio::sync::Mutex;
use torut::onion::TorSecretKeyV3;
use tracing::instrument;
use super::interface::InterfaceId;
use crate::s9pk::manifest::PackageId;
@@ -59,17 +59,64 @@ impl MdnsController {
}
pub struct MdnsControllerInner {
hostname: Vec<u8>,
hostname_raw: *const libc::c_char,
entry_group: *mut AvahiEntryGroup,
entry_group: Option<MdnsEntryGroup>,
services: BTreeMap<(PackageId, InterfaceId), TorSecretKeyV3>,
_client_error: std::pin::Pin<Box<i32>>,
}
unsafe impl Send for MdnsControllerInner {}
unsafe impl Sync for MdnsControllerInner {}
impl MdnsControllerInner {
fn load_services(&mut self) {
fn init() -> Self {
MdnsControllerInner {
entry_group: Some(MdnsEntryGroup::init(&BTreeMap::new())),
services: BTreeMap::new(),
}
}
fn sync(&mut self) {
drop(self.entry_group.take());
self.entry_group = Some(MdnsEntryGroup::init(&self.services));
}
fn add<'a, I: IntoIterator<Item = (InterfaceId, TorSecretKeyV3)>>(
&mut self,
pkg_id: &PackageId,
interfaces: I,
) {
self.services.extend(
interfaces
.into_iter()
.map(|(interface_id, key)| ((pkg_id.clone(), interface_id), key)),
);
self.sync();
}
fn remove<I: IntoIterator<Item = InterfaceId>>(&mut self, pkg_id: &PackageId, interfaces: I) {
for interface_id in interfaces {
self.services.remove(&(pkg_id.clone(), interface_id));
}
self.sync();
}
fn free(&self) {}
}
fn log_str_error(action: &str, e: i32) {
unsafe {
let e_str = avahi_strerror(e);
tracing::error!(
"Could not {}: {:?}",
action,
std::ffi::CStr::from_ptr(e_str)
);
}
}
struct MdnsEntryGroup {
hostname: Vec<u8>,
hostname_raw: *const libc::c_char,
entry_group: *mut AvahiEntryGroup,
_client_error: std::pin::Pin<Box<i32>>,
}
impl MdnsEntryGroup {
#[instrument(skip(self))]
fn load_services(&mut self, services: &BTreeMap<(PackageId, InterfaceId), TorSecretKeyV3>) {
unsafe {
tracing::debug!("Loading services for mDNS");
let mut res;
@@ -101,7 +148,7 @@ impl MdnsControllerInner {
"Published {:?}",
std::ffi::CStr::from_ptr(self.hostname_raw)
);
for key in self.services.values() {
for key in services.values() {
let lan_address = key
.public()
.get_onion_address()
@@ -131,9 +178,10 @@ impl MdnsControllerInner {
}
}
}
fn init() -> Self {
fn init(services: &BTreeMap<(PackageId, InterfaceId), TorSecretKeyV3>) -> Self {
unsafe {
tracing::debug!("Initializing mDNS controller");
let simple_poll = avahi_sys::avahi_simple_poll_new();
let poll = avahi_sys::avahi_simple_poll_get(simple_poll);
let mut box_err = Box::pin(0 as i32);
@@ -168,15 +216,13 @@ impl MdnsControllerInner {
// assume fixed length prefix on hostname due to local address
hostname_buf[0] = (buflen - 8) as u8; // set the prefix length to len - 8 (leading byte, .local, nul) for the main address
hostname_buf[buflen - 7] = 5; // set the prefix length to 5 for "local"
let mut res = MdnsControllerInner {
let mut res = MdnsEntryGroup {
hostname: hostname_buf,
hostname_raw,
entry_group: group,
services: BTreeMap::new(),
_client_error: box_err,
};
res.load_services();
res.load_services(services);
let commit_err = avahi_entry_group_commit(res.entry_group);
if commit_err < avahi_sys::AVAHI_OK {
log_str_error("reset Avahi entry group", commit_err);
@@ -185,62 +231,17 @@ impl MdnsControllerInner {
res
}
}
fn sync(&mut self) {
unsafe {
let mut res;
res = avahi_entry_group_reset(self.entry_group);
if res < avahi_sys::AVAHI_OK {
log_str_error("reset Avahi entry group", res);
panic!("Failed to load Avahi services: reset");
}
self.load_services();
res = avahi_entry_group_commit(self.entry_group);
if res < avahi_sys::AVAHI_OK {
log_str_error("commit Avahi entry group", res);
panic!("Failed to load Avahi services: commit");
}
}
}
fn add<'a, I: IntoIterator<Item = (InterfaceId, TorSecretKeyV3)>>(
&mut self,
pkg_id: &PackageId,
interfaces: I,
) {
self.services.extend(
interfaces
.into_iter()
.map(|(interface_id, key)| ((pkg_id.clone(), interface_id), key)),
);
self.sync();
}
fn remove<I: IntoIterator<Item = InterfaceId>>(&mut self, pkg_id: &PackageId, interfaces: I) {
for interface_id in interfaces {
self.services.remove(&(pkg_id.clone(), interface_id));
}
self.sync();
}
}
impl Drop for MdnsControllerInner {
impl Drop for MdnsEntryGroup {
fn drop(&mut self) {
unsafe {
avahi_free(self.hostname_raw as *mut c_void);
avahi_entry_group_free(self.entry_group);
// avahi_client_free(self.avahi_client);
}
}
}
fn log_str_error(action: &str, e: i32) {
unsafe {
let e_str = avahi_strerror(e);
tracing::error!(
"Could not {}: {:?}",
action,
std::ffi::CStr::from_ptr(e_str)
);
avahi_free(e_str as *mut c_void);
}
}
unsafe extern "C" fn entry_group_callback(
_group: *mut avahi_sys::AvahiEntryGroup,
state: avahi_sys::AvahiEntryGroupState,

View File

@@ -3,6 +3,7 @@ use std::path::PathBuf;
use openssl::pkey::{PKey, Private};
use openssl::x509::X509;
use patch_db::DbHandle;
use rpc_toolkit::command;
use sqlx::PgPool;
use torut::onion::{OnionAddressV3, TorSecretKeyV3};
@@ -14,6 +15,7 @@ use self::mdns::MdnsController;
use self::nginx::NginxController;
use self::ssl::SslManager;
use self::tor::TorController;
use crate::hostname::get_hostname;
use crate::net::dns::DnsController;
use crate::net::interface::TorConfig;
use crate::net::nginx::InterfaceMetadata;
@@ -50,24 +52,26 @@ pub struct NetController {
pub dns: DnsController,
}
impl NetController {
#[instrument(skip(db))]
pub async fn init(
#[instrument(skip(db, handle))]
pub async fn init<Db: DbHandle>(
embassyd_addr: SocketAddr,
embassyd_tor_key: TorSecretKeyV3,
tor_control: SocketAddr,
dns_bind: &[SocketAddr],
db: PgPool,
import_root_ca: Option<(PKey<Private>, X509)>,
handle: &mut Db,
) -> Result<Self, Error> {
let ssl = match import_root_ca {
None => SslManager::init(db).await,
None => SslManager::init(db, handle).await,
Some(a) => SslManager::import_root_ca(db, a.0, a.1).await,
}?;
let hostname = get_hostname(handle).await?;
Ok(Self {
tor: TorController::init(embassyd_addr, embassyd_tor_key, tor_control).await?,
#[cfg(feature = "avahi")]
mdns: MdnsController::init(),
nginx: NginxController::init(PathBuf::from("/etc/nginx"), &ssl).await?,
nginx: NginxController::init(PathBuf::from("/etc/nginx"), &ssl, &hostname).await?,
ssl,
dns: DnsController::init(dns_bind).await?,
})

View File

@@ -9,7 +9,7 @@ use tracing::instrument;
use super::interface::{InterfaceId, LanPortConfig};
use super::ssl::SslManager;
use crate::hostname::get_hostname;
use crate::hostname::Hostname;
use crate::s9pk::manifest::PackageId;
use crate::util::serde::Port;
use crate::util::Invoke;
@@ -20,9 +20,15 @@ pub struct NginxController {
inner: Mutex<NginxControllerInner>,
}
impl NginxController {
pub async fn init(nginx_root: PathBuf, ssl_manager: &SslManager) -> Result<Self, Error> {
pub async fn init(
nginx_root: PathBuf,
ssl_manager: &SslManager,
host_name: &Hostname,
) -> Result<Self, Error> {
Ok(NginxController {
inner: Mutex::new(NginxControllerInner::init(&nginx_root, ssl_manager).await?),
inner: Mutex::new(
NginxControllerInner::init(&nginx_root, ssl_manager, host_name).await?,
),
nginx_root,
})
}
@@ -53,13 +59,17 @@ pub struct NginxControllerInner {
}
impl NginxControllerInner {
#[instrument]
async fn init(nginx_root: &Path, ssl_manager: &SslManager) -> Result<Self, Error> {
async fn init(
nginx_root: &Path,
ssl_manager: &SslManager,
host_name: &Hostname,
) -> Result<Self, Error> {
let inner = NginxControllerInner {
interfaces: BTreeMap::new(),
};
// write main ssl key/cert to fs location
let (key, cert) = ssl_manager
.certificate_for(&get_hostname().await?, &"embassy".parse().unwrap())
.certificate_for(&host_name.lan_address(), &"embassy".parse().unwrap())
.await?;
let ssl_path_key = nginx_root.join(format!("ssl/embassy_main.key.pem"));
let ssl_path_cert = nginx_root.join(format!("ssl/embassy_main.cert.pem"));

View File

@@ -11,6 +11,7 @@ use openssl::nid::Nid;
use openssl::pkey::{PKey, Private};
use openssl::x509::{X509Builder, X509Extension, X509NameBuilder, X509};
use openssl::*;
use patch_db::DbHandle;
use sqlx::PgPool;
use tokio::process::Command;
use tokio::sync::Mutex;
@@ -161,13 +162,14 @@ lazy_static::lazy_static! {
}
impl SslManager {
#[instrument(skip(db))]
pub async fn init(db: PgPool) -> Result<Self, Error> {
#[instrument(skip(db, handle))]
pub async fn init<Db: DbHandle>(db: PgPool, handle: &mut Db) -> Result<Self, Error> {
let store = SslStore::new(db)?;
let id = crate::hostname::get_id(handle).await?;
let (root_key, root_cert) = match store.load_root_certificate().await? {
None => {
let root_key = generate_key()?;
let server_id = crate::hostname::get_id().await?;
let server_id = id;
let root_cert = make_root_cert(&root_key, &server_id)?;
store.save_root_certificate(&root_key, &root_cert).await?;
Ok::<_, Error>((root_key, root_cert))
@@ -511,56 +513,56 @@ fn make_leaf_cert(
Ok(cert)
}
#[tokio::test]
async fn ca_details_persist() -> Result<(), Error> {
let pool = sqlx::Pool::<sqlx::Postgres>::connect("postgres::memory:").await?;
sqlx::migrate!()
.run(&pool)
.await
.with_kind(crate::ErrorKind::Database)?;
let mgr = SslManager::init(pool.clone()).await?;
let root_cert0 = mgr.root_cert;
let int_key0 = mgr.int_key;
let int_cert0 = mgr.int_cert;
let mgr = SslManager::init(pool).await?;
let root_cert1 = mgr.root_cert;
let int_key1 = mgr.int_key;
let int_cert1 = mgr.int_cert;
assert_eq!(root_cert0.to_pem()?, root_cert1.to_pem()?);
assert_eq!(
int_key0.private_key_to_pem_pkcs8()?,
int_key1.private_key_to_pem_pkcs8()?
);
assert_eq!(int_cert0.to_pem()?, int_cert1.to_pem()?);
Ok(())
}
#[tokio::test]
async fn certificate_details_persist() -> Result<(), Error> {
let pool = sqlx::Pool::<sqlx::Postgres>::connect("postgres::memory:").await?;
sqlx::migrate!()
.run(&pool)
.await
.with_kind(crate::ErrorKind::Database)?;
let mgr = SslManager::init(pool.clone()).await?;
let package_id = "bitcoind".parse().unwrap();
let (key0, cert_chain0) = mgr.certificate_for("start9", &package_id).await?;
let (key1, cert_chain1) = mgr.certificate_for("start9", &package_id).await?;
assert_eq!(
key0.private_key_to_pem_pkcs8()?,
key1.private_key_to_pem_pkcs8()?
);
assert_eq!(
cert_chain0
.iter()
.map(|cert| cert.to_pem().unwrap())
.collect::<Vec<Vec<u8>>>(),
cert_chain1
.iter()
.map(|cert| cert.to_pem().unwrap())
.collect::<Vec<Vec<u8>>>()
);
Ok(())
}
// #[tokio::test]
// async fn ca_details_persist() -> Result<(), Error> {
// let pool = sqlx::Pool::<sqlx::Postgres>::connect("postgres::memory:").await?;
// sqlx::migrate!()
// .run(&pool)
// .await
// .with_kind(crate::ErrorKind::Database)?;
// let mgr = SslManager::init(pool.clone()).await?;
// let root_cert0 = mgr.root_cert;
// let int_key0 = mgr.int_key;
// let int_cert0 = mgr.int_cert;
// let mgr = SslManager::init(pool).await?;
// let root_cert1 = mgr.root_cert;
// let int_key1 = mgr.int_key;
// let int_cert1 = mgr.int_cert;
//
// assert_eq!(root_cert0.to_pem()?, root_cert1.to_pem()?);
// assert_eq!(
// int_key0.private_key_to_pem_pkcs8()?,
// int_key1.private_key_to_pem_pkcs8()?
// );
// assert_eq!(int_cert0.to_pem()?, int_cert1.to_pem()?);
// Ok(())
// }
//
// #[tokio::test]
// async fn certificate_details_persist() -> Result<(), Error> {
// let pool = sqlx::Pool::<sqlx::Postgres>::connect("postgres::memory:").await?;
// sqlx::migrate!()
// .run(&pool)
// .await
// .with_kind(crate::ErrorKind::Database)?;
// let mgr = SslManager::init(pool.clone()).await?;
// let package_id = "bitcoind".parse().unwrap();
// let (key0, cert_chain0) = mgr.certificate_for("start9", &package_id).await?;
// let (key1, cert_chain1) = mgr.certificate_for("start9", &package_id).await?;
//
// assert_eq!(
// key0.private_key_to_pem_pkcs8()?,
// key1.private_key_to_pem_pkcs8()?
// );
// assert_eq!(
// cert_chain0
// .iter()
// .map(|cert| cert.to_pem().unwrap())
// .collect::<Vec<Vec<u8>>>(),
// cert_chain1
// .iter()
// .map(|cert| cert.to_pem().unwrap())
// .collect::<Vec<Vec<u8>>>()
// );
// Ok(())
// }

View File

@@ -16,6 +16,8 @@ server {{
server_name .{lan_hostname};
proxy_buffers 4 512k;
proxy_buffer_size 512k;
proxy_buffering off;
proxy_request_buffering off;
proxy_socket_keepalive on;
@@ -71,6 +73,8 @@ server {{
server_name .{tor_hostname};
proxy_buffers 4 512k;
proxy_buffer_size 512k;
proxy_buffering off;
proxy_request_buffering off;
proxy_socket_keepalive on;

View File

@@ -12,7 +12,6 @@ use tracing::instrument;
use crate::backup::BackupReport;
use crate::context::RpcContext;
use crate::db::util::WithRevision;
use crate::s9pk::manifest::PackageId;
use crate::util::display_none;
use crate::util::serde::display_serializable;
@@ -29,7 +28,7 @@ pub async fn list(
#[context] ctx: RpcContext,
#[arg] before: Option<i32>,
#[arg] limit: Option<u32>,
) -> Result<WithRevision<Vec<Notification>>, Error> {
) -> Result<Vec<Notification>, Error> {
let limit = limit.unwrap_or(40);
let mut handle = ctx.db.handle();
match before {
@@ -72,11 +71,8 @@ pub async fn list(
})
.collect::<Result<Vec<Notification>, Error>>()?;
// set notification count to zero
let r = model.put(&mut handle, &0).await?;
Ok(WithRevision {
response: notifs,
revision: r,
})
model.put(&mut handle, &0).await?;
Ok(notifs)
}
Some(before) => {
let records = sqlx::query!(
@@ -113,10 +109,7 @@ pub async fn list(
})
})
.collect::<Result<Vec<Notification>, Error>>()?;
Ok(WithRevision {
response: res,
revision: None,
})
Ok(res)
}
}
}

View File

@@ -10,6 +10,7 @@ use digest::generic_array::GenericArray;
use digest::OutputSizeUser;
use futures::future::BoxFuture;
use futures::{FutureExt, TryFutureExt, TryStreamExt};
use josekit::jwk::Jwk;
use nix::unistd::{Gid, Uid};
use openssl::x509::X509;
use patch_db::{DbHandle, LockType};
@@ -37,7 +38,7 @@ use crate::disk::mount::filesystem::ReadOnly;
use crate::disk::mount::guard::TmpMountGuard;
use crate::disk::util::{pvscan, recovery_info, DiskListResponse, EmbassyOsRecoveryInfo};
use crate::disk::REPAIR_DISK_PATH;
use crate::hostname::PRODUCT_KEY_PATH;
use crate::hostname::{get_hostname, Hostname};
use crate::id::Id;
use crate::init::init;
use crate::install::PKG_PUBLIC_DIR;
@@ -62,7 +63,7 @@ where
Ok(password)
}
#[command(subcommands(status, disk, attach, execute, recovery, cifs, complete))]
#[command(subcommands(status, disk, attach, execute, recovery, cifs, complete, get_secret))]
pub fn setup() -> Result<(), Error> {
Ok(())
}
@@ -70,14 +71,12 @@ pub fn setup() -> Result<(), Error> {
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct StatusRes {
product_key: bool,
migrating: bool,
}
#[command(rpc_only, metadata(authenticated = false))]
pub async fn status(#[context] ctx: SetupContext) -> Result<StatusRes, Error> {
Ok(StatusRes {
product_key: tokio::fs::metadata(PRODUCT_KEY_PATH).await.is_ok(),
migrating: ctx.recovery_status.read().await.is_some(),
})
}
@@ -123,31 +122,7 @@ pub async fn attach(
ErrorKind::DiskManagement,
));
}
let product_key = ctx.product_key().await?;
let product_key_path = Path::new("/embassy-data/main/product_key.txt");
if tokio::fs::metadata(product_key_path).await.is_ok() {
let pkey = Arc::new(
tokio::fs::read_to_string(product_key_path)
.await?
.trim()
.to_owned(),
);
if pkey != product_key {
crate::disk::main::export(&*guid, &ctx.datadir).await?;
return Err(Error::new(
eyre!(
"The EmbassyOS product key does not match the supplied drive: {}",
pkey
),
ErrorKind::ProductKeyMismatch,
));
}
}
init(
&RpcContextConfig::load(ctx.config_path.as_ref()).await?,
&*product_key,
)
.await?;
init(&RpcContextConfig::load(ctx.config_path.as_ref()).await?).await?;
let secrets = ctx.secret_store().await?;
let db = ctx.db(&secrets).await?;
let mut secrets_handle = secrets.acquire().await?;
@@ -168,16 +143,17 @@ pub async fn attach(
let tor_key = crate::net::tor::os_key(&mut secrets_tx).await?;
db_tx.commit(None).await?;
db_tx.commit().await?;
secrets_tx.commit().await?;
let hostname = get_hostname(&mut db_handle).await?;
let (_, root_ca) = SslManager::init(secrets).await?.export_root_ca().await?;
let (_, root_ca) = SslManager::init(secrets, &mut db_handle)
.await?
.export_root_ca()
.await?;
let setup_result = SetupResult {
tor_address: format!("http://{}", tor_key.public().get_onion_address()),
lan_address: format!(
"https://embassy-{}.local",
crate::hostname::derive_id(&*product_key)
),
lan_address: hostname.lan_address(),
root_ca: String::from_utf8(root_ca.to_pem()?)?,
};
*ctx.setup_result.write().await = Some((guid, setup_result.clone()));
@@ -222,6 +198,29 @@ pub async fn recovery_status(
ctx.recovery_status.read().await.clone().transpose()
}
/// We want to be able to get a secret, a shared private key with the frontend
/// This way the frontend can send a secret, like the password for the setup/ recovory
/// without knowing the password over clearnet. We use the public key shared across the network
/// since it is fine to share the public, and encrypt against the public.
#[command(rename = "get-secret", rpc_only, metadata(authenticated = false))]
pub async fn get_secret(
#[context] ctx: SetupContext,
#[arg] pubkey: Jwk,
) -> Result<String, RpcError> {
let secret = ctx.update_secret().await?;
let mut header = josekit::jwe::JweHeader::new();
header.set_algorithm("ECDH-ES");
header.set_content_encryption("A256GCM");
let encrypter = josekit::jwe::alg::ecdh_es::EcdhEsJweAlgorithm::EcdhEs
.encrypter_from_jwk(&pubkey)
.unwrap();
Ok(josekit::jwe::serialize_compact(secret.as_bytes(), &header, &encrypter).unwrap())
// Need to encrypt from the public key sent
// then encode via hex
}
#[command(subcommands(verify_cifs))]
pub fn cifs() -> Result<(), Error> {
Ok(())
@@ -269,14 +268,11 @@ pub async fn execute(
)
.await
{
Ok((tor_addr, root_ca)) => {
Ok((hostname, tor_addr, root_ca)) => {
tracing::info!("Setup Successful! Tor Address: {}", tor_addr);
Ok(SetupResult {
tor_address: format!("http://{}", tor_addr),
lan_address: format!(
"https://embassy-{}.local",
crate::hostname::derive_id(&ctx.product_key().await?)
),
lan_address: hostname.lan_address(),
root_ca: String::from_utf8(root_ca.to_pem()?)?,
})
}
@@ -299,33 +295,14 @@ pub async fn complete(#[context] ctx: SetupContext) -> Result<SetupResult, Error
crate::ErrorKind::InvalidRequest,
));
};
if tokio::fs::metadata(PRODUCT_KEY_PATH).await.is_err() {
crate::hostname::set_product_key(&*ctx.product_key().await?).await?;
} else {
let key_on_disk = crate::hostname::get_product_key().await?;
let key_in_cache = ctx.product_key().await?;
if *key_in_cache != key_on_disk {
crate::hostname::set_product_key(&*ctx.product_key().await?).await?;
}
}
tokio::fs::write(
Path::new("/embassy-data/main/product_key.txt"),
&*ctx.product_key().await?,
)
.await?;
let secrets = ctx.secret_store().await?;
let mut db = ctx.db(&secrets).await?.handle();
let hostname = crate::hostname::get_hostname().await?;
let hostname = crate::hostname::get_hostname(&mut db).await?;
let si = crate::db::DatabaseModel::new().server_info();
si.clone()
.id()
.put(&mut db, &crate::hostname::get_id().await?)
.await?;
let id = crate::hostname::get_id(&mut db).await?;
si.clone().id().put(&mut db, &id).await?;
si.lan_address()
.put(
&mut db,
&format!("https://{}.local", &hostname).parse().unwrap(),
)
.put(&mut db, &hostname.lan_address().parse().unwrap())
.await?;
let mut guid_file = File::create("/embassy-os/disk.guid").await?;
guid_file.write_all(guid.as_bytes()).await?;
@@ -341,7 +318,7 @@ pub async fn execute_inner(
embassy_password: String,
recovery_source: Option<BackupTargetFS>,
recovery_password: Option<String>,
) -> Result<(OnionAddressV3, X509), Error> {
) -> Result<(Hostname, OnionAddressV3, X509), Error> {
if ctx.recovery_status.read().await.is_some() {
return Err(Error::new(
eyre!("Cannot execute setup while in recovery!"),
@@ -374,12 +351,11 @@ pub async fn execute_inner(
recovery_password,
)
.await?;
init(
&RpcContextConfig::load(ctx.config_path.as_ref()).await?,
&ctx.product_key().await?,
)
.await?;
let res = (tor_addr, root_ca.clone());
let db = init(&RpcContextConfig::load(ctx.config_path.as_ref()).await?)
.await?
.db;
let hostname = get_hostname(&mut db.handle()).await?;
let res = (hostname.clone(), tor_addr, root_ca.clone());
tokio::spawn(async move {
if let Err(e) = recover_fut
.and_then(|_| async {
@@ -387,10 +363,7 @@ pub async fn execute_inner(
guid,
SetupResult {
tor_address: format!("http://{}", tor_addr),
lan_address: format!(
"https://embassy-{}.local",
crate::hostname::derive_id(&ctx.product_key().await?)
),
lan_address: hostname.lan_address(),
root_ca: String::from_utf8(root_ca.to_pem()?)?,
},
));
@@ -412,23 +385,19 @@ pub async fn execute_inner(
res
} else {
let (tor_addr, root_ca) = fresh_setup(&ctx, &embassy_password).await?;
init(
&RpcContextConfig::load(ctx.config_path.as_ref()).await?,
&ctx.product_key().await?,
)
.await?;
let db = init(&RpcContextConfig::load(ctx.config_path.as_ref()).await?)
.await?
.db;
*ctx.setup_result.write().await = Some((
guid,
SetupResult {
tor_address: format!("http://{}", tor_addr),
lan_address: format!(
"https://embassy-{}.local",
crate::hostname::derive_id(&ctx.product_key().await?)
),
lan_address: get_hostname(&mut db.handle()).await?.lan_address(),
root_ca: String::from_utf8(root_ca.to_pem()?)?,
},
));
(tor_addr, root_ca)
let hostname = get_hostname(&mut db.handle()).await?;
(hostname, tor_addr, root_ca)
};
Ok(res)
@@ -455,7 +424,8 @@ async fn fresh_setup(
)
.execute(&mut sqlite_pool.acquire().await?)
.await?;
let (_, root_ca) = SslManager::init(sqlite_pool.clone())
let db = ctx.db(&sqlite_pool).await?;
let (_, root_ca) = SslManager::init(sqlite_pool.clone(), &mut db.handle())
.await?
.export_root_ca()
.await?;

View File

@@ -24,7 +24,6 @@ use tracing::instrument;
use crate::context::RpcContext;
use crate::db::model::UpdateProgress;
use crate::db::util::WithRevision;
use crate::disk::mount::filesystem::block_dev::BlockDev;
use crate::disk::mount::filesystem::{FileSystem, ReadWrite};
use crate::disk::mount::guard::TmpMountGuard;
@@ -46,26 +45,24 @@ lazy_static! {
/// An user/ daemon would call this to update the system to the latest version and do the updates available,
/// and this will return something if there is an update, and in that case there will need to be a restart.
#[command(rename = "update", display(display_update_result))]
#[command(
rename = "update",
display(display_update_result),
metadata(sync_db = true)
)]
#[instrument(skip(ctx))]
pub async fn update_system(
#[context] ctx: RpcContext,
#[arg(rename = "marketplace-url")] marketplace_url: Url,
) -> Result<WithRevision<UpdateResult>, Error> {
let noop = WithRevision {
response: UpdateResult::NoUpdates,
revision: None,
};
) -> Result<UpdateResult, Error> {
if UPDATED.load(Ordering::SeqCst) {
return Ok(noop);
}
match maybe_do_update(ctx, marketplace_url).await? {
None => Ok(noop),
Some(r) => Ok(WithRevision {
response: UpdateResult::Updating,
revision: Some(r),
}),
return Ok(UpdateResult::NoUpdates);
}
Ok(if maybe_do_update(ctx, marketplace_url).await?.is_some() {
UpdateResult::Updating
} else {
UpdateResult::NoUpdates
})
}
/// What is the status of the updates?
@@ -76,8 +73,8 @@ pub enum UpdateResult {
Updating,
}
fn display_update_result(status: WithRevision<UpdateResult>, _: &ArgMatches) {
match status.response {
fn display_update_result(status: UpdateResult, _: &ArgMatches) {
match status {
UpdateResult::Updating => {
println!("Updating...");
}
@@ -190,7 +187,7 @@ async fn maybe_do_update(
downloaded: 0,
});
status.save(&mut tx).await?;
let rev = tx.commit(None).await?;
let rev = tx.commit().await?;
tokio::spawn(async move {
let mut db = ctx.db.handle();

View File

@@ -14,8 +14,9 @@ mod v0_3_0_2;
mod v0_3_0_3;
mod v0_3_1;
mod v0_3_1_1;
mod v0_3_1_2;
pub type Current = v0_3_1_1::Version;
pub type Current = v0_3_1_2::Version;
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
#[serde(untagged)]
@@ -26,6 +27,7 @@ enum Version {
V0_3_0_3(Wrapper<v0_3_0_3::Version>),
V0_3_1(Wrapper<v0_3_1::Version>),
V0_3_1_1(Wrapper<v0_3_1_1::Version>),
V0_3_1_2(Wrapper<v0_3_1_2::Version>),
Other(emver::Version),
}
@@ -47,6 +49,7 @@ impl Version {
Version::V0_3_0_3(Wrapper(x)) => x.semver(),
Version::V0_3_1(Wrapper(x)) => x.semver(),
Version::V0_3_1_1(Wrapper(x)) => x.semver(),
Version::V0_3_1_2(Wrapper(x)) => x.semver(),
Version::Other(x) => x.clone(),
}
}
@@ -179,6 +182,7 @@ pub async fn init<Db: DbHandle>(
Version::V0_3_0_3(v) => v.0.migrate_to(&Current::new(), db, receipts).await?,
Version::V0_3_1(v) => v.0.migrate_to(&Current::new(), db, receipts).await?,
Version::V0_3_1_1(v) => v.0.migrate_to(&Current::new(), db, receipts).await?,
Version::V0_3_1_2(v) => v.0.migrate_to(&Current::new(), db, receipts).await?,
Version::Other(_) => {
return Err(Error::new(
eyre!("Cannot downgrade"),

View File

@@ -0,0 +1,61 @@
use emver::VersionRange;
use crate::hostname::{generate_id, get_hostname, sync_hostname};
use super::v0_3_0::V0_3_0_COMPAT;
use super::*;
const V0_3_1_2: emver::Version = emver::Version::new(0, 3, 1, 2);
#[derive(Clone, Debug)]
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_3_1_1::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> emver::Version {
V0_3_1_2
}
fn compat(&self) -> &'static VersionRange {
&*V0_3_0_COMPAT
}
async fn up<Db: DbHandle>(&self, db: &mut Db) -> Result<(), Error> {
let hostname = get_hostname(db).await?;
crate::db::DatabaseModel::new()
.server_info()
.hostname()
.put(db, &Some(hostname.0))
.await?;
crate::db::DatabaseModel::new()
.server_info()
.id()
.put(db, &generate_id())
.await?;
sync_hostname(db).await?;
let mut ui = crate::db::DatabaseModel::new()
.ui()
.get(db, false)
.await?
.clone();
if let serde_json::Value::Object(ref mut ui) = ui {
ui.insert("ack-instructions".to_string(), serde_json::json!({}));
}
crate::db::DatabaseModel::new().ui().put(db, &ui).await?;
Ok(())
}
async fn down<Db: DbHandle>(&self, db: &mut Db) -> Result<(), Error> {
let mut ui = crate::db::DatabaseModel::new()
.ui()
.get(db, false)
.await?
.clone();
if let serde_json::Value::Object(ref mut ui) = ui {
ui.remove("ack-instructions");
}
crate::db::DatabaseModel::new().ui().put(db, &ui).await?;
Ok(())
}
}

View File

@@ -4,8 +4,6 @@ After=network-online.target systemd-time-wait-sync.service
[Service]
Type=oneshot
Restart=on-failure
RestartSec=5s
ExecStart=/usr/local/bin/initialization.sh
RemainAfterExit=true
StandardOutput=append:/var/log/initialization.log

View File

@@ -1,7 +1,15 @@
#!/bin/bash
# Update repositories, install dependencies, do some initial configurations, set hostname, enable embassy-init, and config Tor
set -e
function flatline {
echo -n "0" > /sys/class/pwm/pwmchip0/export
sleep 0.5
echo -n "2272727" > /sys/class/pwm/pwmchip0/pwm0/period
echo -n "1136364" > /sys/class/pwm/pwmchip0/pwm0/duty_cycle
echo -n "1" > /sys/class/pwm/pwmchip0/pwm0/enable
exit 1
}
(set -e
# introduce start9 username and embassy as default password
if ! awk -F: '{ print $1 }' /etc/passwd | grep start9
@@ -146,9 +154,10 @@ systemctl disable nc-broadcast.service
systemctl disable initialization.service
sudo systemctl restart NetworkManager
sync
echo "fs.inotify.max_user_watches=1048576" > /etc/sysctl.d/97-embassy.conf
# TODO: clean out ssh host keys
sync
reboot
) || flatline

View File

@@ -29,11 +29,13 @@
"dompurify": "^2.3.6",
"fast-json-patch": "^3.1.1",
"fuse.js": "^6.4.6",
"jose": "^4.9.0",
"js-yaml": "^4.1.0",
"marked": "^4.0.0",
"monaco-editor": "^0.33.0",
"mustache": "^4.2.0",
"ng-qrcode": "^7.0.0",
"node-jose": "^2.1.1",
"patch-db-client": "file: ../../../patch-db/client",
"pbkdf2": "^3.1.2",
"rxjs": "^7.5.6",
@@ -56,6 +58,7 @@
"@types/marked": "^4.0.3",
"@types/mustache": "^4.1.2",
"@types/node": "^16.9.1",
"@types/node-jose": "^1.1.10",
"@types/pbkdf2": "^3.1.0",
"@types/uuid": "^8.3.1",
"husky": "^4.3.8",
@@ -3721,6 +3724,15 @@
"integrity": "sha512-aFcUkv7EddxxOa/9f74DINReQ/celqH8DiB3fRYgVDM2Xm5QJL8sl80QKuAnGvwAsMn+H3IFA6WCrQh1CY7m1A==",
"dev": true
},
"node_modules/@types/node-jose": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/@types/node-jose/-/node-jose-1.1.10.tgz",
"integrity": "sha512-7L0ucJTugW4x/sYpQ+c5IudAwr0pFuxDVnZLpHKWpff7p1lVa3wTuNvnrzFBNeLojE+UY0cVCwNGXLxXsMIrzw==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@@ -4469,7 +4481,6 @@
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"dev": true,
"funding": [
{
"type": "github",
@@ -4485,6 +4496,14 @@
}
]
},
"node_modules/base64url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/batch": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
@@ -6127,6 +6146,11 @@
"integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==",
"dev": true
},
"node_modules/es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
},
"node_modules/esbuild": {
"version": "0.15.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.5.tgz",
@@ -7860,7 +7884,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"dev": true,
"funding": [
{
"type": "github",
@@ -8418,6 +8441,14 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/jose": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/jose/-/jose-4.9.0.tgz",
"integrity": "sha512-RgaqEOZLkVO+ViN3KkN44XJt9g7+wMveUv59sVLaTxONcUPc8ZpfqOCeLphVBZyih2dgkvZ0Ap1CNcokvY7Uyw==",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -8866,8 +8897,7 @@
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash._baseassign": {
"version": "3.2.0",
@@ -9111,6 +9141,11 @@
"node": ">=8"
}
},
"node_modules/long": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.0.tgz",
"integrity": "sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w=="
},
"node_modules/lru-cache": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz",
@@ -9731,7 +9766,6 @@
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
"dev": true,
"engines": {
"node": ">= 6.13.0"
}
@@ -9824,6 +9858,50 @@
"he": "1.2.0"
}
},
"node_modules/node-jose": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/node-jose/-/node-jose-2.1.1.tgz",
"integrity": "sha512-19nyuUGShNmFmVTeqDfP6ZJCiikbcjI0Pw2kykBCH7rl8AZgSiDZK2Ww8EDaMrOSbRg6IlfIMhI5ZvCklmOhzg==",
"dependencies": {
"base64url": "^3.0.1",
"buffer": "^6.0.3",
"es6-promise": "^4.2.8",
"lodash": "^4.17.21",
"long": "^5.2.0",
"node-forge": "^1.2.1",
"pako": "^2.0.4",
"process": "^0.11.10",
"uuid": "^8.3.2"
}
},
"node_modules/node-jose/node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/node-jose/node_modules/pako": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz",
"integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg=="
},
"node_modules/node-releases": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz",
@@ -11464,6 +11542,14 @@
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
}
},
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -17246,6 +17332,15 @@
"integrity": "sha512-aFcUkv7EddxxOa/9f74DINReQ/celqH8DiB3fRYgVDM2Xm5QJL8sl80QKuAnGvwAsMn+H3IFA6WCrQh1CY7m1A==",
"dev": true
},
"@types/node-jose": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/@types/node-jose/-/node-jose-1.1.10.tgz",
"integrity": "sha512-7L0ucJTugW4x/sYpQ+c5IudAwr0pFuxDVnZLpHKWpff7p1lVa3wTuNvnrzFBNeLojE+UY0cVCwNGXLxXsMIrzw==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@@ -17863,8 +17958,12 @@
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"dev": true
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"base64url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A=="
},
"batch": {
"version": "0.6.1",
@@ -19096,6 +19195,11 @@
"integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==",
"dev": true
},
"es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
},
"esbuild": {
"version": "0.15.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.5.tgz",
@@ -20296,8 +20400,7 @@
"ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"dev": true
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
},
"ignore": {
"version": "5.2.0",
@@ -20703,6 +20806,11 @@
}
}
},
"jose": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/jose/-/jose-4.9.0.tgz",
"integrity": "sha512-RgaqEOZLkVO+ViN3KkN44XJt9g7+wMveUv59sVLaTxONcUPc8ZpfqOCeLphVBZyih2dgkvZ0Ap1CNcokvY7Uyw=="
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -21035,8 +21143,7 @@
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash._baseassign": {
"version": "3.2.0",
@@ -21239,6 +21346,11 @@
}
}
},
"long": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.0.tgz",
"integrity": "sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w=="
},
"lru-cache": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz",
@@ -21697,8 +21809,7 @@
"node-forge": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
"dev": true
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA=="
},
"node-gyp": {
"version": "9.1.0",
@@ -21770,6 +21881,38 @@
"he": "1.2.0"
}
},
"node-jose": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/node-jose/-/node-jose-2.1.1.tgz",
"integrity": "sha512-19nyuUGShNmFmVTeqDfP6ZJCiikbcjI0Pw2kykBCH7rl8AZgSiDZK2Ww8EDaMrOSbRg6IlfIMhI5ZvCklmOhzg==",
"requires": {
"base64url": "^3.0.1",
"buffer": "^6.0.3",
"es6-promise": "^4.2.8",
"lodash": "^4.17.21",
"long": "^5.2.0",
"node-forge": "^1.2.1",
"pako": "^2.0.4",
"process": "^0.11.10",
"uuid": "^8.3.2"
},
"dependencies": {
"buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"pako": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz",
"integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg=="
}
}
},
"node-releases": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz",
@@ -22889,6 +23032,11 @@
"integrity": "sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw==",
"dev": true
},
"process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="
},
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",

View File

@@ -43,11 +43,13 @@
"dompurify": "^2.3.6",
"fast-json-patch": "^3.1.1",
"fuse.js": "^6.4.6",
"jose": "^4.9.0",
"js-yaml": "^4.1.0",
"marked": "^4.0.0",
"monaco-editor": "^0.33.0",
"mustache": "^4.2.0",
"ng-qrcode": "^7.0.0",
"node-jose": "^2.1.1",
"patch-db-client": "file: ../../../patch-db/client",
"pbkdf2": "^3.1.2",
"rxjs": "^7.5.6",
@@ -70,6 +72,7 @@
"@types/marked": "^4.0.3",
"@types/mustache": "^4.1.2",
"@types/node": "^16.9.1",
"@types/node-jose": "^1.1.10",
"@types/pbkdf2": "^3.1.0",
"@types/uuid": "^8.3.1",
"husky": "^4.3.8",

View File

@@ -156,7 +156,6 @@ export class HomePage {
console.error(e)
}
},
cssClass: 'enter-click',
},
],
cssClass: 'alert-warning-message',

View File

@@ -1,46 +1,61 @@
import { Injectable } from '@angular/core'
import { HttpService } from '@start9labs/shared'
import {
HttpService,
isRpcError,
RpcError,
RPCOptions,
} from '@start9labs/shared'
import { ApiService, GetErrorRes } from './api.service'
import { LogsRes, ServerLogsReq } from '@start9labs/shared'
@Injectable()
export class LiveApiService extends ApiService {
constructor(private readonly http: HttpService) {
super()
}
export class LiveApiService implements ApiService {
constructor(private readonly http: HttpService) {}
getError(): Promise<GetErrorRes> {
return this.http.rpcRequest<GetErrorRes>({
return this.rpcRequest<GetErrorRes>({
method: 'diagnostic.error',
params: {},
})
}
restart(): Promise<void> {
return this.http.rpcRequest<void>({
return this.rpcRequest<void>({
method: 'diagnostic.restart',
params: {},
})
}
forgetDrive(): Promise<void> {
return this.http.rpcRequest<void>({
return this.rpcRequest<void>({
method: 'diagnostic.disk.forget',
params: {},
})
}
repairDisk(): Promise<void> {
return this.http.rpcRequest<void>({
return this.rpcRequest<void>({
method: 'diagnostic.disk.repair',
params: {},
})
}
getLogs(params: ServerLogsReq): Promise<LogsRes> {
return this.http.rpcRequest<LogsRes>({
return this.rpcRequest<LogsRes>({
method: 'diagnostic.logs',
params,
})
}
private async rpcRequest<T>(opts: RPCOptions): Promise<T> {
const res = await this.http.rpcRequest<T>(opts)
const rpcRes = res.body
if (isRpcError(rpcRes)) {
throw new RpcError(rpcRes.error)
}
return rpcRes.result
}
}

View File

@@ -4,11 +4,7 @@ import { ApiService, GetErrorRes } from './api.service'
import { LogsRes, ServerLogsReq, Log } from '@start9labs/shared'
@Injectable()
export class MockApiService extends ApiService {
constructor() {
super()
}
export class MockApiService implements ApiService {
async getError(): Promise<GetErrorRes> {
await pauseFor(1000)
return {

View File

@@ -50,7 +50,6 @@ export class AdditionalComponent {
{
text: 'Ok',
handler: (version: string) => this.version.emit(version),
cssClass: 'enter-click',
},
],
})

View File

@@ -1,45 +1,32 @@
import { NgModule } from '@angular/core'
import { PreloadAllModules, RouterModule, Routes } from '@angular/router'
import { NavGuard, RecoveryNavGuard } from './guards/nav-guard'
const routes: Routes = [
{ path: '', redirectTo: '/product-key', pathMatch: 'full' },
{
path: 'product-key',
loadChildren: () =>
import('./pages/product-key/product-key.module').then(
m => m.ProductKeyPageModule,
),
},
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{
path: 'home',
loadChildren: () =>
import('./pages/home/home.module').then(m => m.HomePageModule),
canActivate: [NavGuard],
},
{
path: 'recover',
loadChildren: () =>
import('./pages/recover/recover.module').then(m => m.RecoverPageModule),
canActivate: [RecoveryNavGuard],
},
{
path: 'embassy',
loadChildren: () =>
import('./pages/embassy/embassy.module').then(m => m.EmbassyPageModule),
canActivate: [NavGuard],
},
{
path: 'loading',
loadChildren: () =>
import('./pages/loading/loading.module').then(m => m.LoadingPageModule),
canActivate: [NavGuard],
},
{
path: 'success',
loadChildren: () =>
import('./pages/success/success.module').then(m => m.SuccessPageModule),
canActivate: [NavGuard],
},
]

View File

@@ -2,7 +2,6 @@ import { Component } from '@angular/core'
import { NavController } from '@ionic/angular'
import { ApiService } from './services/api/api.service'
import { ErrorToastService } from '@start9labs/shared'
import { StateService } from './services/state.service'
@Component({
selector: 'app-root',
@@ -14,21 +13,12 @@ export class AppComponent {
private readonly apiService: ApiService,
private readonly errorToastService: ErrorToastService,
private readonly navCtrl: NavController,
private readonly stateService: StateService,
) {}
async ngOnInit() {
try {
const status = await this.apiService.getStatus()
if (status.migrating || status['product-key']) {
this.stateService.hasProductKey = true
this.stateService.isMigrating = status.migrating
await this.navCtrl.navigateForward(`/product-key`)
} else {
this.stateService.hasProductKey = false
this.stateService.isMigrating = false
await this.navCtrl.navigateForward(`/recover`)
}
const { migrating } = await this.apiService.getStatus()
await this.navCtrl.navigateForward(migrating ? '/loading' : '/home')
} catch (e: any) {
this.errorToastService.present(e)
}

View File

@@ -1,4 +1,4 @@
import { ErrorHandler, NgModule } from '@angular/core'
import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { RouteReuseStrategy } from '@angular/router'
import { HttpClientModule } from '@angular/common/http'
@@ -15,8 +15,6 @@ import { AppRoutingModule } from './app-routing.module'
import { SuccessPageModule } from './pages/success/success.module'
import { HomePageModule } from './pages/home/home.module'
import { LoadingPageModule } from './pages/loading/loading.module'
import { ProdKeyModalModule } from './modals/prod-key-modal/prod-key-modal.module'
import { ProductKeyPageModule } from './pages/product-key/product-key.module'
import { RecoverPageModule } from './pages/recover/recover.module'
import { WorkspaceConfig } from '@start9labs/shared'
@@ -35,8 +33,6 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig
SuccessPageModule,
HomePageModule,
LoadingPageModule,
ProdKeyModalModule,
ProductKeyPageModule,
RecoverPageModule,
],
providers: [

View File

@@ -1,43 +0,0 @@
import { Injectable } from '@angular/core'
import { CanActivate, Router } from '@angular/router'
import { RPCEncryptedService } from '../services/rpc-encrypted.service'
import { StateService } from '../services/state.service'
@Injectable({
providedIn: 'root',
})
export class NavGuard implements CanActivate {
constructor(
private readonly router: Router,
private readonly encrypted: RPCEncryptedService,
) {}
canActivate(): boolean {
if (this.encrypted.productKey) {
return true
} else {
this.router.navigateByUrl('product-key')
return false
}
}
}
@Injectable({
providedIn: 'root',
})
export class RecoveryNavGuard implements CanActivate {
constructor(
private readonly router: Router,
private readonly encrypted: RPCEncryptedService,
private readonly stateService: StateService,
) {}
canActivate(): boolean {
if (this.encrypted.productKey || !this.stateService.hasProductKey) {
return true
} else {
this.router.navigateByUrl('product-key')
return false
}
}
}

View File

@@ -1,7 +1,6 @@
import { Component, Input, ViewChild } from '@angular/core'
import { IonInput, ModalController } from '@ionic/angular'
import {
DiskInfo,
CifsBackupTarget,
DiskBackupTarget,
} from 'src/app/services/api/api.service'
@@ -15,7 +14,7 @@ import * as argon2 from '@start9labs/argon2'
export class PasswordPage {
@ViewChild('focusInput') elem?: IonInput
@Input() target?: CifsBackupTarget | DiskBackupTarget
@Input() storageDrive?: DiskInfo
@Input() storageDrive = false
pwError = ''
password = ''

View File

@@ -1,20 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { FormsModule } from '@angular/forms'
import { ProdKeyModal } from './prod-key-modal.page'
@NgModule({
declarations: [
ProdKeyModal,
],
imports: [
CommonModule,
FormsModule,
IonicModule,
],
exports: [
ProdKeyModal,
],
})
export class ProdKeyModalModule { }

View File

@@ -1,41 +0,0 @@
<ion-header>
<ion-toolbar>
<ion-title>
Enter Product Key
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<form (ngSubmit)="verifyProductKey()">
<div style="padding: 8px 24px;">
<div style="padding-bottom: 16px;">
<p>Enter your 0.2.x Product Key to establish an encrypted connection with your new Embassy.</p>
</div>
<ion-item>
<ion-input
#focusInput
[(ngModel)]="productKey"
placeholder="Enter Product Key"
maxlength="12"
></ion-input>
</ion-item>
<div style="height: 16px;">
<p style="color: var(--ion-color-danger); font-size: x-small;">{{ error }}</p>
</div>
</div>
<input type="submit" style="display: none" />
</form>
</ion-content>
<ion-footer>
<ion-toolbar>
<ion-button class="ion-padding-end" slot="end" color="dark" fill="clear" (click)="cancel()">
Cancel
</ion-button>
<ion-button class="ion-padding-end" slot="end" color="dark" fill="clear" strong="true" (click)="verifyProductKey()">
Submit
</ion-button>
</ion-toolbar>
</ion-footer>

View File

@@ -1,3 +0,0 @@
ion-content {
--ion-text-color: var(--ion-color-dark);
}

View File

@@ -1,54 +0,0 @@
import { Component, Input, ViewChild } from '@angular/core'
import { IonInput, LoadingController, ModalController } from '@ionic/angular'
import { ApiService, DiskBackupTarget } from 'src/app/services/api/api.service'
import { RPCEncryptedService } from 'src/app/services/rpc-encrypted.service'
@Component({
selector: 'prod-key-modal',
templateUrl: 'prod-key-modal.page.html',
styleUrls: ['prod-key-modal.page.scss'],
})
export class ProdKeyModal {
@ViewChild('focusInput') elem?: IonInput
@Input() target!: DiskBackupTarget
error = ''
productKey = ''
unmasked = false
constructor(
private readonly modalController: ModalController,
private readonly apiService: ApiService,
private readonly loadingCtrl: LoadingController,
private readonly encrypted: RPCEncryptedService,
) {}
ngAfterViewInit() {
setTimeout(() => this.elem?.setFocus(), 400)
}
async verifyProductKey() {
if (!this.productKey || !this.target.logicalname) return
const loader = await this.loadingCtrl.create({
message: 'Verifying Product Key',
})
await loader.present()
try {
await this.apiService.set02XDrive(this.target.logicalname)
this.encrypted.productKey = this.productKey
await this.apiService.verifyProductKey()
this.modalController.dismiss({ productKey: this.productKey }, 'success')
} catch (e) {
this.encrypted.productKey = undefined
this.error = 'Invalid Product Key'
} finally {
loader.dismiss()
}
}
cancel() {
this.modalController.dismiss()
}
}

View File

@@ -64,12 +64,7 @@ export class EmbassyPage {
const alert = await this.alertCtrl.create({
header: 'Warning',
message: `One or more devices you connected had to be reconfigured to support the current hardware platform. Please unplug and replug the following device(s), then refresh the page:<br> ${list}`,
buttons: [
{
role: 'cancel',
text: 'OK',
},
],
buttons: ['OK'],
})
await alert.present()
}
@@ -95,40 +90,45 @@ export class EmbassyPage {
text: 'Continue',
handler: () => {
if (this.stateService.recoveryPassword) {
this.setupEmbassy(drive, this.stateService.recoveryPassword)
this.setupEmbassy(
drive.logicalname,
this.stateService.recoveryPassword,
)
} else {
this.presentModalPassword(drive)
this.presentModalPassword(drive.logicalname)
}
},
cssClass: 'enter-click',
},
],
})
await alert.present()
} else {
if (this.stateService.recoveryPassword) {
this.setupEmbassy(drive, this.stateService.recoveryPassword)
this.setupEmbassy(drive.logicalname, this.stateService.recoveryPassword)
} else {
this.presentModalPassword(drive)
this.presentModalPassword(drive.logicalname)
}
}
}
private async presentModalPassword(drive: DiskInfo): Promise<void> {
private async presentModalPassword(logicalname: string): Promise<void> {
const modal = await this.modalController.create({
component: PasswordPage,
componentProps: {
storageDrive: drive,
storageDrive: true,
},
})
modal.onDidDismiss().then(async ret => {
if (!ret.data || !ret.data.password) return
this.setupEmbassy(drive, ret.data.password)
this.setupEmbassy(logicalname, ret.data.password)
})
await modal.present()
}
private async setupEmbassy(drive: DiskInfo, password: string): Promise<void> {
private async setupEmbassy(
logicalname: string,
password: string,
): Promise<void> {
const loader = await this.loadingCtrl.create({
message: 'Initializing data drive. This could take a while...',
})
@@ -136,7 +136,7 @@ export class EmbassyPage {
await loader.present()
try {
await this.stateService.setupEmbassy(drive.logicalname, password)
await this.stateService.setupEmbassy(logicalname, password)
if (!!this.stateService.recoverySource) {
await this.navCtrl.navigateForward(`/loading`)
} else {

View File

@@ -4,9 +4,8 @@ import { IonicModule } from '@ionic/angular'
import { FormsModule } from '@angular/forms'
import { HomePage } from './home.page'
import { PasswordPageModule } from '../../modals/password/password.module'
import { HomePageRoutingModule } from './home-routing.module'
import { SwiperModule } from 'swiper/angular'
@NgModule({
imports: [
@@ -15,6 +14,7 @@ import { HomePageRoutingModule } from './home-routing.module'
IonicModule,
HomePageRoutingModule,
PasswordPageModule,
SwiperModule,
],
declarations: [HomePage],
})

View File

@@ -2,36 +2,78 @@
<ion-grid>
<ion-row>
<ion-col class="ion-text-center">
<div style="padding-bottom: 32px;">
<img src="assets/img/logo.png" style="max-width: 240px;" />
<div style="padding-bottom: 32px">
<img src="assets/img/logo.png" style="max-width: 240px" />
</div>
<ion-card color="dark">
<ion-card-content class="ion-margin">
<!-- fresh -->
<ion-card
<ion-card-header>
<ion-button
*ngIf="swiper?.activeIndex === 1"
class="back-button"
fill="clear"
color="light"
(click)="previous()"
>
<ion-icon slot="icon-only" name="arrow-back"></ion-icon>
</ion-button>
<ion-card-title>
Embassy Setup
<span *ngIf="swiper?.activeIndex === 1"> (recover)</span>
</ion-card-title>
</ion-card-header>
<ion-card-content class="ion-margin-bottom">
<swiper (swiper)="setSwiperInstance($event)">
<ng-template swiperSlide>
<ion-item
button
[disabled]="error"
detail="true"
routerLink="/embassy"
color="light"
style="text-align: center; background-color: #00919b !important; height: 160px; margin-bottom: 20px; box-shadow: 4px 4px 16px var(--ion-color-light);"
>
<ion-card-header>
<ion-card-title style="font-size: 40px;">Start Fresh</ion-card-title>
<ion-card-subtitle>Get started with a brand new Embassy</ion-card-subtitle>
</ion-card-header>
<!-- recover -->
</ion-card>
<ion-card
routerLink="/recover"
color="light"
style="text-align: center; background-color: #bf5900 !important; height: 160px; box-shadow: 4px 4px 16px var(--ion-color-light);"
<ion-icon color="dark" slot="start" name="add"></ion-icon>
<ion-label>
<h2><ion-text color="success">Start Fresh</ion-text></h2>
<p>Get started with a brand new Embassy</p>
</ion-label>
</ion-item>
<ion-item
button
[disabled]="error"
detail="true"
lines="none"
(click)="next()"
>
<ion-card-header>
<ion-card-title style="font-size: 40px;">Recover</ion-card-title>
<ion-card-subtitle>Restore from backup or recover an old Embassy</ion-card-subtitle>
</ion-card-header>
</ion-card>
<ion-icon color="dark" slot="start" name="reload"></ion-icon>
<ion-label>
<h2><ion-text color="danger">Recover</ion-text></h2>
<p>
Restore from backup or use an existing Embassy data drive
</p>
</ion-label>
</ion-item>
</ng-template>
<ng-template swiperSlide>
<ion-item button detail="true" routerLink="/recover">
<ion-icon color="dark" slot="start" name="save"></ion-icon>
<ion-label>
<h2>
<ion-text color="warning">Restore From Backup</ion-text>
</h2>
<p>Recover an Embassy from encrypted backup</p>
</ion-label>
</ion-item>
<ion-item button detail="true" lines="none" (click)="import()">
<ion-icon color="dark" slot="start" name="cube"></ion-icon>
<ion-label>
<h2>
<ion-text color="primary">Use Existing Drive</ion-text>
</h2>
<p>Attach and use a valid Embassy data drive</p>
</ion-label>
</ion-item>
</ng-template>
</swiper>
</ion-card-content>
</ion-card>
</ion-col>

View File

@@ -0,0 +1,15 @@
.back-button {
position: absolute;
left: 16px;
top: 24px;
z-index: 1000000;
}
ion-item {
--background: var(--ion-color-medium);
--color: var(--ion-color-dark);
}
p {
color: var(--ion-color-dark);
}

View File

@@ -1,9 +1,112 @@
import { Component } from '@angular/core'
import {
AlertController,
IonicSlides,
LoadingController,
ModalController,
NavController,
} from '@ionic/angular'
import { PasswordPage } from 'src/app/modals/password/password.page'
import { ApiService } from 'src/app/services/api/api.service'
import { RPCEncryptedService } from 'src/app/services/rpc-encrypted.service'
import { StateService } from 'src/app/services/state.service'
import SwiperCore, { Swiper } from 'swiper'
import { ErrorToastService } from '@start9labs/shared'
SwiperCore.use([IonicSlides])
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
})
export class HomePage { }
export class HomePage {
swiper?: Swiper
guid?: string | null
error = false
constructor(
private readonly unencrypted: ApiService,
private readonly encrypted: RPCEncryptedService,
private readonly modalCtrl: ModalController,
private readonly alertCtrl: AlertController,
private readonly loadingCtrl: LoadingController,
private readonly stateService: StateService,
private readonly navCtrl: NavController,
private readonly errToastService: ErrorToastService,
) {}
async ngOnInit() {
try {
this.encrypted.secret = await this.unencrypted.getSecret()
const { disks } = await this.unencrypted.getDrives()
this.guid = disks.find(d => !!d.guid)?.guid
} catch (e: any) {
this.error = true
this.errToastService.present(e)
}
}
async ionViewDidEnter() {
if (this.swiper) {
this.swiper.allowTouchMove = false
}
}
setSwiperInstance(swiper: any) {
this.swiper = swiper
}
next() {
this.swiper?.slideNext(500)
}
previous() {
this.swiper?.slidePrev(500)
}
async import() {
if (this.guid) {
const modal = await this.modalCtrl.create({
component: PasswordPage,
componentProps: { storageDrive: true },
})
modal.onDidDismiss().then(res => {
if (res.data && res.data.password) {
this.importDrive(res.data.password)
}
})
await modal.present()
} else {
const alert = await this.alertCtrl.create({
header: 'Drive Not Found',
message:
'Please make sure the drive is a valid Embassy data drive (not a backup) and is firmly connected, then refresh the page.',
})
await alert.present()
}
}
private async importDrive(password: string) {
const loader = await this.loadingCtrl.create({
message: 'Importing Drive',
})
await loader.present()
try {
await this.stateService.importDrive(this.guid!, password)
await this.navCtrl.navigateForward(`/success`)
} catch (e: any) {
this.errToastService.present(e)
} finally {
loader.dismiss()
}
}
}
function decodeHex(hex: string) {
let str = ''
for (let n = 0; n < hex.length; n += 2) {
str += String.fromCharCode(parseInt(hex.substring(n, 2), 16))
}
return str
}

View File

@@ -1,16 +0,0 @@
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { ProductKeyPage } from './product-key.page'
const routes: Routes = [
{
path: '',
component: ProductKeyPage,
},
]
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ProductKeyPageRoutingModule { }

View File

@@ -1,19 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { FormsModule } from '@angular/forms'
import { ProductKeyPage } from './product-key.page'
import { PasswordPageModule } from '../../modals/password/password.module'
import { ProductKeyPageRoutingModule } from './product-key-routing.module'
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
ProductKeyPageRoutingModule,
PasswordPageModule,
],
declarations: [ProductKeyPage],
})
export class ProductKeyPageModule { }

View File

@@ -1,43 +0,0 @@
<ion-content>
<ion-grid>
<ion-row>
<ion-col class="ion-text-center">
<div style="padding-bottom: 32px;">
<img src="assets/img/logo.png" style="max-width: 240px;" />
</div>
<ion-card color="dark">
<ion-card-header style="padding-bottom: 8px;">
<ion-card-title>Product Key</ion-card-title>
<ion-card-subtitle>Enter your product key to establish an encrypted connection with your Embassy</ion-card-subtitle>
</ion-card-header>
<ion-card-content class="ion-margin">
<form (submit)="submit()" style="margin-bottom: 12px;">
<ion-item-group class="ion-padding-bottom">
<ion-item color="dark">
<ion-icon slot="start" name="key-outline" style="margin-right: 16px;"></ion-icon>
<ion-input
#focusInput
name="productKey"
[(ngModel)]="productKey"
(ionChange)="error = ''"
maxlength="12"
>
</ion-input>
</ion-item>
<div class="ion-text-left">
<p *ngIf="error" style="padding-top: 4px"><ion-text color="danger">{{ error }}</ion-text></p>
</div>
</ion-item-group>
<ion-button type="submit" color="light" class="claim-button">
Submit
</ion-button>
</form>
</ion-card-content>
</ion-card>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>

View File

@@ -1,5 +0,0 @@
ion-item {
--border-style: solid;
--border-width: 1px;
--border-color: var(--ion-color-medium);
}

View File

@@ -1,52 +0,0 @@
import { Component, ViewChild } from '@angular/core'
import { IonInput, LoadingController, NavController } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/api.service'
import { RPCEncryptedService } from 'src/app/services/rpc-encrypted.service'
import { StateService } from 'src/app/services/state.service'
@Component({
selector: 'app-product-key',
templateUrl: 'product-key.page.html',
styleUrls: ['product-key.page.scss'],
})
export class ProductKeyPage {
@ViewChild('focusInput') elem?: IonInput
productKey = ''
error = ''
constructor(
private readonly navCtrl: NavController,
private readonly stateService: StateService,
private readonly apiService: ApiService,
private readonly loadingCtrl: LoadingController,
private readonly encrypted: RPCEncryptedService,
) {}
ionViewDidEnter() {
setTimeout(() => this.elem?.setFocus(), 400)
}
async submit() {
if (!this.productKey) return (this.error = 'Must enter product key')
const loader = await this.loadingCtrl.create({
message: 'Verifying Product Key',
})
await loader.present()
try {
this.encrypted.productKey = this.productKey
await this.apiService.verifyProductKey()
if (this.stateService.isMigrating) {
await this.navCtrl.navigateForward(`/loading`)
} else {
await this.navCtrl.navigateForward(`/home`)
}
} catch (e) {
this.error = 'Invalid Product Key'
this.encrypted.productKey = undefined
} finally {
loader.dismiss()
}
}
}

View File

@@ -5,7 +5,6 @@ import { FormsModule } from '@angular/forms'
import { UnitConversionPipesModule } from '@start9labs/shared'
import { DriveStatusComponent, RecoverPage } from './recover.page'
import { PasswordPageModule } from '../../modals/password/password.module'
import { ProdKeyModalModule } from '../../modals/prod-key-modal/prod-key-modal.module'
import { RecoverPageRoutingModule } from './recover-routing.module'
import { CifsModalModule } from 'src/app/modals/cifs-modal/cifs-modal.module'
@@ -17,7 +16,6 @@ import { CifsModalModule } from 'src/app/modals/cifs-modal/cifs-modal.module'
IonicModule,
RecoverPageRoutingModule,
PasswordPageModule,
ProdKeyModalModule,
UnitConversionPipesModule,
CifsModalModule,
],

View File

@@ -1,17 +1,10 @@
import { Component, Input } from '@angular/core'
import {
AlertController,
IonicSafeString,
LoadingController,
ModalController,
NavController,
} from '@ionic/angular'
import { AlertController, ModalController, NavController } from '@ionic/angular'
import { CifsModal } from 'src/app/modals/cifs-modal/cifs-modal.page'
import { ApiService, DiskBackupTarget } from 'src/app/services/api/api.service'
import { ErrorToastService } from '@start9labs/shared'
import { StateService } from 'src/app/services/state.service'
import { PasswordPage } from '../../modals/password/password.page'
import { ProdKeyModal } from '../../modals/prod-key-modal/prod-key-modal.page'
@Component({
selector: 'app-recover',
@@ -21,7 +14,6 @@ import { ProdKeyModal } from '../../modals/prod-key-modal/prod-key-modal.page'
export class RecoverPage {
loading = true
mappedDrives: MappedDisk[] = []
hasShownGuidAlert = false
constructor(
private readonly apiService: ApiService,
@@ -29,8 +21,7 @@ export class RecoverPage {
private readonly modalCtrl: ModalController,
private readonly modalController: ModalController,
private readonly alertCtrl: AlertController,
private readonly loadingCtrl: LoadingController,
private readonly errorToastService: ErrorToastService,
private readonly errToastService: ErrorToastService,
private readonly stateService: StateService,
) {}
@@ -44,10 +35,7 @@ export class RecoverPage {
}
driveClickable(mapped: MappedDisk) {
return (
mapped.drive['embassy-os']?.full &&
(this.stateService.hasProductKey || mapped.is02x)
)
return mapped.drive['embassy-os']?.full
}
async getDrives() {
@@ -89,50 +77,8 @@ export class RecoverPage {
})
await alert.present()
}
const importableDrive = disks.find(d => !!d.guid)
if (
!!importableDrive &&
this.stateService.hasProductKey &&
!this.hasShownGuidAlert
) {
const alert = await this.alertCtrl.create({
header: 'Embassy Data Drive Detected',
message: new IonicSafeString(
`<strong>${importableDrive.vendor || 'Unknown Vendor'} - ${
importableDrive.model || 'Unknown Model'
}</strong> contains Embassy data.
<p>To use this drive and its data, select <strong>"USE DRIVE"</strong>. This will complete the setup process.
<p><strong style="color:red">Important!</strong><br><br>
If you are trying to restore from a backup or update from 0.2.x, <strong>DO NOT</strong> select "USE DRIVE". Instead, select <strong>"CANCEL"</strong> and follow instructions.`,
),
buttons: [
{
role: 'cancel',
text: 'Cancel',
},
{
text: 'Use Drive',
handler: async () => {
const modal = await this.modalController.create({
component: PasswordPage,
componentProps: { storageDrive: importableDrive },
})
modal.onDidDismiss().then(res => {
if (res.data && res.data.password) {
this.importDrive(importableDrive.guid!, res.data.password)
}
})
await modal.present()
},
},
],
})
await alert.present()
this.hasShownGuidAlert = true
}
} catch (e: any) {
this.errorToastService.present(e)
this.errToastService.present(e)
} finally {
this.loading = false
}
@@ -165,7 +111,6 @@ export class RecoverPage {
if (!logicalname) return
if (this.stateService.hasProductKey) {
if (is02x) {
this.selectRecoverySource(logicalname)
} else {
@@ -181,50 +126,6 @@ export class RecoverPage {
})
await modal.present()
}
// if no product key, it means they are an upgrade kit user
} else {
if (!is02x) {
const alert = await this.alertCtrl.create({
header: 'Error',
message:
'In order to use this image, you must select a drive containing a valid 0.2.x Embassy.',
buttons: [
{
role: 'cancel',
text: 'OK',
},
],
})
await alert.present()
} else {
const modal = await this.modalController.create({
component: ProdKeyModal,
componentProps: { target },
cssClass: 'alertlike-modal',
})
modal.onDidDismiss().then(res => {
if (res.data?.productKey) {
this.selectRecoverySource(logicalname)
}
})
await modal.present()
}
}
}
private async importDrive(guid: string, password: string) {
const loader = await this.loadingCtrl.create({
message: 'Importing Drive',
})
await loader.present()
try {
await this.stateService.importDrive(guid, password)
await this.navCtrl.navigateForward(`/success`)
} catch (e: any) {
this.errorToastService.present(e)
} finally {
loader.dismiss()
}
}
private async selectRecoverySource(logicalname: string, password?: string) {

View File

@@ -9,51 +9,48 @@
name="checkmark-circle-outline"
></ion-icon>
<ion-card-title>Setup Complete</ion-card-title>
<ion-card-subtitle
><b
>You have successully claimed your Embassy!</b
></ion-card-subtitle
>
<br />
</ion-card-header>
<ion-card-content>
<br />
<ng-template
[ngIf]="recoverySource && recoverySource.type === 'disk'"
<br />
<h2
*ngIf="recoverySource && recoverySource.type === 'disk'"
class="ion-padding-bottom"
>
<h2>You can now safely unplug your backup drive.</h2>
</ng-template>
<h2>
You have successully claimed your Embassy! You can now access your
device using the methods below.
You can now safely unplug your backup drive.
</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>
<br />
<p>
<b>Note:</b> embassy.local was for setup purposes only, it will no
longer work.
</p>
<div class="line"></div>
<!-- LAN Instructions -->
<div (click)="toggleLan()" class="toggle-label">
<h2>From Home (LAN)</h2>
<ion-icon
name="chevron-down-outline"
[ngStyle]="{
'transform': lanOpen ? 'rotate(-90deg)' : 'rotate(0deg)',
'transition': 'transform 0.4s ease-out'
}"
></ion-icon>
</div>
<h1><b>From Home (LAN)</b></h1>
<div
[ngStyle]="{
'overflow' : 'hidden',
'max-height': lanOpen ? '500px' : '0px',
'transition': 'max-height 0.4s ease-out'
}"
>
<div class="ion-padding ion-text-start">
<p>
Visit the address below when you are conncted to the same WiFi
or Local Area Network (LAN) as your Embassy:
</p>
<br />
<p>
<b>Note:</b> embassy.local was for setup purposes only, it will
no longer work.
</p>
<ion-item
lines="none"
color="dark"
@@ -66,6 +63,14 @@
></code
>
</ion-label>
<ion-button
color="light"
fill="clear"
[href]="lanAddress"
target="_blank"
>
<ion-icon slot="icon-only" name="open-outline"></ion-icon>
</ion-button>
<ion-button
color="light"
fill="clear"
@@ -84,45 +89,25 @@
href="https://start9.com/latest/user-manual/connecting/connecting-lan"
target="_blank"
rel="noreferrer"
class="inline"
>
<b>download and trust</b>
follow the instructions
<ion-icon name="open-outline"></ion-icon>
</a>
your Embassy's Root Certificate Authority.
to downlaod and trust your Embassy's Root Certificate Authority.
</p>
<ion-button
style="margin-top: 24px; margin-bottom: 24px"
color="light"
(click)="installCert()"
>
<ion-button style="margin-top: 24px" (click)="installCert()">
Download Root CA
<ion-icon slot="end" name="download-outline"></ion-icon>
</ion-button>
</div>
<div style="padding-bottom: 24px; border-bottom: solid 1px"></div>
<br />
</div>
<div class="line"></div>
<!-- Tor Instructions -->
<div (click)="toggleTor()" class="toggle-label">
<h2>On The Go (Tor)</h2>
<ion-icon
name="chevron-down-outline"
[ngStyle]="{
'transform': torOpen ? 'rotate(-90deg)' : 'rotate(0deg)',
'transition': 'transform 0.4s ease-out'
}"
></ion-icon>
</div>
<h1><b>On The Go (Tor)</b></h1>
<div
[ngStyle]="{
'overflow' : 'hidden',
'max-height': torOpen ? '500px' : '0px',
'transition': 'max-height 0.4s ease-out'
}"
>
<div class="ion-padding ion-text-start">
<p>Visit the address below when you are away from home:</p>
@@ -154,29 +139,13 @@
href="https://start9.com/latest/user-manual/connecting/connecting-tor"
target="_blank"
rel="noreferrer"
class="inline"
>
<b>Tor-enabled browser</b> </a
Tor-enabled browser
<ion-icon name="open-outline"></ion-icon> </a
>.
</p>
</div>
<div style="padding-bottom: 24px; border-bottom: solid 1px"></div>
<br />
</div>
<div class="ion-text-center ion-padding-top">
<ion-button
color="light"
fill="clear"
color="primary"
strong
(click)="download()"
>
Download this page
<ion-icon slot="end" name="download-outline"></ion-icon>
</ion-button>
</div>
<br />
</ion-card-content>
</ion-card>

View File

@@ -4,26 +4,12 @@ p {
a {
text-decoration: none;
}
.toggle-label {
padding: 24px 0 8px 0;
cursor: pointer;
display: flex;
flex-direction: row;
justify-content: space-between;
* {
display: inline-block;
vertical-align: middle;
}
h2 {
font-weight: bold;
cursor: pointer;
}
ion-icon {
text-align: right;
font-size: 24px;
}
.line {
margin-bottom: 48px;
padding-bottom: 48px;
border-bottom: solid 1px;
}

View File

@@ -16,8 +16,6 @@ import { StateService } from 'src/app/services/state.service'
})
export class SuccessPage {
@Output() onDownload = new EventEmitter()
torOpen = false
lanOpen = false
constructor(
@Inject(DOCUMENT) private readonly document: Document,
@@ -69,14 +67,6 @@ export class SuccessPage {
await toast.present()
}
toggleTor() {
this.torOpen = !this.torOpen
}
toggleLan() {
this.lanOpen = !this.lanOpen
}
installCert() {
this.document.getElementById('install-cert')?.click()
}

View File

@@ -1,20 +1,19 @@
export abstract class ApiService {
// unencrypted
abstract getStatus(): Promise<GetStatusRes> // setup.status
abstract getSecret(): Promise<string> // setup.get-secret
abstract getDrives(): Promise<DiskListResponse> // setup.disk.list
abstract set02XDrive(logicalname: string): Promise<void> // setup.recovery.v2.set
abstract getRecoveryStatus(): Promise<RecoveryStatusRes> // setup.recovery.status
// encrypted
abstract verifyCifs(cifs: CifsRecoverySource): Promise<EmbassyOSRecoveryInfo> // setup.cifs.verify
abstract verifyProductKey(): Promise<void> // echo - throws error if invalid
abstract importDrive(importInfo: ImportDriveReq): Promise<SetupEmbassyRes> // setup.attach
abstract setupEmbassy(setupInfo: SetupEmbassyReq): Promise<SetupEmbassyRes> // setup.execute
abstract setupComplete(): Promise<SetupEmbassyRes> // setup.complete
}
export type GetStatusRes = {
'product-key': boolean
migrating: boolean
}

View File

@@ -1,5 +1,10 @@
import { Injectable } from '@angular/core'
import { HttpService } from '@start9labs/shared'
import {
HttpService,
isRpcError,
RpcError,
RPCOptions,
} from '@start9labs/shared'
import {
ApiService,
CifsRecoverySource,
@@ -13,43 +18,71 @@ import {
SetupEmbassyRes,
} from './api.service'
import { RPCEncryptedService } from '../rpc-encrypted.service'
import * as jose from 'node-jose'
@Injectable({
providedIn: 'root',
})
export class LiveApiService extends ApiService {
export class LiveApiService implements ApiService {
constructor(
private readonly unencrypted: HttpService,
private readonly encrypted: RPCEncryptedService,
) {
super()
}
) {}
// ** UNENCRYPTED **
async getStatus() {
return this.unencrypted.rpcRequest<GetStatusRes>({
return this.rpcRequest<GetStatusRes>({
method: 'setup.status',
params: {},
})
}
/**
* We want to update the secret, which means that we will call in clearnet the
* getSecret, and all the information is never in the clear, and only public
* information is sent across the network. We don't want to expose that we do
* this wil all public/private key, which means that there is no information loss
* through the network.
*/
async getSecret() {
const keystore = jose.JWK.createKeyStore()
const key = await keystore.generate('EC', 'P-256')
// const { privateKey, publicKey } =
// jose.generateKeyPair('ECDH-ES', {
// extractable: true,
// })
console.log({ publicKey: key.toJSON() })
const response: string = await this.rpcRequest({
method: 'setup.get-secret',
params: { pubkey: key.toJSON() },
})
// const { plaintext } = await jose.compactDecrypt(response, privateKey)
const decrypted = await jose.JWE.createDecrypt(key).decrypt(response)
const decoded = new TextDecoder().decode(decrypted.plaintext)
console.log({ decoded })
return decoded
}
async getDrives() {
return this.unencrypted.rpcRequest<DiskListResponse>({
return this.rpcRequest<DiskListResponse>({
method: 'setup.disk.list',
params: {},
})
}
async set02XDrive(logicalname: string) {
return this.unencrypted.rpcRequest<void>({
return this.rpcRequest<void>({
method: 'setup.recovery.v2.set',
params: { logicalname },
})
}
async getRecoveryStatus() {
return this.unencrypted.rpcRequest<RecoveryStatusRes>({
return this.rpcRequest<RecoveryStatusRes>({
method: 'setup.recovery.status',
params: {},
})
@@ -65,13 +98,6 @@ export class LiveApiService extends ApiService {
})
}
async verifyProductKey() {
return this.encrypted.rpcRequest<void>({
method: 'echo',
params: { message: 'hello' },
})
}
async importDrive(params: ImportDriveReq) {
const res = await this.encrypted.rpcRequest<SetupEmbassyRes>({
method: 'setup.attach',
@@ -113,6 +139,18 @@ export class LiveApiService extends ApiService {
'root-ca': btoa(res['root-ca']),
}
}
private async rpcRequest<T>(opts: RPCOptions): Promise<T> {
const res = await this.unencrypted.rpcRequest<T>(opts)
const rpcRes = res.body
if (isRpcError(rpcRes)) {
throw new RpcError(rpcRes.error)
}
return rpcRes.result
}
}
function isCifsSource(

View File

@@ -12,21 +12,29 @@ let tries = 0
@Injectable({
providedIn: 'root',
})
export class MockApiService extends ApiService {
constructor() {
super()
}
export class MockApiService implements ApiService {
// ** UNENCRYPTED **
async getStatus() {
await pauseFor(1000)
return {
'product-key': true,
migrating: false,
}
}
async getSecret() {
await pauseFor(1000)
const ascii = 'thisisasecret'
const arr1 = []
for (let n = 0, l = ascii.length; n < l; n++) {
var hex = Number(ascii.charCodeAt(n)).toString(16)
arr1.push(hex)
}
return arr1.join('')
}
async getDrives() {
await pauseFor(1000)
return {
@@ -84,11 +92,6 @@ export class MockApiService extends ApiService {
}
}
async verifyProductKey() {
await pauseFor(1000)
return
}
async importDrive(params: ImportDriveReq) {
await pauseFor(3000)
return setupRes

View File

@@ -15,13 +15,13 @@ import {
providedIn: 'root',
})
export class RPCEncryptedService {
productKey?: string
secret?: string
constructor(private readonly http: HttpService) {}
async rpcRequest<T>(opts: Omit<RPCOptions, 'timeout'>): Promise<T> {
const encryptedBody = await AES_CTR.encryptPbkdf2(
this.productKey || '',
this.secret || '',
encodeUtf8(JSON.stringify(opts)),
)
@@ -36,7 +36,11 @@ export class RPCEncryptedService {
'Content-Type': 'application/json',
},
})
.then(body => AES_CTR.decryptPbkdf2(this.productKey || '', body))
.then(res => AES_CTR.decryptPbkdf2(this.secret || '', res.body))
.then(x => {
console.log(`Network: ${x}`)
return x
})
.then(res => JSON.parse(res))
.catch(e => {
if (!e.status && !e.statusText) {

View File

@@ -11,9 +11,6 @@ import { pauseFor, ErrorToastService } from '@start9labs/shared'
providedIn: 'root',
})
export class StateService {
hasProductKey = false
isMigrating = false
polling = false
embassyLoaded = false

View File

@@ -38,7 +38,7 @@ ion-content {
ion-grid {
padding-top: 32px;
height: 100%;
max-width: 600px;
max-width: 640px;
}
ion-row {
@@ -47,6 +47,8 @@ ion-row {
ion-item {
--color: var(--ion-color-light);
--highlight-color-valid: transparent;
--highlight-color-invalid: transparent;
}
ion-toolbar {
@@ -61,13 +63,6 @@ ion-avatar {
height: 27px;
}
ion-item {
--highlight-color-valid: transparent;
--highlight-color-invalid: transparent;
--border-radius: 4px;
}
ion-card-title {
margin: 16px 0;
font-family: 'Montserrat';

View File

@@ -1,11 +1,10 @@
import { RpcErrorDetails } from '../types/rpc-error-details'
import { RPCErrorDetails } from '../types/rpc.types'
export class RpcError<T> {
export class RpcError {
readonly code = this.error.code
readonly message = this.getMessage()
readonly revision = this.getRevision()
constructor(private readonly error: RpcErrorDetails<T>) {}
constructor(private readonly error: RPCErrorDetails) {}
private getMessage(): string {
if (typeof this.error.data === 'string') {
@@ -16,10 +15,4 @@ export class RpcError<T> {
? `${this.error.message}\n\n${this.error.data.details}`
: this.error.message
}
private getRevision(): T | null {
return typeof this.error.data === 'string'
? null
: this.error.data?.revision || null
}
}

View File

@@ -14,17 +14,15 @@ export class MarkdownComponent {
@Input() content!: string | Observable<string>
@Input() title!: string
private readonly data$ = defer(() =>
readonly content$ = defer(() =>
isObservable(this.content) ? this.content : of(this.content),
).pipe(share())
readonly error$ = this.data$.pipe(
readonly error$ = this.content$.pipe(
ignoreElements(),
catchError(e => of(getErrorMessage(e))),
)
readonly content$ = this.data$.pipe(catchError(() => of([])))
constructor(private readonly modalCtrl: ModalController) {}
async dismiss() {

View File

@@ -6,11 +6,22 @@ import * as DOMPurify from 'dompurify'
name: 'markdown',
})
export class MarkdownPipe implements PipeTransform {
transform(value: any): any {
transform(value: string): string {
if (value && value.length > 0) {
// convert markdown to html
const html = marked(value)
// sanitize html
const sanitized = DOMPurify.sanitize(html)
return sanitized
// parse html to find all links
let parser = new DOMParser()
const doc = parser.parseFromString(sanitized, 'text/html')
const links = Array.from(doc.getElementsByTagName('a'))
// add target="_blank" to every link
links.forEach(link => {
link.setAttribute('target', '_blank')
})
// return new html string
return doc.documentElement.innerHTML
}
return value
}

View File

@@ -40,12 +40,14 @@ export * from './services/error-toast.service'
export * from './services/http.service'
export * from './types/api'
export * from './types/rpc-error-details'
export * from './types/http.types'
export * from './types/rpc.types'
export * from './types/url'
export * from './types/workspace-config'
export * from './util/copy-to-clipboard'
export * from './util/get-pkg-id'
export * from './util/misc.util'
export * from './util/rpc.util'
export * from './util/to-local-iso-string'
export * from './util/unused'

View File

@@ -1,6 +1,14 @@
import { Inject, Injectable } from '@angular/core'
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'
import { HttpError, RpcError, WorkspaceConfig } from '@start9labs/shared'
import { HttpClient } from '@angular/common/http'
import { HttpError } from '../classes/http-error'
import {
HttpAngularOptions,
HttpOptions,
LocalHttpResponse,
Method,
} from '../types/http.types'
import { RPCResponse, RPCOptions } from '../types/rpc.types'
import { WorkspaceConfig } from '../types/workspace-config'
import {
firstValueFrom,
from,
@@ -32,20 +40,21 @@ export class HttpService {
this.fullUrl = `${protocol}//${hostname}:${port}`
}
async rpcRequest<T>(opts: RPCOptions): Promise<T> {
const { method, params, timeout } = opts
async rpcRequest<T>(
opts: RPCOptions,
): Promise<LocalHttpResponse<RPCResponse<T>>> {
const { method, headers, params, timeout } = opts
const res = await this.httpRequest<RPCResponse<T>>({
return this.httpRequest<RPCResponse<T>>({
method: Method.POST,
url: this.relativeUrl,
headers,
body: { method, params },
timeout,
})
if (isRpcError(res)) throw new RpcError(res.error)
return res.result
}
async httpRequest<T>(opts: HttpOptions): Promise<T> {
async httpRequest<T>(opts: HttpOptions): Promise<LocalHttpResponse<T>> {
let { method, url, headers, body, responseType, timeout } = opts
url = opts.url.startsWith('/') ? this.fullUrl + url : url
@@ -67,113 +76,21 @@ export class HttpService {
responseType: responseType || 'json',
}
let req: Observable<{ body: T }>
let req: Observable<LocalHttpResponse<T>>
if (method === Method.GET) {
req = this.http.get(url, options as any) as any
} else {
req = this.http.post(url, body, options as any) as any
}
return firstValueFrom(timeout ? withTimeout(req, timeout) : req)
.then(res => res.body)
.catch(e => {
return firstValueFrom(timeout ? withTimeout(req, timeout) : req).catch(
e => {
throw new HttpError(e)
})
},
)
}
}
// ** RPC types **
interface RPCBase {
jsonrpc: '2.0'
id: string
}
export interface RPCRequest<T> extends RPCBase {
method: string
params?: T
}
export interface RPCSuccess<T> extends RPCBase {
result: T
}
export interface RPCError extends RPCBase {
error: {
code: number
message: string
data?:
| {
details: string
}
| string
}
}
export type RPCResponse<T> = RPCSuccess<T> | RPCError
export interface RPCOptions {
method: string
params: {
[param: string]:
| string
| number
| boolean
| object
| string[]
| number[]
| null
}
timeout?: number
}
export function isRpcError<Error, Result>(
arg: { error: Error } | { result: Result },
): arg is { error: Error } {
return (arg as any).error !== undefined
}
// ** HTTP types **
export enum Method {
GET = 'GET',
POST = 'POST',
}
export interface HttpOptions {
method: Method
url: string
headers?:
| HttpHeaders
| {
[header: string]: string | string[]
}
params?:
| HttpParams
| {
[param: string]: string | string[]
}
responseType?: 'json' | 'text' | 'arrayBuffer'
body?: any
timeout?: number
}
interface HttpAngularOptions {
observe: 'response'
withCredentials: true
headers?:
| HttpHeaders
| {
[header: string]: string | string[]
}
params?:
| HttpParams
| {
[param: string]: string | string[]
}
responseType?: 'json' | 'text' | 'arrayBuffer'
}
function hasParams(
params?: HttpOptions['params'],
): params is Record<string, string | string[]> {
@@ -191,9 +108,3 @@ function withTimeout<U>(req: Observable<U>, timeout: number): Observable<U> {
),
)
}
export interface RequestError {
code: number
message: string
details: string
}

View File

@@ -0,0 +1,44 @@
import { HttpHeaders, HttpResponse } from '@angular/common/http'
export enum Method {
GET = 'GET',
POST = 'POST',
}
export interface HttpOptions {
method: Method
url: string
headers?: {
[header: string]: string | string[]
}
params?: {
[param: string]: string | string[]
}
responseType?: 'json' | 'text' | 'arrayBuffer'
body?: any
timeout?: number
}
export interface HttpAngularOptions {
observe: 'response'
withCredentials: true
headers?:
| HttpHeaders
| {
[header: string]: string | string[]
}
params?: {
[param: string]: string | string[]
}
responseType?: 'json' | 'text' | 'arrayBuffer'
}
export interface LocalHttpResponse<T> extends HttpResponse<T> {
body: T
}
export interface RequestError {
code: number
message: string
details: string
}

View File

@@ -1,10 +0,0 @@
export interface RpcErrorDetails<T> {
code: number
message: string
data?:
| {
details: string
revision?: T | null
}
| string
}

View File

@@ -0,0 +1,55 @@
// ** RPC types **
interface RPCBase {
jsonrpc: '2.0'
id: string
}
export interface RPCRequest<T> extends RPCBase {
method: string
params?: T
}
export interface RPCSuccessRes<T> extends RPCBase {
result: T
}
export interface RPCErrorRes extends RPCBase {
error: RPCErrorDetails
}
export interface RPCErrorDetails {
code: number
message: string
data?:
| {
details: string
}
| string
}
export type RPCResponse<T> = RPCSuccessRes<T> | RPCErrorRes
export interface RPCOptions {
method: string
headers?: {
[header: string]: string | string[]
}
params: {
[param: string]:
| string
| number
| boolean
| object
| string[]
| number[]
| null
}
timeout?: number
}
export function isRpcError<Error, Result>(
arg: { error: Error } | { result: Result },
): arg is { error: Error } {
return (arg as any).error !== undefined
}

View File

@@ -0,0 +1,11 @@
import { RPCErrorDetails } from '../types/rpc.types'
export function getRpcErrorMessage(error: RPCErrorDetails): string {
if (typeof error.data === 'string') {
return `${error.message}\n\n${error.data}`
}
return error.data?.details
? `${error.message}\n\n${error.data.details}`
: error.message
}

View File

@@ -24,3 +24,6 @@
@import "~@ionic/angular/css/text-alignment.css";
@import "~@ionic/angular/css/text-transformation.css";
@import "~@ionic/angular/css/flex-utils.css";
/* Import swiper styles for slides */
@import '~swiper/scss';

View File

@@ -18,6 +18,12 @@ ion-alert {
}
}
.swiper {
.swiper-slide {
display: unset;
}
}
ion-modal::part(content) {
position: absolute;
height: 90% !important;

View File

@@ -34,7 +34,7 @@ import { ConnectionBarComponentModule } from './components/connection-bar/connec
storeName: '_embassykv',
dbKey: '_embassykey',
name: '_embassystorage',
driverOrder: [Drivers.LocalStorage, Drivers.IndexedDB],
driverOrder: [Drivers.LocalStorage],
}),
MenuModule,
PreloaderModule,

View File

@@ -1,5 +1,4 @@
import { Bootstrapper, DBCache } from 'patch-db-client'
import { APP_INITIALIZER, ErrorHandler, Provider } from '@angular/core'
import { APP_INITIALIZER, Provider } from '@angular/core'
import { UntypedFormBuilder } from '@angular/forms'
import { Router, RouteReuseStrategy } from '@angular/router'
import { IonicRouteStrategy, IonNav } from '@ionic/angular'
@@ -8,10 +7,8 @@ import { WorkspaceConfig } from '@start9labs/shared'
import { ApiService } from './services/api/embassy-api.service'
import { MockApiService } from './services/api/embassy-mock-api.service'
import { LiveApiService } from './services/api/embassy-live-api.service'
import { BOOTSTRAPPER, PATCH_CACHE } from './services/patch-db/patch-db.factory'
import { AuthService } from './services/auth.service'
import { LocalStorageService } from './services/local-storage.service'
import { DataModel } from './services/patch-db/data-model'
import { FilterPackagesPipe } from '../../../marketplace/src/pipes/filter-packages.pipe'
const { useMocks } = require('../../../../config.json') as WorkspaceConfig
@@ -30,14 +27,7 @@ export const APP_PROVIDERS: Provider[] = [
},
{
provide: APP_INITIALIZER,
deps: [
Storage,
AuthService,
LocalStorageService,
Router,
BOOTSTRAPPER,
PATCH_CACHE,
],
deps: [Storage, AuthService, LocalStorageService, Router],
useFactory: appInitializer,
multi: true,
},
@@ -48,19 +38,12 @@ export function appInitializer(
auth: AuthService,
localStorage: LocalStorageService,
router: Router,
bootstrapper: Bootstrapper<DataModel>,
cache: DBCache<DataModel>,
): () => Promise<void> {
return async () => {
await storage.create()
await auth.init()
await localStorage.init()
const localCache = await bootstrapper.init()
cache.sequence = localCache.sequence
cache.data = localCache.data
router.initialNavigation()
}
}

View File

@@ -1,8 +1,8 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { heightCollapse } from '../../util/animations'
import { PatchDbService } from '../../services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import { map } from 'rxjs/operators'
import { ServerInfo } from '../../services/patch-db/data-model'
import { DataModel, ServerInfo } from '../../services/patch-db/data-model'
@Component({
selector: 'footer[appFooter]',
@@ -24,7 +24,7 @@ export class FooterComponent {
},
}
constructor(private readonly patch: PatchDbService) {}
constructor(private readonly patch: PatchDB<DataModel>) {}
getProgress({
downloaded,

View File

@@ -57,6 +57,6 @@
src="assets/img/icons/snek.png"
[appSnekHighScore]="snekScore$ | async"
/>
<ion-footer class="bottom">
<ion-footer *ngIf="sidebarOpen$ | async" class="bottom">
<connection-bar></connection-bar>
</ion-footer>

View File

@@ -1,11 +1,13 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
import { LocalStorageService } from '../../services/local-storage.service'
import { EOSService } from '../../services/eos.service'
import { PatchDbService } from '../../services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
import { AbstractMarketplaceService } from '@start9labs/marketplace'
import { MarketplaceService } from 'src/app/services/marketplace.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { SplitPaneTracker } from 'src/app/services/split-pane.service'
@Component({
selector: 'app-menu',
@@ -57,11 +59,14 @@ export class MenuComponent {
.getUpdates()
.pipe(map(pkgs => pkgs.length))
readonly sidebarOpen$ = this.splitPane.sidebarOpen$
constructor(
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
private readonly localStorageService: LocalStorageService,
private readonly eosService: EOSService,
@Inject(AbstractMarketplaceService)
private readonly marketplaceService: MarketplaceService,
private readonly splitPane: SplitPaneTracker,
) {}
}

View File

@@ -4,9 +4,6 @@
<!-- 3rd party components -->
<qr-code value="hello"></qr-code>
<swiper>
<ng-template swiperSlide>Slide 1</ng-template>
</swiper>
<!-- Ionic components -->
<ion-action-sheet></ion-action-sheet>

View File

@@ -2,11 +2,10 @@ import { CommonModule } from '@angular/common'
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular'
import { QrCodeModule } from 'ng-qrcode'
import { SwiperModule } from 'swiper/angular'
import { PreloaderComponent } from './preloader.component'
@NgModule({
imports: [CommonModule, IonicModule, QrCodeModule, SwiperModule],
imports: [CommonModule, IonicModule, QrCodeModule],
declarations: [PreloaderComponent],
exports: [PreloaderComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],

View File

@@ -1,9 +1,7 @@
import { Directive, HostListener, Input } from '@angular/core'
import { LoadingController, ModalController } from '@ionic/angular'
import { ErrorToastService } from '@start9labs/shared'
import { SnakePage } from '../../modals/snake/snake.page'
import { PatchDbService } from '../../services/patch-db/patch-db.service'
import { ApiService } from '../../services/api/embassy-api.service'
@Directive({

View File

@@ -1,4 +0,0 @@
<h1>
<ion-text color="warning">Warning</ion-text>
</h1>
<div class="ion-text-left" [innerHTML]="params.message || '' | markdown"></div>

View File

@@ -1,18 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { AlertComponent } from './alert.component'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { MarkdownPipeModule } from '@start9labs/shared'
@NgModule({
declarations: [AlertComponent],
imports: [
CommonModule,
IonicModule,
RouterModule.forChild([]),
MarkdownPipeModule,
],
exports: [AlertComponent],
})
export class AlertComponentModule {}

View File

@@ -1,16 +0,0 @@
import { Component, Input } from '@angular/core'
import { BaseSlide } from '../wizard-types'
@Component({
selector: 'alert',
templateUrl: './alert.component.html',
styleUrls: ['../app-wizard.component.scss'],
})
export class AlertComponent implements BaseSlide {
@Input()
params!: { message: string }
async load() {}
loading = false
}

View File

@@ -1,93 +0,0 @@
<ion-header>
<ion-toolbar>
<div style="padding: 10px 0">
<ion-title style="font-size: 32px">{{ params.title }}</ion-title>
<div class="underline"></div>
<ion-title>
<i
>{{ params.action | titlecase
}}<span *ngIf="params.version"
>: {{ params.version | displayEmver }}</span
></i
>
</ion-title>
</div>
<ion-buttons slot="end">
<ion-button (click)="dismiss()">
<ion-icon slot="icon-only" name="close"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<div style="padding: 36px; height: 100%">
<swiper
*ngIf="!error; else hasError"
(swiper)="setSwiperInstance($event)"
(slideNextTransitionStart)="loadSlide()"
>
<ng-template swiperSlide *ngFor="let slide of params.slides">
<alert
#components
*ngIf="slide.selector === 'alert'"
[params]="slide.params"
></alert>
<dependents
#components
*ngIf="slide.selector === 'dependents'"
[params]="slide.params"
(onSuccess)="next()"
(onError)="setError($event)"
></dependents>
<complete
#components
*ngIf="slide.selector === 'complete'"
[params]="slide.params"
(onSuccess)="dismiss('success')"
(onError)="setError($event)"
></complete>
</ng-template>
</swiper>
<ng-template #hasError>
<p>
<ion-text color="danger">{{ error }}</ion-text>
</p>
</ng-template>
</div>
</ion-content>
<ion-footer>
<ion-toolbar>
<ng-container *ngIf="!initializing && swiper">
<ion-buttons slot="end" class="ion-padding-end">
<ion-button
*ngIf="error; else noError"
fill="solid"
color="dark"
(click)="dismiss()"
class="enter-click btn-128"
>
Dismiss
</ion-button>
<ng-template #noError>
<ion-button
*ngIf="!currentSlide.loading && !swiper.isEnd"
fill="solid"
color="primary"
(click)="next()"
class="enter-click btn-128"
[class.no-click]="currentSlide.loading"
>
{{
currentIndex < swiper.slides.length - 2
? 'Continue'
: params.submitBtn
}}
</ion-button>
</ng-template>
</ion-buttons>
</ng-container>
</ion-toolbar>
</ion-footer>

View File

@@ -1,26 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { AppWizardComponent } from './app-wizard.component'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { EmverPipesModule } from '@start9labs/shared'
import { DependentsComponentModule } from './dependents/dependents.component.module'
import { CompleteComponentModule } from './complete/complete.component.module'
import { AlertComponentModule } from './alert/alert.component.module'
import { SwiperModule } from 'swiper/angular'
@NgModule({
declarations: [AppWizardComponent],
imports: [
CommonModule,
IonicModule,
RouterModule.forChild([]),
EmverPipesModule,
DependentsComponentModule,
CompleteComponentModule,
AlertComponentModule,
SwiperModule,
],
exports: [AppWizardComponent],
})
export class AppWizardComponentModule {}

View File

@@ -1,6 +0,0 @@
.underline {
margin: 6px 0 8px 16px;
border-style: solid;
border-width: 0px 0px 1px 0px;
border-color: #404040;
}

View File

@@ -1,107 +0,0 @@
import {
Component,
Input,
QueryList,
ViewChild,
ViewChildren,
} from '@angular/core'
import { IonContent, ModalController } from '@ionic/angular'
import { CompleteComponent } from './complete/complete.component'
import { DependentsComponent } from './dependents/dependents.component'
import { AlertComponent } from './alert/alert.component'
import { WizardAction } from './wizard-types'
import SwiperCore, { Swiper } from 'swiper'
import { IonicSlides } from '@ionic/angular'
import { BaseSlide } from './wizard-types'
SwiperCore.use([IonicSlides])
@Component({
selector: 'app-wizard',
templateUrl: './app-wizard.component.html',
styleUrls: ['./app-wizard.component.scss'],
})
export class AppWizardComponent {
@Input()
params!: {
action: WizardAction
title: string
slides: SlideDefinition[]
submitBtn: string
version?: string
}
// content container so we can scroll to top between slide transitions
@ViewChild(IonContent)
content?: IonContent
swiper?: Swiper
//a slide component gives us hook into a slide. Allows us to call load when slide comes into view
@ViewChildren('components')
slideComponentsQL?: QueryList<BaseSlide>
get slideComponents(): BaseSlide[] {
return this.slideComponentsQL?.toArray() || []
}
get currentSlide(): BaseSlide {
return this.slideComponents[this.currentIndex]
}
get currentIndex(): number {
return this.swiper?.activeIndex || NaN
}
initializing = true
error = ''
constructor(private readonly modalController: ModalController) {}
ionViewDidEnter() {
this.initializing = false
if (this.swiper) this.swiper.allowTouchMove = false
this.loadSlide()
}
setSwiperInstance(swiper: any) {
this.swiper = swiper
}
dismiss(role = 'cancelled') {
this.modalController.dismiss(null, role)
}
async next() {
await this.content?.scrollToTop()
this.swiper?.slideNext(500)
}
setError(e: any) {
this.error = e
}
async loadSlide() {
this.currentSlide.load()
}
}
export type SlideDefinition =
| { selector: 'alert'; params: AlertComponent['params'] }
| { selector: 'dependents'; params: DependentsComponent['params'] }
| { selector: 'complete'; params: CompleteComponent['params'] }
export async function wizardModal(
modalController: ModalController,
params: AppWizardComponent['params'],
): Promise<boolean> {
const modal = await modalController.create({
backdropDismiss: false,
cssClass: 'wizard-modal',
component: AppWizardComponent,
componentProps: { params },
})
await modal.present()
return modal.onDidDismiss().then(({ role }) => role === 'success')
}

View File

@@ -1,4 +0,0 @@
<div style="padding: 32px">
<ion-spinner color="warning" name="lines"></ion-spinner>
<p>{{ message }}</p>
</div>

Some files were not shown because too many files have changed in this diff Show More