src/server/safs/service.c

Sun, 23 Nov 2025 13:48:29 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Sun, 23 Nov 2025 13:48:29 +0100
changeset 639
1e8416350254
parent 601
d9bc7c2dfae2
permissions
-rw-r--r--

respect DirectoryIndex location setting in the service_index SAF

/*
 * 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 "../daemon/httprequest.h"

#include "../util/strbuf.h"
#include <cx/string.h>
#include <cx/linked_list.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);

    // everything ok for now
    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;
            errno = 0;
            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;
                    errno = 0;
                    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);
}


// TODO: maybe move these util functions to another file or library
static char* util_size_str2(const CxAllocator *a, WSBool iscollection, uint64_t contentlength, uint64_t dimension, int precision) {
    char *str = cxMalloc(a, 16);
    uint64_t size = contentlength;

    if(iscollection) {
        str[0] = '\0'; // currently no information for collections
    } else if(dimension < 0x400) {
        snprintf(str, 16, "%" PRIu64 " bytes", size);
    } else if(dimension < 0x100000) {
        float s = (float)size/0x400;
        int diff = (s*100 - (int)s*100);
        if(diff > 90) {
            diff = 0;
            s += 0.10f;
        }
        if(dimension < 0x2800 && diff != 0) {
            // size < 10 KiB
            snprintf(str, 16, "%.*f KiB", precision, s);
        } else {
            snprintf(str, 16, "%.0f KiB", s);
        }
    } else if(dimension < 0x40000000) {
        float s = (float)size/0x100000;
        int diff = (s*100 - (int)s*100);
        if(diff > 90) {
            diff = 0;
            s += 0.10f;
        }
        if(dimension < 0xa00000 && diff != 0) {
            // size < 10 MiB
            snprintf(str, 16, "%.*f MiB", precision, s);
        } else {
            size /= 0x100000;
            snprintf(str, 16, "%.0f MiB", s);
        }
    } else if(dimension < 0x1000000000ULL) {
        float s = (float)size/0x40000000;
        int diff = (s*100 - (int)s*100);
        if(diff > 90) {
            diff = 0;
            s += 0.10f;
        }
        if(dimension < 0x280000000 && diff != 0) {
            // size < 10 GiB
            snprintf(str, 16, "%.*f GiB", precision, s);
        } else {
            size /= 0x40000000;
            snprintf(str, 16, "%.0f GiB", s);
        }
    } else {
        size /= 1024;
        float s = (float)size/0x40000000;
        int diff = (s*100 - (int)s*100);
        if(diff > 90) {
            diff = 0;
            s += 0.10f;
        }
        if(dimension < 0x280000000 && diff != 0) {
            // size < 10 TiB
            snprintf(str, 16, "%.*f TiB", precision, s);
        } else {
            size /= 0x40000000;
            snprintf(str, 16, "%.0f TiB", s);
        }
    }
    return str;
}

static char* util_size_str(const CxAllocator *a, WSBool iscollection, uint64_t contentlength) {
    return util_size_str2(a, iscollection, contentlength, contentlength, 1);
}

static char* util_date_str(const CxAllocator *a, time_t tm) {
    struct tm t;
    struct tm n;
    time_t now = time(NULL);
#ifdef _WIN32
    memcpy(&t, localtime(&tm), sizeof(struct tm));
    memcpy(&n, localtime(&now), sizeof(struct tm));
#else
    localtime_r(&tm, &t);
    localtime_r(&now, &n);
#endif /* _WIN32 */
    char *str = cxMalloc(a, 16);
    if(t.tm_year == n.tm_year) {
        strftime(str, 16, "%b %d %H:%M", &t);
    } else {
        strftime(str, 16, "%b %d  %Y", &t);
    }
    return str;
}

static int cmp_file_type_name(IndexEntry *a, IndexEntry *b) {
    if(a->isdir != b->isdir) {
        return a->isdir ? -1 : 1;
    }
    int ret = strcasecmp(a->name, b->name);
    if(ret != 0) {
        return ret;
    }
    return strcmp(a->name, b->name);
}

