mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
wip: start-tunnel
This commit is contained in:
@@ -5,7 +5,7 @@ use openssl::pkey::{PKey, Private};
|
||||
use openssl::x509::X509;
|
||||
|
||||
use crate::db::model::DatabaseModel;
|
||||
use crate::hostname::{Hostname, generate_hostname, generate_id};
|
||||
use crate::hostname::{generate_hostname, generate_id, Hostname};
|
||||
use crate::net::ssl::{generate_key, make_root_cert};
|
||||
use crate::net::tor::TorSecretKey;
|
||||
use crate::prelude::*;
|
||||
@@ -107,6 +107,7 @@ impl AccountInfo {
|
||||
.map(|tor_key| tor_key.onion_address())
|
||||
.collect(),
|
||||
)?;
|
||||
server_info.as_password_hash_mut().ser(&self.password)?;
|
||||
db.as_private_mut().as_password_mut().ser(&self.password)?;
|
||||
db.as_private_mut()
|
||||
.as_ssh_privkey_mut()
|
||||
|
||||
@@ -3,11 +3,11 @@ use std::collections::BTreeMap;
|
||||
use chrono::{DateTime, Utc};
|
||||
use clap::Parser;
|
||||
use color_eyre::eyre::eyre;
|
||||
use imbl_value::{InternedString, json};
|
||||
use imbl_value::{json, InternedString};
|
||||
use itertools::Itertools;
|
||||
use josekit::jwk::Jwk;
|
||||
use rpc_toolkit::yajrc::RpcError;
|
||||
use rpc_toolkit::{CallRemote, Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
|
||||
use rpc_toolkit::{from_fn_async, CallRemote, Context, HandlerArgs, HandlerExt, ParentHandler};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tracing::instrument;
|
||||
@@ -20,8 +20,8 @@ use crate::middleware::auth::{
|
||||
use crate::prelude::*;
|
||||
use crate::util::crypto::EncryptedWire;
|
||||
use crate::util::io::create_file_mod;
|
||||
use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable};
|
||||
use crate::{Error, ResultExt, ensure_code};
|
||||
use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat};
|
||||
use crate::{ensure_code, Error, ResultExt};
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize, TS)]
|
||||
pub struct Sessions(pub BTreeMap<InternedString, Session>);
|
||||
@@ -220,7 +220,7 @@ pub fn check_password(hash: &str, password: &str) -> Result<(), Error> {
|
||||
pub struct LoginParams {
|
||||
password: String,
|
||||
#[ts(skip)]
|
||||
#[serde(rename = "__auth_userAgent")] // from Auth middleware
|
||||
#[serde(rename = "__Auth_userAgent")] // from Auth middleware
|
||||
user_agent: Option<String>,
|
||||
#[serde(default)]
|
||||
ephemeral: bool,
|
||||
@@ -279,7 +279,7 @@ pub async fn login_impl<C: AuthContext>(
|
||||
#[command(rename_all = "kebab-case")]
|
||||
pub struct LogoutParams {
|
||||
#[ts(skip)]
|
||||
#[serde(rename = "__auth_session")] // from Auth middleware
|
||||
#[serde(rename = "__Auth_session")] // from Auth middleware
|
||||
session: InternedString,
|
||||
}
|
||||
|
||||
@@ -373,7 +373,7 @@ fn display_sessions(params: WithIoFormat<ListParams>, arg: SessionList) -> Resul
|
||||
pub struct ListParams {
|
||||
#[arg(skip)]
|
||||
#[ts(skip)]
|
||||
#[serde(rename = "__auth_session")] // from Auth middleware
|
||||
#[serde(rename = "__Auth_session")] // from Auth middleware
|
||||
session: Option<InternedString>,
|
||||
}
|
||||
|
||||
@@ -474,30 +474,19 @@ pub async fn reset_password_impl(
|
||||
let old_password = old_password.unwrap_or_default().decrypt(&ctx)?;
|
||||
let new_password = new_password.unwrap_or_default().decrypt(&ctx)?;
|
||||
|
||||
let mut account = ctx.account.write().await;
|
||||
if !argon2::verify_encoded(&account.password, old_password.as_bytes())
|
||||
.with_kind(crate::ErrorKind::IncorrectPassword)?
|
||||
{
|
||||
return Err(Error::new(
|
||||
eyre!("Incorrect Password"),
|
||||
crate::ErrorKind::IncorrectPassword,
|
||||
));
|
||||
}
|
||||
account.set_password(&new_password)?;
|
||||
let account_password = &account.password;
|
||||
let account = account.clone();
|
||||
ctx.db
|
||||
.mutate(|d| {
|
||||
d.as_public_mut()
|
||||
.as_server_info_mut()
|
||||
.as_password_hash_mut()
|
||||
.ser(account_password)?;
|
||||
account.save(d)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.result
|
||||
let account = ctx.account.mutate(|account| {
|
||||
if !argon2::verify_encoded(&account.password, old_password.as_bytes())
|
||||
.with_kind(crate::ErrorKind::IncorrectPassword)?
|
||||
{
|
||||
return Err(Error::new(
|
||||
eyre!("Incorrect Password"),
|
||||
crate::ErrorKind::IncorrectPassword,
|
||||
));
|
||||
}
|
||||
account.set_password(&new_password)?;
|
||||
Ok(account.clone())
|
||||
})?;
|
||||
ctx.db.mutate(|d| account.save(d)).await.result
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
|
||||
@@ -13,8 +13,8 @@ use tokio::io::AsyncWriteExt;
|
||||
use tracing::instrument;
|
||||
use ts_rs::TS;
|
||||
|
||||
use super::PackageBackupReport;
|
||||
use super::target::{BackupTargetId, PackageBackupInfo};
|
||||
use super::PackageBackupReport;
|
||||
use crate::backup::os::OsBackup;
|
||||
use crate::backup::{BackupReport, ServerBackupReport};
|
||||
use crate::context::RpcContext;
|
||||
@@ -24,7 +24,7 @@ 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::{NotificationLevel, notify};
|
||||
use crate::notifications::{notify, NotificationLevel};
|
||||
use crate::prelude::*;
|
||||
use crate::util::io::dir_copy;
|
||||
use crate::util::serde::IoFormat;
|
||||
@@ -317,7 +317,7 @@ async fn perform_backup(
|
||||
.with_kind(ErrorKind::Filesystem)?;
|
||||
os_backup_file
|
||||
.write_all(&IoFormat::Json.to_vec(&OsBackup {
|
||||
account: ctx.account.read().await.clone(),
|
||||
account: ctx.account.peek(|a| a.clone()),
|
||||
ui,
|
||||
})?)
|
||||
.await?;
|
||||
@@ -342,7 +342,7 @@ async fn perform_backup(
|
||||
let timestamp = Utc::now();
|
||||
|
||||
backup_guard.unencrypted_metadata.version = crate::version::Current::default().semver().into();
|
||||
backup_guard.unencrypted_metadata.hostname = ctx.account.read().await.hostname.clone();
|
||||
backup_guard.unencrypted_metadata.hostname = ctx.account.peek(|a| a.hostname.clone());
|
||||
backup_guard.unencrypted_metadata.timestamp = timestamp.clone();
|
||||
backup_guard.metadata.version = crate::version::Current::default().semver().into();
|
||||
backup_guard.metadata.timestamp = Some(timestamp);
|
||||
|
||||
@@ -3,8 +3,8 @@ use std::ffi::OsString;
|
||||
use rpc_toolkit::CliApp;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::context::CliContext;
|
||||
use crate::context::config::ClientConfig;
|
||||
use crate::context::CliContext;
|
||||
use crate::util::logger::LOGGER;
|
||||
use crate::version::{Current, VersionT};
|
||||
|
||||
@@ -17,7 +17,7 @@ pub fn main(args: impl IntoIterator<Item = OsString>) {
|
||||
|
||||
if let Err(e) = CliApp::new(
|
||||
|cfg: ClientConfig| Ok(CliContext::init(cfg.load()?)?),
|
||||
crate::expanded_api(),
|
||||
crate::main_api(),
|
||||
)
|
||||
.run(args)
|
||||
{
|
||||
|
||||
@@ -6,9 +6,9 @@ use tracing::instrument;
|
||||
use crate::context::config::ServerConfig;
|
||||
use crate::context::rpc::InitRpcContextPhases;
|
||||
use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext};
|
||||
use crate::disk::REPAIR_DISK_PATH;
|
||||
use crate::disk::fsck::RepairStrategy;
|
||||
use crate::disk::main::DEFAULT_PASSWORD;
|
||||
use crate::disk::REPAIR_DISK_PATH;
|
||||
use crate::firmware::{check_for_firmware_update, update_firmware};
|
||||
use crate::init::{InitPhases, STANDBY_MODE_PATH};
|
||||
use crate::net::web_server::{UpgradableListener, WebServer};
|
||||
@@ -37,7 +37,7 @@ async fn setup_or_init(
|
||||
let mut update_phase = handle.add_phase("Updating Firmware".into(), Some(10));
|
||||
let mut reboot_phase = handle.add_phase("Rebooting".into(), Some(1));
|
||||
|
||||
server.serve_init(init_ctx);
|
||||
server.serve_ui_for(init_ctx);
|
||||
|
||||
update_phase.start();
|
||||
if let Err(e) = update_firmware(firmware).await {
|
||||
@@ -93,7 +93,7 @@ async fn setup_or_init(
|
||||
|
||||
let ctx = InstallContext::init().await?;
|
||||
|
||||
server.serve_install(ctx.clone());
|
||||
server.serve_ui_for(ctx.clone());
|
||||
|
||||
ctx.shutdown
|
||||
.subscribe()
|
||||
@@ -113,7 +113,7 @@ async fn setup_or_init(
|
||||
{
|
||||
let ctx = SetupContext::init(server, config)?;
|
||||
|
||||
server.serve_setup(ctx.clone());
|
||||
server.serve_ui_for(ctx.clone());
|
||||
|
||||
let mut shutdown = ctx.shutdown.subscribe();
|
||||
if let Some(shutdown) = shutdown.recv().await.expect("context dropped") {
|
||||
@@ -149,7 +149,7 @@ async fn setup_or_init(
|
||||
let init_phases = InitPhases::new(&handle);
|
||||
let rpc_ctx_phases = InitRpcContextPhases::new(&handle);
|
||||
|
||||
server.serve_init(init_ctx);
|
||||
server.serve_ui_for(init_ctx);
|
||||
|
||||
async {
|
||||
disk_phase.start();
|
||||
@@ -247,7 +247,7 @@ pub async fn main(
|
||||
e,
|
||||
)?;
|
||||
|
||||
server.serve_diagnostic(ctx.clone());
|
||||
server.serve_ui_for(ctx.clone());
|
||||
|
||||
let shutdown = ctx.shutdown.subscribe().recv().await.unwrap();
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ async fn inner_main(
|
||||
};
|
||||
tokio::fs::write("/run/startos/initialized", "").await?;
|
||||
|
||||
server.serve_main(ctx.clone());
|
||||
server.serve_ui_for(ctx.clone());
|
||||
LOGGER.set_logfile(None);
|
||||
handle.complete();
|
||||
|
||||
@@ -47,7 +47,7 @@ async fn inner_main(
|
||||
let init_ctx = InitContext::init(config).await?;
|
||||
let handle = init_ctx.progress.clone();
|
||||
let rpc_ctx_phases = InitRpcContextPhases::new(&handle);
|
||||
server.serve_init(init_ctx);
|
||||
server.serve_ui_for(init_ctx);
|
||||
|
||||
let ctx = RpcContext::init(
|
||||
&server.acceptor_setter(),
|
||||
@@ -63,14 +63,14 @@ async fn inner_main(
|
||||
)
|
||||
.await?;
|
||||
|
||||
server.serve_main(ctx.clone());
|
||||
server.serve_ui_for(ctx.clone());
|
||||
handle.complete();
|
||||
|
||||
ctx
|
||||
};
|
||||
|
||||
let (rpc_ctx, shutdown) = async {
|
||||
crate::hostname::sync_hostname(&rpc_ctx.account.read().await.hostname).await?;
|
||||
crate::hostname::sync_hostname(&rpc_ctx.account.peek(|a| a.hostname.clone())).await?;
|
||||
|
||||
let mut shutdown_recv = rpc_ctx.shutdown.subscribe();
|
||||
|
||||
@@ -177,7 +177,7 @@ pub fn main(args: impl IntoIterator<Item = OsString>) {
|
||||
e,
|
||||
)?;
|
||||
|
||||
server.serve_diagnostic(ctx.clone());
|
||||
server.serve_ui_for(ctx.clone());
|
||||
|
||||
let mut shutdown = ctx.shutdown.subscribe();
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::sync::Arc;
|
||||
|
||||
use cookie::{Cookie, Expiration, SameSite};
|
||||
use cookie_store::CookieStore;
|
||||
use http::HeaderMap;
|
||||
use imbl_value::InternedString;
|
||||
use josekit::jwk::Jwk;
|
||||
use once_cell::sync::OnceCell;
|
||||
@@ -20,13 +21,13 @@ use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
|
||||
use tracing::instrument;
|
||||
|
||||
use super::setup::CURRENT_SECRET;
|
||||
use crate::context::config::{ClientConfig, local_config_path};
|
||||
use crate::context::config::{local_config_path, ClientConfig};
|
||||
use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext};
|
||||
use crate::developer::{OS_DEVELOPER_KEY_PATH, default_developer_key_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::util::io::read_file_to_string;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CliContextSeed {
|
||||
@@ -159,7 +160,7 @@ impl CliContext {
|
||||
continue;
|
||||
}
|
||||
let pair = <ed25519::KeypairBytes as ed25519::pkcs8::DecodePrivateKey>::from_pkcs8_pem(
|
||||
&std::fs::read_to_string(&self.developer_key_path)?,
|
||||
&std::fs::read_to_string(path)?,
|
||||
)
|
||||
.with_kind(crate::ErrorKind::Pem)?;
|
||||
let secret = ed25519_dalek::SecretKey::try_from(&pair.secret_key[..]).map_err(|_| {
|
||||
@@ -279,9 +280,15 @@ impl Context for CliContext {
|
||||
)
|
||||
}
|
||||
}
|
||||
impl AsRef<Client> for CliContext {
|
||||
fn as_ref(&self) -> &Client {
|
||||
&self.client
|
||||
}
|
||||
}
|
||||
|
||||
impl CallRemote<RpcContext> for CliContext {
|
||||
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> {
|
||||
if let Ok(local) = std::fs::read_to_string(RpcContext::LOCAL_AUTH_COOKIE_PATH) {
|
||||
if let Ok(local) = read_file_to_string(RpcContext::LOCAL_AUTH_COOKIE_PATH).await {
|
||||
self.cookie_store
|
||||
.lock()
|
||||
.unwrap()
|
||||
@@ -298,7 +305,8 @@ impl CallRemote<RpcContext> for CliContext {
|
||||
crate::middleware::signature::call_remote(
|
||||
self,
|
||||
self.rpc_url.clone(),
|
||||
self.rpc_url.host_str().or_not_found("rpc url hostname")?,
|
||||
HeaderMap::new(),
|
||||
self.rpc_url.host_str(),
|
||||
method,
|
||||
params,
|
||||
)
|
||||
@@ -307,24 +315,11 @@ impl CallRemote<RpcContext> for CliContext {
|
||||
}
|
||||
impl CallRemote<DiagnosticContext> for CliContext {
|
||||
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> {
|
||||
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")?,
|
||||
HeaderMap::new(),
|
||||
self.rpc_url.host_str(),
|
||||
method,
|
||||
params,
|
||||
)
|
||||
@@ -336,7 +331,8 @@ impl CallRemote<InitContext> for CliContext {
|
||||
crate::middleware::signature::call_remote(
|
||||
self,
|
||||
self.rpc_url.clone(),
|
||||
self.rpc_url.host_str().or_not_found("rpc url hostname")?,
|
||||
HeaderMap::new(),
|
||||
self.rpc_url.host_str(),
|
||||
method,
|
||||
params,
|
||||
)
|
||||
@@ -348,7 +344,8 @@ impl CallRemote<SetupContext> for CliContext {
|
||||
crate::middleware::signature::call_remote(
|
||||
self,
|
||||
self.rpc_url.clone(),
|
||||
self.rpc_url.host_str().or_not_found("rpc url hostname")?,
|
||||
HeaderMap::new(),
|
||||
self.rpc_url.host_str(),
|
||||
method,
|
||||
params,
|
||||
)
|
||||
@@ -360,7 +357,8 @@ impl CallRemote<InstallContext> for CliContext {
|
||||
crate::middleware::signature::call_remote(
|
||||
self,
|
||||
self.rpc_url.clone(),
|
||||
self.rpc_url.host_str().or_not_found("rpc url hostname")?,
|
||||
HeaderMap::new(),
|
||||
self.rpc_url.host_str(),
|
||||
method,
|
||||
params,
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::ffi::OsStr;
|
||||
use std::future::Future;
|
||||
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
@@ -46,7 +45,7 @@ use crate::service::ServiceMap;
|
||||
use crate::shutdown::Shutdown;
|
||||
use crate::util::io::delete_file;
|
||||
use crate::util::lshw::LshwDevice;
|
||||
use crate::util::sync::{SyncMutex, Watch};
|
||||
use crate::util::sync::{SyncMutex, SyncRwLock, Watch};
|
||||
use crate::{DATA_DIR, HOST_IP};
|
||||
|
||||
pub struct RpcContextSeed {
|
||||
@@ -58,7 +57,7 @@ pub struct RpcContextSeed {
|
||||
pub ephemeral_sessions: SyncMutex<Sessions>,
|
||||
pub db: TypedPatchDb<Database>,
|
||||
pub sync_db: watch::Sender<u64>,
|
||||
pub account: RwLock<AccountInfo>,
|
||||
pub account: SyncRwLock<AccountInfo>,
|
||||
pub net_controller: Arc<NetController>,
|
||||
pub os_net_service: NetService,
|
||||
pub s9pk_arch: Option<&'static str>,
|
||||
@@ -225,7 +224,7 @@ impl RpcContext {
|
||||
ephemeral_sessions: SyncMutex::new(Sessions::new()),
|
||||
sync_db: watch::Sender::new(db.sequence().await),
|
||||
db,
|
||||
account: RwLock::new(account),
|
||||
account: SyncRwLock::new(account),
|
||||
callbacks: net_controller.callbacks.clone(),
|
||||
net_controller,
|
||||
os_net_service,
|
||||
@@ -483,6 +482,11 @@ impl RpcContext {
|
||||
<Self as CallRemote<RemoteContext, T>>::call_remote(&self, method, params, extra).await
|
||||
}
|
||||
}
|
||||
impl AsRef<Client> for RpcContext {
|
||||
fn as_ref(&self) -> &Client {
|
||||
&self.client
|
||||
}
|
||||
}
|
||||
impl AsRef<Jwk> for RpcContext {
|
||||
fn as_ref(&self) -> &Jwk {
|
||||
&CURRENT_SECRET
|
||||
|
||||
@@ -127,7 +127,7 @@ pub struct SubscribeParams {
|
||||
#[ts(type = "string | null")]
|
||||
pointer: Option<JsonPointer>,
|
||||
#[ts(skip)]
|
||||
#[serde(rename = "__auth_session")]
|
||||
#[serde(rename = "__Auth_session")]
|
||||
session: Option<InternedString>,
|
||||
}
|
||||
|
||||
|
||||
@@ -216,13 +216,18 @@ impl<T: Map> Model<T>
|
||||
where
|
||||
T::Value: Serialize,
|
||||
{
|
||||
pub fn insert(&mut self, key: &T::Key, value: &T::Value) -> Result<(), Error> {
|
||||
pub fn insert_model(
|
||||
&mut self,
|
||||
key: &T::Key,
|
||||
value: Model<T::Value>,
|
||||
) -> Result<Option<Model<T::Value>>, Error> {
|
||||
use patch_db::ModelExt;
|
||||
use serde::ser::Error;
|
||||
let v = patch_db::value::to_value(value)?;
|
||||
let v = value.into_value();
|
||||
match &mut self.value {
|
||||
Value::Object(o) => {
|
||||
o.insert(T::key_string(key)?, v);
|
||||
Ok(())
|
||||
let prev = o.insert(T::key_string(key)?, v);
|
||||
Ok(prev.map(|v| Model::from_value(v)))
|
||||
}
|
||||
v => Err(patch_db::value::Error {
|
||||
source: patch_db::value::ErrorSource::custom(format!("expected object found {v}")),
|
||||
@@ -231,6 +236,13 @@ where
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
key: &T::Key,
|
||||
value: &T::Value,
|
||||
) -> Result<Option<Model<T::Value>>, Error> {
|
||||
self.insert_model(key, Model::new(value)?)
|
||||
}
|
||||
pub fn upsert<F>(&mut self, key: &T::Key, value: F) -> Result<&mut Model<T::Value>, Error>
|
||||
where
|
||||
F: FnOnce() -> Result<T::Value, Error>,
|
||||
@@ -257,22 +269,6 @@ where
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
pub fn insert_model(&mut self, key: &T::Key, value: Model<T::Value>) -> Result<(), Error> {
|
||||
use patch_db::ModelExt;
|
||||
use serde::ser::Error;
|
||||
let v = value.into_value();
|
||||
match &mut self.value {
|
||||
Value::Object(o) => {
|
||||
o.insert(T::key_string(key)?, v);
|
||||
Ok(())
|
||||
}
|
||||
v => Err(patch_db::value::Error {
|
||||
source: patch_db::value::ErrorSource::custom(format!("expected object found {v}")),
|
||||
kind: patch_db::value::ErrorKind::Serialization,
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Map> Model<T>
|
||||
|
||||
@@ -175,7 +175,7 @@ pub async fn install(
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SideloadParams {
|
||||
#[ts(skip)]
|
||||
#[serde(rename = "__auth_session")]
|
||||
#[serde(rename = "__Auth_session")]
|
||||
session: Option<InternedString>,
|
||||
}
|
||||
|
||||
|
||||
@@ -79,19 +79,18 @@ pub use error::{Error, ErrorKind, ResultExt};
|
||||
use imbl_value::Value;
|
||||
use rpc_toolkit::yajrc::RpcError;
|
||||
use rpc_toolkit::{
|
||||
CallRemoteHandler, Context, Empty, HandlerExt, ParentHandler, from_fn, from_fn_async,
|
||||
from_fn_blocking,
|
||||
from_fn, from_fn_async, from_fn_blocking, CallRemoteHandler, Context, Empty, HandlerExt,
|
||||
ParentHandler,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::context::{
|
||||
CliContext, DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext,
|
||||
};
|
||||
use crate::context::{CliContext, DiagnosticContext, InitContext, RpcContext};
|
||||
use crate::disk::fsck::RequiresReboot;
|
||||
use crate::registry::context::{RegistryContext, RegistryUrlParams};
|
||||
use crate::system::kiosk;
|
||||
use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable};
|
||||
use crate::tunnel::context::TunnelUrlParams;
|
||||
use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat};
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -139,6 +138,20 @@ pub fn main_api<C: Context>() -> ParentHandler<C> {
|
||||
.with_about("Display the API that is currently serving")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"state",
|
||||
from_fn(|_: InitContext| Ok::<_, Error>(ApiState::Initializing))
|
||||
.with_metadata("authenticated", Value::Bool(false))
|
||||
.with_about("Display the API that is currently serving")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"state",
|
||||
from_fn(|_: DiagnosticContext| Ok::<_, Error>(ApiState::Error))
|
||||
.with_metadata("authenticated", Value::Bool(false))
|
||||
.with_about("Display the API that is currently serving")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"server",
|
||||
server::<C>()
|
||||
@@ -191,6 +204,19 @@ pub fn main_api<C: Context>() -> ParentHandler<C> {
|
||||
)
|
||||
.no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"registry",
|
||||
registry::registry_api::<CliContext>().with_about("Commands related to the registry"),
|
||||
)
|
||||
.subcommand(
|
||||
"tunnel",
|
||||
CallRemoteHandler::<RpcContext, _, _, TunnelUrlParams>::new(tunnel::api::tunnel_api())
|
||||
.no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"tunnel",
|
||||
tunnel::api::tunnel_api::<CliContext>().with_about("Commands related to StartTunnel"),
|
||||
)
|
||||
.subcommand(
|
||||
"s9pk",
|
||||
s9pk::rpc::s9pk().with_about("Commands for interacting with s9pk files"),
|
||||
@@ -198,6 +224,28 @@ pub fn main_api<C: Context>() -> ParentHandler<C> {
|
||||
.subcommand(
|
||||
"util",
|
||||
util::rpc::util::<C>().with_about("Command for calculating the blake3 hash of a file"),
|
||||
)
|
||||
.subcommand(
|
||||
"init",
|
||||
from_fn_async(developer::init)
|
||||
.no_display()
|
||||
.with_about("Create developer key if it doesn't exist"),
|
||||
)
|
||||
.subcommand(
|
||||
"pubkey",
|
||||
from_fn_blocking(developer::pubkey)
|
||||
.with_about("Get public key for developer private key"),
|
||||
)
|
||||
.subcommand(
|
||||
"diagnostic",
|
||||
diagnostic::diagnostic::<C>()
|
||||
.with_about("Commands to display logs, restart the server, etc"),
|
||||
)
|
||||
.subcommand("setup", setup::setup::<C>())
|
||||
.subcommand(
|
||||
"install",
|
||||
os_install::install::<C>()
|
||||
.with_about("Commands to list disk info, install StartOS, and reboot"),
|
||||
);
|
||||
if &*PLATFORM != "raspberrypi" {
|
||||
api = api.subcommand("kiosk", kiosk::<C>());
|
||||
@@ -484,127 +532,3 @@ pub fn package<C: Context>() -> ParentHandler<C> {
|
||||
net::host::host_api::<C>().with_about("Manage network hosts for a package"),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn diagnostic_api() -> ParentHandler<DiagnosticContext> {
|
||||
ParentHandler::new()
|
||||
.subcommand(
|
||||
"git-info",
|
||||
from_fn(|_: DiagnosticContext| version::git_info())
|
||||
.with_metadata("authenticated", Value::Bool(false))
|
||||
.with_about("Display the githash of StartOS CLI"),
|
||||
)
|
||||
.subcommand(
|
||||
"echo",
|
||||
from_fn(echo::<DiagnosticContext>)
|
||||
.with_about("Echo a message")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"state",
|
||||
from_fn(|_: DiagnosticContext| Ok::<_, Error>(ApiState::Error))
|
||||
.with_metadata("authenticated", Value::Bool(false))
|
||||
.with_about("Display the API that is currently serving")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"diagnostic",
|
||||
diagnostic::diagnostic::<DiagnosticContext>()
|
||||
.with_about("Diagnostic commands i.e. logs, restart, rebuild"),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn init_api() -> ParentHandler<InitContext> {
|
||||
ParentHandler::new()
|
||||
.subcommand(
|
||||
"git-info",
|
||||
from_fn(|_: InitContext| version::git_info())
|
||||
.with_metadata("authenticated", Value::Bool(false))
|
||||
.with_about("Display the githash of StartOS CLI"),
|
||||
)
|
||||
.subcommand(
|
||||
"echo",
|
||||
from_fn(echo::<InitContext>)
|
||||
.with_about("Echo a message")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"state",
|
||||
from_fn(|_: InitContext| Ok::<_, Error>(ApiState::Initializing))
|
||||
.with_metadata("authenticated", Value::Bool(false))
|
||||
.with_about("Display the API that is currently serving")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"init",
|
||||
init::init_api::<InitContext>()
|
||||
.with_about("Commands to get logs or initialization progress"),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn setup_api() -> ParentHandler<SetupContext> {
|
||||
ParentHandler::new()
|
||||
.subcommand(
|
||||
"git-info",
|
||||
from_fn(|_: SetupContext| version::git_info())
|
||||
.with_metadata("authenticated", Value::Bool(false))
|
||||
.with_about("Display the githash of StartOS CLI"),
|
||||
)
|
||||
.subcommand(
|
||||
"echo",
|
||||
from_fn(echo::<SetupContext>)
|
||||
.with_about("Echo a message")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand("setup", setup::setup::<SetupContext>())
|
||||
}
|
||||
|
||||
pub fn install_api() -> ParentHandler<InstallContext> {
|
||||
ParentHandler::new()
|
||||
.subcommand(
|
||||
"git-info",
|
||||
from_fn(|_: InstallContext| version::git_info())
|
||||
.with_metadata("authenticated", Value::Bool(false))
|
||||
.with_about("Display the githash of StartOS CLI"),
|
||||
)
|
||||
.subcommand(
|
||||
"echo",
|
||||
from_fn(echo::<InstallContext>)
|
||||
.with_about("Echo a message")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"install",
|
||||
os_install::install::<InstallContext>()
|
||||
.with_about("Commands to list disk info, install StartOS, and reboot"),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn expanded_api() -> ParentHandler<CliContext> {
|
||||
main_api()
|
||||
.subcommand(
|
||||
"init",
|
||||
from_fn_async(developer::init)
|
||||
.no_display()
|
||||
.with_about("Create developer key if it doesn't exist"),
|
||||
)
|
||||
.subcommand(
|
||||
"pubkey",
|
||||
from_fn_blocking(developer::pubkey)
|
||||
.with_about("Get public key for developer private key"),
|
||||
)
|
||||
.subcommand(
|
||||
"diagnostic",
|
||||
diagnostic::diagnostic::<CliContext>()
|
||||
.with_about("Commands to display logs, restart the server, etc"),
|
||||
)
|
||||
.subcommand("setup", setup::setup::<CliContext>())
|
||||
.subcommand(
|
||||
"install",
|
||||
os_install::install::<CliContext>()
|
||||
.with_about("Commands to list disk info, install StartOS, and reboot"),
|
||||
)
|
||||
.subcommand(
|
||||
"registry",
|
||||
registry::registry_api::<CliContext>().with_about("Commands related to the registry"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ use chrono::Utc;
|
||||
use color_eyre::eyre::eyre;
|
||||
use digest::Digest;
|
||||
use helpers::const_true;
|
||||
use http::HeaderValue;
|
||||
use http::header::{COOKIE, USER_AGENT};
|
||||
use imbl_value::{InternedString, json};
|
||||
use http::HeaderValue;
|
||||
use imbl_value::{json, InternedString};
|
||||
use rand::random;
|
||||
use rpc_toolkit::yajrc::INTERNAL_ERROR;
|
||||
use rpc_toolkit::{Middleware, RpcRequest, RpcResponse};
|
||||
@@ -25,18 +25,15 @@ use tokio::io::AsyncWriteExt;
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::auth::{Sessions, check_password, write_shadow};
|
||||
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::Invoke;
|
||||
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 trait AuthContext: SignatureAuthContext {
|
||||
const LOCAL_AUTH_COOKIE_PATH: &str;
|
||||
@@ -66,65 +63,6 @@ pub trait AuthContext: SignatureAuthContext {
|
||||
}
|
||||
}
|
||||
|
||||
impl SignatureAuthContext for RpcContext {
|
||||
type Database = Database;
|
||||
type AdditionalMetadata = ();
|
||||
type CheckPubkeyRes = ();
|
||||
fn db(&self) -> &TypedPatchDb<Self::Database> {
|
||||
&self.db
|
||||
}
|
||||
async fn sig_context(
|
||||
&self,
|
||||
) -> impl IntoIterator<Item = Result<impl AsRef<str> + 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_public_domains()
|
||||
.keys()
|
||||
.map(|k| k.into_iter())
|
||||
.transpose(),
|
||||
)
|
||||
.chain(
|
||||
peek.as_public()
|
||||
.as_server_info()
|
||||
.as_network()
|
||||
.as_host()
|
||||
.as_private_domains()
|
||||
.de()
|
||||
.map(|k| k.into_iter())
|
||||
.transpose(),
|
||||
)
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
fn check_pubkey(
|
||||
db: &Model<Self::Database>,
|
||||
pubkey: Option<&AnyVerifyingKey>,
|
||||
_: Self::AdditionalMetadata,
|
||||
) -> Result<Self::CheckPubkeyRes, Error> {
|
||||
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";
|
||||
@@ -439,7 +377,7 @@ impl<C: AuthContext> Middleware<C> for Auth {
|
||||
));
|
||||
}
|
||||
if let Some(user_agent) = self.user_agent.as_ref().and_then(|h| h.to_str().ok()) {
|
||||
request.params["__auth_userAgent"] =
|
||||
request.params["__Auth_userAgent"] =
|
||||
Value::String(Arc::new(user_agent.to_owned()))
|
||||
// TODO: will this panic?
|
||||
}
|
||||
@@ -458,7 +396,7 @@ impl<C: AuthContext> Middleware<C> for Auth {
|
||||
{
|
||||
match HasValidSession::from_header(self.cookie.as_ref(), context).await? {
|
||||
HasValidSession(SessionType::Session(s)) if metadata.get_session => {
|
||||
request.params["__auth_session"] =
|
||||
request.params["__Auth_session"] =
|
||||
Value::String(Arc::new(s.hashed().deref().to_owned()));
|
||||
}
|
||||
_ => (),
|
||||
|
||||
55
core/startos/src/middleware/connect_info.rs
Normal file
55
core/startos/src/middleware/connect_info.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use axum::extract::Request;
|
||||
use axum::response::Response;
|
||||
use imbl_value::json;
|
||||
use rpc_toolkit::Middleware;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct ConnectInfo {
|
||||
peer_addr: Option<SocketAddr>,
|
||||
local_addr: Option<SocketAddr>,
|
||||
}
|
||||
impl ConnectInfo {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Metadata {
|
||||
get_connect_info: bool,
|
||||
}
|
||||
|
||||
impl<Context: Send + Sync + 'static> Middleware<Context> for ConnectInfo {
|
||||
type Metadata = Metadata;
|
||||
async fn process_http_request(
|
||||
&mut self,
|
||||
_: &Context,
|
||||
request: &mut Request,
|
||||
) -> Result<(), Response> {
|
||||
if let Some(axum::extract::ConnectInfo((peer, local))) = request.extensions().get().cloned()
|
||||
{
|
||||
self.peer_addr = Some(peer);
|
||||
self.local_addr = Some(local);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
async fn process_rpc_request(
|
||||
&mut self,
|
||||
_: &Context,
|
||||
metadata: Self::Metadata,
|
||||
request: &mut rpc_toolkit::RpcRequest,
|
||||
) -> Result<(), rpc_toolkit::RpcResponse> {
|
||||
if metadata.get_connect_info {
|
||||
if let Some(peer_addr) = self.peer_addr {
|
||||
request.params["__ConnectInfo_peer_addr"] = json!(peer_addr);
|
||||
}
|
||||
if let Some(local_addr) = self.local_addr {
|
||||
request.params["__ConnectInfo_local_addr"] = json!(local_addr);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod auth;
|
||||
pub mod connect_info;
|
||||
pub mod cors;
|
||||
pub mod db;
|
||||
pub mod signature;
|
||||
|
||||
@@ -5,21 +5,26 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::extract::Request;
|
||||
use http::HeaderValue;
|
||||
use http::{HeaderMap, HeaderValue};
|
||||
use reqwest::Client;
|
||||
use rpc_toolkit::yajrc::RpcError;
|
||||
use rpc_toolkit::{Context, Middleware, RpcRequest, RpcResponse};
|
||||
use serde::Deserialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::Mutex;
|
||||
use url::Url;
|
||||
|
||||
use crate::context::CliContext;
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::db::model::Database;
|
||||
use crate::prelude::*;
|
||||
use crate::sign::commitment::Commitment;
|
||||
use crate::sign::commitment::request::RequestCommitment;
|
||||
use crate::sign::commitment::Commitment;
|
||||
use crate::sign::{AnySignature, AnySigningKey, AnyVerifyingKey, SignatureScheme};
|
||||
use crate::util::iter::TransposeResultIterExt;
|
||||
use crate::util::serde::Base64;
|
||||
|
||||
pub const AUTH_SIG_HEADER: &str = "X-StartOS-Auth-Sig";
|
||||
|
||||
pub trait SignatureAuthContext: Context {
|
||||
type Database: HasModel<Model = Model<Self::Database>> + Send + Sync;
|
||||
type AdditionalMetadata: DeserializeOwned + Send;
|
||||
@@ -28,7 +33,7 @@ pub trait SignatureAuthContext: Context {
|
||||
fn sig_context(
|
||||
&self,
|
||||
) -> impl Future<Output = impl IntoIterator<Item = Result<impl AsRef<str> + Send, Error>> + Send>
|
||||
+ Send;
|
||||
+ Send;
|
||||
fn check_pubkey(
|
||||
db: &Model<Self::Database>,
|
||||
pubkey: Option<&AnyVerifyingKey>,
|
||||
@@ -41,7 +46,82 @@ pub trait SignatureAuthContext: Context {
|
||||
) -> impl Future<Output = Result<(), Error>> + Send;
|
||||
}
|
||||
|
||||
pub const AUTH_SIG_HEADER: &str = "X-StartOS-Auth-Sig";
|
||||
impl SignatureAuthContext for RpcContext {
|
||||
type Database = Database;
|
||||
type AdditionalMetadata = ();
|
||||
type CheckPubkeyRes = ();
|
||||
fn db(&self) -> &TypedPatchDb<Self::Database> {
|
||||
&self.db
|
||||
}
|
||||
async fn sig_context(
|
||||
&self,
|
||||
) -> impl IntoIterator<Item = Result<impl AsRef<str> + Send, Error>> + Send {
|
||||
let peek = self.db.peek().await;
|
||||
self.account.peek(|a| {
|
||||
a.hostnames()
|
||||
.into_iter()
|
||||
.map(Ok)
|
||||
.chain(
|
||||
peek.as_public()
|
||||
.as_server_info()
|
||||
.as_network()
|
||||
.as_host()
|
||||
.as_public_domains()
|
||||
.keys()
|
||||
.map(|k| k.into_iter())
|
||||
.transpose(),
|
||||
)
|
||||
.chain(
|
||||
peek.as_public()
|
||||
.as_server_info()
|
||||
.as_network()
|
||||
.as_host()
|
||||
.as_private_domains()
|
||||
.de()
|
||||
.map(|k| k.into_iter())
|
||||
.transpose(),
|
||||
)
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
}
|
||||
fn check_pubkey(
|
||||
db: &Model<Self::Database>,
|
||||
pubkey: Option<&AnyVerifyingKey>,
|
||||
_: Self::AdditionalMetadata,
|
||||
) -> Result<Self::CheckPubkeyRes, Error> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SigningContext {
|
||||
fn signing_key(&self) -> Result<AnySigningKey, Error>;
|
||||
}
|
||||
|
||||
impl SigningContext for CliContext {
|
||||
fn signing_key(&self) -> Result<AnySigningKey, Error> {
|
||||
Ok(AnySigningKey::Ed25519(self.developer_key()?.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl SigningContext for RpcContext {
|
||||
fn signing_key(&self) -> Result<AnySigningKey, Error> {
|
||||
Ok(AnySigningKey::Ed25519(
|
||||
self.account.peek(|a| a.developer_key.clone()),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Metadata<Additional> {
|
||||
@@ -203,7 +283,7 @@ impl<C: SignatureAuthContext> Middleware<C> for SignatureAuth {
|
||||
let signer = self.signer.take().transpose()?;
|
||||
if metadata.get_signer {
|
||||
if let Some(signer) = &signer {
|
||||
request.params["__auth_signer"] = to_value(signer)?;
|
||||
request.params["__Auth_signer"] = to_value(signer)?;
|
||||
}
|
||||
}
|
||||
let db = context.db().peek().await;
|
||||
@@ -216,17 +296,18 @@ impl<C: SignatureAuthContext> Middleware<C> for SignatureAuth {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn call_remote(
|
||||
ctx: &CliContext,
|
||||
pub async fn call_remote<Ctx: SigningContext + AsRef<Client>>(
|
||||
ctx: &Ctx,
|
||||
url: Url,
|
||||
sig_context: &str,
|
||||
headers: HeaderMap,
|
||||
sig_context: Option<&str>,
|
||||
method: &str,
|
||||
params: Value,
|
||||
) -> Result<Value, RpcError> {
|
||||
use reqwest::Method;
|
||||
use reqwest::header::{ACCEPT, CONTENT_LENGTH, CONTENT_TYPE};
|
||||
use rpc_toolkit::RpcResponse;
|
||||
use reqwest::Method;
|
||||
use rpc_toolkit::yajrc::{GenericRpcMethod, Id, RpcRequest};
|
||||
use rpc_toolkit::RpcResponse;
|
||||
|
||||
let rpc_req = RpcRequest {
|
||||
id: Some(Id::Number(0.into())),
|
||||
@@ -235,16 +316,16 @@ pub async fn call_remote(
|
||||
};
|
||||
let body = serde_json::to_vec(&rpc_req)?;
|
||||
let mut req = ctx
|
||||
.client
|
||||
.as_ref()
|
||||
.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() {
|
||||
.header(CONTENT_LENGTH, body.len())
|
||||
.headers(headers);
|
||||
if let (Some(sig_ctx), Ok(key)) = (sig_context, ctx.signing_key()) {
|
||||
req = req.header(
|
||||
AUTH_SIG_HEADER,
|
||||
SignatureHeader::sign(&AnySigningKey::Ed25519(key.clone()), &body, sig_context)?
|
||||
.to_header(),
|
||||
SignatureHeader::sign(&key, &body, sig_ctx)?.to_header(),
|
||||
);
|
||||
}
|
||||
let res = req.body(body).send().await?;
|
||||
|
||||
@@ -244,6 +244,8 @@ mod active_connection {
|
||||
default_service = "org.freedesktop.NetworkManager"
|
||||
)]
|
||||
trait ConnectionSettings {
|
||||
fn delete(&self) -> Result<(), Error>;
|
||||
|
||||
fn get_settings(&self) -> Result<HashMap<String, HashMap<String, OwnedValue>>, Error>;
|
||||
|
||||
fn update2(
|
||||
@@ -1095,7 +1097,7 @@ impl NetworkInterfaceController {
|
||||
.ip_info
|
||||
.peek(|ifaces| ifaces.get(interface).map(|i| i.ip_info.is_some()))
|
||||
else {
|
||||
return Ok(());
|
||||
return self.forget(interface).await;
|
||||
};
|
||||
|
||||
if has_ip_info {
|
||||
@@ -1115,7 +1117,21 @@ impl NetworkInterfaceController {
|
||||
|
||||
let device_proxy = DeviceProxy::new(&connection, device).await?;
|
||||
|
||||
device_proxy.delete().await?;
|
||||
let ac = device_proxy.active_connection().await?;
|
||||
|
||||
if &*ac == "/" {
|
||||
return Err(Error::new(
|
||||
eyre!("Cannot delete device without active connection"),
|
||||
ErrorKind::InvalidRequest,
|
||||
));
|
||||
}
|
||||
|
||||
let ac_proxy = active_connection::ActiveConnectionProxy::new(&connection, ac).await?;
|
||||
|
||||
let settings =
|
||||
ConnectionSettingsProxy::new(&connection, ac_proxy.connection().await?).await?;
|
||||
|
||||
settings.delete().await?;
|
||||
|
||||
ip_info
|
||||
.wait_for(|ifaces| ifaces.get(interface).map_or(true, |i| i.ip_info.is_none()))
|
||||
|
||||
@@ -6,11 +6,11 @@ use std::sync::Arc;
|
||||
use std::time::UNIX_EPOCH;
|
||||
|
||||
use async_compression::tokio::bufread::GzipEncoder;
|
||||
use axum::Router;
|
||||
use axum::body::Body;
|
||||
use axum::extract::{self as x, Request};
|
||||
use axum::response::{Redirect, Response};
|
||||
use axum::routing::{any, get};
|
||||
use axum::Router;
|
||||
use base64::display::Base64Display;
|
||||
use digest::Digest;
|
||||
use futures::future::ready;
|
||||
@@ -33,20 +33,20 @@ use url::Url;
|
||||
|
||||
use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext};
|
||||
use crate::hostname::Hostname;
|
||||
use crate::main_api;
|
||||
use crate::middleware::auth::{Auth, HasValidSession};
|
||||
use crate::middleware::cors::Cors;
|
||||
use crate::middleware::db::SyncDb;
|
||||
use crate::prelude::*;
|
||||
use crate::rpc_continuations::{Guid, RpcContinuations};
|
||||
use crate::s9pk::S9pk;
|
||||
use crate::s9pk::merkle_archive::source::FileSource;
|
||||
use crate::s9pk::merkle_archive::source::http::HttpSource;
|
||||
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
|
||||
use crate::s9pk::merkle_archive::source::FileSource;
|
||||
use crate::s9pk::S9pk;
|
||||
use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment;
|
||||
use crate::util::io::open_file;
|
||||
use crate::util::net::SyncBody;
|
||||
use crate::util::serde::BASE64;
|
||||
use crate::{diagnostic_api, init_api, install_api, main_api, setup_api};
|
||||
|
||||
const NOT_FOUND: &[u8] = b"Not Found";
|
||||
const METHOD_NOT_ALLOWED: &[u8] = b"Method Not Allowed";
|
||||
@@ -61,20 +61,84 @@ const EMBEDDED_UIS: Dir<'_> =
|
||||
#[cfg(not(all(feature = "startd", not(feature = "test"))))]
|
||||
const EMBEDDED_UIS: Dir<'_> = Dir::new("", &[]);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum UiMode {
|
||||
Setup,
|
||||
Install,
|
||||
Main,
|
||||
pub trait UiContext: Context + AsRef<RpcContinuations> + Clone + Sized {
|
||||
fn path(path: &str) -> PathBuf;
|
||||
fn middleware(server: Server<Self>) -> HttpServer<Self>;
|
||||
fn extend_router(self, router: Router) -> Router {
|
||||
router
|
||||
}
|
||||
}
|
||||
|
||||
impl UiMode {
|
||||
fn path(&self, path: &str) -> PathBuf {
|
||||
match self {
|
||||
Self::Setup => Path::new("setup-wizard").join(path),
|
||||
Self::Install => Path::new("install-wizard").join(path),
|
||||
Self::Main => Path::new("ui").join(path),
|
||||
}
|
||||
impl UiContext for RpcContext {
|
||||
fn path(path: &str) -> PathBuf {
|
||||
Path::new("ui").join(path)
|
||||
}
|
||||
fn middleware(server: Server<Self>) -> HttpServer<Self> {
|
||||
server
|
||||
.middleware(Cors::new())
|
||||
.middleware(Auth::new())
|
||||
.middleware(SyncDb::new())
|
||||
}
|
||||
fn extend_router(self, router: Router) -> Router {
|
||||
router
|
||||
.route("/proxy/{url}", {
|
||||
let ctx = self.clone();
|
||||
any(move |x::Path(url): x::Path<String>, request: Request| {
|
||||
let ctx = ctx.clone();
|
||||
async move {
|
||||
proxy_request(ctx, request, url)
|
||||
.await
|
||||
.unwrap_or_else(server_error)
|
||||
}
|
||||
})
|
||||
})
|
||||
.nest("/s9pk", s9pk_router(self.clone()))
|
||||
.route(
|
||||
"/static/local-root-ca.crt",
|
||||
get(move || {
|
||||
let ctx = self.clone();
|
||||
async move {
|
||||
ctx.account
|
||||
.peek(|account| cert_send(&account.root_ca_cert, &account.hostname))
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl UiContext for InitContext {
|
||||
fn path(path: &str) -> PathBuf {
|
||||
Path::new("ui").join(path)
|
||||
}
|
||||
fn middleware(server: Server<Self>) -> HttpServer<Self> {
|
||||
server.middleware(Cors::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl UiContext for DiagnosticContext {
|
||||
fn path(path: &str) -> PathBuf {
|
||||
Path::new("ui").join(path)
|
||||
}
|
||||
fn middleware(server: Server<Self>) -> HttpServer<Self> {
|
||||
server.middleware(Cors::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl UiContext for SetupContext {
|
||||
fn path(path: &str) -> PathBuf {
|
||||
Path::new("setup-wizard").join(path)
|
||||
}
|
||||
fn middleware(server: Server<Self>) -> HttpServer<Self> {
|
||||
server.middleware(Cors::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl UiContext for InstallContext {
|
||||
fn path(path: &str) -> PathBuf {
|
||||
Path::new("install-wizard").join(path)
|
||||
}
|
||||
fn middleware(server: Server<Self>) -> HttpServer<Self> {
|
||||
server.middleware(Cors::new())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,11 +175,11 @@ pub fn rpc_router<C: Context + Clone + AsRef<RpcContinuations>>(
|
||||
)
|
||||
}
|
||||
|
||||
fn serve_ui(req: Request, ui_mode: UiMode) -> Result<Response, Error> {
|
||||
fn serve_ui<C: UiContext>(req: Request) -> Result<Response, Error> {
|
||||
let (request_parts, _body) = req.into_parts();
|
||||
match &request_parts.method {
|
||||
&Method::GET | &Method::HEAD => {
|
||||
let uri_path = ui_mode.path(
|
||||
let uri_path = C::path(
|
||||
request_parts
|
||||
.uri
|
||||
.path()
|
||||
@@ -125,7 +189,7 @@ fn serve_ui(req: Request, ui_mode: UiMode) -> Result<Response, Error> {
|
||||
|
||||
let file = EMBEDDED_UIS
|
||||
.get_file(&*uri_path)
|
||||
.or_else(|| EMBEDDED_UIS.get_file(&*ui_mode.path("index.html")));
|
||||
.or_else(|| EMBEDDED_UIS.get_file(&*C::path("index.html")));
|
||||
|
||||
if let Some(file) = file {
|
||||
FileData::from_embedded(&request_parts, file)?.into_response(&request_parts)
|
||||
@@ -137,79 +201,15 @@ fn serve_ui(req: Request, ui_mode: UiMode) -> Result<Response, Error> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup_ui_router(ctx: SetupContext) -> Router {
|
||||
rpc_router(
|
||||
ctx.clone(),
|
||||
Server::new(move || ready(Ok(ctx.clone())), setup_api()).middleware(Cors::new()),
|
||||
)
|
||||
.fallback(any(|request: Request| async move {
|
||||
serve_ui(request, UiMode::Setup).unwrap_or_else(server_error)
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn diagnostic_ui_router(ctx: DiagnosticContext) -> Router {
|
||||
rpc_router(
|
||||
ctx.clone(),
|
||||
Server::new(move || ready(Ok(ctx.clone())), diagnostic_api()).middleware(Cors::new()),
|
||||
)
|
||||
.fallback(any(|request: Request| async move {
|
||||
serve_ui(request, UiMode::Main).unwrap_or_else(server_error)
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn install_ui_router(ctx: InstallContext) -> Router {
|
||||
rpc_router(
|
||||
ctx.clone(),
|
||||
Server::new(move || ready(Ok(ctx.clone())), install_api()).middleware(Cors::new()),
|
||||
)
|
||||
.fallback(any(|request: Request| async move {
|
||||
serve_ui(request, UiMode::Install).unwrap_or_else(server_error)
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn init_ui_router(ctx: InitContext) -> Router {
|
||||
rpc_router(
|
||||
ctx.clone(),
|
||||
Server::new(move || ready(Ok(ctx.clone())), init_api()).middleware(Cors::new()),
|
||||
)
|
||||
.fallback(any(|request: Request| async move {
|
||||
serve_ui(request, UiMode::Main).unwrap_or_else(server_error)
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn main_ui_router(ctx: RpcContext) -> Router {
|
||||
rpc_router(ctx.clone(), {
|
||||
let ctx = ctx.clone();
|
||||
Server::new(move || ready(Ok(ctx.clone())), main_api::<RpcContext>())
|
||||
.middleware(Cors::new())
|
||||
.middleware(Auth::new())
|
||||
.middleware(SyncDb::new())
|
||||
})
|
||||
.route("/proxy/{url}", {
|
||||
let ctx = ctx.clone();
|
||||
any(move |x::Path(url): x::Path<String>, request: Request| {
|
||||
let ctx = ctx.clone();
|
||||
async move {
|
||||
proxy_request(ctx, request, url)
|
||||
.await
|
||||
.unwrap_or_else(server_error)
|
||||
}
|
||||
})
|
||||
})
|
||||
.nest("/s9pk", s9pk_router(ctx.clone()))
|
||||
.route(
|
||||
"/static/local-root-ca.crt",
|
||||
get(move || {
|
||||
let ctx = ctx.clone();
|
||||
async move {
|
||||
let account = ctx.account.read().await;
|
||||
cert_send(&account.root_ca_cert, &account.hostname)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.fallback(any(|request: Request| async move {
|
||||
serve_ui(request, UiMode::Main).unwrap_or_else(server_error)
|
||||
}))
|
||||
pub fn ui_router<C: UiContext>(ctx: C) -> Router {
|
||||
ctx.clone()
|
||||
.extend_router(rpc_router(
|
||||
ctx.clone(),
|
||||
C::middleware(Server::new(move || ready(Ok(ctx.clone())), main_api())),
|
||||
))
|
||||
.fallback(any(|request: Request| async move {
|
||||
serve_ui::<C>(request).unwrap_or_else(server_error)
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn refresher() -> Router {
|
||||
|
||||
@@ -191,7 +191,8 @@ impl Model<OnionStore> {
|
||||
Ok(key)
|
||||
}
|
||||
pub fn insert_key(&mut self, key: &TorSecretKey) -> Result<(), Error> {
|
||||
self.insert(&key.onion_address(), &key)
|
||||
self.insert(&key.onion_address(), &key)?;
|
||||
Ok(())
|
||||
}
|
||||
pub fn get_key(&self, address: &OnionAddress) -> Result<TorSecretKey, Error> {
|
||||
self.as_idx(address)
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::sync::Arc;
|
||||
use std::task::Poll;
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::extract::ConnectInfo;
|
||||
use axum::Router;
|
||||
use futures::future::Either;
|
||||
use futures::FutureExt;
|
||||
@@ -13,19 +14,17 @@ use hyper_util::rt::{TokioIo, TokioTimer};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext};
|
||||
use crate::net::gateway::{
|
||||
lookup_info_by_addr, NetworkInterfaceListener, SelfContainedNetworkInterfaceListener,
|
||||
};
|
||||
use crate::net::static_server::{
|
||||
diagnostic_ui_router, init_ui_router, install_ui_router, main_ui_router, redirecter, refresher,
|
||||
setup_ui_router,
|
||||
};
|
||||
use crate::net::static_server::{redirecter, refresher, ui_router, UiContext};
|
||||
use crate::prelude::*;
|
||||
use crate::util::actor::background::BackgroundJobQueue;
|
||||
use crate::util::sync::{SyncRwLock, Watch};
|
||||
|
||||
pub struct Accepted {
|
||||
pub peer_addr: SocketAddr,
|
||||
pub local_addr: SocketAddr,
|
||||
pub https_redirect: bool,
|
||||
pub stream: TcpStream,
|
||||
}
|
||||
@@ -37,8 +36,10 @@ pub trait Accept {
|
||||
impl Accept for Vec<TcpListener> {
|
||||
fn poll_accept(&mut self, cx: &mut std::task::Context<'_>) -> Poll<Result<Accepted, Error>> {
|
||||
for listener in &*self {
|
||||
if let Poll::Ready((stream, _)) = listener.poll_accept(cx)? {
|
||||
if let Poll::Ready((stream, peer_addr)) = listener.poll_accept(cx)? {
|
||||
return Poll::Ready(Ok(Accepted {
|
||||
local_addr: listener.local_addr()?,
|
||||
peer_addr,
|
||||
https_redirect: false,
|
||||
stream,
|
||||
}));
|
||||
@@ -55,6 +56,8 @@ impl Accept for NetworkInterfaceListener {
|
||||
.ip_info
|
||||
.peek(|i| lookup_info_by_addr(i, a.bind).map_or(true, |(_, i)| i.public()));
|
||||
Accepted {
|
||||
peer_addr: a.peer,
|
||||
local_addr: a.bind,
|
||||
https_redirect: public,
|
||||
stream: a.stream,
|
||||
}
|
||||
@@ -187,7 +190,12 @@ impl<A: Accept + Send + Sync + 'static> WebServer<A> {
|
||||
}
|
||||
}
|
||||
|
||||
struct SwappableRouter(Watch<Option<Router>>, bool);
|
||||
struct SwappableRouter {
|
||||
router: Watch<Option<Router>>,
|
||||
redirect: bool,
|
||||
local_addr: SocketAddr,
|
||||
peer_addr: SocketAddr,
|
||||
}
|
||||
impl hyper::service::Service<hyper::Request<hyper::body::Incoming>> for SwappableRouter {
|
||||
type Response = <Router as tower_service::Service<
|
||||
hyper::Request<hyper::body::Incoming>,
|
||||
@@ -199,13 +207,16 @@ impl<A: Accept + Send + Sync + 'static> WebServer<A> {
|
||||
hyper::Request<hyper::body::Incoming>,
|
||||
>>::Future;
|
||||
|
||||
fn call(&self, req: hyper::Request<hyper::body::Incoming>) -> Self::Future {
|
||||
fn call(&self, mut req: hyper::Request<hyper::body::Incoming>) -> Self::Future {
|
||||
use tower_service::Service;
|
||||
|
||||
if self.1 {
|
||||
req.extensions_mut()
|
||||
.insert(ConnectInfo((self.peer_addr, self.local_addr)));
|
||||
|
||||
if self.redirect {
|
||||
redirecter().call(req)
|
||||
} else {
|
||||
let router = self.0.read();
|
||||
let router = self.router.read();
|
||||
if let Some(mut router) = router {
|
||||
router.call(req)
|
||||
} else {
|
||||
@@ -239,15 +250,18 @@ impl<A: Accept + Send + Sync + 'static> WebServer<A> {
|
||||
for _ in 0..5 {
|
||||
if let Err(e) = async {
|
||||
let accepted = acceptor.accept().await?;
|
||||
let src = accepted.stream.peer_addr().ok();
|
||||
queue.add_job(
|
||||
graceful.watch(
|
||||
server
|
||||
.serve_connection_with_upgrades(
|
||||
TokioIo::new(accepted.stream),
|
||||
SwappableRouter(
|
||||
service.clone(),
|
||||
accepted.https_redirect,
|
||||
),
|
||||
SwappableRouter {
|
||||
router: service.clone(),
|
||||
redirect: accepted.https_redirect,
|
||||
peer_addr: accepted.peer_addr,
|
||||
local_addr: accepted.local_addr,
|
||||
},
|
||||
)
|
||||
.into_owned(),
|
||||
),
|
||||
@@ -303,23 +317,7 @@ impl<A: Accept + Send + Sync + 'static> WebServer<A> {
|
||||
self.router.send(Some(router))
|
||||
}
|
||||
|
||||
pub fn serve_main(&mut self, ctx: RpcContext) {
|
||||
self.serve_router(main_ui_router(ctx))
|
||||
}
|
||||
|
||||
pub fn serve_setup(&mut self, ctx: SetupContext) {
|
||||
self.serve_router(setup_ui_router(ctx))
|
||||
}
|
||||
|
||||
pub fn serve_diagnostic(&mut self, ctx: DiagnosticContext) {
|
||||
self.serve_router(diagnostic_ui_router(ctx))
|
||||
}
|
||||
|
||||
pub fn serve_install(&mut self, ctx: InstallContext) {
|
||||
self.serve_router(install_ui_router(ctx))
|
||||
}
|
||||
|
||||
pub fn serve_init(&mut self, ctx: InitContext) {
|
||||
self.serve_router(init_ui_router(ctx))
|
||||
pub fn serve_ui_for<C: UiContext>(&mut self, ctx: C) {
|
||||
self.serve_router(ui_router(ctx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::Parser;
|
||||
use clap::builder::TypedValueParser;
|
||||
use clap::Parser;
|
||||
use isocountry::CountryCode;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use rpc_toolkit::{Context, Empty, HandlerExt, ParentHandler, from_fn_async};
|
||||
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::RwLock;
|
||||
@@ -16,11 +16,11 @@ use tracing::instrument;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::db::model::Database;
|
||||
use crate::db::model::public::WifiInfo;
|
||||
use crate::db::model::Database;
|
||||
use crate::prelude::*;
|
||||
use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat};
|
||||
use crate::util::Invoke;
|
||||
use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable};
|
||||
use crate::{Error, ErrorKind};
|
||||
|
||||
type WifiManager = Arc<RwLock<Option<WpaCli>>>;
|
||||
@@ -1017,6 +1017,31 @@ pub async fn synchronize_network_manager<P: AsRef<Path>>(
|
||||
.await?;
|
||||
}
|
||||
|
||||
Command::new("ip")
|
||||
.arg("rule")
|
||||
.arg("add")
|
||||
.arg("pref")
|
||||
.arg("1000")
|
||||
.arg("from")
|
||||
.arg("all")
|
||||
.arg("lookup")
|
||||
.arg("main")
|
||||
.invoke(ErrorKind::Network)
|
||||
.await
|
||||
.log_err();
|
||||
Command::new("ip")
|
||||
.arg("rule")
|
||||
.arg("add")
|
||||
.arg("pref")
|
||||
.arg("1100")
|
||||
.arg("from")
|
||||
.arg("all")
|
||||
.arg("lookup")
|
||||
.arg("default")
|
||||
.invoke(ErrorKind::Network)
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
Command::new("systemctl")
|
||||
.arg("restart")
|
||||
.arg("NetworkManager")
|
||||
|
||||
@@ -463,7 +463,8 @@ pub fn notify<T: NotificationType>(
|
||||
data,
|
||||
seen: false,
|
||||
},
|
||||
)
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::sync::Arc;
|
||||
|
||||
use chrono::Utc;
|
||||
use clap::Parser;
|
||||
use http::HeaderMap;
|
||||
use imbl_value::InternedString;
|
||||
use patch_db::PatchDb;
|
||||
use reqwest::{Client, Proxy};
|
||||
@@ -17,13 +18,13 @@ use tracing::instrument;
|
||||
use ts_rs::TS;
|
||||
use url::Url;
|
||||
|
||||
use crate::context::config::{CONFIG_PATH, ContextConfig};
|
||||
use crate::context::config::{ContextConfig, CONFIG_PATH};
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::middleware::signature::SignatureAuthContext;
|
||||
use crate::prelude::*;
|
||||
use crate::registry::RegistryDatabase;
|
||||
use crate::registry::device_info::{DEVICE_INFO_HEADER, DeviceInfo};
|
||||
use crate::registry::device_info::{DeviceInfo, DEVICE_INFO_HEADER};
|
||||
use crate::registry::signer::SignerInfo;
|
||||
use crate::registry::RegistryDatabase;
|
||||
use crate::rpc_continuations::RpcContinuations;
|
||||
use crate::sign::AnyVerifyingKey;
|
||||
use crate::util::io::append_file;
|
||||
@@ -183,10 +184,17 @@ impl CallRemote<RegistryContext> for CliContext {
|
||||
let sig_context = self
|
||||
.registry_hostname
|
||||
.clone()
|
||||
.or(url.host().as_ref().map(InternedString::from_display))
|
||||
.or_not_found("registry hostname")?;
|
||||
.or_else(|| url.host().as_ref().map(InternedString::from_display));
|
||||
|
||||
crate::middleware::signature::call_remote(self, url, &sig_context, method, params).await
|
||||
crate::middleware::signature::call_remote(
|
||||
self,
|
||||
url,
|
||||
HeaderMap::new(),
|
||||
sig_context.as_deref(),
|
||||
method,
|
||||
params,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,59 +205,24 @@ impl CallRemote<RegistryContext, RegistryUrlParams> for RpcContext {
|
||||
params: Value,
|
||||
RegistryUrlParams { registry }: RegistryUrlParams,
|
||||
) -> Result<Value, RpcError> {
|
||||
use reqwest::Method;
|
||||
use reqwest::header::{ACCEPT, CONTENT_LENGTH, CONTENT_TYPE};
|
||||
use rpc_toolkit::RpcResponse;
|
||||
use rpc_toolkit::yajrc::{GenericRpcMethod, Id, RpcRequest};
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
DEVICE_INFO_HEADER,
|
||||
DeviceInfo::load(self).await?.to_header_value(),
|
||||
);
|
||||
|
||||
let url = registry.join("rpc/v0")?;
|
||||
method = method.strip_prefix("registry.").unwrap_or(method);
|
||||
let sig_context = registry.host_str().map(InternedString::from);
|
||||
|
||||
let rpc_req = RpcRequest {
|
||||
id: Some(Id::Number(0.into())),
|
||||
method: GenericRpcMethod::<_, _, Value>::new(method),
|
||||
crate::middleware::signature::call_remote(
|
||||
self,
|
||||
registry,
|
||||
headers,
|
||||
sig_context.as_deref(),
|
||||
method,
|
||||
params,
|
||||
};
|
||||
let body = serde_json::to_vec(&rpc_req)?;
|
||||
let res = self
|
||||
.client
|
||||
.request(Method::POST, url)
|
||||
.header(CONTENT_TYPE, "application/json")
|
||||
.header(ACCEPT, "application/json")
|
||||
.header(CONTENT_LENGTH, body.len())
|
||||
.header(
|
||||
DEVICE_INFO_HEADER,
|
||||
DeviceInfo::load(self).await?.to_header_value(),
|
||||
)
|
||||
.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::<RpcResponse>(&*res.bytes().await?)
|
||||
.with_kind(ErrorKind::Deserialization)?
|
||||
.result
|
||||
}
|
||||
_ => Err(Error::new(eyre!("unknown content type"), ErrorKind::Network).into()),
|
||||
}
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ use url::Url;
|
||||
use crate::context::RpcContext;
|
||||
use crate::prelude::*;
|
||||
use crate::registry::context::RegistryContext;
|
||||
use crate::util::VersionString;
|
||||
use crate::util::lshw::{LshwDevice, LshwDisplay, LshwProcessor};
|
||||
use crate::util::VersionString;
|
||||
use crate::version::VersionT;
|
||||
|
||||
pub const DEVICE_INFO_HEADER: &str = "X-StartOS-Device-Info";
|
||||
@@ -175,7 +175,7 @@ impl Middleware<RegistryContext> for DeviceInfoMiddleware {
|
||||
async move {
|
||||
if metadata.get_device_info {
|
||||
if let Some(device_info) = &self.device_info {
|
||||
request.params["__device_info"] =
|
||||
request.params["__DeviceInfo_device_info"] =
|
||||
to_value(&DeviceInfo::from_header_value(device_info)?)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ pub struct AddAssetParams {
|
||||
pub platform: InternedString,
|
||||
#[ts(type = "string")]
|
||||
pub url: Url,
|
||||
#[serde(rename = "__auth_signer")]
|
||||
#[serde(rename = "__Auth_signer")]
|
||||
#[ts(skip)]
|
||||
pub signer: AnyVerifyingKey,
|
||||
pub signature: AnySignature,
|
||||
@@ -289,7 +289,7 @@ pub struct RemoveAssetParams {
|
||||
pub version: Version,
|
||||
#[ts(type = "string")]
|
||||
pub platform: InternedString,
|
||||
#[serde(rename = "__auth_signer")]
|
||||
#[serde(rename = "__Auth_signer")]
|
||||
#[ts(skip)]
|
||||
pub signer: AnyVerifyingKey,
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ pub struct SignAssetParams {
|
||||
#[ts(type = "string")]
|
||||
platform: InternedString,
|
||||
#[ts(skip)]
|
||||
#[serde(rename = "__auth_signer")]
|
||||
#[serde(rename = "__Auth_signer")]
|
||||
signer: AnyVerifyingKey,
|
||||
signature: AnySignature,
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ pub struct AddVersionParams {
|
||||
pub source_version: VersionRange,
|
||||
#[arg(skip)]
|
||||
#[ts(skip)]
|
||||
#[serde(rename = "__auth_signer")]
|
||||
#[serde(rename = "__Auth_signer")]
|
||||
pub signer: Option<AnyVerifyingKey>,
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ pub struct GetOsVersionParams {
|
||||
platform: Option<InternedString>,
|
||||
#[ts(skip)]
|
||||
#[arg(skip)]
|
||||
#[serde(rename = "__device_info")]
|
||||
#[serde(rename = "__DeviceInfo_device_info")]
|
||||
pub device_info: Option<DeviceInfo>,
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ pub struct AddPackageParams {
|
||||
#[ts(type = "string")]
|
||||
pub url: Url,
|
||||
#[ts(skip)]
|
||||
#[serde(rename = "__auth_signer")]
|
||||
#[serde(rename = "__Auth_signer")]
|
||||
pub uploader: AnyVerifyingKey,
|
||||
pub commitment: MerkleArchiveCommitment,
|
||||
pub signature: AnySignature,
|
||||
@@ -169,7 +169,7 @@ pub struct RemovePackageParams {
|
||||
pub version: VersionString,
|
||||
#[ts(skip)]
|
||||
#[arg(skip)]
|
||||
#[serde(rename = "__auth_signer")]
|
||||
#[serde(rename = "__Auth_signer")]
|
||||
pub signer: Option<AnyVerifyingKey>,
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ use crate::prelude::*;
|
||||
use crate::registry::context::RegistryContext;
|
||||
use crate::registry::device_info::DeviceInfo;
|
||||
use crate::registry::package::index::{PackageIndex, PackageVersionInfo};
|
||||
use crate::util::serde::{display_serializable, WithIoFormat};
|
||||
use crate::util::VersionString;
|
||||
use crate::util::serde::{WithIoFormat, display_serializable};
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS, ValueEnum,
|
||||
@@ -51,7 +51,7 @@ pub struct GetPackageParams {
|
||||
pub source_version: Option<VersionString>,
|
||||
#[ts(skip)]
|
||||
#[arg(skip)]
|
||||
#[serde(rename = "__device_info")]
|
||||
#[serde(rename = "__DeviceInfo_device_info")]
|
||||
pub device_info: Option<DeviceInfo>,
|
||||
#[serde(default)]
|
||||
#[arg(default_value = "none")]
|
||||
|
||||
@@ -768,7 +768,7 @@ pub struct AttachParams {
|
||||
pub stderr_tty: bool,
|
||||
pub pty_size: Option<TermSize>,
|
||||
#[ts(skip)]
|
||||
#[serde(rename = "__auth_session")]
|
||||
#[serde(rename = "__Auth_session")]
|
||||
session: Option<InternedString>,
|
||||
#[ts(type = "string | null")]
|
||||
subcontainer: Option<InternedString>,
|
||||
|
||||
@@ -1,146 +1,179 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use futures::future::{BoxFuture, Either};
|
||||
use futures::FutureExt;
|
||||
use imbl::vector;
|
||||
|
||||
use super::ServiceActorSeed;
|
||||
use super::start_stop::StartStop;
|
||||
use super::ServiceActorSeed;
|
||||
use crate::prelude::*;
|
||||
use crate::service::SYNC_RETRY_COOLDOWN_SECONDS;
|
||||
use crate::service::persistent_container::ServiceStateKinds;
|
||||
use crate::service::transition::TransitionKind;
|
||||
use crate::service::SYNC_RETRY_COOLDOWN_SECONDS;
|
||||
use crate::status::MainStatus;
|
||||
use crate::util::actor::Actor;
|
||||
use crate::util::actor::background::BackgroundJobQueue;
|
||||
use crate::util::actor::Actor;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct ServiceActor(pub(super) Arc<ServiceActorSeed>);
|
||||
|
||||
enum ServiceActorLoopNext {
|
||||
Wait,
|
||||
DontWait,
|
||||
}
|
||||
|
||||
impl Actor for ServiceActor {
|
||||
fn init(&mut self, jobs: &BackgroundJobQueue) {
|
||||
let seed = self.0.clone();
|
||||
let mut current = seed.persistent_container.state.subscribe();
|
||||
jobs.add_job(async move {
|
||||
let _ = current.wait_for(|s| s.rt_initialized).await;
|
||||
let mut start_stop_task: Option<Either<_, _>> = None;
|
||||
|
||||
loop {
|
||||
match service_actor_loop(¤t, &seed).await {
|
||||
ServiceActorLoopNext::Wait => tokio::select! {
|
||||
_ = current.changed() => (),
|
||||
},
|
||||
ServiceActorLoopNext::DontWait => (),
|
||||
}
|
||||
let wait = match service_actor_loop(¤t, &seed, &mut start_stop_task).await {
|
||||
Ok(()) => Either::Right(current.changed().then(|res| async move {
|
||||
match res {
|
||||
Ok(()) => (),
|
||||
Err(_) => futures::future::pending().await,
|
||||
}
|
||||
})),
|
||||
Err(e) => {
|
||||
tracing::error!("error synchronizing state of service: {e}");
|
||||
tracing::debug!("{e:?}");
|
||||
|
||||
seed.synchronized.notify_waiters();
|
||||
|
||||
tracing::error!("Retrying in {}s...", SYNC_RETRY_COOLDOWN_SECONDS);
|
||||
Either::Left(tokio::time::sleep(Duration::from_secs(
|
||||
SYNC_RETRY_COOLDOWN_SECONDS,
|
||||
)))
|
||||
}
|
||||
};
|
||||
tokio::pin!(wait);
|
||||
let start_stop_handler = async {
|
||||
match &mut start_stop_task {
|
||||
Some(task) => {
|
||||
let err = task.await.log_err().is_none(); // TODO: ideally this error should be sent to service logs
|
||||
start_stop_task.take();
|
||||
if err {
|
||||
tokio::time::sleep(Duration::from_secs(
|
||||
SYNC_RETRY_COOLDOWN_SECONDS,
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
_ => futures::future::pending().await,
|
||||
}
|
||||
};
|
||||
tokio::pin!(start_stop_handler);
|
||||
futures::future::select(wait, start_stop_handler).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn service_actor_loop(
|
||||
async fn service_actor_loop<'a>(
|
||||
current: &tokio::sync::watch::Receiver<super::persistent_container::ServiceState>,
|
||||
seed: &Arc<ServiceActorSeed>,
|
||||
) -> ServiceActorLoopNext {
|
||||
seed: &'a Arc<ServiceActorSeed>,
|
||||
start_stop_task: &mut Option<
|
||||
Either<BoxFuture<'a, Result<(), Error>>, BoxFuture<'a, Result<(), Error>>>,
|
||||
>,
|
||||
) -> Result<(), Error> {
|
||||
let id = &seed.id;
|
||||
let kinds = current.borrow().kinds();
|
||||
if let Err(e) = async {
|
||||
let major_changes_state = seed
|
||||
.ctx
|
||||
.db
|
||||
.mutate(|d| {
|
||||
if let Some(i) = d.as_public_mut().as_package_data_mut().as_idx_mut(&id) {
|
||||
let previous = i.as_status().de()?;
|
||||
let main_status = match &kinds {
|
||||
ServiceStateKinds {
|
||||
transition_state: Some(TransitionKind::Restarting),
|
||||
..
|
||||
} => MainStatus::Restarting,
|
||||
ServiceStateKinds {
|
||||
transition_state: Some(TransitionKind::BackingUp),
|
||||
..
|
||||
} => previous.backing_up(),
|
||||
ServiceStateKinds {
|
||||
running_status: Some(status),
|
||||
desired_state: StartStop::Start,
|
||||
..
|
||||
} => MainStatus::Running {
|
||||
started: status.started,
|
||||
health: previous.health().cloned().unwrap_or_default(),
|
||||
},
|
||||
ServiceStateKinds {
|
||||
running_status: None,
|
||||
desired_state: StartStop::Start,
|
||||
..
|
||||
} => MainStatus::Starting {
|
||||
health: previous.health().cloned().unwrap_or_default(),
|
||||
},
|
||||
ServiceStateKinds {
|
||||
running_status: Some(_),
|
||||
desired_state: StartStop::Stop,
|
||||
..
|
||||
} => MainStatus::Stopping,
|
||||
ServiceStateKinds {
|
||||
running_status: None,
|
||||
desired_state: StartStop::Stop,
|
||||
..
|
||||
} => MainStatus::Stopped,
|
||||
};
|
||||
i.as_status_mut().ser(&main_status)?;
|
||||
return Ok(previous
|
||||
.major_changes(&main_status)
|
||||
.then_some((previous, main_status)));
|
||||
}
|
||||
Ok(None)
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
if let Some((previous, new_state)) = major_changes_state {
|
||||
if let Some(callbacks) = seed.ctx.callbacks.get_status(id) {
|
||||
callbacks
|
||||
.call(vector![to_value(&previous)?, to_value(&new_state)?])
|
||||
.await?;
|
||||
|
||||
let major_changes_state = seed
|
||||
.ctx
|
||||
.db
|
||||
.mutate(|d| {
|
||||
if let Some(i) = d.as_public_mut().as_package_data_mut().as_idx_mut(&id) {
|
||||
let previous = i.as_status().de()?;
|
||||
let main_status = match &kinds {
|
||||
ServiceStateKinds {
|
||||
transition_state: Some(TransitionKind::Restarting),
|
||||
..
|
||||
} => MainStatus::Restarting,
|
||||
ServiceStateKinds {
|
||||
transition_state: Some(TransitionKind::BackingUp),
|
||||
..
|
||||
} => previous.backing_up(),
|
||||
ServiceStateKinds {
|
||||
running_status: Some(status),
|
||||
desired_state: StartStop::Start,
|
||||
..
|
||||
} => MainStatus::Running {
|
||||
started: status.started,
|
||||
health: previous.health().cloned().unwrap_or_default(),
|
||||
},
|
||||
ServiceStateKinds {
|
||||
running_status: None,
|
||||
desired_state: StartStop::Start,
|
||||
..
|
||||
} => MainStatus::Starting {
|
||||
health: previous.health().cloned().unwrap_or_default(),
|
||||
},
|
||||
ServiceStateKinds {
|
||||
running_status: Some(_),
|
||||
desired_state: StartStop::Stop,
|
||||
..
|
||||
} => MainStatus::Stopping,
|
||||
ServiceStateKinds {
|
||||
running_status: None,
|
||||
desired_state: StartStop::Stop,
|
||||
..
|
||||
} => MainStatus::Stopped,
|
||||
};
|
||||
i.as_status_mut().ser(&main_status)?;
|
||||
return Ok(previous
|
||||
.major_changes(&main_status)
|
||||
.then_some((previous, main_status)));
|
||||
}
|
||||
Ok(None)
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
|
||||
if let Some((previous, new_state)) = major_changes_state {
|
||||
if let Some(callbacks) = seed.ctx.callbacks.get_status(id) {
|
||||
callbacks
|
||||
.call(vector![to_value(&previous)?, to_value(&new_state)?])
|
||||
.await?;
|
||||
}
|
||||
seed.synchronized.notify_waiters();
|
||||
|
||||
match kinds {
|
||||
ServiceStateKinds {
|
||||
running_status: None,
|
||||
desired_state: StartStop::Start,
|
||||
..
|
||||
} => {
|
||||
seed.persistent_container.start().await?;
|
||||
}
|
||||
ServiceStateKinds {
|
||||
running_status: Some(_),
|
||||
desired_state: StartStop::Stop,
|
||||
..
|
||||
} => {
|
||||
seed.persistent_container.stop().await?;
|
||||
seed.persistent_container
|
||||
.state
|
||||
.send_if_modified(|s| s.running_status.take().is_some());
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|
||||
Ok::<_, Error>(())
|
||||
}
|
||||
.await
|
||||
{
|
||||
tracing::error!("error synchronizing state of service: {e}");
|
||||
tracing::debug!("{e:?}");
|
||||
|
||||
seed.synchronized.notify_waiters();
|
||||
|
||||
tracing::error!("Retrying in {}s...", SYNC_RETRY_COOLDOWN_SECONDS);
|
||||
tokio::time::sleep(Duration::from_secs(SYNC_RETRY_COOLDOWN_SECONDS)).await;
|
||||
return ServiceActorLoopNext::DontWait;
|
||||
}
|
||||
seed.synchronized.notify_waiters();
|
||||
|
||||
ServiceActorLoopNext::Wait
|
||||
match kinds {
|
||||
ServiceStateKinds {
|
||||
running_status: None,
|
||||
desired_state: StartStop::Start,
|
||||
..
|
||||
} => {
|
||||
let task = start_stop_task
|
||||
.take()
|
||||
.filter(|task| matches!(task, Either::Right(_)));
|
||||
*start_stop_task = Some(
|
||||
task.unwrap_or_else(|| Either::Right(seed.persistent_container.start().boxed())),
|
||||
);
|
||||
}
|
||||
ServiceStateKinds {
|
||||
running_status: Some(_),
|
||||
desired_state: StartStop::Stop,
|
||||
..
|
||||
} => {
|
||||
let task = start_stop_task
|
||||
.take()
|
||||
.filter(|task| matches!(task, Either::Left(_)));
|
||||
*start_stop_task = Some(task.unwrap_or_else(|| {
|
||||
Either::Left(
|
||||
async {
|
||||
seed.persistent_container.stop().await?;
|
||||
seed.persistent_container
|
||||
.state
|
||||
.send_if_modified(|s| s.running_status.take().is_some());
|
||||
Ok::<_, Error>(())
|
||||
}
|
||||
.boxed(),
|
||||
)
|
||||
}));
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use color_eyre::eyre::eyre;
|
||||
use futures::{FutureExt, TryStreamExt};
|
||||
use imbl::vector;
|
||||
use imbl_value::InternedString;
|
||||
use rpc_toolkit::{Context, Empty, HandlerExt, ParentHandler, from_fn_async};
|
||||
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
|
||||
use rustls::RootCertStore;
|
||||
use rustls_pki_types::CertificateDer;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
@@ -24,12 +24,12 @@ use crate::logs::{LogSource, LogsParams, SYSTEM_UNIT};
|
||||
use crate::prelude::*;
|
||||
use crate::rpc_continuations::{Guid, RpcContinuation, RpcContinuations};
|
||||
use crate::shutdown::Shutdown;
|
||||
use crate::util::Invoke;
|
||||
use crate::util::cpupower::{Governor, get_available_governors, set_governor};
|
||||
use crate::util::cpupower::{get_available_governors, set_governor, Governor};
|
||||
use crate::util::io::open_file;
|
||||
use crate::util::net::WebSocketExt;
|
||||
use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable};
|
||||
use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat};
|
||||
use crate::util::sync::Watch;
|
||||
use crate::util::Invoke;
|
||||
use crate::{MAIN_DATA, PACKAGE_DATA};
|
||||
|
||||
pub fn experimental<C: Context>() -> ParentHandler<C> {
|
||||
@@ -498,7 +498,7 @@ pub struct MetricsFollowResponse {
|
||||
#[command(rename_all = "kebab-case")]
|
||||
pub struct MetricsFollowParams {
|
||||
#[ts(skip)]
|
||||
#[serde(rename = "__auth_session")] // from Auth middleware
|
||||
#[serde(rename = "__Auth_session")] // from Auth middleware
|
||||
session: Option<InternedString>,
|
||||
}
|
||||
|
||||
@@ -1039,8 +1039,8 @@ pub async fn test_smtp(
|
||||
) -> Result<(), Error> {
|
||||
#[cfg(feature = "mail-send")]
|
||||
{
|
||||
use mail_send::SmtpClientBuilder;
|
||||
use mail_send::mail_builder::{self, MessageBuilder};
|
||||
use mail_send::SmtpClientBuilder;
|
||||
use rustls_pki_types::pem::PemObject;
|
||||
|
||||
let Some(pass_val) = password else {
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
use std::net::Ipv4Addr;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
|
||||
use clap::Parser;
|
||||
use imbl_value::InternedString;
|
||||
use ipnet::Ipv4Net;
|
||||
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
|
||||
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerArgs, HandlerExt, ParentHandler};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::context::CliContext;
|
||||
use crate::prelude::*;
|
||||
use crate::tunnel::context::TunnelContext;
|
||||
use crate::tunnel::wg::WgSubnetConfig;
|
||||
use crate::tunnel::wg::{ClientConfig, WgConfig, WgSubnetClients, WgSubnetConfig};
|
||||
use crate::util::serde::{display_serializable, HandlerExtSerde};
|
||||
|
||||
pub fn tunnel_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
@@ -17,6 +19,10 @@ pub fn tunnel_api<C: Context>() -> ParentHandler<C> {
|
||||
super::db::db_api::<C>()
|
||||
.with_about("Commands to interact with the db i.e. dump and apply"),
|
||||
)
|
||||
.subcommand(
|
||||
"auth",
|
||||
super::auth::auth_api::<C>().with_about("Add or remove authorized clients"),
|
||||
)
|
||||
.subcommand(
|
||||
"subnet",
|
||||
subnet_api::<C>().with_about("Add, remove, or modify subnets"),
|
||||
@@ -44,6 +50,7 @@ pub fn tunnel_api<C: Context>() -> ParentHandler<C> {
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SubnetParams {
|
||||
subnet: Ipv4Net,
|
||||
}
|
||||
@@ -68,43 +75,94 @@ pub fn subnet_api<C: Context>() -> ParentHandler<C, SubnetParams> {
|
||||
.with_about("Remove a subnet")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
// .subcommand(
|
||||
// "set-default-forward-target",
|
||||
// from_fn_async(set_default_forward_target)
|
||||
// .with_metadata("sync_db", Value::Bool(true))
|
||||
// .no_display()
|
||||
// .with_about("Set the default target for port forwarding")
|
||||
// .with_call_remote::<CliContext>(),
|
||||
// )
|
||||
// .subcommand(
|
||||
// "add-device",
|
||||
// from_fn_async(add_device)
|
||||
// .with_metadata("sync_db", Value::Bool(true))
|
||||
// .no_display()
|
||||
// .with_about("Add a device to a subnet")
|
||||
// .with_call_remote::<CliContext>(),
|
||||
// )
|
||||
// .subcommand(
|
||||
// "remove-device",
|
||||
// from_fn_async(remove_device)
|
||||
// .with_metadata("sync_db", Value::Bool(true))
|
||||
// .no_display()
|
||||
// .with_about("Remove a device from a subnet")
|
||||
// .with_call_remote::<CliContext>(),
|
||||
// )
|
||||
.subcommand(
|
||||
"add-device",
|
||||
from_fn_async(add_device)
|
||||
.with_metadata("sync_db", Value::Bool(true))
|
||||
.with_inherited(|a, _| a)
|
||||
.no_display()
|
||||
.with_about("Add a device to a subnet")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"remove-device",
|
||||
from_fn_async(remove_device)
|
||||
.with_metadata("sync_db", Value::Bool(true))
|
||||
.with_inherited(|a, _| a)
|
||||
.no_display()
|
||||
.with_about("Remove a device from a subnet")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"list-devices",
|
||||
from_fn_async(list_devices)
|
||||
.with_inherited(|a, _| a)
|
||||
.with_display_serializable()
|
||||
.with_custom_display_fn(|HandlerArgs { params, .. }, res| {
|
||||
use prettytable::*;
|
||||
|
||||
if let Some(format) = params.format {
|
||||
return display_serializable(format, res);
|
||||
}
|
||||
|
||||
let mut table = Table::new();
|
||||
table.add_row(row![bc => "NAME", "IP", "PUBLIC KEY"]);
|
||||
for (ip, config) in res.clients.0 {
|
||||
table.add_row(row![config.name, ip, config.key.verifying_key()]);
|
||||
}
|
||||
|
||||
table.print_tty(false)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.with_about("List devices in a subnet")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"show-config",
|
||||
from_fn_async(show_config)
|
||||
.with_inherited(|a, _| a)
|
||||
.with_display_serializable()
|
||||
.with_custom_display_fn(|HandlerArgs { params, .. }, res| {
|
||||
if let Some(format) = params.format {
|
||||
return display_serializable(format, res);
|
||||
}
|
||||
|
||||
println!("{}", res);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.with_about("Show the WireGuard configuration for a subnet")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddSubnetParams {
|
||||
name: InternedString,
|
||||
}
|
||||
|
||||
pub async fn add_subnet(
|
||||
ctx: TunnelContext,
|
||||
_: Empty,
|
||||
AddSubnetParams { name }: AddSubnetParams,
|
||||
SubnetParams { subnet }: SubnetParams,
|
||||
) -> Result<(), Error> {
|
||||
if subnet.addr().octets()[3] == 0
|
||||
|| subnet.addr().octets()[3] == 255
|
||||
|| subnet.prefix_len() > 24
|
||||
{
|
||||
return Err(Error::new(
|
||||
eyre!("invalid subnet"),
|
||||
ErrorKind::InvalidRequest,
|
||||
));
|
||||
}
|
||||
let server = ctx
|
||||
.db
|
||||
.mutate(|db| {
|
||||
let map = db.as_wg_mut().as_subnets_mut();
|
||||
if !map.contains_key(&subnet)? {
|
||||
map.insert(&subnet, &WgSubnetConfig::new())?;
|
||||
map.insert(&subnet, &WgSubnetConfig::new(name))?;
|
||||
}
|
||||
db.as_wg().de()
|
||||
})
|
||||
@@ -128,3 +186,162 @@ pub async fn remove_subnet(
|
||||
.result?;
|
||||
server.sync().await
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddDeviceParams {
|
||||
name: InternedString,
|
||||
ip: Option<Ipv4Addr>,
|
||||
}
|
||||
|
||||
pub async fn add_device(
|
||||
ctx: TunnelContext,
|
||||
AddDeviceParams { name, ip }: AddDeviceParams,
|
||||
SubnetParams { subnet }: SubnetParams,
|
||||
) -> Result<(), Error> {
|
||||
let config = WgConfig::generate(name);
|
||||
let server = ctx
|
||||
.db
|
||||
.mutate(|db| {
|
||||
db.as_wg_mut()
|
||||
.as_subnets_mut()
|
||||
.as_idx_mut(&subnet)
|
||||
.or_not_found(&subnet)?
|
||||
.as_clients_mut()
|
||||
.mutate(|WgSubnetClients(clients)| {
|
||||
let ip = if let Some(ip) = ip {
|
||||
ip
|
||||
} else {
|
||||
subnet
|
||||
.hosts()
|
||||
.find(|ip| !clients.contains_key(ip) && *ip != subnet.addr())
|
||||
.ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("no available ips in subnet"),
|
||||
ErrorKind::InvalidRequest,
|
||||
)
|
||||
})?
|
||||
};
|
||||
|
||||
if ip.octets()[3] == 0 || ip.octets()[3] == 255 {
|
||||
return Err(Error::new(eyre!("invalid ip"), ErrorKind::InvalidRequest));
|
||||
}
|
||||
if !subnet.contains(&ip) {
|
||||
return Err(Error::new(
|
||||
eyre!("ip not in subnet"),
|
||||
ErrorKind::InvalidRequest,
|
||||
));
|
||||
}
|
||||
clients.insert(ip, config).map_or(Ok(()), |_| {
|
||||
Err(Error::new(
|
||||
eyre!("ip already in use"),
|
||||
ErrorKind::InvalidRequest,
|
||||
))
|
||||
})
|
||||
})?;
|
||||
db.as_wg().de()
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
server.sync().await
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RemoveDeviceParams {
|
||||
device: Ipv4Addr,
|
||||
}
|
||||
|
||||
pub async fn remove_device(
|
||||
ctx: TunnelContext,
|
||||
RemoveDeviceParams { device }: RemoveDeviceParams,
|
||||
SubnetParams { subnet }: SubnetParams,
|
||||
) -> Result<(), Error> {
|
||||
let server = ctx
|
||||
.db
|
||||
.mutate(|db| {
|
||||
db.as_wg_mut()
|
||||
.as_subnets_mut()
|
||||
.as_idx_mut(&subnet)
|
||||
.or_not_found(&subnet)?
|
||||
.as_clients_mut()
|
||||
.remove(&device)?
|
||||
.or_not_found(&device)?;
|
||||
db.as_wg().de()
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
server.sync().await
|
||||
}
|
||||
|
||||
pub async fn list_devices(
|
||||
ctx: TunnelContext,
|
||||
_: Empty,
|
||||
SubnetParams { subnet }: SubnetParams,
|
||||
) -> Result<WgSubnetConfig, Error> {
|
||||
ctx.db
|
||||
.peek()
|
||||
.await
|
||||
.as_wg()
|
||||
.as_subnets()
|
||||
.as_idx(&subnet)
|
||||
.or_not_found(&subnet)?
|
||||
.de()
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ShowConfigParams {
|
||||
device: Ipv4Addr,
|
||||
wan_addr: Option<IpAddr>,
|
||||
#[serde(rename = "__ConnectInfo_local_addr")]
|
||||
#[arg(skip)]
|
||||
local_addr: Option<SocketAddr>,
|
||||
}
|
||||
|
||||
pub async fn show_config(
|
||||
ctx: TunnelContext,
|
||||
ShowConfigParams {
|
||||
device,
|
||||
wan_addr,
|
||||
local_addr,
|
||||
}: ShowConfigParams,
|
||||
SubnetParams { subnet }: SubnetParams,
|
||||
) -> Result<ClientConfig, Error> {
|
||||
let wg = ctx.db.peek().await.into_wg();
|
||||
let client = wg
|
||||
.as_subnets()
|
||||
.as_idx(&subnet)
|
||||
.or_not_found(&subnet)?
|
||||
.as_clients()
|
||||
.as_idx(&device)
|
||||
.or_not_found(&device)?
|
||||
.de()?;
|
||||
let wan_addr = if let Some(wan_addr) = wan_addr.or(local_addr.map(|a| a.ip())).filter(|ip| {
|
||||
!ip.is_loopback()
|
||||
&& !match ip {
|
||||
IpAddr::V4(ipv4) => ipv4.is_private() || ipv4.is_link_local(),
|
||||
IpAddr::V6(ipv6) => ipv6.is_unique_local() || ipv6.is_unicast_link_local(),
|
||||
}
|
||||
}) {
|
||||
wan_addr
|
||||
} else {
|
||||
ctx.net_iface
|
||||
.ip_info()
|
||||
.into_iter()
|
||||
.find_map(|(_, info)| {
|
||||
info.public()
|
||||
.then_some(info.ip_info)
|
||||
.flatten()
|
||||
.into_iter()
|
||||
.find_map(|info| info.subnets.into_iter().next())
|
||||
})
|
||||
.or_not_found("a public IP address")?
|
||||
.addr()
|
||||
};
|
||||
Ok(client.client_config(
|
||||
device,
|
||||
wg.as_key().de()?.verifying_key(),
|
||||
(wan_addr, wg.as_port().de()?).into(),
|
||||
))
|
||||
}
|
||||
|
||||
183
core/startos/src/tunnel/auth.rs
Normal file
183
core/startos/src/tunnel/auth.rs
Normal file
@@ -0,0 +1,183 @@
|
||||
use std::net::IpAddr;
|
||||
|
||||
use clap::Parser;
|
||||
use imbl::HashMap;
|
||||
use imbl_value::InternedString;
|
||||
use patch_db::HasModel;
|
||||
use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::auth::{check_password, Sessions};
|
||||
use crate::context::CliContext;
|
||||
use crate::middleware::auth::AuthContext;
|
||||
use crate::middleware::signature::SignatureAuthContext;
|
||||
use crate::prelude::*;
|
||||
use crate::rpc_continuations::OpenAuthedContinuations;
|
||||
use crate::sign::AnyVerifyingKey;
|
||||
use crate::tunnel::context::TunnelContext;
|
||||
use crate::tunnel::db::TunnelDatabase;
|
||||
use crate::util::serde::{display_serializable, HandlerExtSerde};
|
||||
use crate::util::sync::SyncMutex;
|
||||
|
||||
impl SignatureAuthContext for TunnelContext {
|
||||
type Database = TunnelDatabase;
|
||||
type AdditionalMetadata = ();
|
||||
type CheckPubkeyRes = ();
|
||||
fn db(&self) -> &TypedPatchDb<Self::Database> {
|
||||
&self.db
|
||||
}
|
||||
async fn sig_context(
|
||||
&self,
|
||||
) -> impl IntoIterator<Item = Result<impl AsRef<str> + 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<Self::Database>,
|
||||
pubkey: Option<&crate::sign::AnyVerifyingKey>,
|
||||
_: Self::AdditionalMetadata,
|
||||
) -> Result<Self::CheckPubkeyRes, Error> {
|
||||
if let Some(pubkey) = pubkey {
|
||||
if db.as_auth_pubkeys().de()?.contains_key(pubkey) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::new(
|
||||
eyre!("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<Self::Database>) -> &mut Model<crate::auth::Sessions> {
|
||||
db.as_sessions_mut()
|
||||
}
|
||||
fn ephemeral_sessions(&self) -> &SyncMutex<Sessions> {
|
||||
&self.ephemeral_sessions
|
||||
}
|
||||
fn open_authed_continuations(&self) -> &OpenAuthedContinuations<Option<InternedString>> {
|
||||
&self.open_authed_continuations
|
||||
}
|
||||
fn check_password(db: &Model<Self::Database>, password: &str) -> Result<(), Error> {
|
||||
check_password(&db.as_password().de()?, password)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[model = "Model<Self>"]
|
||||
#[ts(export)]
|
||||
pub struct SignerInfo {
|
||||
pub name: InternedString,
|
||||
}
|
||||
|
||||
pub fn auth_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new().subcommand(
|
||||
"key",
|
||||
ParentHandler::<C>::new()
|
||||
.subcommand(
|
||||
"add",
|
||||
from_fn_async(add_key)
|
||||
.with_metadata("sync_db", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("Add a new authorized key")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"remove",
|
||||
from_fn_async(remove_key)
|
||||
.with_metadata("sync_db", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("Remove an authorized key")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"list",
|
||||
from_fn_async(list_keys)
|
||||
.with_metadata("sync_db", Value::Bool(true))
|
||||
.with_display_serializable()
|
||||
.with_custom_display_fn(|HandlerArgs { params, .. }, res| {
|
||||
use prettytable::*;
|
||||
|
||||
if let Some(format) = params.format {
|
||||
return display_serializable(format, res);
|
||||
}
|
||||
|
||||
let mut table = Table::new();
|
||||
table.add_row(row![bc => "NAME", "KEY"]);
|
||||
for (key, info) in res {
|
||||
table.add_row(row![info.name, key]);
|
||||
}
|
||||
|
||||
table.print_tty(false)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.with_about("List authorized keys")
|
||||
.with_call_remote::<CliContext>(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddKeyParams {
|
||||
pub name: InternedString,
|
||||
pub key: AnyVerifyingKey,
|
||||
}
|
||||
|
||||
pub async fn add_key(
|
||||
ctx: TunnelContext,
|
||||
AddKeyParams { name, key }: AddKeyParams,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
db.as_auth_pubkeys_mut().mutate(|auth_pubkeys| {
|
||||
auth_pubkeys.insert(key, SignerInfo { name });
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.await
|
||||
.result
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RemoveKeyParams {
|
||||
pub key: AnyVerifyingKey,
|
||||
}
|
||||
|
||||
pub async fn remove_key(
|
||||
ctx: TunnelContext,
|
||||
RemoveKeyParams { key }: RemoveKeyParams,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
db.as_auth_pubkeys_mut()
|
||||
.mutate(|auth_pubkeys| Ok(auth_pubkeys.remove(&key)))
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_keys(ctx: TunnelContext) -> Result<HashMap<AnyVerifyingKey, SignerInfo>, Error> {
|
||||
ctx.db.peek().await.into_auth_pubkeys().de()
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
# StartTunnel config for {name}
|
||||
|
||||
[Interface]
|
||||
Address = {addr}/24
|
||||
PrivateKey = {privkey}
|
||||
@@ -5,6 +7,6 @@ PrivateKey = {privkey}
|
||||
[Peer]
|
||||
PublicKey = {server_pubkey}
|
||||
PresharedKey = {psk}
|
||||
AllowedIPs = 0.0.0.0/0, ::/0
|
||||
AllowedIPs = 0.0.0.0/0,::/0
|
||||
Endpoint = {server_addr}
|
||||
PersistentKeepalive = 25
|
||||
@@ -5,27 +5,33 @@ use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use cookie::{Cookie, Expiration, SameSite};
|
||||
use helpers::NonDetachingJoinHandle;
|
||||
use http::HeaderMap;
|
||||
use imbl::OrdMap;
|
||||
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::process::Command;
|
||||
use tokio::sync::broadcast::Sender;
|
||||
use tracing::instrument;
|
||||
use url::Url;
|
||||
|
||||
use crate::auth::{Sessions, check_password};
|
||||
use crate::context::CliContext;
|
||||
use crate::auth::Sessions;
|
||||
use crate::context::config::ContextConfig;
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::middleware::auth::AuthContext;
|
||||
use crate::middleware::signature::SignatureAuthContext;
|
||||
use crate::net::forward::PortForwardController;
|
||||
use crate::net::gateway::NetworkInterfaceWatcher;
|
||||
use crate::prelude::*;
|
||||
use crate::rpc_continuations::{OpenAuthedContinuations, RpcContinuations};
|
||||
use crate::tunnel::TUNNEL_DEFAULT_PORT;
|
||||
use crate::tunnel::db::TunnelDatabase;
|
||||
use crate::tunnel::TUNNEL_DEFAULT_PORT;
|
||||
use crate::util::io::read_file_to_string;
|
||||
use crate::util::sync::SyncMutex;
|
||||
use crate::util::Invoke;
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize, Parser)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
@@ -67,6 +73,7 @@ pub struct TunnelContextSeed {
|
||||
pub ephemeral_sessions: SyncMutex<Sessions>,
|
||||
pub net_iface: NetworkInterfaceWatcher,
|
||||
pub forward: PortForwardController,
|
||||
pub masquerade_thread: NonDetachingJoinHandle<()>,
|
||||
pub shutdown: Sender<()>,
|
||||
}
|
||||
|
||||
@@ -75,6 +82,7 @@ pub struct TunnelContext(Arc<TunnelContextSeed>);
|
||||
impl TunnelContext {
|
||||
#[instrument(skip_all)]
|
||||
pub async fn init(config: &TunnelConfig) -> Result<Self, Error> {
|
||||
Self::init_auth_cookie().await?;
|
||||
let (shutdown, _) = tokio::sync::broadcast::channel(1);
|
||||
let datadir = config
|
||||
.datadir
|
||||
@@ -96,6 +104,52 @@ impl TunnelContext {
|
||||
));
|
||||
let net_iface = NetworkInterfaceWatcher::new(async { OrdMap::new() }, []);
|
||||
let forward = PortForwardController::new(net_iface.subscribe());
|
||||
|
||||
Command::new("sysctl")
|
||||
.arg("-w")
|
||||
.arg("net.ipv4.ip_forward=1")
|
||||
.invoke(ErrorKind::Network)
|
||||
.await?;
|
||||
|
||||
let mut masquerade_net_iface = net_iface.subscribe();
|
||||
let masquerade_thread = tokio::spawn(async move {
|
||||
loop {
|
||||
for iface in masquerade_net_iface.peek(|i| i.keys().cloned().collect::<Vec<_>>()) {
|
||||
if Command::new("iptables")
|
||||
.arg("-t")
|
||||
.arg("nat")
|
||||
.arg("-C")
|
||||
.arg("POSTROUTING")
|
||||
.arg("-o")
|
||||
.arg(iface.as_str())
|
||||
.arg("-j")
|
||||
.arg("MASQUERADE")
|
||||
.invoke(ErrorKind::Network)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
Command::new("iptables")
|
||||
.arg("-t")
|
||||
.arg("nat")
|
||||
.arg("-A")
|
||||
.arg("POSTROUTING")
|
||||
.arg("-o")
|
||||
.arg(iface.as_str())
|
||||
.arg("-j")
|
||||
.arg("MASQUERADE")
|
||||
.invoke(ErrorKind::Network)
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
masquerade_net_iface.changed().await;
|
||||
}
|
||||
})
|
||||
.into();
|
||||
|
||||
db.peek().await.into_wg().de()?.sync().await?;
|
||||
|
||||
Ok(Self(Arc::new(TunnelContextSeed {
|
||||
listen,
|
||||
addrs: crate::net::utils::all_socket_addrs_for(listen.port())
|
||||
@@ -110,6 +164,7 @@ impl TunnelContext {
|
||||
ephemeral_sessions: SyncMutex::new(Sessions::new()),
|
||||
net_iface,
|
||||
forward,
|
||||
masquerade_thread,
|
||||
shutdown,
|
||||
})))
|
||||
}
|
||||
@@ -133,66 +188,6 @@ pub struct TunnelAddrParams {
|
||||
pub tunnel: IpAddr,
|
||||
}
|
||||
|
||||
impl SignatureAuthContext for TunnelContext {
|
||||
type Database = TunnelDatabase;
|
||||
type AdditionalMetadata = ();
|
||||
type CheckPubkeyRes = ();
|
||||
fn db(&self) -> &TypedPatchDb<Self::Database> {
|
||||
&self.db
|
||||
}
|
||||
async fn sig_context(
|
||||
&self,
|
||||
) -> impl IntoIterator<Item = Result<impl AsRef<str> + 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<Self::Database>,
|
||||
pubkey: Option<&crate::sign::AnyVerifyingKey>,
|
||||
_: Self::AdditionalMetadata,
|
||||
) -> Result<Self::CheckPubkeyRes, Error> {
|
||||
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<Self::Database>) -> &mut Model<crate::auth::Sessions> {
|
||||
db.as_sessions_mut()
|
||||
}
|
||||
fn ephemeral_sessions(&self) -> &SyncMutex<Sessions> {
|
||||
&self.ephemeral_sessions
|
||||
}
|
||||
fn open_authed_continuations(&self) -> &OpenAuthedContinuations<Option<InternedString>> {
|
||||
&self.open_authed_continuations
|
||||
}
|
||||
fn check_password(db: &Model<Self::Database>, password: &str) -> Result<(), Error> {
|
||||
check_password(&db.as_password().de()?, password)
|
||||
}
|
||||
}
|
||||
|
||||
impl CallRemote<TunnelContext> for CliContext {
|
||||
async fn call_remote(
|
||||
&self,
|
||||
@@ -200,22 +195,84 @@ impl CallRemote<TunnelContext> for CliContext {
|
||||
params: Value,
|
||||
_: Empty,
|
||||
) -> Result<Value, RpcError> {
|
||||
let local =
|
||||
if let Ok(local) = read_file_to_string(TunnelContext::LOCAL_AUTH_COOKIE_PATH).await {
|
||||
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)?;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let tunnel_addr = if let Some(addr) = self.tunnel_addr {
|
||||
addr
|
||||
Some(addr)
|
||||
} else if let Some(addr) = self.tunnel_listen {
|
||||
addr
|
||||
Some(addr)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (url, sig_ctx) = if let Some(tunnel_addr) = tunnel_addr {
|
||||
(
|
||||
format!("https://{tunnel_addr}/rpc/v0").parse()?,
|
||||
Some(InternedString::from_display(
|
||||
&self.tunnel_listen.unwrap_or(tunnel_addr).ip(),
|
||||
)),
|
||||
)
|
||||
} else if local {
|
||||
(
|
||||
format!("http://localhost:{TUNNEL_DEFAULT_PORT}/rpc/v0").parse()?,
|
||||
None,
|
||||
)
|
||||
} 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()),
|
||||
HeaderMap::new(),
|
||||
sig_ctx.as_deref(),
|
||||
method,
|
||||
params,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser)]
|
||||
pub struct TunnelUrlParams {
|
||||
pub tunnel: Url,
|
||||
}
|
||||
|
||||
impl CallRemote<TunnelContext, TunnelUrlParams> for RpcContext {
|
||||
async fn call_remote(
|
||||
&self,
|
||||
mut method: &str,
|
||||
params: Value,
|
||||
TunnelUrlParams { tunnel }: TunnelUrlParams,
|
||||
) -> Result<Value, RpcError> {
|
||||
let url = tunnel.join("rpc/v0")?;
|
||||
method = method.strip_prefix("tunnel.").unwrap_or(method);
|
||||
|
||||
let sig_ctx = url.host_str().map(InternedString::from_display);
|
||||
|
||||
crate::middleware::signature::call_remote(
|
||||
self,
|
||||
url,
|
||||
HeaderMap::new(),
|
||||
sig_ctx.as_deref(),
|
||||
method,
|
||||
params,
|
||||
)
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::net::{Ipv4Addr, SocketAddrV4};
|
||||
use std::collections::BTreeMap;
|
||||
use std::net::SocketAddrV4;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Parser;
|
||||
use imbl_value::InternedString;
|
||||
use ipnet::Ipv4Net;
|
||||
use imbl::HashMap;
|
||||
use itertools::Itertools;
|
||||
use patch_db::Dump;
|
||||
use patch_db::json_ptr::{JsonPointer, ROOT};
|
||||
use patch_db::Dump;
|
||||
use rpc_toolkit::yajrc::RpcError;
|
||||
use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
|
||||
use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::instrument;
|
||||
use ts_rs::TS;
|
||||
@@ -18,9 +17,10 @@ use crate::auth::Sessions;
|
||||
use crate::context::CliContext;
|
||||
use crate::prelude::*;
|
||||
use crate::sign::AnyVerifyingKey;
|
||||
use crate::tunnel::auth::SignerInfo;
|
||||
use crate::tunnel::context::TunnelContext;
|
||||
use crate::tunnel::wg::WgServer;
|
||||
use crate::util::serde::{HandlerExtSerde, apply_expr};
|
||||
use crate::util::serde::{apply_expr, HandlerExtSerde};
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, HasModel)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -28,7 +28,7 @@ use crate::util::serde::{HandlerExtSerde, apply_expr};
|
||||
pub struct TunnelDatabase {
|
||||
pub sessions: Sessions,
|
||||
pub password: String,
|
||||
pub auth_pubkeys: HashSet<AnyVerifyingKey>,
|
||||
pub auth_pubkeys: HashMap<AnyVerifyingKey, SignerInfo>,
|
||||
pub wg: WgServer,
|
||||
pub port_forwards: BTreeMap<SocketAddrV4, SocketAddrV4>,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use axum::Router;
|
||||
use futures::future::ready;
|
||||
use rpc_toolkit::{Context, HandlerExt, ParentHandler, Server, from_fn_async};
|
||||
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler, Server};
|
||||
|
||||
use crate::context::CliContext;
|
||||
use crate::middleware::auth::Auth;
|
||||
@@ -12,6 +12,7 @@ use crate::rpc_continuations::Guid;
|
||||
use crate::tunnel::context::TunnelContext;
|
||||
|
||||
pub mod api;
|
||||
pub mod auth;
|
||||
pub mod context;
|
||||
pub mod db;
|
||||
pub mod forward;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::net::{Ipv4Addr, SocketAddrV4};
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
|
||||
use ed25519_dalek::{SigningKey, VerifyingKey};
|
||||
use imbl_value::InternedString;
|
||||
use ipnet::Ipv4Net;
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::process::Command;
|
||||
use x25519_dalek::{PublicKey, StaticSecret};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::util::Invoke;
|
||||
use crate::util::io::write_file_atomic;
|
||||
use crate::util::serde::Base64;
|
||||
use crate::util::Invoke;
|
||||
|
||||
#[derive(Deserialize, Serialize, HasModel)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -79,31 +79,38 @@ impl Map for WgSubnetMap {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[model = "Model<Self>"]
|
||||
pub struct WgSubnetConfig {
|
||||
pub default_forward_target: Option<Ipv4Addr>,
|
||||
pub clients: BTreeMap<Ipv4Addr, WgConfig>,
|
||||
pub name: InternedString,
|
||||
pub clients: WgSubnetClients,
|
||||
}
|
||||
impl WgSubnetConfig {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn add_client<'a>(
|
||||
&'a mut self,
|
||||
subnet: Ipv4Net,
|
||||
) -> Result<(Ipv4Addr, &'a WgConfig), Error> {
|
||||
let addr = subnet
|
||||
.hosts()
|
||||
.find(|a| !self.clients.contains_key(a))
|
||||
.ok_or_else(|| Error::new(eyre!("subnet exhausted"), ErrorKind::Network))?;
|
||||
let config = self.clients.entry(addr).or_insert(WgConfig::generate());
|
||||
Ok((addr, config))
|
||||
pub fn new(name: InternedString) -> Self {
|
||||
Self {
|
||||
name,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WgKey(SigningKey);
|
||||
#[derive(Default, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WgSubnetClients(pub BTreeMap<Ipv4Addr, WgConfig>);
|
||||
impl Map for WgSubnetClients {
|
||||
type Key = Ipv4Addr;
|
||||
type Value = WgConfig;
|
||||
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
|
||||
Self::key_string(key)
|
||||
}
|
||||
fn key_string(key: &Self::Key) -> Result<InternedString, Error> {
|
||||
Ok(InternedString::from_display(key))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WgKey(StaticSecret);
|
||||
impl WgKey {
|
||||
pub fn generate() -> Self {
|
||||
Self(SigningKey::generate(
|
||||
&mut ssh_key::rand_core::OsRng::default(),
|
||||
Self(StaticSecret::random_from_rng(
|
||||
ssh_key::rand_core::OsRng::default(),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -113,33 +120,39 @@ impl AsRef<[u8]> for WgKey {
|
||||
}
|
||||
}
|
||||
impl TryFrom<Vec<u8>> for WgKey {
|
||||
type Error = ed25519_dalek::SignatureError;
|
||||
type Error = Error;
|
||||
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
|
||||
Ok(Self(value.as_slice().try_into()?))
|
||||
Ok(Self(
|
||||
<[u8; 32]>::try_from(value)
|
||||
.map_err(|_| Error::new(eyre!("invalid key length"), ErrorKind::Deserialization))?
|
||||
.into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
impl std::ops::Deref for WgKey {
|
||||
type Target = SigningKey;
|
||||
type Target = StaticSecret;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl Base64<WgKey> {
|
||||
pub fn verifying_key(&self) -> Base64<VerifyingKey> {
|
||||
Base64(self.0.verifying_key())
|
||||
pub fn verifying_key(&self) -> Base64<PublicKey> {
|
||||
Base64((&*self.0).into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, HasModel)]
|
||||
#[derive(Clone, Deserialize, Serialize, HasModel)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[model = "Model<Self>"]
|
||||
pub struct WgConfig {
|
||||
pub name: InternedString,
|
||||
pub key: Base64<WgKey>,
|
||||
pub psk: Base64<[u8; 32]>,
|
||||
}
|
||||
impl WgConfig {
|
||||
pub fn generate() -> Self {
|
||||
pub fn generate(name: InternedString) -> Self {
|
||||
Self {
|
||||
name,
|
||||
key: Base64(WgKey::generate()),
|
||||
psk: Base64(rand::random()),
|
||||
}
|
||||
@@ -150,12 +163,12 @@ impl WgConfig {
|
||||
client_addr: addr,
|
||||
}
|
||||
}
|
||||
pub fn client_config<'a>(
|
||||
&'a self,
|
||||
pub fn client_config(
|
||||
self,
|
||||
addr: Ipv4Addr,
|
||||
server_pubkey: Base64<VerifyingKey>,
|
||||
server_addr: SocketAddrV4,
|
||||
) -> ClientConfig<'a> {
|
||||
server_pubkey: Base64<PublicKey>,
|
||||
server_addr: SocketAddr,
|
||||
) -> ClientConfig {
|
||||
ClientConfig {
|
||||
client_config: self,
|
||||
client_addr: addr,
|
||||
@@ -181,19 +194,33 @@ impl<'a> std::fmt::Display for ServerPeerConfig<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClientConfig<'a> {
|
||||
client_config: &'a WgConfig,
|
||||
client_addr: Ipv4Addr,
|
||||
server_pubkey: Base64<VerifyingKey>,
|
||||
server_addr: SocketAddrV4,
|
||||
fn deserialize_verifying_key<'de, D>(deserializer: D) -> Result<Base64<PublicKey>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
Base64::<Vec<u8>>::deserialize(deserializer).and_then(|b| {
|
||||
Ok(Base64(PublicKey::from(<[u8; 32]>::try_from(b.0).map_err(
|
||||
|e: Vec<u8>| serde::de::Error::invalid_length(e.len(), &"a 32 byte base64 string"),
|
||||
)?)))
|
||||
})
|
||||
}
|
||||
impl<'a> std::fmt::Display for ClientConfig<'a> {
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct ClientConfig {
|
||||
client_config: WgConfig,
|
||||
client_addr: Ipv4Addr,
|
||||
#[serde(deserialize_with = "deserialize_verifying_key")]
|
||||
server_pubkey: Base64<PublicKey>,
|
||||
server_addr: SocketAddr,
|
||||
}
|
||||
impl std::fmt::Display for ClientConfig {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
include_str!("./client.conf.template"),
|
||||
name = self.client_config.name,
|
||||
privkey = self.client_config.key.to_padded_string(),
|
||||
psk = self.client_config.psk,
|
||||
psk = self.client_config.psk.to_padded_string(),
|
||||
addr = self.client_addr,
|
||||
server_pubkey = self.server_pubkey.to_padded_string(),
|
||||
server_addr = self.server_addr,
|
||||
@@ -212,7 +239,7 @@ impl<'a> std::fmt::Display for ServerConfig<'a> {
|
||||
server_port = server.port,
|
||||
server_privkey = server.key.to_padded_string(),
|
||||
)?;
|
||||
for (addr, peer) in server.subnets.0.values().flat_map(|s| &s.clients) {
|
||||
for (addr, peer) in server.subnets.0.values().flat_map(|s| &s.clients.0) {
|
||||
write!(f, "{}", peer.server_peer_config(*addr))?;
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -1055,7 +1055,11 @@ impl<T: TryFrom<Vec<u8>>> ValueParserFactory for Base64<T> {
|
||||
Self::Parser::new()
|
||||
}
|
||||
}
|
||||
impl<'de, T: TryFrom<Vec<u8>>> Deserialize<'de> for Base64<T> {
|
||||
impl<'de, T> Deserialize<'de> for Base64<T>
|
||||
where
|
||||
Base64<T>: FromStr,
|
||||
<Base64<T> as FromStr>::Err: std::fmt::Display,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
|
||||
@@ -3,7 +3,7 @@ use imbl_value::json;
|
||||
use tokio::process::Command;
|
||||
|
||||
use super::v0_3_5::V0_3_0_COMPAT;
|
||||
use super::{VersionT, v0_3_6_alpha_6};
|
||||
use super::{v0_3_6_alpha_6, VersionT};
|
||||
use crate::context::RpcContext;
|
||||
use crate::prelude::*;
|
||||
use crate::util::Invoke;
|
||||
@@ -50,10 +50,7 @@ impl VersionT for Version {
|
||||
async fn post_up(self, ctx: &RpcContext, _input: Value) -> Result<(), Error> {
|
||||
Command::new("systemd-firstboot")
|
||||
.arg("--root=/media/startos/config/overlay/")
|
||||
.arg(format!(
|
||||
"--hostname={}",
|
||||
ctx.account.read().await.hostname.0
|
||||
))
|
||||
.arg(ctx.account.peek(|a| format!("--hostname={}", a.hostname.0)))
|
||||
.invoke(ErrorKind::ParseSysInfo)
|
||||
.await?;
|
||||
Ok(())
|
||||
|
||||
@@ -4,18 +4,18 @@ use exver::{PreReleaseSegment, VersionRange};
|
||||
use tokio::fs::File;
|
||||
|
||||
use super::v0_3_5::V0_3_0_COMPAT;
|
||||
use super::{VersionT, v0_3_6_alpha_7};
|
||||
use crate::DATA_DIR;
|
||||
use super::{v0_3_6_alpha_7, VersionT};
|
||||
use crate::context::RpcContext;
|
||||
use crate::install::PKG_ARCHIVE_DIR;
|
||||
use crate::prelude::*;
|
||||
use crate::s9pk::S9pk;
|
||||
use crate::s9pk::manifest::{DeviceFilter, Manifest};
|
||||
use crate::s9pk::merkle_archive::MerkleArchive;
|
||||
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
|
||||
use crate::s9pk::merkle_archive::MerkleArchive;
|
||||
use crate::s9pk::v2::SIG_CONTEXT;
|
||||
use crate::s9pk::S9pk;
|
||||
use crate::service::LoadDisposition;
|
||||
use crate::util::io::create_file;
|
||||
use crate::DATA_DIR;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref V0_3_6_alpha_8: exver::Version = exver::Version::new(
|
||||
@@ -115,7 +115,7 @@ impl VersionT for Version {
|
||||
let manifest: Manifest = from_value(manifest.clone())?;
|
||||
let id = manifest.id.clone();
|
||||
let mut s9pk: S9pk<_> = S9pk::new_with_manifest(archive, None, manifest);
|
||||
let s9pk_compat_key = ctx.account.read().await.developer_key.clone();
|
||||
let s9pk_compat_key = ctx.account.peek(|a| a.developer_key.clone());
|
||||
s9pk.as_archive_mut()
|
||||
.set_signer(s9pk_compat_key, SIG_CONTEXT);
|
||||
s9pk.serialize(&mut tmp_file, true).await?;
|
||||
|
||||
Reference in New Issue
Block a user