src/server/safs/service.c

Sat, 21 Jan 2017 15:31:17 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Sat, 21 Jan 2017 15:31:17 +0100
changeset 151
74d21dd5fd5d
parent 127
84e206063b64
child 172
5580517faafc
permissions
-rw-r--r--

adds more error handling and logging to send_cgi

/*
 * 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 "../util/io.h"
#include "../util/pblock.h"
#include "../util/util.h"
#include "../daemon/protocol.h"
#include "../daemon/vfs.h"

#include "../util/strbuf.h"
#include <ucx/string.h>
#include <ucx/utils.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) {
    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
        return NULL;
    }

    // get stat
    if(vfs_fstat(vfs, fd, s) != 0) {
        //perror("prepare_service_file: stat");
        protocol_status(sn, rq, 500, NULL);
        return NULL;
    }
    
    // check if the file is a directory
    if(S_ISDIR(s->st_mode)) {
        pblock_nvinsert("content-length", "0", rq->srvhdrs);
        pblock_removekey(pb_key_content_type, rq->srvhdrs);
        char *uri = pblock_findkeyval(pb_key_uri, rq->reqpb);
        size_t urilen = strlen(uri);
        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);
        vfs_close(fd);
        return NULL;
    }
    
    // sets last-modified, content-length and checks conditions
    if(http_set_finfo(sn, rq, s) != REQ_PROCEED) {
        vfs_close(fd);
        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;
    
    sstr_t range = sstrtrim(sstr(header));
    if(!sstrprefix(range, S("bytes="))) {
        // unknown range unit - ignore range header
        return NULL;
    }
    
    // get byte-range-set
    range = sstrsubs(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 == '-') {
            sstr_t num = sstrsubsl(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') {
            sstr_t num = sstrsubsl(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;
    
    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;
}

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

static int send_multi_range(Session *sn, Request *rq, SYS_FILE fd, off_t filelen, HttpRange *range) {
    pb_param *content_type = pblock_remove("content-type", rq->srvhdrs);
    
    char sep[64];
    int seplen = util_mime_separator(sep);
    
    sstr_t newct = ucx_sprintf("multipart/byteranges; boundary=%s", sep+4);
    pblock_kvinsert(
            pb_key_content_type,
            newct.ptr,
            newct.length,
            rq->srvhdrs);
    free(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 = calloc(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 = ucx_sprintf(
                "%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,
                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);
    
    free(r);
    return 0;
}

int send_file(pblock *pb, Session *sn, Request *rq) {
    struct stat s;
    VFSContext *vfs = vfs_request_context(sn, rq);
    SYS_FILE fd = prepare_service_file(sn, rq, vfs, &s);
    if(!fd) {
        // if an error occurs, prepare_service_file sets the http status code
        // we can just return REQ_ABORTED
        return REQ_ABORTED;
    }
    
    // get and validate range header
    char *range_header = pblock_findkeyval(pb_key_range, rq->headers);
    HttpRange *range = NULL;
    if(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);
            
            sstr_t content_range = ucx_sprintf(
                    "%lld-%lld/%lld",
                    (long long)offset,
                    (long long)offset+length - 1,
                    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
        if(send_range(sn, fd, offset, length, NULL, 0)) {
            // TODO: error
        }
    } else {
        if(send_multi_range(sn, rq, fd, s.st_size, range)) {
            // TODO: error
        }
    }
    
    // cleanup
    vfs_close(fd);
    free_range(sn, range);

    return REQ_PROCEED;
}



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);

    sstr_t r_uri = sstr(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)) {
        sstr_t filename = sstr(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