DAV4Rack - Web Authoring for Rack

DAV4Rack is a framework for providing WebDAV via Rack allowing content authoring over HTTP. It is based off the original RackDAV framework adding some useful new features:

  • Better resource support for building fully virtualized resource structures

  • Generic locking as well as Resource level specific locking

  • Interceptor middleware to provide virtual mapping to resources

  • Mapped resource paths

  • Authentication support

  • Resource callbacks

  • Remote file proxying (including sendfile support for remote files)

  • Nokogiri based document parsing and building

If you find issues, please create a new issue on github. If you have fixes, please fork the repo and send me a pull request with your modifications. If you are just here to use the library, enjoy!

Install

Via RubyGems

gem install dav4rack

Documentation

Quickstart

If you just want to share a folder over WebDAV, you can just start a simple server with:

dav4rack

This will start a Unicorn, Mongrel or WEBrick server on port 3000, which you can connect to without authentication. Unicorn and Mongrel will be much more responsive than WEBrick, so if you are having slowness issues, install one of them and restart the dav4rack process. The simple file resource allows very basic authentication which is used for an example. To enable it:

dav4rack --username=user --password=pass

Rack Handler

Using DAV4Rack within a rack application is pretty simple. A very slim rackup script would look something like this:

require 'rubygems'
require 'dav4rack'

use Rack::CommonLogger
run DAV4Rack::Handler.new(:root => '/path/to/public/fileshare')

This will use the included FileResource and set the share path. However, DAV4Rack has some nifty little extras that can be enabled in the rackup script. First, an example of how to use a custom resource:

run DAV4Rack::Handler.new(:resource_class => CustomResource, :custom => 'options', :passed => 'to resource')

Next, lets venture into mapping a path for our WebDAV access. In this example, we will use default FileResource like in the first example, but instead of the WebDAV content being available at the root directory, we will map it to a specific directory: /webdav/share/

require 'rubygems'
require 'dav4rack'

use Rack::CommonLogger

app = Rack::Builder.new{
  map '/webdav/share/' do
    run DAV4Rack::Handler.new(:root => '/path/to/public/fileshare', :root_uri_path => '/webdav/share/')
  end
}.to_app
run app

Aside from the Builder#map block, notice the new option passed to the Handler’s initialization, :root_uri_path. When DAV4Rack receives a request, it will automatically convert the request to the proper path and pass it to the resource.

Another tool available when building the rackup script is the Interceptor. The Interceptor’s job is to simply intecept WebDAV requests received up the path hierarchy where no resources are currently mapped. For example, lets continue with the last example but this time include the interceptor:

require 'rubygems'
require 'dav4rack'

use Rack::CommonLogger
app = Rack::Builder.new{
  map '/webdav/share/' do
    run DAV4Rack::Handler.new(:root => '/path/to/public/fileshare', :root_uri_path => '/webdav/share/')
  end
  map '/webdav/share2/' do
    run DAV4Rack::Handler.new(:resource_class => CustomResource, :root_uri_path => '/webdav/share2/')
  end
  map '/' do
    use DAV4Rack::Interceptor, :mappings => {
      '/webdav/share/' => {:resource_class => FileResource, :custom => 'option'},
      '/webdav/share2/' => {:resource_class => CustomResource}
    }
    use Rails::Rack::Static
    run ActionController::Dispatcher.new
  end
}.to_app
run app

