src/server/webdav/webdav.c

Sun, 26 Jan 2020 10:13:11 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Sun, 26 Jan 2020 10:13:11 +0100
branch
webdav
changeset 242
c337a7ac82a8
parent 241
4adad7faf452
child 243
1a29b1d8d9d8
permissions
-rw-r--r--

implement webdav_proppatch

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2019 Olaf Wintermann. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   1. Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *
 *   2. Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <ucx/buffer.h>
#include <ucx/list.h>

#include "webdav.h"

#include "search.h"
#include "versioning.h"
#include "multistatus.h"
#include "requestparser.h"
#include "operation.h"

#include "../util/pblock.h"
#include "../util/util.h"
#include "../daemon/session.h"
#include "../daemon/http.h"
#include "../daemon/protocol.h"

static UcxMap *method_handler_map;

static WebdavBackend default_backend;

static WSNamespace dav_namespace;

static WebdavProperty dav_resourcetype_empty;
static WebdavProperty dav_resourcetype_collection;
static WSXmlData dav_resourcetype_collection_value;

#define WEBDAV_RESOURCE_TYPE_COLLECTION "<D:collection/>"

static void init_default_backend(void) {
    memset(&default_backend, 0, sizeof(WebdavBackend));
    default_backend.propfind_init = default_propfind_init;
    default_backend.propfind_do = default_propfind_do;
    default_backend.propfind_finish = default_propfind_finish;
    default_backend.proppatch_do = NULL;
    default_backend.proppatch_finish = NULL;
    default_backend.settings = WS_WEBDAV_PROPFIND_USE_VFS;
}

int webdav_init(pblock *pb, Session *sn, Request *rq) {
    init_default_backend();
    
    method_handler_map = ucx_map_new(64);
    
    ucx_map_cstr_put(method_handler_map, "OPTIONS", webdav_options);
    ucx_map_cstr_put(method_handler_map, "PROPFIND", webdav_propfind);
    ucx_map_cstr_put(method_handler_map, "PROPPATCH", webdav_proppatch);
    ucx_map_cstr_put(method_handler_map, "MKCOL", webdav_mkcol);
    ucx_map_cstr_put(method_handler_map, "POST", webdav_post);
    ucx_map_cstr_put(method_handler_map, "DELETE", webdav_delete);
    ucx_map_cstr_put(method_handler_map, "PUT", webdav_put);
    ucx_map_cstr_put(method_handler_map, "COPY", webdav_copy);
    ucx_map_cstr_put(method_handler_map, "MOVE", webdav_move);
    ucx_map_cstr_put(method_handler_map, "LOCK", webdav_lock);
    ucx_map_cstr_put(method_handler_map, "UNLOCK", webdav_unlock);
    ucx_map_cstr_put(method_handler_map, "REPORT", webdav_report);
    ucx_map_cstr_put(method_handler_map, "ACL", webdav_acl);
    
    ucx_map_cstr_put(method_handler_map, "SEARCH", webdav_search);
    
    ucx_map_cstr_put(method_handler_map, "VERSION-CONTROL", webdav_version_control);
    ucx_map_cstr_put(method_handler_map, "CHECKOUT", webdav_checkout);
    ucx_map_cstr_put(method_handler_map, "CHECKIN", webdav_checkin);
    ucx_map_cstr_put(method_handler_map, "UNCHECKOUT", webdav_uncheckout);
    ucx_map_cstr_put(method_handler_map, "MKWORKSPACE", webdav_mkworkspace);
    ucx_map_cstr_put(method_handler_map, "UPDATE", webdav_update);
    ucx_map_cstr_put(method_handler_map, "LABEL", webdav_label);
    ucx_map_cstr_put(method_handler_map, "MERGE", webdav_merge);
    
    dav_namespace.href = (xmlChar*)"DAV:";
    dav_namespace.prefix = (xmlChar*)"D";
    
    dav_resourcetype_empty.namespace = &dav_namespace;
    dav_resourcetype_empty.name = "resourcetype";
    
    dav_resourcetype_collection.namespace = &dav_namespace;
    dav_resourcetype_collection.name = "resourcetype";
    dav_resourcetype_collection.value.data = &dav_resourcetype_collection_value;
    dav_resourcetype_collection.vtype = WS_VALUE_XML_DATA;
    dav_resourcetype_collection_value.data = WEBDAV_RESOURCE_TYPE_COLLECTION;
    dav_resourcetype_collection_value.length = sizeof(WEBDAV_RESOURCE_TYPE_COLLECTION)-1;
    
    return REQ_PROCEED;
}


