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::extract::Request;
55use axum::http::StatusCode;
56use axum::middleware::Next;
57use axum::response::{IntoResponse, Response};
58use axum::Router;
59use libfw_core::auth::{AuthError, TokenVerifier, Validator};
60use libfw_core::compress::CompressionFormat;
61use libfw_core::storage::StorageBackend;
62use libfw_core::{protocol_compatible, protocol_header_value, DEFAULT_MAX_UPLOAD_SIZE, HEADER_PROTOCOL};
63pub use libfw_core::{HEADER_COMPRESS, HEADER_FILE_META, HEADER_OFFSET};
64
65/// Immutable server configuration shared by all handlers.
66pub struct ServerState {
67    /// The storage backend serving file content.
68    pub storage: Arc<dyn StorageBackend>,
69    /// Turns bearer tokens into claims.
70    pub verifier: Arc<dyn TokenVerifier>,
71    /// Decides whether claims may access a path.
72    pub validator: Arc<dyn Validator>,
73    /// Compression applied to downloads when the client asks for it.
74    pub compression: CompressionFormat,
75    /// Upper bound for a single upload body.
76    pub max_upload_size: u64,
77}
78
79impl ServerState {
80    /// Start building a server state.
81    pub fn builder() -> ServerStateBuilder {
82        ServerStateBuilder::default()
83    }
84
85    /// Check whether `claims` may perform `action` on `path`.
86    pub fn authorize(
87        &self,
88        claims: &libfw_core::claims::TokenClaims,
89        path: &str,
90        action: libfw_core::auth::Action,
91    ) -> Result<(), AuthError> {
92        self.validator.validate(claims, path, action)
93    }
94}
95
96/// Builder for [`ServerState`].
97pub struct ServerStateBuilder {
98    storage: Option<Arc<dyn StorageBackend>>,
99    verifier: Option<Arc<dyn TokenVerifier>>,
100    validator: Option<Arc<dyn Validator>>,
101    compression: CompressionFormat,
102    max_upload_size: u64,
103}
104
105impl Default for ServerStateBuilder {
106    fn default() -> Self {
107        ServerStateBuilder {
108            storage: None,
109            verifier: None,
110            validator: None,
111            compression: CompressionFormat::Zrip,
112            max_upload_size: DEFAULT_MAX_UPLOAD_SIZE,
113        }
114    }
115}
116
117impl ServerStateBuilder {
118    /// Required: the storage backend.
119    pub fn storage(mut self, storage: impl StorageBackend) -> Self {
120        self.storage = Some(Arc::new(storage));
121        self
122    }
123
124    /// Required: the token verifier.
125    pub fn verifier(mut self, verifier: impl TokenVerifier) -> Self {
126        self.verifier = Some(Arc::new(verifier));
127        self
128    }
129
130    /// Required: the path/permission validator.
131    pub fn validator(mut self, validator: impl Validator) -> Self {
132        self.validator = Some(Arc::new(validator));
133        self
134    }
135
136    /// Compression for downloads (default: `Zrip`).
137    pub fn compression(mut self, format: CompressionFormat) -> Self {
138        self.compression = format;
139        self
140    }
141
142    /// Maximum upload size in bytes (default: 100 GiB).
143    pub fn max_upload_size(mut self, size: u64) -> Self {
144        self.max_upload_size = size;
145        self
146    }
147
148    /// Build the state, panicking if required fields are missing.
149    pub fn build(self) -> ServerState {
150        ServerState {
151            storage: self.storage.expect("storage is required"),
152            verifier: self.verifier.expect("verifier is required"),
153            validator: self.validator.expect("validator is required"),
154            compression: self.compression,
155            max_upload_size: self.max_upload_size,
156        }
157    }
158}
159
160/// Reject requests that explicitly advertise an incompatible protocol
161/// version with `426 Upgrade Required`.
162///
163/// Requests *without* the handshake header are allowed, so raw HTTP clients
164/// (curl, tests, older builds) keep working; the WASM/SDK client always
165/// sends the header so it is guaranteed to be matched with this server.
166async fn validate_protocol(req: Request, next: Next) -> Response {
167    if let Some(value) = req
168        .headers()
169        .get(HEADER_PROTOCOL)
170        .and_then(|v| v.to_str().ok())
171    {
172        if !protocol_compatible(value) {
173            return (
174                StatusCode::UPGRADE_REQUIRED,
175                format!(
176                    "unsupported protocol `{value}`; expected `{}`",
177                    protocol_header_value()
178                ),
179            )
180                .into_response();
181        }
182    }
183    next.run(req).await
184}
185
186/// Build the axum router with the libfw routes mounted.
187///
188/// Routes:
189/// - `GET  /file/{*path}` — download with Range / ETag / compression
190/// - `HEAD /file/{*path}` — metadata only
191/// - `POST /file/{*path}` — streaming upload (headers: `x-libfw-file-meta`,
192///   optional `x-libfw-offset`, optional `x-libfw-compress`)
193/// - `GET  /dir/{*path}`  — directory listing (JSON)
194///
195/// All routes first pass through [`validate_protocol`], which enforces the
196/// `x-libfw-protocol` handshake shared with the WASM client.
197pub fn router(state: Arc<ServerState>) -> Router {
198    use axum::routing::{get, post};
199
200    Router::new()
201        .route("/file/{*path}", get(handlers::download).head(handlers::head_file))
202        .route("/file/{*path}", post(handlers::upload))
203        .route("/dir", get(handlers::list_dir_root))
204        .route("/dir/{*path}", get(handlers::list_dir))
205        .layer(axum::middleware::from_fn(validate_protocol))
206        .with_state(state)
207}
208
209/// Normalize and validate a virtual path from the URL.
210///
211/// Rejects absolute paths, `..` segments, NUL bytes and empty segments.
212pub fn validate_rel_path(path: &str) -> Result<String, &'static str> {
213    if path.contains('\0') {
214        return Err("path contains NUL byte");
215    }
216    if path.starts_with('/') {
217        return Err("path must be relative");
218    }
219    let mut out = String::with_capacity(path.len());
220    for segment in path.split('/') {
221        match segment {
222            "" | "." => {}
223            ".." => return Err("path escapes the mount root"),
224            seg => {
225                if !out.is_empty() {
226                    out.push('/');
227                }
228                out.push_str(seg);
229            }
230        }
231    }
232    Ok(out)
233}