Refactor response-generation

I wasn't happy with how responses were generated. HTTP-headers were
handled by hand and it was duplicated in multiple parts of the code.
Due to the duplication, some functions like timestamp() had really
ugly semantics.

The HTTP requests are parsed much better: We have an enum of fields
we care about that are automatically read into our request struct. This
commit adapts this idea to the response: We have an enum of fields
we might put into our response, and a response-struct holds the
content of these fields. A function http_send_header() automatically
sends a header based on the entries in response. In case we don't
use a field, we just leave the field in the response-struct empty.

With this commit, some logical changes came with it:

  - timestamp() now has a sane signature, TIMESTAMP_LEN is no more and
    it can now return proper errors and is also reentrant by using
    gmtime_r() instead of gmtime()
  - No more use of a static timestamp-array, making all the methods
    also reentrant
  - Better internal-error-reporting: Because the fields are filled
    before and not during sending the response-headers, we can better
    report any internal errors as status 500 instead of sending a
    partial non-500-header and then dying.

These improved data structures make it easier to read and hack the code
and implement new features, if desired.

Signed-off-by: Laslo Hunhold <dev@frign.de>
This commit is contained in:
Laslo Hunhold 2020-08-05 13:41:44 +02:00
parent 26c593ade1
commit c51b31d7ac
No known key found for this signature in database
GPG key ID: 69576BD24CFCB980
5 changed files with 175 additions and 119 deletions

152
http.c
View file

