В Microsoft Windows начиная с 2к встроен Веб Клиент, по сути это средство для монтирования сетевых дисков по протоколу WebDav, который ходит поверх HTTP/HTTPS.
Клиент писался явно под себя (связка с IIS) и от того работает весьма корявосвоеобразно.
Это единственный способ “малой” кровью примонтировать диск через интернет не настраивая VPN для проброса SAMBA протокола.
SAMB-у в принципе тоже без VPN можно пробросить, но это из области поиска приключений с последствиями.
Nginx в базовом функционале имеет не полную поддержку Dav: PUT DELETE MKCOL COPY MOVE.
Расширить его ещё двумя: PROPFIND и OPTIONS можно с помощью модуля: dav-ext
Для работы потребуется Nginx собранный с поддержкой Dav, dav-ext, rewrite и headers_more.
[x] HTTP_DAV Enable http_webdav module [x] HTTP_DAV_EXT 3rd party webdav_ext module [x] HTTP_REWRITE Enable http_rewrite module [x] HEADERS_MORE 3rd party headers_more module
Перед тем как создать файл WebDav клиент проверяет наличие файла посылая запрос:
PROPFIND /!!!!/test.lnk HTTP/1.1 Connection: Keep-Alive User-Agent: Microsoft-WebDAV-MiniRedir/6.1.7601 Depth: 0 translate: f Content-Length: 0 Host: 172.16.0.254:8089
В ответ IIS ему выдаёт:
HTTP/1.1 404 Resource Not Found Content-Length: 1635 Content-Type: text/html Server: Microsoft-IIS/6.0 X-Powered-By: ASP.NET Date: Sun, 10 Aug 2014 20:06:08 GMT <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> ...
Только это не правильно, должен быть код 207 и xml в котором описан элемент и код для него, те 404 должно быть в xml.
с точки зрения стандартов: http://tools.ietf.org/html/rfc2518#page-24
с точки зрения доков мс: http://msdn.microsoft.com/en-us/library/aa142960(v=exchg.65).aspx
У яндекса тоже в примерах xml: http://api.yandex.ru/disk/doc/dg/reference/propfind_property-request.xml
Ответ Nginx:
HTTP/1.1 207 Multi-Status Server: nginx/1.7.4 Date: Sun, 10 Aug 2014 21:17:34 GMT Transfer-Encoding: chunked Connection: keep-alive Keep-Alive: timeout=60 47 <?xml version="1.0" encoding="utf-8" ?> <D:multistatus xmlns:D="DAV:"> cc <D:response> <D:href>/Family/test.lnk</D:href> <D:propstat> <D:prop> </D:prop> <D:status>HTTP/1.1 404 Not Found</D:status> </D:propstat> </D:response> 11 </D:multistatus> 0
Такой “неожиданный” ответ сносит голову WebDav клиенту винды.
Фикс 1: в конфиге (возможно это сведёт с ума остальные, порядочные WebDav клиенты, но лично мне нужен был только один не такой как все):
error_page 599 = @propfind_handler; if ($request_method = PROPFIND) { return 599; } location @propfind_handler { internal; open_file_cache off; if (!-e $webdav_root/$uri) { # Microsoft specific handle. return 404; } root $webdav_root; dav_ext_methods PROPFIND; }
WebDav от мс очень хочет метод PROPPATCH, которого в Nginx и расширениях нет. Совсем нет.
Я рассматривал два варианта решения:
1. Написать плагин к Nginx или патч к dav-ext, короче Си код и пересборка Nginx.
2. Положится на кривость виндовой реализации WebDav и скормить статический ответ.
Запрос:
PROPPATCH /Family/test.lnk HTTP/1.1 Cache-Control: no-cache Connection: Keep-Alive Pragma: no-cache Content-Type: text/xml; charset="utf-8" User-Agent: Microsoft-WebDAV-MiniRedir/6.1.7601 translate: f Content-Length: 443 Host: xxx.xxx.net <?xml version="1.0" encoding="utf-8" ?><D:propertyupdate xmlns:D="DAV:" xmlns:Z="urn:schemas-microsoft-com:"><D:set><D:prop><Z:Win32CreationTime>Sun, 10 Aug 2014 21:30:21 GMT</Z:Win32CreationTime><Z:Win32LastAccessTime>Sun, 10 Aug 2014 21:30:22 GMT</Z:Win32LastAccessTime><Z:Win32LastModifiedTime>Sun, 10 Aug 2014 21:30:22 GMT</Z:Win32LastModifiedTime><Z:Win32FileAttributes>00000020</Z:Win32FileAttributes></D:prop></D:set></D:propertyupdate>
Ответ IIS:
HTTP/1.1 207 Multi-Status Date: Sun, 10 Aug 2014 12:24:47 GMT Server: Microsoft-IIS/6.0 X-Powered-By: ASP.NET Content-Type: text/xml Transfer-Encoding: chunked <?xml version="1.0"?><a:multistatus xmlns:a="DAV:" xmlns:b="urn:schemas-microsoft-com:"><a:response><a:href>http://172.16.0.254:8088//!!!!/test.lnk</a:href><a:propstat><a:status>HTTP/1.1 200 OK</a:status><a:prop><b:Win32CreationTime/><b:Win32LastAccessTime/><b:Win32LastModifiedTime/><b:Win32FileAttributes/></a:prop></a:propstat></a:response></a:multistatus> 0
“Пропритериарщина”, - подумал я. Вот кому такое вообще надо? И как эти все атрибуты писать в разных ОС и разных ФС?…
Фикс 2
if ($request_method = PROPPATCH) { # Unsupported, allways return OK. add_header Content-Type 'text/xml'; return 207 '<?xml version="1.0"?><a:multistatus xmlns:a="DAV:"><a:response><a:propstat><a:status>HTTP/1.1 200 OK</a:status></a:propstat></a:response></a:multistatus>'; }
Из ответа IIS я выкинул всё что мне не понравилось и виндовый WebDav это проглотил.
Теперь одиночные и группы файлов без проблем можно копировать на сетевой диск примонтированный по WebDav.
Минусом - не выставляется оригинальная дата создания, модификации и атрибуты.
Приходит запрос:
MKCOL /Family/MR3020 HTTP/1.1 Connection: Keep-Alive User-Agent: Microsoft-WebDAV-MiniRedir/6.1.7601 translate: f Content-Length: 0 Host: xxx.xxx.net
Nginx на него отвечает (немного странно выбирая код, на мой взгляд, но в принципе правильно):
HTTP/1.1 409 Conflict Server: nginx/1.7.4 Date: Sun, 10 Aug 2014 21:43:15 GMT Content-Type: text/html Content-Length: 166 Connection: keep-alive Keep-Alive: timeout=60 <html> <head><title>409 Conflict</title></head> <body bgcolor="white"> <center><h1>409 Conflict</h1></center> <hr><center>nginx/1.7.4</center> </body> </html>
IIS отвечает: “HTTP/1.1 201 Created”, - смотреть там не на что.
Как должно быть описано:
У майкрософта: http://msdn.microsoft.com/en-us/library/aa142923(v=exchg.65).aspx
У яндекса: http://api.yandex.ru/disk/doc/dg/reference/mkcol.xml
И даже в RFC: http://tools.ietf.org/html/rfc2518#page-33
Во всех примерах URL оканчивается слешем.
Но только не в запросе WebDav клиента от мс.
Вариантов опять было два:
1. Поправить файл: http://lxr.nginx.org/source/src/http/modules/ngx_http_dav_module.c строчки 484 - 493, там как раз проверка наличия слеша и его отрезание.
2. Пофиксить через конфиг.
Вариант 1 я оставляю на усмотрение програмеров nginx, может по стандарту оно и должно ругаться.
Фикс 3:
if ($request_method = MKCOL) { # Microsoft specific handle: add trailing slash. rewrite ^(.*[^/])$ $1/ break; }
Вот для этого пустяка и потребовался REWRITE плагин.
Ноги тут те же что и в п3: отсутствие слеша на конце урла.
Однако я столкнулся с тем, что nginx тоже ведёт себя несколько странно: если слеш на конце отсутствует, то nginx считает что его просят удалить файл, и получает ошибку: 21: Is a directory при попытке удалить.
Фикс 4:
error_page 598 = @delete_handler; if ($request_method = DELETE) { return 598; } location @delete_handler { internal; open_file_cache off; if (-d $webdav_root/$uri) { # Add trailing slash to dirs. rewrite ^(.*[^/])$ $1/ break; } root $webdav_root; dav_methods DELETE; }
Если кратко, то переносим обработку DELETE в отдельный локейшин (процедуру), дальше проверяем, если удаляется папка то добавляем слеш.
Ноги тут те же что и в п4: отсутствие слеша на конце урла и в Destination заголовке.
Для решения проблемы с добавлением “/” в заголовок Destinaton нам потребуется модуль headers_more.
Фикс 6:
error_page 597 = @copy_move_handler; if ($request_method = COPY) { return 597; } if ($request_method = MOVE) { return 597; } location @copy_move_handler { internal; open_file_cache off; if (-d $webdav_root/$uri) { # Add trailing slash to dirs. more_set_input_headers 'Destination: $http_destination/'; rewrite ^(.*[^/])$ $1/ break; } root $webdav_root; dav_methods COPY MOVE; }
Клиент запрашивает опции не у той папки которую мы хотим чтобы он подключил а у корня сервера.
Фикс 6:
location / { if ($request_method = OPTIONS) { add_header Allow 'OPTIONS, GET, HEAD, POST, PUT, MKCOL, MOVE, COPY, DELETE, PROPFIND, PROPPATCH'; add_header DAV '1, 2'; return 200; } }
Возвращаем ему статический список.
Можно было просто разрешить: dav_ext_methods OPTIONS;
Соединение происходит но в логах nginx ничего нет, а клиент сообщает об ошибке.
В логах с включённым дебагом ssl видно что рукопожатие не проходит.
Проблема у меня проявилась не так давно, после обновления OpenSSL.
При этом IE и другие браузеры могут авторизоваться по этому URL и отобразить листинг директории.
Фикс 7: Заменить OpenSSL на LibreSSL и пересобрать nginx. Возможно OpenSSL уже исправили.
Не трудно заметить, что nginx не добавляет в ответы:
Content-Type: text/xml
и хотя в данном случае это не создаёт проблем, но всё же это не правильно.
WebDav для Nginx:
# WebDAV set $webdav_root "/mnt/WebDav_folder"; location ^~ /Family { if ($ssl_protocol = "") { # Block non ssl/tls connections. add_header Strict-Transport-Security 'max-age=600'; return 403; } auth_basic "Private site"; auth_basic_user_file /usr/local/etc/nginx/secure/authbasic.htpasswd; root $webdav_root; error_page 599 = @propfind_handler; error_page 598 = @delete_handler; error_page 597 = @copy_move_handler; open_file_cache off; client_max_body_size 50m; if ($request_method = PROPFIND) { return 599; } if ($request_method = PROPPATCH) { # Unsupported, allways return OK. add_header Content-Type 'text/xml'; return 207 '<?xml version="1.0"?><a:multistatus xmlns:a="DAV:"><a:response><a:propstat><a:status>HTTP/1.1 200 OK</a:status></a:propstat></a:response></a:multistatus>'; } if ($request_method = MKCOL) { # Microsoft specific handle: add trailing slash. rewrite ^(.*[^/])$ $1/ break; } if ($request_method = DELETE) { return 598; } if ($request_method = COPY) { return 597; } if ($request_method = MOVE) { return 597; } dav_methods PUT MKCOL; dav_ext_methods OPTIONS; create_full_put_path on; min_delete_depth 0; dav_access user:rw group:rw all:rw; autoindex on; autoindex_exact_size on; autoindex_localtime on; } location @propfind_handler { internal; auth_basic "Private site"; auth_basic_user_file /usr/local/etc/nginx/secure/authbasic.htpasswd; open_file_cache off; if (!-e $webdav_root/$uri) { # Microsoft specific handle. return 404; } root $webdav_root; dav_ext_methods PROPFIND; } location @delete_handler { internal; auth_basic "Private site"; auth_basic_user_file /usr/local/etc/nginx/secure/authbasic.htpasswd; open_file_cache off; if (-d $webdav_root/$uri) { # Microsoft specific handle: Add trailing slash to dirs. rewrite ^(.*[^/])$ $1/ break; } root $webdav_root; dav_methods DELETE; } location @copy_move_handler { internal; auth_basic "Private site"; auth_basic_user_file /usr/local/etc/nginx/secure/authbasic.htpasswd; open_file_cache off; if (-d $webdav_root/$uri) { # Microsoft specific handle: Add trailing slash to dirs. more_set_input_headers 'Destination: $http_destination/'; rewrite ^(.*[^/])$ $1/ break; } root $webdav_root; dav_methods COPY MOVE; } location / { if ($request_method = OPTIONS) { add_header Allow 'OPTIONS, GET, HEAD, POST, PUT, MKCOL, MOVE, COPY, DELETE, PROPFIND, PROPPATCH'; add_header DAV '1, 2'; return 200; } }
open_file_cache off; - нужно потому что мы меняем файлы, если кеширование включено то можно будет долго гадать почему не видно файлы которые мы только что закинули на сервер.
Также в самом начале локейшена есть требование использовать SSL и если SSL используется то запрашивается BASIC авторизация.
Он как капризный ребёнок, из коробки ему подавай SSL и никакой Basic аутентификации, файлы не больше 50 мегабайт, в папках не более 500-1000 файлов.
Увы, но даже после всех проделанных настроек файлы более 4гб передавать не получится. (Скорее всего из за кривой реализации, которая файлы при открытии скачивает в память/на диск чтобы получить более менее стандартный файловый дискриптор, это мои домыслы.)
Вот здесь собраны все настройки с описанием: http://blogs.msdn.com/b/robert_mcmurray/archive/2008/01/17/webdav-redirector-registry-settings.aspx
На данный момент мои настройки WebClient выглядят так:
Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\WebClient\Parameters] "SupportLocking"=dword:00000000 "InternetServerTimeoutInSec"=dword:0000001e "ServiceDllUnloadOnStop"=dword:00000001 "ServerNotFoundCacheLifeTimeInSec"=dword:0000000a "ClientDebug"=dword:00000000 "FileSizeLimitInBytes"=dword:ffffffff "SendReceiveTimeoutInSec"=dword:0000003c "LocalServerTimeoutInSec"=dword:0000000f "FileAttributesLimitInBytes"=dword:00989680 "AcceptOfficeAndTahoeServers"=dword:00000001 "ServiceDebug"=dword:00000000 "BasicAuthLevel"=dword:00000002