src/server/safs/service.c

Wed, 31 May 2023 13:08:49 +0200

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Wed, 31 May 2023 13:08:49 +0200
changeset 496
d511c13ba68d
parent 493
56cf890dd9ed
permissions
-rw-r--r--

add http range debug logging

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2013 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 <sys/file.h>
#include <sys/stat.h>

#include "service.h"
#include "objecttype.h"
#include "../util/io.h"
#include "../util/pblock.h"
#include "../util/util.h"
#include "../daemon/protocol.h"
#include "../daemon/vfs.h"

#include "../util/strbuf.h"
#include <cx/string.h>
#include <cx/utils.h>
#include <cx/printf.h>

#include <errno.h>

/*
 * prepares servicing a file
 *
 * adds content-length header
 *
 * return the opened file
 */
SYS_FILE prepare_service_file(Session *sn, Request *rq, VFSContext *vfs, struct stat *s, int *ret) {
    char *path = pblock_findkeyval(pb_key_path, rq->vars);

    // open the file
    SYS_FILE fd = vfs_open(vfs, path, O_RDONLY);
    if(!fd) {
        // vfs_open sets http status code
        *ret = REQ_ABORTED;
        return NULL;
    }

    // get stat
    if(vfs_fstat(vfs, fd, s) != 0) {
        //perror("prepare_service_file: stat");
        protocol_status(sn, rq, 500, NULL);
        *ret = REQ_ABORTED;
        return NULL;
    }
    
    // check if the file is a directory
    if(S_ISDIR(s->st_mode)) {
        pblock_removekey(pb_key_content_type, rq->srvhdrs);
        char *uri = pblock_findkeyval(pb_key_uri, rq->reqpb);
        size_t urilen = strlen(uri);
        if(urilen > 0 && uri[urilen-1] != '/') {
            pblock_nvinsert("content-length", "0", rq->srvhdrs);
            char *location = pool_malloc(sn->pool, urilen + 2);
            memcpy(location, uri, urilen);
            location[urilen] = '/';
            location[urilen+1] = '\0';
            pblock_kvinsert(pb_key_location, location, urilen + 1, rq->srvhdrs);
            protocol_status(sn, rq, 302, NULL);
            http_start_response(sn, rq);
            *ret = REQ_PROCEED;
        } else {
            // set content-type to "internal/directory" and ret to REQ_NOACTION
            // maybe a SAF will respond to that
            pblock_kvinsert(
                    pb_key_content_type,
                    OBJTYPE_INTERNAL_DIRECTORY,
                    sizeof(OBJTYPE_INTERNAL_DIRECTORY)-1,
                    rq->srvhdrs);
            *ret = REQ_NOACTION;
        }
        vfs_close(fd);
        return NULL;
    }
    
    // sets last-modified, content-length and checks conditions
    const char *etag = vfs_getetag(fd); // optionally, get etag from file
    if(http_set_finfo_etag(sn, rq, s, etag) != REQ_PROCEED) {
        vfs_close(fd);
        *ret = REQ_ABORTED;
        return NULL;
    }
    
    // TODO: check if vfs can seek
    pblock_kvinsert(pb_key_accept_ranges, "bytes", 5, rq->srvhdrs);

    // start response
    protocol_status(sn, rq, 200, NULL);

    return fd;
}

static void free_range(Session *sn, HttpRange *range) {
    HttpRange *elm = range;
    while(elm) {
        HttpRange *next = elm->next;
        pool_free(sn->pool, elm);
        elm = next;
    }
}

