src/server/webdav/multistatus.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 513
9a49c245a49c
permissions
-rw-r--r--

add TODO to use a future ucx feature

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2020 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 "../daemon/session.h"
#include "../daemon/protocol.h"
#include "../util/platform.h"

#include <cx/string.h>
#include <cx/hash_map.h>

#include "multistatus.h"

#include "operation.h"
#include "xml.h"

#define MULTISTATUS_BUFFER_LENGTH 2048

Multistatus* multistatus_response(Session *sn, Request *rq) {
    Multistatus *ms = pool_malloc(sn->pool, sizeof(Multistatus));
    if(!ms) {
        return NULL;
    }
    ZERO(ms, sizeof(Multistatus)); 
    ms->response.addresource = multistatus_addresource;
    ms->sn = sn;
    ms->rq = rq;
    ms->namespaces = cxHashMapCreate(pool_allocator(sn->pool), CX_STORE_POINTERS, 8);
    ms->proppatch = FALSE;
    if(!ms->namespaces) {
        return NULL;
    }
    if(cxMapPut(ms->namespaces, cx_hash_key_str("D"), webdav_dav_namespace())) {
        return NULL;
    }
    return ms;
}

static int send_xml_root(Multistatus *ms, Writer *out) {
    writer_put_lit(out, "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"
                        "<D:multistatus");
    
    // write the namespaces definitions
    // key is the namespace prefix
    // the map always contains the "DAV:" namespace with the prefix "D"
    CxIterator i = cxMapIterator(ms->namespaces);  
    cx_foreach(CxMapEntry*, entry, i) {
        WSNamespace *ns = entry->value;
        writer_put_lit(out, " xmlns:");
        writer_put    (out, entry->key->data, entry->key->len);
        writer_put_lit(out, "=\"");
        writer_put_str(out, (char*)ns->href);
        writer_put_lit(out, "\"");
    }
    
    writer_put_lit(out, ">\n");
    
    return out->error;
}

static void send_nsdef(WSNamespace *ns, Writer *out) {
    writer_put_lit(out, " xmlns:");
    writer_put_str(out, (char*)ns->prefix);
    writer_put_lit(out, "=\"");
    writer_put_str(out, (char*)ns->href);
    writer_putc   (out, '\"');
}


// html escape
/*
static void send_string_escaped(Writer *out, cxmutstr str) {
    char *begin = str.ptr;
    char *end = begin;
    char *escape = NULL;
    int esclen;
    for(size_t i=0;i<str.length;i++) {
        char c = str.ptr[i];
        end = str.ptr + i;
        switch(c) {
            case '"': {
                escape = "&quot;";
                esclen = 6;
                break;
            }
            case '&': {
                escape = "&amp;";
                esclen = 5;
                break;
            }
            case '\'': {
                escape = "&apos;";
                esclen = 6;
                break;
            }
            case '<': {
                escape = "&lt;";
                esclen = 4;
                break;
            }
            case '>': {
                escape = "&gt;";
                esclen = 4;
                break;
            }
            default: continue;
        }
        ptrdiff_t len = end - begin;
        if(len > 0) {
            writer_put(out, begin, len);
            begin = end + 1;
        }
        writer_put(out, escape, esclen);
    }
    ptrdiff_t len = end - begin;
    if(len > 0) {
        writer_put(out, begin, len + 1);
        begin = end + 1;
    }
}
*/

static const char hex_ch[] = "0123456789ABCDEF";

