fix(rpc): negotiate authenticated file writes (#5880)

* fix(rpc): negotiate authenticated file writes

* fix(rpc): share capability probe failures

* test(rpc): cover dedicated capability route

* fix(rpc): satisfy capability cache lints

* fix(rpc): retry timed out capability probes

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
cxymds
2026-08-09 21:19:47 +08:00
committed by GitHub
parent 1be636b914
commit 8f9633ee83
11 changed files with 1134 additions and 65 deletions
+90 -3
View File
@@ -19,8 +19,8 @@ use http::{HeaderMap, Version};
use pin_project_lite::pin_project;
use reqwest::{Certificate, Client, Identity, Method, RequestBuilder};
use rustfs_io_metrics::internode_metrics::{
INTERNODE_OPERATION_NS_SCANNER, INTERNODE_OPERATION_PUT_FILE_STREAM, INTERNODE_OPERATION_READ_FILE_STREAM,
INTERNODE_OPERATION_WALK_DIR,
INTERNODE_OPERATION_NS_SCANNER, INTERNODE_OPERATION_PUT_FILE_CAPABILITY, INTERNODE_OPERATION_PUT_FILE_STREAM,
INTERNODE_OPERATION_READ_FILE_STREAM, INTERNODE_OPERATION_WALK_DIR,
};
use rustfs_tls_runtime::load_cert_bundle_der_bytes;
use rustfs_utils::{get_env_bool, get_env_opt_str, get_env_opt_u64, get_env_opt_usize};
@@ -43,6 +43,8 @@ use tracing::{error, warn};
const READ_FILE_STREAM_PATH: &str = "/rustfs/rpc/read_file_stream";
const PUT_FILE_STREAM_PATH: &str = "/rustfs/rpc/put_file_stream";
const PUT_FILE_AUTH_STREAM_PATH: &str = "/rustfs/rpc/put_file_stream_v1";
const PUT_FILE_CAPABILITY_PATH: &str = "/rustfs/rpc/put_file_capability";
const WALK_DIR_PATH: &str = "/rustfs/rpc/walk_dir";
const NS_SCANNER_PATH: &str = "/rustfs/rpc/ns_scanner";
const HTTP_VERSION_09_LABEL: &str = "http/0.9";
@@ -261,6 +263,31 @@ pub fn new_test_internode_http_io_error(kind: InternodeHttpErrorKind) -> io::Err
InternodeHttpError::new_for_test(kind).into_io_error()
}
/// Build a retryable internode timeout error with the request's operation context.
#[doc(hidden)]
pub fn internode_http_timeout_error(method: &Method, url: &str) -> io::Error {
internode_kind_error(method, url, internode_rpc_operation(url), InternodeHttpErrorKind::ConnectTimeout)
}
/// Clone an internode HTTP I/O error while retaining its structured classification.
///
/// The underlying transport source is intentionally omitted because it is not
/// cloneable. The request context and remote disk marker remain available to
/// retry and error-mapping code.
#[doc(hidden)]
pub fn clone_internode_http_io_error(error: &io::Error) -> Option<io::Error> {
let source = error.get_ref()?.downcast_ref::<InternodeHttpError>()?;
Some(
InternodeHttpError {
kind: source.kind,
context: source.context.clone(),
remote_disk_error: source.remote_disk_error,
source: None,
}
.into_io_error(),
)
}
#[doc(hidden)]
pub fn new_test_remote_file_not_found_http_io_error() -> io::Error {
InternodeHttpError::with_remote_disk_error(
@@ -1223,7 +1250,8 @@ fn internode_rpc_operation(url: &str) -> Option<&'static str> {
let url = reqwest::Url::parse(url).ok()?;
match url.path() {
READ_FILE_STREAM_PATH => Some(INTERNODE_OPERATION_READ_FILE_STREAM),
PUT_FILE_STREAM_PATH => Some(INTERNODE_OPERATION_PUT_FILE_STREAM),
PUT_FILE_STREAM_PATH | PUT_FILE_AUTH_STREAM_PATH => Some(INTERNODE_OPERATION_PUT_FILE_STREAM),
PUT_FILE_CAPABILITY_PATH => Some(INTERNODE_OPERATION_PUT_FILE_CAPABILITY),
WALK_DIR_PATH => Some(INTERNODE_OPERATION_WALK_DIR),
NS_SCANNER_PATH => Some(INTERNODE_OPERATION_NS_SCANNER),
_ => None,
@@ -1920,6 +1948,14 @@ mod tests {
internode_rpc_operation(&format!("http://node:9000{PUT_FILE_STREAM_PATH}?disk=d")),
Some(INTERNODE_OPERATION_PUT_FILE_STREAM)
);
assert_eq!(
internode_rpc_operation(&format!("http://node:9000{PUT_FILE_AUTH_STREAM_PATH}?disk=d")),
Some(INTERNODE_OPERATION_PUT_FILE_STREAM)
);
assert_eq!(
internode_rpc_operation(&format!("http://node:9000{PUT_FILE_CAPABILITY_PATH}?put_file_capability=1")),
Some(INTERNODE_OPERATION_PUT_FILE_CAPABILITY)
);
assert_eq!(
internode_rpc_operation(&format!("http://node:9000{WALK_DIR_PATH}?disk=d")),
Some(INTERNODE_OPERATION_WALK_DIR)
@@ -1935,6 +1971,21 @@ mod tests {
);
}
#[test]
fn internode_http_timeout_error_retains_operation_context() {
let error =
internode_http_timeout_error(&Method::GET, "http://node:9000/rustfs/rpc/put_file_capability?put_file_capability=1");
let source = error
.get_ref()
.and_then(|source| source.downcast_ref::<InternodeHttpError>())
.expect("timeout should retain internode classification");
assert_eq!(source.kind(), InternodeHttpErrorKind::ConnectTimeout);
assert_eq!(source.context().method(), "GET");
assert_eq!(source.context().target(), PUT_FILE_CAPABILITY_PATH);
assert_eq!(source.context().operation(), Some(INTERNODE_OPERATION_PUT_FILE_CAPABILITY));
}
#[test]
fn http_version_metrics_labels_are_low_cardinality() {
assert_eq!(http_version_metric_label(Version::HTTP_09), HTTP_VERSION_09_LABEL);
@@ -2385,6 +2436,42 @@ mod tests {
assert!(source.context().target().contains(PUT_FILE_STREAM_PATH));
}
#[test]
fn cloned_internode_http_error_retains_classification_and_context() {
let original = internode_status_error(
&Method::GET,
"http://node:9000/rustfs/rpc/put_file_capability?put_file_capability=1",
Some(INTERNODE_OPERATION_PUT_FILE_CAPABILITY),
reqwest::StatusCode::SERVICE_UNAVAILABLE,
);
let cloned = clone_internode_http_io_error(&original).expect("internode error should clone");
let source = cloned
.get_ref()
.and_then(|source| source.downcast_ref::<InternodeHttpError>())
.expect("clone should retain internode source");
assert_eq!(
source.kind(),
InternodeHttpErrorKind::HttpStatus(reqwest::StatusCode::SERVICE_UNAVAILABLE)
);
assert!(source.kind().is_retryable());
assert_eq!(source.context().method(), "GET");
assert_eq!(source.context().target(), PUT_FILE_CAPABILITY_PATH);
assert_eq!(source.context().operation(), Some(INTERNODE_OPERATION_PUT_FILE_CAPABILITY));
}
#[test]
fn cloned_internode_http_error_retains_remote_disk_marker() {
let original = new_test_remote_file_not_found_http_io_error();
let cloned = clone_internode_http_io_error(&original).expect("internode error should clone");
let source = cloned
.get_ref()
.and_then(|source| source.downcast_ref::<InternodeHttpError>())
.expect("clone should retain internode source");
assert!(source.is_remote_file_not_found());
}
#[test]
fn loopback_urls_bypass_proxy_selection() {
assert!(should_bypass_proxy_for_url("http://127.0.0.1:9000/stream"));