static HttpRange* parse_range(Session *sn, char *header, int *status) {
    *status = PROTOCOL_OK;
    
    cxstring range = cx_strtrim(cx_str(header));
    if(!cx_strprefix(range, (cxstring)CX_STR("bytes="))) {
        // unknown range unit - ignore range header
        return NULL;
    }
    
    // get byte-range-set
    range = cx_strsubs(range, 6);
    if(range.length < 1) {
        return NULL;
    }
    
    HttpRange *range_list = NULL;
    HttpRange *last = NULL;
    off_t begin = -1;
    int start = 0;
    int hasbegin = 0;
    for(int i=0;i<=range.length;i++) {
        char c = range.ptr[i];
        if(c == '-') {
            cxstring num = cx_strsubsl(range, start, i-start);
            if(num.length == 0) {
                // empty string before '-' is legal
                hasbegin = 1;
                begin = -1;
                start = i+1;
                continue;
            }
            char *end;
            long long n = strtoll(num.ptr, &end, 10);
            if(errno == 0 && end == range.ptr + i && n >= 0) {
                begin = n;
                hasbegin = 1;
                start = i+1;
            } else {
                // syntax error
                free_range(sn, range_list);
                return NULL;
            }
        } else if(c == ',' || c == '\0') {
            cxstring num = cx_strsubsl(range, start, i-start);
            if(hasbegin) {
                long long n;
                if(num.length == 0) {
                    // empty string after '-' is legal
                    n = -1;
                } else {
                    char *end;
                    n = strtoll(num.ptr, &end, 10);
                    if(errno != 0 || end != range.ptr + i || n < 0) {
                        // syntax error
                        free_range(sn, range_list);
                        return NULL;
                    }
                }
                
                if(!(begin < 0 && n < 0)) {
                    // range: begin - n
                    HttpRange *rangeelm = pool_malloc(sn->pool, sizeof(HttpRange));
                    if(!rangeelm) {
                        free_range(sn, range_list);
                        *status = PROTOCOL_SERVER_ERROR;
                        return NULL;
                    }
                    rangeelm->begin = begin;
                    rangeelm->end = n;
                    rangeelm->next = NULL;
                    if(!last) {
                        range_list = rangeelm;
                        last = rangeelm;
                    } else {
                        last->next = rangeelm;
                        last = rangeelm;
                    }
                    
                    hasbegin = 0;
                    start = i+1;
                    continue;
                }
            }
            
            // syntax error
            free_range(sn, range_list);
            return NULL;
        }
    }

    return range_list;
}

static int validate_range(HttpRange *range, struct stat *finfo, int *status) {
    off_t max_len = finfo->st_size;
    while(range) {
        if(range->begin > 0 && range->end > 0) {
            if(range->end < range->begin) {
                *status = PROTOCOL_REQUESTED_RANGE_NOT_SATISFIABLE;
                return 0;
            }
        }
        if(range->begin >= max_len) {
            *status = PROTOCOL_REQUESTED_RANGE_NOT_SATISFIABLE;
            return 0;
        }
        if(range->end >= max_len) {
            *status = PROTOCOL_REQUESTED_RANGE_NOT_SATISFIABLE;
            return 0;
        }
        
        range = range->next;
    }
    
    // TODO: check for Denial-of-Service Attacks
    
    return 1;
}

/*
 * translates a HttpRange element to a begin offset and a length
 * the HttpRange must be validated
 */
static void range2off(HttpRange *range, off_t filelen, off_t *begin, off_t *length) {
    if(range->begin < 0) {
        // bytes=-a
        *begin = filelen - range->end;
        *length = range->end;
    } else if(range->end < 0) {
        // bytes=a-
        *begin = range->begin;
        *length = filelen - range->begin;
    } else {
        // bytes=a-b
        *begin = range->begin;
        *length = range->end + 1 - range->begin;
    }
}

#define SF_MAX_LEN 0x8000000

static int send_range(Session *sn, SYS_FILE fd, off_t offset, off_t length, char *header, int headerlen) {
    off_t remaining = length;
    
    sendfiledata sfd;
    sfd.fd = fd;
    sfd.header = header;
    sfd.hlen = headerlen;
    sfd.trailer = NULL;
    sfd.tlen = 0;
    
    while(remaining > 0) {
        size_t sflen = remaining < SF_MAX_LEN ? remaining : SF_MAX_LEN;
        sfd.offset = offset;
        sfd.len = sflen;
        
        ssize_t r = net_sendfile(sn->csd, &sfd);
        if(r < 0) {
            return -1;
        }
        
        sfd.header = NULL; // make sure the header is only sent once
        offset += r;
        remaining -= r;
    }
    
    return 0;
}


