博文記錄
PHP 2021-04-18 00:06:58 2571 2

WEBDAV(Web Distributed Authoring and Versioning) 协议在跨设备存储上非常有用,很多客户端都支持此协议,这是基于 HTTP 协议的一些扩展升级,以此来实现对目录文件实现存储读写。本文主要是记录如何实现一个 WEBDAV 协议服务端,最终你可以利用系统内置的 WEBDAV 协议,或者支持 WEBDAV 协议的客户端软件来将你的服务挂载为一块可用的网络硬盘,也可以在应用程序中进行数据的存取使用。

参考 wiki


关于 webdav 的协议简介及协议细节,在具体协议实现过程中可以查询此文档。

基于 Web 的分布式编写和版本控制

https://zh.wikipedia.org/wiki/%E5%9F%BA%E4%BA%8EWeb%E7%9A%84%E5%88%86%E5%B8%83%E5%BC%8F%E7%BC%96%E5%86%99%E5%92%8C%E7%89%88%E6%9C%AC%E6%8E%A7%E5%88%B6

WEBDAV 协议细则 Web Distributed Authoring and Versioning (WebDAV) Access Control Protocol

http://webdav.org/specs/rfc3744.html

入手

这个文档看起来比较生硬,也很难入手,不知道如何去写,如何去验证协议有效性,同时,我也没有找到合适测试 WEBDAV 协议的工具。

PHP 还有一款 webdav 协议的软件 sabre, 这个 sabre 比较庞大,服务拆分的非常细,入门学习,问题追踪非常困难。

在这里换一种方式去理解去实现,比如先找到一款其他人可用能用的服务端,然后通过客户端去挂载访问,进行简单的目录文件预览、文件读取、文件写入删除等操作,同时呢因为是基于 HTTP 协议的你也很容易通过抓包去了解每一步操作发生了些什么。

这里推荐使用 wireshark 去进行抓包,记得将你服务端部署在其他机器上,方便通过 IP 过滤无效的数据包。

无论通过什么方式,在这里我们大概了解到了,实现 WEBDAV 协议服务端最基础的几个操作就是:

  • PUT 创建写入文件
  • DELETE 删除文件
  • GET 获取文件内容
  • PROPFIND 获取文件或目录列表信息

实现了上面的,应该就能实现基本的硬盘挂载了吧,下面开始进行实验。

由于是 HTTP 协议,所以也是基于 web 服务的,不需要我们做过多的其他配置。

新建一个工作目录:

由于 PHP 本身已经具有 HTTP 协议解析服务,在这里我们直接通过 PHP 内置功能创建一个 web 服务:

php -S 0.0.0.0:9999

然后在工作目录中创建 php 文件,就可以通过浏览器访问到。

创建一个 webdav.php 文件,并通过浏览器访问,确保没有问题。

实现基础类

首先实现一个基础的类:

<?php
class dav{

    public function options()
    {

    }

    public function head()
    {

    }

    public function get()
    {

    }

    public function put()
    {

    }

    public function propfind()
    {

    }

    public function delete()
    {

    }
}

$dav = new dav();
$request_method = strtolower($_SERVER['REQUEST_METHOD']);
if (method_exists($dav, $request_method)) {
    $dav->$request_method();
} else {
    // 405 Method Not Allowed
}

当 PHP 接收到请求,会根据具体的请求方法执行到对应的类方法。

根据抓包,我们发现 windows 尝试连接 我们服务时,还会请求一次 options 操作,返回当前服务允许访问的方法:

    public function options()
    {
        header('Allow: OPTIONS, GET, PUT, PROPFIND, PROPPATCH');
        // Allow: OPTIONS, GET, PUT, PROPFIND, PROPPATCH, ACL
        response_http_code(200);
    }

同时,我们还发现 当文件目录不存在时,或者出现错误还会返回一些其他 HTTP 状态码,同时这也是 WEBDAV 服务最基础的一些协商,当资源或请求存在问题时,会返回对应的 HTTP 状态码。

简单实现一下:

