pub use http::Extensions;
use crate::s3::error::Error;
use crate::s3::http::Url;
use crate::s3::multimap_ext::Multimap;
use crate::s3::segmented_bytes::SegmentedBytes;
use crate::s3::types::{BucketName, ObjectKey};
use http::Method;
use reqwest::Response;
use std::fmt::Debug;
#[async_trait::async_trait]
pub trait RequestHooks: Debug {
fn name(&self) -> &'static str;
async fn before_signing_mut(
&self,
_method: &Method,
_url: &mut Url,
_region: &str,
_headers: &mut Multimap,
_query_params: &Multimap,
_bucket: Option<&BucketName>,
_object: Option<&ObjectKey>,
_body: Option<&SegmentedBytes>,
_extensions: &mut Extensions,
) -> Result<(), Error> {
Ok(())
}
async fn after_execute(
&self,
_method: &Method,
_url: &Url,
_region: &str,
_headers: &Multimap,
_query_params: &Multimap,
_bucket: Option<&BucketName>,
_object: Option<&ObjectKey>,
_resp: &Result<Response, reqwest::Error>,
_extensions: &mut Extensions,
) {
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::s3::multimap_ext::MultimapExt;
#[test]
fn test_hook_trait_has_default_implementations() {
#[derive(Debug)]
struct MinimalHook;
#[async_trait::async_trait]
impl RequestHooks for MinimalHook {
fn name(&self) -> &'static str {
"minimal-hook"
}
}
let hook = MinimalHook;
assert_eq!(hook.name(), "minimal-hook");
}
#[tokio::test]
async fn test_hook_can_modify_url() {
#[derive(Debug)]
struct UrlModifyingHook;
#[async_trait::async_trait]
impl RequestHooks for UrlModifyingHook {
fn name(&self) -> &'static str {
"url-modifier"
}
async fn before_signing_mut(
&self,
_method: &Method,
url: &mut Url,
_region: &str,
_headers: &mut Multimap,
_query_params: &Multimap,
_bucket: Option<&BucketName>,
_object: Option<&ObjectKey>,
_body: Option<&SegmentedBytes>,
_extensions: &mut Extensions,
) -> Result<(), Error> {
url.host = "modified-host.example.com".to_string();
url.port = 9000;
Ok(())
}
}
let hook = UrlModifyingHook;
let mut url = Url {
https: true,
host: "original-host.example.com".to_string(),
port: 443,
path: "/bucket/object".to_string(),
query: Multimap::new(),
};
let mut headers = Multimap::new();
let query_params = Multimap::new();
let mut extensions = Extensions::default();
let bucket = BucketName::new("bucket").unwrap();
let object = ObjectKey::new("object").unwrap();
let result = hook
.before_signing_mut(
&Method::GET,
&mut url,
"us-east-1",
&mut headers,
&query_params,
Some(&bucket),
Some(&object),
None,
&mut extensions,
)
.await;
assert!(result.is_ok());
assert_eq!(url.host, "modified-host.example.com");
assert_eq!(url.port, 9000);
}
#[tokio::test]
async fn test_hook_can_modify_headers() {
#[derive(Debug)]
struct HeaderModifyingHook;
#[async_trait::async_trait]
impl RequestHooks for HeaderModifyingHook {
fn name(&self) -> &'static str {
"header-modifier"
}
async fn before_signing_mut(
&self,
_method: &Method,
_url: &mut Url,
_region: &str,
headers: &mut Multimap,
_query_params: &Multimap,
_bucket: Option<&BucketName>,
_object: Option<&ObjectKey>,
_body: Option<&SegmentedBytes>,
_extensions: &mut Extensions,
) -> Result<(), Error> {
headers.add("X-Custom-Header", "custom-value");
Ok(())
}
}
let hook = HeaderModifyingHook;
let mut url = Url::default();
let mut headers = Multimap::new();
let query_params = Multimap::new();
let mut extensions = Extensions::default();
let result = hook
.before_signing_mut(
&Method::GET,
&mut url,
"us-east-1",
&mut headers,
&query_params,
None,
None,
None,
&mut extensions,
)
.await;
assert!(result.is_ok());
assert!(headers.contains_key("X-Custom-Header"));
assert_eq!(
headers.get("X-Custom-Header"),
Some(&"custom-value".to_string())
);
}
#[tokio::test]
async fn test_hook_can_use_extensions() {
#[derive(Debug)]
struct ExtensionWritingHook;
#[async_trait::async_trait]
impl RequestHooks for ExtensionWritingHook {
fn name(&self) -> &'static str {
"extension-writer"
}
async fn before_signing_mut(
&self,
_method: &Method,
_url: &mut Url,
_region: &str,
_headers: &mut Multimap,
_query_params: &Multimap,
_bucket: Option<&BucketName>,
_object: Option<&ObjectKey>,
_body: Option<&SegmentedBytes>,
extensions: &mut Extensions,
) -> Result<(), Error> {
extensions.insert("test-data".to_string());
extensions.insert(42u32);
Ok(())
}
}
let hook = ExtensionWritingHook;
let mut url = Url::default();
let mut headers = Multimap::new();
let query_params = Multimap::new();
let mut extensions = Extensions::default();
let result = hook
.before_signing_mut(
&Method::GET,
&mut url,
"us-east-1",
&mut headers,
&query_params,
None,
None,
None,
&mut extensions,
)
.await;
assert!(result.is_ok());
assert_eq!(extensions.get::<String>(), Some(&"test-data".to_string()));
assert_eq!(extensions.get::<u32>(), Some(&42u32));
}
#[tokio::test]
async fn test_hook_can_return_error() {
use crate::s3::error::ValidationErr;
#[derive(Debug)]
struct ErrorReturningHook;
#[async_trait::async_trait]
impl RequestHooks for ErrorReturningHook {
fn name(&self) -> &'static str {
"error-hook"
}
async fn before_signing_mut(
&self,
_method: &Method,
_url: &mut Url,
_region: &str,
_headers: &mut Multimap,
_query_params: &Multimap,
bucket: Option<&BucketName>,
_object: Option<&ObjectKey>,
_body: Option<&SegmentedBytes>,
_extensions: &mut Extensions,
) -> Result<(), Error> {
if bucket.map(|b| b.as_str()) == Some("forbidden-bucket") {
return Err(Error::Validation(ValidationErr::InvalidBucketName {
name: "forbidden-bucket".to_string(),
reason: "Bucket access denied by hook".to_string(),
}));
}
Ok(())
}
}
let hook = ErrorReturningHook;
let mut url = Url::default();
let mut headers = Multimap::new();
let query_params = Multimap::new();
let mut extensions = Extensions::default();
let forbidden_bucket = BucketName::new("forbidden-bucket").unwrap();
let result = hook
.before_signing_mut(
&Method::GET,
&mut url,
"us-east-1",
&mut headers,
&query_params,
Some(&forbidden_bucket),
None,
None,
&mut extensions,
)
.await;
assert!(result.is_err());
match result {
Err(Error::Validation(ValidationErr::InvalidBucketName { name, reason })) => {
assert_eq!(name, "forbidden-bucket");
assert!(reason.contains("denied by hook"));
}
_ => panic!("Expected InvalidBucketName error"),
}
}
#[tokio::test]
async fn test_hook_default_after_execute() {
#[derive(Debug)]
struct NoOpHook;
#[async_trait::async_trait]
impl RequestHooks for NoOpHook {
fn name(&self) -> &'static str {
"noop-hook"
}
}
let hook = NoOpHook;
let mut url = Url::default();
let mut headers = Multimap::new();
let query_params = Multimap::new();
let mut extensions = Extensions::default();
let result = hook
.before_signing_mut(
&Method::GET,
&mut url,
"us-east-1",
&mut headers,
&query_params,
None,
None,
None,
&mut extensions,
)
.await;
assert!(result.is_ok());
}
#[test]
fn test_debug_logging_format() {
let method = Method::PUT;
let url = Url {
https: true,
host: "minio.example.com".to_string(),
port: 443,
path: "/bucket/object".to_string(),
query: Multimap::new(),
};
let mut headers = Multimap::new();
headers.add("Content-Type", "application/json");
headers.add("Authorization", "AWS4-HMAC-SHA256 Credential=...");
headers.add("x-amz-date", "20240101T000000Z");
let mut header_strings: Vec<String> = headers
.iter_all()
.map(|(k, v)| format!("{}: {}", k, v.join(",")))
.collect();
header_strings.sort();
let debug_str = format!(
"S3 request: {} url={}; headers={}",
method,
url,
header_strings.join("; ")
);
let truncated = if debug_str.len() > 1000 {
format!("{}...", &debug_str[..997])
} else {
debug_str.clone()
};
assert!(debug_str.contains("S3 request:"));
assert!(debug_str.contains("PUT"));
assert!(debug_str.contains("minio.example.com"));
assert!(debug_str.contains("Content-Type"));
assert!(debug_str.contains("Authorization"));
assert!(debug_str.contains("x-amz-date"));
assert_eq!(truncated, debug_str);
}
}