static void send_range_cleanup(AsyncSendRange *asr) {
    WSBool error = asr->error;
    Session *sn = asr->sn;
    Request *rq = asr->rq;
    
    pool_handle_t *pool = asr->sn->pool;
    vfs_close(asr->in);
    pool_free(pool, asr->aio->buf);
    pool_free(pool, asr->aio);
    pool_free(pool, asr->readev);
    pool_free(pool, asr->writeev);
    pool_free(pool, asr);
    
    int ret = REQ_PROCEED;
    if(error) {
        rq->rq_attr.keep_alive = 0;
        ret = REQ_ABORTED;
    }
    // return to nsapi loop
    nsapi_function_return(sn, rq, ret);
}

static int send_buf(
        SYS_NETFD out,
        char *restrict buf,
        size_t len,
        size_t *restrict pos)
{
    while(*pos < len) {
        ssize_t w = net_write(out, buf + *pos, len - *pos);
        if(w <= 0) {
            return -1;
        }
        *pos += w;
    }
    return 0;
}

static int send_bytes(AsyncSendRange *asr, WSBool *completed) {
    *completed = FALSE;
    if(asr->header) {
        if(send_buf(asr->out, asr->header, asr->headerlen, &asr->headerpos)) {
            if(net_errno(asr->out) == EAGAIN) {
                return 0;
            } else {
                asr->error = TRUE;
                return 1;
            }
        }
        if(asr->headerpos >= asr->headerlen) {
            asr->header = NULL;
        }
    }
    
    if(send_buf(asr->out, asr->aio->buf, asr->aio->result, &asr->wpos)) {
        if(net_errno(asr->out) == EAGAIN) {
            return 0;
        } else {
            asr->error = TRUE;
            return 1;
        }
    }
    
    if(!asr->read_complete) {
        // write completed => new asynchronous read
        asr->aio->offset += asr->aio->result;
        size_t length = asr->end - asr->offset;
        asr->aio->nbytes = AIO_BUF_SIZE < length ? AIO_BUF_SIZE : length;
        asr->read_inprogress = TRUE;
        if(system_aio_read(asr->aio)) {
            asr->error = TRUE;
            return 1;
        }
    }
    *completed = TRUE;
    return 0;
}

static int send_range_readevent(EventHandler *ev, Event *event) {
    AsyncSendRange *asr = event->cookie;
    asr->read_inprogress = FALSE;
    asr->wpos = 0;
    asr->offset += asr->aio->result;
    if(asr->error || asr->aio->result < 0) {
        return 0;
    }
    
    int ret = 1;
    if(asr->aio->result == 0 || asr->offset >= asr->end) {
        asr->read_complete = TRUE;
        ret = 0;
    }
    
    WSBool completed;
    if(send_bytes(asr, &completed)) {
        return 0;
    }
    if(!completed && !asr->write_inprogress) {
        asr->write_inprogress = TRUE;
        if(event_pollout(ev, asr->out, asr->writeev)) {
            asr->error = TRUE;
            return 0;
        }
    }
    
    return ret;
}

static int send_range_writeevent(EventHandler *ev, Event *event) {
    AsyncSendRange *asr = event->cookie;
    if(asr->error) {
        return 1;
    }
    
    WSBool completed;
    if(send_bytes(asr, &completed)) {
        return 1;
    }
    
    if(completed) {
        return 0;
    }
    
    return 1;
}

