summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--FEATURES2
-rw-r--r--LICENSE2
-rw-r--r--Makefile2
-rw-r--r--hittpd.c162
4 files changed, 120 insertions, 48 deletions
diff --git a/FEATURES b/FEATURES
index 5524dd8..8afda8a 100644
--- a/FEATURES
+++ b/FEATURES
@@ -6,7 +6,7 @@ Features of hittpd:
 - HTTP 1.0 and HTTP 1.1 support
 - Ranges (basic variants only)
 - If-Modified-Since
-- Keep-Alive
+- Keep-Alive (but no pipelining)
 - IPv6
 - sendfile(2) on Linux
 - Virtual Hosts
diff --git a/LICENSE b/LICENSE
index 20471fb..cb12f14 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright 2020 Leah Neukirchen <leah@vuxu.org>
+Copyright 2020, 2021, 2022 Leah Neukirchen <leah@vuxu.org>
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to
diff --git a/Makefile b/Makefile
index 4203428..2253be0 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,7 @@
 ALL=hittpd
 OBJ=hittpd.o http-parser/http_parser.o
 
-CFLAGS=-g -O2 -Wall -Wno-switch -Wextra -Wwrite-strings
+CFLAGS=-g -O2 -Wall -Wno-switch -Wextra -Wwrite-strings -Werror=incompatible-pointer-types
 CPPFLAGS=-Ihttp-parser
 
 DESTDIR=
diff --git a/hittpd.c b/hittpd.c
index 1f943de..7a979a4 100644
--- a/hittpd.c
+++ b/hittpd.c
@@ -1,6 +1,6 @@
 /* hittpd - efficient, no-frills HTTP 1.1 server */
 
