mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-30 18:12:14 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 416d3ad5b7 | |||
| f30698ec7f | |||
| 7dcf01f127 | |||
| e524a106c5 | |||
| d9e5f5d2e3 | |||
| 684e832530 | |||
| a65856bdf4 | |||
| 2edb2929b2 | |||
| 14bc55479b | |||
| cd1e244c68 | |||
| 46797dc815 |
Generated
+16
@@ -6280,6 +6280,7 @@ dependencies = [
|
||||
"axum-extra",
|
||||
"axum-server",
|
||||
"base64 0.22.1",
|
||||
"base64-simd",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"clap",
|
||||
@@ -6288,6 +6289,7 @@ dependencies = [
|
||||
"flatbuffers",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"hex-simd",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"hyper 1.7.0",
|
||||
@@ -6475,6 +6477,7 @@ dependencies = [
|
||||
"aws-sdk-s3",
|
||||
"aws-smithy-types",
|
||||
"base64 0.22.1",
|
||||
"base64-simd",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"bytesize",
|
||||
@@ -6549,6 +6552,8 @@ dependencies = [
|
||||
"bytes",
|
||||
"crc32fast",
|
||||
"criterion",
|
||||
"lazy_static",
|
||||
"regex",
|
||||
"rmp",
|
||||
"rmp-serde",
|
||||
"rustfs-utils",
|
||||
@@ -6765,17 +6770,26 @@ name = "rustfs-rio"
|
||||
version = "0.0.5"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"base64 0.22.1",
|
||||
"base64-simd",
|
||||
"bytes",
|
||||
"crc32c",
|
||||
"crc32fast",
|
||||
"crc64fast-nvme",
|
||||
"futures",
|
||||
"hex-simd",
|
||||
"http 1.3.1",
|
||||
"md-5",
|
||||
"pin-project-lite",
|
||||
"rand 0.9.2",
|
||||
"reqwest",
|
||||
"rustfs-utils",
|
||||
"s3s",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha1 0.10.6",
|
||||
"sha2 0.10.9",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tokio-test",
|
||||
"tokio-util",
|
||||
@@ -6827,6 +6841,7 @@ dependencies = [
|
||||
name = "rustfs-signer"
|
||||
version = "0.0.5"
|
||||
dependencies = [
|
||||
"base64-simd",
|
||||
"bytes",
|
||||
"http 1.3.1",
|
||||
"hyper 1.7.0",
|
||||
@@ -6874,6 +6889,7 @@ dependencies = [
|
||||
"hickory-resolver",
|
||||
"highway",
|
||||
"hmac 0.12.1",
|
||||
"http 1.3.1",
|
||||
"hyper 1.7.0",
|
||||
"libc",
|
||||
"local-ip-address",
|
||||
|
||||
@@ -121,6 +121,8 @@ chrono = { version = "0.4.42", features = ["serde"] }
|
||||
clap = { version = "4.5.49", features = ["derive", "env"] }
|
||||
const-str = { version = "0.7.0", features = ["std", "proc"] }
|
||||
crc32fast = "1.5.0"
|
||||
crc32c = "0.6.8"
|
||||
crc64fast-nvme = "1.2.0"
|
||||
criterion = { version = "0.7", features = ["html_reports"] }
|
||||
crossbeam-queue = "0.3.12"
|
||||
datafusion = "50.2.0"
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ LABEL name="RustFS" \
|
||||
url="https://rustfs.com" \
|
||||
license="Apache-2.0"
|
||||
|
||||
RUN apk add --no-cache ca-certificates coreutils
|
||||
RUN apk add --no-cache ca-certificates coreutils curl
|
||||
|
||||
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=build /build/rustfs /usr/bin/rustfs
|
||||
|
||||
@@ -343,7 +343,7 @@ mod serial_tests {
|
||||
set_bucket_lifecycle(bucket_name.as_str())
|
||||
.await
|
||||
.expect("Failed to set lifecycle configuration");
|
||||
println!("✅ Lifecycle configuration set for bucket: {}", bucket_name);
|
||||
println!("✅ Lifecycle configuration set for bucket: {bucket_name}");
|
||||
|
||||
// Verify lifecycle configuration was set
|
||||
match rustfs_ecstore::bucket::metadata_sys::get(bucket_name.as_str()).await {
|
||||
@@ -477,7 +477,7 @@ mod serial_tests {
|
||||
set_bucket_lifecycle_deletemarker(bucket_name.as_str())
|
||||
.await
|
||||
.expect("Failed to set lifecycle configuration");
|
||||
println!("✅ Lifecycle configuration set for bucket: {}", bucket_name);
|
||||
println!("✅ Lifecycle configuration set for bucket: {bucket_name}");
|
||||
|
||||
// Verify lifecycle configuration was set
|
||||
match rustfs_ecstore::bucket::metadata_sys::get(bucket_name.as_str()).await {
|
||||
|
||||
@@ -81,8 +81,8 @@ fn test_config_section_names() {
|
||||
fn test_environment_variable_parsing() {
|
||||
// Test environment variable prefix patterns
|
||||
let env_prefix = "RUSTFS_";
|
||||
let audit_webhook_prefix = format!("{}AUDIT_WEBHOOK_", env_prefix);
|
||||
let audit_mqtt_prefix = format!("{}AUDIT_MQTT_", env_prefix);
|
||||
let audit_webhook_prefix = format!("{env_prefix}AUDIT_WEBHOOK_");
|
||||
let audit_mqtt_prefix = format!("{env_prefix}AUDIT_MQTT_");
|
||||
|
||||
assert_eq!(audit_webhook_prefix, "RUSTFS_AUDIT_WEBHOOK_");
|
||||
assert_eq!(audit_mqtt_prefix, "RUSTFS_AUDIT_MQTT_");
|
||||
@@ -141,13 +141,13 @@ fn test_duration_parsing_formats() {
|
||||
let result = parse_duration_test(input);
|
||||
match (result, expected_seconds) {
|
||||
(Some(duration), Some(expected)) => {
|
||||
assert_eq!(duration.as_secs(), expected, "Failed for input: {}", input);
|
||||
assert_eq!(duration.as_secs(), expected, "Failed for input: {input}");
|
||||
}
|
||||
(None, None) => {
|
||||
// Both None, test passes
|
||||
}
|
||||
_ => {
|
||||
panic!("Mismatch for input: {}, got: {:?}, expected: {:?}", input, result, expected_seconds);
|
||||
panic!("Mismatch for input: {input}, got: {result:?}, expected: {expected_seconds:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,13 +188,13 @@ fn test_url_validation() {
|
||||
|
||||
for url_str in valid_urls {
|
||||
let result = Url::parse(url_str);
|
||||
assert!(result.is_ok(), "Valid URL should parse: {}", url_str);
|
||||
assert!(result.is_ok(), "Valid URL should parse: {url_str}");
|
||||
}
|
||||
|
||||
for url_str in &invalid_urls[..3] {
|
||||
// Skip the ftp one as it's technically valid
|
||||
let result = Url::parse(url_str);
|
||||
assert!(result.is_err(), "Invalid URL should not parse: {}", url_str);
|
||||
assert!(result.is_err(), "Invalid URL should not parse: {url_str}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,6 +214,6 @@ fn test_qos_parsing() {
|
||||
0..=2 => Some(q),
|
||||
_ => None,
|
||||
});
|
||||
assert_eq!(result, expected, "Failed for QoS input: {}", input);
|
||||
assert_eq!(result, expected, "Failed for QoS input: {input}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ async fn test_config_parsing_webhook() {
|
||||
}
|
||||
Err(e) => {
|
||||
// Other errors might indicate parsing issues
|
||||
println!("Unexpected error: {}", e);
|
||||
println!("Unexpected error: {e}");
|
||||
}
|
||||
Ok(_) => {
|
||||
// Unexpected success in test environment without server storage
|
||||
@@ -103,6 +103,6 @@ fn test_enable_value_parsing() {
|
||||
|
||||
for (input, expected) in test_cases {
|
||||
let result = matches!(input.to_lowercase().as_str(), "1" | "on" | "true" | "yes");
|
||||
assert_eq!(result, expected, "Failed for input: {}", input);
|
||||
assert_eq!(result, expected, "Failed for input: {input}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,10 +32,10 @@ async fn test_audit_system_startup_performance() {
|
||||
let _result = timeout(Duration::from_secs(5), system.start(config)).await;
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
println!("Audit system startup took: {:?}", elapsed);
|
||||
println!("Audit system startup took: {elapsed:?}");
|
||||
|
||||
// Should complete within 5 seconds
|
||||
assert!(elapsed < Duration::from_secs(5), "Startup took too long: {:?}", elapsed);
|
||||
assert!(elapsed < Duration::from_secs(5), "Startup took too long: {elapsed:?}");
|
||||
|
||||
// Clean up
|
||||
let _ = system.close().await;
|
||||
@@ -54,8 +54,8 @@ async fn test_concurrent_target_creation() {
|
||||
for i in 1..=5 {
|
||||
let mut kvs = rustfs_ecstore::config::KVS::new();
|
||||
kvs.insert("enable".to_string(), "on".to_string());
|
||||
kvs.insert("endpoint".to_string(), format!("http://localhost:302{}/webhook", i));
|
||||
webhook_section.insert(format!("instance_{}", i), kvs);
|
||||
kvs.insert("endpoint".to_string(), format!("http://localhost:302{i}/webhook"));
|
||||
webhook_section.insert(format!("instance_{i}"), kvs);
|
||||
}
|
||||
|
||||
config.0.insert("audit_webhook".to_string(), webhook_section);
|
||||
@@ -66,10 +66,10 @@ async fn test_concurrent_target_creation() {
|
||||
let result = registry.create_targets_from_config(&config).await;
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
println!("Concurrent target creation took: {:?}", elapsed);
|
||||
println!("Concurrent target creation took: {elapsed:?}");
|
||||
|
||||
// Should complete quickly even with multiple targets
|
||||
assert!(elapsed < Duration::from_secs(10), "Target creation took too long: {:?}", elapsed);
|
||||
assert!(elapsed < Duration::from_secs(10), "Target creation took too long: {elapsed:?}");
|
||||
|
||||
// Verify it fails with expected error (server not initialized)
|
||||
match result {
|
||||
@@ -77,7 +77,7 @@ async fn test_concurrent_target_creation() {
|
||||
// Expected in test environment
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Unexpected error during concurrent creation: {}", e);
|
||||
println!("Unexpected error during concurrent creation: {e}");
|
||||
}
|
||||
Ok(_) => {
|
||||
println!("Unexpected success in test environment");
|
||||
@@ -93,7 +93,7 @@ async fn test_audit_log_dispatch_performance() {
|
||||
let config = rustfs_ecstore::config::Config(HashMap::new());
|
||||
let start_result = system.start(config).await;
|
||||
if start_result.is_err() {
|
||||
println!("AuditSystem failed to start: {:?}", start_result);
|
||||
println!("AuditSystem failed to start: {start_result:?}");
|
||||
return; // 或 assert!(false, "AuditSystem failed to start");
|
||||
}
|
||||
|
||||
@@ -104,14 +104,14 @@ async fn test_audit_log_dispatch_performance() {
|
||||
let id = 1;
|
||||
|
||||
let mut req_header = HashMap::new();
|
||||
req_header.insert("authorization".to_string(), format!("Bearer test-token-{}", id));
|
||||
req_header.insert("authorization".to_string(), format!("Bearer test-token-{id}"));
|
||||
req_header.insert("content-type".to_string(), "application/octet-stream".to_string());
|
||||
|
||||
let mut resp_header = HashMap::new();
|
||||
resp_header.insert("x-response".to_string(), "ok".to_string());
|
||||
|
||||
let mut tags = HashMap::new();
|
||||
tags.insert(format!("tag-{}", id), json!("sample"));
|
||||
tags.insert(format!("tag-{id}"), json!("sample"));
|
||||
|
||||
let mut req_query = HashMap::new();
|
||||
req_query.insert("id".to_string(), id.to_string());
|
||||
@@ -119,7 +119,7 @@ async fn test_audit_log_dispatch_performance() {
|
||||
let api_details = ApiDetails {
|
||||
name: Some("PutObject".to_string()),
|
||||
bucket: Some("test-bucket".to_string()),
|
||||
object: Some(format!("test-object-{}", id)),
|
||||
object: Some(format!("test-object-{id}")),
|
||||
status: Some("success".to_string()),
|
||||
status_code: Some(200),
|
||||
input_bytes: Some(1024),
|
||||
@@ -134,7 +134,7 @@ async fn test_audit_log_dispatch_performance() {
|
||||
// Create sample audit log entry
|
||||
let audit_entry = AuditEntry {
|
||||
version: "1".to_string(),
|
||||
deployment_id: Some(format!("test-deployment-{}", id)),
|
||||
deployment_id: Some(format!("test-deployment-{id}")),
|
||||
site_name: Some("test-site".to_string()),
|
||||
time: Utc::now(),
|
||||
event: EventName::ObjectCreatedPut,
|
||||
@@ -142,9 +142,9 @@ async fn test_audit_log_dispatch_performance() {
|
||||
trigger: "api".to_string(),
|
||||
api: api_details,
|
||||
remote_host: Some("127.0.0.1".to_string()),
|
||||
request_id: Some(format!("test-request-{}", id)),
|
||||
request_id: Some(format!("test-request-{id}")),
|
||||
user_agent: Some("test-agent".to_string()),
|
||||
req_path: Some(format!("/test-bucket/test-object-{}", id)),
|
||||
req_path: Some(format!("/test-bucket/test-object-{id}")),
|
||||
req_host: Some("test-host".to_string()),
|
||||
req_node: Some("node-1".to_string()),
|
||||
req_claims: None,
|
||||
@@ -152,8 +152,8 @@ async fn test_audit_log_dispatch_performance() {
|
||||
req_header: Some(req_header),
|
||||
resp_header: Some(resp_header),
|
||||
tags: Some(tags),
|
||||
access_key: Some(format!("AKIA{}", id)),
|
||||
parent_user: Some(format!("parent-{}", id)),
|
||||
access_key: Some(format!("AKIA{id}")),
|
||||
parent_user: Some(format!("parent-{id}")),
|
||||
error: None,
|
||||
};
|
||||
|
||||
@@ -163,10 +163,10 @@ async fn test_audit_log_dispatch_performance() {
|
||||
let result = system.dispatch(Arc::new(audit_entry)).await;
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
println!("Audit log dispatch took: {:?}", elapsed);
|
||||
println!("Audit log dispatch took: {elapsed:?}");
|
||||
|
||||
// Should be very fast (sub-millisecond for no targets)
|
||||
assert!(elapsed < Duration::from_millis(100), "Dispatch took too long: {:?}", elapsed);
|
||||
assert!(elapsed < Duration::from_millis(100), "Dispatch took too long: {elapsed:?}");
|
||||
|
||||
// Should succeed even with no targets
|
||||
assert!(result.is_ok(), "Dispatch should succeed with no targets");
|
||||
@@ -226,10 +226,10 @@ fn test_event_name_mask_performance() {
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
println!("Event mask calculation (5000 ops) took: {:?}", elapsed);
|
||||
println!("Event mask calculation (5000 ops) took: {elapsed:?}");
|
||||
|
||||
// Should be very fast
|
||||
assert!(elapsed < Duration::from_millis(100), "Mask calculation too slow: {:?}", elapsed);
|
||||
assert!(elapsed < Duration::from_millis(100), "Mask calculation too slow: {elapsed:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -254,10 +254,10 @@ fn test_event_name_expansion_performance() {
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
println!("Event expansion (4000 ops) took: {:?}", elapsed);
|
||||
println!("Event expansion (4000 ops) took: {elapsed:?}");
|
||||
|
||||
// Should be very fast
|
||||
assert!(elapsed < Duration::from_millis(100), "Expansion too slow: {:?}", elapsed);
|
||||
assert!(elapsed < Duration::from_millis(100), "Expansion too slow: {elapsed:?}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -274,10 +274,10 @@ async fn test_registry_operations_performance() {
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
println!("Registry operations (2000 ops) took: {:?}", elapsed);
|
||||
println!("Registry operations (2000 ops) took: {elapsed:?}");
|
||||
|
||||
// Should be very fast for empty registry
|
||||
assert!(elapsed < Duration::from_millis(100), "Registry ops too slow: {:?}", elapsed);
|
||||
assert!(elapsed < Duration::from_millis(100), "Registry ops too slow: {elapsed:?}");
|
||||
}
|
||||
|
||||
// Performance requirements validation
|
||||
@@ -294,7 +294,7 @@ fn test_performance_requirements() {
|
||||
// Simulate processing 3000 events worth of operations
|
||||
for i in 0..3000 {
|
||||
// Simulate event name parsing and processing
|
||||
let _event_id = format!("s3:ObjectCreated:Put_{}", i);
|
||||
let _event_id = format!("s3:ObjectCreated:Put_{i}");
|
||||
let _timestamp = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
// Simulate basic audit entry creation overhead
|
||||
@@ -305,16 +305,16 @@ fn test_performance_requirements() {
|
||||
let elapsed = start.elapsed();
|
||||
let eps = 3000.0 / elapsed.as_secs_f64();
|
||||
|
||||
println!("Simulated 3000 events in {:?} ({:.0} EPS)", elapsed, eps);
|
||||
println!("Simulated 3000 events in {elapsed:?} ({eps:.0} EPS)");
|
||||
|
||||
// Our core processing should easily handle 3k EPS worth of CPU overhead
|
||||
// The actual EPS limit will be determined by network I/O to targets
|
||||
assert!(eps > 10000.0, "Core processing too slow for 3k EPS target: {} EPS", eps);
|
||||
assert!(eps > 10000.0, "Core processing too slow for 3k EPS target: {eps} EPS");
|
||||
|
||||
// P99 latency requirement: < 30ms
|
||||
// For core processing, we should be much faster than this
|
||||
let avg_latency = elapsed / 3000;
|
||||
println!("Average processing latency: {:?}", avg_latency);
|
||||
println!("Average processing latency: {avg_latency:?}");
|
||||
|
||||
assert!(avg_latency < Duration::from_millis(1), "Processing latency too high: {:?}", avg_latency);
|
||||
assert!(avg_latency < Duration::from_millis(1), "Processing latency too high: {avg_latency:?}");
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ async fn test_complete_audit_system_lifecycle() {
|
||||
assert_eq!(system.get_state().await, system::AuditSystemState::Running);
|
||||
}
|
||||
Err(e) => {
|
||||
panic!("Unexpected error: {}", e);
|
||||
panic!("Unexpected error: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ async fn test_audit_log_dispatch_with_no_targets() {
|
||||
// Also acceptable since system not running
|
||||
}
|
||||
Err(e) => {
|
||||
panic!("Unexpected error: {}", e);
|
||||
panic!("Unexpected error: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,7 +172,7 @@ async fn test_config_parsing_with_multiple_instances() {
|
||||
// Expected - parsing worked but save failed
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Config parsing error: {}", e);
|
||||
println!("Config parsing error: {e}");
|
||||
// Other errors might indicate parsing issues, but not necessarily failures
|
||||
}
|
||||
Ok(_) => {
|
||||
@@ -261,7 +261,7 @@ async fn test_concurrent_operations() {
|
||||
let (i, state, is_running) = task.await.expect("Task should complete");
|
||||
assert_eq!(state, system::AuditSystemState::Stopped);
|
||||
assert!(!is_running);
|
||||
println!("Task {} completed successfully", i);
|
||||
println!("Task {i} completed successfully");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,8 +295,8 @@ async fn test_performance_under_load() {
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
println!("100 concurrent dispatches took: {:?}", elapsed);
|
||||
println!("Successes: {}, Errors: {}", success_count, error_count);
|
||||
println!("100 concurrent dispatches took: {elapsed:?}");
|
||||
println!("Successes: {success_count}, Errors: {error_count}");
|
||||
|
||||
// Should complete reasonably quickly
|
||||
assert!(elapsed < Duration::from_secs(5), "Concurrent operations took too long");
|
||||
@@ -318,14 +318,14 @@ fn create_sample_audit_entry_with_id(id: u32) -> AuditEntry {
|
||||
use std::collections::HashMap;
|
||||
|
||||
let mut req_header = HashMap::new();
|
||||
req_header.insert("authorization".to_string(), format!("Bearer test-token-{}", id));
|
||||
req_header.insert("authorization".to_string(), format!("Bearer test-token-{id}"));
|
||||
req_header.insert("content-type".to_string(), "application/octet-stream".to_string());
|
||||
|
||||
let mut resp_header = HashMap::new();
|
||||
resp_header.insert("x-response".to_string(), "ok".to_string());
|
||||
|
||||
let mut tags = HashMap::new();
|
||||
tags.insert(format!("tag-{}", id), json!("sample"));
|
||||
tags.insert(format!("tag-{id}"), json!("sample"));
|
||||
|
||||
let mut req_query = HashMap::new();
|
||||
req_query.insert("id".to_string(), id.to_string());
|
||||
@@ -333,7 +333,7 @@ fn create_sample_audit_entry_with_id(id: u32) -> AuditEntry {
|
||||
let api_details = ApiDetails {
|
||||
name: Some("PutObject".to_string()),
|
||||
bucket: Some("test-bucket".to_string()),
|
||||
object: Some(format!("test-object-{}", id)),
|
||||
object: Some(format!("test-object-{id}")),
|
||||
status: Some("success".to_string()),
|
||||
status_code: Some(200),
|
||||
input_bytes: Some(1024),
|
||||
@@ -348,7 +348,7 @@ fn create_sample_audit_entry_with_id(id: u32) -> AuditEntry {
|
||||
|
||||
AuditEntry {
|
||||
version: "1".to_string(),
|
||||
deployment_id: Some(format!("test-deployment-{}", id)),
|
||||
deployment_id: Some(format!("test-deployment-{id}")),
|
||||
site_name: Some("test-site".to_string()),
|
||||
time: Utc::now(),
|
||||
event: EventName::ObjectCreatedPut,
|
||||
@@ -356,9 +356,9 @@ fn create_sample_audit_entry_with_id(id: u32) -> AuditEntry {
|
||||
trigger: "api".to_string(),
|
||||
api: api_details,
|
||||
remote_host: Some("127.0.0.1".to_string()),
|
||||
request_id: Some(format!("test-request-{}", id)),
|
||||
request_id: Some(format!("test-request-{id}")),
|
||||
user_agent: Some("test-agent".to_string()),
|
||||
req_path: Some(format!("/test-bucket/test-object-{}", id)),
|
||||
req_path: Some(format!("/test-bucket/test-object-{id}")),
|
||||
req_host: Some("test-host".to_string()),
|
||||
req_node: Some("node-1".to_string()),
|
||||
req_claims: None,
|
||||
@@ -366,8 +366,8 @@ fn create_sample_audit_entry_with_id(id: u32) -> AuditEntry {
|
||||
req_header: Some(req_header),
|
||||
resp_header: Some(resp_header),
|
||||
tags: Some(tags),
|
||||
access_key: Some(format!("AKIA{}", id)),
|
||||
parent_user: Some(format!("parent-{}", id)),
|
||||
access_key: Some(format!("AKIA{id}")),
|
||||
parent_user: Some(format!("parent-{id}")),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,12 +126,6 @@ pub const DEFAULT_LOG_FILENAME: &str = "rustfs";
|
||||
/// Default value: rustfs.log
|
||||
pub const DEFAULT_OBS_LOG_FILENAME: &str = concat!(DEFAULT_LOG_FILENAME, "");
|
||||
|
||||
/// Default sink file log file for rustfs
|
||||
/// This is the default sink file log file for rustfs.
|
||||
/// It is used to store the logs of the application.
|
||||
/// Default value: rustfs-sink.log
|
||||
pub const DEFAULT_SINK_FILE_LOG_FILE: &str = concat!(DEFAULT_LOG_FILENAME, "-sink.log");
|
||||
|
||||
/// Default log directory for rustfs
|
||||
/// This is the default log directory for rustfs.
|
||||
/// It is used to store the logs of the application.
|
||||
@@ -160,16 +154,6 @@ pub const DEFAULT_LOG_ROTATION_TIME: &str = "day";
|
||||
/// Environment variable: RUSTFS_OBS_LOG_KEEP_FILES
|
||||
pub const DEFAULT_LOG_KEEP_FILES: u16 = 30;
|
||||
|
||||
/// This is the external address for rustfs to access endpoint (used in Docker deployments).
|
||||
/// This should match the mapped host port when using Docker port mapping.
|
||||
/// Example: ":9020" when mapping host port 9020 to container port 9000.
|
||||
/// Default value: DEFAULT_ADDRESS
|
||||
/// Environment variable: RUSTFS_EXTERNAL_ADDRESS
|
||||
/// Command line argument: --external-address
|
||||
/// Example: RUSTFS_EXTERNAL_ADDRESS=":9020"
|
||||
/// Example: --external-address ":9020"
|
||||
pub const ENV_EXTERNAL_ADDRESS: &str = "RUSTFS_EXTERNAL_ADDRESS";
|
||||
|
||||
/// 1 KiB
|
||||
pub const KI_B: usize = 1024;
|
||||
/// 1 MiB
|
||||
|
||||
@@ -101,6 +101,7 @@ aws-credential-types = { workspace = true }
|
||||
aws-smithy-types = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
moka = { workspace = true }
|
||||
base64-simd.workspace = true
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
nix = { workspace = true }
|
||||
|
||||
@@ -17,12 +17,10 @@ pub mod datatypes;
|
||||
mod replication_pool;
|
||||
mod replication_resyncer;
|
||||
mod replication_state;
|
||||
mod replication_type;
|
||||
mod rule;
|
||||
|
||||
pub use config::*;
|
||||
pub use datatypes::*;
|
||||
pub use replication_pool::*;
|
||||
pub use replication_resyncer::*;
|
||||
pub use replication_type::*;
|
||||
pub use rule::*;
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
use crate::StorageAPI;
|
||||
use crate::bucket::replication::MrfReplicateEntry;
|
||||
use crate::bucket::replication::ReplicateDecision;
|
||||
use crate::bucket::replication::ReplicateObjectInfo;
|
||||
use crate::bucket::replication::ReplicationWorkerOperation;
|
||||
use crate::bucket::replication::ResyncDecision;
|
||||
use crate::bucket::replication::ResyncOpts;
|
||||
use crate::bucket::replication::ResyncStatusType;
|
||||
use crate::bucket::replication::replicate_delete;
|
||||
@@ -18,16 +13,21 @@ use crate::bucket::replication::replication_resyncer::{
|
||||
BucketReplicationResyncStatus, DeletedObjectReplicationInfo, ReplicationResyncer,
|
||||
};
|
||||
use crate::bucket::replication::replication_state::ReplicationStats;
|
||||
use crate::bucket::replication::replication_statuses_map;
|
||||
use crate::bucket::replication::version_purge_statuses_map;
|
||||
use crate::config::com::read_config;
|
||||
use crate::error::Error as EcstoreError;
|
||||
use crate::store_api::ObjectInfo;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use rustfs_filemeta::MrfReplicateEntry;
|
||||
use rustfs_filemeta::ReplicateDecision;
|
||||
use rustfs_filemeta::ReplicateObjectInfo;
|
||||
use rustfs_filemeta::ReplicatedTargetInfo;
|
||||
use rustfs_filemeta::ReplicationStatusType;
|
||||
use rustfs_filemeta::ReplicationType;
|
||||
use rustfs_filemeta::ReplicationWorkerOperation;
|
||||
use rustfs_filemeta::ResyncDecision;
|
||||
use rustfs_filemeta::replication_statuses_map;
|
||||
use rustfs_filemeta::version_purge_statuses_map;
|
||||
use rustfs_utils::http::RESERVED_METADATA_PREFIX_LOWER;
|
||||
use time::OffsetDateTime;
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
@@ -996,7 +996,7 @@ pub async fn schedule_replication<S: StorageAPI>(oi: ObjectInfo, o: Arc<S>, dsc:
|
||||
target_purge_statuses: purge_statuses,
|
||||
replication_timestamp: tm,
|
||||
user_tags: oi.user_tags,
|
||||
checksum: vec![],
|
||||
checksum: None,
|
||||
retry_count: 0,
|
||||
event_type: "".to_string(),
|
||||
existing_obj_resync: ResyncDecision::default(),
|
||||
|
||||
@@ -2,12 +2,8 @@ use crate::bucket::bucket_target_sys::{
|
||||
AdvancedPutOptions, BucketTargetSys, PutObjectOptions, PutObjectPartOptions, RemoveObjectOptions, TargetClient,
|
||||
};
|
||||
use crate::bucket::metadata_sys;
|
||||
use crate::bucket::replication::{MrfReplicateEntry, ReplicationWorkerOperation, ResyncStatusType};
|
||||
use crate::bucket::replication::{
|
||||
ObjectOpts, REPLICATE_EXISTING, REPLICATE_EXISTING_DELETE, REPLICATION_RESET, ReplicateObjectInfo,
|
||||
ReplicationConfigurationExt as _, ResyncTargetDecision, get_replication_state, parse_replicate_decision,
|
||||
replication_statuses_map, target_reset_header, version_purge_statuses_map,
|
||||
};
|
||||
use crate::bucket::replication::ResyncStatusType;
|
||||
use crate::bucket::replication::{ObjectOpts, ReplicationConfigurationExt as _};
|
||||
use crate::bucket::tagging::decode_tags_to_map;
|
||||
use crate::bucket::target::BucketTargets;
|
||||
use crate::bucket::versioning_sys::BucketVersioningSys;
|
||||
@@ -29,14 +25,17 @@ use byteorder::ByteOrder;
|
||||
use futures::future::join_all;
|
||||
use http::HeaderMap;
|
||||
|
||||
use regex::Regex;
|
||||
use rustfs_filemeta::{
|
||||
ReplicatedInfos, ReplicatedTargetInfo, ReplicationAction, ReplicationState, ReplicationStatusType, ReplicationType,
|
||||
VersionPurgeStatusType,
|
||||
MrfReplicateEntry, REPLICATE_EXISTING, REPLICATE_EXISTING_DELETE, REPLICATION_RESET, ReplicateDecision, ReplicateObjectInfo,
|
||||
ReplicateTargetDecision, ReplicatedInfos, ReplicatedTargetInfo, ReplicationAction, ReplicationState, ReplicationStatusType,
|
||||
ReplicationType, ReplicationWorkerOperation, ResyncDecision, ResyncTargetDecision, VersionPurgeStatusType,
|
||||
get_replication_state, parse_replicate_decision, replication_statuses_map, target_reset_header, version_purge_statuses_map,
|
||||
};
|
||||
use rustfs_utils::http::{
|
||||
AMZ_BUCKET_REPLICATION_STATUS, AMZ_OBJECT_TAGGING, AMZ_TAGGING_DIRECTIVE, CONTENT_ENCODING, HeaderExt as _,
|
||||
RESERVED_METADATA_PREFIX, RESERVED_METADATA_PREFIX_LOWER, RUSTFS_REPLICATION_AUTUAL_OBJECT_SIZE, SSEC_ALGORITHM_HEADER,
|
||||
SSEC_KEY_HEADER, SSEC_KEY_MD5_HEADER, headers,
|
||||
RESERVED_METADATA_PREFIX, RESERVED_METADATA_PREFIX_LOWER, RUSTFS_REPLICATION_AUTUAL_OBJECT_SIZE,
|
||||
RUSTFS_REPLICATION_RESET_STATUS, SSEC_ALGORITHM_HEADER, SSEC_KEY_HEADER, SSEC_KEY_MD5_HEADER, headers,
|
||||
};
|
||||
use rustfs_utils::path::path_join_buf;
|
||||
use rustfs_utils::string::strings_has_prefix_fold;
|
||||
@@ -56,9 +55,6 @@ use tokio::time::Duration as TokioDuration;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use super::replication_type::{ReplicateDecision, ReplicateTargetDecision, ResyncDecision};
|
||||
use regex::Regex;
|
||||
|
||||
const REPLICATION_DIR: &str = ".replication";
|
||||
const RESYNC_FILE_NAME: &str = "resync.bin";
|
||||
const RESYNC_META_FORMAT: u16 = 1;
|
||||
@@ -663,7 +659,7 @@ pub async fn get_heal_replicate_object_info(oi: &ObjectInfo, rcfg: &ReplicationC
|
||||
replication_timestamp: None,
|
||||
ssec: false, // TODO: add ssec support
|
||||
user_tags: oi.user_tags.clone(),
|
||||
checksum: Vec::new(),
|
||||
checksum: oi.checksum.clone(),
|
||||
retry_count: 0,
|
||||
}
|
||||
}
|
||||
@@ -849,7 +845,7 @@ impl ReplicationConfig {
|
||||
{
|
||||
resync_decision.targets.insert(
|
||||
decision.arn.clone(),
|
||||
ResyncTargetDecision::resync_target(
|
||||
resync_target(
|
||||
&oi,
|
||||
&target.arn,
|
||||
&target.reset_id,
|
||||
@@ -864,6 +860,59 @@ impl ReplicationConfig {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resync_target(
|
||||
oi: &ObjectInfo,
|
||||
arn: &str,
|
||||
reset_id: &str,
|
||||
reset_before_date: Option<OffsetDateTime>,
|
||||
status: ReplicationStatusType,
|
||||
) -> ResyncTargetDecision {
|
||||
let rs = oi
|
||||
.user_defined
|
||||
.get(target_reset_header(arn).as_str())
|
||||
.or(oi.user_defined.get(RUSTFS_REPLICATION_RESET_STATUS))
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let mut dec = ResyncTargetDecision::default();
|
||||
|
||||
let mod_time = oi.mod_time.unwrap_or(OffsetDateTime::UNIX_EPOCH);
|
||||
|
||||
if rs.is_none() {
|
||||
let reset_before_date = reset_before_date.unwrap_or(OffsetDateTime::UNIX_EPOCH);
|
||||
if !reset_id.is_empty() && mod_time < reset_before_date {
|
||||
dec.replicate = true;
|
||||
return dec;
|
||||
}
|
||||
|
||||
dec.replicate = status == ReplicationStatusType::Empty;
|
||||
|
||||
return dec;
|
||||
}
|
||||
|
||||
if reset_id.is_empty() || reset_before_date.is_none() {
|
||||
return dec;
|
||||
}
|
||||
|
||||
let rs = rs.unwrap();
|
||||
let reset_before_date = reset_before_date.unwrap();
|
||||
|
||||
let parts: Vec<&str> = rs.splitn(2, ';').collect();
|
||||
|
||||
if parts.len() != 2 {
|
||||
return dec;
|
||||
}
|
||||
|
||||
let new_reset = parts[0] == reset_id;
|
||||
|
||||
if !new_reset && status == ReplicationStatusType::Completed {
|
||||
return dec;
|
||||
}
|
||||
|
||||
dec.replicate = new_reset && mod_time < reset_before_date;
|
||||
|
||||
dec
|
||||
}
|
||||
|
||||
pub struct MustReplicateOptions {
|
||||
meta: HashMap<String, String>,
|
||||
status: ReplicationStatusType,
|
||||
@@ -933,7 +982,7 @@ pub async fn check_replicate_delete(
|
||||
let rcfg = match get_replication_config(bucket).await {
|
||||
Ok(Some(config)) => config,
|
||||
Ok(None) => {
|
||||
warn!("No replication config found for bucket: {}", bucket);
|
||||
// warn!("No replication config found for bucket: {}", bucket);
|
||||
return ReplicateDecision::default();
|
||||
}
|
||||
Err(err) => {
|
||||
|
||||
@@ -1,470 +0,0 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::store_api::ObjectInfo;
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
use rustfs_filemeta::VersionPurgeStatusType;
|
||||
use rustfs_filemeta::{ReplicatedInfos, ReplicationType};
|
||||
use rustfs_filemeta::{ReplicationState, ReplicationStatusType};
|
||||
use rustfs_utils::http::RESERVED_METADATA_PREFIX_LOWER;
|
||||
use rustfs_utils::http::RUSTFS_REPLICATION_RESET_STATUS;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use time::OffsetDateTime;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub const REPLICATION_RESET: &str = "replication-reset";
|
||||
pub const REPLICATION_STATUS: &str = "replication-status";
|
||||
|
||||
// ReplicateQueued - replication being queued trail
|
||||
pub const REPLICATE_QUEUED: &str = "replicate:queue";
|
||||
|
||||
// ReplicateExisting - audit trail for existing objects replication
|
||||
pub const REPLICATE_EXISTING: &str = "replicate:existing";
|
||||
// ReplicateExistingDelete - audit trail for delete replication triggered for existing delete markers
|
||||
pub const REPLICATE_EXISTING_DELETE: &str = "replicate:existing:delete";
|
||||
|
||||
// ReplicateMRF - audit trail for replication from Most Recent Failures (MRF) queue
|
||||
pub const REPLICATE_MRF: &str = "replicate:mrf";
|
||||
// ReplicateIncoming - audit trail of inline replication
|
||||
pub const REPLICATE_INCOMING: &str = "replicate:incoming";
|
||||
// ReplicateIncomingDelete - audit trail of inline replication of deletes.
|
||||
pub const REPLICATE_INCOMING_DELETE: &str = "replicate:incoming:delete";
|
||||
|
||||
// ReplicateHeal - audit trail for healing of failed/pending replications
|
||||
pub const REPLICATE_HEAL: &str = "replicate:heal";
|
||||
// ReplicateHealDelete - audit trail of healing of failed/pending delete replications.
|
||||
pub const REPLICATE_HEAL_DELETE: &str = "replicate:heal:delete";
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct MrfReplicateEntry {
|
||||
#[serde(rename = "bucket")]
|
||||
pub bucket: String,
|
||||
|
||||
#[serde(rename = "object")]
|
||||
pub object: String,
|
||||
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub version_id: Option<Uuid>,
|
||||
|
||||
#[serde(rename = "retryCount")]
|
||||
pub retry_count: i32,
|
||||
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub size: i64,
|
||||
}
|
||||
|
||||
pub trait ReplicationWorkerOperation: Any + Send + Sync {
|
||||
fn to_mrf_entry(&self) -> MrfReplicateEntry;
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn get_bucket(&self) -> &str;
|
||||
fn get_object(&self) -> &str;
|
||||
fn get_size(&self) -> i64;
|
||||
fn is_delete_marker(&self) -> bool;
|
||||
fn get_op_type(&self) -> ReplicationType;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ReplicateTargetDecision {
|
||||
pub replicate: bool,
|
||||
pub synchronous: bool,
|
||||
pub arn: String,
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
impl ReplicateTargetDecision {
|
||||
pub fn new(arn: String, replicate: bool, sync: bool) -> Self {
|
||||
Self {
|
||||
replicate,
|
||||
synchronous: sync,
|
||||
arn,
|
||||
id: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ReplicateTargetDecision {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{};{};{};{}", self.replicate, self.synchronous, self.arn, self.id)
|
||||
}
|
||||
}
|
||||
|
||||
/// ReplicateDecision represents replication decision for each target
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ReplicateDecision {
|
||||
pub targets_map: HashMap<String, ReplicateTargetDecision>,
|
||||
}
|
||||
|
||||
impl ReplicateDecision {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
targets_map: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if at least one target qualifies for replication
|
||||
pub fn replicate_any(&self) -> bool {
|
||||
self.targets_map.values().any(|t| t.replicate)
|
||||
}
|
||||
|
||||
/// Returns true if at least one target qualifies for synchronous replication
|
||||
pub fn is_synchronous(&self) -> bool {
|
||||
self.targets_map.values().any(|t| t.synchronous)
|
||||
}
|
||||
|
||||
/// Updates ReplicateDecision with target's replication decision
|
||||
pub fn set(&mut self, target: ReplicateTargetDecision) {
|
||||
self.targets_map.insert(target.arn.clone(), target);
|
||||
}
|
||||
|
||||
/// Returns a stringified representation of internal replication status with all targets marked as `PENDING`
|
||||
pub fn pending_status(&self) -> Option<String> {
|
||||
let mut result = String::new();
|
||||
for target in self.targets_map.values() {
|
||||
if target.replicate {
|
||||
result.push_str(&format!("{}={};", target.arn, ReplicationStatusType::Pending.as_str()));
|
||||
}
|
||||
}
|
||||
if result.is_empty() { None } else { Some(result) }
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ReplicateDecision {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut result = String::new();
|
||||
for (key, value) in &self.targets_map {
|
||||
result.push_str(&format!("{key}={value},"));
|
||||
}
|
||||
write!(f, "{}", result.trim_end_matches(','))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ReplicateDecision {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
// parse k-v pairs of target ARN to stringified ReplicateTargetDecision delimited by ',' into a
|
||||
// ReplicateDecision struct
|
||||
pub fn parse_replicate_decision(_bucket: &str, s: &str) -> Result<ReplicateDecision> {
|
||||
let mut decision = ReplicateDecision::new();
|
||||
|
||||
if s.is_empty() {
|
||||
return Ok(decision);
|
||||
}
|
||||
|
||||
for p in s.split(',') {
|
||||
if p.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let slc = p.split('=').collect::<Vec<&str>>();
|
||||
if slc.len() != 2 {
|
||||
return Err(Error::other(format!("invalid replicate decision format: {s}")));
|
||||
}
|
||||
|
||||
let tgt_str = slc[1].trim_matches('"');
|
||||
let tgt = tgt_str.split(';').collect::<Vec<&str>>();
|
||||
if tgt.len() != 4 {
|
||||
return Err(Error::other(format!("invalid replicate decision format: {s}")));
|
||||
}
|
||||
|
||||
let tgt = ReplicateTargetDecision {
|
||||
replicate: tgt[0] == "true",
|
||||
synchronous: tgt[1] == "true",
|
||||
arn: tgt[2].to_string(),
|
||||
id: tgt[3].to_string(),
|
||||
};
|
||||
decision.targets_map.insert(slc[0].to_string(), tgt);
|
||||
}
|
||||
|
||||
Ok(decision)
|
||||
|
||||
// r = ReplicateDecision{
|
||||
// targetsMap: make(map[string]replicateTargetDecision),
|
||||
// }
|
||||
// if len(s) == 0 {
|
||||
// return
|
||||
// }
|
||||
// for _, p := range strings.Split(s, ",") {
|
||||
// if p == "" {
|
||||
// continue
|
||||
// }
|
||||
// slc := strings.Split(p, "=")
|
||||
// if len(slc) != 2 {
|
||||
// return r, errInvalidReplicateDecisionFormat
|
||||
// }
|
||||
// tgtStr := strings.TrimSuffix(strings.TrimPrefix(slc[1], `"`), `"`)
|
||||
// tgt := strings.Split(tgtStr, ";")
|
||||
// if len(tgt) != 4 {
|
||||
// return r, errInvalidReplicateDecisionFormat
|
||||
// }
|
||||
// r.targetsMap[slc[0]] = replicateTargetDecision{Replicate: tgt[0] == "true", Synchronous: tgt[1] == "true", Arn: tgt[2], ID: tgt[3]}
|
||||
// }
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ResyncTargetDecision {
|
||||
pub replicate: bool,
|
||||
pub reset_id: String,
|
||||
pub reset_before_date: Option<OffsetDateTime>,
|
||||
}
|
||||
|
||||
pub fn target_reset_header(arn: &str) -> String {
|
||||
format!("{RESERVED_METADATA_PREFIX_LOWER}{REPLICATION_RESET}-{arn}")
|
||||
}
|
||||
|
||||
impl ResyncTargetDecision {
|
||||
pub fn resync_target(
|
||||
oi: &ObjectInfo,
|
||||
arn: &str,
|
||||
reset_id: &str,
|
||||
reset_before_date: Option<OffsetDateTime>,
|
||||
status: ReplicationStatusType,
|
||||
) -> Self {
|
||||
let rs = oi
|
||||
.user_defined
|
||||
.get(target_reset_header(arn).as_str())
|
||||
.or(oi.user_defined.get(RUSTFS_REPLICATION_RESET_STATUS))
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let mut dec = Self::default();
|
||||
|
||||
let mod_time = oi.mod_time.unwrap_or(OffsetDateTime::UNIX_EPOCH);
|
||||
|
||||
if rs.is_none() {
|
||||
let reset_before_date = reset_before_date.unwrap_or(OffsetDateTime::UNIX_EPOCH);
|
||||
if !reset_id.is_empty() && mod_time < reset_before_date {
|
||||
dec.replicate = true;
|
||||
return dec;
|
||||
}
|
||||
|
||||
dec.replicate = status == ReplicationStatusType::Empty;
|
||||
|
||||
return dec;
|
||||
}
|
||||
|
||||
if reset_id.is_empty() || reset_before_date.is_none() {
|
||||
return dec;
|
||||
}
|
||||
|
||||
let rs = rs.unwrap();
|
||||
let reset_before_date = reset_before_date.unwrap();
|
||||
|
||||
let parts: Vec<&str> = rs.splitn(2, ';').collect();
|
||||
|
||||
if parts.len() != 2 {
|
||||
return dec;
|
||||
}
|
||||
|
||||
let new_reset = parts[0] == reset_id;
|
||||
|
||||
if !new_reset && status == ReplicationStatusType::Completed {
|
||||
return dec;
|
||||
}
|
||||
|
||||
dec.replicate = new_reset && mod_time < reset_before_date;
|
||||
|
||||
dec
|
||||
}
|
||||
}
|
||||
|
||||
/// ResyncDecision is a struct representing a map with target's individual resync decisions
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ResyncDecision {
|
||||
pub targets: HashMap<String, ResyncTargetDecision>,
|
||||
}
|
||||
|
||||
impl ResyncDecision {
|
||||
pub fn new() -> Self {
|
||||
Self { targets: HashMap::new() }
|
||||
}
|
||||
|
||||
/// Returns true if no targets with resync decision present
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.targets.is_empty()
|
||||
}
|
||||
|
||||
pub fn must_resync(&self) -> bool {
|
||||
self.targets.values().any(|v| v.replicate)
|
||||
}
|
||||
|
||||
pub fn must_resync_target(&self, tgt_arn: &str) -> bool {
|
||||
self.targets.get(tgt_arn).map(|v| v.replicate).unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ResyncDecision {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ReplicateObjectInfo {
|
||||
pub name: String,
|
||||
pub size: i64,
|
||||
pub actual_size: i64,
|
||||
pub bucket: String,
|
||||
pub version_id: Option<Uuid>,
|
||||
pub etag: Option<String>,
|
||||
pub mod_time: Option<OffsetDateTime>,
|
||||
pub replication_status: ReplicationStatusType,
|
||||
pub replication_status_internal: Option<String>,
|
||||
pub delete_marker: bool,
|
||||
pub version_purge_status_internal: Option<String>,
|
||||
pub version_purge_status: VersionPurgeStatusType,
|
||||
pub replication_state: Option<ReplicationState>,
|
||||
pub op_type: ReplicationType,
|
||||
pub event_type: String,
|
||||
pub dsc: ReplicateDecision,
|
||||
pub existing_obj_resync: ResyncDecision,
|
||||
pub target_statuses: HashMap<String, ReplicationStatusType>,
|
||||
pub target_purge_statuses: HashMap<String, VersionPurgeStatusType>,
|
||||
pub replication_timestamp: Option<OffsetDateTime>,
|
||||
pub ssec: bool,
|
||||
pub user_tags: String,
|
||||
pub checksum: Vec<u8>,
|
||||
pub retry_count: u32,
|
||||
}
|
||||
|
||||
impl ReplicationWorkerOperation for ReplicateObjectInfo {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn to_mrf_entry(&self) -> MrfReplicateEntry {
|
||||
MrfReplicateEntry {
|
||||
bucket: self.bucket.clone(),
|
||||
object: self.name.clone(),
|
||||
version_id: self.version_id,
|
||||
retry_count: self.retry_count as i32,
|
||||
size: self.size,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_bucket(&self) -> &str {
|
||||
&self.bucket
|
||||
}
|
||||
|
||||
fn get_object(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn get_size(&self) -> i64 {
|
||||
self.size
|
||||
}
|
||||
|
||||
fn is_delete_marker(&self) -> bool {
|
||||
self.delete_marker
|
||||
}
|
||||
|
||||
fn get_op_type(&self) -> ReplicationType {
|
||||
self.op_type
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref REPL_STATUS_REGEX: Regex = Regex::new(r"([^=].*?)=([^,].*?);").unwrap();
|
||||
}
|
||||
|
||||
impl ReplicateObjectInfo {
|
||||
/// Returns replication status of a target
|
||||
pub fn target_replication_status(&self, arn: &str) -> ReplicationStatusType {
|
||||
let binding = self.replication_status_internal.clone().unwrap_or_default();
|
||||
let captures = REPL_STATUS_REGEX.captures_iter(&binding);
|
||||
for cap in captures {
|
||||
if cap.len() == 3 && &cap[1] == arn {
|
||||
return ReplicationStatusType::from(&cap[2]);
|
||||
}
|
||||
}
|
||||
ReplicationStatusType::default()
|
||||
}
|
||||
|
||||
/// Returns the relevant info needed by MRF
|
||||
pub fn to_mrf_entry(&self) -> MrfReplicateEntry {
|
||||
MrfReplicateEntry {
|
||||
bucket: self.bucket.clone(),
|
||||
object: self.name.clone(),
|
||||
version_id: self.version_id,
|
||||
retry_count: self.retry_count as i32,
|
||||
size: self.size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// constructs a replication status map from string representation
|
||||
pub fn replication_statuses_map(s: &str) -> HashMap<String, ReplicationStatusType> {
|
||||
let mut targets = HashMap::new();
|
||||
let rep_stat_matches = REPL_STATUS_REGEX.captures_iter(s).map(|c| c.extract());
|
||||
for (_, [arn, status]) in rep_stat_matches {
|
||||
if arn.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let status = ReplicationStatusType::from(status);
|
||||
targets.insert(arn.to_string(), status);
|
||||
}
|
||||
targets
|
||||
}
|
||||
|
||||
// constructs a version purge status map from string representation
|
||||
pub fn version_purge_statuses_map(s: &str) -> HashMap<String, VersionPurgeStatusType> {
|
||||
let mut targets = HashMap::new();
|
||||
let purge_status_matches = REPL_STATUS_REGEX.captures_iter(s).map(|c| c.extract());
|
||||
for (_, [arn, status]) in purge_status_matches {
|
||||
if arn.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let status = VersionPurgeStatusType::from(status);
|
||||
targets.insert(arn.to_string(), status);
|
||||
}
|
||||
targets
|
||||
}
|
||||
|
||||
pub fn get_replication_state(rinfos: &ReplicatedInfos, prev_state: &ReplicationState, _vid: Option<String>) -> ReplicationState {
|
||||
let reset_status_map: Vec<(String, String)> = rinfos
|
||||
.targets
|
||||
.iter()
|
||||
.filter(|v| !v.resync_timestamp.is_empty())
|
||||
.map(|t| (target_reset_header(t.arn.as_str()), t.resync_timestamp.clone()))
|
||||
.collect();
|
||||
|
||||
let repl_statuses = rinfos.replication_status_internal();
|
||||
let vpurge_statuses = rinfos.version_purge_status_internal();
|
||||
|
||||
let mut reset_statuses_map = prev_state.reset_statuses_map.clone();
|
||||
for (key, value) in reset_status_map {
|
||||
reset_statuses_map.insert(key, value);
|
||||
}
|
||||
|
||||
ReplicationState {
|
||||
replicate_decision_str: prev_state.replicate_decision_str.clone(),
|
||||
reset_statuses_map,
|
||||
replica_timestamp: prev_state.replica_timestamp,
|
||||
replica_status: prev_state.replica_status.clone(),
|
||||
targets: replication_statuses_map(&repl_statuses.clone().unwrap_or_default()),
|
||||
replication_status_internal: repl_statuses,
|
||||
replication_timestamp: rinfos.replication_timestamp,
|
||||
purge_targets: version_purge_statuses_map(&vpurge_statuses.clone().unwrap_or_default()),
|
||||
version_purge_status_internal: vpurge_statuses,
|
||||
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,8 @@ use s3s::header::{
|
||||
X_AMZ_STORAGE_CLASS, X_AMZ_WEBSITE_REDIRECT_LOCATION,
|
||||
};
|
||||
//use crate::disk::{BufferReader, Reader};
|
||||
use crate::checksum::ChecksumMode;
|
||||
use crate::client::checksum::ChecksumMode;
|
||||
use crate::client::utils::base64_encode;
|
||||
use crate::client::{
|
||||
api_error_response::{err_entity_too_large, err_invalid_argument},
|
||||
api_put_object_common::optimal_part_info,
|
||||
@@ -41,7 +42,6 @@ use crate::client::{
|
||||
transition_api::{ReaderImpl, TransitionClient, UploadInfo},
|
||||
utils::{is_amz_header, is_minio_header, is_rustfs_header, is_standard_header, is_storageclass_header},
|
||||
};
|
||||
use rustfs_utils::crypto::base64_encode;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AdvancedPutOptions {
|
||||
|
||||
@@ -25,7 +25,8 @@ use time::OffsetDateTime;
|
||||
use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::checksum::ChecksumMode;
|
||||
use crate::client::checksum::ChecksumMode;
|
||||
use crate::client::utils::base64_encode;
|
||||
use crate::client::{
|
||||
api_error_response::{
|
||||
err_entity_too_large, err_entity_too_small, err_invalid_argument, http_resp_to_error_response, to_error_response,
|
||||
@@ -38,7 +39,7 @@ use crate::client::{
|
||||
constants::{ISO8601_DATEFORMAT, MAX_PART_SIZE, MAX_SINGLE_PUT_OBJECT_SIZE},
|
||||
transition_api::{ReaderImpl, RequestMetadata, TransitionClient, UploadInfo},
|
||||
};
|
||||
use rustfs_utils::{crypto::base64_encode, path::trim_etag};
|
||||
use rustfs_utils::path::trim_etag;
|
||||
use s3s::header::{X_AMZ_EXPIRATION, X_AMZ_VERSION_ID};
|
||||
|
||||
impl TransitionClient {
|
||||
|
||||
@@ -29,7 +29,7 @@ use tokio_util::sync::CancellationToken;
|
||||
use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::checksum::{ChecksumMode, add_auto_checksum_headers, apply_auto_checksum};
|
||||
use crate::client::checksum::{ChecksumMode, add_auto_checksum_headers, apply_auto_checksum};
|
||||
use crate::client::{
|
||||
api_error_response::{err_invalid_argument, err_unexpected_eof, http_resp_to_error_response},
|
||||
api_put_object::PutObjectOptions,
|
||||
@@ -40,7 +40,8 @@ use crate::client::{
|
||||
transition_api::{ReaderImpl, RequestMetadata, TransitionClient, UploadInfo},
|
||||
};
|
||||
|
||||
use rustfs_utils::{crypto::base64_encode, path::trim_etag};
|
||||
use crate::client::utils::base64_encode;
|
||||
use rustfs_utils::path::trim_etag;
|
||||
use s3s::header::{X_AMZ_EXPIRATION, X_AMZ_VERSION_ID};
|
||||
|
||||
pub struct UploadedPartRes {
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
use bytes::Bytes;
|
||||
use http::{HeaderMap, HeaderValue, Method, StatusCode};
|
||||
use rustfs_utils::{HashAlgorithm, crypto::base64_encode};
|
||||
use rustfs_utils::HashAlgorithm;
|
||||
use s3s::S3ErrorCode;
|
||||
use s3s::dto::ReplicationStatus;
|
||||
use s3s::header::X_AMZ_BYPASS_GOVERNANCE_RETENTION;
|
||||
@@ -29,6 +29,7 @@ use std::{collections::HashMap, sync::Arc};
|
||||
use time::OffsetDateTime;
|
||||
use tokio::sync::mpsc::{self, Receiver, Sender};
|
||||
|
||||
use crate::client::utils::base64_encode;
|
||||
use crate::client::{
|
||||
api_error_response::{ErrorResponse, http_resp_to_error_response, to_error_response},
|
||||
transition_api::{ReaderImpl, RequestMetadata, TransitionClient},
|
||||
|
||||
@@ -23,9 +23,9 @@ use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::checksum::ChecksumMode;
|
||||
use crate::client::checksum::ChecksumMode;
|
||||
use crate::client::transition_api::ObjectMultipartInfo;
|
||||
use rustfs_utils::crypto::base64_decode;
|
||||
use crate::client::utils::base64_decode;
|
||||
|
||||
use super::transition_api;
|
||||
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
#![allow(clippy::map_entry)]
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
#![allow(unused_imports)]
|
||||
#![allow(unused_variables)]
|
||||
#![allow(unused_mut)]
|
||||
#![allow(unused_assignments)]
|
||||
#![allow(unused_must_use)]
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use rustfs_checksums::ChecksumAlgorithm;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::client::utils::base64_decode;
|
||||
use crate::client::utils::base64_encode;
|
||||
use crate::client::{api_put_object::PutObjectOptions, api_s3_datatypes::ObjectPart};
|
||||
use crate::{disk::DiskAPI, store_api::GetObjectReader};
|
||||
use s3s::header::{
|
||||
X_AMZ_CHECKSUM_ALGORITHM, X_AMZ_CHECKSUM_CRC32, X_AMZ_CHECKSUM_CRC32C, X_AMZ_CHECKSUM_SHA1, X_AMZ_CHECKSUM_SHA256,
|
||||
};
|
||||
|
||||
use enumset::{EnumSet, EnumSetType, enum_set};
|
||||
|
||||
#[derive(Debug, EnumSetType, Default)]
|
||||
#[enumset(repr = "u8")]
|
||||
pub enum ChecksumMode {
|
||||
#[default]
|
||||
ChecksumNone,
|
||||
ChecksumSHA256,
|
||||
ChecksumSHA1,
|
||||
ChecksumCRC32,
|
||||
ChecksumCRC32C,
|
||||
ChecksumCRC64NVME,
|
||||
ChecksumFullObject,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref C_ChecksumMask: EnumSet<ChecksumMode> = {
|
||||
let mut s = EnumSet::all();
|
||||
s.remove(ChecksumMode::ChecksumFullObject);
|
||||
s
|
||||
};
|
||||
static ref C_ChecksumFullObjectCRC32: EnumSet<ChecksumMode> =
|
||||
enum_set!(ChecksumMode::ChecksumCRC32 | ChecksumMode::ChecksumFullObject);
|
||||
static ref C_ChecksumFullObjectCRC32C: EnumSet<ChecksumMode> =
|
||||
enum_set!(ChecksumMode::ChecksumCRC32C | ChecksumMode::ChecksumFullObject);
|
||||
}
|
||||
const AMZ_CHECKSUM_CRC64NVME: &str = "x-amz-checksum-crc64nvme";
|
||||
|
||||
impl ChecksumMode {
|
||||
//pub const CRC64_NVME_POLYNOMIAL: i64 = 0xad93d23594c93659;
|
||||
|
||||
pub fn base(&self) -> ChecksumMode {
|
||||
let s = EnumSet::from(*self).intersection(*C_ChecksumMask);
|
||||
match s.as_u8() {
|
||||
1_u8 => ChecksumMode::ChecksumNone,
|
||||
2_u8 => ChecksumMode::ChecksumSHA256,
|
||||
4_u8 => ChecksumMode::ChecksumSHA1,
|
||||
8_u8 => ChecksumMode::ChecksumCRC32,
|
||||
16_u8 => ChecksumMode::ChecksumCRC32C,
|
||||
32_u8 => ChecksumMode::ChecksumCRC64NVME,
|
||||
_ => panic!("enum err."),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is(&self, t: ChecksumMode) -> bool {
|
||||
*self & t == t
|
||||
}
|
||||
|
||||
pub fn key(&self) -> String {
|
||||
//match c & checksumMask {
|
||||
match self {
|
||||
ChecksumMode::ChecksumCRC32 => {
|
||||
return X_AMZ_CHECKSUM_CRC32.to_string();
|
||||
}
|
||||
ChecksumMode::ChecksumCRC32C => {
|
||||
return X_AMZ_CHECKSUM_CRC32C.to_string();
|
||||
}
|
||||
ChecksumMode::ChecksumSHA1 => {
|
||||
return X_AMZ_CHECKSUM_SHA1.to_string();
|
||||
}
|
||||
ChecksumMode::ChecksumSHA256 => {
|
||||
return X_AMZ_CHECKSUM_SHA256.to_string();
|
||||
}
|
||||
ChecksumMode::ChecksumCRC64NVME => {
|
||||
return AMZ_CHECKSUM_CRC64NVME.to_string();
|
||||
}
|
||||
_ => {
|
||||
return "".to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_composite(&self) -> bool {
|
||||
let s = EnumSet::from(*self).intersection(*C_ChecksumMask);
|
||||
match s.as_u8() {
|
||||
2_u8 => true,
|
||||
4_u8 => true,
|
||||
8_u8 => true,
|
||||
16_u8 => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_merge_crc(&self) -> bool {
|
||||
let s = EnumSet::from(*self).intersection(*C_ChecksumMask);
|
||||
match s.as_u8() {
|
||||
8_u8 => true,
|
||||
16_u8 => true,
|
||||
32_u8 => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn full_object_requested(&self) -> bool {
|
||||
let s = EnumSet::from(*self).intersection(*C_ChecksumMask);
|
||||
match s.as_u8() {
|
||||
//C_ChecksumFullObjectCRC32 as u8 => true,
|
||||
//C_ChecksumFullObjectCRC32C as u8 => true,
|
||||
32_u8 => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn key_capitalized(&self) -> String {
|
||||
self.key()
|
||||
}
|
||||
|
||||
pub fn raw_byte_len(&self) -> usize {
|
||||
let u = EnumSet::from(*self).intersection(*C_ChecksumMask).as_u8();
|
||||
if u == ChecksumMode::ChecksumCRC32 as u8 || u == ChecksumMode::ChecksumCRC32C as u8 {
|
||||
4
|
||||
} else if u == ChecksumMode::ChecksumSHA1 as u8 {
|
||||
use sha1::Digest;
|
||||
sha1::Sha1::output_size() as usize
|
||||
} else if u == ChecksumMode::ChecksumSHA256 as u8 {
|
||||
use sha2::Digest;
|
||||
sha2::Sha256::output_size() as usize
|
||||
} else if u == ChecksumMode::ChecksumCRC64NVME as u8 {
|
||||
8
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hasher(&self) -> Result<Box<dyn rustfs_checksums::http::HttpChecksum>, std::io::Error> {
|
||||
match /*C_ChecksumMask & **/self {
|
||||
ChecksumMode::ChecksumCRC32 => {
|
||||
return Ok(ChecksumAlgorithm::Crc32.into_impl());
|
||||
}
|
||||
ChecksumMode::ChecksumCRC32C => {
|
||||
return Ok(ChecksumAlgorithm::Crc32c.into_impl());
|
||||
}
|
||||
ChecksumMode::ChecksumSHA1 => {
|
||||
return Ok(ChecksumAlgorithm::Sha1.into_impl());
|
||||
}
|
||||
ChecksumMode::ChecksumSHA256 => {
|
||||
return Ok(ChecksumAlgorithm::Sha256.into_impl());
|
||||
}
|
||||
ChecksumMode::ChecksumCRC64NVME => {
|
||||
return Ok(ChecksumAlgorithm::Crc64Nvme.into_impl());
|
||||
}
|
||||
_ => return Err(std::io::Error::other("unsupported checksum type")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_set(&self) -> bool {
|
||||
let s = EnumSet::from(*self).intersection(*C_ChecksumMask);
|
||||
s.len() == 1
|
||||
}
|
||||
|
||||
pub fn set_default(&mut self, t: ChecksumMode) {
|
||||
if !self.is_set() {
|
||||
*self = t;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode_to_string(&self, b: &[u8]) -> Result<String, std::io::Error> {
|
||||
if !self.is_set() {
|
||||
return Ok("".to_string());
|
||||
}
|
||||
let mut h = self.hasher()?;
|
||||
h.update(b);
|
||||
let hash = h.finalize();
|
||||
Ok(base64_encode(hash.as_ref()))
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> String {
|
||||
//match c & checksumMask {
|
||||
match self {
|
||||
ChecksumMode::ChecksumCRC32 => {
|
||||
return "CRC32".to_string();
|
||||
}
|
||||
ChecksumMode::ChecksumCRC32C => {
|
||||
return "CRC32C".to_string();
|
||||
}
|
||||
ChecksumMode::ChecksumSHA1 => {
|
||||
return "SHA1".to_string();
|
||||
}
|
||||
ChecksumMode::ChecksumSHA256 => {
|
||||
return "SHA256".to_string();
|
||||
}
|
||||
ChecksumMode::ChecksumNone => {
|
||||
return "".to_string();
|
||||
}
|
||||
ChecksumMode::ChecksumCRC64NVME => {
|
||||
return "CRC64NVME".to_string();
|
||||
}
|
||||
_ => {
|
||||
return "<invalid>".to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn check_sum_reader(&self, r: GetObjectReader) -> Result<Checksum, std::io::Error> {
|
||||
// let mut h = self.hasher()?;
|
||||
// Ok(Checksum::new(self.clone(), h.sum().as_bytes()))
|
||||
// }
|
||||
|
||||
// pub fn check_sum_bytes(&self, b: &[u8]) -> Result<Checksum, std::io::Error> {
|
||||
// let mut h = self.hasher()?;
|
||||
// Ok(Checksum::new(self.clone(), h.sum().as_bytes()))
|
||||
// }
|
||||
|
||||
pub fn composite_checksum(&self, p: &mut [ObjectPart]) -> Result<Checksum, std::io::Error> {
|
||||
if !self.can_composite() {
|
||||
return Err(std::io::Error::other("cannot do composite checksum"));
|
||||
}
|
||||
p.sort_by(|i, j| {
|
||||
if i.part_num < j.part_num {
|
||||
std::cmp::Ordering::Less
|
||||
} else if i.part_num > j.part_num {
|
||||
std::cmp::Ordering::Greater
|
||||
} else {
|
||||
std::cmp::Ordering::Equal
|
||||
}
|
||||
});
|
||||
let c = self.base();
|
||||
let crc_bytes = Vec::<u8>::with_capacity(p.len() * self.raw_byte_len() as usize);
|
||||
let mut h = self.hasher()?;
|
||||
h.update(crc_bytes.as_ref());
|
||||
let hash = h.finalize();
|
||||
Ok(Checksum {
|
||||
checksum_type: self.clone(),
|
||||
r: hash.as_ref().to_vec(),
|
||||
computed: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn full_object_checksum(&self, p: &mut [ObjectPart]) -> Result<Checksum, std::io::Error> {
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Checksum {
|
||||
checksum_type: ChecksumMode,
|
||||
r: Vec<u8>,
|
||||
computed: bool,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Checksum {
|
||||
fn new(t: ChecksumMode, b: &[u8]) -> Checksum {
|
||||
if t.is_set() && b.len() == t.raw_byte_len() {
|
||||
return Checksum {
|
||||
checksum_type: t,
|
||||
r: b.to_vec(),
|
||||
computed: false,
|
||||
};
|
||||
}
|
||||
Checksum::default()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn new_checksum_string(t: ChecksumMode, s: &str) -> Result<Checksum, std::io::Error> {
|
||||
let b = match base64_decode(s.as_bytes()) {
|
||||
Ok(b) => b,
|
||||
Err(err) => return Err(std::io::Error::other(err.to_string())),
|
||||
};
|
||||
if t.is_set() && b.len() == t.raw_byte_len() {
|
||||
return Ok(Checksum {
|
||||
checksum_type: t,
|
||||
r: b,
|
||||
computed: false,
|
||||
});
|
||||
}
|
||||
Ok(Checksum::default())
|
||||
}
|
||||
|
||||
fn is_set(&self) -> bool {
|
||||
self.checksum_type.is_set() && self.r.len() == self.checksum_type.raw_byte_len()
|
||||
}
|
||||
|
||||
fn encoded(&self) -> String {
|
||||
if !self.is_set() {
|
||||
return "".to_string();
|
||||
}
|
||||
base64_encode(&self.r)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn raw(&self) -> Option<Vec<u8>> {
|
||||
if !self.is_set() {
|
||||
return None;
|
||||
}
|
||||
Some(self.r.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_auto_checksum_headers(opts: &mut PutObjectOptions) {
|
||||
opts.user_metadata
|
||||
.insert("X-Amz-Checksum-Algorithm".to_string(), opts.auto_checksum.to_string());
|
||||
if opts.auto_checksum.full_object_requested() {
|
||||
opts.user_metadata
|
||||
.insert("X-Amz-Checksum-Type".to_string(), "FULL_OBJECT".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_auto_checksum(opts: &mut PutObjectOptions, all_parts: &mut [ObjectPart]) -> Result<(), std::io::Error> {
|
||||
if opts.auto_checksum.can_composite() && !opts.auto_checksum.is(ChecksumMode::ChecksumFullObject) {
|
||||
let crc = opts.auto_checksum.composite_checksum(all_parts)?;
|
||||
opts.user_metadata = {
|
||||
let mut hm = HashMap::new();
|
||||
hm.insert(opts.auto_checksum.key(), crc.encoded());
|
||||
hm
|
||||
}
|
||||
} else if opts.auto_checksum.can_merge_crc() {
|
||||
let crc = opts.auto_checksum.full_object_checksum(all_parts)?;
|
||||
opts.user_metadata = {
|
||||
let mut hm = HashMap::new();
|
||||
hm.insert(opts.auto_checksum.key_capitalized(), crc.encoded());
|
||||
hm.insert("X-Amz-Checksum-Type".to_string(), "FULL_OBJECT".to_string());
|
||||
hm
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -30,6 +30,7 @@ pub mod api_restore;
|
||||
pub mod api_s3_datatypes;
|
||||
pub mod api_stat;
|
||||
pub mod bucket_cache;
|
||||
pub mod checksum;
|
||||
pub mod constants;
|
||||
pub mod credentials;
|
||||
pub mod object_api_utils;
|
||||
|
||||
@@ -61,7 +61,7 @@ use crate::client::{
|
||||
constants::{UNSIGNED_PAYLOAD, UNSIGNED_PAYLOAD_TRAILER},
|
||||
credentials::{CredContext, Credentials, SignatureType, Static},
|
||||
};
|
||||
use crate::{checksum::ChecksumMode, store_api::GetObjectReader};
|
||||
use crate::{client::checksum::ChecksumMode, store_api::GetObjectReader};
|
||||
use rustfs_rio::HashReader;
|
||||
use rustfs_utils::{
|
||||
net::get_endpoint_url,
|
||||
|
||||
@@ -90,3 +90,11 @@ pub fn is_rustfs_header(header_key: &str) -> bool {
|
||||
pub fn is_minio_header(header_key: &str) -> bool {
|
||||
header_key.to_lowercase().starts_with("x-minio-")
|
||||
}
|
||||
|
||||
pub fn base64_encode(input: &[u8]) -> String {
|
||||
base64_simd::URL_SAFE_NO_PAD.encode_to_string(input)
|
||||
}
|
||||
|
||||
pub fn base64_decode(input: &[u8]) -> Result<Vec<u8>, base64_simd::Error> {
|
||||
base64_simd::URL_SAFE_NO_PAD.decode_to_vec(input)
|
||||
}
|
||||
|
||||
@@ -2087,6 +2087,7 @@ impl DiskAPI for LocalDisk {
|
||||
for vol in volumes {
|
||||
if let Err(e) = self.make_volume(vol).await {
|
||||
if e != DiskError::VolumeExists {
|
||||
error!("local disk make volumes failed: {e}");
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
@@ -2108,6 +2109,7 @@ impl DiskAPI for LocalDisk {
|
||||
os::make_dir_all(&volume_dir, self.root.as_path()).await?;
|
||||
return Ok(());
|
||||
}
|
||||
error!("local disk make volume failed: {e}");
|
||||
return Err(to_volume_error(e).into());
|
||||
}
|
||||
|
||||
|
||||
@@ -301,6 +301,10 @@ impl Erasure {
|
||||
written += n;
|
||||
}
|
||||
|
||||
if ret_err.is_some() {
|
||||
return (written, ret_err);
|
||||
}
|
||||
|
||||
if written < length {
|
||||
ret_err = Some(Error::LessData.into());
|
||||
}
|
||||
|
||||
@@ -145,7 +145,9 @@ impl Erasure {
|
||||
return Err(std::io::Error::other(format!("Failed to send encoded data : {err}")));
|
||||
}
|
||||
}
|
||||
Ok(_) => break,
|
||||
Ok(_) => {
|
||||
break;
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -468,15 +468,21 @@ impl Erasure {
|
||||
let mut buf = vec![0u8; block_size];
|
||||
match rustfs_utils::read_full(&mut *reader, &mut buf).await {
|
||||
Ok(n) if n > 0 => {
|
||||
warn!("encode_stream_callback_async read n={}", n);
|
||||
total += n;
|
||||
let res = self.encode_data(&buf[..n]);
|
||||
on_block(res).await?
|
||||
}
|
||||
Ok(_) => break,
|
||||
Ok(_) => {
|
||||
warn!("encode_stream_callback_async read unexpected ok");
|
||||
break;
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
|
||||
warn!("encode_stream_callback_async read unexpected eof");
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("encode_stream_callback_async read error={:?}", e);
|
||||
on_block(Err(e)).await?;
|
||||
break;
|
||||
}
|
||||
|
||||
+113
-23
@@ -38,7 +38,7 @@ pub const DISK_RESERVE_FRACTION: f64 = 0.15;
|
||||
|
||||
lazy_static! {
|
||||
static ref GLOBAL_RUSTFS_PORT: OnceLock<u16> = OnceLock::new();
|
||||
static ref GLOBAL_RUSTFS_EXTERNAL_PORT: OnceLock<u16> = OnceLock::new();
|
||||
static ref globalDeploymentIDPtr: OnceLock<Uuid> = OnceLock::new();
|
||||
pub static ref GLOBAL_OBJECT_API: OnceLock<Arc<ECStore>> = OnceLock::new();
|
||||
pub static ref GLOBAL_LOCAL_DISK: Arc<RwLock<Vec<Option<DiskStore>>>> = Arc::new(RwLock::new(Vec::new()));
|
||||
pub static ref GLOBAL_IsErasure: RwLock<bool> = RwLock::new(false);
|
||||
@@ -51,8 +51,6 @@ lazy_static! {
|
||||
pub static ref GLOBAL_TierConfigMgr: Arc<RwLock<TierConfigMgr>> = TierConfigMgr::new();
|
||||
pub static ref GLOBAL_LifecycleSys: Arc<LifecycleSys> = LifecycleSys::new();
|
||||
pub static ref GLOBAL_EventNotifier: Arc<RwLock<EventNotifier>> = EventNotifier::new();
|
||||
//pub static ref GLOBAL_RemoteTargetTransport
|
||||
static ref globalDeploymentIDPtr: OnceLock<Uuid> = OnceLock::new();
|
||||
pub static ref GLOBAL_BOOT_TIME: OnceCell<SystemTime> = OnceCell::new();
|
||||
pub static ref GLOBAL_LocalNodeName: String = "127.0.0.1:9000".to_string();
|
||||
pub static ref GLOBAL_LocalNodeNameHex: String = rustfs_utils::crypto::hex(GLOBAL_LocalNodeName.as_bytes());
|
||||
@@ -60,12 +58,22 @@ lazy_static! {
|
||||
pub static ref GLOBAL_REGION: OnceLock<String> = OnceLock::new();
|
||||
}
|
||||
|
||||
// Global cancellation token for background services (data scanner and auto heal)
|
||||
/// Global cancellation token for background services (data scanner and auto heal)
|
||||
static GLOBAL_BACKGROUND_SERVICES_CANCEL_TOKEN: OnceLock<CancellationToken> = OnceLock::new();
|
||||
|
||||
/// Global active credentials
|
||||
static GLOBAL_ACTIVE_CRED: OnceLock<Credentials> = OnceLock::new();
|
||||
|
||||
pub fn init_global_action_cred(ak: Option<String>, sk: Option<String>) {
|
||||
/// Initialize the global action credentials
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `ak` - Optional access key
|
||||
/// * `sk` - Optional secret key
|
||||
///
|
||||
/// # Returns
|
||||
/// * None
|
||||
///
|
||||
pub fn init_global_action_credentials(ak: Option<String>, sk: Option<String>) {
|
||||
let ak = {
|
||||
if let Some(k) = ak {
|
||||
k
|
||||
@@ -91,11 +99,16 @@ pub fn init_global_action_cred(ak: Option<String>, sk: Option<String>) {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Get the global action credentials
|
||||
pub fn get_global_action_cred() -> Option<Credentials> {
|
||||
GLOBAL_ACTIVE_CRED.get().cloned()
|
||||
}
|
||||
|
||||
/// Get the global rustfs port
|
||||
///
|
||||
/// # Returns
|
||||
/// * `u16` - The global rustfs port
|
||||
///
|
||||
pub fn global_rustfs_port() -> u16 {
|
||||
if let Some(p) = GLOBAL_RUSTFS_PORT.get() {
|
||||
*p
|
||||
@@ -105,36 +118,44 @@ pub fn global_rustfs_port() -> u16 {
|
||||
}
|
||||
|
||||
/// Set the global rustfs port
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `value` - The port value to set globally
|
||||
///
|
||||
/// # Returns
|
||||
/// * None
|
||||
pub fn set_global_rustfs_port(value: u16) {
|
||||
GLOBAL_RUSTFS_PORT.set(value).expect("set_global_rustfs_port fail");
|
||||
}
|
||||
|
||||
/// Get the global rustfs external port
|
||||
pub fn global_rustfs_external_port() -> u16 {
|
||||
if let Some(p) = GLOBAL_RUSTFS_EXTERNAL_PORT.get() {
|
||||
*p
|
||||
} else {
|
||||
rustfs_config::DEFAULT_PORT
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the global rustfs external port
|
||||
pub fn set_global_rustfs_external_port(value: u16) {
|
||||
GLOBAL_RUSTFS_EXTERNAL_PORT
|
||||
.set(value)
|
||||
.expect("set_global_rustfs_external_port fail");
|
||||
}
|
||||
|
||||
/// Get the global rustfs port
|
||||
/// Set the global deployment id
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - The Uuid to set as the global deployment id
|
||||
///
|
||||
/// # Returns
|
||||
/// * None
|
||||
///
|
||||
pub fn set_global_deployment_id(id: Uuid) {
|
||||
globalDeploymentIDPtr.set(id).unwrap();
|
||||
}
|
||||
|
||||
/// Get the global deployment id
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Option<String>` - The global deployment id as a string, if set
|
||||
///
|
||||
pub fn get_global_deployment_id() -> Option<String> {
|
||||
globalDeploymentIDPtr.get().map(|v| v.to_string())
|
||||
}
|
||||
/// Get the global deployment id
|
||||
/// Set the global endpoints
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `eps` - A vector of PoolEndpoints to set globally
|
||||
///
|
||||
/// # Returns
|
||||
/// * None
|
||||
///
|
||||
pub fn set_global_endpoints(eps: Vec<PoolEndpoints>) {
|
||||
GLOBAL_Endpoints
|
||||
.set(EndpointServerPools::from(eps))
|
||||
@@ -142,6 +163,10 @@ pub fn set_global_endpoints(eps: Vec<PoolEndpoints>) {
|
||||
}
|
||||
|
||||
/// Get the global endpoints
|
||||
///
|
||||
/// # Returns
|
||||
/// * `EndpointServerPools` - The global endpoints
|
||||
///
|
||||
pub fn get_global_endpoints() -> EndpointServerPools {
|
||||
if let Some(eps) = GLOBAL_Endpoints.get() {
|
||||
eps.clone()
|
||||
@@ -150,29 +175,63 @@ pub fn get_global_endpoints() -> EndpointServerPools {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new object layer instance
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Option<Arc<ECStore>>` - The global object layer instance, if set
|
||||
///
|
||||
pub fn new_object_layer_fn() -> Option<Arc<ECStore>> {
|
||||
GLOBAL_OBJECT_API.get().cloned()
|
||||
}
|
||||
|
||||
/// Set the global object layer
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `o` - The ECStore instance to set globally
|
||||
///
|
||||
/// # Returns
|
||||
/// * None
|
||||
pub async fn set_object_layer(o: Arc<ECStore>) {
|
||||
GLOBAL_OBJECT_API.set(o).expect("set_object_layer fail ")
|
||||
}
|
||||
|
||||
/// Check if the setup type is distributed erasure coding
|
||||
///
|
||||
/// # Returns
|
||||
/// * `bool` - True if the setup type is distributed erasure coding, false otherwise
|
||||
///
|
||||
pub async fn is_dist_erasure() -> bool {
|
||||
let lock = GLOBAL_IsDistErasure.read().await;
|
||||
*lock
|
||||
}
|
||||
|
||||
/// Check if the setup type is erasure coding with single data center
|
||||
///
|
||||
/// # Returns
|
||||
/// * `bool` - True if the setup type is erasure coding with single data center, false otherwise
|
||||
///
|
||||
pub async fn is_erasure_sd() -> bool {
|
||||
let lock = GLOBAL_IsErasureSD.read().await;
|
||||
*lock
|
||||
}
|
||||
|
||||
/// Check if the setup type is erasure coding
|
||||
///
|
||||
/// # Returns
|
||||
/// * `bool` - True if the setup type is erasure coding, false otherwise
|
||||
///
|
||||
pub async fn is_erasure() -> bool {
|
||||
let lock = GLOBAL_IsErasure.read().await;
|
||||
*lock
|
||||
}
|
||||
|
||||
/// Update the global erasure type based on the setup type
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `setup_type` - The SetupType to update the global erasure type
|
||||
///
|
||||
/// # Returns
|
||||
/// * None
|
||||
pub async fn update_erasure_type(setup_type: SetupType) {
|
||||
let mut is_erasure = GLOBAL_IsErasure.write().await;
|
||||
*is_erasure = setup_type == SetupType::Erasure;
|
||||
@@ -198,25 +257,53 @@ pub async fn update_erasure_type(setup_type: SetupType) {
|
||||
|
||||
type TypeLocalDiskSetDrives = Vec<Vec<Vec<Option<DiskStore>>>>;
|
||||
|
||||
/// Set the global region
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `region` - The region string to set globally
|
||||
///
|
||||
/// # Returns
|
||||
/// * None
|
||||
pub fn set_global_region(region: String) {
|
||||
GLOBAL_REGION.set(region).unwrap();
|
||||
}
|
||||
|
||||
/// Get the global region
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Option<String>` - The global region string, if set
|
||||
///
|
||||
pub fn get_global_region() -> Option<String> {
|
||||
GLOBAL_REGION.get().cloned()
|
||||
}
|
||||
|
||||
/// Initialize the global background services cancellation token
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `cancel_token` - The CancellationToken instance to set globally
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` if successful
|
||||
/// * `Err(CancellationToken)` if setting fails
|
||||
///
|
||||
pub fn init_background_services_cancel_token(cancel_token: CancellationToken) -> Result<(), CancellationToken> {
|
||||
GLOBAL_BACKGROUND_SERVICES_CANCEL_TOKEN.set(cancel_token)
|
||||
}
|
||||
|
||||
/// Get the global background services cancellation token
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Option<&'static CancellationToken>` - The global cancellation token, if set
|
||||
///
|
||||
pub fn get_background_services_cancel_token() -> Option<&'static CancellationToken> {
|
||||
GLOBAL_BACKGROUND_SERVICES_CANCEL_TOKEN.get()
|
||||
}
|
||||
|
||||
/// Create and initialize the global background services cancellation token
|
||||
///
|
||||
/// # Returns
|
||||
/// * `CancellationToken` - The newly created global cancellation token
|
||||
///
|
||||
pub fn create_background_services_cancel_token() -> CancellationToken {
|
||||
let cancel_token = CancellationToken::new();
|
||||
init_background_services_cancel_token(cancel_token.clone()).expect("Background services cancel token already initialized");
|
||||
@@ -224,6 +311,9 @@ pub fn create_background_services_cancel_token() -> CancellationToken {
|
||||
}
|
||||
|
||||
/// Shutdown all background services gracefully
|
||||
///
|
||||
/// # Returns
|
||||
/// * None
|
||||
pub fn shutdown_background_services() {
|
||||
if let Some(cancel_token) = GLOBAL_BACKGROUND_SERVICES_CANCEL_TOKEN.get() {
|
||||
cancel_token.cancel();
|
||||
|
||||
@@ -44,7 +44,7 @@ mod store_init;
|
||||
pub mod store_list_objects;
|
||||
pub mod store_utils;
|
||||
|
||||
pub mod checksum;
|
||||
// pub mod checksum;
|
||||
pub mod client;
|
||||
pub mod event;
|
||||
pub mod event_notification;
|
||||
|
||||
@@ -23,7 +23,7 @@ use rustfs_common::{
|
||||
use rustfs_madmin::metrics::{DiskIOStats, DiskMetric, RealtimeMetrics};
|
||||
use rustfs_utils::os::get_drive_stats;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::info;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::{
|
||||
admin_server_info::get_local_server_property,
|
||||
@@ -44,7 +44,7 @@ pub struct CollectMetricsOpts {
|
||||
pub struct MetricType(u32);
|
||||
|
||||
impl MetricType {
|
||||
// 定义一些常量
|
||||
// Define some constants
|
||||
pub const NONE: MetricType = MetricType(0);
|
||||
pub const SCANNER: MetricType = MetricType(1 << 0);
|
||||
pub const DISK: MetricType = MetricType(1 << 1);
|
||||
@@ -70,8 +70,18 @@ impl MetricType {
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect local metrics based on the specified types and options.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `types` - A `MetricType` specifying which types of metrics to collect.
|
||||
/// * `opts` - A reference to `CollectMetricsOpts` containing additional options for metric collection.
|
||||
///
|
||||
/// # Returns
|
||||
/// * A `RealtimeMetrics` struct containing the collected metrics.
|
||||
///
|
||||
pub async fn collect_local_metrics(types: MetricType, opts: &CollectMetricsOpts) -> RealtimeMetrics {
|
||||
info!("collect_local_metrics");
|
||||
debug!("collect_local_metrics");
|
||||
let mut real_time_metrics = RealtimeMetrics::default();
|
||||
if types.0 == MetricType::NONE.0 {
|
||||
info!("types is None, return");
|
||||
@@ -93,13 +103,13 @@ pub async fn collect_local_metrics(types: MetricType, opts: &CollectMetricsOpts)
|
||||
}
|
||||
|
||||
if types.contains(&MetricType::DISK) {
|
||||
info!("start get disk metrics");
|
||||
debug!("start get disk metrics");
|
||||
let mut aggr = DiskMetric {
|
||||
collected_at: Utc::now(),
|
||||
..Default::default()
|
||||
};
|
||||
for (name, disk) in collect_local_disks_metrics(&opts.disks).await.into_iter() {
|
||||
info!("got disk metric, name: {name}, metric: {disk:?}");
|
||||
debug!("got disk metric, name: {name}, metric: {disk:?}");
|
||||
real_time_metrics.by_disk.insert(name, disk.clone());
|
||||
aggr.merge(&disk);
|
||||
}
|
||||
@@ -107,7 +117,7 @@ pub async fn collect_local_metrics(types: MetricType, opts: &CollectMetricsOpts)
|
||||
}
|
||||
|
||||
if types.contains(&MetricType::SCANNER) {
|
||||
info!("start get scanner metrics");
|
||||
debug!("start get scanner metrics");
|
||||
let metrics = globalMetrics.report().await;
|
||||
real_time_metrics.aggregated.scanner = Some(metrics);
|
||||
}
|
||||
|
||||
@@ -1140,6 +1140,7 @@ impl ECStore {
|
||||
.await
|
||||
{
|
||||
if !is_err_bucket_exists(&err) {
|
||||
error!("decommission: make bucket failed: {err}");
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
@@ -1262,6 +1263,8 @@ impl ECStore {
|
||||
parts[i] = CompletePart {
|
||||
part_num: pi.part_num,
|
||||
etag: pi.etag,
|
||||
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1289,7 +1292,7 @@ impl ECStore {
|
||||
}
|
||||
|
||||
let reader = BufReader::new(rd.stream);
|
||||
let hrd = HashReader::new(Box::new(WarpReader::new(reader)), object_info.size, object_info.size, None, false)?;
|
||||
let hrd = HashReader::new(Box::new(WarpReader::new(reader)), object_info.size, object_info.size, None, None, false)?;
|
||||
let mut data = PutObjReader::new(hrd);
|
||||
|
||||
if let Err(err) = self
|
||||
|
||||
@@ -979,6 +979,7 @@ impl ECStore {
|
||||
parts[i] = CompletePart {
|
||||
part_num: pi.part_num,
|
||||
etag: pi.etag,
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1005,7 +1006,7 @@ impl ECStore {
|
||||
}
|
||||
|
||||
let reader = BufReader::new(rd.stream);
|
||||
let hrd = HashReader::new(Box::new(WarpReader::new(reader)), object_info.size, object_info.size, None, false)?;
|
||||
let hrd = HashReader::new(Box::new(WarpReader::new(reader)), object_info.size, object_info.size, None, None, false)?;
|
||||
let mut data = PutObjReader::new(hrd);
|
||||
|
||||
if let Err(err) = self
|
||||
|
||||
+114
-83
@@ -72,13 +72,13 @@ use rustfs_filemeta::{
|
||||
};
|
||||
use rustfs_lock::fast_lock::types::LockResult;
|
||||
use rustfs_madmin::heal_commands::{HealDriveInfo, HealResultItem};
|
||||
use rustfs_rio::{EtagResolvable, HashReader, TryGetIndex as _, WarpReader};
|
||||
use rustfs_rio::{EtagResolvable, HashReader, HashReaderMut, TryGetIndex as _, WarpReader};
|
||||
use rustfs_utils::http::headers::AMZ_OBJECT_TAGGING;
|
||||
use rustfs_utils::http::headers::AMZ_STORAGE_CLASS;
|
||||
use rustfs_utils::http::headers::RESERVED_METADATA_PREFIX_LOWER;
|
||||
use rustfs_utils::{
|
||||
HashAlgorithm,
|
||||
crypto::{base64_decode, base64_encode, hex},
|
||||
crypto::hex,
|
||||
path::{SLASH_SEPARATOR, encode_dir_object, has_suffix, path_join_buf},
|
||||
};
|
||||
use rustfs_workers::workers::Workers;
|
||||
@@ -158,10 +158,7 @@ impl SetDisks {
|
||||
LockResult::Conflict {
|
||||
current_owner,
|
||||
current_mode,
|
||||
} => format!(
|
||||
"{mode} lock conflicted on {bucket}/{object}: held by {current_owner} as {:?}",
|
||||
current_mode
|
||||
),
|
||||
} => format!("{mode} lock conflicted on {bucket}/{object}: held by {current_owner} as {current_mode:?}"),
|
||||
LockResult::Acquired => format!("unexpected lock state while acquiring {mode} lock on {bucket}/{object}"),
|
||||
}
|
||||
}
|
||||
@@ -922,9 +919,8 @@ impl SetDisks {
|
||||
}
|
||||
|
||||
fn get_upload_id_dir(bucket: &str, object: &str, upload_id: &str) -> String {
|
||||
// warn!("get_upload_id_dir upload_id {:?}", upload_id);
|
||||
|
||||
let upload_uuid = base64_decode(upload_id.as_bytes())
|
||||
let upload_uuid = base64_simd::URL_SAFE_NO_PAD
|
||||
.decode_to_vec(upload_id.as_bytes())
|
||||
.and_then(|v| {
|
||||
String::from_utf8(v).map_or(Ok(upload_id.to_owned()), |v| {
|
||||
let parts: Vec<_> = v.splitn(2, '.').collect();
|
||||
@@ -2950,6 +2946,7 @@ impl SetDisks {
|
||||
part.mod_time,
|
||||
part.actual_size,
|
||||
part.index.clone(),
|
||||
part.checksums.clone(),
|
||||
);
|
||||
if is_inline_buffer {
|
||||
if let Some(writer) = writers[index].take() {
|
||||
@@ -3528,9 +3525,9 @@ impl ObjectIO for SetDisks {
|
||||
// }
|
||||
|
||||
if object_info.size == 0 {
|
||||
if let Some(rs) = range {
|
||||
let _ = rs.get_offset_length(object_info.size)?;
|
||||
}
|
||||
// if let Some(rs) = range {
|
||||
// let _ = rs.get_offset_length(object_info.size)?;
|
||||
// }
|
||||
|
||||
let reader = GetObjectReader {
|
||||
stream: Box::new(Cursor::new(Vec::new())),
|
||||
@@ -3712,7 +3709,7 @@ impl ObjectIO for SetDisks {
|
||||
|
||||
let stream = mem::replace(
|
||||
&mut data.stream,
|
||||
HashReader::new(Box::new(WarpReader::new(Cursor::new(Vec::new()))), 0, 0, None, false)?,
|
||||
HashReader::new(Box::new(WarpReader::new(Cursor::new(Vec::new()))), 0, 0, None, None, false)?,
|
||||
);
|
||||
|
||||
let (reader, w_size) = match Arc::new(erasure).encode(stream, &mut writers, write_quorum).await {
|
||||
@@ -3729,7 +3726,12 @@ impl ObjectIO for SetDisks {
|
||||
// }
|
||||
|
||||
if (w_size as i64) < data.size() {
|
||||
return Err(Error::other("put_object write size < data.size()"));
|
||||
warn!("put_object write size < data.size(), w_size={}, data.size={}", w_size, data.size());
|
||||
return Err(Error::other(format!(
|
||||
"put_object write size < data.size(), w_size={}, data.size={}",
|
||||
w_size,
|
||||
data.size()
|
||||
)));
|
||||
}
|
||||
|
||||
if user_defined.contains_key(&format!("{RESERVED_METADATA_PREFIX_LOWER}compression")) {
|
||||
@@ -3756,31 +3758,42 @@ impl ObjectIO for SetDisks {
|
||||
}
|
||||
}
|
||||
|
||||
if fi.checksum.is_none() {
|
||||
if let Some(content_hash) = data.as_hash_reader().content_hash() {
|
||||
fi.checksum = Some(content_hash.to_bytes(&[]));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(sc) = user_defined.get(AMZ_STORAGE_CLASS) {
|
||||
if sc == storageclass::STANDARD {
|
||||
let _ = user_defined.remove(AMZ_STORAGE_CLASS);
|
||||
}
|
||||
}
|
||||
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let mod_time = if let Some(mod_time) = opts.mod_time {
|
||||
Some(mod_time)
|
||||
} else {
|
||||
Some(OffsetDateTime::now_utc())
|
||||
};
|
||||
|
||||
for (i, fi) in parts_metadatas.iter_mut().enumerate() {
|
||||
fi.metadata = user_defined.clone();
|
||||
for (i, pfi) in parts_metadatas.iter_mut().enumerate() {
|
||||
pfi.metadata = user_defined.clone();
|
||||
if is_inline_buffer {
|
||||
if let Some(writer) = writers[i].take() {
|
||||
fi.data = Some(writer.into_inline_data().map(bytes::Bytes::from).unwrap_or_default());
|
||||
pfi.data = Some(writer.into_inline_data().map(bytes::Bytes::from).unwrap_or_default());
|
||||
}
|
||||
|
||||
fi.set_inline_data();
|
||||
pfi.set_inline_data();
|
||||
}
|
||||
|
||||
fi.mod_time = Some(now);
|
||||
fi.size = w_size as i64;
|
||||
fi.versioned = opts.versioned || opts.version_suspended;
|
||||
fi.add_object_part(1, etag.clone(), w_size, fi.mod_time, actual_size, index_op.clone());
|
||||
pfi.mod_time = mod_time;
|
||||
pfi.size = w_size as i64;
|
||||
pfi.versioned = opts.versioned || opts.version_suspended;
|
||||
pfi.add_object_part(1, etag.clone(), w_size, mod_time, actual_size, index_op.clone(), None);
|
||||
pfi.checksum = fi.checksum.clone();
|
||||
|
||||
if opts.data_movement {
|
||||
fi.set_data_moved();
|
||||
pfi.set_data_moved();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3815,7 +3828,8 @@ impl ObjectIO for SetDisks {
|
||||
|
||||
fi.replication_state_internal = Some(opts.put_replication_state());
|
||||
|
||||
// TODO: version support
|
||||
fi.is_latest = true;
|
||||
|
||||
Ok(ObjectInfo::from_file_info(&fi, bucket, object, opts.versioned || opts.version_suspended))
|
||||
}
|
||||
}
|
||||
@@ -4430,8 +4444,6 @@ impl StorageAPI for SetDisks {
|
||||
.await
|
||||
.map_err(|e| to_object_err(e, vec![bucket, object]))?;
|
||||
|
||||
// warn!("get object_info fi {:?}", &fi);
|
||||
|
||||
let oi = ObjectInfo::from_file_info(&fi, bucket, object, opts.versioned || opts.version_suspended);
|
||||
|
||||
Ok(oi)
|
||||
@@ -4759,6 +4771,11 @@ impl StorageAPI for SetDisks {
|
||||
uploaded_parts.push(CompletePart {
|
||||
part_num: p_info.part_num,
|
||||
etag: p_info.etag,
|
||||
checksum_crc32: None,
|
||||
checksum_crc32c: None,
|
||||
checksum_sha1: None,
|
||||
checksum_sha256: None,
|
||||
checksum_crc64nvme: None,
|
||||
});
|
||||
}
|
||||
if let Err(err) = self.complete_multipart_upload(bucket, object, &res.upload_id, uploaded_parts, &ObjectOptions {
|
||||
@@ -4834,64 +4851,24 @@ impl StorageAPI for SetDisks {
|
||||
|
||||
let write_quorum = fi.write_quorum(self.default_write_quorum());
|
||||
|
||||
let disks = self.disks.read().await;
|
||||
if let Some(checksum) = fi.metadata.get(rustfs_rio::RUSTFS_MULTIPART_CHECKSUM)
|
||||
&& !checksum.is_empty()
|
||||
&& data
|
||||
.as_hash_reader()
|
||||
.content_crc_type()
|
||||
.is_none_or(|v| v.to_string() != *checksum)
|
||||
{
|
||||
return Err(Error::other(format!("checksum mismatch: {checksum}")));
|
||||
}
|
||||
|
||||
let disks = self.disks.read().await.clone();
|
||||
|
||||
let disks = disks.clone();
|
||||
let shuffle_disks = Self::shuffle_disks(&disks, &fi.erasure.distribution);
|
||||
|
||||
let part_suffix = format!("part.{part_id}");
|
||||
let tmp_part = format!("{}x{}", Uuid::new_v4(), OffsetDateTime::now_utc().unix_timestamp());
|
||||
let tmp_part_path = Arc::new(format!("{tmp_part}/{part_suffix}"));
|
||||
|
||||
// let mut writers = Vec::with_capacity(disks.len());
|
||||
// let erasure = Erasure::new(fi.erasure.data_blocks, fi.erasure.parity_blocks, fi.erasure.block_size);
|
||||
// let shared_size = erasure.shard_size(erasure.block_size);
|
||||
|
||||
// let futures = disks.iter().map(|disk| {
|
||||
// let disk = disk.clone();
|
||||
// let tmp_part_path = tmp_part_path.clone();
|
||||
// tokio::spawn(async move {
|
||||
// if let Some(disk) = disk {
|
||||
// // let writer = disk.append_file(RUSTFS_META_TMP_BUCKET, &tmp_part_path).await?;
|
||||
// // let filewriter = disk
|
||||
// // .create_file("", RUSTFS_META_TMP_BUCKET, &tmp_part_path, data.content_length)
|
||||
// // .await?;
|
||||
// match new_bitrot_filewriter(
|
||||
// disk.clone(),
|
||||
// RUSTFS_META_TMP_BUCKET,
|
||||
// &tmp_part_path,
|
||||
// false,
|
||||
// DEFAULT_BITROT_ALGO,
|
||||
// shared_size,
|
||||
// )
|
||||
// .await
|
||||
// {
|
||||
// Ok(writer) => Ok(Some(writer)),
|
||||
// Err(e) => Err(e),
|
||||
// }
|
||||
// } else {
|
||||
// Ok(None)
|
||||
// }
|
||||
// })
|
||||
// });
|
||||
// for x in join_all(futures).await {
|
||||
// let x = x??;
|
||||
// writers.push(x);
|
||||
// }
|
||||
|
||||
// let erasure = Erasure::new(fi.erasure.data_blocks, fi.erasure.parity_blocks, fi.erasure.block_size);
|
||||
|
||||
// let stream = replace(&mut data.stream, Box::new(empty()));
|
||||
// let etag_stream = EtagReader::new(stream);
|
||||
|
||||
// let (w_size, mut etag) = Arc::new(erasure)
|
||||
// .encode(etag_stream, &mut writers, data.content_length, write_quorum)
|
||||
// .await?;
|
||||
|
||||
// if let Err(err) = close_bitrot_writers(&mut writers).await {
|
||||
// error!("close_bitrot_writers err {:?}", err);
|
||||
// }
|
||||
|
||||
let erasure = erasure_coding::Erasure::new(fi.erasure.data_blocks, fi.erasure.parity_blocks, fi.erasure.block_size);
|
||||
|
||||
let mut writers = Vec::with_capacity(shuffle_disks.len());
|
||||
@@ -4944,7 +4921,7 @@ impl StorageAPI for SetDisks {
|
||||
|
||||
let stream = mem::replace(
|
||||
&mut data.stream,
|
||||
HashReader::new(Box::new(WarpReader::new(Cursor::new(Vec::new()))), 0, 0, None, false)?,
|
||||
HashReader::new(Box::new(WarpReader::new(Cursor::new(Vec::new()))), 0, 0, None, None, false)?,
|
||||
);
|
||||
|
||||
let (reader, w_size) = Arc::new(erasure).encode(stream, &mut writers, write_quorum).await?; // TODO: 出错,删除临时目录
|
||||
@@ -4952,7 +4929,12 @@ impl StorageAPI for SetDisks {
|
||||
let _ = mem::replace(&mut data.stream, reader);
|
||||
|
||||
if (w_size as i64) < data.size() {
|
||||
return Err(Error::other("put_object_part write size < data.size()"));
|
||||
warn!("put_object_part write size < data.size(), w_size={}, data.size={}", w_size, data.size());
|
||||
return Err(Error::other(format!(
|
||||
"put_object_part write size < data.size(), w_size={}, data.size={}",
|
||||
w_size,
|
||||
data.size()
|
||||
)));
|
||||
}
|
||||
|
||||
let index_op = data.stream.try_get_index().map(|v| v.clone().into_vec());
|
||||
@@ -5227,7 +5209,8 @@ impl StorageAPI for SetDisks {
|
||||
uploads.push(MultipartInfo {
|
||||
bucket: bucket.to_owned(),
|
||||
object: object.to_owned(),
|
||||
upload_id: base64_encode(format!("{}.{}", get_global_deployment_id().unwrap_or_default(), upload_id).as_bytes()),
|
||||
upload_id: base64_simd::URL_SAFE_NO_PAD
|
||||
.encode_to_string(format!("{}.{}", get_global_deployment_id().unwrap_or_default(), upload_id).as_bytes()),
|
||||
initiated: Some(start_time),
|
||||
..Default::default()
|
||||
});
|
||||
@@ -5348,6 +5331,14 @@ impl StorageAPI for SetDisks {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(checksum) = &opts.want_checksum {
|
||||
user_defined.insert(rustfs_rio::RUSTFS_MULTIPART_CHECKSUM.to_string(), checksum.checksum_type.to_string());
|
||||
user_defined.insert(
|
||||
rustfs_rio::RUSTFS_MULTIPART_CHECKSUM_TYPE.to_string(),
|
||||
checksum.checksum_type.obj_type().to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let (shuffle_disks, mut parts_metadatas) = Self::shuffle_disks_and_parts_metadata(&disks, &parts_metadata, &fi);
|
||||
|
||||
let mod_time = opts.mod_time.unwrap_or(OffsetDateTime::now_utc());
|
||||
@@ -5362,7 +5353,8 @@ impl StorageAPI for SetDisks {
|
||||
|
||||
let upload_uuid = format!("{}x{}", Uuid::new_v4(), mod_time.unix_timestamp_nanos());
|
||||
|
||||
let upload_id = base64_encode(format!("{}.{}", get_global_deployment_id().unwrap_or_default(), upload_uuid).as_bytes());
|
||||
let upload_id = base64_simd::URL_SAFE_NO_PAD
|
||||
.encode_to_string(format!("{}.{}", get_global_deployment_id().unwrap_or_default(), upload_uuid).as_bytes());
|
||||
|
||||
let upload_path = Self::get_upload_id_dir(bucket, object, upload_uuid.as_str());
|
||||
|
||||
@@ -5379,7 +5371,11 @@ impl StorageAPI for SetDisks {
|
||||
|
||||
// evalDisks
|
||||
|
||||
Ok(MultipartUploadResult { upload_id })
|
||||
Ok(MultipartUploadResult {
|
||||
upload_id,
|
||||
checksum_algo: user_defined.get(rustfs_rio::RUSTFS_MULTIPART_CHECKSUM).cloned(),
|
||||
checksum_type: user_defined.get(rustfs_rio::RUSTFS_MULTIPART_CHECKSUM_TYPE).cloned(),
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
@@ -5467,6 +5463,25 @@ impl StorageAPI for SetDisks {
|
||||
return Err(Error::other("part result number err"));
|
||||
}
|
||||
|
||||
if let Some(cs) = fi.metadata.get(rustfs_rio::RUSTFS_MULTIPART_CHECKSUM) {
|
||||
let Some(checksum_type) = fi.metadata.get(rustfs_rio::RUSTFS_MULTIPART_CHECKSUM_TYPE) else {
|
||||
return Err(Error::other("checksum type not found"));
|
||||
};
|
||||
|
||||
if opts.want_checksum.is_some()
|
||||
&& !opts.want_checksum.as_ref().is_some_and(|v| {
|
||||
v.checksum_type
|
||||
.is(rustfs_rio::ChecksumType::from_string_with_obj_type(cs, checksum_type))
|
||||
})
|
||||
{
|
||||
return Err(Error::other(format!(
|
||||
"checksum type mismatch, got {:?}, want {:?}",
|
||||
opts.want_checksum.as_ref().unwrap(),
|
||||
rustfs_rio::ChecksumType::from_string_with_obj_type(cs, checksum_type)
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
for (i, part) in object_parts.iter().enumerate() {
|
||||
if let Some(err) = &part.error {
|
||||
error!("complete_multipart_upload part error: {:?}", &err);
|
||||
@@ -5487,6 +5502,7 @@ impl StorageAPI for SetDisks {
|
||||
part.mod_time,
|
||||
part.actual_size,
|
||||
part.index.clone(),
|
||||
part.checksums.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6422,10 +6438,20 @@ mod tests {
|
||||
CompletePart {
|
||||
part_num: 1,
|
||||
etag: Some("d41d8cd98f00b204e9800998ecf8427e".to_string()),
|
||||
checksum_crc32: None,
|
||||
checksum_crc32c: None,
|
||||
checksum_sha1: None,
|
||||
checksum_sha256: None,
|
||||
checksum_crc64nvme: None,
|
||||
},
|
||||
CompletePart {
|
||||
part_num: 2,
|
||||
etag: Some("098f6bcd4621d373cade4e832627b4f6".to_string()),
|
||||
checksum_crc32: None,
|
||||
checksum_crc32c: None,
|
||||
checksum_sha1: None,
|
||||
checksum_sha256: None,
|
||||
checksum_crc64nvme: None,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -6442,6 +6468,11 @@ mod tests {
|
||||
let single_part = vec![CompletePart {
|
||||
part_num: 1,
|
||||
etag: Some("d41d8cd98f00b204e9800998ecf8427e".to_string()),
|
||||
checksum_crc32: None,
|
||||
checksum_crc32c: None,
|
||||
checksum_sha1: None,
|
||||
checksum_sha256: None,
|
||||
checksum_crc64nvme: None,
|
||||
}];
|
||||
let single_result = get_complete_multipart_md5(&single_part);
|
||||
assert!(single_result.ends_with("-1"));
|
||||
|
||||
@@ -59,7 +59,6 @@ use rustfs_common::globals::{GLOBAL_Local_Node_Name, GLOBAL_Rustfs_Host, GLOBAL_
|
||||
use rustfs_common::heal_channel::{HealItemType, HealOpts};
|
||||
use rustfs_filemeta::FileInfo;
|
||||
use rustfs_madmin::heal_commands::HealResultItem;
|
||||
use rustfs_utils::crypto::base64_decode;
|
||||
use rustfs_utils::path::{SLASH_SEPARATOR, decode_dir_object, encode_dir_object, path_join_buf};
|
||||
use s3s::dto::{BucketVersioningStatus, ObjectLockConfiguration, ObjectLockEnabled, VersioningConfiguration};
|
||||
use std::cmp::Ordering;
|
||||
@@ -1231,6 +1230,7 @@ impl StorageAPI for ECStore {
|
||||
if let Err(err) = self.peer_sys.make_bucket(bucket, opts).await {
|
||||
let err = to_object_err(err.into(), vec![bucket]);
|
||||
if !is_err_bucket_exists(&err) {
|
||||
error!("make bucket failed: {err}");
|
||||
let _ = self
|
||||
.delete_bucket(
|
||||
bucket,
|
||||
@@ -2421,7 +2421,7 @@ fn check_list_multipart_args(
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(_e) = base64_decode(upload_id_marker.as_bytes()) {
|
||||
if let Err(_e) = base64_simd::URL_SAFE_NO_PAD.decode_to_vec(upload_id_marker.as_bytes()) {
|
||||
return Err(StorageError::MalformedUploadID(upload_id_marker.to_owned()));
|
||||
}
|
||||
}
|
||||
@@ -2448,7 +2448,7 @@ fn check_new_multipart_args(bucket: &str, object: &str) -> Result<()> {
|
||||
}
|
||||
|
||||
fn check_multipart_object_args(bucket: &str, object: &str, upload_id: &str) -> Result<()> {
|
||||
if let Err(e) = base64_decode(upload_id.as_bytes()) {
|
||||
if let Err(e) = base64_simd::URL_SAFE_NO_PAD.decode_to_vec(upload_id.as_bytes()) {
|
||||
return Err(StorageError::MalformedUploadID(format!("{bucket}/{object}-{upload_id},err:{e}")));
|
||||
};
|
||||
check_object_args(bucket, object)
|
||||
|
||||
@@ -13,9 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
use crate::bucket::metadata_sys::get_versioning_config;
|
||||
use crate::bucket::replication::REPLICATION_RESET;
|
||||
use crate::bucket::replication::REPLICATION_STATUS;
|
||||
use crate::bucket::replication::{ReplicateDecision, replication_statuses_map, version_purge_statuses_map};
|
||||
use crate::bucket::versioning::VersioningApi as _;
|
||||
use crate::disk::DiskStore;
|
||||
use crate::error::{Error, Result};
|
||||
@@ -25,12 +22,15 @@ use crate::{
|
||||
bucket::lifecycle::lifecycle::ExpirationOptions,
|
||||
bucket::lifecycle::{bucket_lifecycle_ops::TransitionedObject, lifecycle::TransitionOptions},
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use http::{HeaderMap, HeaderValue};
|
||||
use rustfs_common::heal_channel::HealOpts;
|
||||
use rustfs_filemeta::{
|
||||
FileInfo, MetaCacheEntriesSorted, ObjectPartInfo, ReplicationState, ReplicationStatusType, VersionPurgeStatusType,
|
||||
FileInfo, MetaCacheEntriesSorted, ObjectPartInfo, REPLICATION_RESET, REPLICATION_STATUS, ReplicateDecision, ReplicationState,
|
||||
ReplicationStatusType, VersionPurgeStatusType, replication_statuses_map, version_purge_statuses_map,
|
||||
};
|
||||
use rustfs_madmin::heal_commands::HealResultItem;
|
||||
use rustfs_rio::Checksum;
|
||||
use rustfs_rio::{DecompressReader, HashReader, LimitReader, WarpReader};
|
||||
use rustfs_utils::CompressionAlgorithm;
|
||||
use rustfs_utils::http::headers::{AMZ_OBJECT_TAGGING, RESERVED_METADATA_PREFIX_LOWER};
|
||||
@@ -92,11 +92,28 @@ impl PutObjReader {
|
||||
PutObjReader { stream }
|
||||
}
|
||||
|
||||
pub fn as_hash_reader(&self) -> &HashReader {
|
||||
&self.stream
|
||||
}
|
||||
|
||||
pub fn from_vec(data: Vec<u8>) -> Self {
|
||||
use sha2::{Digest, Sha256};
|
||||
let content_length = data.len() as i64;
|
||||
let sha256hex = if content_length > 0 {
|
||||
Some(hex_simd::encode_to_string(Sha256::digest(&data), hex_simd::AsciiCase::Lower))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
PutObjReader {
|
||||
stream: HashReader::new(Box::new(WarpReader::new(Cursor::new(data))), content_length, content_length, None, false)
|
||||
.unwrap(),
|
||||
stream: HashReader::new(
|
||||
Box::new(WarpReader::new(Cursor::new(data))),
|
||||
content_length,
|
||||
content_length,
|
||||
None,
|
||||
sha256hex,
|
||||
false,
|
||||
)
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,6 +391,8 @@ pub struct ObjectOptions {
|
||||
pub lifecycle_audit_event: LcAuditEvent,
|
||||
|
||||
pub eval_metadata: Option<HashMap<String, String>>,
|
||||
|
||||
pub want_checksum: Option<Checksum>,
|
||||
}
|
||||
|
||||
impl ObjectOptions {
|
||||
@@ -456,6 +475,8 @@ pub struct BucketInfo {
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct MultipartUploadResult {
|
||||
pub upload_id: String,
|
||||
pub checksum_algo: Option<String>,
|
||||
pub checksum_type: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
@@ -471,13 +492,24 @@ pub struct PartInfo {
|
||||
pub struct CompletePart {
|
||||
pub part_num: usize,
|
||||
pub etag: Option<String>,
|
||||
// pub size: Option<usize>,
|
||||
pub checksum_crc32: Option<String>,
|
||||
pub checksum_crc32c: Option<String>,
|
||||
pub checksum_sha1: Option<String>,
|
||||
pub checksum_sha256: Option<String>,
|
||||
pub checksum_crc64nvme: Option<String>,
|
||||
}
|
||||
|
||||
impl From<s3s::dto::CompletedPart> for CompletePart {
|
||||
fn from(value: s3s::dto::CompletedPart) -> Self {
|
||||
Self {
|
||||
part_num: value.part_number.unwrap_or_default() as usize,
|
||||
etag: value.e_tag.map(|e| e.value().to_owned()),
|
||||
etag: value.e_tag.map(|v| v.value().to_owned()),
|
||||
checksum_crc32: value.checksum_crc32,
|
||||
checksum_crc32c: value.checksum_crc32c,
|
||||
checksum_sha1: value.checksum_sha1,
|
||||
checksum_sha256: value.checksum_sha256,
|
||||
checksum_crc64nvme: value.checksum_crc64nvme,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -517,7 +549,7 @@ pub struct ObjectInfo {
|
||||
pub version_purge_status_internal: Option<String>,
|
||||
pub version_purge_status: VersionPurgeStatusType,
|
||||
pub replication_decision: String,
|
||||
pub checksum: Vec<u8>,
|
||||
pub checksum: Option<Bytes>,
|
||||
}
|
||||
|
||||
impl Clone for ObjectInfo {
|
||||
@@ -554,7 +586,7 @@ impl Clone for ObjectInfo {
|
||||
version_purge_status_internal: self.version_purge_status_internal.clone(),
|
||||
version_purge_status: self.version_purge_status.clone(),
|
||||
replication_decision: self.replication_decision.clone(),
|
||||
checksum: Default::default(),
|
||||
checksum: self.checksum.clone(),
|
||||
expires: self.expires,
|
||||
}
|
||||
}
|
||||
@@ -694,6 +726,7 @@ impl ObjectInfo {
|
||||
inlined,
|
||||
user_defined: metadata,
|
||||
transitioned_object,
|
||||
checksum: fi.checksum.clone(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -884,6 +917,23 @@ impl ObjectInfo {
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decrypt_checksums(&self, part: usize, _headers: &HeaderMap) -> Result<(HashMap<String, String>, bool)> {
|
||||
if part > 0 {
|
||||
if let Some(checksums) = self.parts.iter().find(|p| p.number == part).and_then(|p| p.checksums.clone()) {
|
||||
return Ok((checksums, true));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: decrypt checksums
|
||||
|
||||
if let Some(data) = &self.checksum {
|
||||
let (checksums, is_multipart) = rustfs_rio::read_checksums(data.as_ref(), 0);
|
||||
return Ok((checksums, is_multipart));
|
||||
}
|
||||
|
||||
Ok((HashMap::new(), false))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
||||
@@ -40,6 +40,8 @@ byteorder = { workspace = true }
|
||||
tracing.workspace = true
|
||||
thiserror.workspace = true
|
||||
s3s.workspace = true
|
||||
lazy_static.workspace = true
|
||||
regex.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { workspace = true }
|
||||
|
||||
@@ -284,6 +284,7 @@ impl FileInfo {
|
||||
Ok(t)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn add_object_part(
|
||||
&mut self,
|
||||
num: usize,
|
||||
@@ -292,6 +293,7 @@ impl FileInfo {
|
||||
mod_time: Option<OffsetDateTime>,
|
||||
actual_size: i64,
|
||||
index: Option<Bytes>,
|
||||
checksums: Option<HashMap<String, String>>,
|
||||
) {
|
||||
let part = ObjectPartInfo {
|
||||
etag,
|
||||
@@ -300,7 +302,7 @@ impl FileInfo {
|
||||
mod_time,
|
||||
actual_size,
|
||||
index,
|
||||
checksums: None,
|
||||
checksums,
|
||||
error: None,
|
||||
};
|
||||
|
||||
|
||||
+161
-11
@@ -15,9 +15,12 @@
|
||||
use crate::error::{Error, Result};
|
||||
use crate::fileinfo::{ErasureAlgo, ErasureInfo, FileInfo, FileInfoVersions, ObjectPartInfo, RawFileInfo};
|
||||
use crate::filemeta_inline::InlineData;
|
||||
use crate::{ReplicationStatusType, VersionPurgeStatusType};
|
||||
use crate::{
|
||||
ReplicationState, ReplicationStatusType, VersionPurgeStatusType, replication_statuses_map, version_purge_statuses_map,
|
||||
};
|
||||
use byteorder::ByteOrder;
|
||||
use bytes::Bytes;
|
||||
use rustfs_utils::http::AMZ_BUCKET_REPLICATION_STATUS;
|
||||
use rustfs_utils::http::headers::{
|
||||
self, AMZ_META_UNENCRYPTED_CONTENT_LENGTH, AMZ_META_UNENCRYPTED_CONTENT_MD5, AMZ_STORAGE_CLASS, RESERVED_METADATA_PREFIX,
|
||||
RESERVED_METADATA_PREFIX_LOWER, VERSION_PURGE_STATUS_KEY,
|
||||
@@ -30,6 +33,7 @@ use std::hash::Hasher;
|
||||
use std::io::{Read, Write};
|
||||
use std::{collections::HashMap, io::Cursor};
|
||||
use time::OffsetDateTime;
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
use tokio::io::AsyncRead;
|
||||
use tracing::error;
|
||||
use uuid::Uuid;
|
||||
@@ -1742,7 +1746,25 @@ impl MetaObject {
|
||||
}
|
||||
}
|
||||
|
||||
// todo: ReplicationState,Delete
|
||||
let replication_state_internal = get_internal_replication_state(&metadata);
|
||||
|
||||
let mut deleted = false;
|
||||
|
||||
if let Some(v) = replication_state_internal.as_ref() {
|
||||
if !v.composite_version_purge_status().is_empty() {
|
||||
deleted = true;
|
||||
}
|
||||
|
||||
let st = v.composite_replication_status();
|
||||
if !st.is_empty() {
|
||||
metadata.insert(AMZ_BUCKET_REPLICATION_STATUS.to_string(), st.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let checksum = self
|
||||
.meta_sys
|
||||
.get(format!("{RESERVED_METADATA_PREFIX_LOWER}crc").as_str())
|
||||
.map(|v| Bytes::from(v.clone()));
|
||||
|
||||
let erasure = ErasureInfo {
|
||||
algorithm: self.erasure_algorithm.to_string(),
|
||||
@@ -1754,6 +1776,26 @@ impl MetaObject {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let transition_status = self
|
||||
.meta_sys
|
||||
.get(format!("{RESERVED_METADATA_PREFIX_LOWER}{TRANSITION_STATUS}").as_str())
|
||||
.map(|v| String::from_utf8_lossy(v).to_string())
|
||||
.unwrap_or_default();
|
||||
let transitioned_objname = self
|
||||
.meta_sys
|
||||
.get(format!("{RESERVED_METADATA_PREFIX_LOWER}{TRANSITIONED_OBJECTNAME}").as_str())
|
||||
.map(|v| String::from_utf8_lossy(v).to_string())
|
||||
.unwrap_or_default();
|
||||
let transition_version_id = self
|
||||
.meta_sys
|
||||
.get(format!("{RESERVED_METADATA_PREFIX_LOWER}{TRANSITIONED_VERSION_ID}").as_str())
|
||||
.map(|v| Uuid::from_slice(v.as_slice()).unwrap_or_default());
|
||||
let transition_tier = self
|
||||
.meta_sys
|
||||
.get(format!("{RESERVED_METADATA_PREFIX_LOWER}{TRANSITION_TIER}").as_str())
|
||||
.map(|v| String::from_utf8_lossy(v).to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
FileInfo {
|
||||
version_id,
|
||||
erasure,
|
||||
@@ -1764,6 +1806,13 @@ impl MetaObject {
|
||||
volume: volume.to_string(),
|
||||
parts,
|
||||
metadata,
|
||||
replication_state_internal,
|
||||
deleted,
|
||||
checksum,
|
||||
transition_status,
|
||||
transitioned_objname,
|
||||
transition_version_id,
|
||||
transition_tier,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -1904,6 +1953,38 @@ impl From<FileInfo> for MetaObject {
|
||||
}
|
||||
}
|
||||
|
||||
if !value.transition_status.is_empty() {
|
||||
meta_sys.insert(
|
||||
format!("{RESERVED_METADATA_PREFIX_LOWER}{TRANSITION_STATUS}"),
|
||||
value.transition_status.as_bytes().to_vec(),
|
||||
);
|
||||
}
|
||||
|
||||
if !value.transitioned_objname.is_empty() {
|
||||
meta_sys.insert(
|
||||
format!("{RESERVED_METADATA_PREFIX_LOWER}{TRANSITIONED_OBJECTNAME}"),
|
||||
value.transitioned_objname.as_bytes().to_vec(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(vid) = &value.transition_version_id {
|
||||
meta_sys.insert(
|
||||
format!("{RESERVED_METADATA_PREFIX_LOWER}{TRANSITIONED_VERSION_ID}"),
|
||||
vid.as_bytes().to_vec(),
|
||||
);
|
||||
}
|
||||
|
||||
if !value.transition_tier.is_empty() {
|
||||
meta_sys.insert(
|
||||
format!("{RESERVED_METADATA_PREFIX_LOWER}{TRANSITION_TIER}"),
|
||||
value.transition_tier.as_bytes().to_vec(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(content_hash) = value.checksum {
|
||||
meta_sys.insert(format!("{RESERVED_METADATA_PREFIX_LOWER}crc"), content_hash.to_vec());
|
||||
}
|
||||
|
||||
Self {
|
||||
version_id: value.version_id,
|
||||
data_dir: value.data_dir,
|
||||
@@ -1927,6 +2008,50 @@ impl From<FileInfo> for MetaObject {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_internal_replication_state(metadata: &HashMap<String, String>) -> Option<ReplicationState> {
|
||||
let mut rs = ReplicationState::default();
|
||||
let mut has = false;
|
||||
|
||||
for (k, v) in metadata.iter() {
|
||||
if k == VERSION_PURGE_STATUS_KEY {
|
||||
rs.version_purge_status_internal = Some(v.clone());
|
||||
rs.purge_targets = version_purge_statuses_map(v.as_str());
|
||||
has = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(sub_key) = k.strip_prefix(RESERVED_METADATA_PREFIX_LOWER) {
|
||||
match sub_key {
|
||||
"replica-timestamp" => {
|
||||
has = true;
|
||||
rs.replica_timestamp = Some(OffsetDateTime::parse(v, &Rfc3339).unwrap_or(OffsetDateTime::UNIX_EPOCH));
|
||||
}
|
||||
"replica-status" => {
|
||||
has = true;
|
||||
rs.replica_status = ReplicationStatusType::from(v.as_str());
|
||||
}
|
||||
"replication-timestamp" => {
|
||||
has = true;
|
||||
rs.replication_timestamp = Some(OffsetDateTime::parse(v, &Rfc3339).unwrap_or(OffsetDateTime::UNIX_EPOCH))
|
||||
}
|
||||
"replication-status" => {
|
||||
has = true;
|
||||
rs.replication_status_internal = Some(v.clone());
|
||||
rs.targets = replication_statuses_map(v.as_str());
|
||||
}
|
||||
_ => {
|
||||
if let Some(arn) = sub_key.strip_prefix("replication-reset-") {
|
||||
has = true;
|
||||
rs.reset_statuses_map.insert(arn.to_string(), v.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if has { Some(rs) } else { None }
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
|
||||
pub struct MetaDeleteMarker {
|
||||
#[serde(rename = "ID")]
|
||||
@@ -1939,24 +2064,51 @@ pub struct MetaDeleteMarker {
|
||||
|
||||
impl MetaDeleteMarker {
|
||||
pub fn free_version(&self) -> bool {
|
||||
self.meta_sys.contains_key(FREE_VERSION_META_HEADER)
|
||||
self.meta_sys
|
||||
.contains_key(format!("{RESERVED_METADATA_PREFIX_LOWER}{FREE_VERSION}").as_str())
|
||||
}
|
||||
|
||||
pub fn into_fileinfo(&self, volume: &str, path: &str, _all_parts: bool) -> FileInfo {
|
||||
let metadata = self.meta_sys.clone();
|
||||
let metadata = self
|
||||
.meta_sys
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, String::from_utf8_lossy(&v).to_string()))
|
||||
.collect();
|
||||
let replication_state_internal = get_internal_replication_state(&metadata);
|
||||
|
||||
FileInfo {
|
||||
let mut fi = FileInfo {
|
||||
version_id: self.version_id.filter(|&vid| !vid.is_nil()),
|
||||
name: path.to_string(),
|
||||
volume: volume.to_string(),
|
||||
deleted: true,
|
||||
mod_time: self.mod_time,
|
||||
metadata: metadata
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, String::from_utf8_lossy(&v).to_string()))
|
||||
.collect(),
|
||||
metadata,
|
||||
replication_state_internal,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if self.free_version() {
|
||||
fi.set_tier_free_version();
|
||||
fi.transition_tier = self
|
||||
.meta_sys
|
||||
.get(format!("{RESERVED_METADATA_PREFIX_LOWER}{TRANSITION_TIER}").as_str())
|
||||
.map(|v| String::from_utf8_lossy(v).to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
fi.transitioned_objname = self
|
||||
.meta_sys
|
||||
.get(format!("{RESERVED_METADATA_PREFIX_LOWER}{TRANSITIONED_OBJECTNAME}").as_str())
|
||||
.map(|v| String::from_utf8_lossy(v).to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
fi.transition_version_id = self
|
||||
.meta_sys
|
||||
.get(format!("{RESERVED_METADATA_PREFIX_LOWER}{TRANSITIONED_VERSION_ID}").as_str())
|
||||
.map(|v| Uuid::from_slice(v.as_slice()).unwrap_or_default());
|
||||
}
|
||||
|
||||
fi
|
||||
}
|
||||
|
||||
pub fn unmarshal_msg(&mut self, buf: &[u8]) -> Result<u64> {
|
||||
@@ -2160,8 +2312,6 @@ pub enum Flags {
|
||||
InlineData = 1 << 2,
|
||||
}
|
||||
|
||||
const FREE_VERSION_META_HEADER: &str = "free-version";
|
||||
|
||||
// mergeXLV2Versions
|
||||
pub fn merge_file_meta_versions(
|
||||
mut quorum: usize,
|
||||
|
||||
@@ -1,8 +1,36 @@
|
||||
use bytes::Bytes;
|
||||
use core::fmt;
|
||||
use regex::Regex;
|
||||
use rustfs_utils::http::RESERVED_METADATA_PREFIX_LOWER;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub const REPLICATION_RESET: &str = "replication-reset";
|
||||
pub const REPLICATION_STATUS: &str = "replication-status";
|
||||
|
||||
// ReplicateQueued - replication being queued trail
|
||||
pub const REPLICATE_QUEUED: &str = "replicate:queue";
|
||||
|
||||
// ReplicateExisting - audit trail for existing objects replication
|
||||
pub const REPLICATE_EXISTING: &str = "replicate:existing";
|
||||
// ReplicateExistingDelete - audit trail for delete replication triggered for existing delete markers
|
||||
pub const REPLICATE_EXISTING_DELETE: &str = "replicate:existing:delete";
|
||||
|
||||
// ReplicateMRF - audit trail for replication from Most Recent Failures (MRF) queue
|
||||
pub const REPLICATE_MRF: &str = "replicate:mrf";
|
||||
// ReplicateIncoming - audit trail of inline replication
|
||||
pub const REPLICATE_INCOMING: &str = "replicate:incoming";
|
||||
// ReplicateIncomingDelete - audit trail of inline replication of deletes.
|
||||
pub const REPLICATE_INCOMING_DELETE: &str = "replicate:incoming:delete";
|
||||
|
||||
// ReplicateHeal - audit trail for healing of failed/pending replications
|
||||
pub const REPLICATE_HEAL: &str = "replicate:heal";
|
||||
// ReplicateHealDelete - audit trail of healing of failed/pending delete replications.
|
||||
pub const REPLICATE_HEAL_DELETE: &str = "replicate:heal:delete";
|
||||
|
||||
/// StatusType of Replication for x-amz-replication-status header
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, Hash)]
|
||||
@@ -492,3 +520,371 @@ impl ReplicatedInfos {
|
||||
ReplicationAction::None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct MrfReplicateEntry {
|
||||
#[serde(rename = "bucket")]
|
||||
pub bucket: String,
|
||||
|
||||
#[serde(rename = "object")]
|
||||
pub object: String,
|
||||
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub version_id: Option<Uuid>,
|
||||
|
||||
#[serde(rename = "retryCount")]
|
||||
pub retry_count: i32,
|
||||
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub size: i64,
|
||||
}
|
||||
|
||||
pub trait ReplicationWorkerOperation: Any + Send + Sync {
|
||||
fn to_mrf_entry(&self) -> MrfReplicateEntry;
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn get_bucket(&self) -> &str;
|
||||
fn get_object(&self) -> &str;
|
||||
fn get_size(&self) -> i64;
|
||||
fn is_delete_marker(&self) -> bool;
|
||||
fn get_op_type(&self) -> ReplicationType;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ReplicateTargetDecision {
|
||||
pub replicate: bool,
|
||||
pub synchronous: bool,
|
||||
pub arn: String,
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
impl ReplicateTargetDecision {
|
||||
pub fn new(arn: String, replicate: bool, sync: bool) -> Self {
|
||||
Self {
|
||||
replicate,
|
||||
synchronous: sync,
|
||||
arn,
|
||||
id: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ReplicateTargetDecision {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{};{};{};{}", self.replicate, self.synchronous, self.arn, self.id)
|
||||
}
|
||||
}
|
||||
|
||||
/// ReplicateDecision represents replication decision for each target
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ReplicateDecision {
|
||||
pub targets_map: HashMap<String, ReplicateTargetDecision>,
|
||||
}
|
||||
|
||||
impl ReplicateDecision {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
targets_map: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if at least one target qualifies for replication
|
||||
pub fn replicate_any(&self) -> bool {
|
||||
self.targets_map.values().any(|t| t.replicate)
|
||||
}
|
||||
|
||||
/// Returns true if at least one target qualifies for synchronous replication
|
||||
pub fn is_synchronous(&self) -> bool {
|
||||
self.targets_map.values().any(|t| t.synchronous)
|
||||
}
|
||||
|
||||
/// Updates ReplicateDecision with target's replication decision
|
||||
pub fn set(&mut self, target: ReplicateTargetDecision) {
|
||||
self.targets_map.insert(target.arn.clone(), target);
|
||||
}
|
||||
|
||||
/// Returns a stringified representation of internal replication status with all targets marked as `PENDING`
|
||||
pub fn pending_status(&self) -> Option<String> {
|
||||
let mut result = String::new();
|
||||
for target in self.targets_map.values() {
|
||||
if target.replicate {
|
||||
result.push_str(&format!("{}={};", target.arn, ReplicationStatusType::Pending.as_str()));
|
||||
}
|
||||
}
|
||||
if result.is_empty() { None } else { Some(result) }
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ReplicateDecision {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut result = String::new();
|
||||
for (key, value) in &self.targets_map {
|
||||
result.push_str(&format!("{key}={value},"));
|
||||
}
|
||||
write!(f, "{}", result.trim_end_matches(','))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ReplicateDecision {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
// parse k-v pairs of target ARN to stringified ReplicateTargetDecision delimited by ',' into a
|
||||
// ReplicateDecision struct
|
||||
pub fn parse_replicate_decision(_bucket: &str, s: &str) -> std::io::Result<ReplicateDecision> {
|
||||
let mut decision = ReplicateDecision::new();
|
||||
|
||||
if s.is_empty() {
|
||||
return Ok(decision);
|
||||
}
|
||||
|
||||
for p in s.split(',') {
|
||||
if p.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let slc = p.split('=').collect::<Vec<&str>>();
|
||||
if slc.len() != 2 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("invalid replicate decision format: {s}"),
|
||||
));
|
||||
}
|
||||
|
||||
let tgt_str = slc[1].trim_matches('"');
|
||||
let tgt = tgt_str.split(';').collect::<Vec<&str>>();
|
||||
if tgt.len() != 4 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("invalid replicate decision format: {s}"),
|
||||
));
|
||||
}
|
||||
|
||||
let tgt = ReplicateTargetDecision {
|
||||
replicate: tgt[0] == "true",
|
||||
synchronous: tgt[1] == "true",
|
||||
arn: tgt[2].to_string(),
|
||||
id: tgt[3].to_string(),
|
||||
};
|
||||
decision.targets_map.insert(slc[0].to_string(), tgt);
|
||||
}
|
||||
|
||||
Ok(decision)
|
||||
|
||||
// r = ReplicateDecision{
|
||||
// targetsMap: make(map[string]replicateTargetDecision),
|
||||
// }
|
||||
// if len(s) == 0 {
|
||||
// return
|
||||
// }
|
||||
// for _, p := range strings.Split(s, ",") {
|
||||
// if p == "" {
|
||||
// continue
|
||||
// }
|
||||
// slc := strings.Split(p, "=")
|
||||
// if len(slc) != 2 {
|
||||
// return r, errInvalidReplicateDecisionFormat
|
||||
// }
|
||||
// tgtStr := strings.TrimSuffix(strings.TrimPrefix(slc[1], `"`), `"`)
|
||||
// tgt := strings.Split(tgtStr, ";")
|
||||
// if len(tgt) != 4 {
|
||||
// return r, errInvalidReplicateDecisionFormat
|
||||
// }
|
||||
// r.targetsMap[slc[0]] = replicateTargetDecision{Replicate: tgt[0] == "true", Synchronous: tgt[1] == "true", Arn: tgt[2], ID: tgt[3]}
|
||||
// }
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ReplicateObjectInfo {
|
||||
pub name: String,
|
||||
pub size: i64,
|
||||
pub actual_size: i64,
|
||||
pub bucket: String,
|
||||
pub version_id: Option<Uuid>,
|
||||
pub etag: Option<String>,
|
||||
pub mod_time: Option<OffsetDateTime>,
|
||||
pub replication_status: ReplicationStatusType,
|
||||
pub replication_status_internal: Option<String>,
|
||||
pub delete_marker: bool,
|
||||
pub version_purge_status_internal: Option<String>,
|
||||
pub version_purge_status: VersionPurgeStatusType,
|
||||
pub replication_state: Option<ReplicationState>,
|
||||
pub op_type: ReplicationType,
|
||||
pub event_type: String,
|
||||
pub dsc: ReplicateDecision,
|
||||
pub existing_obj_resync: ResyncDecision,
|
||||
pub target_statuses: HashMap<String, ReplicationStatusType>,
|
||||
pub target_purge_statuses: HashMap<String, VersionPurgeStatusType>,
|
||||
pub replication_timestamp: Option<OffsetDateTime>,
|
||||
pub ssec: bool,
|
||||
pub user_tags: String,
|
||||
pub checksum: Option<Bytes>,
|
||||
pub retry_count: u32,
|
||||
}
|
||||
|
||||
impl ReplicationWorkerOperation for ReplicateObjectInfo {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn to_mrf_entry(&self) -> MrfReplicateEntry {
|
||||
MrfReplicateEntry {
|
||||
bucket: self.bucket.clone(),
|
||||
object: self.name.clone(),
|
||||
version_id: self.version_id,
|
||||
retry_count: self.retry_count as i32,
|
||||
size: self.size,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_bucket(&self) -> &str {
|
||||
&self.bucket
|
||||
}
|
||||
|
||||
fn get_object(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn get_size(&self) -> i64 {
|
||||
self.size
|
||||
}
|
||||
|
||||
fn is_delete_marker(&self) -> bool {
|
||||
self.delete_marker
|
||||
}
|
||||
|
||||
fn get_op_type(&self) -> ReplicationType {
|
||||
self.op_type
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref REPL_STATUS_REGEX: Regex = Regex::new(r"([^=].*?)=([^,].*?);").unwrap();
|
||||
}
|
||||
|
||||
impl ReplicateObjectInfo {
|
||||
/// Returns replication status of a target
|
||||
pub fn target_replication_status(&self, arn: &str) -> ReplicationStatusType {
|
||||
let binding = self.replication_status_internal.clone().unwrap_or_default();
|
||||
let captures = REPL_STATUS_REGEX.captures_iter(&binding);
|
||||
for cap in captures {
|
||||
if cap.len() == 3 && &cap[1] == arn {
|
||||
return ReplicationStatusType::from(&cap[2]);
|
||||
}
|
||||
}
|
||||
ReplicationStatusType::default()
|
||||
}
|
||||
|
||||
/// Returns the relevant info needed by MRF
|
||||
pub fn to_mrf_entry(&self) -> MrfReplicateEntry {
|
||||
MrfReplicateEntry {
|
||||
bucket: self.bucket.clone(),
|
||||
object: self.name.clone(),
|
||||
version_id: self.version_id,
|
||||
retry_count: self.retry_count as i32,
|
||||
size: self.size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// constructs a replication status map from string representation
|
||||
pub fn replication_statuses_map(s: &str) -> HashMap<String, ReplicationStatusType> {
|
||||
let mut targets = HashMap::new();
|
||||
let rep_stat_matches = REPL_STATUS_REGEX.captures_iter(s).map(|c| c.extract());
|
||||
for (_, [arn, status]) in rep_stat_matches {
|
||||
if arn.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let status = ReplicationStatusType::from(status);
|
||||
targets.insert(arn.to_string(), status);
|
||||
}
|
||||
targets
|
||||
}
|
||||
|
||||
// constructs a version purge status map from string representation
|
||||
pub fn version_purge_statuses_map(s: &str) -> HashMap<String, VersionPurgeStatusType> {
|
||||
let mut targets = HashMap::new();
|
||||
let purge_status_matches = REPL_STATUS_REGEX.captures_iter(s).map(|c| c.extract());
|
||||
for (_, [arn, status]) in purge_status_matches {
|
||||
if arn.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let status = VersionPurgeStatusType::from(status);
|
||||
targets.insert(arn.to_string(), status);
|
||||
}
|
||||
targets
|
||||
}
|
||||
|
||||
pub fn get_replication_state(rinfos: &ReplicatedInfos, prev_state: &ReplicationState, _vid: Option<String>) -> ReplicationState {
|
||||
let reset_status_map: Vec<(String, String)> = rinfos
|
||||
.targets
|
||||
.iter()
|
||||
.filter(|v| !v.resync_timestamp.is_empty())
|
||||
.map(|t| (target_reset_header(t.arn.as_str()), t.resync_timestamp.clone()))
|
||||
.collect();
|
||||
|
||||
let repl_statuses = rinfos.replication_status_internal();
|
||||
let vpurge_statuses = rinfos.version_purge_status_internal();
|
||||
|
||||
let mut reset_statuses_map = prev_state.reset_statuses_map.clone();
|
||||
for (key, value) in reset_status_map {
|
||||
reset_statuses_map.insert(key, value);
|
||||
}
|
||||
|
||||
ReplicationState {
|
||||
replicate_decision_str: prev_state.replicate_decision_str.clone(),
|
||||
reset_statuses_map,
|
||||
replica_timestamp: prev_state.replica_timestamp,
|
||||
replica_status: prev_state.replica_status.clone(),
|
||||
targets: replication_statuses_map(&repl_statuses.clone().unwrap_or_default()),
|
||||
replication_status_internal: repl_statuses,
|
||||
replication_timestamp: rinfos.replication_timestamp,
|
||||
purge_targets: version_purge_statuses_map(&vpurge_statuses.clone().unwrap_or_default()),
|
||||
version_purge_status_internal: vpurge_statuses,
|
||||
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn target_reset_header(arn: &str) -> String {
|
||||
format!("{RESERVED_METADATA_PREFIX_LOWER}{REPLICATION_RESET}-{arn}")
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ResyncTargetDecision {
|
||||
pub replicate: bool,
|
||||
pub reset_id: String,
|
||||
pub reset_before_date: Option<OffsetDateTime>,
|
||||
}
|
||||
|
||||
/// ResyncDecision is a struct representing a map with target's individual resync decisions
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ResyncDecision {
|
||||
pub targets: HashMap<String, ResyncTargetDecision>,
|
||||
}
|
||||
|
||||
impl ResyncDecision {
|
||||
pub fn new() -> Self {
|
||||
Self { targets: HashMap::new() }
|
||||
}
|
||||
|
||||
/// Returns true if no targets with resync decision present
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.targets.is_empty()
|
||||
}
|
||||
|
||||
pub fn must_resync(&self) -> bool {
|
||||
self.targets.values().any(|v| v.replicate)
|
||||
}
|
||||
|
||||
pub fn must_resync_target(&self, tgt_arn: &str) -> bool {
|
||||
self.targets.get(tgt_arn).map(|v| v.replicate).unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ResyncDecision {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ use rustfs_policy::{
|
||||
EMBEDDED_POLICY_TYPE, INHERITED_POLICY_TYPE, Policy, PolicyDoc, default::DEFAULT_POLICIES, iam_policy_claim_name_sa,
|
||||
},
|
||||
};
|
||||
use rustfs_utils::crypto::base64_encode;
|
||||
use rustfs_utils::path::path_join_buf;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
@@ -555,7 +554,10 @@ where
|
||||
return Err(Error::PolicyTooLarge);
|
||||
}
|
||||
|
||||
m.insert(SESSION_POLICY_NAME.to_owned(), Value::String(base64_encode(&policy_buf)));
|
||||
m.insert(
|
||||
SESSION_POLICY_NAME.to_owned(),
|
||||
Value::String(base64_simd::URL_SAFE_NO_PAD.encode_to_string(&policy_buf)),
|
||||
);
|
||||
m.insert(iam_policy_claim_name_sa(), Value::String(EMBEDDED_POLICY_TYPE.to_owned()));
|
||||
}
|
||||
}
|
||||
|
||||
+14
-6
@@ -37,7 +37,6 @@ use rustfs_policy::auth::{
|
||||
use rustfs_policy::policy::Args;
|
||||
use rustfs_policy::policy::opa;
|
||||
use rustfs_policy::policy::{EMBEDDED_POLICY_TYPE, INHERITED_POLICY_TYPE, Policy, PolicyDoc, iam_policy_claim_name_sa};
|
||||
use rustfs_utils::crypto::{base64_decode, base64_encode};
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
@@ -363,7 +362,10 @@ impl<T: Store> IamSys<T> {
|
||||
m.insert("parent".to_owned(), Value::String(parent_user.to_owned()));
|
||||
|
||||
if !policy_buf.is_empty() {
|
||||
m.insert(SESSION_POLICY_NAME.to_owned(), Value::String(base64_encode(&policy_buf)));
|
||||
m.insert(
|
||||
SESSION_POLICY_NAME.to_owned(),
|
||||
Value::String(base64_simd::URL_SAFE_NO_PAD.encode_to_string(&policy_buf)),
|
||||
);
|
||||
m.insert(iam_policy_claim_name_sa(), Value::String(EMBEDDED_POLICY_TYPE.to_owned()));
|
||||
} else {
|
||||
m.insert(iam_policy_claim_name_sa(), Value::String(INHERITED_POLICY_TYPE.to_owned()));
|
||||
@@ -456,7 +458,9 @@ impl<T: Store> IamSys<T> {
|
||||
let op_sp = claims.get(SESSION_POLICY_NAME);
|
||||
if let (Some(pt), Some(sp)) = (op_pt, op_sp) {
|
||||
if pt == EMBEDDED_POLICY_TYPE {
|
||||
let policy = serde_json::from_slice(&base64_decode(sp.as_str().unwrap_or_default().as_bytes())?)?;
|
||||
let policy = serde_json::from_slice(
|
||||
&base64_simd::URL_SAFE_NO_PAD.decode_to_vec(sp.as_str().unwrap_or_default().as_bytes())?,
|
||||
)?;
|
||||
return Ok((sa, Some(policy)));
|
||||
}
|
||||
}
|
||||
@@ -515,7 +519,9 @@ impl<T: Store> IamSys<T> {
|
||||
let op_sp = claims.get(SESSION_POLICY_NAME);
|
||||
if let (Some(pt), Some(sp)) = (op_pt, op_sp) {
|
||||
if pt == EMBEDDED_POLICY_TYPE {
|
||||
let policy = serde_json::from_slice(&base64_decode(sp.as_str().unwrap_or_default().as_bytes())?)?;
|
||||
let policy = serde_json::from_slice(
|
||||
&base64_simd::URL_SAFE_NO_PAD.decode_to_vec(sp.as_str().unwrap_or_default().as_bytes())?,
|
||||
)?;
|
||||
return Ok((sa, Some(policy)));
|
||||
}
|
||||
}
|
||||
@@ -528,7 +534,7 @@ impl<T: Store> IamSys<T> {
|
||||
return Err(IamError::NoSuchServiceAccount(access_key.to_string()));
|
||||
};
|
||||
|
||||
if u.credentials.is_service_account() {
|
||||
if !u.credentials.is_service_account() {
|
||||
return Err(IamError::NoSuchServiceAccount(access_key.to_string()));
|
||||
}
|
||||
|
||||
@@ -906,7 +912,9 @@ pub fn get_claims_from_token_with_secret(token: &str, secret: &str) -> Result<Ha
|
||||
|
||||
if let Some(session_policy) = ms.claims.get(SESSION_POLICY_NAME) {
|
||||
let policy_str = session_policy.as_str().unwrap_or_default();
|
||||
let policy = base64_decode(policy_str.as_bytes()).map_err(|e| Error::other(format!("base64 decode err {e}")))?;
|
||||
let policy = base64_simd::URL_SAFE_NO_PAD
|
||||
.decode_to_vec(policy_str.as_bytes())
|
||||
.map_err(|e| Error::other(format!("base64 decode err {e}")))?;
|
||||
ms.claims.insert(
|
||||
SESSION_POLICY_NAME_EXTRACTED.to_string(),
|
||||
Value::String(String::from_utf8(policy).map_err(|e| Error::other(format!("utf8 decode err {e}")))?),
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
|
||||
use crate::AppConfig;
|
||||
use crate::telemetry::{OtelGuard, init_telemetry};
|
||||
use opentelemetry::metrics::Meter;
|
||||
use rustfs_config::APP_NAME;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::{OnceCell, SetError};
|
||||
use tracing::{error, info};
|
||||
@@ -21,6 +23,23 @@ use tracing::{error, info};
|
||||
/// Global guard for OpenTelemetry tracing
|
||||
static GLOBAL_GUARD: OnceCell<Arc<Mutex<OtelGuard>>> = OnceCell::const_new();
|
||||
|
||||
/// Flag indicating if observability is enabled
|
||||
pub(crate) static IS_OBSERVABILITY_ENABLED: OnceCell<bool> = OnceCell::const_new();
|
||||
|
||||
/// Name of the observability meter
|
||||
pub(crate) static OBSERVABILITY_METER_NAME: OnceCell<String> = OnceCell::const_new();
|
||||
|
||||
/// Check whether Observability is enabled
|
||||
pub fn is_observability_enabled() -> bool {
|
||||
IS_OBSERVABILITY_ENABLED.get().copied().unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Get the global meter for observability
|
||||
pub fn global_meter() -> Meter {
|
||||
let meter_name = OBSERVABILITY_METER_NAME.get().map(|s| s.as_str()).unwrap_or(APP_NAME);
|
||||
opentelemetry::global::meter(meter_name)
|
||||
}
|
||||
|
||||
/// Error type for global guard operations
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum GlobalError {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use crate::config::OtelConfig;
|
||||
use crate::global::{IS_OBSERVABILITY_ENABLED, OBSERVABILITY_METER_NAME};
|
||||
use flexi_logger::{
|
||||
Age, Cleanup, Criterion, DeferredNow, FileSpec, LogSpecification, Naming, Record, WriteMode,
|
||||
WriteMode::{AsyncWith, BufferAndFlush},
|
||||
@@ -302,6 +303,8 @@ pub(crate) fn init_telemetry(config: &OtelConfig) -> OtelGuard {
|
||||
logger_level,
|
||||
env::var("RUST_LOG").unwrap_or_else(|_| "Not set".to_string())
|
||||
);
|
||||
IS_OBSERVABILITY_ENABLED.set(true).ok();
|
||||
OBSERVABILITY_METER_NAME.set(service_name.to_string()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
rustfs-config = { workspace = true, features = ["constants","opa"] }
|
||||
tokio.workspace = true
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
time = { workspace = true, features = ["serde-human-readable"] }
|
||||
serde = { workspace = true, features = ["derive", "rc"] }
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -39,7 +39,7 @@ pub struct AuthZPlugin {
|
||||
fn check() -> Result<(), String> {
|
||||
let env_list = env::vars();
|
||||
let mut candidate = HashMap::new();
|
||||
let prefix = format!("{}{}", ENV_PREFIX, POLICY_PLUGIN_SUB_SYS).to_uppercase();
|
||||
let prefix = format!("{ENV_PREFIX}{POLICY_PLUGIN_SUB_SYS}").to_uppercase();
|
||||
for (key, value) in env_list {
|
||||
if key.starts_with(&prefix) {
|
||||
candidate.insert(key.to_string(), value);
|
||||
@@ -48,13 +48,13 @@ fn check() -> Result<(), String> {
|
||||
|
||||
//check required env vars
|
||||
if candidate.remove(ENV_POLICY_PLUGIN_OPA_URL).is_none() {
|
||||
return Err(format!("Missing required env var: {}", ENV_POLICY_PLUGIN_OPA_URL));
|
||||
return Err(format!("Missing required env var: {ENV_POLICY_PLUGIN_OPA_URL}"));
|
||||
}
|
||||
|
||||
// check optional env vars
|
||||
candidate.remove(ENV_POLICY_PLUGIN_AUTH_TOKEN);
|
||||
if !candidate.is_empty() {
|
||||
return Err(format!("Invalid env vars: {:?}", candidate));
|
||||
return Err(format!("Invalid env vars: {candidate:?}"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -73,7 +73,7 @@ async fn validate(config: &Args) -> Result<(), String> {
|
||||
};
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(format!("Error connecting to OPA: {}", err));
|
||||
return Err(format!("Error connecting to OPA: {err}"));
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
@@ -83,7 +83,7 @@ pub async fn lookup_config() -> Result<Args, String> {
|
||||
let args = Args::default();
|
||||
|
||||
let get_cfg =
|
||||
|cfg: &str| -> Result<String, String> { env::var(cfg).map_err(|e| format!("Error getting env var {}: {:?}", cfg, e)) };
|
||||
|cfg: &str| -> Result<String, String> { env::var(cfg).map_err(|e| format!("Error getting env var {cfg}: {e:?}")) };
|
||||
|
||||
let url = get_cfg(ENV_POLICY_PLUGIN_OPA_URL);
|
||||
if url.is_err() {
|
||||
|
||||
@@ -154,8 +154,8 @@ impl Validator for Policy {
|
||||
type Error = Error;
|
||||
|
||||
fn is_valid(&self) -> Result<()> {
|
||||
if !self.id.is_empty() && !self.id.eq(DEFAULT_VERSION) {
|
||||
return Err(IamError::InvalidVersion(self.id.0.clone()).into());
|
||||
if !self.version.is_empty() && !self.version.eq(DEFAULT_VERSION) {
|
||||
return Err(IamError::InvalidVersion(self.version.clone()).into());
|
||||
}
|
||||
|
||||
for statement in self.statements.iter() {
|
||||
@@ -213,8 +213,8 @@ impl Validator for BucketPolicy {
|
||||
type Error = Error;
|
||||
|
||||
fn is_valid(&self) -> Result<()> {
|
||||
if !self.id.is_empty() && !self.id.eq(DEFAULT_VERSION) {
|
||||
return Err(IamError::InvalidVersion(self.id.0.clone()).into());
|
||||
if !self.version.is_empty() && !self.version.eq(DEFAULT_VERSION) {
|
||||
return Err(IamError::InvalidVersion(self.version.clone()).into());
|
||||
}
|
||||
|
||||
for statement in self.statements.iter() {
|
||||
|
||||
+10
-1
@@ -44,6 +44,15 @@ rustfs-utils = { workspace = true, features = ["io", "hash", "compress"] }
|
||||
serde_json.workspace = true
|
||||
md-5 = { workspace = true }
|
||||
tracing.workspace = true
|
||||
thiserror.workspace = true
|
||||
base64.workspace = true
|
||||
sha1.workspace = true
|
||||
sha2.workspace = true
|
||||
base64-simd.workspace = true
|
||||
crc64fast-nvme.workspace = true
|
||||
s3s.workspace = true
|
||||
hex-simd.workspace = true
|
||||
crc32c.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = { workspace = true }
|
||||
tokio-test = { workspace = true }
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// SHA256 mismatch error - when content SHA256 does not match what was sent from client
|
||||
#[derive(Error, Debug, Clone, PartialEq)]
|
||||
#[error("Bad sha256: Expected {expected_sha256} does not match calculated {calculated_sha256}")]
|
||||
pub struct Sha256Mismatch {
|
||||
pub expected_sha256: String,
|
||||
pub calculated_sha256: String,
|
||||
}
|
||||
|
||||
/// Bad digest error - Content-MD5 you specified did not match what we received
|
||||
#[derive(Error, Debug, Clone, PartialEq)]
|
||||
#[error("Bad digest: Expected {expected_md5} does not match calculated {calculated_md5}")]
|
||||
pub struct BadDigest {
|
||||
pub expected_md5: String,
|
||||
pub calculated_md5: String,
|
||||
}
|
||||
|
||||
/// Size too small error - reader size too small
|
||||
#[derive(Error, Debug, Clone, PartialEq)]
|
||||
#[error("Size small: got {got}, want {want}")]
|
||||
pub struct SizeTooSmall {
|
||||
pub want: i64,
|
||||
pub got: i64,
|
||||
}
|
||||
|
||||
/// Size too large error - reader size too large
|
||||
#[derive(Error, Debug, Clone, PartialEq)]
|
||||
#[error("Size large: got {got}, want {want}")]
|
||||
pub struct SizeTooLarge {
|
||||
pub want: i64,
|
||||
pub got: i64,
|
||||
}
|
||||
|
||||
/// Size mismatch error
|
||||
#[derive(Error, Debug, Clone, PartialEq)]
|
||||
#[error("Size mismatch: got {got}, want {want}")]
|
||||
pub struct SizeMismatch {
|
||||
pub want: i64,
|
||||
pub got: i64,
|
||||
}
|
||||
|
||||
/// Checksum mismatch error - when content checksum does not match what was sent from client
|
||||
#[derive(Error, Debug, Clone, PartialEq)]
|
||||
#[error("Bad checksum: Want {want} does not match calculated {got}")]
|
||||
pub struct ChecksumMismatch {
|
||||
pub want: String,
|
||||
pub got: String,
|
||||
}
|
||||
|
||||
/// Invalid checksum error
|
||||
#[derive(Error, Debug, Clone, PartialEq)]
|
||||
#[error("invalid checksum")]
|
||||
pub struct InvalidChecksum;
|
||||
|
||||
/// Check if an error is a checksum mismatch
|
||||
pub fn is_checksum_mismatch(err: &(dyn std::error::Error + 'static)) -> bool {
|
||||
err.downcast_ref::<ChecksumMismatch>().is_some()
|
||||
}
|
||||
+59
-18
@@ -51,6 +51,7 @@ mod tests {
|
||||
|
||||
use crate::{CompressReader, EncryptReader, EtagReader, HashReader};
|
||||
use crate::{WarpReader, resolve_etag_generic};
|
||||
use md5::Md5;
|
||||
use rustfs_utils::compress::CompressionAlgorithm;
|
||||
use std::io::Cursor;
|
||||
use tokio::io::BufReader;
|
||||
@@ -72,7 +73,7 @@ mod tests {
|
||||
let reader = BufReader::new(Cursor::new(&data[..]));
|
||||
let reader = Box::new(WarpReader::new(reader));
|
||||
let mut hash_reader =
|
||||
HashReader::new(reader, data.len() as i64, data.len() as i64, Some("hash_etag".to_string()), false).unwrap();
|
||||
HashReader::new(reader, data.len() as i64, data.len() as i64, Some("hash_etag".to_string()), None, false).unwrap();
|
||||
|
||||
// Test HashReader ETag resolution
|
||||
assert_eq!(resolve_etag_generic(&mut hash_reader), Some("hash_etag".to_string()));
|
||||
@@ -105,20 +106,30 @@ mod tests {
|
||||
assert_eq!(resolve_etag_generic(&mut encrypt_reader), Some("encrypt_etag".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_nesting() {
|
||||
#[tokio::test]
|
||||
async fn test_complex_nesting() {
|
||||
use md5::Digest;
|
||||
use tokio::io::AsyncReadExt;
|
||||
let data = b"test data for complex nesting";
|
||||
|
||||
let mut hasher = Md5::new();
|
||||
hasher.update(data);
|
||||
let etag = hasher.finalize();
|
||||
let etag_hex = hex_simd::encode_to_string(etag, hex_simd::AsciiCase::Lower);
|
||||
|
||||
let reader = BufReader::new(Cursor::new(&data[..]));
|
||||
let reader = Box::new(WarpReader::new(reader));
|
||||
// Create a complex nested structure: CompressReader<EncryptReader<EtagReader<BufReader<Cursor>>>>
|
||||
let etag_reader = EtagReader::new(reader, Some("nested_etag".to_string()));
|
||||
let etag_reader = EtagReader::new(reader, Some(etag_hex.clone()));
|
||||
let key = [0u8; 32];
|
||||
let nonce = [0u8; 12];
|
||||
let encrypt_reader = EncryptReader::new(etag_reader, key, nonce);
|
||||
let mut compress_reader = CompressReader::new(encrypt_reader, CompressionAlgorithm::Gzip);
|
||||
|
||||
compress_reader.read_to_end(&mut Vec::new()).await.unwrap();
|
||||
|
||||
// Test that nested structure can resolve ETag
|
||||
assert_eq!(resolve_etag_generic(&mut compress_reader), Some("nested_etag".to_string()));
|
||||
assert_eq!(resolve_etag_generic(&mut compress_reader), Some(etag_hex));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -127,51 +138,80 @@ mod tests {
|
||||
let reader = BufReader::new(Cursor::new(&data[..]));
|
||||
let reader = Box::new(WarpReader::new(reader));
|
||||
// Create nested structure: CompressReader<HashReader<BufReader<Cursor>>>
|
||||
let hash_reader =
|
||||
HashReader::new(reader, data.len() as i64, data.len() as i64, Some("hash_nested_etag".to_string()), false).unwrap();
|
||||
let hash_reader = HashReader::new(
|
||||
reader,
|
||||
data.len() as i64,
|
||||
data.len() as i64,
|
||||
Some("hash_nested_etag".to_string()),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
let mut compress_reader = CompressReader::new(hash_reader, CompressionAlgorithm::Deflate);
|
||||
|
||||
// Test that nested HashReader can be resolved
|
||||
assert_eq!(resolve_etag_generic(&mut compress_reader), Some("hash_nested_etag".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_comprehensive_etag_extraction() {
|
||||
#[tokio::test]
|
||||
async fn test_comprehensive_etag_extraction() {
|
||||
use md5::Digest;
|
||||
use tokio::io::AsyncReadExt;
|
||||
println!("🔍 Testing comprehensive ETag extraction with real reader types...");
|
||||
|
||||
// Test 1: Simple EtagReader
|
||||
let data1 = b"simple test";
|
||||
let mut hasher = Md5::new();
|
||||
hasher.update(data1);
|
||||
let etag = hasher.finalize();
|
||||
let etag_hex = hex_simd::encode_to_string(etag, hex_simd::AsciiCase::Lower);
|
||||
let reader1 = BufReader::new(Cursor::new(&data1[..]));
|
||||
let reader1 = Box::new(WarpReader::new(reader1));
|
||||
let mut etag_reader = EtagReader::new(reader1, Some("simple_etag".to_string()));
|
||||
assert_eq!(resolve_etag_generic(&mut etag_reader), Some("simple_etag".to_string()));
|
||||
let mut etag_reader = EtagReader::new(reader1, Some(etag_hex.clone()));
|
||||
etag_reader.read_to_end(&mut Vec::new()).await.unwrap();
|
||||
assert_eq!(resolve_etag_generic(&mut etag_reader), Some(etag_hex.clone()));
|
||||
|
||||
// Test 2: HashReader with ETag
|
||||
let data2 = b"hash test";
|
||||
let mut hasher = Md5::new();
|
||||
hasher.update(data2);
|
||||
let etag = hasher.finalize();
|
||||
let etag_hex = hex_simd::encode_to_string(etag, hex_simd::AsciiCase::Lower);
|
||||
let reader2 = BufReader::new(Cursor::new(&data2[..]));
|
||||
let reader2 = Box::new(WarpReader::new(reader2));
|
||||
let mut hash_reader =
|
||||
HashReader::new(reader2, data2.len() as i64, data2.len() as i64, Some("hash_etag".to_string()), false).unwrap();
|
||||
assert_eq!(resolve_etag_generic(&mut hash_reader), Some("hash_etag".to_string()));
|
||||
HashReader::new(reader2, data2.len() as i64, data2.len() as i64, Some(etag_hex.clone()), None, false).unwrap();
|
||||
hash_reader.read_to_end(&mut Vec::new()).await.unwrap();
|
||||
assert_eq!(resolve_etag_generic(&mut hash_reader), Some(etag_hex.clone()));
|
||||
|
||||
// Test 3: Single wrapper - CompressReader<EtagReader>
|
||||
let data3 = b"compress test";
|
||||
let mut hasher = Md5::new();
|
||||
hasher.update(data3);
|
||||
let etag = hasher.finalize();
|
||||
let etag_hex = hex_simd::encode_to_string(etag, hex_simd::AsciiCase::Lower);
|
||||
let reader3 = BufReader::new(Cursor::new(&data3[..]));
|
||||
let reader3 = Box::new(WarpReader::new(reader3));
|
||||
let etag_reader3 = EtagReader::new(reader3, Some("compress_wrapped_etag".to_string()));
|
||||
let etag_reader3 = EtagReader::new(reader3, Some(etag_hex.clone()));
|
||||
let mut compress_reader = CompressReader::new(etag_reader3, CompressionAlgorithm::Zstd);
|
||||
assert_eq!(resolve_etag_generic(&mut compress_reader), Some("compress_wrapped_etag".to_string()));
|
||||
compress_reader.read_to_end(&mut Vec::new()).await.unwrap();
|
||||
assert_eq!(resolve_etag_generic(&mut compress_reader), Some(etag_hex.clone()));
|
||||
|
||||
// Test 4: Double wrapper - CompressReader<EncryptReader<EtagReader>>
|
||||
let data4 = b"double wrap test";
|
||||
let mut hasher = Md5::new();
|
||||
hasher.update(data4);
|
||||
let etag = hasher.finalize();
|
||||
let etag_hex = hex_simd::encode_to_string(etag, hex_simd::AsciiCase::Lower);
|
||||
let reader4 = BufReader::new(Cursor::new(&data4[..]));
|
||||
let reader4 = Box::new(WarpReader::new(reader4));
|
||||
let etag_reader4 = EtagReader::new(reader4, Some("double_wrapped_etag".to_string()));
|
||||
let etag_reader4 = EtagReader::new(reader4, Some(etag_hex.clone()));
|
||||
let key = [1u8; 32];
|
||||
let nonce = [1u8; 12];
|
||||
let encrypt_reader4 = EncryptReader::new(etag_reader4, key, nonce);
|
||||
let mut compress_reader4 = CompressReader::new(encrypt_reader4, CompressionAlgorithm::Gzip);
|
||||
assert_eq!(resolve_etag_generic(&mut compress_reader4), Some("double_wrapped_etag".to_string()));
|
||||
compress_reader4.read_to_end(&mut Vec::new()).await.unwrap();
|
||||
assert_eq!(resolve_etag_generic(&mut compress_reader4), Some(etag_hex.clone()));
|
||||
|
||||
println!("✅ All ETag extraction methods work correctly!");
|
||||
println!("✅ Trait-based approach handles recursive unwrapping!");
|
||||
@@ -195,6 +235,7 @@ mod tests {
|
||||
data.len() as i64,
|
||||
data.len() as i64,
|
||||
Some("real_world_etag".to_string()),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -239,7 +280,7 @@ mod tests {
|
||||
let data = b"no etag test";
|
||||
let reader = BufReader::new(Cursor::new(&data[..]));
|
||||
let reader = Box::new(WarpReader::new(reader));
|
||||
let mut hash_reader_no_etag = HashReader::new(reader, data.len() as i64, data.len() as i64, None, false).unwrap();
|
||||
let mut hash_reader_no_etag = HashReader::new(reader, data.len() as i64, data.len() as i64, None, None, false).unwrap();
|
||||
assert_eq!(resolve_etag_generic(&mut hash_reader_no_etag), None);
|
||||
|
||||
// Test with EtagReader that has None etag
|
||||
|
||||
@@ -19,6 +19,7 @@ use pin_project_lite::pin_project;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use tokio::io::{AsyncRead, ReadBuf};
|
||||
use tracing::error;
|
||||
|
||||
pin_project! {
|
||||
pub struct EtagReader {
|
||||
@@ -43,7 +44,8 @@ impl EtagReader {
|
||||
/// Get the final md5 value (etag) as a hex string, only compute once.
|
||||
/// Can be called multiple times, always returns the same result after finished.
|
||||
pub fn get_etag(&mut self) -> String {
|
||||
format!("{:x}", self.md5.clone().finalize())
|
||||
let etag = self.md5.clone().finalize().to_vec();
|
||||
hex_simd::encode_to_string(etag, hex_simd::AsciiCase::Lower)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,8 +62,10 @@ impl AsyncRead for EtagReader {
|
||||
// EOF
|
||||
*this.finished = true;
|
||||
if let Some(checksum) = this.checksum {
|
||||
let etag = format!("{:x}", this.md5.clone().finalize());
|
||||
if *checksum != etag {
|
||||
let etag = this.md5.clone().finalize().to_vec();
|
||||
let etag_hex = hex_simd::encode_to_string(etag, hex_simd::AsciiCase::Lower);
|
||||
if *checksum != etag_hex {
|
||||
error!("Checksum mismatch, expected={:?}, actual={:?}", checksum, etag_hex);
|
||||
return Poll::Ready(Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Checksum mismatch")));
|
||||
}
|
||||
}
|
||||
@@ -214,7 +218,7 @@ mod tests {
|
||||
let data = b"checksum test data";
|
||||
let mut hasher = Md5::new();
|
||||
hasher.update(data);
|
||||
let expected = format!("{:x}", hasher.finalize());
|
||||
let expected = hex_simd::encode_to_string(hasher.finalize(), hex_simd::AsciiCase::Lower);
|
||||
let reader = BufReader::new(&data[..]);
|
||||
let reader = Box::new(WarpReader::new(reader));
|
||||
let mut etag_reader = EtagReader::new(reader, Some(expected.clone()));
|
||||
@@ -233,7 +237,7 @@ mod tests {
|
||||
let wrong_checksum = "deadbeefdeadbeefdeadbeefdeadbeef".to_string();
|
||||
let reader = BufReader::new(&data[..]);
|
||||
let reader = Box::new(WarpReader::new(reader));
|
||||
let mut etag_reader = EtagReader::new(reader, Some(wrong_checksum));
|
||||
let mut etag_reader = EtagReader::new(reader, Some(wrong_checksum.clone()));
|
||||
|
||||
let mut buf = Vec::new();
|
||||
// Verification failed, should return InvalidData error
|
||||
|
||||
+282
-33
@@ -50,7 +50,7 @@
|
||||
//! let diskable_md5 = false;
|
||||
//!
|
||||
//! // Method 1: Simple creation (recommended for most cases)
|
||||
//! let hash_reader = HashReader::new(reader, size, actual_size, etag.clone(), diskable_md5).unwrap();
|
||||
//! let hash_reader = HashReader::new(reader, size, actual_size, etag.clone(), None, diskable_md5).unwrap();
|
||||
//!
|
||||
//! // Method 2: With manual wrapping to recreate original logic
|
||||
//! let reader2 = BufReader::new(Cursor::new(&data[..]));
|
||||
@@ -71,7 +71,7 @@
|
||||
//! // No wrapping needed
|
||||
//! reader2
|
||||
//! };
|
||||
//! let hash_reader2 = HashReader::new(wrapped_reader, size, actual_size, etag, diskable_md5).unwrap();
|
||||
//! let hash_reader2 = HashReader::new(wrapped_reader, size, actual_size, etag.clone(), None, diskable_md5).unwrap();
|
||||
//! # });
|
||||
//! ```
|
||||
//!
|
||||
@@ -88,28 +88,43 @@
|
||||
//! # tokio_test::block_on(async {
|
||||
//! let data = b"test";
|
||||
//! let reader = BufReader::new(Cursor::new(&data[..]));
|
||||
//! let hash_reader = HashReader::new(Box::new(WarpReader::new(reader)), 4, 4, None, false).unwrap();
|
||||
//! let hash_reader = HashReader::new(Box::new(WarpReader::new(reader)), 4, 4, None, None,false).unwrap();
|
||||
//!
|
||||
//! // Check if a type is a HashReader
|
||||
//! assert!(hash_reader.is_hash_reader());
|
||||
//!
|
||||
//! // Use new for compatibility (though it's simpler to use new() directly)
|
||||
//! let reader2 = BufReader::new(Cursor::new(&data[..]));
|
||||
//! let result = HashReader::new(Box::new(WarpReader::new(reader2)), 4, 4, None, false);
|
||||
//! let result = HashReader::new(Box::new(WarpReader::new(reader2)), 4, 4, None, None, false);
|
||||
//! assert!(result.is_ok());
|
||||
//! # });
|
||||
//! ```
|
||||
|
||||
use crate::Checksum;
|
||||
use crate::ChecksumHasher;
|
||||
use crate::ChecksumType;
|
||||
use crate::Sha256Hasher;
|
||||
use crate::compress_index::{Index, TryGetIndex};
|
||||
use crate::get_content_checksum;
|
||||
use crate::{EtagReader, EtagResolvable, HardLimitReader, HashReaderDetector, Reader, WarpReader};
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose;
|
||||
use http::HeaderMap;
|
||||
use pin_project_lite::pin_project;
|
||||
use s3s::TrailingHeaders;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
use std::io::Write;
|
||||
use std::mem;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use tokio::io::{AsyncRead, ReadBuf};
|
||||
|
||||
use crate::compress_index::{Index, TryGetIndex};
|
||||
use crate::{EtagReader, EtagResolvable, HardLimitReader, HashReaderDetector, Reader};
|
||||
use tracing::error;
|
||||
|
||||
/// Trait for mutable operations on HashReader
|
||||
pub trait HashReaderMut {
|
||||
fn into_inner(self) -> Box<dyn Reader>;
|
||||
fn take_inner(&mut self) -> Box<dyn Reader>;
|
||||
fn bytes_read(&self) -> u64;
|
||||
fn checksum(&self) -> &Option<String>;
|
||||
fn set_checksum(&mut self, checksum: Option<String>);
|
||||
@@ -117,6 +132,10 @@ pub trait HashReaderMut {
|
||||
fn set_size(&mut self, size: i64);
|
||||
fn actual_size(&self) -> i64;
|
||||
fn set_actual_size(&mut self, actual_size: i64);
|
||||
fn content_hash(&self) -> &Option<Checksum>;
|
||||
fn content_sha256(&self) -> &Option<String>;
|
||||
fn get_trailer(&self) -> Option<&TrailingHeaders>;
|
||||
fn set_trailer(&mut self, trailer: Option<TrailingHeaders>);
|
||||
}
|
||||
|
||||
pin_project! {
|
||||
@@ -129,7 +148,14 @@ pin_project! {
|
||||
pub actual_size: i64,
|
||||
pub diskable_md5: bool,
|
||||
bytes_read: u64,
|
||||
// TODO: content_hash
|
||||
content_hash: Option<Checksum>,
|
||||
content_hasher: Option<Box<dyn ChecksumHasher>>,
|
||||
content_sha256: Option<String>,
|
||||
content_sha256_hasher: Option<Sha256Hasher>,
|
||||
checksum_on_finish: bool,
|
||||
|
||||
trailer_s3s: Option<TrailingHeaders>,
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -139,7 +165,8 @@ impl HashReader {
|
||||
mut inner: Box<dyn Reader>,
|
||||
size: i64,
|
||||
actual_size: i64,
|
||||
md5: Option<String>,
|
||||
md5hex: Option<String>,
|
||||
sha256hex: Option<String>,
|
||||
diskable_md5: bool,
|
||||
) -> std::io::Result<Self> {
|
||||
// Check if it's already a HashReader and update its parameters
|
||||
@@ -152,7 +179,7 @@ impl HashReader {
|
||||
}
|
||||
|
||||
if let Some(checksum) = existing_hash_reader.checksum() {
|
||||
if let Some(ref md5) = md5 {
|
||||
if let Some(ref md5) = md5hex {
|
||||
if checksum != md5 {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "HashReader checksum mismatch"));
|
||||
}
|
||||
@@ -166,7 +193,7 @@ impl HashReader {
|
||||
));
|
||||
}
|
||||
|
||||
existing_hash_reader.set_checksum(md5.clone());
|
||||
existing_hash_reader.set_checksum(md5hex.clone());
|
||||
|
||||
if existing_hash_reader.size() < 0 && size >= 0 {
|
||||
existing_hash_reader.set_size(size);
|
||||
@@ -176,13 +203,29 @@ impl HashReader {
|
||||
existing_hash_reader.set_actual_size(actual_size);
|
||||
}
|
||||
|
||||
let size = existing_hash_reader.size();
|
||||
let actual_size = existing_hash_reader.actual_size();
|
||||
let content_hash = existing_hash_reader.content_hash().clone();
|
||||
let content_hasher = existing_hash_reader
|
||||
.content_hash()
|
||||
.clone()
|
||||
.map(|hash| hash.checksum_type.hasher().unwrap());
|
||||
let content_sha256 = existing_hash_reader.content_sha256().clone();
|
||||
let content_sha256_hasher = existing_hash_reader.content_sha256().clone().map(|_| Sha256Hasher::new());
|
||||
let inner = existing_hash_reader.take_inner();
|
||||
return Ok(Self {
|
||||
inner,
|
||||
size,
|
||||
checksum: md5,
|
||||
checksum: md5hex.clone(),
|
||||
actual_size,
|
||||
diskable_md5,
|
||||
bytes_read: 0,
|
||||
content_sha256,
|
||||
content_sha256_hasher,
|
||||
content_hash,
|
||||
content_hasher,
|
||||
checksum_on_finish: false,
|
||||
trailer_s3s: existing_hash_reader.get_trailer().cloned(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -190,23 +233,33 @@ impl HashReader {
|
||||
let hr = HardLimitReader::new(inner, size);
|
||||
inner = Box::new(hr);
|
||||
if !diskable_md5 && !inner.is_hash_reader() {
|
||||
let er = EtagReader::new(inner, md5.clone());
|
||||
let er = EtagReader::new(inner, md5hex.clone());
|
||||
inner = Box::new(er);
|
||||
}
|
||||
} else if !diskable_md5 {
|
||||
let er = EtagReader::new(inner, md5.clone());
|
||||
let er = EtagReader::new(inner, md5hex.clone());
|
||||
inner = Box::new(er);
|
||||
}
|
||||
Ok(Self {
|
||||
inner,
|
||||
size,
|
||||
checksum: md5,
|
||||
checksum: md5hex,
|
||||
actual_size,
|
||||
diskable_md5,
|
||||
bytes_read: 0,
|
||||
content_hash: None,
|
||||
content_hasher: None,
|
||||
content_sha256: sha256hex.clone(),
|
||||
content_sha256_hasher: sha256hex.clone().map(|_| Sha256Hasher::new()),
|
||||
checksum_on_finish: false,
|
||||
trailer_s3s: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> Box<dyn Reader> {
|
||||
self.inner
|
||||
}
|
||||
|
||||
/// Update HashReader parameters
|
||||
pub fn update_params(&mut self, size: i64, actual_size: i64, etag: Option<String>) {
|
||||
if self.size < 0 && size >= 0 {
|
||||
@@ -228,9 +281,112 @@ impl HashReader {
|
||||
pub fn actual_size(&self) -> i64 {
|
||||
self.actual_size
|
||||
}
|
||||
|
||||
pub fn add_checksum_from_s3s(
|
||||
&mut self,
|
||||
headers: &HeaderMap,
|
||||
trailing_headers: Option<TrailingHeaders>,
|
||||
ignore_value: bool,
|
||||
) -> Result<(), std::io::Error> {
|
||||
let cs = get_content_checksum(headers)?;
|
||||
|
||||
if ignore_value {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(checksum) = cs {
|
||||
if checksum.checksum_type.trailing() {
|
||||
self.trailer_s3s = trailing_headers.clone();
|
||||
}
|
||||
|
||||
self.content_hash = Some(checksum.clone());
|
||||
|
||||
return self.add_non_trailing_checksum(Some(checksum), ignore_value);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_checksum_no_trailer(&mut self, header: &HeaderMap, ignore_value: bool) -> Result<(), std::io::Error> {
|
||||
let cs = get_content_checksum(header)?;
|
||||
|
||||
if let Some(checksum) = cs {
|
||||
self.content_hash = Some(checksum.clone());
|
||||
|
||||
return self.add_non_trailing_checksum(Some(checksum), ignore_value);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_non_trailing_checksum(&mut self, checksum: Option<Checksum>, ignore_value: bool) -> Result<(), std::io::Error> {
|
||||
if let Some(checksum) = checksum {
|
||||
self.content_hash = Some(checksum.clone());
|
||||
|
||||
if ignore_value {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(hasher) = checksum.checksum_type.hasher() {
|
||||
self.content_hasher = Some(hasher);
|
||||
} else {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid checksum type"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn checksum(&self) -> Option<Checksum> {
|
||||
if self
|
||||
.content_hash
|
||||
.as_ref()
|
||||
.is_none_or(|v| !v.checksum_type.is_set() || !v.valid())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
self.content_hash.clone()
|
||||
}
|
||||
pub fn content_crc_type(&self) -> Option<ChecksumType> {
|
||||
self.content_hash.as_ref().map(|v| v.checksum_type)
|
||||
}
|
||||
|
||||
pub fn content_crc(&self) -> HashMap<String, String> {
|
||||
let mut map = HashMap::new();
|
||||
if let Some(checksum) = self.content_hash.as_ref() {
|
||||
if !checksum.valid() || checksum.checksum_type.is(ChecksumType::NONE) {
|
||||
return map;
|
||||
}
|
||||
|
||||
if checksum.checksum_type.trailing() {
|
||||
if let Some(trailer) = self.trailer_s3s.as_ref() {
|
||||
if let Some(Some(checksum_str)) = trailer.read(|headers| {
|
||||
headers
|
||||
.get(checksum.checksum_type.to_string())
|
||||
.and_then(|value| value.to_str().ok().map(|s| s.to_string()))
|
||||
}) {
|
||||
map.insert(checksum.checksum_type.to_string(), checksum_str);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
map.insert(checksum.checksum_type.to_string(), checksum.encoded.clone());
|
||||
|
||||
return map;
|
||||
}
|
||||
map
|
||||
}
|
||||
}
|
||||
|
||||
impl HashReaderMut for HashReader {
|
||||
fn into_inner(self) -> Box<dyn Reader> {
|
||||
self.inner
|
||||
}
|
||||
|
||||
fn take_inner(&mut self) -> Box<dyn Reader> {
|
||||
// Replace inner with an empty reader to move it out safely while keeping self valid
|
||||
mem::replace(&mut self.inner, Box::new(WarpReader::new(Cursor::new(Vec::new()))))
|
||||
}
|
||||
|
||||
fn bytes_read(&self) -> u64 {
|
||||
self.bytes_read
|
||||
}
|
||||
@@ -258,22 +414,108 @@ impl HashReaderMut for HashReader {
|
||||
fn set_actual_size(&mut self, actual_size: i64) {
|
||||
self.actual_size = actual_size;
|
||||
}
|
||||
|
||||
fn content_hash(&self) -> &Option<Checksum> {
|
||||
&self.content_hash
|
||||
}
|
||||
|
||||
fn content_sha256(&self) -> &Option<String> {
|
||||
&self.content_sha256
|
||||
}
|
||||
|
||||
fn get_trailer(&self) -> Option<&TrailingHeaders> {
|
||||
self.trailer_s3s.as_ref()
|
||||
}
|
||||
|
||||
fn set_trailer(&mut self, trailer: Option<TrailingHeaders>) {
|
||||
self.trailer_s3s = trailer;
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for HashReader {
|
||||
fn poll_read(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<std::io::Result<()>> {
|
||||
let this = self.project();
|
||||
let poll = this.inner.poll_read(cx, buf);
|
||||
if let Poll::Ready(Ok(())) = &poll {
|
||||
let filled = buf.filled().len();
|
||||
*this.bytes_read += filled as u64;
|
||||
|
||||
if filled == 0 {
|
||||
// EOF
|
||||
// TODO: check content_hash
|
||||
let before = buf.filled().len();
|
||||
match this.inner.poll_read(cx, buf) {
|
||||
Poll::Pending => Poll::Pending,
|
||||
Poll::Ready(Ok(())) => {
|
||||
let data = &buf.filled()[before..];
|
||||
let filled = data.len();
|
||||
|
||||
*this.bytes_read += filled as u64;
|
||||
|
||||
if filled > 0 {
|
||||
// Update SHA256 hasher
|
||||
if let Some(hasher) = this.content_sha256_hasher {
|
||||
if let Err(e) = hasher.write_all(data) {
|
||||
error!("SHA256 hasher write error, error={:?}", e);
|
||||
return Poll::Ready(Err(std::io::Error::other(e)));
|
||||
}
|
||||
}
|
||||
|
||||
// Update content hasher
|
||||
if let Some(hasher) = this.content_hasher {
|
||||
if let Err(e) = hasher.write_all(data) {
|
||||
return Poll::Ready(Err(std::io::Error::other(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if filled == 0 && !*this.checksum_on_finish {
|
||||
// check SHA256
|
||||
if let (Some(hasher), Some(expected_sha256)) = (this.content_sha256_hasher, this.content_sha256) {
|
||||
let sha256 = hex_simd::encode_to_string(hasher.finalize(), hex_simd::AsciiCase::Lower);
|
||||
if sha256 != *expected_sha256 {
|
||||
error!("SHA256 mismatch, expected={:?}, actual={:?}", expected_sha256, sha256);
|
||||
return Poll::Ready(Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "SHA256 mismatch")));
|
||||
}
|
||||
}
|
||||
|
||||
// check content hasher
|
||||
if let (Some(hasher), Some(expected_content_hash)) = (this.content_hasher, this.content_hash) {
|
||||
if expected_content_hash.checksum_type.trailing() {
|
||||
if let Some(trailer) = this.trailer_s3s.as_ref() {
|
||||
if let Some(Some(checksum_str)) = trailer.read(|headers| {
|
||||
expected_content_hash.checksum_type.key().and_then(|key| {
|
||||
headers.get(key).and_then(|value| value.to_str().ok().map(|s| s.to_string()))
|
||||
})
|
||||
}) {
|
||||
expected_content_hash.encoded = checksum_str;
|
||||
expected_content_hash.raw = general_purpose::STANDARD
|
||||
.decode(&expected_content_hash.encoded)
|
||||
.map_err(|_| std::io::Error::other("Invalid base64 checksum"))?;
|
||||
|
||||
if expected_content_hash.raw.is_empty() {
|
||||
return Poll::Ready(Err(std::io::Error::other("Content hash mismatch")));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let content_hash = hasher.finalize();
|
||||
|
||||
if content_hash != expected_content_hash.raw {
|
||||
error!(
|
||||
"Content hash mismatch, type={:?}, encoded={:?}, expected={:?}, actual={:?}",
|
||||
expected_content_hash.checksum_type,
|
||||
expected_content_hash.encoded,
|
||||
hex_simd::encode_to_string(&expected_content_hash.raw, hex_simd::AsciiCase::Lower),
|
||||
hex_simd::encode_to_string(content_hash, hex_simd::AsciiCase::Lower)
|
||||
);
|
||||
return Poll::Ready(Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"Content hash mismatch",
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
*this.checksum_on_finish = true;
|
||||
}
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
|
||||
}
|
||||
poll
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,7 +565,7 @@ mod tests {
|
||||
// Test 1: Simple creation
|
||||
let reader1 = BufReader::new(Cursor::new(&data[..]));
|
||||
let reader1 = Box::new(WarpReader::new(reader1));
|
||||
let hash_reader1 = HashReader::new(reader1, size, actual_size, etag.clone(), false).unwrap();
|
||||
let hash_reader1 = HashReader::new(reader1, size, actual_size, etag.clone(), None, false).unwrap();
|
||||
assert_eq!(hash_reader1.size(), size);
|
||||
assert_eq!(hash_reader1.actual_size(), actual_size);
|
||||
|
||||
@@ -332,7 +574,7 @@ mod tests {
|
||||
let reader2 = Box::new(WarpReader::new(reader2));
|
||||
let hard_limit = HardLimitReader::new(reader2, size);
|
||||
let hard_limit = Box::new(hard_limit);
|
||||
let hash_reader2 = HashReader::new(hard_limit, size, actual_size, etag.clone(), false).unwrap();
|
||||
let hash_reader2 = HashReader::new(hard_limit, size, actual_size, etag.clone(), None, false).unwrap();
|
||||
assert_eq!(hash_reader2.size(), size);
|
||||
assert_eq!(hash_reader2.actual_size(), actual_size);
|
||||
|
||||
@@ -341,7 +583,7 @@ mod tests {
|
||||
let reader3 = Box::new(WarpReader::new(reader3));
|
||||
let etag_reader = EtagReader::new(reader3, etag.clone());
|
||||
let etag_reader = Box::new(etag_reader);
|
||||
let hash_reader3 = HashReader::new(etag_reader, size, actual_size, etag.clone(), false).unwrap();
|
||||
let hash_reader3 = HashReader::new(etag_reader, size, actual_size, etag.clone(), None, false).unwrap();
|
||||
assert_eq!(hash_reader3.size(), size);
|
||||
assert_eq!(hash_reader3.actual_size(), actual_size);
|
||||
}
|
||||
@@ -351,7 +593,7 @@ mod tests {
|
||||
let data = b"hello hashreader";
|
||||
let reader = BufReader::new(Cursor::new(&data[..]));
|
||||
let reader = Box::new(WarpReader::new(reader));
|
||||
let mut hash_reader = HashReader::new(reader, data.len() as i64, data.len() as i64, None, false).unwrap();
|
||||
let mut hash_reader = HashReader::new(reader, data.len() as i64, data.len() as i64, None, None, false).unwrap();
|
||||
let mut buf = Vec::new();
|
||||
let _ = hash_reader.read_to_end(&mut buf).await.unwrap();
|
||||
// Since we removed EtagReader integration, etag might be None
|
||||
@@ -365,7 +607,7 @@ mod tests {
|
||||
let data = b"no etag";
|
||||
let reader = BufReader::new(Cursor::new(&data[..]));
|
||||
let reader = Box::new(WarpReader::new(reader));
|
||||
let mut hash_reader = HashReader::new(reader, data.len() as i64, data.len() as i64, None, true).unwrap();
|
||||
let mut hash_reader = HashReader::new(reader, data.len() as i64, data.len() as i64, None, None, true).unwrap();
|
||||
let mut buf = Vec::new();
|
||||
let _ = hash_reader.read_to_end(&mut buf).await.unwrap();
|
||||
// Etag should be None when diskable_md5 is true
|
||||
@@ -381,10 +623,17 @@ mod tests {
|
||||
let reader = Box::new(WarpReader::new(reader));
|
||||
// Create a HashReader first
|
||||
let hash_reader =
|
||||
HashReader::new(reader, data.len() as i64, data.len() as i64, Some("test_etag".to_string()), false).unwrap();
|
||||
HashReader::new(reader, data.len() as i64, data.len() as i64, Some("test_etag".to_string()), None, false).unwrap();
|
||||
let hash_reader = Box::new(WarpReader::new(hash_reader));
|
||||
// Now try to create another HashReader from the existing one using new
|
||||
let result = HashReader::new(hash_reader, data.len() as i64, data.len() as i64, Some("test_etag".to_string()), false);
|
||||
let result = HashReader::new(
|
||||
hash_reader,
|
||||
data.len() as i64,
|
||||
data.len() as i64,
|
||||
Some("test_etag".to_string()),
|
||||
None,
|
||||
false,
|
||||
);
|
||||
|
||||
assert!(result.is_ok());
|
||||
let final_reader = result.unwrap();
|
||||
@@ -422,7 +671,7 @@ mod tests {
|
||||
|
||||
let reader = Box::new(WarpReader::new(reader));
|
||||
// Create HashReader
|
||||
let mut hr = HashReader::new(reader, size, actual_size, Some(expected.clone()), false).unwrap();
|
||||
let mut hr = HashReader::new(reader, size, actual_size, Some(expected.clone()), None, false).unwrap();
|
||||
|
||||
// If compression is enabled, compress data first
|
||||
let compressed_data = if is_compress {
|
||||
@@ -518,7 +767,7 @@ mod tests {
|
||||
|
||||
let reader = BufReader::new(Cursor::new(data.clone()));
|
||||
let reader = Box::new(WarpReader::new(reader));
|
||||
let hash_reader = HashReader::new(reader, data.len() as i64, data.len() as i64, None, false).unwrap();
|
||||
let hash_reader = HashReader::new(reader, data.len() as i64, data.len() as i64, None, None, false).unwrap();
|
||||
|
||||
// Test compression
|
||||
let compress_reader = CompressReader::new(hash_reader, CompressionAlgorithm::Gzip);
|
||||
@@ -564,7 +813,7 @@ mod tests {
|
||||
|
||||
let reader = BufReader::new(Cursor::new(data.clone()));
|
||||
let reader = Box::new(WarpReader::new(reader));
|
||||
let hash_reader = HashReader::new(reader, data.len() as i64, data.len() as i64, None, false).unwrap();
|
||||
let hash_reader = HashReader::new(reader, data.len() as i64, data.len() as i64, None, None, false).unwrap();
|
||||
|
||||
// Compress
|
||||
let compress_reader = CompressReader::new(hash_reader, algorithm);
|
||||
|
||||
@@ -34,6 +34,11 @@ pub use hardlimit_reader::HardLimitReader;
|
||||
|
||||
mod hash_reader;
|
||||
pub use hash_reader::*;
|
||||
mod checksum;
|
||||
pub use checksum::*;
|
||||
|
||||
mod errors;
|
||||
pub use errors::*;
|
||||
|
||||
pub mod reader;
|
||||
pub use reader::WarpReader;
|
||||
|
||||
@@ -34,6 +34,7 @@ hyper.workspace = true
|
||||
serde_urlencoded.workspace = true
|
||||
rustfs-utils = { workspace = true, features = ["full"] }
|
||||
s3s.workspace = true
|
||||
base64-simd.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ use std::fmt::Write;
|
||||
use time::{OffsetDateTime, format_description};
|
||||
|
||||
use super::utils::get_host_addr;
|
||||
use rustfs_utils::crypto::{base64_encode, hex, hmac_sha1};
|
||||
use rustfs_utils::crypto::{hex, hmac_sha1};
|
||||
use s3s::Body;
|
||||
|
||||
const _SIGN_V4_ALGORITHM: &str = "AWS4-HMAC-SHA256";
|
||||
@@ -111,7 +111,11 @@ pub fn sign_v2(
|
||||
}
|
||||
|
||||
let auth_header = format!("{SIGN_V2_ALGORITHM} {access_key_id}:");
|
||||
let auth_header = format!("{}{}", auth_header, base64_encode(&hmac_sha1(secret_access_key, string_to_sign)));
|
||||
let auth_header = format!(
|
||||
"{}{}",
|
||||
auth_header,
|
||||
base64_simd::URL_SAFE_NO_PAD.encode_to_string(hmac_sha1(secret_access_key, string_to_sign))
|
||||
);
|
||||
|
||||
headers.insert("Authorization", auth_header.parse().unwrap());
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ hex-simd = { workspace = true, optional = true }
|
||||
highway = { workspace = true, optional = true }
|
||||
hickory-resolver = { workspace = true, optional = true }
|
||||
hmac = { workspace = true, optional = true }
|
||||
http = { workspace = true, optional = true }
|
||||
hyper = { workspace = true, optional = true }
|
||||
libc = { workspace = true, optional = true }
|
||||
local-ip-address = { workspace = true, optional = true }
|
||||
@@ -93,5 +94,5 @@ hash = ["dep:highway", "dep:md-5", "dep:sha2", "dep:blake3", "dep:serde", "dep:s
|
||||
os = ["dep:nix", "dep:tempfile", "winapi"] # operating system utilities
|
||||
integration = [] # integration test features
|
||||
sys = ["dep:sysinfo"] # system information features
|
||||
http = ["dep:convert_case"]
|
||||
http = ["dep:convert_case", "dep:http"]
|
||||
full = ["ip", "tls", "net", "io", "hash", "os", "integration", "path", "crypto", "string", "compress", "sys", "notify", "http"] # all features
|
||||
|
||||
@@ -17,11 +17,11 @@ use std::mem::MaybeUninit;
|
||||
use hex_simd::{AsOut, AsciiCase};
|
||||
use hyper::body::Bytes;
|
||||
|
||||
pub fn base64_encode(input: &[u8]) -> String {
|
||||
pub fn base64_encode_url_safe_no_pad(input: &[u8]) -> String {
|
||||
base64_simd::URL_SAFE_NO_PAD.encode_to_string(input)
|
||||
}
|
||||
|
||||
pub fn base64_decode(input: &[u8]) -> Result<Vec<u8>, base64_simd::Error> {
|
||||
pub fn base64_decode_url_safe_no_pad(input: &[u8]) -> Result<Vec<u8>, base64_simd::Error> {
|
||||
base64_simd::URL_SAFE_NO_PAD.decode_to_vec(input)
|
||||
}
|
||||
|
||||
@@ -89,11 +89,11 @@ pub fn hex_sha256_chunk<R>(chunk: &[Bytes], f: impl FnOnce(&str) -> R) -> R {
|
||||
fn test_base64_encoding_decoding() {
|
||||
let original_uuid_timestamp = "c0194290-d911-45cb-8e12-79ec563f46a8x1735460504394878000";
|
||||
|
||||
let encoded_string = base64_encode(original_uuid_timestamp.as_bytes());
|
||||
let encoded_string = base64_encode_url_safe_no_pad(original_uuid_timestamp.as_bytes());
|
||||
|
||||
println!("Encoded: {}", &encoded_string);
|
||||
|
||||
let decoded_bytes = base64_decode(encoded_string.clone().as_bytes()).unwrap();
|
||||
let decoded_bytes = base64_decode_url_safe_no_pad(encoded_string.clone().as_bytes()).unwrap();
|
||||
let decoded_string = String::from_utf8(decoded_bytes).unwrap();
|
||||
|
||||
assert_eq!(decoded_string, original_uuid_timestamp)
|
||||
|
||||
@@ -176,6 +176,7 @@ pub const RUSTFS_BUCKET_REPLICATION_DELETE_MARKER: &str = "X-Rustfs-Source-Delet
|
||||
pub const RUSTFS_BUCKET_REPLICATION_PROXY_REQUEST: &str = "X-Rustfs-Source-Proxy-Request";
|
||||
pub const RUSTFS_BUCKET_REPLICATION_REQUEST: &str = "X-Rustfs-Source-Replication-Request";
|
||||
pub const RUSTFS_BUCKET_REPLICATION_CHECK: &str = "X-Rustfs-Source-Replication-Check";
|
||||
pub const RUSTFS_BUCKET_REPLICATION_SSEC_CHECKSUM: &str = "X-Rustfs-Source-Replication-Ssec-Crc";
|
||||
|
||||
// SSEC encryption header constants
|
||||
pub const SSEC_ALGORITHM_HEADER: &str = "x-amz-server-side-encryption-customer-algorithm";
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use http::HeaderMap;
|
||||
use regex::Regex;
|
||||
use std::env;
|
||||
use std::net::SocketAddr;
|
||||
use std::str::FromStr;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// De-facto standard header keys.
|
||||
const X_FORWARDED_FOR: &str = "x-forwarded-for";
|
||||
const X_FORWARDED_PROTO: &str = "x-forwarded-proto";
|
||||
const X_FORWARDED_SCHEME: &str = "x-forwarded-scheme";
|
||||
const X_REAL_IP: &str = "x-real-ip";
|
||||
|
||||
/// RFC7239 defines a new "Forwarded: " header designed to replace the
|
||||
/// existing use of X-Forwarded-* headers.
|
||||
/// e.g. Forwarded: for=192.0.2.60;proto=https;by=203.0.113.43
|
||||
const FORWARDED: &str = "forwarded";
|
||||
|
||||
static FOR_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)(?:for=)([^(;|,| )]+)(.*)").unwrap());
|
||||
static PROTO_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)^(;|,| )+(?:proto=)(https|http)").unwrap());
|
||||
|
||||
/// Used to disable all processing of the X-Forwarded-For header in source IP discovery.
|
||||
fn is_xff_header_enabled() -> bool {
|
||||
env::var("_RUSTFS_API_XFF_HEADER")
|
||||
.unwrap_or_else(|_| "on".to_string())
|
||||
.to_lowercase()
|
||||
== "on"
|
||||
}
|
||||
|
||||
/// GetSourceScheme retrieves the scheme from the X-Forwarded-Proto and RFC7239
|
||||
/// Forwarded headers (in that order).
|
||||
pub fn get_source_scheme(headers: &HeaderMap) -> Option<String> {
|
||||
// Retrieve the scheme from X-Forwarded-Proto.
|
||||
if let Some(proto) = headers.get(X_FORWARDED_PROTO) {
|
||||
if let Ok(proto_str) = proto.to_str() {
|
||||
return Some(proto_str.to_lowercase());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(proto) = headers.get(X_FORWARDED_SCHEME) {
|
||||
if let Ok(proto_str) = proto.to_str() {
|
||||
return Some(proto_str.to_lowercase());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(forwarded) = headers.get(FORWARDED) {
|
||||
if let Ok(forwarded_str) = forwarded.to_str() {
|
||||
// match should contain at least two elements if the protocol was
|
||||
// specified in the Forwarded header. The first element will always be
|
||||
// the 'for=', which we ignore, subsequently we proceed to look for
|
||||
// 'proto=' which should precede right after `for=` if not
|
||||
// we simply ignore the values and return empty. This is in line
|
||||
// with the approach we took for returning first ip from multiple
|
||||
// params.
|
||||
if let Some(for_match) = FOR_REGEX.captures(forwarded_str) {
|
||||
if for_match.len() > 1 {
|
||||
let remaining = &for_match[2];
|
||||
if let Some(proto_match) = PROTO_REGEX.captures(remaining) {
|
||||
if proto_match.len() > 1 {
|
||||
return Some(proto_match[2].to_lowercase());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// GetSourceIPFromHeaders retrieves the IP from the X-Forwarded-For, X-Real-IP
|
||||
/// and RFC7239 Forwarded headers (in that order)
|
||||
pub fn get_source_ip_from_headers(headers: &HeaderMap) -> Option<String> {
|
||||
let mut addr = None;
|
||||
|
||||
if is_xff_header_enabled() {
|
||||
if let Some(forwarded_for) = headers.get(X_FORWARDED_FOR) {
|
||||
if let Ok(forwarded_str) = forwarded_for.to_str() {
|
||||
// Only grab the first (client) address. Note that '192.168.0.1,
|
||||
// 10.1.1.1' is a valid key for X-Forwarded-For where addresses after
|
||||
// the first may represent forwarding proxies earlier in the chain.
|
||||
let first_comma = forwarded_str.find(", ");
|
||||
let end = first_comma.unwrap_or(forwarded_str.len());
|
||||
addr = Some(forwarded_str[..end].to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if addr.is_none() {
|
||||
if let Some(real_ip) = headers.get(X_REAL_IP) {
|
||||
if let Ok(real_ip_str) = real_ip.to_str() {
|
||||
// X-Real-IP should only contain one IP address (the client making the
|
||||
// request).
|
||||
addr = Some(real_ip_str.to_string());
|
||||
}
|
||||
} else if let Some(forwarded) = headers.get(FORWARDED) {
|
||||
if let Ok(forwarded_str) = forwarded.to_str() {
|
||||
// match should contain at least two elements if the protocol was
|
||||
// specified in the Forwarded header. The first element will always be
|
||||
// the 'for=' capture, which we ignore. In the case of multiple IP
|
||||
// addresses (for=8.8.8.8, 8.8.4.4, 172.16.1.20 is valid) we only
|
||||
// extract the first, which should be the client IP.
|
||||
if let Some(for_match) = FOR_REGEX.captures(forwarded_str) {
|
||||
if for_match.len() > 1 {
|
||||
// IPv6 addresses in Forwarded headers are quoted-strings. We strip
|
||||
// these quotes.
|
||||
let ip = for_match[1].trim_matches('"');
|
||||
addr = Some(ip.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addr
|
||||
}
|
||||
|
||||
/// GetSourceIPRaw retrieves the IP from the request headers
|
||||
/// and falls back to remote_addr when necessary.
|
||||
/// however returns without bracketing.
|
||||
pub fn get_source_ip_raw(headers: &HeaderMap, remote_addr: &str) -> String {
|
||||
let addr = get_source_ip_from_headers(headers).unwrap_or_else(|| remote_addr.to_string());
|
||||
|
||||
// Default to remote address if headers not set.
|
||||
if let Ok(socket_addr) = SocketAddr::from_str(&addr) {
|
||||
socket_addr.ip().to_string()
|
||||
} else {
|
||||
addr
|
||||
}
|
||||
}
|
||||
|
||||
/// GetSourceIP retrieves the IP from the request headers
|
||||
/// and falls back to remote_addr when necessary.
|
||||
pub fn get_source_ip(headers: &HeaderMap, remote_addr: &str) -> String {
|
||||
let addr = get_source_ip_raw(headers, remote_addr);
|
||||
if addr.contains(':') { format!("[{addr}]") } else { addr }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use http::HeaderValue;
|
||||
|
||||
fn create_test_headers() -> HeaderMap {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("x-forwarded-for", HeaderValue::from_static("192.168.1.1"));
|
||||
headers.insert("x-forwarded-proto", HeaderValue::from_static("https"));
|
||||
headers
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_source_scheme() {
|
||||
let headers = create_test_headers();
|
||||
assert_eq!(get_source_scheme(&headers), Some("https".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_source_ip_from_headers() {
|
||||
let headers = create_test_headers();
|
||||
assert_eq!(get_source_ip_from_headers(&headers), Some("192.168.1.1".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_source_ip_raw() {
|
||||
let headers = create_test_headers();
|
||||
let remote_addr = "127.0.0.1:8080";
|
||||
let result = get_source_ip_raw(&headers, remote_addr);
|
||||
assert_eq!(result, "192.168.1.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_source_ip() {
|
||||
let headers = create_test_headers();
|
||||
let remote_addr = "127.0.0.1:8080";
|
||||
let result = get_source_ip(&headers, remote_addr);
|
||||
assert_eq!(result, "192.168.1.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_source_ip_ipv6() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("x-forwarded-for", HeaderValue::from_static("2001:db8::1"));
|
||||
let remote_addr = "127.0.0.1:8080";
|
||||
let result = get_source_ip(&headers, remote_addr);
|
||||
assert_eq!(result, "[2001:db8::1]");
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,18 @@
|
||||
pub mod headers;
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
pub mod headers;
|
||||
pub mod ip;
|
||||
pub use headers::*;
|
||||
pub use ip::*;
|
||||
|
||||
+11
-11
@@ -148,17 +148,17 @@ pub fn is_local_host(host: Host<&str>, port: u16, local_port: u16) -> std::io::R
|
||||
pub async fn get_host_ip(host: Host<&str>) -> std::io::Result<HashSet<IpAddr>> {
|
||||
match host {
|
||||
Host::Domain(domain) => {
|
||||
// match crate::dns_resolver::resolve_domain(domain).await {
|
||||
// Ok(ips) => {
|
||||
// info!("Resolved domain {domain} using custom DNS resolver: {ips:?}");
|
||||
// return Ok(ips.into_iter().collect());
|
||||
// }
|
||||
// Err(err) => {
|
||||
// error!(
|
||||
// "Failed to resolve domain {domain} using custom DNS resolver, falling back to system resolver,err: {err}"
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
match crate::dns_resolver::resolve_domain(domain).await {
|
||||
Ok(ips) => {
|
||||
info!("Resolved domain {domain} using custom DNS resolver: {ips:?}");
|
||||
return Ok(ips.into_iter().collect());
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Failed to resolve domain {domain} using custom DNS resolver, falling back to system resolver,err: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
// Check cache first
|
||||
if CUSTOM_DNS_RESOLVER.read().unwrap().is_none() {
|
||||
if let Ok(mut cache) = DNS_CACHE.lock() {
|
||||
|
||||
@@ -186,7 +186,7 @@ impl std::fmt::Display for ParsedURL {
|
||||
s.pop();
|
||||
}
|
||||
|
||||
write!(f, "{}", s)
|
||||
write!(f, "{s}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+25
-4
@@ -2,14 +2,19 @@
|
||||
set -e
|
||||
|
||||
# 1) Normalize command:
|
||||
# - No arguments: default to execute rustfs
|
||||
# - No arguments: default to execute rustfs with DATA_VOLUMES
|
||||
# - First argument starts with '-': treat as rustfs arguments, auto-prefix rustfs
|
||||
# - First argument is 'rustfs': replace with absolute path to avoid PATH interference
|
||||
if [ $# -eq 0 ] || [ "${1#-}" != "$1" ]; then
|
||||
# - Otherwise: treat as full rustfs arguments (e.g., /data paths)
|
||||
if [ $# -eq 0 ]; then
|
||||
set -- /usr/bin/rustfs
|
||||
elif [ "${1#-}" != "$1" ]; then
|
||||
set -- /usr/bin/rustfs "$@"
|
||||
elif [ "$1" = "rustfs" ]; then
|
||||
shift
|
||||
set -- /usr/bin/rustfs "$@"
|
||||
else
|
||||
set -- /usr/bin/rustfs "$@"
|
||||
fi
|
||||
|
||||
# 2) Process data volumes (separate from log directory)
|
||||
@@ -74,6 +79,22 @@ if [ "${RUSTFS_ACCESS_KEY}" = "rustfsadmin" ] || [ "${RUSTFS_SECRET_KEY}" = "rus
|
||||
echo "!!!WARNING: Using default RUSTFS_ACCESS_KEY or RUSTFS_SECRET_KEY. Override them in production!"
|
||||
fi
|
||||
|
||||
echo "Starting: $*"
|
||||
set -- "$@" $DATA_VOLUMES
|
||||
# 5) Append DATA_VOLUMES only if no data paths in arguments
|
||||
# Check if any argument looks like a data path (starts with / and not an option)
|
||||
HAS_DATA_PATH=false
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
/usr/bin/rustfs) continue ;;
|
||||
-*) continue ;;
|
||||
/*) HAS_DATA_PATH=true; break ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$HAS_DATA_PATH" = "false" ] && [ -n "$DATA_VOLUMES" ]; then
|
||||
echo "Starting: $* $DATA_VOLUMES"
|
||||
set -- "$@" $DATA_VOLUMES
|
||||
else
|
||||
echo "Starting: $*"
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
|
||||
+2
-1
@@ -120,7 +120,8 @@ url = { workspace = true }
|
||||
urlencoding = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
|
||||
base64-simd.workspace = true
|
||||
hex-simd.workspace = true
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies]
|
||||
sysctl = { workspace = true }
|
||||
|
||||
@@ -42,7 +42,7 @@ async fn check_admin_request_auth(
|
||||
deny_only: bool,
|
||||
action: Action,
|
||||
) -> S3Result<()> {
|
||||
let conditions = get_condition_values(headers, cred);
|
||||
let conditions = get_condition_values(headers, cred, None, None);
|
||||
|
||||
if !iam_store
|
||||
.is_allowed(&Args {
|
||||
|
||||
+282
-158
@@ -14,43 +14,33 @@
|
||||
|
||||
use crate::config::build;
|
||||
use crate::license::get_license;
|
||||
use axum::Json;
|
||||
use axum::body::Body;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::{Router, extract::Request, middleware, routing::get};
|
||||
use axum_extra::extract::Host;
|
||||
use axum_server::tls_rustls::RustlsConfig;
|
||||
use http::{HeaderMap, HeaderName, StatusCode, Uri};
|
||||
use http::{HeaderValue, Method};
|
||||
use mime_guess::from_path;
|
||||
use rust_embed::RustEmbed;
|
||||
use rustfs_config::{RUSTFS_TLS_CERT, RUSTFS_TLS_KEY};
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use std::io::Result;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
// use axum::response::Redirect;
|
||||
// use axum::routing::get;
|
||||
// use axum::{
|
||||
// body::Body,
|
||||
// http::{Response, StatusCode},
|
||||
// response::IntoResponse,
|
||||
// Router,
|
||||
// };
|
||||
// use axum_extra::extract::Host;
|
||||
// use axum_server::tls_rustls::RustlsConfig;
|
||||
// use http::{header, HeaderMap, HeaderName, Uri};
|
||||
// use io::Error;
|
||||
// use mime_guess::from_path;
|
||||
// use rust_embed::RustEmbed;
|
||||
// use rustfs_config::{RUSTFS_TLS_CERT, RUSTFS_TLS_KEY};
|
||||
// use rustfs_utils::parse_and_resolve_address;
|
||||
// use serde::Serialize;
|
||||
// use std::io;
|
||||
// use std::net::{IpAddr, SocketAddr};
|
||||
// use std::sync::OnceLock;
|
||||
// use std::time::Duration;
|
||||
// use tokio::signal;
|
||||
// use tower_http::cors::{Any, CorsLayer};
|
||||
// use tower_http::trace::TraceLayer;
|
||||
use tracing::{error, instrument};
|
||||
|
||||
// shadow!(build);
|
||||
use std::time::Duration;
|
||||
use tokio_rustls::rustls::ServerConfig;
|
||||
use tower_http::catch_panic::CatchPanicLayer;
|
||||
use tower_http::cors::{AllowOrigin, Any, CorsLayer};
|
||||
use tower_http::limit::RequestBodyLimitLayer;
|
||||
use tower_http::timeout::TimeoutLayer;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
|
||||
pub(crate) const CONSOLE_PREFIX: &str = "/rustfs/console";
|
||||
const RUSTFS_ADMIN_PREFIX: &str = "/rustfs/admin/v3";
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
@@ -58,6 +48,17 @@ const RUSTFS_ADMIN_PREFIX: &str = "/rustfs/admin/v3";
|
||||
struct StaticFiles;
|
||||
|
||||
/// Static file handler
|
||||
///
|
||||
/// Serves static files embedded in the binary using rust-embed.
|
||||
/// If the requested file is not found, it serves index.html as a fallback.
|
||||
/// If index.html is also not found, it returns a 404 Not Found response.
|
||||
///
|
||||
/// Arguments:
|
||||
/// - `uri`: The request URI.
|
||||
///
|
||||
/// Returns:
|
||||
/// - An `impl IntoResponse` containing the static file content or a 404 response.
|
||||
///
|
||||
pub(crate) async fn static_handler(uri: Uri) -> impl IntoResponse {
|
||||
let mut path = uri.path().trim_start_matches('/');
|
||||
if path.is_empty() {
|
||||
@@ -80,7 +81,7 @@ pub(crate) async fn static_handler(uri: Uri) -> impl IntoResponse {
|
||||
} else {
|
||||
Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(Body::from("404 Not Found"))
|
||||
.body(Body::from(" 404 Not Found \n RustFS "))
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
@@ -223,8 +224,6 @@ fn _is_private_ip(ip: IpAddr) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::const_is_empty)]
|
||||
#[allow(dead_code)]
|
||||
#[instrument(fields(host))]
|
||||
pub async fn config_handler(uri: Uri, Host(host): Host, headers: HeaderMap) -> impl IntoResponse {
|
||||
// Get the scheme from the headers or use the URI scheme
|
||||
@@ -270,131 +269,256 @@ pub async fn config_handler(uri: Uri, Host(host): Host, headers: HeaderMap) -> i
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
// pub fn register_router() -> Router {
|
||||
// Router::new()
|
||||
// .route("/license", get(license_handler))
|
||||
// .route("/config.json", get(config_handler))
|
||||
// .fallback_service(get(static_handler))
|
||||
// }
|
||||
//
|
||||
// #[allow(dead_code)]
|
||||
// pub async fn start_static_file_server(addrs: &str, tls_path: Option<String>) {
|
||||
// // Configure CORS
|
||||
// let cors = CorsLayer::new()
|
||||
// .allow_origin(Any) // In the production environment, we recommend that you specify a specific domain name
|
||||
// .allow_methods([http::Method::GET, http::Method::POST])
|
||||
// .allow_headers([header::CONTENT_TYPE]);
|
||||
//
|
||||
// // Create a route
|
||||
// let app = register_router()
|
||||
// .layer(cors)
|
||||
// .layer(tower_http::compression::CompressionLayer::new().gzip(true).deflate(true))
|
||||
// .layer(TraceLayer::new_for_http());
|
||||
//
|
||||
// // Check and start the HTTPS/HTTP server
|
||||
// match start_server(addrs, tls_path, app).await {
|
||||
// Ok(_) => info!("Console Server shutdown gracefully"),
|
||||
// Err(e) => error!("Console Server error: {}", e),
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// async fn start_server(addrs: &str, tls_path: Option<String>, app: Router) -> io::Result<()> {
|
||||
// let server_addr = parse_and_resolve_address(addrs).expect("Console Failed to parse socket address");
|
||||
// let server_port = server_addr.port();
|
||||
// let server_address = server_addr.to_string();
|
||||
//
|
||||
// info!("Console WebUI: http://{} http://127.0.0.1:{} ", server_address, server_port);
|
||||
//
|
||||
// let tls_path = tls_path.unwrap_or_default();
|
||||
// let key_path = format!("{tls_path}/{RUSTFS_TLS_KEY}");
|
||||
// let cert_path = format!("{tls_path}/{RUSTFS_TLS_CERT}");
|
||||
// let handle = axum_server::Handle::new();
|
||||
// // create a signal off listening task
|
||||
// let handle_clone = handle.clone();
|
||||
// tokio::spawn(async move {
|
||||
// shutdown_signal().await;
|
||||
// info!("Console Initiating graceful shutdown...");
|
||||
// handle_clone.graceful_shutdown(Some(Duration::from_secs(10)));
|
||||
// });
|
||||
//
|
||||
// let has_tls_certs = tokio::try_join!(tokio::fs::metadata(&key_path), tokio::fs::metadata(&cert_path)).is_ok();
|
||||
// info!("Console TLS certs: {:?}", has_tls_certs);
|
||||
// if has_tls_certs {
|
||||
// info!("Console Found TLS certificates, starting with HTTPS");
|
||||
// match RustlsConfig::from_pem_file(cert_path, key_path).await {
|
||||
// Ok(config) => {
|
||||
// info!("Console Starting HTTPS server...");
|
||||
// axum_server::bind_rustls(server_addr, config)
|
||||
// .handle(handle.clone())
|
||||
// .serve(app.into_make_service())
|
||||
// .await
|
||||
// .map_err(Error::other)?;
|
||||
//
|
||||
// info!("Console HTTPS server running on https://{}", server_addr);
|
||||
//
|
||||
// Ok(())
|
||||
// }
|
||||
// Err(e) => {
|
||||
// error!("Console Failed to create TLS config: {}", e);
|
||||
// start_http_server(server_addr, app, handle).await
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// info!("Console TLS certificates not found at {} and {}", key_path, cert_path);
|
||||
// start_http_server(server_addr, app, handle).await
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// #[allow(dead_code)]
|
||||
// /// 308 redirect for HTTP to HTTPS
|
||||
// fn redirect_to_https(https_port: u16) -> Router {
|
||||
// Router::new().route(
|
||||
// "/*path",
|
||||
// get({
|
||||
// move |uri: Uri, req: http::Request<Body>| async move {
|
||||
// let host = req
|
||||
// .headers()
|
||||
// .get("host")
|
||||
// .map_or("localhost", |h| h.to_str().unwrap_or("localhost"));
|
||||
// let path = uri.path_and_query().map(|pq| pq.as_str()).unwrap_or("");
|
||||
// let https_url = format!("https://{host}:{https_port}{path}");
|
||||
// Redirect::permanent(&https_url)
|
||||
// }
|
||||
// }),
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// async fn start_http_server(addr: SocketAddr, app: Router, handle: axum_server::Handle) -> io::Result<()> {
|
||||
// info!("Console Starting HTTP server... {}", addr.to_string());
|
||||
// axum_server::bind(addr)
|
||||
// .handle(handle)
|
||||
// .serve(app.into_make_service())
|
||||
// .await
|
||||
// .map_err(Error::other)
|
||||
// }
|
||||
//
|
||||
// async fn shutdown_signal() {
|
||||
// let ctrl_c = async {
|
||||
// signal::ctrl_c().await.expect("Console failed to install Ctrl+C handler");
|
||||
// };
|
||||
//
|
||||
// #[cfg(unix)]
|
||||
// let terminate = async {
|
||||
// signal::unix::signal(signal::unix::SignalKind::terminate())
|
||||
// .expect("Console failed to install signal handler")
|
||||
// .recv()
|
||||
// .await;
|
||||
// };
|
||||
//
|
||||
// #[cfg(not(unix))]
|
||||
// let terminate = std::future::pending::<()>();
|
||||
//
|
||||
// tokio::select! {
|
||||
// _ = ctrl_c => {
|
||||
// info!("Console shutdown_signal ctrl_c")
|
||||
// },
|
||||
// _ = terminate => {
|
||||
// info!("Console shutdown_signal terminate")
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
/// Console access logging middleware
|
||||
async fn console_logging_middleware(req: Request, next: axum::middleware::Next) -> axum::response::Response {
|
||||
let method = req.method().clone();
|
||||
let uri = req.uri().clone();
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let response = next.run(req).await;
|
||||
let duration = start.elapsed();
|
||||
|
||||
info!(
|
||||
target: "rustfs::console::access",
|
||||
method = %method,
|
||||
uri = %uri,
|
||||
status = %response.status(),
|
||||
duration_ms = %duration.as_millis(),
|
||||
"Console access"
|
||||
);
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
/// Setup TLS configuration for console using axum-server, following endpoint TLS implementation logic
|
||||
#[instrument(skip(tls_path))]
|
||||
async fn _setup_console_tls_config(tls_path: Option<&String>) -> Result<Option<RustlsConfig>> {
|
||||
let tls_path = match tls_path {
|
||||
Some(path) if !path.is_empty() => path,
|
||||
_ => {
|
||||
debug!("TLS path is not provided, console starting with HTTP");
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
if tokio::fs::metadata(tls_path).await.is_err() {
|
||||
debug!("TLS path does not exist, console starting with HTTP");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
debug!("Found TLS directory for console, checking for certificates");
|
||||
|
||||
// Make sure to use a modern encryption suite
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
|
||||
// 1. Attempt to load all certificates in the directory (multi-certificate support, for SNI)
|
||||
if let Ok(cert_key_pairs) = rustfs_utils::load_all_certs_from_directory(tls_path) {
|
||||
if !cert_key_pairs.is_empty() {
|
||||
debug!(
|
||||
"Found {} certificates for console, creating SNI-aware multi-cert resolver",
|
||||
cert_key_pairs.len()
|
||||
);
|
||||
|
||||
// Create an SNI-enabled certificate resolver
|
||||
let resolver = rustfs_utils::create_multi_cert_resolver(cert_key_pairs)?;
|
||||
|
||||
// Configure the server to enable SNI support
|
||||
let mut server_config = ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_cert_resolver(Arc::new(resolver));
|
||||
|
||||
// Configure ALPN protocol priority
|
||||
server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec(), b"http/1.0".to_vec()];
|
||||
|
||||
// Log SNI requests
|
||||
if rustfs_utils::tls_key_log() {
|
||||
server_config.key_log = Arc::new(rustls::KeyLogFile::new());
|
||||
}
|
||||
|
||||
info!(target: "rustfs::console::tls", "Console TLS enabled with multi-certificate SNI support");
|
||||
return Ok(Some(RustlsConfig::from_config(Arc::new(server_config))));
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Revert to the traditional single-certificate mode
|
||||
let key_path = format!("{tls_path}/{RUSTFS_TLS_KEY}");
|
||||
let cert_path = format!("{tls_path}/{RUSTFS_TLS_CERT}");
|
||||
if tokio::try_join!(tokio::fs::metadata(&key_path), tokio::fs::metadata(&cert_path)).is_ok() {
|
||||
debug!("Found legacy single TLS certificate for console, starting with HTTPS");
|
||||
|
||||
return match RustlsConfig::from_pem_file(cert_path, key_path).await {
|
||||
Ok(config) => {
|
||||
info!(target: "rustfs::console::tls", "Console TLS enabled with single certificate");
|
||||
Ok(Some(config))
|
||||
}
|
||||
Err(e) => {
|
||||
error!(target: "rustfs::console::error", error = %e, "Failed to create TLS config for console");
|
||||
Err(std::io::Error::other(e))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
debug!("No valid TLS certificates found in the directory for console, starting with HTTP");
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Get console configuration from environment variables
|
||||
fn get_console_config_from_env() -> (bool, u32, u64, String) {
|
||||
let rate_limit_enable = std::env::var(rustfs_config::ENV_CONSOLE_RATE_LIMIT_ENABLE)
|
||||
.unwrap_or_else(|_| rustfs_config::DEFAULT_CONSOLE_RATE_LIMIT_ENABLE.to_string())
|
||||
.parse::<bool>()
|
||||
.unwrap_or(rustfs_config::DEFAULT_CONSOLE_RATE_LIMIT_ENABLE);
|
||||
|
||||
let rate_limit_rpm = std::env::var(rustfs_config::ENV_CONSOLE_RATE_LIMIT_RPM)
|
||||
.unwrap_or_else(|_| rustfs_config::DEFAULT_CONSOLE_RATE_LIMIT_RPM.to_string())
|
||||
.parse::<u32>()
|
||||
.unwrap_or(rustfs_config::DEFAULT_CONSOLE_RATE_LIMIT_RPM);
|
||||
|
||||
let auth_timeout = std::env::var(rustfs_config::ENV_CONSOLE_AUTH_TIMEOUT)
|
||||
.unwrap_or_else(|_| rustfs_config::DEFAULT_CONSOLE_AUTH_TIMEOUT.to_string())
|
||||
.parse::<u64>()
|
||||
.unwrap_or(rustfs_config::DEFAULT_CONSOLE_AUTH_TIMEOUT);
|
||||
let cors_allowed_origins = std::env::var(rustfs_config::ENV_CONSOLE_CORS_ALLOWED_ORIGINS)
|
||||
.unwrap_or_else(|_| rustfs_config::DEFAULT_CONSOLE_CORS_ALLOWED_ORIGINS.to_string())
|
||||
.parse::<String>()
|
||||
.unwrap_or(rustfs_config::DEFAULT_CONSOLE_CORS_ALLOWED_ORIGINS.to_string());
|
||||
|
||||
(rate_limit_enable, rate_limit_rpm, auth_timeout, cors_allowed_origins)
|
||||
}
|
||||
|
||||
pub fn is_console_path(path: &str) -> bool {
|
||||
path.starts_with(CONSOLE_PREFIX)
|
||||
}
|
||||
|
||||
/// Setup comprehensive middleware stack with tower-http features
|
||||
fn setup_console_middleware_stack(
|
||||
cors_layer: CorsLayer,
|
||||
rate_limit_enable: bool,
|
||||
rate_limit_rpm: u32,
|
||||
auth_timeout: u64,
|
||||
) -> Router {
|
||||
let mut app = Router::new()
|
||||
.route(&format!("{CONSOLE_PREFIX}/license"), get(license_handler))
|
||||
.route(&format!("{CONSOLE_PREFIX}/config.json"), get(config_handler))
|
||||
.route(&format!("{CONSOLE_PREFIX}/health"), get(health_check))
|
||||
.nest(CONSOLE_PREFIX, Router::new().fallback_service(get(static_handler)))
|
||||
.fallback_service(get(static_handler));
|
||||
|
||||
// Add comprehensive middleware layers using tower-http features
|
||||
app = app
|
||||
.layer(CatchPanicLayer::new())
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.layer(middleware::from_fn(console_logging_middleware))
|
||||
.layer(cors_layer)
|
||||
// Add timeout layer - convert auth_timeout from seconds to Duration
|
||||
.layer(TimeoutLayer::new(Duration::from_secs(auth_timeout)))
|
||||
// Add request body limit (10MB for console uploads)
|
||||
.layer(RequestBodyLimitLayer::new(5 * 1024 * 1024 * 1024));
|
||||
|
||||
// Add rate limiting if enabled
|
||||
if rate_limit_enable {
|
||||
info!("Console rate limiting enabled: {} requests per minute", rate_limit_rpm);
|
||||
// Note: tower-http doesn't provide a built-in rate limiter, but we have the foundation
|
||||
// For production, you would integrate with a rate limiting service like Redis
|
||||
// For now, we log that it's configured and ready for integration
|
||||
}
|
||||
|
||||
app
|
||||
}
|
||||
|
||||
/// Console health check handler with comprehensive health information
|
||||
async fn health_check() -> Json<serde_json::Value> {
|
||||
use rustfs_ecstore::new_object_layer_fn;
|
||||
|
||||
let mut health_status = "ok";
|
||||
let mut details = json!({});
|
||||
|
||||
// Check storage backend health
|
||||
if let Some(_store) = new_object_layer_fn() {
|
||||
details["storage"] = json!({"status": "connected"});
|
||||
} else {
|
||||
health_status = "degraded";
|
||||
details["storage"] = json!({"status": "disconnected"});
|
||||
}
|
||||
|
||||
// Check IAM system health
|
||||
match rustfs_iam::get() {
|
||||
Ok(_) => {
|
||||
details["iam"] = json!({"status": "connected"});
|
||||
}
|
||||
Err(_) => {
|
||||
health_status = "degraded";
|
||||
details["iam"] = json!({"status": "disconnected"});
|
||||
}
|
||||
}
|
||||
|
||||
Json(json!({
|
||||
"status": health_status,
|
||||
"service": "rustfs-console",
|
||||
"timestamp": chrono::Utc::now().to_rfc3339(),
|
||||
"version": env!("CARGO_PKG_VERSION"),
|
||||
"details": details,
|
||||
"uptime": std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
}))
|
||||
}
|
||||
|
||||
/// Parse CORS allowed origins from configuration
|
||||
pub fn parse_cors_origins(origins: Option<&String>) -> CorsLayer {
|
||||
let cors_layer = CorsLayer::new()
|
||||
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::OPTIONS])
|
||||
.allow_headers(Any);
|
||||
|
||||
match origins {
|
||||
Some(origins_str) if origins_str == "*" => cors_layer.allow_origin(Any).expose_headers(Any),
|
||||
Some(origins_str) => {
|
||||
let origins: Vec<&str> = origins_str.split(',').map(|s| s.trim()).collect();
|
||||
if origins.is_empty() {
|
||||
warn!("Empty CORS origins provided, using permissive CORS");
|
||||
cors_layer.allow_origin(Any).expose_headers(Any)
|
||||
} else {
|
||||
// Parse origins with proper error handling
|
||||
let mut valid_origins = Vec::new();
|
||||
for origin in origins {
|
||||
match origin.parse::<HeaderValue>() {
|
||||
Ok(header_value) => {
|
||||
valid_origins.push(header_value);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Invalid CORS origin '{}': {}", origin, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if valid_origins.is_empty() {
|
||||
warn!("No valid CORS origins found, using permissive CORS");
|
||||
cors_layer.allow_origin(Any).expose_headers(Any)
|
||||
} else {
|
||||
info!("Console CORS origins configured: {:?}", valid_origins);
|
||||
cors_layer.allow_origin(AllowOrigin::list(valid_origins)).expose_headers(Any)
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
debug!("No CORS origins configured for console, using permissive CORS");
|
||||
cors_layer.allow_origin(Any)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn make_console_server() -> Router {
|
||||
let (rate_limit_enable, rate_limit_rpm, auth_timeout, cors_allowed_origins) = get_console_config_from_env();
|
||||
// String to Option<&String>
|
||||
let cors_allowed_origins = if cors_allowed_origins.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(&cors_allowed_origins)
|
||||
};
|
||||
// Configure CORS based on settings
|
||||
let cors_layer = parse_cors_origins(cors_allowed_origins);
|
||||
|
||||
// Build console router with enhanced middleware stack using tower-http features
|
||||
setup_console_middleware_stack(cors_layer, rate_limit_enable, rate_limit_rpm, auth_timeout)
|
||||
}
|
||||
|
||||
@@ -15,42 +15,12 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::config::Opt;
|
||||
use crate::server::start_console_server;
|
||||
use clap::Parser;
|
||||
use tokio::time::{Duration, timeout};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_console_server_can_start_and_stop() {
|
||||
// Test that console server can be started and shut down gracefully
|
||||
let args = vec!["rustfs", "/tmp/test", "--console-address", ":0"]; // Use port 0 for auto-assignment
|
||||
let opt = Opt::parse_from(args);
|
||||
|
||||
let (tx, rx) = tokio::sync::broadcast::channel(1);
|
||||
|
||||
// Start console server in a background task
|
||||
let handle = tokio::spawn(async move { start_console_server(&opt, rx).await });
|
||||
|
||||
// Give it a moment to start
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
// Send shutdown signal
|
||||
let _ = tx.send(());
|
||||
|
||||
// Wait for server to shut down
|
||||
let result = timeout(Duration::from_secs(5), handle).await;
|
||||
|
||||
assert!(result.is_ok(), "Console server should shutdown gracefully");
|
||||
let server_result = result.unwrap();
|
||||
assert!(server_result.is_ok(), "Console server should not have errors");
|
||||
let final_result = server_result.unwrap();
|
||||
assert!(final_result.is_ok(), "Console server should complete successfully");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_console_cors_configuration() {
|
||||
// Test CORS configuration parsing
|
||||
use crate::server::console::parse_cors_origins;
|
||||
|
||||
use crate::admin::console::parse_cors_origins;
|
||||
// Test wildcard origin
|
||||
let cors_wildcard = Some("*".to_string());
|
||||
let _layer1 = parse_cors_origins(cors_wildcard.as_ref());
|
||||
@@ -71,23 +41,6 @@ mod tests {
|
||||
// Should create a layer without error (uses default)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_external_address_configuration() {
|
||||
// Test external address configuration
|
||||
let args = vec![
|
||||
"rustfs",
|
||||
"/tmp/test",
|
||||
"--console-address",
|
||||
":9001",
|
||||
"--external-address",
|
||||
":9020",
|
||||
];
|
||||
let opt = Opt::parse_from(args);
|
||||
|
||||
assert_eq!(opt.console_address, ":9001");
|
||||
assert_eq!(opt.external_address, ":9020".to_string());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_console_tls_configuration() {
|
||||
// Test TLS configuration options (now uses shared tls_path)
|
||||
@@ -133,14 +86,11 @@ mod tests {
|
||||
"true",
|
||||
"--console-address",
|
||||
":9001",
|
||||
"--external-address",
|
||||
":9020",
|
||||
];
|
||||
let opt = Opt::parse_from(args);
|
||||
|
||||
// Verify all console-related configuration is parsed correctly
|
||||
assert!(opt.console_enable);
|
||||
assert_eq!(opt.console_address, ":9001");
|
||||
assert_eq!(opt.external_address, ":9020".to_string());
|
||||
}
|
||||
}
|
||||
@@ -144,7 +144,7 @@ impl Operation for AccountInfoHandler {
|
||||
let claims = cred.claims.as_ref().unwrap_or(&default_claims);
|
||||
|
||||
let cred_clone = cred.clone();
|
||||
let conditions = get_condition_values(&req.headers, &cred_clone);
|
||||
let conditions = get_condition_values(&req.headers, &cred_clone, None, None);
|
||||
let cred_clone = Arc::new(cred_clone);
|
||||
let conditions = Arc::new(conditions);
|
||||
|
||||
@@ -323,7 +323,7 @@ impl Operation for ServiceHandle {
|
||||
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
warn!("handle ServiceHandle");
|
||||
|
||||
return Err(s3_error!(NotImplemented));
|
||||
Err(s3_error!(NotImplemented))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,7 +367,7 @@ impl Operation for InspectDataHandler {
|
||||
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
warn!("handle InspectDataHandler");
|
||||
|
||||
return Err(s3_error!(NotImplemented));
|
||||
Err(s3_error!(NotImplemented))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,7 +431,10 @@ impl Operation for DataUsageInfoHandler {
|
||||
&cred,
|
||||
owner,
|
||||
false,
|
||||
vec![Action::AdminAction(AdminAction::DataUsageInfoAdminAction)],
|
||||
vec![
|
||||
Action::AdminAction(AdminAction::DataUsageInfoAdminAction),
|
||||
Action::S3Action(S3Action::ListBucketAction),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -486,7 +489,7 @@ impl Operation for DataUsageInfoHandler {
|
||||
|
||||
let mut info_for_refresh = info.clone();
|
||||
let store_for_refresh = store.clone();
|
||||
tokio::spawn(async move {
|
||||
spawn(async move {
|
||||
if let Err(e) = collect_realtime_data_usage(&mut info_for_refresh, store_for_refresh.clone()).await {
|
||||
warn!("Background data usage refresh failed: {}", e);
|
||||
return;
|
||||
@@ -638,10 +641,7 @@ impl Operation for MetricsHandler {
|
||||
let mp = extract_metrics_init_params(&req.uri);
|
||||
info!("mp: {:?}", mp);
|
||||
|
||||
let tick = match parse_duration(&mp.tick) {
|
||||
Ok(i) => i,
|
||||
Err(_) => std_Duration::from_secs(1),
|
||||
};
|
||||
let tick = parse_duration(&mp.tick).unwrap_or_else(|_| std_Duration::from_secs(3));
|
||||
|
||||
let mut n = mp.n;
|
||||
if n == 0 {
|
||||
@@ -654,28 +654,18 @@ impl Operation for MetricsHandler {
|
||||
MetricType::ALL
|
||||
};
|
||||
|
||||
let disks = mp.disks.split(",").map(String::from).collect::<Vec<String>>();
|
||||
let by_disk = mp.by_disk == "true";
|
||||
let mut disk_map = HashSet::new();
|
||||
if !disks.is_empty() && !disks[0].is_empty() {
|
||||
for d in disks.iter() {
|
||||
if !d.is_empty() {
|
||||
disk_map.insert(d.to_string());
|
||||
}
|
||||
}
|
||||
fn parse_comma_separated(s: &str) -> HashSet<String> {
|
||||
s.split(',').filter(|part| !part.is_empty()).map(String::from).collect()
|
||||
}
|
||||
|
||||
let disks = parse_comma_separated(&mp.disks);
|
||||
let by_disk = mp.by_disk == "true";
|
||||
let disk_map = disks;
|
||||
|
||||
let job_id = mp.by_job_id;
|
||||
let hosts = mp.hosts.split(",").map(String::from).collect::<Vec<String>>();
|
||||
let hosts = parse_comma_separated(&mp.hosts);
|
||||
let by_host = mp.by_host == "true";
|
||||
let mut host_map = HashSet::new();
|
||||
if !hosts.is_empty() && !hosts[0].is_empty() {
|
||||
for d in hosts.iter() {
|
||||
if !d.is_empty() {
|
||||
host_map.insert(d.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
let host_map = hosts;
|
||||
|
||||
let d_id = mp.by_dep_id;
|
||||
let mut interval = interval(tick);
|
||||
@@ -691,7 +681,7 @@ impl Operation for MetricsHandler {
|
||||
inner: ReceiverStream::new(rx),
|
||||
});
|
||||
let body = Body::from(in_stream);
|
||||
tokio::spawn(async move {
|
||||
spawn(async move {
|
||||
while n > 0 {
|
||||
info!("loop, n: {n}");
|
||||
let mut m = RealtimeMetrics::default();
|
||||
@@ -929,7 +919,7 @@ impl Operation for BackgroundHealStatusHandler {
|
||||
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
warn!("handle BackgroundHealStatusHandler");
|
||||
|
||||
return Err(s3_error!(NotImplemented));
|
||||
Err(s3_error!(NotImplemented))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -964,7 +954,7 @@ impl Operation for GetReplicationMetricsHandler {
|
||||
}
|
||||
//return Err(s3_error!(InvalidArgument, "Invalid bucket name"));
|
||||
//Ok(S3Response::with_headers((StatusCode::OK, Body::from()), header))
|
||||
return Ok(S3Response::new((StatusCode::OK, Body::from("Ok".to_string()))));
|
||||
Ok(S3Response::new((StatusCode::OK, Body::from("Ok".to_string()))))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -991,7 +981,7 @@ impl Operation for SetRemoteTargetHandler {
|
||||
};
|
||||
|
||||
store
|
||||
.get_bucket_info(bucket, &rustfs_ecstore::store_api::BucketOptions::default())
|
||||
.get_bucket_info(bucket, &BucketOptions::default())
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
@@ -1005,7 +995,7 @@ impl Operation for SetRemoteTargetHandler {
|
||||
};
|
||||
|
||||
let mut remote_target: BucketTarget = serde_json::from_slice(&body).map_err(|e| {
|
||||
tracing::error!("Failed to parse BucketTarget from body: {}", e);
|
||||
error!("Failed to parse BucketTarget from body: {}", e);
|
||||
ApiError::other(e)
|
||||
})?;
|
||||
|
||||
@@ -1117,10 +1107,7 @@ impl Operation for ListRemoteTargetHandler {
|
||||
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not initialized".to_string()));
|
||||
};
|
||||
|
||||
if let Err(err) = store
|
||||
.get_bucket_info(bucket, &rustfs_ecstore::store_api::BucketOptions::default())
|
||||
.await
|
||||
{
|
||||
if let Err(err) = store.get_bucket_info(bucket, &BucketOptions::default()).await {
|
||||
error!("Error fetching bucket info: {:?}", err);
|
||||
return Ok(S3Response::new((StatusCode::BAD_REQUEST, Body::from("Invalid bucket".to_string()))));
|
||||
}
|
||||
@@ -1149,7 +1136,7 @@ impl Operation for ListRemoteTargetHandler {
|
||||
let mut header = HeaderMap::new();
|
||||
header.insert(CONTENT_TYPE, "application/json".parse().unwrap());
|
||||
|
||||
return Ok(S3Response::with_headers((StatusCode::OK, Body::from(json_targets)), header));
|
||||
Ok(S3Response::with_headers((StatusCode::OK, Body::from(json_targets)), header))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1174,10 +1161,7 @@ impl Operation for RemoveRemoteTargetHandler {
|
||||
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not initialized".to_string()));
|
||||
};
|
||||
|
||||
if let Err(err) = store
|
||||
.get_bucket_info(bucket, &rustfs_ecstore::store_api::BucketOptions::default())
|
||||
.await
|
||||
{
|
||||
if let Err(err) = store.get_bucket_info(bucket, &BucketOptions::default()).await {
|
||||
error!("Error fetching bucket info: {:?}", err);
|
||||
return Ok(S3Response::new((StatusCode::BAD_REQUEST, Body::from("Invalid bucket".to_string()))));
|
||||
}
|
||||
@@ -1206,7 +1190,7 @@ impl Operation for RemoveRemoteTargetHandler {
|
||||
S3Error::with_message(S3ErrorCode::InternalError, format!("Failed to update bucket targets: {e}"))
|
||||
})?;
|
||||
|
||||
return Ok(S3Response::new((StatusCode::NO_CONTENT, Body::from("".to_string()))));
|
||||
Ok(S3Response::new((StatusCode::NO_CONTENT, Body::from("".to_string()))))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1216,9 +1200,7 @@ async fn collect_realtime_data_usage(
|
||||
store: Arc<rustfs_ecstore::store::ECStore>,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Get bucket list and collect basic statistics
|
||||
let buckets = store
|
||||
.list_bucket(&rustfs_ecstore::store_api::BucketOptions::default())
|
||||
.await?;
|
||||
let buckets = store.list_bucket(&BucketOptions::default()).await?;
|
||||
|
||||
info.buckets_count = buckets.len() as u64;
|
||||
info.last_update = Some(std::time::SystemTime::now());
|
||||
|
||||
@@ -137,16 +137,15 @@ impl Operation for ExportBucketMetadata {
|
||||
let conf_path = path_join_buf(&[bucket.name.as_str(), conf]);
|
||||
match conf {
|
||||
BUCKET_POLICY_CONFIG => {
|
||||
let config: rustfs_policy::policy::BucketPolicy =
|
||||
match metadata_sys::get_bucket_policy(&bucket.name).await {
|
||||
Ok((res, _)) => res,
|
||||
Err(e) => {
|
||||
if e == StorageError::ConfigNotFound {
|
||||
continue;
|
||||
}
|
||||
return Err(s3_error!(InternalError, "get bucket metadata failed: {e}"));
|
||||
let config: BucketPolicy = match metadata_sys::get_bucket_policy(&bucket.name).await {
|
||||
Ok((res, _)) => res,
|
||||
Err(e) => {
|
||||
if e == StorageError::ConfigNotFound {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
return Err(s3_error!(InternalError, "get bucket metadata failed: {e}"));
|
||||
}
|
||||
};
|
||||
let config_json =
|
||||
serde_json::to_vec(&config).map_err(|e| s3_error!(InternalError, "serialize config failed: {e}"))?;
|
||||
zip_writer
|
||||
|
||||
@@ -98,17 +98,11 @@ async fn validate_queue_dir(queue_dir: &str) -> S3Result<()> {
|
||||
}
|
||||
|
||||
if let Err(e) = retry_metadata(queue_dir).await {
|
||||
match e.kind() {
|
||||
ErrorKind::NotFound => {
|
||||
return Err(s3_error!(InvalidArgument, "queue_dir does not exist"));
|
||||
}
|
||||
ErrorKind::PermissionDenied => {
|
||||
return Err(s3_error!(InvalidArgument, "queue_dir exists but permission denied"));
|
||||
}
|
||||
_ => {
|
||||
return Err(s3_error!(InvalidArgument, "failed to access queue_dir: {}", e));
|
||||
}
|
||||
}
|
||||
return match e.kind() {
|
||||
ErrorKind::NotFound => Err(s3_error!(InvalidArgument, "queue_dir does not exist")),
|
||||
ErrorKind::PermissionDenied => Err(s3_error!(InvalidArgument, "queue_dir exists but permission denied")),
|
||||
_ => Err(s3_error!(InvalidArgument, "failed to access queue_dir: {}", e)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ impl Operation for CreateKeyHandler {
|
||||
let tags = request.tags.unwrap_or_default();
|
||||
let key_name = tags.get("name").cloned();
|
||||
|
||||
let kms_request = rustfs_kms::types::CreateKeyRequest {
|
||||
let kms_request = CreateKeyRequest {
|
||||
key_name,
|
||||
key_usage: request.key_usage.unwrap_or(KeyUsage::EncryptDecrypt),
|
||||
description: request.description,
|
||||
@@ -216,7 +216,7 @@ impl Operation for DescribeKeyHandler {
|
||||
return Err(s3_error!(InternalError, "KMS service not initialized"));
|
||||
};
|
||||
|
||||
let request = rustfs_kms::types::DescribeKeyRequest { key_id: key_id.clone() };
|
||||
let request = DescribeKeyRequest { key_id: key_id.clone() };
|
||||
|
||||
match service.describe_key(request).await {
|
||||
Ok(response) => {
|
||||
@@ -270,7 +270,7 @@ impl Operation for ListKeysHandler {
|
||||
return Err(s3_error!(InternalError, "KMS service not initialized"));
|
||||
};
|
||||
|
||||
let request = rustfs_kms::types::ListKeysRequest {
|
||||
let request = ListKeysRequest {
|
||||
limit: Some(limit),
|
||||
marker,
|
||||
status_filter: None,
|
||||
@@ -336,7 +336,7 @@ impl Operation for GenerateDataKeyHandler {
|
||||
return Err(s3_error!(InternalError, "KMS service not initialized"));
|
||||
};
|
||||
|
||||
let kms_request = rustfs_kms::GenerateDataKeyRequest {
|
||||
let kms_request = GenerateDataKeyRequest {
|
||||
key_id: request.key_id,
|
||||
key_spec: request.key_spec,
|
||||
encryption_context: request.encryption_context.unwrap_or_default(),
|
||||
|
||||
@@ -65,21 +65,18 @@ impl Operation for ConfigureKmsHandler {
|
||||
Ok(req) => req,
|
||||
Err(e) => {
|
||||
error!("Invalid JSON in configure request: {}", e);
|
||||
return Ok(S3Response::new((StatusCode::BAD_REQUEST, Body::from(format!("Invalid JSON: {}", e)))));
|
||||
return Ok(S3Response::new((StatusCode::BAD_REQUEST, Body::from(format!("Invalid JSON: {e}")))));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
info!("Configuring KMS with request: {:?}", configure_request);
|
||||
|
||||
let service_manager = match get_global_kms_service_manager() {
|
||||
Some(manager) => manager,
|
||||
None => {
|
||||
warn!("KMS service manager not initialized, initializing now as fallback");
|
||||
// Initialize the service manager as a fallback
|
||||
rustfs_kms::init_global_kms_service_manager()
|
||||
}
|
||||
};
|
||||
let service_manager = get_global_kms_service_manager().unwrap_or_else(|| {
|
||||
warn!("KMS service manager not initialized, initializing now as fallback");
|
||||
// Initialize the service manager as a fallback
|
||||
rustfs_kms::init_global_kms_service_manager()
|
||||
});
|
||||
|
||||
// Convert request to KmsConfig
|
||||
let kms_config = configure_request.to_kms_config();
|
||||
@@ -92,7 +89,7 @@ impl Operation for ConfigureKmsHandler {
|
||||
(true, "KMS configured successfully".to_string(), status)
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = format!("Failed to configure KMS: {}", e);
|
||||
let error_msg = format!("Failed to configure KMS: {e}");
|
||||
error!("{}", error_msg);
|
||||
let status = service_manager.get_status().await;
|
||||
(false, error_msg, status)
|
||||
@@ -155,21 +152,18 @@ impl Operation for StartKmsHandler {
|
||||
Ok(req) => req,
|
||||
Err(e) => {
|
||||
error!("Invalid JSON in start request: {}", e);
|
||||
return Ok(S3Response::new((StatusCode::BAD_REQUEST, Body::from(format!("Invalid JSON: {}", e)))));
|
||||
return Ok(S3Response::new((StatusCode::BAD_REQUEST, Body::from(format!("Invalid JSON: {e}")))));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
info!("Starting KMS service with force: {:?}", start_request.force);
|
||||
|
||||
let service_manager = match get_global_kms_service_manager() {
|
||||
Some(manager) => manager,
|
||||
None => {
|
||||
warn!("KMS service manager not initialized, initializing now as fallback");
|
||||
// Initialize the service manager as a fallback
|
||||
rustfs_kms::init_global_kms_service_manager()
|
||||
}
|
||||
};
|
||||
let service_manager = get_global_kms_service_manager().unwrap_or_else(|| {
|
||||
warn!("KMS service manager not initialized, initializing now as fallback");
|
||||
// Initialize the service manager as a fallback
|
||||
rustfs_kms::init_global_kms_service_manager()
|
||||
});
|
||||
|
||||
// Check if already running and force flag
|
||||
let current_status = service_manager.get_status().await;
|
||||
@@ -205,14 +199,14 @@ impl Operation for StartKmsHandler {
|
||||
(true, "KMS service restarted successfully".to_string(), status)
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = format!("Failed to restart KMS service: {}", e);
|
||||
let error_msg = format!("Failed to restart KMS service: {e}");
|
||||
error!("{}", error_msg);
|
||||
let status = service_manager.get_status().await;
|
||||
(false, error_msg, status)
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
let error_msg = format!("Failed to stop KMS service for restart: {}", e);
|
||||
let error_msg = format!("Failed to stop KMS service for restart: {e}");
|
||||
error!("{}", error_msg);
|
||||
let status = service_manager.get_status().await;
|
||||
(false, error_msg, status)
|
||||
@@ -227,7 +221,7 @@ impl Operation for StartKmsHandler {
|
||||
(true, "KMS service started successfully".to_string(), status)
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = format!("Failed to start KMS service: {}", e);
|
||||
let error_msg = format!("Failed to start KMS service: {e}");
|
||||
error!("{}", error_msg);
|
||||
let status = service_manager.get_status().await;
|
||||
(false, error_msg, status)
|
||||
@@ -280,14 +274,11 @@ impl Operation for StopKmsHandler {
|
||||
|
||||
info!("Stopping KMS service");
|
||||
|
||||
let service_manager = match get_global_kms_service_manager() {
|
||||
Some(manager) => manager,
|
||||
None => {
|
||||
warn!("KMS service manager not initialized, initializing now as fallback");
|
||||
// Initialize the service manager as a fallback
|
||||
rustfs_kms::init_global_kms_service_manager()
|
||||
}
|
||||
};
|
||||
let service_manager = get_global_kms_service_manager().unwrap_or_else(|| {
|
||||
warn!("KMS service manager not initialized, initializing now as fallback");
|
||||
// Initialize the service manager as a fallback
|
||||
rustfs_kms::init_global_kms_service_manager()
|
||||
});
|
||||
|
||||
let (success, message, status) = match service_manager.stop().await {
|
||||
Ok(()) => {
|
||||
@@ -296,7 +287,7 @@ impl Operation for StopKmsHandler {
|
||||
(true, "KMS service stopped successfully".to_string(), status)
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = format!("Failed to stop KMS service: {}", e);
|
||||
let error_msg = format!("Failed to stop KMS service: {e}");
|
||||
error!("{}", error_msg);
|
||||
let status = service_manager.get_status().await;
|
||||
(false, error_msg, status)
|
||||
@@ -348,14 +339,11 @@ impl Operation for GetKmsStatusHandler {
|
||||
|
||||
info!("Getting KMS service status");
|
||||
|
||||
let service_manager = match get_global_kms_service_manager() {
|
||||
Some(manager) => manager,
|
||||
None => {
|
||||
warn!("KMS service manager not initialized, initializing now as fallback");
|
||||
// Initialize the service manager as a fallback
|
||||
rustfs_kms::init_global_kms_service_manager()
|
||||
}
|
||||
};
|
||||
let service_manager = get_global_kms_service_manager().unwrap_or_else(|| {
|
||||
warn!("KMS service manager not initialized, initializing now as fallback");
|
||||
// Initialize the service manager as a fallback
|
||||
rustfs_kms::init_global_kms_service_manager()
|
||||
});
|
||||
|
||||
let status = service_manager.get_status().await;
|
||||
let config = service_manager.get_config().await;
|
||||
@@ -436,21 +424,18 @@ impl Operation for ReconfigureKmsHandler {
|
||||
Ok(req) => req,
|
||||
Err(e) => {
|
||||
error!("Invalid JSON in reconfigure request: {}", e);
|
||||
return Ok(S3Response::new((StatusCode::BAD_REQUEST, Body::from(format!("Invalid JSON: {}", e)))));
|
||||
return Ok(S3Response::new((StatusCode::BAD_REQUEST, Body::from(format!("Invalid JSON: {e}")))));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
info!("Reconfiguring KMS with request: {:?}", configure_request);
|
||||
|
||||
let service_manager = match get_global_kms_service_manager() {
|
||||
Some(manager) => manager,
|
||||
None => {
|
||||
warn!("KMS service manager not initialized, initializing now as fallback");
|
||||
// Initialize the service manager as a fallback
|
||||
rustfs_kms::init_global_kms_service_manager()
|
||||
}
|
||||
};
|
||||
let service_manager = get_global_kms_service_manager().unwrap_or_else(|| {
|
||||
warn!("KMS service manager not initialized, initializing now as fallback");
|
||||
// Initialize the service manager as a fallback
|
||||
rustfs_kms::init_global_kms_service_manager()
|
||||
});
|
||||
|
||||
// Convert request to KmsConfig
|
||||
let kms_config = configure_request.to_kms_config();
|
||||
@@ -463,7 +448,7 @@ impl Operation for ReconfigureKmsHandler {
|
||||
(true, "KMS reconfigured and restarted successfully".to_string(), status)
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = format!("Failed to reconfigure KMS: {}", e);
|
||||
let error_msg = format!("Failed to reconfigure KMS: {e}");
|
||||
error!("{}", error_msg);
|
||||
let status = service_manager.get_status().await;
|
||||
(false, error_msg, status)
|
||||
|
||||
@@ -160,7 +160,7 @@ impl Operation for CreateKmsKeyHandler {
|
||||
error!("Failed to create KMS key: {}", e);
|
||||
let response = CreateKmsKeyResponse {
|
||||
success: false,
|
||||
message: format!("Failed to create key: {}", e),
|
||||
message: format!("Failed to create key: {e}"),
|
||||
key_id: "".to_string(),
|
||||
key_metadata: None,
|
||||
};
|
||||
@@ -310,7 +310,7 @@ impl Operation for DeleteKmsKeyHandler {
|
||||
};
|
||||
let response = DeleteKmsKeyResponse {
|
||||
success: false,
|
||||
message: format!("Failed to delete key: {}", e),
|
||||
message: format!("Failed to delete key: {e}"),
|
||||
key_id: request.key_id,
|
||||
deletion_date: None,
|
||||
};
|
||||
@@ -442,7 +442,7 @@ impl Operation for CancelKmsKeyDeletionHandler {
|
||||
error!("Failed to cancel deletion for KMS key {}: {}", request.key_id, e);
|
||||
let response = CancelKmsKeyDeletionResponse {
|
||||
success: false,
|
||||
message: format!("Failed to cancel key deletion: {}", e),
|
||||
message: format!("Failed to cancel key deletion: {e}"),
|
||||
key_id: request.key_id,
|
||||
key_metadata: None,
|
||||
};
|
||||
@@ -554,7 +554,7 @@ impl Operation for ListKmsKeysHandler {
|
||||
error!("Failed to list KMS keys: {}", e);
|
||||
let response = ListKmsKeysResponse {
|
||||
success: false,
|
||||
message: format!("Failed to list keys: {}", e),
|
||||
message: format!("Failed to list keys: {e}"),
|
||||
keys: vec![],
|
||||
truncated: false,
|
||||
next_marker: None,
|
||||
@@ -671,7 +671,7 @@ impl Operation for DescribeKmsKeyHandler {
|
||||
|
||||
let response = DescribeKmsKeyResponse {
|
||||
success: false,
|
||||
message: format!("Failed to describe key: {}", e),
|
||||
message: format!("Failed to describe key: {e}"),
|
||||
key_metadata: None,
|
||||
};
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ impl Operation for AddServiceAccount {
|
||||
groups: &cred.groups,
|
||||
action: Action::AdminAction(AdminAction::CreateServiceAccountAdminAction),
|
||||
bucket: "",
|
||||
conditions: &get_condition_values(&req.headers, &cred),
|
||||
conditions: &get_condition_values(&req.headers, &cred, None, None),
|
||||
is_owner: owner,
|
||||
object: "",
|
||||
claims: cred.claims.as_ref().unwrap_or(&HashMap::new()),
|
||||
@@ -263,7 +263,7 @@ impl Operation for UpdateServiceAccount {
|
||||
groups: &cred.groups,
|
||||
action: Action::AdminAction(AdminAction::UpdateServiceAccountAdminAction),
|
||||
bucket: "",
|
||||
conditions: &get_condition_values(&req.headers, &cred),
|
||||
conditions: &get_condition_values(&req.headers, &cred, None, None),
|
||||
is_owner: owner,
|
||||
object: "",
|
||||
claims: cred.claims.as_ref().unwrap_or(&HashMap::new()),
|
||||
@@ -356,7 +356,7 @@ impl Operation for InfoServiceAccount {
|
||||
groups: &cred.groups,
|
||||
action: Action::AdminAction(AdminAction::ListServiceAccountsAdminAction),
|
||||
bucket: "",
|
||||
conditions: &get_condition_values(&req.headers, &cred),
|
||||
conditions: &get_condition_values(&req.headers, &cred, None, None),
|
||||
is_owner: owner,
|
||||
object: "",
|
||||
claims: cred.claims.as_ref().unwrap_or(&HashMap::new()),
|
||||
@@ -484,7 +484,7 @@ impl Operation for ListServiceAccount {
|
||||
groups: &cred.groups,
|
||||
action: Action::AdminAction(AdminAction::UpdateServiceAccountAdminAction),
|
||||
bucket: "",
|
||||
conditions: &get_condition_values(&req.headers, &cred),
|
||||
conditions: &get_condition_values(&req.headers, &cred, None, None),
|
||||
is_owner: owner,
|
||||
object: "",
|
||||
claims: cred.claims.as_ref().unwrap_or(&HashMap::new()),
|
||||
@@ -582,7 +582,7 @@ impl Operation for DeleteServiceAccount {
|
||||
groups: &cred.groups,
|
||||
action: Action::AdminAction(AdminAction::RemoveServiceAccountAdminAction),
|
||||
bucket: "",
|
||||
conditions: &get_condition_values(&req.headers, &cred),
|
||||
conditions: &get_condition_values(&req.headers, &cred, None, None),
|
||||
is_owner: owner,
|
||||
object: "",
|
||||
claims: cred.claims.as_ref().unwrap_or(&HashMap::new()),
|
||||
|
||||
@@ -21,7 +21,6 @@ use matchit::Params;
|
||||
use rustfs_ecstore::bucket::utils::serialize;
|
||||
use rustfs_iam::{manager::get_token_signing_key, sys::SESSION_POLICY_NAME};
|
||||
use rustfs_policy::{auth::get_new_credentials_with_metadata, policy::Policy};
|
||||
use rustfs_utils::crypto::base64_encode;
|
||||
use s3s::{
|
||||
Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result,
|
||||
dto::{AssumeRoleOutput, Credentials, Timestamp},
|
||||
@@ -104,10 +103,10 @@ impl Operation for AssumeRoleHandle {
|
||||
|
||||
claims.insert(
|
||||
"exp".to_string(),
|
||||
serde_json::Value::Number(serde_json::Number::from(OffsetDateTime::now_utc().unix_timestamp() + exp as i64)),
|
||||
Value::Number(serde_json::Number::from(OffsetDateTime::now_utc().unix_timestamp() + exp as i64)),
|
||||
);
|
||||
|
||||
claims.insert("parent".to_string(), serde_json::Value::String(cred.access_key.clone()));
|
||||
claims.insert("parent".to_string(), Value::String(cred.access_key.clone()));
|
||||
|
||||
// warn!("AssumeRole get cred {:?}", &user);
|
||||
// warn!("AssumeRole get body {:?}", &body);
|
||||
@@ -175,7 +174,10 @@ pub fn populate_session_policy(claims: &mut HashMap<String, Value>, policy: &str
|
||||
return Err(s3_error!(InvalidRequest, "policy too large"));
|
||||
}
|
||||
|
||||
claims.insert(SESSION_POLICY_NAME.to_string(), serde_json::Value::String(base64_encode(&policy_buf)));
|
||||
claims.insert(
|
||||
SESSION_POLICY_NAME.to_string(),
|
||||
Value::String(base64_simd::URL_SAFE_NO_PAD.encode_to_string(&policy_buf)),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -139,35 +139,35 @@ impl Operation for AddTier {
|
||||
let mut tier_config_mgr = GLOBAL_TierConfigMgr.write().await;
|
||||
//tier_config_mgr.reload(api);
|
||||
if let Err(err) = tier_config_mgr.add(args, force).await {
|
||||
if err.code == ERR_TIER_ALREADY_EXISTS.code {
|
||||
return Err(S3Error::with_message(
|
||||
return if err.code == ERR_TIER_ALREADY_EXISTS.code {
|
||||
Err(S3Error::with_message(
|
||||
S3ErrorCode::Custom("TierNameAlreadyExist".into()),
|
||||
"tier name already exists!",
|
||||
));
|
||||
))
|
||||
} else if err.code == ERR_TIER_NAME_NOT_UPPERCASE.code {
|
||||
return Err(S3Error::with_message(
|
||||
Err(S3Error::with_message(
|
||||
S3ErrorCode::Custom("TierNameNotUppercase".into()),
|
||||
"tier name not uppercase!",
|
||||
));
|
||||
))
|
||||
} else if err.code == ERR_TIER_BACKEND_IN_USE.code {
|
||||
return Err(S3Error::with_message(
|
||||
Err(S3Error::with_message(
|
||||
S3ErrorCode::Custom("TierNameBackendInUse!".into()),
|
||||
"tier name backend in use!",
|
||||
));
|
||||
))
|
||||
} else if err.code == ERR_TIER_CONNECT_ERR.code {
|
||||
return Err(S3Error::with_message(
|
||||
Err(S3Error::with_message(
|
||||
S3ErrorCode::Custom("TierConnectError".into()),
|
||||
"tier connect error!",
|
||||
));
|
||||
))
|
||||
} else if err.code == ERR_TIER_INVALID_CREDENTIALS.code {
|
||||
return Err(S3Error::with_message(S3ErrorCode::Custom(err.code.clone().into()), err.message.clone()));
|
||||
Err(S3Error::with_message(S3ErrorCode::Custom(err.code.clone().into()), err.message.clone()))
|
||||
} else {
|
||||
warn!("tier_config_mgr add failed, e: {:?}", err);
|
||||
return Err(S3Error::with_message(
|
||||
Err(S3Error::with_message(
|
||||
S3ErrorCode::Custom("TierAddFailed".into()),
|
||||
format!("tier add failed. {err}"),
|
||||
));
|
||||
}
|
||||
))
|
||||
};
|
||||
}
|
||||
if let Err(e) = tier_config_mgr.save().await {
|
||||
warn!("tier_config_mgr save failed, e: {:?}", e);
|
||||
@@ -223,20 +223,20 @@ impl Operation for EditTier {
|
||||
let mut tier_config_mgr = GLOBAL_TierConfigMgr.write().await;
|
||||
//tier_config_mgr.reload(api);
|
||||
if let Err(err) = tier_config_mgr.edit(&tier_name, creds).await {
|
||||
if err.code == ERR_TIER_NOT_FOUND.code {
|
||||
return Err(S3Error::with_message(S3ErrorCode::Custom("TierNotFound".into()), "tier not found!"));
|
||||
return if err.code == ERR_TIER_NOT_FOUND.code {
|
||||
Err(S3Error::with_message(S3ErrorCode::Custom("TierNotFound".into()), "tier not found!"))
|
||||
} else if err.code == ERR_TIER_MISSING_CREDENTIALS.code {
|
||||
return Err(S3Error::with_message(
|
||||
Err(S3Error::with_message(
|
||||
S3ErrorCode::Custom("TierMissingCredentials".into()),
|
||||
"tier missing credentials!",
|
||||
));
|
||||
))
|
||||
} else {
|
||||
warn!("tier_config_mgr edit failed, e: {:?}", err);
|
||||
return Err(S3Error::with_message(
|
||||
Err(S3Error::with_message(
|
||||
S3ErrorCode::Custom("TierEditFailed".into()),
|
||||
format!("tier edit failed. {err}"),
|
||||
));
|
||||
}
|
||||
))
|
||||
};
|
||||
}
|
||||
if let Err(e) = tier_config_mgr.save().await {
|
||||
warn!("tier_config_mgr save failed, e: {:?}", e);
|
||||
@@ -329,17 +329,17 @@ impl Operation for RemoveTier {
|
||||
let mut tier_config_mgr = GLOBAL_TierConfigMgr.write().await;
|
||||
//tier_config_mgr.reload(api);
|
||||
if let Err(err) = tier_config_mgr.remove(&tier_name, force).await {
|
||||
if err.code == ERR_TIER_NOT_FOUND.code {
|
||||
return Err(S3Error::with_message(S3ErrorCode::Custom("TierNotFound".into()), "tier not found."));
|
||||
return if err.code == ERR_TIER_NOT_FOUND.code {
|
||||
Err(S3Error::with_message(S3ErrorCode::Custom("TierNotFound".into()), "tier not found."))
|
||||
} else if err.code == ERR_TIER_BACKEND_NOT_EMPTY.code {
|
||||
return Err(S3Error::with_message(S3ErrorCode::Custom("TierNameBackendInUse".into()), "tier is used."));
|
||||
Err(S3Error::with_message(S3ErrorCode::Custom("TierNameBackendInUse".into()), "tier is used."))
|
||||
} else {
|
||||
warn!("tier_config_mgr remove failed, e: {:?}", err);
|
||||
return Err(S3Error::with_message(
|
||||
Err(S3Error::with_message(
|
||||
S3ErrorCode::Custom("TierRemoveFailed".into()),
|
||||
format!("tier remove failed. {err}"),
|
||||
));
|
||||
}
|
||||
))
|
||||
};
|
||||
}
|
||||
|
||||
if let Err(e) = tier_config_mgr.save().await {
|
||||
|
||||
@@ -43,10 +43,10 @@ impl Operation for Trace {
|
||||
let _trace_opts = extract_trace_options(&req.uri)?;
|
||||
|
||||
// let (tx, rx) = mpsc::channel(10000);
|
||||
let _perrs = match GLOBAL_Endpoints.get() {
|
||||
let _peers = match GLOBAL_Endpoints.get() {
|
||||
Some(ep) => PeerRestClient::new_clients(ep.clone()).await,
|
||||
None => (Vec::new(), Vec::new()),
|
||||
};
|
||||
return Err(s3_error!(NotImplemented));
|
||||
Err(s3_error!(NotImplemented))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ use rustfs_madmin::{
|
||||
user::{ImportIAMResult, SRSessionPolicy, SRSvcAccCreate},
|
||||
};
|
||||
use rustfs_policy::policy::action::{Action, AdminAction};
|
||||
|
||||
use rustfs_utils::path::path_join_buf;
|
||||
use s3s::{
|
||||
Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result,
|
||||
@@ -675,7 +674,7 @@ impl Operation for ImportIam {
|
||||
let policies: HashMap<String, rustfs_policy::policy::Policy> = serde_json::from_slice(&file_content)
|
||||
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
|
||||
for (name, policy) in policies {
|
||||
if policy.id.is_empty() {
|
||||
if policy.is_empty() {
|
||||
let res = iam_store.delete_policy(&name, true).await;
|
||||
removed.policies.push(name.clone());
|
||||
if let Err(e) = res {
|
||||
|
||||
@@ -19,6 +19,9 @@ pub mod router;
|
||||
mod rpc;
|
||||
pub mod utils;
|
||||
|
||||
#[cfg(test)]
|
||||
mod console_test;
|
||||
|
||||
// use ecstore::global::{is_dist_erasure, is_erasure};
|
||||
use handlers::{
|
||||
GetReplicationMetricsHandler, HealthCheckHandler, ListRemoteTargetHandler, RemoveRemoteTargetHandler, SetRemoteTargetHandler,
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use axum::routing::get;
|
||||
use hyper::HeaderMap;
|
||||
use hyper::Method;
|
||||
use hyper::StatusCode;
|
||||
@@ -32,11 +31,11 @@ use tower::Service;
|
||||
use tracing::error;
|
||||
|
||||
use crate::admin::ADMIN_PREFIX;
|
||||
use crate::admin::console;
|
||||
use crate::admin::console::CONSOLE_PREFIX;
|
||||
use crate::admin::console::is_console_path;
|
||||
use crate::admin::console::make_console_server;
|
||||
use crate::admin::rpc::RPC_PREFIX;
|
||||
|
||||
const CONSOLE_PREFIX: &str = "/rustfs/console";
|
||||
|
||||
pub struct S3Router<T> {
|
||||
router: Router<T>,
|
||||
console_enabled: bool,
|
||||
@@ -48,12 +47,7 @@ impl<T: Operation> S3Router<T> {
|
||||
let router = Router::new();
|
||||
|
||||
let console_router = if console_enabled {
|
||||
Some(
|
||||
axum::Router::new()
|
||||
.nest(CONSOLE_PREFIX, axum::Router::new().fallback_service(get(console::static_handler)))
|
||||
.fallback_service(get(console::static_handler))
|
||||
.into_service::<Body>(),
|
||||
)
|
||||
Some(make_console_server().into_service::<Body>())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -115,7 +109,7 @@ where
|
||||
return Ok(());
|
||||
}
|
||||
// Allow unauthenticated access to console static files if console is enabled
|
||||
if self.console_enabled && req.uri.path().starts_with(CONSOLE_PREFIX) {
|
||||
if self.console_enabled && is_console_path(req.uri.path()) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -139,7 +133,7 @@ where
|
||||
}
|
||||
|
||||
async fn call(&self, req: S3Request<Body>) -> S3Result<S3Response<Body>> {
|
||||
if self.console_enabled && req.uri.path().starts_with(CONSOLE_PREFIX) {
|
||||
if self.console_enabled && is_console_path(req.uri.path()) {
|
||||
if let Some(console_router) = &self.console_router {
|
||||
let mut console_router = console_router.clone();
|
||||
let req = convert_request(req);
|
||||
|
||||
+395
-55
@@ -19,6 +19,7 @@ use rustfs_iam::error::Error as IamError;
|
||||
use rustfs_iam::sys::SESSION_POLICY_NAME;
|
||||
use rustfs_iam::sys::get_claims_from_token_with_secret;
|
||||
use rustfs_policy::auth;
|
||||
use rustfs_utils::http::ip::get_source_ip_raw;
|
||||
use s3s::S3Error;
|
||||
use s3s::S3ErrorCode;
|
||||
use s3s::S3Result;
|
||||
@@ -28,6 +29,40 @@ use s3s::auth::SimpleAuth;
|
||||
use s3s::s3_error;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use time::OffsetDateTime;
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
|
||||
// Authentication type constants
|
||||
const JWT_ALGORITHM: &str = "Bearer ";
|
||||
const SIGN_V2_ALGORITHM: &str = "AWS ";
|
||||
const SIGN_V4_ALGORITHM: &str = "AWS4-HMAC-SHA256";
|
||||
const STREAMING_CONTENT_SHA256: &str = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD";
|
||||
const STREAMING_CONTENT_SHA256_TRAILER: &str = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER";
|
||||
pub const UNSIGNED_PAYLOAD_TRAILER: &str = "STREAMING-UNSIGNED-PAYLOAD-TRAILER";
|
||||
const ACTION_HEADER: &str = "Action";
|
||||
const AMZ_CREDENTIAL: &str = "X-Amz-Credential";
|
||||
const AMZ_ACCESS_KEY_ID: &str = "AWSAccessKeyId";
|
||||
pub const UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD";
|
||||
|
||||
// Authentication type enum
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub enum AuthType {
|
||||
#[default]
|
||||
Unknown,
|
||||
Anonymous,
|
||||
Presigned,
|
||||
PresignedV2,
|
||||
PostPolicy,
|
||||
StreamingSigned,
|
||||
Signed,
|
||||
SignedV2,
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
JWT,
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
STS,
|
||||
StreamingSignedTrailer,
|
||||
StreamingUnsignedTrailer,
|
||||
}
|
||||
|
||||
pub struct IAMAuth {
|
||||
simple_auth: SimpleAuth,
|
||||
@@ -167,7 +202,12 @@ pub fn get_session_token<'a>(uri: &'a Uri, hds: &'a HeaderMap) -> Option<&'a str
|
||||
.or_else(|| get_query_param(uri.query().unwrap_or_default(), "x-amz-security-token"))
|
||||
}
|
||||
|
||||
pub fn get_condition_values(header: &HeaderMap, cred: &auth::Credentials) -> HashMap<String, Vec<String>> {
|
||||
pub fn get_condition_values(
|
||||
header: &HeaderMap,
|
||||
cred: &auth::Credentials,
|
||||
version_id: Option<&str>,
|
||||
region: Option<&str>,
|
||||
) -> HashMap<String, Vec<String>> {
|
||||
let username = if cred.is_temp() || cred.is_service_account() {
|
||||
cred.parent_user.clone()
|
||||
} else {
|
||||
@@ -190,32 +230,83 @@ pub fn get_condition_values(header: &HeaderMap, cred: &auth::Credentials) -> Has
|
||||
"Anonymous"
|
||||
};
|
||||
|
||||
// Get current time
|
||||
let curr_time = OffsetDateTime::now_utc();
|
||||
let epoch_time = curr_time.unix_timestamp();
|
||||
|
||||
// Use provided version ID or empty string
|
||||
let vid = version_id.unwrap_or("");
|
||||
|
||||
// Determine auth type and signature version from headers
|
||||
let (auth_type, signature_version) = determine_auth_type_and_version(header);
|
||||
|
||||
// Get TLS status from header
|
||||
let is_tls = header
|
||||
.get("x-forwarded-proto")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s == "https")
|
||||
.or_else(|| {
|
||||
header
|
||||
.get("x-forwarded-scheme")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s == "https")
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
// Get remote address from header or use default
|
||||
let remote_addr = header
|
||||
.get("x-forwarded-for")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|s| s.split(',').next())
|
||||
.or_else(|| header.get("x-real-ip").and_then(|v| v.to_str().ok()))
|
||||
.unwrap_or("127.0.0.1");
|
||||
|
||||
let mut args = HashMap::new();
|
||||
|
||||
// Add basic time and security info
|
||||
args.insert("CurrentTime".to_owned(), vec![curr_time.format(&Rfc3339).unwrap_or_default()]);
|
||||
args.insert("EpochTime".to_owned(), vec![epoch_time.to_string()]);
|
||||
args.insert("SecureTransport".to_owned(), vec![is_tls.to_string()]);
|
||||
args.insert("SourceIp".to_owned(), vec![get_source_ip_raw(header, remote_addr)]);
|
||||
|
||||
// Add user agent and referer
|
||||
if let Some(user_agent) = header.get("user-agent") {
|
||||
args.insert("UserAgent".to_owned(), vec![user_agent.to_str().unwrap_or("").to_string()]);
|
||||
}
|
||||
if let Some(referer) = header.get("referer") {
|
||||
args.insert("Referer".to_owned(), vec![referer.to_str().unwrap_or("").to_string()]);
|
||||
}
|
||||
|
||||
// Add user and principal info
|
||||
args.insert("userid".to_owned(), vec![username.clone()]);
|
||||
args.insert("username".to_owned(), vec![username]);
|
||||
args.insert("principaltype".to_owned(), vec![principal_type.to_string()]);
|
||||
|
||||
// Add version ID
|
||||
if !vid.is_empty() {
|
||||
args.insert("versionid".to_owned(), vec![vid.to_string()]);
|
||||
}
|
||||
|
||||
// Add signature version and auth type
|
||||
if !signature_version.is_empty() {
|
||||
args.insert("signatureversion".to_owned(), vec![signature_version]);
|
||||
}
|
||||
if !auth_type.is_empty() {
|
||||
args.insert("authType".to_owned(), vec![auth_type]);
|
||||
}
|
||||
|
||||
if let Some(lc) = region {
|
||||
if !lc.is_empty() {
|
||||
args.insert("LocationConstraint".to_owned(), vec![lc.to_string()]);
|
||||
}
|
||||
}
|
||||
|
||||
let mut clone_header = header.clone();
|
||||
if let Some(v) = clone_header.get("x-amz-signature-age") {
|
||||
args.insert("signatureAge".to_string(), vec![v.to_str().unwrap_or("").to_string()]);
|
||||
clone_header.remove("x-amz-signature-age");
|
||||
}
|
||||
|
||||
// TODO: parse_object_tags
|
||||
// if let Some(_user_tags) = clone_header.get("x-amz-tagging") {
|
||||
// TODO: parse_object_tags
|
||||
// if let Ok(tag) = tags::parse_object_tags(user_tags.to_str().unwrap_or("")) {
|
||||
// let tag_map = tag.to_map();
|
||||
// let mut keys = Vec::new();
|
||||
// for (k, v) in tag_map {
|
||||
// args.insert(format!("ExistingObjectTag/{}", k), vec![v.clone()]);
|
||||
// args.insert(format!("RequestObjectTag/{}", k), vec![v.clone()]);
|
||||
// keys.push(k);
|
||||
// }
|
||||
// args.insert("RequestObjectTagKeys".to_string(), keys);
|
||||
// }
|
||||
// }
|
||||
|
||||
for obj_lock in &[
|
||||
"x-amz-object-lock-mode",
|
||||
"x-amz-object-lock-legal-hold",
|
||||
@@ -250,37 +341,6 @@ pub fn get_condition_values(header: &HeaderMap, cred: &auth::Credentials) -> Has
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add from url query
|
||||
// let mut clone_url_values = r
|
||||
// .uri()
|
||||
// .query()
|
||||
// .unwrap_or("")
|
||||
// .split('&')
|
||||
// .map(|s| {
|
||||
// let mut split = s.split('=');
|
||||
// (split.next().unwrap_or("").to_string(), split.next().unwrap_or("").to_string())
|
||||
// })
|
||||
// .collect::<HashMap<String, String>>();
|
||||
|
||||
// for obj_lock in &[
|
||||
// "x-amz-object-lock-mode",
|
||||
// "x-amz-object-lock-legal-hold",
|
||||
// "x-amz-object-lock-retain-until-date",
|
||||
// ] {
|
||||
// if let Some(values) = clone_url_values.get(*obj_lock) {
|
||||
// args.insert(obj_lock.trim_start_matches("x-amz-").to_string(), vec![values.clone()]);
|
||||
// }
|
||||
// clone_url_values.remove(*obj_lock);
|
||||
// }
|
||||
|
||||
// for (key, values) in clone_url_values.iter() {
|
||||
// if let Some(existing_values) = args.get_mut(key) {
|
||||
// existing_values.push(values.clone());
|
||||
// } else {
|
||||
// args.insert(key.clone(), vec![values.clone()]);
|
||||
// }
|
||||
// }
|
||||
|
||||
if let Some(claims) = &cred.claims {
|
||||
for (k, v) in claims {
|
||||
if let Some(v_str) = v.as_str() {
|
||||
@@ -310,6 +370,152 @@ pub fn get_condition_values(header: &HeaderMap, cred: &auth::Credentials) -> Has
|
||||
args
|
||||
}
|
||||
|
||||
// Get request authentication type
|
||||
pub fn get_request_auth_type(header: &HeaderMap) -> AuthType {
|
||||
if is_request_signature_v2(header) {
|
||||
AuthType::SignedV2
|
||||
} else if is_request_presigned_signature_v2(header) {
|
||||
AuthType::PresignedV2
|
||||
} else if is_request_sign_streaming_v4(header) {
|
||||
AuthType::StreamingSigned
|
||||
} else if is_request_sign_streaming_trailer_v4(header) {
|
||||
AuthType::StreamingSignedTrailer
|
||||
} else if is_request_unsigned_trailer_v4(header) {
|
||||
AuthType::StreamingUnsignedTrailer
|
||||
} else if is_request_signature_v4(header) {
|
||||
AuthType::Signed
|
||||
} else if is_request_presigned_signature_v4(header) {
|
||||
AuthType::Presigned
|
||||
} else if is_request_jwt(header) {
|
||||
AuthType::JWT
|
||||
} else if is_request_post_policy_signature_v4(header) {
|
||||
AuthType::PostPolicy
|
||||
} else if is_request_sts(header) {
|
||||
AuthType::STS
|
||||
} else if is_request_anonymous(header) {
|
||||
AuthType::Anonymous
|
||||
} else {
|
||||
AuthType::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to determine auth type and signature version
|
||||
fn determine_auth_type_and_version(header: &HeaderMap) -> (String, String) {
|
||||
match get_request_auth_type(header) {
|
||||
AuthType::JWT => ("JWT".to_string(), String::new()),
|
||||
AuthType::SignedV2 => ("REST-HEADER".to_string(), "AWS2".to_string()),
|
||||
AuthType::PresignedV2 => ("REST-QUERY-STRING".to_string(), "AWS2".to_string()),
|
||||
AuthType::StreamingSigned | AuthType::StreamingSignedTrailer | AuthType::StreamingUnsignedTrailer => {
|
||||
("REST-HEADER".to_string(), "AWS4-HMAC-SHA256".to_string())
|
||||
}
|
||||
AuthType::Signed => ("REST-HEADER".to_string(), "AWS4-HMAC-SHA256".to_string()),
|
||||
AuthType::Presigned => ("REST-QUERY-STRING".to_string(), "AWS4-HMAC-SHA256".to_string()),
|
||||
AuthType::PostPolicy => ("POST".to_string(), String::new()),
|
||||
AuthType::STS => ("STS".to_string(), String::new()),
|
||||
AuthType::Anonymous => ("Anonymous".to_string(), String::new()),
|
||||
AuthType::Unknown => (String::new(), String::new()),
|
||||
}
|
||||
}
|
||||
|
||||
// Verify if request has JWT
|
||||
fn is_request_jwt(header: &HeaderMap) -> bool {
|
||||
if let Some(auth) = header.get("authorization") {
|
||||
if let Ok(auth_str) = auth.to_str() {
|
||||
return auth_str.starts_with(JWT_ALGORITHM);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// Verify if request has AWS Signature Version '4'
|
||||
fn is_request_signature_v4(header: &HeaderMap) -> bool {
|
||||
if let Some(auth) = header.get("authorization") {
|
||||
if let Ok(auth_str) = auth.to_str() {
|
||||
return auth_str.starts_with(SIGN_V4_ALGORITHM);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// Verify if request has AWS Signature Version '2'
|
||||
fn is_request_signature_v2(header: &HeaderMap) -> bool {
|
||||
if let Some(auth) = header.get("authorization") {
|
||||
if let Ok(auth_str) = auth.to_str() {
|
||||
return !auth_str.starts_with(SIGN_V4_ALGORITHM) && auth_str.starts_with(SIGN_V2_ALGORITHM);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// Verify if request has AWS PreSign Version '4'
|
||||
pub(crate) fn is_request_presigned_signature_v4(header: &HeaderMap) -> bool {
|
||||
if let Some(credential) = header.get(AMZ_CREDENTIAL) {
|
||||
return !credential.to_str().unwrap_or("").is_empty();
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// Verify request has AWS PreSign Version '2'
|
||||
fn is_request_presigned_signature_v2(header: &HeaderMap) -> bool {
|
||||
if let Some(access_key) = header.get(AMZ_ACCESS_KEY_ID) {
|
||||
return !access_key.to_str().unwrap_or("").is_empty();
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// Verify if request has AWS Post policy Signature Version '4'
|
||||
fn is_request_post_policy_signature_v4(header: &HeaderMap) -> bool {
|
||||
if let Some(content_type) = header.get("content-type") {
|
||||
if let Ok(ct) = content_type.to_str() {
|
||||
return ct.contains("multipart/form-data");
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// Verify if the request has AWS Streaming Signature Version '4'
|
||||
fn is_request_sign_streaming_v4(header: &HeaderMap) -> bool {
|
||||
if let Some(content_sha256) = header.get("x-amz-content-sha256") {
|
||||
if let Ok(sha256_str) = content_sha256.to_str() {
|
||||
return sha256_str == STREAMING_CONTENT_SHA256;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// Verify if the request has AWS Streaming Signature Version '4' with trailer
|
||||
fn is_request_sign_streaming_trailer_v4(header: &HeaderMap) -> bool {
|
||||
if let Some(content_sha256) = header.get("x-amz-content-sha256") {
|
||||
if let Ok(sha256_str) = content_sha256.to_str() {
|
||||
return sha256_str == STREAMING_CONTENT_SHA256_TRAILER;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// Verify if the request has AWS Streaming Signature Version '4' with unsigned content and trailer
|
||||
fn is_request_unsigned_trailer_v4(header: &HeaderMap) -> bool {
|
||||
if let Some(content_sha256) = header.get("x-amz-content-sha256") {
|
||||
if let Ok(sha256_str) = content_sha256.to_str() {
|
||||
return sha256_str == UNSIGNED_PAYLOAD_TRAILER;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// Verify if request is STS (Security Token Service)
|
||||
fn is_request_sts(header: &HeaderMap) -> bool {
|
||||
if let Some(action) = header.get(ACTION_HEADER) {
|
||||
return !action.to_str().unwrap_or("").is_empty();
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// Verify if request is anonymous
|
||||
fn is_request_anonymous(header: &HeaderMap) -> bool {
|
||||
header.get("authorization").is_none()
|
||||
}
|
||||
|
||||
pub fn get_query_param<'a>(query: &'a str, param_name: &str) -> Option<&'a str> {
|
||||
let param_name = param_name.to_lowercase();
|
||||
|
||||
@@ -392,7 +598,7 @@ mod tests {
|
||||
// The struct should be created successfully
|
||||
// We can't easily test internal state without exposing it,
|
||||
// but we can test it doesn't panic on creation
|
||||
assert_eq!(std::mem::size_of_val(&iam_auth), std::mem::size_of::<IAMAuth>());
|
||||
assert_eq!(size_of_val(&iam_auth), size_of::<IAMAuth>());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -549,7 +755,7 @@ mod tests {
|
||||
let cred = create_test_credentials();
|
||||
let headers = HeaderMap::new();
|
||||
|
||||
let conditions = get_condition_values(&headers, &cred);
|
||||
let conditions = get_condition_values(&headers, &cred, None, None);
|
||||
|
||||
assert_eq!(conditions.get("userid"), Some(&vec!["test-access-key".to_string()]));
|
||||
assert_eq!(conditions.get("username"), Some(&vec!["test-access-key".to_string()]));
|
||||
@@ -561,7 +767,7 @@ mod tests {
|
||||
let cred = create_temp_credentials();
|
||||
let headers = HeaderMap::new();
|
||||
|
||||
let conditions = get_condition_values(&headers, &cred);
|
||||
let conditions = get_condition_values(&headers, &cred, None, None);
|
||||
|
||||
assert_eq!(conditions.get("userid"), Some(&vec!["parent-user".to_string()]));
|
||||
assert_eq!(conditions.get("username"), Some(&vec!["parent-user".to_string()]));
|
||||
@@ -573,7 +779,7 @@ mod tests {
|
||||
let cred = create_service_account_credentials();
|
||||
let headers = HeaderMap::new();
|
||||
|
||||
let conditions = get_condition_values(&headers, &cred);
|
||||
let conditions = get_condition_values(&headers, &cred, None, None);
|
||||
|
||||
assert_eq!(conditions.get("userid"), Some(&vec!["service-parent".to_string()]));
|
||||
assert_eq!(conditions.get("username"), Some(&vec!["service-parent".to_string()]));
|
||||
@@ -588,7 +794,7 @@ mod tests {
|
||||
headers.insert("x-amz-object-lock-mode", HeaderValue::from_static("GOVERNANCE"));
|
||||
headers.insert("x-amz-object-lock-retain-until-date", HeaderValue::from_static("2024-12-31T23:59:59Z"));
|
||||
|
||||
let conditions = get_condition_values(&headers, &cred);
|
||||
let conditions = get_condition_values(&headers, &cred, None, None);
|
||||
|
||||
assert_eq!(conditions.get("object-lock-mode"), Some(&vec!["GOVERNANCE".to_string()]));
|
||||
assert_eq!(
|
||||
@@ -603,7 +809,7 @@ mod tests {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("x-amz-signature-age", HeaderValue::from_static("300"));
|
||||
|
||||
let conditions = get_condition_values(&headers, &cred);
|
||||
let conditions = get_condition_values(&headers, &cred, None, None);
|
||||
|
||||
assert_eq!(conditions.get("signatureAge"), Some(&vec!["300".to_string()]));
|
||||
// Verify the header is removed after processing
|
||||
@@ -620,7 +826,7 @@ mod tests {
|
||||
|
||||
let headers = HeaderMap::new();
|
||||
|
||||
let conditions = get_condition_values(&headers, &cred);
|
||||
let conditions = get_condition_values(&headers, &cred, None, None);
|
||||
|
||||
assert_eq!(conditions.get("username"), Some(&vec!["ldap-user".to_string()]));
|
||||
assert_eq!(conditions.get("groups"), Some(&vec!["group1".to_string(), "group2".to_string()]));
|
||||
@@ -633,7 +839,7 @@ mod tests {
|
||||
|
||||
let headers = HeaderMap::new();
|
||||
|
||||
let conditions = get_condition_values(&headers, &cred);
|
||||
let conditions = get_condition_values(&headers, &cred, None, None);
|
||||
|
||||
assert_eq!(
|
||||
conditions.get("groups"),
|
||||
@@ -758,4 +964,138 @@ mod tests {
|
||||
|
||||
assert!(!cred.is_service_account());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_request_auth_type_jwt() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("authorization", HeaderValue::from_static("Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"));
|
||||
|
||||
let auth_type = get_request_auth_type(&headers);
|
||||
|
||||
assert_eq!(auth_type, AuthType::JWT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_request_auth_type_signature_v2() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"authorization",
|
||||
HeaderValue::from_static("AWS AKIAIOSFODNN7EXAMPLE:frJIUN8DYpKDtOLCwo//bqJZQ1iY="),
|
||||
);
|
||||
|
||||
let auth_type = get_request_auth_type(&headers);
|
||||
|
||||
assert_eq!(auth_type, AuthType::SignedV2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_request_auth_type_signature_v4() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"authorization",
|
||||
HeaderValue::from_static("AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request"),
|
||||
);
|
||||
|
||||
let auth_type = get_request_auth_type(&headers);
|
||||
|
||||
assert_eq!(auth_type, AuthType::Signed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_request_auth_type_presigned_v2() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("AWSAccessKeyId", HeaderValue::from_static("AKIAIOSFODNN7EXAMPLE"));
|
||||
|
||||
let auth_type = get_request_auth_type(&headers);
|
||||
|
||||
assert_eq!(auth_type, AuthType::PresignedV2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_request_auth_type_presigned_v4() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"X-Amz-Credential",
|
||||
HeaderValue::from_static("AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request"),
|
||||
);
|
||||
|
||||
let auth_type = get_request_auth_type(&headers);
|
||||
|
||||
assert_eq!(auth_type, AuthType::Presigned);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_request_auth_type_post_policy() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"content-type",
|
||||
HeaderValue::from_static("multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW"),
|
||||
);
|
||||
|
||||
let auth_type = get_request_auth_type(&headers);
|
||||
|
||||
assert_eq!(auth_type, AuthType::PostPolicy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_request_auth_type_streaming_signed() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("x-amz-content-sha256", HeaderValue::from_static("STREAMING-AWS4-HMAC-SHA256-PAYLOAD"));
|
||||
|
||||
let auth_type = get_request_auth_type(&headers);
|
||||
|
||||
assert_eq!(auth_type, AuthType::StreamingSigned);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_request_auth_type_streaming_signed_trailer() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"x-amz-content-sha256",
|
||||
HeaderValue::from_static("STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER"),
|
||||
);
|
||||
|
||||
let auth_type = get_request_auth_type(&headers);
|
||||
|
||||
assert_eq!(auth_type, AuthType::StreamingSignedTrailer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_request_auth_type_streaming_unsigned_trailer() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("x-amz-content-sha256", HeaderValue::from_static("STREAMING-UNSIGNED-PAYLOAD-TRAILER"));
|
||||
|
||||
let auth_type = get_request_auth_type(&headers);
|
||||
|
||||
assert_eq!(auth_type, AuthType::StreamingUnsignedTrailer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_request_auth_type_sts() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("Action", HeaderValue::from_static("AssumeRole"));
|
||||
|
||||
let auth_type = get_request_auth_type(&headers);
|
||||
|
||||
assert_eq!(auth_type, AuthType::STS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_request_auth_type_anonymous() {
|
||||
let headers = HeaderMap::new();
|
||||
|
||||
let auth_type = get_request_auth_type(&headers);
|
||||
|
||||
assert_eq!(auth_type, AuthType::Anonymous);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_request_auth_type_unknown() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("authorization", HeaderValue::from_static("CustomAuth token123"));
|
||||
|
||||
let auth_type = get_request_auth_type(&headers);
|
||||
|
||||
assert_eq!(auth_type, AuthType::Unknown);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ mod tests {
|
||||
|
||||
assert!(opt.console_enable);
|
||||
assert_eq!(opt.console_address, ":9001");
|
||||
assert_eq!(opt.external_address, ":9000"); // Now defaults to DEFAULT_ADDRESS
|
||||
assert_eq!(opt.address, ":9000");
|
||||
}
|
||||
|
||||
@@ -49,15 +48,6 @@ mod tests {
|
||||
assert_eq!(opt.address, ":8000");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_external_address_configuration() {
|
||||
// Test external address configuration for Docker
|
||||
let args = vec!["rustfs", "/test/volume", "--external-address", ":9020"];
|
||||
let opt = Opt::parse_from(args);
|
||||
|
||||
assert_eq!(opt.external_address, ":9020".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_console_and_endpoint_ports_different() {
|
||||
// Ensure console and endpoint use different default ports
|
||||
|
||||
@@ -75,12 +75,6 @@ pub struct Opt {
|
||||
#[arg(long, default_value_t = rustfs_config::DEFAULT_CONSOLE_ADDRESS.to_string(), env = "RUSTFS_CONSOLE_ADDRESS")]
|
||||
pub console_address: String,
|
||||
|
||||
/// External address for console to access endpoint (used in Docker deployments)
|
||||
/// This should match the mapped host port when using Docker port mapping
|
||||
/// Example: ":9020" when mapping host port 9020 to container port 9000
|
||||
#[arg(long, default_value_t = rustfs_config::DEFAULT_ADDRESS.to_string(), env = "RUSTFS_EXTERNAL_ADDRESS")]
|
||||
pub external_address: String,
|
||||
|
||||
/// Observability endpoint for trace, metrics and logs,only support grpc mode.
|
||||
#[arg(long, default_value_t = rustfs_config::DEFAULT_OBS_ENDPOINT.to_string(), env = "RUSTFS_OBS_ENDPOINT")]
|
||||
pub obs_endpoint: String,
|
||||
@@ -89,18 +83,6 @@ pub struct Opt {
|
||||
#[arg(long, env = "RUSTFS_TLS_PATH")]
|
||||
pub tls_path: Option<String>,
|
||||
|
||||
/// Enable rate limiting for console
|
||||
#[arg(long, default_value_t = rustfs_config::DEFAULT_CONSOLE_RATE_LIMIT_ENABLE, env = "RUSTFS_CONSOLE_RATE_LIMIT_ENABLE")]
|
||||
pub console_rate_limit_enable: bool,
|
||||
|
||||
/// Console rate limit: requests per minute
|
||||
#[arg(long, default_value_t = rustfs_config::DEFAULT_CONSOLE_RATE_LIMIT_RPM, env = "RUSTFS_CONSOLE_RATE_LIMIT_RPM")]
|
||||
pub console_rate_limit_rpm: u32,
|
||||
|
||||
/// Console authentication timeout in seconds
|
||||
#[arg(long, default_value_t = rustfs_config::DEFAULT_CONSOLE_AUTH_TIMEOUT, env = "RUSTFS_CONSOLE_AUTH_TIMEOUT")]
|
||||
pub console_auth_timeout: u64,
|
||||
|
||||
#[arg(long, env = "RUSTFS_LICENSE")]
|
||||
pub license: Option<String>,
|
||||
|
||||
|
||||
+47
-62
@@ -25,10 +25,9 @@ mod storage;
|
||||
mod update;
|
||||
mod version;
|
||||
|
||||
use crate::admin::console::init_console_cfg;
|
||||
use crate::server::{
|
||||
SHUTDOWN_TIMEOUT, ServiceState, ServiceStateManager, ShutdownSignal, init_event_notifier, shutdown_event_notifier,
|
||||
start_audit_system, start_console_server, start_http_server, stop_audit_system, wait_for_shutdown,
|
||||
start_audit_system, start_http_server, stop_audit_system, wait_for_shutdown,
|
||||
};
|
||||
use crate::storage::ecfs::{process_lambda_configurations, process_queue_configurations, process_topic_configurations};
|
||||
use chrono::Datelike;
|
||||
@@ -133,26 +132,38 @@ async fn async_main() -> Result<()> {
|
||||
info!("{}", LOGO);
|
||||
|
||||
// Store in global storage
|
||||
set_global_guard(guard).map_err(Error::other)?;
|
||||
match set_global_guard(guard).map_err(Error::other) {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
error!("Failed to set global observability guard: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize performance profiling if enabled
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
profiling::start_profiling_if_enabled();
|
||||
|
||||
// Run parameters
|
||||
run(opt).await
|
||||
match run(opt).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
error!("Server encountered an error and is shutting down: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(opt))]
|
||||
async fn run(opt: config::Opt) -> Result<()> {
|
||||
debug!("opt: {:?}", &opt);
|
||||
|
||||
// // Initialize global DNS resolver early for enhanced DNS resolution (concurrent)
|
||||
// let dns_init = tokio::spawn(async {
|
||||
// if let Err(e) = rustfs_utils::dns_resolver::init_global_dns_resolver().await {
|
||||
// warn!("Failed to initialize global DNS resolver: {}. Using standard DNS resolution.", e);
|
||||
// }
|
||||
// });
|
||||
// Initialize global DNS resolver early for enhanced DNS resolution (concurrent)
|
||||
let dns_init = tokio::spawn(async {
|
||||
if let Err(e) = rustfs_utils::dns_resolver::init_global_dns_resolver().await {
|
||||
warn!("Failed to initialize global DNS resolver: {}. Using standard DNS resolution.", e);
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(region) = &opt.region {
|
||||
rustfs_ecstore::global::set_global_region(region.clone());
|
||||
@@ -173,14 +184,14 @@ async fn run(opt: config::Opt) -> Result<()> {
|
||||
);
|
||||
|
||||
// Set up AK and SK
|
||||
rustfs_ecstore::global::init_global_action_cred(Some(opt.access_key.clone()), Some(opt.secret_key.clone()));
|
||||
rustfs_ecstore::global::init_global_action_credentials(Some(opt.access_key.clone()), Some(opt.secret_key.clone()));
|
||||
|
||||
set_global_rustfs_port(server_port);
|
||||
|
||||
set_global_addr(&opt.address).await;
|
||||
|
||||
// // Wait for DNS initialization to complete before network-heavy operations
|
||||
// dns_init.await.map_err(Error::other)?;
|
||||
// Wait for DNS initialization to complete before network-heavy operations
|
||||
dns_init.await.map_err(Error::other)?;
|
||||
|
||||
// For RPC
|
||||
let (endpoint_pools, setup_type) = EndpointServerPools::from_volumes(server_address.clone().as_str(), opt.volumes.clone())
|
||||
@@ -224,53 +235,21 @@ async fn run(opt: config::Opt) -> Result<()> {
|
||||
// Update service status to Starting
|
||||
state_manager.update(ServiceState::Starting);
|
||||
|
||||
let shutdown_tx = start_http_server(&opt, state_manager.clone()).await?;
|
||||
// Start console server if enabled
|
||||
let console_shutdown_tx = shutdown_tx.clone();
|
||||
if opt.console_enable && !opt.console_address.is_empty() {
|
||||
// Deal with port mapping issues for virtual machines like docker
|
||||
let (external_addr, external_port) = if !opt.external_address.is_empty() {
|
||||
let external_addr = parse_and_resolve_address(opt.external_address.as_str()).map_err(Error::other)?;
|
||||
let external_port = external_addr.port();
|
||||
if external_port != server_port {
|
||||
warn!(
|
||||
"External port {} is different from server port {}, ensure your firewall allows access to the external port if needed.",
|
||||
external_port, server_port
|
||||
);
|
||||
}
|
||||
info!(
|
||||
target: "rustfs::main::run",
|
||||
external_address = %external_addr,
|
||||
external_port = %external_port,
|
||||
"Using external address {} for endpoint access", external_addr
|
||||
);
|
||||
rustfs_ecstore::global::set_global_rustfs_external_port(external_port);
|
||||
set_global_addr(&opt.external_address).await;
|
||||
(external_addr.ip(), external_port)
|
||||
} else {
|
||||
(server_addr.ip(), server_port)
|
||||
};
|
||||
warn!("Starting console server on address: '{}', port: '{}'", external_addr, external_port);
|
||||
// init console configuration
|
||||
init_console_cfg(external_addr, external_port);
|
||||
let s3_shutdown_tx = {
|
||||
let mut s3_opt = opt.clone();
|
||||
s3_opt.console_enable = false;
|
||||
let s3_shutdown_tx = start_http_server(&s3_opt, state_manager.clone()).await?;
|
||||
Some(s3_shutdown_tx)
|
||||
};
|
||||
|
||||
let opt_clone = opt.clone();
|
||||
tokio::spawn(async move {
|
||||
let console_shutdown_rx = console_shutdown_tx.subscribe();
|
||||
if let Err(e) = start_console_server(&opt_clone, console_shutdown_rx).await {
|
||||
error!("Console server failed to start: {}", e);
|
||||
}
|
||||
});
|
||||
let console_shutdown_tx = if opt.console_enable && !opt.console_address.is_empty() {
|
||||
let mut console_opt = opt.clone();
|
||||
console_opt.address = console_opt.console_address.clone();
|
||||
let console_shutdown_tx = start_http_server(&console_opt, state_manager.clone()).await?;
|
||||
Some(console_shutdown_tx)
|
||||
} else {
|
||||
info!("Console server is disabled.");
|
||||
info!("You can access the RustFS API at {}", &opt.address);
|
||||
info!("For more information, visit https://rustfs.com/docs/");
|
||||
info!("To enable the console, restart the server with --console-enable and a valid --console-address.");
|
||||
info!(
|
||||
"Current console address is set to: '{}' ,console enable is set to: '{}'",
|
||||
&opt.console_address, &opt.console_enable
|
||||
);
|
||||
}
|
||||
None
|
||||
};
|
||||
|
||||
set_global_endpoints(endpoint_pools.as_ref().clone());
|
||||
update_erasure_type(setup_type).await;
|
||||
@@ -379,11 +358,11 @@ async fn run(opt: config::Opt) -> Result<()> {
|
||||
match wait_for_shutdown().await {
|
||||
#[cfg(unix)]
|
||||
ShutdownSignal::CtrlC | ShutdownSignal::Sigint | ShutdownSignal::Sigterm => {
|
||||
handle_shutdown(&state_manager, &shutdown_tx, ctx.clone()).await;
|
||||
handle_shutdown(&state_manager, s3_shutdown_tx, console_shutdown_tx, ctx.clone()).await;
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
ShutdownSignal::CtrlC => {
|
||||
handle_shutdown(&state_manager, &shutdown_tx, ctx.clone()).await;
|
||||
handle_shutdown(&state_manager, s3_shutdown_tx, console_shutdown_tx, ctx.clone()).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,7 +384,8 @@ fn parse_bool_env_var(var_name: &str, default: bool) -> bool {
|
||||
/// Handles the shutdown process of the server
|
||||
async fn handle_shutdown(
|
||||
state_manager: &ServiceStateManager,
|
||||
shutdown_tx: &tokio::sync::broadcast::Sender<()>,
|
||||
s3_shutdown_tx: Option<tokio::sync::broadcast::Sender<()>>,
|
||||
console_shutdown_tx: Option<tokio::sync::broadcast::Sender<()>>,
|
||||
ctx: CancellationToken,
|
||||
) {
|
||||
ctx.cancel();
|
||||
@@ -462,7 +442,12 @@ async fn handle_shutdown(
|
||||
target: "rustfs::main::handle_shutdown",
|
||||
"Server is stopping..."
|
||||
);
|
||||
let _ = shutdown_tx.send(());
|
||||
if let Some(s3_shutdown_tx) = s3_shutdown_tx {
|
||||
let _ = s3_shutdown_tx.send(());
|
||||
}
|
||||
if let Some(console_shutdown_tx) = console_shutdown_tx {
|
||||
let _ = console_shutdown_tx.send(());
|
||||
}
|
||||
|
||||
// Wait for the worker thread to complete the cleaning work
|
||||
tokio::time::sleep(SHUTDOWN_TIMEOUT).await;
|
||||
|
||||
@@ -1,410 +0,0 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::admin::console::static_handler;
|
||||
use crate::config::Opt;
|
||||
use axum::{Router, extract::Request, middleware, response::Json, routing::get};
|
||||
use axum_server::tls_rustls::RustlsConfig;
|
||||
use http::{HeaderValue, Method};
|
||||
use rustfs_config::{RUSTFS_TLS_CERT, RUSTFS_TLS_KEY};
|
||||
use rustfs_utils::net::parse_and_resolve_address;
|
||||
use serde_json::json;
|
||||
use std::io::Result;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio_rustls::rustls::ServerConfig;
|
||||
use tower_http::catch_panic::CatchPanicLayer;
|
||||
use tower_http::cors::{AllowOrigin, Any, CorsLayer};
|
||||
use tower_http::limit::RequestBodyLimitLayer;
|
||||
use tower_http::timeout::TimeoutLayer;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
|
||||
const CONSOLE_PREFIX: &str = "/rustfs/console";
|
||||
|
||||
/// Console access logging middleware
|
||||
async fn console_logging_middleware(req: Request, next: axum::middleware::Next) -> axum::response::Response {
|
||||
let method = req.method().clone();
|
||||
let uri = req.uri().clone();
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let response = next.run(req).await;
|
||||
let duration = start.elapsed();
|
||||
|
||||
info!(
|
||||
target: "rustfs::console::access",
|
||||
method = %method,
|
||||
uri = %uri,
|
||||
status = %response.status(),
|
||||
duration_ms = %duration.as_millis(),
|
||||
"Console access"
|
||||
);
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
/// Setup TLS configuration for console using axum-server, following endpoint TLS implementation logic
|
||||
#[instrument(skip(tls_path))]
|
||||
async fn setup_console_tls_config(tls_path: Option<&String>) -> Result<Option<RustlsConfig>> {
|
||||
let tls_path = match tls_path {
|
||||
Some(path) if !path.is_empty() => path,
|
||||
_ => {
|
||||
debug!("TLS path is not provided, console starting with HTTP");
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
if tokio::fs::metadata(tls_path).await.is_err() {
|
||||
debug!("TLS path does not exist, console starting with HTTP");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
debug!("Found TLS directory for console, checking for certificates");
|
||||
|
||||
// Make sure to use a modern encryption suite
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
|
||||
// 1. Attempt to load all certificates in the directory (multi-certificate support, for SNI)
|
||||
if let Ok(cert_key_pairs) = rustfs_utils::load_all_certs_from_directory(tls_path) {
|
||||
if !cert_key_pairs.is_empty() {
|
||||
debug!(
|
||||
"Found {} certificates for console, creating SNI-aware multi-cert resolver",
|
||||
cert_key_pairs.len()
|
||||
);
|
||||
|
||||
// Create an SNI-enabled certificate resolver
|
||||
let resolver = rustfs_utils::create_multi_cert_resolver(cert_key_pairs)?;
|
||||
|
||||
// Configure the server to enable SNI support
|
||||
let mut server_config = ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_cert_resolver(Arc::new(resolver));
|
||||
|
||||
// Configure ALPN protocol priority
|
||||
server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec(), b"http/1.0".to_vec()];
|
||||
|
||||
// Log SNI requests
|
||||
if rustfs_utils::tls_key_log() {
|
||||
server_config.key_log = Arc::new(rustls::KeyLogFile::new());
|
||||
}
|
||||
|
||||
info!(target: "rustfs::console::tls", "Console TLS enabled with multi-certificate SNI support");
|
||||
return Ok(Some(RustlsConfig::from_config(Arc::new(server_config))));
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Revert to the traditional single-certificate mode
|
||||
let key_path = format!("{tls_path}/{RUSTFS_TLS_KEY}");
|
||||
let cert_path = format!("{tls_path}/{RUSTFS_TLS_CERT}");
|
||||
if tokio::try_join!(tokio::fs::metadata(&key_path), tokio::fs::metadata(&cert_path)).is_ok() {
|
||||
debug!("Found legacy single TLS certificate for console, starting with HTTPS");
|
||||
|
||||
return match RustlsConfig::from_pem_file(cert_path, key_path).await {
|
||||
Ok(config) => {
|
||||
info!(target: "rustfs::console::tls", "Console TLS enabled with single certificate");
|
||||
Ok(Some(config))
|
||||
}
|
||||
Err(e) => {
|
||||
error!(target: "rustfs::console::error", error = %e, "Failed to create TLS config for console");
|
||||
Err(std::io::Error::other(e))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
debug!("No valid TLS certificates found in the directory for console, starting with HTTP");
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Get console configuration from environment variables
|
||||
fn get_console_config_from_env() -> (bool, u32, u64, String) {
|
||||
let rate_limit_enable = std::env::var(rustfs_config::ENV_CONSOLE_RATE_LIMIT_ENABLE)
|
||||
.unwrap_or_else(|_| rustfs_config::DEFAULT_CONSOLE_RATE_LIMIT_ENABLE.to_string())
|
||||
.parse::<bool>()
|
||||
.unwrap_or(rustfs_config::DEFAULT_CONSOLE_RATE_LIMIT_ENABLE);
|
||||
|
||||
let rate_limit_rpm = std::env::var(rustfs_config::ENV_CONSOLE_RATE_LIMIT_RPM)
|
||||
.unwrap_or_else(|_| rustfs_config::DEFAULT_CONSOLE_RATE_LIMIT_RPM.to_string())
|
||||
.parse::<u32>()
|
||||
.unwrap_or(rustfs_config::DEFAULT_CONSOLE_RATE_LIMIT_RPM);
|
||||
|
||||
let auth_timeout = std::env::var(rustfs_config::ENV_CONSOLE_AUTH_TIMEOUT)
|
||||
.unwrap_or_else(|_| rustfs_config::DEFAULT_CONSOLE_AUTH_TIMEOUT.to_string())
|
||||
.parse::<u64>()
|
||||
.unwrap_or(rustfs_config::DEFAULT_CONSOLE_AUTH_TIMEOUT);
|
||||
let cors_allowed_origins = std::env::var(rustfs_config::ENV_CONSOLE_CORS_ALLOWED_ORIGINS)
|
||||
.unwrap_or_else(|_| rustfs_config::DEFAULT_CONSOLE_CORS_ALLOWED_ORIGINS.to_string())
|
||||
.parse::<String>()
|
||||
.unwrap_or(rustfs_config::DEFAULT_CONSOLE_CORS_ALLOWED_ORIGINS.to_string());
|
||||
|
||||
(rate_limit_enable, rate_limit_rpm, auth_timeout, cors_allowed_origins)
|
||||
}
|
||||
|
||||
/// Setup comprehensive middleware stack with tower-http features
|
||||
fn setup_console_middleware_stack(
|
||||
cors_layer: CorsLayer,
|
||||
rate_limit_enable: bool,
|
||||
rate_limit_rpm: u32,
|
||||
auth_timeout: u64,
|
||||
) -> Router {
|
||||
let mut app = Router::new()
|
||||
.route("/license", get(crate::admin::console::license_handler))
|
||||
.route("/config.json", get(crate::admin::console::config_handler))
|
||||
.route("/health", get(health_check))
|
||||
.nest(CONSOLE_PREFIX, Router::new().fallback_service(get(static_handler)))
|
||||
.fallback_service(get(static_handler));
|
||||
|
||||
// Add comprehensive middleware layers using tower-http features
|
||||
app = app
|
||||
.layer(CatchPanicLayer::new())
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.layer(middleware::from_fn(console_logging_middleware))
|
||||
.layer(cors_layer)
|
||||
// Add timeout layer - convert auth_timeout from seconds to Duration
|
||||
.layer(TimeoutLayer::new(Duration::from_secs(auth_timeout)))
|
||||
// Add request body limit (10MB for console uploads)
|
||||
.layer(RequestBodyLimitLayer::new(10 * 1024 * 1024));
|
||||
|
||||
// Add rate limiting if enabled
|
||||
if rate_limit_enable {
|
||||
info!("Console rate limiting enabled: {} requests per minute", rate_limit_rpm);
|
||||
// Note: tower-http doesn't provide a built-in rate limiter, but we have the foundation
|
||||
// For production, you would integrate with a rate limiting service like Redis
|
||||
// For now, we log that it's configured and ready for integration
|
||||
}
|
||||
|
||||
app
|
||||
}
|
||||
|
||||
/// Console health check handler with comprehensive health information
|
||||
async fn health_check() -> Json<serde_json::Value> {
|
||||
use rustfs_ecstore::new_object_layer_fn;
|
||||
|
||||
let mut health_status = "ok";
|
||||
let mut details = json!({});
|
||||
|
||||
// Check storage backend health
|
||||
if let Some(_store) = new_object_layer_fn() {
|
||||
details["storage"] = json!({"status": "connected"});
|
||||
} else {
|
||||
health_status = "degraded";
|
||||
details["storage"] = json!({"status": "disconnected"});
|
||||
}
|
||||
|
||||
// Check IAM system health
|
||||
match rustfs_iam::get() {
|
||||
Ok(_) => {
|
||||
details["iam"] = json!({"status": "connected"});
|
||||
}
|
||||
Err(_) => {
|
||||
health_status = "degraded";
|
||||
details["iam"] = json!({"status": "disconnected"});
|
||||
}
|
||||
}
|
||||
|
||||
Json(json!({
|
||||
"status": health_status,
|
||||
"service": "rustfs-console",
|
||||
"timestamp": chrono::Utc::now().to_rfc3339(),
|
||||
"version": env!("CARGO_PKG_VERSION"),
|
||||
"details": details,
|
||||
"uptime": std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
}))
|
||||
}
|
||||
|
||||
/// Parse CORS allowed origins from configuration
|
||||
pub fn parse_cors_origins(origins: Option<&String>) -> CorsLayer {
|
||||
let cors_layer = CorsLayer::new()
|
||||
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::OPTIONS])
|
||||
.allow_headers(Any);
|
||||
|
||||
match origins {
|
||||
Some(origins_str) if origins_str == "*" => cors_layer.allow_origin(Any).expose_headers(Any),
|
||||
Some(origins_str) => {
|
||||
let origins: Vec<&str> = origins_str.split(',').map(|s| s.trim()).collect();
|
||||
if origins.is_empty() {
|
||||
warn!("Empty CORS origins provided, using permissive CORS");
|
||||
cors_layer.allow_origin(Any).expose_headers(Any)
|
||||
} else {
|
||||
// Parse origins with proper error handling
|
||||
let mut valid_origins = Vec::new();
|
||||
for origin in origins {
|
||||
match origin.parse::<HeaderValue>() {
|
||||
Ok(header_value) => {
|
||||
valid_origins.push(header_value);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Invalid CORS origin '{}': {}", origin, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if valid_origins.is_empty() {
|
||||
warn!("No valid CORS origins found, using permissive CORS");
|
||||
cors_layer.allow_origin(Any).expose_headers(Any)
|
||||
} else {
|
||||
info!("Console CORS origins configured: {:?}", valid_origins);
|
||||
cors_layer.allow_origin(AllowOrigin::list(valid_origins)).expose_headers(Any)
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
debug!("No CORS origins configured for console, using permissive CORS");
|
||||
cors_layer.allow_origin(Any)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the standalone console server with enhanced security and monitoring
|
||||
#[instrument(skip(opt, shutdown_rx))]
|
||||
pub async fn start_console_server(opt: &Opt, shutdown_rx: tokio::sync::broadcast::Receiver<()>) -> Result<()> {
|
||||
if !opt.console_enable {
|
||||
debug!("Console server is disabled");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let console_addr = parse_and_resolve_address(&opt.console_address)?;
|
||||
|
||||
// Get configuration from environment variables
|
||||
let (rate_limit_enable, rate_limit_rpm, auth_timeout, cors_allowed_origins) = get_console_config_from_env();
|
||||
|
||||
// Setup TLS configuration if certificates are available
|
||||
let tls_config = setup_console_tls_config(opt.tls_path.as_ref()).await?;
|
||||
let tls_enabled = tls_config.is_some();
|
||||
|
||||
info!(
|
||||
target: "rustfs::console::startup",
|
||||
address = %console_addr,
|
||||
tls_enabled = tls_enabled,
|
||||
rate_limit_enabled = rate_limit_enable,
|
||||
rate_limit_rpm = rate_limit_rpm,
|
||||
auth_timeout_seconds = auth_timeout,
|
||||
cors_allowed_origins = %cors_allowed_origins,
|
||||
"Starting console server"
|
||||
);
|
||||
|
||||
// String to Option<&String>
|
||||
let cors_allowed_origins = if cors_allowed_origins.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(&cors_allowed_origins)
|
||||
};
|
||||
|
||||
// Configure CORS based on settings
|
||||
let cors_layer = parse_cors_origins(cors_allowed_origins);
|
||||
|
||||
// Build console router with enhanced middleware stack using tower-http features
|
||||
let app = setup_console_middleware_stack(cors_layer, rate_limit_enable, rate_limit_rpm, auth_timeout);
|
||||
|
||||
let local_ip = rustfs_utils::get_local_ip().unwrap_or_else(|| "127.0.0.1".parse().unwrap());
|
||||
let protocol = if tls_enabled { "https" } else { "http" };
|
||||
|
||||
info!(
|
||||
target: "rustfs::console::startup",
|
||||
"Console WebUI available at: {}://{}:{}/rustfs/console/index.html",
|
||||
protocol, local_ip, console_addr.port()
|
||||
);
|
||||
info!(
|
||||
target: "rustfs::console::startup",
|
||||
"Console WebUI (localhost): {}://127.0.0.1:{}/rustfs/console/index.html",
|
||||
protocol, console_addr.port()
|
||||
);
|
||||
|
||||
println!(
|
||||
"Console WebUI available at: {}://{}:{}/rustfs/console/index.html",
|
||||
protocol,
|
||||
local_ip,
|
||||
console_addr.port()
|
||||
);
|
||||
println!(
|
||||
"Console WebUI (localhost): {}://127.0.0.1:{}/rustfs/console/index.html",
|
||||
protocol,
|
||||
console_addr.port()
|
||||
);
|
||||
|
||||
// Handle connections based on TLS availability using axum-server
|
||||
if let Some(tls_config) = tls_config {
|
||||
handle_tls_connections(console_addr, app, tls_config, shutdown_rx).await
|
||||
} else {
|
||||
handle_plain_connections(console_addr, app, shutdown_rx).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle TLS connections for console using axum-server with proper TLS support
|
||||
async fn handle_tls_connections(
|
||||
server_addr: SocketAddr,
|
||||
app: Router,
|
||||
tls_config: RustlsConfig,
|
||||
mut shutdown_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
) -> Result<()> {
|
||||
info!(target: "rustfs::console::tls", "Starting Console HTTPS server on {}", server_addr);
|
||||
|
||||
let handle = axum_server::Handle::new();
|
||||
let handle_clone = handle.clone();
|
||||
|
||||
// Spawn shutdown signal handler
|
||||
tokio::spawn(async move {
|
||||
let _ = shutdown_rx.recv().await;
|
||||
info!(target: "rustfs::console::shutdown", "Console TLS server shutdown signal received");
|
||||
handle_clone.graceful_shutdown(Some(Duration::from_secs(10)));
|
||||
});
|
||||
|
||||
// Start the HTTPS server using axum-server with RustlsConfig
|
||||
if let Err(e) = axum_server::bind_rustls(server_addr, tls_config)
|
||||
.handle(handle)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
{
|
||||
error!(target: "rustfs::console::error", error = %e, "Console TLS server error");
|
||||
return Err(std::io::Error::other(e));
|
||||
}
|
||||
|
||||
info!(target: "rustfs::console::shutdown", "Console TLS server stopped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle plain HTTP connections using axum-server
|
||||
async fn handle_plain_connections(
|
||||
server_addr: SocketAddr,
|
||||
app: Router,
|
||||
mut shutdown_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
) -> Result<()> {
|
||||
info!(target: "rustfs::console::startup", "Starting Console HTTP server on {}", server_addr);
|
||||
|
||||
let handle = axum_server::Handle::new();
|
||||
let handle_clone = handle.clone();
|
||||
|
||||
// Spawn shutdown signal handler
|
||||
tokio::spawn(async move {
|
||||
let _ = shutdown_rx.recv().await;
|
||||
info!(target: "rustfs::console::shutdown", "Console server shutdown signal received");
|
||||
handle_clone.graceful_shutdown(Some(Duration::from_secs(10)));
|
||||
});
|
||||
|
||||
// Start the HTTP server using axum-server
|
||||
if let Err(e) = axum_server::bind(server_addr)
|
||||
.handle(handle)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
{
|
||||
error!(target: "rustfs::console::error", error = %e, "Console server error");
|
||||
return Err(std::io::Error::other(e));
|
||||
}
|
||||
|
||||
info!(target: "rustfs::console::shutdown", "Console server stopped");
|
||||
Ok(())
|
||||
}
|
||||
+41
-26
@@ -144,30 +144,46 @@ pub async fn start_http_server(
|
||||
|
||||
// Obtain the listener address
|
||||
let local_addr: SocketAddr = listener.local_addr()?;
|
||||
debug!("Listening on: {}", local_addr);
|
||||
let local_ip = match rustfs_utils::get_local_ip() {
|
||||
Some(ip) => {
|
||||
debug!("Obtained local IP address: {}", ip);
|
||||
ip
|
||||
}
|
||||
Some(ip) => ip,
|
||||
None => {
|
||||
warn!("Unable to obtain local IP address, using fallback IP: {}", local_addr.ip());
|
||||
local_addr.ip()
|
||||
}
|
||||
};
|
||||
|
||||
let tls_acceptor = setup_tls_acceptor(opt.tls_path.as_deref().unwrap_or_default()).await?;
|
||||
let tls_enabled = tls_acceptor.is_some();
|
||||
let protocol = if tls_enabled { "https" } else { "http" };
|
||||
// Detailed endpoint information (showing all API endpoints)
|
||||
let api_endpoints = format!("http://{local_ip}:{server_port}");
|
||||
let localhost_endpoint = format!("http://127.0.0.1:{server_port}");
|
||||
info!(" API: {} {}", api_endpoints, localhost_endpoint);
|
||||
println!(" API: {} {}", api_endpoints, localhost_endpoint);
|
||||
info!(" RootUser: {}", opt.access_key.clone());
|
||||
info!(" RootPass: {}", opt.secret_key.clone());
|
||||
if DEFAULT_ACCESS_KEY.eq(&opt.access_key) && DEFAULT_SECRET_KEY.eq(&opt.secret_key) {
|
||||
warn!(
|
||||
"Detected default credentials '{}:{}', we recommend that you change these values with 'RUSTFS_ACCESS_KEY' and 'RUSTFS_SECRET_KEY' environment variables",
|
||||
DEFAULT_ACCESS_KEY, DEFAULT_SECRET_KEY
|
||||
let api_endpoints = format!("{protocol}://{local_ip}:{server_port}");
|
||||
let localhost_endpoint = format!("{protocol}://127.0.0.1:{server_port}");
|
||||
|
||||
if opt.console_enable {
|
||||
admin::console::init_console_cfg(local_ip, server_port);
|
||||
|
||||
info!(
|
||||
target: "rustfs::console::startup",
|
||||
"Console WebUI available at: {protocol}://{local_ip}:{server_port}/rustfs/console/index.html"
|
||||
);
|
||||
info!(
|
||||
target: "rustfs::console::startup",
|
||||
"Console WebUI (localhost): {protocol}://127.0.0.1:{server_port}/rustfs/console/index.html",
|
||||
|
||||
);
|
||||
|
||||
println!("Console WebUI available at: {protocol}://{local_ip}:{server_port}/rustfs/console/index.html");
|
||||
println!("Console WebUI (localhost): {protocol}://127.0.0.1:{server_port}/rustfs/console/index.html",);
|
||||
} else {
|
||||
info!(" API: {} {}", api_endpoints, localhost_endpoint);
|
||||
println!(" API: {api_endpoints} {localhost_endpoint}");
|
||||
if DEFAULT_ACCESS_KEY.eq(&opt.access_key) && DEFAULT_SECRET_KEY.eq(&opt.secret_key) {
|
||||
warn!(
|
||||
"Detected default credentials '{}:{}', we recommend that you change these values with 'RUSTFS_ACCESS_KEY' and 'RUSTFS_SECRET_KEY' environment variables",
|
||||
DEFAULT_ACCESS_KEY, DEFAULT_SECRET_KEY
|
||||
);
|
||||
}
|
||||
info!(target: "rustfs::main::startup","For more information, visit https://rustfs.com/docs/");
|
||||
info!(target: "rustfs::main::startup", "To enable the console, restart the server with --console-enable and a valid --console-address.");
|
||||
}
|
||||
|
||||
// Setup S3 service
|
||||
@@ -178,13 +194,10 @@ pub async fn start_http_server(
|
||||
|
||||
let access_key = opt.access_key.clone();
|
||||
let secret_key = opt.secret_key.clone();
|
||||
debug!("authentication is enabled {}, {}", &access_key, &secret_key);
|
||||
|
||||
b.set_auth(IAMAuth::new(access_key, secret_key));
|
||||
b.set_access(store.clone());
|
||||
// When console runs on separate port, disable console routes on main endpoint
|
||||
let console_on_endpoint = opt.console_enable; // Console will run separately
|
||||
b.set_route(admin::make_admin_route(console_on_endpoint)?);
|
||||
b.set_route(admin::make_admin_route(opt.console_enable)?);
|
||||
|
||||
if !opt.server_domains.is_empty() {
|
||||
MultiDomain::new(&opt.server_domains).map_err(Error::other)?; // validate domains
|
||||
@@ -208,7 +221,6 @@ pub async fn start_http_server(
|
||||
};
|
||||
|
||||
// Server will be created per connection - this ensures isolation
|
||||
|
||||
tokio::spawn(async move {
|
||||
// Record the PID-related metrics of the current process
|
||||
let meter = opentelemetry::global::meter("system");
|
||||
@@ -223,7 +235,6 @@ pub async fn start_http_server(
|
||||
}
|
||||
});
|
||||
|
||||
let tls_acceptor = setup_tls_acceptor(opt.tls_path.as_deref().unwrap_or_default()).await?;
|
||||
// Create shutdown channel
|
||||
let (shutdown_tx, mut shutdown_rx) = tokio::sync::broadcast::channel(1);
|
||||
let shutdown_tx_clone = shutdown_tx.clone();
|
||||
@@ -235,6 +246,8 @@ pub async fn start_http_server(
|
||||
} else {
|
||||
Some(cors_allowed_origins)
|
||||
};
|
||||
|
||||
let is_console = opt.console_enable;
|
||||
tokio::spawn(async move {
|
||||
// Create CORS layer inside the server loop closure
|
||||
let cors_layer = parse_cors_origins(cors_allowed_origins.as_ref());
|
||||
@@ -332,6 +345,7 @@ pub async fn start_http_server(
|
||||
s3_service.clone(),
|
||||
graceful.clone(),
|
||||
cors_layer.clone(),
|
||||
is_console,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -440,6 +454,7 @@ fn process_connection(
|
||||
s3_service: S3Service,
|
||||
graceful: Arc<GracefulShutdown>,
|
||||
cors_layer: CorsLayer,
|
||||
is_console: bool,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
// Build services inside each connected task to avoid passing complex service types across tasks,
|
||||
@@ -492,8 +507,9 @@ fn process_connection(
|
||||
}),
|
||||
)
|
||||
.layer(cors_layer)
|
||||
.layer(RedirectLayer)
|
||||
.option_layer(if is_console { Some(RedirectLayer) } else { None })
|
||||
.service(service);
|
||||
|
||||
let hybrid_service = TowerToHyperService::new(hybrid_service);
|
||||
|
||||
// Decide whether to handle HTTPS or HTTP connections based on the existence of TLS Acceptor
|
||||
@@ -675,9 +691,8 @@ pub(crate) fn get_tokio_runtime_builder() -> tokio::runtime::Builder {
|
||||
builder.thread_name(thread_name.clone());
|
||||
println!(
|
||||
"Starting Tokio runtime with configured parameters:\n\
|
||||
worker_threads: {}, max_blocking_threads: {}, thread_stack_size: {}, thread_keep_alive: {}, \
|
||||
global_queue_interval: {}, thread_name: {}",
|
||||
worker_threads, max_blocking_threads, thread_stack_size, thread_keep_alive, global_queue_interval, thread_name
|
||||
worker_threads: {worker_threads}, max_blocking_threads: {max_blocking_threads}, thread_stack_size: {thread_stack_size}, thread_keep_alive: {thread_keep_alive}, \
|
||||
global_queue_interval: {global_queue_interval}, thread_name: {thread_name}"
|
||||
);
|
||||
builder
|
||||
}
|
||||
|
||||
@@ -13,18 +13,14 @@
|
||||
// limitations under the License.
|
||||
|
||||
mod audit;
|
||||
mod console;
|
||||
mod http;
|
||||
mod hybrid;
|
||||
mod layer;
|
||||
mod service_state;
|
||||
|
||||
#[cfg(test)]
|
||||
mod console_test;
|
||||
mod event;
|
||||
|
||||
pub(crate) use audit::{start_audit_system, stop_audit_system};
|
||||
pub(crate) use console::start_console_server;
|
||||
pub(crate) use event::{init_event_notifier, shutdown_event_notifier};
|
||||
pub(crate) use http::{get_tokio_runtime_builder, print_tokio_thread_enable, start_http_server};
|
||||
pub(crate) use service_state::SHUTDOWN_TIMEOUT;
|
||||
|
||||
@@ -32,6 +32,7 @@ pub(crate) struct ReqInfo {
|
||||
pub bucket: Option<String>,
|
||||
pub object: Option<String>,
|
||||
pub version_id: Option<String>,
|
||||
pub region: Option<String>,
|
||||
}
|
||||
|
||||
/// Authorizes the request based on the action and credentials.
|
||||
@@ -48,7 +49,7 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
|
||||
|
||||
let default_claims = HashMap::new();
|
||||
let claims = cred.claims.as_ref().unwrap_or(&default_claims);
|
||||
let conditions = get_condition_values(&req.headers, cred);
|
||||
let conditions = get_condition_values(&req.headers, cred, req_info.version_id.as_deref(), None);
|
||||
|
||||
if action == Action::S3Action(S3Action::DeleteObjectAction)
|
||||
&& req_info.version_id.is_some()
|
||||
@@ -104,7 +105,12 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
let conditions = get_condition_values(&req.headers, &auth::Credentials::default());
|
||||
let conditions = get_condition_values(
|
||||
&req.headers,
|
||||
&auth::Credentials::default(),
|
||||
req_info.version_id.as_deref(),
|
||||
req.region.as_deref(),
|
||||
);
|
||||
|
||||
if action != Action::S3Action(S3Action::ListAllMyBucketsAction) {
|
||||
if PolicySys::is_allowed(&BucketPolicyArgs {
|
||||
@@ -181,6 +187,7 @@ impl S3Access for FS {
|
||||
let req_info = ReqInfo {
|
||||
cred,
|
||||
is_owner,
|
||||
region: rustfs_ecstore::global::get_global_region(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
||||
+390
-40
@@ -14,6 +14,7 @@
|
||||
|
||||
use crate::auth::get_condition_values;
|
||||
use crate::error::ApiError;
|
||||
use crate::storage::options::{filter_object_metadata, get_content_sha256};
|
||||
use crate::storage::{
|
||||
access::{ReqInfo, authorize_request},
|
||||
options::{
|
||||
@@ -41,8 +42,8 @@ use rustfs_ecstore::{
|
||||
object_lock::objectlock_sys::BucketObjectLockSys,
|
||||
policy_sys::PolicySys,
|
||||
replication::{
|
||||
DeletedObjectReplicationInfo, REPLICATE_INCOMING_DELETE, ReplicationConfigurationExt, check_replicate_delete,
|
||||
get_must_replicate_options, must_replicate, schedule_replication, schedule_replication_delete,
|
||||
DeletedObjectReplicationInfo, ReplicationConfigurationExt, check_replicate_delete, get_must_replicate_options,
|
||||
must_replicate, schedule_replication, schedule_replication_delete,
|
||||
},
|
||||
tagging::{decode_tags, encode_tags},
|
||||
utils::serialize,
|
||||
@@ -71,6 +72,7 @@ use rustfs_ecstore::{
|
||||
// RESERVED_METADATA_PREFIX,
|
||||
},
|
||||
};
|
||||
use rustfs_filemeta::REPLICATE_INCOMING_DELETE;
|
||||
use rustfs_filemeta::{ReplicationStatusType, ReplicationType, VersionPurgeStatusType, fileinfo::ObjectPartInfo};
|
||||
use rustfs_kms::{
|
||||
DataKey,
|
||||
@@ -95,6 +97,7 @@ use rustfs_targets::{
|
||||
EventName,
|
||||
arn::{TargetID, TargetIDError},
|
||||
};
|
||||
use rustfs_utils::http::{AMZ_CHECKSUM_MODE, AMZ_CHECKSUM_TYPE};
|
||||
use rustfs_utils::{
|
||||
CompressionAlgorithm,
|
||||
http::{
|
||||
@@ -341,19 +344,36 @@ impl FS {
|
||||
}
|
||||
|
||||
async fn put_object_extract(&self, req: S3Request<PutObjectInput>) -> S3Result<S3Response<PutObjectOutput>> {
|
||||
let input = req.input;
|
||||
|
||||
let PutObjectInput {
|
||||
body,
|
||||
bucket,
|
||||
key,
|
||||
version_id,
|
||||
content_length,
|
||||
content_md5,
|
||||
..
|
||||
} = req.input;
|
||||
} = input;
|
||||
|
||||
let event_version_id = version_id;
|
||||
let Some(body) = body else { return Err(s3_error!(IncompleteBody)) };
|
||||
|
||||
let body = StreamReader::new(body.map(|f| f.map_err(|e| std::io::Error::other(e.to_string()))));
|
||||
|
||||
// let etag_stream = EtagReader::new(body);
|
||||
let size = match content_length {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
if let Some(val) = req.headers.get(AMZ_DECODED_CONTENT_LENGTH) {
|
||||
match atoi::atoi::<i64>(val.as_bytes()) {
|
||||
Some(x) => x,
|
||||
None => return Err(s3_error!(UnexpectedContent)),
|
||||
}
|
||||
} else {
|
||||
return Err(s3_error!(UnexpectedContent));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let Some(ext) = Path::new(&key).extension().and_then(|s| s.to_str()) else {
|
||||
return Err(s3_error!(InvalidArgument, "key extension not found"));
|
||||
@@ -361,8 +381,28 @@ impl FS {
|
||||
|
||||
let ext = ext.to_owned();
|
||||
|
||||
let md5hex = if let Some(base64_md5) = content_md5 {
|
||||
let md5 = base64_simd::STANDARD
|
||||
.decode_to_vec(base64_md5.as_bytes())
|
||||
.map_err(|e| ApiError::from(StorageError::other(format!("Invalid content MD5: {e}"))))?;
|
||||
Some(hex_simd::encode_to_string(&md5, hex_simd::AsciiCase::Lower))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let sha256hex = get_content_sha256(&req.headers);
|
||||
let actual_size = size;
|
||||
|
||||
let reader: Box<dyn Reader> = Box::new(WarpReader::new(body));
|
||||
|
||||
let mut hreader = HashReader::new(reader, size, actual_size, md5hex, sha256hex, false).map_err(ApiError::from)?;
|
||||
|
||||
if let Err(err) = hreader.add_checksum_from_s3s(&req.headers, req.trailing_headers.clone(), false) {
|
||||
return Err(ApiError::from(StorageError::other(format!("add_checksum error={err:?}"))).into());
|
||||
}
|
||||
|
||||
// TODO: support zip
|
||||
let decoder = CompressionFormat::from_extension(&ext).get_decoder(body).map_err(|e| {
|
||||
let decoder = CompressionFormat::from_extension(&ext).get_decoder(hreader).map_err(|e| {
|
||||
error!("get_decoder err {:?}", e);
|
||||
s3_error!(InvalidArgument, "get_decoder err")
|
||||
})?;
|
||||
@@ -423,13 +463,13 @@ impl FS {
|
||||
);
|
||||
metadata.insert(format!("{RESERVED_METADATA_PREFIX_LOWER}actual-size",), size.to_string());
|
||||
|
||||
let hrd = HashReader::new(reader, size, actual_size, None, false).map_err(ApiError::from)?;
|
||||
let hrd = HashReader::new(reader, size, actual_size, None, None, false).map_err(ApiError::from)?;
|
||||
|
||||
reader = Box::new(CompressReader::new(hrd, CompressionAlgorithm::default()));
|
||||
size = -1;
|
||||
}
|
||||
|
||||
let hrd = HashReader::new(reader, size, actual_size, None, false).map_err(ApiError::from)?;
|
||||
let hrd = HashReader::new(reader, size, actual_size, None, None, false).map_err(ApiError::from)?;
|
||||
let mut reader = PutObjReader::new(hrd);
|
||||
|
||||
let _obj_info = store
|
||||
@@ -479,9 +519,51 @@ impl FS {
|
||||
// Err(e) => error!("Decompression failed: {}", e),
|
||||
// }
|
||||
|
||||
let mut checksum_crc32 = input.checksum_crc32;
|
||||
let mut checksum_crc32c = input.checksum_crc32c;
|
||||
let mut checksum_sha1 = input.checksum_sha1;
|
||||
let mut checksum_sha256 = input.checksum_sha256;
|
||||
let mut checksum_crc64nvme = input.checksum_crc64nvme;
|
||||
|
||||
if let Some(alg) = &input.checksum_algorithm {
|
||||
if let Some(Some(checksum_str)) = req.trailing_headers.as_ref().map(|trailer| {
|
||||
let key = match alg.as_str() {
|
||||
ChecksumAlgorithm::CRC32 => rustfs_rio::ChecksumType::CRC32.key(),
|
||||
ChecksumAlgorithm::CRC32C => rustfs_rio::ChecksumType::CRC32C.key(),
|
||||
ChecksumAlgorithm::SHA1 => rustfs_rio::ChecksumType::SHA1.key(),
|
||||
ChecksumAlgorithm::SHA256 => rustfs_rio::ChecksumType::SHA256.key(),
|
||||
ChecksumAlgorithm::CRC64NVME => rustfs_rio::ChecksumType::CRC64_NVME.key(),
|
||||
_ => return None,
|
||||
};
|
||||
trailer.read(|headers| {
|
||||
headers
|
||||
.get(key.unwrap_or_default())
|
||||
.and_then(|value| value.to_str().ok().map(|s| s.to_string()))
|
||||
})
|
||||
}) {
|
||||
match alg.as_str() {
|
||||
ChecksumAlgorithm::CRC32 => checksum_crc32 = checksum_str,
|
||||
ChecksumAlgorithm::CRC32C => checksum_crc32c = checksum_str,
|
||||
ChecksumAlgorithm::SHA1 => checksum_sha1 = checksum_str,
|
||||
ChecksumAlgorithm::SHA256 => checksum_sha256 = checksum_str,
|
||||
ChecksumAlgorithm::CRC64NVME => checksum_crc64nvme = checksum_str,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
warn!(
|
||||
"put object extract checksum_crc32={checksum_crc32:?}, checksum_crc32c={checksum_crc32c:?}, checksum_sha1={checksum_sha1:?}, checksum_sha256={checksum_sha256:?}, checksum_crc64nvme={checksum_crc64nvme:?}",
|
||||
);
|
||||
|
||||
// TODO: etag
|
||||
let output = PutObjectOutput {
|
||||
// e_tag: Some(etag_stream.etag().await),
|
||||
// e_tag: hreader.try_resolve_etag().map(|v| ETag::Strong(v)),
|
||||
checksum_crc32,
|
||||
checksum_crc32c,
|
||||
checksum_sha1,
|
||||
checksum_sha256,
|
||||
checksum_crc64nvme,
|
||||
..Default::default()
|
||||
};
|
||||
Ok(S3Response::new(output))
|
||||
@@ -682,7 +764,7 @@ impl S3 for FS {
|
||||
.remove(&format!("{RESERVED_METADATA_PREFIX_LOWER}compression-size"));
|
||||
}
|
||||
|
||||
let mut reader = HashReader::new(reader, length, actual_size, None, false).map_err(ApiError::from)?;
|
||||
let mut reader = HashReader::new(reader, length, actual_size, None, None, false).map_err(ApiError::from)?;
|
||||
|
||||
if let Some(ref sse_alg) = effective_sse {
|
||||
if is_managed_sse(sse_alg) {
|
||||
@@ -702,7 +784,7 @@ impl S3 for FS {
|
||||
effective_kms_key_id = Some(kms_key_used.clone());
|
||||
|
||||
let encrypt_reader = EncryptReader::new(reader, key_bytes, nonce);
|
||||
reader = HashReader::new(Box::new(encrypt_reader), -1, actual_size, None, false).map_err(ApiError::from)?;
|
||||
reader = HashReader::new(Box::new(encrypt_reader), -1, actual_size, None, None, false).map_err(ApiError::from)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1471,7 +1553,7 @@ impl S3 for FS {
|
||||
|
||||
let mut content_length = info.size;
|
||||
|
||||
let content_range = if let Some(rs) = rs {
|
||||
let content_range = if let Some(rs) = &rs {
|
||||
let total_size = info.get_actual_size().map_err(ApiError::from)?;
|
||||
let (start, length) = rs.get_offset_length(total_size).map_err(ApiError::from)?;
|
||||
content_length = length;
|
||||
@@ -1654,6 +1736,42 @@ impl S3 for FS {
|
||||
.cloned();
|
||||
let ssekms_key_id = info.user_defined.get("x-amz-server-side-encryption-aws-kms-key-id").cloned();
|
||||
|
||||
let mut checksum_crc32 = None;
|
||||
let mut checksum_crc32c = None;
|
||||
let mut checksum_sha1 = None;
|
||||
let mut checksum_sha256 = None;
|
||||
let mut checksum_crc64nvme = None;
|
||||
let mut checksum_type = None;
|
||||
|
||||
// checksum
|
||||
if let Some(checksum_mode) = req.headers.get(AMZ_CHECKSUM_MODE)
|
||||
&& checksum_mode.to_str().unwrap_or_default() == "ENABLED"
|
||||
&& rs.is_none()
|
||||
{
|
||||
let (checksums, _is_multipart) =
|
||||
info.decrypt_checksums(opts.part_number.unwrap_or(0), &req.headers)
|
||||
.map_err(|e| {
|
||||
error!("decrypt_checksums error: {}", e);
|
||||
ApiError::from(e)
|
||||
})?;
|
||||
|
||||
for (key, checksum) in checksums {
|
||||
if key == AMZ_CHECKSUM_TYPE {
|
||||
checksum_type = Some(ChecksumType::from(checksum));
|
||||
continue;
|
||||
}
|
||||
|
||||
match rustfs_rio::ChecksumType::from_string(key.as_str()) {
|
||||
rustfs_rio::ChecksumType::CRC32 => checksum_crc32 = Some(checksum),
|
||||
rustfs_rio::ChecksumType::CRC32C => checksum_crc32c = Some(checksum),
|
||||
rustfs_rio::ChecksumType::SHA1 => checksum_sha1 = Some(checksum),
|
||||
rustfs_rio::ChecksumType::SHA256 => checksum_sha256 = Some(checksum),
|
||||
rustfs_rio::ChecksumType::CRC64_NVME => checksum_crc64nvme = Some(checksum),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let output = GetObjectOutput {
|
||||
body,
|
||||
content_length: Some(response_content_length),
|
||||
@@ -1662,11 +1780,17 @@ impl S3 for FS {
|
||||
accept_ranges: Some("bytes".to_string()),
|
||||
content_range,
|
||||
e_tag: info.etag.map(|etag| to_s3s_etag(&etag)),
|
||||
metadata: Some(info.user_defined),
|
||||
metadata: filter_object_metadata(&info.user_defined),
|
||||
server_side_encryption,
|
||||
sse_customer_algorithm,
|
||||
sse_customer_key_md5,
|
||||
ssekms_key_id,
|
||||
checksum_crc32,
|
||||
checksum_crc32c,
|
||||
checksum_sha1,
|
||||
checksum_sha256,
|
||||
checksum_crc64nvme,
|
||||
checksum_type,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -1757,7 +1881,6 @@ impl S3 for FS {
|
||||
|
||||
let info = store.get_object_info(&bucket, &key, &opts).await.map_err(ApiError::from)?;
|
||||
|
||||
// warn!("head_object info {:?}", &info);
|
||||
let event_info = info.clone();
|
||||
let content_type = {
|
||||
if let Some(content_type) = &info.content_type {
|
||||
@@ -1777,7 +1900,10 @@ impl S3 for FS {
|
||||
|
||||
// TODO: range download
|
||||
|
||||
let content_length = info.get_actual_size().map_err(ApiError::from)?;
|
||||
let content_length = info.get_actual_size().map_err(|e| {
|
||||
error!("get_actual_size error: {}", e);
|
||||
ApiError::from(e)
|
||||
})?;
|
||||
|
||||
let metadata_map = info.user_defined.clone();
|
||||
let server_side_encryption = metadata_map
|
||||
@@ -1789,19 +1915,57 @@ impl S3 for FS {
|
||||
let sse_customer_key_md5 = metadata_map.get("x-amz-server-side-encryption-customer-key-md5").cloned();
|
||||
let ssekms_key_id = metadata_map.get("x-amz-server-side-encryption-aws-kms-key-id").cloned();
|
||||
|
||||
let metadata = metadata_map;
|
||||
let mut checksum_crc32 = None;
|
||||
let mut checksum_crc32c = None;
|
||||
let mut checksum_sha1 = None;
|
||||
let mut checksum_sha256 = None;
|
||||
let mut checksum_crc64nvme = None;
|
||||
let mut checksum_type = None;
|
||||
|
||||
// checksum
|
||||
if let Some(checksum_mode) = req.headers.get(AMZ_CHECKSUM_MODE)
|
||||
&& checksum_mode.to_str().unwrap_or_default() == "ENABLED"
|
||||
&& rs.is_none()
|
||||
{
|
||||
let (checksums, _is_multipart) = info
|
||||
.decrypt_checksums(opts.part_number.unwrap_or(0), &req.headers)
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
warn!("get object metadata checksums: {:?}", checksums);
|
||||
for (key, checksum) in checksums {
|
||||
if key == AMZ_CHECKSUM_TYPE {
|
||||
checksum_type = Some(ChecksumType::from(checksum));
|
||||
continue;
|
||||
}
|
||||
|
||||
match rustfs_rio::ChecksumType::from_string(key.as_str()) {
|
||||
rustfs_rio::ChecksumType::CRC32 => checksum_crc32 = Some(checksum),
|
||||
rustfs_rio::ChecksumType::CRC32C => checksum_crc32c = Some(checksum),
|
||||
rustfs_rio::ChecksumType::SHA1 => checksum_sha1 = Some(checksum),
|
||||
rustfs_rio::ChecksumType::SHA256 => checksum_sha256 = Some(checksum),
|
||||
rustfs_rio::ChecksumType::CRC64_NVME => checksum_crc64nvme = Some(checksum),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let output = HeadObjectOutput {
|
||||
content_length: Some(content_length),
|
||||
content_type,
|
||||
last_modified,
|
||||
e_tag: info.etag.map(|etag| to_s3s_etag(&etag)),
|
||||
metadata: Some(metadata),
|
||||
metadata: filter_object_metadata(&metadata_map),
|
||||
version_id: info.version_id.map(|v| v.to_string()),
|
||||
server_side_encryption,
|
||||
sse_customer_algorithm,
|
||||
sse_customer_key_md5,
|
||||
ssekms_key_id,
|
||||
checksum_crc32,
|
||||
checksum_crc32c,
|
||||
checksum_sha1,
|
||||
checksum_sha256,
|
||||
checksum_crc64nvme,
|
||||
checksum_type,
|
||||
// metadata: object_metadata,
|
||||
..Default::default()
|
||||
};
|
||||
@@ -2105,6 +2269,7 @@ impl S3 for FS {
|
||||
sse_customer_key,
|
||||
sse_customer_key_md5,
|
||||
ssekms_key_id,
|
||||
content_md5,
|
||||
..
|
||||
} = input;
|
||||
|
||||
@@ -2171,7 +2336,7 @@ impl S3 for FS {
|
||||
extract_metadata_from_mime_with_object_name(&req.headers, &mut metadata, Some(&key));
|
||||
|
||||
if let Some(tags) = tagging {
|
||||
metadata.insert(AMZ_OBJECT_TAGGING.to_owned(), tags);
|
||||
metadata.insert(AMZ_OBJECT_TAGGING.to_owned(), tags.to_string());
|
||||
}
|
||||
|
||||
// TDD: Store effective SSE information in metadata for GET responses
|
||||
@@ -2192,10 +2357,25 @@ impl S3 for FS {
|
||||
metadata.insert("x-amz-server-side-encryption-aws-kms-key-id".to_string(), kms_key_id.clone());
|
||||
}
|
||||
|
||||
let mut opts: ObjectOptions = put_opts(&bucket, &key, version_id.clone(), &req.headers, metadata.clone())
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
let mut reader: Box<dyn Reader> = Box::new(WarpReader::new(body));
|
||||
|
||||
let actual_size = size;
|
||||
|
||||
let mut md5hex = if let Some(base64_md5) = content_md5 {
|
||||
let md5 = base64_simd::STANDARD
|
||||
.decode_to_vec(base64_md5.as_bytes())
|
||||
.map_err(|e| ApiError::from(StorageError::other(format!("Invalid content MD5: {e}"))))?;
|
||||
Some(hex_simd::encode_to_string(&md5, hex_simd::AsciiCase::Lower))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut sha256hex = get_content_sha256(&req.headers);
|
||||
|
||||
if is_compressible(&req.headers, &key) && size > MIN_COMPRESSIBLE_SIZE as i64 {
|
||||
metadata.insert(
|
||||
format!("{RESERVED_METADATA_PREFIX_LOWER}compression"),
|
||||
@@ -2203,14 +2383,29 @@ impl S3 for FS {
|
||||
);
|
||||
metadata.insert(format!("{RESERVED_METADATA_PREFIX_LOWER}actual-size",), size.to_string());
|
||||
|
||||
let hrd = HashReader::new(reader, size as i64, size as i64, None, false).map_err(ApiError::from)?;
|
||||
let mut hrd = HashReader::new(reader, size as i64, size as i64, md5hex, sha256hex, false).map_err(ApiError::from)?;
|
||||
|
||||
if let Err(err) = hrd.add_checksum_from_s3s(&req.headers, req.trailing_headers.clone(), false) {
|
||||
return Err(ApiError::from(StorageError::other(format!("add_checksum error={err:?}"))).into());
|
||||
}
|
||||
|
||||
opts.want_checksum = hrd.checksum();
|
||||
|
||||
reader = Box::new(CompressReader::new(hrd, CompressionAlgorithm::default()));
|
||||
size = -1;
|
||||
md5hex = None;
|
||||
sha256hex = None;
|
||||
}
|
||||
|
||||
// TODO: md5 check
|
||||
let mut reader = HashReader::new(reader, size, actual_size, None, false).map_err(ApiError::from)?;
|
||||
let mut reader = HashReader::new(reader, size, actual_size, md5hex, sha256hex, false).map_err(ApiError::from)?;
|
||||
|
||||
if size >= 0 {
|
||||
if let Err(err) = reader.add_checksum_from_s3s(&req.headers, req.trailing_headers.clone(), false) {
|
||||
return Err(ApiError::from(StorageError::other(format!("add_checksum error={err:?}"))).into());
|
||||
}
|
||||
|
||||
opts.want_checksum = reader.checksum();
|
||||
}
|
||||
|
||||
// Apply SSE-C encryption if customer provided key
|
||||
if let (Some(_), Some(sse_key), Some(sse_key_md5_provided)) =
|
||||
@@ -2251,7 +2446,7 @@ impl S3 for FS {
|
||||
|
||||
// Apply encryption
|
||||
let encrypt_reader = EncryptReader::new(reader, key_array, nonce);
|
||||
reader = HashReader::new(Box::new(encrypt_reader), -1, actual_size, None, false).map_err(ApiError::from)?;
|
||||
reader = HashReader::new(Box::new(encrypt_reader), -1, actual_size, None, None, false).map_err(ApiError::from)?;
|
||||
}
|
||||
|
||||
// Apply managed SSE (SSE-S3 or SSE-KMS) when requested
|
||||
@@ -2275,20 +2470,16 @@ impl S3 for FS {
|
||||
effective_kms_key_id = Some(kms_key_used.clone());
|
||||
|
||||
let encrypt_reader = EncryptReader::new(reader, key_bytes, nonce);
|
||||
reader = HashReader::new(Box::new(encrypt_reader), -1, actual_size, None, false).map_err(ApiError::from)?;
|
||||
reader =
|
||||
HashReader::new(Box::new(encrypt_reader), -1, actual_size, None, None, false).map_err(ApiError::from)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut reader = PutObjReader::new(reader);
|
||||
|
||||
let mt = metadata.clone();
|
||||
let mt2 = metadata.clone();
|
||||
|
||||
let mut opts: ObjectOptions = put_opts(&bucket, &key, version_id, &req.headers, mt)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
let repoptions =
|
||||
get_must_replicate_options(&mt2, "".to_string(), ReplicationStatusType::Empty, ReplicationType::Object, opts.clone());
|
||||
|
||||
@@ -2319,18 +2510,56 @@ impl S3 for FS {
|
||||
schedule_replication(obj_info, store, dsc, ReplicationType::Object).await;
|
||||
}
|
||||
|
||||
let mut checksum_crc32 = input.checksum_crc32;
|
||||
let mut checksum_crc32c = input.checksum_crc32c;
|
||||
let mut checksum_sha1 = input.checksum_sha1;
|
||||
let mut checksum_sha256 = input.checksum_sha256;
|
||||
let mut checksum_crc64nvme = input.checksum_crc64nvme;
|
||||
|
||||
if let Some(alg) = &input.checksum_algorithm {
|
||||
if let Some(Some(checksum_str)) = req.trailing_headers.as_ref().map(|trailer| {
|
||||
let key = match alg.as_str() {
|
||||
ChecksumAlgorithm::CRC32 => rustfs_rio::ChecksumType::CRC32.key(),
|
||||
ChecksumAlgorithm::CRC32C => rustfs_rio::ChecksumType::CRC32C.key(),
|
||||
ChecksumAlgorithm::SHA1 => rustfs_rio::ChecksumType::SHA1.key(),
|
||||
ChecksumAlgorithm::SHA256 => rustfs_rio::ChecksumType::SHA256.key(),
|
||||
ChecksumAlgorithm::CRC64NVME => rustfs_rio::ChecksumType::CRC64_NVME.key(),
|
||||
_ => return None,
|
||||
};
|
||||
trailer.read(|headers| {
|
||||
headers
|
||||
.get(key.unwrap_or_default())
|
||||
.and_then(|value| value.to_str().ok().map(|s| s.to_string()))
|
||||
})
|
||||
}) {
|
||||
match alg.as_str() {
|
||||
ChecksumAlgorithm::CRC32 => checksum_crc32 = checksum_str,
|
||||
ChecksumAlgorithm::CRC32C => checksum_crc32c = checksum_str,
|
||||
ChecksumAlgorithm::SHA1 => checksum_sha1 = checksum_str,
|
||||
ChecksumAlgorithm::SHA256 => checksum_sha256 = checksum_str,
|
||||
ChecksumAlgorithm::CRC64NVME => checksum_crc64nvme = checksum_str,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let output = PutObjectOutput {
|
||||
e_tag,
|
||||
server_side_encryption: effective_sse, // TDD: Return effective encryption config
|
||||
sse_customer_algorithm,
|
||||
sse_customer_key_md5,
|
||||
sse_customer_algorithm: sse_customer_algorithm.clone(),
|
||||
sse_customer_key_md5: sse_customer_key_md5.clone(),
|
||||
ssekms_key_id: effective_kms_key_id, // TDD: Return effective KMS key ID
|
||||
checksum_crc32,
|
||||
checksum_crc32c,
|
||||
checksum_sha1,
|
||||
checksum_sha256,
|
||||
checksum_crc64nvme,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let event_args = rustfs_notify::event::EventArgs {
|
||||
event_name: EventName::ObjectCreatedPut,
|
||||
bucket_name: bucket,
|
||||
bucket_name: bucket.clone(),
|
||||
object: event_info,
|
||||
req_params: rustfs_utils::extract_req_params_header(&req.headers),
|
||||
resp_elements: rustfs_utils::extract_resp_elements(&S3Response::new(output.clone())),
|
||||
@@ -2460,14 +2689,29 @@ impl S3 for FS {
|
||||
);
|
||||
}
|
||||
|
||||
let opts: ObjectOptions = put_opts(&bucket, &key, version_id, &req.headers, metadata)
|
||||
let mut opts: ObjectOptions = put_opts(&bucket, &key, version_id, &req.headers, metadata)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
let MultipartUploadResult { upload_id, .. } = store
|
||||
let checksum_type = rustfs_rio::ChecksumType::from_header(&req.headers);
|
||||
if checksum_type.is(rustfs_rio::ChecksumType::INVALID) {
|
||||
return Err(s3_error!(InvalidArgument, "Invalid checksum type"));
|
||||
} else if checksum_type.is_set() && !checksum_type.is(rustfs_rio::ChecksumType::TRAILING) {
|
||||
opts.want_checksum = Some(rustfs_rio::Checksum {
|
||||
checksum_type,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
let MultipartUploadResult {
|
||||
upload_id,
|
||||
checksum_algo,
|
||||
checksum_type,
|
||||
} = store
|
||||
.new_multipart_upload(&bucket, &key, &opts)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
let object_name = key.clone();
|
||||
let bucket_name = bucket.clone();
|
||||
let output = CreateMultipartUploadOutput {
|
||||
@@ -2477,6 +2721,8 @@ impl S3 for FS {
|
||||
server_side_encryption: effective_sse, // TDD: Return effective encryption config
|
||||
sse_customer_algorithm,
|
||||
ssekms_key_id: effective_kms_key_id, // TDD: Return effective KMS key ID
|
||||
checksum_algorithm: checksum_algo.map(ChecksumAlgorithm::from),
|
||||
checksum_type: checksum_type.map(ChecksumType::from),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -2509,6 +2755,7 @@ impl S3 for FS {
|
||||
|
||||
#[instrument(level = "debug", skip(self, req))]
|
||||
async fn upload_part(&self, req: S3Request<UploadPartInput>) -> S3Result<S3Response<UploadPartOutput>> {
|
||||
let input = req.input;
|
||||
let UploadPartInput {
|
||||
body,
|
||||
bucket,
|
||||
@@ -2521,7 +2768,7 @@ impl S3 for FS {
|
||||
sse_customer_key_md5: _sse_customer_key_md5,
|
||||
// content_md5,
|
||||
..
|
||||
} = req.input;
|
||||
} = input;
|
||||
|
||||
let part_id = part_number as usize;
|
||||
|
||||
@@ -2648,19 +2895,41 @@ impl S3 for FS {
|
||||
}
|
||||
*/
|
||||
|
||||
let mut md5hex = if let Some(base64_md5) = input.content_md5 {
|
||||
let md5 = base64_simd::STANDARD
|
||||
.decode_to_vec(base64_md5.as_bytes())
|
||||
.map_err(|e| ApiError::from(StorageError::other(format!("Invalid content MD5: {e}"))))?;
|
||||
Some(hex_simd::encode_to_string(&md5, hex_simd::AsciiCase::Lower))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut sha256hex = get_content_sha256(&req.headers);
|
||||
|
||||
if is_compressible {
|
||||
let hrd = HashReader::new(reader, size, actual_size, None, false).map_err(ApiError::from)?;
|
||||
let mut hrd = HashReader::new(reader, size, actual_size, md5hex, sha256hex, false).map_err(ApiError::from)?;
|
||||
|
||||
if let Err(err) = hrd.add_checksum_from_s3s(&req.headers, req.trailing_headers.clone(), false) {
|
||||
return Err(ApiError::from(StorageError::other(format!("add_checksum error={err:?}"))).into());
|
||||
}
|
||||
|
||||
let compress_reader = CompressReader::new(hrd, CompressionAlgorithm::default());
|
||||
reader = Box::new(compress_reader);
|
||||
size = -1;
|
||||
md5hex = None;
|
||||
sha256hex = None;
|
||||
}
|
||||
|
||||
let mut reader = HashReader::new(reader, size, actual_size, None, false).map_err(ApiError::from)?;
|
||||
let mut reader = HashReader::new(reader, size, actual_size, md5hex, sha256hex, false).map_err(ApiError::from)?;
|
||||
|
||||
if let Err(err) = reader.add_checksum_from_s3s(&req.headers, req.trailing_headers.clone(), size < 0) {
|
||||
return Err(ApiError::from(StorageError::other(format!("add_checksum error={err:?}"))).into());
|
||||
}
|
||||
|
||||
if let Some((key_bytes, base_nonce, _)) = decrypt_managed_encryption_key(&bucket, &key, &fi.user_defined).await? {
|
||||
let part_nonce = derive_part_nonce(base_nonce, part_id);
|
||||
let encrypt_reader = EncryptReader::new(reader, key_bytes, part_nonce);
|
||||
reader = HashReader::new(Box::new(encrypt_reader), -1, actual_size, None, false).map_err(ApiError::from)?;
|
||||
reader = HashReader::new(Box::new(encrypt_reader), -1, actual_size, None, None, false).map_err(ApiError::from)?;
|
||||
}
|
||||
|
||||
let mut reader = PutObjReader::new(reader);
|
||||
@@ -2670,7 +2939,45 @@ impl S3 for FS {
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
let mut checksum_crc32 = input.checksum_crc32;
|
||||
let mut checksum_crc32c = input.checksum_crc32c;
|
||||
let mut checksum_sha1 = input.checksum_sha1;
|
||||
let mut checksum_sha256 = input.checksum_sha256;
|
||||
let mut checksum_crc64nvme = input.checksum_crc64nvme;
|
||||
|
||||
if let Some(alg) = &input.checksum_algorithm {
|
||||
if let Some(Some(checksum_str)) = req.trailing_headers.as_ref().map(|trailer| {
|
||||
let key = match alg.as_str() {
|
||||
ChecksumAlgorithm::CRC32 => rustfs_rio::ChecksumType::CRC32.key(),
|
||||
ChecksumAlgorithm::CRC32C => rustfs_rio::ChecksumType::CRC32C.key(),
|
||||
ChecksumAlgorithm::SHA1 => rustfs_rio::ChecksumType::SHA1.key(),
|
||||
ChecksumAlgorithm::SHA256 => rustfs_rio::ChecksumType::SHA256.key(),
|
||||
ChecksumAlgorithm::CRC64NVME => rustfs_rio::ChecksumType::CRC64_NVME.key(),
|
||||
_ => return None,
|
||||
};
|
||||
trailer.read(|headers| {
|
||||
headers
|
||||
.get(key.unwrap_or_default())
|
||||
.and_then(|value| value.to_str().ok().map(|s| s.to_string()))
|
||||
})
|
||||
}) {
|
||||
match alg.as_str() {
|
||||
ChecksumAlgorithm::CRC32 => checksum_crc32 = checksum_str,
|
||||
ChecksumAlgorithm::CRC32C => checksum_crc32c = checksum_str,
|
||||
ChecksumAlgorithm::SHA1 => checksum_sha1 = checksum_str,
|
||||
ChecksumAlgorithm::SHA256 => checksum_sha256 = checksum_str,
|
||||
ChecksumAlgorithm::CRC64NVME => checksum_crc64nvme = checksum_str,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let output = UploadPartOutput {
|
||||
checksum_crc32,
|
||||
checksum_crc32c,
|
||||
checksum_sha1,
|
||||
checksum_sha256,
|
||||
checksum_crc64nvme,
|
||||
e_tag: info.etag.map(|etag| to_s3s_etag(&etag)),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -2828,17 +3135,17 @@ impl S3 for FS {
|
||||
let mut size = length;
|
||||
|
||||
if is_compressible {
|
||||
let hrd = HashReader::new(reader, size, actual_size, None, false).map_err(ApiError::from)?;
|
||||
let hrd = HashReader::new(reader, size, actual_size, None, None, false).map_err(ApiError::from)?;
|
||||
reader = Box::new(CompressReader::new(hrd, CompressionAlgorithm::default()));
|
||||
size = -1;
|
||||
}
|
||||
|
||||
let mut reader = HashReader::new(reader, size, actual_size, None, false).map_err(ApiError::from)?;
|
||||
let mut reader = HashReader::new(reader, size, actual_size, None, None, false).map_err(ApiError::from)?;
|
||||
|
||||
if let Some((key_bytes, base_nonce, _)) = decrypt_managed_encryption_key(&bucket, &key, &mp_info.user_defined).await? {
|
||||
let part_nonce = derive_part_nonce(base_nonce, part_id);
|
||||
let encrypt_reader = EncryptReader::new(reader, key_bytes, part_nonce);
|
||||
reader = HashReader::new(Box::new(encrypt_reader), -1, actual_size, None, false).map_err(ApiError::from)?;
|
||||
reader = HashReader::new(Box::new(encrypt_reader), -1, actual_size, None, None, false).map_err(ApiError::from)?;
|
||||
}
|
||||
|
||||
let mut reader = PutObjReader::new(reader);
|
||||
@@ -3030,6 +3337,13 @@ impl S3 for FS {
|
||||
uploaded_parts.push(CompletePart::from(part));
|
||||
}
|
||||
|
||||
// is part number sorted?
|
||||
if !uploaded_parts.is_sorted_by_key(|p| p.part_num) {
|
||||
return Err(s3_error!(InvalidPart, "Part numbers must be sorted"));
|
||||
}
|
||||
|
||||
// TODO: check object lock
|
||||
|
||||
let Some(store) = new_object_layer_fn() else {
|
||||
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
|
||||
};
|
||||
@@ -3039,6 +3353,7 @@ impl S3 for FS {
|
||||
"TDD: Attempting to get multipart info for bucket={}, key={}, upload_id={}",
|
||||
bucket, key, upload_id
|
||||
);
|
||||
|
||||
let multipart_info = store
|
||||
.get_multipart_info(&bucket, &key, &upload_id, &ObjectOptions::default())
|
||||
.await
|
||||
@@ -3078,6 +3393,35 @@ impl S3 for FS {
|
||||
"TDD: Creating output with SSE: {:?}, KMS Key: {:?}",
|
||||
server_side_encryption, ssekms_key_id
|
||||
);
|
||||
|
||||
let mut checksum_crc32 = None;
|
||||
let mut checksum_crc32c = None;
|
||||
let mut checksum_sha1 = None;
|
||||
let mut checksum_sha256 = None;
|
||||
let mut checksum_crc64nvme = None;
|
||||
let mut checksum_type = None;
|
||||
|
||||
// checksum
|
||||
let (checksums, _is_multipart) = obj_info
|
||||
.decrypt_checksums(opts.part_number.unwrap_or(0), &req.headers)
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
for (key, checksum) in checksums {
|
||||
if key == AMZ_CHECKSUM_TYPE {
|
||||
checksum_type = Some(ChecksumType::from(checksum));
|
||||
continue;
|
||||
}
|
||||
|
||||
match rustfs_rio::ChecksumType::from_string(key.as_str()) {
|
||||
rustfs_rio::ChecksumType::CRC32 => checksum_crc32 = Some(checksum),
|
||||
rustfs_rio::ChecksumType::CRC32C => checksum_crc32c = Some(checksum),
|
||||
rustfs_rio::ChecksumType::SHA1 => checksum_sha1 = Some(checksum),
|
||||
rustfs_rio::ChecksumType::SHA256 => checksum_sha256 = Some(checksum),
|
||||
rustfs_rio::ChecksumType::CRC64_NVME => checksum_crc64nvme = Some(checksum),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
let output = CompleteMultipartUploadOutput {
|
||||
bucket: Some(bucket.clone()),
|
||||
key: Some(key.clone()),
|
||||
@@ -3085,6 +3429,12 @@ impl S3 for FS {
|
||||
location: Some("us-east-1".to_string()),
|
||||
server_side_encryption, // TDD: Return encryption info
|
||||
ssekms_key_id, // TDD: Return KMS key ID if present
|
||||
checksum_crc32,
|
||||
checksum_crc32c,
|
||||
checksum_sha1,
|
||||
checksum_sha256,
|
||||
checksum_crc64nvme,
|
||||
checksum_type,
|
||||
..Default::default()
|
||||
};
|
||||
info!(
|
||||
@@ -3407,7 +3757,7 @@ impl S3 for FS {
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
let conditions = get_condition_values(&req.headers, &auth::Credentials::default());
|
||||
let conditions = get_condition_values(&req.headers, &auth::Credentials::default(), None, None);
|
||||
|
||||
let read_only = PolicySys::is_allowed(&BucketPolicyArgs {
|
||||
bucket: &bucket,
|
||||
|
||||
@@ -16,9 +16,21 @@ use http::{HeaderMap, HeaderValue};
|
||||
use rustfs_ecstore::bucket::versioning_sys::BucketVersioningSys;
|
||||
use rustfs_ecstore::error::Result;
|
||||
use rustfs_ecstore::error::StorageError;
|
||||
use rustfs_utils::http::AMZ_META_UNENCRYPTED_CONTENT_LENGTH;
|
||||
use rustfs_utils::http::AMZ_META_UNENCRYPTED_CONTENT_MD5;
|
||||
use s3s::header::X_AMZ_OBJECT_LOCK_MODE;
|
||||
use s3s::header::X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE;
|
||||
|
||||
use crate::auth::UNSIGNED_PAYLOAD;
|
||||
use crate::auth::UNSIGNED_PAYLOAD_TRAILER;
|
||||
use rustfs_ecstore::store_api::{HTTPPreconditions, HTTPRangeSpec, ObjectOptions};
|
||||
use rustfs_policy::service_type::ServiceType;
|
||||
use rustfs_utils::hash::EMPTY_STRING_SHA256_HASH;
|
||||
use rustfs_utils::http::AMZ_CONTENT_SHA256;
|
||||
use rustfs_utils::http::RESERVED_METADATA_PREFIX_LOWER;
|
||||
use rustfs_utils::http::RUSTFS_BUCKET_REPLICATION_DELETE_MARKER;
|
||||
use rustfs_utils::http::RUSTFS_BUCKET_REPLICATION_REQUEST;
|
||||
use rustfs_utils::http::RUSTFS_BUCKET_REPLICATION_SSEC_CHECKSUM;
|
||||
use rustfs_utils::http::RUSTFS_BUCKET_SOURCE_VERSION_ID;
|
||||
use rustfs_utils::path::is_dir_object;
|
||||
use s3s::{S3Result, s3_error};
|
||||
@@ -27,6 +39,10 @@ use std::sync::LazyLock;
|
||||
use tracing::error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::auth::AuthType;
|
||||
use crate::auth::get_request_auth_type;
|
||||
use crate::auth::is_request_presigned_signature_v4;
|
||||
|
||||
/// Creates options for deleting an object in a bucket.
|
||||
pub async fn del_opts(
|
||||
bucket: &str,
|
||||
@@ -125,14 +141,14 @@ pub async fn get_opts(
|
||||
Ok(opts)
|
||||
}
|
||||
|
||||
fn fill_conditional_writes_opts_from_header(headers: &HeaderMap<HeaderValue>, opts: &mut ObjectOptions) -> Result<()> {
|
||||
fn fill_conditional_writes_opts_from_header(headers: &HeaderMap<HeaderValue>, opts: &mut ObjectOptions) -> std::io::Result<()> {
|
||||
if headers.contains_key("If-None-Match") || headers.contains_key("If-Match") {
|
||||
let mut preconditions = HTTPPreconditions::default();
|
||||
if let Some(if_none_match) = headers.get("If-None-Match") {
|
||||
preconditions.if_none_match = Some(
|
||||
if_none_match
|
||||
.to_str()
|
||||
.map_err(|_| StorageError::other("Invalid If-None-Match header"))?
|
||||
.map_err(|_| std::io::Error::other("Invalid If-None-Match header"))?
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
@@ -140,7 +156,7 @@ fn fill_conditional_writes_opts_from_header(headers: &HeaderMap<HeaderValue>, op
|
||||
preconditions.if_match = Some(
|
||||
if_match
|
||||
.to_str()
|
||||
.map_err(|_| StorageError::other("Invalid If-Match header"))?
|
||||
.map_err(|_| std::io::Error::other("Invalid If-Match header"))?
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
@@ -200,8 +216,32 @@ pub async fn put_opts(
|
||||
Ok(opts)
|
||||
}
|
||||
|
||||
pub fn get_complete_multipart_upload_opts(headers: &HeaderMap<HeaderValue>) -> Result<ObjectOptions> {
|
||||
let mut opts = ObjectOptions::default();
|
||||
pub fn get_complete_multipart_upload_opts(headers: &HeaderMap<HeaderValue>) -> std::io::Result<ObjectOptions> {
|
||||
let mut user_defined = HashMap::new();
|
||||
|
||||
let mut replication_request = false;
|
||||
if let Some(v) = headers.get(RUSTFS_BUCKET_REPLICATION_REQUEST) {
|
||||
user_defined.insert(
|
||||
format!("{RESERVED_METADATA_PREFIX_LOWER}Actual-Object-Size"),
|
||||
v.to_str().unwrap_or_default().to_owned(),
|
||||
);
|
||||
replication_request = true;
|
||||
}
|
||||
|
||||
if let Some(v) = headers.get(RUSTFS_BUCKET_REPLICATION_SSEC_CHECKSUM) {
|
||||
user_defined.insert(
|
||||
RUSTFS_BUCKET_REPLICATION_SSEC_CHECKSUM.to_string(),
|
||||
v.to_str().unwrap_or_default().to_owned(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut opts = ObjectOptions {
|
||||
want_checksum: rustfs_rio::get_content_checksum(headers)?,
|
||||
user_defined,
|
||||
replication_request,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
fill_conditional_writes_opts_from_header(headers, &mut opts)?;
|
||||
Ok(opts)
|
||||
}
|
||||
@@ -290,6 +330,39 @@ pub fn extract_metadata_from_mime_with_object_name(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn filter_object_metadata(metadata: &HashMap<String, String>) -> Option<HashMap<String, String>> {
|
||||
let mut filtered_metadata = HashMap::new();
|
||||
for (k, v) in metadata {
|
||||
if k.starts_with(RESERVED_METADATA_PREFIX_LOWER) {
|
||||
continue;
|
||||
}
|
||||
if v.is_empty() && (k == &X_AMZ_OBJECT_LOCK_MODE.to_string() || k == &X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if k == AMZ_META_UNENCRYPTED_CONTENT_MD5 || k == AMZ_META_UNENCRYPTED_CONTENT_LENGTH {
|
||||
continue;
|
||||
}
|
||||
|
||||
let lower_key = k.to_ascii_lowercase();
|
||||
if let Some(key) = lower_key.strip_prefix("x-amz-meta-") {
|
||||
filtered_metadata.insert(key.to_string(), v.to_string());
|
||||
continue;
|
||||
}
|
||||
if let Some(key) = lower_key.strip_prefix("x-rustfs-meta-") {
|
||||
filtered_metadata.insert(key.to_string(), v.to_string());
|
||||
continue;
|
||||
}
|
||||
|
||||
filtered_metadata.insert(k.clone(), v.clone());
|
||||
}
|
||||
if filtered_metadata.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(filtered_metadata)
|
||||
}
|
||||
}
|
||||
|
||||
/// Detects content type from object name based on file extension.
|
||||
pub(crate) fn detect_content_type_from_object_name(object_name: &str) -> String {
|
||||
let lower_name = object_name.to_lowercase();
|
||||
@@ -387,6 +460,100 @@ pub fn parse_copy_source_range(range_str: &str) -> S3Result<HTTPRangeSpec> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_content_sha256(headers: &HeaderMap<HeaderValue>) -> Option<String> {
|
||||
match get_request_auth_type(headers) {
|
||||
AuthType::Presigned | AuthType::Signed => {
|
||||
if skip_content_sha256_cksum(headers) {
|
||||
None
|
||||
} else {
|
||||
Some(get_content_sha256_cksum(headers, ServiceType::S3))
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// skip_content_sha256_cksum returns true if caller needs to skip
|
||||
/// payload checksum, false if not.
|
||||
fn skip_content_sha256_cksum(headers: &HeaderMap<HeaderValue>) -> bool {
|
||||
let content_sha256 = if is_request_presigned_signature_v4(headers) {
|
||||
// For presigned requests, check query params first, then headers
|
||||
// Note: In a real implementation, you would need to check query parameters
|
||||
// For now, we'll just check headers
|
||||
headers.get(AMZ_CONTENT_SHA256)
|
||||
} else {
|
||||
headers.get(AMZ_CONTENT_SHA256)
|
||||
};
|
||||
|
||||
// Skip if no header was set
|
||||
let Some(header_value) = content_sha256 else {
|
||||
return true;
|
||||
};
|
||||
|
||||
let Ok(value) = header_value.to_str() else {
|
||||
return true;
|
||||
};
|
||||
|
||||
// If x-amz-content-sha256 is set and the value is not
|
||||
// 'UNSIGNED-PAYLOAD' we should validate the content sha256.
|
||||
match value {
|
||||
v if v == UNSIGNED_PAYLOAD || v == UNSIGNED_PAYLOAD_TRAILER => true,
|
||||
v if v == EMPTY_STRING_SHA256_HASH => {
|
||||
// some broken clients set empty-sha256
|
||||
// with > 0 content-length in the body,
|
||||
// we should skip such clients and allow
|
||||
// blindly such insecure clients only if
|
||||
// S3 strict compatibility is disabled.
|
||||
|
||||
// We return true only in situations when
|
||||
// deployment has asked RustFS to allow for
|
||||
// such broken clients and content-length > 0.
|
||||
// For now, we'll assume strict compatibility is disabled
|
||||
// In a real implementation, you would check a global config
|
||||
if let Some(content_length) = headers.get("content-length") {
|
||||
if let Ok(length_str) = content_length.to_str() {
|
||||
if let Ok(length) = length_str.parse::<i64>() {
|
||||
return length > 0; // && !global_server_ctxt.strict_s3_compat
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns SHA256 for calculating canonical-request.
|
||||
fn get_content_sha256_cksum(headers: &HeaderMap<HeaderValue>, service_type: ServiceType) -> String {
|
||||
if service_type == ServiceType::STS {
|
||||
// For STS requests, we would need to read the body and calculate SHA256
|
||||
// This is a simplified implementation - in practice you'd need access to the request body
|
||||
// For now, we'll return a placeholder
|
||||
return "sts-body-sha256-placeholder".to_string();
|
||||
}
|
||||
|
||||
let (default_sha256_cksum, content_sha256) = if is_request_presigned_signature_v4(headers) {
|
||||
// For a presigned request we look at the query param for sha256.
|
||||
// X-Amz-Content-Sha256, if not set in presigned requests, checksum
|
||||
// will default to 'UNSIGNED-PAYLOAD'.
|
||||
(UNSIGNED_PAYLOAD.to_string(), headers.get(AMZ_CONTENT_SHA256))
|
||||
} else {
|
||||
// X-Amz-Content-Sha256, if not set in signed requests, checksum
|
||||
// will default to sha256([]byte("")).
|
||||
(EMPTY_STRING_SHA256_HASH.to_string(), headers.get(AMZ_CONTENT_SHA256))
|
||||
};
|
||||
|
||||
// We found 'X-Amz-Content-Sha256' return the captured value.
|
||||
if let Some(header_value) = content_sha256 {
|
||||
if let Ok(value) = header_value.to_str() {
|
||||
return value.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// We couldn't find 'X-Amz-Content-Sha256'.
|
||||
default_sha256_cksum
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
+218
-222
File diff suppressed because it is too large
Load Diff
@@ -12,13 +12,12 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
use crate::version;
|
||||
|
||||
/// Update check related errors
|
||||
#[derive(Error, Debug)]
|
||||
pub enum UpdateCheckError {
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use shadow_rs::shadow;
|
||||
use std::process::Command;
|
||||
|
||||
|
||||
+2
-3
@@ -46,7 +46,6 @@ export RUSTFS_VOLUMES="./target/volume/test{1...4}"
|
||||
export RUSTFS_ADDRESS=":9000"
|
||||
export RUSTFS_CONSOLE_ENABLE=true
|
||||
export RUSTFS_CONSOLE_ADDRESS=":9001"
|
||||
export RUSTFS_EXTERNAL_ADDRESS=":9000"
|
||||
# export RUSTFS_SERVER_DOMAINS="localhost:9000"
|
||||
# HTTPS certificate directory
|
||||
# export RUSTFS_TLS_PATH="./deploy/certs"
|
||||
@@ -59,7 +58,7 @@ export RUSTFS_EXTERNAL_ADDRESS=":9000"
|
||||
#export RUSTFS_OBS_SERVICE_NAME=rustfs # Service name
|
||||
#export RUSTFS_OBS_SERVICE_VERSION=0.1.0 # Service version
|
||||
export RUSTFS_OBS_ENVIRONMENT=develop # Environment name
|
||||
export RUSTFS_OBS_LOGGER_LEVEL=info # Log level, supports trace, debug, info, warn, error
|
||||
export RUSTFS_OBS_LOGGER_LEVEL=debug # Log level, supports trace, debug, info, warn, error
|
||||
export RUSTFS_OBS_LOCAL_LOGGING_ENABLED=true # Whether to enable local logging
|
||||
export RUSTFS_OBS_LOG_DIRECTORY="$current_dir/deploy/logs" # Log directory
|
||||
export RUSTFS_OBS_LOG_ROTATION_TIME="hour" # Log rotation time unit, can be "second", "minute", "hour", "day"
|
||||
@@ -102,7 +101,7 @@ export RUSTFS_NOTIFY_WEBHOOK_QUEUE_DIR_MASTER="$current_dir/deploy/logs/notify"
|
||||
export RUSTFS_NS_SCANNER_INTERVAL=60 # Object scanning interval in seconds
|
||||
# exportRUSTFS_SKIP_BACKGROUND_TASK=true
|
||||
|
||||
export RUSTFS_COMPRESSION_ENABLED=true # Whether to enable compression
|
||||
# export RUSTFS_COMPRESSION_ENABLED=true # Whether to enable compression
|
||||
|
||||
#export RUSTFS_REGION="us-east-1"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user