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 rpc_toolkit::yajrc::RpcError;
use rpc_toolkit::Context;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use sqlx::sqlite::SqliteConnectOptions;
use sqlx::SqlitePool;
use tokio::fs::File;
@@ -25,6 +25,14 @@ use crate::util::io::from_toml_async_reader;
use crate::util::AsyncFileExt;
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)]
#[serde(rename_all = "kebab-case")]
pub struct SetupContextConfig {
@@ -62,7 +70,7 @@ pub struct SetupContextSeed {
pub selected_v2_drive: RwLock<Option<PathBuf>>,
pub cached_product_key: RwLock<Option<Arc<String>>>,
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)]
@@ -81,7 +89,7 @@ impl SetupContext {
selected_v2_drive: RwLock::new(None),
cached_product_key: RwLock::new(None),
recovery_status: RwLock::new(None),
disk_guid: RwLock::new(None),
setup_result: RwLock::new(None),
})))
}
#[instrument(skip(self))]

View File

@@ -25,6 +25,7 @@ use tracing::instrument;
use crate::backup::restore::recover_full_embassy;
use crate::backup::target::BackupTargetFS;
use crate::context::rpc::RpcContextConfig;
use crate::context::setup::SetupResult;
use crate::context::SetupContext;
use crate::db::model::RecoveredPackageInfo;
use crate::disk::main::DEFAULT_PASSWORD;
@@ -112,18 +113,19 @@ pub async fn attach(
&*ctx.product_key().await?,
)
.await?;
*ctx.disk_guid.write().await = Some(guid.clone());
let secrets = ctx.secret_store().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?;
Ok(SetupResult {
let setup_result = SetupResult {
tor_address: format!("http://{}", tor_key.public().get_onion_address()),
lan_address: format!(
"https://embassy-{}.local",
crate::hostname::derive_id(&*ctx.product_key().await?)
),
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))]
@@ -191,14 +193,6 @@ pub async fn verify_cifs(
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)]
pub async fn execute(
#[context] ctx: SetupContext,
@@ -240,9 +234,9 @@ pub async fn execute(
#[instrument(skip(ctx))]
#[command(rpc_only)]
pub async fn complete(#[context] ctx: SetupContext) -> Result<(), Error> {
let guid = if let Some(guid) = &*ctx.disk_guid.read().await {
guid.clone()
pub async fn complete(#[context] ctx: SetupContext) -> Result<SetupResult, Error> {
let (guid, setup_result) = if let Some((guid, setup_result)) = &*ctx.setup_result.read().await {
(guid.clone(), setup_result.clone())
} else {
return Err(Error::new(
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.sync_all().await?;
ctx.shutdown.send(()).expect("failed to shutdown");
Ok(())
Ok(setup_result)
}
#[instrument(skip(ctx, embassy_password, recovery_password))]
@@ -323,10 +317,21 @@ pub async fn execute_inner(
&ctx.product_key().await?,
)
.await?;
let res = (tor_addr, root_ca.clone());
tokio::spawn(async move {
if let Err(e) = recover_fut
.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 {
recovery_status.complete = true;
}
@@ -342,16 +347,26 @@ pub async fn execute_inner(
tracing::info!("Recovery Complete!");
}
});
(tor_addr, root_ca)
res
} else {
let res = fresh_setup(&ctx, &embassy_password).await?;
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?;
*ctx.disk_guid.write().await = Some(guid);
res
*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()?)?,
},
));
(tor_addr, root_ca)
};
Ok(res)

View File

@@ -4,33 +4,41 @@ import { NavGuard, RecoveryNavGuard } from './guards/nav-guard'
const routes: Routes = [
{ path: '', redirectTo: '/product-key', pathMatch: 'full' },
{
path: 'init',
loadChildren: () => import('./pages/init/init.module').then( m => m.InitPageModule),
canActivate: [NavGuard],
},
{
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',
loadChildren: () => import('./pages/home/home.module').then( m => m.HomePageModule),
loadChildren: () =>
import('./pages/home/home.module').then(m => m.HomePageModule),
canActivate: [NavGuard],
},
{
path: 'recover',
loadChildren: () => import('./pages/recover/recover.module').then( m => m.RecoverPageModule),
loadChildren: () =>
import('./pages/recover/recover.module').then(m => m.RecoverPageModule),
canActivate: [RecoveryNavGuard],
},
{
path: 'embassy',
loadChildren: () => import('./pages/embassy/embassy.module').then( m => m.EmbassyPageModule),
loadChildren: () =>
import('./pages/embassy/embassy.module').then(m => m.EmbassyPageModule),
canActivate: [NavGuard],
},
{
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],
},
]
@@ -46,4 +54,4 @@ const routes: Routes = [
],
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 { GlobalErrorHandler } from './services/global-error-handler.service'
import { SuccessPageModule } from './pages/success/success.module'
import { InitPageModule } from './pages/init/init.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'
@@ -42,7 +41,6 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig
ProdKeyModalModule,
ProductKeyPageModule,
RecoverPageModule,
InitPageModule,
],
providers: [
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },

