diff --git a/rustfs/src/admin/router.rs b/rustfs/src/admin/router.rs index 960ef47f1..c5fbc7111 100644 --- a/rustfs/src/admin/router.rs +++ b/rustfs/src/admin/router.rs @@ -18,7 +18,9 @@ use crate::app::object_usecase::DefaultObjectUsecase; use crate::auth::{check_key_valid, get_session_token}; use crate::error::ApiError; use crate::license::license_check; -use crate::server::{ADMIN_PREFIX, HEALTH_PREFIX, HEALTH_READY_PATH, MINIO_ADMIN_PREFIX, PROFILE_CPU_PATH, PROFILE_MEMORY_PATH}; +use crate::server::{ + ADMIN_PREFIX, HEALTH_PREFIX, HEALTH_READY_PATH, MINIO_ADMIN_PREFIX, PROFILE_CPU_PATH, PROFILE_MEMORY_PATH, is_admin_path, +}; use crate::storage::access::{ReqInfo, authorize_request}; use aws_sdk_s3::primitives::ByteStream as AwsByteStream; use bytes::Bytes; @@ -2216,12 +2218,10 @@ fn is_public_health_path(path: &str) -> bool { path == HEALTH_PREFIX || path == HEALTH_READY_PATH } -fn is_admin_path(path: &str) -> bool { - path.starts_with(ADMIN_PREFIX) || path.starts_with(MINIO_ADMIN_PREFIX) -} - fn canonicalize_admin_path(path: &str) -> std::borrow::Cow<'_, str> { - if let Some(suffix) = path.strip_prefix(MINIO_ADMIN_PREFIX) { + if is_admin_path(path) + && let Some(suffix) = path.strip_prefix(MINIO_ADMIN_PREFIX) + { return std::borrow::Cow::Owned(format!("{ADMIN_PREFIX}{suffix}")); } @@ -2445,7 +2445,13 @@ mod tests { #[test] fn canonicalize_admin_path_maps_compat_prefix_to_rustfs_prefix() { assert_eq!(canonicalize_admin_path("/minio/admin/v3/info").as_ref(), "/rustfs/admin/v3/info"); + assert_eq!(canonicalize_admin_path("/minio/admin").as_ref(), "/rustfs/admin"); assert_eq!(canonicalize_admin_path("/rustfs/admin/v3/info").as_ref(), "/rustfs/admin/v3/info"); + assert_eq!( + canonicalize_admin_path("/minio/administrator/object").as_ref(), + "/minio/administrator/object" + ); + assert_eq!(canonicalize_admin_path("/minio/adminx/object").as_ref(), "/minio/adminx/object"); } #[test] @@ -2453,6 +2459,10 @@ mod tests { assert!(is_admin_path("/rustfs/admin/v3/info")); assert!(is_admin_path("/minio/admin/v3/info")); assert!(!is_admin_path("/bucket/object")); + assert!(!is_admin_path("/rustfs/administrator/object")); + assert!(!is_admin_path("/minio/administrator/object")); + assert!(!is_admin_path("/rustfs/adminx/object")); + assert!(!is_admin_path("/minio/adminx/object")); } #[test] diff --git a/rustfs/src/server/layer.rs b/rustfs/src/server/layer.rs index 23d471e0c..19b9e5202 100644 --- a/rustfs/src/server/layer.rs +++ b/rustfs/src/server/layer.rs @@ -20,6 +20,7 @@ use crate::server::hybrid::HybridBody; use crate::server::{ ADMIN_PREFIX, CONSOLE_PREFIX, HEALTH_COMPAT_LIVE_PATH, HEALTH_PREFIX, HEALTH_READY_PATH, MINIO_ADMIN_PREFIX, MINIO_ADMIN_V3_PREFIX, RPC_PREFIX, RUSTFS_ADMIN_PREFIX, active_http_requests, collect_dependency_readiness_report, + is_admin_path, }; use crate::storage::apply_cors_headers; use crate::storage::request_context::{RequestContext, extract_request_id_from_headers}; @@ -289,6 +290,7 @@ fn should_force_zero_content_length_for_empty_body_route(req: &HttpRequest fn is_empty_body_admin_path(method: &Method, uri: &http::Uri) -> bool { let path = uri.path(); match *method { + Method::GET => is_admin_path(path), Method::PUT => matches!( path, "/minio/admin/v3/set-user-status" @@ -1428,6 +1430,68 @@ mod tests { } } + #[test] + fn admin_empty_body_get_without_content_length_is_normalized() { + let paths = [ + format!("{MINIO_ADMIN_PREFIX}/v3/is-admin"), + format!("{MINIO_ADMIN_PREFIX}/v3/accountinfo"), + format!("{MINIO_ADMIN_PREFIX}/v3/info"), + format!("{ADMIN_PREFIX}/v3/is-admin"), + format!("{ADMIN_PREFIX}/v3/accountinfo"), + format!("{ADMIN_PREFIX}/v3/info"), + ]; + + for path in paths { + let request = Request::builder() + .method(Method::GET) + .uri(path.as_str()) + .body(()) + .expect("request"); + + assert!( + should_force_zero_content_length_for_empty_body_route(&request), + "{path} should force Content-Length: 0" + ); + } + } + + #[test] + fn non_admin_get_without_content_length_is_not_normalized() { + let paths = [ + "/bucket/object", + "/rustfs/administrator/object", + "/minio/administrator/object", + "/rustfs/adminx/object", + "/minio/adminx/object", + ]; + + for path in paths { + let request = Request::builder().method(Method::GET).uri(path).body(()).expect("request"); + + assert!( + !should_force_zero_content_length_for_empty_body_route(&request), + "{path} should not force Content-Length: 0" + ); + } + } + + #[tokio::test] + async fn empty_body_layer_inserts_zero_content_length_for_admin_get() { + let capture = HeaderCaptureService::default(); + let headers = capture.headers(); + let mut service = EmptyBodyContentLengthCompatLayer.layer(capture); + let request = Request::builder() + .method(Method::GET) + .uri("/rustfs/admin/v3/accountinfo") + .body(()) + .expect("request"); + + let _ = service.call(request).await.expect("service call"); + + let headers = headers.lock().expect("captured headers").take().expect("captured headers"); + assert_eq!(headers.get(http::header::CONTENT_LENGTH).unwrap(), "0"); + } + #[tokio::test] async fn empty_body_layer_inserts_zero_content_length_for_admin_post() { let capture = HeaderCaptureService::default(); diff --git a/rustfs/src/server/mod.rs b/rustfs/src/server/mod.rs index 386d48065..345f75ccf 100644 --- a/rustfs/src/server/mod.rs +++ b/rustfs/src/server/mod.rs @@ -49,7 +49,7 @@ pub(crate) use module_switch::{ pub(crate) use prefix::{ ADMIN_PREFIX, CONSOLE_PREFIX, FAVICON_PATH, HEALTH_COMPAT_LIVE_PATH, HEALTH_PREFIX, HEALTH_READY_PATH, LICENSE, MINIO_ADMIN_PREFIX, MINIO_ADMIN_V3_PREFIX, PROFILE_CPU_PATH, PROFILE_MEMORY_PATH, RPC_PREFIX, RUSTFS_ADMIN_PREFIX, - TONIC_PREFIX, VERSION, + TONIC_PREFIX, VERSION, is_admin_path, }; pub(crate) use readiness::DependencyReadiness; pub(crate) use readiness::DependencyReadinessReport; diff --git a/rustfs/src/server/prefix.rs b/rustfs/src/server/prefix.rs index be0c64a56..077e126e9 100644 --- a/rustfs/src/server/prefix.rs +++ b/rustfs/src/server/prefix.rs @@ -43,6 +43,15 @@ pub(crate) const ADMIN_PREFIX: &str = "/rustfs/admin"; /// This alias allows stock MinIO admin tooling to reach RustFS handlers. pub(crate) const MINIO_ADMIN_PREFIX: &str = "/minio/admin"; +/// Returns true for the admin prefix itself or slash-delimited children. +pub(crate) fn is_admin_path(path: &str) -> bool { + has_path_prefix(path, ADMIN_PREFIX) || has_path_prefix(path, MINIO_ADMIN_PREFIX) +} + +fn has_path_prefix(path: &str, prefix: &str) -> bool { + path == prefix || path.strip_prefix(prefix).is_some_and(|suffix| suffix.starts_with('/')) +} + /// Environment variable name for overriding the default /// administrative prefix path. pub(crate) const RUSTFS_ADMIN_PREFIX: &str = "/rustfs/admin/v3";