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:
parent
26c593ade1
commit
c51b31d7ac
5 changed files with 175 additions and 119 deletions
158
http.c
158
http.c
|
@ -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),
|
||||
ipv6host ? "[" : "",
|
||||
targethost,
|
||||
ipv6host ? "]" : "", hasport ? ":" : "",
|
||||
hasport ? s.port : "", tmptarget) < 0) {
|
||||
return S_REQUEST_TIMEOUT;
|
||||
/* 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)) {
|
||||
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
19
http.h
|
@ -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
100
resp.c
|
@ -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
13
util.c
|
@ -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
4
util.h
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue