Files
inbuxa-migrate/tests/integration/webdav.rs
T
2026-09-28 10:45:15 +02:00

322 lines
9.4 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*/
use std::time::Duration;
use encodify::base64::STANDARD as B64;
use testcontainers::core::{IntoContainerPort, WaitFor};
use testcontainers::runners::{SyncBuilder, SyncRunner};
use testcontainers::{Container, GenericBuildableImage, GenericImage, ImageExt};
use super::dav_client::DavSeed;
use super::error::{ContainerError, ContainerResult};
use super::layouts::{self, FileSpec};
use super::{Account, Endpoint};
const HTTP_PORT: u16 = 80;
const DOCKERFILE: &str = r#"FROM debian:bookworm-20260518-slim
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
apache2 apache2-utils ca-certificates && \
a2enmod dav dav_fs auth_basic authn_file && \
rm -rf /var/lib/apt/lists/*
COPY apache.conf /etc/apache2/sites-available/000-default.conf
COPY htpasswd /etc/apache2/htpasswd
RUN mkdir -p /var/dav && \
chown -R www-data:www-data /var/dav
EXPOSE 80
CMD ["apachectl", "-D", "FOREGROUND"]
"#;
const APACHE_CONF: &str = r#"DavLockDB /tmp/DavLock
<VirtualHost *:80>
DocumentRoot /var/dav
ErrorLog /dev/stderr
CustomLog /dev/stdout combined
<Directory /var/dav>
Dav On
AuthType Basic
AuthName "WebDAV"
AuthUserFile /etc/apache2/htpasswd
Require valid-user
Options Indexes FollowSymLinks
</Directory>
</VirtualHost>
"#;
fn htpasswd_for_test() -> String {
let mut out = String::new();
for u in layouts::accounts() {
out.push_str(u);
out.push(':');
out.push_str("{SHA}");
out.push_str(&sha1_b64(layouts::PASSWORD.as_bytes()));
out.push('\n');
}
out
}
fn sha1_b64(bytes: &[u8]) -> String {
let digest = simple_sha1(bytes);
B64.encode(digest)
}
fn simple_sha1(bytes: &[u8]) -> [u8; 20] {
let mut h0: u32 = 0x6745_2301;
let mut h1: u32 = 0xEFCD_AB89;
let mut h2: u32 = 0x98BA_DCFE;
let mut h3: u32 = 0x1032_5476;
let mut h4: u32 = 0xC3D2_E1F0;
let bit_len = (bytes.len() as u64).wrapping_mul(8);
let mut msg = bytes.to_vec();
msg.push(0x80);
while msg.len() % 64 != 56 {
msg.push(0);
}
msg.extend_from_slice(&bit_len.to_be_bytes());
for chunk in msg.chunks(64) {
let mut w = [0u32; 80];
for (i, word) in w.iter_mut().take(16).enumerate() {
*word = u32::from_be_bytes(chunk[i * 4..i * 4 + 4].try_into().unwrap());
}
for i in 16..80 {
w[i] = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]).rotate_left(1);
}
let (mut a, mut b, mut c, mut d, mut e) = (h0, h1, h2, h3, h4);
for (i, &wi) in w.iter().enumerate() {
let (f, k): (u32, u32) = match i {
0..=19 => ((b & c) | ((!b) & d), 0x5A82_7999),
20..=39 => (b ^ c ^ d, 0x6ED9_EBA1),
40..=59 => ((b & c) | (b & d) | (c & d), 0x8F1B_BCDC),
_ => (b ^ c ^ d, 0xCA62_C1D6),
};
let temp = a
.rotate_left(5)
.wrapping_add(f)
.wrapping_add(e)
.wrapping_add(k)
.wrapping_add(wi);
e = d;
d = c;
c = b.rotate_left(30);
b = a;
a = temp;
}
h0 = h0.wrapping_add(a);
h1 = h1.wrapping_add(b);
h2 = h2.wrapping_add(c);
h3 = h3.wrapping_add(d);
h4 = h4.wrapping_add(e);
}
let mut out = [0u8; 20];
out[..4].copy_from_slice(&h0.to_be_bytes());
out[4..8].copy_from_slice(&h1.to_be_bytes());
out[8..12].copy_from_slice(&h2.to_be_bytes());
out[12..16].copy_from_slice(&h3.to_be_bytes());
out[16..20].copy_from_slice(&h4.to_be_bytes());
out
}
pub struct WebDav {
container: Container<GenericImage>,
pub http: Endpoint,
pub accounts: Vec<Account>,
}
impl WebDav {
pub fn start() -> ContainerResult<Self> {
let image: GenericImage = GenericBuildableImage::new("vandelay-webdav", "test")
.with_dockerfile_string(DOCKERFILE.to_owned())
.with_data(APACHE_CONF.as_bytes().to_vec(), "apache.conf")
.with_data(htpasswd_for_test().into_bytes(), "htpasswd")
.build_image()
.map_err(|e| ContainerError::Seed(format!("webdav build: {e}")))?;
let request = image
.with_exposed_port(HTTP_PORT.tcp())
.with_wait_for(WaitFor::message_on_stderr("AH00558"))
.with_startup_timeout(Duration::from_secs(120))
.with_labels([(super::OWNER_LABEL, "1")]);
let container = request.start()?;
let host = container.get_host()?.to_string();
let http = Endpoint::new(host, container.get_host_port_ipv4(HTTP_PORT.tcp())?);
let accounts: Vec<Account> = layouts::accounts()
.iter()
.map(|name| Account {
username: (*name).to_owned(),
password: layouts::PASSWORD.to_owned(),
layout: layouts::layout_for(name),
})
.collect();
Ok(Self {
container,
http,
accounts,
})
}
pub fn base_url(&self) -> String {
self.http.http_base()
}
pub fn account_url(&self, account: &Account) -> String {
format!("{}/{}/", self.base_url(), account.username)
}
pub fn seed_all(&self) -> ContainerResult<Vec<AccountSeed>> {
let mut out = Vec::new();
for acct in &self.accounts {
let m = self.seed_account(acct)?;
out.push(m);
}
Ok(out)
}
pub fn seed_account(&self, account: &Account) -> ContainerResult<AccountSeed> {
let client = DavSeed::new(self.base_url(), &account.username, &account.password);
let root = format!("/{}/", account.username);
client.mkcol(&root, None)?;
let mut seed = AccountSeed::new(account.username.clone());
let specs = account.layout.files;
for spec in specs {
let path = build_path(&root, specs, spec.key)
.ok_or_else(|| ContainerError::Seed(format!("missing file key: {}", spec.key)))?;
if spec.directory {
client.mkcol(&path, None)?;
seed.directories += 1;
} else {
let payload = synth_payload(spec.name);
client.put(&path, "application/octet-stream", payload.as_bytes())?;
seed.files.push(SeededFile {
key: spec.key.to_owned(),
name: spec.name.to_owned(),
href: path,
payload: payload.into_bytes(),
});
}
}
Ok(seed)
}
pub fn delete_resource(&self, account: &Account, href: &str) -> ContainerResult<()> {
let client = DavSeed::new(self.base_url(), &account.username, &account.password);
client.delete(href)?;
Ok(())
}
pub fn add_file(
&self,
account: &Account,
parent_segments: &[&str],
name: &str,
payload: &[u8],
) -> ContainerResult<String> {
let client = DavSeed::new(self.base_url(), &account.username, &account.password);
let mut href = format!("/{}/", account.username);
for seg in parent_segments {
href.push_str(seg);
href.push('/');
}
href.push_str(name);
client.put(&href, "application/octet-stream", payload)?;
Ok(href)
}
pub fn verify_seed(&self) -> ContainerResult<()> {
for acct in &self.accounts {
let client = DavSeed::new(self.base_url(), &acct.username, &acct.password);
let body = client.propfind(&format!("/{}/", acct.username), 1)?;
if !body.contains("multistatus") {
return Err(ContainerError::Protocol(format!(
"webdav propfind for {} returned no multistatus",
acct.username
)));
}
}
Ok(())
}
pub fn stop(self) -> ContainerResult<()> {
self.container.stop()?;
Ok(())
}
}
fn build_path(root: &str, specs: &[FileSpec], key: &str) -> Option<String> {
let mut chain: Vec<&str> = Vec::new();
let mut cur = key;
let mut leaf_dir = false;
loop {
let spec = specs.iter().find(|s| s.key == cur)?;
chain.push(spec.name);
if chain.len() == 1 {
leaf_dir = spec.directory;
}
match spec.parent {
Some(p) => cur = p,
None => break,
}
}
chain.reverse();
let mut path = String::from(root);
for (i, name) in chain.iter().enumerate() {
path.push_str(name);
let is_last = i + 1 == chain.len();
if !is_last || leaf_dir {
path.push('/');
}
}
Some(path)
}
pub fn synth_payload(name: &str) -> String {
let mut out = String::new();
out.push_str(name);
out.push('\n');
for i in 0..16 {
out.push_str(&format!("line {i} for {name}\n"));
}
out
}
#[derive(Debug, Clone)]
pub struct SeededFile {
pub key: String,
pub name: String,
pub href: String,
pub payload: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct AccountSeed {
pub username: String,
pub directories: usize,
pub files: Vec<SeededFile>,
}
impl AccountSeed {
fn new(username: String) -> Self {
Self {
username,
directories: 0,
files: Vec::new(),
}
}
}