src/server/webdav/xattrbackend.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 490
d218607f5a7e
permissions
-rw-r--r--

add TODO to use a future ucx feature

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2023 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 "xattrbackend.h"

#include "webdav.h"

#include "../util/util.h"
#include "../util/libxattr.h"
#include "../util/pblock.h"

#include <inttypes.h>

#include <cx/hash_map.h>
#include <cx/buffer.h>
#include <cx/utils.h>
#include <cx/printf.h>
#include <libxml/tree.h>



static WebdavBackend webdav_xattr_backend = {
    webdav_xattr_propfind_init,
    webdav_xattr_propfind_do,
    webdav_xattr_propfind_finish,
    webdav_xattr_proppatch_do,
    webdav_xattr_proppatch_finish,
    NULL, // opt_mkcol
    NULL, // opt_mkcol_finish
    NULL, // opt_delete
    NULL, // opt_delete_finish
    0,    // settings
    NULL, // instance
    NULL  // next
};

int webdav_init_xattr_backend(void) {
    
    
    if(webdav_register_backend("xattr", webdav_xattr_init, webdav_xattr_create)) {
        return 1;
    }
    return 0;
}

void* webdav_xattr_init(ServerConfiguration *cfg, pool_handle_t *pool, WSConfigNode *config) {
    WebdavXAttrRepository *repo = pool_malloc(pool, sizeof(WebdavXAttrRepository));
    if(!repo) {
        return NULL;
    }
    
    // TODO: config
    repo->xattr_name = "webdav_properties";
    
    return repo;
}

WebdavBackend* webdav_xattr_create(Session *sn, Request *rq, pblock *pb, void *initData) {
    WebdavBackend *dav = pool_malloc(sn->pool, sizeof(WebdavBackend));
    if(!dav) {
        return NULL;
    }
    
    WebdavXAttrBackend *instance = pool_malloc(sn->pool, sizeof(WebdavXAttrBackend));
    if(!instance) {
        return NULL;
    }
    instance->repo = initData;
    
    *dav = webdav_xattr_backend;
    dav->instance = instance;
    
    return dav; 
}



/* -------------------- webdav backend imlementation ----------------------*/

int webdav_xattr_propfind_init(
        WebdavPropfindRequest *rq,
        const char *path,
        const char *href,
        WebdavPList **outplist)
{
    // make sure the sys vfs is used, because currently only
    // native sysfs xattr is supported
    if(rq->rq->vfs) {
        log_ereport(LOG_FAILURE, "webdav-propfind: xattr backend unsupported with non-native VFS");
        return 1;
    }
    
    XAttrPropfind *xprop = pool_malloc(rq->sn->pool, sizeof(XAttrPropfind));
    if(!xprop) {
        return 1;
    }
    rq->userdata = xprop;
    
    xprop->base_href = href;
    xprop->base_path = path;
    
    return 0;
}

int webdav_xattr_propfind_do(
        WebdavPropfindRequest *request,
        WebdavResponse *response,
        VFS_DIR parent,
        WebdavResource *resource,
        struct stat *s)
{
    Session *sn = request->sn;
    Request *rq = request->rq;
    CxAllocator *a = pool_allocator(sn->pool);
    
    WebdavXAttrBackend *xdav = request->dav->instance;
    WebdavXAttrRepository *repo = xdav->repo;
    XAttrPropfind *xprop = request->userdata;
    
    // get resource path
    const char *path;
    char *path_dp = NULL;
    if(!parent) {
        // use base path
        path = xprop->base_path;
    } else {
        size_t base_href_len = strlen(xprop->base_href);
        size_t base_path_len = strlen(xprop->base_path);
        char *res_path = resource->href + base_href_len;
        size_t res_path_len = strlen(res_path);
        
        path_dp = pool_malloc(sn->pool, base_path_len + res_path_len + 2);
        memcpy(path_dp, xprop->base_path, base_path_len);
        int s = 0;
        if(path_dp[base_path_len-1] != '/' && res_path[0] != '/') {
            path_dp[base_path_len] = '/';
            s = 1;
        }
        memcpy(path_dp + base_path_len + s, res_path, res_path_len);
        path_dp[base_path_len + s + res_path_len] = 0;
        
        path = path_dp;
    }
    
