Range requests as defined in RFC7233 is required for resuming
interrupted http(s) downloads for example:
ftp -C http://foo.bar/install57.iso

With this diff, httpd parses "Range" header in the requests and
provide either 206(Partial Content) or 416(Range not Satisfiable)
responses with "Content-Range" header set appropriately. Further,
it understands multi range request and generate satisfiable payloads
with "multipart/byteranges" media type.

Suggestions/comments to improve the diff are welcome.

Note, "If-Range" isn't implemented yet.

Index: server_file.c
===================================================================
RCS file: /cvs/src/usr.sbin/httpd/server_file.c,v
retrieving revision 1.51
diff -u -p -r1.51 server_file.c
--- server_file.c       12 Feb 2015 10:05:29 -0000      1.51
+++ server_file.c       17 Apr 2015 02:22:12 -0000
@@ -36,12 +36,23 @@
 
 #define MINIMUM(a, b)  (((a) < (b)) ? (a) : (b))
 #define MAXIMUM(a, b)  (((a) > (b)) ? (a) : (b))
+#define MAX_RANGES     4
+
+struct range {
+       off_t   start;
+       off_t   end;
+};
 
 int     server_file_access(struct httpd *, struct client *, char *, size_t);
 int     server_file_request(struct httpd *, struct client *, char *,
            struct stat *);
+int     server_partial_file_request(struct httpd *, struct client *, char *,
+           struct stat *, char *);
 int     server_file_index(struct httpd *, struct client *, struct stat *);
 int     server_file_method(struct client *);
