setup complete fix and minor copy changes

return SetupResult on setup.complete
This commit is contained in:
Drew Ansbacher
2022-02-27 19:07:43 -07:00
committed by Aiden McClelland
parent df16943502
commit 61ee46f289
21 changed files with 456 additions and 406 deletions

View File

@@ -8,7 +8,7 @@ use patch_db::json_ptr::JsonPointer;
use patch_db::PatchDb; use patch_db::PatchDb;
use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::yajrc::RpcError;
use rpc_toolkit::Context; use rpc_toolkit::Context;
use serde::Deserialize; use serde::{Deserialize, Serialize};
use sqlx::sqlite::SqliteConnectOptions; use sqlx::sqlite::SqliteConnectOptions;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use tokio::fs::File; use tokio::fs::File;
@@ -25,6 +25,14 @@ use crate::util::io::from_toml_async_reader;
use crate::util::AsyncFileExt; use crate::util::AsyncFileExt;
use crate::{Error, ResultExt}; use crate::{Error, ResultExt};
#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct SetupResult {
pub tor_address: String,
pub lan_address: String,
pub root_ca: String,
}
#[derive(Debug, Default, Deserialize)] #[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct SetupContextConfig { pub struct SetupContextConfig {
@@ -62,7 +70,7 @@ pub struct SetupContextSeed {
pub selected_v2_drive: RwLock<Option<PathBuf>>, pub selected_v2_drive: RwLock<Option<PathBuf>>,
pub cached_product_key: RwLock<Option<Arc<String>>>, pub cached_product_key: RwLock<Option<Arc<String>>>,
pub recovery_status: RwLock<Option<Result<RecoveryStatus, RpcError>>>, pub recovery_status: RwLock<Option<Result<RecoveryStatus, RpcError>>>,
pub disk_guid: RwLock<Option<Arc<String>>>, pub setup_result: RwLock<Option<(Arc<String>, SetupResult)>>,
} }
#[derive(Clone)] #[derive(Clone)]
@@ -81,7 +89,7 @@ impl SetupContext {
selected_v2_drive: RwLock::new(None), selected_v2_drive: RwLock::new(None),
cached_product_key: RwLock::new(None), cached_product_key: RwLock::new(None),
recovery_status: RwLock::new(None), recovery_status: RwLock::new(None),
disk_guid: RwLock::new(None), setup_result: RwLock::new(None),
}))) })))
} }
#[instrument(skip(self))] #[instrument(skip(self))]

View File

