xref: /openbsd-src/usr.sbin/httpd/server_file.c (revision 0b7734b3d77bb9b21afec6f4621cae6c805dbd45)
1 /*	$OpenBSD: server_file.c,v 1.62 2016/05/17 03:12:39 deraadt Exp $	*/
2 
3 /*
4  * Copyright (c) 2006 - 2015 Reyk Floeter <reyk@openbsd.org>
5  *
6  * Permission to use, copy, modify, and distribute this software for any
7  * purpose with or without fee is hereby granted, provided that the above
8  * copyright notice and this permission notice appear in all copies.
9  *
10  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17  */
18 
19 #include <sys/types.h>
20 #include <sys/time.h>
21 #include <sys/stat.h>
22 
23 #include <limits.h>
24 #include <errno.h>
25 #include <fcntl.h>
26 #include <stdlib.h>
27 #include <string.h>
28 #include <unistd.h>
29 #include <stdio.h>
30 #include <dirent.h>
31 #include <time.h>
32 #include <event.h>
33 
34 #include "httpd.h"
35 #include "http.h"
36 
37 #define MINIMUM(a, b)	(((a) < (b)) ? (a) : (b))
38 #define MAXIMUM(a, b)	(((a) > (b)) ? (a) : (b))
39 #define MAX_RANGES	4
40 
41 struct range {
42 	off_t	start;
43 	off_t	end;
44 };
45 
46 int		 server_file_access(struct httpd *, struct client *,
47 		    char *, size_t);
48 int		 server_file_request(struct httpd *, struct client *,
49 		    char *, struct stat *);
50 int		 server_partial_file_request(struct httpd *, struct client *,
51 		    char *, struct stat *, char *);
52 int		 server_file_index(struct httpd *, struct client *,
53 		    struct stat *);
54 int		 server_file_modified_since(struct http_descriptor *,
55 		    struct stat *);
56 int		 server_file_method(struct client *);
57 int		 parse_range_spec(char *, size_t, struct range *);
58 struct range	*parse_range(char *, size_t, int *);
59 int		 buffer_add_range(int, struct evbuffer *, struct range *);
60 
61 int
62 server_file_access(struct httpd *env, struct client *clt,
63     char *path, size_t len)
64 {
65 	struct http_descriptor	*desc = clt->clt_descreq;
66 	struct server_config	*srv_conf = clt->clt_srv_conf;
67 	struct stat		 st;
68 	struct kv		*r, key;
69 	char			*newpath, *encodedpath;
70 	int			 ret;
71 
72 	errno = 0;
73 
74 	if (access(path, R_OK) == -1) {
75 		goto fail;
76 	} else if (stat(path, &st) == -1) {
77 		goto fail;
78 	} else if (S_ISDIR(st.st_mode)) {
79 		/* Deny access if directory indexing is disabled */
80 		if (srv_conf->flags & SRVFLAG_NO_INDEX) {
81 			errno = EACCES;
82 			goto fail;
83 		}
84 
85 		if (desc->http_path_alias != NULL) {
86 			/* Recursion - the index "file" is a directory? */
87 			errno = EINVAL;
88 			goto fail;
89 		}
90 
91 		/* Redirect to path with trailing "/" */
92 		if (path[strlen(path) - 1] != '/') {
93 			if ((encodedpath = url_encode(desc->http_path)) == NULL)
94 				return (500);
95 			if (asprintf(&newpath, "http%s://%s%s/",
96 			    srv_conf->flags & SRVFLAG_TLS ? "s" : "",
97 			    desc->http_host, encodedpath) == -1) {
98 				free(encodedpath);
99 				return (500);
100 			}
101 			free(encodedpath);
102 
103 			/* Path alias will be used for the redirection */
104 			desc->http_path_alias = newpath;
105 
106 			/* Indicate that the file has been moved */
107 			return (301);
108 		}
109 
110 		/* Append the default index file to the location */
111 		if (asprintf(&newpath, "%s%s", desc->http_path,
112 		    srv_conf->index) == -1)
113 			return (500);
114 		desc->http_path_alias = newpath;
115 		if (server_getlocation(clt, newpath) != srv_conf) {
116 			/* The location has changed */
117 			return (server_file(env, clt));
118 		}
119 
120 		/* Otherwise append the default index file to the path */
121 		if (strlcat(path, srv_conf->index, len) >= len) {
122 			errno = EACCES;
123 			goto fail;
124 		}
125 
126 		ret = server_file_access(env, clt, path, len);
127 		if (ret == 404) {
128 			/*
129 			 * Index file not found; fail if auto-indexing is
130 			 * not enabled, otherwise return success but
131 			 * indicate directory with S_ISDIR of the previous
132 			 * stat.
133 			 */
134 			if ((srv_conf->flags & SRVFLAG_AUTO_INDEX) == 0) {
135 				errno = EACCES;
136 				goto fail;
137 			}
138 
139 			return (server_file_index(env, clt, &st));
140 		}
141 		return (ret);
142 	} else if (!S_ISREG(st.st_mode)) {
143 		/* Don't follow symlinks and ignore special files */
144 		errno = EACCES;
145 		goto fail;
146 	}
147 
148 	key.kv_key = "Range";
149 	r = kv_find(&desc->http_headers, &key);
150 	if (r != NULL)
151 		return (server_partial_file_request(env, clt, path, &st,
152 		    r->kv_value));
153 	else
154 		return (server_file_request(env, clt, path, &st));
155 
156  fail:
157 	switch (errno) {
158 	case ENOENT:
159 	case ENOTDIR:
160 		return (404);
161 	case EACCES:
162 		return (403);
163 	default:
164 		return (500);
165 	}
166 
167 	/* NOTREACHED */
168 }
169 
170 int
171 server_file(struct httpd *env, struct client *clt)
172 {
173 	struct http_descriptor	*desc = clt->clt_descreq;
174 	struct server_config	*srv_conf = clt->clt_srv_conf;
175 	char			 path[PATH_MAX];
176 	const char		*stripped, *errstr = NULL;
177 	int			 ret = 500;
178 
179 	if (srv_conf->flags & SRVFLAG_FCGI)
180 		return (server_fcgi(env, clt));
181 
182 	/* Request path is already canonicalized */
183 	stripped = server_root_strip(
184 	    desc->http_path_alias != NULL ?
185 	    desc->http_path_alias : desc->http_path,
186 	    srv_conf->strip);
187 	if ((size_t)snprintf(path, sizeof(path), "%s%s",
188 	    srv_conf->root, stripped) >= sizeof(path)) {
189 		errstr = desc->http_path;
190 		goto abort;
191 	}
192 
193 	/* Returns HTTP status code on error */
194 	if ((ret = server_file_access(env, clt, path, sizeof(path))) > 0) {
195 		errstr = desc->http_path_alias != NULL ?
196 		    desc->http_path_alias : desc->http_path;
197 		goto abort;
198 	}
199 
200 	return (ret);
201 
202  abort:
203 	if (errstr == NULL)
204 		errstr = strerror(errno);
205 	server_abort_http(clt, ret, errstr);
206 	return (-1);
207 }
208 
209 int
210 server_file_method(struct client *clt)
211 {
212 	struct http_descriptor	*desc = clt->clt_descreq;
213 
214 	switch (desc->http_method) {
215 	case HTTP_METHOD_GET:
216 	case HTTP_METHOD_HEAD:
217 		return (0);
218 	default:
219 		/* Other methods are not allowed */
220 		errno = EACCES;
221 		return (405);
222 	}
223 	/* NOTREACHED */
224 }
225 
226 int
227 server_file_request(struct httpd *env, struct client *clt, char *path,
228     struct stat *st)
229 {
230 	struct server_config	*srv_conf = clt->clt_srv_conf;
231 	struct media_type	*media;
232 	const char		*errstr = NULL;
233 	int			 fd = -1, ret, code = 500;
234 
235 	if ((ret = server_file_method(clt)) != 0) {
236 		code = ret;
237 		goto abort;
238 	}
239 
240 	if ((ret = server_file_modified_since(clt->clt_descreq, st)) != -1)
241 		return (ret);
242 
243 	/* Now open the file, should be readable or we have another problem */
244 	if ((fd = open(path, O_RDONLY)) == -1)
245 		goto abort;
246 
247 	media = media_find_config(env, srv_conf, path);
248 	ret = server_response_http(clt, 200, media, st->st_size,
249 	    MINIMUM(time(NULL), st->st_mtim.tv_sec));
250 	switch (ret) {
251 	case -1:
252 		goto fail;
253 	case 0:
254 		/* Connection is already finished */
255 		close(fd);
256 		goto done;
257 	default:
258 		break;
259 	}
260 
261 	clt->clt_fd = fd;
262 	if (clt->clt_srvbev != NULL)
263 		bufferevent_free(clt->clt_srvbev);
264 
265 	clt->clt_srvbev_throttled = 0;
266 	clt->clt_srvbev = bufferevent_new(clt->clt_fd, server_read,
267 	    server_write, server_file_error, clt);
268 	if (clt->clt_srvbev == NULL) {
269 		errstr = "failed to allocate file buffer event";
270 		goto fail;
271 	}
272 
273 	/* Adjust read watermark to the socket output buffer size */
274 	bufferevent_setwatermark(clt->clt_srvbev, EV_READ, 0,
275 	    clt->clt_sndbufsiz);
276 
277 	bufferevent_settimeout(clt->clt_srvbev,
278 	    srv_conf->timeout.tv_sec, srv_conf->timeout.tv_sec);
279 	bufferevent_enable(clt->clt_srvbev, EV_READ);
280 	bufferevent_disable(clt->clt_bev, EV_READ);
281 
282  done:
283 	server_reset_http(clt);
284 	return (0);
285  fail:
286 	bufferevent_disable(clt->clt_bev, EV_READ|EV_WRITE);
287 	bufferevent_free(clt->clt_bev);
288 	clt->clt_bev = NULL;
289  abort:
290 	if (fd != -1)
291 		close(fd);
292 	if (errstr == NULL)
293 		errstr = strerror(errno);
294 	server_abort_http(clt, code, errstr);
295 	return (-1);
296 }
297 
298 int
299 server_partial_file_request(struct httpd *env, struct client *clt, char *path,
300     struct stat *st, char *range_str)
301 {
302 	struct server_config	*srv_conf = clt->clt_srv_conf;
303 	struct http_descriptor	*resp = clt->clt_descresp;
304 	struct http_descriptor	*desc = clt->clt_descreq;
305 	struct media_type	*media, multipart_media;
306 	struct range		*range;
307 	struct evbuffer		*evb = NULL;
308 	size_t			 content_length;
309 	int			 code = 500, fd = -1, i, nranges, ret;
310 	uint32_t		 boundary;
311 	char			 content_range[64];
312 	const char		*errstr = NULL;
313 
314 	/* Ignore range request for methods other than GET */
315 	if (desc->http_method != HTTP_METHOD_GET)
316 		return server_file_request(env, clt, path, st);
317 
318 	if ((range = parse_range(range_str, st->st_size, &nranges)) == NULL) {
319 		code = 416;
320 		(void)snprintf(content_range, sizeof(content_range),
321 		    "bytes */%lld", st->st_size);
322 		errstr = content_range;
323 		goto abort;
324 	}
325 
326 	/* Now open the file, should be readable or we have another problem */
327 	if ((fd = open(path, O_RDONLY)) == -1)
328 		goto abort;
329 
330 	media = media_find_config(env, srv_conf, path);
331 	if ((evb = evbuffer_new()) == NULL) {
332 		errstr = "failed to allocate file buffer";
333 		goto abort;
334 	}
335 
336 	if (nranges == 1) {
337 		(void)snprintf(content_range, sizeof(content_range),
338 		    "bytes %lld-%lld/%lld", range->start, range->end,
339 		    st->st_size);
340 		if (kv_add(&resp->http_headers, "Content-Range",
341 		    content_range) == NULL)
342 			goto abort;
343 
344 		content_length = range->end - range->start + 1;
345 		if (buffer_add_range(fd, evb, range) == 0)
346 			goto abort;
347 
348 	} else {
349 		content_length = 0;
350 		boundary = arc4random();
351 		/* Generate a multipart payload of byteranges */
352 		while (nranges--) {
353 			if ((i = evbuffer_add_printf(evb, "\r\n--%ud\r\n",
354 			    boundary)) == -1)
355 				goto abort;
356 
357 			content_length += i;
358 			if ((i = evbuffer_add_printf(evb,
359 			    "Content-Type: %s/%s\r\n",
360 			    media->media_type, media->media_subtype)) == -1)
361 				goto abort;
362 
363 			content_length += i;
364 			if ((i = evbuffer_add_printf(evb,
365 			    "Content-Range: bytes %lld-%lld/%lld\r\n\r\n",
366 			    range->start, range->end, st->st_size)) == -1)
367 				goto abort;
368 
369 			content_length += i;
370 			if (buffer_add_range(fd, evb, range) == 0)
371 				goto abort;
372 
373 			content_length += range->end - range->start + 1;
374 			range++;
375 		}
376 
377 		if ((i = evbuffer_add_printf(evb, "\r\n--%ud--\r\n",
378 		    boundary)) == -1)
379 			goto abort;
380 
381 		content_length += i;
382 
383 		/* prepare multipart/byteranges media type */
384 		(void)strlcpy(multipart_media.media_type, "multipart",
385 		    sizeof(multipart_media.media_type));
386 		(void)snprintf(multipart_media.media_subtype,
387 		    sizeof(multipart_media.media_subtype),
388 		    "byteranges; boundary=%ud", boundary);
389 		media = &multipart_media;
390 	}
391 
392 	close(fd);
393 	fd = -1;
394 
395 	ret = server_response_http(clt, 206, media, content_length,
396 	    MINIMUM(time(NULL), st->st_mtim.tv_sec));
397 	switch (ret) {
398 	case -1:
399 		goto fail;
400 	case 0:
401 		/* Connection is already finished */
402 		evbuffer_free(evb);
403 		evb = NULL;
404 		goto done;
405 	default:
406 		break;
407 	}
408 
409 	if (server_bufferevent_write_buffer(clt, evb) == -1)
410 		goto fail;
411 
412 	evbuffer_free(evb);
413 	evb = NULL;
414 
415 	bufferevent_enable(clt->clt_bev, EV_READ|EV_WRITE);
416 	if (clt->clt_persist)
417 		clt->clt_toread = TOREAD_HTTP_HEADER;
418 	else
419 		clt->clt_toread = TOREAD_HTTP_NONE;
420 	clt->clt_done = 0;
421 
422  done:
423 	server_reset_http(clt);
424 	return (0);
425  fail:
426 	bufferevent_disable(clt->clt_bev, EV_READ|EV_WRITE);
427 	bufferevent_free(clt->clt_bev);
428 	clt->clt_bev = NULL;
429  abort:
430 	if (fd != -1)
431 		close(fd);
432 	if (errstr == NULL)
433 		errstr = strerror(errno);
434 	server_abort_http(clt, code, errstr);
435 	return (-1);
436 }
437 
438 int
439 server_file_index(struct httpd *env, struct client *clt, struct stat *st)
440 {
441 	char			  path[PATH_MAX];
442 	char			  tmstr[21];
443 	struct http_descriptor	 *desc = clt->clt_descreq;
444 	struct server_config	 *srv_conf = clt->clt_srv_conf;
445 	struct dirent		**namelist, *dp;
446 	int			  namesize, i, ret, fd = -1, namewidth, skip;
447 	int			  code = 500;
448 	struct evbuffer		 *evb = NULL;
449 	struct media_type	 *media;
450 	const char		 *stripped, *style;
451 	char			 *escapeduri, *escapedhtml, *escapedpath;
452 	struct tm		  tm;
453 	time_t			  t, dir_mtime;
454 
455 	if ((ret = server_file_method(clt)) != 0) {
456 		code = ret;
457 		goto abort;
458 	}
459 
460 	/* Request path is already canonicalized */
461 	stripped = server_root_strip(desc->http_path, srv_conf->strip);
462 	if ((size_t)snprintf(path, sizeof(path), "%s%s",
463 	    srv_conf->root, stripped) >= sizeof(path))
464 		goto abort;
465 
466 	/* Now open the file, should be readable or we have another problem */
467 	if ((fd = open(path, O_RDONLY)) == -1)
468 		goto abort;
469 
470 	/* Save last modification time */
471 	dir_mtime = MINIMUM(time(NULL), st->st_mtim.tv_sec);
472 
473 	if ((evb = evbuffer_new()) == NULL)
474 		goto abort;
475 
476 	if ((namesize = scandir(path, &namelist, NULL, alphasort)) == -1)
477 		goto abort;
478 
479 	/* Indicate failure but continue going through the list */
480 	skip = 0;
481 
482 	if ((escapedpath = escape_html(desc->http_path)) == NULL)
483 		goto fail;
484 
485 	/* A CSS stylesheet allows minimal customization by the user */
486 	style = "body { background-color: white; color: black; font-family: "
487 	    "sans-serif; }\nhr { border: 0; border-bottom: 1px dashed; }\n";
488 	/* Generate simple HTML index document */
489 	if (evbuffer_add_printf(evb,
490 	    "<!DOCTYPE html>\n"
491 	    "<html>\n"
492 	    "<head>\n"
493 	    "<meta http-equiv=\"Content-Type\" content=\"text/html; "
494 	    "charset=utf-8\"/>\n"
495 	    "<title>Index of %s</title>\n"
496 	    "<style type=\"text/css\"><!--\n%s\n--></style>\n"
497 	    "</head>\n"
498 	    "<body>\n"
499 	    "<h1>Index of %s</h1>\n"
500 	    "<hr>\n<pre>\n",
501 	    escapedpath, style, escapedpath) == -1)
502 		skip = 1;
503 
504 	free(escapedpath);
505 
506 	for (i = 0; i < namesize; i++) {
507 		dp = namelist[i];
508 
509 		if (skip ||
510 		    fstatat(fd, dp->d_name, st, 0) == -1) {
511 			free(dp);
512 			continue;
513 		}
514 
515 		t = st->st_mtime;
516 		localtime_r(&t, &tm);
517 		strftime(tmstr, sizeof(tmstr), "%d-%h-%Y %R", &tm);
518 		namewidth = 51 - strlen(dp->d_name);
519 
520 		if ((escapeduri = url_encode(dp->d_name)) == NULL)
521 			goto fail;
522 		if ((escapedhtml = escape_html(dp->d_name)) == NULL)
523 			goto fail;
524 
525 		if (dp->d_name[0] == '.' &&
526 		    !(dp->d_name[1] == '.' && dp->d_name[2] == '\0')) {
527 			/* ignore hidden files starting with a dot */
528 		} else if (S_ISDIR(st->st_mode)) {
529 			namewidth -= 1; /* trailing slash */
530 			if (evbuffer_add_printf(evb,
531 			    "<a href=\"%s%s/\">%s/</a>%*s%s%20s\n",
532 			    strchr(escapeduri, ':') != NULL ? "./" : "",
533 			    escapeduri, escapedhtml,
534 			    MAXIMUM(namewidth, 0), " ", tmstr, "-") == -1)
535 				skip = 1;
536 		} else if (S_ISREG(st->st_mode)) {
537 			if (evbuffer_add_printf(evb,
538 			    "<a href=\"%s%s\">%s</a>%*s%s%20llu\n",
539 			    strchr(escapeduri, ':') != NULL ? "./" : "",
540 			    escapeduri, escapedhtml,
541 			    MAXIMUM(namewidth, 0), " ",
542 			    tmstr, st->st_size) == -1)
543 				skip = 1;
544 		}
545 		free(escapeduri);
546 		free(escapedhtml);
547 		free(dp);
548 	}
549 	free(namelist);
550 
551 	if (skip ||
552 	    evbuffer_add_printf(evb,
553 	    "</pre>\n<hr>\n</body>\n</html>\n") == -1)
554 		goto abort;
555 
556 	close(fd);
557 	fd = -1;
558 
559 	media = media_find_config(env, srv_conf, "index.html");
560 	ret = server_response_http(clt, 200, media, EVBUFFER_LENGTH(evb),
561 	    dir_mtime);
562 	switch (ret) {
563 	case -1:
564 		goto fail;
565 	case 0:
566 		/* Connection is already finished */
567 		evbuffer_free(evb);
568 		goto done;
569 	default:
570 		break;
571 	}
572 
573 	if (server_bufferevent_write_buffer(clt, evb) == -1)
574 		goto fail;
575 	evbuffer_free(evb);
576 	evb = NULL;
577 
578 	bufferevent_enable(clt->clt_bev, EV_READ|EV_WRITE);
579 	if (clt->clt_persist)
580 		clt->clt_toread = TOREAD_HTTP_HEADER;
581 	else
582 		clt->clt_toread = TOREAD_HTTP_NONE;
583 	clt->clt_done = 0;
584 
585  done:
586 	server_reset_http(clt);
587 	return (0);
588  fail:
589 	bufferevent_disable(clt->clt_bev, EV_READ|EV_WRITE);
590 	bufferevent_free(clt->clt_bev);
591 	clt->clt_bev = NULL;
592  abort:
593 	if (fd != -1)
594 		close(fd);
595 	if (evb != NULL)
596 		evbuffer_free(evb);
597 	server_abort_http(clt, code, desc->http_path);
598 	return (-1);
599 }
600 
601 void
602 server_file_error(struct bufferevent *bev, short error, void *arg)
603 {
604 	struct client		*clt = arg;
605 	struct evbuffer		*dst;
606 
607 	if (error & EVBUFFER_TIMEOUT) {
608 		server_close(clt, "buffer event timeout");
609 		return;
610 	}
611 	if (error & EVBUFFER_ERROR) {
612 		if (errno == EFBIG) {
613 			bufferevent_enable(bev, EV_READ);
614 			return;
615 		}
616 		server_close(clt, "buffer event error");
617 		return;
618 	}
619 	if (error & (EVBUFFER_READ|EVBUFFER_WRITE|EVBUFFER_EOF)) {
620 		bufferevent_disable(bev, EV_READ|EV_WRITE);
621 
622 		clt->clt_done = 1;
623 
624 		if (clt->clt_persist) {
625 			/* Close input file and wait for next HTTP request */
626 			if (clt->clt_fd != -1)
627 				close(clt->clt_fd);
628 			clt->clt_fd = -1;
629 			clt->clt_toread = TOREAD_HTTP_HEADER;
630 			server_reset_http(clt);
631 			bufferevent_enable(clt->clt_bev, EV_READ|EV_WRITE);
632 			return;
633 		}
634 
635 		dst = EVBUFFER_OUTPUT(clt->clt_bev);
636 		if (EVBUFFER_LENGTH(dst)) {
637 			/* Finish writing all data first */
638 			bufferevent_enable(clt->clt_bev, EV_WRITE);
639 			return;
640 		}
641 
642 		server_close(clt, "done");
643 		return;
644 	}
645 	server_close(clt, "unknown event error");
646 	return;
647 }
648 
649 int
650 server_file_modified_since(struct http_descriptor *desc, struct stat *st)
651 {
652 	struct kv	 key, *since;
653 	struct tm	 tm;
654 
655 	key.kv_key = "If-Modified-Since";
656 	if ((since = kv_find(&desc->http_headers, &key)) != NULL &&
657 	    since->kv_value != NULL) {
658 		memset(&tm, 0, sizeof(struct tm));
659 
660 		/*
661 		 * Return "Not modified" if the file hasn't changed since
662 		 * the requested time.
663 		 */
664 		if (strptime(since->kv_value,
665 		    "%a, %d %h %Y %T %Z", &tm) != NULL &&
666 		    timegm(&tm) >= st->st_mtim.tv_sec)
667 			return (304);
668 	}
669 
670 	return (-1);
671 }
672 
673 struct range *
674 parse_range(char *str, size_t file_sz, int *nranges)
675 {
676 	static struct range	 ranges[MAX_RANGES];
677 	int			 i = 0;
678 	char			*p, *q;
679 
680 	/* Extract range unit */
681 	if ((p = strchr(str, '=')) == NULL)
682 		return (NULL);
683 
684 	*p++ = '\0';
685 	/* Check if it's a bytes range spec */
686 	if (strcmp(str, "bytes") != 0)
687 		return (NULL);
688 
689 	while ((q = strchr(p, ',')) != NULL) {
690 		*q++ = '\0';
691 
692 		/* Extract start and end positions */
693 		if (parse_range_spec(p, file_sz, &ranges[i]) == 0)
694 			continue;
695 
696 		i++;
697 		if (i == MAX_RANGES)
698 			return (NULL);
699 
700 		p = q;
701 	}
702 
703 	if (parse_range_spec(p, file_sz, &ranges[i]) != 0)
704 		i++;
705 
706 	*nranges = i;
707 	return (i ? ranges : NULL);
708 }
709 
710 int
711 parse_range_spec(char *str, size_t size, struct range *r)
712 {
713 	size_t		 start_str_len, end_str_len;
714 	char		*p, *start_str, *end_str;
715 	const char	*errstr;
716 
717 	if ((p = strchr(str, '-')) == NULL)
718 		return (0);
719 
720 	*p++ = '\0';
721 	start_str = str;
722 	end_str = p;
723 	start_str_len = strlen(start_str);
724 	end_str_len = strlen(end_str);
725 
726 	/* Either 'start' or 'end' is optional but not both */
727 	if ((start_str_len == 0) && (end_str_len == 0))
728 		return (0);
729 
730 	if (end_str_len) {
731 		r->end = strtonum(end_str, 0, LLONG_MAX, &errstr);
732 		if (errstr)
733 			return (0);
734 
735 		if ((size_t)r->end >= size)
736 			r->end = size - 1;
737 	} else
738 		r->end = size - 1;
739 
740 	if (start_str_len) {
741 		r->start = strtonum(start_str, 0, LLONG_MAX, &errstr);
742 		if (errstr)
743 			return (0);
744 
745 		if ((size_t)r->start >= size)
746 			return (0);
747 	} else {
748 		r->start = size - r->end;
749 		r->end = size - 1;
750 	}
751 
752 	if (r->end < r->start)
753 		return (0);
754 
755 	return (1);
756 }
757 
758 int
759 buffer_add_range(int fd, struct evbuffer *evb, struct range *range)
760 {
761 	char	buf[BUFSIZ];
762 	size_t	n, range_sz;
763 	ssize_t	nread;
764 
765 	if (lseek(fd, range->start, SEEK_SET) == -1)
766 		return (0);
767 
768 	range_sz = range->end - range->start + 1;
769 	while (range_sz) {
770 		n = MINIMUM(range_sz, sizeof(buf));
771 		if ((nread = read(fd, buf, n)) == -1)
772 			return (0);
773 
774 		evbuffer_add(evb, buf, nread);
775 		range_sz -= nread;
776 	}
777 
778 	return (1);
779 }
780