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 */