@@ -25,6 +25,7 @@ use tracing::instrument;
use crate::backup::restore::recover_full_embassy; use crate::backup::restore::recover_full_embassy;
use crate::backup::target::BackupTargetFS; use crate::backup::target::BackupTargetFS;
use crate::context::rpc::RpcContextConfig; use crate::context::rpc::RpcContextConfig;
use crate::context::setup::SetupResult;
use crate::context::SetupContext; use crate::context::SetupContext;
use crate::db::model::RecoveredPackageInfo; use crate::db::model::RecoveredPackageInfo;
use crate::disk::main::DEFAULT_PASSWORD; use crate::disk::main::DEFAULT_PASSWORD;
@@ -112,18 +113,19 @@ pub async fn attach(
&*ctx.product_key().await?, &*ctx.product_key().await?,
) )
.await?; .await?;
*ctx.disk_guid.write().await = Some(guid.clone());
let secrets = ctx.secret_store().await?; let secrets = ctx.secret_store().await?;
let tor_key = crate::net::tor::os_key(&mut secrets.acquire().await?).await?; let tor_key = crate::net::tor::os_key(&mut secrets.acquire().await?).await?;
let (_, root_ca) = SslManager::init(secrets).await?.export_root_ca().await?; let (_, root_ca) = SslManager::init(secrets).await?.export_root_ca().await?;
Ok(SetupResult { let setup_result = SetupResult {
tor_address: format!("http://{}", tor_key.public().get_onion_address()), tor_address: format!("http://{}", tor_key.public().get_onion_address()),
lan_address: format!( lan_address: format!(
"https://embassy-{}.local", "https://embassy-{}.local",
crate::hostname::derive_id(&*ctx.product_key().await?) crate::hostname::derive_id(&*ctx.product_key().await?)
), ),
root_ca: String::from_utf8(root_ca.to_pem()?)?, root_ca: String::from_utf8(root_ca.to_pem()?)?,
}) };
*ctx.setup_result.write().await = Some((guid, setup_result.clone()));
Ok(setup_result)
} }
#[command(subcommands(v2, recovery_status))] #[command(subcommands(v2, recovery_status))]
@@ -191,14 +193,6 @@ pub async fn verify_cifs(
embassy_os.ok_or_else(|| Error::new(eyre!("No Backup Found"), crate::ErrorKind::NotFound)) embassy_os.ok_or_else(|| Error::new(eyre!("No Backup Found"), crate::ErrorKind::NotFound))
} }
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct SetupResult {
tor_address: String,
lan_address: String,
root_ca: String,
}
#[command(rpc_only)] #[command(rpc_only)]
pub async fn execute( pub async fn execute(
#[context] ctx: SetupContext, #[context] ctx: SetupContext,
@@ -240,9 +234,9 @@ pub async fn execute(
#[instrument(skip(ctx))] #[instrument(skip(ctx))]
#[command(rpc_only)] #[command(rpc_only)]
pub async fn complete(#[context] ctx: SetupContext) -> Result<(), Error> { pub async fn complete(#[context] ctx: SetupContext) -> Result<SetupResult, Error> {
let guid = if let Some(guid) = &*ctx.disk_guid.read().await { let (guid, setup_result) = if let Some((guid, setup_result)) = &*ctx.setup_result.read().await {
guid.clone() (guid.clone(), setup_result.clone())
} else { } else {
return Err(Error::new( return Err(Error::new(
eyre!("setup.execute has not completed successfully"), eyre!("setup.execute has not completed successfully"),
@@ -281,7 +275,7 @@ pub async fn complete(#[context] ctx: SetupContext) -> Result<(), Error> {
guid_file.write_all(guid.as_bytes()).await?; guid_file.write_all(guid.as_bytes()).await?;
guid_file.sync_all().await?; guid_file.sync_all().await?;
ctx.shutdown.send(()).expect("failed to shutdown"); ctx.shutdown.send(()).expect("failed to shutdown");
Ok(()) Ok(setup_result)
} }
#[instrument(skip(ctx, embassy_password, recovery_password))] #[instrument(skip(ctx, embassy_password, recovery_password))]
@@ -323,10 +317,21 @@ pub async fn execute_inner(
&ctx.product_key().await?, &ctx.product_key().await?,
) )
.await?; .await?;
let res = (tor_addr, root_ca.clone());
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = recover_fut if let Err(e) = recover_fut
.and_then(|_| async { .and_then(|_| async {
*ctx.disk_guid.write().await = Some(guid); *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?)
),
root_ca: String::from_utf8(root_ca.to_pem()?)?,
},
));
if let Some(Ok(recovery_status)) = &mut *ctx.recovery_status.write().await { if let Some(Ok(recovery_status)) = &mut *ctx.recovery_status.write().await {
recovery_status.complete = true; recovery_status.complete = true;
} }
@@ -342,16 +347,26 @@ pub async fn execute_inner(
tracing::info!("Recovery Complete!"); tracing::info!("Recovery Complete!");
} }
}); });
(tor_addr, root_ca) res
} else { } else {
let res = fresh_setup(&ctx, &embassy_password).await?; let (tor_addr, root_ca) = fresh_setup(&ctx, &embassy_password).await?;
init( init(
&RpcContextConfig::load(ctx.config_path.as_ref()).await?, &RpcContextConfig::load(ctx.config_path.as_ref()).await?,
&ctx.product_key().await?, &ctx.product_key().await?,
) )
.await?; .await?;
*ctx.disk_guid.write().await = Some(guid); *ctx.setup_result.write().await = Some((
res guid,
SetupResult {
tor_address: format!("http://{}", tor_addr),
lan_address: format!(
"https://embassy-{}.local",
crate::hostname::derive_id(&ctx.product_key().await?)
),
root_ca: String::from_utf8(root_ca.to_pem()?)?,
},
));
(tor_addr, root_ca)
}; };
Ok(res) Ok(res)

View File

@@ -4,33 +4,41 @@ import { NavGuard, RecoveryNavGuard } from './guards/nav-guard'
const routes: Routes = [ const routes: Routes = [
{ path: '', redirectTo: '/product-key', pathMatch: 'full' }, { path: '', redirectTo: '/product-key', pathMatch: 'full' },
{
path: 'init',
loadChildren: () => import('./pages/init/init.module').then( m => m.InitPageModule),
canActivate: [NavGuard],
},
{ {
path: 'product-key', path: 'product-key',
loadChildren: () => import('./pages/product-key/product-key.module').then( m => m.ProductKeyPageModule), loadChildren: () =>
import('./pages/product-key/product-key.module').then(
m => m.ProductKeyPageModule,
),
}, },
{ {
path: 'home', path: 'home',
loadChildren: () => import('./pages/home/home.module').then( m => m.HomePageModule), loadChildren: () =>
import('./pages/home/home.module').then(m => m.HomePageModule),
canActivate: [NavGuard], canActivate: [NavGuard],
}, },
{ {
path: 'recover', path: 'recover',
loadChildren: () => import('./pages/recover/recover.module').then( m => m.RecoverPageModule), loadChildren: () =>
import('./pages/recover/recover.module').then(m => m.RecoverPageModule),
canActivate: [RecoveryNavGuard], canActivate: [RecoveryNavGuard],
}, },
{ {
path: 'embassy', path: 'embassy',
loadChildren: () => import('./pages/embassy/embassy.module').then( m => m.EmbassyPageModule), loadChildren: () =>
import('./pages/embassy/embassy.module').then(m => m.EmbassyPageModule),
canActivate: [NavGuard], canActivate: [NavGuard],
}, },
{ {
path: 'loading', path: 'loading',
loadChildren: () => import('./pages/loading/loading.module').then( m => m.LoadingPageModule), 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], canActivate: [NavGuard],
}, },
] ]
@@ -46,4 +54,4 @@ const routes: Routes = [
], ],
exports: [RouterModule], exports: [RouterModule],
}) })
export class AppRoutingModule { } export class AppRoutingModule {}

