WIP: IP, pubkey, system time, system uptime, ca fingerprint (#2091)

* closes #923, #2063, #2012, #1153

* add ca fingerprint

* add `server.time`

* add `ip-info` to `server-info`

* add ssh pubkey

* support multiple IPs

* rename key

* add `ca-fingerprint` and `system-start-time`

* fix off-by-one

* update compat cargo lock

Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
Matt Hill
2023-01-16 14:54:35 -07:00
committed by Aiden McClelland
parent 673e5af030
commit 06cf83b901
40 changed files with 2244 additions and 925 deletions

244
backend/Cargo.lock generated
View File

@@ -212,6 +212,12 @@ dependencies = [
"rustc-demangle",
]
[[package]]
name = "base16ct"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce"
[[package]]
name = "base32"
version = "0.4.0"
@@ -636,9 +642,9 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "cookie"
version = "0.16.1"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "344adc371239ef32293cb1c4fe519592fcf21206c79c02854320afcdf3ab4917"
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
dependencies = [
"percent-encoding",
"time 0.3.17",
@@ -661,6 +667,22 @@ dependencies = [
"url",
]
[[package]]
name = "cookie_store"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bca9b3c618262fc0c85ecbc814c144e04be9c6eec08b315e7cd1cfbe0bb6ca84"
dependencies = [
"cookie",
"idna 0.3.0",
"log",
"publicsuffix",
"serde",
"serde_json",
"time 0.3.17",
"url",
]
[[package]]
name = "core-foundation"
version = "0.9.3"
@@ -735,6 +757,18 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "crypto-bigint"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef"
dependencies = [
"generic-array",
"rand_core 0.6.4",
"subtle",
"zeroize",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
@@ -1088,6 +1122,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c"
dependencies = [
"block-buffer 0.10.3",
"const-oid",
"crypto-common",
"subtle",
]
@@ -1160,6 +1195,18 @@ dependencies = [
"text_lines",
]
[[package]]
name = "ecdsa"
version = "0.14.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c"
dependencies = [
"der",
"elliptic-curve",
"rfc6979",
"signature",
]
[[package]]
name = "ed25519"
version = "1.5.2"
@@ -1195,6 +1242,25 @@ dependencies = [
"serde",
]
[[package]]
name = "elliptic-curve"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3"
dependencies = [
"base16ct",
"crypto-bigint",
"der",
"digest 0.10.5",
"ff",
"generic-array",
"group",
"rand_core 0.6.4",
"sec1",
"subtle",
"zeroize",
]
[[package]]
name = "embassy-os"
version = "0.3.3"
@@ -1213,7 +1279,8 @@ dependencies = [
"ciborium",
"clap 3.2.23",
"color-eyre",
"cookie_store",
"cookie",
"cookie_store 0.19.0",
"current_platform",
"digest 0.10.5",
"digest 0.9.0",
@@ -1273,6 +1340,7 @@ dependencies = [
"sha2 0.9.9",
"simple-logging",
"sqlx",
"ssh-key",
"stderrlog",
"tar",
"thiserror",
@@ -1462,6 +1530,16 @@ dependencies = [
"nix 0.24.2",
]
[[package]]
name = "ff"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160"
dependencies = [
"rand_core 0.6.4",
"subtle",
]
[[package]]
name = "filetime"
version = "0.2.18"
@@ -1723,6 +1801,17 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
[[package]]
name = "group"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7"
dependencies = [
"ff",
"rand_core 0.6.4",
"subtle",
]
[[package]]
name = "h2"
version = "0.3.15"
@@ -2296,6 +2385,9 @@ name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
dependencies = [
"spin",
]
[[package]]
name = "lazycell"
@@ -2392,6 +2484,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "libm"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb"
[[package]]
name = "link-cplusplus"
version = "1.0.7"
@@ -2538,6 +2636,7 @@ dependencies = [
"serde",
"serde_json",
"sqlx",
"ssh-key",
"thiserror",
"tokio",
"torut",
@@ -2672,6 +2771,23 @@ dependencies = [
"serde",
]
[[package]]
name = "num-bigint-dig"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2399c9463abc5f909349d8aa9ba080e0b88b3ce2885389b60b993f39b1a56905"
dependencies = [
"byteorder",
"lazy_static",
"libm",
"num-integer",
"num-iter",
"num-traits",
"rand 0.8.5",
"smallvec",
"zeroize",
]
[[package]]
name = "num-complex"
version = "0.4.2"
@@ -2721,6 +2837,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
"libm",
]
[[package]]
@@ -2861,6 +2978,28 @@ version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
[[package]]
name = "p256"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594"
dependencies = [
"ecdsa",
"elliptic-curve",
"sha2 0.10.6",
]
[[package]]
name = "p384"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc8c5bf642dde52bb9e87c0ecd8ca5a76faac2eeed98dedb7c717997e1080aa"
dependencies = [
"ecdsa",
"elliptic-curve",
"sha2 0.10.6",
]
[[package]]
name = "parking_lot"
version = "0.11.2"
@@ -3092,6 +3231,18 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkcs1"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719"
dependencies = [
"der",
"pkcs8",
"spki",
"zeroize",
]
[[package]]
name = "pkcs8"
version = "0.9.0"
@@ -3447,7 +3598,7 @@ dependencies = [
"base64 0.13.1",
"bytes",
"cookie",
"cookie_store",
"cookie_store 0.16.1",
"encoding_rs",
"futures-core",
"futures-util",
@@ -3482,17 +3633,28 @@ dependencies = [
[[package]]
name = "reqwest_cookie_store"
version = "0.4.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0568e27f107b933735a07b3f8cb985ecfe3d3ce2f2225f82f10b3750f5981263"
checksum = "06b407c05de7a0f7e4cc2a56af5e9bd6468e509124e81078ce1f8bc2ed3536bf"
dependencies = [
"bytes",
"cookie",
"cookie_store",
"cookie_store 0.19.0",
"reqwest",
"url",
]
[[package]]
name = "rfc6979"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb"
dependencies = [
"crypto-bigint",
"hmac 0.12.1",
"zeroize",
]
[[package]]
name = "ring"
version = "0.16.20"
@@ -3562,6 +3724,27 @@ dependencies = [
"syn 1.0.103",
]
[[package]]
name = "rsa"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "094052d5470cbcef561cb848a7209968c9f12dfa6d668f4bca048ac5de51099c"
dependencies = [
"byteorder",
"digest 0.10.5",
"num-bigint-dig",
"num-integer",
"num-iter",
"num-traits",
"pkcs1",
"pkcs8",
"rand_core 0.6.4",
"signature",
"smallvec",
"subtle",
"zeroize",
]
[[package]]
name = "rust-argon2"
version = "1.0.0"
@@ -3701,6 +3884,20 @@ dependencies = [
"untrusted",
]
[[package]]
name = "sec1"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928"
dependencies = [
"base16ct",
"der",
"generic-array",
"pkcs8",
"subtle",
"zeroize",
]
[[package]]
name = "security-framework"
version = "2.7.0"
@@ -3987,6 +4184,10 @@ name = "signature"
version = "1.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
dependencies = [
"digest 0.10.5",
"rand_core 0.6.4",
]
[[package]]
name = "simple-logging"
@@ -4180,6 +4381,35 @@ dependencies = [
"tokio-rustls",
]
[[package]]
name = "ssh-encoding"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19cfdc32e0199062113edf41f344fbf784b8205a94600233c84eb838f45191e1"
dependencies = [
"base64ct",
"pem-rfc7468",
"sha2 0.10.6",
]
[[package]]
name = "ssh-key"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "288d8f5562af5a3be4bda308dd374b2c807b940ac370b5efa1c99311da91d9a1"
dependencies = [
"ed25519-dalek",
"p256",
"p384",
"rand_core 0.6.4",
"rsa",
"sec1",
"sha2 0.10.6",
"signature",
"ssh-encoding",
"zeroize",
]
[[package]]
name = "static_assertions"
version = "1.1.0"

View File

@@ -62,7 +62,8 @@ bytes = "1"
chrono = { version = "0.4.19", features = ["serde"] }
clap = "3.2.8"
color-eyre = "0.6.1"
cookie_store = "0.16.1"
cookie = "0.16.2"
cookie_store = "0.19.0"
current_platform = "0.2.0"
digest = "0.10.3"
digest-old = { package = "digest", version = "0.9.0" }
@@ -113,7 +114,7 @@ rand = { version = "0.8.5", features = ["std"] }
rand-old = { package = "rand", version = "0.7.3" }
regex = "1.6.0"
reqwest = { version = "0.11.11", features = ["stream", "json", "socks"] }
reqwest_cookie_store = "0.4.0"
reqwest_cookie_store = "0.5.0"
rpassword = "7.0.0"
rpc-toolkit = "0.2.2"
rust-argon2 = "1.0.0"
@@ -133,6 +134,7 @@ sqlx = { version = "0.6.0", features = [
"runtime-tokio-rustls",
"postgres",
] }
ssh-key = { version = "0.5.1", features = ["ed25519"] }
stderrlog = "0.5.3"
tar = "0.4.38"
thiserror = "1.0.31"

View File

@@ -0,0 +1,21 @@
-- Add migration script here
CREATE EXTENSION pgcrypto;
ALTER TABLE
account
ADD
COLUMN ssh_key BYTEA CHECK (length(ssh_key) = 32);
UPDATE
account
SET
ssh_key = gen_random_bytes(32)
WHERE
id = 0;
ALTER TABLE
account
ALTER COLUMN
ssh_key
SET
NOT NULL;

View File

@@ -102,6 +102,21 @@
},
"query": "SELECT hostname, path, username, password FROM cifs_shares WHERE id = $1"
},
"2f615764532e975c964f1d0e063a02110d781644b0eaae1ff85a7d6ed903bfe5": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Int4",
"Text",
"Bytea",
"Bytea"
]
}
},
"query": "INSERT INTO account (id, password, tor_key, ssh_key) VALUES ($1, $2, $3, $4) ON CONFLICT (id) DO UPDATE SET password = $2, tor_key = $3, ssh_key = $4"
},
"3502e58f2ab48fb4566d21c920c096f81acfa3ff0d02f970626a4dcd67bac71d": {
"describe": {
"columns": [
@@ -256,6 +271,24 @@
},
"query": "SELECT setval('certificates_id_seq', GREATEST(MAX(id) + 1, nextval('certificates_id_seq') - 1)) FROM certificates"
},
"5c0ea94081695dba827e525ecc0c555757b43ea513c2c93f9c7f7f8c174d36bf": {
"describe": {
"columns": [
{
"name": "ssh_key",
"ordinal": 0,
"type_info": "Bytea"
}
],
"nullable": [
false
],
"parameters": {
"Left": []
}
},
"query": "SELECT ssh_key FROM account"
},
"629be61c3c341c131ddbbff0293a83dbc6afd07cae69d246987f62cf0cc35c2a": {
"describe": {
"columns": [
@@ -536,33 +569,6 @@
},
"query": "DELETE FROM cifs_shares WHERE id = $1"
},
"a645d636be810a4ba61dcadf22e90de6e9baf3614aa9e97f053ff480cb3118a2": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Bytea"
]
}
},
"query": "INSERT INTO tor (package, interface, key) VALUES ($1, 'main', $2) ON CONFLICT (package, interface) DO UPDATE SET key = $2"
},
"a6645d91f76b3d5fac2191ea3bec5dab7d7d124715fde02e6a816fa5dbc7acf2": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Int4",
"Text",
"Bytea"
]
}
},
"query": "INSERT INTO account (id, password, tor_key) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET password = $2, tor_key = $3"
},
"a6b0c8909a3a5d6d9156aebfb359424e6b5a1d1402e028219e21726f1ebd282e": {
"describe": {
"columns": [

View File

@@ -8,9 +8,11 @@ use helpers::AtomicFile;
use openssl::pkey::{PKey, Private};
use openssl::x509::X509;
use patch_db::{DbHandle, LockType, PatchDbHandle};
use rand::random;
use rpc_toolkit::command;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use ssh_key::private::Ed25519PrivateKey;
use tokio::io::AsyncWriteExt;
use torut::onion::TorSecretKeyV3;
use tracing::instrument;
@@ -35,6 +37,7 @@ use crate::{Error, ErrorKind, ResultExt};
#[derive(Debug)]
pub struct OsBackup {
pub tor_key: TorSecretKeyV3,
pub ssh_key: Ed25519PrivateKey,
pub root_ca_key: PKey<Private>,
pub root_ca_cert: X509,
pub ui: Value,
@@ -48,32 +51,52 @@ impl<'de> Deserialize<'de> for OsBackup {
#[serde(rename = "kebab-case")]
struct OsBackupDe {
tor_key: String,
ssh_key: Option<String>,
root_ca_key: String,
root_ca_cert: String,
ui: Value,
}
let int = OsBackupDe::deserialize(deserializer)?;
let key_vec = base32::decode(base32::Alphabet::RFC4648 { padding: true }, &int.tor_key)
.ok_or_else(|| {
serde::de::Error::invalid_value(
serde::de::Unexpected::Str(&int.tor_key),
&"an RFC4648 encoded string",
)
})?;
if key_vec.len() != 64 {
return Err(serde::de::Error::invalid_value(
serde::de::Unexpected::Str(&int.tor_key),
&"a 64 byte value encoded as an RFC4648 string",
));
fn vec_from_base32<E: serde::de::Error>(base32: &str, len: usize) -> Result<Vec<u8>, E> {
let key_vec = base32::decode(base32::Alphabet::RFC4648 { padding: true }, base32)
.ok_or_else(|| {
serde::de::Error::invalid_value(
serde::de::Unexpected::Str(base32),
&"an RFC4648 encoded string",
)
})?;
if key_vec.len() != 64 {
return Err(serde::de::Error::invalid_value(
serde::de::Unexpected::Str(base32),
&"a 64 byte value encoded as an RFC4648 string",
));
}
Ok(key_vec)
}
let mut key_slice = [0; 64];
key_slice.clone_from_slice(&key_vec);
let int = OsBackupDe::deserialize(deserializer)?;
let tor_key = {
let mut key_slice = [0; 64];
key_slice.clone_from_slice(&vec_from_base32(&int.tor_key, 64)?);
TorSecretKeyV3::from(key_slice)
};
let ssh_key = int
.ssh_key
.as_ref()
.map(|ssh_key| {
let mut key_slice = [0; 32];
key_slice.clone_from_slice(&vec_from_base32(ssh_key, 32)?);
Ok(Ed25519PrivateKey::from_bytes(&key_slice))
})
.transpose()?
.unwrap_or_else(|| Ed25519PrivateKey::from_bytes(&random()));
let root_ca_key = PKey::<Private>::private_key_from_pem(int.root_ca_key.as_bytes())
.map_err(serde::de::Error::custom)?;
let root_ca_cert =
X509::from_pem(int.root_ca_cert.as_bytes()).map_err(serde::de::Error::custom)?;
Ok(OsBackup {
tor_key: TorSecretKeyV3::from(key_slice),
root_ca_key: PKey::<Private>::private_key_from_pem(int.root_ca_key.as_bytes())
.map_err(serde::de::Error::custom)?,
root_ca_cert: X509::from_pem(int.root_ca_cert.as_bytes())
.map_err(serde::de::Error::custom)?,
tor_key,
ssh_key,
root_ca_key,
root_ca_cert,
ui: int.ui,
})
}
@@ -437,6 +460,7 @@ async fn perform_backup<Db: DbHandle>(
.write_all(
&IoFormat::Cbor.to_vec(&OsBackup {
tor_key: ctx.net_controller.tor.embassyd_tor_key().await,
ssh_key: crate::ssh::os_key(&mut ctx.secret_store.acquire().await?).await?,
root_ca_key,
root_ca_cert,
ui: crate::db::DatabaseModel::new()

View File

@@ -198,13 +198,15 @@ pub async fn recover_full_embassy(
&argon2::Config::default(),
)
.with_kind(crate::ErrorKind::PasswordHashGeneration)?;
let key_vec = os_backup.tor_key.as_bytes().to_vec();
let tor_key_bytes = os_backup.tor_key.as_bytes().to_vec();
let ssh_key_bytes = os_backup.ssh_key.to_bytes().to_vec();
let secret_store = ctx.secret_store().await?;
sqlx::query!(
"INSERT INTO account (id, password, tor_key) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET password = $2, tor_key = $3",
"INSERT INTO account (id, password, tor_key, ssh_key) VALUES ($1, $2, $3, $4) ON CONFLICT (id) DO UPDATE SET password = $2, tor_key = $3, ssh_key = $4",
0,
password,
key_vec,
tor_key_bytes,
ssh_key_bytes,
)
.execute(&mut secret_store.acquire().await?)
.await?;

View File

@@ -6,6 +6,7 @@ use std::sync::Arc;
use clap::ArgMatches;
use color_eyre::eyre::eyre;
use cookie::Cookie;
use cookie_store::CookieStore;
use josekit::jwk::Jwk;
use reqwest::Proxy;
@@ -16,6 +17,7 @@ use rpc_toolkit::Context;
use serde::Deserialize;
use tracing::instrument;
use crate::middleware::auth::LOCAL_AUTH_COOKIE_PATH;
use crate::util::config::{load_config_from_paths, local_config_path};
use crate::ResultExt;
@@ -83,7 +85,7 @@ impl CliContext {
} else if let Some(host) = base.host {
host
} else {
format!("http://localhost").parse()?
"http://localhost".parse()?
};
let proxy = if let Some(proxy) = matches.value_of("proxy") {
Some(proxy.parse()?)
@@ -100,9 +102,15 @@ impl CliContext {
.join(".cookies.json")
});
let cookie_store = Arc::new(CookieStoreMutex::new(if cookie_path.exists() {
CookieStore::load_json(BufReader::new(File::open(&cookie_path)?))
let mut store = CookieStore::load_json(BufReader::new(File::open(&cookie_path)?))
.map_err(|e| eyre!("{}", e))
.with_kind(crate::ErrorKind::Deserialization)?
.with_kind(crate::ErrorKind::Deserialization)?;
if let Ok(local) = std::fs::read_to_string(LOCAL_AUTH_COOKIE_PATH) {
store
.insert_raw(&Cookie::new("local", local), &"http://localhost".parse()?)
.with_kind(crate::ErrorKind::Network)?;
}
store
} else {
CookieStore::default()
}));

View File

@@ -7,7 +7,7 @@ use serde::Deserialize;
use tokio::sync::broadcast::Sender;
use tracing::instrument;
use crate::os_install::find_eth_iface;
use crate::net::net_utils::find_eth_iface;
use crate::util::config::load_config_from_paths;
use crate::Error;

View File

@@ -28,7 +28,7 @@ use crate::install::cleanup::{cleanup_failed, uninstall, CleanupFailedReceipts};
use crate::manager::ManagerMap;
use crate::middleware::auth::HashSessionToken;
use crate::net::net_controller::NetController;
use crate::net::tor::os_key;
use crate::net::ssl::SslManager;
use crate::net::wifi::WpaCli;
use crate::notifications::NotificationManager;
use crate::setup::password_hash;
@@ -85,8 +85,14 @@ impl RpcContextConfig {
db.put(
&<JsonPointer>::default(),
&Database::init(
&os_key(&mut secret_store.acquire().await?).await?,
&crate::net::tor::os_key(&mut secret_store.acquire().await?).await?,
password_hash(&mut secret_store.acquire().await?).await?,
&crate::ssh::os_key(&mut secret_store.acquire().await?).await?,
&SslManager::init(secret_store.clone(), &mut db.handle())
.await?
.export_root_ca()
.await?
.1,
),
)
.await?;

View File

@@ -17,7 +17,7 @@ use tracing::instrument;
use crate::db::model::Database;
use crate::disk::OsPartitionInfo;
use crate::init::{init_postgres, pgloader};
use crate::net::tor::os_key;
use crate::net::ssl::SslManager;
use crate::setup::{password_hash, SetupStatus};
use crate::util::config::load_config_from_paths;
use crate::{Error, ResultExt};
@@ -120,8 +120,14 @@ impl SetupContext {
db.put(
&<JsonPointer>::default(),
&Database::init(
&os_key(&mut secret_store.acquire().await?).await?,
&crate::net::tor::os_key(&mut secret_store.acquire().await?).await?,
password_hash(&mut secret_store.acquire().await?).await?,
&crate::ssh::os_key(&mut secret_store.acquire().await?).await?,
&SslManager::init(secret_store.clone(), &mut db.handle())
.await?
.export_root_ca()
.await?
.1,
),
)
.await?;

View File

@@ -132,7 +132,7 @@ pub async fn subscribe(ctx: RpcContext, req: Request<Body>) -> Result<Response<B
let (parts, body) = req.into_parts();
let session = match async {
let token = HashSessionToken::from_request_parts(&parts)?;
let session = HasValidSession::from_session(&token, &ctx).await?;
let session = HasValidSession::from_request_parts(&parts, &ctx).await?;
Ok::<_, Error>((session, token))
}
.await

View File

@@ -1,25 +1,33 @@
use std::collections::{BTreeMap, BTreeSet};
use std::net::{Ipv4Addr, Ipv6Addr};
use std::sync::Arc;
use chrono::{DateTime, Utc};
use emver::VersionRange;
use isocountry::CountryCode;
use itertools::Itertools;
use openssl::hash::MessageDigest;
use openssl::x509::X509;
use patch_db::json_ptr::JsonPointer;
use patch_db::{HasModel, Map, MapModel, OptionModel};
use reqwest::Url;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use ssh_key::private::Ed25519PrivateKey;
use ssh_key::public::Ed25519PublicKey;
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::net::net_utils::{get_iface_ipv4_addr, get_iface_ipv6_addr};
use crate::s9pk::manifest::{Manifest, ManifestModel, PackageId};
use crate::status::health_check::HealthCheckId;
use crate::status::Status;
use crate::util::Version;
use crate::version::{Current, VersionT};
use crate::Error;
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
@@ -31,7 +39,12 @@ pub struct Database {
pub ui: Value,
}
impl Database {
pub fn init(tor_key: &TorSecretKeyV3, password_hash: String) -> Self {
pub fn init(
tor_key: &TorSecretKeyV3,
password_hash: String,
ssh_key: &Ed25519PrivateKey,
cert: &X509,
) -> Self {
let id = generate_id();
let my_hostname = generate_hostname();
let lan_address = my_hostname.lan_address().parse().unwrap();
@@ -48,6 +61,7 @@ impl Database {
tor_address: format!("http://{}", tor_key.public().get_onion_address())
.parse()
.unwrap(),
ip_info: BTreeMap::new(),
status_info: ServerStatus {
backup_progress: None,
updated: false,
@@ -64,6 +78,16 @@ impl Database {
clearnet: Vec::new(),
},
password_hash,
pubkey: ssh_key::PublicKey::from(Ed25519PublicKey::from(ssh_key))
.to_openssh()
.unwrap(),
ca_fingerprint: cert
.digest(MessageDigest::sha256())
.unwrap()
.iter()
.map(|x| format!("{x:X}"))
.join(":"),
system_start_time: Utc::now().to_rfc3339(),
},
package_data: AllPackageData::default(),
ui: serde_json::from_str(include_str!("../../../frontend/patchdb-ui-seed.json"))
@@ -90,12 +114,32 @@ pub struct ServerInfo {
pub lan_address: Url,
pub tor_address: Url,
#[model]
pub ip_info: BTreeMap<String, IpInfo>,
#[model]
#[serde(default)]
pub status_info: ServerStatus,
pub wifi: WifiInfo,
pub unread_notification_count: u64,
pub connection_addresses: ConnectionAddresses,
pub password_hash: String,
pub pubkey: String,
pub ca_fingerprint: String,
pub system_start_time: String,
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
pub struct IpInfo {
ipv4: Option<Ipv4Addr>,
ipv6: Option<Ipv6Addr>,
}
impl IpInfo {
pub async fn for_interface(iface: &str) -> Result<Self, Error> {
Ok(Self {
ipv4: get_iface_ipv4_addr(iface).await?,
ipv6: get_iface_ipv6_addr(iface).await?,
})
}
}
#[derive(Debug, Default, Deserialize, Serialize, HasModel)]

View File

@@ -1,18 +1,24 @@
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use std::fs::Permissions;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::process::Stdio;
use std::time::Duration;
use color_eyre::eyre::eyre;
use helpers::NonDetachingJoinHandle;
use models::ResultExt;
use patch_db::{DbHandle, LockReceipt, LockType};
use rand::random;
use sqlx::{Pool, Postgres};
use tokio::process::Command;
use crate::context::rpc::RpcContextConfig;
use crate::db::model::ServerStatus;
use crate::db::model::{IpInfo, ServerStatus};
use crate::install::PKG_ARCHIVE_DIR;
use crate::middleware::auth::LOCAL_AUTH_COOKIE_PATH;
use crate::sound::BEP;
use crate::system::time;
use crate::util::Invoke;
use crate::Error;
@@ -37,6 +43,8 @@ pub struct InitReceipts {
pub version_range: LockReceipt<emver::VersionRange, ()>,
pub last_wifi_region: LockReceipt<Option<isocountry::CountryCode>, ()>,
pub status_info: LockReceipt<ServerStatus, ()>,
pub ip_info: LockReceipt<BTreeMap<String, IpInfo>, ()>,
pub system_start_time: LockReceipt<String, ()>,
}
impl InitReceipts {
pub async fn new(db: &mut impl DbHandle) -> Result<Self, Error> {
@@ -57,19 +65,31 @@ impl InitReceipts {
.last_wifi_region()
.make_locker(LockType::Write)
.add_to_keys(&mut locks);
let ip_info = crate::db::DatabaseModel::new()
.server_info()
.ip_info()
.make_locker(LockType::Write)
.add_to_keys(&mut locks);
let status_info = crate::db::DatabaseModel::new()
.server_info()
.status_info()
.into_model()
.make_locker(LockType::Write)
.add_to_keys(&mut locks);
let system_start_time = crate::db::DatabaseModel::new()
.server_info()
.system_start_time()
.make_locker(LockType::Write)
.add_to_keys(&mut locks);
let skeleton_key = db.lock_all(locks).await?;
Ok(Self {
server_version: server_version.verify(&skeleton_key)?,
version_range: version_range.verify(&skeleton_key)?,
ip_info: ip_info.verify(&skeleton_key)?,
status_info: status_info.verify(&skeleton_key)?,
last_wifi_region: last_wifi_region.verify(&skeleton_key)?,
system_start_time: system_start_time.verify(&skeleton_key)?,
})
}
}
@@ -196,6 +216,24 @@ pub struct InitResult {
}
pub async fn init(cfg: &RpcContextConfig) -> Result<InitResult, Error> {
tokio::fs::create_dir_all("/run/embassy")
.await
.with_ctx(|_| (crate::ErrorKind::Filesystem, "mkdir -p /run/embassy"))?;
if tokio::fs::metadata(LOCAL_AUTH_COOKIE_PATH).await.is_err() {
tokio::fs::write(
LOCAL_AUTH_COOKIE_PATH,
base64::encode(random::<[u8; 32]>()).as_bytes(),
)
.await
.with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
format!("write {}", LOCAL_AUTH_COOKIE_PATH),
)
})?;
tokio::fs::set_permissions(LOCAL_AUTH_COOKIE_PATH, Permissions::from_mode(046)).await?;
}
let secret_store = cfg.secret_store().await?;
tracing::info!("Opened Postgres");
@@ -334,6 +372,10 @@ pub async fn init(cfg: &RpcContextConfig) -> Result<InitResult, Error> {
.await?;
tracing::info!("Enabled Docker QEMU Emulation");
receipts
.ip_info
.set(&mut handle, crate::net::dhcp::init_ips().await?)
.await?;
receipts
.status_info
.set(
@@ -345,20 +387,10 @@ pub async fn init(cfg: &RpcContextConfig) -> Result<InitResult, Error> {
},
)
.await?;
let mut warn_time_not_synced = true;
for _ in 0..60 {
if check_time_is_synchronized().await? {
warn_time_not_synced = false;
break;
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
if warn_time_not_synced {
tracing::warn!("Timed out waiting for system time to synchronize");
} else {
tracing::info!("Syncronized system clock");
}
receipts
.system_start_time
.set(&mut handle, time().await?)
.await?;
crate::version::init(&mut handle, &receipts).await?;

View File

@@ -86,6 +86,7 @@ pub fn main_api() -> Result<(), RpcError> {
}
#[command(subcommands(
system::time,
system::logs,
system::kernel_logs,
system::metrics,

View File

@@ -23,6 +23,9 @@ use tokio::sync::Mutex;
use crate::context::RpcContext;
use crate::{Error, ResultExt};
pub const LOCAL_AUTH_COOKIE_PATH: &str = "/run/embassy/rpc.authcookie";
pub trait AsLogoutSessionId {
fn as_logout_session_id(self) -> String;
}
@@ -63,7 +66,29 @@ impl HasValidSession {
request_parts: &RequestParts,
ctx: &RpcContext,
) -> Result<Self, Error> {
Self::from_session(&HashSessionToken::from_request_parts(request_parts)?, ctx).await
if let Some(cookie_header) = request_parts.headers.get(COOKIE) {
let cookies = Cookie::parse(
cookie_header
.to_str()
.with_kind(crate::ErrorKind::Authorization)?,
)
.with_kind(crate::ErrorKind::Authorization)?;
if let Some(cookie) = cookies.iter().find(|c| c.get_name() == "local") {
if let Ok(s) = Self::from_local(cookie).await {
return Ok(s);
}
}
if let Some(cookie) = cookies.iter().find(|c| c.get_name() == "session") {
if let Ok(s) = Self::from_session(&HashSessionToken::from_cookie(cookie), ctx).await
{
return Ok(s);
}
}
}
Err(Error::new(
eyre!("UNAUTHORIZED"),
crate::ErrorKind::Authorization,
))
}
pub async fn from_session(session: &HashSessionToken, ctx: &RpcContext) -> Result<Self, Error> {
@@ -79,6 +104,18 @@ impl HasValidSession {
}
Ok(Self(()))
}
pub async fn from_local(local: &Cookie<'_>) -> Result<Self, Error> {
let token = tokio::fs::read_to_string("/run/embassy/rpc.authcookie").await?;
if local.get_value() == &*token {
Ok(Self(()))
} else {
Err(Error::new(
eyre!("UNAUTHORIZED"),
crate::ErrorKind::Authorization,
))
}
}
}
/// When we have a need to create a new session,

43
backend/src/net/dhcp.rs Normal file
View File

@@ -0,0 +1,43 @@
use std::collections::BTreeMap;
use futures::TryStreamExt;
use rpc_toolkit::command;
use crate::context::RpcContext;
use crate::db::model::IpInfo;
use crate::net::net_utils::{iface_is_physical, list_interfaces};
use crate::util::display_none;
use crate::Error;
pub async fn init_ips() -> Result<BTreeMap<String, IpInfo>, Error> {
let mut res = BTreeMap::new();
let mut ifaces = list_interfaces();
while let Some(iface) = ifaces.try_next().await? {
if iface_is_physical(&iface).await {
let ip_info = IpInfo::for_interface(&iface).await?;
res.insert(iface, ip_info);
}
}
Ok(res)
}
#[command(subcommands(update))]
pub async fn dhcp() -> Result<(), Error> {
Ok(())
}
#[command(display(display_none))]
pub async fn update(#[context] ctx: RpcContext, #[arg] interface: String) -> Result<(), Error> {
if iface_is_physical(&interface).await {
crate::db::DatabaseModel::new()
.server_info()
.ip_info()
.idx_model(&interface)
.put(
&mut ctx.db.handle(),
&IpInfo::for_interface(&interface).await?,
)
.await?;
}
Ok(())
}

View File

@@ -12,6 +12,7 @@ use crate::util::serde::Port;
use crate::Error;
pub mod cert_resolver;
pub mod dhcp;
pub mod dns;
pub mod embassy_service_http_server;
pub mod interface;
@@ -28,7 +29,7 @@ pub mod wifi;
const PACKAGE_CERT_PATH: &str = "/var/lib/embassy/ssl";
#[command(subcommands(tor::tor))]
#[command(subcommands(tor::tor, dhcp::dhcp))]
pub fn net() -> Result<(), Error> {
Ok(())
}

View File

@@ -1,13 +1,115 @@
use std::fmt;
use std::net::IpAddr;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::path::Path;
use std::str::FromStr;
use async_stream::try_stream;
use color_eyre::eyre::eyre;
use futures::stream::BoxStream;
use futures::{StreamExt, TryStreamExt};
use http::{Request, Uri};
use hyper::Body;
use tokio::process::Command;
use crate::util::Invoke;
use crate::Error;
fn parse_iface_ip(output: &str) -> Result<Option<&str>, Error> {
let output = output.trim();
if output.is_empty() {
return Ok(None);
}
if let Some(ip) = output
.split_ascii_whitespace()
.nth(3)
.and_then(|range| range.split("/").next())
{
Ok(Some(ip))
} else {
Err(Error::new(
eyre!("malformed output from `ip`"),
crate::ErrorKind::Network,
))
}
}
pub async fn get_iface_ipv4_addr(iface: &str) -> Result<Option<Ipv4Addr>, Error> {
Ok(parse_iface_ip(&String::from_utf8(
Command::new("ip")
.arg("-4")
.arg("-o")
.arg("addr")
.arg("show")
.arg(iface)
.invoke(crate::ErrorKind::Network)
.await?,
)?)?
.map(|s| s.parse())
.transpose()?)
}
pub async fn get_iface_ipv6_addr(iface: &str) -> Result<Option<Ipv6Addr>, Error> {
Ok(parse_iface_ip(&String::from_utf8(
Command::new("ip")
.arg("-6")
.arg("-o")
.arg("addr")
.arg("show")
.arg(iface)
.invoke(crate::ErrorKind::Network)
.await?,
)?)?
.map(|s| s.parse())
.transpose()?)
}
pub async fn iface_is_physical(iface: &str) -> bool {
tokio::fs::metadata(Path::new("/sys/class/net").join(iface).join("device"))
.await
.is_ok()
}
pub async fn iface_is_wireless(iface: &str) -> bool {
tokio::fs::metadata(Path::new("/sys/class/net").join(iface).join("wireless"))
.await
.is_ok()
}
pub fn list_interfaces() -> BoxStream<'static, Result<String, Error>> {
try_stream! {
let mut ifaces = tokio::fs::read_dir("/sys/class/net").await?;
while let Some(iface) = ifaces.next_entry().await? {
if let Some(iface) = iface.file_name().into_string().ok() {
yield iface;
}
}
}
.boxed()
}
pub async fn find_wifi_iface() -> Result<Option<String>, Error> {
let mut ifaces = list_interfaces();
while let Some(iface) = ifaces.try_next().await? {
if iface_is_wireless(&iface).await {
return Ok(Some(iface));
}
}
Ok(None)
}
pub async fn find_eth_iface() -> Result<String, Error> {
let mut ifaces = list_interfaces();
while let Some(iface) = ifaces.try_next().await? {
if iface_is_physical(&iface).await && !iface_is_wireless(&iface).await {
return Ok(iface);
}
}
Err(Error::new(
eyre!("Could not detect ethernet interface"),
crate::ErrorKind::Network,
))
}
pub fn host_addr_fqdn(req: &Request<Body>) -> Result<ResourceFqdn, Error> {
let host = req.headers().get(http::header::HOST);

View File

@@ -16,7 +16,10 @@ use crate::context::{DiagnosticContext, InstallContext, RpcContext, SetupContext
use crate::core::rpc_continuations::RequestGuid;
use crate::db::subscribe;
use crate::install::PKG_PUBLIC_DIR;
use crate::middleware::auth::HasValidSession;
use crate::middleware::auth::{auth as auth_middleware, HasValidSession};
use crate::middleware::cors::cors;
use crate::middleware::db::db as db_middleware;
use crate::middleware::diagnostic::diagnostic as diagnostic_middleware;
use crate::net::HttpHandler;
use crate::{diagnostic_api, install_api, main_api, setup_api, Error, ErrorKind, ResultExt};
@@ -48,8 +51,14 @@ pub async fn setup_ui_file_router(ctx: SetupContext) -> Result<HttpHandler, Erro
async move {
let res = match req.uri().path() {
path if path.starts_with("/rpc/") => {
let rpc_handler =
rpc_handler!({command: setup_api, context: ctx, status: status_fn});
let rpc_handler = rpc_handler!({
command: setup_api,
context: ctx,
status: status_fn,
middleware: [
cors,
]
});
rpc_handler(req)
.await
@@ -76,8 +85,15 @@ pub async fn diag_ui_file_router(ctx: DiagnosticContext) -> Result<HttpHandler,
async move {
let res = match req.uri().path() {
path if path.starts_with("/rpc/") => {
let rpc_handler =
rpc_handler!({command: diagnostic_api, context: ctx, status: status_fn});
let rpc_handler = rpc_handler!({
command: diagnostic_api,
context: ctx,
status: status_fn,
middleware: [
cors,
diagnostic_middleware,
]
});
rpc_handler(req)
.await
@@ -104,8 +120,14 @@ pub async fn install_ui_file_router(ctx: InstallContext) -> Result<HttpHandler,
async move {
let res = match req.uri().path() {
path if path.starts_with("/rpc/") => {
let rpc_handler =
rpc_handler!({command: install_api, context: ctx, status: status_fn});
let rpc_handler = rpc_handler!({
command: install_api,
context: ctx,
status: status_fn,
middleware: [
cors,
]
});
rpc_handler(req)
.await
@@ -132,8 +154,18 @@ pub async fn main_ui_server_router(ctx: RpcContext) -> Result<HttpHandler, Error
async move {
let res = match req.uri().path() {
path if path.starts_with("/rpc/") => {
let rpc_handler =
rpc_handler!({command: main_api, context: ctx, status: status_fn});
let auth_middleware = auth_middleware(ctx.clone());
let db_middleware = db_middleware(ctx.clone());
let rpc_handler = rpc_handler!({
command: main_api,
context: ctx,
status: status_fn,
middleware: [
cors,
auth_middleware,
db_middleware,
]
});
rpc_handler(req)
.await

View File

@@ -14,6 +14,7 @@ use crate::disk::mount::filesystem::ReadWrite;
use crate::disk::mount::guard::{MountGuard, TmpMountGuard};
use crate::disk::util::DiskInfo;
use crate::disk::OsPartitionInfo;
use crate::net::net_utils::{find_eth_iface, find_wifi_iface};
use crate::util::serde::IoFormat;
use crate::util::{display_none, Invoke};
@@ -69,43 +70,6 @@ pub async fn list() -> Result<Vec<DiskInfo>, Error> {
.collect())
}
pub async fn find_wifi_iface() -> Result<Option<String>, Error> {
let mut ifaces = tokio::fs::read_dir("/sys/class/net").await?;
while let Some(iface) = ifaces.next_entry().await? {
if tokio::fs::metadata(iface.path().join("wireless"))
.await
.is_ok()
{
if let Some(iface) = iface.file_name().into_string().ok() {
return Ok(Some(iface));
}
}
}
Ok(None)
}
pub async fn find_eth_iface() -> Result<String, Error> {
let mut ifaces = tokio::fs::read_dir("/sys/class/net").await?;
while let Some(iface) = ifaces.next_entry().await? {
if tokio::fs::metadata(iface.path().join("wireless"))
.await
.is_err()
&& tokio::fs::metadata(iface.path().join("device"))
.await
.is_ok()
{
if let Some(iface) = iface.file_name().into_string().ok() {
return Ok(iface);
}
}
}
Err(Error::new(
eyre!("Could not detect ethernet interface"),
crate::ErrorKind::Network,
))
}
pub fn partition_for(disk: impl AsRef<Path>, idx: usize) -> PathBuf {
let disk_path = disk.as_ref();
let (root, leaf) = if let (Some(root), Some(leaf)) = (

View File

@@ -7,10 +7,12 @@ use helpers::{Rsync, RsyncOptions};
use josekit::jwk::Jwk;
use openssl::x509::X509;
use patch_db::DbHandle;
use rand::random;
use rpc_toolkit::command;
use rpc_toolkit::yajrc::RpcError;
use serde::{Deserialize, Serialize};
use sqlx::{Connection, Executor, Postgres};
use ssh_key::private::Ed25519PrivateKey;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
use torut::onion::{OnionAddressV3, TorSecretKeyV3};
@@ -390,13 +392,16 @@ async fn fresh_setup(
)
.with_kind(crate::ErrorKind::PasswordHashGeneration)?;
let tor_key = TorSecretKeyV3::generate();
let key_vec = tor_key.as_bytes().to_vec();
let tor_key_bytes = tor_key.as_bytes().to_vec();
let ssh_key = Ed25519PrivateKey::from_bytes(&random());
let ssh_key_bytes = ssh_key.to_bytes().to_vec();
let sqlite_pool = ctx.secret_store().await?;
sqlx::query!(
"INSERT INTO account (id, password, tor_key) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET password = $2, tor_key = $3",
"INSERT INTO account (id, password, tor_key, ssh_key) VALUES ($1, $2, $3, $4) ON CONFLICT (id) DO UPDATE SET password = $2, tor_key = $3, ssh_key = $4",
0,
password,
key_vec,
tor_key_bytes,
ssh_key_bytes,
)
.execute(&mut sqlite_pool.acquire().await?)
.await?;

View File

@@ -4,7 +4,8 @@ use chrono::Utc;
use clap::ArgMatches;
use color_eyre::eyre::eyre;
use rpc_toolkit::command;
use sqlx::{Pool, Postgres};
use sqlx::{Executor, Pool, Postgres};
use ssh_key::private::Ed25519PrivateKey;
use tracing::instrument;
use crate::context::RpcContext;
@@ -14,6 +15,25 @@ use crate::{Error, ErrorKind};
static SSH_AUTHORIZED_KEYS_FILE: &str = "/home/start9/.ssh/authorized_keys";
#[instrument(skip(secrets))]
pub async fn os_key<Ex>(secrets: &mut Ex) -> Result<Ed25519PrivateKey, Error>
where
for<'a> &'a mut Ex: Executor<'a, Database = Postgres>,
{
let key = sqlx::query!("SELECT ssh_key FROM account")
.fetch_one(secrets)
.await?
.ssh_key;
let mut buf = [0; 32];
buf.clone_from_slice(
key.get(0..64).ok_or_else(|| {
Error::new(eyre!("Invalid Ssh Key Length"), crate::ErrorKind::Database)
})?,
);
Ok(Ed25519PrivateKey::from_bytes(&buf))
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct PubKey(
#[serde(serialize_with = "crate::util::serde::serialize_display")]

View File

@@ -1,5 +1,6 @@
use std::fmt;
use chrono::Utc;
use color_eyre::eyre::eyre;
use futures::FutureExt;
use rpc_toolkit::command;
@@ -22,6 +23,11 @@ use crate::{Error, ErrorKind, ResultExt};
pub const SYSTEMD_UNIT: &'static str = "embassyd";
#[command]
pub async fn time() -> Result<String, Error> {
Ok(Utc::now().to_rfc3339())
}
#[command(
custom_cli(cli_logs(async, context(CliContext))),
subcommands(self(logs_nofollow(async)), logs_follow),

View File

@@ -0,0 +1 @@
embassy-cli net dhcp update $interface

View File

@@ -102,6 +102,10 @@ update-locale LANGUAGE
rm "/etc/locale.gen"
dpkg-reconfigure --frontend noninteractive locales
groupadd embassy
ln -s /usr/lib/embassy/scripts/dhclient-exit-hook /etc/dhcp/dhclient-exit-hooks.d/embassy
rm -f /etc/motd
ln -sf /usr/lib/embassy/motd /etc/update-motd.d/00-embassy
chmod -x /etc/update-motd.d/*

View File

@@ -4,7 +4,7 @@ set -e
# introduce start9 username and embassy as default password
if ! awk -F: '{ print $1 }' /etc/passwd | grep start9
then
usermod -l start9 -d /home/start9 -m pi
usermod -l start9 -d /home/start9 -aG embassy -m pi
groupmod --new-name start9 pi
echo start9:embassy | chpasswd
fi

View File

@@ -15,6 +15,32 @@
<div id="metricSection">
<ng-container *ngIf="!loading">
<ion-item-group>
<ion-item-divider>Time</ion-item-divider>
<ion-item>
<ion-label>System Time</ion-label>
<ion-note slot="end" class="metric-note">
<ion-text style="color: white"
>{{ systemTime$ | async | date:'MMMM d, y, h:mm a z':'UTC'
}}</ion-text
>
</ion-note>
</ion-item>
<ion-item>
<ion-label>System Uptime</ion-label>
<ion-note
*ngIf="systemUptime$ | async as uptime"
slot="end"
class="metric-note"
>
<ion-text style="color: white">
<b>{{ uptime.days }}</b> Days, <b>{{ uptime.hours }}</b> Hours,
<b>{{ uptime.minutes }}</b> Minutes
</ion-text>
</ion-note>
</ion-item>
</ion-item-group>
<ion-item-group
*ngFor="let metricGroup of metrics | keyvalue : asIsOrder"
>

View File

@@ -1,6 +1,7 @@
import { Component } from '@angular/core'
import { Metrics } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { TimeService } from 'src/app/services/time-service'
import { pauseFor, ErrorToastService } from '@start9labs/shared'
@Component({
@@ -13,9 +14,13 @@ export class ServerMetricsPage {
going = false
metrics: Metrics = {}
readonly systemTime$ = this.timeService.systemTime$
readonly systemUptime$ = this.timeService.systemUptime$
constructor(
private readonly errToast: ErrorToastService,
private readonly embassyApi: ApiService,
private readonly timeService: TimeService,
) {}
async ngOnInit() {

View File

@@ -8,28 +8,28 @@
</ion-header>
<ion-content>
<ion-item-divider>Basic</ion-item-divider>
<ion-item-group *ngIf="server$ | async as server">
<ion-item-divider>embassyOS Info</ion-item-divider>
<ion-item>
<ion-label>
<h2>embassyOS Version</h2>
<h2>Version</h2>
<p>{{ server.version | displayEmver }}</p>
</ion-label>
</ion-item>
<ion-item>
<ion-label>
<h2>Git Hash</h2>
<p>{{ gitHash }}</p>
</ion-label>
<ion-button slot="end" fill="clear" (click)="copy(gitHash)">
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-item>
<ion-item-divider>Addresses</ion-item-divider>
<ion-item-divider>Web Addresses</ion-item-divider>
<ion-item>
<ion-label class="break-all">
<h2>Tor Address</h2>
<h2>Tor</h2>
<p>{{ server['tor-address'] }}</p>
</ion-label>
<ion-button slot="end" fill="clear" (click)="copy(server['tor-address'])">
@@ -38,12 +38,49 @@
</ion-item>
<ion-item>
<ion-label class="break-all">
<h2>LAN Address</h2>
<h2>LAN</h2>
<p>{{ server['lan-address'] }}</p>
</ion-label>
<ion-button slot="end" fill="clear" (click)="copy(server['lan-address'])">
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-item>
<ng-container *ngFor="let ip of server['ip-info'] | keyvalue">
<ng-container *ngFor="let entry of ip.value | keyvalue">
<ion-item *ngIf="entry.value as address">
<ion-label>
<h2>{{ ip.key }} ({{ entry.key }})</h2>
<p>{{ address }}</p>
</ion-label>
<ion-button slot="end" fill="clear" (click)="copy(address)">
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-item>
</ng-container>
</ng-container>
<ion-item-divider>Device Credentials</ion-item-divider>
<ion-item>
<ion-label>
<h2>Pubkey</h2>
<p>{{ server['pubkey'] }}</p>
</ion-label>
<ion-button slot="end" fill="clear" (click)="copy(server['pubkey'])">
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-item>
<ion-item>
<ion-label>
<h2>CA fingerprint</h2>
<p>{{ server['ca-fingerprint'] }}</p>
</ion-label>
<ion-button
slot="end"
fill="clear"
(click)="copy(server['ca-fingerprint'])"
>
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-item>
</ion-item-group>
</ion-content>

View File

@@ -35,6 +35,9 @@ export module RR {
export type EchoReq = { message: string } // server.echo
export type EchoRes = string
export type GetSystemTimeReq = {} // server.time
export type GetSystemTimeRes = string
export type GetServerLogsReq = ServerLogsReq // server.logs & server.kernel-logs
export type GetServerLogsRes = LogsRes

View File

@@ -57,6 +57,10 @@ export abstract class ApiService {
config: WebSocketSubjectConfig<Log>,
): Observable<Log>
abstract getSystemTime(
params: RR.GetSystemTimeReq,
): Promise<RR.GetSystemTimeRes>
abstract getServerLogs(
params: RR.GetServerLogsReq,
): Promise<RR.GetServerLogsRes>

View File

@@ -117,6 +117,12 @@ export class LiveApiService extends ApiService {
return this.openWebsocket(config)
}
async getSystemTime(
params: RR.GetSystemTimeReq,
): Promise<RR.GetSystemTimeRes> {
return this.rpcRequest({ method: 'server.time', params })
}
async getServerLogs(
params: RR.GetServerLogsReq,
): Promise<RR.GetServerLogsRes> {

View File

@@ -177,6 +177,13 @@ export class MockApiService extends ApiService {
)
}
async getSystemTime(
params: RR.GetSystemTimeReq,
): Promise<RR.GetSystemTimeRes> {
await pauseFor(2000)
return new Date().toUTCString()
}
async getServerLogs(
params: RR.GetServerLogsReq,
): Promise<RR.GetServerLogsRes> {

View File

@@ -39,6 +39,16 @@ export const mockPatchData: DataModel = {
'last-backup': new Date(new Date().valueOf() - 604800001).toISOString(),
'lan-address': 'https://embassy-abcdefgh.local',
'tor-address': 'http://myveryownspecialtoraddress.onion',
'ip-info': {
eth0: {
ipv4: '10.0.0.1',
ipv6: null,
},
wlan0: {
ipv4: '10.0.90.12',
ipv6: 'FE80:CD00:0000:0CDE:1257:0000:211E:729CD',
},
},
'last-wifi-region': null,
'unread-notification-count': 4,
// password is asdfasdf
@@ -51,6 +61,9 @@ export const mockPatchData: DataModel = {
'update-progress': null,
},
hostname: 'random-words',
pubkey: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m',
'ca-fingerprint': 'SHA-256: 63 2B 11 99 44 40 17 DF 37 FC C3 DF 0F 3D 15',
'system-start-time': new Date(new Date().valueOf() - 360042).toUTCString(),
},
'package-data': {
bitcoind: {

View File

@@ -52,12 +52,23 @@ export interface ServerInfo {
'last-backup': string | null
'lan-address': Url
'tor-address': Url
'ip-info': IpInfo
'last-wifi-region': string | null
'unread-notification-count': number
'status-info': ServerStatusInfo
'eos-version-compat': string
'password-hash': string
hostname: string
pubkey: string
'ca-fingerprint': string
'system-start-time': string
}
export interface IpInfo {
[iface: string]: {
ipv4: string | null
ipv6: string | null
}
}
export interface ServerStatusInfo {

View File

@@ -0,0 +1,63 @@
import { Injectable } from '@angular/core'
import {
map,
shareReplay,
startWith,
switchMap,
take,
tap,
} from 'rxjs/operators'
import { PatchDB } from 'patch-db-client'
import { DataModel } from './patch-db/data-model'
import { ApiService } from './api/embassy-api.service'
import { combineLatest, from, timer } from 'rxjs'
@Injectable({
providedIn: 'root',
})
export class TimeService {
private readonly startTimeMs$ = this.patch
.watch$('server-info', 'system-start-time')
.pipe(
take(1),
map(startTime => new Date(startTime).valueOf()),
shareReplay(),
)
readonly systemTime$ = from(this.apiService.getSystemTime({})).pipe(
switchMap(utcStr => {
const dateObj = new Date(utcStr)
const msRemaining = (60 - dateObj.getSeconds()) * 1000
dateObj.setSeconds(0)
const current = dateObj.valueOf()
return timer(msRemaining, 60000).pipe(
map(index => {
const incremented = index + 1
const msToAdd = 60000 * incremented
return current + msToAdd
}),
startWith(current),
)
}),
)
readonly systemUptime$ = combineLatest([
this.startTimeMs$,
this.systemTime$,
]).pipe(
map(([startTime, currentTime]) => {
const ms = currentTime - startTime
const days = Math.floor(ms / (24 * 60 * 60 * 1000))
const daysms = ms % (24 * 60 * 60 * 1000)
const hours = Math.floor(daysms / (60 * 60 * 1000))
const hoursms = ms % (60 * 60 * 1000)
const minutes = Math.floor(hoursms / (60 * 1000))
return { days, hours, minutes }
}),
)
constructor(
private readonly patch: PatchDB<DataModel>,
private readonly apiService: ApiService,
) {}
}

776
libs/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,6 +27,7 @@ sqlx = { version = "0.6.0", features = [
"runtime-tokio-rustls",
"postgres",
] }
ssh-key = "0.5.1"
thiserror = "1.0"
tokio = { version = "1", features = ["full"] }
torut = "0.2.1"

View File

@@ -261,6 +261,11 @@ impl From<InvalidUri> for Error {
Error::new(eyre!("{}", e), ErrorKind::ParseUrl)
}
}
impl From<ssh_key::Error> for Error {
fn from(e: ssh_key::Error) -> Self {
Error::new(e, ErrorKind::OpenSsh)
}
}
impl From<Error> for RpcError {
fn from(e: Error) -> Self {

File diff suppressed because it is too large Load Diff