assorted fixes

This commit is contained in:
Aiden McClelland
2021-08-13 12:34:29 -06:00
parent 26dd880633
commit c223894943
10 changed files with 154 additions and 75 deletions

View File

@@ -30,34 +30,6 @@
"nullable": [] "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": { "3efd0daa61f4f8bead1adbe78a8225bc31fb940406d0415b578d3adc03a5e414": {
"query": "SELECT hash FROM password", "query": "SELECT hash FROM password",
"describe": { "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": { "6440354d73a67c041ea29508b43b5f309d45837a44f1a562051ad540d894c7d6": {
"query": "DELETE FROM ssh_keys WHERE fingerprint = ?", "query": "DELETE FROM ssh_keys WHERE fingerprint = ?",
"describe": { "describe": {

View File

@@ -1,21 +1,22 @@
use anyhow::anyhow; use anyhow::anyhow;
use basic_cookies::Cookie; use basic_cookies::Cookie;
use chrono::NaiveDateTime; use chrono::{DateTime, NaiveDateTime, Utc};
use clap::ArgMatches; use clap::ArgMatches;
use http::header::COOKIE; use http::header::COOKIE;
use http::HeaderValue; use http::HeaderValue;
use indexmap::IndexMap; use indexmap::IndexMap;
use rpc_toolkit::command; use rpc_toolkit::command;
use rpc_toolkit::command_helpers::prelude::{RequestParts, ResponseParts}; use rpc_toolkit::command_helpers::prelude::{RequestParts, ResponseParts};
use rpc_toolkit::yajrc::RpcError;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use crate::context::EitherContext; use crate::context::EitherContext;
use crate::middleware::auth::{get_id, hash_token}; use crate::middleware::auth::{get_id, hash_token};
use crate::util::{display_none, display_serializable, IoFormat}; 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<EitherContext, Error> { pub fn auth(#[context] ctx: EitherContext) -> Result<EitherContext, Error> {
Ok(ctx) Ok(ctx)
} }
@@ -58,12 +59,16 @@ pub async fn login(
.fetch_one(&mut handle) .fetch_one(&mut handle)
.await? .await?
.hash; .hash;
argon2::verify_encoded(&pw_hash, password.as_bytes()).map_err(|_| { ensure_code!(
Error::new( argon2::verify_encoded(&pw_hash, password.as_bytes()).map_err(|_| {
anyhow!("Password Incorrect"), Error::new(
crate::ErrorKind::Authorization, anyhow!("Password Incorrect"),
) crate::ErrorKind::Authorization,
})?; )
})?,
crate::ErrorKind::Authorization,
"Password Incorrect"
);
let token = base32::encode( let token = base32::encode(
base32::Alphabet::RFC4648 { padding: false }, base32::Alphabet::RFC4648 { padding: false },
&rand::random::<[u8; 16]>(), &rand::random::<[u8; 16]>(),
@@ -107,7 +112,7 @@ pub async fn logout(
if let Some(session) = cookies.iter().find(|c| c.get_name() == "session") { if let Some(session) = cookies.iter().find(|c| c.get_name() == "session") {
let token = session.get_value(); let token = session.get_value();
let id = hash_token(token); let id = hash_token(token);
kill(ctx, id).await?; kill(ctx, vec![id]).await?;
} }
} }
Ok(()) Ok(())
@@ -116,8 +121,8 @@ pub async fn logout(
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct Session { pub struct Session {
logged_in: NaiveDateTime, logged_in: DateTime<Utc>,
last_active: NaiveDateTime, last_active: DateTime<Utc>,
user_agent: Option<String>, user_agent: Option<String>,
metadata: Value, metadata: Value,
} }
@@ -187,8 +192,8 @@ pub async fn list(
Ok(( Ok((
row.id, row.id,
Session { Session {
logged_in: row.logged_in, logged_in: DateTime::from_utc(row.logged_in, Utc),
last_active: row.last_active, last_active: DateTime::from_utc(row.last_active, Utc),
user_agent: row.user_agent, user_agent: row.user_agent,
metadata: serde_json::from_str(&row.metadata) metadata: serde_json::from_str(&row.metadata)
.with_kind(crate::ErrorKind::Database)?, .with_kind(crate::ErrorKind::Database)?,
@@ -199,13 +204,20 @@ pub async fn list(
}) })
} }
fn parse_comma_separated(arg: &str, _: &ArgMatches<'_>) -> Result<Vec<String>, RpcError> {
Ok(arg.split(",").map(|s| s.to_owned()).collect())
}
#[command(display(display_none))] #[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<String>,
) -> Result<(), Error> {
let rpc_ctx = ctx.as_rpc().unwrap(); let rpc_ctx = ctx.as_rpc().unwrap();
sqlx::query!( sqlx::query(&format!(
"UPDATE session SET logged_out = CURRENT_TIMESTAMP WHERE id = ?", "UPDATE session SET logged_out = CURRENT_TIMESTAMP WHERE id IN ('{}')",
id ids.join("','")
) ))
.execute(&mut rpc_ctx.secret_store.acquire().await?) .execute(&mut rpc_ctx.secret_store.acquire().await?)
.await?; .await?;
Ok(()) Ok(())

View File

@@ -38,7 +38,7 @@ fn inner_main() -> Result<(), Error> {
|e: RpcError| { |e: RpcError| {
match e.data { match e.data {
Some(Value::String(s)) => eprintln!("{}: {}", e.message, s), 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) eprintln!("{}: {}", e.message, s)
} }
Some(a) => eprintln!("{}: {}", e.message, a), Some(a) => eprintln!("{}: {}", e.message, a),

View File

@@ -35,7 +35,7 @@ fn inner_main() -> Result<(), Error> {
|e: RpcError| { |e: RpcError| {
match e.data { match e.data {
Some(Value::String(s)) => eprintln!("{}: {}", e.message, s), 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) eprintln!("{}: {}", e.message, s)
} }
Some(a) => eprintln!("{}: {}", e.message, a), Some(a) => eprintln!("{}: {}", e.message, a),

View File

@@ -20,6 +20,7 @@ use tokio_tungstenite::WebSocketStream;
pub use self::model::DatabaseModel; pub use self::model::DatabaseModel;
use self::util::WithRevision; use self::util::WithRevision;
use crate::context::{EitherContext, RpcContext}; use crate::context::{EitherContext, RpcContext};
use crate::middleware::auth::is_authed;
use crate::util::{display_serializable, IoFormat}; use crate::util::{display_serializable, IoFormat};
use crate::{Error, ResultExt}; use crate::{Error, ResultExt};
@@ -55,6 +56,9 @@ async fn ws_handler<
} }
pub async fn subscribe(ctx: RpcContext, req: Request<Body>) -> Result<Response<Body>, Error> { pub async fn subscribe(ctx: RpcContext, req: Request<Body>) -> Result<Response<Body>, 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)?; let (res, ws_fut) = hyper_ws_listener::create_ws(req).with_kind(crate::ErrorKind::Network)?;
if let Some(ws_fut) = ws_fut { if let Some(ws_fut) = ws_fut {
tokio::task::spawn(async move { tokio::task::spawn(async move {

View File

@@ -204,6 +204,8 @@ pub struct InstalledPackageDataEntry {
pub manifest: Manifest, pub manifest: Manifest,
pub system_pointers: Vec<SystemPointerSpec>, pub system_pointers: Vec<SystemPointerSpec>,
#[model] #[model]
pub dependency_info: IndexMap<PackageId, StaticDependencyInfo>,
#[model]
pub current_dependents: IndexMap<PackageId, CurrentDependencyInfo>, pub current_dependents: IndexMap<PackageId, CurrentDependencyInfo>,
#[model] #[model]
pub current_dependencies: IndexMap<PackageId, CurrentDependencyInfo>, pub current_dependencies: IndexMap<PackageId, CurrentDependencyInfo>,
@@ -211,6 +213,13 @@ pub struct InstalledPackageDataEntry {
pub interface_addresses: InterfaceAddressMap, pub interface_addresses: InterfaceAddressMap,
} }
#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
pub struct StaticDependencyInfo {
pub manifest: Option<Manifest>,
pub icon: String,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel)] #[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct CurrentDependencyInfo { pub struct CurrentDependencyInfo {

View File

@@ -11,7 +11,7 @@ use std::time::Duration;
use anyhow::anyhow; use anyhow::anyhow;
use emver::VersionRange; use emver::VersionRange;
use futures::TryStreamExt; use futures::TryStreamExt;
use http::HeaderMap; use http::{HeaderMap, StatusCode};
use indexmap::{IndexMap, IndexSet}; use indexmap::{IndexMap, IndexSet};
use patch_db::json_ptr::JsonPointer; use patch_db::json_ptr::JsonPointer;
use patch_db::{ use patch_db::{
@@ -28,7 +28,8 @@ use tokio::io::{AsyncRead, AsyncSeek, AsyncSeekExt, AsyncWrite, AsyncWriteExt};
use self::progress::{InstallProgress, InstallProgressTracker}; use self::progress::{InstallProgress, InstallProgressTracker};
use crate::context::{EitherContext, ExtendedContext, RpcContext}; use crate::context::{EitherContext, ExtendedContext, RpcContext};
use crate::db::model::{ use crate::db::model::{
CurrentDependencyInfo, InstalledPackageDataEntry, PackageDataEntry, StaticFiles, CurrentDependencyInfo, InstalledPackageDataEntry, PackageDataEntry, StaticDependencyInfo,
StaticFiles,
}; };
use crate::s9pk::manifest::{Manifest, PackageId}; use crate::s9pk::manifest::{Manifest, PackageId};
use crate::s9pk::reader::S9pkReader; use crate::s9pk::reader::S9pkReader;
@@ -272,6 +273,76 @@ pub async fn install_s9pk<R: AsyncRead + AsyncSeek + Unpin>(
.await?; .await?;
log::info!("Install {}@{}: Unpacked Manifest", pkg_id, version); 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<Manifest> = 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) let public_dir_path = Path::new(PKG_PUBLIC_DIR)
.join(pkg_id) .join(pkg_id)
.join(version.as_str()); .join(version.as_str());
@@ -408,6 +479,7 @@ pub async fn install_s9pk<R: AsyncRead + AsyncSeek + Unpin>(
}, },
manifest: manifest.clone(), manifest: manifest.clone(),
system_pointers: Vec::new(), system_pointers: Vec::new(),
dependency_info,
current_dependents: { current_dependents: {
// search required dependencies // search required dependencies
let mut deps = IndexMap::new(); let mut deps = IndexMap::new();

View File

@@ -47,18 +47,18 @@ pub fn hash_token(token: &str) -> String {
.to_lowercase() .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 id = get_id(req)?;
let exp = sqlx::query!("SELECT logged_out FROM session WHERE id = ?", id) let session = sqlx::query!("UPDATE session SET last_active = CURRENT_TIMESTAMP WHERE id = ? AND logged_out IS NULL OR logged_out > CURRENT_TIMESTAMP", id)
.fetch_one(&mut ctx.secret_store.acquire().await?) .execute(&mut ctx.secret_store.acquire().await?)
.await?; .await?;
match exp.logged_out { if session.rows_affected() == 0 {
Some(exp) if exp >= Utc::now().naive_utc() => Err(Error::new( return Err(Error::new(
anyhow!("UNAUTHORIZED"), anyhow!("UNAUTHORIZED"),
crate::ErrorKind::Authorization, crate::ErrorKind::Authorization,
)), ));
_ => Ok(()),
} }
Ok(())
} }
pub fn auth<M: Metadata>(ctx: RpcContext) -> DynMiddleware<M> { pub fn auth<M: Metadata>(ctx: RpcContext) -> DynMiddleware<M> {

View File

@@ -22,7 +22,7 @@ export class SessionsPage {
async ngOnInit () { async ngOnInit () {
try { try {
this.sessionInfo = await this.embassyApi.getSessions({ }) this.sessionInfo = await this.embassyApi.getSessions({})
} catch (e) { } catch (e) {
this.errToast.present(e.message) this.errToast.present(e.message)
} finally { } finally {

View File

@@ -19,7 +19,7 @@ export module RR {
export type LoginReq = { password: string, metadata: SessionMetadata } // auth.login - unauthed export type LoginReq = { password: string, metadata: SessionMetadata } // auth.login - unauthed
export type loginRes = null export type loginRes = null
export type LogoutReq = { } // auth.logout export type LogoutReq = {} // auth.logout
export type LogoutRes = null export type LogoutRes = null
// server // server
@@ -30,26 +30,26 @@ export module RR {
export type GetServerLogsReq = { before?: string } // server.logs export type GetServerLogsReq = { before?: string } // server.logs
export type GetServerLogsRes = Log[] export type GetServerLogsRes = Log[]
export type GetServerMetricsReq = { } // server.metrics export type GetServerMetricsReq = {} // server.metrics
export type GetServerMetricsRes = Metrics export type GetServerMetricsRes = Metrics
export type UpdateServerReq = WithExpire<{ }> // server.update export type UpdateServerReq = WithExpire<{}> // server.update
export type UpdateServerRes = WithRevision<null> export type UpdateServerRes = WithRevision<null>
export type RestartServerReq = { } // server.restart export type RestartServerReq = {} // server.restart
export type RestartServerRes = null export type RestartServerRes = null
export type ShutdownServerReq = { } // server.shutdown export type ShutdownServerReq = {} // server.shutdown
export type ShutdownServerRes = null export type ShutdownServerRes = null
// network // network
export type RefreshLanReq = { } // network.lan.refresh export type RefreshLanReq = {} // network.lan.refresh
export type RefreshLanRes = null export type RefreshLanRes = null
// sessions // sessions
export type GetSessionsReq = { } // sessions.list export type GetSessionsReq = {} // sessions.list
export type GetSessionsRes = { export type GetSessionsRes = {
current: string, current: string,
sessions: { [hash: string]: Session } sessions: { [hash: string]: Session }
@@ -78,7 +78,7 @@ export module RR {
export type DeleteNotificationReq = { id: string } // notification.delete export type DeleteNotificationReq = { id: string } // notification.delete
export type DeleteNotificationRes = null export type DeleteNotificationRes = null
export type DeleteAllNotificationsReq = { } // notification.delete.all export type DeleteAllNotificationsReq = {} // notification.delete.all
export type DeleteAllNotificationsRes = null export type DeleteAllNotificationsRes = null
// wifi // wifi
@@ -100,7 +100,7 @@ export module RR {
// ssh // ssh
export type GetSSHKeysReq = { } // ssh.get export type GetSSHKeysReq = {} // ssh.get
export type GetSSHKeysRes = SSHKeys export type GetSSHKeysRes = SSHKeys
export type AddSSHKeyReq = { pubkey: string } // ssh.add export type AddSSHKeyReq = { pubkey: string } // ssh.add
@@ -119,7 +119,7 @@ export module RR {
// disk // disk
export type GetDisksReq = { } // disk.list export type GetDisksReq = {} // disk.list
export type GetDisksRes = DiskInfo export type GetDisksRes = DiskInfo
export type EjectDisksReq = { logicalname: string } // disk.eject export type EjectDisksReq = { logicalname: string } // disk.eject
@@ -178,10 +178,10 @@ export module RR {
// marketplace // marketplace
export type GetMarketplaceDataReq = { } export type GetMarketplaceDataReq = {}
export type GetMarketplaceDataRes = MarketplaceData export type GetMarketplaceDataRes = MarketplaceData
export type GetMarketplaceEOSReq = { } export type GetMarketplaceEOSReq = {}
export type GetMarketplaceEOSRes = MarketplaceEOS export type GetMarketplaceEOSRes = MarketplaceEOS
export type GetMarketplacePackagesReq = { export type GetMarketplacePackagesReq = {
@@ -195,10 +195,10 @@ export module RR {
export type GetMarketplacePackagesRes = MarketplacePkg[] export type GetMarketplacePackagesRes = MarketplacePkg[]
export type GetReleaseNotesReq = { id: string } export type GetReleaseNotesReq = { id: string }
export type GetReleaseNotesRes = { [version: string]: string} export type GetReleaseNotesRes = { [version: string]: string }
export type GetLatestVersionReq = { ids: 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> = T extends 0 ? null : export type NotificationData<T> = T extends 0 ? null :
T extends 1 ? BackupReport : T extends 1 ? BackupReport :
any any
export interface BackupReport { export interface BackupReport {
server: { server: {