View File

@@ -15,7 +15,6 @@ import { AppComponent } from './app.component'
import { AppRoutingModule } from './app-routing.module' import { AppRoutingModule } from './app-routing.module'
import { GlobalErrorHandler } from './services/global-error-handler.service' import { GlobalErrorHandler } from './services/global-error-handler.service'
import { SuccessPageModule } from './pages/success/success.module' import { SuccessPageModule } from './pages/success/success.module'
import { InitPageModule } from './pages/init/init.module'
import { HomePageModule } from './pages/home/home.module' import { HomePageModule } from './pages/home/home.module'
import { LoadingPageModule } from './pages/loading/loading.module' import { LoadingPageModule } from './pages/loading/loading.module'
import { ProdKeyModalModule } from './modals/prod-key-modal/prod-key-modal.module' import { ProdKeyModalModule } from './modals/prod-key-modal/prod-key-modal.module'
@@ -42,7 +41,6 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig
ProdKeyModalModule, ProdKeyModalModule,
ProductKeyPageModule, ProductKeyPageModule,
RecoverPageModule, RecoverPageModule,
InitPageModule,
], ],
providers: [ providers: [
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },

View File

@@ -129,7 +129,7 @@ export class EmbassyPage {
private async setupEmbassy(drive: DiskInfo, password: string): Promise<void> { private async setupEmbassy(drive: DiskInfo, password: string): Promise<void> {
const loader = await this.loadingCtrl.create({ const loader = await this.loadingCtrl.create({
message: 'Transferring encrypted data. This could take a while...', message: 'Initializing data drive. This could take a while...',
}) })
await loader.present() await loader.present()
@@ -139,7 +139,7 @@ export class EmbassyPage {
if (!!this.stateService.recoverySource) { if (!!this.stateService.recoverySource) {
await this.navCtrl.navigateForward(`/loading`) await this.navCtrl.navigateForward(`/loading`)
} else { } else {
await this.navCtrl.navigateForward(`/init`) await this.navCtrl.navigateForward(`/success`)
} }
} catch (e) { } catch (e) {
this.errorToastService.present( this.errorToastService.present(

View File

@@ -1,18 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { InitPage } from './init.page'
import { InitPageRoutingModule } from './init-routing.module'
import { SuccessPageModule } from '../success/success.module'
@NgModule({
imports: [
CommonModule,
IonicModule,
InitPageRoutingModule,
SuccessPageModule,
],
declarations: [InitPage],
exports: [InitPage],
})
export class InitPageModule { }

View File

@@ -1,48 +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>
<success
[hidden]="!stateService.embassyLoaded"
(onDownload)="download()"
></success>
<ion-card [hidden]="stateService.embassyLoaded" color="dark">
<ion-card-header>
<ion-card-title style="font-size: 40px"
>Initializing Embassy</ion-card-title
>
<ion-card-subtitle>Progress: {{ progress }}%</ion-card-subtitle>
</ion-card-header>
<ion-card-content class="ion-margin">
<ion-progress-bar
color="primary"
style="
max-width: 700px;
margin: auto;
padding-bottom: 20px;
margin-bottom: 40px;
"
[value]="progress / 100"
></ion-progress-bar>
<p class="ion-text-start">
After completion, you will be prompted to download a file from
your Embassy. Save the file somewhere safe, it is the easiest way
to recover your Embassy's addresses and SSL certificate in case
you lose them.
</p>
<p class="ion-text-start" style="color: var(--ion-color-danger)">
<b>DO NOT:</b> Close or refresh the browser window during
intialization process.
</p>
</ion-card-content>
</ion-card>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>

View File

@@ -1,61 +0,0 @@
import { Component } from '@angular/core'
import { interval, Subscription } from 'rxjs'
import { finalize, take, tap } from 'rxjs/operators'
import { ApiService } from 'src/app/services/api/api.service'
import { StateService } from 'src/app/services/state.service'
@Component({
selector: 'app-init',
templateUrl: 'init.page.html',
styleUrls: ['init.page.scss'],
})
export class InitPage {
progress = 0
sub: Subscription
constructor (
private readonly apiService: ApiService,
public readonly stateService: StateService,
) { }
ngOnInit () {
// call setup.complete to tear down embassy.local and spin up embassy-[id].local
this.apiService.setupComplete()
this.sub = interval(130)
.pipe(
take(101),
tap(num => {
this.progress = num
}),
finalize(() => {
setTimeout(() => {
this.stateService.embassyLoaded = true
this.download()
}, 500)
}),
).subscribe()
}
ngOnDestroy () {
if (this.sub) this.sub.unsubscribe()
}
download () {
document.getElementById('tor-addr').innerHTML = this.stateService.torAddress
document.getElementById('lan-addr').innerHTML = this.stateService.lanAddress
document.getElementById('cert').setAttribute('href', 'data:application/x-x509-ca-cert;base64,' + encodeURIComponent(this.stateService.cert))
let html = document.getElementById('downloadable').innerHTML
const filename = 'embassy-info.html'
const elem = document.createElement('a')
elem.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(html))
elem.setAttribute('download', filename)
elem.style.display = 'none'
document.body.appendChild(elem)
elem.click()
document.body.removeChild(elem)
}
}

View File

@@ -8,19 +8,20 @@ import { StateService } from 'src/app/services/state.service'
styleUrls: ['loading.page.scss'], styleUrls: ['loading.page.scss'],
}) })
export class LoadingPage { export class LoadingPage {
constructor ( constructor(
public stateService: StateService, public stateService: StateService,
private navCtrl: NavController, private navCtrl: NavController,
) { } ) {}
ngOnInit () { ngOnInit() {
this.stateService.pollDataTransferProgress() this.stateService.pollDataTransferProgress()
const progSub = this.stateService.dataCompletionSubject.subscribe(async complete => { const progSub = this.stateService.dataCompletionSubject.subscribe(
if (complete) { async complete => {
progSub.unsubscribe() if (complete) {
await this.navCtrl.navigateForward(`/init`) progSub.unsubscribe()
} await this.navCtrl.navigateForward(`/success`)
}) }
},
)
} }
} }

View File

@@ -204,7 +204,7 @@ export class RecoverPage {
await loader.present() await loader.present()
try { try {
await this.stateService.importDrive(guid) await this.stateService.importDrive(guid)
await this.navCtrl.navigateForward(`/init`) await this.navCtrl.navigateForward(`/success`)
} catch (e) { } catch (e) {
this.errorToastService.present(e.message) this.errorToastService.present(e.message)
} finally { } finally {

View File

@@ -1,11 +1,11 @@
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router' import { RouterModule, Routes } from '@angular/router'
import { InitPage } from './init.page' import { SuccessPage } from './success.page'
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
component: InitPage, component: SuccessPage,
}, },
] ]
@@ -13,4 +13,4 @@ const routes: Routes = [
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule], exports: [RouterModule],
}) })
export class InitPageRoutingModule { } export class SuccessPageRoutingModule {}

