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