implemented range requests

Fri, 23 Oct 2015 17:28:09 +0200

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Fri, 23 Oct 2015 17:28:09 +0200
changeset 104
a8acbb12f27c
parent 103
d3b514e2ddbd
child 105
63d9051fe35c

implemented range requests

src/server/daemon/http.c file | annotate | diff | comparison | revisions
src/server/daemon/protocol.c file | annotate | diff | comparison | revisions
src/server/public/nsapi.h file | annotate | diff | comparison | revisions
src/server/safs/service.c file | annotate | diff | comparison | revisions
src/server/safs/service.h file | annotate | diff | comparison | revisions
src/server/util/io.c file | annotate | diff | comparison | revisions
src/server/util/util.c file | annotate | diff | comparison | revisions
src/server/util/util.h file | annotate | diff | comparison | revisions
--- a/src/server/daemon/http.c	Sat Oct 17 23:05:23 2015 +0200
+++ b/src/server/daemon/http.c	Fri Oct 23 17:28:09 2015 +0200
@@ -138,12 +138,10 @@
     header = pblock_findkeyval(pb_key_if_unmodified_since, rq->headers);
     if (header) {
         if (mtm && !util_later_than(mtm, header)) {
-            //PRTime temptime;
-            //PRStatus status = PR_ParseTimeString(header, PR_TRUE, &temptime);
-            //if (status == PR_SUCCESS) {
-            //    http_status(sn, rq, PROTOCOL_PRECONDITION_FAIL, NULL);
-            //    return REQ_ABORTED;
-            //}
+            if(util_isdate(header)) {
+                protocol_status(sn, rq, PROTOCOL_PRECONDITION_FAIL, NULL);
+                return REQ_ABORTED;
+            }
         }
     }
 
@@ -173,6 +171,26 @@
             return REQ_ABORTED;
         }
     }
+    
+    /* If-range */
+    header = pblock_findkeyval(pb_key_if_range, rq->headers);
+    char *range = pblock_findkeyval(pb_key_range, rq->headers);
+    if (range && header) {
+        int rmir = PR_FALSE;
+        if (util_isdate(header)) {
+            if (mtm && !util_later_than(mtm, header)) {
+                rmir = PR_TRUE;
+            }
+        } else {
+            if (!http_match_etag(header, etag, PR_TRUE)) {
+                rmir = PR_TRUE;
+            }
+        }
+        
+        if(rmir) {
+            pblock_removekey(pb_key_range, rq->headers);
+        }
+    }
 
     return REQ_PROCEED;
 }
--- a/src/server/daemon/protocol.c	Sat Oct 17 23:05:23 2015 +0200
+++ b/src/server/daemon/protocol.c	Fri Oct 23 17:28:09 2015 +0200
@@ -305,9 +305,6 @@
 
     // add the http status line to the output buffer
     add_http_status_line(out, sn->pool, rq);
-
-    // add server header
-    sbuf_write(out, "Server: webserver\r\n", 19);
     
     // add date header
     struct tm mtms;
@@ -318,6 +315,9 @@
     sbuf_write(out, date, strlen(date));
     sbuf_write(out, "\r\n", 2);
     
+    // add server header
+    sbuf_write(out, "Server: webserver\r\n", 19);
+    
     // check content length ans transfer encoding
     char *ctlen = pblock_findkeyval(pb_key_content_length, rq->srvhdrs);
     char *enc = pblock_findkeyval(pb_key_transfer_encoding, rq->srvhdrs);