function http_code($num)
{
    $http = array(
        100 => "HTTP/1.1 100 Continue",
        101 => "HTTP/1.1 101 Switching Protocols",
        200 => "HTTP/1.1 200 OK",
        201 => "HTTP/1.1 201 Created",
        202 => "HTTP/1.1 202 Accepted",
        203 => "HTTP/1.1 203 Non-Authoritative Information",
        204 => "HTTP/1.1 204 No Content",
        205 => "HTTP/1.1 205 Reset Content",
        206 => "HTTP/1.1 206 Partial Content",
        207 => "HTTP/1.1 207 Multi-Status",
        300 => "HTTP/1.1 300 Multiple Choices",
        301 => "HTTP/1.1 301 Moved Permanently",
        302 => "HTTP/1.1 302 Found",
        303 => "HTTP/1.1 303 See Other",
        304 => "HTTP/1.1 304 Not Modified",
        305 => "HTTP/1.1 305 Use Proxy",
        307 => "HTTP/1.1 307 Temporary Redirect",
        400 => "HTTP/1.1 400 Bad Request",
        401 => "HTTP/1.1 401 Unauthorized",
        402 => "HTTP/1.1 402 Payment Required",
        403 => "HTTP/1.1 403 Forbidden",
        404 => "HTTP/1.1 404 Not Found",
        405 => "HTTP/1.1 405 Method Not Allowed",
        406 => "HTTP/1.1 406 Not Acceptable",
        407 => "HTTP/1.1 407 Proxy Authentication Required",
        408 => "HTTP/1.1 408 Request Time-out",
        409 => "HTTP/1.1 409 Conflict",
        410 => "HTTP/1.1 410 Gone",
        411 => "HTTP/1.1 411 Length Required",
        412 => "HTTP/1.1 412 Precondition Failed",
        413 => "HTTP/1.1 413 Request Entity Too Large",
        414 => "HTTP/1.1 414 Request-URI Too Large",
        415 => "HTTP/1.1 415 Unsupported Media Type",
        416 => "HTTP/1.1 416 Requested range not satisfiable",
        417 => "HTTP/1.1 417 Expectation Failed",
        500 => "HTTP/1.1 500 Internal Server Error",
        501 => "HTTP/1.1 501 Not Implemented",
        502 => "HTTP/1.1 502 Bad Gateway",
        503 => "HTTP/1.1 503 Service Unavailable",
        504 => "HTTP/1.1 504 Gateway Time-out"
    );
    return $http[$num];
}

function response_http_code($num)
{
    header(http_code($num));
}

并将入口处,加入 405 状态码响应:

$dav = new dav();
$request_method = strtolower($_SERVER['REQUEST_METHOD']);
if (method_exists($dav, $request_method)) {
    $dav->$request_method();
} else {
    // 405 Method Not Allowed
    response_http_code(405);
}

新增一个构造方法,并将我们的存储目录设为 public:

    protected  $public;

    public function __construct()
    {
        $this->public = __DIR__.'/public';
    }

PROPFIND 实现目录文件列表

PROPFIND 方法一般返回的文件属性详情,同时也可以返回目录的文件列表及目录本身的属性。

rfc3744 中的请求体和响应内容参考:

http://webdav.org/specs/rfc3744.html#n-example--retrieving-dav-owne

>> Request <<

PROPFIND /papers/ HTTP/1.1 
Host: www.example.com 
Content-type: text/xml; charset="utf-8"  
Content-Length: xxx 
Depth: 0 
Authorization: Digest username="jim",  
  realm="[email protected]", nonce="...", 
  uri="/papers/", response="...", opaque="..." 

<?xml version="1.0" encoding="utf-8" ?> 
<D:propfind xmlns:D="DAV:"> 
  <D:prop> 
    <D:owner/> 
  </D:prop> 
</D:propfind> 

>> Response <<

HTTP/1.1 207 Multi-Status 
Content-Type: text/xml; charset="utf-8" 
Content-Length: xxx 

<?xml version="1.0" encoding="utf-8" ?> 
<D:multistatus xmlns:D="DAV:">  
  <D:response>  
    <D:href>http://www.example.com/papers/</D:href> 
    <D:propstat> 
      <D:prop> 
        <D:owner> 
          <D:href>http://www.example.com/acl/users/gstein</D:href>        
        </D:owner> 
      </D:prop> 
      <D:status>HTTP/1.1 200 OK</D:status> 
    </D:propstat> 
  </D:response> 
</D:multistatus> 

我们接下来实现这个请求方法的返回内容就行了。

