// 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 use bytes::Bytes; use chrono::Utc; use futures::stream; use hmac::{Hmac, KeyInit, Mac}; use reqwest::{Client, Method, Response}; use rustfs::embedded::{RustFSServerBuilder, find_available_port}; use sha2::{Digest, Sha256}; use std::time::Duration; type HmacSha256 = Hmac; fn hex(bytes: impl AsRef<[u8]>) -> String { bytes.as_ref().iter().map(|byte| format!("{byte:02x}")).collect() } fn sha256_hex(bytes: &[u8]) -> String { hex(Sha256::digest(bytes)) } fn hmac(key: &[u8], value: &str) -> Vec { let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts arbitrary key lengths"); mac.update(value.as_bytes()); mac.finalize().into_bytes().to_vec() } fn signed_admin_request( client: &Client, endpoint: &str, method: Method, path: &str, access_key: &str, secret_key: &str, payload_hash: &str, ) -> reqwest::RequestBuilder { let host = endpoint .strip_prefix("http://") .or_else(|| endpoint.strip_prefix("https://")) .expect("embedded endpoint scheme"); let now = Utc::now(); let amz_date = now.format("%Y%m%dT%H%M%SZ").to_string(); let date = now.format("%Y%m%d").to_string(); let canonical_headers = format!("host:{host}\nx-amz-content-sha256:{payload_hash}\nx-amz-date:{amz_date}\n"); let signed_headers = "host;x-amz-content-sha256;x-amz-date"; let canonical_request = format!("{}\n{path}\n\n{canonical_headers}\n{signed_headers}\n{payload_hash}", method.as_str()); let scope = format!("{date}/us-east-1/s3/aws4_request"); let string_to_sign = format!("AWS4-HMAC-SHA256\n{amz_date}\n{scope}\n{}", sha256_hex(canonical_request.as_bytes())); let date_key = hmac(format!("AWS4{secret_key}").as_bytes(), &date); let region_key = hmac(&date_key, "us-east-1"); let service_key = hmac(®ion_key, "s3"); let signing_key = hmac(&service_key, "aws4_request"); let signature = hex(hmac(&signing_key, &string_to_sign)); let authorization = format!("AWS4-HMAC-SHA256 Credential={access_key}/{scope}, SignedHeaders={signed_headers}, Signature={signature}"); client .request(method, format!("{endpoint}{path}")) .header("host", host) .header("x-amz-content-sha256", payload_hash) .header("x-amz-date", amz_date) .header("authorization", authorization) } async fn signed_bytes_request( client: &Client, endpoint: &str, method: Method, path: &str, access_key: &str, secret_key: &str, body: &'static [u8], ) -> Response { signed_admin_request(client, endpoint, method, path, access_key, secret_key, &sha256_hex(body)) .body(body) .send() .await .expect("signed admin request") } async fn wait_for_ready(client: &Client, endpoint: &str) { let ready_url = format!("{endpoint}/health/ready"); tokio::time::timeout(Duration::from_secs(10), async { loop { if let Ok(response) = client.get(&ready_url).send().await && response.status().is_success() { return; } tokio::time::sleep(Duration::from_millis(100)).await; } }) .await .expect("embedded server should become ready"); } #[tokio::test] async fn diagnostic_handlers_enforce_advertised_runtime_contract() { let port = match find_available_port() { Ok(port) => port, Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return, Err(err) => panic!("find free port: {err}"), }; let server = RustFSServerBuilder::new() .address(format!("127.0.0.1:{port}")) .access_key("diagnostic-e2e-access") .secret_key("diagnostic-e2e-secret") .build() .await .expect("start embedded server"); let endpoint = server.endpoint(); let client = Client::builder() .no_proxy() .timeout(Duration::from_secs(5)) .build() .expect("HTTP client"); wait_for_ready(&client, &endpoint).await; let response = signed_bytes_request( &client, &endpoint, Method::POST, "/rustfs/admin/v3/speedtest/client/devnull", server.access_key(), server.secret_key(), b"bounded", ) .await; assert!(response.status().is_success()); let value: serde_json::Value = response.json().await.expect("client devnull JSON"); assert_eq!(value["kind"], "client-devnull"); assert_eq!(value["measured"], true); assert_eq!(value["rx_bytes"], 7); let oversized = signed_admin_request( &client, &endpoint, Method::POST, "/rustfs/admin/v3/speedtest/client/devnull", server.access_key(), server.secret_key(), &sha256_hex(b""), ) .header("content-length", 1024_u64 * 1024 * 1024 + 1) .body(reqwest::Body::wrap_stream(stream::pending::>())) .send() .await .expect("oversized request must receive an early response"); assert_eq!(oversized.status(), reqwest::StatusCode::BAD_REQUEST); assert!( oversized .text() .await .expect("oversized error body") .contains("EntityTooLarge") ); let stalled_client = Client::builder() .no_proxy() .timeout(Duration::from_secs(60)) .build() .expect("stalled HTTP client"); let mut stalled = Vec::new(); for _ in 0..4 { let client = stalled_client.clone(); let endpoint = endpoint.clone(); let access_key = server.access_key().to_string(); let secret_key = server.secret_key().to_string(); stalled.push(tokio::spawn(async move { signed_admin_request( &client, &endpoint, Method::POST, "/rustfs/admin/v3/speedtest/client/devnull", &access_key, &secret_key, &sha256_hex(b"x"), ) .header("content-length", 1) .body(reqwest::Body::wrap_stream(stream::pending::>())) .send() .await })); } tokio::time::timeout(Duration::from_secs(10), async { loop { let probe = signed_bytes_request( &client, &endpoint, Method::POST, "/rustfs/admin/v3/speedtest/client/devnull", server.access_key(), server.secret_key(), b"x", ) .await; let status = probe.status(); let body = probe.text().await.expect("admission probe body"); if status == reqwest::StatusCode::SERVICE_UNAVAILABLE { assert!(body.contains("SlowDown")); break; } assert!(status.is_success(), "unexpected admission probe status {status}: {body}"); tokio::time::sleep(Duration::from_millis(10)).await; } }) .await .expect("stalled requests must occupy every admission slot"); for request in stalled { request.abort(); } let netperf = signed_bytes_request( &client, &endpoint, Method::POST, "/rustfs/admin/v3/site-replication/netperf", server.access_key(), server.secret_key(), b"", ) .await; assert!(netperf.status().is_success()); let gob = netperf.bytes().await.expect("site netperf gob"); assert!( gob.windows(b"site-replication netperf is unsupported because RustFS does not perform peer traffic".len()) .any(|window| window == b"site-replication netperf is unsupported because RustFS does not perform peer traffic") ); server.shutdown().await; }