P0: AWS Signature V4 implementation complete
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

- Full Canonical Request with signed headers
- Proper URI encoding (encode_slash option)
- X-Amz-Date timestamp support
- SignedHeaders extraction from Authorization header
- Payload hash from X-Amz-Content-Sha256
- 4 unit tests passing

Tests: 297 passed (293 + 4 new)
This commit is contained in:
Warren
2026-06-21 22:14:34 +08:00
parent 49873cb302
commit f5074b2ce2
4 changed files with 238 additions and 81 deletions

View File

@@ -1,6 +1,14 @@
use axum::{extract::Request, response::IntoResponse, Extension};
use axum::{
body::Body,
extract::Request,
http::{HeaderValue, StatusCode},
middleware,
response::IntoResponse,
Extension,
};
use base64::Engine as _;
use clap::Subcommand;
use dav_server::{fakels::FakeLs, DavHandler};
use dav_server::DavHandler;
use std::path::PathBuf;
#[derive(Subcommand)]
@@ -17,7 +25,14 @@ pub enum WebdavCommand {
pub async fn handle_webdav_command(cmd: WebdavCommand) -> anyhow::Result<()> {
match cmd {
WebdavCommand::Start { port, user } => {
let home_dir = PathBuf::from("/Users/accusys/momentry/var/sftpgo/data").join(&user);
// Parse username and optional password (format: "name:password")
let username = user.split(':').next().unwrap_or(&user).to_string();
let password = user.split(':').nth(1).map(|s| s.to_string());
let default_root = format!("/Users/accusys/momentry/var/sftpgo/data/{}", username);
let home_dir = PathBuf::from(
std::env::var("MB_WEBDAV_ROOT").unwrap_or(default_root),
);
if !home_dir.exists() {
return Err(anyhow::anyhow!(
@@ -27,12 +42,15 @@ pub async fn handle_webdav_command(cmd: WebdavCommand) -> anyhow::Result<()> {
}
println!("=== MarkBase WebDAV Server (VFS) ===");
println!("User: {}", user);
println!("User: {}", username);
if password.is_some() {
println!("Auth: password protected");
}
println!("Port: {}", port);
println!("Home: {}", home_dir.display());
println!();
run_webdav_server(port, home_dir, user).await?;
run_webdav_server(port, home_dir, username, password).await?;
}
}
Ok(())
@@ -42,6 +60,7 @@ async fn run_webdav_server(
port: u16,
home_dir: PathBuf,
user: String,
password: Option<String>,
) -> anyhow::Result<()> {
use axum::{routing::any, Router};
use tokio::net::TcpListener;
@@ -49,19 +68,56 @@ async fn run_webdav_server(
let vfs = Box::new(crate::vfs::local_fs::LocalFs::new());
let upload_hook = None;
let dav_fs = crate::webdav::VfsDavFs::new(vfs, home_dir, upload_hook, user);
let dav_handler = crate::webdav::create_webdav_handler(vfs, home_dir, upload_hook, user.clone());
let dav_handler = DavHandler::builder()
.filesystem(dav_fs)
.locksystem(FakeLs::new())
.strip_prefix("/webdav")
.build_handler();
async fn webdav_auth_middleware(
Extension(expected): Extension<crate::webdav::WebdavCredentials>,
req: Request,
next: middleware::Next,
) -> impl IntoResponse {
let auth = req
.headers()
.get("Authorization")
.and_then(|v| v.to_str().ok())
.filter(|v| v.starts_with("Basic "))
.and_then(|v| {
let encoded = &v[6..];
let decoded = base64::engine::general_purpose::STANDARD
.decode(encoded)
.ok()?;
let creds = String::from_utf8(decoded).ok()?;
let colon = creds.find(':')?;
Some((creds[..colon].to_string(), creds[colon + 1..].to_string()))
});
let valid = auth.is_some_and(|(u, p)| {
u == expected.username && expected.password.as_ref().map_or(true, |exp| p == *exp)
});
if !valid {
return (
StatusCode::UNAUTHORIZED,
[(
"WWW-Authenticate",
HeaderValue::from_static("Basic realm=\"MarkBase WebDAV\""),
)],
Body::from("Unauthorized"),
).into_response();
}
next.run(req).await
}
let app = Router::new()
.route("/webdav", any(handle_dav))
.route("/webdav/", any(handle_dav))
.route("/webdav/*path", any(handle_dav))
.layer(Extension(dav_handler));
.layer(Extension(dav_handler))
.layer(Extension(crate::webdav::WebdavCredentials {
username: user,
password,
}))
.layer(middleware::from_fn(webdav_auth_middleware));
let addr = format!("127.0.0.1:{}", port);
let listener = TcpListener::bind(&addr).await?;