WEBDAV 协议传输数据都是通过 XML 数据返回,我从其他 WEBDAV 服务中复制了一份 PROPFIND 返回的内容,大概类似这样子的:

<?xml version="1.0" encoding="utf-8"?>
<D:multistatus xmlns:D="DAV:">
  <D:response>
    <D:href>/dav/</D:href>
    <D:propstat>
      <D:prop>
        <D:supportedlock>
          <D:lockentry>
            <D:lockscope>
              <D:exclusive/>
            </D:lockscope>
            <D:locktype>
              <D:write/>
            </D:locktype>
          </D:lockentry>
        </D:supportedlock>
        <D:resourcetype>
          <D:collection></D:collection>
        </D:resourcetype>
        <D:getlastmodified>Sun, 11 Apr 2021 16:23:30 GMT</D:getlastmodified>
        <D:displayname/>
      </D:prop>
      <D:status>HTTP/1.1 200 OK</D:status>
    </D:propstat>
  </D:response>
  <D:response>
    <D:href>/dav/%E6%96%B0%E5%BB%BA%E6%96%87%E6%9C%AC%E6%96%87%E6%A1%A3.txt</D:href>
    <D:propstat>
      <D:prop>
        <D:supportedlock>
          <D:lockentry>
            <D:lockscope>
              <D:exclusive/>
            </D:lockscope>
            <D:locktype>
              <D:write/>
            </D:locktype>
          </D:lockentry>
        </D:supportedlock>
        <D:resourcetype/>
        <D:getcontentlength>0</D:getcontentlength>
        <D:getetag>"167508a952fb5c180"</D:getetag>
        <D:getcontenttype/>
        <D:displayname/>
        <D:getlastmodified>Mon, 12 Apr 2021 06:32:44 GMT</D:getlastmodified>
      </D:prop>
      <D:status>HTTP/1.1 200 OK</D:status>
    </D:propstat>
  </D:response>
</D:multistatus>

通过查看已有的返回内容,这个返回内容中只有一个文件,并且还包含了目录本身的属性信息。

那么我们也按照其规律将目录的本身和目录下的文件一并返回。

首先创建一个简单的 文件 XML 信息构建函数,为了方便我们直接通过变量替换,不用 XML 对象去做。

function response_basedir($dir, $lastmod, $status)
{
    $lastmod = gmdate("D, d M Y H:i:s", $lastmod)." GMT";
    $fmt = <<<EOF
<d:response>
        <d:href>{$dir}</d:href>
        <d:propstat>
            <d:prop>
                <d:getlastmodified>{$lastmod}</d:getlastmodified>
                <d:resourcetype>
                    <d:collection/>
                </d:resourcetype>
            </d:prop>
            <d:status>{$status}</d:status>
        </d:propstat>
    </d:response>
EOF;
    // /dav/
    //Sun, 11 Apr 2021 16:23:30 GMT
    // HTTP/1.1 200 OK

    return $fmt;
}

function response_dir($dir, $lastmod, $status)
{
    $lastmod = gmdate("D, d M Y H:i:s", $lastmod)." GMT";
    $fmt = <<<EOF
  <D:response>
    <D:href>{$dir}</D:href>
    <D:propstat>
      <D:prop>
        <D:resourcetype>
          <D:collection></D:collection>
        </D:resourcetype>
        <D:getlastmodified>{$lastmod}</D:getlastmodified>
        <D:displayname/>
      </D:prop>
      <D:status>{$status}</D:status>
    </D:propstat>
  </D:response>
EOF;
    // /dav/
    //Sun, 11 Apr 2021 16:23:30 GMT
    // HTTP/1.1 200 OK

    return $fmt;
}

function response_file($file_path, $lastmod, $file_length, $status)
{
    $lastmod = gmdate("D, d M Y H:i:s", $lastmod)." GMT";
    $tag = md5($lastmod.$file_path);
    $fmt = <<<EOF
  <D:response>
    <D:href>{$file_path}</D:href>
    <D:propstat>
      <D:prop>
        <D:resourcetype/>
        <D:getcontentlength>{$file_length}</D:getcontentlength>
        <D:getetag>"{$tag}"</D:getetag>
        <D:getcontenttype/>
        <D:displayname/>
        <D:getlastmodified>{$lastmod}</D:getlastmodified>
      </D:prop>
      <D:status>{$status}</D:status>
    </D:propstat>
  </D:response>
EOF;
    // /dav/%E6%96%B0%E5%BB%BA%E6%96%87%E6%9C%AC%E6%96%87%E6%A1%A3.txt
    // 0
    // HTTP/1.1 200 OK
    // Mon, 12 Apr 2021 06:32:44 GMT
    return $fmt;

}

