Skip to main content

libfw_server/
lib.rs

1//! Embeddable `libfw` server: axum routing, bearer-token authorization,
2//! HTTP range handling and streaming upload/download.
3//!
4//! # Example
5//!
6//! ```no_run
7//! use std::sync::Arc;
8//! use axum::Router;
9//! use libfw_core::auth::{Action, PathValidator, TokenVerifier, Validator};
10//! use libfw_core::claims::{Permission, TokenClaims};
11//! use libfw_server::{router, ServerState};
12//!
13//! // 1. Token verifier: parse & verify bearer tokens into claims.
14//! #[derive(Clone)]
15//! struct MyVerifier;
16//! impl TokenVerifier for MyVerifier {
17//!     fn verify(&self, token: &str) -> Result<TokenClaims, libfw_core::auth::AuthError> {
18//!         Ok(TokenClaims {
19//!             sub: token.to_string(),
20//!             exp: None,
21//!             permissions: vec![Permission::Read, Permission::Write],
22//!             allowed_paths: vec!["/".to_string()],
23//!         })
24//!     }
25//! }
26//!
27//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
28//! let state = Arc::new(ServerState::builder()
29//!     .storage(libfw_server::FsStorage::new("/srv/files"))
30//!     .verifier(MyVerifier)
31//!     .validator(PathValidator::new())
32//!     .build());
33//!
34//! let app: Router = router(state);
35//! // ... serve `app` with your preferred hyper/tokio setup
36//! # Ok(())
37//! # }
38//! ```
39
40mod auth;
41mod handlers;
42mod http;
43mod storage;
44
45pub use auth::{AuthRejection, BearerClaims};
46pub use http::{
47    content_range_none_value, content_range_value, etag_matches_if_none_match, if_range_matches,
48    parse_range_header, ParsedRange, RangeParseError,
49};
50pub use storage::{FsStorage, FsSink};
51
52use std::sync::Arc;
53
54use axum::Router;
55use libfw_core::auth::{AuthError, TokenVerifier, Validator};
56use libfw_core::compress::CompressionFormat;
57use libfw_core::storage::StorageBackend;
58use libfw_core::DEFAULT_MAX_UPLOAD_SIZE;
59pub use libfw_core::{HEADER_COMPRESS, HEADER_FILE_META, HEADER_OFFSET};
60
61/// Immutable server configuration shared by all handlers.
62pub struct ServerState {
63    /// The storage backend serving file content.
64    pub storage: Arc<dyn StorageBackend>,
65    /// Turns bearer tokens into claims.
66    pub verifier: Arc<dyn TokenVerifier>,
67    /// Decides whether claims may access a path.
68    pub validator: Arc<dyn Validator>,
69    /// Compression applied to downloads when the client asks for it.
70    pub compression: CompressionFormat,
71    /// Upper bound for a single upload body.
72    pub max_upload_size: u64,
73}
74
75impl ServerState {
76    /// Start building a server state.
77    pub fn builder() -> ServerStateBuilder {
78        ServerStateBuilder::default()
79    }
80
81    /// Check whether `claims` may perform `action` on `path`.
82    pub fn authorize(
83        &self,
84        claims: &libfw_core::claims::TokenClaims,
85        path: &str,
86        action: libfw_core::auth::Action,
87    ) -> Result<(), AuthError> {
88        self.validator.validate(claims, path, action)
89    }
90}
91
92/// Builder for [`ServerState`].
93pub struct ServerStateBuilder {
94    storage: Option<Arc<dyn StorageBackend>>,
95    verifier: Option<Arc<dyn TokenVerifier>>,
96    validator: Option<Arc<dyn Validator>>,
97    compression: CompressionFormat,
98    max_upload_size: u64,
99}
100
101impl Default for ServerStateBuilder {
102    fn default() -> Self {
103        ServerStateBuilder {
104            storage: None,
105            verifier: None,
106            validator: None,
107            compression: CompressionFormat::Zrip,
108            max_upload_size: DEFAULT_MAX_UPLOAD_SIZE,
109        }
110    }
111}
112
113impl ServerStateBuilder {
114    /// Required: the storage backend.
115    pub fn storage(mut self, storage: impl StorageBackend) -> Self {
116        self.storage = Some(Arc::new(storage));
117        self
118    }
119
120    /// Required: the token verifier.
121    pub fn verifier(mut self, verifier: impl TokenVerifier) -> Self {
122        self.verifier = Some(Arc::new(verifier));
123        self
124    }
125
126    /// Required: the path/permission validator.
127    pub fn validator(mut self, validator: impl Validator) -> Self {
128        self.validator = Some(Arc::new(validator));
129        self
130    }
131
132    /// Compression for downloads (default: `Zrip`).
133    pub fn compression(mut self, format: CompressionFormat) -> Self {
134        self.compression = format;
135        self
136    }
137
138    /// Maximum upload size in bytes (default: 100 GiB).
139    pub fn max_upload_size(mut self, size: u64) -> Self {
140        self.max_upload_size = size;
141        self
142    }
143
144    /// Build the state, panicking if required fields are missing.
145    pub fn build(self) -> ServerState {
146        ServerState {
147            storage: self.storage.expect("storage is required"),
148            verifier: self.verifier.expect("verifier is required"),
149            validator: self.validator.expect("validator is required"),
150            compression: self.compression,
151            max_upload_size: self.max_upload_size,
152        }
153    }
154}
155
156/// Build the axum router with the libfw routes mounted.
157///
158/// Routes:
159/// - `GET  /file/{*path}` — download with Range / ETag / compression
160/// - `HEAD /file/{*path}` — metadata only
161/// - `POST /file/{*path}` — streaming upload (headers: `x-libfw-file-meta`,
162///   optional `x-libfw-offset`, optional `x-libfw-compress`)
163/// - `GET  /dir/{*path}`  — directory listing (JSON)
164pub fn router(state: Arc<ServerState>) -> Router {
165    use axum::routing::{get, post};
166
167    Router::new()
168        .route("/file/{*path}", get(handlers::download).head(handlers::head_file))
169        .route("/file/{*path}", post(handlers::upload))
170        .route("/dir/{*path}", get(handlers::list_dir))
171        .with_state(state)
172}
173
174/// Normalize and validate a virtual path from the URL.
175///
176/// Rejects absolute paths, `..` segments, NUL bytes and empty segments.
177pub fn validate_rel_path(path: &str) -> Result<String, &'static str> {
178    if path.contains('\0') {
179        return Err("path contains NUL byte");
180    }
181    if path.starts_with('/') {
182        return Err("path must be relative");
183    }
184    let mut out = String::with_capacity(path.len());
185    for segment in path.split('/') {
186        match segment {
187            "" | "." => {}
188            ".." => return Err("path escapes the mount root"),
189            seg => {
190                if !out.is_empty() {
191                    out.push('/');
192                }
193                out.push_str(seg);
194            }
195        }
196    }
197    Ok(out)
198}