// 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 super::config::{WebDavConfig, WebDavInitError}; use super::driver::WebDavDriver; use crate::common::client::s3::StorageBackend; use crate::common::session::{Protocol, ProtocolPrincipal, SessionContext}; use bytes::Bytes; use dav_server::DavHandler; use dav_server::fakels::FakeLs; use http_body_util::{BodyExt, Full}; use hyper::server::conn::http1; use hyper::service::service_fn; use hyper::{Request, Response, StatusCode}; use hyper_util::rt::TokioIo; use rustfs_config::{RUSTFS_TLS_CERT, RUSTFS_TLS_KEY}; use rustls::ServerConfig; use std::convert::Infallible; use std::net::IpAddr; use std::sync::Arc; use tokio::net::TcpListener; use tokio::sync::broadcast; use tokio_rustls::TlsAcceptor; use tracing::{debug, error, info, warn}; /// WebDAV server implementation pub struct WebDavServer where S: StorageBackend + Clone + Send + Sync + 'static + std::fmt::Debug, { /// Server configuration config: WebDavConfig, /// S3 storage backend storage: S, } impl WebDavServer where S: StorageBackend + Clone + Send + Sync + 'static + std::fmt::Debug, { /// Create a new WebDAV server pub async fn new(config: WebDavConfig, storage: S) -> Result { config.validate().await?; Ok(Self { config, storage }) } /// Start the WebDAV server pub async fn start(&self, mut shutdown_rx: broadcast::Receiver<()>) -> Result<(), WebDavInitError> { info!("Initializing WebDAV server on {}", self.config.bind_addr); let listener = TcpListener::bind(self.config.bind_addr).await?; info!("WebDAV server listening on {}", self.config.bind_addr); // Setup TLS if enabled let tls_acceptor = if self.config.tls_enabled { if let Some(cert_dir) = &self.config.cert_dir { debug!("Enabling WebDAV TLS with certificates from: {}", cert_dir); let cert_key_pairs = rustfs_utils::load_all_certs_from_directory( rustfs_utils::CertDirectoryLoadOptions::builder(cert_dir, RUSTFS_TLS_CERT, RUSTFS_TLS_KEY).build(), ) .map_err(|e| WebDavInitError::Tls(format!("Failed to load certificates: {}", e)))?; if cert_key_pairs.is_empty() { return Err(WebDavInitError::InvalidConfig("No valid certificates found".into())); } let resolver = rustfs_utils::create_multi_cert_resolver(cert_key_pairs) .map_err(|e| WebDavInitError::Tls(format!("Failed to create certificate resolver: {}", e)))?; let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); let server_config = ServerConfig::builder() .with_no_client_auth() .with_cert_resolver(Arc::new(resolver)); Some(TlsAcceptor::from(Arc::new(server_config))) } else { None } } else { None }; let storage = self.storage.clone(); loop { tokio::select! { accept_result = listener.accept() => { match accept_result { Ok((stream, addr)) => { let storage = storage.clone(); let tls_acceptor = tls_acceptor.clone(); let max_body_size = self.config.max_body_size; tokio::spawn(async move { let source_ip: IpAddr = addr.ip(); if let Some(acceptor) = tls_acceptor { match acceptor.accept(stream).await { Ok(tls_stream) => { let io = TokioIo::new(tls_stream); if let Err(e) = Self::handle_connection_impl(io, storage, source_ip, max_body_size).await { debug!("Connection error: {}", e); } } Err(e) => { debug!("TLS handshake failed: {}", e); } } } else { let io = TokioIo::new(stream); if let Err(e) = Self::handle_connection_impl(io, storage, source_ip, max_body_size).await { debug!("Connection error: {}", e); } } }); } Err(e) => { error!("Failed to accept connection: {}", e); } } } _ = shutdown_rx.recv() => { info!("WebDAV server received shutdown signal"); break; } } } info!("WebDAV server stopped"); Ok(()) } /// Handle a single connection with hyper-util TokioIo wrapper async fn handle_connection_impl( io: TokioIo, storage: S, source_ip: IpAddr, max_body_size: u64, ) -> Result<(), Box> where I: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static, { let service = service_fn(move |req: Request| { let storage = storage.clone(); async move { Self::handle_request(req, storage, source_ip, max_body_size).await } }); http1::Builder::new().serve_connection(io, service).await?; Ok(()) } /// Handle a single WebDAV request async fn handle_request( req: Request, storage: S, source_ip: IpAddr, max_body_size: u64, ) -> Result>, Infallible> { // Check Content-Length against max_body_size before reading body if let Some(content_length) = req.headers().get("content-length") && let Ok(length_str) = content_length.to_str() && let Ok(length) = length_str.parse::() && length > max_body_size { warn!("Request body too large: {} > {}", length, max_body_size); return Ok(error_response( StatusCode::PAYLOAD_TOO_LARGE, &format!("Request body too large. Maximum size is {} bytes", max_body_size), )); } // Extract authorization header let auth_header = req.headers().get("authorization").and_then(|h| h.to_str().ok()); // Parse Basic auth credentials let (access_key, secret_key) = match auth_header { Some(auth) if auth.starts_with("Basic ") => { let encoded = &auth[6..]; match base64_decode(encoded) { Ok(decoded) => { let decoded_str = String::from_utf8_lossy(&decoded); if let Some((user, pass)) = decoded_str.split_once(':') { (user.to_string(), pass.to_string()) } else { return Ok(unauthorized_response()); } } Err(_) => return Ok(unauthorized_response()), } } _ => return Ok(unauthorized_response()), }; // Authenticate user let session_context = match Self::authenticate(&access_key, &secret_key, source_ip).await { Ok(ctx) => ctx, Err(_) => return Ok(unauthorized_response()), }; // Create WebDAV driver with session context let driver = WebDavDriver::new(storage, Arc::new(session_context)); // Build DAV handler with boxed filesystem let dav_handler = DavHandler::builder() .filesystem(Box::new(driver)) .locksystem(FakeLs::new()) .build_handler(); // Convert request body let (parts, body) = req.into_parts(); let body_bytes = match body.collect().await { Ok(collected) => collected.to_bytes(), Err(e) => { error!("Failed to read request body: {}", e); return Ok(error_response(StatusCode::BAD_REQUEST, "Failed to read request body")); } }; // Create request for dav-server using Bytes let dav_req = Request::from_parts(parts, dav_server::body::Body::from(body_bytes)); // Handle the request let dav_resp = dav_handler.handle(dav_req).await; // Convert response let (parts, body) = dav_resp.into_parts(); let body_bytes = match body.collect().await { Ok(collected) => collected.to_bytes(), Err(e) => { error!("Failed to read response body: {}", e); return Ok(error_response(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error")); } }; Ok(Response::from_parts(parts, Full::new(body_bytes))) } /// Authenticate user against IAM system async fn authenticate(access_key: &str, secret_key: &str, source_ip: IpAddr) -> Result { use rustfs_credentials::Credentials as S3Credentials; use rustfs_iam::get; // Access IAM system let iam_sys = get().map_err(|e| { error!("IAM system unavailable during WebDAV auth: {}", e); WebDavInitError::Server("Internal authentication service unavailable".to_string()) })?; let s3_creds = S3Credentials { access_key: access_key.to_string(), secret_key: secret_key.to_string(), session_token: String::new(), expiration: None, status: String::new(), parent_user: String::new(), groups: None, claims: None, name: None, description: None, }; let (user_identity, is_valid) = iam_sys.check_key(&s3_creds.access_key).await.map_err(|e| { error!("IAM check_key failed for {}: {}", access_key, e); WebDavInitError::Server("Authentication verification failed".to_string()) })?; if !is_valid { warn!("WebDAV login failed: Invalid access key '{}'", access_key); return Err(WebDavInitError::Server("Invalid credentials".to_string())); } let identity = user_identity.ok_or_else(|| { error!("User identity missing despite valid key for {}", access_key); WebDavInitError::Server("User not found".to_string()) })?; if !identity.credentials.secret_key.eq(&s3_creds.secret_key) { warn!("WebDAV login failed: Invalid secret key for '{}'", access_key); return Err(WebDavInitError::Server("Invalid credentials".to_string())); } info!("WebDAV user '{}' authenticated successfully", access_key); Ok(SessionContext::new( ProtocolPrincipal::new(Arc::new(identity)), Protocol::WebDav, source_ip, )) } /// Get server configuration pub fn config(&self) -> &WebDavConfig { &self.config } /// Get storage backend pub fn storage(&self) -> &S { &self.storage } } /// Create unauthorized response with WWW-Authenticate header fn unauthorized_response() -> Response> { Response::builder() .status(StatusCode::UNAUTHORIZED) .header("WWW-Authenticate", "Basic realm=\"RustFS WebDAV\"") .body(Full::new(Bytes::from("Unauthorized"))) .unwrap_or_else(|_| Response::new(Full::new(Bytes::from("Unauthorized")))) } /// Create error response fn error_response(status: StatusCode, message: &str) -> Response> { Response::builder() .status(status) .body(Full::new(Bytes::from(message.to_string()))) .unwrap_or_else(|_| Response::new(Full::new(Bytes::from("Internal Server Error")))) } /// Decode base64 string fn base64_decode(encoded: &str) -> Result, ()> { use base64::Engine; base64::engine::general_purpose::STANDARD.decode(encoded).map_err(|_| ()) }