mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-21 11:56:38 +00:00
fix(webdav): allow bucket-scoped root listings (#6298)
* fix(webdav): allow bucket-scoped root listings * test(webdav): use public protocol export * test(webdav): initialize identity inline --------- Co-authored-by: cxymds <cxymds@gmail.com>
This commit is contained in:
@@ -106,7 +106,7 @@ swift = [
|
||||
"dep:base64",
|
||||
"dep:async-compression",
|
||||
]
|
||||
webdav = ["dep:dav-server", "dep:hyper", "dep:hyper-util", "dep:http-body-util", "dep:tokio-rustls", "dep:base64", "dep:rustls", "dep:percent-encoding", "dep:rustfs-tls-runtime", "dep:subtle"]
|
||||
webdav = ["dep:dav-server", "dep:hyper", "dep:hyper-util", "dep:http", "dep:http-body-util", "dep:tokio-rustls", "dep:base64", "dep:rustls", "dep:percent-encoding", "dep:rustfs-tls-runtime", "dep:subtle"]
|
||||
sftp = ["dep:russh", "dep:russh-sftp", "dep:uuid", "dep:subtle", "dep:tokio-util", "dep:socket2"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
use async_trait::async_trait;
|
||||
use s3s::dto::*;
|
||||
|
||||
#[cfg(feature = "webdav")]
|
||||
use crate::common::session::SessionContext;
|
||||
|
||||
#[async_trait]
|
||||
pub trait StorageBackend: Send + Sync {
|
||||
/// Error type for this storage backend
|
||||
@@ -65,8 +68,24 @@ pub trait StorageBackend: Send + Sync {
|
||||
access_key: &str,
|
||||
secret_key: &str,
|
||||
) -> Result<ListObjectsV2Output, Self::Error>;
|
||||
/// List all buckets (requires authentication)
|
||||
/// List all buckets (requires authentication).
|
||||
async fn list_buckets(&self, access_key: &str, secret_key: &str) -> Result<ListBucketsOutput, Self::Error>;
|
||||
/// List buckets visible to the authenticated session.
|
||||
///
|
||||
/// Backends that implement this must apply per-bucket authorization. The default denies the
|
||||
/// request so existing backends cannot expose unfiltered bucket names.
|
||||
#[cfg(feature = "webdav")]
|
||||
async fn list_buckets_for_session(
|
||||
&self,
|
||||
_session_context: &SessionContext,
|
||||
_request_headers: &http::HeaderMap,
|
||||
_secure_transport: bool,
|
||||
) -> s3s::S3Result<ListBucketsOutput> {
|
||||
Err(s3s::S3Error::with_message(
|
||||
s3s::S3ErrorCode::AccessDenied,
|
||||
"Session-aware bucket listing is not supported",
|
||||
))
|
||||
}
|
||||
/// Create a new bucket
|
||||
async fn create_bucket(&self, bucket: &str, access_key: &str, secret_key: &str) -> Result<CreateBucketOutput, Self::Error>;
|
||||
/// Delete a bucket (must be empty)
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
//! SessionContext type in common::session.
|
||||
|
||||
use crate::common::client::s3::StorageBackend;
|
||||
#[cfg(feature = "webdav")]
|
||||
use crate::common::session::SessionContext;
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use futures_util::stream::{self, StreamExt};
|
||||
@@ -140,6 +142,8 @@ struct Inner {
|
||||
head_bucket: VecDeque<Result<HeadBucketOutput, DummyError>>,
|
||||
list_objects_v2: VecDeque<Result<ListObjectsV2Output, DummyError>>,
|
||||
list_buckets: VecDeque<Result<ListBucketsOutput, DummyError>>,
|
||||
session_list_buckets: VecDeque<s3s::S3Result<ListBucketsOutput>>,
|
||||
last_session_list_context: Option<(http::HeaderMap, bool)>,
|
||||
create_bucket: VecDeque<Result<CreateBucketOutput, DummyError>>,
|
||||
delete_bucket: VecDeque<Result<DeleteBucketOutput, DummyError>>,
|
||||
copy_object: VecDeque<Result<CopyObjectOutput, DummyError>>,
|
||||
@@ -193,6 +197,8 @@ impl Inner {
|
||||
head_bucket: VecDeque::new(),
|
||||
list_objects_v2: VecDeque::new(),
|
||||
list_buckets: VecDeque::new(),
|
||||
session_list_buckets: VecDeque::new(),
|
||||
last_session_list_context: None,
|
||||
create_bucket: VecDeque::new(),
|
||||
delete_bucket: VecDeque::new(),
|
||||
copy_object: VecDeque::new(),
|
||||
@@ -301,6 +307,31 @@ impl DummyBackend {
|
||||
.push_back(Ok(CreateBucketOutput::default()));
|
||||
}
|
||||
|
||||
/// Queue a legacy list_buckets response.
|
||||
pub fn queue_list_buckets_ok(&self, output: ListBucketsOutput) {
|
||||
self.inner.lock().expect("lock").list_buckets.push_back(Ok(output));
|
||||
}
|
||||
|
||||
/// Queue a legacy list_buckets error.
|
||||
pub fn queue_list_buckets_err(&self, error: DummyError) {
|
||||
self.inner.lock().expect("lock").list_buckets.push_back(Err(error));
|
||||
}
|
||||
|
||||
/// Queue a session-aware list_buckets response.
|
||||
pub fn queue_session_list_buckets_ok(&self, output: ListBucketsOutput) {
|
||||
self.inner.lock().expect("lock").session_list_buckets.push_back(Ok(output));
|
||||
}
|
||||
|
||||
/// Queue a session-aware list_buckets error.
|
||||
pub fn queue_session_list_buckets_err(&self, error: s3s::S3Error) {
|
||||
self.inner.lock().expect("lock").session_list_buckets.push_back(Err(error));
|
||||
}
|
||||
|
||||
/// Return the context from the last session-aware list_buckets request.
|
||||
pub fn last_session_list_context(&self) -> Option<(http::HeaderMap, bool)> {
|
||||
self.inner.lock().expect("lock").last_session_list_context.clone()
|
||||
}
|
||||
|
||||
/// Queue a put_object error. Used by the commit_write retry tests
|
||||
/// to script SlowDown / AccessDenied sequences against the
|
||||
/// rustfs_utils::retry::is_s3code_in_message_retryable predicate.
|
||||
@@ -690,13 +721,29 @@ impl StorageBackend for DummyBackend {
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_buckets(&self, _ak: &str, _sk: &str) -> Result<ListBucketsOutput, Self::Error> {
|
||||
async fn list_buckets(&self, _access_key: &str, _secret_key: &str) -> Result<ListBucketsOutput, Self::Error> {
|
||||
match self.inner.lock().expect("lock").list_buckets.pop_front() {
|
||||
Some(r) => r,
|
||||
None => Ok(ListBucketsOutput::default()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "webdav")]
|
||||
async fn list_buckets_for_session(
|
||||
&self,
|
||||
session_context: &SessionContext,
|
||||
request_headers: &http::HeaderMap,
|
||||
secure_transport: bool,
|
||||
) -> s3s::S3Result<ListBucketsOutput> {
|
||||
let _ = session_context;
|
||||
let mut inner = self.inner.lock().expect("lock");
|
||||
inner.last_session_list_context = Some((request_headers.clone(), secure_transport));
|
||||
inner
|
||||
.session_list_buckets
|
||||
.pop_front()
|
||||
.unwrap_or_else(|| Ok(ListBucketsOutput::default()))
|
||||
}
|
||||
|
||||
async fn create_bucket(&self, _bucket: &str, _ak: &str, _sk: &str) -> Result<CreateBucketOutput, Self::Error> {
|
||||
match self.inner.lock().expect("lock").create_bucket.pop_front() {
|
||||
Some(r) => r,
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use crate::common::client::s3::StorageBackend as S3StorageBackend;
|
||||
use crate::common::gateway::{S3Action, authorize_operation};
|
||||
use crate::common::gateway::{AuthorizationError, S3Action, authorize_operation};
|
||||
use crate::common::session::SessionContext;
|
||||
use bytes::Bytes;
|
||||
use dav_server::davpath::DavPath;
|
||||
@@ -24,6 +24,7 @@ use futures_util::{FutureExt, StreamExt, stream};
|
||||
use percent_encoding::percent_decode_str;
|
||||
use rustfs_utils::MaskedAccessKey;
|
||||
use rustfs_utils::path;
|
||||
use s3s::S3ErrorCode;
|
||||
use s3s::dto::*;
|
||||
use std::fmt::Debug;
|
||||
use std::io::SeekFrom;
|
||||
@@ -457,6 +458,10 @@ where
|
||||
storage: S,
|
||||
/// Session context for authorization
|
||||
session_context: Arc<SessionContext>,
|
||||
/// Policy-safe WebDAV request headers used by IAM conditions.
|
||||
request_headers: Option<http::HeaderMap>,
|
||||
/// Whether the WebDAV connection uses TLS.
|
||||
secure_transport: bool,
|
||||
}
|
||||
|
||||
enum ResolvedPath {
|
||||
@@ -490,6 +495,8 @@ where
|
||||
Self {
|
||||
storage: self.storage.clone(),
|
||||
session_context: self.session_context.clone(),
|
||||
request_headers: self.request_headers.clone(),
|
||||
secure_transport: self.secure_transport,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -503,9 +510,18 @@ where
|
||||
Self {
|
||||
storage,
|
||||
session_context,
|
||||
request_headers: None,
|
||||
secure_transport: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach the request context used by IAM policy conditions.
|
||||
pub fn with_request_context(mut self, request_headers: http::HeaderMap, secure_transport: bool) -> Self {
|
||||
self.request_headers = Some(request_headers);
|
||||
self.secure_transport = secure_transport;
|
||||
self
|
||||
}
|
||||
|
||||
fn credentials(&self) -> (&str, &str) {
|
||||
(
|
||||
&self.session_context.principal.user_identity.credentials.access_key,
|
||||
@@ -799,50 +815,41 @@ where
|
||||
/// List all buckets (for root path)
|
||||
async fn list_buckets(&self) -> FsResult<Vec<WebDavDirEntry>> {
|
||||
match authorize_operation(&self.session_context, &S3Action::ListBuckets, "", None).await {
|
||||
Ok(_) => {}
|
||||
Err(_e) => {
|
||||
return Err(FsError::Forbidden);
|
||||
Ok(()) => {
|
||||
let (access_key, secret_key) = self.credentials();
|
||||
return match self.storage.list_buckets(access_key, secret_key).await {
|
||||
Ok(output) => Ok(Self::bucket_entries(output)),
|
||||
Err(error) => {
|
||||
error!(
|
||||
event = EVENT_WEBDAV_BUCKET_LIST_FAILED,
|
||||
component = LOG_COMPONENT_PROTOCOLS,
|
||||
subsystem = LOG_SUBSYSTEM_WEBDAV_DRIVER,
|
||||
error = %error,
|
||||
access_key = %MaskedAccessKey(access_key),
|
||||
"webdav bucket list failed"
|
||||
);
|
||||
Err(FsError::GeneralFailure)
|
||||
}
|
||||
};
|
||||
}
|
||||
Err(AuthorizationError::AccessDenied) => {}
|
||||
Err(AuthorizationError::IamUnavailable) => return Err(FsError::GeneralFailure),
|
||||
}
|
||||
|
||||
match self
|
||||
let Some(request_headers) = self.request_headers.as_ref() else {
|
||||
return Err(FsError::Forbidden);
|
||||
};
|
||||
let result = self
|
||||
.storage
|
||||
.list_buckets(
|
||||
&self.session_context.principal.user_identity.credentials.access_key,
|
||||
&self.session_context.principal.user_identity.credentials.secret_key,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(output) => {
|
||||
let mut entries = Vec::new();
|
||||
if let Some(buckets) = output.buckets {
|
||||
for bucket in buckets {
|
||||
if let Some(ref bucket_name) = bucket.name {
|
||||
let modified = bucket
|
||||
.creation_date
|
||||
.map(|dt| {
|
||||
let offset_dt: time::OffsetDateTime = dt.into();
|
||||
SystemTime::from(offset_dt)
|
||||
})
|
||||
.unwrap_or_else(SystemTime::now);
|
||||
.list_buckets_for_session(&self.session_context, request_headers, self.secure_transport)
|
||||
.await;
|
||||
|
||||
entries.push(WebDavDirEntry {
|
||||
name: bucket_name.clone(),
|
||||
metadata: WebDavMetaData {
|
||||
size: 0,
|
||||
modified,
|
||||
created: modified,
|
||||
is_dir: true,
|
||||
etag: None,
|
||||
content_type: None,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
match result {
|
||||
Ok(output) => Ok(Self::bucket_entries(output)),
|
||||
Err(e) => {
|
||||
if matches!(e.code(), S3ErrorCode::AccessDenied) {
|
||||
return Err(FsError::Forbidden);
|
||||
}
|
||||
error!(
|
||||
event = EVENT_WEBDAV_BUCKET_LIST_FAILED,
|
||||
component = LOG_COMPONENT_PROTOCOLS,
|
||||
@@ -856,6 +863,35 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn bucket_entries(output: ListBucketsOutput) -> Vec<WebDavDirEntry> {
|
||||
output
|
||||
.buckets
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter_map(|bucket| {
|
||||
let name = bucket.name?;
|
||||
let modified = bucket
|
||||
.creation_date
|
||||
.map(|date| {
|
||||
let date: time::OffsetDateTime = date.into();
|
||||
SystemTime::from(date)
|
||||
})
|
||||
.unwrap_or_else(SystemTime::now);
|
||||
Some(WebDavDirEntry {
|
||||
name,
|
||||
metadata: WebDavMetaData {
|
||||
size: 0,
|
||||
modified,
|
||||
created: modified,
|
||||
is_dir: true,
|
||||
etag: None,
|
||||
content_type: None,
|
||||
},
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// List objects in a bucket
|
||||
async fn list_objects(&self, bucket: &str, prefix: Option<&str>) -> FsResult<Vec<WebDavDirEntry>> {
|
||||
// Authorize the operation
|
||||
@@ -1715,8 +1751,9 @@ where
|
||||
mod tests {
|
||||
use super::WebDavDriver;
|
||||
use crate::common::client::s3::StorageBackend as S3StorageBackend;
|
||||
use crate::common::gateway::{S3Action, with_test_auth_override};
|
||||
use crate::common::session::{Protocol, ProtocolPrincipal, SessionContext};
|
||||
use crate::common::dummy_storage::DummyBackend;
|
||||
use crate::common::gateway::{S3Action, with_test_auth_override, with_test_iam_unavailable};
|
||||
use crate::common::session::{Protocol, ProtocolPrincipal, SessionContext, test_session};
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use dav_server::davpath::DavPath;
|
||||
@@ -1906,6 +1943,134 @@ mod tests {
|
||||
WebDavDriver::new(DummyStorage, Arc::new(session_context))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_bucket_listing_does_not_require_global_list_permission() {
|
||||
let storage = DummyBackend::new();
|
||||
storage.queue_session_list_buckets_ok(ListBucketsOutput {
|
||||
buckets: Some(vec![Bucket {
|
||||
name: Some("allowed-bucket".to_string()),
|
||||
..Default::default()
|
||||
}]),
|
||||
..Default::default()
|
||||
});
|
||||
let driver = WebDavDriver::new(storage.clone(), Arc::new(test_session(Protocol::WebDav))).with_request_context(
|
||||
http::HeaderMap::from_iter([(http::header::USER_AGENT, http::HeaderValue::from_static("webdav-test"))]),
|
||||
false,
|
||||
);
|
||||
|
||||
let entries = with_test_auth_override(|_, _, _| false, driver.list_buckets())
|
||||
.await
|
||||
.expect("session-aware backend should own bucket filtering");
|
||||
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert_eq!(entries[0].name, "allowed-bucket");
|
||||
let (headers, secure_transport) = storage
|
||||
.last_session_list_context()
|
||||
.expect("request context should be forwarded");
|
||||
assert_eq!(headers.get("user-agent").expect("user agent"), "webdav-test");
|
||||
assert!(!secure_transport);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_buckets_maps_typed_access_denied_to_forbidden() {
|
||||
let storage = DummyBackend::new();
|
||||
storage.queue_session_list_buckets_err(s3s::S3Error::with_message(s3s::S3ErrorCode::AccessDenied, "policy denied"));
|
||||
let driver = WebDavDriver::new(storage, Arc::new(test_session(Protocol::WebDav)))
|
||||
.with_request_context(http::HeaderMap::new(), false);
|
||||
|
||||
let error = with_test_auth_override(|_, _, _| false, driver.list_buckets())
|
||||
.await
|
||||
.expect_err("bucket listing should be denied");
|
||||
|
||||
assert!(matches!(error, FsError::Forbidden));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_buckets_does_not_classify_error_text_as_access_denied() {
|
||||
let storage = DummyBackend::new();
|
||||
storage.queue_session_list_buckets_err(s3s::S3Error::with_message(
|
||||
s3s::S3ErrorCode::InternalError,
|
||||
"AccessDenied appears only in the message",
|
||||
));
|
||||
let driver = WebDavDriver::new(storage, Arc::new(test_session(Protocol::WebDav)))
|
||||
.with_request_context(http::HeaderMap::new(), false);
|
||||
|
||||
let error = with_test_auth_override(|_, _, _| false, driver.list_buckets())
|
||||
.await
|
||||
.expect_err("bucket listing should fail");
|
||||
|
||||
assert!(matches!(error, FsError::GeneralFailure));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn global_list_permission_keeps_the_legacy_backend_path() {
|
||||
let storage = DummyBackend::new();
|
||||
storage.queue_list_buckets_ok(ListBucketsOutput {
|
||||
buckets: Some(vec![Bucket {
|
||||
name: Some("legacy-bucket".to_string()),
|
||||
..Default::default()
|
||||
}]),
|
||||
..Default::default()
|
||||
});
|
||||
let driver = WebDavDriver::new(storage.clone(), Arc::new(test_session(Protocol::WebDav)));
|
||||
|
||||
let entries = with_test_auth_override(|_, _, _| true, driver.list_buckets())
|
||||
.await
|
||||
.expect("globally authorized legacy backend should keep working");
|
||||
|
||||
assert_eq!(entries[0].name, "legacy-bucket");
|
||||
assert!(storage.last_session_list_context().is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn legacy_bucket_list_error_is_a_general_failure() {
|
||||
let storage = DummyBackend::new();
|
||||
storage.queue_list_buckets_err(crate::common::dummy_storage::DummyError::Injected("backend failed".to_string()));
|
||||
let driver = WebDavDriver::new(storage.clone(), Arc::new(test_session(Protocol::WebDav)));
|
||||
|
||||
let error = with_test_auth_override(|_, _, _| true, driver.list_buckets())
|
||||
.await
|
||||
.expect_err("legacy backend error should fail the listing");
|
||||
|
||||
assert!(matches!(error, FsError::GeneralFailure));
|
||||
assert!(storage.last_session_list_context().is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn iam_unavailable_does_not_enter_the_session_fallback() {
|
||||
let storage = DummyBackend::new();
|
||||
storage.queue_session_list_buckets_ok(ListBucketsOutput::default());
|
||||
let driver = WebDavDriver::new(storage.clone(), Arc::new(test_session(Protocol::WebDav)))
|
||||
.with_request_context(http::HeaderMap::new(), false);
|
||||
|
||||
let error = with_test_iam_unavailable(driver.list_buckets())
|
||||
.await
|
||||
.expect_err("IAM outage must fail closed");
|
||||
|
||||
assert!(matches!(error, FsError::GeneralFailure));
|
||||
assert!(storage.last_session_list_context().is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn missing_request_context_fails_closed() {
|
||||
let error = with_test_auth_override(|_, _, _| false, driver().list_buckets())
|
||||
.await
|
||||
.expect_err("bucket listing should require the original request context");
|
||||
|
||||
assert!(matches!(error, FsError::Forbidden));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn backend_without_session_listing_fails_closed() {
|
||||
let driver = driver().with_request_context(http::HeaderMap::new(), false);
|
||||
|
||||
let error = with_test_auth_override(|_, _, _| false, driver.list_buckets())
|
||||
.await
|
||||
.expect_err("default session listing must deny the request");
|
||||
|
||||
assert!(matches!(error, FsError::Forbidden));
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct RecordingStorageState {
|
||||
objects: HashMap<(String, String), Vec<u8>>,
|
||||
|
||||
@@ -19,6 +19,8 @@ use crate::common::session::{Protocol, ProtocolPrincipal, SessionContext, is_tem
|
||||
use bytes::Bytes;
|
||||
use dav_server::DavHandler;
|
||||
use dav_server::fakels::FakeLs;
|
||||
use http::header::{AUTHORIZATION, REFERER, USER_AGENT};
|
||||
use http::{HeaderMap, HeaderValue};
|
||||
use http_body_util::{BodyExt, Full, LengthLimitError, Limited};
|
||||
use hyper::body::Body as HttpBody;
|
||||
use hyper::server::conn::http1;
|
||||
@@ -59,6 +61,20 @@ const EVENT_WEBDAV_CONNECTION_CAP_STATE: &str = "webdav_connection_cap_state";
|
||||
/// materialise a whole object in memory for every GET.
|
||||
type WebDavBody = Pin<Box<dyn HttpBody<Data = Bytes, Error = io::Error> + Send>>;
|
||||
|
||||
fn policy_request_headers(headers: &HeaderMap) -> HeaderMap {
|
||||
let mut policy_headers = HeaderMap::new();
|
||||
for name in [USER_AGENT, REFERER] {
|
||||
if let Some(value) = headers.get(&name) {
|
||||
policy_headers.insert(name, value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let mut authorization = HeaderValue::from_static("Basic");
|
||||
authorization.set_sensitive(true);
|
||||
policy_headers.insert(AUTHORIZATION, authorization);
|
||||
policy_headers
|
||||
}
|
||||
|
||||
/// WebDAV server implementation
|
||||
pub struct WebDavServer<S>
|
||||
where
|
||||
@@ -216,7 +232,7 @@ where
|
||||
match timeout(request_timeout, acceptor.accept(stream)).await {
|
||||
Ok(Ok(tls_stream)) => {
|
||||
let io = TokioIo::new(tls_stream);
|
||||
if let Err(e) = Self::handle_connection_impl(io, storage, source_ip, max_body_size, request_timeout).await {
|
||||
if let Err(e) = Self::handle_connection_impl(io, storage, source_ip, true, max_body_size, request_timeout).await {
|
||||
debug!(
|
||||
event = EVENT_WEBDAV_CONNECTION_STATE,
|
||||
component = LOG_COMPONENT_PROTOCOLS,
|
||||
@@ -254,7 +270,7 @@ where
|
||||
}
|
||||
} else {
|
||||
let io = TokioIo::new(stream);
|
||||
if let Err(e) = Self::handle_connection_impl(io, storage, source_ip, max_body_size, request_timeout).await {
|
||||
if let Err(e) = Self::handle_connection_impl(io, storage, source_ip, false, max_body_size, request_timeout).await {
|
||||
debug!(
|
||||
event = EVENT_WEBDAV_CONNECTION_STATE,
|
||||
component = LOG_COMPONENT_PROTOCOLS,
|
||||
@@ -313,6 +329,7 @@ where
|
||||
io: TokioIo<I>,
|
||||
storage: S,
|
||||
source_ip: IpAddr,
|
||||
secure_transport: bool,
|
||||
max_body_size: u64,
|
||||
request_timeout: Duration,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
|
||||
@@ -321,7 +338,7 @@ where
|
||||
{
|
||||
let service = service_fn(move |req: Request<hyper::body::Incoming>| {
|
||||
let storage = storage.clone();
|
||||
async move { Self::handle_request(req, storage, source_ip, max_body_size, request_timeout).await }
|
||||
async move { Self::handle_request(req, storage, source_ip, secure_transport, max_body_size, request_timeout).await }
|
||||
});
|
||||
|
||||
// A peer that opens a connection and dribbles (or never finishes)
|
||||
@@ -341,6 +358,7 @@ where
|
||||
req: Request<hyper::body::Incoming>,
|
||||
storage: S,
|
||||
source_ip: IpAddr,
|
||||
secure_transport: bool,
|
||||
max_body_size: u64,
|
||||
request_timeout: Duration,
|
||||
) -> Result<Response<WebDavBody>, Infallible> {
|
||||
@@ -398,7 +416,8 @@ where
|
||||
};
|
||||
|
||||
// Create WebDAV driver with session context
|
||||
let driver = WebDavDriver::new(storage, Arc::new(session_context));
|
||||
let driver = WebDavDriver::new(storage, Arc::new(session_context))
|
||||
.with_request_context(policy_request_headers(req.headers()), secure_transport);
|
||||
|
||||
// Build DAV handler with boxed filesystem
|
||||
let dav_handler = DavHandler::builder()
|
||||
@@ -883,6 +902,30 @@ mod tests {
|
||||
.expect("build get request")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn policy_headers_drop_credentials_and_s3_auth_spoofing() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(AUTHORIZATION, HeaderValue::from_static("Basic dXNlcjpwYXNzd29yZA=="));
|
||||
headers.insert(USER_AGENT, HeaderValue::from_static("webdav-client"));
|
||||
headers.insert(REFERER, HeaderValue::from_static("https://example.test/"));
|
||||
headers.insert("x-amz-content-sha256", HeaderValue::from_static("STREAMING-AWS4-HMAC-SHA256-PAYLOAD"));
|
||||
headers.insert("x-amz-signature-age", HeaderValue::from_static("0"));
|
||||
|
||||
let policy_headers = policy_request_headers(&headers);
|
||||
|
||||
assert_eq!(policy_headers.get(AUTHORIZATION).expect("authorization marker"), "Basic");
|
||||
assert!(
|
||||
policy_headers
|
||||
.get(AUTHORIZATION)
|
||||
.expect("authorization marker")
|
||||
.is_sensitive()
|
||||
);
|
||||
assert_eq!(policy_headers.get(USER_AGENT).expect("user agent"), "webdav-client");
|
||||
assert_eq!(policy_headers.get(REFERER).expect("referer"), "https://example.test/");
|
||||
assert!(!policy_headers.contains_key("x-amz-content-sha256"));
|
||||
assert!(!policy_headers.contains_key("x-amz-signature-age"));
|
||||
}
|
||||
|
||||
/// R03-CAN-051 / R03-CAN-067 / R05-CAN-094: a chunked upload declares no
|
||||
/// Content-Length, so the limit has to hold on the bytes actually read.
|
||||
#[tokio::test]
|
||||
@@ -959,6 +1002,7 @@ mod tests {
|
||||
TokioIo::new(server),
|
||||
StubStorage,
|
||||
TEST_IP,
|
||||
false,
|
||||
1024,
|
||||
Duration::from_secs(30),
|
||||
));
|
||||
|
||||
Reference in New Issue
Block a user