int webdav_service(pblock *pb, Session *sn, Request *rq) {
    if(!method_handler_map) {
        log_ereport(LOG_FAILURE, "WebDAV module not initialized");
        protocol_status(sn, rq, 500, NULL);
        return REQ_ABORTED;
    }
    char *method = pblock_findkeyval(pb_key_method, rq->reqpb);
    
    FuncPtr saf = (FuncPtr)ucx_map_cstr_get(method_handler_map, method);
    if(!saf) {
        return REQ_NOACTION;
    }
    
    return saf(pb, sn, rq);
}

UcxBuffer* rqbody2buffer(Session *sn, Request *rq) {
    if(!sn->inbuf) {
        //request body required, set http response code
        protocol_status(sn, rq, 400, NULL);
        return NULL;
    }
    
    UcxBuffer *buf = ucx_buffer_new(
            NULL,
            sn->inbuf->maxsize,
            UCX_BUFFER_AUTOEXTEND);
    if(!buf) {
        protocol_status(sn, rq, 500, NULL);
        return NULL;
    }
    
    char in[2048];
    int r;
    while((r = netbuf_getbytes(sn->inbuf, in, 2048)) > 0) {
        if(ucx_buffer_write(in, 1, r, buf) != r) {
            protocol_status(sn, rq, 500, NULL);
            ucx_buffer_free(buf);
            return NULL;
        }
    }
    
    return buf;
}

int webdav_options(pblock *pb, Session *sn, Request *rq) {
    return REQ_ABORTED;
}

int webdav_propfind(pblock *pb, Session *sn, Request *rq) {
    char *expect = pblock_findkeyval(pb_key_expect, rq->headers);
    if(expect) {
        if(!strcasecmp(expect, "100-continue")) {
            if(http_send_continue(sn)) {
                return REQ_ABORTED;
            }
        }
    }
    
    UcxBuffer *reqbody = rqbody2buffer(sn, rq);
    if(!reqbody) {
        return REQ_ABORTED;
    }
    
    UcxAllocator *a = session_get_allocator(sn);
    
    int error = 0;
    WebdavPropfindRequest *propfind = propfind_parse(
            sn,
            rq,
            reqbody->space,
            reqbody->size,
            &error);
    ucx_buffer_free(reqbody);
    if(!propfind) {
        switch(error) {
            // TODO: handle all errors
            default: return REQ_ABORTED;
        }
    }
    
    WebdavBackend *dav =  rq->davCollection ?
                              rq->davCollection : &default_backend;
    
    
    // requested uri and path
    char *path = pblock_findkeyval(pb_key_path, rq->vars);
    char *uri = pblock_findkeyval(pb_key_uri, rq->reqpb);
    
    // VFS settings are only taken from the first backend
    uint32_t settings = dav->settings;
    
    // list of individual WebdavPropfindRequest objects for each Backend
    UcxList *requestObjects = NULL;
    
    // Initialize all Webdav Backends
    if(webdav_propfind_init(dav, propfind, path, &requestObjects)) {
        return REQ_ABORTED;
    }
    
    // The multistatus response object contains responses for all
    // requested resources. At the end the Multistatus object will be
    // serialized to xml
    Multistatus *ms = multistatus_response(sn, rq);
    if(!ms) {
        return REQ_ABORTED;
    }
    // WebdavResponse is the public interface used by Backends
    // for adding resources to the response
    WebdavResponse *response = (WebdavResponse*)ms;
    
    WebdavOperation *op = webdav_create_propfind_operation(
            sn,
            rq,
            dav,
            propfind->properties,
            requestObjects,
            response);
    
    // some Backends can list all children by themselves, but some
    // require the VFS for this
    WSBool usevfs = (settings & WS_WEBDAV_PROPFIND_USE_VFS)
                        == WS_WEBDAV_PROPFIND_USE_VFS;
    struct stat s;
    struct stat *statptr = NULL;
    
    VFSContext *vfs = NULL;
    if(usevfs) {
        vfs = vfs_request_context(sn, rq);
        if(!vfs) {
            return REQ_ABORTED;
        }
        
        if(vfs_stat(vfs, path, &s)) {
            protocol_status(sn, rq, util_errno2status(vfs->vfs_errno), NULL);
            return REQ_ABORTED;
        }
        statptr = &s;
        if(!S_ISDIR(s.st_mode)) {
            // the file is not a directory, therefore we don't need the VFS
            usevfs = FALSE;
        }
    }
    if(propfind->depth == 0) {
        usevfs = FALSE;
    }
    
    int ret = REQ_PROCEED;
    
    // create WebdavResource object for requested resource
    if(!webdav_op_propfind_begin(op, uri, NULL, statptr)) {
        // propfind for the requested resource was successful
        
        // usevfsdir is TRUE if
        //   the webdav backend has not disabled vfs usage
        //   the file is a directory
        //   depth is not 0
        // in this case we need to execute propfind_do for all children
        if(usevfs) {
            if(webdav_op_propfind_children(op, vfs, uri, path)) {
                ret = REQ_ABORTED;
            }
        }
    }
    
    // if propfind was successful, send the result to the client
    if(ret == REQ_PROCEED && multistatus_send(ms, sn->csd)) {
        ret = REQ_ABORTED;
        // TODO: log error
    } else {
        // TODO: error response
    }
    
    // finish the propfind request
    // this function should cleanup all resources, therefore we execute it
    // even if a previous function failed
    if(webdav_op_propfind_finish(op)) {
        // TODO: log error
        ret = REQ_ABORTED;
    }
    
    // cleanup
    xmlFreeDoc(propfind->doc);
    
    return ret;
}

