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