From c18a119c70a2a3a8e1cb063f64ce912298d31c00 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Fri, 13 Aug 2021 12:34:29 -0600 Subject: [PATCH] assorted fixes --- appmgr/sqlx-data.json | 38 +++------- appmgr/src/auth.rs | 50 +++++++----- appmgr/src/bin/embassy-cli.rs | 2 +- appmgr/src/bin/embassy-sdk.rs | 2 +- appmgr/src/db/mod.rs | 4 + appmgr/src/db/model.rs | 9 +++ appmgr/src/install/mod.rs | 76 ++++++++++++++++++- appmgr/src/middleware/auth.rs | 14 ++-- .../security-routes/sessions/sessions.page.ts | 2 +- ui/src/app/services/api/api.types.ts | 32 ++++---- 10 files changed, 154 insertions(+), 75 deletions(-) diff --git a/appmgr/sqlx-data.json b/appmgr/sqlx-data.json index a35673e21..045dbba77 100644 --- a/appmgr/sqlx-data.json +++ b/appmgr/sqlx-data.json @@ -30,34 +30,6 @@ "nullable": [] } }, - "18aa5fba13df20495611fbbf7cb331f4b6da5b906148e0d9565905d3bb10071c": { - "query": "SELECT logged_out FROM session WHERE id = ?", - "describe": { - "columns": [ - { - "name": "logged_out", - "ordinal": 0, - "type_info": "Datetime" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - true - ] - } - }, - "2932aa02735b6422fca4ba889abfb3de8598178d4690076dc278898753d9df62": { - "query": "UPDATE session SET logged_out = CURRENT_TIMESTAMP WHERE id = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - } - }, "3efd0daa61f4f8bead1adbe78a8225bc31fb940406d0415b578d3adc03a5e414": { "query": "SELECT hash FROM password", "describe": { @@ -124,6 +96,16 @@ ] } }, + "63785dc5f193ea31e6f641a910c75857ccd288a3f6e9c4f704331531e4f0689f": { + "query": "UPDATE session SET last_active = CURRENT_TIMESTAMP WHERE id = ? AND logged_out IS NULL OR logged_out > CURRENT_TIMESTAMP", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + } + }, "6440354d73a67c041ea29508b43b5f309d45837a44f1a562051ad540d894c7d6": { "query": "DELETE FROM ssh_keys WHERE fingerprint = ?", "describe": { diff --git a/appmgr/src/auth.rs b/appmgr/src/auth.rs index 27ed089f4..7f5f51455 100644 --- a/appmgr/src/auth.rs +++ b/appmgr/src/auth.rs @@ -1,21 +1,22 @@ use anyhow::anyhow; use basic_cookies::Cookie; -use chrono::NaiveDateTime; +use chrono::{DateTime, NaiveDateTime, Utc}; use clap::ArgMatches; use http::header::COOKIE; use http::HeaderValue; use indexmap::IndexMap; use rpc_toolkit::command; use rpc_toolkit::command_helpers::prelude::{RequestParts, ResponseParts}; +use rpc_toolkit::yajrc::RpcError; use serde::{Deserialize, Serialize}; use serde_json::Value; use crate::context::EitherContext; use crate::middleware::auth::{get_id, hash_token}; use crate::util::{display_none, display_serializable, IoFormat}; -use crate::{Error, ResultExt}; +use crate::{ensure_code, Error, ResultExt}; -#[command(subcommands(login, logout))] +#[command(subcommands(login, logout, session))] pub fn auth(#[context] ctx: EitherContext) -> Result { Ok(ctx) } @@ -58,12 +59,16 @@ pub async fn login( .fetch_one(&mut handle) .await? .hash; - argon2::verify_encoded(&pw_hash, password.as_bytes()).map_err(|_| { - Error::new( - anyhow!("Password Incorrect"), - crate::ErrorKind::Authorization, - ) - })?; + ensure_code!( + argon2::verify_encoded(&pw_hash, password.as_bytes()).map_err(|_| { + Error::new( + anyhow!("Password Incorrect"), + crate::ErrorKind::Authorization, + ) + })?, + crate::ErrorKind::Authorization, + "Password Incorrect" + ); let token = base32::encode( base32::Alphabet::RFC4648 { padding: false }, &rand::random::<[u8; 16]>(), @@ -107,7 +112,7 @@ pub async fn logout( if let Some(session) = cookies.iter().find(|c| c.get_name() == "session") { let token = session.get_value(); let id = hash_token(token); - kill(ctx, id).await?; + kill(ctx, vec![id]).await?; } } Ok(()) @@ -116,8 +121,8 @@ pub async fn logout( #[derive(Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub struct Session { - logged_in: NaiveDateTime, - last_active: NaiveDateTime, + logged_in: DateTime, + last_active: DateTime, user_agent: Option, metadata: Value, } @@ -187,8 +192,8 @@ pub async fn list( Ok(( row.id, Session { - logged_in: row.logged_in, - last_active: row.last_active, + logged_in: DateTime::from_utc(row.logged_in, Utc), + last_active: DateTime::from_utc(row.last_active, Utc), user_agent: row.user_agent, metadata: serde_json::from_str(&row.metadata) .with_kind(crate::ErrorKind::Database)?, @@ -199,13 +204,20 @@ pub async fn list( }) } +fn parse_comma_separated(arg: &str, _: &ArgMatches<'_>) -> Result, RpcError> { + Ok(arg.split(",").map(|s| s.to_owned()).collect()) +} + #[command(display(display_none))] -pub async fn kill(#[context] ctx: EitherContext, #[arg] id: String) -> Result<(), Error> { +pub async fn kill( + #[context] ctx: EitherContext, + #[arg(parse(parse_comma_separated))] ids: Vec, +) -> Result<(), Error> { let rpc_ctx = ctx.as_rpc().unwrap(); - sqlx::query!( - "UPDATE session SET logged_out = CURRENT_TIMESTAMP WHERE id = ?", - id - ) + sqlx::query(&format!( + "UPDATE session SET logged_out = CURRENT_TIMESTAMP WHERE id IN ('{}')", + ids.join("','") + )) .execute(&mut rpc_ctx.secret_store.acquire().await?) .await?; Ok(()) diff --git a/appmgr/src/bin/embassy-cli.rs b/appmgr/src/bin/embassy-cli.rs index 5035cc240..c34e4cd83 100644 --- a/appmgr/src/bin/embassy-cli.rs +++ b/appmgr/src/bin/embassy-cli.rs @@ -38,7 +38,7 @@ fn inner_main() -> Result<(), Error> { |e: RpcError| { match e.data { Some(Value::String(s)) => eprintln!("{}: {}", e.message, s), - Some(Value::Object(o)) => if let Some(Value::String(s)) = o.get("message") { + Some(Value::Object(o)) => if let Some(Value::String(s)) = o.get("details") { eprintln!("{}: {}", e.message, s) } Some(a) => eprintln!("{}: {}", e.message, a), diff --git a/appmgr/src/bin/embassy-sdk.rs b/appmgr/src/bin/embassy-sdk.rs index 914db6fbc..de668f8f3 100644 --- a/appmgr/src/bin/embassy-sdk.rs +++ b/appmgr/src/bin/embassy-sdk.rs @@ -35,7 +35,7 @@ fn inner_main() -> Result<(), Error> { |e: RpcError| { match e.data { Some(Value::String(s)) => eprintln!("{}: {}", e.message, s), - Some(Value::Object(o)) => if let Some(Value::String(s)) = o.get("message") { + Some(Value::Object(o)) => if let Some(Value::String(s)) = o.get("details") { eprintln!("{}: {}", e.message, s) } Some(a) => eprintln!("{}: {}", e.message, a), diff --git a/appmgr/src/db/mod.rs b/appmgr/src/db/mod.rs index 2fcb2611f..8def1b9f9 100644 --- a/appmgr/src/db/mod.rs +++ b/appmgr/src/db/mod.rs @@ -20,6 +20,7 @@ use tokio_tungstenite::WebSocketStream; pub use self::model::DatabaseModel; use self::util::WithRevision; use crate::context::{EitherContext, RpcContext}; +use crate::middleware::auth::is_authed; use crate::util::{display_serializable, IoFormat}; use crate::{Error, ResultExt}; @@ -55,6 +56,9 @@ async fn ws_handler< } pub async fn subscribe(ctx: RpcContext, req: Request) -> Result, Error> { + let (parts, body) = req.into_parts(); + // is_authed(&ctx, &parts).await?; + let req = Request::from_parts(parts, body); let (res, ws_fut) = hyper_ws_listener::create_ws(req).with_kind(crate::ErrorKind::Network)?; if let Some(ws_fut) = ws_fut { tokio::task::spawn(async move { diff --git a/appmgr/src/db/model.rs b/appmgr/src/db/model.rs index 9c0949a31..a4ee3f607 100644 --- a/appmgr/src/db/model.rs +++ b/appmgr/src/db/model.rs @@ -204,6 +204,8 @@ pub struct InstalledPackageDataEntry { pub manifest: Manifest, pub system_pointers: Vec, #[model] + pub dependency_info: IndexMap, + #[model] pub current_dependents: IndexMap, #[model] pub current_dependencies: IndexMap, @@ -211,6 +213,13 @@ pub struct InstalledPackageDataEntry { pub interface_addresses: InterfaceAddressMap, } +#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +pub struct StaticDependencyInfo { + pub manifest: Option, + pub icon: String, +} + #[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel)] #[serde(rename_all = "kebab-case")] pub struct CurrentDependencyInfo { diff --git a/appmgr/src/install/mod.rs b/appmgr/src/install/mod.rs index 4218c86b9..bff7503ab 100644 --- a/appmgr/src/install/mod.rs +++ b/appmgr/src/install/mod.rs @@ -11,7 +11,7 @@ use std::time::Duration; use anyhow::anyhow; use emver::VersionRange; use futures::TryStreamExt; -use http::HeaderMap; +use http::{HeaderMap, StatusCode}; use indexmap::{IndexMap, IndexSet}; use patch_db::json_ptr::JsonPointer; use patch_db::{ @@ -28,7 +28,8 @@ use tokio::io::{AsyncRead, AsyncSeek, AsyncSeekExt, AsyncWrite, AsyncWriteExt}; use self::progress::{InstallProgress, InstallProgressTracker}; use crate::context::{EitherContext, ExtendedContext, RpcContext}; use crate::db::model::{ - CurrentDependencyInfo, InstalledPackageDataEntry, PackageDataEntry, StaticFiles, + CurrentDependencyInfo, InstalledPackageDataEntry, PackageDataEntry, StaticDependencyInfo, + StaticFiles, }; use crate::s9pk::manifest::{Manifest, PackageId}; use crate::s9pk::reader::S9pkReader; @@ -272,6 +273,76 @@ pub async fn install_s9pk( .await?; log::info!("Install {}@{}: Unpacked Manifest", pkg_id, version); + log::info!("Install {}@{}: Fetching Dependency Info", pkg_id, version); + let mut dependency_info = IndexMap::with_capacity(manifest.dependencies.0.len()); + let reg_url = ctx.package_registry_url().await?; + for (dep, info) in &manifest.dependencies.0 { + let manifest: Option = match reqwest::get(format!( + "{}/package/manifest/{}?version={}", + reg_url, dep, info.version + )) + .await + .with_kind(crate::ErrorKind::Registry)? + .error_for_status() + { + Ok(a) => Ok(Some( + a.json() + .await + .with_kind(crate::ErrorKind::Deserialization)?, + )), + Err(e) if e.status() == Some(StatusCode::BAD_REQUEST) => Ok(None), + Err(e) => Err(e), + } + .with_kind(crate::ErrorKind::Registry)?; + if let Some(manifest) = manifest { + let dir = Path::new(PKG_PUBLIC_DIR) + .join(&manifest.id) + .join(manifest.version.as_str()); + let icon_path = dir.join(format!("icon.{}", manifest.assets.icon_type())); + if tokio::fs::metadata(&icon_path).await.is_err() { + tokio::fs::create_dir_all(&dir).await?; + let icon = reqwest::get(format!( + "{}/package/icon/{}?version={}", + reg_url, dep, info.version + )) + .await + .with_kind(crate::ErrorKind::Registry)?; + let mut dst = File::create(&icon_path).await?; + tokio::io::copy( + &mut tokio_util::io::StreamReader::new(icon.bytes_stream().map_err(|e| { + std::io::Error::new( + if e.is_connect() { + std::io::ErrorKind::ConnectionRefused + } else if e.is_timeout() { + std::io::ErrorKind::TimedOut + } else { + std::io::ErrorKind::Other + }, + e, + ) + })), + &mut dst, + ) + .await?; + dst.sync_all().await?; + } + + dependency_info.insert( + dep.clone(), + StaticDependencyInfo { + icon: format!( + "/public/package-data/{}/{}/icon.{}", + manifest.id, + manifest.version, + manifest.assets.icon_type() + ), + manifest: Some(manifest), + }, + ); + } + } + log::info!("Install {}@{}: Fetched Dependency Info", pkg_id, version); + let public_dir_path = Path::new(PKG_PUBLIC_DIR) .join(pkg_id) .join(version.as_str()); @@ -408,6 +479,7 @@ pub async fn install_s9pk( }, manifest: manifest.clone(), system_pointers: Vec::new(), + dependency_info, current_dependents: { // search required dependencies let mut deps = IndexMap::new(); diff --git a/appmgr/src/middleware/auth.rs b/appmgr/src/middleware/auth.rs index c1fbf9814..b73c30e0a 100644 --- a/appmgr/src/middleware/auth.rs +++ b/appmgr/src/middleware/auth.rs @@ -47,18 +47,18 @@ pub fn hash_token(token: &str) -> String { .to_lowercase() } -async fn is_authed(ctx: &RpcContext, req: &RequestParts) -> Result<(), Error> { +pub async fn is_authed(ctx: &RpcContext, req: &RequestParts) -> Result<(), Error> { let id = get_id(req)?; - let exp = sqlx::query!("SELECT logged_out FROM session WHERE id = ?", id) - .fetch_one(&mut ctx.secret_store.acquire().await?) + let session = sqlx::query!("UPDATE session SET last_active = CURRENT_TIMESTAMP WHERE id = ? AND logged_out IS NULL OR logged_out > CURRENT_TIMESTAMP", id) + .execute(&mut ctx.secret_store.acquire().await?) .await?; - match exp.logged_out { - Some(exp) if exp >= Utc::now().naive_utc() => Err(Error::new( + if session.rows_affected() == 0 { + return Err(Error::new( anyhow!("UNAUTHORIZED"), crate::ErrorKind::Authorization, - )), - _ => Ok(()), + )); } + Ok(()) } pub fn auth(ctx: RpcContext) -> DynMiddleware { diff --git a/ui/src/app/pages/server-routes/security-routes/sessions/sessions.page.ts b/ui/src/app/pages/server-routes/security-routes/sessions/sessions.page.ts index d7f2871fc..6b5e4f047 100644 --- a/ui/src/app/pages/server-routes/security-routes/sessions/sessions.page.ts +++ b/ui/src/app/pages/server-routes/security-routes/sessions/sessions.page.ts @@ -22,7 +22,7 @@ export class SessionsPage { async ngOnInit () { try { - this.sessionInfo = await this.embassyApi.getSessions({ }) + this.sessionInfo = await this.embassyApi.getSessions({}) } catch (e) { this.errToast.present(e.message) } finally { diff --git a/ui/src/app/services/api/api.types.ts b/ui/src/app/services/api/api.types.ts index 3c50701ba..2914a5247 100644 --- a/ui/src/app/services/api/api.types.ts +++ b/ui/src/app/services/api/api.types.ts @@ -19,7 +19,7 @@ export module RR { export type LoginReq = { password: string, metadata: SessionMetadata } // auth.login - unauthed export type loginRes = null - export type LogoutReq = { } // auth.logout + export type LogoutReq = {} // auth.logout export type LogoutRes = null // server @@ -30,26 +30,26 @@ export module RR { export type GetServerLogsReq = { before?: string } // server.logs export type GetServerLogsRes = Log[] - export type GetServerMetricsReq = { } // server.metrics + export type GetServerMetricsReq = {} // server.metrics export type GetServerMetricsRes = Metrics - export type UpdateServerReq = WithExpire<{ }> // server.update + export type UpdateServerReq = WithExpire<{}> // server.update export type UpdateServerRes = WithRevision - export type RestartServerReq = { } // server.restart + export type RestartServerReq = {} // server.restart export type RestartServerRes = null - export type ShutdownServerReq = { } // server.shutdown + export type ShutdownServerReq = {} // server.shutdown export type ShutdownServerRes = null // network - export type RefreshLanReq = { } // network.lan.refresh + export type RefreshLanReq = {} // network.lan.refresh export type RefreshLanRes = null // sessions - export type GetSessionsReq = { } // sessions.list + export type GetSessionsReq = {} // sessions.list export type GetSessionsRes = { current: string, sessions: { [hash: string]: Session } @@ -78,7 +78,7 @@ export module RR { export type DeleteNotificationReq = { id: string } // notification.delete export type DeleteNotificationRes = null - export type DeleteAllNotificationsReq = { } // notification.delete.all + export type DeleteAllNotificationsReq = {} // notification.delete.all export type DeleteAllNotificationsRes = null // wifi @@ -100,7 +100,7 @@ export module RR { // ssh - export type GetSSHKeysReq = { } // ssh.get + export type GetSSHKeysReq = {} // ssh.get export type GetSSHKeysRes = SSHKeys export type AddSSHKeyReq = { pubkey: string } // ssh.add @@ -119,7 +119,7 @@ export module RR { // disk - export type GetDisksReq = { } // disk.list + export type GetDisksReq = {} // disk.list export type GetDisksRes = DiskInfo export type EjectDisksReq = { logicalname: string } // disk.eject @@ -178,10 +178,10 @@ export module RR { // marketplace - export type GetMarketplaceDataReq = { } + export type GetMarketplaceDataReq = {} export type GetMarketplaceDataRes = MarketplaceData - export type GetMarketplaceEOSReq = { } + export type GetMarketplaceEOSReq = {} export type GetMarketplaceEOSRes = MarketplaceEOS export type GetMarketplacePackagesReq = { @@ -195,10 +195,10 @@ export module RR { export type GetMarketplacePackagesRes = MarketplacePkg[] export type GetReleaseNotesReq = { id: string } - export type GetReleaseNotesRes = { [version: string]: string} + export type GetReleaseNotesRes = { [version: string]: string } export type GetLatestVersionReq = { ids: string[] } - export type GetLatestVersionRes = { [id: string]: string} + export type GetLatestVersionRes = { [id: string]: string } } @@ -334,8 +334,8 @@ export enum NotificationLevel { } export type NotificationData = T extends 0 ? null : - T extends 1 ? BackupReport : - any + T extends 1 ? BackupReport : + any export interface BackupReport { server: {