src/server/webdav/operation.c

Wed, 27 Nov 2024 23:00:07 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Wed, 27 Nov 2024 23:00:07 +0100
changeset 563
6ca97c99173e
parent 503
aeaf7db26fac
permissions
-rw-r--r--

add TODO to use a future ucx feature

/*
 * 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 <errno.h>

#include <cx/list.h>

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

#include "operation.h"

#define WEBDAV_PATH_MAX 8192


size_t webdav_num_backends(WebdavBackend *dav) {
    size_t count = 0;
    while(dav) {
        count++;
        dav = dav->next;
    }
    return count;
}

/****************************************************************************
 * 
 *                         PROPFIND OPERATION
 * 
 ****************************************************************************/

WebdavOperation* webdav_create_propfind_operation(
        Session *sn,
        Request *rq,
        WebdavBackend *dav,
        WebdavPList *reqprops,
        WebdavPropfindRequestList *requests,
        WebdavResponse *response)
{
    WebdavOperation *op = pool_malloc(sn->pool, sizeof(WebdavOperation));
    ZERO(op, sizeof(WebdavOperation));
    op->dav = dav;
    op->sn = sn;
    op->rq = rq;
    op->reqprops = reqprops;
    op->requests = requests;
    op->response = response;
    op->response_close = webdav_op_propfiond_close_resource;
    response->op = op;
    
    return op;
}

int webdav_op_propfind_begin(
        WebdavOperation *op,
        const char *href,
        VFS_DIR parent,
        struct stat *s)
{
    // create WebdavResource object for requested resource
    WebdavResource *resource = op->response->addresource(op->response, href);
    if(!resource) {
        return REQ_ABORTED;
    }
    
    // store data that we need when the resource will be closed
    op->stat = s;
    op->parent = parent;
    
    // get first propfind object
    WebdavPropfindRequest *propfind = op->requests->propfind;
    
    // execute propfind_do of the first backend for the first resource
    int ret = REQ_PROCEED;
    if(op->dav->propfind_do(propfind, op->response, NULL, resource, s)) {
        ret = REQ_ABORTED;
    } else {
        // propfind_do successful, close resource if needed
        // closing the resource will execute propfind_do of all remaining
        // backends
        if(!resource->isclosed) {
            ret = resource->close(resource);
        }
    }
    
    return ret;
}

typedef struct PathSearchElm {
    char   *href;
    char   *path;
    size_t hreflen;
    size_t pathlen;
    struct PathSearchElm *next;
} PathSearchElm;

/*
 * concats base + / + elm
 * if baseinit is true, only elm is copied
 */
static int path_buf_concat(
        pool_handle_t *pool,
        char **buf,
        size_t * restrict len,
        WSBool * restrict baseinit,
        const char *base,
        size_t baselen,
        const char *elm,
        size_t elmlen)
{
    if(base[baselen-1] == '/') {
        baselen--;
    }
    
    size_t newlen = baselen + elmlen + 1;
    if(newlen > WEBDAV_PATH_MAX) {
        log_ereport(LOG_FAILURE, "webdav: maximal path length exceeded");
        return 1;
    }
    
    // check if new path + terminator fits in the buffer
    if(newlen + 1 > *len) {
        *len = newlen + 128;
        char *newbuf = pool_realloc(pool, *buf, newlen);
        if(newbuf) {
            log_ereport(LOG_FAILURE, "webdav: path memory allocation failed");
            return 1;
        }
        *baseinit = FALSE;
        
        *buf = newbuf;
    }
    
    // if baseinit is true, the parent is already in the buffer
    // and we don't need to memcpy it again
    if(!(*baseinit)) {
        memcpy(*buf, base, baselen);
        (*buf)[baselen] = '/';
        *baseinit = TRUE;
    }
    // copy child and terminate string
    memcpy((*buf) + baselen + 1, elm, elmlen);
    (*buf)[newlen] = '\0';
    
    return 0;
}

static int propfind_child_cb(
        VFSContext *vfs,
        const char *href,
        const char *path,
        VFSDir *parent,
        struct stat *s,
        void *op)
{
    return webdav_op_propfind_begin(op, href, parent, s);
}

int webdav_op_propfind_children(
        WebdavOperation *op,
        VFSContext *vfs,
        const char *href,
        const char *path)
{
    WebdavPropfindRequest *request = op->requests->propfind;
    return webdav_op_iterate_children(
            vfs, request->depth, href, path, propfind_child_cb, op);
}