-/* Copyright 2020 Leah Neukirchen <leah@vuxu.org>
+/* Copyright 2020, 2021, 2022 Leah Neukirchen <leah@vuxu.org>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to
@@ -93,6 +93,7 @@ char mimetypes[] =
     ":.ico=image/x-icon";
 
 const char *default_mimetype = "application/octet-stream";
+const char index_html[] = "index.html";
 char default_vhost[] = "_default";
 char default_port[] = "80";
 
@@ -105,6 +106,15 @@ int only_public = 0;
 int reuse_port = 0;
 const char *custom_mimetypes = "";
 
+time_t now;
+char timestamp[64];
+
+struct pollfd client[MAX_CLIENTS];
+struct http_parser parsers[MAX_CLIENTS];
+struct conn_data datas[MAX_CLIENTS];
+
+volatile sig_atomic_t stop;
+
 static int
 on_url(http_parser *p, const char *s, size_t l)
 {
@@ -140,8 +150,8 @@ on_header_field(http_parser *p, const char *s, size_t l)
 
 int scan_int64(const char **s, int64_t *u) {
 	const char *t = *s;
-	long x;
-	for (x = 0; *t && (unsigned)(*t)-'0' < 10 && x <= LLONG_MAX/10 - 1; t++)
+	int64_t x;
+	for (x = 0; *t && (unsigned)(*t)-'0' < 10 && x <= INT64_MAX/10 - 1; t++)
 		x = x * 10 + ((*t)-'0');
 	if (t != *s) {
 		*s = t;
@@ -245,9 +255,6 @@ content_length(struct conn_data *data)
 	return data->last - data->first;
 }
 
-time_t now;
-char timestamp[64];
-
 void
 accesslog(http_parser *p, int status)
 {
@@ -256,14 +263,18 @@ accesslog(http_parser *p, int status)
 
 	struct conn_data *data = p->data;
 
-	char buf[64];
-	strftime(buf, 64, "[%d/%b/%Y:%H:%M:%S %z]", localtime(&now));
+	if (!data->path)
+		return;
+
+	char logtimestamp[64];
+	strftime(logtimestamp, sizeof logtimestamp,
+	    "[%d/%b/%Y:%H:%M:%S %z]", localtime(&now));
 
 //	REMOTEHOST - - [DD/MON/YYYY:HH:MM:SS -TZ] "METHOD PATH" STATUS BYTES
 // ?    REFERER USER_AGENT
 	printf("%s - - %s \"%s ",
 	    peername(data->fd),
-	    buf,
+	    logtimestamp,
 	    http_method_str(p->method));
 
 	for (char *s = data->path; *s; s++)
@@ -342,6 +353,11 @@ send_error(http_parser *p, int status, const char *msg)
 	char content[512];
 	snprintf(content, sizeof content, "%03d %s\r\n", status, msg);
 
+	if (p->http_major == 0) {
+		p->http_major = 1;
+		p->http_minor = 0;
+	}
+
 	send_response(p, status, msg, "", content);
 
 	return 0;
@@ -448,31 +464,46 @@ send_ok(http_parser *p, time_t modified, const char *mimetype, off_t filesize)
 		    (intmax_t)data->last - 1,
 		    (intmax_t)filesize,
 		    lastmod);
-		send_response(p, 216, "Partial Content", headers, 0);
+		send_response(p, 206, "Partial Content", headers, 0);
 	}
 }
 
+static const char *
+getmime(const char *mime, char *ext)
+{
+	size_t extlen = strlen(ext);
+	const char *c = mime;
+
+	do {
+		if (*c == ':')
+			c++;
+		if (strncmp(c, ext, extlen) == 0 && c[extlen] == '=')
+			return c + extlen + 1;
+	} while ((c = strchr(c, ':')));
+
+	return 0;
+}
+
 const char *
 mimetype(char *ext)
 {
-	static char type[16];
+	static char type[256];  // RFC 6838
 
 	if (!ext)
 		return default_mimetype;
 
-	char *x = strstr(custom_mimetypes, ext);
+	const char *x = getmime(custom_mimetypes, ext);
 	if (!x)
-		x = strstr(mimetypes, ext);
-
-	if (x && x[-1] == ':' && x[strlen(ext)] == '=') {
-		char *t = type;
-		for (char *c = x + strlen(ext) + 1; *c && *c != ':'; )
-			*t++ = *c++;
-		*t = 0;
-		return type;
-	}
+		x = getmime(mimetypes, ext);
+	if (!x)
+		return default_mimetype;
 
-	return default_mimetype;
+	char *t = type;
+	char *e = type + sizeof type - 1;
+	for (const char *c = x; t < e && *c && *c != ':'; )
+		*t++ = *c++;
+	*t = 0;
+	return type;
 }
 
 static inline int
@@ -493,6 +524,7 @@ on_message_complete(http_parser *p) {
 	struct conn_data *data = p->data;
 
 	data->state = SENDING;
+	http_parser_pause(p, 1);  // can't do more than one request at a time
 
 	if (p->http_major == 0 && p->http_minor == 9)
 		return send_error(p, 400, "Bad Request");
@@ -599,7 +631,11 @@ on_message_complete(http_parser *p) {
 	if (S_ISDIR(st.st_mode)) {
 		int x;
 		if (path[strlen(path)-1] == '/' &&
-		    (x = openat(stream_fd, "index.html", O_RDONLY)) >= 0) {
+		    (x = openat(stream_fd, index_html, O_RDONLY)) >= 0) {
+			if (strlen(path) > PATH_MAX - sizeof index_html) {
+				return send_error(p, 413, "Payload Too Large");
+			}
+			strcat(path, index_html);
 			close(stream_fd);
 			stream_fd = x;
 			if (fstat(stream_fd, &st) < 0)
@@ -651,6 +687,9 @@ on_message_complete(http_parser *p) {
 			if (fstatat(stream_fd, file, &ist, AT_SYMLINK_NOFOLLOW) < 0)
 				continue;
 
+			if (only_public && !(ist.st_mode & S_IROTH))
+				continue;
+
 			fprintf(stream, "<a href=\"");
 			print_urlencoded(stream, file);
 			fprintf(stream, "%s\">",
@@ -662,14 +701,14 @@ on_message_complete(http_parser *p) {
 			int len = strlen(file) + !!S_ISDIR(ist.st_mode);
 			fprintf(stream, "%-*.*s ", 48 - len, 48 - len, "");
 
-			char timestamp[64];
-			strftime(timestamp, sizeof timestamp,
+			char filetimestamp[64];
+			strftime(filetimestamp, sizeof filetimestamp,
 			    "%Y-%m-%d %H:%M", localtime(&ist.st_mtime));
 
 			if (S_ISDIR(ist.st_mode))
-				fprintf(stream, " %s %12s\n", timestamp, "-");
+				fprintf(stream, " %s %12s\n", filetimestamp, "-");
 			else
-				fprintf(stream, " %s %12jd\n", timestamp,
+				fprintf(stream, " %s %12jd\n", filetimestamp,
 				    (intmax_t)ist.st_size);
 
 		}
@@ -728,6 +767,11 @@ file:
 	if (data->last > st.st_size)
 		data->last = st.st_size;
 
+	if (data->first == data->last) {
+		send_rns(p, st.st_size);
+		return 0;
+	}
+
 	send_ok(p, st.st_mtime, mimetype(ext), st.st_size);
 
 	// XXX send short file directly?
@@ -742,10 +786,6 @@ static http_parser_settings settings = {
 	.on_url = on_url,
 };
 
-struct pollfd client[MAX_CLIENTS];
-struct http_parser parsers[MAX_CLIENTS];
-struct conn_data datas[MAX_CLIENTS];
-
 void
 close_connection(int i)
 {
@@ -759,6 +799,7 @@ close_connection(int i)
 	free(datas[i].host);
 
 	datas[i] = (struct conn_data){ 0 };
+	datas[i].stream_fd = -1;
 }
 
 void
@@ -778,6 +819,11 @@ finish_response(int i)
 	datas[i].ims = 0;
 	datas[i].host = 0;
 
+	datas[i].off = 0;
+	datas[i].first = 0;
+	datas[i].last = -1;
+	datas[i].state = NONE;
+
 	client[i].events = POLLRDNORM;
 
 	if (parsers[i].flags & F_CONNECTION_CLOSE)
@@ -816,7 +862,21 @@ write_client(int i)
 	ssize_t w = 0;
 
 	if (data->stream_fd >= 0) {
-#ifndef __linux__
+#ifdef __linux__
+		w = sendfile(sockfd, data->stream_fd,
+		    &(data->off), data->last - data->off);
+		if (data->off == data->last) {
+			finish_response(i);
+			return;
+		} else if (w == 0) {
+			close_connection(i);  // file was truncated!
+			return;
+		} else if (w > 0) {
+			return;
+		}
+
+		/* use default code when sendfile failed with w < 0 */
+#endif
 		char buf[16*4096];
 		ssize_t n = pread(data->stream_fd, buf, sizeof buf, data->off);
 		if (n < 0) {
@@ -834,14 +894,6 @@ write_client(int i)
 			else if (w == 0)
 				close_connection(i);  // file was truncated!
 		}
