Servir WebDAV tras un proxy HTTP/HTTPS

Web Distributed Authoring and Versioning (WebDAV) es una extensión del protocolo HTTP que añade verbos adicionales para poder, por ejemplo, realizar cambios en el servidor. Lo más evidente es almacenar o modificar ficheros, pero los cambios pueden ser semánticos. Por ejemplo, modificaciones coordinadas en documentos compartidos como un calendario o una agenda telefónica.

Apache soporta WebDAV y, de hecho, es la opción que yo recomiendo en vez del vetusto FTP, por ejemplo. Hay muchas ventajas: atravesar cortafuegos con facilidad, cifrado integrado a través de HTTPS y, en general, unificación con toda la infraestructura web.

Una cosa que suelo hacer con frecuencia es dar acceso a infraestructura protegida o unificar varios servidores web bajo un árbol común. Esto lo hago a través del módulo mod_proxy de Apache. Básicamente mod_proxy puede hacer que http://www.example.com/servidorA nos enseñe el contenido del servidor A y que http://www.example.com/servidorB nos muestre el contenido del servidor B. Hacer esto con mod_proxy es trivial, rutina. El cliente web se conecta a http://www.example.com/, indica la carpeta que quiere y mod_proxy realiza una petición HTTP/HTTPS por detrás al servidor adecuado para completar la petición del cliente.

Es importante, en estas circunstancias, que los enlaces que genera el servidor A sean o bien relativos o que se cambien para que parezcan provenir de http://www.example.com/servidorA. En el contexto de mod_proxy solemos usar la directiva ProxyPassReverse. Esta directiva nos permite interceptar las redirecciones HTTP generadas por el servidor A para que parezcan provenir de http://www.example.com/servidorA.

¿Qué pasa con WebDAV?. El problema es que WebDAV nos devuelve direcciones absolutas.

Veamos un ejemplo concreto. Supongamos un servidor web con esta configuración:

 <VirtualHost *:443>
 ServerName webdav.XXXX:443
 [...]

 ProxyPass /Servidor2/ https://webdav2.XXXX/
 ProxyPassReverse /Servidor2/ https://webdav2.XXXX/

 <Location />
  Dav on
  Options FollowSymLinks Indexes
  [...]
  DirectorySlash off # Para permitir renombrar y borrar directorios
 </Location>
 </VirtualHost>

Esta configuración crea un servidor virtual WebDAV en cierta máquina, pero su directorio /Servidor2/ HTTP se mapea al directorio raíz de una segunda máquina. Por lo tanto https://webdav.XXXX/fichero1.txt es un fichero en una máquina, pero https://webdav.XXXX/Servidor2/fichero2.txt es equivalente a https://webdav2.XXXX/fichero2.txt, un fichero en el segundo servidor.

Esto funciona bien, pero tenemos alguna inconsistencia. Por ejemplo, veamos qué pasa si usamos WebDAV para pedir el listado del directorio raíz del primer servidor.

Enviamos al servidor lo siguiente:

 PROPFIND / HTTP/1.0
 host: 127.0.0.1:8080
 Depth: 1
 Authorization: Basic XXXXXXXX
 Content-Type: application/xml; charset="utf-8"
 Content-Length: 92

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

Y la respuesta es:

 HTTP/1.1 207 Multi-Status
 Date: Thu, 07 May 2015 18:39:43 GMT
 Server: Apache/2.4.12 (Unix) OpenSSL/1.0.1
 Strict-Transport-Security: max-age=15552000; includeSubDomains
 Content-Length: 2279
 Connection: close
 Content-Type: text/xml; charset="utf-8"

 <?xml version="1.0" encoding="utf-8"?>
 <D:multistatus xmlns:D="DAV:" xmlns:ns0="DAV:">
 <D:response>
 <D:href>/</D:href>
 <D:propstat>
 <D:prop>
 </D:prop>
 <D:status>HTTP/1.1 200 OK</D:status>
 </D:propstat>
 </D:response>
 <D:response>
 <D:href>/fichero1.txt</D:href>
 <D:propstat>
 <D:prop>
 </D:prop>
 <D:status>HTTP/1.1 200 OK</D:status>
 </D:propstat>
 </D:response>
 <D:response>
 <D:href>/fichero2.txt</D:href>
 <D:propstat>
 <D:prop>
 </D:prop>
 <D:status>HTTP/1.1 200 OK</D:status>
 </D:propstat>
 </D:response>
 <D:response>
 <D:href>/directorio/</D:href>
 <D:propstat>
 <D:prop>
 </D:prop>
 <D:status>HTTP/1.1 200 OK</D:status>
 </D:propstat>
 </D:response>
 [...]
 </D:multistatus>

