mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-10 15:16:56 +00:00
feat(webdav): add WebDAV protocol gateway (#2158)
Signed-off-by: yxrxy <1532529704@qq.com> Co-authored-by: houseme <housemecn@gmail.com> Co-authored-by: 马登山 <Cxymds@qq.com> Co-authored-by: heihutu <30542132+heihutu@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: 安正超 <anzhengchao@gmail.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Protocol E2E Tests
|
||||
|
||||
FTPS protocol end-to-end tests for RustFS.
|
||||
FTPS and WebDAV protocol end-to-end tests for RustFS.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -19,11 +19,21 @@ brew install sshpass openssh
|
||||
|
||||
## Running Tests
|
||||
|
||||
Run all protocol tests:
|
||||
Run all protocol tests (FTPS + WebDAV):
|
||||
```bash
|
||||
RUSTFS_BUILD_FEATURES=ftps,webdav cargo test --package e2e_test test_protocol_core_suite -- --test-threads=1 --nocapture
|
||||
```
|
||||
|
||||
Run FTPS tests only:
|
||||
```bash
|
||||
RUSTFS_BUILD_FEATURES=ftps cargo test --package e2e_test test_protocol_core_suite -- --test-threads=1 --nocapture
|
||||
```
|
||||
|
||||
Run WebDAV tests only:
|
||||
```bash
|
||||
RUSTFS_BUILD_FEATURES=webdav cargo test --package e2e_test test_protocol_core_suite -- --test-threads=1 --nocapture
|
||||
```
|
||||
|
||||
## Test Coverage
|
||||
|
||||
### FTPS Tests
|
||||
@@ -38,3 +48,13 @@ RUSTFS_BUILD_FEATURES=ftps cargo test --package e2e_test test_protocol_core_suit
|
||||
- cdup
|
||||
- rmdir delete bucket
|
||||
|
||||
### WebDAV Tests
|
||||
- PROPFIND at root (list buckets)
|
||||
- MKCOL (create bucket)
|
||||
- PUT (upload file)
|
||||
- GET (download file)
|
||||
- PROPFIND on bucket (list objects)
|
||||
- DELETE file
|
||||
- DELETE bucket
|
||||
- Authentication failure test
|
||||
|
||||
|
||||
@@ -12,8 +12,9 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Protocol tests for FTPS
|
||||
//! Protocol tests for FTPS and WebDAV
|
||||
|
||||
pub mod ftps_core;
|
||||
pub mod test_env;
|
||||
pub mod test_runner;
|
||||
pub mod webdav_core;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
use crate::common::init_logging;
|
||||
use crate::protocols::ftps_core::test_ftps_core_operations;
|
||||
use crate::protocols::webdav_core::test_webdav_core_operations;
|
||||
use std::time::Instant;
|
||||
use tokio::time::{Duration, sleep};
|
||||
use tracing::{error, info};
|
||||
@@ -59,9 +60,14 @@ struct TestDefinition {
|
||||
impl ProtocolTestSuite {
|
||||
/// Create default test suite
|
||||
pub fn new() -> Self {
|
||||
let tests = vec![TestDefinition {
|
||||
name: "test_ftps_core_operations".to_string(),
|
||||
}];
|
||||
let tests = vec![
|
||||
TestDefinition {
|
||||
name: "test_ftps_core_operations".to_string(),
|
||||
},
|
||||
TestDefinition {
|
||||
name: "test_webdav_core_operations".to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
Self { tests }
|
||||
}
|
||||
@@ -83,6 +89,10 @@ impl ProtocolTestSuite {
|
||||
info!("=== Starting FTPS Module Test ===");
|
||||
"FTPS core operations (put, ls, mkdir, rmdir, delete)"
|
||||
}
|
||||
"test_webdav_core_operations" => {
|
||||
info!("=== Starting WebDAV Core Test ===");
|
||||
"WebDAV core operations (MKCOL, PUT, GET, DELETE, PROPFIND)"
|
||||
}
|
||||
_ => "",
|
||||
};
|
||||
|
||||
@@ -121,6 +131,7 @@ impl ProtocolTestSuite {
|
||||
async fn run_single_test(&self, test_def: &TestDefinition) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
match test_def.name.as_str() {
|
||||
"test_ftps_core_operations" => test_ftps_core_operations().await.map_err(|e| e.into()),
|
||||
"test_webdav_core_operations" => test_webdav_core_operations().await.map_err(|e| e.into()),
|
||||
_ => Err(format!("Test {} not implemented", test_def.name).into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
// 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.
|
||||
|
||||
//! Core WebDAV tests
|
||||
|
||||
use crate::common::rustfs_binary_path;
|
||||
use crate::protocols::test_env::{DEFAULT_ACCESS_KEY, DEFAULT_SECRET_KEY, ProtocolTestEnvironment};
|
||||
use anyhow::Result;
|
||||
use base64::Engine;
|
||||
use reqwest::Client;
|
||||
use tokio::process::Command;
|
||||
use tracing::info;
|
||||
|
||||
// Fixed WebDAV port for testing
|
||||
const WEBDAV_PORT: u16 = 9080;
|
||||
const WEBDAV_ADDRESS: &str = "127.0.0.1:9080";
|
||||
|
||||
/// Create HTTP client with basic auth
|
||||
fn create_client() -> Client {
|
||||
Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.expect("Failed to create HTTP client")
|
||||
}
|
||||
|
||||
/// Get basic auth header value
|
||||
fn basic_auth_header() -> String {
|
||||
let credentials = format!("{}:{}", DEFAULT_ACCESS_KEY, DEFAULT_SECRET_KEY);
|
||||
let encoded = base64::engine::general_purpose::STANDARD.encode(credentials);
|
||||
format!("Basic {}", encoded)
|
||||
}
|
||||
|
||||
/// Test WebDAV: MKCOL (create bucket), PUT, GET, DELETE, PROPFIND operations
|
||||
pub async fn test_webdav_core_operations() -> Result<()> {
|
||||
let env = ProtocolTestEnvironment::new().map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||
|
||||
// Start server manually
|
||||
info!("Starting WebDAV server on {}", WEBDAV_ADDRESS);
|
||||
let binary_path = rustfs_binary_path();
|
||||
let mut server_process = Command::new(&binary_path)
|
||||
.env("RUSTFS_WEBDAV_ENABLE", "true")
|
||||
.env("RUSTFS_WEBDAV_ADDRESS", WEBDAV_ADDRESS)
|
||||
.env("RUSTFS_WEBDAV_TLS_ENABLED", "false") // No TLS for testing
|
||||
.arg(&env.temp_dir)
|
||||
.spawn()?;
|
||||
|
||||
// Ensure server is cleaned up even on failure
|
||||
let result = async {
|
||||
// Wait for server to be ready
|
||||
ProtocolTestEnvironment::wait_for_port_ready(WEBDAV_PORT, 30)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||
|
||||
let client = create_client();
|
||||
let auth_header = basic_auth_header();
|
||||
let base_url = format!("http://{}", WEBDAV_ADDRESS);
|
||||
|
||||
// Test PROPFIND at root (list buckets)
|
||||
info!("Testing WebDAV: PROPFIND at root (list buckets)");
|
||||
let resp = client
|
||||
.request(reqwest::Method::from_bytes(b"PROPFIND").unwrap(), &base_url)
|
||||
.header("Authorization", &auth_header)
|
||||
.header("Depth", "1")
|
||||
.send()
|
||||
.await?;
|
||||
assert!(
|
||||
resp.status().is_success() || resp.status().as_u16() == 207,
|
||||
"PROPFIND at root should succeed, got: {}",
|
||||
resp.status()
|
||||
);
|
||||
info!("PASS: PROPFIND at root successful");
|
||||
|
||||
// Test MKCOL (create bucket)
|
||||
let bucket_name = "webdav-test-bucket";
|
||||
info!("Testing WebDAV: MKCOL (create bucket '{}')", bucket_name);
|
||||
let resp = client
|
||||
.request(reqwest::Method::from_bytes(b"MKCOL").unwrap(), format!("{}/{}", base_url, bucket_name))
|
||||
.header("Authorization", &auth_header)
|
||||
.send()
|
||||
.await?;
|
||||
assert!(
|
||||
resp.status().is_success() || resp.status().as_u16() == 201,
|
||||
"MKCOL should succeed, got: {}",
|
||||
resp.status()
|
||||
);
|
||||
info!("PASS: MKCOL bucket '{}' successful", bucket_name);
|
||||
|
||||
// Test PUT (upload file)
|
||||
let filename = "test-file.txt";
|
||||
let file_content = "Hello, WebDAV!";
|
||||
info!("Testing WebDAV: PUT (upload file '{}')", filename);
|
||||
let resp = client
|
||||
.put(format!("{}/{}/{}", base_url, bucket_name, filename))
|
||||
.header("Authorization", &auth_header)
|
||||
.body(file_content)
|
||||
.send()
|
||||
.await?;
|
||||
assert!(
|
||||
resp.status().is_success() || resp.status().as_u16() == 201,
|
||||
"PUT should succeed, got: {}",
|
||||
resp.status()
|
||||
);
|
||||
info!("PASS: PUT file '{}' successful", filename);
|
||||
|
||||
// Test GET (download file)
|
||||
info!("Testing WebDAV: GET (download file '{}')", filename);
|
||||
let resp = client
|
||||
.get(format!("{}/{}/{}", base_url, bucket_name, filename))
|
||||
.header("Authorization", &auth_header)
|
||||
.send()
|
||||
.await?;
|
||||
assert!(resp.status().is_success(), "GET should succeed, got: {}", resp.status());
|
||||
let downloaded_content = resp.text().await?;
|
||||
assert_eq!(downloaded_content, file_content, "Downloaded content should match uploaded content");
|
||||
info!("PASS: GET file '{}' successful, content matches", filename);
|
||||
|
||||
// Test PROPFIND on bucket (list objects)
|
||||
info!("Testing WebDAV: PROPFIND on bucket (list objects)");
|
||||
let resp = client
|
||||
.request(reqwest::Method::from_bytes(b"PROPFIND").unwrap(), format!("{}/{}", base_url, bucket_name))
|
||||
.header("Authorization", &auth_header)
|
||||
.header("Depth", "1")
|
||||
.send()
|
||||
.await?;
|
||||
assert!(
|
||||
resp.status().is_success() || resp.status().as_u16() == 207,
|
||||
"PROPFIND on bucket should succeed, got: {}",
|
||||
resp.status()
|
||||
);
|
||||
let body = resp.text().await?;
|
||||
assert!(body.contains(filename), "File should appear in PROPFIND response");
|
||||
info!("PASS: PROPFIND on bucket successful, file '{}' found", filename);
|
||||
|
||||
// Test DELETE file
|
||||
info!("Testing WebDAV: DELETE file '{}'", filename);
|
||||
let resp = client
|
||||
.delete(format!("{}/{}/{}", base_url, bucket_name, filename))
|
||||
.header("Authorization", &auth_header)
|
||||
.send()
|
||||
.await?;
|
||||
assert!(
|
||||
resp.status().is_success() || resp.status().as_u16() == 204,
|
||||
"DELETE file should succeed, got: {}",
|
||||
resp.status()
|
||||
);
|
||||
info!("PASS: DELETE file '{}' successful", filename);
|
||||
|
||||
// Verify file is deleted
|
||||
info!("Testing WebDAV: Verify file is deleted");
|
||||
let resp = client
|
||||
.get(format!("{}/{}/{}", base_url, bucket_name, filename))
|
||||
.header("Authorization", &auth_header)
|
||||
.send()
|
||||
.await?;
|
||||
assert!(
|
||||
resp.status().as_u16() == 404,
|
||||
"GET deleted file should return 404, got: {}",
|
||||
resp.status()
|
||||
);
|
||||
info!("PASS: Verified file '{}' is deleted", filename);
|
||||
|
||||
// Test DELETE bucket
|
||||
info!("Testing WebDAV: DELETE bucket '{}'", bucket_name);
|
||||
let resp = client
|
||||
.delete(format!("{}/{}", base_url, bucket_name))
|
||||
.header("Authorization", &auth_header)
|
||||
.send()
|
||||
.await?;
|
||||
assert!(
|
||||
resp.status().is_success() || resp.status().as_u16() == 204,
|
||||
"DELETE bucket should succeed, got: {}",
|
||||
resp.status()
|
||||
);
|
||||
info!("PASS: DELETE bucket '{}' successful", bucket_name);
|
||||
|
||||
// Test authentication failure
|
||||
info!("Testing WebDAV: Authentication failure");
|
||||
let resp = client
|
||||
.request(reqwest::Method::from_bytes(b"PROPFIND").unwrap(), &base_url)
|
||||
.header("Authorization", "Basic aW52YWxpZDppbnZhbGlk") // invalid:invalid
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status().as_u16(), 401, "Invalid auth should return 401, got: {}", resp.status());
|
||||
info!("PASS: Authentication failure test successful");
|
||||
|
||||
info!("WebDAV core tests passed");
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
|
||||
// Always cleanup server process
|
||||
let _ = server_process.kill().await;
|
||||
let _ = server_process.wait().await;
|
||||
|
||||
result
|
||||
}
|
||||
Reference in New Issue
Block a user