mod auth;
mod handlers;
mod http;
mod storage;
pub use auth::{AuthRejection, BearerClaims};
pub use http::{
content_range_none_value, content_range_value, etag_matches_if_none_match, if_range_matches,
parse_range_header, ParsedRange, RangeParseError,
};
pub use storage::{FsStorage, FsSink};
use std::sync::Arc;
use axum::extract::Request;
use axum::http::StatusCode;
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
use axum::Router;
use libfw_core::auth::{AuthError, TokenVerifier, Validator};
use libfw_core::capabilities::{Capabilities, Limits, ZripLevels};
use libfw_core::claims::TokenClaims;
use libfw_core::compress::CompressionFormat;
use libfw_core::pathmap::{IdentityPathCodec, PathCodec, PathCodecError};
use libfw_core::storage::StorageBackend;
pub use libfw_core::pathmap::MountPathCodec;
#[cfg(feature = "path-encrypt")]
pub use libfw_core::pathmap::EncryptedPathCodec;
use libfw_core::{protocol_compatible, protocol_header_value, DEFAULT_MAX_UPLOAD_SIZE, HEADER_PROTOCOL};
pub use libfw_core::{
HEADER_COMPRESS, HEADER_COMPRESS_LEVEL, HEADER_FILE_META, HEADER_FINAL, HEADER_OFFSET,
HEADER_SESSION, HEADER_SESSION_STATUS,
};
#[derive(Debug, thiserror::Error)]
pub enum PathResolveError {
#[error("invalid path: {0}")]
Invalid(&'static str),
#[error("cannot decode path: {0}")]
Codec(#[from] PathCodecError),
#[error(transparent)]
Auth(#[from] AuthError),
}
pub struct ServerState {
pub storage: Arc<dyn StorageBackend>,
pub verifier: Arc<dyn TokenVerifier>,
pub validator: Arc<dyn Validator>,
pub path_codec: Arc<dyn PathCodec>,
pub compression: CompressionFormat,
pub max_upload_size: u64,
pub limits: Option<Limits>,
pub zrip_levels: Option<ZripLevels>,
pub cleanup_interval: std::time::Duration,
pub session_ttl: std::time::Duration,
}
impl ServerState {
pub fn builder() -> ServerStateBuilder {
ServerStateBuilder::default()
}
pub fn authorize(
&self,
claims: &libfw_core::claims::TokenClaims,
path: &str,
action: libfw_core::auth::Action,
) -> Result<(), AuthError> {
self.validator.validate(claims, path, action)
}
}
impl ServerState {
pub fn resolve_client_path(
&self,
claims: &TokenClaims,
shadow: &str,
action: libfw_core::auth::Action,
) -> Result<String, PathResolveError> {
let shadow = validate_rel_path(shadow).map_err(PathResolveError::Invalid)?;
let real = if shadow.is_empty() {
shadow
} else {
match self.path_codec.decode(&shadow) {
Ok(real) => real,
Err(err) => match decode_compound(&*self.path_codec, &shadow) {
Some(real) => real,
None => return Err(PathResolveError::Codec(err)),
},
}
};
let real = validate_rel_path(&real).map_err(PathResolveError::Invalid)?;
self.authorize(claims, &real, action)?;
Ok(real)
}
pub fn expose_path(&self, real: &str) -> String {
self.path_codec.encode(real)
}
}
fn decode_compound(codec: &dyn PathCodec, shadow: &str) -> Option<String> {
let mut end = shadow.len();
while let Some(slash) = shadow[..end].rfind('/') {
end = slash;
let prefix = &shadow[..slash];
if let Ok(real) = codec.decode(prefix) {
let rest = &shadow[slash + 1..];
return Some(if real.is_empty() {
rest.to_string()
} else {
format!("{real}/{rest}")
});
}
}
None
}
pub struct ServerStateBuilder {
storage: Option<Arc<dyn StorageBackend>>,
verifier: Option<Arc<dyn TokenVerifier>>,
validator: Option<Arc<dyn Validator>>,
path_codec: Option<Arc<dyn PathCodec>>,
compression: CompressionFormat,
max_upload_size: u64,
limits: Option<Limits>,
zrip_levels: Option<ZripLevels>,
cleanup_interval: std::time::Duration,
session_ttl: std::time::Duration,
}
impl Default for ServerStateBuilder {
fn default() -> Self {
ServerStateBuilder {
storage: None,
verifier: None,
validator: None,
path_codec: None,
compression: CompressionFormat::Zrip,
max_upload_size: DEFAULT_MAX_UPLOAD_SIZE,
limits: None,
zrip_levels: None,
cleanup_interval: std::time::Duration::from_secs(3600),
session_ttl: libfw_core::DEFAULT_SESSION_TTL,
}
}
}
impl ServerStateBuilder {
pub fn storage(mut self, storage: impl StorageBackend) -> Self {
self.storage = Some(Arc::new(storage));
self
}
pub fn verifier(mut self, verifier: impl TokenVerifier) -> Self {
self.verifier = Some(Arc::new(verifier));
self
}
pub fn validator(mut self, validator: impl Validator) -> Self {
self.validator = Some(Arc::new(validator));
self
}
pub fn path_codec(mut self, codec: impl PathCodec) -> Self {
self.path_codec = Some(Arc::new(codec));
self
}
pub fn compression(mut self, format: CompressionFormat) -> Self {
self.compression = format;
self
}
pub fn max_upload_size(mut self, size: u64) -> Self {
self.max_upload_size = size;
self
}
pub fn limits(mut self, limits: Limits) -> Self {
self.limits = Some(limits);
self
}
pub fn zrip_levels(mut self, levels: ZripLevels) -> Self {
self.zrip_levels = Some(levels);
self
}
pub fn cleanup_interval(mut self, interval: std::time::Duration) -> Self {
self.cleanup_interval = interval;
self
}
pub fn session_ttl(mut self, ttl: std::time::Duration) -> Self {
self.session_ttl = ttl;
self
}
pub fn build(self) -> ServerState {
ServerState {
storage: self.storage.expect("storage is required"),
verifier: self.verifier.expect("verifier is required"),
validator: self.validator.expect("validator is required"),
path_codec: self.path_codec.unwrap_or_else(|| Arc::new(IdentityPathCodec)),
compression: self.compression,
max_upload_size: self.max_upload_size,
limits: self.limits,
zrip_levels: self.zrip_levels,
cleanup_interval: self.cleanup_interval,
session_ttl: self.session_ttl,
}
}
}
impl ServerState {
pub fn capabilities(&self) -> Capabilities {
let mut caps = Capabilities::default();
if let Some(limits) = &self.limits {
caps.limits = limits.clone();
}
if let Some(levels) = &self.zrip_levels {
caps.compression.zrip_levels = *levels;
}
caps.compression.formats = match self.compression {
CompressionFormat::Zrip => {
vec![CompressionFormat::None, CompressionFormat::Zrip]
}
CompressionFormat::None => vec![CompressionFormat::None],
};
caps
}
pub fn spawn_stale_session_cleanup(
self: &Arc<Self>,
) -> tokio::task::JoinHandle<()> {
let interval = self.cleanup_interval;
if interval.is_zero() {
return tokio::spawn(async {});
}
let storage = self.storage.clone();
let ttl = self.session_ttl;
let mut tick = tokio::time::interval(interval);
tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
tokio::spawn(async move {
loop {
tick.tick().await;
match storage.cleanup_stale_sessions(ttl).await {
Ok(n) if n > 0 => {
tracing::info!("cleaned {n} stale upload session temp(s)")
}
_ => {}
}
}
})
}
}
async fn validate_protocol(req: Request, next: Next) -> Response {
if let Some(value) = req
.headers()
.get(HEADER_PROTOCOL)
.and_then(|v| v.to_str().ok())
{
if !protocol_compatible(value) {
return (
StatusCode::UPGRADE_REQUIRED,
format!(
"unsupported protocol `{value}`; expected `{}`",
protocol_header_value()
),
)
.into_response();
}
}
next.run(req).await
}
pub fn router(state: Arc<ServerState>) -> Router {
use axum::routing::{get, post};
Router::new()
.route("/file/{*path}", get(handlers::download).head(handlers::head_file))
.route("/file/{*path}", post(handlers::upload))
.route("/dir", get(handlers::list_dir_root))
.route("/dir/{*path}", get(handlers::list_dir))
.route("/capabilities", get(handlers::capabilities))
.layer(axum::middleware::from_fn(validate_protocol))
.with_state(state)
}
pub fn validate_rel_path(path: &str) -> Result<String, &'static str> {
if path.contains('\0') {
return Err("path contains NUL byte");
}
if path.starts_with('/') {
return Err("path must be relative");
}
let mut out = String::with_capacity(path.len());
for segment in path.split('/') {
match segment {
"" | "." => {}
".." => return Err("path escapes the mount root"),
seg => {
if !out.is_empty() {
out.push('/');
}
out.push_str(seg);
}
}
}
Ok(out)
}
#[cfg(test)]
mod path_tests {
use super::decode_compound;
use libfw_core::pathmap::{PathCodec, PathCodecError};
struct DocsOnly;
impl PathCodec for DocsOnly {
fn encode(&self, real: &str) -> String {
real.to_string()
}
fn decode(&self, shadow: &str) -> Result<String, PathCodecError> {
if shadow == "docs" {
Ok("real/docs".to_string())
} else {
Err(PathCodecError::Unmapped(shadow.to_string()))
}
}
}
struct Never;
impl PathCodec for Never {
fn encode(&self, real: &str) -> String {
real.to_string()
}
fn decode(&self, _shadow: &str) -> Result<String, PathCodecError> {
Err(PathCodecError::InvalidShadow("nope".to_string()))
}
}
#[test]
fn compound_decodes_dir_shadow_plus_literal_suffix() {
let real = decode_compound(&DocsOnly, "docs/sub/deep/file.txt").unwrap();
assert_eq!(real, "real/docs/sub/deep/file.txt");
}
#[test]
fn compound_prefers_longest_decodable_prefix() {
struct TwoLevel;
impl PathCodec for TwoLevel {
fn encode(&self, real: &str) -> String {
real.to_string()
}
fn decode(&self, shadow: &str) -> Result<String, PathCodecError> {
match shadow {
"docs" => Ok("real/docs".to_string()),
"docs/sub" => Ok("real/docs/sub".to_string()),
_ => Err(PathCodecError::Unmapped(shadow.to_string())),
}
}
}
let real = decode_compound(&TwoLevel, "docs/sub/deep/file.txt").unwrap();
assert_eq!(real, "real/docs/sub/deep/file.txt");
}
#[test]
fn compound_is_none_when_no_prefix_decodes() {
assert!(decode_compound(&Never, "abc").is_none());
assert!(decode_compound(&Never, "a/b/c").is_none());
assert!(decode_compound(&DocsOnly, "docs").is_none());
}
#[test]
fn compound_decodes_identity_prefix_to_literal_rest() {
struct RootCodec;
impl PathCodec for RootCodec {
fn encode(&self, real: &str) -> String {
real.to_string()
}
fn decode(&self, shadow: &str) -> Result<String, PathCodecError> {
if shadow == "mnt" {
Ok(String::new())
} else {
Err(PathCodecError::Unmapped(shadow.to_string()))
}
}
}
let real = decode_compound(&RootCodec, "mnt/file.txt").unwrap();
assert_eq!(real, "file.txt");
}
}