本文发自 http://www.binss.me/blog/build-webdav-server-by-nginx/,转载请注明出处。

WebDAV 是什么

根据 wikipedia :

Web Distributed Authoring and Versioning (WebDAV) is an extension of the Hypertext Transfer Protocol (HTTP) that allows clients to perform remote Web content authoring operations.

The WebDAV protocol provides a framework for users to create, change and move documents on a server. The most important features of the WebDAV protocol include the maintenance of properties about an author or modification date, namespace management, collections, and overwrite protection. Maintenance of properties includes such things as the creation, removal, and querying of file information.

个人理解,WebDAV 就是通过 Restful API ,实现对服务端文件的 创建 / 删除 / 读取 / 修改,比起其他文件传输协议,它基于 HTTP,不容易被当作不明流量被砍掉。同时能够利用 HTTP 的各种扩展,比如 HTTPS 提供数据加密功能、HTTP 2.0 提供数据流传输、HTTP 范围请求(RFC7233)等。

正是因为这些好处,很多系统和软件都提供了对 WebDAV 的支持。比如说 OS X 的 finder 支持远程连接到 WebDAV 服务器。IOS 的播放器 nPlayer 能够播放 WebDAV 上的视频文件,且传输速度高于 FTP / SMB 等协议。

于是乎,我选择了 WebDAV 作为远程播放 NAS 视频流的传输协议。

Plan A: nextcloud

我将下载下来的电影目录挂载到个人网盘 nextcloud 上,并在 nextcloud 中配置为外部挂载。

我搭建 nextcloud 的架构如下:

nginx - nextcloud - redis
                  - mariadb

组件之间各自容器化部署,然后通过 tcp 进行连接。

由于 nextcloud 自带 WebDAV 服务,我们可以直接连接上去:

但实际实验下来体验很差。在播放大于 2 G 的视频时,加载缓慢,拖动进度条后有大概率卡死,NUC 的 CPU 占用率大幅飙升,不久后 nextcloud 的 3 个 php-handler 进程陷入 D(TASK_UNINTERRUPTIBLE) 状态,简单来说就是卡死了,需要重启进程(容器)。

虽然我的机器是功耗 15W 的 NUC ,但它也没有菜到这种地步。于是我将锅甩给 —— PHP 是世界上最好的语言。启用 Plan B

Plan B: nginx

于是我打起了 nginx 的主意,nginx 挂载电影目录,然后直接作为 WebDAV 服务器提供服务。

为了让 nginx 能够支持 WebDAV 规范中的 PROPFIND 和 OPTIONS ,我们需要安装模块 nginx-dav-ext-module

然后在 configure 时,添加 --with-http_dav_module 和 --add-module=/path/to/nginx-dav-ext-module

并使用以下配置文件:

    server {
        listen 443 ssl http2;
        server_name yourhostname.com;
        ssl_certificate "your cert";
        ssl_certificate_key "your cert key";

        client_max_body_size 102400M;

        # rewrite URL to allow create directory
        location / {

            set $dest $http_destination;
            if (-d $request_filename) {
                rewrite ^(.*[^/])$ $1/;
                set $dest $dest/;
            }
            if ($request_method ~ (MOVE|COPY)) {
                more_set_input_headers 'Destination: $dest';
            }

            if ($request_method ~ MKCOL) {
                rewrite ^(.*[^/])$ $1/ break;
            }

            dav_methods PUT DELETE MKCOL COPY MOVE;
            dav_ext_methods PROPFIND OPTIONS;
            dav_access user:rw;
            create_full_put_path  on;
            root /webdav;

            auth_basic "Restricted access";
            auth_basic_user_file /etc/nginx/htpasswd.conf;
        }
    }

该配置开启了 HTTPS ,因为 HTTP2 需要开启 SSL。因此需要设置域名的证书和密钥。同时为了保证安全性,还设置了密码认证,密码文件通过 htpasswd 工具生成。

而:

            set $dest $http_destination;
            if (-d $request_filename) {
                rewrite ^(.*[^/])$ $1/;
                set $dest $dest/;
            }
            if ($request_method ~ (MOVE|COPY)) {
                more_set_input_headers 'Destination: $dest';
            }

            if ($request_method ~ MKCOL) {
                rewrite ^(.*[^/])$ $1/ break;
            }

处理的是某些 WebDAV 客户端(如 OS X 下的 ForkLift) 在 创建文件夹 / 复制文件夹 / 移动文件夹 失败的问题:

2018/06/03 08:03:58 [error] 7#7: *1 MKCOL can create a collection only, client: 8.8.8.8, server: yourhostname.com, request: "MKCOL /sp HTTP/2.0", host: "yourhostname.com"

2018/06/03 08:17:20 [error] 7#7: *18 "/sp" is collection, client: 8.8.8.8, server: yourhostname.com, request: "MOVE /sp HTTP/2.0", host: "yourhostname.com"

这是因为 nginx 创建的 WebDAV server 要求所有对文件夹的操作,路径结尾都要有斜杠 / 。而某些客户端在对文件夹进行操作时路径结尾没有斜杠 /,于是报错。

解决方法是 url rewrite,如果操作的是文件夹,则补上 /

但对于 MOVE / COPY 操作,文件路径还会设置到 header 的 Destination 中,也需要 rewrite ,这需要借助 headers-more-nginx-module 模块。需要在 configure 时,添加 --add-module=/path/to/headers-more-nginx-module

修改后的 Dockerfile 可见 https://github.com/binss/docker-nginx-webdav ,将它和它的 submodule clone 下来后 docker build 即可。

实测结果令我满意:播放 10G 大小的视频,同局域网下能够在 1 秒内加载和拖动,在教育网同校区内能够在 5 秒内加载和拖动。htop 查看 CPU,发现几乎毫无波动。