int webdav_op_propfiond_close_resource(
        WebdavOperation *op,
        WebdavResource *resource)
{
    // start with second backend and request, because
    // the first one was already called by webdav_op_propfind_begin
    WebdavBackend *dav = op->dav->next;
    WebdavPropfindRequestList *request = op->requests->next;
    
    // call propfind_do of all remaining backends
    int ret = REQ_PROCEED;
    while(dav && request) {
        if(dav->propfind_do(
                request->propfind,
                op->response,
                op->parent,
                resource,
                op->stat))
        {
            ret = REQ_ABORTED;
        }
        
        dav = dav->next;
        request = request->next;
    }
    return ret;
}

/*
 * Executes propfind_finish for each Backend
 */
int webdav_op_propfind_finish(WebdavOperation *op) {
    WebdavBackend *dav = op->dav;
    WebdavPropfindRequestList *requests = op->requests;
    
    int ret = REQ_PROCEED;
    while(dav && requests) {
        if(dav->propfind_finish(requests->propfind)) {
            ret = REQ_ABORTED;
        }
        
        dav = dav->next;
        requests = requests->next;
    }
    return ret;
}

/****************************************************************************
 * 
 *                         PROPPATCH OPERATION
 * 
 ****************************************************************************/

WebdavOperation* webdav_create_proppatch_operation(
        Session *sn,
        Request *rq,
        WebdavBackend *dav,
        WebdavProppatchRequest *proppatch,
        WebdavResponse *response)
{
    WebdavOperation *op = pool_malloc(sn->pool, sizeof(WebdavOperation));
    ZERO(op, sizeof(WebdavOperation));
    op->dav = dav;
    op->sn = sn;
    op->rq = rq;
    op->reqprops = NULL;
    op->response = response;
    op->proppatch = proppatch;
    op->response_close = webdav_op_proppatch_close_resource;
    response->op = op;
    
    return op;
}



