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",
};
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
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,
"HTTP/1.1 %d %s\r\n"
"Date: %s\r\n"
"Connection: close\r\n"
"%s"
"Content-Type: text/html; charset=utf-8\r\n"
"\r\n"
"Connection: close\r\n",
res->status, status_str[res->status], t) < 0) {
return S_REQUEST_TIMEOUT;
}
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"
"\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",
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) {
return S_REQUEST_TIMEOUT;
}
@ -93,7 +146,7 @@ decode(char src[PATH_MAX], char dest[PATH_MAX])
int
http_get_request(int fd, struct request *r)
{
struct in6_addr res;
struct in6_addr addr;
size_t hlen, i, mlen;
ssize_t off;
char h[HEADER_MAX], *p, *q;
@ -260,7 +313,7 @@ http_get_request(int fd, struct request *r)
p = r->field[REQ_HOST] + 1;
/* 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);
}
@ -451,13 +504,14 @@ parse_range(char *s, off_t size, off_t *lower, off_t *upper)
enum status
http_send_response(int fd, struct request *r)
{
struct in6_addr res;
struct in6_addr addr;
struct response res = { 0 };
struct stat st;
struct tm tm = { 0 };
size_t len, i;
off_t lower, upper;
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;
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 */
if (strcmp(r->target, realtarget) || (s.vhost && vhostmatch &&
strcmp(r->field[REQ_HOST], vhostmatch))) {
res.status = S_MOVED_PERMANENTLY;
/* encode realtarget */
encode(realtarget, tmptarget);
/* send redirection header */
/* determine target location */
if (s.vhost) {
/* absolute redirection URL */
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
* honor that later when we fill the "Location"-field */
if ((ipv6host = inet_pton(AF_INET6, targethost,
&res)) < 0) {
&addr)) < 0) {
return http_send_status(fd,
S_INTERNAL_SERVER_ERROR);
}
if (dprintf(fd,
"HTTP/1.1 %d %s\r\n"
"Date: %s\r\n"
"Connection: close\r\n"
"Location: //%s%s%s%s%s%s\r\n"
"\r\n",
S_MOVED_PERMANENTLY,
status_str[S_MOVED_PERMANENTLY],
timestamp(time(NULL), t),
/* write location to response struct */
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) < 0) {
return S_REQUEST_TIMEOUT;
hasport ? s.port : "", tmptarget)) {
return http_send_status(fd, S_REQUEST_TOO_LARGE);
}
} else {
/* relative redirection URL */
if (dprintf(fd,
"HTTP/1.1 %d %s\r\n"
"Date: %s\r\n"
"Connection: close\r\n"
"Location: %s\r\n"
"\r\n",
S_MOVED_PERMANENTLY,
status_str[S_MOVED_PERMANENTLY],
timestamp(time(NULL), t),
tmptarget) < 0) {
return S_REQUEST_TIMEOUT;
/* 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 S_MOVED_PERMANENTLY;
return http_send_header(fd, &res);
}
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 */
if (difftime(st.st_mtim.tv_sec, timegm(&tm)) <= 0) {
if (dprintf(fd,
"HTTP/1.1 %d %s\r\n"
"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;
res.status = S_NOT_MODIFIED;
return http_send_header(fd, &res);
}
}
/* range */
switch (parse_range(r->field[REQ_RANGE], st.st_size, &lower, &upper)) {
case S_RANGE_NOT_SATISFIABLE:
if (dprintf(fd,
"HTTP/1.1 %d %s\r\n"
"Date: %s\r\n"
"Content-Range: bytes */%zu\r\n"
"Connection: close\r\n"
"\r\n",
S_RANGE_NOT_SATISFIABLE,
status_str[S_RANGE_NOT_SATISFIABLE],
timestamp(time(NULL), t),
st.st_size) < 0) {
return S_REQUEST_TIMEOUT;
res.status = S_RANGE_NOT_SATISFIABLE;
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_RANGE_NOT_SATISFIABLE;
return http_send_header(fd, &res);
case S_BAD_REQUEST:
return http_send_status(fd, S_BAD_REQUEST);
default:

19
http.h
View file

@ -48,6 +48,25 @@ enum status {
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);
int http_get_request(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
resp_dir(int fd, char *name, struct request *r)
{
enum status sendstatus;
struct dirent **e;
struct response res = {
.status = S_OK,
.field[RES_CONTENT_TYPE] = "text/html; charset=utf-8",
};
size_t i;
int dirlen, s;
static char t[TIMESTAMP_LEN];
int dirlen;
char esc[PATH_MAX /* > NAME_MAX */ * 6]; /* strlen("&...;") <= 6 */
/* read directory */
@ -98,14 +102,8 @@ resp_dir(int fd, char *name, struct request *r)
}
/* send header as late as possible */
if (dprintf(fd,
"HTTP/1.1 %d %s\r\n"
"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;
if ((sendstatus = http_send_header(fd, &res)) != res.status) {
res.status = sendstatus;
goto cleanup;
}
@ -117,7 +115,7 @@ resp_dir(int fd, char *name, struct request *r)
"<title>Index of %s</title></head>\n"
"\t<body>\n\t\t<a href=\"..\">..</a>",
esc) < 0) {
s = S_REQUEST_TIMEOUT;
res.status = S_REQUEST_TIMEOUT;
goto cleanup;
}
@ -135,18 +133,17 @@ resp_dir(int fd, char *name, struct request *r)
(e[i]->d_type == DT_DIR) ? "/" : "",
esc,
suffix(e[i]->d_type)) < 0) {
s = S_REQUEST_TIMEOUT;
res.status = S_REQUEST_TIMEOUT;
goto cleanup;
}
}
/* listing footer */
if (dprintf(fd, "\n\t</body>\n</html>\n") < 0) {
s = S_REQUEST_TIMEOUT;
res.status = S_REQUEST_TIMEOUT;
goto cleanup;
}
}
s = S_OK;
cleanup:
while (dirlen--) {
@ -154,7 +151,7 @@ cleanup:
}
free(e);
return s;
return res.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)
{
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;
off_t remaining;
int range;
static char buf[BUFSIZ], *p, t1[TIMESTAMP_LEN], t2[TIMESTAMP_LEN];
static char buf[BUFSIZ], *p;
/* open file */
if (!(fp = fopen(name, "r"))) {
s = http_send_status(fd, S_FORBIDDEN);
res.status = http_send_status(fd, S_FORBIDDEN);
goto cleanup;
}
/* seek to lower bound */
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;
}
/* 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 */
range = r->field[REQ_RANGE][0];
s = range ? S_PARTIAL_CONTENT : S_OK;
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;
if ((sendstatus = http_send_header(fd, &res)) != res.status) {
res.status = sendstatus;
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),
(size_t)remaining), fp))) {
if (bread < 0) {
s = S_INTERNAL_SERVER_ERROR;
res.status = S_INTERNAL_SERVER_ERROR;
goto cleanup;
}
remaining -= bread;
@ -225,7 +227,7 @@ resp_file(int fd, char *name, struct request *r, struct stat *st, char *mime,
while (bread > 0) {
bwritten = write(fd, p, bread);
if (bwritten <= 0) {
s = S_REQUEST_TIMEOUT;
res.status = S_REQUEST_TIMEOUT;
goto cleanup;
}
bread -= bwritten;
@ -238,5 +240,5 @@ cleanup:
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__ */
}
char *
timestamp(time_t t, char buf[TIMESTAMP_LEN])
int
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

4
util.h
View file

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