1mod 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_FINAL, HEADER_OFFSET};
64
65pub struct ServerState {
67 pub storage: Arc<dyn StorageBackend>,
69 pub verifier: Arc<dyn TokenVerifier>,
71 pub validator: Arc<dyn Validator>,
73 pub compression: CompressionFormat,
75 pub max_upload_size: u64,
77}
78
79impl ServerState {
80 pub fn builder() -> ServerStateBuilder {
82 ServerStateBuilder::default()
83 }
84
85 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
96pub 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 pub fn storage(mut self, storage: impl StorageBackend) -> Self {
120 self.storage = Some(Arc::new(storage));
121 self
122 }
123
124 pub fn verifier(mut self, verifier: impl TokenVerifier) -> Self {
126 self.verifier = Some(Arc::new(verifier));
127 self
128 }
129
130 pub fn validator(mut self, validator: impl Validator) -> Self {
132 self.validator = Some(Arc::new(validator));
133 self
134 }
135
136 pub fn compression(mut self, format: CompressionFormat) -> Self {
138 self.compression = format;
139 self
140 }
141
142 pub fn max_upload_size(mut self, size: u64) -> Self {
144 self.max_upload_size = size;
145 self
146 }
147
148 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
160async 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
186pub 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
209pub 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}