int webdav_op_proppatch(
        WebdavOperation *op,
        const char *href,
        const char *path)
{
    WebdavProppatchRequest *orig_request = op->proppatch;
    CxAllocator *a = pool_allocator(op->sn->pool);
    
    // create WebdavResource object for the requested resource
    WebdavResource *resource = op->response->addresource(op->response, href);
    if(!resource) {
        return REQ_ABORTED;
    }
    
    // check ACL
    if(acl_evaluate(op->sn, op->rq, ACL_WRITE_XATTR)) {
        // ACL check failed, either unauthorized or forbidden
        // acl_evaluate() sets the http status code and may add
        // response headers for authentication
        if(op->rq->status_num == PROTOCOL_UNAUTHORIZED) {
            return REQ_ABORTED; // return here to send an authenticate response
        }
        
        // send multistatus response with status code 403 for each property
        log_ereport(LOG_VERBOSE, "webdav-proppatch: access forbidden");
        int ret = REQ_PROCEED;
        WebdavPList *plist = op->proppatch->set;
        for(int i=0;i<2;i++) {
            while(plist) {
                if(resource->addproperty(resource, plist->property, PROTOCOL_FORBIDDEN)) {
                    ret = REQ_ABORTED;
                    break; // OOM
                }
                plist = plist->next;
            }
            plist = op->proppatch->remove;
        }
        
        if(resource->close(resource)) {
            ret = REQ_ABORTED; // OOM
        }
        return ret;
    }
    
    VFSContext *ctx = NULL;
    VFSFile *file = NULL;
    
    // requests for each backends
    WebdavProppatchRequest **requests = pool_calloc(
            op->sn->pool,
            webdav_num_backends(op->dav),
            sizeof(WebdavProppatchRequest*));
    if(requests == NULL) {
        return REQ_ABORTED;
    }
    
    WebdavPList *prev_set = orig_request->set;
    WebdavPList *prev_remove = orig_request->remove;
    size_t set_count = orig_request->setcount;
    size_t remove_count = orig_request->removecount;
    
    int ret = REQ_PROCEED;
    
    // iterate backends and execute proppatch_do
    WebdavBackend *dav = op->dav;
    size_t numrequests = 0;
    while(dav) {
        WebdavPList *set = webdav_plist_clone_s(
                op->sn->pool,
                prev_set,
                &set_count);
        WebdavPList *remove = webdav_plist_clone_s(
                op->sn->pool,
                prev_remove,
                &remove_count);
        if((prev_set && !set) || (prev_remove && !remove)) {
            // clone failed, OOM
            ret = REQ_ABORTED;
            break;
        }
        
        // create new WebdavProppatchRequest object for this backend
        WebdavProppatchRequest *req = pool_malloc(
                op->sn->pool,
                sizeof(WebdavProppatchRequest));
        memcpy(req, orig_request, sizeof(WebdavProppatchRequest));
        req->dav = dav;
        req->set = orig_request->set;
        req->setcount = orig_request->setcount;
        req->remove = orig_request->remove;
        req->removecount = orig_request->removecount;
        req->userdata = NULL;
        
        // check if we need to open the file because the backend wants it
        if(!file && (dav->settings & WS_WEBDAV_PROPPATCH_USE_VFS)
                         == WS_WEBDAV_PROPPATCH_USE_VFS)
        {
            ctx = vfs_request_context(op->sn, op->rq);
            if(!ctx) {
                ret = REQ_ABORTED;
                break;
            }
            
            file = vfs_open(ctx, path, O_RDONLY);
            if(!file) {
                protocol_status(
                        op->sn,
                        op->rq,
                        util_errno2status(ctx->vfs_errno),
                        NULL);
                ret = REQ_ABORTED;
            }
        }
        
        // execute proppatch_do
        if(dav->proppatch_do(req, resource, file, &set, &remove)) {
            // return later, because we need do execute proppatch_finish
            // for all successfully called backends
            ret = REQ_ABORTED;
            break;
        }
        
        // proppatch_do should remove all handled props from set and remove
        // in the next iteration, the backend must use these reduced lists
        prev_set = set;
        prev_remove = remove;
        
        requests[numrequests++] = req;
        
        // continue with next backend
        dav = dav->next;
    }
    
    WSBool commit = FALSE;
    if(ret == REQ_PROCEED && resource->err == 0) {
        // no errors, no properties with errors -> save the changes
        commit = TRUE; 
    }
    
    // call proppatch_finish for each successfully called proppatch_do
    dav = op->dav;
    int i = 0;
    while(dav && i < numrequests) {
        if(dav->proppatch_finish(requests[i], resource, file, commit)) {
            ret = REQ_ABORTED;
        }
        i++;
        dav = dav->next;
    }
    
    if(file) {
        vfs_close(file);
    }
    
    if(resource->close(resource)) {
        ret = REQ_ABORTED;
    }
    
    return ret;
}

int webdav_op_proppatch_close_resource(
        WebdavOperation *op,
        WebdavResource *resource)
{
    return 0; // NOP
}


/****************************************************************************
 * 
 *                             VFS OPERATION
 * 
 ****************************************************************************/

WebdavVFSOperation* webdav_vfs_op(
        Session *sn,
        Request *rq,
        WebdavBackend *dav,
        WSBool precondition)
{
    WebdavVFSOperation *op = pool_malloc(sn->pool, sizeof(WebdavVFSOperation));
    if(!op) {
        return NULL;
    }
    ZERO(op, sizeof(WebdavVFSOperation));
    
    op->sn = sn;
    op->rq = rq;
    op->dav = dav;
    op->stat = NULL;
    op->stat_errno = 0;
    
    // create VFS context
    VFSContext *vfs = vfs_request_context(sn, rq);
    if(!vfs) {
        pool_free(sn->pool, op);
        return NULL;
    }
    op->vfs = vfs;
    
    char *path = pblock_findkeyval(pb_key_path, rq->vars);
    op->path = path;  
    
    return op;
}

WebdavVFSOperation webdav_vfs_sub_op(
        WebdavVFSOperation *op,
        char *path,
        struct stat *s)
{
    WebdavVFSOperation sub;
    sub.dav = op->dav;
    sub.path = path;
    sub.sn = op->sn;
    sub.vfs = op->vfs;
    sub.path = path;
    sub.stat = s;
    sub.stat_errno = 0;
    return sub;
}

