1use crate::auth::{AuthError, AuthState, Claims, Permission};
11use axum::{
12 body::Body,
13 extract::{Request, State},
14 http::{header, HeaderMap, HeaderValue, Method, StatusCode},
15 middleware::Next,
16 response::{IntoResponse, Response},
17};
18use std::collections::HashSet;
19use std::sync::Arc;
20use std::time::{Duration, Instant};
21use tokio::sync::Mutex;
22use uuid::Uuid;
23
24#[derive(Debug, Clone)]
30pub struct CorsConfig {
31 pub allowed_origins: HashSet<String>,
33 pub allowed_methods: HashSet<Method>,
35 pub allowed_headers: HashSet<String>,
37 pub exposed_headers: HashSet<String>,
39 pub allow_credentials: bool,
41 pub max_age: u64,
43}
44
45impl Default for CorsConfig {
46 fn default() -> Self {
47 let mut methods = HashSet::new();
48 methods.insert(Method::GET);
49 methods.insert(Method::POST);
50 methods.insert(Method::PUT);
51 methods.insert(Method::DELETE);
52 methods.insert(Method::OPTIONS);
53 methods.insert(Method::HEAD);
54
55 let mut headers = HashSet::new();
56 headers.insert("content-type".to_string());
57 headers.insert("authorization".to_string());
58 headers.insert("accept".to_string());
59 headers.insert("origin".to_string());
60 headers.insert("x-requested-with".to_string());
61
62 Self {
63 allowed_origins: HashSet::new(), allowed_methods: methods,
65 allowed_headers: headers,
66 exposed_headers: HashSet::new(),
67 allow_credentials: false,
68 max_age: 86400, }
70 }
71}
72
73impl CorsConfig {
74 pub fn permissive() -> Self {
76 let mut config = Self::default();
77 config.allowed_origins.insert("*".to_string());
78 config
79 }
80
81 pub fn allow_origin(mut self, origin: impl Into<String>) -> Self {
83 self.allowed_origins.insert(origin.into());
84 self
85 }
86
87 pub fn allow_credentials(mut self, allow: bool) -> Self {
89 self.allow_credentials = allow;
90 self
91 }
92
93 fn is_origin_allowed(&self, origin: &str) -> bool {
95 if self.allowed_origins.is_empty() || self.allowed_origins.contains("*") {
96 true
97 } else {
98 self.allowed_origins.contains(origin)
99 }
100 }
101
102 fn methods_string(&self) -> String {
104 self.allowed_methods
105 .iter()
106 .map(|m| m.as_str())
107 .collect::<Vec<_>>()
108 .join(", ")
109 }
110
111 fn headers_string(&self) -> String {
113 self.allowed_headers
114 .iter()
115 .cloned()
116 .collect::<Vec<_>>()
117 .join(", ")
118 }
119}
120
121#[derive(Clone)]
123pub struct CorsState {
124 pub config: CorsConfig,
125}
126
127pub async fn cors_middleware(
131 State(cors_state): State<CorsState>,
132 req: Request,
133 next: Next,
134) -> Response {
135 let origin = req
136 .headers()
137 .get(header::ORIGIN)
138 .and_then(|h| h.to_str().ok())
139 .map(|s| s.to_string());
140
141 if req.method() == Method::OPTIONS {
143 return build_preflight_response(&cors_state.config, origin.as_deref());
144 }
145
146 let mut response = next.run(req).await;
148
149 add_cors_headers(
151 response.headers_mut(),
152 &cors_state.config,
153 origin.as_deref(),
154 );
155
156 response
157}
158
159fn build_preflight_response(config: &CorsConfig, origin: Option<&str>) -> Response {
161 let mut response = Response::builder()
162 .status(StatusCode::NO_CONTENT)
163 .body(Body::empty())
164 .expect("building NO_CONTENT response with empty body is infallible");
165
166 add_cors_headers(response.headers_mut(), config, origin);
167
168 if let Ok(value) = HeaderValue::from_str(&config.methods_string()) {
170 response
171 .headers_mut()
172 .insert(header::ACCESS_CONTROL_ALLOW_METHODS, value);
173 }
174 if let Ok(value) = HeaderValue::from_str(&config.headers_string()) {
175 response
176 .headers_mut()
177 .insert(header::ACCESS_CONTROL_ALLOW_HEADERS, value);
178 }
179 if let Ok(value) = HeaderValue::from_str(&config.max_age.to_string()) {
180 response
181 .headers_mut()
182 .insert(header::ACCESS_CONTROL_MAX_AGE, value);
183 }
184
185 response
186}
187
188fn add_cors_headers(headers: &mut HeaderMap, config: &CorsConfig, origin: Option<&str>) {
190 let origin_value = if let Some(origin) = origin {
192 if config.is_origin_allowed(origin) {
193 if config.allowed_origins.contains("*") && !config.allow_credentials {
194 "*"
195 } else {
196 origin
197 }
198 } else {
199 return; }
201 } else if config.allowed_origins.contains("*") {
202 "*"
203 } else {
204 return;
205 };
206
207 if let Ok(value) = HeaderValue::from_str(origin_value) {
208 headers.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, value);
209 }
210
211 if config.allow_credentials {
213 headers.insert(
214 header::ACCESS_CONTROL_ALLOW_CREDENTIALS,
215 HeaderValue::from_static("true"),
216 );
217 }
218
219 if !config.exposed_headers.is_empty() {
221 let exposed = config
222 .exposed_headers
223 .iter()
224 .cloned()
225 .collect::<Vec<_>>()
226 .join(", ");
227 if let Ok(value) = HeaderValue::from_str(&exposed) {
228 headers.insert(header::ACCESS_CONTROL_EXPOSE_HEADERS, value);
229 }
230 }
231}
232
233#[derive(Debug, Clone)]
239pub struct RateLimitConfig {
240 pub max_requests: u32,
242 pub window: Duration,
244 pub burst_capacity: u32,
246}
247
248impl Default for RateLimitConfig {
249 fn default() -> Self {
250 Self {
251 max_requests: 100,
252 window: Duration::from_secs(60),
253 burst_capacity: 10,
254 }
255 }
256}
257
258impl RateLimitConfig {
259 pub fn validate(&self) -> Result<(), String> {
261 if self.max_requests == 0 {
262 return Err("Maximum requests must be greater than 0".to_string());
263 }
264
265 if self.window.as_secs() == 0 {
266 return Err("Time window must be greater than 0".to_string());
267 }
268
269 if self.burst_capacity == 0 {
270 return Err("Burst capacity must be greater than 0".to_string());
271 }
272
273 if self.burst_capacity > self.max_requests {
274 return Err(format!(
275 "Burst capacity ({}) cannot exceed max requests ({})",
276 self.burst_capacity, self.max_requests
277 ));
278 }
279
280 Ok(())
281 }
282}
283
284#[derive(Debug)]
286struct TokenBucket {
287 tokens: f64,
288 last_update: Instant,
289 capacity: f64,
290 refill_rate: f64, }
292
293impl TokenBucket {
294 fn new(capacity: u32, refill_rate: f64) -> Self {
295 Self {
296 tokens: capacity as f64,
297 last_update: Instant::now(),
298 capacity: capacity as f64,
299 refill_rate,
300 }
301 }
302
303 fn try_acquire(&mut self) -> bool {
304 self.refill();
305 if self.tokens >= 1.0 {
306 self.tokens -= 1.0;
307 true
308 } else {
309 false
310 }
311 }
312
313 fn refill(&mut self) {
314 let now = Instant::now();
315 let elapsed = now.duration_since(self.last_update).as_secs_f64();
316 self.tokens = (self.tokens + elapsed * self.refill_rate).min(self.capacity);
317 self.last_update = now;
318 }
319
320 fn tokens_remaining(&self) -> u32 {
321 self.tokens as u32
322 }
323}
324
325#[derive(Clone)]
327pub struct RateLimitState {
328 config: RateLimitConfig,
329 buckets: Arc<Mutex<std::collections::HashMap<String, TokenBucket>>>,
330}
331
332impl RateLimitState {
333 pub fn new(config: RateLimitConfig) -> Self {
335 Self {
336 config,
337 buckets: Arc::new(Mutex::new(std::collections::HashMap::new())),
338 }
339 }
340
341 async fn get_bucket(&self, ip: &str) -> (bool, u32) {
343 let mut buckets = self.buckets.lock().await;
344
345 let refill_rate = self.config.max_requests as f64 / self.config.window.as_secs_f64();
346
347 let bucket = buckets
348 .entry(ip.to_string())
349 .or_insert_with(|| TokenBucket::new(self.config.burst_capacity, refill_rate));
350
351 let allowed = bucket.try_acquire();
352 let remaining = bucket.tokens_remaining();
353
354 (allowed, remaining)
355 }
356}
357
358pub async fn rate_limit_middleware(
362 State(rate_state): State<RateLimitState>,
363 req: Request,
364 next: Next,
365) -> Result<Response, RateLimitError> {
366 let ip = extract_client_ip(&req);
368
369 let (allowed, remaining) = rate_state.get_bucket(&ip).await;
370
371 if !allowed {
372 return Err(RateLimitError::TooManyRequests);
373 }
374
375 let mut response = next.run(req).await;
376
377 let headers = response.headers_mut();
379 if let Ok(value) = HeaderValue::from_str(&rate_state.config.max_requests.to_string()) {
380 headers.insert("X-RateLimit-Limit", value);
381 }
382 if let Ok(value) = HeaderValue::from_str(&remaining.to_string()) {
383 headers.insert("X-RateLimit-Remaining", value);
384 }
385
386 Ok(response)
387}
388
389fn extract_client_ip(req: &Request) -> String {
391 if let Some(forwarded) = req.headers().get("x-forwarded-for") {
393 if let Ok(s) = forwarded.to_str() {
394 if let Some(ip) = s.split(',').next() {
395 return ip.trim().to_string();
396 }
397 }
398 }
399
400 if let Some(real_ip) = req.headers().get("x-real-ip") {
402 if let Ok(s) = real_ip.to_str() {
403 return s.to_string();
404 }
405 }
406
407 "unknown".to_string()
409}
410
411#[derive(Debug)]
413pub enum RateLimitError {
414 TooManyRequests,
415}
416
417impl IntoResponse for RateLimitError {
418 fn into_response(self) -> Response {
419 let (status, message) = match self {
420 RateLimitError::TooManyRequests => (
421 StatusCode::TOO_MANY_REQUESTS,
422 "Rate limit exceeded. Please retry later.",
423 ),
424 };
425
426 let mut response = (status, message).into_response();
427
428 response
430 .headers_mut()
431 .insert(header::RETRY_AFTER, HeaderValue::from_static("60"));
432
433 response
434 }
435}
436
437#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
443pub enum CompressionLevel {
444 Fastest,
446 #[default]
448 Balanced,
449 Best,
451 Custom(u32),
453}
454
455impl CompressionLevel {
456 pub fn to_level(self) -> u32 {
458 match self {
459 CompressionLevel::Fastest => 1,
460 CompressionLevel::Balanced => 5,
461 CompressionLevel::Best => 9,
462 CompressionLevel::Custom(level) => level.min(9),
463 }
464 }
465}
466
467#[derive(Debug, Clone)]
469pub struct CompressionConfig {
470 pub enable_gzip: bool,
472 pub level: CompressionLevel,
474 pub min_size: usize,
476}
477
478impl Default for CompressionConfig {
479 fn default() -> Self {
480 Self {
481 enable_gzip: true,
482 level: CompressionLevel::Balanced,
483 min_size: 1024, }
485 }
486}
487
488impl CompressionConfig {
489 pub fn fast() -> Self {
491 Self {
492 level: CompressionLevel::Fastest,
493 ..Default::default()
494 }
495 }
496
497 pub fn best() -> Self {
499 Self {
500 level: CompressionLevel::Best,
501 ..Default::default()
502 }
503 }
504
505 pub fn with_level(mut self, level: CompressionLevel) -> Self {
507 self.level = level;
508 self
509 }
510
511 pub fn with_min_size(mut self, min_size: usize) -> Self {
513 self.min_size = min_size;
514 self
515 }
516
517 pub fn with_gzip(mut self, gzip: bool) -> Self {
519 self.enable_gzip = gzip;
520 self
521 }
522
523 pub fn validate(&self) -> Result<(), String> {
525 if !self.enable_gzip {
527 return Err("At least one compression algorithm must be enabled".to_string());
528 }
529
530 if self.min_size > 100 * 1024 * 1024 {
532 return Err(format!(
533 "Minimum compression size {} is too large (max: 100MB)",
534 self.min_size
535 ));
536 }
537
538 Ok(())
539 }
540}
541
542#[derive(Debug, Clone)]
548pub struct CacheConfig {
549 pub default_max_age: u64,
551 pub public: bool,
553 pub immutable_cids: bool,
555}
556
557impl Default for CacheConfig {
558 fn default() -> Self {
559 Self {
560 default_max_age: 3600, public: true,
562 immutable_cids: true, }
564 }
565}
566
567impl CacheConfig {
568 pub fn validate(&self) -> Result<(), String> {
570 const MAX_AGE_LIMIT: u64 = 365 * 24 * 3600; if self.default_max_age > MAX_AGE_LIMIT {
574 return Err(format!(
575 "Max age {} exceeds maximum {} (1 year)",
576 self.default_max_age, MAX_AGE_LIMIT
577 ));
578 }
579
580 Ok(())
581 }
582}
583
584pub fn add_caching_headers(headers: &mut HeaderMap, cid: &str, config: &CacheConfig) {
586 if let Ok(etag) = HeaderValue::from_str(&format!("\"{}\"", cid)) {
588 headers.insert(header::ETAG, etag);
589 }
590
591 let mut cache_control = String::new();
593 if config.public {
594 cache_control.push_str("public, ");
595 } else {
596 cache_control.push_str("private, ");
597 }
598 cache_control.push_str(&format!("max-age={}", config.default_max_age));
599
600 if config.immutable_cids {
602 cache_control.push_str(", immutable");
603 }
604
605 if let Ok(value) = HeaderValue::from_str(&cache_control) {
606 headers.insert(header::CACHE_CONTROL, value);
607 }
608}
609
610pub fn check_etag_match(headers: &HeaderMap, cid: &str) -> bool {
612 if let Some(if_none_match) = headers.get(header::IF_NONE_MATCH) {
613 if let Ok(value) = if_none_match.to_str() {
614 let etag = value.trim().trim_matches('"');
616 return etag == cid;
617 }
618 }
619 false
620}
621
622pub fn not_modified_response(cid: &str, config: &CacheConfig) -> Response {
624 let mut response = Response::builder()
625 .status(StatusCode::NOT_MODIFIED)
626 .body(Body::empty())
627 .expect("building NOT_MODIFIED response with empty body is infallible");
628
629 add_caching_headers(response.headers_mut(), cid, config);
630
631 response
632}
633
634#[derive(Debug, Clone)]
636pub struct AuthUser {
637 pub user_id: Uuid,
638 pub username: String,
639 pub claims: Option<Claims>,
640}
641
642fn authenticate_user(req: &Request, auth_state: &AuthState) -> Result<AuthUser, AuthError> {
646 let auth_header = req
647 .headers()
648 .get(header::AUTHORIZATION)
649 .and_then(|h| h.to_str().ok())
650 .ok_or(AuthError::InvalidToken(
651 "Missing Authorization header".to_string(),
652 ))?;
653
654 if let Some(token) = auth_header.strip_prefix("Bearer ") {
656 let claims = auth_state.jwt_manager.validate_token(token)?;
657 let user = auth_state.user_store.get_user(&claims.username)?;
658
659 return Ok(AuthUser {
660 user_id: user.id,
661 username: user.username,
662 claims: Some(claims),
663 });
664 }
665
666 if auth_header.starts_with("ipfrs_") {
668 let (_api_key, user_id) = auth_state.api_key_store.authenticate(auth_header)?;
669 let user = auth_state.user_store.get_by_id(&user_id)?;
670
671 return Ok(AuthUser {
672 user_id: user.id,
673 username: user.username,
674 claims: None,
675 });
676 }
677
678 Err(AuthError::InvalidToken(
679 "Authorization header must be either 'Bearer <token>' or 'ipfrs_<key>'".to_string(),
680 ))
681}
682
683pub async fn auth_middleware(
687 State(auth_state): State<AuthState>,
688 mut req: Request,
689 next: Next,
690) -> Result<Response, AuthMiddlewareError> {
691 let auth_user = authenticate_user(&req, &auth_state)?;
693
694 req.extensions_mut().insert(auth_user);
696
697 Ok(next.run(req).await)
698}
699
700type PermissionCheckFuture = std::pin::Pin<
702 Box<dyn std::future::Future<Output = Result<Response, AuthMiddlewareError>> + Send>,
703>;
704
705pub fn require_permission(
709 required: Permission,
710) -> impl Fn(State<AuthState>, Request, Next) -> PermissionCheckFuture + Clone {
711 move |State(auth_state): State<AuthState>, req: Request, next: Next| {
712 let required = required;
713 Box::pin(async move {
714 let auth_user = req
716 .extensions()
717 .get::<AuthUser>()
718 .ok_or_else(|| AuthError::InvalidToken("User not authenticated".to_string()))?;
719
720 let user = auth_state.user_store.get_by_id(&auth_user.user_id)?;
722
723 if !user.has_permission(required) {
725 return Err(AuthMiddlewareError::from(
726 AuthError::InsufficientPermissions,
727 ));
728 }
729
730 Ok(next.run(req).await)
731 })
732 }
733}
734
735#[derive(Debug)]
737pub struct AuthMiddlewareError {
738 error: AuthError,
739}
740
741impl From<AuthError> for AuthMiddlewareError {
742 fn from(error: AuthError) -> Self {
743 Self { error }
744 }
745}
746
747impl IntoResponse for AuthMiddlewareError {
748 fn into_response(self) -> Response {
749 let (status, message) = match self.error {
750 AuthError::InvalidToken(_) | AuthError::TokenExpired => {
751 (StatusCode::UNAUTHORIZED, "Authentication required")
752 }
753 AuthError::InsufficientPermissions => {
754 (StatusCode::FORBIDDEN, "Insufficient permissions")
755 }
756 AuthError::UserNotFound | AuthError::InvalidCredentials => {
757 (StatusCode::UNAUTHORIZED, "Invalid credentials")
758 }
759 _ => (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error"),
760 };
761
762 (status, message).into_response()
763 }
764}
765
766#[derive(Debug, Clone)]
772pub struct ValidationConfig {
773 pub max_body_size: usize,
775 pub max_cid_length: usize,
777 pub validate_cid_format: bool,
779 pub content_type_validation: bool,
781 pub max_batch_size: usize,
783}
784
785impl Default for ValidationConfig {
786 fn default() -> Self {
787 Self {
788 max_body_size: 100 * 1024 * 1024, max_cid_length: 100,
790 validate_cid_format: true,
791 content_type_validation: true,
792 max_batch_size: 1000,
793 }
794 }
795}
796
797impl ValidationConfig {
798 pub fn strict() -> Self {
800 Self {
801 max_body_size: 10 * 1024 * 1024, max_cid_length: 64,
803 validate_cid_format: true,
804 content_type_validation: true,
805 max_batch_size: 100,
806 }
807 }
808
809 pub fn permissive() -> Self {
811 Self {
812 max_body_size: 1024 * 1024 * 1024, max_cid_length: 200,
814 validate_cid_format: false,
815 content_type_validation: false,
816 max_batch_size: 10000,
817 }
818 }
819}
820
821#[derive(Debug)]
823pub enum ValidationError {
824 BodyTooLarge { size: usize, max: usize },
826 InvalidCid(String),
828 InvalidContentType { expected: String, actual: String },
830 MissingParameter(String),
832 BatchTooLarge { size: usize, max: usize },
834 InvalidParameter { name: String, reason: String },
836}
837
838impl std::fmt::Display for ValidationError {
839 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
840 match self {
841 ValidationError::BodyTooLarge { size, max } => {
842 write!(
843 f,
844 "Request body too large: {} bytes (max: {} bytes)",
845 size, max
846 )
847 }
848 ValidationError::InvalidCid(cid) => {
849 write!(f, "Invalid CID format: {}", cid)
850 }
851 ValidationError::InvalidContentType { expected, actual } => {
852 write!(
853 f,
854 "Invalid content type: expected {}, got {}",
855 expected, actual
856 )
857 }
858 ValidationError::MissingParameter(param) => {
859 write!(f, "Missing required parameter: {}", param)
860 }
861 ValidationError::BatchTooLarge { size, max } => {
862 write!(
863 f,
864 "Batch size too large: {} items (max: {} items)",
865 size, max
866 )
867 }
868 ValidationError::InvalidParameter { name, reason } => {
869 write!(f, "Invalid parameter '{}': {}", name, reason)
870 }
871 }
872 }
873}
874
875impl std::error::Error for ValidationError {}
876
877impl IntoResponse for ValidationError {
878 fn into_response(self) -> Response {
879 let request_id = Uuid::new_v4();
880 let error_message = self.to_string();
881
882 let (status, code) = match self {
883 ValidationError::BodyTooLarge { .. } => {
884 (StatusCode::PAYLOAD_TOO_LARGE, "BODY_TOO_LARGE")
885 }
886 ValidationError::InvalidCid(_) => (StatusCode::BAD_REQUEST, "INVALID_CID"),
887 ValidationError::InvalidContentType { .. } => {
888 (StatusCode::UNSUPPORTED_MEDIA_TYPE, "INVALID_CONTENT_TYPE")
889 }
890 ValidationError::MissingParameter(_) => (StatusCode::BAD_REQUEST, "MISSING_PARAMETER"),
891 ValidationError::BatchTooLarge { .. } => (StatusCode::BAD_REQUEST, "BATCH_TOO_LARGE"),
892 ValidationError::InvalidParameter { .. } => {
893 (StatusCode::BAD_REQUEST, "INVALID_PARAMETER")
894 }
895 };
896
897 let body = serde_json::json!({
898 "error": error_message,
899 "code": code,
900 "request_id": request_id.to_string(),
901 });
902
903 (
904 status,
905 serde_json::to_string(&body).expect("serializing JSON Value is infallible"),
906 )
907 .into_response()
908 }
909}
910
911pub fn validate_cid(cid: &str, config: &ValidationConfig) -> Result<(), ValidationError> {
915 if cid.is_empty() {
917 return Err(ValidationError::InvalidCid(
918 "CID cannot be empty".to_string(),
919 ));
920 }
921
922 if !config.validate_cid_format {
923 return Ok(());
924 }
925
926 if cid.len() > config.max_cid_length {
927 return Err(ValidationError::InvalidCid(format!(
928 "CID too long: {} chars (max: {})",
929 cid.len(),
930 config.max_cid_length
931 )));
932 }
933
934 if cid.starts_with("Qm") && cid.len() == 46 {
936 Ok(())
938 } else if cid.starts_with("b") || cid.starts_with("z") || cid.starts_with("f") {
939 Ok(())
941 } else {
942 Err(ValidationError::InvalidCid(
943 "Invalid CID format: must be CIDv0 (Qm...) or CIDv1 (b..., z..., f...)".to_string(),
944 ))
945 }
946}
947
948pub fn validate_batch_size(size: usize, config: &ValidationConfig) -> Result<(), ValidationError> {
950 if size == 0 {
951 return Err(ValidationError::InvalidParameter {
952 name: "batch".to_string(),
953 reason: "Batch cannot be empty".to_string(),
954 });
955 }
956
957 if size > config.max_batch_size {
958 return Err(ValidationError::BatchTooLarge {
959 size,
960 max: config.max_batch_size,
961 });
962 }
963
964 Ok(())
965}
966
967pub fn validate_content_type(
969 headers: &HeaderMap,
970 expected: &str,
971 config: &ValidationConfig,
972) -> Result<(), ValidationError> {
973 if !config.content_type_validation {
974 return Ok(());
975 }
976
977 let content_type = headers
978 .get(header::CONTENT_TYPE)
979 .and_then(|h| h.to_str().ok())
980 .unwrap_or("");
981
982 if !content_type.starts_with(expected) {
983 return Err(ValidationError::InvalidContentType {
984 expected: expected.to_string(),
985 actual: content_type.to_string(),
986 });
987 }
988
989 Ok(())
990}
991
992#[derive(Clone)]
994pub struct ValidationState {
995 pub config: ValidationConfig,
996}
997
998pub async fn validation_middleware(
1002 State(_validation_state): State<ValidationState>,
1003 req: Request,
1004 next: Next,
1005) -> Result<Response, ValidationError> {
1006 let (parts, body) = req.into_parts();
1007
1008 if parts.method == Method::POST || parts.method == Method::PUT {
1010 if let Some(content_type) = parts.headers.get(header::CONTENT_TYPE) {
1012 if let Ok(ct_str) = content_type.to_str() {
1013 if ct_str.contains("multipart/form-data") {
1014 let req = Request::from_parts(parts, body);
1016 return Ok(next.run(req).await);
1017 }
1018 }
1019 }
1020 }
1021
1022 let req = Request::from_parts(parts, body);
1024 Ok(next.run(req).await)
1025}
1026
1027#[cfg(test)]
1028mod tests {
1029 use super::*;
1030
1031 #[test]
1032 fn test_cors_config_default() {
1033 let config = CorsConfig::default();
1034 assert!(config.allowed_origins.is_empty());
1035 assert!(config.allowed_methods.contains(&Method::GET));
1036 assert!(config.allowed_methods.contains(&Method::POST));
1037 assert!(!config.allow_credentials);
1038 assert_eq!(config.max_age, 86400);
1039 }
1040
1041 #[test]
1042 fn test_cors_config_permissive() {
1043 let config = CorsConfig::permissive();
1044 assert!(config.allowed_origins.contains("*"));
1045 assert!(config.is_origin_allowed("https://example.com"));
1046 assert!(config.is_origin_allowed("http://localhost:3000"));
1047 }
1048
1049 #[test]
1050 fn test_cors_config_allow_origin() {
1051 let config = CorsConfig::default()
1052 .allow_origin("https://example.com")
1053 .allow_origin("https://api.example.com");
1054
1055 assert!(config.is_origin_allowed("https://example.com"));
1056 assert!(config.is_origin_allowed("https://api.example.com"));
1057 assert!(!config.is_origin_allowed("https://other.com"));
1058 }
1059
1060 #[test]
1061 fn test_rate_limit_config_default() {
1062 let config = RateLimitConfig::default();
1063 assert_eq!(config.max_requests, 100);
1064 assert_eq!(config.window, Duration::from_secs(60));
1065 assert_eq!(config.burst_capacity, 10);
1066 }
1067
1068 #[test]
1069 fn test_cache_config_default() {
1070 let config = CacheConfig::default();
1071 assert_eq!(config.default_max_age, 3600);
1072 assert!(config.public);
1073 assert!(config.immutable_cids);
1074 }
1075
1076 #[test]
1077 fn test_add_caching_headers() {
1078 let mut headers = HeaderMap::new();
1079 let config = CacheConfig::default();
1080
1081 add_caching_headers(&mut headers, "QmTest123", &config);
1082
1083 assert!(headers.contains_key(header::ETAG));
1084 assert!(headers.contains_key(header::CACHE_CONTROL));
1085
1086 let etag = headers
1087 .get(header::ETAG)
1088 .expect("test: ETAG header must be present")
1089 .to_str()
1090 .expect("test: ETAG header value must be valid UTF-8");
1091 assert_eq!(etag, "\"QmTest123\"");
1092
1093 let cache_control = headers
1094 .get(header::CACHE_CONTROL)
1095 .expect("test: CACHE_CONTROL header must be present")
1096 .to_str()
1097 .expect("test: CACHE_CONTROL header value must be valid UTF-8");
1098 assert!(cache_control.contains("public"));
1099 assert!(cache_control.contains("max-age=3600"));
1100 assert!(cache_control.contains("immutable"));
1101 }
1102
1103 #[test]
1104 fn test_check_etag_match() {
1105 let mut headers = HeaderMap::new();
1106
1107 assert!(!check_etag_match(&headers, "QmTest123"));
1109
1110 headers.insert(
1112 header::IF_NONE_MATCH,
1113 HeaderValue::from_static("\"QmTest123\""),
1114 );
1115 assert!(check_etag_match(&headers, "QmTest123"));
1116
1117 assert!(!check_etag_match(&headers, "QmOther456"));
1119 }
1120
1121 #[tokio::test]
1122 async fn test_rate_limit_state() {
1123 let config = RateLimitConfig {
1124 max_requests: 5,
1125 window: Duration::from_secs(1),
1126 burst_capacity: 3,
1127 };
1128 let state = RateLimitState::new(config);
1129
1130 for _ in 0..3 {
1132 let (allowed, _) = state.get_bucket("127.0.0.1").await;
1133 assert!(allowed);
1134 }
1135 }
1136
1137 #[test]
1138 fn test_compression_level_to_level() {
1139 assert_eq!(CompressionLevel::Fastest.to_level(), 1);
1140 assert_eq!(CompressionLevel::Balanced.to_level(), 5);
1141 assert_eq!(CompressionLevel::Best.to_level(), 9);
1142 assert_eq!(CompressionLevel::Custom(7).to_level(), 7);
1143 assert_eq!(CompressionLevel::Custom(15).to_level(), 9); }
1145
1146 #[test]
1147 fn test_compression_config_default() {
1148 let config = CompressionConfig::default();
1149 assert!(config.enable_gzip);
1150 assert_eq!(config.level, CompressionLevel::Balanced);
1151 assert_eq!(config.min_size, 1024);
1152 }
1153
1154 #[test]
1155 fn test_compression_config_fast() {
1156 let config = CompressionConfig::fast();
1157 assert_eq!(config.level, CompressionLevel::Fastest);
1158 assert!(config.enable_gzip);
1159 }
1160
1161 #[test]
1162 fn test_compression_config_best() {
1163 let config = CompressionConfig::best();
1164 assert_eq!(config.level, CompressionLevel::Best);
1165 }
1166
1167 #[test]
1168 fn test_compression_config_builder() {
1169 let config = CompressionConfig::default()
1170 .with_level(CompressionLevel::Custom(7))
1171 .with_min_size(2048)
1172 .with_gzip(true);
1173
1174 assert_eq!(config.level, CompressionLevel::Custom(7));
1175 assert_eq!(config.min_size, 2048);
1176 assert!(config.enable_gzip);
1177 }
1178
1179 #[test]
1180 fn test_compression_config_validation_valid() {
1181 let config = CompressionConfig::default();
1182 assert!(config.validate().is_ok());
1183
1184 let config = CompressionConfig::default().with_gzip(true);
1185 assert!(config.validate().is_ok());
1186 }
1187
1188 #[test]
1189 fn test_compression_config_validation_invalid() {
1190 let config = CompressionConfig::default().with_gzip(false);
1192 assert!(config.validate().is_err());
1193
1194 let config = CompressionConfig::default().with_min_size(200 * 1024 * 1024);
1196 assert!(config.validate().is_err());
1197 }
1198
1199 #[test]
1200 fn test_rate_limit_config_validation_valid() {
1201 let config = RateLimitConfig::default();
1202 assert!(config.validate().is_ok());
1203
1204 let config = RateLimitConfig {
1205 max_requests: 100,
1206 window: Duration::from_secs(60),
1207 burst_capacity: 50,
1208 };
1209 assert!(config.validate().is_ok());
1210 }
1211
1212 #[test]
1213 fn test_rate_limit_config_validation_invalid() {
1214 let config = RateLimitConfig {
1216 max_requests: 0,
1217 window: Duration::from_secs(60),
1218 burst_capacity: 10,
1219 };
1220 assert!(config.validate().is_err());
1221
1222 let config = RateLimitConfig {
1224 max_requests: 100,
1225 window: Duration::from_secs(0),
1226 burst_capacity: 10,
1227 };
1228 assert!(config.validate().is_err());
1229
1230 let config = RateLimitConfig {
1232 max_requests: 100,
1233 window: Duration::from_secs(60),
1234 burst_capacity: 200,
1235 };
1236 assert!(config.validate().is_err());
1237 }
1238
1239 #[test]
1240 fn test_cache_config_validation_valid() {
1241 let config = CacheConfig::default();
1242 assert!(config.validate().is_ok());
1243
1244 let config = CacheConfig {
1245 default_max_age: 86400, public: true,
1247 immutable_cids: true,
1248 };
1249 assert!(config.validate().is_ok());
1250 }
1251
1252 #[test]
1253 fn test_cache_config_validation_invalid() {
1254 let config = CacheConfig {
1256 default_max_age: 400 * 24 * 3600, public: true,
1258 immutable_cids: true,
1259 };
1260 assert!(config.validate().is_err());
1261 }
1262
1263 #[test]
1266 fn test_validation_config_default() {
1267 let config = ValidationConfig::default();
1268 assert_eq!(config.max_body_size, 100 * 1024 * 1024);
1269 assert_eq!(config.max_cid_length, 100);
1270 assert!(config.validate_cid_format);
1271 assert!(config.content_type_validation);
1272 assert_eq!(config.max_batch_size, 1000);
1273 }
1274
1275 #[test]
1276 fn test_validation_config_strict() {
1277 let config = ValidationConfig::strict();
1278 assert_eq!(config.max_body_size, 10 * 1024 * 1024);
1279 assert_eq!(config.max_cid_length, 64);
1280 assert_eq!(config.max_batch_size, 100);
1281 }
1282
1283 #[test]
1284 fn test_validation_config_permissive() {
1285 let config = ValidationConfig::permissive();
1286 assert_eq!(config.max_body_size, 1024 * 1024 * 1024);
1287 assert_eq!(config.max_cid_length, 200);
1288 assert!(!config.validate_cid_format);
1289 assert!(!config.content_type_validation);
1290 assert_eq!(config.max_batch_size, 10000);
1291 }
1292
1293 #[test]
1294 fn test_validate_cid_v0() {
1295 let config = ValidationConfig::default();
1296
1297 assert!(validate_cid("QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco", &config).is_ok());
1299
1300 assert!(validate_cid("QmShort", &config).is_err());
1302
1303 assert!(validate_cid("", &config).is_err());
1305 }
1306
1307 #[test]
1308 fn test_validate_cid_v1() {
1309 let config = ValidationConfig::default();
1310
1311 assert!(validate_cid(
1313 "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
1314 &config
1315 )
1316 .is_ok());
1317 assert!(validate_cid("zb2rhk6GMPQF8p1kqXvhYnCMp3hGGUQVvqp6qjdvNLKqCqKCo", &config).is_ok());
1318
1319 assert!(validate_cid("invalid_cid_format", &config).is_err());
1321 }
1322
1323 #[test]
1324 fn test_validate_cid_disabled() {
1325 let config = ValidationConfig {
1326 validate_cid_format: false,
1327 ..Default::default()
1328 };
1329
1330 assert!(validate_cid("invalid_format", &config).is_ok());
1332 assert!(validate_cid("", &config).is_err()); }
1334
1335 #[test]
1336 fn test_validate_batch_size_valid() {
1337 let config = ValidationConfig::default();
1338
1339 assert!(validate_batch_size(1, &config).is_ok());
1340 assert!(validate_batch_size(100, &config).is_ok());
1341 assert!(validate_batch_size(1000, &config).is_ok());
1342 }
1343
1344 #[test]
1345 fn test_validate_batch_size_invalid() {
1346 let config = ValidationConfig::default();
1347
1348 assert!(validate_batch_size(0, &config).is_err());
1350
1351 assert!(validate_batch_size(1001, &config).is_err());
1353 assert!(validate_batch_size(10000, &config).is_err());
1354 }
1355
1356 #[test]
1357 fn test_validate_content_type_valid() {
1358 let config = ValidationConfig::default();
1359 let mut headers = HeaderMap::new();
1360 headers.insert(
1361 header::CONTENT_TYPE,
1362 HeaderValue::from_static("application/json"),
1363 );
1364
1365 assert!(validate_content_type(&headers, "application/json", &config).is_ok());
1366 }
1367
1368 #[test]
1369 fn test_validate_content_type_invalid() {
1370 let config = ValidationConfig::default();
1371 let mut headers = HeaderMap::new();
1372 headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("text/plain"));
1373
1374 assert!(validate_content_type(&headers, "application/json", &config).is_err());
1375 }
1376
1377 #[test]
1378 fn test_validate_content_type_disabled() {
1379 let config = ValidationConfig {
1380 content_type_validation: false,
1381 ..Default::default()
1382 };
1383
1384 let mut headers = HeaderMap::new();
1385 headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("text/plain"));
1386
1387 assert!(validate_content_type(&headers, "application/json", &config).is_ok());
1389 }
1390
1391 #[test]
1392 fn test_validation_error_display() {
1393 let err = ValidationError::InvalidCid("test".to_string());
1394 assert_eq!(err.to_string(), "Invalid CID format: test");
1395
1396 let err = ValidationError::BodyTooLarge {
1397 size: 200,
1398 max: 100,
1399 };
1400 assert!(err.to_string().contains("200 bytes"));
1401 assert!(err.to_string().contains("100 bytes"));
1402
1403 let err = ValidationError::BatchTooLarge {
1404 size: 2000,
1405 max: 1000,
1406 };
1407 assert!(err.to_string().contains("2000 items"));
1408 assert!(err.to_string().contains("1000 items"));
1409 }
1410}