function response($text)
{
    return <<<EOF
<?xml version="1.0" encoding="utf-8"?>
<D:multistatus xmlns:D="DAV:">
  {$text}
</D:multistatus>
EOF;
}

注意这里的时间,为了标准化,我转换成了 GMT 格式,tag 简单用 md5 取修改时间和路径名得出。

除了目录 XML 外,额外添加了个 response_basedir 用于返回本级目录信息的 XML 内容,这和子目录略不一样。

为了知道请求时的参数,我们将 PROPFIND 方法暂时修改为输出 $_SERVER 的内容,方便提取自己想要的数据。

    public function propfind()
    {
        var_dump($_SERVER);
        die;
    }

通过 POSTMAN 请求接口,我们可以看到如下的数据:

array(23) {
["DOCUMENT_ROOT"]=>
string(50) "C:\Users\Administrator\Documents\simple-webdav-php"
["REMOTE_ADDR"]=>
string(9) "127.0.0.1"
["REMOTE_PORT"]=>
string(4) "8673"
["SERVER_SOFTWARE"]=>
string(29) "PHP 7.4.16 Development Server"
["SERVER_PROTOCOL"]=>
string(8) "HTTP/1.1"
["SERVER_NAME"]=>
string(7) "0.0.0.0"
["SERVER_PORT"]=>
string(4) "9999"
["REQUEST_URI"]=>
string(12) "/webdav.php/"
["REQUEST_METHOD"]=>
string(8) "PROPFIND"
["SCRIPT_NAME"]=>
string(11) "/webdav.php"
["SCRIPT_FILENAME"]=>
string(61) "C:\Users\Administrator\Documents\simple-webdav-php\webdav.php"
["PATH_INFO"]=>
string(1) "/"
["PHP_SELF"]=>
string(12) "/webdav.php/"
["HTTP_USER_AGENT"]=>
string(21) "PostmanRuntime/7.26.8"
["HTTP_ACCEPT"]=>
string(3) "*/*"
["HTTP_POSTMAN_TOKEN"]=>
string(36) "e4809ebd-013a-41a7-885f-28828990ee10"
["HTTP_HOST"]=>
string(14) "127.0.0.1:9999"
["HTTP_ACCEPT_ENCODING"]=>
string(17) "gzip, deflate, br"
["HTTP_CONNECTION"]=>
string(10) "keep-alive"
["CONTENT_LENGTH"]=>
string(1) "0"
["HTTP_CONTENT_LENGTH"]=>
string(1) "0"
["REQUEST_TIME_FLOAT"]=>
float(1618726819.9309)
["REQUEST_TIME"]=>
int(1618726819)
}

构想一下,webdav.php 为我们的服务器文件,那么请求地址应该是 http://127.0.0.1:9999/webdav.php

列出根目录的请求就应该是: PROPFIND http://127.0.0.1:9999/webdav.php

列出 mobile 的请求就应该是: PROPFIND http://127.0.0.1:9999/webdav.php/mobile

查看 mobile 目录下 test.txt 的请求应该是:  GET  http://127.0.0.1:9999/webdav.php/mobile/test.txt

参考上面 $_SERVER 返回的内容,我们可以通过 $_SERVER['PATH_INFO'] 拿到自己想要的目标文件相对路径,由于我们将 public 作为 webdav 服务的根目录,那么可以写出如下的代码:

    public function propfind()
    {
        $path = $this->public.'/'.ltrim($_SERVER['PATH_INFO'] ?? '','/');
        $dav_base_dir = $_SERVER['SCRIPT_NAME']. '/'.ltrim($_SERVER['PATH_INFO'] ?? '','/');

        $files = scandir($path);

        $response_text = response_basedir($dav_base_dir,filemtime($path),http_code(200));
        foreach ($files as $file){
            if($file == '.' || $file == '..'){
                continue;
            }
            $file_path  = $path.'/'.$file;
            $mtime = filemtime($file_path);

            if(is_dir($file_path)){
                $response_text.= response_dir($dav_base_dir.'/'.$file,$mtime,http_code(200));
            }elseif(is_file($file_path)){
                $response_text.= response_file($dav_base_dir.'/'.$file, $mtime,filesize($file_path),http_code(200));
            }
        }
        response_http_code(207);
        header('Content-Type: text/xml; charset=utf-8');
        echo response($response_text);
    }

