diff --git a/core/Cargo.lock b/core/Cargo.lock index d0b6f1426..d9e378165 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -5290,7 +5290,7 @@ dependencies = [ "nix 0.30.1", "patch-db-macro", "serde", - "serde_cbor", + "serde_cbor 0.11.1", "thiserror 2.0.17", "tokio", "tracing", @@ -6477,7 +6477,7 @@ dependencies = [ [[package]] name = "rpc-toolkit" version = "0.3.2" -source = "git+https://github.com/Start9Labs/rpc-toolkit.git?rev=068db90#068db905ee38a7da97cc4a43b806409204e73723" +source = "git+https://github.com/Start9Labs/rpc-toolkit.git#81d18147fd0ca9725b820c010c006e8a2cada322" dependencies = [ "async-stream", "async-trait", @@ -6494,6 +6494,7 @@ dependencies = [ "pin-project", "reqwest", "serde", + "serde_cbor 0.11.2", "serde_json", "thiserror 2.0.17", "tokio", @@ -6933,6 +6934,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half 1.8.3", + "serde", +] + [[package]] name = "serde_core" version = "1.0.228" diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index b5cc4120d..e1689199c 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -212,8 +212,8 @@ reqwest = { version = "0.12.25", features = [ ] } reqwest_cookie_store = "0.9.0" rpassword = "7.2.0" -rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git", rev = "068db90" } rust-argon2 = "3.0.0" +rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git" } safelog = { version = "0.4.8", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true } semver = { version = "1.0.20", features = ["serde"] } serde = { version = "1.0", features = ["derive", "rc"] } diff --git a/core/startos/src/auth.rs b/core/startos/src/auth.rs index c4ef42475..5d8b71b6c 100644 --- a/core/startos/src/auth.rs +++ b/core/startos/src/auth.rs @@ -14,8 +14,8 @@ use tracing::instrument; use ts_rs::TS; use crate::context::{CliContext, RpcContext}; -use crate::middleware::auth::{ - AsLogoutSessionId, AuthContext, HasLoggedOutSessions, HashSessionToken, LoginRes, +use crate::middleware::auth::session::{ + AsLogoutSessionId, HasLoggedOutSessions, HashSessionToken, LoginRes, SessionAuthContext, }; use crate::prelude::*; use crate::util::crypto::EncryptedWire; @@ -110,7 +110,7 @@ impl std::str::FromStr for PasswordType { }) } } -pub fn auth() -> ParentHandler +pub fn auth() -> ParentHandler where CliContext: CallRemote, { @@ -173,7 +173,7 @@ fn gen_pwd() { } #[instrument(skip_all)] -async fn cli_login( +async fn cli_login( HandlerArgs { context: ctx, parent_method, @@ -227,7 +227,7 @@ pub struct LoginParams { } #[instrument(skip_all)] -pub async fn login_impl( +pub async fn login_impl( ctx: C, LoginParams { password, @@ -283,7 +283,7 @@ pub struct LogoutParams { session: InternedString, } -pub async fn logout( +pub async fn logout( ctx: C, LogoutParams { session }: LogoutParams, ) -> Result, Error> { @@ -312,7 +312,7 @@ pub struct SessionList { sessions: Sessions, } -pub fn session() -> ParentHandler +pub fn session() -> ParentHandler where CliContext: CallRemote, { @@ -379,7 +379,7 @@ pub struct ListParams { // #[command(display(display_sessions))] #[instrument(skip_all)] -pub async fn list( +pub async fn list( ctx: C, ListParams { session, .. }: ListParams, ) -> Result { @@ -418,7 +418,10 @@ pub struct KillParams { } #[instrument(skip_all)] -pub async fn kill(ctx: C, KillParams { ids }: KillParams) -> Result<(), Error> { +pub async fn kill( + ctx: C, + KillParams { ids }: KillParams, +) -> Result<(), Error> { HasLoggedOutSessions::new(ids.into_iter().map(KillSessionId::new), &ctx).await?; Ok(()) } diff --git a/core/startos/src/backup/backup_bulk.rs b/core/startos/src/backup/backup_bulk.rs index d8bfae159..0de39f34c 100644 --- a/core/startos/src/backup/backup_bulk.rs +++ b/core/startos/src/backup/backup_bulk.rs @@ -22,7 +22,7 @@ use crate::db::model::{Database, DatabaseModel}; use crate::disk::mount::backup::BackupMountGuard; use crate::disk::mount::filesystem::ReadWrite; use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; -use crate::middleware::auth::AuthContext; +use crate::middleware::auth::session::SessionAuthContext; use crate::notifications::{NotificationLevel, notify}; use crate::prelude::*; use crate::util::io::{AtomicFile, dir_copy}; diff --git a/core/startos/src/context/cli.rs b/core/startos/src/context/cli.rs index 4a5db1b9e..aafbc796e 100644 --- a/core/startos/src/context/cli.rs +++ b/core/startos/src/context/cli.rs @@ -24,7 +24,7 @@ use super::setup::CURRENT_SECRET; use crate::context::config::{ClientConfig, local_config_path}; use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext}; use crate::developer::{OS_DEVELOPER_KEY_PATH, default_developer_key_path}; -use crate::middleware::auth::AuthContext; +use crate::middleware::auth::local::LocalAuthContext; use crate::prelude::*; use crate::rpc_continuations::Guid; use crate::util::io::read_file_to_string; @@ -307,7 +307,7 @@ impl CallRemote for CliContext { ) .with_kind(crate::ErrorKind::Network)?; } - crate::middleware::signature::call_remote( + crate::middleware::auth::signature::call_remote( self, self.rpc_url.clone(), HeaderMap::new(), @@ -320,7 +320,7 @@ impl CallRemote for CliContext { } impl CallRemote for CliContext { async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { - crate::middleware::signature::call_remote( + crate::middleware::auth::signature::call_remote( self, self.rpc_url.clone(), HeaderMap::new(), @@ -333,7 +333,7 @@ impl CallRemote for CliContext { } impl CallRemote for CliContext { async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { - crate::middleware::signature::call_remote( + crate::middleware::auth::signature::call_remote( self, self.rpc_url.clone(), HeaderMap::new(), @@ -346,7 +346,7 @@ impl CallRemote for CliContext { } impl CallRemote for CliContext { async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { - crate::middleware::signature::call_remote( + crate::middleware::auth::signature::call_remote( self, self.rpc_url.clone(), HeaderMap::new(), @@ -359,7 +359,7 @@ impl CallRemote for CliContext { } impl CallRemote for CliContext { async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { - crate::middleware::signature::call_remote( + crate::middleware::auth::signature::call_remote( self, self.rpc_url.clone(), HeaderMap::new(), diff --git a/core/startos/src/init.rs b/core/startos/src/init.rs index 79d66cd1d..0a42f7291 100644 --- a/core/startos/src/init.rs +++ b/core/startos/src/init.rs @@ -20,7 +20,7 @@ use crate::db::model::Database; use crate::db::model::public::ServerStatus; use crate::developer::OS_DEVELOPER_KEY_PATH; use crate::hostname::Hostname; -use crate::middleware::auth::AuthContext; +use crate::middleware::auth::local::LocalAuthContext; use crate::net::gateway::UpgradableListener; use crate::net::net_controller::{NetController, NetService}; use crate::net::socks::DEFAULT_SOCKS_LISTEN; diff --git a/core/startos/src/middleware/auth/local.rs b/core/startos/src/middleware/auth/local.rs new file mode 100644 index 000000000..342e1a882 --- /dev/null +++ b/core/startos/src/middleware/auth/local.rs @@ -0,0 +1,101 @@ +use base64::Engine; +use basic_cookies::Cookie; +use http::HeaderValue; +use http::header::COOKIE; +use rand::random; +use rpc_toolkit::yajrc::{RpcError, RpcResponse}; +use rpc_toolkit::{Context, Empty, Middleware}; +use tokio::io::AsyncWriteExt; +use tokio::process::Command; + +use crate::context::RpcContext; +use crate::prelude::*; +use crate::util::Invoke; +use crate::util::io::{create_file_mod, read_file_to_string}; +use crate::util::serde::BASE64; + +pub trait LocalAuthContext: Context { + const LOCAL_AUTH_COOKIE_PATH: &str; + const LOCAL_AUTH_COOKIE_OWNERSHIP: &str; + fn init_auth_cookie() -> impl Future> + Send { + async { + let mut file = create_file_mod(Self::LOCAL_AUTH_COOKIE_PATH, 0o640).await?; + file.write_all(BASE64.encode(random::<[u8; 32]>()).as_bytes()) + .await?; + file.sync_all().await?; + drop(file); + Command::new("chown") + .arg(Self::LOCAL_AUTH_COOKIE_OWNERSHIP) + .arg(Self::LOCAL_AUTH_COOKIE_PATH) + .invoke(crate::ErrorKind::Filesystem) + .await?; + Ok(()) + } + } +} + +impl LocalAuthContext for RpcContext { + const LOCAL_AUTH_COOKIE_PATH: &str = "/run/startos/rpc.authcookie"; + const LOCAL_AUTH_COOKIE_OWNERSHIP: &str = "root:startos"; +} + +fn unauthorized() -> Error { + Error::new(eyre!("UNAUTHORIZED"), crate::ErrorKind::Authorization) +} + +async fn check_from_header(header: Option<&HeaderValue>) -> Result<(), Error> { + if let Some(cookie_header) = header { + let cookies = Cookie::parse( + cookie_header + .to_str() + .with_kind(crate::ErrorKind::Authorization)?, + ) + .with_kind(crate::ErrorKind::Authorization)?; + if let Some(cookie) = cookies.iter().find(|c| c.get_name() == "local") { + return check_cookie::(cookie).await; + } + } + Err(unauthorized()) +} + +async fn check_cookie(local: &Cookie<'_>) -> Result<(), Error> { + if let Ok(token) = read_file_to_string(C::LOCAL_AUTH_COOKIE_PATH).await { + if local.get_value() == &*token { + return Ok(()); + } + } + + Err(unauthorized()) +} + +#[derive(Clone)] +pub struct LocalAuth { + cookie: Option, +} +impl LocalAuth { + pub fn new() -> Self { + Self { cookie: None } + } +} + +impl Middleware for LocalAuth { + type Metadata = Empty; + async fn process_http_request( + &mut self, + _: &C, + request: &mut axum::extract::Request, + ) -> Result<(), axum::response::Response> { + self.cookie = request.headers().get(COOKIE).cloned(); + Ok(()) + } + async fn process_rpc_request( + &mut self, + _: &C, + _: Self::Metadata, + _: &mut rpc_toolkit::RpcRequest, + ) -> Result<(), rpc_toolkit::RpcResponse> { + check_from_header::(self.cookie.as_ref()) + .await + .map_err(|e| RpcResponse::from(RpcError::from(e))) + } +} diff --git a/core/startos/src/middleware/auth/mod.rs b/core/startos/src/middleware/auth/mod.rs new file mode 100644 index 000000000..8f933aa49 --- /dev/null +++ b/core/startos/src/middleware/auth/mod.rs @@ -0,0 +1,112 @@ +use axum::extract::Request; +use axum::response::Response; +use rpc_toolkit::{Context, DynMiddleware, Middleware, RpcRequest, RpcResponse}; +use serde::Deserialize; + +use crate::context::RpcContext; +use crate::db::model::Database; +use crate::middleware::auth::local::{LocalAuth, LocalAuthContext}; +use crate::middleware::auth::session::{SessionAuth, SessionAuthContext}; +use crate::middleware::auth::signature::{SignatureAuth, SignatureAuthContext}; +use crate::prelude::*; +use crate::util::serde::const_true; + +pub mod local; +pub mod session; +pub mod signature; + +pub trait DbContext: Context { + type Database: HasModel> + Send + Sync; + fn db(&self) -> &TypedPatchDb; +} +impl DbContext for RpcContext { + type Database = Database; + fn db(&self) -> &TypedPatchDb { + &self.db + } +} + +#[derive(Deserialize)] +pub struct Metadata { + #[serde(default = "const_true")] + authenticated: bool, +} + +pub struct Auth(Vec>); +impl Clone for Auth { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} +impl Auth { + pub fn new() -> Self { + Self(Vec::new()) + } +} +impl Auth { + pub fn with_local_auth(mut self) -> Self { + self.0.push(DynMiddleware::new(LocalAuth::new())); + self + } +} +impl Auth { + pub fn with_signature_auth(mut self) -> Self { + self.0.push(DynMiddleware::new(SignatureAuth::new())); + self + } +} +impl Auth { + pub fn with_session_auth(mut self) -> Self { + self.0.push(DynMiddleware::new(SessionAuth::new())); + self + } +} +impl Middleware for Auth { + type Metadata = Value; + async fn process_http_request( + &mut self, + context: &C, + request: &mut Request, + ) -> Result<(), Response> { + for middleware in self.0.iter_mut() { + middleware.process_http_request(context, request).await?; + } + Ok(()) + } + async fn process_rpc_request( + &mut self, + context: &C, + metadata: Self::Metadata, + request: &mut RpcRequest, + ) -> Result<(), RpcResponse> { + let m: Metadata = + from_value(metadata.clone()).map_err(|e| RpcResponse::from_result(Err(e)))?; + if m.authenticated { + let mut err = None; + for middleware in self.0.iter_mut() { + if let Err(e) = middleware + .process_rpc_request(context, metadata.clone(), request) + .await + { + err = Some(e); + } else { + return Ok(()); + } + } + if let Some(e) = err { + return Err(e); + } + } + Ok(()) + } + async fn process_rpc_response(&mut self, context: &C, response: &mut RpcResponse) { + for middleware in self.0.iter_mut() { + middleware.process_rpc_response(context, response).await; + } + } + async fn process_http_response(&mut self, context: &C, response: &mut Response) { + for middleware in self.0.iter_mut() { + middleware.process_http_response(context, response).await; + } + } +} diff --git a/core/startos/src/middleware/auth.rs b/core/startos/src/middleware/auth/session.rs similarity index 68% rename from core/startos/src/middleware/auth.rs rename to core/startos/src/middleware/auth/session.rs index d746bb1ee..375351ce8 100644 --- a/core/startos/src/middleware/auth.rs +++ b/core/startos/src/middleware/auth/session.rs @@ -1,32 +1,23 @@ use std::borrow::Borrow; use std::collections::BTreeSet; -use std::future::Future; use std::ops::Deref; use std::sync::Arc; use std::time::{Duration, Instant}; use axum::extract::Request; use axum::response::Response; -use base64::Engine; use basic_cookies::Cookie; use chrono::Utc; -use color_eyre::eyre::eyre; -use digest::Digest; use http::HeaderValue; use http::header::{COOKIE, USER_AGENT}; -use imbl_value::{InternedString, json}; -use rand::random; use rpc_toolkit::yajrc::INTERNAL_ERROR; use rpc_toolkit::{Middleware, RpcRequest, RpcResponse}; use serde::{Deserialize, Serialize}; -use sha2::Sha256; -use tokio::io::AsyncWriteExt; -use tokio::process::Command; -use tokio::sync::Mutex; +use sha2::{Digest, Sha256}; use crate::auth::{Sessions, check_password, write_shadow}; use crate::context::RpcContext; -use crate::middleware::signature::{SignatureAuth, SignatureAuthContext}; +use crate::middleware::auth::DbContext; use crate::prelude::*; use crate::rpc_continuations::OpenAuthedContinuations; use crate::util::Invoke; @@ -34,24 +25,7 @@ use crate::util::io::{create_file_mod, read_file_to_string}; use crate::util::serde::{BASE64, const_true}; use crate::util::sync::SyncMutex; -pub trait AuthContext: SignatureAuthContext { - const LOCAL_AUTH_COOKIE_PATH: &str; - const LOCAL_AUTH_COOKIE_OWNERSHIP: &str; - fn init_auth_cookie() -> impl Future> + Send { - async { - let mut file = create_file_mod(Self::LOCAL_AUTH_COOKIE_PATH, 0o640).await?; - file.write_all(BASE64.encode(random::<[u8; 32]>()).as_bytes()) - .await?; - file.sync_all().await?; - drop(file); - Command::new("chown") - .arg(Self::LOCAL_AUTH_COOKIE_OWNERSHIP) - .arg(Self::LOCAL_AUTH_COOKIE_PATH) - .invoke(crate::ErrorKind::Filesystem) - .await?; - Ok(()) - } - } +pub trait SessionAuthContext: DbContext { fn ephemeral_sessions(&self) -> &SyncMutex; fn open_authed_continuations(&self) -> &OpenAuthedContinuations>; fn access_sessions(db: &mut Model) -> &mut Model; @@ -62,9 +36,7 @@ pub trait AuthContext: SignatureAuthContext { } } -impl AuthContext for RpcContext { - const LOCAL_AUTH_COOKIE_PATH: &str = "/run/startos/rpc.authcookie"; - const LOCAL_AUTH_COOKIE_OWNERSHIP: &str = "root:startos"; +impl SessionAuthContext for RpcContext { fn ephemeral_sessions(&self) -> &SyncMutex { &self.ephemeral_sessions } @@ -103,7 +75,7 @@ pub trait AsLogoutSessionId { pub struct HasLoggedOutSessions(()); impl HasLoggedOutSessions { - pub async fn new( + pub async fn new( sessions: impl IntoIterator, ctx: &C, ) -> Result { @@ -134,90 +106,6 @@ impl HasLoggedOutSessions { } } -/// Used when we need to know that we have logged in with a valid user -#[derive(Clone)] -pub struct HasValidSession(SessionType); - -#[derive(Clone)] -enum SessionType { - Local, - Session(HashSessionToken), -} - -impl HasValidSession { - pub async fn from_header( - header: Option<&HeaderValue>, - ctx: &C, - ) -> Result { - if let Some(cookie_header) = header { - let cookies = Cookie::parse( - cookie_header - .to_str() - .with_kind(crate::ErrorKind::Authorization)?, - ) - .with_kind(crate::ErrorKind::Authorization)?; - if let Some(cookie) = cookies.iter().find(|c| c.get_name() == "local") { - if let Ok(s) = Self::from_local::(cookie).await { - return Ok(s); - } - } - if let Some(cookie) = cookies.iter().find(|c| c.get_name() == "session") { - if let Ok(s) = Self::from_session(HashSessionToken::from_cookie(cookie), ctx).await - { - return Ok(s); - } - } - } - Err(Error::new( - eyre!("UNAUTHORIZED"), - crate::ErrorKind::Authorization, - )) - } - - pub async fn from_session( - session_token: HashSessionToken, - ctx: &C, - ) -> Result { - let session_hash = session_token.hashed(); - if !ctx.ephemeral_sessions().mutate(|s| { - if let Some(session) = s.0.get_mut(session_hash) { - session.last_active = Utc::now(); - true - } else { - false - } - }) { - ctx.db() - .mutate(|db| { - C::access_sessions(db) - .as_idx_mut(session_hash) - .ok_or_else(|| { - Error::new(eyre!("UNAUTHORIZED"), crate::ErrorKind::Authorization) - })? - .mutate(|s| { - s.last_active = Utc::now(); - Ok(()) - }) - }) - .await - .result?; - } - Ok(Self(SessionType::Session(session_token))) - } - - pub async fn from_local(local: &Cookie<'_>) -> Result { - let token = read_file_to_string(C::LOCAL_AUTH_COOKIE_PATH).await?; - if local.get_value() == &*token { - Ok(Self(SessionType::Local)) - } else { - Err(Error::new( - eyre!("UNAUTHORIZED"), - crate::ErrorKind::Authorization, - )) - } - } -} - /// When we have a need to create a new session, /// Or when we are using internal valid authenticated service. #[derive(Debug, Clone)] @@ -312,51 +200,97 @@ impl Borrow for HashSessionToken { } } +pub struct ValidSessionToken(pub HashSessionToken); +impl ValidSessionToken { + pub async fn from_header( + header: Option<&HeaderValue>, + ctx: &C, + ) -> Result { + if let Some(cookie_header) = header { + let cookies = Cookie::parse( + cookie_header + .to_str() + .with_kind(crate::ErrorKind::Authorization)?, + ) + .with_kind(crate::ErrorKind::Authorization)?; + if let Some(cookie) = cookies.iter().find(|c| c.get_name() == "session") { + if let Ok(s) = Self::from_session(HashSessionToken::from_cookie(cookie), ctx).await + { + return Ok(s); + } + } + } + Err(Error::new( + eyre!("UNAUTHORIZED"), + crate::ErrorKind::Authorization, + )) + } + + pub async fn from_session( + session_token: HashSessionToken, + ctx: &C, + ) -> Result { + let session_hash = session_token.hashed(); + if !ctx.ephemeral_sessions().mutate(|s| { + if let Some(session) = s.0.get_mut(session_hash) { + session.last_active = Utc::now(); + true + } else { + false + } + }) { + ctx.db() + .mutate(|db| { + C::access_sessions(db) + .as_idx_mut(session_hash) + .ok_or_else(|| { + Error::new(eyre!("UNAUTHORIZED"), crate::ErrorKind::Authorization) + })? + .mutate(|s| { + s.last_active = Utc::now(); + Ok(()) + }) + }) + .await + .result?; + } + Ok(Self(session_token)) + } +} + #[derive(Deserialize)] pub struct Metadata { - #[serde(default = "const_true")] - authenticated: bool, #[serde(default)] login: bool, #[serde(default)] get_session: bool, - #[serde(default)] - get_signer: bool, } #[derive(Clone)] -pub struct Auth { - rate_limiter: Arc>, - cookie: Option, +pub struct SessionAuth { + rate_limiter: Arc>, is_login: bool, + cookie: Option, set_cookie: Option, user_agent: Option, - signature_auth: SignatureAuth, } -impl Auth { +impl SessionAuth { pub fn new() -> Self { Self { - rate_limiter: Arc::new(Mutex::new((0, Instant::now()))), - cookie: None, + rate_limiter: Arc::new(SyncMutex::new((0, Instant::now()))), is_login: false, + cookie: None, set_cookie: None, user_agent: None, - signature_auth: SignatureAuth::new(), } } } -impl Middleware for Auth { + +impl Middleware for SessionAuth { type Metadata = Metadata; - async fn process_http_request( - &mut self, - context: &C, - request: &mut Request, - ) -> Result<(), Response> { - self.cookie = request.headers_mut().remove(COOKIE); - self.user_agent = request.headers_mut().remove(USER_AGENT); - self.signature_auth - .process_http_request(context, request) - .await?; + async fn process_http_request(&mut self, _: &C, request: &mut Request) -> Result<(), Response> { + self.cookie = request.headers().get(COOKIE).cloned(); + self.user_agent = request.headers().get(USER_AGENT).cloned(); Ok(()) } async fn process_rpc_request( @@ -368,56 +302,37 @@ impl Middleware for Auth { async { if metadata.login { self.is_login = true; - let guard = self.rate_limiter.lock().await; - if guard.1.elapsed() < Duration::from_secs(20) && guard.0 >= 3 { - return Err(Error::new( - eyre!("Please limit login attempts to 3 per 20 seconds."), - crate::ErrorKind::RateLimited, - )); - } + self.rate_limiter.mutate(|(count, time)| { + if time.elapsed() < Duration::from_secs(20) && *count >= 3 { + Err(Error::new( + eyre!("Please limit login attempts to 3 per 20 seconds."), + crate::ErrorKind::RateLimited, + )) + } else { + *count += 1; + *time = Instant::now(); + Ok(()) + } + })?; if let Some(user_agent) = self.user_agent.as_ref().and_then(|h| h.to_str().ok()) { request.params["__Auth_userAgent"] = Value::String(Arc::new(user_agent.to_owned())) - // TODO: will this panic? } - } else if metadata.authenticated { - if self - .signature_auth - .process_rpc_request( - context, - from_value(json!({ - "get_signer": metadata.get_signer - }))?, - request, - ) - .await - .is_err() - { - match HasValidSession::from_header(self.cookie.as_ref(), context).await? { - HasValidSession(SessionType::Session(s)) if metadata.get_session => { - request.params["__Auth_session"] = - Value::String(Arc::new(s.hashed().deref().to_owned())); - } - _ => (), - } + } else { + let ValidSessionToken(s) = + ValidSessionToken::from_header(self.cookie.as_ref(), context).await?; + if metadata.get_session { + request.params["__Auth_session"] = + Value::String(Arc::new(s.hashed().deref().to_owned())); } } - Ok(()) + Ok::<_, Error>(()) } .await .map_err(|e| RpcResponse::from_result(Err(e))) } async fn process_rpc_response(&mut self, _: &C, response: &mut RpcResponse) { if self.is_login { - let mut guard = self.rate_limiter.lock().await; - if guard.1.elapsed() < Duration::from_secs(20) { - if response.result.is_err() { - guard.0 += 1; - } - } else { - guard.0 = 0; - } - guard.1 = Instant::now(); if response.result.is_ok() { let res = std::mem::replace(&mut response.result, Err(INTERNAL_ERROR)); response.result = async { diff --git a/core/startos/src/middleware/signature.rs b/core/startos/src/middleware/auth/signature.rs similarity index 96% rename from core/startos/src/middleware/signature.rs rename to core/startos/src/middleware/auth/signature.rs index 1ad72197c..996a4d6a5 100644 --- a/core/startos/src/middleware/signature.rs +++ b/core/startos/src/middleware/auth/signature.rs @@ -8,14 +8,14 @@ use axum::extract::Request; use http::{HeaderMap, HeaderValue}; use reqwest::Client; use rpc_toolkit::yajrc::RpcError; -use rpc_toolkit::{Context, Middleware, RpcRequest, RpcResponse}; +use rpc_toolkit::{Middleware, RpcRequest, RpcResponse}; use serde::Deserialize; use serde::de::DeserializeOwned; use tokio::sync::Mutex; use url::Url; use crate::context::{CliContext, RpcContext}; -use crate::db::model::Database; +use crate::middleware::auth::DbContext; use crate::prelude::*; use crate::sign::commitment::Commitment; use crate::sign::commitment::request::RequestCommitment; @@ -25,11 +25,9 @@ use crate::util::serde::Base64; pub const AUTH_SIG_HEADER: &str = "X-StartOS-Auth-Sig"; -pub trait SignatureAuthContext: Context { - type Database: HasModel> + Send + Sync; +pub trait SignatureAuthContext: DbContext { type AdditionalMetadata: DeserializeOwned + Send; type CheckPubkeyRes: Send; - fn db(&self) -> &TypedPatchDb; fn sig_context( &self, ) -> impl Future + Send, Error>> + Send> @@ -47,12 +45,8 @@ pub trait SignatureAuthContext: Context { } impl SignatureAuthContext for RpcContext { - type Database = Database; type AdditionalMetadata = (); type CheckPubkeyRes = (); - fn db(&self) -> &TypedPatchDb { - &self.db - } async fn sig_context( &self, ) -> impl IntoIterator + Send, Error>> + Send { @@ -96,7 +90,7 @@ impl SignatureAuthContext for RpcContext { } Err(Error::new( - eyre!("Developer Key is not authorized"), + eyre!("Key is not authorized"), ErrorKind::IncorrectPassword, )) } diff --git a/core/startos/src/middleware/mod.rs b/core/startos/src/middleware/mod.rs index f71837a93..9180cc208 100644 --- a/core/startos/src/middleware/mod.rs +++ b/core/startos/src/middleware/mod.rs @@ -2,4 +2,3 @@ pub mod auth; pub mod connect_info; pub mod cors; pub mod db; -pub mod signature; diff --git a/core/startos/src/net/static_server.rs b/core/startos/src/net/static_server.rs index a5ec14822..b01080b25 100644 --- a/core/startos/src/net/static_server.rs +++ b/core/startos/src/net/static_server.rs @@ -32,7 +32,8 @@ use url::Url; use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext}; use crate::hostname::Hostname; -use crate::middleware::auth::{Auth, HasValidSession}; +use crate::middleware::auth::Auth; +use crate::middleware::auth::session::ValidSessionToken; use crate::middleware::cors::Cors; use crate::middleware::db::SyncDb; use crate::net::gateway::GatewayInfo; @@ -79,7 +80,12 @@ impl UiContext for RpcContext { fn middleware(server: Server) -> HttpServer { server .middleware(Cors::new()) - .middleware(Auth::new()) + .middleware( + Auth::new() + .with_local_auth() + .with_signature_auth() + .with_session_auth(), + ) .middleware(SyncDb::new()) } fn extend_router(self, router: Router) -> Router { @@ -404,8 +410,9 @@ async fn if_authorized< f: F, ) -> Result { if let Err(e) = - HasValidSession::from_header(request.headers().get(http::header::COOKIE), ctx).await + ValidSessionToken::from_header(request.headers().get(http::header::COOKIE), ctx).await { + // TODO: other auth methods Ok(unauthorized(e, request.uri().path())) } else { f(request).await diff --git a/core/startos/src/prelude.rs b/core/startos/src/prelude.rs index d42c2c9d9..369890500 100644 --- a/core/startos/src/prelude.rs +++ b/core/startos/src/prelude.rs @@ -1,4 +1,5 @@ pub use color_eyre::eyre::eyre; +pub use imbl_value::InternedString; pub use lazy_format::lazy_format; pub use tracing::instrument; diff --git a/core/startos/src/registry/context.rs b/core/startos/src/registry/context.rs index 772616909..9c4b4f269 100644 --- a/core/startos/src/registry/context.rs +++ b/core/startos/src/registry/context.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use chrono::Utc; use clap::Parser; +use cookie::{Cookie, Expiration, SameSite}; use http::HeaderMap; use imbl_value::InternedString; use patch_db::PatchDb; @@ -21,7 +22,9 @@ use url::Url; use crate::context::config::{CONFIG_PATH, ContextConfig}; use crate::context::{CliContext, RpcContext}; -use crate::middleware::signature::SignatureAuthContext; +use crate::middleware::auth::DbContext; +use crate::middleware::auth::local::LocalAuthContext; +use crate::middleware::auth::signature::SignatureAuthContext; use crate::prelude::*; use crate::registry::RegistryDatabase; use crate::registry::device_info::{DEVICE_INFO_HEADER, DeviceInfo}; @@ -29,7 +32,7 @@ use crate::registry::migrations::run_migrations; use crate::registry::signer::SignerInfo; use crate::rpc_continuations::RpcContinuations; use crate::sign::AnyVerifyingKey; -use crate::util::io::append_file; +use crate::util::io::{append_file, read_file_to_string}; const DEFAULT_REGISTRY_LISTEN: SocketAddr = SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::LOCALHOST), 5959); @@ -104,6 +107,8 @@ impl RegistryContext { } db.mutate(|db| run_migrations(db)).await.result?; + Self::init_auth_cookie().await?; + let tor_proxy_url = config .tor_proxy .clone() @@ -169,9 +174,26 @@ impl CallRemote for CliContext { params: Value, _: Empty, ) -> Result { + let mut has_cookie = false; + if let Ok(local) = read_file_to_string(RegistryContext::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)?; + has_cookie = true; + } + let url = if let Some(url) = self.registry_url.clone() { url - } else if !self.registry_hostname.is_empty() { + } else if has_cookie || !self.registry_hostname.is_empty() { let mut url: Url = format!( "http://{}", self.registry_listen.unwrap_or(DEFAULT_REGISTRY_LISTEN) @@ -196,7 +218,7 @@ impl CallRemote for CliContext { .cloned() .or_else(|| url.host().as_ref().map(InternedString::from_display)); - crate::middleware::signature::call_remote( + crate::middleware::auth::signature::call_remote( self, url, HeaderMap::new(), @@ -230,7 +252,7 @@ impl CallRemote for RpcContext { method = method.strip_prefix("registry.").unwrap_or(method); let sig_context = registry.host_str().map(InternedString::from); - crate::middleware::signature::call_remote( + crate::middleware::auth::signature::call_remote( self, registry, headers, @@ -257,13 +279,19 @@ pub struct AdminLogRecord { pub key: AnyVerifyingKey, } -impl SignatureAuthContext for RegistryContext { +impl DbContext for RegistryContext { type Database = RegistryDatabase; - type AdditionalMetadata = RegistryAuthMetadata; - type CheckPubkeyRes = Option<(AnyVerifyingKey, SignerInfo)>; fn db(&self) -> &TypedPatchDb { &self.db } +} +impl LocalAuthContext for RegistryContext { + const LOCAL_AUTH_COOKIE_PATH: &str = "/run/startos/registry.authcookie"; + const LOCAL_AUTH_COOKIE_OWNERSHIP: &str = "root:root"; +} +impl SignatureAuthContext for RegistryContext { + type AdditionalMetadata = RegistryAuthMetadata; + type CheckPubkeyRes = Option<(AnyVerifyingKey, SignerInfo)>; async fn sig_context( &self, ) -> impl IntoIterator + Send, Error>> + Send { diff --git a/core/startos/src/registry/mod.rs b/core/startos/src/registry/mod.rs index 74b35e7e2..bed299586 100644 --- a/core/startos/src/registry/mod.rs +++ b/core/startos/src/registry/mod.rs @@ -8,8 +8,8 @@ use serde::{Deserialize, Serialize}; use ts_rs::TS; use crate::context::CliContext; +use crate::middleware::auth::Auth; use crate::middleware::cors::Cors; -use crate::middleware::signature::SignatureAuth; use crate::net::static_server::{bad_request, not_found, server_error}; use crate::prelude::*; use crate::registry::context::RegistryContext; @@ -108,7 +108,7 @@ pub fn registry_router(ctx: RegistryContext) -> Router { any( Server::new(move || ready(Ok(ctx.clone())), registry_api()) .middleware(Cors::new()) - .middleware(SignatureAuth::new()) + .middleware(Auth::new().with_local_auth().with_signature_auth()) .middleware(DeviceInfoMiddleware::new()), ) }) diff --git a/core/startos/src/registry/package/mod.rs b/core/startos/src/registry/package/mod.rs index ad6a0abed..b8bd7d506 100644 --- a/core/startos/src/registry/package/mod.rs +++ b/core/startos/src/registry/package/mod.rs @@ -46,6 +46,7 @@ pub fn package_api() -> ParentHandler { .subcommand( "get", from_fn_async(get::get_package) + .with_metadata("authenticated", Value::Bool(false)) .with_metadata("get_device_info", Value::Bool(true)) .with_display_serializable() .with_custom_display_fn(|handle, result| { diff --git a/core/startos/src/tunnel/auth.rs b/core/startos/src/tunnel/auth.rs index 8e4003a6e..713d038a3 100644 --- a/core/startos/src/tunnel/auth.rs +++ b/core/startos/src/tunnel/auth.rs @@ -9,8 +9,10 @@ use ts_rs::TS; use crate::auth::{Sessions, check_password}; use crate::context::CliContext; -use crate::middleware::auth::AuthContext; -use crate::middleware::signature::SignatureAuthContext; +use crate::middleware::auth::DbContext; +use crate::middleware::auth::local::LocalAuthContext; +use crate::middleware::auth::session::SessionAuthContext; +use crate::middleware::auth::signature::SignatureAuthContext; use crate::prelude::*; use crate::rpc_continuations::OpenAuthedContinuations; use crate::sign::AnyVerifyingKey; @@ -19,13 +21,15 @@ use crate::tunnel::db::TunnelDatabase; use crate::util::serde::{HandlerExtSerde, display_serializable}; use crate::util::sync::SyncMutex; -impl SignatureAuthContext for TunnelContext { +impl DbContext for TunnelContext { type Database = TunnelDatabase; - type AdditionalMetadata = (); - type CheckPubkeyRes = (); fn db(&self) -> &TypedPatchDb { &self.db } +} +impl SignatureAuthContext for TunnelContext { + type AdditionalMetadata = (); + type CheckPubkeyRes = (); async fn sig_context( &self, ) -> impl IntoIterator + Send, Error>> + Send { @@ -93,9 +97,11 @@ impl SignatureAuthContext for TunnelContext { Ok(()) } } -impl AuthContext for TunnelContext { - const LOCAL_AUTH_COOKIE_PATH: &str = "/run/start-tunnel/rpc.authcookie"; +impl LocalAuthContext for TunnelContext { + const LOCAL_AUTH_COOKIE_PATH: &str = "/run/startos/tunnel.authcookie"; const LOCAL_AUTH_COOKIE_OWNERSHIP: &str = "root:root"; +} +impl SessionAuthContext for TunnelContext { fn access_sessions(db: &mut Model) -> &mut Model { db.as_sessions_mut() } diff --git a/core/startos/src/tunnel/context.rs b/core/startos/src/tunnel/context.rs index 5b4332f10..daa2164ec 100644 --- a/core/startos/src/tunnel/context.rs +++ b/core/startos/src/tunnel/context.rs @@ -24,7 +24,8 @@ use crate::auth::Sessions; use crate::context::config::ContextConfig; use crate::context::{CliContext, RpcContext}; use crate::db::model::public::{NetworkInterfaceInfo, NetworkInterfaceType}; -use crate::middleware::auth::{Auth, AuthContext}; +use crate::middleware::auth::Auth; +use crate::middleware::auth::local::LocalAuthContext; use crate::middleware::cors::Cors; use crate::net::forward::{PortForwardController, add_iptables_rule}; use crate::net::static_server::{EMPTY_DIR, UiContext}; @@ -279,7 +280,7 @@ impl CallRemote for CliContext { method = method.strip_prefix("tunnel.").unwrap_or(method); - crate::middleware::signature::call_remote( + crate::middleware::auth::signature::call_remote( self, url, HeaderMap::new(), @@ -308,7 +309,7 @@ impl CallRemote for RpcContext { let sig_ctx = url.host_str().map(InternedString::from_display); - crate::middleware::signature::call_remote( + crate::middleware::auth::signature::call_remote( self, url, HeaderMap::new(), @@ -331,6 +332,11 @@ impl UiContext for TunnelContext { tunnel_api() } fn middleware(server: rpc_toolkit::Server) -> rpc_toolkit::HttpServer { - server.middleware(Cors::new()).middleware(Auth::new()) + server.middleware(Cors::new()).middleware( + Auth::new() + .with_local_auth() + .with_signature_auth() + .with_session_auth(), + ) } }