    ssize_t xattr_data_len = 0;
    char *xattr_data = xattr_get_alloc(
            sn->pool,
            (libxattr_malloc_func)pool_malloc,
            (libxattr_free_func)pool_free,
            path,
            repo->xattr_name,
            &xattr_data_len);
    
    if(!xattr_data) {
        // no properties for this resource
        return 0;
    }
    
    CxMap *pmap = webdav_xattr_parse_data(a, xattr_data, xattr_data_len, path);
    pool_free(sn->pool, xattr_data);
    if(!pmap) {
        return 1;
    }
    
    int err;
    if(request->allprop || request->propname) {
        err = webdav_xattr_propfind_allprop(request, resource, a, pmap);
    } else {
        err = webdav_xattr_propfind_get_requested_properties(request, resource, a, pmap);
    } 
    
    return err;
}

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

// helper functions for propfind_do

int webdav_xattr_propfind_get_requested_properties(
        WebdavPropfindRequest *request,
        WebdavResource *resource,
        CxAllocator *a,
        CxMap *pmap)
{
    WebdavPListIterator i = webdav_plist_iterator(&request->properties);
    WebdavPList *cur;
    while(webdav_plist_iterator_next(&i, &cur)) {
        WebdavProperty *reqprop = cur->property;
        
        CxHashKey key = webdav_property_key_a(
                a,
                (const char*)reqprop->namespace->href,
                (const char*)reqprop->name);
        if(!key.data) {
            return 1;
        }
        
        WebdavProperty *prop = cxMapGet(pmap, key);
        if(prop) {
            if(resource->addproperty(resource, prop, 200)) {
                return 1;
            }
        }
    }
    
    return 0;
}

int webdav_xattr_propfind_allprop(
        WebdavPropfindRequest *request,
        WebdavResource *resource,
        CxAllocator *a,
        CxMap *pmap)
{
    CxIterator i = cxMapIteratorValues(pmap);
    cx_foreach(WebdavProperty*, prop, i) {
        if(request->propname) {
            prop->vtype = WS_VALUE_NO_TYPE;
        }
        if(resource->addproperty(resource, prop, 200)) {
            return 1;
        }
    }
    return 0;
}



int webdav_xattr_proppatch_do(
            WebdavProppatchRequest *request,
            WebdavResource *response,
            VFSFile *file,
            WebdavPList **setInOut,
            WebdavPList **removeInOut)
{
    Session *sn = request->sn;
    Request *rq = request->rq;
    CxAllocator *a = pool_allocator(sn->pool);
    
    WebdavXAttrBackend *xdav = request->dav->instance;
    WebdavXAttrRepository *repo = xdav->repo;

    char *path = pblock_findkeyval(pb_key_path, rq->vars);
    
    // How xattr proppatch works:
    // 1. get existing property data from the file's xattr
    // 2. do set/remove operations to the existing data
    // 3. write new property data to the xattr (webdav_xattr_proppatch_finish)
    
    // TODO: proppatch file lock (in-process userspace lock)
    
    XAttrProppatch *xprop = pool_malloc(sn->pool, sizeof(XAttrProppatch));
    if(!xprop) {
        return 1;
    }
    request->userdata = xprop;
    
    // get existing property data
    ssize_t xattr_data_len = 0;
    char *xattr_data = xattr_get_alloc(
            sn->pool,
            (libxattr_malloc_func)pool_malloc,
            (libxattr_free_func)pool_free,
            path,
            repo->xattr_name,
            &xattr_data_len);
    
    CxMap *pmap;
    if(xattr_data) {
        pmap = webdav_xattr_parse_data(a, xattr_data, xattr_data_len, path);
        pool_free(sn->pool, xattr_data);
    } else {
        // empty map
        pmap = cxHashMapCreate(a, CX_STORE_POINTERS, request->setcount + 8);
    }
    if(!pmap) {
        return 1;
    }
    xprop->properties = pmap;
    
    // remove properties
    WebdavPListIterator i = webdav_plist_iterator(removeInOut);
    WebdavPList *cur;
    while(webdav_plist_iterator_next(&i, &cur)) {
        WebdavProperty *prop = cur->property;
        if(!prop->namespace || !prop->namespace->prefix) {
            // not sure if this check is required
            log_ereport(LOG_WARN, "webdav_xattr_proppatch_do: property %s has missing namespace infos", prop->name);
            continue;
        }
        
        CxHashKey key = webdav_property_key_a(
                a,
                (const char*)prop->namespace->href,
                (const char*)prop->name);
        if(!key.data) {
            cxMapDestroy(pmap);
            return 1;
        }
        void *rmprop = cxMapRemoveAndGet(pmap, key);
        cxFree(a, (void*)key.data);
        
        // TODO: free rmprop
        
        if(rmprop) {
            webdav_plist_iterator_remove_current(&i);
        }
    }
    
    i = webdav_plist_iterator(setInOut);
    while(webdav_plist_iterator_next(&i, &cur)) {
        WebdavProperty *prop = cur->property;
        if(!prop->namespace || !prop->namespace->prefix) {
            // not sure if this check is required
            log_ereport(LOG_WARN, "webdav_xattr_proppatch_do: property %s has missing namespace infos", prop->name);
            continue;
        }
        
        if(webdav_xattr_put_prop(pmap, prop)) {
            cxMapDestroy(pmap);
            return 1;
        }
        
        webdav_plist_iterator_remove_current(&i);
    }
    
    return 0;
}