/*
 * Initializes Backend Chain
 * 
 * Calls propfind_init of each Backend and generates a list of custom
 * WebdavPropfindRequest objects for each backend
 */
int webdav_propfind_init(
        WebdavBackend *dav,
        WebdavPropfindRequest *propfind,
        const char *path,
        UcxList **out_req)
{   
    pool_handle_t *pool = propfind->sn->pool;
    UcxAllocator *a = session_get_allocator(propfind->sn);
    
    // list of individual WebdavPropfindRequest objects for each Backend
    UcxList *requestObjects = NULL;
    
    // new properties after init, start with clone of original plist
    WebdavPList *newProp = webdav_plist_clone(pool, propfind->properties);
    size_t newPropCount = propfind->propcount;
    
    // Call propfind_init for each Backend
    // propfind_init can return a new property list, which
    // will be passed to the next backend

    WebdavBackend *davList = dav;
    while(davList) {
        // create WebdavPropfindRequest copy
        WebdavPropfindRequest *pReq = pool_malloc(
                pool,
                sizeof(WebdavPropfindRequest));
        memcpy(pReq, propfind, sizeof(WebdavPropfindRequest));
        // use new plist after previous init (or orig. plist in the first run)
        pReq->properties = newProp;
        pReq->propcount = newPropCount;
        
        // add new WebdavPropfindRequest object to list for later use
        requestObjects = ucx_list_append_a(a, requestObjects, pReq);
        if(!requestObjects) {
            return REQ_ABORTED; // OOM
        }
        
        // create plist copy as out-plist for init
        newProp = webdav_plist_clone(pool, newProp);
        
        // run init: this can generate a new properties list (newProp)
        //           which will be passed to the next backend
        if(davList->propfind_init(pReq, path, &newProp)) {
            return REQ_ABORTED;
        }
        
        newPropCount = webdav_plist_size(newProp);
        
        davList = davList->next;
    }
    
    *out_req = requestObjects;
    return REQ_PROCEED;
}