Aquí vemos que la primera máquina tiene dos ficheros (fichero1.txt y fichero2.txt) y un directorio (directorio).

Veamos qué hay dentro del directorio:

 PROPFIND /directorio/ HTTP/1.0
 host: 127.0.0.1:8080
 Depth: 1
 Authorization: Basic XXXXXXXX
 Content-Type: application/xml; charset="utf-8"
 Content-Length: 92

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

La respuesta es:

 HTTP/1.1 207 Multi-Status
 Date: Thu, 07 May 2015 18:52:27 GMT
 Server: Apache/2.4.12 (Unix) OpenSSL/1.0.1
 Strict-Transport-Security: max-age=15552000; includeSubDomains
 Content-Length: 3507
 Connection: close
 Content-Type: text/xml; charset="utf-8"

 <?xml version="1.0" encoding="utf-8"?>
 <D:multistatus xmlns:D="DAV:" xmlns:ns0="DAV:">
 <D:response>
 <D:href>/directorio/</D:href>
 <D:propstat>
 <D:prop>
 </D:prop>
 <D:status>HTTP/1.1 200 OK</D:status>
 </D:propstat>
 </D:response>
 <D:response>
 <D:href>/directorio/fichero3.txt</D:href>
 <D:propstat>
 <D:prop>
 </D:prop>
 <D:status>HTTP/1.1 200 OK</D:status>
 </D:propstat>
 </D:response>
 [...]
 </D:multistatus>

Veamos ahora qué recibimos cuando accedemos al segundo servidor a través de una URL del primer servidor mapeada con mod_proxy:

 PROPFIND /Servidor2/ HTTP/1.0
 host: 127.0.0.1:8080
 Depth: 1
 Authorization: Basic XXXXXXXX
 Content-Type: application/xml; charset="utf-8"
 Content-Length: 92

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

Nos llega el siguiente resultado:

 HTTP/1.1 207 Multi-Status
 Date: Thu, 07 May 2015 19:01:47 GMT
 Server: Apache/2.4.12 (Unix) OpenSSL/1.0.2a mod_wsgi/4.4.11
 Python/3.4.3
 Strict-Transport-Security: max-age=15552000; includeSubDomains
 Vary: Accept-Encoding
 Content-Length: 442
 Content-Type: text/xml; charset="utf-8"
 Connection: close

 <?xml version="1.0" encoding="utf-8"?>
 <D:multistatus xmlns:D="DAV:" xmlns:ns0="DAV:">
 <D:response>
 <D:href>/</D:href>
 <D:propstat>
 <D:prop>
 </D:prop>
 <D:status>HTTP/1.1 200 OK</D:status>
 </D:propstat>
 </D:response>
 <D:response>
 <D:href>/fichero3.txt</D:href>
 <D:propstat>
 <D:prop>
 </D:prop>
 <D:status>HTTP/1.1 200 OK</D:status>
 </D:propstat>
 </D:response>
 </D:multistatus>

Vemos que el servidor que nos responde es otro diferente, como puede verse en la línea 3.

Obsérvese que hemos preguntado por el directorio Servidor2, ¡pero lo que nos está llegando son respuestas para el directorio raíz!. Dependiendo del cliente WebDAV que usemos y de lo que estemos haciendo, esta inconsistencia puede ser causa de problemas sutiles difíciles de diagnosticar.