int webdav_op_iterate_children(
        VFSContext *vfs,
        int depth,
        const char *href,
        const char *path,
        vfs_op_child_func func,
        void *userdata)
{
    pool_handle_t *pool = vfs->sn->pool;
    
    PathSearchElm *start_elm = pool_malloc(pool, sizeof(PathSearchElm));
    start_elm->href = pool_strdup(pool, href ? href : "");
    start_elm->path = pool_strdup(pool, path ? path : "");
    start_elm->hreflen = href ? strlen(href) : 0;
    start_elm->pathlen = path ? strlen(path) : 0;
    start_elm->next = NULL;
    
    PathSearchElm *stack = start_elm;
    PathSearchElm *stack_end = start_elm;
    
    // reusable buffer for full child path and href
    char *newpath = pool_malloc(pool, 256);
    size_t newpathlen = 256;
    
    char *newhref = pool_malloc(pool, 256);
    size_t newhreflen = 256;
    
    int err = 0;
    while(stack && !err) {
        PathSearchElm *cur_elm = stack;
        
        // when newpath is initialized with the parent path
        // set path_buf_init to TRUE
        WSBool href_buf_init = FALSE;
        WSBool path_buf_init = FALSE;
        
        VFS_DIR dir = vfs_opendir(vfs, cur_elm->path);
        if(!dir) {
            log_ereport(
                    LOG_FAILURE,
                    "webdav: propfind: cannot open directory %d",
                    vfs->vfs_errno);
            err = 1;
            break;
        }
        
        VFS_ENTRY f;
        while(vfs_readdir_stat(dir, &f)) {
            if(f.stat_errno != 0) {
                continue;
            }
            
            size_t child_len = strlen(f.name);
            
            // create new path and href for the child
            if(path_buf_concat(
                    pool,
                    &newhref,
                    &newhreflen,
                    &href_buf_init,
                    cur_elm->href,
                    cur_elm->hreflen,
                    f.name,
                    child_len))
            {
                err = 1;
                break;
            }
            if(path_buf_concat(
                    pool,
                    &newpath,
                    &newpathlen,
                    &path_buf_init,
                    cur_elm->path,
                    cur_elm->pathlen,
                    f.name,
                    child_len))
            {
                err = 1;
                break;
            }
            size_t childhreflen = cur_elm->hreflen + 1 + child_len;
            size_t childpathlen = cur_elm->pathlen + 1 + child_len;
            
            // execute callback func for this file
            if(func(vfs, newhref, newpath, dir, &f.stat, userdata)) {
                err = 1;
                break;
            }
            
            // depth of -1 means infinity
            if(depth == -1 && S_ISDIR(f.stat.st_mode)) {
                char *hrefcp = pool_malloc(pool, childhreflen + 1);
                memcpy(hrefcp, newhref, childhreflen + 1);
                hrefcp[childhreflen] = '\0';
                
                char *pathcp = pool_malloc(pool, childpathlen + 1);
                memcpy(pathcp, newpath, childpathlen + 1);
                pathcp[childpathlen] = '\0';
                
                PathSearchElm *new_elm = pool_malloc(pool,
                                            sizeof(PathSearchElm));
                if(!new_elm) {
                    err = 1;
                    break;
                }
                new_elm->href = hrefcp;
                new_elm->path = pathcp;
                new_elm->hreflen = childhreflen;
                new_elm->pathlen = childpathlen;
                new_elm->next = NULL;
                
                // add the new_elm to the stack
                // stack_end is always not NULL here, because the loop is
                // running as long as we have a stack and we remove
                // the first stack element at the end of the loop
                stack_end->next = new_elm;
                stack_end = new_elm;
            }
        }
        
        vfs_closedir(dir);
        
        stack = stack->next;
        
        pool_free(pool, cur_elm->path);
        pool_free(pool, cur_elm->href);
        pool_free(pool, cur_elm);
    }
    
    // in case of an error, we have to free all remaining stack elements
    for(PathSearchElm *elm=stack;elm;) {
        PathSearchElm *next_elm = elm->next;
        pool_free(pool, elm->path);
        pool_free(pool, elm->href);
        pool_free(pool, elm);
        elm = next_elm;
    }
    
    return err;
}


int webdav_vfs_stat(WebdavVFSOperation *op) {
    if(op->stat) {
        return 0;
    } else if(op->stat_errno != 0) {
        // previous stat failed
        return 1;
    }
    
    // stat file
    struct stat sbuf;
    int ret = vfs_stat(op->vfs, op->path, &sbuf);
    if(!ret) {
        // save result in op->stat and in s
        op->stat = pool_malloc(op->sn->pool, sizeof(struct stat));
        if(op->stat) {
            memcpy(op->stat, &sbuf, sizeof(struct stat));
        } else {
            ret = 1;
            op->stat_errno = ENOMEM;
        }
    } else {
        op->stat_errno = errno;
    }
    
    return ret;
}