static void send_string_escaped(Writer *out, cxmutstr str) {
    char *begin = str.ptr;
    char *end = begin;
    
    char escape[4];
    escape[0] = '%';
    
    for(size_t i=0;i<str.length;i++) {
        unsigned char c = (unsigned char)str.ptr[i];
        end = str.ptr + i;
        
        // check if the character must be escaped
        if(     (c >= 'A' && c <= 'Z') ||
                (c >= 'a' && c <= 'z') ||
                (c >= '/' && c <= '9') ||
                (strchr("_.\\-~", c) != NULL))
        {
            continue;
        }
        
        // convert char to hex number, escape[0] always contains '%'
        escape[1] = hex_ch[(c >> 4) & 0x0F];
        escape[2] = hex_ch[c & 0x0F];
        
        // write previous unescaped chars, if available
        ptrdiff_t len = end - begin;
        if(len > 0) {
            writer_put(out, begin, len);
        }
        
        // write escaped char
        writer_put(out, escape, 3);
        
        // begin next chunk
        begin = end + 1;
    }
    ptrdiff_t len = str.ptr + str.length - begin;
    if(len > 0) {
        writer_put(out, begin, len);
        begin = end + 1;
    }
}

static int send_property(
        Multistatus *ms,
        WebdavProperty *property,
        WebdavNSList *nsdef,
        WSBool writeContent,
        Writer *out)
{
    // write: "<prefix:name"
    writer_putc   (out, '<');
    writer_put_str(out, (char*)property->namespace->prefix);
    writer_putc   (out, ':');
    writer_put_str(out, (char*)property->name);
    
    // send additional namespace definitions required for the value
    WebdavNSList *def = nsdef;
    while(def) {
        send_nsdef(def->namespace, out);
        def = def->next;
    }
    
    // send xml lang attribute
    if(property->lang) {
        writer_put_lit(out, " xml:lang=\"");
        writer_put_str(out, (char*)property->lang);
        writer_putc   (out, '\"');
    }
    
    // end property tag and write content
    if(writeContent) {
        writer_putc(out, '>');
        
        // content
        switch(property->vtype) {
            case WS_VALUE_NO_TYPE: break;
            case WS_VALUE_XML_NODE: {
                wsxml_write_nodes_without_nsdef(
                        ms->sn->pool,
                        out,
                        property->value.node);
                break;
            }
            case WS_VALUE_XML_DATA: {
                // only write data, data->namespaces is already handled
                writer_put(
                        out,
                        property->value.data.data,
                        property->value.data.length);
                break;
            }
            case WS_VALUE_TEXT: {
                // asume the text is already escaped
                writer_put(
                        out,
                        property->value.text.str,
                        property->value.text.length);
                break;
            }
        }
        
        // end tag
        writer_put_lit(out, "</");
        writer_put_str(out, (char*)property->namespace->prefix);
        writer_putc   (out, ':');
        writer_put_str(out, (char*)property->name);
        writer_putc   (out, '>');
    } else {
        writer_put_lit(out, "/>");
    }
    
    return out->error;
}

static int send_response_tag(Multistatus *ms, MSResponse *rp, Writer *out) {
    writer_put_lit(out, " <D:response>\n"
                        "  <D:href>");
    send_string_escaped(out, cx_mutstr(rp->resource.href));
    writer_put_lit(out, "</D:href>\n");
    
    WSBool writeContent = ms->proppatch ? FALSE : TRUE;
    
    if(rp->plist_begin) {
        writer_put_lit(out, "  <D:propstat>\n"
                            "   <D:prop>\n");
        // send properties
        PropertyOkList *p = rp->plist_begin;
        while(p) {
            writer_put_lit(out, "    ");
            if(send_property(ms, p->property, p->nsdef, writeContent, out)) {
                return out->error;
            }
            writer_put_lit(out, "\n");
            p = p->next;
        }
        
        writer_put_lit(out, "   </D:prop>\n"
                            "   <D:status>HTTP/1.1 200 OK</D:status>\n"
                            "  </D:propstat>\n");
    }
    
    // send error properties
    PropertyErrorList *error = rp->errors;
    while(error) {
        writer_put_lit(out, "  <D:propstat>\n"
                            "   <D:prop>\n");
        
        WebdavPList *errprop = error->begin;
        while(errprop) {
            writer_put_lit(out, "    ");
            if(send_property(ms, errprop->property, NULL, FALSE, out)) {
                return out->error;
            }
            writer_putc(out, '\n');
            errprop = errprop->next;
        }
        
        char statuscode[8];
        int sclen = snprintf(statuscode, 8, "%d ", error->status);
        if(sclen > 4) {
            statuscode[0] = '5';
            statuscode[1] = '0';
            statuscode[2] = '0';
            statuscode[3] = ' ';
            sclen = 4;
        }
        writer_put_lit(out, "   </D:prop>\n"
                            "   <D:status>HTTP/1.1 ");
        writer_put(out, statuscode, sclen);
        const char *status_msg = protocol_status_message(error->status);
        if(status_msg) {
            writer_put(out, status_msg, strlen(status_msg));
        } else {
            writer_put_lit(out, "Server Error");
        }
        writer_put_lit(out, "</D:status>\n"
                            "  </D:propstat>\n");
        
        
        error = error->next;
    }
    
    // end response tag
    writer_put_lit(out, " </D:response>\n");
    
    return out->error;
}

