I've ran the Litmus test suite against various WebDAV servers to test their compliancy to the spec. Here are the results:
Apache | Nginx | Nginx w/ nginx-dav-ext-module | Sabre/dav | Sabre/dav w/ FSExt | Lighttpd | Chezdav | Rclone | |
---|---|---|---|---|---|---|---|---|
basic | ||||||||
init | pass | pass | pass | pass | pass | pass | pass | pass |
begin | pass | pass | pass | pass | pass | pass | pass | pass |
options | pass | FAIL | pass | pass | pass | pass | pass | pass |
put_get | pass | pass | pass | pass | pass | pass | pass | pass |
put_get_utf8_segment | pass | pass | pass | pass | pass | pass | pass | pass |
put_no_parent | pass | pass | pass | pass | pass | pass | pass | pass |
mkcol_over_plain | pass | pass | pass | pass | pass | pass | pass | pass |
delete | pass | pass | pass | pass | pass | pass | pass | pass |
delete_null | pass | pass | pass | pass | pass | pass | pass | pass |
delete_fragment | pass | WARNING | WARNING | pass | pass | pass | pass | pass |
mkcol | pass | pass | pass | pass | pass | pass | pass | pass |
mkcol_again | pass | pass | pass | pass | pass | pass | pass | FAIL |
delete_coll | pass | pass | pass | pass | pass | pass | pass | pass |
mkcol_no_parent | pass | pass | pass | pass | pass | pass | pass | pass |
mkcol_with_body | pass | pass | pass | pass | pass | pass | pass | pass |
finish | pass | pass | pass | pass | pass | pass | pass | pass |
copymove | ||||||||
init | pass | pass | pass | pass | pass | pass | pass | pass |
begin | pass | pass | pass | pass | pass | pass | pass | pass |
copy_init | pass | pass | pass | pass | pass | pass | pass | pass |
copy_simple | pass | WARNING | WARNING | pass | pass | pass | pass | pass |
copy_overwrite | pass | FAIL | FAIL | pass | pass | pass | pass | pass |
copy_nodestcoll | pass | WARNING | WARNING | pass | pass | pass | pass | pass |
copy_cleanup | pass | pass | pass | pass | pass | pass | pass | pass |
copy_coll | pass | pass | pass | pass | pass | pass | pass | pass |
copy_shallow | pass | pass | pass | pass | pass | pass | pass | pass |
move | pass | WARNING | WARNING | pass | pass | pass | pass | pass |
move_coll | pass | FAIL | FAIL | pass | pass | pass | pass | pass |
move_cleanup | pass | pass | pass | pass | pass | pass | pass | pass |
finish | pass | pass | pass | pass | pass | pass | pass | pass |
props | ||||||||
init | pass | pass | pass | pass | pass | pass | pass | pass |
begin | pass | pass | pass | pass | pass | pass | pass | pass |
propfind_invalid | pass | FAIL | pass | pass | pass | FAIL | pass | pass |
propfind_invalid2 | pass | FAIL | FAIL | pass | pass | FAIL | pass | FAIL |
propfind_d0 | pass | FAIL | pass | pass | pass | pass | pass | pass |
propinit | pass | pass | pass | pass | pass | pass | pass | pass |
propset | pass | FAIL | FAIL | FAIL | pass | pass | pass | FAIL |
propget | pass | FAIL | FAIL | FAIL | pass | pass | pass | FAIL |
propextended | pass | pass | pass | pass | pass | pass | pass | pass |
propmove | pass | SKIPPED | SKIPPED | SKIPPED | pass | pass | pass | SKIPPED |
propdeletes | pass | SKIPPED | SKIPPED | SKIPPED | pass | pass | pass | SKIPPED |
propreplace | pass | SKIPPED | SKIPPED | SKIPPED | pass | pass | pass | SKIPPED |
propnullns | pass | SKIPPED | SKIPPED | SKIPPED | pass | FAIL | pass | SKIPPED |
prophighunicode | pass | SKIPPED | SKIPPED | SKIPPED | pass | FAIL | pass | SKIPPED |
propremoveset | pass | SKIPPED | SKIPPED | SKIPPED | pass | FAIL | pass | SKIPPED |
propsetremove | pass | SKIPPED | SKIPPED | SKIPPED | pass | FAIL | pass | SKIPPED |
propvalnspace | pass | SKIPPED | SKIPPED | SKIPPED | pass | FAIL | pass | SKIPPED |
propwformed | pass | pass | pass | pass | pass | pass | pass | pass |
propmanyns | pass | FAIL | FAIL | FAIL | pass | pass | pass | FAIL |
propcleanup | pass | pass | pass | pass | pass | pass | pass | pass |
finish | pass | pass | pass | pass | pass | pass | pass | pass |
locks | ||||||||
init | pass | pass | pass | pass | pass | pass | pass | pass |
begin | pass | pass | pass | pass | pass | pass | pass | pass |
options | pass | FAIL | pass | pass | pass | pass | pass | pass |
precond | pass | SKIPPED | pass | pass | pass | pass | pass | pass |
init_locks | pass | pass | pass | pass | pass | pass | pass | |
put | pass | pass | pass | pass | pass | pass | pass | |
lock_excl | pass | pass | pass | pass | pass | pass | pass | |
discover | pass | FAIL | pass | pass | pass | pass | pass | |
refresh | pass | pass | pass | pass | pass | pass | pass | |
notowner_modify | pass | WARNING | pass | pass | pass | pass | pass | |
notowner_lock | pass | SKIPPED | pass | pass | pass | pass | SKIPPED | |
owner_modify | pass | FAIL | FAIL | pass | pass | pass | FAIL | |
copy | pass | pass | pass | pass | pass | pass | pass | |
cond_put | pass | pass | SKIPPED | pass | pass | pass | pass | |
fail_cond_put | pass | WARNING | SKIPPED | pass | pass | pass | pass | |
cond_put_with_not | pass | pass | pass | pass | pass | pass | pass | |
cond_put_corrupt_token | WARNING | pass | pass | pass | pass | pass | WARNING | |
complex_cond_put | pass | pass | SKIPPED | pass | pass | pass | pass | |
fail_complex_cond_put | pass | FAIL | SKIPPED | pass | pass | pass | FAIL | |
unlock | pass | pass | pass | pass | pass | pass | pass | |
fail_cond_put_unlocked | pass | FAIL | pass | pass | pass | pass | pass | |
lock_shared | pass | FAIL | pass | pass | pass | pass | FAIL | |
double_sharedlock | pass | SKIPPED | pass | pass | pass | pass | SKIPPED | |
prep_collection | pass | pass | pass | pass | pass | pass | pass | |
lock_collection | pass | pass | pass | pass | pass | pass | pass | |
indirect_refresh | pass | pass | pass | pass | pass | pass | pass | |
unmapped_lock | WARNING | pass | pass | pass | pass | pass | pass | |
finish | pass | pass | pass | pass | pass | pass | pass | |
http | ||||||||
init | pass | pass | pass | pass | pass | pass | pass | pass |
begin | pass | pass | pass | pass | pass | pass | pass | pass |
expect100 | pass | pass | pass | pass | pass | pass | pass | pass |
finish | pass | pass | pass | pass | pass | pass | pass | pass |
Some notes:
- Candidates with a lot of SKIPPEDs in the props section don't support custom properties. Custom properties are per-file key-value pairs that are stored in a database separate from the file system. Sabre/dav optionally supports custom properties through the
FSExt\Directory
class (see below). - Tests with
cond
in their name mostly test theIf
header. TheIf
header is WebDAV's generalized version of HTTP'sIf-Match
andIf-None-Match
headers. TheIf-Match
andIf-None-Match
headers are strictly speaking not part of the WebDAV standard and thus not tested by Litmus but they can always be rewritten in terms of the more powerfulIf
header. - Nginx's built-in WebDAV implementation is quite poor and lacks basic methods such as
PROPFIND
,OPTIONS
andLOCK
. This is somewhat alleviated by using the nginx-dav-ext-module but even with the module Nginx compares poorly to other implementations. Nevertheless, Nginx might be sufficient for your needs.
All tests use Litmus version 0.13. The versions tested are generally those available on Debian 12 (bookworm).
- Apache HTTP Server version 2.4.57
- Nginx version 1.22.1 with nginx-dav-ext-module version 3.0.0
- Sabre/dav version 1.8.12 running on Apache
- Lighttpd version 1.4.69
- Chezdav aka phởdav version 3.0
- Rclone version 1.60.1
Litmus writes a carriage return (\r
) to stdout to overwrite the current line. This works in a terminal but leads to duplicate content when writing to a file. We can strip out anything that came before the carriage return with sed:
litmus -k http://localhost:8080 | sed 's/.*\r//' > results/rclone.txt
Apache requires the dav
and dav_fs
modules for WebDAV support. On Debian-based systems these can be enabled using the a2enmod
command. I've used the following virtual host config for the test:
DavLockDB /usr/local/apache2/var/DavLock
Listen 8001
<VirtualHost *:8001>
DocumentRoot /var/www/webdav
<Directory /var/www/webdav>
DAV On
</Directory>
</VirtualHost>
I've used the following site config to test Nginx without the nginx-dav-ext-module:
server {
listen 80;
root /var/www/webdav;
dav_methods PUT DELETE MKCOL COPY MOVE;
}
The nginx-dav-ext-module needs to be installed before it can be used within Nginx. On Debian it can be installed via the libnginx-mod-http-dav-ext
package. I've used the following site config to test Nginx with the nginx-dav-ext-module:
dav_ext_lock_zone zone=foo:10m;
server {
listen 80;
root /var/www/webdav;
dav_methods PUT DELETE MKCOL COPY MOVE;
dav_ext_methods PROPFIND OPTIONS LOCK UNLOCK;
dav_ext_lock zone=foo;
# nginx-dav-ext-module only sets the class 2 flag instead of setting
# both class 1 and class 2 flags as required by the standard.
# This is a workaround until https://github.com/arut/nginx-dav-ext-module/pull/46 is merged.
add_header DAV 1,2;
}
I mostly followed the official guide to get started but instead of installing sabre/dav via composer I installed the Debian package php-sabre-dav
. The Debian package is still on version 1.8.12 from 2015.
I've used the following virtual host config for Apache:
Listen 8002
<VirtualHost *:8002>
# Copied from https://sabre.io/dav/webservers/
# The DocumentRoot is also required
DocumentRoot /var/www/sabredav
RewriteEngine On
# This makes every request go to server.php
RewriteRule ^/(.*)$ /server.php [L]
# Output buffering needs to be off, to prevent high memory usage
php_flag output_buffering off
# This is also to prevent high memory usage
php_flag always_populate_raw_post_data off
# This is almost a given, but magic quotes is *still* on on some
# linux distributions
php_flag magic_quotes_gpc off
# SabreDAV is not compatible with mbstring function overloading
php_flag mbstring.func_overload off
</VirtualHost>
And the following server.php
:
<?php
// Copied from https://sabre.io/dav/gettingstarted/
use
Sabre\DAV;
// The autoloader
require 'Sabre/autoload.php';
// Now we're creating a whole bunch of objects
$rootDirectory = new DAV\FS\Directory('public');
// The server object is responsible for making sense out of the WebDAV protocol
$server = new DAV\Server($rootDirectory);
// If your server is not on your webroot, make sure the following line has the
// correct information
$server->setBaseUri('/');
// The lock manager is reponsible for making sure users don't overwrite
// each others changes.
$lockBackend = new DAV\Locks\Backend\File('data/locks');
$lockPlugin = new DAV\Locks\Plugin($lockBackend);
$server->addPlugin($lockPlugin);
// This ensures that we get a pretty index in the browser, but it is
// optional.
$server->addPlugin(new DAV\Browser\Plugin());
// All we need to do now, is to fire up the server
$server->exec();
To enable support for custom properties the line in server.php
using FS\Directory
needs to be changed to use the FSExt\Directory
class:
$rootDirectory = new DAV\FSExt\Directory('public');
Lighttpd requires the webdav
module for WebDAV support which I've enabled using the lighty-enable-mod
command. Additionally I've added the following config to lighttpd.conf
:
webdav.activate = "enable"