-#else
-		w = sendfile(sockfd, data->stream_fd,
-		    &(data->off), data->last - data->off);
-		if (data->off == data->last)
-			finish_response(i);
-		else if (w == 0)
-			close_connection(i);  // file was truncated!
-#endif
 	} else if (data->buf) {
 		if (data->off == data->last) {
 			finish_response(i);
@@ -882,7 +934,24 @@ read_client(int i)
 	} else if (n == 0) {
 		close_connection(i);
 	} else {
-		http_parser_execute(&parsers[i], &settings, buf, n);
+		size_t r = http_parser_execute(&parsers[i], &settings, buf, n);
+
+		if ((size_t)n == r &&
+		    HTTP_PARSER_ERRNO(&parsers[i]) == HPE_PAUSED) {
+			// we handled a complete request, we can reuse
+			// the parser
+			http_parser_pause(&parsers[i], 0);
+		} else if (HTTP_PARSER_ERRNO(&parsers[i]) > 0) {
+			send_error(&parsers[i], 400, "Bad Request");
+			close_connection(i);
+		} else {
+			// the read data was longer than a single request
+			// drop the rest and make sure we close the connection,
+			// so the client tries without pipelining
+			// (this is conformant)
+			http_parser_pause(&parsers[i], 0);
+			parsers[i].flags |= F_CONNECTION_CLOSE;
+		}
 
 		if (parsers[i].http_errno) {
 			printf("err=%s\n",
@@ -902,8 +971,6 @@ read_client(int i)
 	}
 }
 
-sig_atomic_t stop;
-
 void
 do_stop(int sig)
 {
@@ -1108,8 +1175,13 @@ main(int argc, char *argv[])
 						accept_client(i, connfd);
 					break;
 				}
-			if (i == MAX_CLIENTS)
+			if (i == MAX_CLIENTS) {
 				printf("too many clients\n");
+				int connfd = accept(listenfd, 0, 0);
+				if (connfd >= 0)
+					close(connfd);
+				continue;
+			}
 			if (i > maxi)
 				maxi = i; /* max index in client[] array */
 			if (--nready <= 0)
@@ -1131,7 +1203,7 @@ main(int argc, char *argv[])
 				if (--nready <= 0)
 					break; /* no more readable descriptors */
 			}
-			else if (client[i].revents & (POLLRDNORM | POLLERR)) {
+			else if (client[i].revents & (POLLRDNORM | POLLHUP | POLLERR)) {
 				read_client(i);
 				datas[i].deadline = now + TIMEOUT;