View File

@@ -4,6 +4,7 @@ import { IonicModule } from '@ionic/angular'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { SuccessPage } from './success.page' import { SuccessPage } from './success.page'
import { PasswordPageModule } from '../../modals/password/password.module' import { PasswordPageModule } from '../../modals/password/password.module'
import { SuccessPageRoutingModule } from './success-routing.module'
@NgModule({ @NgModule({
imports: [ imports: [
@@ -11,8 +12,9 @@ import { PasswordPageModule } from '../../modals/password/password.module'
FormsModule, FormsModule,
IonicModule, IonicModule,
PasswordPageModule, PasswordPageModule,
SuccessPageRoutingModule,
], ],
declarations: [SuccessPage], declarations: [SuccessPage],
exports: [SuccessPage], exports: [SuccessPage],
}) })
export class SuccessPageModule { } export class SuccessPageModule {}

View File

@@ -1,167 +1,243 @@
<ion-content>
<ion-grid>
<ion-row>
<ion-col>
<ion-card color="dark">
<ion-card-header class="ion-text-center" color="success">
<ion-icon
style="font-size: 80px"
name="checkmark-circle-outline"
></ion-icon>
<ion-card-title>Setup Complete!</ion-card-title>
</ion-card-header>
<ion-card-content>
<br />
<ng-template
[ngIf]="stateService.recoverySource && stateService.recoverySource.type === 'disk'"
>
<h2>You can now safely unplug your backup drive.</h2>
</ng-template>
<!-- Tor Instructions -->
<div (click)="toggleTor()" class="toggle-label">
<h2>Tor Instructions:</h2>
<ion-icon
name="chevron-down-outline"
[ngStyle]="{
'transform': torOpen ? 'rotate(-90deg)' : 'rotate(0deg)',
'transition': 'transform 0.4s ease-out'
}"
></ion-icon>
</div>
<ion-card color="dark"> <div
<ion-card-header class="ion-text-center" color="success"> [ngStyle]="{
<ion-icon style="font-size: 80px;" name="checkmark-circle-outline"></ion-icon> 'overflow' : 'hidden',
<ion-card-title>Setup Complete!</ion-card-title> 'max-height': torOpen ? '500px' : '0px',
</ion-card-header> 'transition': 'max-height 0.4s ease-out'
<ion-card-content> }"
<br /> >
<ng-template [ngIf]="stateService.recoverySource && stateService.recoverySource.type === 'disk'"> <div class="ion-padding ion-text-start">
<h2>You can now safely unplug your backup drive.</h2> <p>
</ng-template> To use your Embassy over Tor, visit its unique Tor address
<!-- Tor Instructions --> from any Tor-enabled browser. For a list of recommended
<div (click)="toggleTor()" class="toggle-label"> browsers, click
<h2>Tor Instructions:</h2> <a
<ion-icon href="https://start9.com/latest/user-manual/connecting/connecting-tor"
name="chevron-down-outline" target="_blank"
[ngStyle]="{ rel="noreferrer"
'transform': torOpen ? 'rotate(-90deg)' : 'rotate(0deg)', ><b>here</b></a
'transition': 'transform 0.4s ease-out' >.
}" </p>
></ion-icon> <br />
</div> <p>Tor Address</p>
<ion-item lines="none" color="dark">
<ion-label class="ion-text-wrap">
<code
><ion-text color="light"
>{{ stateService.torAddress }}</ion-text
></code
>
</ion-label>
<ion-button
color="light"
fill="clear"
(click)="copy(stateService.torAddress)"
>
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-item>
</div>
<div style="padding-bottom: 24px; border-bottom: solid 1px"></div>
<br />
</div>
<div <!-- LAN Instructions -->
[ngStyle]="{ <div (click)="toggleLan()" class="toggle-label">
'overflow' : 'hidden', <h2>LAN Instructions (Slightly Advanced):</h2>
'max-height': torOpen ? '500px' : '0px', <ion-icon
'transition': 'max-height 0.4s ease-out' name="chevron-down-outline"
}" [ngStyle]="{
> 'transform': lanOpen ? 'rotate(-90deg)' : 'rotate(0deg)',
<div class="ion-padding ion-text-start"> 'transition': 'transform 0.4s ease-out'
<p> }"
To use your Embassy over Tor, visit its unique Tor address from any Tor-enabled browser. ></ion-icon>
For a list of recommended browsers, click <a href="https://start9.com/latest/user-manual/connecting" target="_blank" rel="noreferrer"><b>here</b></a>. </div>
</p>
<br />
<p>Tor Address</p>
<ion-item lines="none" color="dark">
<ion-label class="ion-text-wrap">
<code><ion-text color="light">{{ stateService.torAddress }}</ion-text></code>
</ion-label>
<ion-button color="light" fill="clear" (click)="copy(stateService.torAddress)">
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-item>
</div>
<div style="padding-bottom: 24px; border-bottom: solid 1px;"></div>
<br />
</div>
<!-- LAN Instructions --> <div
<div (click)="toggleLan()" class="toggle-label"> [ngStyle]="{
<h2>LAN Instructions (Slightly Advanced):</h2> 'overflow' : 'hidden',
<ion-icon 'max-height': lanOpen ? '500px' : '0px',
name="chevron-down-outline" 'transition': 'max-height 0.4s ease-out'
[ngStyle]="{ }"
'transform': lanOpen ? 'rotate(-90deg)' : 'rotate(0deg)', >
'transition': 'transform 0.4s ease-out' <div class="ion-padding ion-text-start">
}" <p>To use your Embassy locally, you must:</p>
></ion-icon> <ol>
</div> <li>
Currently be connected to the same Local Area Network (LAN)
as your Embassy.
</li>
<li>Download your Embassy's Root Certificate Authority.</li>
<li>
Trust your Embassy's Root CA on <i>both</i> your
computer/phone and in your browser settings.
</li>
</ol>
<p>
For step-by-step instructions, click
<a
href="https://start9.com/latest/user-manual/connecting/connecting-lan"
target="_blank"
rel="noreferrer"
><b>here</b></a
>.
</p>
<div <p>
[ngStyle]="{ <b
'overflow' : 'hidden', >Please note, once setup is complete, the embassy.local
'max-height': lanOpen ? '500px' : '0px', address will no longer connect to your Embassy.</b
'transition': 'max-height 0.4s ease-out' >
}" </p>
>
<div class="ion-padding ion-text-start">
<p>To use your Embassy locally, you must:</p>
<ol>
<li>Currently be connected to the same Local Area Network (LAN) as your Embassy.</li>
<li>Download your Embassy's Root Certificate Authority.</li>
<li>Trust your Embassy's Root CA on <i>both</i> your computer/phone and in your browser settings.</li>
</ol>
<p>
For step-by-step instructions, click
<a href="https://start9.com/latest/user-manual/connecting/connecting-lan" target="_blank" rel="noreferrer"><b>here</b></a>.
</p>
<p> <ion-button
<b>Please note, once setup is complete, the embassy.local address will no longer connect to your Embassy.</b> style="margin-top: 24px; margin-bottom: 24px"
</p> color="light"
(click)="installCert()"
>
Download Root CA
<ion-icon slot="end" name="download-outline"></ion-icon>
</ion-button>
<ion-button style="margin-top: 24px; margin-bottom: 24px;" color="light" (click)="installCert()"> <p>LAN Address</p>
Download Root CA <ion-item lines="none" color="dark">
<ion-icon slot="end" name="download-outline"></ion-icon> <ion-label class="ion-text-wrap">
</ion-button> <code
><ion-text color="light"
>{{ stateService.lanAddress }}</ion-text
></code
>
</ion-label>
<ion-button
color="light"
fill="clear"
(click)="copy(stateService.lanAddress)"
>
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-item>
</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>
<p>LAN Address</p> <!-- cert elem -->
<ion-item lines="none" color="dark"> <a hidden id="install-cert" download="embassy.crt"></a>
<ion-label class="ion-text-wrap">
<code><ion-text color="light">{{ stateService.lanAddress }}</ion-text></code>
</ion-label>
<ion-button color="light" fill="clear" (click)="copy(stateService.lanAddress)">
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-item>
</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>
<!-- download elem -->
<div hidden id="downloadable">
<div style="padding: 0 24px; font-family: Courier">
<h1>Embassy Info</h1>
<!-- cert elem --> <section style="padding: 16px; border: solid 1px">
<a hidden id="install-cert" download="embassy.crt"></a> <h2>Tor Info</h2>
<p>
To use your Embassy over Tor, visit its unique Tor address from
any Tor-enabled browser.
</p>
<p>
For a list of recommended browsers, click
<a
href="https://start9.com/latest/user-manual/connecting/connecting-tor"
target="_blank"
rel="noreferrer"
><b>here</b></a
>.
</p>
<p><b>Tor Address: </b><code id="tor-addr"></code></p>
</section>
<!-- download elem --> <section style="padding: 16px; border: solid 1px; border-top: none">
<div hidden id="downloadable"> <h2>LAN Info</h2>
<div style="padding: 0 24px; font-family: Courier;"> <p>To use your Embassy locally, you must:</p>
<h1>Embassy Info</h1> <ol>
<li>
Currently be connected to the same Local Area Network (LAN) as
your Embassy.
</li>
<li>Download your Embassy's Root Certificate Authority.</li>
<li>
Trust your Embassy's Root CA on <i>both</i> your
computer/phone and in your browser settings.
</li>
</ol>
<p>
For step-by-step instructions, click
<a
href="https://start9.com/latest/user-manual/connecting/connecting-lan"
target="_blank"
rel="noreferrer"
><b>here</b></a
>.
</p>
<section style="padding: 16px; border: solid 1px;"> <div style="margin: 42px 0">
<h2>Tor Info</h2> <a
<p> id="cert"
To use your Embassy over Tor, visit its unique Tor address from any Tor-enabled browser. download="embassy.crt"
</p> style="
<p> background: #25272b;
For a list of recommended browsers, click <a href="https://start9.com/latest/user-manual/connecting" target="_blank" rel="noreferrer"><b>here</b></a>. padding: 10px;
</p> text-decoration: none;
<p><b>Tor Address: </b><code id="tor-addr"></code></p> text-align: center;
</section> border-radius: 4px;
color: white;
"
>
Download Root CA
</a>
</div>
<section style="padding: 16px; border: solid 1px; border-top: none;"> <p><b>LAN Address: </b><code id="lan-addr"></code></p>
<h2>LAN Info</h2> </section>
<p>To use your Embassy locally, you must:</p> </div>
<ol> </div>
<li>Currently be connected to the same Local Area Network (LAN) as your Embassy.</li> </ion-col>
<li>Download your Embassy's Root Certificate Authority.</li> </ion-row>
<li>Trust your Embassy's Root CA on <i>both</i> your computer/phone and in your browser settings.</li> </ion-grid>
</ol> </ion-content>
<p>
For step-by-step instructions, click
<a href="https://start9.com/latest/user-manual/connecting/connecting-lan" target="_blank" rel="noreferrer"><b>here</b></a>.
</p>
<div style="margin: 42px 0;">
<a
id="cert"
download="embassy.crt"
style="
background: #25272b;
padding: 10px;
text-decoration: none;
text-align: center;
border-radius: 4px;
color: white;
"
>
Download Root CA
</a>
</div>
<p><b>LAN Address: </b><code id="lan-addr"></code></p>
</section>
</div>
</div>