int service_index(pblock *pb, Session *sn, Request *rq) {
    NSAPIRequest *req = (NSAPIRequest*)rq;
    if(req->location && req->location->set_dirindex && !req->location->dirindex) {
        return REQ_NOACTION;
    }
    
    //printf("service_index\n");
    const CxAllocator *a = pool_allocator(sn->pool);

    char *path = pblock_findkeyval(pb_key_path, rq->vars);
    cxstring uri = cx_str(pblock_findkeyval(pb_key_uri, rq->reqpb));
    if(uri.length == 0) {
        return REQ_ABORTED;
    }
    if(uri.ptr[uri.length-1] != '/') {
        cxmutstr newuri = cx_strcat_a(a, 2, uri, CX_STR("/"));
        uri = cx_strcast(newuri);
    }
    
    
    // params
    char *useemojis = pblock_findval("use-emojis", pb);
    char *diricon = pblock_findval("dir-icon", pb);
    char *fileicon = pblock_findval("file-icon", pb);
    char *hidden = pblock_findval("show-hidden", pb);
    
    WSBool show_hidden = TRUE;
    char *dir_str = "&lt;dir&gt;";
    char *file_str = "&lt;file&gt;";
    
    if(hidden) {
        show_hidden = util_getboolean(hidden, FALSE);
    }
    if(useemojis && util_getboolean(useemojis, FALSE)) {
        dir_str = "📁";
        file_str = "📄";
    } else {
        if(diricon) {
            dir_str = cx_asprintf_a(a, "<img src=\"%s\" alt=\"directory\" border=\"0\"/>", diricon).ptr;
        }
        if(fileicon) {
            file_str = cx_asprintf_a(a, "<img src=\"%s\" alt=\"directory\" border=\"0\"/>", fileicon).ptr;
        }
    }

    // open the file
    VFSContext *vfs = vfs_request_context(sn, rq);
    VFS_DIR dir = vfs_opendir(vfs, path);
    if(!dir) {
        return REQ_ABORTED;
    }
    
    CxList *files = cxLinkedListCreate(a, (cx_compare_func)cmp_file_type_name, sizeof(IndexEntry));
    if(!files) {
        vfs_closedir(dir);
        return REQ_ABORTED;
    }
    

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

    // write html header
    sbuf_puts(out, "<!DOCTYPE html>\n<html>\n<head>\n<title>Index of ");
    sbuf_append(out, uri);
    sbuf_puts(out, "</title>\n");
    sbuf_puts(out, "<style>\n");
    sbuf_puts(out, "body { font-family: sans; }\n");
    sbuf_puts(out, "h1 { font-size: 1.1em; }\n");
    sbuf_puts(out, "th { text-align: left; }\n");
    sbuf_puts(out, "td { padding-right: 2em; }\n");
    sbuf_puts(out, "a { text-decoration: none; }\n");
    sbuf_puts(out, ".type { padding-right: 0em; }\n");
    sbuf_puts(out, ".path { background-color: #e0e0e0; border-radius: 0.5em; padding: 0.25em 0.75em 0.25em 0.75em; }");
    sbuf_puts(out, "</style>\n");
    sbuf_puts(out, "</head><body><h1>");
    
    if(uri.length > 0) {
        sbuf_puts(out, "<span class=\"path\"><a href=\"/\">/</a></span>\n");
        size_t start = 1;
        for(size_t i=1;i<uri.length;i++) {
            if(uri.ptr[i] == '/') {
                sbuf_puts(out, "<span class=\"path\"><a href=\"");
                sbuf_write(out, uri.ptr, i);
                sbuf_puts(out, "\">");
                sbuf_write(out, uri.ptr+start, i-start);
                sbuf_puts(out, "</a></span>\n");
                start = i+1;
            }
        }
    }
    
    sbuf_puts(out, "</h1><hr>");

    // read directory at store entries in the files list
    int ret = REQ_PROCEED;
    VFS_ENTRY f;
    while(vfs_readdir_stat(dir, &f)) {
        if(!show_hidden && f.name[0] == '.') {
            continue;
        }
        
        IndexEntry entry;
        entry.name = pool_strdup(sn->pool, f.name);
        if(!entry.name) {
            ret = REQ_ABORTED;
            break;
        }
        if(f.stat_errno == 0) {
            entry.isdir = S_ISDIR(f.stat.st_mode);
            entry.size = (size_t)f.stat.st_size;
            entry.size_str = util_size_str(a, entry.isdir, entry.size);
            entry.lastmodified = util_date_str(a, f.stat.st_mtime);
        } else {
            entry.isdir = 0;
            entry.lastmodified = NULL;
            entry.size_str = NULL;
            entry.size = 0;
        }
        
        if(cxListAdd(files, &entry)) {
            ret = REQ_ABORTED;
            break;
        }
    }
    
    // generate html output
    sbuf_puts(out, "<table>\n<tr><th colspan=\"2\">Name</th><th>Size</th><th>Last Modified</th></tr>\n");
    cxListSort(files);
    CxIterator i = cxListIterator(files);
    cx_foreach(IndexEntry *, entry, i) {
        sbuf_puts(out, "<tr>");
        
        sbuf_puts(out, "<td class=\"type\">");
        sbuf_puts(out, entry->isdir ? dir_str : file_str);
        sbuf_puts(out, "</td>");
        
        sbuf_puts(out, "<td>");
        sbuf_puts(out, "<a href=\"");
        sbuf_append(out, uri);
        sbuf_puts(out, entry->name);
        sbuf_puts(out, "\">");
        sbuf_puts(out, entry->name);
        sbuf_puts(out, "</a>");
        sbuf_puts(out, "</td>");
        
        sbuf_puts(out, "<td>");
        if(entry->size_str) {
            char buf[64];
            snprintf(buf, 64, "<span title=\"%zu bytes\">", entry->size);
            sbuf_puts(out, buf);
            sbuf_puts(out, entry->size_str);
            sbuf_puts(out, "</span>");
        }
        sbuf_puts(out, "</td>");
        
        sbuf_puts(out, "<td>");
        if(entry->size_str) {
            sbuf_puts(out, entry->lastmodified);
        }
        sbuf_puts(out, "</td>");
        
        sbuf_puts(out, "</tr>\n");
    }
    
    sbuf_puts(out, "</table>\n</body>\n</html>\n");

    // send stuff to client
    if(ret == REQ_PROCEED) {
        pblock_removekey(pb_key_content_type, rq->srvhdrs);
        pblock_kvinsert(pb_key_content_type, "text/html; charset=utf-8", 24, 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 ret;
}

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