View File

@@ -129,7 +129,7 @@ export class EmbassyPage {
private async setupEmbassy(drive: DiskInfo, password: string): Promise<void> {
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()
@@ -139,7 +139,7 @@ export class EmbassyPage {
if (!!this.stateService.recoverySource) {
await this.navCtrl.navigateForward(`/loading`)
} else {
await this.navCtrl.navigateForward(`/init`)
await this.navCtrl.navigateForward(`/success`)
}
} catch (e) {
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'],
})
export class LoadingPage {
constructor (
constructor(
public stateService: StateService,
private navCtrl: NavController,
) { }
) {}
ngOnInit () {
ngOnInit() {
this.stateService.pollDataTransferProgress()
const progSub = this.stateService.dataCompletionSubject.subscribe(async complete => {
if (complete) {
progSub.unsubscribe()
await this.navCtrl.navigateForward(`/init`)
}
})
const progSub = this.stateService.dataCompletionSubject.subscribe(
async complete => {
if (complete) {
progSub.unsubscribe()
await this.navCtrl.navigateForward(`/success`)
}
},
)
}
}

View File

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

View File

@@ -1,11 +1,11 @@
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { InitPage } from './init.page'
import { SuccessPage } from './success.page'
const routes: Routes = [
{
path: '',
component: InitPage,
component: SuccessPage,
},
]
@@ -13,4 +13,4 @@ const routes: Routes = [
imports: [RouterModule.forChild(routes)],
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 { SuccessPage } from './success.page'
import { PasswordPageModule } from '../../modals/password/password.module'
import { SuccessPageRoutingModule } from './success-routing.module'
@NgModule({
imports: [
@@ -11,8 +12,9 @@ import { PasswordPageModule } from '../../modals/password/password.module'
FormsModule,
IonicModule,
PasswordPageModule,
SuccessPageRoutingModule,
],
declarations: [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">
<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>
<div
[ngStyle]="{
'overflow' : 'hidden',
'max-height': torOpen ? '500px' : '0px',
'transition': 'max-height 0.4s ease-out'
}"
>
<div class="ion-padding ion-text-start">
<p>
To use your Embassy over Tor, visit its unique Tor address
from any Tor-enabled browser. 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>
<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>
<div
[ngStyle]="{
'overflow' : 'hidden',
'max-height': torOpen ? '500px' : '0px',
'transition': 'max-height 0.4s ease-out'
}"
>
<div class="ion-padding ion-text-start">
<p>
To use your Embassy over Tor, visit its unique Tor address from any Tor-enabled browser.
For a list of recommended browsers, click <a href="https://start9.com/latest/user-manual/connecting" target="_blank" rel="noreferrer"><b>here</b></a>.
</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 (click)="toggleLan()" class="toggle-label">
<h2>LAN Instructions (Slightly Advanced):</h2>
<ion-icon
name="chevron-down-outline"
[ngStyle]="{
'transform': lanOpen ? 'rotate(-90deg)' : 'rotate(0deg)',
'transition': 'transform 0.4s ease-out'
}"
></ion-icon>
</div>
<!-- LAN Instructions -->
<div (click)="toggleLan()" class="toggle-label">
<h2>LAN Instructions (Slightly Advanced):</h2>
<ion-icon
name="chevron-down-outline"
[ngStyle]="{
'transform': lanOpen ? 'rotate(-90deg)' : 'rotate(0deg)',
'transition': 'transform 0.4s ease-out'
}"
></ion-icon>
</div>
<div
[ngStyle]="{
'overflow' : 'hidden',
'max-height': lanOpen ? '500px' : '0px',
'transition': 'max-height 0.4s ease-out'
}"
>
<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>
<div
[ngStyle]="{
'overflow' : 'hidden',
'max-height': lanOpen ? '500px' : '0px',
'transition': 'max-height 0.4s ease-out'
}"
>
<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>
<b
>Please note, once setup is complete, the embassy.local
address will no longer connect to your Embassy.</b
>
</p>
<p>
<b>Please note, once setup is complete, the embassy.local address will no longer connect to your Embassy.</b>
</p>
<ion-button
style="margin-top: 24px; margin-bottom: 24px"
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()">
Download Root CA
<ion-icon slot="end" name="download-outline"></ion-icon>
</ion-button>
<p>LAN Address</p>
<ion-item lines="none" color="dark">
<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>
<p>LAN Address</p>
<ion-item lines="none" color="dark">
<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>
<!-- cert elem -->
<a hidden id="install-cert" download="embassy.crt"></a>
<!-- download elem -->
<div hidden id="downloadable">
<div style="padding: 0 24px; font-family: Courier">
<h1>Embassy Info</h1>
<!-- cert elem -->
<a hidden id="install-cert" download="embassy.crt"></a>
<section style="padding: 16px; border: solid 1px">
<h2>Tor Info</h2>
<p>
To use your Embassy over Tor, visit its unique Tor address from
any Tor-enabled browser.
</p>
<p>
For 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 -->
<div hidden id="downloadable">
<div style="padding: 0 24px; font-family: Courier;">
<h1>Embassy Info</h1>
<section style="padding: 16px; border: solid 1px; border-top: none">
<h2>LAN Info</h2>
<p>To use your Embassy locally, you must:</p>
<ol>
<li>
Currently be connected to the same Local Area Network (LAN) as
your Embassy.
</li>
<li>Download your Embassy's Root Certificate Authority.</li>
<li>
Trust your Embassy's Root CA on <i>both</i> your
computer/phone and in your browser settings.
</li>
</ol>
<p>
For step-by-step instructions, click
<a
href="https://start9.com/latest/user-manual/connecting/connecting-lan"
target="_blank"
rel="noreferrer"
><b>here</b></a
>.
</p>
<section style="padding: 16px; border: solid 1px;">
<h2>Tor Info</h2>
<p>
To use your Embassy over Tor, visit its unique Tor address from any Tor-enabled browser.
</p>
<p>
For a list of recommended browsers, click <a href="https://start9.com/latest/user-manual/connecting" target="_blank" rel="noreferrer"><b>here</b></a>.
</p>
<p><b>Tor Address: </b><code id="tor-addr"></code></p>
</section>
<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>
<section style="padding: 16px; border: solid 1px; border-top: none;">
<h2>LAN Info</h2>
<p>To use your Embassy locally, you must:</p>
<ol>
<li>Currently be connected to the same Local Area Network (LAN) as your Embassy.</li>
<li>Download your Embassy's Root Certificate Authority.</li>
<li>Trust your Embassy's Root CA on <i>both</i> your computer/phone and in your browser settings.</li>
</ol>
<p>
For step-by-step instructions, click
<a href="https://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>
<p><b>LAN Address: </b><code id="lan-addr"></code></p>
</section>
</div>
</div>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>

