mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-20 19:42:17 +00:00
refactor: Restructure project layout and clean up dependencies (#30)
This commit introduces a significant reorganization of the project structure to improve maintainability and clarity. Key changes include: - Adjusted the directory layout for a more logical module organization. - Removed unused crate dependencies, reducing the overall project size and potentially speeding up build times. - Updated import paths and configuration files to reflect the structural changes.
This commit is contained in:
@@ -0,0 +1,389 @@
|
||||
// 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::global::get_global_action_cred;
|
||||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose;
|
||||
use hmac::{Hmac, Mac};
|
||||
use http::HeaderMap;
|
||||
use http::HeaderValue;
|
||||
use http::Method;
|
||||
use http::Uri;
|
||||
use sha2::Sha256;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::error;
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
const SIGNATURE_HEADER: &str = "x-rustfs-signature";
|
||||
const TIMESTAMP_HEADER: &str = "x-rustfs-timestamp";
|
||||
const SIGNATURE_VALID_DURATION: i64 = 300; // 5 minutes
|
||||
|
||||
/// Get the shared secret for HMAC signing
|
||||
fn get_shared_secret() -> String {
|
||||
if let Some(cred) = get_global_action_cred() {
|
||||
cred.secret_key
|
||||
} else {
|
||||
// Fallback to environment variable if global credentials are not available
|
||||
std::env::var("RUSTFS_RPC_SECRET").unwrap_or_else(|_| "rustfs-default-secret".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate HMAC-SHA256 signature for the given data
|
||||
fn generate_signature(secret: &str, url: &str, method: &Method, timestamp: i64) -> String {
|
||||
let uri: Uri = url.parse().expect("Invalid URL");
|
||||
|
||||
let path_and_query = uri.path_and_query().unwrap();
|
||||
|
||||
let url = path_and_query.to_string();
|
||||
|
||||
let data = format!("{url}|{method}|{timestamp}");
|
||||
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC can take key of any size");
|
||||
mac.update(data.as_bytes());
|
||||
let result = mac.finalize();
|
||||
general_purpose::STANDARD.encode(result.into_bytes())
|
||||
}
|
||||
|
||||
/// Build headers with authentication signature
|
||||
pub fn build_auth_headers(url: &str, method: &Method, headers: &mut HeaderMap) {
|
||||
let secret = get_shared_secret();
|
||||
let timestamp = OffsetDateTime::now_utc().unix_timestamp();
|
||||
|
||||
let signature = generate_signature(&secret, url, method, timestamp);
|
||||
|
||||
headers.insert(SIGNATURE_HEADER, HeaderValue::from_str(&signature).unwrap());
|
||||
headers.insert(TIMESTAMP_HEADER, HeaderValue::from_str(×tamp.to_string()).unwrap());
|
||||
}
|
||||
|
||||
/// Verify the request signature for RPC requests
|
||||
pub fn verify_rpc_signature(url: &str, method: &Method, headers: &HeaderMap) -> std::io::Result<()> {
|
||||
let secret = get_shared_secret();
|
||||
|
||||
// Get signature from header
|
||||
let signature = headers
|
||||
.get(SIGNATURE_HEADER)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.ok_or_else(|| std::io::Error::other("Missing signature header"))?;
|
||||
|
||||
// Get timestamp from header
|
||||
let timestamp_str = headers
|
||||
.get(TIMESTAMP_HEADER)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.ok_or_else(|| std::io::Error::other("Missing timestamp header"))?;
|
||||
|
||||
let timestamp: i64 = timestamp_str
|
||||
.parse()
|
||||
.map_err(|_| std::io::Error::other("Invalid timestamp format"))?;
|
||||
|
||||
// Check timestamp validity (prevent replay attacks)
|
||||
let current_time = OffsetDateTime::now_utc().unix_timestamp();
|
||||
|
||||
if current_time.saturating_sub(timestamp) > SIGNATURE_VALID_DURATION {
|
||||
return Err(std::io::Error::other("Request timestamp expired"));
|
||||
}
|
||||
|
||||
// Generate expected signature
|
||||
|
||||
let expected_signature = generate_signature(&secret, url, method, timestamp);
|
||||
|
||||
// Compare signatures
|
||||
if signature != expected_signature {
|
||||
error!(
|
||||
"verify_rpc_signature: Invalid signature: secret {}, url {}, method {}, timestamp {}, signature {}, expected_signature {}",
|
||||
secret, url, method, timestamp, signature, expected_signature
|
||||
);
|
||||
|
||||
return Err(std::io::Error::other("Invalid signature"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use http::{HeaderMap, Method};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
#[test]
|
||||
fn test_get_shared_secret() {
|
||||
let secret = get_shared_secret();
|
||||
assert!(!secret.is_empty(), "Secret should not be empty");
|
||||
|
||||
let url = "http://node1:7000/rustfs/rpc/read_file_stream?disk=http%3A%2F%2Fnode1%3A7000%2Fdata%2Frustfs3&volume=.rustfs.sys&path=pool.bin%2Fdd0fd773-a962-4265-b543-783ce83953e9%2Fpart.1&offset=0&length=44";
|
||||
let method = Method::GET;
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
build_auth_headers(url, &method, &mut headers);
|
||||
|
||||
let url = "/rustfs/rpc/read_file_stream?disk=http%3A%2F%2Fnode1%3A7000%2Fdata%2Frustfs3&volume=.rustfs.sys&path=pool.bin%2Fdd0fd773-a962-4265-b543-783ce83953e9%2Fpart.1&offset=0&length=44";
|
||||
|
||||
let result = verify_rpc_signature(url, &method, &headers);
|
||||
assert!(result.is_ok(), "Valid signature should pass verification");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_signature_deterministic() {
|
||||
let secret = "test-secret";
|
||||
let url = "http://example.com/api/test";
|
||||
let method = Method::GET;
|
||||
let timestamp = 1640995200; // Fixed timestamp
|
||||
|
||||
let signature1 = generate_signature(secret, url, &method, timestamp);
|
||||
let signature2 = generate_signature(secret, url, &method, timestamp);
|
||||
|
||||
assert_eq!(signature1, signature2, "Same inputs should produce same signature");
|
||||
assert!(!signature1.is_empty(), "Signature should not be empty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_signature_different_inputs() {
|
||||
let secret = "test-secret";
|
||||
let url = "http://example.com/api/test";
|
||||
let method = Method::GET;
|
||||
let timestamp = 1640995200;
|
||||
|
||||
let signature1 = generate_signature(secret, url, &method, timestamp);
|
||||
let signature2 = generate_signature(secret, "http://different.com/api/test2", &method, timestamp);
|
||||
let signature3 = generate_signature(secret, url, &Method::POST, timestamp);
|
||||
let signature4 = generate_signature(secret, url, &method, timestamp + 1);
|
||||
|
||||
assert_ne!(signature1, signature2, "Different URLs should produce different signatures");
|
||||
assert_ne!(signature1, signature3, "Different methods should produce different signatures");
|
||||
assert_ne!(signature1, signature4, "Different timestamps should produce different signatures");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_auth_headers() {
|
||||
let url = "http://example.com/api/test";
|
||||
let method = Method::POST;
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
build_auth_headers(url, &method, &mut headers);
|
||||
|
||||
// Verify headers are present
|
||||
assert!(headers.contains_key(SIGNATURE_HEADER), "Should contain signature header");
|
||||
assert!(headers.contains_key(TIMESTAMP_HEADER), "Should contain timestamp header");
|
||||
|
||||
// Verify header values are not empty
|
||||
let signature = headers.get(SIGNATURE_HEADER).unwrap().to_str().unwrap();
|
||||
let timestamp_str = headers.get(TIMESTAMP_HEADER).unwrap().to_str().unwrap();
|
||||
|
||||
assert!(!signature.is_empty(), "Signature should not be empty");
|
||||
assert!(!timestamp_str.is_empty(), "Timestamp should not be empty");
|
||||
|
||||
// Verify timestamp is a valid integer
|
||||
let timestamp: i64 = timestamp_str.parse().expect("Timestamp should be valid integer");
|
||||
let current_time = OffsetDateTime::now_utc().unix_timestamp();
|
||||
|
||||
// Should be within a reasonable range (within 1 second of current time)
|
||||
assert!((current_time - timestamp).abs() <= 1, "Timestamp should be close to current time");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_rpc_signature_success() {
|
||||
let url = "http://example.com/api/test";
|
||||
let method = Method::GET;
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
// Build headers with valid signature
|
||||
build_auth_headers(url, &method, &mut headers);
|
||||
|
||||
// Verify should succeed
|
||||
let result = verify_rpc_signature(url, &method, &headers);
|
||||
assert!(result.is_ok(), "Valid signature should pass verification");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_rpc_signature_invalid_signature() {
|
||||
let url = "http://example.com/api/test";
|
||||
let method = Method::GET;
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
// Build headers with valid signature first
|
||||
build_auth_headers(url, &method, &mut headers);
|
||||
|
||||
// Tamper with the signature
|
||||
headers.insert(SIGNATURE_HEADER, HeaderValue::from_str("invalid-signature").unwrap());
|
||||
|
||||
// Verify should fail
|
||||
let result = verify_rpc_signature(url, &method, &headers);
|
||||
assert!(result.is_err(), "Invalid signature should fail verification");
|
||||
|
||||
let error = result.unwrap_err();
|
||||
assert_eq!(error.to_string(), "Invalid signature");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_rpc_signature_expired_timestamp() {
|
||||
let url = "http://example.com/api/test";
|
||||
let method = Method::GET;
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
// Set expired timestamp (older than SIGNATURE_VALID_DURATION)
|
||||
let expired_timestamp = OffsetDateTime::now_utc().unix_timestamp() - SIGNATURE_VALID_DURATION - 10;
|
||||
let secret = get_shared_secret();
|
||||
let signature = generate_signature(&secret, url, &method, expired_timestamp);
|
||||
|
||||
headers.insert(SIGNATURE_HEADER, HeaderValue::from_str(&signature).unwrap());
|
||||
headers.insert(TIMESTAMP_HEADER, HeaderValue::from_str(&expired_timestamp.to_string()).unwrap());
|
||||
|
||||
// Verify should fail due to expired timestamp
|
||||
let result = verify_rpc_signature(url, &method, &headers);
|
||||
assert!(result.is_err(), "Expired timestamp should fail verification");
|
||||
|
||||
let error = result.unwrap_err();
|
||||
assert_eq!(error.to_string(), "Request timestamp expired");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_rpc_signature_missing_signature_header() {
|
||||
let url = "http://example.com/api/test";
|
||||
let method = Method::GET;
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
// Add only timestamp header, missing signature
|
||||
let timestamp = OffsetDateTime::now_utc().unix_timestamp();
|
||||
headers.insert(TIMESTAMP_HEADER, HeaderValue::from_str(×tamp.to_string()).unwrap());
|
||||
|
||||
// Verify should fail
|
||||
let result = verify_rpc_signature(url, &method, &headers);
|
||||
assert!(result.is_err(), "Missing signature header should fail verification");
|
||||
|
||||
let error = result.unwrap_err();
|
||||
assert_eq!(error.to_string(), "Missing signature header");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_rpc_signature_missing_timestamp_header() {
|
||||
let url = "http://example.com/api/test";
|
||||
let method = Method::GET;
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
// Add only signature header, missing timestamp
|
||||
headers.insert(SIGNATURE_HEADER, HeaderValue::from_str("some-signature").unwrap());
|
||||
|
||||
// Verify should fail
|
||||
let result = verify_rpc_signature(url, &method, &headers);
|
||||
assert!(result.is_err(), "Missing timestamp header should fail verification");
|
||||
|
||||
let error = result.unwrap_err();
|
||||
assert_eq!(error.to_string(), "Missing timestamp header");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_rpc_signature_invalid_timestamp_format() {
|
||||
let url = "http://example.com/api/test";
|
||||
let method = Method::GET;
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
headers.insert(SIGNATURE_HEADER, HeaderValue::from_str("some-signature").unwrap());
|
||||
headers.insert(TIMESTAMP_HEADER, HeaderValue::from_str("invalid-timestamp").unwrap());
|
||||
|
||||
// Verify should fail
|
||||
let result = verify_rpc_signature(url, &method, &headers);
|
||||
assert!(result.is_err(), "Invalid timestamp format should fail verification");
|
||||
|
||||
let error = result.unwrap_err();
|
||||
assert_eq!(error.to_string(), "Invalid timestamp format");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_rpc_signature_url_mismatch() {
|
||||
let original_url = "http://example.com/api/test";
|
||||
let different_url = "http://example.com/api/different";
|
||||
let method = Method::GET;
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
// Build headers for one URL
|
||||
build_auth_headers(original_url, &method, &mut headers);
|
||||
|
||||
// Try to verify with a different URL
|
||||
let result = verify_rpc_signature(different_url, &method, &headers);
|
||||
assert!(result.is_err(), "URL mismatch should fail verification");
|
||||
|
||||
let error = result.unwrap_err();
|
||||
assert_eq!(error.to_string(), "Invalid signature");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_rpc_signature_method_mismatch() {
|
||||
let url = "http://example.com/api/test";
|
||||
let original_method = Method::GET;
|
||||
let different_method = Method::POST;
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
// Build headers for one method
|
||||
build_auth_headers(url, &original_method, &mut headers);
|
||||
|
||||
// Try to verify with a different method
|
||||
let result = verify_rpc_signature(url, &different_method, &headers);
|
||||
assert!(result.is_err(), "Method mismatch should fail verification");
|
||||
|
||||
let error = result.unwrap_err();
|
||||
assert_eq!(error.to_string(), "Invalid signature");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_signature_valid_duration_boundary() {
|
||||
let url = "http://example.com/api/test";
|
||||
let method = Method::GET;
|
||||
let secret = get_shared_secret();
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
let current_time = OffsetDateTime::now_utc().unix_timestamp();
|
||||
// Test timestamp just within valid duration
|
||||
let valid_timestamp = current_time - SIGNATURE_VALID_DURATION + 1;
|
||||
|
||||
let signature = generate_signature(&secret, url, &method, valid_timestamp);
|
||||
|
||||
headers.insert(SIGNATURE_HEADER, HeaderValue::from_str(&signature).unwrap());
|
||||
headers.insert(TIMESTAMP_HEADER, HeaderValue::from_str(&valid_timestamp.to_string()).unwrap());
|
||||
|
||||
let result = verify_rpc_signature(url, &method, &headers);
|
||||
assert!(result.is_ok(), "Timestamp within valid duration should pass");
|
||||
|
||||
// Test timestamp just outside valid duration
|
||||
let mut headers = HeaderMap::new();
|
||||
let invalid_timestamp = current_time - SIGNATURE_VALID_DURATION - 15;
|
||||
let signature = generate_signature(&secret, url, &method, invalid_timestamp);
|
||||
|
||||
headers.insert(SIGNATURE_HEADER, HeaderValue::from_str(&signature).unwrap());
|
||||
headers.insert(TIMESTAMP_HEADER, HeaderValue::from_str(&invalid_timestamp.to_string()).unwrap());
|
||||
|
||||
let result = verify_rpc_signature(url, &method, &headers);
|
||||
assert!(result.is_err(), "Timestamp outside valid duration should fail");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_trip_authentication() {
|
||||
let test_cases = vec![
|
||||
("http://example.com/api/test", Method::GET),
|
||||
("https://api.rustfs.com/v1/bucket", Method::POST),
|
||||
("http://localhost:9000/admin/info", Method::PUT),
|
||||
("https://storage.example.com/path/to/object?query=param", Method::DELETE),
|
||||
];
|
||||
|
||||
for (url, method) in test_cases {
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
// Build authentication headers
|
||||
build_auth_headers(url, &method, &mut headers);
|
||||
|
||||
// Verify the signature should succeed
|
||||
let result = verify_rpc_signature(url, &method, &headers);
|
||||
assert!(result.is_ok(), "Round-trip test failed for {method} {url}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user