Refactor server- and connection-logic into separate components - quark - quark … | |
git clone git://git.suckless.org/quark | |
Log | |
Files | |
Refs | |
LICENSE | |
--- | |
commit e2463e733e4880c1d8034e3b90072825509ceb69 | |
parent 2920ba56d92b461c39f3c7c9c5a264f38b899fd7 | |
Author: Laslo Hunhold <[email protected]> | |
Date: Mon, 15 Feb 2021 18:25:33 +0100 | |
Refactor server- and connection-logic into separate components | |
The server-part (creating a pool, launching workers, etc.) and methods | |
concerning the handling of connections (accepting, serving, logging, | |
etc.) were all in main.c, making it unnecessarily complicated on top of | |
the fact that there is already a lot to do there. | |
Now, these parts are moved into server.h/server.c and connection.h/ | |
connection.c respectively, while main() now just only has to call | |
into server_init_thread_pool(). In other words, main() now only has to | |
deal with the argument parsing and preparations, and the rest is | |
done by other program parts. | |
The methods are prefixed with server_* and connection_* respectively, | |
making it much easier to see where each method is defined in the | |
program. | |
This refactoring also separates concerns well. The server-struct is | |
now in server.h (and not in util.h, where it was always out of place) | |
and spacetok() is moved into util.c. | |
Signed-off-by: Laslo Hunhold <[email protected]> | |
Diffstat: | |
M Makefile | 14 ++++++++------ | |
A connection.c | 314 +++++++++++++++++++++++++++++… | |
A connection.h | 32 +++++++++++++++++++++++++++++… | |
M http.h | 19 +------------------ | |
M main.c | 552 +----------------------------… | |
A server.c | 177 +++++++++++++++++++++++++++++… | |
A server.h | 35 +++++++++++++++++++++++++++++… | |
M util.c | 73 +++++++++++++++++++++++++++++… | |
M util.h | 27 +-------------------------- | |
9 files changed, 650 insertions(+), 593 deletions(-) | |
--- | |
diff --git a/Makefile b/Makefile | |
@@ -4,15 +4,17 @@ | |
include config.mk | |
-COMPONENTS = data http queue sock util | |
+COMPONENTS = connection data http queue server sock util | |
all: quark | |
-data.o: data.c data.h http.h util.h config.mk | |
-http.o: http.c config.h http.h util.h config.mk | |
-main.o: main.c arg.h data.h http.h queue.h sock.h util.h config.mk | |
-sock.o: sock.c sock.h util.h config.mk | |
-util.o: util.c util.h config.mk | |
+connection.o: connection.c config.h connection.h data.h http.h server.h sock.h… | |
+data.o: data.c config.h data.h http.h server.h util.h config.mk | |
+http.o: http.c config.h http.h server.h util.h config.mk | |
+main.o: main.c arg.h config.h server.h sock.h util.h config.mk | |
+server.o: server.c config.h connection.h http.h queue.h server.h util.h config… | |
+sock.o: sock.c config.h sock.h util.h config.mk | |
+util.o: util.c config.h util.h config.mk | |
quark: config.h $(COMPONENTS:=.o) $(COMPONENTS:=.h) main.o config.mk | |
$(CC) -o $@ $(CPPFLAGS) $(CFLAGS) $(COMPONENTS:=.o) main.o $(LDFLAGS) | |
diff --git a/connection.c b/connection.c | |
@@ -0,0 +1,314 @@ | |
+/* See LICENSE file for copyright and license details. */ | |
+#include <errno.h> | |
+#include <netinet/in.h> | |
+#include <stdio.h> | |
+#include <string.h> | |
+#include <sys/socket.h> | |
+#include <sys/types.h> | |
+#include <time.h> | |
+#include <unistd.h> | |
+ | |
+#include "connection.h" | |
+#include "data.h" | |
+#include "http.h" | |
+#include "server.h" | |
+#include "sock.h" | |
+#include "util.h" | |
+ | |
+struct worker_data { | |
+ int insock; | |
+ size_t nslots; | |
+ const struct server *srv; | |
+}; | |
+ | |
+void | |
+connection_log(const struct connection *c) | |
+{ | |
+ char inaddr_str[INET6_ADDRSTRLEN /* > INET_ADDRSTRLEN */]; | |
+ char tstmp[21]; | |
+ | |
+ /* create timestamp */ | |
+ if (!strftime(tstmp, sizeof(tstmp), "%Y-%m-%dT%H:%M:%SZ", | |
+ gmtime(&(time_t){time(NULL)}))) { | |
+ warn("strftime: Exceeded buffer capacity"); | |
+ /* continue anyway (we accept the truncation) */ | |
+ } | |
+ | |
+ /* generate address-string */ | |
+ if (sock_get_inaddr_str(&c->ia, inaddr_str, LEN(inaddr_str))) { | |
+ warn("sock_get_inaddr_str: Couldn't generate adress-string"); | |
+ inaddr_str[0] = '\0'; | |
+ } | |
+ | |
+ printf("%s\t%s\t%s%.*d\t%s\t%s%s%s%s%s\n", | |
+ tstmp, | |
+ inaddr_str, | |
+ (c->res.status == 0) ? "dropped" : "", | |
+ (c->res.status == 0) ? 0 : 3, | |
+ c->res.status, | |
+ c->req.field[REQ_HOST][0] ? c->req.field[REQ_HOST] : "-", | |
+ c->req.path[0] ? c->req.path : "-", | |
+ c->req.query[0] ? "?" : "", | |
+ c->req.query, | |
+ c->req.fragment[0] ? "#" : "", | |
+ c->req.fragment); | |
+} | |
+ | |
+void | |
+connection_reset(struct connection *c) | |
+{ | |
+ if (c != NULL) { | |
+ shutdown(c->fd, SHUT_RDWR); | |
+ close(c->fd); | |
+ memset(c, 0, sizeof(*c)); | |
+ } | |
+} | |
+ | |
+void | |
+connection_serve(struct connection *c, const struct server *srv) | |
+{ | |
+ enum status s; | |
+ int done; | |
+ | |
+ switch (c->state) { | |
+ case C_VACANT: | |
+ /* | |
+ * we were passed a "fresh" connection which should now | |
+ * try to receive the header, reset buf beforehand | |
+ */ | |
+ memset(&c->buf, 0, sizeof(c->buf)); | |
+ | |
+ c->state = C_RECV_HEADER; | |
+ /* fallthrough */ | |
+ case C_RECV_HEADER: | |
+ /* receive header */ | |
+ done = 0; | |
+ if ((s = http_recv_header(c->fd, &c->buf, &done))) { | |
+ http_prepare_error_response(&c->req, &c->res, s); | |
+ goto response; | |
+ } | |
+ if (!done) { | |
+ /* not done yet */ | |
+ return; | |
+ } | |
+ | |
+ /* parse header */ | |
+ if ((s = http_parse_header(c->buf.data, &c->req))) { | |
+ http_prepare_error_response(&c->req, &c->res, s); | |
+ goto response; | |
+ } | |
+ | |
+ /* prepare response struct */ | |
+ http_prepare_response(&c->req, &c->res, srv); | |
+response: | |
+ /* generate response header */ | |
+ if ((s = http_prepare_header_buf(&c->res, &c->buf))) { | |
+ http_prepare_error_response(&c->req, &c->res, s); | |
+ if ((s = http_prepare_header_buf(&c->res, &c->buf))) { | |
+ /* couldn't generate the header, we failed for… | |
+ c->res.status = s; | |
+ goto err; | |
+ } | |
+ } | |
+ | |
+ c->state = C_SEND_HEADER; | |
+ /* fallthrough */ | |
+ case C_SEND_HEADER: | |
+ if ((s = http_send_buf(c->fd, &c->buf))) { | |
+ c->res.status = s; | |
+ goto err; | |
+ } | |
+ if (c->buf.len > 0) { | |
+ /* not done yet */ | |
+ return; | |
+ } | |
+ | |
+ c->state = C_SEND_BODY; | |
+ /* fallthrough */ | |
+ case C_SEND_BODY: | |
+ if (c->req.method == M_GET) { | |
+ if (c->buf.len == 0) { | |
+ /* fill buffer with body data */ | |
+ if ((s = data_fct[c->res.type](&c->res, &c->bu… | |
+ &c->progress)))… | |
+ /* too late to do any real error handl… | |
+ c->res.status = s; | |
+ goto err; | |
+ } | |
+ | |
+ /* if the buffer remains empty, we are done */ | |
+ if (c->buf.len == 0) { | |
+ break; | |
+ } | |
+ } else { | |
+ /* send buffer */ | |
+ if ((s = http_send_buf(c->fd, &c->buf))) { | |
+ /* too late to do any real error handl… | |
+ c->res.status = s; | |
+ goto err; | |
+ } | |
+ } | |
+ return; | |
+ } | |
+ break; | |
+ default: | |
+ warn("serve: invalid connection state"); | |
+ return; | |
+ } | |
+err: | |
+ connection_log(c); | |
+ connection_reset(c); | |
+} | |
+ | |
+static struct connection * | |
+connection_get_drop_candidate(struct connection *connection, size_t nslots) | |
+{ | |
+ struct connection *c, *minc; | |
+ size_t i, j, maxcnt, cnt; | |
+ | |
+ /* | |
+ * determine the most-unimportant connection 'minc' of the in-address | |
+ * with most connections; this algorithm has a complexity of O(n²) | |
+ * in time but is O(1) in space; there are algorithms with O(n) in | |
+ * time and space, but this would require memory allocation, | |
+ * which we avoid. Given the simplicity of the inner loop and | |
+ * relatively small number of slots per thread, this is fine. | |
+ */ | |
+ for (i = 0, minc = NULL, maxcnt = 0; i < nslots; i++) { | |
+ /* | |
+ * we determine how many connections have the same | |
+ * in-address as connection[i], but also minimize over | |
+ * that set with other criteria, yielding a general | |
+ * minimizer c. We first set it to connection[i] and | |
+ * update it, if a better candidate shows up, in the inner | |
+ * loop | |
+ */ | |
+ c = &connection[i]; | |
+ | |
+ for (j = 0, cnt = 0; j < nslots; j++) { | |
+ if (!sock_same_addr(&connection[i].ia, | |
+ &connection[j].ia)) { | |
+ continue; | |
+ } | |
+ cnt++; | |
+ | |
+ /* minimize over state */ | |
+ if (connection[j].state < c->state) { | |
+ c = &connection[j]; | |
+ } else if (connection[j].state == c->state) { | |
+ /* minimize over progress */ | |
+ if (c->state == C_SEND_BODY && | |
+ connection[i].res.type != c->res.type) { | |
+ /* | |
+ * mixed response types; progress | |
+ * is not comparable | |
+ * | |
+ * the res-type-enum is ordered as | |
+ * DIRLISTING, ERROR, FILE, i.e. | |
+ * in rising priority, because a | |
+ * file transfer is most important, | |
+ * followed by error-messages. | |
+ * Dirlistings as an "interactive" | |
+ * feature (that take up lots of | |
+ * resources) have the lowest | |
+ * priority | |
+ */ | |
+ if (connection[i].res.type < | |
+ c->res.type) { | |
+ c = &connection[j]; | |
+ } | |
+ } else if (connection[j].progress < | |
+ c->progress) { | |
+ /* | |
+ * for C_SEND_BODY with same response | |
+ * type, C_RECV_HEADER and C_SEND_BODY | |
+ * it is sufficient to compare the | |
+ * raw progress | |
+ */ | |
+ c = &connection[j]; | |
+ } | |
+ } | |
+ } | |
+ | |
+ if (cnt > maxcnt) { | |
+ /* this run yielded an even greedier in-address */ | |
+ minc = c; | |
+ maxcnt = cnt; | |
+ } | |
+ } | |
+ | |
+ return minc; | |
+} | |
+ | |
+struct connection * | |
+connection_accept(int insock, struct connection *connection, size_t nslots) | |
+{ | |
+ struct connection *c = NULL; | |
+ size_t i; | |
+ | |
+ /* find vacant connection (i.e. one with no fd assigned to it) */ | |
+ for (i = 0; i < nslots; i++) { | |
+ if (connection[i].fd == 0) { | |
+ c = &connection[i]; | |
+ break; | |
+ } | |
+ } | |
+ if (i == nslots) { | |
+ /* | |
+ * all our connection-slots are occupied and the only | |
+ * way out is to drop another connection, because not | |
+ * accepting this connection just kicks this can further | |
+ * down the road (to the next queue_wait()) without | |
+ * solving anything. | |
+ * | |
+ * This may sound bad, but this case can only be hit | |
+ * either when there's a (D)DoS-attack or a massive | |
+ * influx of requests. The latter is impossible to solve | |
+ * at this moment without expanding resources, but the | |
+ * former has certain characteristics allowing us to | |
+ * handle this gracefully. | |
+ * | |
+ * During an attack (e.g. Slowloris, R-U-Dead-Yet, Slow | |
+ * Read or just plain flooding) we can not see who is | |
+ * waiting to be accept()ed. | |
+ * However, an attacker usually already has many | |
+ * connections open (while well-behaved clients could | |
+ * do everything with just one connection using | |
+ * keep-alive). Inferring a likely attacker-connection | |
+ * is an educated guess based on which in-address is | |
+ * occupying the most connection slots. Among those, | |
+ * connections in early stages (receiving or sending | |
+ * headers) are preferred over connections in late | |
+ * stages (sending body). | |
+ * | |
+ * This quantitative approach effectively drops malicious | |
+ * connections while preserving even long-running | |
+ * benevolent connections like downloads. | |
+ */ | |
+ c = connection_get_drop_candidate(connection, nslots); | |
+ c->res.status = 0; | |
+ connection_log(c); | |
+ connection_reset(c); | |
+ } | |
+ | |
+ /* accept connection */ | |
+ if ((c->fd = accept(insock, (struct sockaddr *)&c->ia, | |
+ &(socklen_t){sizeof(c->ia)})) < 0) { | |
+ if (errno != EAGAIN && errno != EWOULDBLOCK) { | |
+ /* | |
+ * this should not happen, as we received the | |
+ * event that there are pending connections here | |
+ */ | |
+ warn("accept:"); | |
+ } | |
+ return NULL; | |
+ } | |
+ | |
+ /* set socket to non-blocking mode */ | |
+ if (sock_set_nonblocking(c->fd)) { | |
+ /* we can't allow blocking sockets */ | |
+ return NULL; | |
+ } | |
+ | |
+ return c; | |
+} | |
diff --git a/connection.h b/connection.h | |
@@ -0,0 +1,32 @@ | |
+/* See LICENSE file for copyright and license details. */ | |
+#ifndef CONNECTION_H | |
+#define CONNECTION_H | |
+ | |
+#include "http.h" | |
+#include "server.h" | |
+#include "util.h" | |
+ | |
+enum connection_state { | |
+ C_VACANT, | |
+ C_RECV_HEADER, | |
+ C_SEND_HEADER, | |
+ C_SEND_BODY, | |
+ NUM_CONN_STATES, | |
+}; | |
+ | |
+struct connection { | |
+ enum connection_state state; | |
+ int fd; | |
+ struct sockaddr_storage ia; | |
+ struct request req; | |
+ struct response res; | |
+ struct buffer buf; | |
+ size_t progress; | |
+}; | |
+ | |
+struct connection *connection_accept(int, struct connection *, size_t); | |
+void connection_log(const struct connection *); | |
+void connection_reset(struct connection *); | |
+void connection_serve(struct connection *, const struct server *); | |
+ | |
+#endif /* CONNECTION_H */ | |
diff --git a/http.h b/http.h | |
@@ -6,6 +6,7 @@ | |
#include <sys/socket.h> | |
#include "config.h" | |
+#include "server.h" | |
#include "util.h" | |
enum req_field { | |
@@ -84,24 +85,6 @@ struct response { | |
} file; | |
}; | |
-enum conn_state { | |
- C_VACANT, | |
- C_RECV_HEADER, | |
- C_SEND_HEADER, | |
- C_SEND_BODY, | |
- NUM_CONN_STATES, | |
-}; | |
- | |
-struct connection { | |
- enum conn_state state; | |
- int fd; | |
- struct sockaddr_storage ia; | |
- struct request req; | |
- struct response res; | |
- struct buffer buf; | |
- size_t progress; | |
-}; | |
- | |
enum status http_prepare_header_buf(const struct response *, struct buffer *); | |
enum status http_send_buf(int, struct buffer *); | |
enum status http_recv_header(int, struct buffer *, int *); | |
diff --git a/main.c b/main.c | |
@@ -2,494 +2,31 @@ | |
#include <errno.h> | |
#include <grp.h> | |
#include <limits.h> | |
-#include <netinet/in.h> | |
-#include <pthread.h> | |
#include <pwd.h> | |
#include <regex.h> | |
#include <signal.h> | |
+#include <stddef.h> | |
+#include <stdlib.h> | |
+#include <string.h> | |
#include <sys/resource.h> | |
-#include <sys/socket.h> | |
+#include <sys/time.h> | |
#include <sys/types.h> | |
#include <sys/wait.h> | |
-#include <stdio.h> | |
-#include <stdlib.h> | |
-#include <string.h> | |
-#include <time.h> | |
#include <unistd.h> | |
#include "arg.h" | |
-#include "data.h" | |
-#include "http.h" | |
-#include "queue.h" | |
+#include "server.h" | |
#include "sock.h" | |
#include "util.h" | |
static char *udsname; | |
static void | |
-logmsg(const struct connection *c) | |
-{ | |
- char inaddr_str[INET6_ADDRSTRLEN /* > INET_ADDRSTRLEN */]; | |
- char tstmp[21]; | |
- | |
- /* create timestamp */ | |
- if (!strftime(tstmp, sizeof(tstmp), "%Y-%m-%dT%H:%M:%SZ", | |
- gmtime(&(time_t){time(NULL)}))) { | |
- warn("strftime: Exceeded buffer capacity"); | |
- /* continue anyway (we accept the truncation) */ | |
- } | |
- | |
- /* generate address-string */ | |
- if (sock_get_inaddr_str(&c->ia, inaddr_str, LEN(inaddr_str))) { | |
- warn("sock_get_inaddr_str: Couldn't generate adress-string"); | |
- inaddr_str[0] = '\0'; | |
- } | |
- | |
- printf("%s\t%s\t%s%.*d\t%s\t%s%s%s%s%s\n", | |
- tstmp, | |
- inaddr_str, | |
- (c->res.status == 0) ? "dropped" : "", | |
- (c->res.status == 0) ? 0 : 3, | |
- c->res.status, | |
- c->req.field[REQ_HOST][0] ? c->req.field[REQ_HOST] : "-", | |
- c->req.path[0] ? c->req.path : "-", | |
- c->req.query[0] ? "?" : "", | |
- c->req.query, | |
- c->req.fragment[0] ? "#" : "", | |
- c->req.fragment); | |
-} | |
- | |
-static void | |
-reset_connection(struct connection *c) | |
-{ | |
- if (c != NULL) { | |
- shutdown(c->fd, SHUT_RDWR); | |
- close(c->fd); | |
- memset(c, 0, sizeof(*c)); | |
- } | |
-} | |
- | |
-static void | |
-serve_connection(struct connection *c, const struct server *srv) | |
-{ | |
- enum status s; | |
- int done; | |
- | |
- switch (c->state) { | |
- case C_VACANT: | |
- /* | |
- * we were passed a "fresh" connection which should now | |
- * try to receive the header, reset buf beforehand | |
- */ | |
- memset(&c->buf, 0, sizeof(c->buf)); | |
- | |
- c->state = C_RECV_HEADER; | |
- /* fallthrough */ | |
- case C_RECV_HEADER: | |
- /* receive header */ | |
- done = 0; | |
- if ((s = http_recv_header(c->fd, &c->buf, &done))) { | |
- http_prepare_error_response(&c->req, &c->res, s); | |
- goto response; | |
- } | |
- if (!done) { | |
- /* not done yet */ | |
- return; | |
- } | |
- | |
- /* parse header */ | |
- if ((s = http_parse_header(c->buf.data, &c->req))) { | |
- http_prepare_error_response(&c->req, &c->res, s); | |
- goto response; | |
- } | |
- | |
- /* prepare response struct */ | |
- http_prepare_response(&c->req, &c->res, srv); | |
-response: | |
- /* generate response header */ | |
- if ((s = http_prepare_header_buf(&c->res, &c->buf))) { | |
- http_prepare_error_response(&c->req, &c->res, s); | |
- if ((s = http_prepare_header_buf(&c->res, &c->buf))) { | |
- /* couldn't generate the header, we failed for… | |
- c->res.status = s; | |
- goto err; | |
- } | |
- } | |
- | |
- c->state = C_SEND_HEADER; | |
- /* fallthrough */ | |
- case C_SEND_HEADER: | |
- if ((s = http_send_buf(c->fd, &c->buf))) { | |
- c->res.status = s; | |
- goto err; | |
- } | |
- if (c->buf.len > 0) { | |
- /* not done yet */ | |
- return; | |
- } | |
- | |
- c->state = C_SEND_BODY; | |
- /* fallthrough */ | |
- case C_SEND_BODY: | |
- if (c->req.method == M_GET) { | |
- if (c->buf.len == 0) { | |
- /* fill buffer with body data */ | |
- if ((s = data_fct[c->res.type](&c->res, &c->bu… | |
- &c->progress)))… | |
- /* too late to do any real error handl… | |
- c->res.status = s; | |
- goto err; | |
- } | |
- | |
- /* if the buffer remains empty, we are done */ | |
- if (c->buf.len == 0) { | |
- break; | |
- } | |
- } else { | |
- /* send buffer */ | |
- if ((s = http_send_buf(c->fd, &c->buf))) { | |
- /* too late to do any real error handl… | |
- c->res.status = s; | |
- goto err; | |
- } | |
- } | |
- return; | |
- } | |
- break; | |
- default: | |
- warn("serve: invalid connection state"); | |
- return; | |
- } | |
-err: | |
- logmsg(c); | |
- reset_connection(c); | |
-} | |
- | |
-static struct connection * | |
-get_connection_drop_candidate(struct connection *connection, size_t nslots) | |
-{ | |
- struct connection *c, *minc; | |
- size_t i, j, maxcnt, cnt; | |
- | |
- /* | |
- * determine the most-unimportant connection 'minc' of the in-address | |
- * with most connections; this algorithm has a complexity of O(n²) | |
- * in time but is O(1) in space; there are algorithms with O(n) in | |
- * time and space, but this would require memory allocation, | |
- * which we avoid. Given the simplicity of the inner loop and | |
- * relatively small number of slots per thread, this is fine. | |
- */ | |
- for (i = 0, minc = NULL, maxcnt = 0; i < nslots; i++) { | |
- /* | |
- * we determine how many connections have the same | |
- * in-address as connection[i], but also minimize over | |
- * that set with other criteria, yielding a general | |
- * minimizer c. We first set it to connection[i] and | |
- * update it, if a better candidate shows up, in the inner | |
- * loop | |
- */ | |
- c = &connection[i]; | |
- | |
- for (j = 0, cnt = 0; j < nslots; j++) { | |
- if (!sock_same_addr(&connection[i].ia, | |
- &connection[j].ia)) { | |
- continue; | |
- } | |
- cnt++; | |
- | |
- /* minimize over state */ | |
- if (connection[j].state < c->state) { | |
- c = &connection[j]; | |
- } else if (connection[j].state == c->state) { | |
- /* minimize over progress */ | |
- if (c->state == C_SEND_BODY && | |
- connection[i].res.type != c->res.type) { | |
- /* | |
- * mixed response types; progress | |
- * is not comparable | |
- * | |
- * the res-type-enum is ordered as | |
- * DIRLISTING, ERROR, FILE, i.e. | |
- * in rising priority, because a | |
- * file transfer is most important, | |
- * followed by error-messages. | |
- * Dirlistings as an "interactive" | |
- * feature (that take up lots of | |
- * resources) have the lowest | |
- * priority | |
- */ | |
- if (connection[i].res.type < | |
- c->res.type) { | |
- c = &connection[j]; | |
- } | |
- } else if (connection[j].progress < | |
- c->progress) { | |
- /* | |
- * for C_SEND_BODY with same response | |
- * type, C_RECV_HEADER and C_SEND_BODY | |
- * it is sufficient to compare the | |
- * raw progress | |
- */ | |
- c = &connection[j]; | |
- } | |
- } | |
- } | |
- | |
- if (cnt > maxcnt) { | |
- /* this run yielded an even greedier in-address */ | |
- minc = c; | |
- maxcnt = cnt; | |
- } | |
- } | |
- | |
- return minc; | |
-} | |
- | |
-struct connection * | |
-accept_connection(int insock, struct connection *connection, | |
- size_t nslots) | |
-{ | |
- struct connection *c = NULL; | |
- size_t i; | |
- | |
- /* find vacant connection (i.e. one with no fd assigned to it) */ | |
- for (i = 0; i < nslots; i++) { | |
- if (connection[i].fd == 0) { | |
- c = &connection[i]; | |
- break; | |
- } | |
- } | |
- if (i == nslots) { | |
- /* | |
- * all our connection-slots are occupied and the only | |
- * way out is to drop another connection, because not | |
- * accepting this connection just kicks this can further | |
- * down the road (to the next queue_wait()) without | |
- * solving anything. | |
- * | |
- * This may sound bad, but this case can only be hit | |
- * either when there's a (D)DoS-attack or a massive | |
- * influx of requests. The latter is impossible to solve | |
- * at this moment without expanding resources, but the | |
- * former has certain characteristics allowing us to | |
- * handle this gracefully. | |
- * | |
- * During an attack (e.g. Slowloris, R-U-Dead-Yet, Slow | |
- * Read or just plain flooding) we can not see who is | |
- * waiting to be accept()ed. | |
- * However, an attacker usually already has many | |
- * connections open (while well-behaved clients could | |
- * do everything with just one connection using | |
- * keep-alive). Inferring a likely attacker-connection | |
- * is an educated guess based on which in-address is | |
- * occupying the most connection slots. Among those, | |
- * connections in early stages (receiving or sending | |
- * headers) are preferred over connections in late | |
- * stages (sending body). | |
- * | |
- * This quantitative approach effectively drops malicious | |
- * connections while preserving even long-running | |
- * benevolent connections like downloads. | |
- */ | |
- c = get_connection_drop_candidate(connection, nslots); | |
- c->res.status = 0; | |
- logmsg(c); | |
- reset_connection(c); | |
- } | |
- | |
- /* accept connection */ | |
- if ((c->fd = accept(insock, (struct sockaddr *)&c->ia, | |
- &(socklen_t){sizeof(c->ia)})) < 0) { | |
- if (errno != EAGAIN && errno != EWOULDBLOCK) { | |
- /* | |
- * this should not happen, as we received the | |
- * event that there are pending connections here | |
- */ | |
- warn("accept:"); | |
- } | |
- return NULL; | |
- } | |
- | |
- /* set socket to non-blocking mode */ | |
- if (sock_set_nonblocking(c->fd)) { | |
- /* we can't allow blocking sockets */ | |
- return NULL; | |
- } | |
- | |
- return c; | |
-} | |
- | |
-struct worker_data { | |
- int insock; | |
- size_t nslots; | |
- const struct server *srv; | |
-}; | |
- | |
-static void * | |
-thread_method(void *data) | |
-{ | |
- queue_event *event = NULL; | |
- struct connection *connection, *c, *newc; | |
- struct worker_data *d = (struct worker_data *)data; | |
- int qfd; | |
- ssize_t nready; | |
- size_t i; | |
- | |
- /* allocate connections */ | |
- if (!(connection = calloc(d->nslots, sizeof(*connection)))) { | |
- die("calloc:"); | |
- } | |
- | |
- /* create event queue */ | |
- if ((qfd = queue_create()) < 0) { | |
- exit(1); | |
- } | |
- | |
- /* add insock to the interest list (with data=NULL) */ | |
- if (queue_add_fd(qfd, d->insock, QUEUE_EVENT_IN, 1, NULL) < 0) { | |
- exit(1); | |
- } | |
- | |
- /* allocate event array */ | |
- if (!(event = reallocarray(event, d->nslots, sizeof(*event)))) { | |
- die("reallocarray:"); | |
- } | |
- | |
- for (;;) { | |
- /* wait for new activity */ | |
- if ((nready = queue_wait(qfd, event, d->nslots)) < 0) { | |
- exit(1); | |
- } | |
- | |
- /* handle events */ | |
- for (i = 0; i < (size_t)nready; i++) { | |
- c = queue_event_get_data(&event[i]); | |
- | |
- if (queue_event_is_error(&event[i])) { | |
- if (c != NULL) { | |
- queue_rem_fd(qfd, c->fd); | |
- c->res.status = 0; | |
- logmsg(c); | |
- reset_connection(c); | |
- } | |
- | |
- continue; | |
- } | |
- | |
- if (c == NULL) { | |
- /* add new connection to the interest list */ | |
- if (!(newc = accept_connection(d->insock, | |
- connection, | |
- d->nslots))) { | |
- /* | |
- * the socket is either blocking | |
- * or something failed. | |
- * In both cases, we just carry on | |
- */ | |
- continue; | |
- } | |
- | |
- /* | |
- * add event to the interest list | |
- * (we want IN, because we start | |
- * with receiving the header) | |
- */ | |
- if (queue_add_fd(qfd, newc->fd, | |
- QUEUE_EVENT_IN, | |
- 0, newc) < 0) { | |
- /* not much we can do here */ | |
- continue; | |
- } | |
- } else { | |
- /* serve existing connection */ | |
- serve_connection(c, d->srv); | |
- | |
- if (c->fd == 0) { | |
- /* we are done */ | |
- memset(c, 0, sizeof(struct connection)… | |
- continue; | |
- } | |
- | |
- /* | |
- * rearm the event based on the state | |
- * we are "stuck" at | |
- */ | |
- switch(c->state) { | |
- case C_RECV_HEADER: | |
- if (queue_mod_fd(qfd, c->fd, | |
- QUEUE_EVENT_IN, | |
- c) < 0) { | |
- reset_connection(c); | |
- break; | |
- } | |
- break; | |
- case C_SEND_HEADER: | |
- case C_SEND_BODY: | |
- if (queue_mod_fd(qfd, c->fd, | |
- QUEUE_EVENT_OUT, | |
- c) < 0) { | |
- reset_connection(c); | |
- break; | |
- } | |
- break; | |
- default: | |
- break; | |
- } | |
- } | |
- } | |
- } | |
- | |
- return NULL; | |
-} | |
- | |
-static void | |
-handle_connections(int *insock, size_t nthreads, size_t nslots, | |
- const struct server *srv) | |
-{ | |
- pthread_t *thread = NULL; | |
- struct worker_data *d = NULL; | |
- size_t i; | |
- | |
- /* allocate worker_data structs */ | |
- if (!(d = reallocarray(d, nthreads, sizeof(*d)))) { | |
- die("reallocarray:"); | |
- } | |
- for (i = 0; i < nthreads; i++) { | |
- d[i].insock = insock[i]; | |
- d[i].nslots = nslots; | |
- d[i].srv = srv; | |
- } | |
- | |
- /* allocate and initialize thread pool */ | |
- if (!(thread = reallocarray(thread, nthreads, sizeof(*thread)))) { | |
- die("reallocarray:"); | |
- } | |
- for (i = 0; i < nthreads; i++) { | |
- if (pthread_create(&thread[i], NULL, thread_method, &d[i]) != … | |
- if (errno == EAGAIN) { | |
- die("You need to run as root or have " | |
- "CAP_SYS_RESOURCE set, or are trying " | |
- "to create more threads than the " | |
- "system can offer"); | |
- } else { | |
- die("pthread_create:"); | |
- } | |
- } | |
- } | |
- | |
- /* wait for threads */ | |
- for (i = 0; i < nthreads; i++) { | |
- if ((errno = pthread_join(thread[i], NULL))) { | |
- warn("pthread_join:"); | |
- } | |
- } | |
-} | |
- | |
-static void | |
cleanup(void) | |
{ | |
- if (udsname) | |
- sock_rem_uds(udsname); | |
+ if (udsname) { | |
+ sock_rem_uds(udsname); | |
+ } | |
} | |
static void | |
@@ -514,77 +51,6 @@ handlesignals(void(*hdl)(int)) | |
sigaction(SIGQUIT, &sa, NULL); | |
} | |
-static int | |
-spacetok(const char *s, char **t, size_t tlen) | |
-{ | |
- const char *tok; | |
- size_t i, j, toki, spaces; | |
- | |
- /* fill token-array with NULL-pointers */ | |
- for (i = 0; i < tlen; i++) { | |
- t[i] = NULL; | |
- } | |
- toki = 0; | |
- | |
- /* don't allow NULL string or leading spaces */ | |
- if (!s || *s == ' ') { | |
- return 1; | |
- } | |
-start: | |
- /* skip spaces */ | |
- for (; *s == ' '; s++) | |
- ; | |
- | |
- /* don't allow trailing spaces */ | |
- if (*s == '\0') { | |
- goto err; | |
- } | |
- | |
- /* consume token */ | |
- for (tok = s, spaces = 0; ; s++) { | |
- if (*s == '\\' && *(s + 1) == ' ') { | |
- spaces++; | |
- s++; | |
- continue; | |
- } else if (*s == ' ') { | |
- /* end of token */ | |
- goto token; | |
- } else if (*s == '\0') { | |
- /* end of string */ | |
- goto token; | |
- } | |
- } | |
-token: | |
- if (toki >= tlen) { | |
- goto err; | |
- } | |
- if (!(t[toki] = malloc(s - tok - spaces + 1))) { | |
- die("malloc:"); | |
- } | |
- for (i = 0, j = 0; j < s - tok - spaces + 1; i++, j++) { | |
- if (tok[i] == '\\' && tok[i + 1] == ' ') { | |
- i++; | |
- } | |
- t[toki][j] = tok[i]; | |
- } | |
- t[toki][s - tok - spaces] = '\0'; | |
- toki++; | |
- | |
- if (*s == ' ') { | |
- s++; | |
- goto start; | |
- } | |
- | |
- return 0; | |
-err: | |
- for (i = 0; i < tlen; i++) { | |
- free(t[i]); | |
- t[i] = NULL; | |
- } | |
- | |
- return 1; | |
-} | |
- | |
static void | |
usage(void) | |
{ | |
@@ -864,7 +330,7 @@ main(int argc, char *argv[]) | |
} | |
/* accept incoming connections */ | |
- handle_connections(insock, nthreads, nslots, &srv); | |
+ server_init_thread_pool(insock, nthreads, nslots, &srv); | |
exit(0); | |
default: | |
diff --git a/server.c b/server.c | |
@@ -0,0 +1,177 @@ | |
+/* See LICENSE file for copyright and license details. */ | |
+#include <errno.h> | |
+#include <pthread.h> | |
+#include <stddef.h> | |
+#include <stdlib.h> | |
+#include <string.h> | |
+ | |
+#include "connection.h" | |
+#include "queue.h" | |
+#include "server.h" | |
+#include "util.h" | |
+ | |
+struct worker_data { | |
+ int insock; | |
+ size_t nslots; | |
+ const struct server *srv; | |
+}; | |
+ | |
+static void * | |
+server_worker(void *data) | |
+{ | |
+ queue_event *event = NULL; | |
+ struct connection *connection, *c, *newc; | |
+ struct worker_data *d = (struct worker_data *)data; | |
+ int qfd; | |
+ ssize_t nready; | |
+ size_t i; | |
+ | |
+ /* allocate connections */ | |
+ if (!(connection = calloc(d->nslots, sizeof(*connection)))) { | |
+ die("calloc:"); | |
+ } | |
+ | |
+ /* create event queue */ | |
+ if ((qfd = queue_create()) < 0) { | |
+ exit(1); | |
+ } | |
+ | |
+ /* add insock to the interest list (with data=NULL) */ | |
+ if (queue_add_fd(qfd, d->insock, QUEUE_EVENT_IN, 1, NULL) < 0) { | |
+ exit(1); | |
+ } | |
+ | |
+ /* allocate event array */ | |
+ if (!(event = reallocarray(event, d->nslots, sizeof(*event)))) { | |
+ die("reallocarray:"); | |
+ } | |
+ | |
+ for (;;) { | |
+ /* wait for new activity */ | |
+ if ((nready = queue_wait(qfd, event, d->nslots)) < 0) { | |
+ exit(1); | |
+ } | |
+ | |
+ /* handle events */ | |
+ for (i = 0; i < (size_t)nready; i++) { | |
+ c = queue_event_get_data(&event[i]); | |
+ | |
+ if (queue_event_is_error(&event[i])) { | |
+ if (c != NULL) { | |
+ queue_rem_fd(qfd, c->fd); | |
+ c->res.status = 0; | |
+ connection_log(c); | |
+ connection_reset(c); | |
+ } | |
+ | |
+ continue; | |
+ } | |
+ | |
+ if (c == NULL) { | |
+ /* add new connection to the interest list */ | |
+ if (!(newc = connection_accept(d->insock, | |
+ connection, | |
+ d->nslots))) { | |
+ /* | |
+ * the socket is either blocking | |
+ * or something failed. | |
+ * In both cases, we just carry on | |
+ */ | |
+ continue; | |
+ } | |
+ | |
+ /* | |
+ * add event to the interest list | |
+ * (we want IN, because we start | |
+ * with receiving the header) | |
+ */ | |
+ if (queue_add_fd(qfd, newc->fd, | |
+ QUEUE_EVENT_IN, | |
+ 0, newc) < 0) { | |
+ /* not much we can do here */ | |
+ continue; | |
+ } | |
+ } else { | |
+ /* serve existing connection */ | |
+ connection_serve(c, d->srv); | |
+ | |
+ if (c->fd == 0) { | |
+ /* we are done */ | |
+ memset(c, 0, sizeof(struct connection)… | |
+ continue; | |
+ } | |
+ | |
+ /* | |
+ * rearm the event based on the state | |
+ * we are "stuck" at | |
+ */ | |
+ switch(c->state) { | |
+ case C_RECV_HEADER: | |
+ if (queue_mod_fd(qfd, c->fd, | |
+ QUEUE_EVENT_IN, | |
+ c) < 0) { | |
+ connection_reset(c); | |
+ break; | |
+ } | |
+ break; | |
+ case C_SEND_HEADER: | |
+ case C_SEND_BODY: | |
+ if (queue_mod_fd(qfd, c->fd, | |
+ QUEUE_EVENT_OUT, | |
+ c) < 0) { | |
+ connection_reset(c); | |
+ break; | |
+ } | |
+ break; | |
+ default: | |
+ break; | |
+ } | |
+ } | |
+ } | |
+ } | |
+ | |
+ return NULL; | |
+} | |
+ | |
+void | |
+server_init_thread_pool(int *insock, size_t nthreads, size_t nslots, | |
+ const struct server *srv) | |
+{ | |
+ pthread_t *thread = NULL; | |
+ struct worker_data *d = NULL; | |
+ size_t i; | |
+ | |
+ /* allocate worker_data structs */ | |
+ if (!(d = reallocarray(d, nthreads, sizeof(*d)))) { | |
+ die("reallocarray:"); | |
+ } | |
+ for (i = 0; i < nthreads; i++) { | |
+ d[i].insock = insock[i]; | |
+ d[i].nslots = nslots; | |
+ d[i].srv = srv; | |
+ } | |
+ | |
+ /* allocate and initialize thread pool */ | |
+ if (!(thread = reallocarray(thread, nthreads, sizeof(*thread)))) { | |
+ die("reallocarray:"); | |
+ } | |
+ for (i = 0; i < nthreads; i++) { | |
+ if (pthread_create(&thread[i], NULL, server_worker, &d[i]) != … | |
+ if (errno == EAGAIN) { | |
+ die("You need to run as root or have " | |
+ "CAP_SYS_RESOURCE set, or are trying " | |
+ "to create more threads than the " | |
+ "system can offer"); | |
+ } else { | |
+ die("pthread_create:"); | |
+ } | |
+ } | |
+ } | |
+ | |
+ /* wait for threads */ | |
+ for (i = 0; i < nthreads; i++) { | |
+ if ((errno = pthread_join(thread[i], NULL))) { | |
+ warn("pthread_join:"); | |
+ } | |
+ } | |
+} | |
diff --git a/server.h b/server.h | |
@@ -0,0 +1,35 @@ | |
+/* See LICENSE file for copyright and license details. */ | |
+#ifndef SERVER_H | |
+#define SERVER_H | |
+ | |
+#include <regex.h> | |
+#include <stddef.h> | |
+ | |
+struct vhost { | |
+ char *chost; | |
+ char *regex; | |
+ char *dir; | |
+ char *prefix; | |
+ regex_t re; | |
+}; | |
+ | |
+struct map { | |
+ char *chost; | |
+ char *from; | |
+ char *to; | |
+}; | |
+ | |
+struct server { | |
+ char *host; | |
+ char *port; | |
+ char *docindex; | |
+ int listdirs; | |
+ struct vhost *vhost; | |
+ size_t vhost_len; | |
+ struct map *map; | |
+ size_t map_len; | |
+}; | |
+ | |
+void server_init_thread_pool(int *, size_t, size_t, const struct server *); | |
+ | |
+#endif /* SERVER_H */ | |
diff --git a/util.c b/util.c | |
@@ -123,6 +123,79 @@ prepend(char *str, size_t size, const char *prefix) | |
return 0; | |
} | |
+int | |
+spacetok(const char *s, char **t, size_t tlen) | |
+{ | |
+ const char *tok; | |
+ size_t i, j, toki, spaces; | |
+ | |
+ /* fill token-array with NULL-pointers */ | |
+ for (i = 0; i < tlen; i++) { | |
+ t[i] = NULL; | |
+ } | |
+ toki = 0; | |
+ | |
+ /* don't allow NULL string or leading spaces */ | |
+ if (!s || *s == ' ') { | |
+ return 1; | |
+ } | |
+start: | |
+ /* skip spaces */ | |
+ for (; *s == ' '; s++) | |
+ ; | |
+ | |
+ /* don't allow trailing spaces */ | |
+ if (*s == '\0') { | |
+ goto err; | |
+ } | |
+ | |
+ /* consume token */ | |
+ for (tok = s, spaces = 0; ; s++) { | |
+ if (*s == '\\' && *(s + 1) == ' ') { | |
+ spaces++; | |
+ s++; | |
+ continue; | |
+ } else if (*s == ' ') { | |
+ /* end of token */ | |
+ goto token; | |
+ } else if (*s == '\0') { | |
+ /* end of string */ | |
+ goto token; | |
+ } | |
+ } | |
+token: | |
+ if (toki >= tlen) { | |
+ goto err; | |
+ } | |
+ if (!(t[toki] = malloc(s - tok - spaces + 1))) { | |
+ die("malloc:"); | |
+ } | |
+ for (i = 0, j = 0; j < s - tok - spaces + 1; i++, j++) { | |
+ if (tok[i] == '\\' && tok[i + 1] == ' ') { | |
+ i++; | |
+ } | |
+ t[toki][j] = tok[i]; | |
+ } | |
+ t[toki][s - tok - spaces] = '\0'; | |
+ toki++; | |
+ | |
+ if (*s == ' ') { | |
+ s++; | |
+ goto start; | |
+ } | |
+ | |
+ return 0; | |
+err: | |
+ for (i = 0; i < tlen; i++) { | |
+ free(t[i]); | |
+ t[i] = NULL; | |
+ } | |
+ | |
+ return 1; | |
+} | |
+ | |
+ | |
+ | |
#define INVALID 1 | |
#define TOOSMALL 2 | |
#define TOOLARGE 3 | |
diff --git a/util.h b/util.h | |
@@ -8,32 +8,6 @@ | |
#include "config.h" | |
-/* main server struct */ | |
-struct vhost { | |
- char *chost; | |
- char *regex; | |
- char *dir; | |
- char *prefix; | |
- regex_t re; | |
-}; | |
- | |
-struct map { | |
- char *chost; | |
- char *from; | |
- char *to; | |
-}; | |
- | |
-struct server { | |
- char *host; | |
- char *port; | |
- char *docindex; | |
- int listdirs; | |
- struct vhost *vhost; | |
- size_t vhost_len; | |
- struct map *map; | |
- size_t map_len; | |
-}; | |
- | |
/* general purpose buffer */ | |
struct buffer { | |
char data[BUFFER_SIZE]; | |
@@ -58,6 +32,7 @@ void eunveil(const char *, const char *); | |
int timestamp(char *, size_t, time_t); | |
int esnprintf(char *, size_t, const char *, ...); | |
int prepend(char *, size_t, const char *); | |
+int spacetok(const char *, char **, size_t); | |
void *reallocarray(void *, size_t, size_t); | |
long long strtonum(const char *, long long, long long, const char **); |