int webdav_xattr_proppatch_finish(
            WebdavProppatchRequest *request,
            WebdavResource *response,
            VFSFile *file,
            WSBool commit)
{
    Session *sn = request->sn;
    Request *rq = request->rq;
    CxAllocator *a = pool_allocator(sn->pool);
    
    WebdavXAttrBackend *xdav = request->dav->instance;
    WebdavXAttrRepository *repo = xdav->repo;
    
    XAttrProppatch *xprop = request->userdata;
    
    char *path = pblock_findkeyval(pb_key_path, rq->vars);
    
    int ret = 0;
    if(commit) {
        cxmutstr new_data = webdav_xattr_serialze_map(a, xprop->properties);
        if(new_data.ptr) {
            if(xattr_set(path, repo->xattr_name, new_data.ptr, new_data.length)) {
                ret = 1;
            }
        } else {
            ret = 1;
        }
    }
    
    return 0;
}


/* ----------------------- properties xattr data ----------------------- */

static int get_next_line(cxstring data, size_t *pos, cxstring *line) {
    size_t p = *pos;
    cxstring str = cx_strsubs(data, p);
    size_t i;
    int skip = 0;
    for(i=0;i<str.length;i++) {
        if(str.ptr[i] == '\n') {
            skip = 1;
            break;
        }
    }
    p += i;
    *pos = p + skip;
    *line = cx_strsubsl(str, 0, i);
    return i > 0 ? TRUE : FALSE;
}

static int webdav_xattr_parse_elm(cxstring line, cxstring *name, cxstring *prefix, cxstring *xmlns, cxstring *lang) {
    cxstring s_xmlns = CX_STR("xmlns:");
    
    // check if line starts with 'xmlns:'
    if(!cx_strprefix(line, s_xmlns)) {
        return 1;
    }
    line.ptr += 6;
    line.length -= 6;
    
    
    // format: <prefix>="<href>"
    // find '='
    size_t i;
    size_t token_end = 0;
    for(i=0;i<line.length;i++) {
        if(line.ptr[i] == '=') {
            token_end = i;
            break;
        }
    }
    if(token_end == 0) {
        return 1;
    }
    *prefix = cx_strn(line.ptr, token_end);
            
    // make sure the line length is big enough
    if(token_end + 4 > line.length) {
        return 1;
    }
    if(line.ptr[token_end + 1] != '\"') {
        return 1;
    }
    line.ptr += token_end + 2;
    line.length -= token_end + 2;
    
    // get <href>
    int escape = 0;
    token_end = 0;
    for(i=0;i<line.length;i++) {
        if(line.ptr[i] == '\\') {
            escape = 1;
            continue;
        } else if(!escape && line.ptr[i] == '\"') {
            token_end = i;
            break;
        }
        escape = 0;
    }
    if(token_end == 0) {
        return 1;
    }
    *xmlns = cx_strn(line.ptr, token_end);
    
    // check length
    if(token_end + 2 > line.length) {
        return 1;
    }
    line.ptr += token_end + 2;
    line.length -= token_end + 2;
    *name = cx_strtrim(line);
    
    if(name->length == 0) {
        return 1;
    }
    if(prefix->length == 0) {
        return 1;
    }
    if(xmlns->length == 0) {
        return 1;
    }
    
    return 0;
}