给 public 目录创建一个文件,我们再通过 POSTMAN 请求接口:

<?xml version="1.0" encoding="utf-8"?>
<D:multistatus xmlns:D="DAV:">
    <D:response>
        <D:href>/webdav.php/</D:href>
        <D:propstat>
            <D:prop>
                <D:resourcetype>
                    <D:collection></D:collection>
                </D:resourcetype>
                <D:getlastmodified>Sun, 18 Apr 2021 06:31:01 GMT</D:getlastmodified>
                <D:displayname/>
            </D:prop>
            <D:status>HTTP/1.1 200 OK</D:status>
        </D:propstat>
    </D:response>
    <D:response>
        <D:href>/webdav.php//新建文本文档.txt</D:href>
        <D:propstat>
            <D:prop>
                <D:resourcetype/>
                <D:getcontentlength>0</D:getcontentlength>
                <D:getetag>"018ef2aa2b63d83e6b1c6f2ff90fb792"</D:getetag>
                <D:getcontenttype/>
                <D:displayname/>
                <D:getlastmodified>Sun, 18 Apr 2021 06:31:01 GMT</D:getlastmodified>
            </D:prop>
            <D:status>HTTP/1.1 200 OK</D:status>
        </D:propstat>
    </D:response>
</D:multistatus>

看样子已经可以了。

尝试将这个地址挂载到 windows10 的系统中访问,http://127.0.0.1:9999/webdav.php

但很不幸失败了:

为了查清楚发生了什么,我们将请求日志记录一下。

$dav = new dav();
$request_method = strtolower($_SERVER['REQUEST_METHOD']);
$header_text = "";
foreach (getallheaders() as $name => $value) {
    $header_text.="$name: $value\n";
}
$input = file_get_contents("php://input");
file_put_contents('./HEAD.log', $request_method.' '.$_SERVER['REQUEST_URI'].PHP_EOL.$header_text. PHP_EOL.$input.PHP_EOL,FILE_APPEND);
if (method_exists($dav, $request_method)) {
    $dav->$request_method();
} else {
    // 405 Method Not Allowed
    response_http_code(405);
}

再尝试点击 下一步 ,打开产生的日志。

options /webdav.php
Connection: Keep-Alive
User-Agent: Microsoft-WebDAV-MiniRedir/10.0.19042
translate: f
Host: 127.0.0.1:9999


propfind /webdav.php
Connection: Keep-Alive
User-Agent: Microsoft-WebDAV-MiniRedir/10.0.19042
Depth: 0
translate: f
Content-Length: 0
Host: 127.0.0.1:9999


propfind /webdav.php
Connection: Keep-Alive
User-Agent: Microsoft-WebDAV-MiniRedir/10.0.19042
Depth: 0
translate: f
Content-Length: 0
Host: 127.0.0.1:9999

经过对比测试,发现 Depth: 0 时,只需要返回目录本身属性信息,不需要目录下其他文件。

修改 PROPFIND 方法为:

 public function propfind()
    {
        $path = $this->public.'/'.ltrim($_SERVER['PATH_INFO'] ?? '','/');
        $dav_base_dir = $_SERVER['SCRIPT_NAME']. '/'.ltrim($_SERVER['PATH_INFO'] ?? '','/');

        if(isset($_SERVER['HTTP_DEPTH'])){
            if($_SERVER['HTTP_DEPTH'] == 0){
                if(is_file($path)){
                    $response_text = response_file($dav_base_dir,filemtime($path),filesize($path),http_code(200));
                }elseif(is_dir($path)){
                    $response_text = response_basedir($dav_base_dir,filemtime($path),http_code(200));
                }else{
                    response_http_code(404);
                    return;
                }
                response_http_code(207);
                header('Content-Type: text/xml; charset=utf-8');
                echo response($response_text);
                exit;
            }
        }

        $files = scandir($path);

        $response_text = response_basedir($dav_base_dir,filemtime($path),http_code(200));
        foreach ($files as $file){
            if($file == '.' || $file == '..'){
                continue;
            }
            $file_path  = $path.'/'.$file;
            $mtime = filemtime($file_path);

            if(is_dir($file_path)){
                $response_text.= response_dir($dav_base_dir.'/'.$file,$mtime,http_code(200));
            }elseif(is_file($file_path)){
                $response_text.= response_file($dav_base_dir.'/'.$file, $mtime,filesize($file_path),http_code(200));
            }
        }
        response_http_code(207);
        header('Content-Type: text/xml; charset=utf-8');
        echo response($response_text);
    }