View File

@@ -1,5 +1,6 @@
import { Component, EventEmitter, Output } from '@angular/core' import { Component, EventEmitter, Output } from '@angular/core'
import { ToastController } from '@ionic/angular' import { ToastController } from '@ionic/angular'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { StateService } from 'src/app/services/state.service' import { StateService } from 'src/app/services/state.service'
@Component({ @Component({
@@ -12,16 +13,29 @@ export class SuccessPage {
torOpen = true torOpen = true
lanOpen = false lanOpen = false
constructor ( constructor(
private readonly toastCtrl: ToastController, private readonly toastCtrl: ToastController,
private readonly errCtrl: ErrorToastService,
public readonly stateService: StateService, public readonly stateService: StateService,
) { } ) {}
ngAfterViewInit () { async ngAfterViewInit() {
document.getElementById('install-cert').setAttribute('href', 'data:application/x-x509-ca-cert;base64,' + encodeURIComponent(this.stateService.cert)) try {
await this.stateService.completeEmbassy()
document
.getElementById('install-cert')
.setAttribute(
'href',
'data:application/x-x509-ca-cert;base64,' +
encodeURIComponent(this.stateService.cert),
)
this.download()
} catch (e) {
await this.errCtrl.present(e)
}
} }
async copy (address: string): Promise<void> { async copy(address: string): Promise<void> {
const success = await this.copyToClipboard(address) const success = await this.copyToClipboard(address)
const message = success ? 'copied to clipboard!' : 'failed to copy' const message = success ? 'copied to clipboard!' : 'failed to copy'
@@ -33,23 +47,45 @@ export class SuccessPage {
await toast.present() await toast.present()
} }
toggleTor () { toggleTor() {
this.torOpen = !this.torOpen this.torOpen = !this.torOpen
} }
toggleLan () { toggleLan() {
this.lanOpen = !this.lanOpen this.lanOpen = !this.lanOpen
} }
installCert () { installCert() {
document.getElementById('install-cert').click() document.getElementById('install-cert').click()
} }
download () { download() {
this.onDownload.emit() document.getElementById('tor-addr').innerHTML = this.stateService.torAddress
document.getElementById('lan-addr').innerHTML = this.stateService.lanAddress
document
.getElementById('cert')
.setAttribute(
'href',
'data:application/x-x509-ca-cert;base64,' +
encodeURIComponent(this.stateService.cert),
)
let html = document.getElementById('downloadable').innerHTML
const filename = 'embassy-info.html'
const elem = document.createElement('a')
elem.setAttribute(
'href',
'data:text/plain;charset=utf-8,' + encodeURIComponent(html),
)
elem.setAttribute('download', filename)
elem.style.display = 'none'
document.body.appendChild(elem)
elem.click()
document.body.removeChild(elem)
} }
private async copyToClipboard (str: string): Promise<boolean> { private async copyToClipboard(str: string): Promise<boolean> {
const el = document.createElement('textarea') const el = document.createElement('textarea')
el.value = str el.value = str
el.setAttribute('readonly', '') el.setAttribute('readonly', '')
@@ -62,4 +98,3 @@ export class SuccessPage {
return copy return copy
} }
} }

View File

@@ -1,16 +1,16 @@
export abstract class ApiService { export abstract class ApiService {
// unencrypted // unencrypted
abstract getStatus (): Promise<GetStatusRes> // setup.status abstract getStatus(): Promise<GetStatusRes> // setup.status
abstract getDrives (): Promise<DiskListResponse> // setup.disk.list abstract getDrives(): Promise<DiskListResponse> // setup.disk.list
abstract set02XDrive (logicalname: string): Promise<void> // setup.recovery.v2.set abstract set02XDrive(logicalname: string): Promise<void> // setup.recovery.v2.set
abstract getRecoveryStatus (): Promise<RecoveryStatusRes> // setup.recovery.status abstract getRecoveryStatus(): Promise<RecoveryStatusRes> // setup.recovery.status
// encrypted // encrypted
abstract verifyCifs (cifs: CifsRecoverySource): Promise<EmbassyOSRecoveryInfo> // setup.cifs.verify abstract verifyCifs(cifs: CifsRecoverySource): Promise<EmbassyOSRecoveryInfo> // setup.cifs.verify
abstract verifyProductKey (): Promise<void> // echo - throws error if invalid abstract verifyProductKey(): Promise<void> // echo - throws error if invalid
abstract importDrive (guid: string): Promise<SetupEmbassyRes> // setup.execute abstract importDrive(guid: string): Promise<SetupEmbassyRes> // setup.execute
abstract setupEmbassy (setupInfo: SetupEmbassyReq): Promise<SetupEmbassyRes> // setup.execute abstract setupEmbassy(setupInfo: SetupEmbassyReq): Promise<SetupEmbassyRes> // setup.execute
abstract setupComplete (): Promise<void> // setup.complete abstract setupComplete(): Promise<SetupEmbassyRes> // setup.complete
} }
export interface GetStatusRes { export interface GetStatusRes {
@@ -75,12 +75,12 @@ export interface CifsRecoverySource {
} }
export interface DiskInfo { export interface DiskInfo {
logicalname: string, logicalname: string
vendor: string | null, vendor: string | null
model: string | null, model: string | null
partitions: PartitionInfo[], partitions: PartitionInfo[]
capacity: number, capacity: number
guid: string | null, // cant back up if guid exists guid: string | null // cant back up if guid exists
} }
export interface RecoveryStatusRes { export interface RecoveryStatusRes {
@@ -90,9 +90,9 @@ export interface RecoveryStatusRes {
} }
export interface PartitionInfo { export interface PartitionInfo {
logicalname: string, logicalname: string
label: string | null, label: string | null
capacity: number, capacity: number
used: number | null, used: number | null
'embassy-os': EmbassyOSRecoveryInfo | null, 'embassy-os': EmbassyOSRecoveryInfo | null
} }

View File

@@ -1,49 +1,71 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { ApiService, CifsRecoverySource, DiskInfo, DiskListResponse, DiskRecoverySource, EmbassyOSRecoveryInfo, GetStatusRes, RecoveryStatusRes, SetupEmbassyReq, SetupEmbassyRes } from './api.service' import {
ApiService,
CifsRecoverySource,
DiskInfo,
DiskListResponse,
DiskRecoverySource,
EmbassyOSRecoveryInfo,
GetStatusRes,
RecoveryStatusRes,
SetupEmbassyReq,
SetupEmbassyRes,
} from './api.service'
import { HttpService } from './http.service' import { HttpService } from './http.service'
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class LiveApiService extends ApiService { export class LiveApiService extends ApiService {
constructor(private readonly http: HttpService) {
constructor ( super()
private readonly http: HttpService, }
) { super() }
// ** UNENCRYPTED ** // ** UNENCRYPTED **
async getStatus () { async getStatus() {
return this.http.rpcRequest<GetStatusRes>({ return this.http.rpcRequest<GetStatusRes>(
method: 'setup.status', {
params: { }, method: 'setup.status',
}, false) params: {},
},
false,
)
} }
async getDrives () { async getDrives() {
return this.http.rpcRequest<DiskListResponse>({ return this.http.rpcRequest<DiskListResponse>(
method: 'setup.disk.list', {
params: { }, method: 'setup.disk.list',
}, false) params: {},
},
false,
)
} }
async set02XDrive (logicalname) { async set02XDrive(logicalname) {
return this.http.rpcRequest<void>({ return this.http.rpcRequest<void>(
method: 'setup.recovery.v2.set', {
params: { logicalname }, method: 'setup.recovery.v2.set',
}, false) params: { logicalname },
},
false,
)
} }
async getRecoveryStatus () { async getRecoveryStatus() {
return this.http.rpcRequest<RecoveryStatusRes>({ return this.http.rpcRequest<RecoveryStatusRes>(
method: 'setup.recovery.status', {
params: { }, method: 'setup.recovery.status',
}, false) params: {},
},
false,
)
} }
// ** ENCRYPTED ** // ** ENCRYPTED **
async verifyCifs (source: CifsRecoverySource) { async verifyCifs(source: CifsRecoverySource) {
source.path = source.path.replace('/\\/g', '/') source.path = source.path.replace('/\\/g', '/')
return this.http.rpcRequest<EmbassyOSRecoveryInfo>({ return this.http.rpcRequest<EmbassyOSRecoveryInfo>({
method: 'setup.cifs.verify', method: 'setup.cifs.verify',
@@ -51,14 +73,14 @@ export class LiveApiService extends ApiService {
}) })
} }
async verifyProductKey () { async verifyProductKey() {
return this.http.rpcRequest<void>({ return this.http.rpcRequest<void>({
method: 'echo', method: 'echo',
params: { 'message': 'hello' }, params: { message: 'hello' },
}) })
} }
async importDrive (guid: string) { async importDrive(guid: string) {
const res = await this.http.rpcRequest<SetupEmbassyRes>({ const res = await this.http.rpcRequest<SetupEmbassyRes>({
method: 'setup.attach', method: 'setup.attach',
params: { guid }, params: { guid },
@@ -70,9 +92,11 @@ export class LiveApiService extends ApiService {
} }
} }
async setupEmbassy (setupInfo: SetupEmbassyReq) { async setupEmbassy(setupInfo: SetupEmbassyReq) {
if (isCifsSource(setupInfo['recovery-source'])) { if (isCifsSource(setupInfo['recovery-source'])) {
setupInfo['recovery-source'].path = setupInfo['recovery-source'].path.replace('/\\/g', '/') setupInfo['recovery-source'].path = setupInfo[
'recovery-source'
].path.replace('/\\/g', '/')
} }
const res = await this.http.rpcRequest<SetupEmbassyRes>({ const res = await this.http.rpcRequest<SetupEmbassyRes>({
@@ -86,14 +110,16 @@ export class LiveApiService extends ApiService {
} }
} }
async setupComplete () { async setupComplete() {
await this.http.rpcRequest<SetupEmbassyRes>({ return this.http.rpcRequest<SetupEmbassyRes>({
method: 'setup.complete', method: 'setup.complete',
params: { }, params: {},
}) })
} }
} }
function isCifsSource (source: CifsRecoverySource | DiskRecoverySource | undefined): source is CifsRecoverySource { function isCifsSource(
source: CifsRecoverySource | DiskRecoverySource | undefined,
): source is CifsRecoverySource {
return !!(source as CifsRecoverySource)?.hostname return !!(source as CifsRecoverySource)?.hostname
} }

View File

@@ -96,6 +96,7 @@ export class MockApiService extends ApiService {
async setupComplete() { async setupComplete() {
await pauseFor(1000) await pauseFor(1000)
return setupRes
} }
} }

View File

@@ -91,4 +91,11 @@ export class StateService {
this.lanAddress = ret['lan-address'] this.lanAddress = ret['lan-address']
this.cert = ret['root-ca'] this.cert = ret['root-ca']
} }
async completeEmbassy(): Promise<void> {
const ret = await this.apiService.setupComplete()
this.torAddress = ret['tor-address']
this.lanAddress = ret['lan-address']
this.cert = ret['root-ca']
}
} }

View File

@@ -26,9 +26,6 @@ export class MarkdownPage {
if (!this.content) { if (!this.content) {
this.content = await this.embassyApi.getStatic(this.contentUrl) this.content = await this.embassyApi.getStatic(this.contentUrl)
} }
} catch (e) {
this.loadingError = getErrorMessage(e)
} finally {
this.loading = false this.loading = false
await pauseFor(50) await pauseFor(50)
const links = document.links const links = document.links
@@ -39,6 +36,9 @@ export class MarkdownPage {
links[i].className += ' externalLink' links[i].className += ' externalLink'
} }
} }
} catch (e) {
this.loadingError = getErrorMessage(e)
this.loading = false
} }
} }

View File

@@ -110,7 +110,7 @@ export class AppShowStatusComponent {
const { id, title, version } = this.pkg.manifest const { id, title, version } = this.pkg.manifest
const hasDependents = !!Object.keys( const hasDependents = !!Object.keys(
this.pkg.installed['current-dependents'], this.pkg.installed['current-dependents'],
).filter(depId => depId !== this.pkg.manifest.id).length ).filter(depId => depId !== id).length
if (!hasDependents) { if (!hasDependents) {
const loader = await this.loadingCtrl.create({ const loader = await this.loadingCtrl.create({