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