int multistatus_send(Multistatus *ms, SYS_NETFD net) { 
    // make sure every resource is closed
    if(ms->current && !ms->current->resource.isclosed) {
        if(msresponse_close((WebdavResource*)ms->current)) {
            return 1;
        }
    }
    
    // start http response
    protocol_status(ms->sn, ms->rq, 207, NULL);
    if(protocol_start_response(ms->sn, ms->rq)) {
        return 1;
    }
    
    char buffer[MULTISTATUS_BUFFER_LENGTH];
    // create a writer, that flushes the buffer when it is filled
    Writer writer;
    Writer *out = &writer;
    writer_init(out, net, buffer, MULTISTATUS_BUFFER_LENGTH);
    
    // send the xml root element with namespace defs
    if(send_xml_root(ms, out)) {
        return 1;
    }
    
    // send response tags
    MSResponse *response = ms->first;
    while(response) {
        if(send_response_tag(ms, response, out)) {
            return 1;
        }
        response = response->next;
    }
    
    // end multistatus
    writer_put_lit(out, "</D:multistatus>\n");
    
    //printf("\n\n");
    //fflush(stdout);
    
    writer_flush(out);
    
    return 0;
}

WebdavResource * multistatus_addresource(
        WebdavResponse *response,
        const char *path)
{
    Multistatus *ms = (Multistatus*)response;
    MSResponse *res = pool_malloc(ms->sn->pool, sizeof(MSResponse));
    if(!res) {
        return NULL;
    }
    ZERO(res, sizeof(MSResponse));
    
    // set href
    res->resource.href = pool_strdup(ms->sn->pool, path);
    if(!res->resource.href) {
        return NULL;
    }
    
    res->resource.err = 0;
    
    // add resource funcs
    res->resource.addproperty = msresponse_addproperty;
    res->resource.close = msresponse_close;
    
    res->properties = cxHashMapCreate(pool_allocator(ms->sn->pool), CX_STORE_POINTERS, 32);
    if(!res->properties) {
        return NULL;
    }
    
    res->multistatus = ms;
    res->errors = NULL;
    res->resource.isclosed = 0;
    res->closing = 0;
    
    // add new resource to the resource list
    if(ms->current) {
        // before adding a new resource, the current resource must be closed
        if(!ms->current->resource.isclosed) {
            msresponse_close((WebdavResource*)ms->current);
        }
        ms->current->next = res;
    } else {
        ms->first = res;
    }
    ms->current = res;
    
    return (WebdavResource*)res;
}

static int oklist_add(
        pool_handle_t *pool,
        PropertyOkList **begin,
        PropertyOkList **end,
        WebdavProperty *property,
        WebdavNSList *nsdef)
{
    PropertyOkList *newelm = pool_malloc(pool, sizeof(PropertyOkList));
    if(!newelm) {
        return 1;
    }
    newelm->property = property;
    newelm->nsdef = nsdef;
    newelm->next = NULL;
    if(*end) {
        (*end)->next = newelm;
    } else {
        *begin = newelm;
    }
    *end = newelm;
    return 0;
}