static int send_range_aio_finish(EventHandler *ev, Event *event) {
    AsyncSendRange *asr = event->cookie;
    if(!asr->write_inprogress) {
        send_range_cleanup(asr);
    }
    asr->read_inprogress = FALSE;
    return 0;
}

static int send_range_poll_finish(EventHandler *ev, Event *event) {
    AsyncSendRange *asr = event->cookie;
    if(!asr->read_inprogress) {
        send_range_cleanup(asr);
    }
    asr->write_inprogress = FALSE;
    return 0;
}

static int send_range_aio(Session *sn, Request *rq, SYS_FILE fd, off_t offset, off_t length, char *header, int headerlen) {
    net_setnonblock(sn->csd, TRUE);
    
    // try to send the header
    ssize_t hw = net_write(sn->csd, header, headerlen);
    if(hw < 0) {
        if(net_errno(sn->csd) == EAGAIN) {
            hw = 0;
        } else {
            return REQ_ABORTED;
        }
    }
    
    AsyncSendRange *asr = pool_malloc(sn->pool, sizeof(AsyncSendRange));
    asr->sn = sn;
    asr->rq = rq;
    asr->in = fd;
    asr->out = sn->csd;
    asr->offset = offset;
    asr->end = offset + length;
    //asr->length = length;
    asr->pos = offset;
    asr->read_complete = FALSE;
    asr->read_inprogress = FALSE;
    asr->write_inprogress = FALSE;
    asr->error = FALSE;
    if(hw == headerlen) {
        asr->header = NULL;
        asr->headerlen = 0;
        asr->headerpos = 0;
    } else {
        asr->header = header;
        asr->headerlen = headerlen;
        asr->headerpos = hw;
    }
    
    Event *readev = pool_malloc(sn->pool, sizeof(Event));
    ZERO(readev, sizeof(Event));
    readev->cookie = asr;
    readev->fn = send_range_readevent;
    readev->finish = send_range_aio_finish;
    
    Event *writeev = pool_malloc(sn->pool, sizeof(Event));
    ZERO(writeev, sizeof(Event));
    writeev->cookie = asr;
    writeev->fn = send_range_writeevent;
    writeev->finish = send_range_poll_finish;
    
    asr->readev = readev;
    asr->writeev = writeev;
    
    aiocb_s *aio = pool_malloc(sn->pool, sizeof(aiocb_s));
    aio->buf = pool_malloc(sn->pool, AIO_BUF_SIZE);
    aio->nbytes = AIO_BUF_SIZE < length ? AIO_BUF_SIZE : length;
    aio->filedes = fd;
    aio->offset = offset;
    aio->evhandler = sn->ev;
    aio->event = readev;
    
    asr->aio = aio;
    asr->wpos = 0;
    
    asr->read_inprogress = TRUE;
    if(system_aio_read(aio)) {
        send_range_cleanup(asr);
        return REQ_ABORTED;
    }
    asr->read_inprogress = TRUE;
    
    return REQ_PROCESSING;
}

struct multi_range_elm {
    cxmutstr header;
    off_t  offset;
    off_t  length;
};