int webdav_vfs_op_do(WebdavVFSOperation *op, WebdavVFSOpType type) {
    WSBool exec_vfs = TRUE;
    
    // requests for each backends
    WebdavVFSRequest **requests = pool_calloc(
            op->sn->pool,
            webdav_num_backends(op->dav),
            sizeof(WebdavVFSRequest*));
    if(requests == NULL) {
        return REQ_ABORTED;
    }
    
    int ret = REQ_PROCEED;
    
    // call opt_* func for each backend
    WebdavBackend *dav = op->dav;
    int called_backends = 0;
    while(dav) {    
        WebdavVFSRequest *request = NULL;
        
        // get vfs operation functions
        vfs_op_func        op_func        = NULL;
        vfs_op_finish_func op_finish_func = NULL;
        
        if(type == WEBDAV_VFS_MKDIR) {
            op_func        = dav->opt_mkcol;
            op_finish_func = dav->opt_mkcol_finish;
        } else if(type == WEBDAV_VFS_DELETE) {
            op_func        = dav->opt_delete;
            op_finish_func = dav->opt_delete_finish;
        }
        
        if(op_func || op_finish_func) {
            // we need a request object
            request = pool_malloc(op->sn->pool, sizeof(WebdavVFSRequest));
            if(!request) {
                exec_vfs = FALSE;
                ret = REQ_ABORTED;
                break;
            }
            request->sn = op->sn;
            request->rq = op->rq;
            request->path = op->path;
            request->userdata = NULL;
            
            requests[called_backends] = request;
        }
        
        // exec backend func for this operation
        // this will set 'done' to TRUE, if no further vfs call is required
        WSBool done = FALSE;
        called_backends++;
        if(op_func) {
            if(op_func(request, &done)) {
                exec_vfs = FALSE;
                ret = REQ_ABORTED;
                break;
            }
        }
        if(done) {
            exec_vfs = FALSE;
        }
        
        dav = dav->next;
    }
    
    // if needed, call vfs func for this operation
    if(exec_vfs) {
        int r = 0;
        if(type == WEBDAV_VFS_MKDIR) {
            r = vfs_mkdir(op->vfs, op->path);
            if(r) {
                // mkcol specific status codes
                switch(op->vfs->vfs_errno) {
                    case ENOENT: {
                        op->rq->status_num = 409;
                        break;
                    }
                    case EEXIST: {
                        op->rq->status_num = 405;
                        break;
                    }
                    case EACCES: {
                        op->rq->status_num = 403;
                        break;
                    }
                    default: op->rq->status_num = 500;
                }
            }
        } else if(type == WEBDAV_VFS_DELETE) {
            r = webdav_vfs_unlink(op);
        }
        
        if(r) {
            ret = REQ_ABORTED;
        }
    }
    
    WSBool success = ret == REQ_PROCEED ? TRUE : FALSE;
    
    // finish mkcol (cleanup) by calling opt_*_finish for each backend
    dav = op->dav;
    int i = 0;
    while(dav && i < called_backends) {
        // get vfs operation functions
        vfs_op_finish_func op_finish_func = NULL;
        
        if(type == WEBDAV_VFS_MKDIR) {
            op_finish_func = dav->opt_mkcol_finish;
        } else if(type == WEBDAV_VFS_DELETE) {
            op_finish_func = dav->opt_delete_finish;
        }
        
        if(op_finish_func) {
            if(op_finish_func(requests[i], success)) {
                ret = REQ_ABORTED; // don't exit loop
            }
        }
        
        dav = dav->next;
        i++;
    }
    
    return ret;
}

int webdav_vfs_unlink(WebdavVFSOperation *op) {
    // stat the file first, to check if the file is a directory
    if(webdav_vfs_stat(op)) {
        return 1; // error
    } else {
        if(!S_ISDIR(op->stat->st_mode)) {
            return vfs_unlink(op->vfs, op->path);
        } else {
            return vfs_rmdir(op->vfs, op->path);
        }
    }
}

mercurial