View File

@@ -1,5 +1,6 @@
import { Component, EventEmitter, Output } from '@angular/core'
import { ToastController } from '@ionic/angular'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { StateService } from 'src/app/services/state.service'
@Component({
@@ -12,16 +13,29 @@ export class SuccessPage {
torOpen = true
lanOpen = false
constructor (
constructor(
private readonly toastCtrl: ToastController,
private readonly errCtrl: ErrorToastService,
public readonly stateService: StateService,
) { }
) {}
ngAfterViewInit () {
document.getElementById('install-cert').setAttribute('href', 'data:application/x-x509-ca-cert;base64,' + encodeURIComponent(this.stateService.cert))
async ngAfterViewInit() {
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 message = success ? 'copied to clipboard!' : 'failed to copy'
@@ -33,23 +47,45 @@ export class SuccessPage {
await toast.present()
}
toggleTor () {
toggleTor() {
this.torOpen = !this.torOpen
}
toggleLan () {
toggleLan() {
this.lanOpen = !this.lanOpen
}
installCert () {
installCert() {
document.getElementById('install-cert').click()
}
download () {
this.onDownload.emit()
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)
}
private async copyToClipboard (str: string): Promise<boolean> {
private async copyToClipboard(str: string): Promise<boolean> {
const el = document.createElement('textarea')
el.value = str
el.setAttribute('readonly', '')
@@ -62,4 +98,3 @@ export class SuccessPage {
return copy
}
}

View File

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

View File

@@ -1,49 +1,71 @@
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'
@Injectable({
providedIn: 'root',
})
export class LiveApiService extends ApiService {
constructor (
private readonly http: HttpService,
) { super() }
constructor(private readonly http: HttpService) {
super()
}
// ** UNENCRYPTED **
async getStatus () {
return this.http.rpcRequest<GetStatusRes>({
method: 'setup.status',
params: { },
}, false)
async getStatus() {
return this.http.rpcRequest<GetStatusRes>(
{
method: 'setup.status',
params: {},
},
false,
)
}
async getDrives () {
return this.http.rpcRequest<DiskListResponse>({
method: 'setup.disk.list',
params: { },
}, false)
async getDrives() {
return this.http.rpcRequest<DiskListResponse>(
{
method: 'setup.disk.list',
params: {},
},
false,
)
}
async set02XDrive (logicalname) {
return this.http.rpcRequest<void>({
method: 'setup.recovery.v2.set',
params: { logicalname },
}, false)
async set02XDrive(logicalname) {
return this.http.rpcRequest<void>(
{
method: 'setup.recovery.v2.set',
params: { logicalname },
},
false,
)
}
async getRecoveryStatus () {
return this.http.rpcRequest<RecoveryStatusRes>({
method: 'setup.recovery.status',
params: { },
}, false)
async getRecoveryStatus() {
return this.http.rpcRequest<RecoveryStatusRes>(
{
method: 'setup.recovery.status',
params: {},
},
false,
)
}
// ** ENCRYPTED **
async verifyCifs (source: CifsRecoverySource) {
async verifyCifs(source: CifsRecoverySource) {
source.path = source.path.replace('/\\/g', '/')
return this.http.rpcRequest<EmbassyOSRecoveryInfo>({
method: 'setup.cifs.verify',
@@ -51,14 +73,14 @@ export class LiveApiService extends ApiService {
})
}
async verifyProductKey () {
async verifyProductKey() {
return this.http.rpcRequest<void>({
method: 'echo',
params: { 'message': 'hello' },
params: { message: 'hello' },
})
}
async importDrive (guid: string) {
async importDrive(guid: string) {
const res = await this.http.rpcRequest<SetupEmbassyRes>({
method: 'setup.attach',
params: { guid },
@@ -70,9 +92,11 @@ export class LiveApiService extends ApiService {
}
}
async setupEmbassy (setupInfo: SetupEmbassyReq) {
async setupEmbassy(setupInfo: SetupEmbassyReq) {
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>({
@@ -86,14 +110,16 @@ export class LiveApiService extends ApiService {
}
}
async setupComplete () {
await this.http.rpcRequest<SetupEmbassyRes>({
async setupComplete() {
return this.http.rpcRequest<SetupEmbassyRes>({
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
}

View File

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

View File

@@ -91,4 +91,11 @@ export class StateService {
this.lanAddress = ret['lan-address']
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) {
this.content = await this.embassyApi.getStatic(this.contentUrl)
}
} catch (e) {
this.loadingError = getErrorMessage(e)
} finally {
this.loading = false
await pauseFor(50)
const links = document.links
@@ -39,6 +36,9 @@ export class MarkdownPage {
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 hasDependents = !!Object.keys(
this.pkg.installed['current-dependents'],
).filter(depId => depId !== this.pkg.manifest.id).length
).filter(depId => depId !== id).length
if (!hasDependents) {
const loader = await this.loadingCtrl.create({