+int     parse_range_spec(char *, size_t, struct range *);
+struct range *parse_range(char *, size_t, int *);
+int     buffer_add_range(int, struct evbuffer *, struct range *);
 
 int
 server_file_access(struct httpd *env, struct client *clt,
@@ -50,6 +61,7 @@ server_file_access(struct httpd *env, st
        struct http_descriptor  *desc = clt->clt_descreq;
        struct server_config    *srv_conf = clt->clt_srv_conf;
        struct stat              st;
+       struct kv               *r, key;
        char                    *newpath;
        int                      ret;
 
@@ -123,7 +135,13 @@ server_file_access(struct httpd *env, st
                goto fail;
        }
 
-       return (server_file_request(env, clt, path, &st));
+       key.kv_key = "Range";
+       r = kv_find(&desc->http_headers, &key);
+       if (r != NULL)
+               return (server_partial_file_request(env, clt, path, &st,
+                   r->kv_value));
+       else
+               return (server_file_request(env, clt, path, &st));
 
  fail:
        switch (errno) {
@@ -262,6 +280,138 @@ server_file_request(struct httpd *env, s
 }
 
 int
+server_partial_file_request(struct httpd *env, struct client *clt, char *path,
+    struct stat *st, char *range_str)
+{
+       struct http_descriptor  *resp = clt->clt_descresp;
+       struct media_type       *media, multipart_media;
+       struct range            *range;
+       struct evbuffer         *evb = NULL;
+       size_t                   content_length;
+       int                      code = 500, fd = -1, i, nranges, ret;
+       char                     content_range[64];
+       const char              *errstr = NULL;
+       uint32_t                 boundary;
+
+       if ((range = parse_range(range_str, st->st_size, &nranges)) == NULL) {
+               code = 416;
+               (void)snprintf(content_range, sizeof(content_range),
+                   "bytes */%lld", st->st_size);
+               errstr = content_range;
+               goto abort;
+       }
+
+       /* Now open the file, should be readable or we have another problem */
+       if ((fd = open(path, O_RDONLY)) == -1)
+               goto abort;
+
+       media = media_find(env->sc_mediatypes, path);
+       if ((evb = evbuffer_new()) == NULL) {
+               errstr = "failed to allocate file buffer";
+               goto abort;
+       }
+
+       if (nranges == 1) {
+               (void)snprintf(content_range, sizeof(content_range),
+                   "bytes %lld-%lld/%lld", range->start, range->end,
+                   st->st_size);
+               if (kv_add(&resp->http_headers, "Content-Range",
+                   content_range) == NULL)
+                       goto abort;
+
+               content_length = range->end - range->start + 1;
+               if (buffer_add_range(fd, evb, range) == 0)
+                       goto abort;
+
+       } else {
+               content_length = 0;
+               boundary = arc4random();
+               /* Generate a multipart payload of byteranges */
+               while (nranges--) {
+                       if ((i = evbuffer_add_printf(evb, "\r\n--%ud\r\n",
+                           boundary)) == -1)
+                               goto abort;
+
+                       content_length += i;
+                       if ((i = evbuffer_add_printf(evb,
+                           "Content-Type: %s/%s\r\n",
+                           media == NULL ? "application" : media->media_type,
+                           media == NULL ?
+                           "octet-stream" : media->media_subtype)) == -1)
+                               goto abort;
+
+                       content_length += i;
+                       if ((i = evbuffer_add_printf(evb,
+                           "Content-Range: bytes %lld-%lld/%lld\r\n\r\n",
+                           range->start, range->end, st->st_size)) == -1)
+                               goto abort;
+                       
+                       content_length += i;
+                       if (buffer_add_range(fd, evb, range) == 0)
+                               goto abort;
+
+                       content_length += range->end - range->start + 1;
+                       range++;
+               }
+
+               if ((i = evbuffer_add_printf(evb, "\r\n--%ud--\r\n",
+                   boundary)) == -1)
+                       goto abort;
+
+               content_length += i;
+
+               /* prepare multipart/byteranges media type */
+               (void)strlcpy(multipart_media.media_type, "multipart",
+                   sizeof(multipart_media.media_type));
+               (void)snprintf(multipart_media.media_subtype,
+                   sizeof(multipart_media.media_subtype),
+                   "byteranges; boundary=%ud", boundary);
+               media = &multipart_media;
+       }
+
+       ret = server_response_http(clt, 206, media, content_length,
+           MINIMUM(time(NULL), st->st_mtim.tv_sec));
+       switch (ret) {
+       case -1:
+               goto fail;
+       case 0:
+               /* Connection is already finished */
+               close(fd);
+               evbuffer_free(evb);
+               evb = NULL;
+               goto done;
+       default:
+               break;
+       }
+
+       if (server_bufferevent_write_buffer(clt, evb) == -1)
+               goto fail;
+
+       evbuffer_free(evb);
+       evb = NULL;
+
+       bufferevent_enable(clt->clt_bev, EV_READ|EV_WRITE);
+       if (clt->clt_persist)
+               clt->clt_toread = TOREAD_HTTP_HEADER;
+       else
+               clt->clt_toread = TOREAD_HTTP_NONE;
+       clt->clt_done = 0;
+
+ done:
+       server_reset_http(clt);
+       return (0);
+ fail:
+       bufferevent_disable(clt->clt_bev, EV_READ|EV_WRITE);
+       bufferevent_free(clt->clt_bev);
+       clt->clt_bev = NULL;
+ abort:
+       if (errstr == NULL)
+               errstr = strerror(errno);
+       server_abort_http(clt, code, errstr);
+       return (-1);
+}
+
+int
 server_file_index(struct httpd *env, struct client *clt, struct stat *st)
 {
        char                      path[PATH_MAX];
@@ -467,3 +617,112 @@ server_file_error(struct bufferevent *be
        server_close(clt, "unknown event error");
        return;
 }
+
+struct range *
+parse_range(char *str, size_t file_sz, int *nranges)
+{
+       static struct range      ranges[MAX_RANGES];
+       char                    *p, *q;
+       int                      i = 0;
+
+       /* Extract range unit */
+       if ((p = strchr(str, '=')) == NULL)
+               return (NULL);
+
+       *p++ = '\0';
+       /* Check if it's a bytes range spec */
+       if (strcmp(str, "bytes") != 0)
+               return (NULL);
+
+       while ((q = strchr(p, ',')) != NULL) {
+               *q++ = '\0';
+
+               /* Extract start and end positions */
+               if (parse_range_spec(p, file_sz, &ranges[i]) == 0)
+                       continue;
+
+               i += 1;
+               if (i == MAX_RANGES)
+                       return (NULL);
+
+               p = q;
+       }
+
+       if (p != NULL && parse_range_spec(p, file_sz, &ranges[i]) != 0)
+               i += 1;
+
+       *nranges = i;
+       return (i ? ranges : NULL);
+}
+
+int
+parse_range_spec(char *str, size_t size, struct range *r)
+{
+       size_t           start_str_len, end_str_len;
+       char            *p, *start_str, *end_str;
+       const char      *errstr;
+
+       if ((p = strchr(str, '-')) == NULL)
+               return (0);
+
+       *p++ = '\0';
+       start_str = str;
+       end_str = p;
+       start_str_len = strlen(start_str);
+       end_str_len = strlen(end_str);
+
+       /* Either 'start' or 'end' is optional but not both */
+       if ((start_str_len == 0) && (end_str_len == 0))
+               return (0);
+
+       if (end_str_len) {
+               r->end = strtonum(end_str, 0, LLONG_MAX, &errstr);
+               if (errstr)
+                       return (0);
+
+               if ((size_t)r->end >= size)
+                       r->end = size - 1;
+       } else
+               r->end = size - 1;
+
+       if (start_str_len) {
+               r->start = strtonum(start_str, 0, LLONG_MAX, &errstr);
+               if (errstr)
+                       return (0);
+
+               if ((size_t)r->start >= size)
+                       return (0);
+       } else {
+               r->start = size - r->end;
+               r->end = size - 1;
+       }
+
+       if (r->end < r->start)
+               return (0);
+
+       return (1);
+}
+
+int
+buffer_add_range(int fd, struct evbuffer *evb, struct range *range)
+{
+       char    buf[BUFSIZ];
+       size_t  n, range_sz;
+       ssize_t nread;
+
+       if (lseek(fd, range->start, SEEK_SET) == -1)
+               return (0);
+
+       range_sz = range->end - range->start + 1;
+       while (range_sz) {
+               n = MINIMUM(range_sz, sizeof(buf));
+               if ((nread = read(fd, buf, n)) == -1)
+                       return (0);
+
+               evbuffer_add(evb, buf, nread);
+               range_sz -= nread;
+       }
+       
+       return (1);
+}
+
Index: server_http.c
===================================================================
RCS file: /cvs/src/usr.sbin/httpd/server_http.c,v
retrieving revision 1.77
diff -u -p -r1.77 server_http.c
--- server_http.c       9 Apr 2015 16:48:29 -0000       1.77
+++ server_http.c       17 Apr 2015 02:22:12 -0000
@@ -787,6 +787,13 @@ server_abort_http(struct client *clt, u_
                        extraheader = NULL;
                }
                break;
+       case 416:
+               if (asprintf(&extraheader,
+                   "Content-Range: %s\r\n", msg) == -1) {
+                       code = 500;
+                       extraheader = NULL;
+               }
+               break;
        default:
                /*
                 * Do not send details of the error.  Traditionally,
@@ -1177,6 +1184,11 @@ server_response_http(struct client *clt,
        /* Date header is mandatory and should be added as late as possible */
        if (server_http_time(time(NULL), tmbuf, sizeof(tmbuf)) <= 0 ||
            kv_add(&resp->http_headers, "Date", tmbuf) == NULL)
+               return (-1);
+
+       /* Accept byte ranges */
+       if (code == 200 &&
+           kv_add(&resp->http_headers, "Accept-Ranges", "bytes") == NULL)
                return (-1);
 
        /* Write completed header */

Reply via email to