/*
 * should only be called from msresponse_addproperty
 * 
 * Adds a property to the error list with the specified statuscode
 */
static int msresponse_addproperror(
        MSResponse *response,
        WebdavProperty *property,
        int statuscode)
{
    pool_handle_t *pool = response->multistatus->sn->pool;
       
    response->resource.err++;
      
    // MSResponse contains a list of properties for each status code
    // at first find the list for this status code
    PropertyErrorList *errlist = NULL;
    PropertyErrorList *list = response->errors;
    PropertyErrorList *last = NULL;
    while(list) {
        if(list->status == statuscode) {
            errlist = list;
            break;
        }
        last = list;
        list = list->next;
    }
    
    if(!errlist) {
        // no list available for this statuscode
        PropertyErrorList *newelm = pool_malloc(pool,
                sizeof(PropertyErrorList));
        if(!newelm) {
            return 1;
        }
        newelm->begin = NULL;
        newelm->end = NULL;
        newelm->next = NULL;
        newelm->status = statuscode;
        
        if(last) {
            last->next = newelm;
        } else {
            response->errors = newelm;
        }
        errlist = newelm;
    }
    
    // we have the list -> add the new element
    if(webdav_plist_add(pool, &errlist->begin, &errlist->end, property)) {
        return 1;
    }
    return 0;
}

static CxHashKey ms_property_key(
        CxAllocator *a,
        const xmlChar *href,
        const char *property_name)
{
    cxmutstr key_data = cx_strcat_a(a, 3, cx_str((const char*)href), (cxstring){ "\0", 1 }, cx_str(property_name));
    return cx_hash_key_bytes((unsigned char*)key_data.ptr, key_data.length);
}

int msresponse_addproperty(
        WebdavResource *res,
        WebdavProperty *property,
        int status)
{
    MSResponse *response = (MSResponse*)res;
    Session *sn = response->multistatus->sn;
    if(response->resource.isclosed) {
        log_ereport(
                LOG_WARN,
                "%s",
                "webdav: cannot add property to closed response tag");
        return 0;
    }
    
    // some WebdavProperty checks to make sure nothing explodes  
    if(!property->namespace || !property->namespace->href) {
        // error: namespace is required
        log_ereport(
                LOG_FAILURE,
                "%s",
                "webdav: property '%s' has no namespace",
                property->name);
        return 1;
    }
    
    // check if the property was already added to the resource
    CxAllocator *a = pool_allocator(sn->pool);
    CxHashKey key = ms_property_key(a, property->namespace->href, property->name);
    if(cxMapGet(response->properties, key)) {
        cxFree(a, (void*)key.data);
        return 0;
    }
    if(cxMapPut(response->properties, key, property)) {
        return 1; // OOM
    }
    cxFree(a, (void*)key.data);
    
    // list of namespace definitions for this property
    WebdavNSList *nsdef_begin = NULL;
    WebdavNSList *nsdef_end = NULL;
    
    // add namespace of this property to the namespace map
    // the namespace map will be used for global namespace definitions
    if(property->namespace->prefix) {
        WSNamespace *ns = cxMapGet(
                response->multistatus->namespaces,
                cx_hash_key_str((const char*)property->namespace->prefix));
        if(!ns) {
            // prefix is not in use -> we can add the namespace to the ns map
            int err = cxMapPut(
                    response->multistatus->namespaces,
                    cx_hash_key_str((const char*)property->namespace->prefix),
                    property->namespace);
            if(err) {
                return 1; // OOM
            }
        } else if(
                strcmp((const char*)property->namespace->href,
                (const char*)ns->href))
        {
            // global namespace != local namespace
            // therefore we need a namespace definition in this element
            
            // ns-prefix != property-prefix -> add ns to nsdef
            if(webdav_nslist_add(
                    sn->pool,
                    &nsdef_begin,
                    &nsdef_end,
                    property->namespace))
            {
                return 1; // OOM
            }
        }
    }
    
    if(response->multistatus->proppatch && response->errors) {
        // in a proppatch request all operations must succeed
        // if we have an error, the property update status code must be
        // 424 Failed Dependency
        status = 424;
    }
    
    // error properties will be added to a separate list
    if(status != 200) { 
        return msresponse_addproperror(response, property, status);
    }
    
    // add all namespaces used by this property to the nsdef list
    WebdavNSList *nslist = NULL;
    if(property->vtype == WS_VALUE_XML_NODE) {
        // iterate over xml tree and collect all namespaces
        int err = 0;
        nslist = wsxml_get_required_namespaces(
                response->multistatus->sn->pool,
                property->value.node,
                &err);
        if(err) {
            return 1; // OOM
        }
    } else if(property->vtype == WS_VALUE_XML_DATA) {
        // xml data contains a list of all used namespaces
        nslist = property->value.data.namespaces;
    } // other value types don't contain xml namespaces
    
    while(nslist) {
        // only add the namespace to the definitions list, if it isn't a
        // property namespace, because the prop ns is already added
        // to the element's def list or global definitions list
        if(strcmp(
                (const char*)nslist->namespace->prefix,
                (const char*)property->namespace->prefix))
        {
            // ns-prefix != property-prefix -> add ns to nsdef
            if(webdav_nslist_add(
                    sn->pool,
                    &nsdef_begin,
                    &nsdef_end,
                    nslist->namespace))
            {
                return 1; // OOM
            }
        }
        nslist = nslist->next;
    }
    
    // add property to the list
    if(oklist_add(
            sn->pool,
            &response->plist_begin,
            &response->plist_end,
            property,
            nsdef_begin))
    {
        return 1;
    }
    return 0;
}

