From e6b7390a61ca328ecbfc6b60b7f249bf5e75af5c Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Thu, 24 Jul 2025 18:33:55 -0600 Subject: [PATCH] wip start-tunneld --- Makefile | 8 +- .../container-runtime-failure.service | 2 +- container-runtime/update-image.sh | 4 +- core/build-containerbox.sh | 2 +- core/build-registrybox.sh | 2 +- core/build-startbox.sh | 2 +- core/build-tunnelbox.sh | 36 +++ core/install-cli.sh | 2 +- core/startos/Cargo.toml | 20 +- core/startos/src/account.rs | 28 ++- core/startos/src/auth.rs | 91 ++++--- core/startos/src/backup/backup_bulk.rs | 4 +- core/startos/src/backup/os.rs | 8 +- core/startos/src/bins/mod.rs | 59 +++-- core/startos/src/bins/registry.rs | 30 +++ core/startos/src/bins/tunnel.rs | 117 +++++++++ core/startos/src/context/cli.rs | 165 +++++++++---- core/startos/src/context/config.rs | 15 +- core/startos/src/db/model/mod.rs | 6 +- core/startos/src/db/model/private.rs | 10 +- core/startos/src/developer/mod.rs | 63 +++-- core/startos/src/init.rs | 54 ++-- core/startos/src/install/mod.rs | 2 +- core/startos/src/lib.rs | 9 +- core/startos/src/middleware/auth.rs | 233 ++++++++++++++---- core/startos/src/middleware/mod.rs | 1 + .../auth.rs => middleware/signature.rs} | 191 +++++++++----- core/startos/src/net/acme.rs | 6 +- core/startos/src/net/static_server.rs | 6 +- core/startos/src/registry/admin.rs | 2 +- core/startos/src/registry/asset.rs | 6 +- core/startos/src/registry/context.rs | 193 +++++++++------ core/startos/src/registry/mod.rs | 5 +- core/startos/src/registry/os/asset/add.rs | 6 +- core/startos/src/registry/os/asset/get.rs | 4 +- core/startos/src/registry/os/asset/sign.rs | 6 +- core/startos/src/registry/os/index.rs | 2 +- core/startos/src/registry/os/version/mod.rs | 2 +- core/startos/src/registry/package/add.rs | 6 +- core/startos/src/registry/package/index.rs | 4 +- .../src/registry/{signer/mod.rs => signer.rs} | 7 +- core/startos/src/s9pk/merkle_archive/mod.rs | 6 +- core/startos/src/s9pk/v2/mod.rs | 2 +- .../src/service/effects/subcontainer/sync.rs | 8 +- core/startos/src/service/mod.rs | 2 +- core/startos/src/service/service_map.rs | 2 +- .../signer => sign}/commitment/blake3.rs | 2 +- .../commitment/merkle_archive.rs | 2 +- .../signer => sign}/commitment/mod.rs | 0 .../signer => sign}/commitment/request.rs | 2 +- .../src/{registry/signer => }/sign/ed25519.rs | 2 +- .../src/{registry/signer => }/sign/mod.rs | 9 +- core/startos/src/ssh.rs | 2 +- core/startos/src/tunnel/context.rs | 226 +++++++++++++++++ core/startos/src/tunnel/db.rs | 180 ++++++++++++++ core/startos/src/tunnel/mod.rs | 99 ++++++++ core/startos/src/update/mod.rs | 4 +- core/startos/src/util/iter.rs | 31 +++ core/startos/src/util/mod.rs | 1 + core/startos/src/version/v0_3_6_alpha_0.rs | 7 +- core/startos/src/version/v0_3_6_alpha_8.rs | 2 +- sdk/package/lib/util/SubContainer.ts | 18 +- 62 files changed, 1555 insertions(+), 471 deletions(-) create mode 100755 core/build-tunnelbox.sh create mode 100644 core/startos/src/bins/tunnel.rs rename core/startos/src/{registry/auth.rs => middleware/signature.rs} (53%) rename core/startos/src/registry/{signer/mod.rs => signer.rs} (96%) rename core/startos/src/{registry/signer => sign}/commitment/blake3.rs (96%) rename core/startos/src/{registry/signer => sign}/commitment/merkle_archive.rs (98%) rename core/startos/src/{registry/signer => sign}/commitment/mod.rs (100%) rename core/startos/src/{registry/signer => sign}/commitment/request.rs (98%) rename core/startos/src/{registry/signer => }/sign/ed25519.rs (94%) rename core/startos/src/{registry/signer => }/sign/mod.rs (98%) create mode 100644 core/startos/src/tunnel/context.rs create mode 100644 core/startos/src/tunnel/db.rs create mode 100644 core/startos/src/tunnel/mod.rs create mode 100644 core/startos/src/util/iter.rs diff --git a/Makefile b/Makefile index cdf40c7e2..7ddeb7d82 100644 --- a/Makefile +++ b/Makefile @@ -102,10 +102,13 @@ test-container-runtime: container-runtime/node_modules/.package-lock.json $(shel cd container-runtime && npm test cli: - cd core && ./install-cli.sh + ./core/install-cli.sh registry: - cd core && ./build-registrybox.sh + ./core/build-registrybox.sh + +tunnel: + ./core/build-tunnelbox.sh deb: results/$(BASENAME).deb @@ -129,7 +132,6 @@ install: $(ALL_TARGETS) $(call cp,core/target/$(ARCH)-unknown-linux-musl/release/startbox,$(DESTDIR)/usr/bin/startbox) $(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/startd) $(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/start-cli) - $(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/start-sdk) if [ "$(PLATFORM)" = "raspberrypi" ]; then $(call cp,cargo-deps/aarch64-unknown-linux-musl/release/pi-beep,$(DESTDIR)/usr/bin/pi-beep); fi if /bin/bash -c '[[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]'; then $(call cp,cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console,$(DESTDIR)/usr/bin/tokio-console); fi $(call cp,cargo-deps/$(ARCH)-unknown-linux-musl/release/startos-backup-fs,$(DESTDIR)/usr/bin/startos-backup-fs) diff --git a/container-runtime/container-runtime-failure.service b/container-runtime/container-runtime-failure.service index 295132bab..78b422243 100644 --- a/container-runtime/container-runtime-failure.service +++ b/container-runtime/container-runtime-failure.service @@ -3,4 +3,4 @@ Description=StartOS Container Runtime Failure Handler [Service] Type=oneshot -ExecStart=/usr/bin/start-cli rebuild \ No newline at end of file +ExecStart=/usr/bin/start-container rebuild \ No newline at end of file diff --git a/container-runtime/update-image.sh b/container-runtime/update-image.sh index f1f28f896..721e46094 100755 --- a/container-runtime/update-image.sh +++ b/container-runtime/update-image.sh @@ -39,8 +39,8 @@ sudo cp container-runtime.service tmp/combined/lib/systemd/system/container-runt sudo chown 0:0 tmp/combined/lib/systemd/system/container-runtime.service sudo cp container-runtime-failure.service tmp/combined/lib/systemd/system/container-runtime-failure.service sudo chown 0:0 tmp/combined/lib/systemd/system/container-runtime-failure.service -sudo cp ../core/target/$ARCH-unknown-linux-musl/release/containerbox tmp/combined/usr/bin/start-cli -sudo chown 0:0 tmp/combined/usr/bin/start-cli +sudo cp ../core/target/$ARCH-unknown-linux-musl/release/containerbox tmp/combined/usr/bin/start-container +sudo chown 0:0 tmp/combined/usr/bin/start-container echo container-runtime | sha256sum | head -c 32 | cat - <(echo) | sudo tee tmp/combined/etc/machine-id cat deb-install.sh | sudo systemd-nspawn --console=pipe -D tmp/combined $QEMU /bin/bash sudo truncate -s 0 tmp/combined/etc/machine-id diff --git a/core/build-containerbox.sh b/core/build-containerbox.sh index e81efcc97..7fbd6c69c 100755 --- a/core/build-containerbox.sh +++ b/core/build-containerbox.sh @@ -30,7 +30,7 @@ alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v echo "FEATURES=\"$FEATURES\"" echo "RUSTFLAGS=\"$RUSTFLAGS\"" -rust-musl-builder sh -c "cd core && cargo build --release --no-default-features --features container-runtime,$FEATURES --locked --bin containerbox --target=$ARCH-unknown-linux-musl" +rust-musl-builder sh -c "cd core && cargo build --release --no-default-features --features cli-container,$FEATURES --locked --bin containerbox --target=$ARCH-unknown-linux-musl" if [ "$(ls -nd core/target/$ARCH-unknown-linux-musl/release/containerbox | awk '{ print $3 }')" != "$UID" ]; then rust-musl-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo" fi \ No newline at end of file diff --git a/core/build-registrybox.sh b/core/build-registrybox.sh index 3659b372a..41e67a87f 100755 --- a/core/build-registrybox.sh +++ b/core/build-registrybox.sh @@ -30,7 +30,7 @@ alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v echo "FEATURES=\"$FEATURES\"" echo "RUSTFLAGS=\"$RUSTFLAGS\"" -rust-musl-builder sh -c "cd core && cargo build --release --no-default-features --features cli,registry,$FEATURES --locked --bin registrybox --target=$ARCH-unknown-linux-musl" +rust-musl-builder sh -c "cd core && cargo build --release --no-default-features --features cli-registry,registry,$FEATURES --locked --bin registrybox --target=$ARCH-unknown-linux-musl" if [ "$(ls -nd core/target/$ARCH-unknown-linux-musl/release/registrybox | awk '{ print $3 }')" != "$UID" ]; then rust-musl-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo" fi diff --git a/core/build-startbox.sh b/core/build-startbox.sh index 9fad6fa3d..51431e0f2 100755 --- a/core/build-startbox.sh +++ b/core/build-startbox.sh @@ -30,7 +30,7 @@ alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v echo "FEATURES=\"$FEATURES\"" echo "RUSTFLAGS=\"$RUSTFLAGS\"" -rust-musl-builder sh -c "cd core && cargo build --release --no-default-features --features cli,daemon,$FEATURES --locked --bin startbox --target=$ARCH-unknown-linux-musl" +rust-musl-builder sh -c "cd core && cargo build --release --no-default-features --features cli,startd,$FEATURES --locked --bin startbox --target=$ARCH-unknown-linux-musl" if [ "$(ls -nd core/target/$ARCH-unknown-linux-musl/release/startbox | awk '{ print $3 }')" != "$UID" ]; then rust-musl-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo" fi \ No newline at end of file diff --git a/core/build-tunnelbox.sh b/core/build-tunnelbox.sh new file mode 100755 index 000000000..76a5ea6e7 --- /dev/null +++ b/core/build-tunnelbox.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +cd "$(dirname "${BASH_SOURCE[0]}")" + +set -ea +shopt -s expand_aliases + +if [ -z "$ARCH" ]; then + ARCH=$(uname -m) +fi + +if [ "$ARCH" = "arm64" ]; then + ARCH="aarch64" +fi + +USE_TTY= +if tty -s; then + USE_TTY="-it" +fi + +cd .. +FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')" +RUSTFLAGS="" + +if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then + RUSTFLAGS="--cfg tokio_unstable" +fi + +alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl' + +echo "FEATURES=\"$FEATURES\"" +echo "RUSTFLAGS=\"$RUSTFLAGS\"" +rust-musl-builder sh -c "cd core && cargo build --release --no-default-features --features cli-tunnel,tunnel,$FEATURES --locked --bin tunnelbox --target=$ARCH-unknown-linux-musl" +if [ "$(ls -nd core/target/$ARCH-unknown-linux-musl/release/tunnelbox | awk '{ print $3 }')" != "$UID" ]; then + rust-musl-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo" +fi diff --git a/core/install-cli.sh b/core/install-cli.sh index b278947a3..7a0d725f2 100755 --- a/core/install-cli.sh +++ b/core/install-cli.sh @@ -16,4 +16,4 @@ if [ "$PLATFORM" = "arm64" ]; then PLATFORM="aarch64" fi -cargo install --path=./startos --no-default-features --features=cli,docker,registry --bin start-cli --locked +cargo install --path=./startos --no-default-features --features=cli,docker --bin start-cli --locked diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index 9acc84445..35827a419 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -37,16 +37,24 @@ path = "src/main.rs" name = "registrybox" path = "src/main.rs" +[[bin]] +name = "tunnelbox" +path = "src/main.rs" + [features] -cli = [] -container-runtime = ["procfs", "pty-process"] -daemon = ["mail-send"] -registry = [] -default = ["cli", "daemon", "registry", "container-runtime"] +cli = ["cli-startd", "cli-registry", "cli-tunnel"] +cli-container = ["procfs", "pty-process"] +cli-registry = [] +cli-startd = [] +cli-tunnel = [] +default = ["cli", "startd", "registry", "cli-container", "tunnel"] dev = [] -unstable = ["console-subscriber", "tokio/tracing"] docker = [] +registry = [] +startd = ["mail-send"] test = [] +tunnel = [] +unstable = ["console-subscriber", "tokio/tracing"] [dependencies] aes = { version = "0.7.5", features = ["ctr"] } diff --git a/core/startos/src/account.rs b/core/startos/src/account.rs index 46fd2c1f9..6abe18c28 100644 --- a/core/startos/src/account.rs +++ b/core/startos/src/account.rs @@ -1,5 +1,6 @@ use std::time::SystemTime; +use imbl_value::InternedString; use openssl::pkey::{PKey, Private}; use openssl::x509::X509; use torut::onion::TorSecretKeyV3; @@ -28,7 +29,7 @@ pub struct AccountInfo { pub root_ca_key: PKey, pub root_ca_cert: X509, pub ssh_key: ssh_key::PrivateKey, - pub compat_s9pk_key: ed25519_dalek::SigningKey, + pub developer_key: ed25519_dalek::SigningKey, } impl AccountInfo { pub fn new(password: &str, start_time: SystemTime) -> Result { @@ -40,7 +41,7 @@ impl AccountInfo { let ssh_key = ssh_key::PrivateKey::from(ssh_key::private::Ed25519Keypair::random( &mut ssh_key::rand_core::OsRng::default(), )); - let compat_s9pk_key = + let developer_key = ed25519_dalek::SigningKey::generate(&mut ssh_key::rand_core::OsRng::default()); Ok(Self { server_id, @@ -50,7 +51,7 @@ impl AccountInfo { root_ca_key, root_ca_cert, ssh_key, - compat_s9pk_key, + developer_key, }) } @@ -74,7 +75,7 @@ impl AccountInfo { let root_ca_key = cert_store.as_root_key().de()?.0; let root_ca_cert = cert_store.as_root_cert().de()?.0; let ssh_key = db.as_private().as_ssh_privkey().de()?.0; - let compat_s9pk_key = db.as_private().as_compat_s9pk_key().de()?.0; + let compat_s9pk_key = db.as_private().as_developer_key().de()?.0; Ok(Self { server_id, @@ -84,7 +85,7 @@ impl AccountInfo { root_ca_key, root_ca_cert, ssh_key, - compat_s9pk_key, + developer_key: compat_s9pk_key, }) } @@ -111,8 +112,8 @@ impl AccountInfo { .as_ssh_privkey_mut() .ser(Pem::new_ref(&self.ssh_key))?; db.as_private_mut() - .as_compat_s9pk_key_mut() - .ser(Pem::new_ref(&self.compat_s9pk_key))?; + .as_developer_key_mut() + .ser(Pem::new_ref(&self.developer_key))?; let key_store = db.as_private_mut().as_key_store_mut(); for tor_key in &self.tor_keys { key_store.as_onion_mut().insert_key(tor_key)?; @@ -131,4 +132,17 @@ impl AccountInfo { self.password = hash_password(password)?; Ok(()) } + + pub fn hostnames(&self) -> impl IntoIterator + Send + '_ { + [ + self.hostname.no_dot_host_name(), + self.hostname.local_domain_name(), + ] + .into_iter() + .chain( + self.tor_keys + .iter() + .map(|k| InternedString::from_display(&k.public().get_onion_address())), + ) + } } diff --git a/core/startos/src/auth.rs b/core/startos/src/auth.rs index 1249bb984..67a4fba3e 100644 --- a/core/startos/src/auth.rs +++ b/core/startos/src/auth.rs @@ -7,16 +7,15 @@ use imbl_value::{json, InternedString}; use itertools::Itertools; use josekit::jwk::Jwk; use rpc_toolkit::yajrc::RpcError; -use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; +use rpc_toolkit::{from_fn_async, CallRemote, Context, HandlerArgs, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use tokio::io::AsyncWriteExt; use tracing::instrument; use ts_rs::TS; use crate::context::{CliContext, RpcContext}; -use crate::db::model::DatabaseModel; use crate::middleware::auth::{ - AsLogoutSessionId, HasLoggedOutSessions, HashSessionToken, LoginRes, + AsLogoutSessionId, AuthContext, HasLoggedOutSessions, HashSessionToken, LoginRes, }; use crate::prelude::*; use crate::util::crypto::EncryptedWire; @@ -112,31 +111,34 @@ impl std::str::FromStr for PasswordType { }) } } -pub fn auth() -> ParentHandler { +pub fn auth() -> ParentHandler +where + CliContext: CallRemote, +{ ParentHandler::new() .subcommand( "login", - from_fn_async(login_impl) + from_fn_async(login_impl::) .with_metadata("login", Value::Bool(true)) .no_cli(), ) .subcommand( "login", - from_fn_async(cli_login) + from_fn_async(cli_login::) .no_display() - .with_about("Log in to StartOS server"), + .with_about("Log in a new auth session"), ) .subcommand( "logout", - from_fn_async(logout) + from_fn_async(logout::) .with_metadata("get_session", Value::Bool(true)) .no_display() - .with_about("Log out of StartOS server") + .with_about("Log out of current auth session") .with_call_remote::(), ) .subcommand( "session", - session::().with_about("List or kill StartOS sessions"), + session::().with_about("List or kill auth sessions"), ) .subcommand( "reset-password", @@ -146,7 +148,7 @@ pub fn auth() -> ParentHandler { "reset-password", from_fn_async(cli_reset_password) .no_display() - .with_about("Reset StartOS password"), + .with_about("Reset password"), ) .subcommand( "get-pubkey", @@ -172,17 +174,20 @@ fn gen_pwd() { } #[instrument(skip_all)] -async fn cli_login( +async fn cli_login( HandlerArgs { context: ctx, parent_method, method, .. }: HandlerArgs, -) -> Result<(), RpcError> { +) -> Result<(), RpcError> +where + CliContext: CallRemote, +{ let password = rpassword::prompt_password("Password: ")?; - ctx.call_remote::( + ctx.call_remote::( &parent_method.into_iter().chain(method).join("."), json!({ "password": password, @@ -210,17 +215,11 @@ pub fn check_password(hash: &str, password: &str) -> Result<(), Error> { Ok(()) } -pub fn check_password_against_db(db: &DatabaseModel, password: &str) -> Result<(), Error> { - let pw_hash = db.as_private().as_password().de()?; - check_password(&pw_hash, password)?; - Ok(()) -} - #[derive(Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export)] pub struct LoginParams { - password: Option, + password: String, #[ts(skip)] #[serde(rename = "__auth_userAgent")] // from Auth middleware user_agent: Option, @@ -229,20 +228,18 @@ pub struct LoginParams { } #[instrument(skip_all)] -pub async fn login_impl( - ctx: RpcContext, +pub async fn login_impl( + ctx: C, LoginParams { password, user_agent, ephemeral, }: LoginParams, ) -> Result { - let password = password.unwrap_or_default().decrypt(&ctx)?; - let tok = if ephemeral { - check_password_against_db(&ctx.db.peek().await, &password)?; + C::check_password(&ctx.db().peek().await, &password)?; let hash_token = HashSessionToken::new(); - ctx.ephemeral_sessions.mutate(|s| { + ctx.ephemeral_sessions().mutate(|s| { s.0.insert( hash_token.hashed().clone(), Session { @@ -254,11 +251,11 @@ pub async fn login_impl( }); Ok(hash_token.to_login_res()) } else { - ctx.db + ctx.db() .mutate(|db| { - check_password_against_db(db, &password)?; + C::check_password(db, &password)?; let hash_token = HashSessionToken::new(); - db.as_private_mut().as_sessions_mut().insert( + C::access_sessions(db).insert( hash_token.hashed(), &Session { logged_in: Utc::now(), @@ -273,12 +270,7 @@ pub async fn login_impl( .result }?; - if tokio::fs::metadata("/media/startos/config/overlay/etc/shadow") - .await - .is_err() - { - write_shadow(&password).await?; - } + ctx.post_login_hook(&password).await?; Ok(tok) } @@ -292,8 +284,8 @@ pub struct LogoutParams { session: InternedString, } -pub async fn logout( - ctx: RpcContext, +pub async fn logout( + ctx: C, LogoutParams { session }: LogoutParams, ) -> Result, Error> { Ok(Some( @@ -321,22 +313,25 @@ pub struct SessionList { sessions: Sessions, } -pub fn session() -> ParentHandler { +pub fn session() -> ParentHandler +where + CliContext: CallRemote, +{ ParentHandler::new() .subcommand( "list", - from_fn_async(list) + from_fn_async(list::) .with_metadata("get_session", Value::Bool(true)) .with_display_serializable() .with_custom_display_fn(|handle, result| display_sessions(handle.params, result)) - .with_about("Display all server sessions") + .with_about("Display all auth sessions") .with_call_remote::(), ) .subcommand( "kill", - from_fn_async(kill) + from_fn_async(kill::) .no_display() - .with_about("Terminate existing server session(s)") + .with_about("Terminate existing auth session(s)") .with_call_remote::(), ) } @@ -385,12 +380,12 @@ pub struct ListParams { // #[command(display(display_sessions))] #[instrument(skip_all)] -pub async fn list( - ctx: RpcContext, +pub async fn list( + ctx: C, ListParams { session, .. }: ListParams, ) -> Result { - let mut sessions = ctx.db.peek().await.into_private().into_sessions().de()?; - ctx.ephemeral_sessions.peek(|s| { + let mut sessions = C::access_sessions(&mut ctx.db().peek().await).de()?; + ctx.ephemeral_sessions().peek(|s| { sessions .0 .extend(s.0.iter().map(|(k, v)| (k.clone(), v.clone()))) @@ -424,7 +419,7 @@ pub struct KillParams { } #[instrument(skip_all)] -pub async fn kill(ctx: RpcContext, KillParams { ids }: KillParams) -> Result<(), Error> { +pub async fn kill(ctx: C, KillParams { ids }: KillParams) -> Result<(), Error> { HasLoggedOutSessions::new(ids.into_iter().map(KillSessionId::new), &ctx).await?; Ok(()) } diff --git a/core/startos/src/backup/backup_bulk.rs b/core/startos/src/backup/backup_bulk.rs index 5ece0cf7d..c24904cf8 100644 --- a/core/startos/src/backup/backup_bulk.rs +++ b/core/startos/src/backup/backup_bulk.rs @@ -15,7 +15,6 @@ use ts_rs::TS; use super::target::{BackupTargetId, PackageBackupInfo}; use super::PackageBackupReport; -use crate::auth::check_password_against_db; use crate::backup::os::OsBackup; use crate::backup::{BackupReport, ServerBackupReport}; use crate::context::RpcContext; @@ -24,6 +23,7 @@ use crate::db::model::{Database, DatabaseModel}; use crate::disk::mount::backup::BackupMountGuard; use crate::disk::mount::filesystem::ReadWrite; use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; +use crate::middleware::auth::AuthContext; use crate::notifications::{notify, NotificationLevel}; use crate::prelude::*; use crate::util::io::dir_copy; @@ -170,7 +170,7 @@ pub async fn backup_all( let ((fs, package_ids, server_id), status_guard) = ( ctx.db .mutate(|db| { - check_password_against_db(db, &password)?; + RpcContext::check_password(db, &password)?; let fs = target_id.load(db)?; let package_ids = if let Some(ids) = package_ids { ids.into_iter().collect() diff --git a/core/startos/src/backup/os.rs b/core/startos/src/backup/os.rs index 4c5798103..d0d5c83f5 100644 --- a/core/startos/src/backup/os.rs +++ b/core/startos/src/backup/os.rs @@ -86,7 +86,7 @@ impl OsBackupV0 { ssh_key::Algorithm::Ed25519, )?, tor_keys: vec![TorSecretKeyV3::from(self.tor_key.0)], - compat_s9pk_key: ed25519_dalek::SigningKey::generate( + developer_key: ed25519_dalek::SigningKey::generate( &mut ssh_key::rand_core::OsRng::default(), ), }, @@ -117,7 +117,7 @@ impl OsBackupV1 { root_ca_cert: self.root_ca_cert.0, ssh_key: ssh_key::PrivateKey::from(Ed25519Keypair::from_seed(&self.net_key.0)), tor_keys: vec![TorSecretKeyV3::from(ed25519_expand_key(&self.net_key.0))], - compat_s9pk_key: ed25519_dalek::SigningKey::from_bytes(&self.net_key), + developer_key: ed25519_dalek::SigningKey::from_bytes(&self.net_key), }, ui: self.ui, } @@ -149,7 +149,7 @@ impl OsBackupV2 { root_ca_cert: self.root_ca_cert.0, ssh_key: self.ssh_key.0, tor_keys: self.tor_keys, - compat_s9pk_key: self.compat_s9pk_key.0, + developer_key: self.compat_s9pk_key.0, }, ui: self.ui, } @@ -162,7 +162,7 @@ impl OsBackupV2 { root_ca_cert: Pem(backup.account.root_ca_cert.clone()), ssh_key: Pem(backup.account.ssh_key.clone()), tor_keys: backup.account.tor_keys.clone(), - compat_s9pk_key: Pem(backup.account.compat_s9pk_key.clone()), + compat_s9pk_key: Pem(backup.account.developer_key.clone()), ui: backup.ui.clone(), } } diff --git a/core/startos/src/bins/mod.rs b/core/startos/src/bins/mod.rs index 6ffecfce9..7230d04d5 100644 --- a/core/startos/src/bins/mod.rs +++ b/core/startos/src/bins/mod.rs @@ -2,41 +2,64 @@ use std::collections::VecDeque; use std::ffi::OsString; use std::path::Path; -#[cfg(feature = "container-runtime")] +#[cfg(feature = "cli-container")] pub mod container_cli; pub mod deprecated; #[cfg(feature = "registry")] pub mod registry; #[cfg(feature = "cli")] pub mod start_cli; -#[cfg(feature = "daemon")] +#[cfg(feature = "startd")] pub mod start_init; -#[cfg(feature = "daemon")] +#[cfg(feature = "startd")] pub mod startd; +#[cfg(feature = "tunnel")] +pub mod tunnel; fn select_executable(name: &str) -> Option)> { match name { - #[cfg(feature = "cli")] - "start-cli" => Some(start_cli::main), - #[cfg(feature = "container-runtime")] - "start-cli" => Some(container_cli::main), - #[cfg(feature = "daemon")] + #[cfg(feature = "startd")] "startd" => Some(startd::main), - #[cfg(feature = "registry")] - "registry" => Some(registry::main), - "embassy-cli" => Some(|_| deprecated::renamed("embassy-cli", "start-cli")), - "embassy-sdk" => Some(|_| deprecated::renamed("embassy-sdk", "start-sdk")), + #[cfg(feature = "startd")] "embassyd" => Some(|_| deprecated::renamed("embassyd", "startd")), + #[cfg(feature = "startd")] "embassy-init" => Some(|_| deprecated::removed("embassy-init")), + + #[cfg(feature = "cli-startd")] + "start-cli" => Some(start_cli::main), + #[cfg(feature = "cli-startd")] + "embassy-cli" => Some(|_| deprecated::renamed("embassy-cli", "start-cli")), + #[cfg(feature = "cli-startd")] + "embassy-sdk" => Some(|_| deprecated::removed("embassy-sdk")), + + #[cfg(feature = "cli-container")] + "start-container" => Some(container_cli::main), + + #[cfg(feature = "registry")] + "start-registryd" => Some(registry::main), + #[cfg(feature = "cli-registry")] + "start-registry" => Some(registry::cli), + + #[cfg(feature = "tunnel")] + "start-tunneld" => Some(tunnel::main), + #[cfg(feature = "cli-tunnel")] + "start-tunnel" => Some(tunnel::cli), + "contents" => Some(|_| { - #[cfg(feature = "cli")] - println!("start-cli"); - #[cfg(feature = "container-runtime")] - println!("start-cli (container)"); - #[cfg(feature = "daemon")] + #[cfg(feature = "startd")] println!("startd"); + #[cfg(feature = "cli-startd")] + println!("start-cli"); + #[cfg(feature = "cli-container")] + println!("start-container"); #[cfg(feature = "registry")] - println!("registry"); + println!("start-registryd"); + #[cfg(feature = "cli-registry")] + println!("start-registry"); + #[cfg(feature = "tunnel")] + println!("start-tunneld"); + #[cfg(feature = "cli-tunnel")] + println!("start-tunnel"); }), _ => None, } diff --git a/core/startos/src/bins/registry.rs b/core/startos/src/bins/registry.rs index a71b737af..4f122fbdd 100644 --- a/core/startos/src/bins/registry.rs +++ b/core/startos/src/bins/registry.rs @@ -2,9 +2,12 @@ use std::ffi::OsString; use clap::Parser; use futures::FutureExt; +use rpc_toolkit::CliApp; use tokio::signal::unix::signal; use tracing::instrument; +use crate::context::config::ClientConfig; +use crate::context::CliContext; use crate::net::web_server::{Acceptor, WebServer}; use crate::prelude::*; use crate::registry::context::{RegistryConfig, RegistryContext}; @@ -85,3 +88,30 @@ pub fn main(args: impl IntoIterator) { } } } + +pub fn cli(args: impl IntoIterator) { + LOGGER.enable(); + + if let Err(e) = CliApp::new( + |cfg: ClientConfig| Ok(CliContext::init(cfg.load()?)?), + crate::registry::registry_api(), + ) + .run(args) + { + match e.data { + Some(serde_json::Value::String(s)) => eprintln!("{}: {}", e.message, s), + Some(serde_json::Value::Object(o)) => { + if let Some(serde_json::Value::String(s)) = o.get("details") { + eprintln!("{}: {}", e.message, s); + if let Some(serde_json::Value::String(s)) = o.get("debug") { + tracing::debug!("{}", s) + } + } + } + Some(a) => eprintln!("{}: {}", e.message, a), + None => eprintln!("{}", e.message), + } + + std::process::exit(e.code); + } +} diff --git a/core/startos/src/bins/tunnel.rs b/core/startos/src/bins/tunnel.rs new file mode 100644 index 000000000..bf80848a5 --- /dev/null +++ b/core/startos/src/bins/tunnel.rs @@ -0,0 +1,117 @@ +use std::ffi::OsString; + +use clap::Parser; +use futures::FutureExt; +use rpc_toolkit::CliApp; +use tokio::signal::unix::signal; +use tracing::instrument; + +use crate::context::config::ClientConfig; +use crate::context::CliContext; +use crate::net::web_server::{Acceptor, WebServer}; +use crate::prelude::*; +use crate::tunnel::context::{TunnelConfig, TunnelContext}; +use crate::util::logger::LOGGER; + +#[instrument(skip_all)] +async fn inner_main(config: &TunnelConfig) -> Result<(), Error> { + let server = async { + let ctx = TunnelContext::init(config).await?; + let mut server = WebServer::new(Acceptor::bind([ctx.listen]).await?); + server.serve_tunnel(ctx.clone()); + + let mut shutdown_recv = ctx.shutdown.subscribe(); + + let sig_handler_ctx = ctx; + let sig_handler = tokio::spawn(async move { + use tokio::signal::unix::SignalKind; + futures::future::select_all( + [ + SignalKind::interrupt(), + SignalKind::quit(), + SignalKind::terminate(), + ] + .iter() + .map(|s| { + async move { + signal(*s) + .unwrap_or_else(|_| panic!("register {:?} handler", s)) + .recv() + .await + } + .boxed() + }), + ) + .await; + sig_handler_ctx + .shutdown + .send(()) + .map_err(|_| ()) + .expect("send shutdown signal"); + }); + + shutdown_recv + .recv() + .await + .with_kind(crate::ErrorKind::Unknown)?; + + sig_handler.abort(); + + Ok::<_, Error>(server) + } + .await?; + server.shutdown().await; + + Ok(()) +} + +pub fn main(args: impl IntoIterator) { + LOGGER.enable(); + + let config = TunnelConfig::parse_from(args).load().unwrap(); + + let res = { + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("failed to initialize runtime"); + rt.block_on(inner_main(&config)) + }; + + match res { + Ok(()) => (), + Err(e) => { + eprintln!("{}", e.source); + tracing::debug!("{:?}", e.source); + drop(e.source); + std::process::exit(e.kind as i32) + } + } +} + +pub fn cli(args: impl IntoIterator) { + LOGGER.enable(); + + if let Err(e) = CliApp::new( + |cfg: ClientConfig| Ok(CliContext::init(cfg.load()?)?), + crate::tunnel::tunnel_api(), + ) + .run(args) + { + match e.data { + Some(serde_json::Value::String(s)) => eprintln!("{}: {}", e.message, s), + Some(serde_json::Value::Object(o)) => { + if let Some(serde_json::Value::String(s)) = o.get("details") { + eprintln!("{}: {}", e.message, s); + if let Some(serde_json::Value::String(s)) = o.get("debug") { + tracing::debug!("{}", s) + } + } + } + Some(a) => eprintln!("{}: {}", e.message, a), + None => eprintln!("{}", e.message), + } + + std::process::exit(e.code); + } +} diff --git a/core/startos/src/context/cli.rs b/core/startos/src/context/cli.rs index dff20ddc1..533c627c9 100644 --- a/core/startos/src/context/cli.rs +++ b/core/startos/src/context/cli.rs @@ -1,9 +1,12 @@ use std::fs::File; use std::io::BufReader; +use std::net::SocketAddr; use std::path::{Path, PathBuf}; use std::sync::Arc; +use cookie::{Cookie, Expiration, SameSite}; use cookie_store::{CookieStore, RawCookie}; +use imbl_value::InternedString; use josekit::jwk::Jwk; use once_cell::sync::OnceCell; use reqwest::Proxy; @@ -19,9 +22,12 @@ use tracing::instrument; use super::setup::CURRENT_SECRET; use crate::context::config::{local_config_path, ClientConfig}; use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext}; -use crate::middleware::auth::LOCAL_AUTH_COOKIE_PATH; +use crate::developer::{default_developer_key_path, OS_DEVELOPER_KEY_PATH}; +use crate::middleware::auth::AuthContext; use crate::prelude::*; use crate::rpc_continuations::Guid; +use crate::tunnel::context::TunnelContext; +use crate::tunnel::TUNNEL_DEFAULT_PORT; #[derive(Debug)] pub struct CliContextSeed { @@ -29,6 +35,10 @@ pub struct CliContextSeed { pub base_url: Url, pub rpc_url: Url, pub registry_url: Option, + pub registry_hostname: Option, + pub registry_listen: Option, + pub tunnel_addr: Option, + pub tunnel_listen: Option, pub client: Client, pub cookie_store: Arc, pub cookie_path: PathBuf, @@ -55,9 +65,8 @@ impl Drop for CliContextSeed { true, ) .unwrap(); - let mut store = self.cookie_store.lock().unwrap(); - store.remove("localhost", "", "local"); - store.save_json(&mut *writer).unwrap(); + let store = self.cookie_store.lock().unwrap(); + cookie_store::serde::json::save(&store, &mut *writer).unwrap(); writer.sync_all().unwrap(); std::fs::rename(tmp, &self.cookie_path).unwrap(); } @@ -85,26 +94,14 @@ impl CliContext { .unwrap_or(Path::new("/")) .join(".cookies.json") }); - let cookie_store = Arc::new(CookieStoreMutex::new({ - let mut store = if cookie_path.exists() { - CookieStore::load_json(BufReader::new( - File::open(&cookie_path) - .with_ctx(|_| (ErrorKind::Filesystem, cookie_path.display()))?, - )) - .map_err(|e| eyre!("{}", e)) - .with_kind(crate::ErrorKind::Deserialization)? - } else { - CookieStore::default() - }; - if let Ok(local) = std::fs::read_to_string(LOCAL_AUTH_COOKIE_PATH) { - store - .insert_raw( - &RawCookie::new("local", local), - &"http://localhost".parse()?, - ) - .with_kind(crate::ErrorKind::Network)?; - } - store + let cookie_store = Arc::new(CookieStoreMutex::new(if cookie_path.exists() { + cookie_store::serde::json::load(BufReader::new( + File::open(&cookie_path) + .with_ctx(|_| (ErrorKind::Filesystem, cookie_path.display()))?, + )) + .unwrap_or_default() + } else { + CookieStore::default() })); Ok(CliContext(Arc::new(CliContextSeed { @@ -129,6 +126,10 @@ impl CliContext { Ok::<_, Error>(registry) }) .transpose()?, + registry_hostname: config.registry_hostname, + registry_listen: config.registry_listen, + tunnel_addr: config.tunnel, + tunnel_listen: config.tunnel_listen, client: { let mut builder = Client::builder().cookie_provider(cookie_store.clone()); if let Some(proxy) = config.proxy { @@ -139,14 +140,9 @@ impl CliContext { }, cookie_store, cookie_path, - developer_key_path: config.developer_key_path.unwrap_or_else(|| { - local_config_path() - .as_deref() - .unwrap_or_else(|| Path::new(super::config::CONFIG_PATH)) - .parent() - .unwrap_or(Path::new("/")) - .join("developer.key.pem") - }), + developer_key_path: config + .developer_key_path + .unwrap_or_else(default_developer_key_path), developer_key: OnceCell::new(), }))) } @@ -155,20 +151,26 @@ impl CliContext { #[instrument(skip_all)] pub fn developer_key(&self) -> Result<&ed25519_dalek::SigningKey, Error> { self.developer_key.get_or_try_init(|| { - if !self.developer_key_path.exists() { - return Err(Error::new(eyre!("Developer Key does not exist! Please run `start-cli init` before running this command."), crate::ErrorKind::Uninitialized)); - } - let pair = ::from_pkcs8_pem( - &std::fs::read_to_string(&self.developer_key_path)?, - ) - .with_kind(crate::ErrorKind::Pem)?; - let secret = ed25519_dalek::SecretKey::try_from(&pair.secret_key[..]).map_err(|_| { - Error::new( - eyre!("pkcs8 key is of incorrect length"), - ErrorKind::OpenSsl, + for path in [Path::new(OS_DEVELOPER_KEY_PATH), &self.developer_key_path] { + if !path.exists() { + continue; + } + let pair = ::from_pkcs8_pem( + &std::fs::read_to_string(&self.developer_key_path)?, ) - })?; - Ok(secret.into()) + .with_kind(crate::ErrorKind::Pem)?; + let secret = ed25519_dalek::SecretKey::try_from(&pair.secret_key[..]).map_err(|_| { + Error::new( + eyre!("pkcs8 key is of incorrect length"), + ErrorKind::OpenSsl, + ) + })?; + return Ok(secret.into()) + } + Err(Error::new( + eyre!("Developer Key does not exist! Please run `start-cli init` before running this command."), + crate::ErrorKind::Uninitialized + )) }) } @@ -276,27 +278,90 @@ impl Context for CliContext { } impl CallRemote for CliContext { async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { - call_remote_http(&self.client, self.rpc_url.clone(), method, params).await + if let Ok(local) = std::fs::read_to_string(RpcContext::LOCAL_AUTH_COOKIE_PATH) { + self.cookie_store + .lock() + .unwrap() + .insert_raw( + &Cookie::build(("local", local)) + .domain("localhost") + .expires(Expiration::Session) + .same_site(SameSite::Strict) + .build(), + &"http://localhost".parse()?, + ) + .with_kind(crate::ErrorKind::Network)?; + } + crate::middleware::signature::call_remote( + self, + self.rpc_url.clone(), + self.rpc_url.host_str().or_not_found("rpc url hostname")?, + method, + params, + ) + .await } } impl CallRemote for CliContext { async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { - call_remote_http(&self.client, self.rpc_url.clone(), method, params).await + if let Ok(local) = std::fs::read_to_string(TunnelContext::LOCAL_AUTH_COOKIE_PATH) { + self.cookie_store + .lock() + .unwrap() + .insert_raw( + &Cookie::build(("local", local)) + .domain("localhost") + .expires(Expiration::Session) + .same_site(SameSite::Strict) + .build(), + &"http://localhost".parse()?, + ) + .with_kind(crate::ErrorKind::Network)?; + } + crate::middleware::signature::call_remote( + self, + self.rpc_url.clone(), + self.rpc_url.host_str().or_not_found("rpc url hostname")?, + method, + params, + ) + .await } } impl CallRemote for CliContext { async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { - call_remote_http(&self.client, self.rpc_url.clone(), method, params).await + crate::middleware::signature::call_remote( + self, + self.rpc_url.clone(), + self.rpc_url.host_str().or_not_found("rpc url hostname")?, + method, + params, + ) + .await } } impl CallRemote for CliContext { async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { - call_remote_http(&self.client, self.rpc_url.clone(), method, params).await + crate::middleware::signature::call_remote( + self, + self.rpc_url.clone(), + self.rpc_url.host_str().or_not_found("rpc url hostname")?, + method, + params, + ) + .await } } impl CallRemote for CliContext { async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { - call_remote_http(&self.client, self.rpc_url.clone(), method, params).await + crate::middleware::signature::call_remote( + self, + self.rpc_url.clone(), + self.rpc_url.host_str().or_not_found("rpc url hostname")?, + method, + params, + ) + .await } } diff --git a/core/startos/src/context/config.rs b/core/startos/src/context/config.rs index b98b07f16..f89fd17a3 100644 --- a/core/startos/src/context/config.rs +++ b/core/startos/src/context/config.rs @@ -3,6 +3,7 @@ use std::net::SocketAddr; use std::path::{Path, PathBuf}; use clap::Parser; +use imbl_value::InternedString; use reqwest::Url; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; @@ -55,7 +56,6 @@ pub trait ContextConfig: DeserializeOwned + Default { #[derive(Debug, Default, Deserialize, Serialize, Parser)] #[serde(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")] -#[command(name = "start-cli")] #[command(version = crate::version::Current::default().semver().to_string())] pub struct ClientConfig { #[arg(short = 'c', long)] @@ -64,6 +64,14 @@ pub struct ClientConfig { pub host: Option, #[arg(short = 'r', long)] pub registry: Option, + #[arg(long)] + pub registry_hostname: Option, + #[arg(skip)] + pub registry_listen: Option, + #[arg(short = 't', long)] + pub tunnel: Option, + #[arg(skip)] + pub tunnel_listen: Option, #[arg(short = 'p', long)] pub proxy: Option, #[arg(long)] @@ -78,6 +86,8 @@ impl ContextConfig for ClientConfig { fn merge_with(&mut self, other: Self) { self.host = self.host.take().or(other.host); self.registry = self.registry.take().or(other.registry); + self.registry_hostname = self.registry_hostname.take().or(other.registry_hostname); + self.tunnel = self.tunnel.take().or(other.tunnel); self.proxy = self.proxy.take().or(other.proxy); self.cookie_path = self.cookie_path.take().or(other.cookie_path); self.developer_key_path = self.developer_key_path.take().or(other.developer_key_path); @@ -113,6 +123,8 @@ pub struct ServerConfig { pub disable_encryption: Option, #[arg(long)] pub multi_arch_s9pks: Option, + #[arg(long)] + pub developer_key_path: Option, } impl ContextConfig for ServerConfig { fn next(&mut self) -> Option { @@ -129,6 +141,7 @@ impl ContextConfig for ServerConfig { .or(other.revision_cache_size); self.disable_encryption = self.disable_encryption.take().or(other.disable_encryption); self.multi_arch_s9pks = self.multi_arch_s9pks.take().or(other.multi_arch_s9pks); + self.developer_key_path = self.developer_key_path.take().or(other.developer_key_path); } } diff --git a/core/startos/src/db/model/mod.rs b/core/startos/src/db/model/mod.rs index 4806d8621..b153ea44b 100644 --- a/core/startos/src/db/model/mod.rs +++ b/core/startos/src/db/model/mod.rs @@ -12,6 +12,7 @@ use crate::net::forward::AvailablePorts; use crate::net::keys::KeyStore; use crate::notifications::Notifications; use crate::prelude::*; +use crate::sign::AnyVerifyingKey; use crate::ssh::SshKeys; use crate::util::serde::Pem; @@ -33,6 +34,9 @@ impl Database { private: Private { key_store: KeyStore::new(account)?, password: account.password.clone(), + auth_pubkeys: [AnyVerifyingKey::Ed25519((&account.developer_key).into())] + .into_iter() + .collect(), ssh_privkey: Pem(account.ssh_key.clone()), ssh_pubkeys: SshKeys::new(), available_ports: AvailablePorts::new(), @@ -40,7 +44,7 @@ impl Database { notifications: Notifications::new(), cifs: CifsTargets::new(), package_stores: BTreeMap::new(), - compat_s9pk_key: Pem(account.compat_s9pk_key.clone()), + developer_key: Pem(account.developer_key.clone()), }, // TODO }) } diff --git a/core/startos/src/db/model/private.rs b/core/startos/src/db/model/private.rs index 2108b32a4..47de5093a 100644 --- a/core/startos/src/db/model/private.rs +++ b/core/startos/src/db/model/private.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashSet}; use models::PackageId; use patch_db::{HasModel, Value}; @@ -10,6 +10,7 @@ use crate::net::forward::AvailablePorts; use crate::net::keys::KeyStore; use crate::notifications::Notifications; use crate::prelude::*; +use crate::sign::AnyVerifyingKey; use crate::ssh::SshKeys; use crate::util::serde::Pem; @@ -19,8 +20,9 @@ use crate::util::serde::Pem; pub struct Private { pub key_store: KeyStore, pub password: String, // argon2 hash - #[serde(default = "generate_compat_key")] - pub compat_s9pk_key: Pem, + pub auth_pubkeys: HashSet, + #[serde(default = "generate_developer_key")] + pub developer_key: Pem, pub ssh_privkey: Pem, pub ssh_pubkeys: SshKeys, pub available_ports: AvailablePorts, @@ -31,7 +33,7 @@ pub struct Private { pub package_stores: BTreeMap, } -pub fn generate_compat_key() -> Pem { +pub fn generate_developer_key() -> Pem { Pem(ed25519_dalek::SigningKey::generate( &mut ssh_key::rand_core::OsRng::default(), )) diff --git a/core/startos/src/developer/mod.rs b/core/startos/src/developer/mod.rs index de71b0fc0..c5cf864e2 100644 --- a/core/startos/src/developer/mod.rs +++ b/core/startos/src/developer/mod.rs @@ -1,40 +1,57 @@ -use std::fs::File; -use std::io::Write; -use std::path::Path; +use std::path::{Path, PathBuf}; use ed25519::pkcs8::EncodePrivateKey; use ed25519::PublicKeyBytes; use ed25519_dalek::{SigningKey, VerifyingKey}; +use tokio::io::AsyncWriteExt; use tracing::instrument; +use crate::context::config::local_config_path; use crate::context::CliContext; use crate::prelude::*; +use crate::util::io::create_file_mod; use crate::util::serde::Pem; +pub const OS_DEVELOPER_KEY_PATH: &str = "/run/startos/developer.key.pem"; + +pub fn default_developer_key_path() -> PathBuf { + local_config_path() + .as_deref() + .unwrap_or_else(|| Path::new(crate::context::config::CONFIG_PATH)) + .parent() + .unwrap_or(Path::new("/")) + .join("developer.key.pem") +} + +pub async fn write_developer_key( + secret: &ed25519_dalek::SigningKey, + path: impl AsRef, +) -> Result<(), Error> { + let keypair_bytes = ed25519::KeypairBytes { + secret_key: secret.to_bytes(), + public_key: Some(PublicKeyBytes(VerifyingKey::from(secret).to_bytes())), + }; + let mut file = create_file_mod(path, 0o046).await?; + file.write_all( + keypair_bytes + .to_pkcs8_pem(base64ct::LineEnding::default()) + .with_kind(crate::ErrorKind::Pem)? + .as_bytes(), + ) + .await?; + file.sync_all().await?; + Ok(()) +} + #[instrument(skip_all)] -pub fn init(ctx: CliContext) -> Result<(), Error> { - if !ctx.developer_key_path.exists() { - let parent = ctx.developer_key_path.parent().unwrap_or(Path::new("/")); - if !parent.exists() { - std::fs::create_dir_all(parent) - .with_ctx(|_| (crate::ErrorKind::Filesystem, parent.display().to_string()))?; - } +pub async fn init(ctx: CliContext) -> Result<(), Error> { + if tokio::fs::metadata(OS_DEVELOPER_KEY_PATH).await.is_ok() { + println!("Developer key already exists at {}", OS_DEVELOPER_KEY_PATH); + } else if tokio::fs::metadata(&ctx.developer_key_path).await.is_err() { tracing::info!("Generating new developer key..."); let secret = SigningKey::generate(&mut ssh_key::rand_core::OsRng::default()); tracing::info!("Writing key to {}", ctx.developer_key_path.display()); - let keypair_bytes = ed25519::KeypairBytes { - secret_key: secret.to_bytes(), - public_key: Some(PublicKeyBytes(VerifyingKey::from(&secret).to_bytes())), - }; - let mut dev_key_file = File::create(&ctx.developer_key_path) - .with_ctx(|_| (ErrorKind::Filesystem, ctx.developer_key_path.display()))?; - dev_key_file.write_all( - keypair_bytes - .to_pkcs8_pem(base64ct::LineEnding::default()) - .with_kind(crate::ErrorKind::Pem)? - .as_bytes(), - )?; - dev_key_file.sync_all()?; + write_developer_key(&secret, &ctx.developer_key_path).await?; println!( "New developer key generated at {}", ctx.developer_key_path.display() diff --git a/core/startos/src/init.rs b/core/startos/src/init.rs index 55b6ef462..6f1c7f011 100644 --- a/core/startos/src/init.rs +++ b/core/startos/src/init.rs @@ -1,18 +1,14 @@ -use std::fs::Permissions; use std::io::Cursor; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; -use std::os::unix::fs::PermissionsExt; use std::path::Path; use std::sync::Arc; use std::time::{Duration, SystemTime}; -use axum::extract::ws::{self}; -use color_eyre::eyre::eyre; +use axum::extract::ws; use const_format::formatcp; use futures::{StreamExt, TryStreamExt}; use itertools::Itertools; use models::ResultExt; -use rand::random; use rpc_toolkit::{from_fn_async, Context, Empty, HandlerArgs, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use tokio::process::Command; @@ -21,12 +17,12 @@ use ts_rs::TS; use crate::account::AccountInfo; use crate::context::config::ServerConfig; -use crate::context::{CliContext, InitContext}; +use crate::context::{CliContext, InitContext, RpcContext}; use crate::db::model::public::ServerStatus; use crate::db::model::Database; -use crate::disk::mount::util::unmount; +use crate::developer::OS_DEVELOPER_KEY_PATH; use crate::hostname::Hostname; -use crate::middleware::auth::LOCAL_AUTH_COOKIE_PATH; +use crate::middleware::auth::AuthContext; use crate::net::net_controller::{NetController, NetService}; use crate::net::utils::find_wifi_iface; use crate::net::web_server::{UpgradableListener, WebServerAcceptorSetter}; @@ -38,7 +34,7 @@ use crate::rpc_continuations::{Guid, RpcContinuation}; use crate::s9pk::v2::pack::{CONTAINER_DATADIR, CONTAINER_TOOL}; use crate::ssh::SSH_DIR; use crate::system::{get_mem_info, sync_kiosk}; -use crate::util::io::{create_file, open_file, IOHook}; +use crate::util::io::{open_file, IOHook}; use crate::util::lshw::lshw; use crate::util::net::WebSocketExt; use crate::util::{cpupower, Invoke}; @@ -167,28 +163,7 @@ pub async fn init( } local_auth.start(); - tokio::fs::create_dir_all("/run/startos") - .await - .with_ctx(|_| (crate::ErrorKind::Filesystem, "mkdir -p /run/startos"))?; - 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(0o046)).await?; - Command::new("chown") - .arg("root:startos") - .arg(LOCAL_AUTH_COOKIE_PATH) - .invoke(crate::ErrorKind::Filesystem) - .await?; - } + RpcContext::init_auth_cookie().await?; local_auth.complete(); load_database.start(); @@ -199,6 +174,16 @@ pub async fn init( load_database.complete(); load_ssh_keys.start(); + crate::developer::write_developer_key( + &peek.as_private().as_developer_key().de()?.0, + OS_DEVELOPER_KEY_PATH, + ) + .await?; + Command::new("chown") + .arg("root:startos") + .arg(OS_DEVELOPER_KEY_PATH) + .invoke(ErrorKind::Filesystem) + .await?; crate::ssh::sync_keys( &Hostname(peek.as_public().as_server_info().as_hostname().de()?), &peek.as_private().as_ssh_privkey().de()?, @@ -206,6 +191,13 @@ pub async fn init( SSH_DIR, ) .await?; + crate::ssh::sync_keys( + &Hostname(peek.as_public().as_server_info().as_hostname().de()?), + &peek.as_private().as_ssh_privkey().de()?, + &Default::default(), + "/root/.ssh", + ) + .await?; load_ssh_keys.complete(); let account = AccountInfo::load(&peek)?; diff --git a/core/startos/src/install/mod.rs b/core/startos/src/install/mod.rs index 2adac3169..c290bba03 100644 --- a/core/startos/src/install/mod.rs +++ b/core/startos/src/install/mod.rs @@ -253,7 +253,7 @@ pub async fn sideload( .await; tokio::spawn(async move { if let Err(e) = async { - let key = ctx.db.peek().await.into_private().into_compat_s9pk_key(); + let key = ctx.db.peek().await.into_private().into_developer_key(); ctx.services .install( diff --git a/core/startos/src/lib.rs b/core/startos/src/lib.rs index 86bba8264..cfd9125e2 100644 --- a/core/startos/src/lib.rs +++ b/core/startos/src/lib.rs @@ -60,10 +60,12 @@ pub mod s9pk; pub mod service; pub mod setup; pub mod shutdown; +pub mod sign; pub mod sound; pub mod ssh; pub mod status; pub mod system; +pub mod tunnel; pub mod update; pub mod upload; pub mod util; @@ -152,9 +154,8 @@ pub fn main_api() -> ParentHandler { ) .subcommand( "auth", - auth::auth::().with_about( - "Commands related to Authentication i.e. login, logout, reset-password", - ), + auth::auth::() + .with_about("Commands related to Authentication i.e. login, logout"), ) .subcommand( "db", @@ -582,7 +583,7 @@ pub fn expanded_api() -> ParentHandler { main_api() .subcommand( "init", - from_fn_blocking(developer::init) + from_fn_async(developer::init) .no_display() .with_about("Create developer key if it doesn't exist"), ) diff --git a/core/startos/src/middleware/auth.rs b/core/startos/src/middleware/auth.rs index a43f22529..5c057ebc2 100644 --- a/core/startos/src/middleware/auth.rs +++ b/core/startos/src/middleware/auth.rs @@ -1,11 +1,13 @@ use std::borrow::Borrow; use std::collections::BTreeSet; +use std::future::Future; use std::ops::Deref; use std::sync::Arc; use std::time::{Duration, Instant}; use axum::extract::Request; use axum::response::Response; +use base64::Engine; use basic_cookies::Cookie; use chrono::Utc; use color_eyre::eyre::eyre; @@ -13,17 +15,131 @@ use digest::Digest; use helpers::const_true; use http::header::{COOKIE, USER_AGENT}; use http::HeaderValue; -use imbl_value::InternedString; +use imbl_value::{json, InternedString}; +use rand::random; use rpc_toolkit::yajrc::INTERNAL_ERROR; use rpc_toolkit::{Middleware, RpcRequest, RpcResponse}; use serde::{Deserialize, Serialize}; use sha2::Sha256; +use tokio::io::AsyncWriteExt; +use tokio::process::Command; use tokio::sync::Mutex; +use crate::auth::{check_password, write_shadow, Sessions}; use crate::context::RpcContext; +use crate::db::model::Database; +use crate::middleware::signature::{SignatureAuth, SignatureAuthContext}; use crate::prelude::*; +use crate::rpc_continuations::OpenAuthedContinuations; +use crate::sign::AnyVerifyingKey; +use crate::util::io::{create_file_mod, read_file_to_string}; +use crate::util::iter::TransposeResultIterExt; +use crate::util::serde::BASE64; +use crate::util::sync::SyncMutex; +use crate::util::Invoke; -pub const LOCAL_AUTH_COOKIE_PATH: &str = "/run/startos/rpc.authcookie"; +pub trait AuthContext: SignatureAuthContext { + const LOCAL_AUTH_COOKIE_PATH: &str; + const LOCAL_AUTH_COOKIE_OWNERSHIP: &str; + fn init_auth_cookie() -> impl Future> + Send { + async { + let mut file = create_file_mod(Self::LOCAL_AUTH_COOKIE_PATH, 0o046).await?; + file.write_all(BASE64.encode(random::<[u8; 32]>()).as_bytes()) + .await?; + file.sync_all().await?; + drop(file); + Command::new("chown") + .arg(Self::LOCAL_AUTH_COOKIE_OWNERSHIP) + .arg(Self::LOCAL_AUTH_COOKIE_PATH) + .invoke(crate::ErrorKind::Filesystem) + .await?; + Ok(()) + } + } + fn ephemeral_sessions(&self) -> &SyncMutex; + fn open_authed_continuations(&self) -> &OpenAuthedContinuations>; + fn access_sessions(db: &mut Model) -> &mut Model; + fn check_password(db: &Model, password: &str) -> Result<(), Error>; + #[allow(unused_variables)] + fn post_login_hook(&self, password: &str) -> impl Future> + Send { + async { Ok(()) } + } +} + +impl SignatureAuthContext for RpcContext { + type Database = Database; + type AdditionalMetadata = (); + type CheckPubkeyRes = (); + fn db(&self) -> &TypedPatchDb { + &self.db + } + async fn sig_context( + &self, + ) -> impl IntoIterator + Send, Error>> + Send { + let peek = self.db.peek().await; + self.account + .read() + .await + .hostnames() + .into_iter() + .map(Ok) + .chain( + peek.as_public() + .as_server_info() + .as_network() + .as_host() + .as_domains() + .keys() + .map(|k| k.into_iter()) + .transpose(), + ) + .collect::>() + } + fn check_pubkey( + db: &Model, + pubkey: Option<&AnyVerifyingKey>, + _: Self::AdditionalMetadata, + ) -> Result { + if let Some(pubkey) = pubkey { + if db.as_private().as_auth_pubkeys().de()?.contains(pubkey) { + return Ok(()); + } + } + + Err(Error::new( + eyre!("Developer Key is not authorized"), + ErrorKind::IncorrectPassword, + )) + } + async fn post_auth_hook(&self, _: Self::CheckPubkeyRes, _: &RpcRequest) -> Result<(), Error> { + Ok(()) + } +} +impl AuthContext for RpcContext { + const LOCAL_AUTH_COOKIE_PATH: &str = "/run/startos/rpc.authcookie"; + const LOCAL_AUTH_COOKIE_OWNERSHIP: &str = "root:startos"; + fn ephemeral_sessions(&self) -> &SyncMutex { + &self.ephemeral_sessions + } + fn open_authed_continuations(&self) -> &OpenAuthedContinuations> { + &self.open_authed_continuations + } + fn access_sessions(db: &mut Model) -> &mut Model { + db.as_private_mut().as_sessions_mut() + } + fn check_password(db: &Model, password: &str) -> Result<(), Error> { + check_password(&db.as_private().as_password().de()?, password) + } + async fn post_login_hook(&self, password: &str) -> Result<(), Error> { + if tokio::fs::metadata("/media/startos/config/overlay/etc/shadow") + .await + .is_err() + { + write_shadow(&password).await?; + } + Ok(()) + } +} #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -40,25 +156,25 @@ pub trait AsLogoutSessionId { pub struct HasLoggedOutSessions(()); impl HasLoggedOutSessions { - pub async fn new( + pub async fn new( sessions: impl IntoIterator, - ctx: &RpcContext, + ctx: &C, ) -> Result { let to_log_out: BTreeSet<_> = sessions .into_iter() .map(|s| s.as_logout_session_id()) .collect(); for sid in &to_log_out { - ctx.open_authed_continuations.kill(&Some(sid.clone())) + ctx.open_authed_continuations().kill(&Some(sid.clone())) } - ctx.ephemeral_sessions.mutate(|s| { + ctx.ephemeral_sessions().mutate(|s| { for sid in &to_log_out { s.0.remove(sid); } }); - ctx.db + ctx.db() .mutate(|db| { - let sessions = db.as_private_mut().as_sessions_mut(); + let sessions = C::access_sessions(db); for sid in &to_log_out { sessions.remove(sid)?; } @@ -82,9 +198,9 @@ enum SessionType { } impl HasValidSession { - pub async fn from_header( + pub async fn from_header( header: Option<&HeaderValue>, - ctx: &RpcContext, + ctx: &C, ) -> Result { if let Some(cookie_header) = header { let cookies = Cookie::parse( @@ -94,7 +210,7 @@ impl HasValidSession { ) .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 { + if let Ok(s) = Self::from_local::(cookie).await { return Ok(s); } } @@ -111,12 +227,12 @@ impl HasValidSession { )) } - pub async fn from_session( + pub async fn from_session( session_token: HashSessionToken, - ctx: &RpcContext, + ctx: &C, ) -> Result { let session_hash = session_token.hashed(); - if !ctx.ephemeral_sessions.mutate(|s| { + if !ctx.ephemeral_sessions().mutate(|s| { if let Some(session) = s.0.get_mut(session_hash) { session.last_active = Utc::now(); true @@ -124,10 +240,9 @@ impl HasValidSession { false } }) { - ctx.db + ctx.db() .mutate(|db| { - db.as_private_mut() - .as_sessions_mut() + C::access_sessions(db) .as_idx_mut(session_hash) .ok_or_else(|| { Error::new(eyre!("UNAUTHORIZED"), crate::ErrorKind::Authorization) @@ -143,8 +258,8 @@ impl HasValidSession { Ok(Self(SessionType::Session(session_token))) } - pub async fn from_local(local: &Cookie<'_>) -> Result { - let token = tokio::fs::read_to_string(LOCAL_AUTH_COOKIE_PATH).await?; + pub async fn from_local(local: &Cookie<'_>) -> Result { + let token = read_file_to_string(C::LOCAL_AUTH_COOKIE_PATH).await?; if local.get_value() == &*token { Ok(Self(SessionType::Local)) } else { @@ -258,6 +373,8 @@ pub struct Metadata { login: bool, #[serde(default)] get_session: bool, + #[serde(default)] + get_signer: bool, } #[derive(Clone)] @@ -267,6 +384,7 @@ pub struct Auth { is_login: bool, set_cookie: Option, user_agent: Option, + signature_auth: SignatureAuth, } impl Auth { pub fn new() -> Self { @@ -276,62 +394,73 @@ impl Auth { is_login: false, set_cookie: None, user_agent: None, + signature_auth: SignatureAuth::new(), } } } -impl Middleware for Auth { +impl Middleware for Auth { type Metadata = Metadata; async fn process_http_request( &mut self, - _: &RpcContext, + context: &C, request: &mut Request, ) -> Result<(), Response> { self.cookie = request.headers_mut().remove(COOKIE); self.user_agent = request.headers_mut().remove(USER_AGENT); + self.signature_auth + .process_http_request(context, request) + .await?; Ok(()) } async fn process_rpc_request( &mut self, - context: &RpcContext, + context: &C, metadata: Self::Metadata, request: &mut RpcRequest, ) -> Result<(), RpcResponse> { - if metadata.login { - self.is_login = true; - let guard = self.rate_limiter.lock().await; - if guard.1.elapsed() < Duration::from_secs(20) && guard.0 >= 3 { - return Err(RpcResponse { - id: request.id.take(), - result: Err(Error::new( + async { + if metadata.login { + self.is_login = true; + let guard = self.rate_limiter.lock().await; + if guard.1.elapsed() < Duration::from_secs(20) && guard.0 >= 3 { + return Err(Error::new( eyre!("Please limit login attempts to 3 per 20 seconds."), crate::ErrorKind::RateLimited, - ) - .into()), - }); - } - if let Some(user_agent) = self.user_agent.as_ref().and_then(|h| h.to_str().ok()) { - request.params["__auth_userAgent"] = Value::String(Arc::new(user_agent.to_owned())) - // TODO: will this panic? - } - } else if metadata.authenticated { - match HasValidSession::from_header(self.cookie.as_ref(), &context).await { - Err(e) => { - return Err(RpcResponse { - id: request.id.take(), - result: Err(e.into()), - }) + )); } - Ok(HasValidSession(SessionType::Session(s))) if metadata.get_session => { - request.params["__auth_session"] = - Value::String(Arc::new(s.hashed().deref().to_owned())); + if let Some(user_agent) = self.user_agent.as_ref().and_then(|h| h.to_str().ok()) { + request.params["__auth_userAgent"] = + Value::String(Arc::new(user_agent.to_owned())) // TODO: will this panic? } - _ => (), + } else if metadata.authenticated { + if self + .signature_auth + .process_rpc_request( + context, + from_value(json!({ + "get_signer": metadata.get_signer + }))?, + request, + ) + .await + .is_err() + { + match HasValidSession::from_header(self.cookie.as_ref(), context).await? { + HasValidSession(SessionType::Session(s)) if metadata.get_session => { + request.params["__auth_session"] = + Value::String(Arc::new(s.hashed().deref().to_owned())); + } + _ => (), + } + } } + Ok(()) } - Ok(()) + .await + .map_err(|e| RpcResponse::from_result(Err(e))) } - async fn process_rpc_response(&mut self, _: &RpcContext, response: &mut RpcResponse) { + async fn process_rpc_response(&mut self, _: &C, response: &mut RpcResponse) { if self.is_login { let mut guard = self.rate_limiter.lock().await; if guard.1.elapsed() < Duration::from_secs(20) { @@ -349,7 +478,7 @@ impl Middleware for Auth { let login_res = from_value::(res.clone())?; self.set_cookie = Some( HeaderValue::from_str(&format!( - "session={}; Path=/; SameSite=Lax; Expires=Fri, 31 Dec 9999 23:59:59 GMT;", + "session={}; Path=/; SameSite=Strict; Expires=Fri, 31 Dec 9999 23:59:59 GMT;", login_res.session )) .with_kind(crate::ErrorKind::Network)?, @@ -361,7 +490,7 @@ impl Middleware for Auth { } } } - async fn process_http_response(&mut self, _: &RpcContext, response: &mut Response) { + async fn process_http_response(&mut self, _: &C, response: &mut Response) { if let Some(set_cookie) = self.set_cookie.take() { response.headers_mut().insert("set-cookie", set_cookie); } diff --git a/core/startos/src/middleware/mod.rs b/core/startos/src/middleware/mod.rs index 3438dc3db..c1b8cb573 100644 --- a/core/startos/src/middleware/mod.rs +++ b/core/startos/src/middleware/mod.rs @@ -1,3 +1,4 @@ pub mod auth; pub mod cors; pub mod db; +pub mod signature; diff --git a/core/startos/src/registry/auth.rs b/core/startos/src/middleware/signature.rs similarity index 53% rename from core/startos/src/registry/auth.rs rename to core/startos/src/middleware/signature.rs index 4707bf809..9b651b73a 100644 --- a/core/startos/src/registry/auth.rs +++ b/core/startos/src/middleware/signature.rs @@ -1,45 +1,62 @@ use std::collections::BTreeMap; +use std::future::Future; use std::sync::Arc; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use axum::body::Body; use axum::extract::Request; -use axum::response::Response; -use chrono::Utc; use http::HeaderValue; use rpc_toolkit::yajrc::RpcError; -use rpc_toolkit::{Middleware, RpcRequest, RpcResponse}; -use serde::{Deserialize, Serialize}; -use tokio::io::AsyncWriteExt; +use rpc_toolkit::{Context, Middleware, RpcRequest, RpcResponse}; +use serde::de::DeserializeOwned; +use serde::Deserialize; use tokio::sync::Mutex; -use ts_rs::TS; use url::Url; +use crate::context::CliContext; use crate::prelude::*; -use crate::registry::context::RegistryContext; -use crate::registry::signer::commitment::request::RequestCommitment; -use crate::registry::signer::commitment::Commitment; -use crate::registry::signer::sign::{ - AnySignature, AnySigningKey, AnyVerifyingKey, SignatureScheme, -}; +use crate::sign::commitment::request::RequestCommitment; +use crate::sign::commitment::Commitment; +use crate::sign::{AnySignature, AnySigningKey, AnyVerifyingKey, SignatureScheme}; use crate::util::serde::Base64; -pub const AUTH_SIG_HEADER: &str = "X-StartOS-Registry-Auth-Sig"; +pub trait SignatureAuthContext: Context { + type Database: HasModel> + Send + Sync; + type AdditionalMetadata: DeserializeOwned + Send; + type CheckPubkeyRes: Send; + fn db(&self) -> &TypedPatchDb; + fn sig_context( + &self, + ) -> impl Future + Send, Error>> + Send> + + Send; + fn check_pubkey( + db: &Model, + pubkey: Option<&AnyVerifyingKey>, + metadata: Self::AdditionalMetadata, + ) -> Result; + fn post_auth_hook( + &self, + check_pubkey_res: Self::CheckPubkeyRes, + request: &RpcRequest, + ) -> impl Future> + Send; +} + +pub const AUTH_SIG_HEADER: &str = "X-StartOS-Auth-Sig"; #[derive(Deserialize)] -pub struct Metadata { - #[serde(default)] - admin: bool, +pub struct Metadata { + #[serde(flatten)] + additional: Additional, #[serde(default)] get_signer: bool, } #[derive(Clone)] -pub struct Auth { +pub struct SignatureAuth { nonce_cache: Arc>>, // for replay protection signer: Option>, } -impl Auth { +impl SignatureAuth { pub fn new() -> Self { Self { nonce_cache: Arc::new(Mutex::new(BTreeMap::new())), @@ -65,15 +82,6 @@ impl Auth { } } -#[derive(Serialize, Deserialize, TS)] -pub struct RegistryAdminLogRecord { - pub timestamp: String, - pub name: String, - #[ts(type = "{ id: string | number | null; method: string; params: any }")] - pub request: RpcRequest, - pub key: AnyVerifyingKey, -} - pub struct SignatureHeader { pub commitment: RequestCommitment, pub signer: AnyVerifyingKey, @@ -120,13 +128,13 @@ impl SignatureHeader { } } -impl Middleware for Auth { - type Metadata = Metadata; +impl Middleware for SignatureAuth { + type Metadata = Metadata; async fn process_http_request( &mut self, - ctx: &RegistryContext, + context: &C, request: &mut Request, - ) -> Result<(), Response> { + ) -> Result<(), axum::response::Response> { if request.headers().contains_key(AUTH_SIG_HEADER) { self.signer = Some( async { @@ -138,15 +146,27 @@ impl Middleware for Auth { request .headers() .get(AUTH_SIG_HEADER) - .or_not_found("missing X-StartOS-Registry-Auth-Sig") + .or_not_found(AUTH_SIG_HEADER) .with_kind(ErrorKind::InvalidRequest)?, )?; - signer.scheme().verify_commitment( - &signer, - &commitment, - &ctx.hostname, - &signature, + context.sig_context().await.into_iter().fold( + Err(Error::new( + eyre!("no valid signature context available to verify"), + ErrorKind::Authorization, + )), + |acc, x| { + if acc.is_ok() { + acc + } else { + signer.scheme().verify_commitment( + &signer, + &commitment, + x?.as_ref(), + &signature, + ) + } + }, )?; let now = SystemTime::now() @@ -175,48 +195,83 @@ impl Middleware for Auth { } async fn process_rpc_request( &mut self, - ctx: &RegistryContext, + context: &C, metadata: Self::Metadata, request: &mut RpcRequest, ) -> Result<(), RpcResponse> { - async move { + async { let signer = self.signer.take().transpose()?; if metadata.get_signer { if let Some(signer) = &signer { request.params["__auth_signer"] = to_value(signer)?; } } - if metadata.admin { - let signer = signer - .ok_or_else(|| Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization))?; - let db = ctx.db.peek().await; - let (guid, admin) = db.as_index().as_signers().get_signer_info(&signer)?; - if db.into_admins().de()?.contains(&guid) { - let mut log = tokio::fs::OpenOptions::new() - .create(true) - .append(true) - .open(ctx.datadir.join("admin.log")) - .await?; - log.write_all( - (serde_json::to_string(&RegistryAdminLogRecord { - timestamp: Utc::now().to_rfc3339(), - name: admin.name, - request: request.clone(), - key: signer, - }) - .with_kind(ErrorKind::Serialization)? - + "\n") - .as_bytes(), - ) - .await?; - } else { - return Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization)); - } - } - + let db = context.db().peek().await; + let res = C::check_pubkey(&db, signer.as_ref(), metadata.additional)?; + context.post_auth_hook(res, request).await?; Ok(()) } .await - .map_err(|e| RpcResponse::from_result(Err(e))) + .map_err(|e: Error| rpc_toolkit::RpcResponse::from_result(Err(e))) + } +} + +pub async fn call_remote( + ctx: &CliContext, + url: Url, + sig_context: &str, + method: &str, + params: Value, +) -> Result { + use reqwest::header::{ACCEPT, CONTENT_LENGTH, CONTENT_TYPE}; + use reqwest::Method; + use rpc_toolkit::yajrc::{GenericRpcMethod, Id, RpcRequest}; + use rpc_toolkit::RpcResponse; + + let rpc_req = RpcRequest { + id: Some(Id::Number(0.into())), + method: GenericRpcMethod::<_, _, Value>::new(method), + params, + }; + let body = serde_json::to_vec(&rpc_req)?; + let mut req = ctx + .client + .request(Method::POST, url) + .header(CONTENT_TYPE, "application/json") + .header(ACCEPT, "application/json") + .header(CONTENT_LENGTH, body.len()); + if let Ok(key) = ctx.developer_key() { + req = req.header( + AUTH_SIG_HEADER, + SignatureHeader::sign(&AnySigningKey::Ed25519(key.clone()), &body, sig_context)? + .to_header(), + ); + } + let res = req.body(body).send().await?; + + if !res.status().is_success() { + let status = res.status(); + let txt = res.text().await?; + let mut res = Err(Error::new( + eyre!("{}", status.canonical_reason().unwrap_or(status.as_str())), + ErrorKind::Network, + )); + if !txt.is_empty() { + res = res.with_ctx(|_| (ErrorKind::Network, txt)); + } + return res.map_err(From::from); + } + + match res + .headers() + .get(CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + { + Some("application/json") => { + serde_json::from_slice::(&*res.bytes().await?) + .with_kind(ErrorKind::Deserialization)? + .result + } + _ => Err(Error::new(eyre!("unknown content type"), ErrorKind::Network).into()), } } diff --git a/core/startos/src/net/acme.rs b/core/startos/src/net/acme.rs index 57bcfe25e..cf5b5fdf6 100644 --- a/core/startos/src/net/acme.rs +++ b/core/startos/src/net/acme.rs @@ -257,7 +257,8 @@ pub async fn init( ctx.db .mutate(|db| { db.as_public_mut() - .as_server_info_mut().as_network_mut() + .as_server_info_mut() + .as_network_mut() .as_acme_mut() .insert(&provider, &AcmeSettings { contact }) }) @@ -279,7 +280,8 @@ pub async fn remove( ctx.db .mutate(|db| { db.as_public_mut() - .as_server_info_mut().as_network_mut() + .as_server_info_mut() + .as_network_mut() .as_acme_mut() .remove(&provider) }) diff --git a/core/startos/src/net/static_server.rs b/core/startos/src/net/static_server.rs index ab38a0d71..ca19990d1 100644 --- a/core/startos/src/net/static_server.rs +++ b/core/startos/src/net/static_server.rs @@ -37,7 +37,7 @@ use crate::middleware::auth::{Auth, HasValidSession}; use crate::middleware::cors::Cors; use crate::middleware::db::SyncDb; use crate::prelude::*; -use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; +use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment; use crate::rpc_continuations::{Guid, RpcContinuations}; use crate::s9pk::merkle_archive::source::http::HttpSource; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; @@ -55,10 +55,10 @@ const INTERNAL_SERVER_ERROR: &[u8] = b"Internal Server Error"; const PROXY_STRIP_HEADERS: &[&str] = &["cookie", "host", "origin", "referer", "user-agent"]; -#[cfg(all(feature = "daemon", not(feature = "test")))] +#[cfg(all(feature = "startd", not(feature = "test")))] const EMBEDDED_UIS: Dir<'_> = include_dir::include_dir!("$CARGO_MANIFEST_DIR/../../web/dist/static"); -#[cfg(not(all(feature = "daemon", not(feature = "test"))))] +#[cfg(not(all(feature = "startd", not(feature = "test"))))] const EMBEDDED_UIS: Dir<'_> = Dir::new("", &[]); #[derive(Clone)] diff --git a/core/startos/src/registry/admin.rs b/core/startos/src/registry/admin.rs index 7619cdebc..04e7fdfc4 100644 --- a/core/startos/src/registry/admin.rs +++ b/core/startos/src/registry/admin.rs @@ -10,7 +10,7 @@ use ts_rs::TS; use crate::context::CliContext; use crate::prelude::*; use crate::registry::context::RegistryContext; -use crate::registry::signer::sign::AnyVerifyingKey; +use crate::sign::AnyVerifyingKey; use crate::registry::signer::{ContactInfo, SignerInfo}; use crate::registry::RegistryDatabase; use crate::rpc_continuations::Guid; diff --git a/core/startos/src/registry/asset.rs b/core/startos/src/registry/asset.rs index f79dee343..274da5955 100644 --- a/core/startos/src/registry/asset.rs +++ b/core/startos/src/registry/asset.rs @@ -11,9 +11,9 @@ use url::Url; use crate::prelude::*; use crate::progress::PhaseProgressTrackerHandle; -use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; -use crate::registry::signer::commitment::{Commitment, Digestable}; -use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey}; +use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment; +use crate::sign::commitment::{Commitment, Digestable}; +use crate::sign::{AnySignature, AnyVerifyingKey}; use crate::registry::signer::AcceptSigners; use crate::s9pk::merkle_archive::source::http::HttpSource; use crate::s9pk::merkle_archive::source::{ArchiveSource, Section}; diff --git a/core/startos/src/registry/context.rs b/core/startos/src/registry/context.rs index 1da189c91..402f03bb3 100644 --- a/core/startos/src/registry/context.rs +++ b/core/startos/src/registry/context.rs @@ -3,26 +3,33 @@ use std::ops::Deref; use std::path::{Path, PathBuf}; use std::sync::Arc; +use chrono::Utc; use clap::Parser; use imbl_value::InternedString; use patch_db::PatchDb; use reqwest::{Client, Proxy}; use rpc_toolkit::yajrc::RpcError; -use rpc_toolkit::{CallRemote, Context, Empty}; +use rpc_toolkit::{CallRemote, Context, Empty, RpcRequest}; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use tokio::sync::broadcast::Sender; use tracing::instrument; +use ts_rs::TS; use url::Url; use crate::context::config::{ContextConfig, CONFIG_PATH}; use crate::context::{CliContext, RpcContext}; +use crate::middleware::signature::SignatureAuthContext; use crate::prelude::*; -use crate::registry::auth::{SignatureHeader, AUTH_SIG_HEADER}; use crate::registry::device_info::{DeviceInfo, DEVICE_INFO_HEADER}; -use crate::registry::signer::sign::AnySigningKey; +use crate::registry::signer::SignerInfo; use crate::registry::RegistryDatabase; use crate::rpc_continuations::RpcContinuations; +use crate::sign::AnyVerifyingKey; +use crate::util::io::append_file; + +const DEFAULT_REGISTRY_LISTEN: SocketAddr = + SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::LOCALHOST), 5959); #[derive(Debug, Clone, Default, Deserialize, Serialize, Parser)] #[serde(rename_all = "kebab-case")] @@ -31,9 +38,9 @@ pub struct RegistryConfig { #[arg(short = 'c', long = "config")] pub config: Option, #[arg(short = 'l', long = "listen")] - pub listen: Option, - #[arg(short = 'h', long = "hostname")] - pub hostname: Option, + pub registry_listen: Option, + #[arg(short = 'H', long = "hostname")] + pub registry_hostname: Vec, #[arg(short = 'p', long = "tor-proxy")] pub tor_proxy: Option, #[arg(short = 'd', long = "datadir")] @@ -45,9 +52,9 @@ impl ContextConfig for RegistryConfig { fn next(&mut self) -> Option { self.config.take() } - fn merge_with(&mut self, other: Self) { - self.listen = self.listen.take().or(other.listen); - self.hostname = self.hostname.take().or(other.hostname); + fn merge_with(&mut self, mut other: Self) { + self.registry_listen = self.registry_listen.take().or(other.registry_listen); + self.registry_hostname.append(&mut other.registry_hostname); self.tor_proxy = self.tor_proxy.take().or(other.tor_proxy); self.datadir = self.datadir.take().or(other.datadir); } @@ -63,7 +70,7 @@ impl RegistryConfig { } pub struct RegistryContextSeed { - pub hostname: InternedString, + pub hostnames: Vec, pub listen: SocketAddr, pub db: TypedPatchDb, pub datadir: PathBuf, @@ -105,20 +112,15 @@ impl RegistryContext { }, None => None, }; + if config.registry_hostname.is_empty() { + return Err(Error::new( + eyre!("missing required configuration: registry-hostname"), + ErrorKind::NotFound, + )); + } Ok(Self(Arc::new(RegistryContextSeed { - hostname: config - .hostname - .as_ref() - .ok_or_else(|| { - Error::new( - eyre!("missing required configuration: hostname"), - ErrorKind::NotFound, - ) - })? - .clone(), - listen: config - .listen - .unwrap_or(SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 5959)), + hostnames: config.registry_hostname.clone(), + listen: config.registry_listen.unwrap_or(DEFAULT_REGISTRY_LISTEN), db, datadir, rpc_continuations: RpcContinuations::new(), @@ -163,64 +165,28 @@ impl CallRemote for CliContext { params: Value, _: Empty, ) -> Result { - use reqwest::header::{ACCEPT, CONTENT_LENGTH, CONTENT_TYPE}; - use reqwest::Method; - use rpc_toolkit::yajrc::{GenericRpcMethod, Id, RpcRequest}; - use rpc_toolkit::RpcResponse; - - let url = self - .registry_url - .clone() - .ok_or_else(|| Error::new(eyre!("`--registry` required"), ErrorKind::InvalidRequest))?; - method = method.strip_prefix("registry.").unwrap_or(method); - - let rpc_req = RpcRequest { - id: Some(Id::Number(0.into())), - method: GenericRpcMethod::<_, _, Value>::new(method), - params, - }; - let body = serde_json::to_vec(&rpc_req)?; - let host = url.host().or_not_found("registry hostname")?.to_string(); - let mut req = self - .client - .request(Method::POST, url) - .header(CONTENT_TYPE, "application/json") - .header(ACCEPT, "application/json") - .header(CONTENT_LENGTH, body.len()); - if let Ok(key) = self.developer_key() { - req = req.header( - AUTH_SIG_HEADER, - SignatureHeader::sign(&AnySigningKey::Ed25519(key.clone()), &body, &host)? - .to_header(), + let url = if let Some(url) = self.registry_url.clone() { + url + } else if self.registry_hostname.is_some() { + format!( + "http://{}", + self.registry_listen.unwrap_or(DEFAULT_REGISTRY_LISTEN) + ) + .parse() + .map_err(Error::from)? + } else { + return Err( + Error::new(eyre!("`--registry` required"), ErrorKind::InvalidRequest).into(), ); - } - let res = req.body(body).send().await?; + }; + method = method.strip_prefix("registry.").unwrap_or(method); + let sig_context = self + .registry_hostname + .clone() + .or(url.host().as_ref().map(InternedString::from_display)) + .or_not_found("registry hostname")?; - if !res.status().is_success() { - let status = res.status(); - let txt = res.text().await?; - let mut res = Err(Error::new( - eyre!("{}", status.canonical_reason().unwrap_or(status.as_str())), - ErrorKind::Network, - )); - if !txt.is_empty() { - res = res.with_ctx(|_| (ErrorKind::Network, txt)); - } - return res.map_err(From::from); - } - - match res - .headers() - .get(CONTENT_TYPE) - .and_then(|v| v.to_str().ok()) - { - Some("application/json") => { - serde_json::from_slice::(&*res.bytes().await?) - .with_kind(ErrorKind::Deserialization)? - .result - } - _ => Err(Error::new(eyre!("unknown content type"), ErrorKind::Network).into()), - } + crate::middleware::signature::call_remote(self, url, &sig_context, method, params).await } } @@ -286,3 +252,72 @@ impl CallRemote for RpcContext { } } } + +#[derive(Deserialize)] +pub struct RegistryAuthMetadata { + #[serde(default)] + admin: bool, +} + +#[derive(Serialize, Deserialize, TS)] +pub struct AdminLogRecord { + pub timestamp: String, + pub name: String, + #[ts(type = "{ id: string | number | null; method: string; params: any }")] + pub request: RpcRequest, + pub key: AnyVerifyingKey, +} + +impl SignatureAuthContext for RegistryContext { + type Database = RegistryDatabase; + type AdditionalMetadata = RegistryAuthMetadata; + type CheckPubkeyRes = Option<(AnyVerifyingKey, SignerInfo)>; + fn db(&self) -> &TypedPatchDb { + &self.db + } + async fn sig_context( + &self, + ) -> impl IntoIterator + Send, Error>> + Send { + self.hostnames.iter().map(Ok) + } + fn check_pubkey( + db: &Model, + pubkey: Option<&AnyVerifyingKey>, + metadata: Self::AdditionalMetadata, + ) -> Result { + if metadata.admin { + if let Some(pubkey) = pubkey { + let (guid, admin) = db.as_index().as_signers().get_signer_info(pubkey)?; + if db.as_admins().de()?.contains(&guid) { + return Ok(Some((pubkey.clone(), admin))); + } + } + Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization)) + } else { + Ok(None) + } + } + async fn post_auth_hook( + &self, + check_pubkey_res: Self::CheckPubkeyRes, + request: &RpcRequest, + ) -> Result<(), Error> { + use tokio::io::AsyncWriteExt; + if let Some((pubkey, admin)) = check_pubkey_res { + let mut log = append_file(self.datadir.join("admin.log")).await?; + log.write_all( + (serde_json::to_string(&AdminLogRecord { + timestamp: Utc::now().to_rfc3339(), + name: admin.name, + request: request.clone(), + key: pubkey, + }) + .with_kind(ErrorKind::Serialization)? + + "\n") + .as_bytes(), + ) + .await?; + } + Ok(()) + } +} diff --git a/core/startos/src/registry/mod.rs b/core/startos/src/registry/mod.rs index 3b865cb96..df67e5786 100644 --- a/core/startos/src/registry/mod.rs +++ b/core/startos/src/registry/mod.rs @@ -9,10 +9,10 @@ use ts_rs::TS; use crate::context::CliContext; use crate::middleware::cors::Cors; +use crate::middleware::signature::SignatureAuth; use crate::net::static_server::{bad_request, not_found, server_error}; use crate::net::web_server::{Accept, WebServer}; use crate::prelude::*; -use crate::registry::auth::Auth; use crate::registry::context::RegistryContext; use crate::registry::device_info::DeviceInfoMiddleware; use crate::registry::os::index::OsIndex; @@ -23,7 +23,6 @@ use crate::util::serde::HandlerExtSerde; pub mod admin; pub mod asset; -pub mod auth; pub mod context; pub mod db; pub mod device_info; @@ -95,7 +94,7 @@ pub fn registry_router(ctx: RegistryContext) -> Router { any( Server::new(move || ready(Ok(ctx.clone())), registry_api()) .middleware(Cors::new()) - .middleware(Auth::new()) + .middleware(SignatureAuth::new()) .middleware(DeviceInfoMiddleware::new()), ) }) diff --git a/core/startos/src/registry/os/asset/add.rs b/core/startos/src/registry/os/asset/add.rs index ffea754e3..32e5fa1a4 100644 --- a/core/startos/src/registry/os/asset/add.rs +++ b/core/startos/src/registry/os/asset/add.rs @@ -19,9 +19,9 @@ use crate::registry::asset::RegistryAsset; use crate::registry::context::RegistryContext; use crate::registry::os::index::OsVersionInfo; use crate::registry::os::SIG_CONTEXT; -use crate::registry::signer::commitment::blake3::Blake3Commitment; -use crate::registry::signer::sign::ed25519::Ed25519; -use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey, SignatureScheme}; +use crate::sign::commitment::blake3::Blake3Commitment; +use crate::sign::ed25519::Ed25519; +use crate::sign::{AnySignature, AnyVerifyingKey, SignatureScheme}; use crate::s9pk::merkle_archive::hash::VerifyingWriter; use crate::s9pk::merkle_archive::source::http::HttpSource; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; diff --git a/core/startos/src/registry/os/asset/get.rs b/core/startos/src/registry/os/asset/get.rs index cf9baca37..2d84e7ce7 100644 --- a/core/startos/src/registry/os/asset/get.rs +++ b/core/startos/src/registry/os/asset/get.rs @@ -18,8 +18,8 @@ use crate::registry::asset::RegistryAsset; use crate::registry::context::RegistryContext; use crate::registry::os::index::OsVersionInfo; use crate::registry::os::SIG_CONTEXT; -use crate::registry::signer::commitment::blake3::Blake3Commitment; -use crate::registry::signer::commitment::Commitment; +use crate::sign::commitment::blake3::Blake3Commitment; +use crate::sign::commitment::Commitment; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::util::io::open_file; diff --git a/core/startos/src/registry/os/asset/sign.rs b/core/startos/src/registry/os/asset/sign.rs index a2094741b..67cdb1c0d 100644 --- a/core/startos/src/registry/os/asset/sign.rs +++ b/core/startos/src/registry/os/asset/sign.rs @@ -17,9 +17,9 @@ use crate::registry::asset::RegistryAsset; use crate::registry::context::RegistryContext; use crate::registry::os::index::OsVersionInfo; use crate::registry::os::SIG_CONTEXT; -use crate::registry::signer::commitment::blake3::Blake3Commitment; -use crate::registry::signer::sign::ed25519::Ed25519; -use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey, SignatureScheme}; +use crate::sign::commitment::blake3::Blake3Commitment; +use crate::sign::ed25519::Ed25519; +use crate::sign::{AnySignature, AnyVerifyingKey, SignatureScheme}; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::ArchiveSource; use crate::util::io::open_file; diff --git a/core/startos/src/registry/os/index.rs b/core/startos/src/registry/os/index.rs index b61cb8f96..e182b4e46 100644 --- a/core/startos/src/registry/os/index.rs +++ b/core/startos/src/registry/os/index.rs @@ -8,7 +8,7 @@ use ts_rs::TS; use crate::prelude::*; use crate::registry::asset::RegistryAsset; use crate::registry::context::RegistryContext; -use crate::registry::signer::commitment::blake3::Blake3Commitment; +use crate::sign::commitment::blake3::Blake3Commitment; use crate::rpc_continuations::Guid; #[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] diff --git a/core/startos/src/registry/os/version/mod.rs b/core/startos/src/registry/os/version/mod.rs index 263ce5073..7064348dd 100644 --- a/core/startos/src/registry/os/version/mod.rs +++ b/core/startos/src/registry/os/version/mod.rs @@ -15,7 +15,7 @@ use crate::prelude::*; use crate::registry::context::RegistryContext; use crate::registry::device_info::DeviceInfo; use crate::registry::os::index::OsVersionInfo; -use crate::registry::signer::sign::AnyVerifyingKey; +use crate::sign::AnyVerifyingKey; use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat}; pub mod signer; diff --git a/core/startos/src/registry/package/add.rs b/core/startos/src/registry/package/add.rs index e3e1a0bb6..995aba54f 100644 --- a/core/startos/src/registry/package/add.rs +++ b/core/startos/src/registry/package/add.rs @@ -15,9 +15,9 @@ use crate::prelude::*; use crate::progress::{FullProgressTracker, ProgressTrackerWriter, ProgressUnits}; use crate::registry::context::RegistryContext; use crate::registry::package::index::PackageVersionInfo; -use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; -use crate::registry::signer::sign::ed25519::Ed25519; -use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey, SignatureScheme}; +use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment; +use crate::sign::ed25519::Ed25519; +use crate::sign::{AnySignature, AnyVerifyingKey, SignatureScheme}; use crate::s9pk::merkle_archive::source::http::HttpSource; use crate::s9pk::merkle_archive::source::ArchiveSource; use crate::s9pk::v2::SIG_CONTEXT; diff --git a/core/startos/src/registry/package/index.rs b/core/startos/src/registry/package/index.rs index dc8df7461..b5af40414 100644 --- a/core/startos/src/registry/package/index.rs +++ b/core/startos/src/registry/package/index.rs @@ -12,8 +12,8 @@ use crate::prelude::*; use crate::registry::asset::RegistryAsset; use crate::registry::context::RegistryContext; use crate::registry::device_info::DeviceInfo; -use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; -use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey}; +use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment; +use crate::sign::{AnySignature, AnyVerifyingKey}; use crate::rpc_continuations::Guid; use crate::s9pk::git_hash::GitHash; use crate::s9pk::manifest::{Alerts, Description, HardwareRequirements}; diff --git a/core/startos/src/registry/signer/mod.rs b/core/startos/src/registry/signer.rs similarity index 96% rename from core/startos/src/registry/signer/mod.rs rename to core/startos/src/registry/signer.rs index 137c40f0f..f38ff81a8 100644 --- a/core/startos/src/registry/signer/mod.rs +++ b/core/startos/src/registry/signer.rs @@ -9,11 +9,8 @@ use ts_rs::TS; use url::Url; use crate::prelude::*; -use crate::registry::signer::commitment::Digestable; -use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey, SignatureScheme}; - -pub mod commitment; -pub mod sign; +use crate::sign::commitment::Digestable; +use crate::sign::{AnySignature, AnyVerifyingKey, SignatureScheme}; #[derive(Debug, Deserialize, Serialize, HasModel, TS)] #[serde(rename_all = "camelCase")] diff --git a/core/startos/src/s9pk/merkle_archive/mod.rs b/core/startos/src/s9pk/merkle_archive/mod.rs index 3f30a4ce1..04cfbb294 100644 --- a/core/startos/src/s9pk/merkle_archive/mod.rs +++ b/core/startos/src/s9pk/merkle_archive/mod.rs @@ -7,9 +7,9 @@ use sha2::{Digest, Sha512}; use tokio::io::AsyncRead; use crate::prelude::*; -use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; -use crate::registry::signer::sign::ed25519::Ed25519; -use crate::registry::signer::sign::SignatureScheme; +use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment; +use crate::sign::ed25519::Ed25519; +use crate::sign::SignatureScheme; use crate::s9pk::merkle_archive::directory_contents::DirectoryContents; use crate::s9pk::merkle_archive::file_contents::FileContents; use crate::s9pk::merkle_archive::sink::Sink; diff --git a/core/startos/src/s9pk/v2/mod.rs b/core/startos/src/s9pk/v2/mod.rs index 30a91de18..9a6c51df3 100644 --- a/core/startos/src/s9pk/v2/mod.rs +++ b/core/startos/src/s9pk/v2/mod.rs @@ -8,7 +8,7 @@ use tokio::fs::File; use crate::dependencies::DependencyMetadata; use crate::prelude::*; -use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; +use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment; use crate::s9pk::manifest::Manifest; use crate::s9pk::merkle_archive::sink::Sink; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; diff --git a/core/startos/src/service/effects/subcontainer/sync.rs b/core/startos/src/service/effects/subcontainer/sync.rs index d088516d5..5c14a84ed 100644 --- a/core/startos/src/service/effects/subcontainer/sync.rs +++ b/core/startos/src/service/effects/subcontainer/sync.rs @@ -284,7 +284,7 @@ pub fn launch( if tty { use pty_process::blocking as pty_process; let (pty, pts) = pty_process::open().with_kind(ErrorKind::Filesystem)?; - let mut cmd = pty_process::Command::new("/usr/bin/start-cli"); + let mut cmd = pty_process::Command::new("/usr/bin/start-container"); cmd = cmd.arg("subcontainer").arg("launch-init"); if let Some(env) = env { cmd = cmd.arg("--env").arg(env); @@ -339,7 +339,7 @@ pub fn launch( )) } } else { - let mut cmd = StdCommand::new("/usr/bin/start-cli"); + let mut cmd = StdCommand::new("/usr/bin/start-container"); cmd.arg("subcontainer").arg("launch-init"); if let Some(env) = env { cmd.arg("--env").arg(env); @@ -534,7 +534,7 @@ pub fn exec( if tty { use pty_process::blocking as pty_process; let (pty, pts) = pty_process::open().with_kind(ErrorKind::Filesystem)?; - let mut cmd = pty_process::Command::new("/usr/bin/start-cli"); + let mut cmd = pty_process::Command::new("/usr/bin/start-container"); cmd = cmd.arg("subcontainer").arg("exec-command"); if let Some(env) = env { cmd = cmd.arg("--env").arg(env); @@ -589,7 +589,7 @@ pub fn exec( )) } } else { - let mut cmd = StdCommand::new("/usr/bin/start-cli"); + let mut cmd = StdCommand::new("/usr/bin/start-container"); cmd.arg("subcontainer").arg("exec-command"); if let Some(env) = env { cmd.arg("--env").arg(env); diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index 722fd5240..aae6f4bdd 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -910,7 +910,7 @@ pub async fn attach( cmd.arg(&*container_id) .arg("--") - .arg("start-cli") + .arg("start-container") .arg("subcontainer") .arg("exec") .arg("--env") diff --git a/core/startos/src/service/service_map.rs b/core/startos/src/service/service_map.rs index c14aca6b4..07747504e 100644 --- a/core/startos/src/service/service_map.rs +++ b/core/startos/src/service/service_map.rs @@ -23,7 +23,7 @@ use crate::install::PKG_ARCHIVE_DIR; use crate::notifications::{notify, NotificationLevel}; use crate::prelude::*; use crate::progress::{FullProgressTracker, PhaseProgressTrackerHandle, ProgressTrackerWriter}; -use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; +use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment; use crate::s9pk::manifest::PackageId; use crate::s9pk::merkle_archive::source::FileSource; use crate::s9pk::S9pk; diff --git a/core/startos/src/registry/signer/commitment/blake3.rs b/core/startos/src/sign/commitment/blake3.rs similarity index 96% rename from core/startos/src/registry/signer/commitment/blake3.rs rename to core/startos/src/sign/commitment/blake3.rs index d99e68c16..98c73555b 100644 --- a/core/startos/src/registry/signer/commitment/blake3.rs +++ b/core/startos/src/sign/commitment/blake3.rs @@ -5,7 +5,7 @@ use tokio::io::AsyncWrite; use ts_rs::TS; use crate::prelude::*; -use crate::registry::signer::commitment::{Commitment, Digestable}; +use crate::sign::commitment::{Commitment, Digestable}; use crate::s9pk::merkle_archive::hash::VerifyingWriter; use crate::s9pk::merkle_archive::source::ArchiveSource; use crate::util::io::{ParallelBlake3Writer, TrackingIO}; diff --git a/core/startos/src/registry/signer/commitment/merkle_archive.rs b/core/startos/src/sign/commitment/merkle_archive.rs similarity index 98% rename from core/startos/src/registry/signer/commitment/merkle_archive.rs rename to core/startos/src/sign/commitment/merkle_archive.rs index b27fb7ef4..caddc72e8 100644 --- a/core/startos/src/registry/signer/commitment/merkle_archive.rs +++ b/core/startos/src/sign/commitment/merkle_archive.rs @@ -4,10 +4,10 @@ use tokio::io::AsyncWrite; use ts_rs::TS; use crate::prelude::*; -use crate::registry::signer::commitment::{Commitment, Digestable}; use crate::s9pk::merkle_archive::source::FileSource; use crate::s9pk::merkle_archive::MerkleArchive; use crate::s9pk::S9pk; +use crate::sign::commitment::{Commitment, Digestable}; use crate::util::io::TrackingIO; use crate::util::serde::Base64; diff --git a/core/startos/src/registry/signer/commitment/mod.rs b/core/startos/src/sign/commitment/mod.rs similarity index 100% rename from core/startos/src/registry/signer/commitment/mod.rs rename to core/startos/src/sign/commitment/mod.rs diff --git a/core/startos/src/registry/signer/commitment/request.rs b/core/startos/src/sign/commitment/request.rs similarity index 98% rename from core/startos/src/registry/signer/commitment/request.rs rename to core/startos/src/sign/commitment/request.rs index e5bb776bf..4d9109d69 100644 --- a/core/startos/src/registry/signer/commitment/request.rs +++ b/core/startos/src/sign/commitment/request.rs @@ -13,8 +13,8 @@ use ts_rs::TS; use url::Url; use crate::prelude::*; -use crate::registry::signer::commitment::{Commitment, Digestable}; use crate::s9pk::merkle_archive::hash::VerifyingWriter; +use crate::sign::commitment::{Commitment, Digestable}; use crate::util::serde::Base64; #[derive(Clone, Debug, Deserialize, Serialize, HasModel, PartialEq, Eq, TS)] diff --git a/core/startos/src/registry/signer/sign/ed25519.rs b/core/startos/src/sign/ed25519.rs similarity index 94% rename from core/startos/src/registry/signer/sign/ed25519.rs rename to core/startos/src/sign/ed25519.rs index 3ec4c136e..5411bc3ec 100644 --- a/core/startos/src/registry/signer/sign/ed25519.rs +++ b/core/startos/src/sign/ed25519.rs @@ -2,7 +2,7 @@ use ed25519_dalek::{Signature, SigningKey, VerifyingKey}; use sha2::Sha512; use crate::prelude::*; -use crate::registry::signer::sign::SignatureScheme; +use crate::sign::SignatureScheme; pub struct Ed25519; impl SignatureScheme for Ed25519 { diff --git a/core/startos/src/registry/signer/sign/mod.rs b/core/startos/src/sign/mod.rs similarity index 98% rename from core/startos/src/registry/signer/sign/mod.rs rename to core/startos/src/sign/mod.rs index 6a95a2490..2c8c1289c 100644 --- a/core/startos/src/registry/signer/sign/mod.rs +++ b/core/startos/src/sign/mod.rs @@ -12,10 +12,11 @@ use sha2::Sha512; use ts_rs::TS; use crate::prelude::*; -use crate::registry::signer::commitment::Digestable; -use crate::registry::signer::sign::ed25519::Ed25519; +use crate::sign::commitment::Digestable; +use crate::sign::ed25519::Ed25519; use crate::util::serde::{deserialize_from_str, serialize_display}; +pub mod commitment; pub mod ed25519; pub trait SignatureScheme { @@ -60,6 +61,7 @@ pub trait SignatureScheme { } } +#[non_exhaustive] pub enum AnyScheme { Ed25519(Ed25519), } @@ -118,6 +120,7 @@ impl SignatureScheme for AnyScheme { #[derive(Clone, Debug, PartialEq, Eq, TS)] #[ts(export, type = "string")] +#[non_exhaustive] pub enum AnySigningKey { Ed25519(::SigningKey), } @@ -189,6 +192,7 @@ impl Serialize for AnySigningKey { #[derive(Clone, Debug, PartialEq, Eq, Hash, TS)] #[ts(export, type = "string")] +#[non_exhaustive] pub enum AnyVerifyingKey { Ed25519(::VerifyingKey), } @@ -261,6 +265,7 @@ impl ValueParserFactory for AnyVerifyingKey { } #[derive(Clone, Debug)] +#[non_exhaustive] pub enum AnyDigest { Sha512(Sha512), } diff --git a/core/startos/src/ssh.rs b/core/startos/src/ssh.rs index dc2f9440a..406c70ac1 100644 --- a/core/startos/src/ssh.rs +++ b/core/startos/src/ssh.rs @@ -21,7 +21,7 @@ use crate::util::Invoke; pub const SSH_DIR: &str = "/home/start9/.ssh"; -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct SshKeys(BTreeMap>); impl SshKeys { pub fn new() -> Self { diff --git a/core/startos/src/tunnel/context.rs b/core/startos/src/tunnel/context.rs new file mode 100644 index 000000000..bbcf9bf0d --- /dev/null +++ b/core/startos/src/tunnel/context.rs @@ -0,0 +1,226 @@ +use std::collections::BTreeSet; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::ops::Deref; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use clap::Parser; +use imbl_value::InternedString; +use patch_db::PatchDb; +use rpc_toolkit::yajrc::RpcError; +use rpc_toolkit::{CallRemote, Context, Empty}; +use serde::{Deserialize, Serialize}; +use tokio::sync::broadcast::Sender; +use tracing::instrument; + +use crate::auth::{check_password, Sessions}; +use crate::context::config::ContextConfig; +use crate::context::{CliContext, RpcContext}; +use crate::middleware::auth::AuthContext; +use crate::middleware::signature::SignatureAuthContext; +use crate::prelude::*; +use crate::rpc_continuations::{OpenAuthedContinuations, RpcContinuations}; +use crate::tunnel::{TunnelDatabase, TUNNEL_DEFAULT_PORT}; +use crate::util::iter::TransposeResultIterExt; +use crate::util::sync::SyncMutex; + +#[derive(Debug, Clone, Default, Deserialize, Serialize, Parser)] +#[serde(rename_all = "kebab-case")] +#[command(rename_all = "kebab-case")] +pub struct TunnelConfig { + #[arg(short = 'c', long = "config")] + pub config: Option, + #[arg(short = 'l', long = "listen")] + pub tunnel_listen: Option, + #[arg(short = 'd', long = "datadir")] + pub datadir: Option, +} +impl ContextConfig for TunnelConfig { + fn next(&mut self) -> Option { + self.config.take() + } + fn merge_with(&mut self, other: Self) { + self.tunnel_listen = self.tunnel_listen.take().or(other.tunnel_listen); + self.datadir = self.datadir.take().or(other.datadir); + } +} + +impl TunnelConfig { + pub fn load(mut self) -> Result { + let path = self.next(); + self.load_path_rec(path)?; + self.load_path_rec(Some("/etc/start-tunneld"))?; + Ok(self) + } +} + +pub struct TunnelContextSeed { + pub listen: SocketAddr, + pub addrs: BTreeSet, + pub db: TypedPatchDb, + pub datadir: PathBuf, + pub rpc_continuations: RpcContinuations, + pub open_authed_continuations: OpenAuthedContinuations>, + pub ephemeral_sessions: SyncMutex, + pub shutdown: Sender<()>, +} + +#[derive(Clone)] +pub struct TunnelContext(Arc); +impl TunnelContext { + #[instrument(skip_all)] + pub async fn init(config: &TunnelConfig) -> Result { + let (shutdown, _) = tokio::sync::broadcast::channel(1); + let datadir = config + .datadir + .as_deref() + .unwrap_or_else(|| Path::new("/var/lib/start-tunnel")) + .to_owned(); + if tokio::fs::metadata(&datadir).await.is_err() { + tokio::fs::create_dir_all(&datadir).await?; + } + let db_path = datadir.join("tunnel.db"); + let db = TypedPatchDb::::load_or_init( + PatchDb::open(&db_path).await?, + || async { Ok(Default::default()) }, + ) + .await?; + let listen = config.tunnel_listen.unwrap_or(SocketAddr::new( + Ipv6Addr::UNSPECIFIED.into(), + TUNNEL_DEFAULT_PORT, + )); + Ok(Self(Arc::new(TunnelContextSeed { + listen, + addrs: crate::net::utils::all_socket_addrs_for(listen.port()) + .await? + .into_iter() + .map(|(_, a)| a.ip()) + .collect(), + db, + datadir, + rpc_continuations: RpcContinuations::new(), + open_authed_continuations: OpenAuthedContinuations::new(), + ephemeral_sessions: SyncMutex::new(Sessions::new()), + shutdown, + }))) + } +} +impl AsRef for TunnelContext { + fn as_ref(&self) -> &RpcContinuations { + &self.rpc_continuations + } +} + +impl Context for TunnelContext {} +impl Deref for TunnelContext { + type Target = TunnelContextSeed; + fn deref(&self) -> &Self::Target { + &*self.0 + } +} + +#[derive(Debug, Deserialize, Serialize, Parser)] +pub struct TunnelAddrParams { + pub tunnel: IpAddr, +} + +impl SignatureAuthContext for TunnelContext { + type Database = TunnelDatabase; + type AdditionalMetadata = (); + type CheckPubkeyRes = (); + fn db(&self) -> &TypedPatchDb { + &self.db + } + async fn sig_context( + &self, + ) -> impl IntoIterator + Send, Error>> + Send { + self.addrs + .iter() + .filter(|a| !match a { + IpAddr::V4(a) => a.is_loopback() || a.is_unspecified(), + IpAddr::V6(a) => a.is_loopback() || a.is_unspecified(), + }) + .map(|a| InternedString::from_display(&a)) + .map(Ok) + } + fn check_pubkey( + db: &Model, + pubkey: Option<&crate::sign::AnyVerifyingKey>, + _: Self::AdditionalMetadata, + ) -> Result { + if let Some(pubkey) = pubkey { + if db.as_auth_pubkeys().de()?.contains(pubkey) { + return Ok(()); + } + } + + Err(Error::new( + eyre!("Developer Key is not authorized"), + ErrorKind::IncorrectPassword, + )) + } + async fn post_auth_hook( + &self, + _: Self::CheckPubkeyRes, + _: &rpc_toolkit::RpcRequest, + ) -> Result<(), Error> { + Ok(()) + } +} +impl AuthContext for TunnelContext { + const LOCAL_AUTH_COOKIE_PATH: &str = "/run/start-tunnel/rpc.authcookie"; + const LOCAL_AUTH_COOKIE_OWNERSHIP: &str = "root:root"; + fn access_sessions(db: &mut Model) -> &mut Model { + db.as_sessions_mut() + } + fn ephemeral_sessions(&self) -> &SyncMutex { + &self.ephemeral_sessions + } + fn open_authed_continuations(&self) -> &OpenAuthedContinuations> { + &self.open_authed_continuations + } + fn check_password(db: &Model, password: &str) -> Result<(), Error> { + check_password(&db.as_password().de()?, password) + } +} + +impl CallRemote for CliContext { + async fn call_remote( + &self, + mut method: &str, + params: Value, + _: Empty, + ) -> Result { + let tunnel_addr = if let Some(addr) = self.tunnel_addr { + addr + } else if let Some(addr) = self.tunnel_listen { + addr + } else { + return Err(Error::new(eyre!("`--tunnel` required"), ErrorKind::InvalidRequest).into()); + }; + let sig_addr = self.tunnel_listen.unwrap_or(tunnel_addr); + let url = format!("https://{tunnel_addr}").parse()?; + + method = method.strip_prefix("tunnel.").unwrap_or(method); + + crate::middleware::signature::call_remote( + self, + url, + &InternedString::from_display(&sig_addr.ip()), + method, + params, + ) + .await + } +} + +impl CallRemote for RpcContext { + async fn call_remote( + &self, + mut method: &str, + params: Value, + TunnelAddrParams { tunnel }: TunnelAddrParams, + ) -> Result { + todo!() + } +} diff --git a/core/startos/src/tunnel/db.rs b/core/startos/src/tunnel/db.rs new file mode 100644 index 000000000..79a69055a --- /dev/null +++ b/core/startos/src/tunnel/db.rs @@ -0,0 +1,180 @@ +use std::path::PathBuf; + +use clap::Parser; +use itertools::Itertools; +use patch_db::json_ptr::{JsonPointer, ROOT}; +use patch_db::Dump; +use rpc_toolkit::yajrc::RpcError; +use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; +use tracing::instrument; +use ts_rs::TS; + +use crate::context::CliContext; +use crate::prelude::*; +use crate::tunnel::context::TunnelContext; +use crate::tunnel::TunnelDatabase; +use crate::util::serde::{apply_expr, HandlerExtSerde}; + +pub fn db_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "dump", + from_fn_async(cli_dump) + .with_display_serializable() + .with_about("Filter/query db to display tables and records"), + ) + .subcommand( + "dump", + from_fn_async(dump) + .with_metadata("admin", Value::Bool(true)) + .no_cli(), + ) + .subcommand( + "apply", + from_fn_async(cli_apply) + .no_display() + .with_about("Update a db record"), + ) + .subcommand( + "apply", + from_fn_async(apply) + .with_metadata("admin", Value::Bool(true)) + .no_cli(), + ) +} + +#[derive(Deserialize, Serialize, Parser)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct CliDumpParams { + #[arg(long = "pointer", short = 'p')] + pointer: Option, + path: Option, +} + +#[instrument(skip_all)] +async fn cli_dump( + HandlerArgs { + context, + parent_method, + method, + params: CliDumpParams { pointer, path }, + .. + }: HandlerArgs, +) -> Result { + let dump = if let Some(path) = path { + PatchDb::open(path).await?.dump(&ROOT).await + } else { + let method = parent_method.into_iter().chain(method).join("."); + from_value::( + context + .call_remote::(&method, imbl_value::json!({ "pointer": pointer })) + .await?, + )? + }; + + Ok(dump) +} + +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct DumpParams { + #[arg(long = "pointer", short = 'p')] + #[ts(type = "string | null")] + pointer: Option, +} + +pub async fn dump(ctx: TunnelContext, DumpParams { pointer }: DumpParams) -> Result { + Ok(ctx + .db + .dump(&pointer.as_ref().map_or(ROOT, |p| p.borrowed())) + .await) +} + +#[derive(Deserialize, Serialize, Parser)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct CliApplyParams { + expr: String, + path: Option, +} + +#[instrument(skip_all)] +async fn cli_apply( + HandlerArgs { + context, + parent_method, + method, + params: CliApplyParams { expr, path }, + .. + }: HandlerArgs, +) -> Result<(), RpcError> { + if let Some(path) = path { + PatchDb::open(path) + .await? + .apply_function(|db| { + let res = apply_expr( + serde_json::to_value(patch_db::Value::from(db)) + .with_kind(ErrorKind::Deserialization)? + .into(), + &expr, + )?; + + Ok::<_, Error>(( + to_value( + &serde_json::from_value::(res.clone().into()).with_ctx( + |_| { + ( + crate::ErrorKind::Deserialization, + "result does not match database model", + ) + }, + )?, + )?, + (), + )) + }) + .await + .result?; + } else { + let method = parent_method.into_iter().chain(method).join("."); + context + .call_remote::(&method, imbl_value::json!({ "expr": expr })) + .await?; + } + + Ok(()) +} + +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct ApplyParams { + expr: String, + path: Option, +} + +pub async fn apply(ctx: TunnelContext, ApplyParams { expr, .. }: ApplyParams) -> Result<(), Error> { + ctx.db + .mutate(|db| { + let res = apply_expr( + serde_json::to_value(patch_db::Value::from(db.clone())) + .with_kind(ErrorKind::Deserialization)? + .into(), + &expr, + )?; + + db.ser( + &serde_json::from_value::(res.clone().into()).with_ctx(|_| { + ( + crate::ErrorKind::Deserialization, + "result does not match database model", + ) + })?, + ) + }) + .await + .result +} diff --git a/core/startos/src/tunnel/mod.rs b/core/startos/src/tunnel/mod.rs new file mode 100644 index 000000000..a414e2031 --- /dev/null +++ b/core/startos/src/tunnel/mod.rs @@ -0,0 +1,99 @@ +use std::collections::HashSet; + +use axum::Router; +use futures::future::ready; +use rpc_toolkit::{Context, HandlerExt, ParentHandler, Server}; +use serde::{Deserialize, Serialize}; + +use crate::auth::Sessions; +use crate::middleware::auth::Auth; +use crate::middleware::cors::Cors; +use crate::net::static_server::{bad_request, not_found, server_error}; +use crate::net::web_server::{Accept, WebServer}; +use crate::prelude::*; +use crate::rpc_continuations::Guid; +use crate::sign::AnyVerifyingKey; +use crate::tunnel::context::TunnelContext; + +pub mod context; +pub mod db; + +pub const TUNNEL_DEFAULT_PORT: u16 = 5960; + +#[derive(Debug, Default, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +pub struct TunnelDatabase { + pub sessions: Sessions, + pub password: String, + pub auth_pubkeys: HashSet, +} + +pub fn tunnel_api() -> ParentHandler { + ParentHandler::new().subcommand( + "db", + db::db_api::().with_about("Commands to interact with the db i.e. dump and apply"), + ) +} + +pub fn tunnel_router(ctx: TunnelContext) -> Router { + use axum::extract as x; + use axum::routing::{any, get}; + Router::new() + .route("/rpc/{*path}", { + let ctx = ctx.clone(); + any( + Server::new(move || ready(Ok(ctx.clone())), tunnel_api()) + .middleware(Cors::new()) + .middleware(Auth::new()) + ) + }) + .route( + "/ws/rpc/{*path}", + get({ + let ctx = ctx.clone(); + move |x::Path(path): x::Path, + ws: axum::extract::ws::WebSocketUpgrade| async move { + match Guid::from(&path) { + None => { + tracing::debug!("No Guid Path"); + bad_request() + } + Some(guid) => match ctx.rpc_continuations.get_ws_handler(&guid).await { + Some(cont) => ws.on_upgrade(cont), + _ => not_found(), + }, + } + } + }), + ) + .route( + "/rest/rpc/{*path}", + any({ + let ctx = ctx.clone(); + move |request: x::Request| async move { + let path = request + .uri() + .path() + .strip_prefix("/rest/rpc/") + .unwrap_or_default(); + match Guid::from(&path) { + None => { + tracing::debug!("No Guid Path"); + bad_request() + } + Some(guid) => match ctx.rpc_continuations.get_rest_handler(&guid).await { + None => not_found(), + Some(cont) => cont(request).await.unwrap_or_else(server_error), + }, + } + } + }), + ) +} + +impl WebServer { + pub fn serve_tunnel(&mut self, ctx: TunnelContext) { + self.serve_router(tunnel_router(ctx)) + } +} diff --git a/core/startos/src/update/mod.rs b/core/startos/src/update/mod.rs index 7a346d4af..35a44c498 100644 --- a/core/startos/src/update/mod.rs +++ b/core/startos/src/update/mod.rs @@ -33,8 +33,8 @@ use crate::registry::asset::RegistryAsset; use crate::registry::context::{RegistryContext, RegistryUrlParams}; use crate::registry::os::index::OsVersionInfo; use crate::registry::os::SIG_CONTEXT; -use crate::registry::signer::commitment::blake3::Blake3Commitment; -use crate::registry::signer::commitment::Commitment; +use crate::sign::commitment::blake3::Blake3Commitment; +use crate::sign::commitment::Commitment; use crate::rpc_continuations::{Guid, RpcContinuation}; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::sound::{ diff --git a/core/startos/src/util/iter.rs b/core/startos/src/util/iter.rs new file mode 100644 index 000000000..99e1f569c --- /dev/null +++ b/core/startos/src/util/iter.rs @@ -0,0 +1,31 @@ +pub trait TransposeResultIterExt: Sized { + type Ok: IntoIterator; + type Err; + fn transpose(self) -> TransposeResult; +} +impl TransposeResultIterExt for Result { + type Ok = T; + type Err = E; + fn transpose(self) -> TransposeResult { + TransposeResult(Some(self.map(|t| t.into_iter()))) + } +} + +pub struct TransposeResult(Option>); +impl Iterator for TransposeResult +where + T: IntoIterator, +{ + type Item = Result; + fn next(&mut self) -> Option { + if self.0.as_ref().map_or(true, |r| r.is_err()) { + self.0 + .take() + .map(|e| Err(e.map_or_else(|e| e, |_| unreachable!()))) + } else if let Some(Ok(res)) = &mut self.0 { + res.next().map(Ok) + } else { + None + } + } +} diff --git a/core/startos/src/util/mod.rs b/core/startos/src/util/mod.rs index a31332efa..646b07f99 100644 --- a/core/startos/src/util/mod.rs +++ b/core/startos/src/util/mod.rs @@ -42,6 +42,7 @@ pub mod crypto; pub mod future; pub mod http_reader; pub mod io; +pub mod iter; pub mod logger; pub mod lshw; pub mod net; diff --git a/core/startos/src/version/v0_3_6_alpha_0.rs b/core/startos/src/version/v0_3_6_alpha_0.rs index 80390432f..f5ac17016 100644 --- a/core/startos/src/version/v0_3_6_alpha_0.rs +++ b/core/startos/src/version/v0_3_6_alpha_0.rs @@ -293,7 +293,8 @@ impl VersionT for Version { let mut value = json!({}); value["keyStore"] = to_value(&KeyStore::new(&account)?)?; value["password"] = to_value(&account.password)?; - value["compatS9pkKey"] = to_value(&crate::db::model::private::generate_compat_key())?; + value["compatS9pkKey"] = + to_value(&crate::db::model::private::generate_developer_key())?; value["sshPrivkey"] = to_value(Pem::new_ref(&account.ssh_key))?; value["sshPubkeys"] = to_value(&ssh_keys)?; value["availablePorts"] = to_value(&AvailablePorts::new())?; @@ -377,7 +378,7 @@ impl VersionT for Version { let package_s9pk = tokio::fs::File::open(path).await?; let file = MultiCursorFile::open(&package_s9pk).await?; - let key = ctx.db.peek().await.into_private().into_compat_s9pk_key(); + let key = ctx.db.peek().await.into_private().into_developer_key(); ctx.services .install( ctx.clone(), @@ -512,7 +513,7 @@ async fn previous_account_info(pg: &sqlx::Pool) -> Result = S9pk::new_with_manifest(archive, None, manifest); - let s9pk_compat_key = ctx.account.read().await.compat_s9pk_key.clone(); + let s9pk_compat_key = ctx.account.read().await.developer_key.clone(); s9pk.as_archive_mut() .set_signer(s9pk_compat_key, SIG_CONTEXT); s9pk.serialize(&mut tmp_file, true).await?; diff --git a/sdk/package/lib/util/SubContainer.ts b/sdk/package/lib/util/SubContainer.ts index 9f89d6115..31a691baa 100644 --- a/sdk/package/lib/util/SubContainer.ts +++ b/sdk/package/lib/util/SubContainer.ts @@ -157,10 +157,14 @@ export class SubContainerOwned< ) { super() this.leaderExited = false - this.leader = cp.spawn("start-cli", ["subcontainer", "launch", rootfs], { - killSignal: "SIGKILL", - stdio: "inherit", - }) + this.leader = cp.spawn( + "start-container", + ["subcontainer", "launch", rootfs], + { + killSignal: "SIGKILL", + stdio: "inherit", + }, + ) this.leader.on("exit", () => { this.leaderExited = true }) @@ -407,7 +411,7 @@ export class SubContainerOwned< delete options.cwd } const child = cp.spawn( - "start-cli", + "start-container", [ "subcontainer", "exec", @@ -529,7 +533,7 @@ export class SubContainerOwned< await this.killLeader() this.leaderExited = false this.leader = cp.spawn( - "start-cli", + "start-container", [ "subcontainer", "launch", @@ -571,7 +575,7 @@ export class SubContainerOwned< delete options.cwd } return cp.spawn( - "start-cli", + "start-container", [ "subcontainer", "exec",