int webdav_proppatch(pblock *pb, Session *sn, Request *rq) {
    char *expect = pblock_findkeyval(pb_key_expect, rq->headers);
    if(expect) {
        if(!strcasecmp(expect, "100-continue")) {
            if(http_send_continue(sn)) {
                return REQ_ABORTED;
            }
        }
    }
    
    UcxBuffer *reqbody = rqbody2buffer(sn, rq);
    if(!reqbody) {
        return REQ_ABORTED;
    }
    
    int error = 0;
    WebdavProppatchRequest *proppatch = proppatch_parse(
            sn,
            rq,
            reqbody->space,
            reqbody->size,
            &error);
    ucx_buffer_free(reqbody);
    if(!proppatch) {
        switch(error) {
            // TODO: handle all errors
            default: return REQ_ABORTED;
        }
    }
    
    WebdavBackend *dav =  rq->davCollection ?
                              rq->davCollection : &default_backend;
    
    // requested uri and path
    char *path = pblock_findkeyval(pb_key_path, rq->vars);
    char *uri = pblock_findkeyval(pb_key_uri, rq->reqpb);
    
    // The multistatus response object contains responses for all
    // requested resources. At the end the Multistatus object will be
    // serialized to xml
    Multistatus *ms = multistatus_response(sn, rq);
    if(!ms) {
        return REQ_ABORTED;
    }
    ms->proppatch = TRUE;
    
    // WebdavResponse is the public interface used by Backends
    // for adding resources to the response
    WebdavResponse *response = (WebdavResponse*)ms;
    
    WebdavOperation *op = webdav_create_proppatch_operation(
            sn,
            rq,
            dav,
            proppatch,
            response);
    
    int ret = REQ_PROCEED;
    
    // Execute proppatch
    if(webdav_op_proppatch(op, uri, path)) {
        ret = REQ_ABORTED;
    }
    
    // send response
    if(ret == REQ_PROCEED && multistatus_send(ms, sn->csd)) {
        ret = REQ_ABORTED;
        // TODO: log error
    } else {
        // TODO: error response
    }
    
    // cleanup
    xmlFreeDoc(proppatch->doc);
    
    return ret;
}

int webdav_mkcol(pblock *pb, Session *sn, Request *rq) {
    return REQ_ABORTED;
}

int webdav_post(pblock *pb, Session *sn, Request *rq) {
    return REQ_ABORTED;
}

int webdav_delete(pblock *pb, Session *sn, Request *rq) {
    return REQ_ABORTED;
}

int webdav_put(pblock *pb, Session *sn, Request *rq) {
    return REQ_ABORTED;
}

int webdav_copy(pblock *pb, Session *sn, Request *rq) {
    return REQ_ABORTED;
}

int webdav_move(pblock *pb, Session *sn, Request *rq) {
    return REQ_ABORTED;
}

int webdav_lock(pblock *pb, Session *sn, Request *rq) {
    return REQ_ABORTED;
}

int webdav_unlock(pblock *pb, Session *sn, Request *rq) {
    return REQ_ABORTED;
}

int webdav_report(pblock *pb, Session *sn, Request *rq) {
    return REQ_ABORTED;
}

int webdav_acl(pblock *pb, Session *sn, Request *rq) {
    return REQ_ABORTED;
}


/* ------------------------ default webdav backend  ------------------------ */

int default_propfind_init(
        WebdavPropfindRequest *rq,
        const char* path,
        WebdavPList **outplist)
{
    DefaultWebdavData *data = pool_malloc(
            rq->sn->pool,
            sizeof(DefaultWebdavData));
    if(!data) {
        return 1;
    }
    rq->userdata = data;
    
    data->vfsproperties = webdav_vfs_properties(outplist, TRUE, 0);
    
    return 0;
}

int default_propfind_do(
        WebdavPropfindRequest *request,
        WebdavResponse *response,
        VFS_DIR parent,
        WebdavResource *resource,
        struct stat *s)
{
    DefaultWebdavData *data = request->userdata;
    
    // add all requested vfs properties like getcontentlength ...
    if(webdav_add_vfs_properties(
            resource,
            request->sn->pool,
            data->vfsproperties,
            s))
    {
        return 1;
    }
    
    return 0;
}

int default_propfind_finish(WebdavPropfindRequest *rq) {
    return 0;
}


/* ------------------------------ public API ------------------------------ */