int msresponse_close(WebdavResource *res) {
    MSResponse *response = (MSResponse*)res;
    if(response->closing) {
        return 0; // close already in progress
    }
    response->closing = TRUE;
    Multistatus *ms = response->multistatus;
    
    int ret = REQ_PROCEED;
    
    // PROPFIND:
    // response_close will execute propfind_do of all remaining backends
    // after that we will have all available properties
    WebdavOperation *op = ms->response.op;
    if(op->response_close(op, res)) {
        ret = REQ_ABORTED;
    }
    
    // add missing properties with status code 404
    CxAllocator *a = pool_allocator(ms->sn->pool);
    WebdavPList *pl = ms->response.op->reqprops;
    while(pl) {
        CxHashKey key = ms_property_key(a, pl->property->namespace->href, pl->property->name);
        if(!cxMapGet(response->properties, key)) {
            // property was not added to this response
            if(ms->proppatch) {
                if(msresponse_addproperty(res, pl->property, 424)) {
                    ret = REQ_ABORTED;
                    break;
                }
            } else {
                if(msresponse_addproperty(res, pl->property, 404)) {
                    ret = REQ_ABORTED;
                    break;
                }
            }
        }
        
        pl = pl->next;
    }
    
    if(ms->proppatch && response->errors) {
        // a proppatch response must succeed entirely
        // if we have a single error prop, move all props with status 200
        // to the error list
        PropertyOkList *elm = response->plist_begin;
        PropertyOkList *nextelm;
        while(elm) {
            if(msresponse_addproperty(res, elm->property, 424)) {
                return 1;
            }
            nextelm = elm->next;
            pool_free(response->multistatus->sn->pool, elm);
            elm = nextelm;
        }
        response->plist_begin = NULL;
        response->plist_end = NULL;
    }
    
    // we don't need the properties anymore
    cxMapDestroy(response->properties);
    
    response->resource.isclosed = TRUE;
    return ret;
}

mercurial