From 3bd49b24561ce3c7be916ab0abbc78288721ddc4 Mon Sep 17 00:00:00 2001 From: Laslo Hunhold Date: Mon, 17 Aug 2020 10:33:55 +0200 Subject: [PATCH 01/12] Implement RFC 8615 (Well-Known URIs) and refine access errors We generally rejected any URI that had a path component beginning with a '.', i.e. a hidden file. RFC 8615 specifies the well-known URI, which is used, for instance, with the "http-01" challenge type in acme-client(1) and will probably see more usage in the future. To support it, we move the hidden target check after the stat(), so we don't have to worry about canonicalization of dir-URIs (i.e. missing trailing '/'). This changes the behaviour a bit, as now quark won't only send out a 403 whenever a hidden target is requested, but only if it actually exists, and a 404 otherwise. Given the earlier call to normabspath() ensures that our path begins with a '/', we don't need the first check "realtarget[0] == '.'" anymore, so it can be removed. Thanks to Robert Russell for reporting the lack of support of the RFC 8615 in quark. Signed-off-by: Laslo Hunhold --- http.c | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/http.c b/http.c index 49b30dc..6bf4264 100644 --- a/http.c +++ b/http.c @@ -600,11 +600,6 @@ http_send_response(int fd, const struct request *req) return http_send_status(fd, S_BAD_REQUEST); } - /* reject hidden target */ - if (realtarget[0] == '.' || strstr(realtarget, "/.")) { - return http_send_status(fd, S_FORBIDDEN); - } - /* stat the target */ if (stat(RELPATH(realtarget), &st) < 0) { return http_send_status(fd, (errno == EACCES) ? @@ -623,6 +618,15 @@ http_send_response(int fd, const struct request *req) } } + /* + * reject hidden target, except if it is a well-known URI + * according to RFC 8615 + */ + if (strstr(realtarget, "/.") && strncmp(realtarget, + "/.well-known/", sizeof("/.well-known/") - 1)) { + return http_send_status(fd, S_FORBIDDEN); + } + /* redirect if targets differ, host is non-canonical or we prefixed */ if (strcmp(req->target, realtarget) || (s.vhost && vhostmatch && strcmp(req->field[REQ_HOST], vhostmatch))) { From 65600ffe7a2868e95cf172550c85aa074e209e0d Mon Sep 17 00:00:00 2001 From: Laslo Hunhold Date: Mon, 17 Aug 2020 11:37:25 +0200 Subject: [PATCH 02/12] Reduce global state by localizing the server-struct The server-struct variable s was global, which made it readable and modifiable from any point in the code. Making it a local variable in main() instead and passing it as a pointer to constant memory to each function needing it makes much more sense and allows the compiler to warn us if we do try to modify it, which it wouldn't have before. Signed-off-by: Laslo Hunhold --- http.c | 48 ++++++++++++++++++++++++------------------------ http.h | 5 ++++- main.c | 16 ++++++---------- util.c | 1 - util.h | 4 ++-- 5 files changed, 36 insertions(+), 38 deletions(-) diff --git a/http.c b/http.c index 6bf4264..c9cf4dc 100644 --- a/http.c +++ b/http.c @@ -528,7 +528,7 @@ parse_range(const char *str, size_t size, size_t *lower, size_t *upper) #define RELPATH(x) ((!*(x) || !strcmp(x, "/")) ? "." : ((x) + 1)) enum status -http_send_response(int fd, const struct request *req) +http_send_response(int fd, const struct request *req, const struct server *s) { enum status returnstatus; struct in6_addr addr; @@ -547,27 +547,27 @@ http_send_response(int fd, const struct request *req) /* match vhost */ vhostmatch = NULL; - if (s.vhost) { - for (i = 0; i < s.vhost_len; i++) { + if (s->vhost) { + for (i = 0; i < s->vhost_len; i++) { /* switch to vhost directory if there is a match */ - if (!regexec(&s.vhost[i].re, req->field[REQ_HOST], 0, + if (!regexec(&(s->vhost[i].re), req->field[REQ_HOST], 0, NULL, 0)) { - if (chdir(s.vhost[i].dir) < 0) { + if (chdir(s->vhost[i].dir) < 0) { return http_send_status(fd, (errno == EACCES) ? S_FORBIDDEN : S_NOT_FOUND); } - vhostmatch = s.vhost[i].chost; + vhostmatch = s->vhost[i].chost; break; } } - if (i == s.vhost_len) { + if (i == s->vhost_len) { return http_send_status(fd, S_NOT_FOUND); } /* if we have a vhost prefix, prepend it to the target */ - if (s.vhost[i].prefix) { + if (s->vhost[i].prefix) { if (esnprintf(tmptarget, sizeof(tmptarget), "%s%s", - s.vhost[i].prefix, realtarget)) { + s->vhost[i].prefix, realtarget)) { return http_send_status(fd, S_REQUEST_TOO_LARGE); } memcpy(realtarget, tmptarget, sizeof(realtarget)); @@ -575,19 +575,19 @@ http_send_response(int fd, const struct request *req) } /* apply target prefix mapping */ - for (i = 0; i < s.map_len; i++) { - len = strlen(s.map[i].from); - if (!strncmp(realtarget, s.map[i].from, len)) { + for (i = 0; i < s->map_len; i++) { + len = strlen(s->map[i].from); + if (!strncmp(realtarget, s->map[i].from, len)) { /* match canonical host if vhosts are enabled and * the mapping specifies a canonical host */ - if (s.vhost && s.map[i].chost && - strcmp(s.map[i].chost, vhostmatch)) { + if (s->vhost && s->map[i].chost && + strcmp(s->map[i].chost, vhostmatch)) { continue; } /* swap out target prefix */ if (esnprintf(tmptarget, sizeof(tmptarget), "%s%s", - s.map[i].to, realtarget + len)) { + s->map[i].to, realtarget + len)) { return http_send_status(fd, S_REQUEST_TOO_LARGE); } memcpy(realtarget, tmptarget, sizeof(realtarget)); @@ -628,7 +628,7 @@ http_send_response(int fd, const struct request *req) } /* redirect if targets differ, host is non-canonical or we prefixed */ - if (strcmp(req->target, realtarget) || (s.vhost && vhostmatch && + if (strcmp(req->target, realtarget) || (s->vhost && vhostmatch && strcmp(req->field[REQ_HOST], vhostmatch))) { res.status = S_MOVED_PERMANENTLY; @@ -636,14 +636,14 @@ http_send_response(int fd, const struct request *req) encode(realtarget, tmptarget); /* determine target location */ - if (s.vhost) { + if (s->vhost) { /* absolute redirection URL */ targethost = req->field[REQ_HOST][0] ? vhostmatch ? - vhostmatch : req->field[REQ_HOST] : s.host ? - s.host : "localhost"; + vhostmatch : req->field[REQ_HOST] : s->host ? + s->host : "localhost"; /* do we need to add a port to the Location? */ - hasport = s.port && strcmp(s.port, "80"); + hasport = s->port && strcmp(s->port, "80"); /* RFC 2732 specifies to use brackets for IPv6-addresses * in URLs, so we need to check if our host is one and @@ -661,7 +661,7 @@ http_send_response(int fd, const struct request *req) ipv6host ? "[" : "", targethost, ipv6host ? "]" : "", hasport ? ":" : "", - hasport ? s.port : "", tmptarget)) { + hasport ? s->port : "", tmptarget)) { return http_send_status(fd, S_REQUEST_TOO_LARGE); } } else { @@ -679,16 +679,16 @@ http_send_response(int fd, const struct request *req) if (S_ISDIR(st.st_mode)) { /* append docindex to target */ if (esnprintf(realtarget, sizeof(realtarget), "%s%s", - req->target, s.docindex)) { + req->target, s->docindex)) { return http_send_status(fd, S_REQUEST_TOO_LARGE); } /* stat the docindex, which must be a regular file */ if (stat(RELPATH(realtarget), &st) < 0 || !S_ISREG(st.st_mode)) { - if (s.listdirs) { + if (s->listdirs) { /* remove index suffix and serve dir */ realtarget[strlen(realtarget) - - strlen(s.docindex)] = '\0'; + strlen(s->docindex)] = '\0'; return resp_dir(fd, RELPATH(realtarget), req); } else { /* reject */ diff --git a/http.h b/http.h index b6ba073..563d9fd 100644 --- a/http.h +++ b/http.h @@ -4,6 +4,8 @@ #include +#include "util.h" + #define HEADER_MAX 4096 #define FIELD_MAX 200 @@ -69,6 +71,7 @@ struct response { enum status http_send_header(int, const struct response *); enum status http_send_status(int, enum status); int http_get_request(int, struct request *); -enum status http_send_response(int, const struct request *); +enum status http_send_response(int, const struct request *, + const struct server *); #endif /* HTTP_H */ diff --git a/main.c b/main.c index 0542fab..ddd8468 100644 --- a/main.c +++ b/main.c @@ -23,7 +23,7 @@ static char *udsname; static void -serve(int infd, const struct sockaddr_storage *in_sa) +serve(int infd, const struct sockaddr_storage *in_sa, const struct server *s) { struct request req; time_t t; @@ -38,7 +38,7 @@ serve(int infd, const struct sockaddr_storage *in_sa) /* handle request */ if (!(status = http_get_request(infd, &req))) { - status = http_send_response(infd, &req); + status = http_send_response(infd, &req, s); } /* write output to log */ @@ -177,6 +177,9 @@ main(int argc, char *argv[]) struct group *grp = NULL; struct passwd *pwd = NULL; struct rlimit rlim; + struct server s = { + .docindex = "index.html", + }; struct sockaddr_storage in_sa; size_t i; socklen_t in_sa_len; @@ -190,13 +193,6 @@ main(int argc, char *argv[]) char *user = "nobody"; char *group = "nogroup"; - s.host = s.port = NULL; - s.vhost = NULL; - s.map = NULL; - s.vhost_len = s.map_len = 0; - s.docindex = "index.html"; - s.listdirs = 0; - ARGBEGIN { case 'd': servedir = EARGF(usage()); @@ -372,7 +368,7 @@ main(int argc, char *argv[]) /* fork and handle */ switch (fork()) { case 0: - serve(infd, &in_sa); + serve(infd, &in_sa, &s); exit(0); break; case -1: diff --git a/util.c b/util.c index 7ad512f..518a3eb 100644 --- a/util.c +++ b/util.c @@ -16,7 +16,6 @@ #include "util.h" char *argv0; -struct server s; static void verr(const char *fmt, va_list ap) diff --git a/util.h b/util.h index b23a192..21c18f8 100644 --- a/util.h +++ b/util.h @@ -23,7 +23,7 @@ struct map { char *to; }; -extern struct server { +struct server { char *host; char *port; char *docindex; @@ -32,7 +32,7 @@ extern struct server { size_t vhost_len; struct map *map; size_t map_len; -} s; +}; #undef MIN #define MIN(x,y) ((x) < (y) ? (x) : (y)) From ce77dd79624177387dbfc95cabae7c851b51b7fb Mon Sep 17 00:00:00 2001 From: Laslo Hunhold Date: Tue, 18 Aug 2020 08:46:52 +0200 Subject: [PATCH 03/12] Update manpage to list capabilities and behaviour Signed-off-by: Laslo Hunhold --- quark.1 | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/quark.1 b/quark.1 index a86f8e7..7188df2 100644 --- a/quark.1 +++ b/quark.1 @@ -1,4 +1,4 @@ -.Dd 2019-02-24 +.Dd 2020-08-18 .Dt QUARK 1 .Os suckless.org .Sh NAME @@ -30,6 +30,15 @@ .Sh DESCRIPTION .Nm is a simple HTTP GET/HEAD-only web server for static content. +It supports virtual hosts (see +.Fl v ) , +explicit redirects (see +.Fl m ) , +directory listings (see +.Fl l ) , +conditional "If-Modified-Since"-requests (RFC 7232), range requests +(RFC 7233) and well-known URIs (RFC 8615), while refusing to serve +hidden files and directories. .Sh OPTIONS .Bl -tag -width Ds .It Fl d Ar dir From 6d2fe7f29e12190d9be062852cf3f21b7f695369 Mon Sep 17 00:00:00 2001 From: Laslo Hunhold Date: Fri, 21 Aug 2020 19:38:29 +0200 Subject: [PATCH 04/12] Move infd and header into request-struct This compacts the connection state into one struct. Signed-off-by: Laslo Hunhold --- http.c | 86 +++++++++++++++++++++++++++++----------------------------- http.h | 6 ++-- main.c | 6 ++-- 3 files changed, 51 insertions(+), 47 deletions(-) diff --git a/http.c b/http.c index c9cf4dc..2a987cb 100644 --- a/http.c +++ b/http.c @@ -144,12 +144,12 @@ decode(const char src[PATH_MAX], char dest[PATH_MAX]) } int -http_get_request(int fd, struct request *req) +http_get_request(struct request *req) { struct in6_addr addr; size_t hlen, i, mlen; ssize_t off; - char h[HEADER_MAX], *p, *q; + char *h = req->header, *p, *q; /* empty all fields */ memset(req, 0, sizeof(*req)); @@ -158,8 +158,8 @@ http_get_request(int fd, struct request *req) * receive header */ for (hlen = 0; ;) { - if ((off = read(fd, h + hlen, sizeof(h) - hlen)) < 0) { - return http_send_status(fd, S_REQUEST_TIMEOUT); + if ((off = read(req->fd, h + hlen, sizeof(h) - hlen)) < 0) { + return http_send_status(req->fd, S_REQUEST_TIMEOUT); } else if (off == 0) { break; } @@ -168,13 +168,13 @@ http_get_request(int fd, struct request *req) break; } if (hlen == sizeof(h)) { - return http_send_status(fd, S_REQUEST_TOO_LARGE); + return http_send_status(req->fd, S_REQUEST_TOO_LARGE); } } /* remove terminating empty line */ if (hlen < 2) { - return http_send_status(fd, S_BAD_REQUEST); + return http_send_status(req->fd, S_BAD_REQUEST); } hlen -= 2; @@ -194,12 +194,12 @@ http_get_request(int fd, struct request *req) } } if (i == NUM_REQ_METHODS) { - return http_send_status(fd, S_METHOD_NOT_ALLOWED); + return http_send_status(req->fd, S_METHOD_NOT_ALLOWED); } /* a single space must follow the method */ if (h[mlen] != ' ') { - return http_send_status(fd, S_BAD_REQUEST); + return http_send_status(req->fd, S_BAD_REQUEST); } /* basis for next step */ @@ -207,11 +207,11 @@ http_get_request(int fd, struct request *req) /* TARGET */ if (!(q = strchr(p, ' '))) { - return http_send_status(fd, S_BAD_REQUEST); + return http_send_status(req->fd, S_BAD_REQUEST); } *q = '\0'; if (q - p + 1 > PATH_MAX) { - return http_send_status(fd, S_REQUEST_TOO_LARGE); + return http_send_status(req->fd, S_REQUEST_TOO_LARGE); } memcpy(req->target, p, q - p + 1); decode(req->target, req->target); @@ -221,18 +221,18 @@ http_get_request(int fd, struct request *req) /* HTTP-VERSION */ if (strncmp(p, "HTTP/", sizeof("HTTP/") - 1)) { - return http_send_status(fd, S_BAD_REQUEST); + return http_send_status(req->fd, S_BAD_REQUEST); } p += sizeof("HTTP/") - 1; if (strncmp(p, "1.0", sizeof("1.0") - 1) && strncmp(p, "1.1", sizeof("1.1") - 1)) { - return http_send_status(fd, S_VERSION_NOT_SUPPORTED); + return http_send_status(req->fd, S_VERSION_NOT_SUPPORTED); } p += sizeof("1.*") - 1; /* check terminator */ if (strncmp(p, "\r\n", sizeof("\r\n") - 1)) { - return http_send_status(fd, S_BAD_REQUEST); + return http_send_status(req->fd, S_BAD_REQUEST); } /* basis for next step */ @@ -253,7 +253,7 @@ http_get_request(int fd, struct request *req) if (i == NUM_REQ_FIELDS) { /* unmatched field, skip this line */ if (!(q = strstr(p, "\r\n"))) { - return http_send_status(fd, S_BAD_REQUEST); + return http_send_status(req->fd, S_BAD_REQUEST); } p = q + (sizeof("\r\n") - 1); continue; @@ -263,7 +263,7 @@ http_get_request(int fd, struct request *req) /* a single colon must follow the field name */ if (*p != ':') { - return http_send_status(fd, S_BAD_REQUEST); + return http_send_status(req->fd, S_BAD_REQUEST); } /* skip whitespace */ @@ -272,11 +272,11 @@ http_get_request(int fd, struct request *req) /* extract field content */ if (!(q = strstr(p, "\r\n"))) { - return http_send_status(fd, S_BAD_REQUEST); + return http_send_status(req->fd, S_BAD_REQUEST); } *q = '\0'; if (q - p + 1 > FIELD_MAX) { - return http_send_status(fd, S_REQUEST_TOO_LARGE); + return http_send_status(req->fd, S_REQUEST_TOO_LARGE); } memcpy(req->field[i], p, q - p + 1); @@ -296,7 +296,7 @@ http_get_request(int fd, struct request *req) if (p && (!q || p > q)) { /* port suffix must not be empty */ if (*(p + 1) == '\0') { - return http_send_status(fd, S_BAD_REQUEST); + return http_send_status(req->fd, S_BAD_REQUEST); } *p = '\0'; } @@ -305,7 +305,7 @@ http_get_request(int fd, struct request *req) if (q) { /* brackets must be on the outside */ if (req->field[REQ_HOST][0] != '[' || *(q + 1) != '\0') { - return http_send_status(fd, S_BAD_REQUEST); + return http_send_status(req->fd, S_BAD_REQUEST); } /* remove the right bracket */ @@ -314,7 +314,7 @@ http_get_request(int fd, struct request *req) /* validate the contained IPv6 address */ if (inet_pton(AF_INET6, p, &addr) != 1) { - return http_send_status(fd, S_BAD_REQUEST); + return http_send_status(req->fd, S_BAD_REQUEST); } /* copy it into the host field */ @@ -528,7 +528,7 @@ parse_range(const char *str, size_t size, size_t *lower, size_t *upper) #define RELPATH(x) ((!*(x) || !strcmp(x, "/")) ? "." : ((x) + 1)) enum status -http_send_response(int fd, const struct request *req, const struct server *s) +http_send_response(const struct request *req, const struct server *s) { enum status returnstatus; struct in6_addr addr; @@ -553,7 +553,7 @@ http_send_response(int fd, const struct request *req, const struct server *s) if (!regexec(&(s->vhost[i].re), req->field[REQ_HOST], 0, NULL, 0)) { if (chdir(s->vhost[i].dir) < 0) { - return http_send_status(fd, (errno == EACCES) ? + return http_send_status(req->fd, (errno == EACCES) ? S_FORBIDDEN : S_NOT_FOUND); } vhostmatch = s->vhost[i].chost; @@ -561,14 +561,14 @@ http_send_response(int fd, const struct request *req, const struct server *s) } } if (i == s->vhost_len) { - return http_send_status(fd, S_NOT_FOUND); + return http_send_status(req->fd, S_NOT_FOUND); } /* if we have a vhost prefix, prepend it to the target */ if (s->vhost[i].prefix) { if (esnprintf(tmptarget, sizeof(tmptarget), "%s%s", s->vhost[i].prefix, realtarget)) { - return http_send_status(fd, S_REQUEST_TOO_LARGE); + return http_send_status(req->fd, S_REQUEST_TOO_LARGE); } memcpy(realtarget, tmptarget, sizeof(realtarget)); } @@ -588,7 +588,7 @@ http_send_response(int fd, const struct request *req, const struct server *s) /* swap out target prefix */ if (esnprintf(tmptarget, sizeof(tmptarget), "%s%s", s->map[i].to, realtarget + len)) { - return http_send_status(fd, S_REQUEST_TOO_LARGE); + return http_send_status(req->fd, S_REQUEST_TOO_LARGE); } memcpy(realtarget, tmptarget, sizeof(realtarget)); break; @@ -597,12 +597,12 @@ http_send_response(int fd, const struct request *req, const struct server *s) /* normalize target */ if (normabspath(realtarget)) { - return http_send_status(fd, S_BAD_REQUEST); + return http_send_status(req->fd, S_BAD_REQUEST); } /* stat the target */ if (stat(RELPATH(realtarget), &st) < 0) { - return http_send_status(fd, (errno == EACCES) ? + return http_send_status(req->fd, (errno == EACCES) ? S_FORBIDDEN : S_NOT_FOUND); } @@ -610,7 +610,7 @@ http_send_response(int fd, const struct request *req, const struct server *s) /* add / to target if not present */ len = strlen(realtarget); if (len >= PATH_MAX - 2) { - return http_send_status(fd, S_REQUEST_TOO_LARGE); + return http_send_status(req->fd, S_REQUEST_TOO_LARGE); } if (len && realtarget[len - 1] != '/') { realtarget[len] = '/'; @@ -624,7 +624,7 @@ http_send_response(int fd, const struct request *req, const struct server *s) */ if (strstr(realtarget, "/.") && strncmp(realtarget, "/.well-known/", sizeof("/.well-known/") - 1)) { - return http_send_status(fd, S_FORBIDDEN); + return http_send_status(req->fd, S_FORBIDDEN); } /* redirect if targets differ, host is non-canonical or we prefixed */ @@ -650,7 +650,7 @@ http_send_response(int fd, const struct request *req, const struct server *s) * honor that later when we fill the "Location"-field */ if ((ipv6host = inet_pton(AF_INET6, targethost, &addr)) < 0) { - return http_send_status(fd, + return http_send_status(req->fd, S_INTERNAL_SERVER_ERROR); } @@ -662,25 +662,25 @@ http_send_response(int fd, const struct request *req, const struct server *s) targethost, ipv6host ? "]" : "", hasport ? ":" : "", hasport ? s->port : "", tmptarget)) { - return http_send_status(fd, S_REQUEST_TOO_LARGE); + return http_send_status(req->fd, S_REQUEST_TOO_LARGE); } } else { /* write relative redirection URL to response struct */ if (esnprintf(res.field[RES_LOCATION], sizeof(res.field[RES_LOCATION]), tmptarget)) { - return http_send_status(fd, S_REQUEST_TOO_LARGE); + return http_send_status(req->fd, S_REQUEST_TOO_LARGE); } } - return http_send_header(fd, &res); + return http_send_header(req->fd, &res); } if (S_ISDIR(st.st_mode)) { /* append docindex to target */ if (esnprintf(realtarget, sizeof(realtarget), "%s%s", req->target, s->docindex)) { - return http_send_status(fd, S_REQUEST_TOO_LARGE); + return http_send_status(req->fd, S_REQUEST_TOO_LARGE); } /* stat the docindex, which must be a regular file */ @@ -689,13 +689,13 @@ http_send_response(int fd, const struct request *req, const struct server *s) /* remove index suffix and serve dir */ realtarget[strlen(realtarget) - strlen(s->docindex)] = '\0'; - return resp_dir(fd, RELPATH(realtarget), req); + return resp_dir(req->fd, RELPATH(realtarget), req); } else { /* reject */ if (!S_ISREG(st.st_mode) || errno == EACCES) { - return http_send_status(fd, S_FORBIDDEN); + return http_send_status(req->fd, S_FORBIDDEN); } else { - return http_send_status(fd, S_NOT_FOUND); + return http_send_status(req->fd, S_NOT_FOUND); } } } @@ -706,13 +706,13 @@ http_send_response(int fd, const struct request *req, const struct server *s) /* parse field */ if (!strptime(req->field[REQ_IF_MODIFIED_SINCE], "%a, %d %b %Y %T GMT", &tm)) { - return http_send_status(fd, S_BAD_REQUEST); + return http_send_status(req->fd, S_BAD_REQUEST); } /* compare with last modification date of the file */ if (difftime(st.st_mtim.tv_sec, timegm(&tm)) <= 0) { res.status = S_NOT_MODIFIED; - return http_send_header(fd, &res); + return http_send_header(req->fd, &res); } } @@ -725,13 +725,13 @@ http_send_response(int fd, const struct request *req, const struct server *s) if (esnprintf(res.field[RES_CONTENT_RANGE], sizeof(res.field[RES_CONTENT_RANGE]), "bytes */%zu", st.st_size)) { - return http_send_status(fd, + return http_send_status(req->fd, S_INTERNAL_SERVER_ERROR); } - return http_send_header(fd, &res); + return http_send_header(req->fd, &res); } else { - return http_send_status(fd, returnstatus); + return http_send_status(req->fd, returnstatus); } } @@ -746,5 +746,5 @@ http_send_response(int fd, const struct request *req, const struct server *s) } } - return resp_file(fd, RELPATH(realtarget), req, &st, mime, lower, upper); + return resp_file(req->fd, RELPATH(realtarget), req, &st, mime, lower, upper); } diff --git a/http.h b/http.h index 563d9fd..60e38da 100644 --- a/http.h +++ b/http.h @@ -27,6 +27,8 @@ enum req_method { extern const char *req_method_str[]; struct request { + int fd; + char header[HEADER_MAX]; enum req_method method; char target[PATH_MAX]; char field[NUM_REQ_FIELDS][FIELD_MAX]; @@ -70,8 +72,8 @@ struct response { enum status http_send_header(int, const struct response *); enum status http_send_status(int, enum status); -int http_get_request(int, struct request *); -enum status http_send_response(int, const struct request *, +int http_get_request(struct request *); +enum status http_send_response(const struct request *, const struct server *); #endif /* HTTP_H */ diff --git a/main.c b/main.c index ddd8468..293f5c0 100644 --- a/main.c +++ b/main.c @@ -37,8 +37,10 @@ serve(int infd, const struct sockaddr_storage *in_sa, const struct server *s) } /* handle request */ - if (!(status = http_get_request(infd, &req))) { - status = http_send_response(infd, &req, s); + req.fd = infd; + + if (!(status = http_get_request(&req))) { + status = http_send_response(&req, s); } /* write output to log */ From c1b242e405d40067c282e8116d21c6f2641e4eee Mon Sep 17 00:00:00 2001 From: Laslo Hunhold Date: Sat, 22 Aug 2020 09:24:57 +0200 Subject: [PATCH 05/12] Add connection struct This struct contains the request and response structs, represents a state and has some utility-buffers. Signed-off-by: Laslo Hunhold --- http.c | 84 +++++++++++++++++++++++++++++----------------------------- http.h | 40 +++++++++++++++++++++++++--- main.c | 18 ++++++------- 3 files changed, 86 insertions(+), 56 deletions(-) diff --git a/http.c b/http.c index 2a987cb..3514f69 100644 --- a/http.c +++ b/http.c @@ -144,7 +144,7 @@ decode(const char src[PATH_MAX], char dest[PATH_MAX]) } int -http_get_request(struct request *req) +http_get_request(int fd, struct request *req) { struct in6_addr addr; size_t hlen, i, mlen; @@ -158,8 +158,8 @@ http_get_request(struct request *req) * receive header */ for (hlen = 0; ;) { - if ((off = read(req->fd, h + hlen, sizeof(h) - hlen)) < 0) { - return http_send_status(req->fd, S_REQUEST_TIMEOUT); + if ((off = read(fd, h + hlen, sizeof(h) - hlen)) < 0) { + return http_send_status(fd, S_REQUEST_TIMEOUT); } else if (off == 0) { break; } @@ -168,13 +168,13 @@ http_get_request(struct request *req) break; } if (hlen == sizeof(h)) { - return http_send_status(req->fd, S_REQUEST_TOO_LARGE); + return http_send_status(fd, S_REQUEST_TOO_LARGE); } } /* remove terminating empty line */ if (hlen < 2) { - return http_send_status(req->fd, S_BAD_REQUEST); + return http_send_status(fd, S_BAD_REQUEST); } hlen -= 2; @@ -194,12 +194,12 @@ http_get_request(struct request *req) } } if (i == NUM_REQ_METHODS) { - return http_send_status(req->fd, S_METHOD_NOT_ALLOWED); + return http_send_status(fd, S_METHOD_NOT_ALLOWED); } /* a single space must follow the method */ if (h[mlen] != ' ') { - return http_send_status(req->fd, S_BAD_REQUEST); + return http_send_status(fd, S_BAD_REQUEST); } /* basis for next step */ @@ -207,11 +207,11 @@ http_get_request(struct request *req) /* TARGET */ if (!(q = strchr(p, ' '))) { - return http_send_status(req->fd, S_BAD_REQUEST); + return http_send_status(fd, S_BAD_REQUEST); } *q = '\0'; if (q - p + 1 > PATH_MAX) { - return http_send_status(req->fd, S_REQUEST_TOO_LARGE); + return http_send_status(fd, S_REQUEST_TOO_LARGE); } memcpy(req->target, p, q - p + 1); decode(req->target, req->target); @@ -221,18 +221,18 @@ http_get_request(struct request *req) /* HTTP-VERSION */ if (strncmp(p, "HTTP/", sizeof("HTTP/") - 1)) { - return http_send_status(req->fd, S_BAD_REQUEST); + return http_send_status(fd, S_BAD_REQUEST); } p += sizeof("HTTP/") - 1; if (strncmp(p, "1.0", sizeof("1.0") - 1) && strncmp(p, "1.1", sizeof("1.1") - 1)) { - return http_send_status(req->fd, S_VERSION_NOT_SUPPORTED); + return http_send_status(fd, S_VERSION_NOT_SUPPORTED); } p += sizeof("1.*") - 1; /* check terminator */ if (strncmp(p, "\r\n", sizeof("\r\n") - 1)) { - return http_send_status(req->fd, S_BAD_REQUEST); + return http_send_status(fd, S_BAD_REQUEST); } /* basis for next step */ @@ -253,7 +253,7 @@ http_get_request(struct request *req) if (i == NUM_REQ_FIELDS) { /* unmatched field, skip this line */ if (!(q = strstr(p, "\r\n"))) { - return http_send_status(req->fd, S_BAD_REQUEST); + return http_send_status(fd, S_BAD_REQUEST); } p = q + (sizeof("\r\n") - 1); continue; @@ -263,7 +263,7 @@ http_get_request(struct request *req) /* a single colon must follow the field name */ if (*p != ':') { - return http_send_status(req->fd, S_BAD_REQUEST); + return http_send_status(fd, S_BAD_REQUEST); } /* skip whitespace */ @@ -272,11 +272,11 @@ http_get_request(struct request *req) /* extract field content */ if (!(q = strstr(p, "\r\n"))) { - return http_send_status(req->fd, S_BAD_REQUEST); + return http_send_status(fd, S_BAD_REQUEST); } *q = '\0'; if (q - p + 1 > FIELD_MAX) { - return http_send_status(req->fd, S_REQUEST_TOO_LARGE); + return http_send_status(fd, S_REQUEST_TOO_LARGE); } memcpy(req->field[i], p, q - p + 1); @@ -296,7 +296,7 @@ http_get_request(struct request *req) if (p && (!q || p > q)) { /* port suffix must not be empty */ if (*(p + 1) == '\0') { - return http_send_status(req->fd, S_BAD_REQUEST); + return http_send_status(fd, S_BAD_REQUEST); } *p = '\0'; } @@ -305,7 +305,7 @@ http_get_request(struct request *req) if (q) { /* brackets must be on the outside */ if (req->field[REQ_HOST][0] != '[' || *(q + 1) != '\0') { - return http_send_status(req->fd, S_BAD_REQUEST); + return http_send_status(fd, S_BAD_REQUEST); } /* remove the right bracket */ @@ -314,7 +314,7 @@ http_get_request(struct request *req) /* validate the contained IPv6 address */ if (inet_pton(AF_INET6, p, &addr) != 1) { - return http_send_status(req->fd, S_BAD_REQUEST); + return http_send_status(fd, S_BAD_REQUEST); } /* copy it into the host field */ @@ -528,7 +528,7 @@ parse_range(const char *str, size_t size, size_t *lower, size_t *upper) #define RELPATH(x) ((!*(x) || !strcmp(x, "/")) ? "." : ((x) + 1)) enum status -http_send_response(const struct request *req, const struct server *s) +http_send_response(int fd, const struct request *req, const struct server *s) { enum status returnstatus; struct in6_addr addr; @@ -553,7 +553,7 @@ http_send_response(const struct request *req, const struct server *s) if (!regexec(&(s->vhost[i].re), req->field[REQ_HOST], 0, NULL, 0)) { if (chdir(s->vhost[i].dir) < 0) { - return http_send_status(req->fd, (errno == EACCES) ? + return http_send_status(fd, (errno == EACCES) ? S_FORBIDDEN : S_NOT_FOUND); } vhostmatch = s->vhost[i].chost; @@ -561,14 +561,14 @@ http_send_response(const struct request *req, const struct server *s) } } if (i == s->vhost_len) { - return http_send_status(req->fd, S_NOT_FOUND); + return http_send_status(fd, S_NOT_FOUND); } /* if we have a vhost prefix, prepend it to the target */ if (s->vhost[i].prefix) { if (esnprintf(tmptarget, sizeof(tmptarget), "%s%s", s->vhost[i].prefix, realtarget)) { - return http_send_status(req->fd, S_REQUEST_TOO_LARGE); + return http_send_status(fd, S_REQUEST_TOO_LARGE); } memcpy(realtarget, tmptarget, sizeof(realtarget)); } @@ -588,7 +588,7 @@ http_send_response(const struct request *req, const struct server *s) /* swap out target prefix */ if (esnprintf(tmptarget, sizeof(tmptarget), "%s%s", s->map[i].to, realtarget + len)) { - return http_send_status(req->fd, S_REQUEST_TOO_LARGE); + return http_send_status(fd, S_REQUEST_TOO_LARGE); } memcpy(realtarget, tmptarget, sizeof(realtarget)); break; @@ -597,12 +597,12 @@ http_send_response(const struct request *req, const struct server *s) /* normalize target */ if (normabspath(realtarget)) { - return http_send_status(req->fd, S_BAD_REQUEST); + return http_send_status(fd, S_BAD_REQUEST); } /* stat the target */ if (stat(RELPATH(realtarget), &st) < 0) { - return http_send_status(req->fd, (errno == EACCES) ? + return http_send_status(fd, (errno == EACCES) ? S_FORBIDDEN : S_NOT_FOUND); } @@ -610,7 +610,7 @@ http_send_response(const struct request *req, const struct server *s) /* add / to target if not present */ len = strlen(realtarget); if (len >= PATH_MAX - 2) { - return http_send_status(req->fd, S_REQUEST_TOO_LARGE); + return http_send_status(fd, S_REQUEST_TOO_LARGE); } if (len && realtarget[len - 1] != '/') { realtarget[len] = '/'; @@ -624,7 +624,7 @@ http_send_response(const struct request *req, const struct server *s) */ if (strstr(realtarget, "/.") && strncmp(realtarget, "/.well-known/", sizeof("/.well-known/") - 1)) { - return http_send_status(req->fd, S_FORBIDDEN); + return http_send_status(fd, S_FORBIDDEN); } /* redirect if targets differ, host is non-canonical or we prefixed */ @@ -650,7 +650,7 @@ http_send_response(const struct request *req, const struct server *s) * honor that later when we fill the "Location"-field */ if ((ipv6host = inet_pton(AF_INET6, targethost, &addr)) < 0) { - return http_send_status(req->fd, + return http_send_status(fd, S_INTERNAL_SERVER_ERROR); } @@ -662,25 +662,25 @@ http_send_response(const struct request *req, const struct server *s) targethost, ipv6host ? "]" : "", hasport ? ":" : "", hasport ? s->port : "", tmptarget)) { - return http_send_status(req->fd, S_REQUEST_TOO_LARGE); + return http_send_status(fd, S_REQUEST_TOO_LARGE); } } else { /* write relative redirection URL to response struct */ if (esnprintf(res.field[RES_LOCATION], sizeof(res.field[RES_LOCATION]), tmptarget)) { - return http_send_status(req->fd, S_REQUEST_TOO_LARGE); + return http_send_status(fd, S_REQUEST_TOO_LARGE); } } - return http_send_header(req->fd, &res); + return http_send_header(fd, &res); } if (S_ISDIR(st.st_mode)) { /* append docindex to target */ if (esnprintf(realtarget, sizeof(realtarget), "%s%s", req->target, s->docindex)) { - return http_send_status(req->fd, S_REQUEST_TOO_LARGE); + return http_send_status(fd, S_REQUEST_TOO_LARGE); } /* stat the docindex, which must be a regular file */ @@ -689,13 +689,13 @@ http_send_response(const struct request *req, const struct server *s) /* remove index suffix and serve dir */ realtarget[strlen(realtarget) - strlen(s->docindex)] = '\0'; - return resp_dir(req->fd, RELPATH(realtarget), req); + return resp_dir(fd, RELPATH(realtarget), req); } else { /* reject */ if (!S_ISREG(st.st_mode) || errno == EACCES) { - return http_send_status(req->fd, S_FORBIDDEN); + return http_send_status(fd, S_FORBIDDEN); } else { - return http_send_status(req->fd, S_NOT_FOUND); + return http_send_status(fd, S_NOT_FOUND); } } } @@ -706,13 +706,13 @@ http_send_response(const struct request *req, const struct server *s) /* parse field */ if (!strptime(req->field[REQ_IF_MODIFIED_SINCE], "%a, %d %b %Y %T GMT", &tm)) { - return http_send_status(req->fd, S_BAD_REQUEST); + return http_send_status(fd, S_BAD_REQUEST); } /* compare with last modification date of the file */ if (difftime(st.st_mtim.tv_sec, timegm(&tm)) <= 0) { res.status = S_NOT_MODIFIED; - return http_send_header(req->fd, &res); + return http_send_header(fd, &res); } } @@ -725,13 +725,13 @@ http_send_response(const struct request *req, const struct server *s) if (esnprintf(res.field[RES_CONTENT_RANGE], sizeof(res.field[RES_CONTENT_RANGE]), "bytes */%zu", st.st_size)) { - return http_send_status(req->fd, + return http_send_status(fd, S_INTERNAL_SERVER_ERROR); } - return http_send_header(req->fd, &res); + return http_send_header(fd, &res); } else { - return http_send_status(req->fd, returnstatus); + return http_send_status(fd, returnstatus); } } @@ -746,5 +746,5 @@ http_send_response(const struct request *req, const struct server *s) } } - return resp_file(req->fd, RELPATH(realtarget), req, &st, mime, lower, upper); + return resp_file(fd, RELPATH(realtarget), req, &st, mime, lower, upper); } diff --git a/http.h b/http.h index 60e38da..44d0495 100644 --- a/http.h +++ b/http.h @@ -3,6 +3,7 @@ #define HTTP_H #include +#include #include "util.h" @@ -27,8 +28,8 @@ enum req_method { extern const char *req_method_str[]; struct request { - int fd; - char header[HEADER_MAX]; + char header[HEADER_MAX]; /* deprecated */ + enum req_method method; char target[PATH_MAX]; char field[NUM_REQ_FIELDS][FIELD_MAX]; @@ -65,15 +66,46 @@ enum res_field { extern const char *res_field_str[]; +enum res_type { + RESTYPE_FILE, + RESTYPE_DIR, + NUM_RES_TYPES, +}; + struct response { + enum res_type type; enum status status; char field[NUM_RES_FIELDS][FIELD_MAX]; + char path[PATH_MAX]; + struct stat st; + struct { + char *mime; + size_t lower; + size_t upper; + } file; +}; + +enum conn_state { + C_VACANT, + C_RECV_HEADER, + C_SEND_HEADER, + C_SEND_DATA, + NUM_CONN_STATES, +}; + +struct connection { + enum conn_state state; + int fd; + char header[HEADER_MAX]; /* general req/res-header buffer */ + size_t off; /* general offset (header/file/dir) */ + struct request req; + struct response res; }; enum status http_send_header(int, const struct response *); enum status http_send_status(int, enum status); -int http_get_request(struct request *); -enum status http_send_response(const struct request *, +int http_get_request(int fd, struct request *); +enum status http_send_response(int fd, const struct request *, const struct server *); #endif /* HTTP_H */ diff --git a/main.c b/main.c index 293f5c0..3b7cd32 100644 --- a/main.c +++ b/main.c @@ -25,22 +25,20 @@ static char *udsname; static void serve(int infd, const struct sockaddr_storage *in_sa, const struct server *s) { - struct request req; + struct connection c = { .fd = infd }; time_t t; enum status status; char inaddr[INET6_ADDRSTRLEN /* > INET_ADDRSTRLEN */]; char tstmp[21]; /* set connection timeout */ - if (sock_set_timeout(infd, 30)) { + if (sock_set_timeout(c.fd, 30)) { goto cleanup; } /* handle request */ - req.fd = infd; - - if (!(status = http_get_request(&req))) { - status = http_send_response(&req, s); + if (!(status = http_get_request(c.fd, &c.req))) { + status = http_send_response(c.fd, &c.req, s); } /* write output to log */ @@ -54,12 +52,12 @@ serve(int infd, const struct sockaddr_storage *in_sa, const struct server *s) goto cleanup; } printf("%s\t%s\t%d\t%s\t%s\n", tstmp, inaddr, status, - req.field[REQ_HOST], req.target); + c.req.field[REQ_HOST], c.req.target); cleanup: /* clean up and finish */ - shutdown(infd, SHUT_RD); - shutdown(infd, SHUT_WR); - close(infd); + shutdown(c.fd, SHUT_RD); + shutdown(c.fd, SHUT_WR); + close(c.fd); } static void From a5163d08135b81b271b1d7ba36b24e342b57961f Mon Sep 17 00:00:00 2001 From: Laslo Hunhold Date: Sat, 22 Aug 2020 11:05:20 +0200 Subject: [PATCH 06/12] Split up http_get_request() The function has become too long and basically did two things: Receiving the header and parsing it. To better reflect this, we split it up into the two functions http_recv_header() and http_parse_header(). This way, we also obtain a better separation of concerns and can further reduce the scope of each parameter-list. http_recv_header() has been written in such a way that it can be reentered and fill up the header-buffer bit by bit using a pointer to an offset value. The error handling was improved by only returning the immediate error status codes and letting the caller do the error-handling with http_send_status(). Signed-off-by: Laslo Hunhold --- http.c | 130 ++++++++++++++++++++++++++++++--------------------------- http.h | 3 +- main.c | 6 ++- 3 files changed, 76 insertions(+), 63 deletions(-) diff --git a/http.c b/http.c index 3514f69..6c0b519 100644 --- a/http.c +++ b/http.c @@ -143,44 +143,52 @@ decode(const char src[PATH_MAX], char dest[PATH_MAX]) dest[i] = '\0'; } -int -http_get_request(int fd, struct request *req) +enum status +http_recv_header(int fd, char *h, size_t hsiz, size_t *off) +{ + ssize_t r; + + if (h == NULL || off == NULL || *off > hsiz) { + return S_INTERNAL_SERVER_ERROR; + } + + while (1) { + if ((r = read(fd, h + *off, hsiz - *off)) <= 0) { + return S_REQUEST_TIMEOUT; + } + *off += r; + + /* check if we are done (header terminated) */ + if (*off >= 4 && !memcmp(h + *off - 4, "\r\n\r\n", 4)) { + break; + } + + /* buffer is full or read over, but header is not terminated */ + if (r == 0 || *off == hsiz) { + return S_REQUEST_TOO_LARGE; + } + } + + /* header is complete, remove last \r\n and null-terminate */ + h[*off - 2] = '\0'; + + /* set *off to 0 to indicate we are finished */ + *off = 0; + + return 0; +} + +enum status +http_parse_header(const char *h, struct request *req) { struct in6_addr addr; - size_t hlen, i, mlen; - ssize_t off; - char *h = req->header, *p, *q; + size_t i, mlen; + const char *p, *q; + char *m, *n; /* empty all fields */ memset(req, 0, sizeof(*req)); - /* - * receive header - */ - for (hlen = 0; ;) { - if ((off = read(fd, h + hlen, sizeof(h) - hlen)) < 0) { - return http_send_status(fd, S_REQUEST_TIMEOUT); - } else if (off == 0) { - break; - } - hlen += off; - if (hlen >= 4 && !memcmp(h + hlen - 4, "\r\n\r\n", 4)) { - break; - } - if (hlen == sizeof(h)) { - return http_send_status(fd, S_REQUEST_TOO_LARGE); - } - } - - /* remove terminating empty line */ - if (hlen < 2) { - return http_send_status(fd, S_BAD_REQUEST); - } - hlen -= 2; - - /* null-terminate the header */ - h[hlen] = '\0'; - /* * parse request line */ @@ -194,12 +202,12 @@ http_get_request(int fd, struct request *req) } } if (i == NUM_REQ_METHODS) { - return http_send_status(fd, S_METHOD_NOT_ALLOWED); + return S_METHOD_NOT_ALLOWED; } /* a single space must follow the method */ if (h[mlen] != ' ') { - return http_send_status(fd, S_BAD_REQUEST); + return S_BAD_REQUEST; } /* basis for next step */ @@ -207,13 +215,13 @@ http_get_request(int fd, struct request *req) /* TARGET */ if (!(q = strchr(p, ' '))) { - return http_send_status(fd, S_BAD_REQUEST); + return S_BAD_REQUEST; } - *q = '\0'; if (q - p + 1 > PATH_MAX) { - return http_send_status(fd, S_REQUEST_TOO_LARGE); + return S_REQUEST_TOO_LARGE; } - memcpy(req->target, p, q - p + 1); + memcpy(req->target, p, q - p); + req->target[q - p] = '\0'; decode(req->target, req->target); /* basis for next step */ @@ -221,18 +229,18 @@ http_get_request(int fd, struct request *req) /* HTTP-VERSION */ if (strncmp(p, "HTTP/", sizeof("HTTP/") - 1)) { - return http_send_status(fd, S_BAD_REQUEST); + return S_BAD_REQUEST; } p += sizeof("HTTP/") - 1; if (strncmp(p, "1.0", sizeof("1.0") - 1) && strncmp(p, "1.1", sizeof("1.1") - 1)) { - return http_send_status(fd, S_VERSION_NOT_SUPPORTED); + return S_VERSION_NOT_SUPPORTED; } p += sizeof("1.*") - 1; /* check terminator */ if (strncmp(p, "\r\n", sizeof("\r\n") - 1)) { - return http_send_status(fd, S_BAD_REQUEST); + return S_BAD_REQUEST; } /* basis for next step */ @@ -253,7 +261,7 @@ http_get_request(int fd, struct request *req) if (i == NUM_REQ_FIELDS) { /* unmatched field, skip this line */ if (!(q = strstr(p, "\r\n"))) { - return http_send_status(fd, S_BAD_REQUEST); + return S_BAD_REQUEST; } p = q + (sizeof("\r\n") - 1); continue; @@ -263,7 +271,7 @@ http_get_request(int fd, struct request *req) /* a single colon must follow the field name */ if (*p != ':') { - return http_send_status(fd, S_BAD_REQUEST); + return S_BAD_REQUEST; } /* skip whitespace */ @@ -272,13 +280,13 @@ http_get_request(int fd, struct request *req) /* extract field content */ if (!(q = strstr(p, "\r\n"))) { - return http_send_status(fd, S_BAD_REQUEST); + return S_BAD_REQUEST; } - *q = '\0'; if (q - p + 1 > FIELD_MAX) { - return http_send_status(fd, S_REQUEST_TOO_LARGE); + return S_REQUEST_TOO_LARGE; } - memcpy(req->field[i], p, q - p + 1); + memcpy(req->field[i], p, q - p); + req->field[i][q - p] = '\0'; /* go to next line */ p = q + (sizeof("\r\n") - 1); @@ -288,37 +296,37 @@ http_get_request(int fd, struct request *req) * clean up host */ - p = strrchr(req->field[REQ_HOST], ':'); - q = strrchr(req->field[REQ_HOST], ']'); + m = strrchr(req->field[REQ_HOST], ':'); + n = strrchr(req->field[REQ_HOST], ']'); /* strip port suffix but don't interfere with IPv6 bracket notation * as per RFC 2732 */ - if (p && (!q || p > q)) { + if (m && (!n || m > n)) { /* port suffix must not be empty */ - if (*(p + 1) == '\0') { - return http_send_status(fd, S_BAD_REQUEST); + if (*(m + 1) == '\0') { + return S_BAD_REQUEST; } - *p = '\0'; + *m = '\0'; } /* strip the brackets from the IPv6 notation and validate the address */ - if (q) { + if (n) { /* brackets must be on the outside */ - if (req->field[REQ_HOST][0] != '[' || *(q + 1) != '\0') { - return http_send_status(fd, S_BAD_REQUEST); + if (req->field[REQ_HOST][0] != '[' || *(n + 1) != '\0') { + return S_BAD_REQUEST; } /* remove the right bracket */ - *q = '\0'; - p = req->field[REQ_HOST] + 1; + *n = '\0'; + m = req->field[REQ_HOST] + 1; /* validate the contained IPv6 address */ - if (inet_pton(AF_INET6, p, &addr) != 1) { - return http_send_status(fd, S_BAD_REQUEST); + if (inet_pton(AF_INET6, m, &addr) != 1) { + return S_BAD_REQUEST; } /* copy it into the host field */ - memmove(req->field[REQ_HOST], p, q - p + 1); + memmove(req->field[REQ_HOST], m, n - m + 1); } return 0; diff --git a/http.h b/http.h index 44d0495..0b1f6be 100644 --- a/http.h +++ b/http.h @@ -104,7 +104,8 @@ struct connection { enum status http_send_header(int, const struct response *); enum status http_send_status(int, enum status); -int http_get_request(int fd, struct request *); +enum status http_recv_header(int, char *, size_t, size_t *); +enum status http_parse_header(const char *, struct request *); enum status http_send_response(int fd, const struct request *, const struct server *); diff --git a/main.c b/main.c index 3b7cd32..a3b8fa3 100644 --- a/main.c +++ b/main.c @@ -37,7 +37,11 @@ serve(int infd, const struct sockaddr_storage *in_sa, const struct server *s) } /* handle request */ - if (!(status = http_get_request(c.fd, &c.req))) { + if ((status = http_recv_header(c.fd, c.header, LEN(c.header), &c.off))) { + status = http_send_status(c.fd, status); + } else if ((status = http_parse_header(c.header, &c.req))) { + status = http_send_status(c.fd, status); + } else { status = http_send_response(c.fd, &c.req, s); } From 58d0f44e0395fe37b3575da35992b3d3e7f262d7 Mon Sep 17 00:00:00 2001 From: Laslo Hunhold Date: Sat, 22 Aug 2020 23:20:00 +0200 Subject: [PATCH 07/12] Refactor http_send_response() into http_prepare_response() The function http_send_response() did too much. It not only took the request fields and built them together into a response, it delegated too little and many functions were "hacked" into it, for instance shady directory-changes for vhosts and hand-construction of response structs. The preparations for a rework were already made in previous commits, including a tighter focus on the response-struct itself. Instead of doing everything locally in the http_send_response() function, the new http_prepare_response() only really takes the request-struct and builds a response-struct. The response-struct is expanded such that it's possible to do the data-sending simply with the response-struct itself and not any other magic parameters that just drop out of the function. Another matter are the http_send_status()-calls. Because the aforementioned function is so central, this refactoring has included many areas. Instead of calling http_send_status() in every error-case, which makes little sense now given we first delegate everything through a response struct, errors are just sent as a return value and caught centrally (in serve() in main.c), which centralizes the error handling a bit. It might look a bit strange now and it might not be clear in which direction this is going, but subsequent commits will hopefully give clarity in this regard. Signed-off-by: Laslo Hunhold --- http.c | 261 ++++++++++++++++++++++++++++++++++++--------------------- http.h | 11 ++- main.c | 16 +++- resp.c | 208 +++++++++++---------------------------------- resp.h | 5 +- util.c | 60 +++++++++++++ util.h | 2 + 7 files changed, 297 insertions(+), 266 deletions(-) diff --git a/http.c b/http.c index 6c0b519..b8ae1aa 100644 --- a/http.c +++ b/http.c @@ -61,7 +61,7 @@ const char *res_field_str[] = { enum status http_send_header(int fd, const struct response *res) { - char t[FIELD_MAX]; + char t[FIELD_MAX], esc[PATH_MAX]; size_t i; if (timestamp(t, sizeof(t), time(NULL))) { @@ -89,6 +89,18 @@ http_send_header(int fd, const struct response *res) return S_REQUEST_TIMEOUT; } + /* listing header */ + if (res->type == RESTYPE_DIRLISTING) { + html_escape(res->uri, esc, sizeof(esc)); + if (dprintf(fd, + "\n\n\t" + "Index of %s\n" + "\t\n\t\t..", + esc) < 0) { + return S_REQUEST_TIMEOUT; + } + } + return res->status; } @@ -536,93 +548,90 @@ parse_range(const char *str, size_t size, size_t *lower, size_t *upper) #define RELPATH(x) ((!*(x) || !strcmp(x, "/")) ? "." : ((x) + 1)) enum status -http_send_response(int fd, const struct request *req, const struct server *s) +http_prepare_response(const struct request *req, struct response *res, + const struct server *s) { enum status returnstatus; struct in6_addr addr; - struct response res = { 0 }; struct stat st; struct tm tm = { 0 }; + struct vhost *vhost; size_t len, i; - size_t lower, upper; int hasport, ipv6host; - static char realtarget[PATH_MAX], tmptarget[PATH_MAX]; + static char realuri[PATH_MAX], tmpuri[PATH_MAX]; char *p, *mime; - const char *vhostmatch, *targethost; + const char *targethost; - /* make a working copy of the target */ - memcpy(realtarget, req->target, sizeof(realtarget)); + /* empty all response fields */ + memset(res, 0, sizeof(*res)); + + /* make a working copy of the URI and normalize it */ + memcpy(realuri, req->target, sizeof(realuri)); + if (normabspath(realuri)) { + return S_BAD_REQUEST; + } /* match vhost */ - vhostmatch = NULL; + vhost = NULL; if (s->vhost) { for (i = 0; i < s->vhost_len; i++) { - /* switch to vhost directory if there is a match */ - if (!regexec(&(s->vhost[i].re), req->field[REQ_HOST], 0, - NULL, 0)) { - if (chdir(s->vhost[i].dir) < 0) { - return http_send_status(fd, (errno == EACCES) ? - S_FORBIDDEN : S_NOT_FOUND); - } - vhostmatch = s->vhost[i].chost; + if (!regexec(&(s->vhost[i].re), req->field[REQ_HOST], + 0, NULL, 0)) { + /* we have a matching vhost */ + vhost = &(s->vhost[i]); break; } } if (i == s->vhost_len) { - return http_send_status(fd, S_NOT_FOUND); + return S_NOT_FOUND; } - /* if we have a vhost prefix, prepend it to the target */ - if (s->vhost[i].prefix) { - if (esnprintf(tmptarget, sizeof(tmptarget), "%s%s", - s->vhost[i].prefix, realtarget)) { - return http_send_status(fd, S_REQUEST_TOO_LARGE); - } - memcpy(realtarget, tmptarget, sizeof(realtarget)); + /* if we have a vhost prefix, prepend it to the URI */ + if (vhost->prefix && + prepend(realuri, LEN(realuri), vhost->prefix)) { + return S_REQUEST_TOO_LARGE; } } /* apply target prefix mapping */ for (i = 0; i < s->map_len; i++) { len = strlen(s->map[i].from); - if (!strncmp(realtarget, s->map[i].from, len)) { + if (!strncmp(realuri, s->map[i].from, len)) { /* match canonical host if vhosts are enabled and * the mapping specifies a canonical host */ if (s->vhost && s->map[i].chost && - strcmp(s->map[i].chost, vhostmatch)) { + strcmp(s->map[i].chost, vhost->chost)) { continue; } /* swap out target prefix */ - if (esnprintf(tmptarget, sizeof(tmptarget), "%s%s", - s->map[i].to, realtarget + len)) { - return http_send_status(fd, S_REQUEST_TOO_LARGE); + memmove(realuri, realuri + len, strlen(realuri) + 1); + if (prepend(realuri, LEN(realuri), s->map[i].to)) { + return S_REQUEST_TOO_LARGE; } - memcpy(realtarget, tmptarget, sizeof(realtarget)); break; } } - /* normalize target */ - if (normabspath(realtarget)) { - return http_send_status(fd, S_BAD_REQUEST); + /* normalize URI again, in case we introduced dirt */ + if (normabspath(realuri)) { + return S_BAD_REQUEST; } - /* stat the target */ - if (stat(RELPATH(realtarget), &st) < 0) { - return http_send_status(fd, (errno == EACCES) ? - S_FORBIDDEN : S_NOT_FOUND); + /* stat the relative path derived from the URI */ + if (stat(RELPATH(realuri), &st) < 0) { + return (errno == EACCES) ? S_FORBIDDEN : S_NOT_FOUND; } if (S_ISDIR(st.st_mode)) { - /* add / to target if not present */ - len = strlen(realtarget); - if (len >= PATH_MAX - 2) { - return http_send_status(fd, S_REQUEST_TOO_LARGE); + /* append '/' to URI if not present */ + len = strlen(realuri); + if (len + 1 + 1 > PATH_MAX) { + return S_REQUEST_TOO_LARGE; } - if (len && realtarget[len - 1] != '/') { - realtarget[len] = '/'; - realtarget[len + 1] = '\0'; + if (len > 0 && realuri[len - 1] != '/') { + realuri[len] = '/'; + realuri[len + 1] = '\0'; } } @@ -630,24 +639,27 @@ http_send_response(int fd, const struct request *req, const struct server *s) * reject hidden target, except if it is a well-known URI * according to RFC 8615 */ - if (strstr(realtarget, "/.") && strncmp(realtarget, + if (strstr(realuri, "/.") && strncmp(realuri, "/.well-known/", sizeof("/.well-known/") - 1)) { - return http_send_status(fd, S_FORBIDDEN); + return S_FORBIDDEN; } - /* redirect if targets differ, host is non-canonical or we prefixed */ - if (strcmp(req->target, realtarget) || (s->vhost && vhostmatch && - strcmp(req->field[REQ_HOST], vhostmatch))) { - res.status = S_MOVED_PERMANENTLY; + /* + * redirect if the original URI and the "real" URI differ or if + * the requested host is non-canonical + */ + if (strcmp(req->target, realuri) || (s->vhost && vhost && + strcmp(req->field[REQ_HOST], vhost->chost))) { + res->status = S_MOVED_PERMANENTLY; - /* encode realtarget */ - encode(realtarget, tmptarget); + /* encode realuri */ + encode(realuri, tmpuri); /* determine target location */ if (s->vhost) { /* absolute redirection URL */ - targethost = req->field[REQ_HOST][0] ? vhostmatch ? - vhostmatch : req->field[REQ_HOST] : s->host ? + targethost = req->field[REQ_HOST][0] ? vhost->chost ? + vhost->chost : req->field[REQ_HOST] : s->host ? s->host : "localhost"; /* do we need to add a port to the Location? */ @@ -658,53 +670,74 @@ http_send_response(int fd, const struct request *req, const struct server *s) * honor that later when we fill the "Location"-field */ if ((ipv6host = inet_pton(AF_INET6, targethost, &addr)) < 0) { - return http_send_status(fd, - S_INTERNAL_SERVER_ERROR); + return S_INTERNAL_SERVER_ERROR; } /* write location to response struct */ - if (esnprintf(res.field[RES_LOCATION], - sizeof(res.field[RES_LOCATION]), + if (esnprintf(res->field[RES_LOCATION], + sizeof(res->field[RES_LOCATION]), "//%s%s%s%s%s%s", ipv6host ? "[" : "", targethost, ipv6host ? "]" : "", hasport ? ":" : "", - hasport ? s->port : "", tmptarget)) { - return http_send_status(fd, S_REQUEST_TOO_LARGE); + hasport ? s->port : "", tmpuri)) { + return S_REQUEST_TOO_LARGE; } } else { - /* write relative redirection URL to response struct */ - if (esnprintf(res.field[RES_LOCATION], - sizeof(res.field[RES_LOCATION]), - tmptarget)) { - return http_send_status(fd, S_REQUEST_TOO_LARGE); + /* write relative redirection URI to response struct */ + if (esnprintf(res->field[RES_LOCATION], + sizeof(res->field[RES_LOCATION]), + "%s", tmpuri)) { + return S_REQUEST_TOO_LARGE; } } - return http_send_header(fd, &res); + return 0; + } else { + /* + * the URI is well-formed, we can now write the URI into + * the response-URI and corresponding relative path + * (optionally including the vhost servedir as a prefix) + * into the actual response-path + */ + if (esnprintf(res->uri, sizeof(res->uri), "%s", req->target)) { + return S_REQUEST_TOO_LARGE; + } + if (esnprintf(res->path, sizeof(res->path), "%s%s", + vhost ? vhost->dir : "", RELPATH(req->target))) { + return S_REQUEST_TOO_LARGE; + } } if (S_ISDIR(st.st_mode)) { - /* append docindex to target */ - if (esnprintf(realtarget, sizeof(realtarget), "%s%s", + /* + * check if the directory index exists by appending it to + * the URI + */ + if (esnprintf(tmpuri, sizeof(tmpuri), "%s%s", req->target, s->docindex)) { - return http_send_status(fd, S_REQUEST_TOO_LARGE); + return S_REQUEST_TOO_LARGE; } /* stat the docindex, which must be a regular file */ - if (stat(RELPATH(realtarget), &st) < 0 || !S_ISREG(st.st_mode)) { + if (stat(RELPATH(tmpuri), &st) < 0 || !S_ISREG(st.st_mode)) { if (s->listdirs) { - /* remove index suffix and serve dir */ - realtarget[strlen(realtarget) - - strlen(s->docindex)] = '\0'; - return resp_dir(fd, RELPATH(realtarget), req); + /* serve directory listing */ + res->type = RESTYPE_DIRLISTING; + res->status = (access(res->path, R_OK)) ? + S_FORBIDDEN : S_OK; + + if (esnprintf(res->field[RES_CONTENT_TYPE], + sizeof(res->field[RES_CONTENT_TYPE]), + "%s", "text/html; charset=utf-8")) { + return S_INTERNAL_SERVER_ERROR; + } + + return 0; } else { /* reject */ - if (!S_ISREG(st.st_mode) || errno == EACCES) { - return http_send_status(fd, S_FORBIDDEN); - } else { - return http_send_status(fd, S_NOT_FOUND); - } + return (!S_ISREG(st.st_mode) || errno == EACCES) ? + S_FORBIDDEN : S_NOT_FOUND; } } } @@ -714,39 +747,39 @@ http_send_response(int fd, const struct request *req, const struct server *s) /* parse field */ if (!strptime(req->field[REQ_IF_MODIFIED_SINCE], "%a, %d %b %Y %T GMT", &tm)) { - return http_send_status(fd, S_BAD_REQUEST); + return S_BAD_REQUEST; } /* compare with last modification date of the file */ if (difftime(st.st_mtim.tv_sec, timegm(&tm)) <= 0) { - res.status = S_NOT_MODIFIED; - return http_send_header(fd, &res); + res->status = S_NOT_MODIFIED; + return 0; } } /* range */ - if ((returnstatus = parse_range(req->field[REQ_RANGE], - st.st_size, &lower, &upper))) { + if ((returnstatus = parse_range(req->field[REQ_RANGE], st.st_size, + &(res->file.lower), + &(res->file.upper)))) { if (returnstatus == S_RANGE_NOT_SATISFIABLE) { - res.status = S_RANGE_NOT_SATISFIABLE; + res->status = S_RANGE_NOT_SATISFIABLE; - if (esnprintf(res.field[RES_CONTENT_RANGE], - sizeof(res.field[RES_CONTENT_RANGE]), + if (esnprintf(res->field[RES_CONTENT_RANGE], + sizeof(res->field[RES_CONTENT_RANGE]), "bytes */%zu", st.st_size)) { - return http_send_status(fd, - S_INTERNAL_SERVER_ERROR); + return S_INTERNAL_SERVER_ERROR; } - return http_send_header(fd, &res); + return 0; } else { - return http_send_status(fd, returnstatus); + return returnstatus; } } /* mime */ mime = "application/octet-stream"; - if ((p = strrchr(realtarget, '.'))) { - for (i = 0; i < sizeof(mimes) / sizeof(*mimes); i++) { + if ((p = strrchr(realuri, '.'))) { + for (i = 0; i < LEN(mimes); i++) { if (!strcmp(mimes[i].ext, p + 1)) { mime = mimes[i].type; break; @@ -754,5 +787,43 @@ http_send_response(int fd, const struct request *req, const struct server *s) } } - return resp_file(fd, RELPATH(realtarget), req, &st, mime, lower, upper); + /* fill response struct */ + res->type = RESTYPE_FILE; + + /* check if file is readable */ + res->status = (access(res->path, R_OK)) ? S_FORBIDDEN : + (req->field[REQ_RANGE][0] != '\0') ? + S_PARTIAL_CONTENT : S_OK; + + if (esnprintf(res->field[RES_ACCEPT_RANGES], + sizeof(res->field[RES_ACCEPT_RANGES]), + "%s", "bytes")) { + return S_INTERNAL_SERVER_ERROR; + } + + if (esnprintf(res->field[RES_CONTENT_LENGTH], + sizeof(res->field[RES_CONTENT_LENGTH]), + "%zu", res->file.upper - res->file.lower + 1)) { + return S_INTERNAL_SERVER_ERROR; + } + if (req->field[REQ_RANGE][0] != '\0') { + if (esnprintf(res->field[RES_CONTENT_RANGE], + sizeof(res->field[RES_CONTENT_RANGE]), + "bytes %zd-%zd/%zu", res->file.lower, + res->file.upper, st.st_size)) { + return S_INTERNAL_SERVER_ERROR; + } + } + if (esnprintf(res->field[RES_CONTENT_TYPE], + sizeof(res->field[RES_CONTENT_TYPE]), + "%s", mime)) { + return S_INTERNAL_SERVER_ERROR; + } + if (timestamp(res->field[RES_LAST_MODIFIED], + sizeof(res->field[RES_LAST_MODIFIED]), + st.st_mtim.tv_sec)) { + return S_INTERNAL_SERVER_ERROR; + } + + return 0; } diff --git a/http.h b/http.h index 0b1f6be..5226dca 100644 --- a/http.h +++ b/http.h @@ -3,7 +3,6 @@ #define HTTP_H #include -#include #include "util.h" @@ -67,8 +66,9 @@ enum res_field { extern const char *res_field_str[]; enum res_type { + RESTYPE_ERROR, RESTYPE_FILE, - RESTYPE_DIR, + RESTYPE_DIRLISTING, NUM_RES_TYPES, }; @@ -76,10 +76,9 @@ struct response { enum res_type type; enum status status; char field[NUM_RES_FIELDS][FIELD_MAX]; + char uri[PATH_MAX]; char path[PATH_MAX]; - struct stat st; struct { - char *mime; size_t lower; size_t upper; } file; @@ -106,7 +105,7 @@ enum status http_send_header(int, const struct response *); enum status http_send_status(int, enum status); enum status http_recv_header(int, char *, size_t, size_t *); enum status http_parse_header(const char *, struct request *); -enum status http_send_response(int fd, const struct request *, - const struct server *); +enum status http_prepare_response(const struct request *, struct response *, + const struct server *); #endif /* HTTP_H */ diff --git a/main.c b/main.c index a3b8fa3..bb7233c 100644 --- a/main.c +++ b/main.c @@ -16,6 +16,7 @@ #include #include +#include "resp.h" #include "http.h" #include "sock.h" #include "util.h" @@ -37,12 +38,19 @@ serve(int infd, const struct sockaddr_storage *in_sa, const struct server *s) } /* handle request */ - if ((status = http_recv_header(c.fd, c.header, LEN(c.header), &c.off))) { - status = http_send_status(c.fd, status); - } else if ((status = http_parse_header(c.header, &c.req))) { + if ((status = http_recv_header(c.fd, c.header, LEN(c.header), &c.off)) || + (status = http_parse_header(c.header, &c.req)) || + (status = http_prepare_response(&c.req, &c.res, s))) { status = http_send_status(c.fd, status); } else { - status = http_send_response(c.fd, &c.req, s); + status = http_send_header(c.fd, &c.res); + + /* send data */ + if (c.res.type == RESTYPE_FILE) { + resp_file(c.fd, &c.res); + } else if (c.res.type == RESTYPE_DIRLISTING) { + resp_dir(c.fd, &c.res); + } } /* write output to log */ diff --git a/resp.c b/resp.c index 2f639e5..b7441dc 100644 --- a/resp.c +++ b/resp.c @@ -38,202 +38,94 @@ suffix(int t) return ""; } -static void -html_escape(const char *src, char *dst, size_t dst_siz) -{ - const struct { - char c; - char *s; - } escape[] = { - { '&', "&" }, - { '<', "<" }, - { '>', ">" }, - { '"', """ }, - { '\'', "'" }, - }; - size_t i, j, k, esclen; - - for (i = 0, j = 0; src[i] != '\0'; i++) { - for (k = 0; k < LEN(escape); k++) { - if (src[i] == escape[k].c) { - break; - } - } - if (k == LEN(escape)) { - /* no escape char at src[i] */ - if (j == dst_siz - 1) { - /* silent truncation */ - break; - } else { - dst[j++] = src[i]; - } - } else { - /* escape char at src[i] */ - esclen = strlen(escape[k].s); - - if (j >= dst_siz - esclen) { - /* silent truncation */ - break; - } else { - memcpy(&dst[j], escape[k].s, esclen); - j += esclen; - } - } - } - dst[j] = '\0'; -} - enum status -resp_dir(int fd, const char *name, const struct request *req) +resp_dir(int fd, const struct response *res) { - enum status sendstatus; + enum status ret; struct dirent **e; - struct response res = { - .status = S_OK, - .field[RES_CONTENT_TYPE] = "text/html; charset=utf-8", - }; size_t i; int dirlen; char esc[PATH_MAX /* > NAME_MAX */ * 6]; /* strlen("&...;") <= 6 */ /* read directory */ - if ((dirlen = scandir(name, &e, NULL, compareent)) < 0) { - return http_send_status(fd, S_FORBIDDEN); + if ((dirlen = scandir(res->path, &e, NULL, compareent)) < 0) { + return S_FORBIDDEN; } - /* send header as late as possible */ - if ((sendstatus = http_send_header(fd, &res)) != res.status) { - res.status = sendstatus; + /* listing */ + for (i = 0; i < (size_t)dirlen; i++) { + /* skip hidden files, "." and ".." */ + if (e[i]->d_name[0] == '.') { + continue; + } + + /* entry line */ + html_escape(e[i]->d_name, esc, sizeof(esc)); + if (dprintf(fd, "
\n\t\t%s%s", + esc, + (e[i]->d_type == DT_DIR) ? "/" : "", + esc, + suffix(e[i]->d_type)) < 0) { + ret = S_REQUEST_TIMEOUT; + goto cleanup; + } + } + + /* listing footer */ + if (dprintf(fd, "\n\t\n\n") < 0) { + ret = S_REQUEST_TIMEOUT; goto cleanup; } - if (req->method == M_GET) { - /* listing header */ - html_escape(name, esc, sizeof(esc)); - if (dprintf(fd, - "\n\n\t" - "Index of %s\n" - "\t\n\t\t..", - esc) < 0) { - res.status = S_REQUEST_TIMEOUT; - goto cleanup; - } - - /* listing */ - for (i = 0; i < (size_t)dirlen; i++) { - /* skip hidden files, "." and ".." */ - if (e[i]->d_name[0] == '.') { - continue; - } - - /* entry line */ - html_escape(e[i]->d_name, esc, sizeof(esc)); - if (dprintf(fd, "
\n\t\t%s%s", - esc, - (e[i]->d_type == DT_DIR) ? "/" : "", - esc, - suffix(e[i]->d_type)) < 0) { - res.status = S_REQUEST_TIMEOUT; - goto cleanup; - } - } - - /* listing footer */ - if (dprintf(fd, "\n\t\n\n") < 0) { - res.status = S_REQUEST_TIMEOUT; - goto cleanup; - } - } - cleanup: while (dirlen--) { free(e[dirlen]); } free(e); - return res.status; + return ret; } enum status -resp_file(int fd, const char *name, const struct request *req, - const struct stat *st, const char *mime, size_t lower, - size_t upper) +resp_file(int fd, const struct response *res) { FILE *fp; - enum status sendstatus; - struct response res = { - .status = (req->field[REQ_RANGE][0] != '\0') ? - S_PARTIAL_CONTENT : S_OK, - .field[RES_ACCEPT_RANGES] = "bytes", - }; + enum status ret = 0; ssize_t bread, bwritten; size_t remaining; static char buf[BUFSIZ], *p; /* open file */ - if (!(fp = fopen(name, "r"))) { - res.status = http_send_status(fd, S_FORBIDDEN); + if (!(fp = fopen(res->path, "r"))) { + ret = S_FORBIDDEN; goto cleanup; } /* seek to lower bound */ - if (fseek(fp, lower, SEEK_SET)) { - res.status = http_send_status(fd, S_INTERNAL_SERVER_ERROR); + if (fseek(fp, res->file.lower, SEEK_SET)) { + ret = S_INTERNAL_SERVER_ERROR; goto cleanup; } - /* build header */ - if (esnprintf(res.field[RES_CONTENT_LENGTH], - sizeof(res.field[RES_CONTENT_LENGTH]), - "%zu", upper - lower + 1)) { - return http_send_status(fd, S_INTERNAL_SERVER_ERROR); - } - if (req->field[REQ_RANGE][0] != '\0') { - if (esnprintf(res.field[RES_CONTENT_RANGE], - sizeof(res.field[RES_CONTENT_RANGE]), - "bytes %zd-%zd/%zu", lower, upper, - st->st_size)) { - return http_send_status(fd, S_INTERNAL_SERVER_ERROR); + /* write data until upper bound is hit */ + remaining = res->file.upper - res->file.lower + 1; + + while ((bread = fread(buf, 1, MIN(sizeof(buf), + remaining), fp))) { + if (bread < 0) { + ret = S_INTERNAL_SERVER_ERROR; + goto cleanup; } - } - if (esnprintf(res.field[RES_CONTENT_TYPE], - sizeof(res.field[RES_CONTENT_TYPE]), - "%s", mime)) { - return http_send_status(fd, S_INTERNAL_SERVER_ERROR); - } - if (timestamp(res.field[RES_LAST_MODIFIED], - sizeof(res.field[RES_LAST_MODIFIED]), - st->st_mtim.tv_sec)) { - return http_send_status(fd, S_INTERNAL_SERVER_ERROR); - } - - /* send header as late as possible */ - if ((sendstatus = http_send_header(fd, &res)) != res.status) { - res.status = sendstatus; - goto cleanup; - } - - if (req->method == M_GET) { - /* write data until upper bound is hit */ - remaining = upper - lower + 1; - - while ((bread = fread(buf, 1, MIN(sizeof(buf), - remaining), fp))) { - if (bread < 0) { - res.status = S_INTERNAL_SERVER_ERROR; + remaining -= bread; + p = buf; + while (bread > 0) { + bwritten = write(fd, p, bread); + if (bwritten <= 0) { + ret = S_REQUEST_TIMEOUT; goto cleanup; } - remaining -= bread; - p = buf; - while (bread > 0) { - bwritten = write(fd, p, bread); - if (bwritten <= 0) { - res.status = S_REQUEST_TIMEOUT; - goto cleanup; - } - bread -= bwritten; - p += bwritten; - } + bread -= bwritten; + p += bwritten; } } cleanup: @@ -241,5 +133,5 @@ cleanup: fclose(fp); } - return res.status; + return ret; } diff --git a/resp.h b/resp.h index 32d4c5d..92e5ad6 100644 --- a/resp.h +++ b/resp.h @@ -7,8 +7,7 @@ #include "http.h" -enum status resp_dir(int, const char *, const struct request *); -enum status resp_file(int, const char *, const struct request *, - const struct stat *, const char *, size_t, size_t); +enum status resp_dir(int, const struct response *); +enum status resp_file(int, const struct response *); #endif /* RESP_H */ diff --git a/util.c b/util.c index 518a3eb..2b54df1 100644 --- a/util.c +++ b/util.c @@ -108,6 +108,66 @@ esnprintf(char *str, size_t size, const char *fmt, ...) return (ret < 0 || (size_t)ret >= size); } +int +prepend(char *str, size_t size, const char *prefix) +{ + size_t len = strlen(str), prefixlen = strlen(prefix); + + if (len + prefixlen + 1 > size) { + return 1; + } + + memmove(str + prefixlen, str, len + 1); + memcpy(str, prefix, prefixlen); + + return 0; +} + +void +html_escape(const char *src, char *dst, size_t dst_siz) +{ + const struct { + char c; + char *s; + } escape[] = { + { '&', "&" }, + { '<', "<" }, + { '>', ">" }, + { '"', """ }, + { '\'', "'" }, + }; + size_t i, j, k, esclen; + + for (i = 0, j = 0; src[i] != '\0'; i++) { + for (k = 0; k < LEN(escape); k++) { + if (src[i] == escape[k].c) { + break; + } + } + if (k == LEN(escape)) { + /* no escape char at src[i] */ + if (j == dst_siz - 1) { + /* silent truncation */ + break; + } else { + dst[j++] = src[i]; + } + } else { + /* escape char at src[i] */ + esclen = strlen(escape[k].s); + + if (j >= dst_siz - esclen) { + /* silent truncation */ + break; + } else { + memcpy(&dst[j], escape[k].s, esclen); + j += esclen; + } + } + } + dst[j] = '\0'; +} + #define INVALID 1 #define TOOSMALL 2 #define TOOLARGE 3 diff --git a/util.h b/util.h index 21c18f8..6b6d17d 100644 --- a/util.h +++ b/util.h @@ -51,6 +51,8 @@ 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 *); +void html_escape(const char *, char *, size_t); void *reallocarray(void *, size_t, size_t); long long strtonum(const char *, long long, long long, const char **); From 68be64e2c12f6ab5355a147484896eae12d2b166 Mon Sep 17 00:00:00 2001 From: Laslo Hunhold Date: Sat, 22 Aug 2020 23:31:32 +0200 Subject: [PATCH 08/12] Remove unused field in the request-struct Signed-off-by: Laslo Hunhold --- http.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/http.h b/http.h index 5226dca..2ee79af 100644 --- a/http.h +++ b/http.h @@ -27,8 +27,6 @@ enum req_method { extern const char *req_method_str[]; struct request { - char header[HEADER_MAX]; /* deprecated */ - enum req_method method; char target[PATH_MAX]; char field[NUM_REQ_FIELDS][FIELD_MAX]; From 50c85ec642d1327135eb5a58c6d1ffc1ee0d41dc Mon Sep 17 00:00:00 2001 From: Laslo Hunhold Date: Sat, 22 Aug 2020 23:37:08 +0200 Subject: [PATCH 09/12] Rename "target" to "URI" where appropriate Of course URIs point at "targets", but the URIs themselves should be called what they are, not only in the interest of clarity in terms of nomenclature. Signed-off-by: Laslo Hunhold --- http.c | 22 +++++++++++----------- http.h | 2 +- main.c | 2 +- quark.1 | 8 ++++---- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/http.c b/http.c index b8ae1aa..7bf682d 100644 --- a/http.c +++ b/http.c @@ -232,9 +232,9 @@ http_parse_header(const char *h, struct request *req) if (q - p + 1 > PATH_MAX) { return S_REQUEST_TOO_LARGE; } - memcpy(req->target, p, q - p); - req->target[q - p] = '\0'; - decode(req->target, req->target); + memcpy(req->uri, p, q - p); + req->uri[q - p] = '\0'; + decode(req->uri, req->uri); /* basis for next step */ p = q + 1; @@ -566,7 +566,7 @@ http_prepare_response(const struct request *req, struct response *res, memset(res, 0, sizeof(*res)); /* make a working copy of the URI and normalize it */ - memcpy(realuri, req->target, sizeof(realuri)); + memcpy(realuri, req->uri, sizeof(realuri)); if (normabspath(realuri)) { return S_BAD_REQUEST; } @@ -593,7 +593,7 @@ http_prepare_response(const struct request *req, struct response *res, } } - /* apply target prefix mapping */ + /* apply URI prefix mapping */ for (i = 0; i < s->map_len; i++) { len = strlen(s->map[i].from); if (!strncmp(realuri, s->map[i].from, len)) { @@ -604,7 +604,7 @@ http_prepare_response(const struct request *req, struct response *res, continue; } - /* swap out target prefix */ + /* swap out URI prefix */ memmove(realuri, realuri + len, strlen(realuri) + 1); if (prepend(realuri, LEN(realuri), s->map[i].to)) { return S_REQUEST_TOO_LARGE; @@ -636,7 +636,7 @@ http_prepare_response(const struct request *req, struct response *res, } /* - * reject hidden target, except if it is a well-known URI + * reject hidden targets, except if it is a well-known URI * according to RFC 8615 */ if (strstr(realuri, "/.") && strncmp(realuri, @@ -648,7 +648,7 @@ http_prepare_response(const struct request *req, struct response *res, * redirect if the original URI and the "real" URI differ or if * the requested host is non-canonical */ - if (strcmp(req->target, realuri) || (s->vhost && vhost && + if (strcmp(req->uri, realuri) || (s->vhost && vhost && strcmp(req->field[REQ_HOST], vhost->chost))) { res->status = S_MOVED_PERMANENTLY; @@ -700,11 +700,11 @@ http_prepare_response(const struct request *req, struct response *res, * (optionally including the vhost servedir as a prefix) * into the actual response-path */ - if (esnprintf(res->uri, sizeof(res->uri), "%s", req->target)) { + if (esnprintf(res->uri, sizeof(res->uri), "%s", req->uri)) { return S_REQUEST_TOO_LARGE; } if (esnprintf(res->path, sizeof(res->path), "%s%s", - vhost ? vhost->dir : "", RELPATH(req->target))) { + vhost ? vhost->dir : "", RELPATH(req->uri))) { return S_REQUEST_TOO_LARGE; } } @@ -715,7 +715,7 @@ http_prepare_response(const struct request *req, struct response *res, * the URI */ if (esnprintf(tmpuri, sizeof(tmpuri), "%s%s", - req->target, s->docindex)) { + req->uri, s->docindex)) { return S_REQUEST_TOO_LARGE; } diff --git a/http.h b/http.h index 2ee79af..a9cf871 100644 --- a/http.h +++ b/http.h @@ -28,7 +28,7 @@ extern const char *req_method_str[]; struct request { enum req_method method; - char target[PATH_MAX]; + char uri[PATH_MAX]; char field[NUM_REQ_FIELDS][FIELD_MAX]; }; diff --git a/main.c b/main.c index bb7233c..f292506 100644 --- a/main.c +++ b/main.c @@ -64,7 +64,7 @@ serve(int infd, const struct sockaddr_storage *in_sa, const struct server *s) goto cleanup; } printf("%s\t%s\t%d\t%s\t%s\n", tstmp, inaddr, status, - c.req.field[REQ_HOST], c.req.target); + c.req.field[REQ_HOST], c.req.uri); cleanup: /* clean up and finish */ shutdown(c.fd, SHUT_RD); diff --git a/quark.1 b/quark.1 index 7188df2..d1dcb3d 100644 --- a/quark.1 +++ b/quark.1 @@ -1,4 +1,4 @@ -.Dd 2020-08-18 +.Dd 2020-08-22 .Dt QUARK 1 .Os suckless.org .Sh NAME @@ -63,7 +63,7 @@ The default is "index.html". .It Fl l Enable directory listing. .It Fl m Ar map -Add the target prefix mapping rule specified by +Add the URI prefix mapping rule specified by .Ar map , which has the form .Qq Pa from to [chost] , @@ -72,7 +72,7 @@ escaped with '\\'. .Pp The prefix .Pa from -of all matching targets is replaced with +of all matching URIs is replaced with .Pa to , optionally limited to the canonical virtual host .Pa chost . @@ -117,7 +117,7 @@ is redirected to the canonical host .Pa chost , if they differ, using the directory .Pa dir -as the root directory, optionally prefixing the target with +as the root directory, optionally prefixing the URI with .Pa prefix . If any virtual hosts are specified, all requests on non-matching hosts are discarded. From 1ccaac023caa4ae415041c190516b0f7ae6ba647 Mon Sep 17 00:00:00 2001 From: Laslo Hunhold Date: Sun, 23 Aug 2020 11:02:38 +0200 Subject: [PATCH 10/12] Rename s to srv This improves readability a bit and helps iron out confusions with status-variables called s in other methods. Signed-off-by: Laslo Hunhold --- http.c | 40 ++++++++++++++++++++-------------------- main.c | 48 ++++++++++++++++++++++++------------------------ 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/http.c b/http.c index 7bf682d..b6b1ab7 100644 --- a/http.c +++ b/http.c @@ -549,7 +549,7 @@ parse_range(const char *str, size_t size, size_t *lower, size_t *upper) enum status http_prepare_response(const struct request *req, struct response *res, - const struct server *s) + const struct server *srv) { enum status returnstatus; struct in6_addr addr; @@ -573,16 +573,16 @@ http_prepare_response(const struct request *req, struct response *res, /* match vhost */ vhost = NULL; - if (s->vhost) { - for (i = 0; i < s->vhost_len; i++) { - if (!regexec(&(s->vhost[i].re), req->field[REQ_HOST], + if (srv->vhost) { + for (i = 0; i < srv->vhost_len; i++) { + if (!regexec(&(srv->vhost[i].re), req->field[REQ_HOST], 0, NULL, 0)) { /* we have a matching vhost */ - vhost = &(s->vhost[i]); + vhost = &(srv->vhost[i]); break; } } - if (i == s->vhost_len) { + if (i == srv->vhost_len) { return S_NOT_FOUND; } @@ -594,19 +594,19 @@ http_prepare_response(const struct request *req, struct response *res, } /* apply URI prefix mapping */ - for (i = 0; i < s->map_len; i++) { - len = strlen(s->map[i].from); - if (!strncmp(realuri, s->map[i].from, len)) { + for (i = 0; i < srv->map_len; i++) { + len = strlen(srv->map[i].from); + if (!strncmp(realuri, srv->map[i].from, len)) { /* match canonical host if vhosts are enabled and * the mapping specifies a canonical host */ - if (s->vhost && s->map[i].chost && - strcmp(s->map[i].chost, vhost->chost)) { + if (srv->vhost && srv->map[i].chost && + strcmp(srv->map[i].chost, vhost->chost)) { continue; } /* swap out URI prefix */ memmove(realuri, realuri + len, strlen(realuri) + 1); - if (prepend(realuri, LEN(realuri), s->map[i].to)) { + if (prepend(realuri, LEN(realuri), srv->map[i].to)) { return S_REQUEST_TOO_LARGE; } break; @@ -648,7 +648,7 @@ http_prepare_response(const struct request *req, struct response *res, * redirect if the original URI and the "real" URI differ or if * the requested host is non-canonical */ - if (strcmp(req->uri, realuri) || (s->vhost && vhost && + if (strcmp(req->uri, realuri) || (srv->vhost && vhost && strcmp(req->field[REQ_HOST], vhost->chost))) { res->status = S_MOVED_PERMANENTLY; @@ -656,14 +656,14 @@ http_prepare_response(const struct request *req, struct response *res, encode(realuri, tmpuri); /* determine target location */ - if (s->vhost) { + if (srv->vhost) { /* absolute redirection URL */ targethost = req->field[REQ_HOST][0] ? vhost->chost ? - vhost->chost : req->field[REQ_HOST] : s->host ? - s->host : "localhost"; + vhost->chost : req->field[REQ_HOST] : + srv->host ? srv->host : "localhost"; /* do we need to add a port to the Location? */ - hasport = s->port && strcmp(s->port, "80"); + hasport = srv->port && strcmp(srv->port, "80"); /* RFC 2732 specifies to use brackets for IPv6-addresses * in URLs, so we need to check if our host is one and @@ -680,7 +680,7 @@ http_prepare_response(const struct request *req, struct response *res, ipv6host ? "[" : "", targethost, ipv6host ? "]" : "", hasport ? ":" : "", - hasport ? s->port : "", tmpuri)) { + hasport ? srv->port : "", tmpuri)) { return S_REQUEST_TOO_LARGE; } } else { @@ -715,13 +715,13 @@ http_prepare_response(const struct request *req, struct response *res, * the URI */ if (esnprintf(tmpuri, sizeof(tmpuri), "%s%s", - req->uri, s->docindex)) { + req->uri, srv->docindex)) { return S_REQUEST_TOO_LARGE; } /* stat the docindex, which must be a regular file */ if (stat(RELPATH(tmpuri), &st) < 0 || !S_ISREG(st.st_mode)) { - if (s->listdirs) { + if (srv->listdirs) { /* serve directory listing */ res->type = RESTYPE_DIRLISTING; res->status = (access(res->path, R_OK)) ? diff --git a/main.c b/main.c index f292506..110af1a 100644 --- a/main.c +++ b/main.c @@ -24,7 +24,7 @@ static char *udsname; static void -serve(int infd, const struct sockaddr_storage *in_sa, const struct server *s) +serve(int infd, const struct sockaddr_storage *in_sa, const struct server *srv) { struct connection c = { .fd = infd }; time_t t; @@ -40,7 +40,7 @@ serve(int infd, const struct sockaddr_storage *in_sa, const struct server *s) /* handle request */ if ((status = http_recv_header(c.fd, c.header, LEN(c.header), &c.off)) || (status = http_parse_header(c.header, &c.req)) || - (status = http_prepare_response(&c.req, &c.res, s))) { + (status = http_prepare_response(&c.req, &c.res, srv))) { status = http_send_status(c.fd, status); } else { status = http_send_header(c.fd, &c.res); @@ -189,7 +189,7 @@ main(int argc, char *argv[]) struct group *grp = NULL; struct passwd *pwd = NULL; struct rlimit rlim; - struct server s = { + struct server srv = { .docindex = "index.html", }; struct sockaddr_storage in_sa; @@ -213,28 +213,28 @@ main(int argc, char *argv[]) group = EARGF(usage()); break; case 'h': - s.host = EARGF(usage()); + srv.host = EARGF(usage()); break; case 'i': - s.docindex = EARGF(usage()); - if (strchr(s.docindex, '/')) { + srv.docindex = EARGF(usage()); + if (strchr(srv.docindex, '/')) { die("The document index must not contain '/'"); } break; case 'l': - s.listdirs = 1; + srv.listdirs = 1; break; case 'm': if (spacetok(EARGF(usage()), tok, 3) || !tok[0] || !tok[1]) { usage(); } - if (!(s.map = reallocarray(s.map, ++s.map_len, + if (!(srv.map = reallocarray(srv.map, ++srv.map_len, sizeof(struct map)))) { die("reallocarray:"); } - s.map[s.map_len - 1].from = tok[0]; - s.map[s.map_len - 1].to = tok[1]; - s.map[s.map_len - 1].chost = tok[2]; + srv.map[srv.map_len - 1].from = tok[0]; + srv.map[srv.map_len - 1].to = tok[1]; + srv.map[srv.map_len - 1].chost = tok[2]; break; case 'n': maxnprocs = strtonum(EARGF(usage()), 1, INT_MAX, &err); @@ -243,7 +243,7 @@ main(int argc, char *argv[]) } break; case 'p': - s.port = EARGF(usage()); + srv.port = EARGF(usage()); break; case 'U': udsname = EARGF(usage()); @@ -256,14 +256,14 @@ main(int argc, char *argv[]) !tok[2]) { usage(); } - if (!(s.vhost = reallocarray(s.vhost, ++s.vhost_len, - sizeof(struct vhost)))) { + if (!(srv.vhost = reallocarray(srv.vhost, ++srv.vhost_len, + sizeof(*srv.vhost)))) { die("reallocarray:"); } - s.vhost[s.vhost_len - 1].chost = tok[0]; - s.vhost[s.vhost_len - 1].regex = tok[1]; - s.vhost[s.vhost_len - 1].dir = tok[2]; - s.vhost[s.vhost_len - 1].prefix = tok[3]; + srv.vhost[srv.vhost_len - 1].chost = tok[0]; + srv.vhost[srv.vhost_len - 1].regex = tok[1]; + srv.vhost[srv.vhost_len - 1].dir = tok[2]; + srv.vhost[srv.vhost_len - 1].prefix = tok[3]; break; default: usage(); @@ -274,7 +274,7 @@ main(int argc, char *argv[]) } /* can't have both host and UDS but must have one of port or UDS*/ - if ((s.host && udsname) || !(s.port || udsname)) { + if ((srv.host && udsname) || !(srv.port || udsname)) { usage(); } @@ -284,11 +284,11 @@ main(int argc, char *argv[]) } /* compile and check the supplied vhost regexes */ - for (i = 0; i < s.vhost_len; i++) { - if (regcomp(&s.vhost[i].re, s.vhost[i].regex, + for (i = 0; i < srv.vhost_len; i++) { + if (regcomp(&srv.vhost[i].re, srv.vhost[i].regex, REG_EXTENDED | REG_ICASE | REG_NOSUB)) { die("regcomp '%s': invalid regex", - s.vhost[i].regex); + srv.vhost[i].regex); } } @@ -317,7 +317,7 @@ main(int argc, char *argv[]) /* bind socket */ insock = udsname ? sock_get_uds(udsname, pwd->pw_uid, grp->gr_gid) : - sock_get_ips(s.host, s.port); + sock_get_ips(srv.host, srv.port); switch (fork()) { case -1: @@ -380,7 +380,7 @@ main(int argc, char *argv[]) /* fork and handle */ switch (fork()) { case 0: - serve(infd, &in_sa, &s); + serve(infd, &in_sa, &srv); exit(0); break; case -1: From 27f8bbfac440d95ef354005d5f7353ec9f3d9294 Mon Sep 17 00:00:00 2001 From: Laslo Hunhold Date: Sun, 23 Aug 2020 13:35:49 +0200 Subject: [PATCH 11/12] Refactor sock_get_uds() a bit This refines the error messages a bit and makes clearer what went wrong. Signed-off-by: Laslo Hunhold --- sock.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/sock.c b/sock.c index dd99bdb..c1fbd43 100644 --- a/sock.c +++ b/sock.c @@ -63,7 +63,7 @@ void sock_rem_uds(const char *udsname) { if (unlink(udsname) < 0) { - die("unlink:"); + die("unlink '%s':", udsname); } } @@ -74,7 +74,8 @@ sock_get_uds(const char *udsname, uid_t uid, gid_t gid) .sun_family = AF_UNIX, }; size_t udsnamelen; - int insock, sockmode = S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH; + int insock, sockmode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | + S_IROTH | S_IWOTH; if ((insock = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) { die("socket:"); @@ -86,7 +87,7 @@ sock_get_uds(const char *udsname, uid_t uid, gid_t gid) memcpy(addr.sun_path, udsname, udsnamelen + 1); if (bind(insock, (const struct sockaddr *)&addr, sizeof(addr)) < 0) { - die("bind %s:", udsname); + die("bind '%s':", udsname); } if (listen(insock, SOMAXCONN) < 0) { @@ -96,12 +97,12 @@ sock_get_uds(const char *udsname, uid_t uid, gid_t gid) if (chmod(udsname, sockmode) < 0) { sock_rem_uds(udsname); - die("chmod:"); + die("chmod '%s':", udsname); } if (chown(udsname, uid, gid) < 0) { sock_rem_uds(udsname); - die("chown:"); + die("chown '%s':", udsname); } return insock; From 601b56d27095e4340e5afcf3385465a1f7f96d98 Mon Sep 17 00:00:00 2001 From: Laslo Hunhold Date: Sun, 23 Aug 2020 13:36:56 +0200 Subject: [PATCH 12/12] Mention default behaviour in the manual when the host is not given Signed-off-by: Laslo Hunhold --- quark.1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quark.1 b/quark.1 index d1dcb3d..6e0e5f8 100644 --- a/quark.1 +++ b/quark.1 @@ -1,4 +1,4 @@ -.Dd 2020-08-22 +.Dd 2020-08-23 .Dt QUARK 1 .Os suckless.org .Sh NAME @@ -55,6 +55,7 @@ The default is "nogroup". Use .Ar host as the server hostname. +The default is the loopback interface (i.e. localhost). .It Fl i Ar file Set .Ar file