int webdav_getdepth(Request *rq) {
    char *depth_str = pblock_findkeyval(pb_key_depth, rq->headers);
    int depth = 0;
    if(depth_str) {
        size_t dlen = strlen(depth_str);
        if(!memcmp(depth_str, "infinity", dlen)) {
            depth = -1;
        } else if(dlen == 1 && depth_str[0] == '1') {
            depth = 1;
        }
    }
    return depth;
}

int webdav_plist_add(
        pool_handle_t *pool,
        WebdavPList **begin,
        WebdavPList **end,
        WebdavProperty *prop)
{
    WebdavPList *elm = pool_malloc(pool, sizeof(WebdavPList));
    if(!elm) {
        return 1;
    }
    elm->prev = *end;
    elm->next = NULL;
    elm->property = prop;
    
    if(!*begin) {
        *begin = elm;
        *end = elm;
        return 0;
    }
    
    (*end)->next = elm;
    *end = elm;
    
    return 0;
}

WebdavPList* webdav_plist_clone(pool_handle_t *pool, WebdavPList *list) {
    return webdav_plist_clone_s(pool, list, NULL);
}

WebdavPList* webdav_plist_clone_s(
        pool_handle_t *pool,
        WebdavPList *list,
        size_t *newlen)
{
    WebdavPList *new_list = NULL;     // start of the new list
    WebdavPList *new_list_end = NULL; // end of the new list
    
    size_t len = 0;
    
    WebdavPList *elm = list;
    while(elm) {
        // copy list item
        WebdavPList *new_elm = pool_malloc(pool, sizeof(WebdavPList));
        if(!new_elm) {
            if(newlen) *newlen = 0;
            return NULL;
        }
        new_elm->property = elm->property; // new list contains original ptr
        new_elm->prev = new_list_end;
        new_elm->next = NULL;
        
        if(new_list_end) {
            new_list_end->next = new_elm;
        } else {
            new_list = new_elm;
        }
        new_list_end = new_elm;
        
        len++;
        elm = elm->next;
    }
    
    if(newlen) *newlen = len;
    return new_list;
}

size_t webdav_plist_size(WebdavPList *list) {
    size_t count = 0;
    WebdavPList *elm = list;
    while(elm) {
        count++;
        elm = elm->next;
    }
    return count;
}

WebdavPListIterator webdav_plist_iterator(WebdavPList **list) {
    WebdavPListIterator i;
    i.list = list;
    i.cur = NULL;
    i.next = *list;
    i.index = 0;
    return i;
}

int webdav_plist_iterator_next(WebdavPListIterator *i, WebdavPList **cur) {
    if(i->cur) {
        i->index++;
    }
    
    i->cur = i->next;
    i->next = i->cur ? i->cur->next : NULL;
    *cur = i->cur;
    
    return i->cur != NULL;
}

void webdav_plist_iterator_remove_current(WebdavPListIterator *i) {
    WebdavPList *cur = i->cur;
    if(cur->prev) {
        cur->prev->next = cur->next;
        if(cur->next) {
            cur->next->prev = cur->prev;
        }
    } else {
        *i->list = cur->next;
        if(cur->next) {
            cur->next->prev = NULL;
        }
    }
}

int webdav_nslist_add(
        pool_handle_t *pool,
        WebdavNSList **begin,
        WebdavNSList **end,
        WSNamespace *ns)
{
    // same as webdav_plist_add but with different type
    WebdavNSList *elm = pool_malloc(pool, sizeof(WebdavNSList));
    if(!elm) {
        return 1;
    }
    elm->prev = *end;
    elm->next = NULL;
    elm->namespace = ns;
    
    if(!*begin) {
        *begin = elm;
        *end = elm;
        return 0;
    }
    
    (*end)->next = elm;
    *end = elm;
    
    return 0;
}


WSNamespace* webdav_dav_namespace(void) {
    return &dav_namespace;
}

WebdavProperty* webdav_dav_property(
        pool_handle_t *pool,
        const char *name)
{
    WebdavProperty *property = pool_malloc(pool, sizeof(WebdavProperty));
    if(!property) {
        return NULL;
    }
    memset(property, 0, sizeof(WebdavProperty));
    
    property->namespace = &dav_namespace;
    property->name = name;
    return property;
}

