mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
Feat--auth-static (#684)
* feat: static server * WIP: Idea (#685) * wip: New Idea Use tokens as proofs. Use proofs as arguments. Return proofs as indication of side effects? Forced constructor pattern? * chore: Use the has notation for reciepts * chore: Example to main db mod * feat: Content headers * polish Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
1
appmgr/Cargo.lock
generated
1
appmgr/Cargo.lock
generated
@@ -845,6 +845,7 @@ dependencies = [
|
||||
"hex",
|
||||
"hmac",
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-ws-listener",
|
||||
"indexmap",
|
||||
"isocountry",
|
||||
|
||||
@@ -66,6 +66,7 @@ hex = "0.4.3"
|
||||
hmac = "0.11.0"
|
||||
http = "0.2.5"
|
||||
hyper-ws-listener = { git = "https://github.com/Start9Labs/hyper-ws-listener.git", branch = "main" }
|
||||
hyper = "0.14.13"
|
||||
indexmap = { version = "1.7.0", features = ["serde"] }
|
||||
isocountry = "0.3.2"
|
||||
itertools = "0.10.1"
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use basic_cookies::Cookie;
|
||||
use chrono::{DateTime, Utc};
|
||||
use clap::ArgMatches;
|
||||
use color_eyre::eyre::eyre;
|
||||
use http::header::COOKIE;
|
||||
use http::HeaderValue;
|
||||
use rpc_toolkit::command;
|
||||
use rpc_toolkit::command_helpers::prelude::{RequestParts, ResponseParts};
|
||||
@@ -15,7 +13,7 @@ use serde_json::Value;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::middleware::auth::{get_id, hash_token};
|
||||
use crate::middleware::auth::{AsLogoutSessionId, HasLoggedOutSessions, HashSessionToken};
|
||||
use crate::util::{display_none, display_serializable, IoFormat};
|
||||
use crate::{ensure_code, Error, ResultExt};
|
||||
|
||||
@@ -98,17 +96,14 @@ pub async fn login(
|
||||
crate::ErrorKind::Authorization,
|
||||
"Password Incorrect"
|
||||
);
|
||||
let token = base32::encode(
|
||||
base32::Alphabet::RFC4648 { padding: false },
|
||||
&rand::random::<[u8; 16]>(),
|
||||
)
|
||||
.to_lowercase();
|
||||
let id = hash_token(&token);
|
||||
|
||||
let hash_token = HashSessionToken::new();
|
||||
let user_agent = req.headers.get("user-agent").and_then(|h| h.to_str().ok());
|
||||
let metadata = serde_json::to_string(&metadata).with_kind(crate::ErrorKind::Database)?;
|
||||
let hash_token_hashed = hash_token.hashed();
|
||||
sqlx::query!(
|
||||
"INSERT INTO session (id, user_agent, metadata) VALUES (?, ?, ?)",
|
||||
id,
|
||||
hash_token_hashed,
|
||||
user_agent,
|
||||
metadata,
|
||||
)
|
||||
@@ -116,11 +111,7 @@ pub async fn login(
|
||||
.await?;
|
||||
res.headers.insert(
|
||||
"set-cookie",
|
||||
HeaderValue::from_str(&format!(
|
||||
"session={}; Path=/; SameSite=Lax; Expires=Fri, 31 Dec 9999 23:59:59 GMT;",
|
||||
token
|
||||
))
|
||||
.with_kind(crate::ErrorKind::Unknown)?, // Should be impossible, but don't want to panic
|
||||
hash_token.header_value()?, // Should be impossible, but don't want to panic
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@@ -131,21 +122,12 @@ pub async fn login(
|
||||
pub async fn logout(
|
||||
#[context] ctx: RpcContext,
|
||||
#[request] req: &RequestParts,
|
||||
) -> Result<(), Error> {
|
||||
if let Some(cookie_header) = req.headers.get(COOKIE) {
|
||||
let cookies = Cookie::parse(
|
||||
cookie_header
|
||||
.to_str()
|
||||
.with_kind(crate::ErrorKind::Authorization)?,
|
||||
)
|
||||
.with_kind(crate::ErrorKind::Authorization)?;
|
||||
if let Some(session) = cookies.iter().find(|c| c.get_name() == "session") {
|
||||
let token = session.get_value();
|
||||
let id = hash_token(token);
|
||||
kill(ctx, vec![id]).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
) -> Result<Option<HasLoggedOutSessions>, Error> {
|
||||
let auth = match HashSessionToken::from_request_parts(req) {
|
||||
Err(_) => return Ok(None),
|
||||
Ok(a) => a,
|
||||
};
|
||||
Ok(Some(HasLoggedOutSessions::new(vec![auth], &ctx).await?))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
@@ -212,7 +194,7 @@ pub async fn list(
|
||||
format: Option<IoFormat>,
|
||||
) -> Result<SessionList, Error> {
|
||||
Ok(SessionList {
|
||||
current: get_id(req)?,
|
||||
current: HashSessionToken::from_request_parts(req)?.as_hash(),
|
||||
sessions: sqlx::query!(
|
||||
"SELECT * FROM session WHERE logged_out IS NULL OR logged_out > CURRENT_TIMESTAMP"
|
||||
)
|
||||
@@ -239,17 +221,21 @@ fn parse_comma_separated(arg: &str, _: &ArgMatches<'_>) -> Result<Vec<String>, R
|
||||
Ok(arg.split(",").map(|s| s.trim().to_owned()).collect())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct KillSessionId(String);
|
||||
|
||||
impl AsLogoutSessionId for KillSessionId {
|
||||
fn as_logout_session_id(self) -> String {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[command(display(display_none))]
|
||||
#[instrument(skip(ctx))]
|
||||
pub async fn kill(
|
||||
#[context] ctx: RpcContext,
|
||||
#[arg(parse(parse_comma_separated))] ids: Vec<String>,
|
||||
) -> Result<(), Error> {
|
||||
sqlx::query(&format!(
|
||||
"UPDATE session SET logged_out = CURRENT_TIMESTAMP WHERE id IN ('{}')",
|
||||
ids.join("','")
|
||||
))
|
||||
.execute(&mut ctx.secret_store.acquire().await?)
|
||||
.await?;
|
||||
HasLoggedOutSessions::new(ids.into_iter().map(KillSessionId), &ctx).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -132,24 +132,6 @@ async fn init(cfg_path: Option<&str>) -> Result<(), Error> {
|
||||
|
||||
embassy::hostname::sync_hostname().await?;
|
||||
tracing::info!("Synced Hostname");
|
||||
|
||||
if tokio::fs::metadata("/var/www/html/main/public")
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
tokio::fs::create_dir_all("/var/www/html/main/public").await?
|
||||
}
|
||||
if tokio::fs::symlink_metadata("/var/www/html/main/public/package-data")
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
tokio::fs::symlink(
|
||||
cfg.datadir().join("package-data").join("public"),
|
||||
"/var/www/html/main/public/package-data",
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
tracing::info!("Enabled nginx public dir");
|
||||
embassy::net::wifi::synchronize_wpa_supplicant_conf(&cfg.datadir().join("main")).await?;
|
||||
tracing::info!("Synchronized wpa_supplicant.conf");
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ use embassy::net::tor::tor_health_check;
|
||||
use embassy::shutdown::Shutdown;
|
||||
use embassy::status::{check_all, synchronize_all};
|
||||
use embassy::util::{daemon, Invoke};
|
||||
use embassy::{Error, ErrorKind, ResultExt};
|
||||
use embassy::{static_server, Error, ErrorKind, ResultExt};
|
||||
use futures::{FutureExt, TryFutureExt};
|
||||
use reqwest::{Client, Proxy};
|
||||
use rpc_toolkit::hyper::{Body, Response, Server, StatusCode};
|
||||
@@ -164,6 +164,16 @@ async fn inner_main(cfg_path: Option<&str>) -> Result<Option<Shutdown>, Error> {
|
||||
}
|
||||
});
|
||||
|
||||
let file_server_ctx = rpc_ctx.clone();
|
||||
let file_server = {
|
||||
static_server::init(file_server_ctx, {
|
||||
let mut shutdown = rpc_ctx.shutdown.subscribe();
|
||||
async move {
|
||||
shutdown.recv().await.expect("context dropped");
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let status_ctx = rpc_ctx.clone();
|
||||
let status_daemon = daemon(
|
||||
move || {
|
||||
@@ -227,6 +237,7 @@ async fn inner_main(cfg_path: Option<&str>) -> Result<Option<Shutdown>, Error> {
|
||||
ErrorKind::Unknown
|
||||
)),
|
||||
ws_server.map_err(|e| Error::new(e, ErrorKind::Network)),
|
||||
file_server.map_err(|e| Error::new(e, ErrorKind::Network)),
|
||||
status_daemon.map_err(|e| Error::new(
|
||||
e.wrap_err("Status Sync daemon panicked!"),
|
||||
ErrorKind::Unknown
|
||||
|
||||
@@ -39,6 +39,7 @@ use crate::{Error, ResultExt};
|
||||
pub struct RpcContextConfig {
|
||||
pub bind_rpc: Option<SocketAddr>,
|
||||
pub bind_ws: Option<SocketAddr>,
|
||||
pub bind_static: Option<SocketAddr>,
|
||||
pub tor_control: Option<SocketAddr>,
|
||||
pub tor_socks: Option<SocketAddr>,
|
||||
pub revision_cache_size: Option<usize>,
|
||||
@@ -112,6 +113,7 @@ impl RpcContextConfig {
|
||||
pub struct RpcContextSeed {
|
||||
pub bind_rpc: SocketAddr,
|
||||
pub bind_ws: SocketAddr,
|
||||
pub bind_static: SocketAddr,
|
||||
pub datadir: PathBuf,
|
||||
pub zfs_pool_name: Arc<String>,
|
||||
pub db: PatchDb,
|
||||
@@ -162,6 +164,7 @@ impl RpcContext {
|
||||
let seed = Arc::new(RpcContextSeed {
|
||||
bind_rpc: base.bind_rpc.unwrap_or(([127, 0, 0, 1], 5959).into()),
|
||||
bind_ws: base.bind_ws.unwrap_or(([127, 0, 0, 1], 5960).into()),
|
||||
bind_static: base.bind_static.unwrap_or(([127, 0, 0, 1], 5961).into()),
|
||||
datadir: base.datadir().to_path_buf(),
|
||||
zfs_pool_name: Arc::new(base.zfs_pool_name().to_owned()),
|
||||
db,
|
||||
|
||||
@@ -24,7 +24,7 @@ use tracing::instrument;
|
||||
pub use self::model::DatabaseModel;
|
||||
use self::util::WithRevision;
|
||||
use crate::context::RpcContext;
|
||||
use crate::middleware::auth::hash_token;
|
||||
use crate::middleware::auth::{HasValidSession, HashSessionToken};
|
||||
use crate::util::{display_serializable, GeneralGuard, IoFormat};
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
@@ -34,7 +34,7 @@ async fn ws_handler<
|
||||
ctx: RpcContext,
|
||||
ws_fut: WSFut,
|
||||
) -> Result<(), Error> {
|
||||
let (dump, mut sub) = ctx.db.dump_and_sub().await;
|
||||
let (dump, sub) = ctx.db.dump_and_sub().await;
|
||||
let mut stream = ws_fut
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Network)?
|
||||
@@ -54,7 +54,7 @@ async fn ws_handler<
|
||||
()
|
||||
});
|
||||
|
||||
loop {
|
||||
let has_valid_session = loop {
|
||||
if let Some(Message::Text(cookie)) = stream
|
||||
.next()
|
||||
.await
|
||||
@@ -63,6 +63,7 @@ async fn ws_handler<
|
||||
{
|
||||
let cookie_str = serde_json::from_str::<Cow<str>>(&cookie)
|
||||
.with_kind(crate::ErrorKind::Deserialization)?;
|
||||
|
||||
let id = basic_cookies::Cookie::parse(&cookie_str)
|
||||
.with_kind(crate::ErrorKind::Authorization)?
|
||||
.into_iter()
|
||||
@@ -70,38 +71,40 @@ async fn ws_handler<
|
||||
.ok_or_else(|| {
|
||||
Error::new(eyre!("UNAUTHORIZED"), crate::ErrorKind::Authorization)
|
||||
})?;
|
||||
if let Err(e) =
|
||||
crate::middleware::auth::is_authed(&ctx, &hash_token(id.get_value())).await
|
||||
{
|
||||
stream
|
||||
.send(Message::Text(
|
||||
serde_json::to_string(
|
||||
&RpcResponse::<GenericRpcMethod<String>>::from_result(
|
||||
Err::<_, RpcError>(e.into()),
|
||||
),
|
||||
)
|
||||
.with_kind(crate::ErrorKind::Serialization)?,
|
||||
))
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Network)?;
|
||||
return Ok(());
|
||||
let authenticated_session = HashSessionToken::from_cookie(&id);
|
||||
match HasValidSession::from_session(&authenticated_session, &ctx).await {
|
||||
Err(e) => {
|
||||
stream
|
||||
.send(Message::Text(
|
||||
serde_json::to_string(
|
||||
&RpcResponse::<GenericRpcMethod<String>>::from_result(Err::<
|
||||
_,
|
||||
RpcError,
|
||||
>(
|
||||
e.into()
|
||||
)),
|
||||
)
|
||||
.with_kind(crate::ErrorKind::Serialization)?,
|
||||
))
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Network)?;
|
||||
return Ok(());
|
||||
}
|
||||
Ok(has_validation) => break has_validation,
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
stream
|
||||
.send(Message::Text(
|
||||
serde_json::to_string(&RpcResponse::<GenericRpcMethod<String>>::from_result(Ok::<
|
||||
_,
|
||||
RpcError,
|
||||
>(
|
||||
serde_json::to_value(&dump).with_kind(crate::ErrorKind::Serialization)?,
|
||||
)))
|
||||
.with_kind(crate::ErrorKind::Serialization)?,
|
||||
))
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Network)?;
|
||||
};
|
||||
send_dump(has_valid_session, &mut stream, dump).await?;
|
||||
|
||||
deal_with_messages(has_valid_session, sub, stream).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn deal_with_messages(
|
||||
_has_valid_authentication: HasValidSession,
|
||||
mut sub: tokio::sync::broadcast::Receiver<Arc<Revision>>,
|
||||
mut stream: WebSocketStream<Upgraded>,
|
||||
) -> Result<(), Error> {
|
||||
loop {
|
||||
futures::select! {
|
||||
new_rev = sub.recv().fuse() => {
|
||||
@@ -149,6 +152,26 @@ async fn ws_handler<
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_dump(
|
||||
_has_valid_authentication: HasValidSession,
|
||||
stream: &mut WebSocketStream<Upgraded>,
|
||||
dump: Dump,
|
||||
) -> Result<(), Error> {
|
||||
stream
|
||||
.send(Message::Text(
|
||||
serde_json::to_string(&RpcResponse::<GenericRpcMethod<String>>::from_result(Ok::<
|
||||
_,
|
||||
RpcError,
|
||||
>(
|
||||
serde_json::to_value(&dump).with_kind(crate::ErrorKind::Serialization)?,
|
||||
)))
|
||||
.with_kind(crate::ErrorKind::Serialization)?,
|
||||
))
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Network)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn subscribe(ctx: RpcContext, req: Request<Body>) -> Result<Response<Body>, Error> {
|
||||
let (parts, body) = req.into_parts();
|
||||
let req = Request::from_parts(parts, body);
|
||||
|
||||
@@ -34,6 +34,7 @@ pub mod setup;
|
||||
pub mod shutdown;
|
||||
pub mod sound;
|
||||
pub mod ssh;
|
||||
pub mod static_server;
|
||||
pub mod status;
|
||||
pub mod system;
|
||||
pub mod update;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
use crate::context::RpcContext;
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
use basic_cookies::Cookie;
|
||||
use color_eyre::eyre::eyre;
|
||||
use digest::Digest;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::{FutureExt, TryFutureExt};
|
||||
use futures::FutureExt;
|
||||
use http::StatusCode;
|
||||
use rpc_toolkit::command_helpers::prelude::RequestParts;
|
||||
use rpc_toolkit::hyper::header::COOKIE;
|
||||
@@ -11,50 +14,133 @@ use rpc_toolkit::hyper::{Body, Request, Response};
|
||||
use rpc_toolkit::rpc_server_helpers::{noop3, to_response, DynMiddleware, DynMiddlewareStage2};
|
||||
use rpc_toolkit::yajrc::RpcMethod;
|
||||
use rpc_toolkit::Metadata;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
|
||||
use crate::context::RpcContext;
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
pub fn get_id(req: &RequestParts) -> Result<String, Error> {
|
||||
if let Some(cookie_header) = req.headers.get(COOKIE) {
|
||||
let cookies = Cookie::parse(
|
||||
cookie_header
|
||||
.to_str()
|
||||
.with_kind(crate::ErrorKind::Authorization)?,
|
||||
)
|
||||
.with_kind(crate::ErrorKind::Authorization)?;
|
||||
if let Some(session) = cookies.iter().find(|c| c.get_name() == "session") {
|
||||
return Ok(hash_token(session.get_value()));
|
||||
}
|
||||
}
|
||||
Err(Error::new(
|
||||
eyre!("UNAUTHORIZED"),
|
||||
crate::ErrorKind::Authorization,
|
||||
))
|
||||
pub trait AsLogoutSessionId {
|
||||
fn as_logout_session_id(self) -> String;
|
||||
}
|
||||
|
||||
pub fn hash_token(token: &str) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(token.as_bytes());
|
||||
base32::encode(
|
||||
base32::Alphabet::RFC4648 { padding: false },
|
||||
hasher.finalize().as_slice(),
|
||||
)
|
||||
.to_lowercase()
|
||||
}
|
||||
/// Will need to know when we have logged out from a route
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct HasLoggedOutSessions(());
|
||||
|
||||
pub async fn is_authed(ctx: &RpcContext, id: &str) -> Result<(), Error> {
|
||||
let session = sqlx::query!("UPDATE session SET last_active = CURRENT_TIMESTAMP WHERE id = ? AND logged_out IS NULL OR logged_out > CURRENT_TIMESTAMP", id)
|
||||
impl HasLoggedOutSessions {
|
||||
pub async fn new(
|
||||
logged_out_sessions: impl IntoIterator<Item = impl AsLogoutSessionId>,
|
||||
ctx: &RpcContext,
|
||||
) -> Result<Self, Error> {
|
||||
sqlx::query(&format!(
|
||||
"UPDATE session SET logged_out = CURRENT_TIMESTAMP WHERE id IN ('{}')",
|
||||
logged_out_sessions
|
||||
.into_iter()
|
||||
.by_ref()
|
||||
.map(|x| x.as_logout_session_id())
|
||||
.collect::<Vec<_>>()
|
||||
.join("','")
|
||||
))
|
||||
.execute(&mut ctx.secret_store.acquire().await?)
|
||||
.await?;
|
||||
if session.rows_affected() == 0 {
|
||||
return Err(Error::new(
|
||||
Ok(Self(()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Used when we need to know that we have logged in with a valid user
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct HasValidSession(());
|
||||
|
||||
impl HasValidSession {
|
||||
pub async fn from_request_parts(
|
||||
request_parts: &RequestParts,
|
||||
ctx: &RpcContext,
|
||||
) -> Result<Self, Error> {
|
||||
Self::from_session(&HashSessionToken::from_request_parts(request_parts)?, ctx).await
|
||||
}
|
||||
|
||||
pub async fn from_session(session: &HashSessionToken, ctx: &RpcContext) -> Result<Self, Error> {
|
||||
let session_hash = session.hashed();
|
||||
let session = sqlx::query!("UPDATE session SET last_active = CURRENT_TIMESTAMP WHERE id = ? AND logged_out IS NULL OR logged_out > CURRENT_TIMESTAMP", session_hash)
|
||||
.execute(&mut ctx.secret_store.acquire().await?)
|
||||
.await?;
|
||||
if session.rows_affected() == 0 {
|
||||
return Err(Error::new(
|
||||
eyre!("UNAUTHORIZED"),
|
||||
crate::ErrorKind::Authorization,
|
||||
));
|
||||
}
|
||||
Ok(Self(()))
|
||||
}
|
||||
}
|
||||
|
||||
/// When we have a need to create a new session,
|
||||
/// Or when we are using internal valid authenticated service.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HashSessionToken {
|
||||
hashed: String,
|
||||
token: String,
|
||||
}
|
||||
impl HashSessionToken {
|
||||
pub fn new() -> Self {
|
||||
let token = base32::encode(
|
||||
base32::Alphabet::RFC4648 { padding: false },
|
||||
&rand::random::<[u8; 16]>(),
|
||||
)
|
||||
.to_lowercase();
|
||||
let hashed = Self::hash(&token);
|
||||
Self { hashed, token }
|
||||
}
|
||||
pub fn from_cookie(cookie: &Cookie) -> Self {
|
||||
let token = cookie.get_value().to_owned();
|
||||
let hashed = Self::hash(&token);
|
||||
Self { hashed, token }
|
||||
}
|
||||
|
||||
pub fn from_request_parts(request_parts: &RequestParts) -> Result<Self, Error> {
|
||||
if let Some(cookie_header) = request_parts.headers.get(COOKIE) {
|
||||
let cookies = Cookie::parse(
|
||||
cookie_header
|
||||
.to_str()
|
||||
.with_kind(crate::ErrorKind::Authorization)?,
|
||||
)
|
||||
.with_kind(crate::ErrorKind::Authorization)?;
|
||||
if let Some(session) = cookies.iter().find(|c| c.get_name() == "session") {
|
||||
return Ok(Self::from_cookie(session));
|
||||
}
|
||||
}
|
||||
Err(Error::new(
|
||||
eyre!("UNAUTHORIZED"),
|
||||
crate::ErrorKind::Authorization,
|
||||
));
|
||||
))
|
||||
}
|
||||
|
||||
pub fn header_value(&self) -> Result<http::HeaderValue, Error> {
|
||||
http::HeaderValue::from_str(&format!(
|
||||
"session={}; Path=/; SameSite=Lax; Expires=Fri, 31 Dec 9999 23:59:59 GMT;",
|
||||
self.token
|
||||
))
|
||||
.with_kind(crate::ErrorKind::Unknown)
|
||||
}
|
||||
|
||||
pub fn hashed(&self) -> &str {
|
||||
self.hashed.as_str()
|
||||
}
|
||||
|
||||
pub fn as_hash(self) -> String {
|
||||
self.hashed
|
||||
}
|
||||
fn hash(token: &str) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(token.as_bytes());
|
||||
base32::encode(
|
||||
base32::Alphabet::RFC4648 { padding: false },
|
||||
hasher.finalize().as_slice(),
|
||||
)
|
||||
.to_lowercase()
|
||||
}
|
||||
}
|
||||
impl AsLogoutSessionId for HashSessionToken {
|
||||
fn as_logout_session_id(self) -> String {
|
||||
self.hashed
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn auth<M: Metadata>(ctx: RpcContext) -> DynMiddleware<M> {
|
||||
@@ -72,10 +158,7 @@ pub fn auth<M: Metadata>(ctx: RpcContext) -> DynMiddleware<M> {
|
||||
.get(rpc_req.method.as_str(), "authenticated")
|
||||
.unwrap_or(true)
|
||||
{
|
||||
if let Err(e) = async { get_id(req) }
|
||||
.and_then(|id| async move { is_authed(&ctx, &id).await })
|
||||
.await
|
||||
{
|
||||
if let Err(e) = HasValidSession::from_request_parts(req, &ctx).await {
|
||||
let (res_parts, _) = Response::new(()).into_parts();
|
||||
return Ok(Err(to_response(
|
||||
&req.headers,
|
||||
|
||||
@@ -25,6 +25,10 @@ server {{
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
}}
|
||||
|
||||
location /public/ {{
|
||||
proxy_pass http://127.0.0.1:5961/;
|
||||
}}
|
||||
|
||||
location /marketplace/ {{
|
||||
proxy_pass https://beta-registry-0-3.start9labs.com/; # TODO
|
||||
@@ -67,6 +71,10 @@ server {{
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
}}
|
||||
|
||||
location /public/ {{
|
||||
proxy_pass http://127.0.0.1:5961/;
|
||||
}}
|
||||
|
||||
location /marketplace/ {{
|
||||
proxy_pass https://beta-registry-0-3.start9labs.com/; # TODO
|
||||
|
||||
181
appmgr/src/static_server.rs
Normal file
181
appmgr/src/static_server.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
use std::fs::Metadata;
|
||||
use std::future::Future;
|
||||
use std::path::PathBuf;
|
||||
use std::time::UNIX_EPOCH;
|
||||
|
||||
use digest::Digest;
|
||||
use http::response::Builder;
|
||||
use hyper::service::{make_service_fn, service_fn};
|
||||
use hyper::{Body, Error as HyperError, Method, Request, Response, Server, StatusCode};
|
||||
use tokio::fs::File;
|
||||
use tokio_util::codec::{BytesCodec, FramedRead};
|
||||
|
||||
use crate::context::RpcContext;
|
||||
use crate::install::PKG_PUBLIC_DIR;
|
||||
use crate::middleware::auth::HasValidSession;
|
||||
use crate::{Error, ErrorKind, ResultExt};
|
||||
|
||||
static NOT_FOUND: &[u8] = b"Not Found";
|
||||
static NOT_AUTHORIZED: &[u8] = b"Not Authorized";
|
||||
|
||||
pub fn init(
|
||||
ctx: RpcContext,
|
||||
shutdown: impl Future<Output = ()> + Send + 'static,
|
||||
) -> impl Future<Output = Result<(), HyperError>> {
|
||||
let addr = ctx.bind_static;
|
||||
|
||||
let make_service = make_service_fn(move |_| {
|
||||
let ctx = ctx.clone();
|
||||
async move {
|
||||
Ok::<_, HyperError>(service_fn(move |req| {
|
||||
let ctx = ctx.clone();
|
||||
async move {
|
||||
match file_server_router(req, ctx).await {
|
||||
Ok(x) => Ok::<_, HyperError>(x),
|
||||
Err(err) => {
|
||||
tracing::error!("{:?}", err);
|
||||
Ok(server_error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
Server::bind(&addr)
|
||||
.serve(make_service)
|
||||
.with_graceful_shutdown(shutdown)
|
||||
}
|
||||
|
||||
async fn file_server_router(req: Request<Body>, ctx: RpcContext) -> Result<Response<Body>, Error> {
|
||||
let (request_parts, _body) = req.into_parts();
|
||||
let valid_session = HasValidSession::from_request_parts(&request_parts, &ctx).await;
|
||||
match (
|
||||
valid_session,
|
||||
request_parts.method,
|
||||
request_parts
|
||||
.uri
|
||||
.path()
|
||||
.strip_prefix("/")
|
||||
.unwrap_or(request_parts.uri.path())
|
||||
.split_once("/"),
|
||||
) {
|
||||
(Err(error), _, _) => {
|
||||
tracing::warn!("unauthorized for {} @{:?}", error, request_parts.uri.path());
|
||||
tracing::debug!("{:?}", error);
|
||||
return Ok(Response::builder()
|
||||
.status(StatusCode::UNAUTHORIZED)
|
||||
.body(NOT_AUTHORIZED.into())
|
||||
.unwrap());
|
||||
}
|
||||
(Ok(valid_session), Method::GET, Some(("package-data", path))) => {
|
||||
file_send(valid_session, &ctx, PathBuf::from(path)).await
|
||||
}
|
||||
_ => Ok(not_found()),
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP status code 404
|
||||
fn not_found() -> Response<Body> {
|
||||
Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(NOT_FOUND.into())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// HTTP status code 500
|
||||
fn server_error() -> Response<Body> {
|
||||
Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body("".into())
|
||||
.unwrap()
|
||||
}
|
||||
async fn file_send(
|
||||
_valid_session: HasValidSession,
|
||||
ctx: &RpcContext,
|
||||
filename: PathBuf,
|
||||
) -> Result<Response<Body>, Error> {
|
||||
// Serve a file by asynchronously reading it by chunks using tokio-util crate.
|
||||
let path = ctx.datadir.join(PKG_PUBLIC_DIR).join(filename);
|
||||
|
||||
if let Ok(file) = File::open(path.clone()).await {
|
||||
let metadata = file.metadata().await.with_kind(ErrorKind::Filesystem)?;
|
||||
let _is_non_empty = match IsNonEmptyFile::new(&metadata, &path) {
|
||||
Some(a) => a,
|
||||
None => return Ok(not_found()),
|
||||
};
|
||||
|
||||
let mut builder = Response::builder().status(StatusCode::OK);
|
||||
builder = with_e_tag(&path, &metadata, builder)?;
|
||||
builder = with_content_type(&path, builder);
|
||||
builder = with_content_length(&metadata, builder);
|
||||
let stream = FramedRead::new(file, BytesCodec::new());
|
||||
let body = Body::wrap_stream(stream);
|
||||
return Ok(builder.body(body).with_kind(ErrorKind::Network)?);
|
||||
}
|
||||
tracing::debug!("File not found: {:?}", path);
|
||||
|
||||
Ok(not_found())
|
||||
}
|
||||
|
||||
struct IsNonEmptyFile(());
|
||||
impl IsNonEmptyFile {
|
||||
fn new(metadata: &Metadata, path: &PathBuf) -> Option<Self> {
|
||||
let length = metadata.len();
|
||||
if !metadata.is_file() || length == 0 {
|
||||
tracing::debug!("File is empty: {:?}", path);
|
||||
return None;
|
||||
}
|
||||
Some(Self(()))
|
||||
}
|
||||
}
|
||||
|
||||
fn with_e_tag(path: &PathBuf, metadata: &Metadata, builder: Builder) -> Result<Builder, Error> {
|
||||
let modified = metadata.modified().with_kind(ErrorKind::Filesystem)?;
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(format!("{:?}", path).as_bytes());
|
||||
hasher.update(
|
||||
format!(
|
||||
"{}",
|
||||
modified
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
)
|
||||
.as_bytes(),
|
||||
);
|
||||
let res = hasher.finalize();
|
||||
Ok(builder.header(
|
||||
"ETag",
|
||||
base32::encode(base32::Alphabet::RFC4648 { padding: false }, res.as_slice()).to_lowercase(),
|
||||
))
|
||||
}
|
||||
///https://en.wikipedia.org/wiki/Media_type
|
||||
fn with_content_type(path: &PathBuf, builder: Builder) -> Builder {
|
||||
let content_type = match path.extension() {
|
||||
Some(os_str) => match os_str.to_str() {
|
||||
Some("apng") => "image/apng",
|
||||
Some("avif") => "image/avif",
|
||||
Some("flif") => "image/flif",
|
||||
Some("gif") => "image/gif",
|
||||
Some("jpg") | Some("jpeg") | Some("jfif") | Some("pjpeg") | Some("pjp") => "image/jpeg",
|
||||
Some("jxl") => "image/jxl",
|
||||
Some("png") => "image/png",
|
||||
Some("svg") => "image/svg+xml",
|
||||
Some("webp") => "image/webp",
|
||||
Some("mng") | Some("x-mng") => "image/x-mng",
|
||||
Some("css") => "text/css",
|
||||
Some("csv") => "text/csv",
|
||||
Some("html") => "text/html",
|
||||
Some("php") => "text/php",
|
||||
Some("plain") | Some("md") | Some("txt") => "text/plain",
|
||||
Some("xml") => "text/xml",
|
||||
None | Some(_) => "text/plain",
|
||||
},
|
||||
None => "text/plain",
|
||||
};
|
||||
builder.header("Content-Type", content_type)
|
||||
}
|
||||
fn with_content_length(metadata: &Metadata, builder: Builder) -> Builder {
|
||||
builder.header("Content-Length", metadata.len())
|
||||
}
|
||||
Reference in New Issue
Block a user