Por ejemplo, mapear directorios WebDAV de esta manera en Kodi inserta películas en la biblioteca de vídeo que luego no se pueden reproducir.

La regla de oro, entonces, es la siguiente:

Cuando mapees servicios WebDAV tras un mod_proxy, asegúrate de que el directorio que ve el cliente WebDAV y el directorio que recibe el servidor final WebDAV son el mismo.

En este caso el cliente entra en /Servidor2/ pero el servidor 2 recibe una petición para su directorio raíz /. La solución es simple:

  1. En el servidor que sirve de punto de entrada para las peticiones del cliente WebDAV cambiamos la configuración mod_proxy para que pase el directorio solicitado al segundo servidor:

    -ProxyPass        /Servidor2/  https://webdav2.XXXX/
    -ProxyPassReverse /Servidor2/  https://webdav2.XXXX/
    +ProxyPass        /Servidor2/  https://webdav2.XXXX/Servidor2/
    +ProxyPassReverse /Servidor2/  https://webdav2.XXXX/Servidor2/
    
  2. En el servidor 2, debemos hacer que acepte /Servidor2/ como un directorio válido y que muestre ahí el contenido del directorio raíz. Hay varias formas de hacerlo. Tal vez la más simple sea utilizar la directiva Alias:

    diff --git a/conf/httpd.conf b/conf/httpd.conf
    --- a/conf/httpd.conf
    +++ b/conf/httpd.conf
    @@ -1029,7 +1029,11 @@
     <VirtualHost *:443>
     ServerName webdav2.XXXX:443
     ServerAdmin jcea@jcea.es
    +
    +# Si se cambia 'DocumentRoot', cambiar también el 'Alias'
    +# Ver https://blog.jcea.es/20150507-proxy_webdav.html
     DocumentRoot /home/Servidor2/
    +
     ErrorLog "logs/webdav2_XXXX_error.log"
     CustomLog "logs/webdav2_XXXX_access.log" combinedSSL
    
    @@ -1037,6 +1041,9 @@
     SSLCertificateFile      XXXX.crt
     SSLCertificateKeyFile   XXXX.key
    
    +# Ver https://blog.jcea.es/20150507-proxy_webdav.html
    +Alias /Servidor2/ /home/Servidor2/
    +
     <Location />
     Dav on
      Options FollowSymLinks Indexes
    

Con este cambio el contenido del directorio raíz se verá también, tal cual, en el directorio /Servidor2/. Ahora la respuesta WebDAV será consistente. Pedimos:

 PROPFIND /Servidor2/ HTTP/1.0
 host: 127.0.0.1:8080
 Depth: 1
 Authorization: Basic XXXXXXXX
 Content-Type: application/xml; charset="utf-8"
 Content-Length: 92

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

La respuesta es:

 HTTP/1.1 207 Multi-Status
 Date: Thu, 07 May 2015 19:01:47 GMT
 Server: Apache/2.4.12 (Unix) OpenSSL/1.0.2a mod_wsgi/4.4.11
 Python/3.4.3
 Strict-Transport-Security: max-age=15552000; includeSubDomains
 Vary: Accept-Encoding
 Content-Length: 442
 Content-Type: text/xml; charset="utf-8"
 Connection: close

 <?xml version="1.0" encoding="utf-8"?>
 <D:multistatus xmlns:D="DAV:" xmlns:ns0="DAV:">
 <D:response>
 <D:href>/Servidor2/</D:href>
 <D:propstat>
 <D:prop>
 </D:prop>
 <D:status>HTTP/1.1 200 OK</D:status>
 </D:propstat>
 </D:response>
 <D:response>
 <D:href>/Servidor2/fichero3.txt</D:href>
 <D:propstat>
 <D:prop>
 </D:prop>
 <D:status>HTTP/1.1 200 OK</D:status>
 </D:propstat>
 </D:response>
 </D:multistatus>

Con este cambio Kodi ya está feliz y resuelvo, de paso, pequeños problemillas sutiles que me perseguían de vez en cuando pero que no me habían molestado lo bastante como para investigarlos. Hoy soy un poquito más feliz :).