CL-WEBDAV - A WebDAV server written in Common Lisp


 

Abstract

CL-WEBDAV is a WebDAV server written in Common Lisp. It aims to be as flexible as possible, allowing you to completely customize the way resources are handled.

The code comes with a BSD-style license so you can basically do with it whatever you want.

Download shortcut: http://weitz.de/files/cl-webdav.tar.gz.

Screen shot

 

Contents

  1. Download and installation
  2. Example
  3. Support and mailing lists
  4. History and current status
  5. Function and variable reference
    1. Resources
      1. Mandatory methods
      2. Optional methods
    2. XML handling
    3. Properties
    4. Handlers and dispatchers
    5. File resources
    6. Authorized file resources
  6. Symbol index
  7. Acknowledgements

 

Download and installation

CL-WEBDAV together with this documentation can be downloaded from http://weitz.de/files/cl-webdav.tar.gz. The current version is 0.2.0. It depends on Closure XML, CL-FAD, and Hunchentoot (version 1.1.0 or higher), and can be compiled and loaded with ASDF.

A Mercurial repository of older versions is available at http://arcanes.fr.eu.org/~pierre/2007/02/weitz/ thanks to Pierre Thierry.
 

Example

The simplest way to start a WebDAV server with CL-WEBDAV is probably this one:
CL-USER 1 > (push (dav:create-dav-dispatcher 'dav:file-resource) tbnl:*dispatch-table*)
(#<Closure ((METHOD CL-WEBDAV:CREATE-DAV-DISPATCHER (SYMBOL)) . 1) 200B8992> HUNCHENTOOT:DEFAULT-DISPATCHER)

CL-USER 2 > (tbnl:start-server :port 4242)
CL-USER 2 > (hunchentoot:start (make-instance 'hunchentoot:acceptor :port 4242))
#<HUNCHENTOOT::SERVER 200BCBCF>
You should now be able to connect to http://localhost:4242/ using a DAV client like cadaver and you should see the files in the /tmp/ directory.
 

Support and mailing lists

For questions, bug reports, feature requests, improvements, or patches please use the tbnl-devel mailing list. If you want to be notified about future releases subscribe to the tbnl-announce mailing list. These mailing lists were made available thanks to the services of common-lisp.net. You can search the devel mailing list here (thanks to TiarnŠn ” CorrŠin).

If you want to send patches, please read this first.
 

History and current status

I was asked to give a tutorial at the International Lisp Conference 2007 in Cambridge, and - as it was about Hunchentoot - spent two nights in March to come up with a nice example of what one could do with Hunchentoot. The result was basically a working, albeit shaky version of what CL-WEBDAV is now. (I chose a WebDAV server because I had already written one in, cough, Perl a couple of years before, so I more or less knew what I had to do.)

I ended up not using CL-WEBDAV for the tutorial, though, as I came to the conclusion that there was too much stuff in it that would distract from the actual subject. But as it was more or less complete, I invested some more time for testing, polishing, cleanup, and documentation, and then released it.

The initial release has been tested mildly with LispWorks on Windows and with SBCL on Linux against cadaver, WebDrive, and the native Windows XP client. I currently (April 2007) don't use CL-WEBDAV myself, so I can't say much about its usefulness, but here's a list of potential pitfalls that come to mind:


 

Function and variable reference

Resources

CL-WEBDAV uses a CLOS-based approach to deal with the resources on the server. Every handler instantiates (based on the request sent by the client) one or more objects of a class which is a subclass of RESOURCE and then calls generic functions on these object to actually manipulate the resources represented by these objects. So, in order to implement your own WebDAV server, you are supposed to subclass RESOURCE and implement a couple of methods. CL-WEBDAV comes with two example classes which demonstrate how this can be done.


[Standard class]
resource


This is the base class you'll have to subclass if you want to create your own custom WebDAV server. Each object of this class represents one resource on the server and most of the time these objects are created by the server using only the :SCRIPT-NAME initarg. If you need more initialization to happen, write an :AFTER method for INITIALIZE-INSTANCE.

See the file file-resources.lisp for an example of a subclass of RESOURCE.


[Generic accessor]
resource-script-name resource => script-name
(setf (resource-script-name resource) script-name)


The base class RESOURCE has only one slot which can be manipulated with this accessor. If a resource is created by the server, then it will fill this slot with the script name that was used to access the resource. In this case you should only read the slot's value.

If you create your own RESOURCE objects (for example in RESOURCE-CHILDREN), you should set the value of this slot to a string that could be used as a script name to retrieve the resource. There are several places where CL-WEBDAV is calling this function internally, so it is important that you use a meaningful value.


[Special variable]
*resource-class*


Whenever a DAV handler is executed, this variable should be bound to the resource class which is to be used. If you're using CREATE-DAV-DISPATCHER, this will already be taken care of for you, so you can ignore this variable.

Mandatory methods

This subsection lists all generic functions which must be specialized for your own resource classes. If you think that this is too much work, you should subclass FILE-RESOURCE where this work is already done for you.


[Generic function]
resource-exists resource => generalized-boolean


This function must return a true value if the resource resource exists on the server and NIL otherwise. You must specialize this generic function for your own classes.


[Generic function]
resource-children resource => children


This function must return a list of all children of resource (which themselves are RESOURCE objects). You must specialize this generic function for your own classes.


[Generic function]
resource-parent resource => parent


This function must return a RESOURCE object which is the parent resource of resource or NIL if there is no parent. You must specialize this generic function for your own classes.


[Generic function]
resource-collection-p resource => generalized-boolean


This function must return a true value iff the resource resource is a collection. You must specialize this generic function for your own classes.


[Generic function]
resource-write-date resource => universal-time


This function must return a universal time denoting the time the resource resource was last modified. You must specialize this generic function for your own classes.


[Generic function]
resource-length resource => length


This function must return an integer denoting the length of the resource resource in octets. You must specialize this generic function for your own classes.


[Generic function]
resource-display-name resource => display-name


This function must return a string which, according to the WebDAV RFC, "provides a name for the resource that is suitable for presentation to a user." You must specialize this generic function for your own classes.


[Generic function]
send-content resource stream => whatever


This function is called for GET requests and must send the complete contents of the (non-collection) resource resource to the (flexi) stream stream. The return value is irrelevant.


[Generic function]
get-content resource stream length => whatever


This function is called for PUT requests and must read length octets of data from the (flexi) stream stream and store them in a place appropriate for the resource resource. The return value is irrelevant.


[Generic function]
remove-resource resource => whatever


This function must completely remove the resource resource. It doesn't have to deal with dead properties, and it can assume that resource doesn't have children in case it's a collection. The return value is irrelevant.


[Generic function]
move-resource source destination => whatever


This function must "move" the (contents of the) resource source in such a way that it can in the future be accessed as destination. It doesn't have to deal with dead properties, and it can assume that source doesn't have children in case it's a collection. The return value is irrelevant.


[Generic function]
copy-resource source destination => whatever


This function must "copy" the (contents of the) resource source in such a way that the copy can in the future be accessed as destination. It doesn't have to deal with dead properties, and it can assume that source doesn't have children in case it's a collection. The return value is irrelevant.


[Generic function]
create-collection resource => whatever


This function must create a collection resource that in the future can be accessed as resource. The return value is irrelevant.


[Generic function]
accept-request-p resource-class request => generalized-boolean


This must be a function which accepts a Hunchentoot request object request and returns a generalized boolean denoting whether request is a resource the DAV server wants to handle. It will be called by the dispatcher created with CREATE-DAV-DISPATCHER. Usually, you'll want to look at the script name of the request or something like that - see the class FILE-RESOURCE for an example.

Note that you specialize this function on the resource class (or its name) and not on the resource like the other functions in this subsection.

Optional methods

This subsection lists all generic functions which can be specialized for your own resource classes. You don't necessarily have to because they have default methods, but in some cases it might make sense.


[Generic function]
resource-creation-date resource => universal-time


This function must return a universal time denoting the time the resource resource was created. There's a default method which returns RESOURCE-WRITE-DATE, but most likely you'll want to specialize this for you own classes and return a more meaningful value.


[Generic function]
resource-content-type resource => type-string


This function must return a string denoting the MIME type of the resource resource. It will only be called if resource is not a collection. There's a default method which always returns "application/octet-stream", but most likely you'll want to specialize this for your own classes.


[Generic function]
resource-content-language resource => language


This function should return either NIL or a language tag as defined in section 14.13 of RFC 2068. If the value returned by this function is not NIL, it will also be used as the Content-Language header returned for GET requests. There's a default method which always returns NIL.


[Generic function]
resource-source resource => xmls-node


This function should return either NIL or a DAV "source" XML node (structured as an XMLS node) that, according to the WebDAV RFC, "identifies the resource that contains the unprocessed source of the link's source." There's a default method which always returns NIL.


[Generic function]
resource-etag resource => etag


This function should return an ETag for the resource resource or NIL. If the value returned by this function is not NIL, it will also be used as the ETag header returned for GET requests. There's a default method which synthesizes a value based on the script name and the write date of the resource, and in most cases you probably don't need to specialize this function.


[Generic function]
resource-type resource => xmls-node


This function should return either NIL or a DAV "resourcetype" XML node (structured as an XMLS node) that, according to the WebDAV RFC, "specifies the nature of the resource." There's a default method which returns something fitting for collections and NIL otherwise, and in most cases you probably don't need to specialize this function.


[Generic function]
resource-uri-prefix resource => prefix-string


This function must return a string which is the part of a resource's HTTPS or HTTPS URI that comprises the scheme, the host, and the port and ends with a slash - something like "http://localhost:4242/" or "https://www.lisp.org/".

The default method synthesizes this from the information Hunchentoot provides and usually you only have to write your own method if you're sitting behind a proxy.

XML handling

When dealing with properties or for some methods like RESOURCE-TYPE you'll need to handle XML data. You can probably get away without it, but in case you need them, this section collects the relevant CL-WEBDAV functions to manipulate XML.

We're representing XML as XMLS nodes which are very similar to CXML's XMLS nodes but try to get namespaces right because they don't purport to be compatible with XMLS.


[Function]
xmls-node-p thing => generalized-boolean


Checks whether thing is an XMLS node.


[Function]
local-name thing => local-name


Returns the local name of the XMLS node or attribute thing.


[Function]
namespace-uri thing => namespace-uri


Returns the namespace URI (which can be NIL) of the XMLS node or attribute thing.


[Accessor]
node-attributes xmls-node => attributes
(setf (node-attributes xmls-node) attributes)


Returns or sets the list of attributes of the XMLS node xmls-node.


[Accessor]
node-children xmls-node => children
(setf (node-children xmls-node) children)


Returns or sets the list of children of the XMLS node xmls-node.


[Function]
dav-node local-name &rest children => xmls-node


Returns an XMLS node with the local name local-name, the namespace URI "DAV:", and the children children (a list of XMLS nodes and/or strings).


[Function]
parse-dav octets &optional root-name => xmls-node


Accepts an array octets of octets representing a DAV XML node and converts it into the corresponding XMLS node. According to the WebDAV RFC, non-DAV elements are skipped unless they appear in positions (like in a "prop" element) where arbitrary elements are allowed. If root-name is given, it should be the local name (a string) of a DAV node. In this case, the XML is validated. This function is expected to be called from within a Hunchentoot request and calls ABORT-REQUEST-HANDLER with a return code of +HTTP-BAD-REQUEST+ if a parsing error occurs or if the XML is invalid.

This is kind of the inverse operation to SERIALIZE-XMLS-NODE.


[Function]
serialize-xmls-node xmls-node => octet-vector


Serializes xmls-node to a vector of octets which is returned.

This is kind of the inverse operation to PARSE-DAV and very similar to CXML-XMLS:MAP-NODE.

Properties

CL-WEBDAV has default methods to store, retrieve, and manipulate dead properties of resources as requested by the client. However, these default methods simply keep all properties in one global hash table without persisting them. For a production server, you definitely want to specialize the generic functions listed in this section for your resource class.


[Generic function]
get-dead-properties resource => property-list


This function must return all dead properties of the resource resource as a list of XML elements structured as XMLS nodes. There's a default method but you should definitely specialize this for production servers.


[Generic function]
remove-dead-property resource property => whatever


This function must remove the currently stored dead property designated by property (an XMLS node) of the resource resource. There's a default method but you should definitely specialize this for production servers. The return value is irrelevant.


[Generic function]
set-dead-property resource property => whatever


This function must replace the currently stored dead property designated by property (an XMLS node) of the resource resource with property, i.e. property doubles as the property itself and as the property designator. There's a default method but you should definitely specialize this for production servers. The return value is irrelevant.


[Generic function]
remove-dead-properties resource => whatever


This function must remove all dead properties of the resource resource. There's a default method but you should definitely specialize this for production servers. The return value is irrelevant.


[Generic function]
move-dead-properties source destination => whatever


This function must move all dead properties of the resource source to the resource destination. There's a default method but you should definitely specialize this for production servers. The return value is irrelevant.


[Generic function]
copy-dead-properties source destination => whatever


This function must copy all dead properties of the resource source to the resource destination. There's a default method but you should definitely specialize this for production servers. The return value is irrelevant.

Handlers and dispatchers

To actually use your WebDAV server, you'll need to add a corresponding dispatcher to Hunchentoot's dispatch table. This is what CREATE-DAV-DISPATCHER is for. The other functions and variables in this section you'll rarely need, if ever.


[Generic function]
create-dav-dispatcher resource-class &optional ms-workaround-p => dispatcher


Creates and returns a dispatcher for the class resource-class which must be a subclass of RESOURCE. If ms-workaround-p is true (which is the default), OPTIONS requests are always handled irrespective of the results of ACCEPT-REQUEST-P - this is needed to work around problems with some Microsoft DAV clients.


[Function]
options-handler => nil


The handler for OPTIONS requests. Output is basically determined by *ALLOWED-METHODS* and *DAV-COMPLIANCE-CLASSES*.


[Function]
options-dispatcher request => handler


A dispatcher which'll dispatch to OPTIONS-HANDLER in case of an OPTIONS request and decline otherwise. This is only useful if you want to cater to Microsoft DAV clients which always unconditionally send OPTIONS requests to the "/" root resource. Sigh...

If you use CREATE-DAV-DISPATCHER with a true value for ms-workaround-p, you don't need this dispatcher.


[Special variable]
*allowed-methods*


The list of methods (as keywords) returned by the Allow header in case of OPTIONS requests (and also utilized by the handler for MKCOL). The initial value is the list
(:options :get :head :delete :propfind :proppatch :put :copy :move :mkcol)
Can be adapted to allow for more methods, but for a WebDAV server at least the methods above should be listed.


[Special variable]
*dav-compliance-classes*


A sorted list of DAV compliance classes reported in the DAV header when answering OPTIONS requests. It doesn't make much sense to have more then class 1 in here as long as there's no lock support, i.e. the initial value is the list (1).

File resources

The class FILE-RESOURCE maps URIs in a straightforward way to (a subtree of) your local file system. This is probably the most common way to serve DAV resources, and if you want to do it like this, you can subclass FILE-RESOURCE and adapt it without the need to implement loads of methods. Of course, you can go wild and store resources in a database, or generate them dynamically, or whatever, but then this class is not for you and you have to invent your own subclass of RESOURCE.

Again, this is not meant to be ready for a production system. You should at least make sure that properties are persisted.


[Standard class]
file-resource


A subclass of RESOURCE representing resources which are mapped to a subtree of the local file system.


[Generic function]
file-resource-base-path-namestring resource-class => namestring


This generic function is called for subclasses of FILE-RESOURCE to determine the base pathname that's currently being used, i.e. the part of the filesystem where the files served by the DAV server are stored. The function must return the namestring of the truename of an absolute pathname denoting a directory, specifically it must return a string starting and ending with slashes. (Note: This should work on Windows as well.) You can specialize this function (either on the class or on the name of the class) if you want.

The default method returns the current value of *FILE-RESOURCE-BASE-PATH-NAMESTRING*.


[Special variable]
*file-resource-base-path-namestring*


The value of this variable is the return value of the default method for FILE-RESOURCE-BASE-PATH-NAMESTRING. It should be the namestring of the truename of an absolute pathname denoting a directory, specifically it must return a string starting and ending with slashes. (Note: This should work on Windows as well.)

The initial value is the result of calling

(namestring (truename (ensure-directories-exist "/tmp/")))
meaning that this directory will be created on your machine at load time if it doesn't exist (which is possible if you're on Windows and pretty unlikely on Linux or Unix).


[Generic function]
file-resource-base-uri resource-class => uri


This generic function is called for subclasses of FILE-RESOURCE to determine the base URI that's currently being used, i.e. the prefix the script name of a resource's URI must have in order to be valid. (In other words: this URI represents the top-level collection of the DAV server.) The function must return a string which starts with a slash if it's not empty and does not end with a slash and is not URL-encoded. You can specialize this function (either on the class or on the name of the class) if you want.

The default method returns the current value of *FILE-RESOURCE-BASE-URI*.


[Special variable]
*file-resource-base-uri*


The value of this variable is the return value of the default method for FILE-RESOURCE-BASE-URI. It should be a string which starts with a slash if it's not empty and does not end with a slash and is not URL-encoded.

The initial value is "" (the empty string).

Authorized file resources

Authorized file resources are specialized file resources, included in CL-WEBDAV solely for demonstration purposes. DAV clients are required to authenticate themselves using basic HTTP authentication (user names must only contain ASCII characters, decimal digits, underlines, and hyphens, and are case-insensitive, the password must be the same as the user name), and users are assigned their own private subtree of the file system.


[Standard class]
authorized-file-resource


A subclass of FILE-RESOURCE representing file resources which are associated with a certain user.

 

Symbol index

Here are all exported symbols of the CL-WEBDAV package in alphabetical order linked to their corresponding entries:
 

Acknowledgements

This documentation was prepared with DOCUMENTATION-TEMPLATE.

$Header: /usr/local/cvsrep/cl-webdav/doc/index.html,v 1.20 2009/02/17 20:17:30 edi Exp $

BACK TO MY HOMEPAGE