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