再次点击 下一步 按钮,发现已经成功了。

GET 实现获取文件信息

GET 直接将对应的资源内容返回就行了,不需要做处理。

    public function get()
    {
        header('Content-Type: application/octet-stream');
        $path = $this->public.'/'.ltrim($_SERVER['PATH_INFO'],'/');
        if(is_file($path)){
            $fh = fopen($path,'r');
            $oh = fopen('php://output','w');
            stream_copy_to_stream($fh, $oh);
            fclose($fh);
            fclose($oh);
        }else{
            response_http_code(404);
        }
    }

PUT 实现创建写入文件内容

    public function put()
    {
        $input = fopen("php://input",'r');
        try{
            $path = $this->public.'/'.ltrim($_SERVER['PATH_INFO'],'/');
            $fh = fopen($path,'w');
            stream_copy_to_stream($input, $fh);
            fclose($fh);

        }catch (Throwable $throwable){
            response_http_code(503);
            echo $throwable->getMessage();
        }
    }

HEAD 方法返回文件大小时间信息

    public function head()
    {
        header('Content-Type: application/octet-stream');

        $path = $this->public.'/'.ltrim($_SERVER['PATH_INFO'],'/');
        if(is_file($path)){
            header('Content-Length: '.filesize($path));
            $lastmod = filemtime($path);
            $lastmod = gmdate("D, d M Y H:i:s", $lastmod)." GMT";
            header('Last-Modified: '.$lastmod);
        }else{
            response_http_code(404);
        }
    }

DELETE 对文件资源进行删除

    public function delete()
    {
        header('Content-Type: application/octet-stream');
        $path = $this->public.'/'.ltrim($_SERVER['PATH_INFO'],'/');
        if($path){
            if(unlink($path)){
                response_http_code(200);
            }else{
                response_http_code(503);
            }
        }else{
            response_http_code(404);
        }
    }

MKCOL 创建目录

当发现目录无法创建,通过提取创建目录时的请求信息用于参考:

propfind /webdav.php/%E6%96%B0%E5%BB%BA%E6%96%87%E4%BB%B6%E5%A4%B9
Connection: Keep-Alive
User-Agent: Microsoft-WebDAV-MiniRedir/10.0.19042
Depth: 0
translate: f
Content-Length: 0
Host: 127.0.0.1:9999


mkcol /webdav.php/%E6%96%B0%E5%BB%BA%E6%96%87%E4%BB%B6%E5%A4%B9
Connection: Keep-Alive
User-Agent: Microsoft-WebDAV-MiniRedir/10.0.19042
translate: f
Content-Length: 0
Host: 127.0.0.1:9999

MOVE 文件或目录更名

请求日志参考:

move /webdav.php/%E6%96%B0%E5%BB%BA%E6%96%87%E4%BB%B6%E5%A4%B9
Connection: Keep-Alive
User-Agent: Microsoft-WebDAV-MiniRedir/10.0.19042
Destination: http://127.0.0.1:9999/webdav.php/dffds
Overwrite: F
translate: f
Content-Length: 0
Host: 127.0.0.1:9999

PROPPATCH 方法设置资源属性

当我们实现了基本的操作方法后,发现我们刚才新建的文本内容并不能进行保存。

通过查看日志,我们发现这里有一个 LOCK 操作:

lock /webdav.php/%E6%96%B0%E5%BB%BA%E6%96%87%E6%9C%AC%E6%96%87%E6%A1%A3.txt
Cache-Control: no-cache
Connection: Keep-Alive
Pragma: no-cache
Content-Type: text/xml; charset="utf-8"
User-Agent: Microsoft-WebDAV-MiniRedir/10.0.19042
translate: f
Timeout: Second-3600
Content-Length: 220
Host: 127.0.0.1:9999

<?xml version="1.0" encoding="utf-8" ?><D:lockinfo xmlns:D="DAV:"><D:lockscope><D:exclusive/></D:lockscope><D:locktype><D:write/></D:locktype><D:owner><D:href>DESKTOP-WHOAMI\Administrator</D:href></D:owner></D:lockinfo>
propfind /webdav.php
Connection: Keep-Alive
User-Agent: Microsoft-WebDAV-MiniRedir/10.0.19042
Depth: 1
translate: f
Content-Length: 0
Host: 127.0.0.1:9999


propfind /webdav.php
Connection: Keep-Alive
User-Agent: Microsoft-WebDAV-MiniRedir/10.0.19042
Depth: 0
translate: f
Content-Length: 0
Host: 127.0.0.1:9999

我们将添加一个 LOCK 方法,不去实现它,但需要正确返回:

    public function lock()
    {
        response_http_code(501);
    }

当再次尝试,就可以进行保存了。

此时新建操作可以进行使用了,但当外部的文件复制进来时,会提示无法读源文件或磁盘。

原因时缺失 PROPPATCH 方法,无法对资源进行属性设置。可以参考 rfc3744 进行响应体构造  http://webdav.org/specs/rfc3744.html#n-example--an-attempt-to-set-dav-owner

这里我们简单返回 403 禁止的内容:

public function proppatch()
{
    $path = $this->public . '/' . ltrim($_SERVER['PATH_INFO'], '/');
    echo <<<EOF
<?xml version="1.0" encoding="utf-8" ?> 
<D:multistatus xmlns:D="DAV:">  
<D:response>  
<D:href>{$path}</D:href> 
<D:propstat> 
  <D:prop><D:owner/></D:prop> 
  <D:status>HTTP/1.1 403 Forbidden</D:status> 
  <D:responsedescription>
    <D:error><D:cannot-modify-protected-property/></D:error>
    Failure to set protected property (DAV:owner) 
  </D:responsedescription> 
</D:propstat> 
</D:response> 
</D:multistatus> 
EOF;

    response_http_code(207);
}

再次从外部拖放文件到 webdav 网络盘中,此时一切正常。

结束

基本到此就结束了,通过 PHP 实现了 WEBDAV 服务端一些基本的功能,也记录了一些抓包和问题排查方法,你可以通过此为基础,参考  rfc3744 将该代码完善起来使用。

但是 PHP 还存在一个问题就是,当触发 PUT 方法请求到 PHP 服务器时,PHP 默认会将所有文件数据存储到内存当中去,之后才会执行脚本,这就导致上传的文件必须小于服务器运行内存,WEBDAV 协议本身我也没有找到关于文件分块的内容,总之这点弊端很严重,对于小内存机器就很难进行大文件传输。

关于上文的所有代码放在了这里,如果有需要可以参考:https://github.com/ellermister/simple-webdav-php

Profile Picture
buy cialis professional

- From Ubuntu Firefox - 2023-05-26 01:46

Greetings! I've been following your site for a while now and finally got the courage to go ahead and give you a shout out from Dallas Texas! Just wanted to tell you keep up the excellent job!

44 Likes
Comment

Profile Picture
whitelabel casino

- From Windows Chrome - 1週前

It's not in any respect a problem to seek out individuals who will say Abloh is main a cult of personality dependent on teenagers who don’t know higher, that his undeniable historic significance as the most prolific designer of his era is at odds together with his seeming disinterest in giving anyone a good reason to care. Before yow will discover a personal label provider, it's essential to know what product you want to promote. Just like the earlier step, the purchaser won’t know where the package deal got here from or how much it prices us (the dropshippers). However, in instances like Amazon present packaging, the buyers will see that the product came from Amazon because we can’t take away their branding. With an unlimited product range and a assured 30 day return period, Amazon is a superb option as our white label dropshipping supplier. Meaning, if we find the same product at a decrease cost, Overstock will supply us the decrease value.


media iamge
StudioEIM - 冒险者讲习所
0:00