int webdav_property_set_value(
        WebdavProperty *p,
        pool_handle_t *pool,
        char *value)
{
    WSXmlNode *node = pool_malloc(pool, sizeof(WSXmlNode));
    if(!node) {
        return 1;
    }
    ZERO(node, sizeof(WSXmlNode));
    
    node->content = (xmlChar*)value;
    node->type = XML_TEXT_NODE;
    
    p->value.node = node;
    p->vtype = WS_VALUE_XML_NODE;
    return 0;
}

WebdavVFSProperties webdav_vfs_properties(
        WebdavPList **plistInOut,
        WSBool removefromlist,
        uint32_t flags)
{
    WebdavVFSProperties ret;
    ZERO(&ret, sizeof(WebdavVFSProperties));
    
    WSBool etag = 1;
    WSBool creationdate = 1;
    
    WebdavPListIterator i = webdav_plist_iterator(plistInOut);
    WebdavPList *cur;
    while(webdav_plist_iterator_next(&i, &cur)) {
        WSNamespace *ns = cur->property->namespace;
        if(ns && !strcmp((const char*)ns->href, "DAV:")) {
            const char *name = cur->property->name;
            WSBool remove_prop = TRUE;
            if(!strcmp(name, "getlastmodified")) {
                ret.getlastmodified = 1;
            } else if(!strcmp(name, "getcontentlength")) {
                ret.getcontentlength = 1;
            } else if(!strcmp(name, "resourcetype")) {
                ret.getresourcetype = 1;
            } else if(etag && !strcmp(name, "getetag")) {
                ret.getetag = 1;
            } else if(creationdate && !strcmp(name, "creationdate")) {
                ret.creationdate = 1;
            } else {
                remove_prop = FALSE;
            }
            
            if(remove_prop) {
                webdav_plist_iterator_remove_current(&i);
            }
        }
    }
    
    return ret;
}

static inline int w_addprop(
        WebdavResource *res,
        pool_handle_t *pool,
        const char *name,
        char *value)
{
    WebdavProperty *p = webdav_dav_property(pool, name);
    if(!p) {
        return 1;
    }
    if(webdav_property_set_value(p, pool, value)) {
        return 1;
    }
    return res->addproperty(res, p, 200);
}

int webdav_add_vfs_properties(
        WebdavResource *res,
        pool_handle_t *pool,
        WebdavVFSProperties properties,
        struct stat *s)
{
    if(properties.getresourcetype) {
        if(S_ISDIR(s->st_mode)) {
            res->addproperty(res, &dav_resourcetype_collection, 200);
        } else {
            res->addproperty(res, &dav_resourcetype_empty, 200);
        }
    }
    if(properties.getcontentlength) {
        char *buf = pool_malloc(pool, 64);
        if(!buf) {
            return 1;
        }
        uint64_t contentlength = s->st_size;
        snprintf(buf, 64, "%" PRIu64 "\0", contentlength);
        if(w_addprop(res, pool, "getcontentlength", buf)) {
            return 1;
        }
    }
    if(properties.getlastmodified) {
        char *buf = pool_malloc(pool, HTTP_DATE_LEN+1);
        if(!buf) {
            return 1;
        }
        buf[HTTP_DATE_LEN] = 0;
        
        struct tm mtms;
        struct tm *mtm = system_gmtime(&s->st_mtim.tv_sec, &mtms);
        
        if(mtm) {
            strftime(buf, HTTP_DATE_LEN, HTTP_DATE_FMT, mtm);
            if(w_addprop(res, pool, "getlastmodified", buf)) {
                return 1;
            }
        } else {
            return 1;
        }
    }
    if(properties.creationdate) {
        // TODO
    }
    if(properties.getetag) {
        char *buf = pool_malloc(pool, 96);
        if(!buf) {
            return 1;
        }
        snprintf(buf,
            96,
            "\"%x-%x\"\0",
            (int)s->st_size,
            (int)s->st_mtim.tv_sec);
        if(w_addprop(res, pool, "getetag", buf)) {
            return 1;
        }
    }
    
    return 0;
}

mercurial