@ -48,23 +48,76 @@ const char *status_str[] = {
[S_VERSION_NOT_SUPPORTED] = "HTTP Version not supported", [S_VERSION_NOT_SUPPORTED] = "HTTP Version not supported",
}; };
const char *res_field_str[] = {
[RES_ACCEPT_RANGES] = "Accept-Ranges",
[RES_ALLOW] = "Allow",
[RES_LOCATION] = "Location",
[RES_LAST_MODIFIED] = "Last-Modified",
[RES_CONTENT_LENGTH] = "Content-Length",
[RES_CONTENT_RANGE] = "Content-Range",
[RES_CONTENT_TYPE] = "Content-Type",
};
enum status enum status
http_send_status(int fd, enum status s) http_send_header(int fd, const struct response *res)
{ {
static char t[TIMESTAMP_LEN]; char t[FIELD_MAX];
size_t i;
if (timestamp(t, sizeof(t), time(NULL))) {
return S_INTERNAL_SERVER_ERROR;
}
if (dprintf(fd, if (dprintf(fd,
"HTTP/1.1 %d %s\r\n" "HTTP/1.1 %d %s\r\n"
"Date: %s\r\n" "Date: %s\r\n"
"Connection: close\r\n" "Connection: close\r\n",
"%s" res->status, status_str[res->status], t) < 0) {
"Content-Type: text/html; charset=utf-8\r\n" return S_REQUEST_TIMEOUT;
"\r\n" }
for (i = 0; i < NUM_RES_FIELDS; i++) {
if (res->field[i][0] != '\0') {
if (dprintf(fd, "%s: %s\r\n", res_field_str[i],
res->field[i]) < 0) {
return S_REQUEST_TIMEOUT;
}
}
}
if (dprintf(fd, "\r\n") < 0) {
return S_REQUEST_TIMEOUT;
}
return res->status;
}
enum status
http_send_status(int fd, enum status s)
{
enum status sendstatus;
struct response res = {
.status = s,
.field[RES_CONTENT_TYPE] = "text/html; charset=utf-8",
};
if (s == S_METHOD_NOT_ALLOWED) {
if (esnprintf(res.field[RES_ALLOW],
sizeof(res.field[RES_ALLOW]), "%s",
"Allow: GET, HEAD")) {
return S_INTERNAL_SERVER_ERROR;
}
}
if ((sendstatus = http_send_header(fd, &res)) != s) {
return sendstatus;
}
if (dprintf(fd,
"<!DOCTYPE html>\n<html>\n\t<head>\n" "<!DOCTYPE html>\n<html>\n\t<head>\n"
"\t\t<title>%d %s</title>\n\t</head>\n\t<body>\n" "\t\t<title>%d %s</title>\n\t</head>\n\t<body>\n"
"\t\t<h1>%d %s</h1>\n\t</body>\n</html>\n", "\t\t<h1>%d %s</h1>\n\t</body>\n</html>\n",
s, status_str[s], timestamp(time(NULL), t),
(s == S_METHOD_NOT_ALLOWED) ? "Allow: HEAD, GET\r\n" : "",
s, status_str[s], s, status_str[s]) < 0) { s, status_str[s], s, status_str[s]) < 0) {
return S_REQUEST_TIMEOUT; return S_REQUEST_TIMEOUT;
} }
@ -93,7 +146,7 @@ decode(char src[PATH_MAX], char dest[PATH_MAX])
int int
http_get_request(int fd, struct request *r) http_get_request(int fd, struct request *r)
{ {
struct in6_addr res; struct in6_addr addr;
size_t hlen, i, mlen; size_t hlen, i, mlen;
ssize_t off; ssize_t off;
char h[HEADER_MAX], *p, *q; char h[HEADER_MAX], *p, *q;
@ -260,7 +313,7 @@ http_get_request(int fd, struct request *r)
p = r->field[REQ_HOST] + 1; p = r->field[REQ_HOST] + 1;
/* validate the contained IPv6 address */ /* validate the contained IPv6 address */
if (inet_pton(AF_INET6, p, &res) != 1) { if (inet_pton(AF_INET6, p, &addr) != 1) {
return http_send_status(fd, S_BAD_REQUEST); return http_send_status(fd, S_BAD_REQUEST);
} }
@ -451,13 +504,14 @@ parse_range(char *s, off_t size, off_t *lower, off_t *upper)
enum status enum status
http_send_response(int fd, struct request *r) http_send_response(int fd, struct request *r)
{ {
struct in6_addr res; struct in6_addr addr;
struct response res = { 0 };
struct stat st; struct stat st;
struct tm tm = { 0 }; struct tm tm = { 0 };
size_t len, i; size_t len, i;
off_t lower, upper; off_t lower, upper;
int hasport, ipv6host; int hasport, ipv6host;
static char realtarget[PATH_MAX], tmptarget[PATH_MAX], t[TIMESTAMP_LEN]; static char realtarget[PATH_MAX], tmptarget[PATH_MAX];
char *p, *mime; char *p, *mime;
const char *vhostmatch, *targethost; const char *vhostmatch, *targethost;
@ -545,10 +599,12 @@ http_send_response(int fd, struct request *r)
/* redirect if targets differ, host is non-canonical or we prefixed */ /* redirect if targets differ, host is non-canonical or we prefixed */
if (strcmp(r->target, realtarget) || (s.vhost && vhostmatch && if (strcmp(r->target, realtarget) || (s.vhost && vhostmatch &&
strcmp(r->field[REQ_HOST], vhostmatch))) { strcmp(r->field[REQ_HOST], vhostmatch))) {
res.status = S_MOVED_PERMANENTLY;
/* encode realtarget */ /* encode realtarget */
encode(realtarget, tmptarget); encode(realtarget, tmptarget);
/* send redirection header */ /* determine target location */
if (s.vhost) { if (s.vhost) {
/* absolute redirection URL */ /* absolute redirection URL */
targethost = r->field[REQ_HOST][0] ? vhostmatch ? targethost = r->field[REQ_HOST][0] ? vhostmatch ?
@ -562,43 +618,31 @@ http_send_response(int fd, struct request *r)
* in URLs, so we need to check if our host is one and * in URLs, so we need to check if our host is one and
* honor that later when we fill the "Location"-field */ * honor that later when we fill the "Location"-field */
if ((ipv6host = inet_pton(AF_INET6, targethost, if ((ipv6host = inet_pton(AF_INET6, targethost,
&res)) < 0) { &addr)) < 0) {
return http_send_status(fd, return http_send_status(fd,
S_INTERNAL_SERVER_ERROR); S_INTERNAL_SERVER_ERROR);
} }
if (dprintf(fd, /* write location to response struct */
"HTTP/1.1 %d %s\r\n" if (esnprintf(res.field[RES_LOCATION],
"Date: %s\r\n" sizeof(res.field[RES_LOCATION]),
"Connection: close\r\n" "//%s%s%s%s%s%s",
"Location: //%s%s%s%s%s%s\r\n"
"\r\n",
S_MOVED_PERMANENTLY,
status_str[S_MOVED_PERMANENTLY],
timestamp(time(NULL), t),
ipv6host ? "[" : "", ipv6host ? "[" : "",
targethost, targethost,
ipv6host ? "]" : "", hasport ? ":" : "", ipv6host ? "]" : "", hasport ? ":" : "",
hasport ? s.port : "", tmptarget) < 0) { hasport ? s.port : "", tmptarget)) {
return S_REQUEST_TIMEOUT; return http_send_status(fd, S_REQUEST_TOO_LARGE);
} }
} else { } else {
/* relative redirection URL */ /* write relative redirection URL to response struct */
if (dprintf(fd, if (esnprintf(res.field[RES_LOCATION],
"HTTP/1.1 %d %s\r\n" sizeof(res.field[RES_LOCATION]),
"Date: %s\r\n" tmptarget)) {
"Connection: close\r\n" return http_send_status(fd, S_REQUEST_TOO_LARGE);
"Location: %s\r\n"
"\r\n",
S_MOVED_PERMANENTLY,
status_str[S_MOVED_PERMANENTLY],
timestamp(time(NULL), t),
tmptarget) < 0) {
return S_REQUEST_TIMEOUT;
} }
} }
return S_MOVED_PERMANENTLY; return http_send_header(fd, &res);
} }
if (S_ISDIR(st.st_mode)) { if (S_ISDIR(st.st_mode)) {
@ -635,35 +679,23 @@ http_send_response(int fd, struct request *r)
/* compare with last modification date of the file */ /* compare with last modification date of the file */
if (difftime(st.st_mtim.tv_sec, timegm(&tm)) <= 0) { if (difftime(st.st_mtim.tv_sec, timegm(&tm)) <= 0) {
if (dprintf(fd, res.status = S_NOT_MODIFIED;
"HTTP/1.1 %d %s\r\n" return http_send_header(fd, &res);
"Date: %s\r\n"
"Connection: close\r\n"
"\r\n",
S_NOT_MODIFIED, status_str[S_NOT_MODIFIED],
timestamp(time(NULL), t)) < 0) {
return S_REQUEST_TIMEOUT;
}
return S_NOT_MODIFIED;
} }
} }
/* range */ /* range */
switch (parse_range(r->field[REQ_RANGE], st.st_size, &lower, &upper)) { switch (parse_range(r->field[REQ_RANGE], st.st_size, &lower, &upper)) {
case S_RANGE_NOT_SATISFIABLE: case S_RANGE_NOT_SATISFIABLE:
if (dprintf(fd, res.status = S_RANGE_NOT_SATISFIABLE;
"HTTP/1.1 %d %s\r\n"
"Date: %s\r\n" if (esnprintf(res.field[RES_CONTENT_RANGE],
"Content-Range: bytes */%zu\r\n" sizeof(res.field[RES_CONTENT_RANGE]),
"Connection: close\r\n" "bytes */%zu", st.st_size)) {
"\r\n", return http_send_status(fd, S_INTERNAL_SERVER_ERROR);
S_RANGE_NOT_SATISFIABLE,
status_str[S_RANGE_NOT_SATISFIABLE],
timestamp(time(NULL), t),
st.st_size) < 0) {
return S_REQUEST_TIMEOUT;
} }
return S_RANGE_NOT_SATISFIABLE;
return http_send_header(fd, &res);
case S_BAD_REQUEST: case S_BAD_REQUEST:
return http_send_status(fd, S_BAD_REQUEST); return http_send_status(fd, S_BAD_REQUEST);
default: default:

19
http.h
View file

@ -48,6 +48,25 @@ enum status {
extern const char *status_str[]; extern const char *status_str[];
enum res_field {
RES_ACCEPT_RANGES,
RES_ALLOW,
RES_LOCATION,
RES_LAST_MODIFIED,
RES_CONTENT_LENGTH,
RES_CONTENT_RANGE,
RES_CONTENT_TYPE,
NUM_RES_FIELDS,
};
extern const char *res_field_str[];
struct response {
enum status status;
char field[NUM_RES_FIELDS][FIELD_MAX];
};
enum status http_send_header(int, const struct response *);
enum status http_send_status(int, enum status); enum status http_send_status(int, enum status);
int http_get_request(int, struct request *); int http_get_request(int, struct request *);
enum status http_send_response(int, struct request *); enum status http_send_response(int, struct request *);

100
resp.c
View file

@ -86,10 +86,14 @@ html_escape(char *src, char *dst, size_t dst_siz)
enum status enum status
resp_dir(int fd, char *name, struct request *r) resp_dir(int fd, char *name, struct request *r)
{ {
enum status sendstatus;
struct dirent **e; struct dirent **e;
struct response res = {
.status = S_OK,
.field[RES_CONTENT_TYPE] = "text/html; charset=utf-8",
};
size_t i; size_t i;
int dirlen, s; int dirlen;
static char t[TIMESTAMP_LEN];
char esc[PATH_MAX /* > NAME_MAX */ * 6]; /* strlen("&...;") <= 6 */ char esc[PATH_MAX /* > NAME_MAX */ * 6]; /* strlen("&...;") <= 6 */
/* read directory */ /* read directory */
@ -98,14 +102,8 @@ resp_dir(int fd, char *name, struct request *r)
} }
/* send header as late as possible */ /* send header as late as possible */
if (dprintf(fd, if ((sendstatus = http_send_header(fd, &res)) != res.status) {
"HTTP/1.1 %d %s\r\n" res.status = sendstatus;
"Date: %s\r\n"
"Connection: close\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"\r\n",
S_OK, status_str[S_OK], timestamp(time(NULL), t)) < 0) {
s = S_REQUEST_TIMEOUT;
goto cleanup; goto cleanup;
} }
@ -117,7 +115,7 @@ resp_dir(int fd, char *name, struct request *r)
"<title>Index of %s</title></head>\n" "<title>Index of %s</title></head>\n"
"\t<body>\n\t\t<a href=\"..\">..</a>", "\t<body>\n\t\t<a href=\"..\">..</a>",
esc) < 0) { esc) < 0) {
s = S_REQUEST_TIMEOUT; res.status = S_REQUEST_TIMEOUT;
goto cleanup; goto cleanup;
} }
@ -135,18 +133,17 @@ resp_dir(int fd, char *name, struct request *r)
(e[i]->d_type == DT_DIR) ? "/" : "", (e[i]->d_type == DT_DIR) ? "/" : "",
esc, esc,
suffix(e[i]->d_type)) < 0) { suffix(e[i]->d_type)) < 0) {
s = S_REQUEST_TIMEOUT; res.status = S_REQUEST_TIMEOUT;
goto cleanup; goto cleanup;
} }
} }
/* listing footer */ /* listing footer */
if (dprintf(fd, "\n\t</body>\n</html>\n") < 0) { if (dprintf(fd, "\n\t</body>\n</html>\n") < 0) {
s = S_REQUEST_TIMEOUT; res.status = S_REQUEST_TIMEOUT;
goto cleanup; goto cleanup;
} }
} }
s = S_OK;
cleanup: cleanup:
while (dirlen--) { while (dirlen--) {
@ -154,7 +151,7 @@ cleanup:
} }
free(e); free(e);
return s; return res.status;
} }
enum status enum status
@ -162,51 +159,56 @@ resp_file(int fd, char *name, struct request *r, struct stat *st, char *mime,
off_t lower, off_t upper) off_t lower, off_t upper)
{ {
FILE *fp; FILE *fp;
enum status s; enum status sendstatus;
struct response res = {
.status = (r->field[REQ_RANGE][0] != '\0') ?
S_PARTIAL_CONTENT : S_OK,
.field[RES_ACCEPT_RANGES] = "bytes",
};
ssize_t bread, bwritten; ssize_t bread, bwritten;
off_t remaining; off_t remaining;
int range; static char buf[BUFSIZ], *p;
static char buf[BUFSIZ], *p, t1[TIMESTAMP_LEN], t2[TIMESTAMP_LEN];
/* open file */ /* open file */
if (!(fp = fopen(name, "r"))) { if (!(fp = fopen(name, "r"))) {
s = http_send_status(fd, S_FORBIDDEN); res.status = http_send_status(fd, S_FORBIDDEN);
goto cleanup; goto cleanup;
} }
/* seek to lower bound */ /* seek to lower bound */
if (fseek(fp, lower, SEEK_SET)) { if (fseek(fp, lower, SEEK_SET)) {
s = http_send_status(fd, S_INTERNAL_SERVER_ERROR); res.status = http_send_status(fd, S_INTERNAL_SERVER_ERROR);
goto cleanup; 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 (r->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);
}
}
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 */ /* send header as late as possible */
range = r->field[REQ_RANGE][0]; if ((sendstatus = http_send_header(fd, &res)) != res.status) {
s = range ? S_PARTIAL_CONTENT : S_OK; res.status = sendstatus;
if (dprintf(fd,
"HTTP/1.1 %d %s\r\n"
"Date: %s\r\n"
"Connection: close\r\n"
"Last-Modified: %s\r\n"
"Content-Type: %s\r\n"
"Content-Length: %zu\r\n"
"Accept-Ranges: bytes\r\n",
s, status_str[s], timestamp(time(NULL), t1),
timestamp(st->st_mtim.tv_sec, t2), mime,
upper - lower + 1) < 0) {
s = S_REQUEST_TIMEOUT;
goto cleanup;
}
if (range) {
if (dprintf(fd, "Content-Range: bytes %zd-%zd/%zu\r\n",
lower, upper + (upper < 0), st->st_size) < 0) {
s = S_REQUEST_TIMEOUT;
goto cleanup;
}
}
if (dprintf(fd, "\r\n") < 0) {
s = S_REQUEST_TIMEOUT;
goto cleanup; goto cleanup;
} }
@ -217,7 +219,7 @@ resp_file(int fd, char *name, struct request *r, struct stat *st, char *mime,
while ((bread = fread(buf, 1, MIN(sizeof(buf), while ((bread = fread(buf, 1, MIN(sizeof(buf),
(size_t)remaining), fp))) { (size_t)remaining), fp))) {
if (bread < 0) { if (bread < 0) {
s = S_INTERNAL_SERVER_ERROR; res.status = S_INTERNAL_SERVER_ERROR;
goto cleanup; goto cleanup;
} }
remaining -= bread; remaining -= bread;
@ -225,7 +227,7 @@ resp_file(int fd, char *name, struct request *r, struct stat *st, char *mime,
while (bread > 0) { while (bread > 0) {
bwritten = write(fd, p, bread); bwritten = write(fd, p, bread);
if (bwritten <= 0) { if (bwritten <= 0) {
s = S_REQUEST_TIMEOUT; res.status = S_REQUEST_TIMEOUT;
goto cleanup; goto cleanup;
} }
bread -= bwritten; bread -= bwritten;
@ -238,5 +240,5 @@ cleanup:
fclose(fp); fclose(fp);
} }
return s; return res.status;
} }

13
util.c
View file

@ -83,12 +83,17 @@ eunveil(const char *path, const char *permissions)
#endif /* __OpenBSD__ */ #endif /* __OpenBSD__ */
} }
char * int
timestamp(time_t t, char buf[TIMESTAMP_LEN]) timestamp(char *buf, size_t len, time_t t)
{ {
strftime(buf, TIMESTAMP_LEN, "%a, %d %b %Y %T GMT", gmtime(&t)); struct tm tm;
return buf; if (gmtime_r(&t, &tm) == NULL ||
strftime(buf, len, "%a, %d %b %Y %T GMT", &tm) == 0) {
return 1;
}
return 0;
} }
int int

4
util.h
View file

@ -49,9 +49,7 @@ void die(const char *, ...);
void epledge(const char *, const char *); void epledge(const char *, const char *);
void eunveil(const char *, const char *); void eunveil(const char *, const char *);
#define TIMESTAMP_LEN 30 int timestamp(char *, size_t, time_t);
char *timestamp(time_t, char buf[TIMESTAMP_LEN]);
int esnprintf(char *, size_t, const char *, ...); int esnprintf(char *, size_t, const char *, ...);
void *reallocarray(void *, size_t, size_t); void *reallocarray(void *, size_t, size_t);