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::{
64 HEADER_COMPRESS, HEADER_FILE_META, HEADER_FINAL, HEADER_OFFSET, HEADER_SESSION,
65 HEADER_SESSION_STATUS,
66};
67
68pub struct ServerState {
70 pub storage: Arc<dyn StorageBackend>,
72 pub verifier: Arc<dyn TokenVerifier>,
74 pub validator: Arc<dyn Validator>,
76 pub compression: CompressionFormat,
78 pub max_upload_size: u64,
80}
81
82impl ServerState {
83 pub fn builder() -> ServerStateBuilder {
85 ServerStateBuilder::default()
86 }
87
88 pub fn authorize(
90 &self,
91 claims: &libfw_core::claims::TokenClaims,
92 path: &str,
93 action: libfw_core::auth::Action,
94 ) -> Result<(), AuthError> {
95 self.validator.validate(claims, path, action)
96 }
97}
98
99pub struct ServerStateBuilder {
101 storage: Option<Arc<dyn StorageBackend>>,
102 verifier: Option<Arc<dyn TokenVerifier>>,
103 validator: Option<Arc<dyn Validator>>,
104 compression: CompressionFormat,
105 max_upload_size: u64,
106}
107
108impl Default for ServerStateBuilder {
109 fn default() -> Self {
110 ServerStateBuilder {
111 storage: None,
112 verifier: None,
113 validator: None,
114 compression: CompressionFormat::Zrip,
115 max_upload_size: DEFAULT_MAX_UPLOAD_SIZE,
116 }
117 }
118}
119
120impl ServerStateBuilder {
121 pub fn storage(mut self, storage: impl StorageBackend) -> Self {
123 self.storage = Some(Arc::new(storage));
124 self
125 }
126
127 pub fn verifier(mut self, verifier: impl TokenVerifier) -> Self {
129 self.verifier = Some(Arc::new(verifier));
130 self
131 }
132
133 pub fn validator(mut self, validator: impl Validator) -> Self {
135 self.validator = Some(Arc::new(validator));
136 self
137 }
138
139 pub fn compression(mut self, format: CompressionFormat) -> Self {
141 self.compression = format;
142 self
143 }
144
145 pub fn max_upload_size(mut self, size: u64) -> Self {
147 self.max_upload_size = size;
148 self
149 }
150
151 pub fn build(self) -> ServerState {
153 ServerState {
154 storage: self.storage.expect("storage is required"),
155 verifier: self.verifier.expect("verifier is required"),
156 validator: self.validator.expect("validator is required"),
157 compression: self.compression,
158 max_upload_size: self.max_upload_size,
159 }
160 }
161}
162
163async fn validate_protocol(req: Request, next: Next) -> Response {
170 if let Some(value) = req
171 .headers()
172 .get(HEADER_PROTOCOL)
173 .and_then(|v| v.to_str().ok())
174 {
175 if !protocol_compatible(value) {
176 return (
177 StatusCode::UPGRADE_REQUIRED,
178 format!(
179 "unsupported protocol `{value}`; expected `{}`",
180 protocol_header_value()
181 ),
182 )
183 .into_response();
184 }
185 }
186 next.run(req).await
187}
188
189pub fn router(state: Arc<ServerState>) -> Router {
201 use axum::routing::{get, post};
202
203 Router::new()
204 .route("/file/{*path}", get(handlers::download).head(handlers::head_file))
205 .route("/file/{*path}", post(handlers::upload))
206 .route("/dir", get(handlers::list_dir_root))
207 .route("/dir/{*path}", get(handlers::list_dir))
208 .layer(axum::middleware::from_fn(validate_protocol))
209 .with_state(state)
210}
211
212pub fn validate_rel_path(path: &str) -> Result<String, &'static str> {
216 if path.contains('\0') {
217 return Err("path contains NUL byte");
218 }
219 if path.starts_with('/') {
220 return Err("path must be relative");
221 }
222 let mut out = String::with_capacity(path.len());
223 for segment in path.split('/') {
224 match segment {
225 "" | "." => {}
226 ".." => return Err("path escapes the mount root"),
227 seg => {
228 if !out.is_empty() {
229 out.push('/');
230 }
231 out.push_str(seg);
232 }
233 }
234 }
235 Ok(out)
236}