static int send_multi_range(Session *sn, Request *rq, SYS_FILE fd, off_t filelen, HttpRange *range) {
    CxAllocator *a = pool_allocator(sn->pool);
    
    pb_param *content_type = pblock_remove("content-type", rq->srvhdrs);
    
    char sep[64];
    int seplen = util_mime_separator(sep);
    
    cxmutstr newct = cx_asprintf_a(a, "multipart/byteranges; boundary=%s", sep+4);
    pblock_kvinsert(
            pb_key_content_type,
            newct.ptr,
            newct.length,
            rq->srvhdrs);
    cxFree(a, newct.ptr);
    
    // calculate content-length
    off_t response_len = 0;
    
    int nrange = 0;
    HttpRange *rangeelm = range;
    while(rangeelm) {
        nrange++;
        rangeelm = rangeelm->next;
    }
    
    struct multi_range_elm *r = pool_calloc(sn->pool, nrange, sizeof(struct multi_range_elm));
    rangeelm = range;
    int i=0;
    while(rangeelm) {
        range2off(rangeelm, filelen, &(r[i].offset), &(r[i].length));
        r[i].header = cx_asprintf_a(
                a,
                "%s\r\nContent-Type: %s\r\nContent-Range: bytes %lld-%lld/%lld\r\n\r\n",
                sep,
                content_type->value,
                (long long)r[i].offset,
                (long long)r[i].offset+r[i].length - 1,
                (long long)filelen);
        
        response_len += r[i].header.length + r[i].length;
        
        rangeelm = rangeelm->next;
        i++;
    }
    
    response_len += seplen + 4; // trailer: sep + '--' + CRLF
    
    // finally, set the content-length header
    pblock_kllinsert(
            pb_key_content_length,
            (long long)response_len,
            rq->srvhdrs);
    
    // and start the response
    http_start_response(sn, rq);
    
    rangeelm = range;
    i = 0;
    while(rangeelm) {
        if(send_range(sn, fd, r[i].offset, r[i].length, r[i].header.ptr, r[i].header.length)) {
            // TODO: error
        }
        rangeelm = rangeelm->next;
        i++;
    }
    net_printf(sn->csd, "%s--\r\n", sep);
    
    pool_free(sn->pool, r);
    return 0;
}

int send_file(pblock *pb, Session *sn, Request *rq) {
    int ret = REQ_NOACTION;
    struct stat s;
    VFSContext *vfs = vfs_request_context(sn, rq);
    SYS_FILE fd = prepare_service_file(sn, rq, vfs, &s, &ret);
    if(!fd) {
        // if an error occurs, prepare_service_file sets the http status code
        // in case fd is a directory and the uri already ends with an trailing
        // '/', ret is set to REQ_NOACTION
        return ret;
    }
    
    // get and validate range header
    char *range_header = pblock_findkeyval(pb_key_range, rq->headers);
    HttpRange *range = NULL;
    if(range_header) {
        log_ereport(LOG_DEBUG, "send_file: range: %s", range_header);
        
        int status;
        range = parse_range(sn, range_header, &status);
        if(status != PROTOCOL_OK) {
            protocol_status(sn, rq, status, NULL);
            vfs_close(fd);
            return REQ_ABORTED;
        }
        
        if(!validate_range(range, &s, &status)) {
            protocol_status(sn, rq, status, NULL);
            free_range(sn, range);
            vfs_close(fd);
            return REQ_ABORTED;
        }
    }
    
    int single_range = 1;
    off_t offset;
    off_t length;
    if(range) {  
        protocol_status(sn, rq, 206, NULL);
        pblock_removekey(pb_key_content_length, rq->srvhdrs);
        
        if(range->next) {
            single_range = 0;
        } else {
            range2off(range, s.st_size, &offset, &length);
            
            pblock_kllinsert(
                    pb_key_content_length,
                    (long long)length,
                    rq->srvhdrs);
            
            cxmutstr content_range = cx_asprintf(
                    "%lld-%lld/%lld",
                    (long long)offset,
                    (long long)offset+length - 1,
                    (long long)s.st_size);
            pblock_kvinsert(
                    pb_key_content_range,
                    content_range.ptr,
                    content_range.length,
                    rq->srvhdrs);
            free(content_range.ptr);
        }
    } else {
        offset = 0;
        length = s.st_size;
    }
    
    if(single_range) {
        // send response header
        http_start_response(sn, rq);
        // send content
        // TODO: fix: send_range_aio is unstable #96
        //ret = send_range_aio(sn, rq, fd, offset, length, NULL, 0);
        //if(ret == REQ_PROCESSING) {
        //    return ret;
        //}
        
        if(send_range(sn, fd, offset, length, NULL, 0)) {
            // TODO: error
        }
    } else {
        ret = send_multi_range(sn, rq, fd, s.st_size, range);
        // TODO: error
    }
    
    // cleanup
    vfs_close(fd);
    free_range(sn, range);

    return ret;
}