static int webdav_xattr_parse_ns(cxstring line, cxstring *prefix, cxstring *href) {
    size_t i = 0;
    for(i=1;i<line.length;i++) {
        if(line.ptr[i] == ':') {
            break;
        }
    }
    if(i == 0 || i+1 >= line.length) {
        // error: ':' not found or ':' is the last character
        return 1;
    }
    
    cxstring pre;
    pre.ptr = line.ptr;
    pre.length = i;
    *prefix = pre;
    
    *href = cx_strsubs(line, i+1);
    
    return 0;
}

int webdav_xattr_put_prop(CxMap *pmap, WebdavProperty *prop) {
    CxHashKey key = webdav_property_key_a(
            pmap->allocator,
            (const char*)prop->namespace->href,
            (const char*)prop->name);
    if(!key.data) {
        return 1;
    }
    int ret = cxMapPut(pmap, key, prop);
    cxFree(pmap->allocator, (void*)key.data);
    return ret;
}

CxMap* webdav_xattr_parse_data(CxAllocator *a, const char *data, size_t len, const char *path) {
    CxMap *pmap = cxHashMapCreate(a, CX_STORE_POINTERS, 32);
    if(!pmap) {
        return NULL;
    }
    cxstring dat = cx_strn(data, len);
    
    cxstring s_elm  = CX_STR("prop ");
    cxstring s_ns   = CX_STR("ns ");
    cxstring s_data = CX_STR("data ");
    
    WebdavProperty *prop = NULL;
    WebdavNSList *ns_begin = NULL;
    WebdavNSList *ns_end = NULL;
    
    int error = 0;
    
    size_t elmno = 0;
    cxstring line;
    size_t pos = 0;
    while(get_next_line(dat, &pos, &line)) {
        if(cx_strprefix(line, s_elm)) {
            if(prop) {
                if(webdav_xattr_put_prop(pmap, prop)) {
                    error = 1;
                    break;
                }
            }
            
            line = cx_strsubs(line, 5);
            
            // get prop name, namespace and lang
            cxstring name;
            cxstring prefix;
            cxstring xmlns;
            cxstring lang;
            if(webdav_xattr_parse_elm(line, &name, &prefix, &xmlns, &lang)) {
                log_ereport(
                        LOG_FAILURE,
                        "webdav xattr backend: file %s: invalid xattr format[%d]: cannot parse elm line",
                        path,
                        elmno);
                error = 1;
                break;
            }
            
            // create property
            prop = cxMalloc(a, sizeof(WebdavProperty));
            if(!prop) {
                error = 1;
                break;
            }
            ZERO(prop, sizeof(WebdavProperty));
            ns_begin = NULL;
            ns_end = NULL;
            
            WSNamespace *ns = cxMalloc(a, sizeof(WSNamespace));
            if(!ns) {
                error = 1;
                break;
            }
            ZERO(ns, sizeof(WSNamespace));
            
            char *name_str = cx_strdup_a(a, name).ptr;
            char *prefix_str = cx_strdup_a(a, prefix).ptr;
            char *xmlns_str = cx_strdup_a(a, xmlns).ptr;
            if(!(name_str && prefix_str && xmlns_str)) {
                error = 1;
                break;
            }
            
            ns->prefix = (const xmlChar*)prefix_str;
            ns->href = (const xmlChar*)xmlns_str;
            
            prop->name = name_str;
            prop->namespace = ns;
            
            elmno++;
        } else if(prop) {
            if(cx_strprefix(line, s_ns)) {
                line = cx_strsubs(line, 3);
                
                cxstring prefix;
                cxstring href;
                if(webdav_xattr_parse_ns(line, &prefix, &href)) {
                    log_ereport(
                            LOG_FAILURE,
                            "webdav xattr backend: file %s: invalid xattr format[%d]: cannot parse ns",
                            path,
                            elmno);
                    error = 1;
                    break;
                }
                
                WSNamespace *ns_decl = cxMalloc(a, sizeof(WSNamespace));
                if(!ns_decl) {
                    error = 1;
                    break;
                }
                ZERO(ns_decl, sizeof(WSNamespace));
                
                ns_decl->prefix = (const xmlChar*)cx_strdup_a(a, prefix).ptr;
                ns_decl->href = (const xmlChar*)cx_strdup_a(a, href).ptr;
                if(!(ns_decl->prefix && ns_decl->href)) {
                    error = 1;
                    break;
                }
                
                if(webdav_nslist_add(a->data, &ns_begin, &ns_end, ns_decl)) {
                    error = 1;
                    break;
                }
            } else if(cx_strprefix(line, s_data)) {
                line = cx_strsubs(line, 5);
                
                // util_strtoint just works here, because the line ends with \n
                // the xattr content data is also garanteed to be 0-terminated
                int64_t data_len = 0;
                if(!util_strtoint(line.ptr, &data_len)) {
                    log_ereport(
                            LOG_FAILURE,
                            "webdav xattr backend: file %s: invalid xattr format[%d]: number expected after data",
                            path,
                            elmno);
                    error = 1;
                    break;
                }
                
                // get data
                if(data_len > 0) {
                    if(data_len > dat.length - pos) {
                        log_ereport(
                                LOG_FAILURE,
                                "webdav xattr backend: file %s: invalid data length %" PRId64 " in prop %d",
                                path,
                                data_len,
                                elmno);
                        error = 1;
                        break;
                    }
                    
                    cxstring propdata;
                    propdata.ptr = dat.ptr + pos;
                    propdata.length = data_len;
                    pos += data_len;
                    
                    cxmutstr propdata_cp = cx_strdup_a(a, propdata);
                    if(!propdata_cp.ptr) {
                        error = 1;
                        break;
                    }
                    prop->vtype = WS_VALUE_XML_DATA;
                    prop->value.data.namespaces = ns_begin;
                    prop->value.data.data = propdata_cp.ptr;
                    prop->value.data.length = propdata_cp.length;
                    
                    if(pos < dat.length && dat.ptr[pos] == '\n') {
                        pos++;
                    }
                }
            } else {
                log_ereport(
                        LOG_FAILURE,
                        "webdav xattr backend: file %s: invalid xattr format[%d]: unknown element",
                        path,
                        elmno);
                error = 1;
                break;
            }
        }
    }
    
    // add last property
    if(prop) {
        if(webdav_xattr_put_prop(pmap, prop)) {
            error = 1;
        }
    }
    
    if(error) {
        // TODO: free pmap content
        cxMapDestroy(pmap);
        pmap = NULL;
    }
    
    return pmap;
}