--- a/src/server/public/nsapi.h	Sat Oct 17 23:05:23 2015 +0200
+++ b/src/server/public/nsapi.h	Fri Oct 23 17:28:09 2015 +0200
@@ -530,7 +530,7 @@
 typedef struct sendfiledata sendfiledata;
 struct sendfiledata {
     SYS_FILE fd;         /* file to send */
-    size_t offset;       /* offset in file to start sending from */
+    off_t  offset;       /* offset in file to start sending from */
     size_t len;          /* number of bytes to send from file */
     const void *header;  /* data to send before file */
     int hlen;            /* number of bytes to send before file */
--- a/src/server/safs/service.c	Sat Oct 17 23:05:23 2015 +0200
+++ b/src/server/safs/service.c	Fri Oct 23 17:28:09 2015 +0200
@@ -33,27 +33,29 @@
 #include "service.h"
 #include "../util/io.h"
 #include "../util/pblock.h"
+#include "../util/util.h"
 #include "../daemon/protocol.h"
 #include "../daemon/vfs.h"
 
 //include <sys/sendfile.h>
 #include "../util/strbuf.h"
+#include <ucx/string.h>
+#include <ucx/utils.h>
 
 #include <errno.h>
 
 
 /*
- * prepares for servicing a file
+ * prepares servicing a file
  *
- * adds content-length header and starts the response
+ * adds content-length header
  *
  * return the opened file
  */
-SYS_FILE prepare_service_file(Session *sn, Request *rq, struct stat *s) {
+SYS_FILE prepare_service_file(Session *sn, Request *rq, VFSContext *vfs, struct stat *s) {
     char *ppath = pblock_findkeyval(pb_key_ppath, rq->vars);
 
     // open the file
-    VFSContext *vfs = vfs_request_context(sn, rq);
     SYS_FILE fd = vfs_open(vfs, ppath, O_RDONLY);
     if(!fd) {
         // vfs_open sets http status code
@@ -89,39 +91,353 @@
         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);
-    http_start_response(sn, rq);
 
     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);
+    
+    return 0;
+}
+
 int send_file(pblock *pb, Session *sn, Request *rq) {
     struct stat s;
-    SYS_FILE fd = prepare_service_file(sn, rq, &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;
     }
     
-    if(!S_ISDIR(s.st_mode)) {
-        // send file
-        sendfiledata sfd;
-        sfd.fd = fd;
-        sfd.len = s.st_size;
-        sfd.offset = 0;
-        sfd.header = NULL;
-        sfd.trailer = NULL;
-        net_sendfile(sn->csd, &sfd);
-    } // else: status 302 set by prepare_service_file
+    // 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);
--- a/src/server/safs/service.h	Sat Oct 17 23:05:23 2015 +0200
+++ b/src/server/safs/service.h	Fri Oct 23 17:28:09 2015 +0200
@@ -35,6 +35,15 @@
 extern "C" {
 #endif
 
+typedef struct HttpRange HttpRange; 
+   
+struct HttpRange {
+    off_t begin;
+    off_t end;
+    HttpRange *next;
+};
+    
+    
 int send_file(pblock *pb, Session *sn, Request *rq);
 
 int service_hello(pblock *pb, Session *sn, Request *rq);
--- a/src/server/util/io.c	Sat Oct 17 23:05:23 2015 +0200
+++ b/src/server/util/io.c	Fri Oct 23 17:28:09 2015 +0200
@@ -136,8 +136,7 @@
     return r;
 }
 
-ssize_t net_stream_sendfile(NetIOStream *st, sendfiledata *sfd) {
-    // TODO: header and trailer
+ssize_t net_stream_sendfile(NetIOStream *st, sendfiledata *sfd) {  
     ssize_t ret = 0;
     off_t fileoffset = sfd->offset;
     if(sfd->fd->fd != -1) {
@@ -168,7 +167,13 @@
                 0);
 #endif
 #else // Solaris/Linux
-        ret = sendfile(st->fd, sfd->fd->fd, &fileoffset, sfd->len);
+        if(sfd->header) {
+            ret += write(st->fd, sfd->header, sfd->hlen);
+        }
+        ret += sendfile(st->fd, sfd->fd->fd, &fileoffset, sfd->len);
+        if(sfd->trailer) {
+            ret += write(st->fd, sfd->trailer, sfd->tlen);
+        }
 #endif
     } else {
         // TODO: regular copy
--- a/src/server/util/util.c	Sat Oct 17 23:05:23 2015 +0200
+++ b/src/server/util/util.c	Fri Oct 23 17:28:09 2015 +0200
@@ -906,3 +906,70 @@
     return gmtime_r(clock, res);
 }
 
+int util_isdate(char *str) {
+    sstr_t datestr = sstr(str);  
+    sstr_t example = S("Sun, 06 Nov 1994 08:49:37 GMT");
+    
+    if(datestr.length != example.length) {
+        return 0;
+    }
+    
+    for(int i=0;i<datestr.length;i++) {
+        char e = example.ptr[i];
+        if(isdigit(e)) {
+            if(!isdigit(datestr.ptr[i])) {
+                return 0;
+            }
+        } else if(e == ' ') {
+            if(datestr.ptr[i] != ' ') {
+                return 0;
+            }
+        } else if(e == ',') {
+            if(datestr.ptr[i] != ',') {
+                return 0;
+            }
+        } else if(e == ':') {
+            if(datestr.ptr[i] != ':') {
+                return 0;
+            }
+        }
+    }
+    
+    if(!sstrsuffix(datestr, S("GMT"))) {
+        return 0;
+    }
+    
+    return 1;
+}
+
+
+/* ------------------------- util_mime_separator -------------------------- */
+
+
+NSAPI_PUBLIC int util_mime_separator(char *sep)
+{
+    int size = 35; // documented in nsapi.h
+    int pos = 0;
+
+    sep[pos++] = CR;
+    sep[pos++] = LF;
+    sep[pos++] = '-';
+    sep[pos++] = '-';
+    
+    int r[6];
+    for(int i=0;i<6;i++) {
+        r[i] = rand() % 10000;
+    }
+    pos += snprintf(
+            sep+4,
+            size-4,
+            "X%04x%04x%04x%04x%04x%04xE",
+            r[0],
+            r[1],
+            r[2],
+            r[3],
+            r[4],
+            r[5]);
+
+    return pos;
+}
--- a/src/server/util/util.h	Sat Oct 17 23:05:23 2015 +0200
+++ b/src/server/util/util.h	Fri Oct 23 17:28:09 2015 +0200
@@ -236,6 +236,8 @@
 /* ucx utils */
 UcxAllocator util_pool_allocator(pool_handle_t *pool);
 
+int util_isdate(char *str);
+
 /* --- End common function prototypes --- */
 
 /* --- Begin Unix-only function prototypes --- */

mercurial