In this example we have two WebDAV resources restricted by path. This means those resources will handle requests to /webdav/share/* and /webdav/share2/* but nothing above that. To allow webdav to respond, we provide the Interceptor. The Interceptor does not provide any authentication support. It simply creates a virtual file system view to the provided mapped paths. Once the actual resources have been reached, authentication will be enforced based on the requirements defined by the individual resource. Also note in the root map you can see we are running a Rails application. This is how you can easily enable DAV4Rack with your Rails application.

Enabling Logging

DAV4Rack provides some simple logging in a Rails style format (simply for consistency) so the output should look some what familiar.

DAV4Rack::Handler.new(:resource_class => CustomResource, :log_to => '/my/log/file')

You can even specify the level of logging:

DAV4Rack::Handler.new(:resource_class => CustomResource, :log_to => ['/my/log/file', Logger::DEBUG])

Custom Resources

Creating your own resource is easy. Simply inherit the DAV4Rack::Resource class, and start redefining all the methods you want to customize. The DAV4Rack::Resource class only has implementations for methods that can be provided extremely generically. This means that most things will require at least some sort of implementation. However, because the Resource is defined so generically, and the Controller simply passes the request on to the Resource, it is easy to create fully virtualized resources.

Helpers

There are some helpers worth mentioning that make things a little easier. DAV4Rack::Resource#accept_redirect? method is available to Resources. If true, the currently connected client will accept and properly use a 302 redirect for a GET request. Most clients do not properly support this, which can be a real pain when working with virtualized files that may be located some where else, like S3. To deal with those clients that don’t support redirects, a helper has been provided so resources don’t have to deal with proxying themselves. The DAV4Rack::RemoteFile is a modified Rack::File that can do some interesting things. First, lets look at its most basic use:

class MyResource < DAV4Rack::Resource
  def setup
    @item = method_to_fill_this_properly
  end

  def get
    if(accept_redirect?)
      response.redirect item[:url]
    else
      response.body = DAV4Rack::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type)
      OK
    end
  end
end

This is a simple proxy. When Rack receives the RemoteFile, it will pull a chunk of data from object, which in turn pulls it from the socket, and sends it to the user over and over again until the EOF is reached. This much the same method that Rack::File uses but instead we are pulling from a socket rather than an actual file. Now, instead of proxying these files from a remote server every time, lets cache them:

response.body = DAV4Rack::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type, :cache_directory => '/tmp')

Providing the :cache_directory will let RemoteFile cache the items locally, and then search for them on subsequent requests before heading out to the network. The cached file name is based off the SHA1 hash of the file path, size and last modified time. It is important to note that for services like S3, the path will often change, making this cache pretty worthless. To combat this, we can provide a reference to use instead:

response.body = DAV4Rack::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type, :cache_directory => '/tmp', :cache_ref => item[:static_url])

These methods will work just fine, but it would be really nice to just let someone else deal with the proxying and let the process get back to dealing with actual requests. RemoteFile will happily do that as long as the frontend server is setup correctly. Using the sendfile approach will tell the RemoteFile to simply pass the headers on and let the server deal with doing the actual proxying. First, lets look at an implementation using all the features, and then degrade that down to the bare minimum. These examples are NGINX specific, but are based off the Rack::Sendfile implementation and as such should be applicable to other servers. First, a simplified NGINX server block:

server {
  listen 80;
  location / {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_set_header X-Sendfile-Type X-Accel-Redirect;
    proxy_set_header X-Accel-Remote-Mapping webdav_redirect
    proxy_pass http://my_app_server;
  }

  location ~* /webdav_redirect {
    internal;
    resolver 127.0.0.1;
    set $r_host $upstream_http_redirect_host;
    set $r_url $upstream_http_redirect_url;
    proxy_set_header Authorization '';
    proxy_set_header Host $r_host;
    proxy_max_temp_file_size 0;
    proxy_pass $r_url;
  }
}

With this in place, the parameters for the RemoteFile change slightly:

response.body = DAV4Rack::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type, :sendfile => true)

The RemoteFile will automatically take care of building out the correct path and sending the proper headers. If the X-Accel-Remote-Mapping header is not available, you can simply pass the value:

response.body = DAV4Rack::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type, :sendfile => true, :sendfile_prefix => 'webdav_redirect')

And if you don’t have the X-Sendfile-Type header set, you can fix that by changing the value of :sendfile:

response.body = DAV4Rack::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type, :sendfile => 'X-Accel-Redirect', :sendfile_prefix => 'webdav_redirect')

And if you have none of the above because your server hasn’t been configured for sendfile support, you’re out of luck until it’s configured.

Authentication

Authentication is performed on a per Resource basis. The Controller object will check the Resource for a Resource#authenticate method. If it exists, any authentication information will be passed to the method. Depending on the result, the Controller will either continue on with the request, or send a 401 Unauthorized response.

As a nicety, Resource#authentication_realm will be checked for existence and the returning string will be used as the Realm. Resource#authentication_error_msg will also be checked for existence and the returning string will be passed in the response upon authentication failure.

Authentication can also be implemented using callbacks, as discussed below.

Callbacks

Resources can make use of callbacks to easily apply permissions, authentication or any other action that needs to be performed before or after any or all actions. Callbacks are applied to all publicly available methods. This is important for methods used internally within the resource. Methods not meant to be called by the Controller, or anyone else, should be scoped protected or private to reduce the interaction with callbacks.

Callbacks can be called before or after a method call. For example:

class MyResource < DAV4Rack::Resource
  before do |resource, method_name|
    resource.send(:my_authentication_method)
  end

  after do |resource, method_name|
    puts "#{Time.now} -> Completed: #{resource}##{method_name}"
  end

  private

  def my_authentication_method
    true
  end
end

In this example MyResource#my_authentication_method will be called before any public method is called. After any method has been called a status line will be printed to STDOUT. Running callbacks before/after every method call is a bit much in most cases, so callbacks can be applied to specific methods:

class MyResource < DAV4Rack::Resource
  before_get do |resource|
    puts "#{Time.now} -> Received GET request from resource: #{resource}"
  end
end

In this example, a simple status line will be printed to STDOUT before the MyResource#get method is called. The current resource object is always provided to callbacks. The method name is only provided to the generic before/after callbacks.

Something very handy for dealing with the mess of files OS X leaves on the system:

class MyResource < DAV4Rack::Resource
  after_unlock do |resource|
    resource.delete if resource.name[0,1] == '.'
  end
end

Because OS X implements locking correctly, we can wait until it releases the lock on the file, and remove it if it’s a hidden file.

Callbacks are called in the order they are defined, so you can easily build callbacks off each other. Like this example:

class MyResource < DAV4Rack::Resource
  before do |resource, method_name|
    resource.DAV_authenticate unless resource.user.is_a?(User)
    raise Unauthorized unless resource.user.is_a?(User)
  end
  before do |resource, method_name|
    resource.user.allowed?(method_name)
  end
end

In this example, the second block checking User#allowed? can count on Resource#user being defined because the blocks are called in order, and if the Resource#user is not a User type, an exception is raised.

Avoiding callbacks

Something special to notice in the last example is the DAV_ prefix on authenticate. Providing the DAV_ prefix will prevent any callbacks being applied to the given method. This allows us to provide a public method that the callback can access on the resource without getting stuck in a loop.

Software using DAV4Rack!

  • meishi - Lightweight CardDAV implementation in Rails

  • dav4rack_ext - CardDAV extension. (CalDAV planned)

Issues/Bugs/Questions

Known Issues

  • OS X Finder PUT fails when using NGINX (this is due to NGINX’s lack of chunked transfer encoding)

  • Windows WebDAV mini-redirector fails (this client is very broken. patches welcome.)

  • Lots of unimplemented parts of the webdav spec (patches always welcome)

Unknown Issues

Please use the issues at github: github.com/chrisroberts/dav4rack/issues

Contributors

A big thanks to everyone contributing to help make this project better.

License

Just like RackDAV before it, this software is distributed under the MIT license.