int service_hello(pblock *pb, Session *sn, Request *rq) {
    pblock_removekey(pb_key_content_type, rq->srvhdrs);
    pblock_nvinsert("content-type", "text/plain", rq->srvhdrs);
    pblock_nninsert("content-length", 13, rq->srvhdrs);
    protocol_status(sn, rq, 200, NULL);
    http_start_response(sn, rq);
    net_write(sn->csd, "Hello World!\n", 13);
    return REQ_PROCEED;
}

static int ws_msghandler(WebSocket *ws, WSMessage *msg) {
    if(msg->type == 1) {
        printf("Message(text): %.*s\n", (int)msg->length, msg->data);
        websocket_send_text(ws->userdata, "hello", 5);
    } else {
        printf("Message: opcode: %d | length: %d\n", msg->type, (int)msg->length);
    }
    return 0;
}

int service_ws_hello(pblock *pb, Session *sn, Request *rq) {
    WebSocket ws;
    ZERO(&ws, sizeof(WebSocket));
    ws.userdata = sn->csd;
    
    ws.on_message = ws_msghandler;
    return http_handle_websocket(sn, rq, &ws);
}

int service_index(pblock *pb, Session *sn, Request *rq) {
    //printf("service_index\n");

    char *path = pblock_findkeyval(pb_key_path, rq->vars);
    char *uri = pblock_findkeyval(pb_key_uri, rq->reqpb);

    cxstring r_uri = cx_str(uri);

    // open the file
    VFSContext *vfs = vfs_request_context(sn, rq);
    VFS_DIR dir = vfs_opendir(vfs, path);
    if(!dir) {
        return REQ_ABORTED;
    }

    sbuf_t *out = sbuf_new(1024); // output buffer

    // write html header
    sbuf_puts(out, "<html>\n<head>\n<title>Index of ");
    sbuf_puts(out, uri);
    sbuf_puts(out, "</title>\n</head><body>\n<h1>Index of ");
    sbuf_puts(out, uri);
    sbuf_puts(out, "</h1><hr>\n\n");

    // list directory
    VFS_ENTRY f;
    while(vfs_readdir(dir, &f)) {
        cxstring filename = cx_str(f.name);

        sbuf_puts(out, "<a href=\"");
        sbuf_append(out, r_uri);
        sbuf_append(out, filename);
        sbuf_puts(out, "\">");
        sbuf_append(out, filename);
        sbuf_puts(out, "</a><br>\n");
    }

    sbuf_puts(out, "\n</body>\n</html>\n");

    // send stuff to client
    pblock_removekey(pb_key_content_type, rq->srvhdrs);
    pblock_kvinsert(pb_key_content_type, "text/html", 9, rq->srvhdrs);
    pblock_nninsert("content-length", out->length, rq->srvhdrs);
    protocol_status(sn, rq, 200, NULL);
    http_start_response(sn, rq);

    net_write(sn->csd, out->ptr, out->length);

    // close
    vfs_closedir(dir);
    sbuf_free(out);

    return REQ_PROCEED;
}

int send_options(pblock *pb, Session *sn, Request *rq) {
    char *allow = "HEAD, GET, PUT, DELETE, TRACE, OPTIONS, MOVE, COPY, "
            "PROPFIND, PROPPATCH, MKCOL, LOCK, UNLOCK, ACL, REPORT";
    char *dav = "1,2,access-control";

    pblock_removekey(pb_key_content_type, rq->srvhdrs);
    pblock_nvinsert("allow", allow, rq->srvhdrs);
    pblock_nvinsert("dav", dav, rq->srvhdrs);
    pblock_nninsert("content-length", 0, rq->srvhdrs);
    protocol_status(sn, rq, 204, NULL);
    http_start_response(sn, rq);

    return REQ_PROCEED;
}

mercurial