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