cxmutstr webdav_xattr_serialze_map(CxAllocator *a, CxMap *pmap) {
    pool_handle_t *pool = a->data;
    
    CxBuffer buf;
    if(cxBufferInit(&buf, NULL, 8192, a, CX_BUFFER_AUTO_EXTEND)) {
        return (cxmutstr){NULL,0};
    }
    
    CxIterator i = cxMapIteratorValues(pmap);
    cx_foreach(WebdavProperty*, prop, i) {
        WSXmlData *property_value = NULL;
        if(prop->vtype == WS_VALUE_XML_NODE) {
            property_value = wsxml_node2data(pool, prop->value.node);
        } else if(prop->vtype == WS_VALUE_XML_DATA) {
            property_value = &prop->value.data;
        }
        
        int ret = cx_bprintf(&buf, "prop xmlns:%s=\"%s\" %s\n", prop->namespace->prefix, prop->namespace->href, prop->name);
        if(ret <= 0) {
            pool_free(pool, buf.space);
            return (cxmutstr){NULL,0};
        }
        if(property_value) {
            WebdavNSList *ns = property_value->namespaces; 
            while(ns) {
                ret = cx_bprintf(&buf, "ns %s:%s\n", ns->namespace->prefix, ns->namespace->href);
                if(ret <= 0) {
                    pool_free(pool, buf.space);
                    return (cxmutstr){NULL,0};
                }
                ns = ns->next;
            }
            
            ret = cx_bprintf(&buf, "data %zu\n", property_value->length);
            if(ret <= 0) {
                pool_free(pool, buf.space);
                return (cxmutstr){NULL,0};
            }
            
            cxBufferWrite(property_value->data, 1, property_value->length, &buf);
            if(cxBufferPut(&buf, '\n') < 0) {
                pool_free(pool, buf.space);
                return (cxmutstr){NULL,0};
            }
        }
    }
    
    return (cxmutstr){buf.space, buf.size};
}

mercurial