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 <dev@frign.de>
This commit is contained in:
parent
a5163d0813
commit
58d0f44e03
7 changed files with 297 additions and 266 deletions
261
http.c
261
http.c
|
@ -61,7 +61,7 @@ const char *res_field_str[] = {
|
||||||
enum status
|
enum status
|
||||||
http_send_header(int fd, const struct response *res)
|
http_send_header(int fd, const struct response *res)
|
||||||
{
|
{
|
||||||
char t[FIELD_MAX];
|
char t[FIELD_MAX], esc[PATH_MAX];
|
||||||
size_t i;
|
size_t i;
|
||||||
|
|
||||||
if (timestamp(t, sizeof(t), time(NULL))) {
|
if (timestamp(t, sizeof(t), time(NULL))) {
|
||||||
|
@ -89,6 +89,18 @@ http_send_header(int fd, const struct response *res)
|
||||||
return S_REQUEST_TIMEOUT;
|
return S_REQUEST_TIMEOUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* listing header */
|
||||||
|
if (res->type == RESTYPE_DIRLISTING) {
|
||||||
|
html_escape(res->uri, esc, sizeof(esc));
|
||||||
|
if (dprintf(fd,
|
||||||
|
"<!DOCTYPE html>\n<html>\n\t<head>"
|
||||||
|
"<title>Index of %s</title></head>\n"
|
||||||
|
"\t<body>\n\t\t<a href=\"..\">..</a>",
|
||||||
|
esc) < 0) {
|
||||||
|
return S_REQUEST_TIMEOUT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return res->status;
|
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))
|
#define RELPATH(x) ((!*(x) || !strcmp(x, "/")) ? "." : ((x) + 1))
|
||||||
|
|
||||||
enum status
|
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;
|
enum status returnstatus;
|
||||||
struct in6_addr addr;
|
struct in6_addr addr;
|
||||||
struct response res = { 0 };
|
|
||||||
struct stat st;
|
struct stat st;
|
||||||
struct tm tm = { 0 };
|
struct tm tm = { 0 };
|
||||||
|
struct vhost *vhost;
|
||||||
size_t len, i;
|
size_t len, i;
|
||||||
size_t lower, upper;
|
|
||||||
int hasport, ipv6host;
|
int hasport, ipv6host;
|
||||||
static char realtarget[PATH_MAX], tmptarget[PATH_MAX];
|
static char realuri[PATH_MAX], tmpuri[PATH_MAX];
|
||||||
char *p, *mime;
|
char *p, *mime;
|
||||||
const char *vhostmatch, *targethost;
|
const char *targethost;
|
||||||
|
|
||||||
/* make a working copy of the target */
|
/* empty all response fields */
|
||||||
memcpy(realtarget, req->target, sizeof(realtarget));
|
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 */
|
/* match vhost */
|
||||||
vhostmatch = NULL;
|
vhost = NULL;
|
||||||
if (s->vhost) {
|
if (s->vhost) {
|
||||||
for (i = 0; i < s->vhost_len; i++) {
|
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],
|
||||||
if (!regexec(&(s->vhost[i].re), req->field[REQ_HOST], 0,
|
0, NULL, 0)) {
|
||||||
NULL, 0)) {
|
/* we have a matching vhost */
|
||||||
if (chdir(s->vhost[i].dir) < 0) {
|
vhost = &(s->vhost[i]);
|
||||||
return http_send_status(fd, (errno == EACCES) ?
|
|
||||||
S_FORBIDDEN : S_NOT_FOUND);
|
|
||||||
}
|
|
||||||
vhostmatch = s->vhost[i].chost;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (i == s->vhost_len) {
|
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 we have a vhost prefix, prepend it to the URI */
|
||||||
if (s->vhost[i].prefix) {
|
if (vhost->prefix &&
|
||||||
if (esnprintf(tmptarget, sizeof(tmptarget), "%s%s",
|
prepend(realuri, LEN(realuri), vhost->prefix)) {
|
||||||
s->vhost[i].prefix, realtarget)) {
|
return S_REQUEST_TOO_LARGE;
|
||||||
return http_send_status(fd, S_REQUEST_TOO_LARGE);
|
|
||||||
}
|
|
||||||
memcpy(realtarget, tmptarget, sizeof(realtarget));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* apply target prefix mapping */
|
/* apply target prefix mapping */
|
||||||
for (i = 0; i < s->map_len; i++) {
|
for (i = 0; i < s->map_len; i++) {
|
||||||
len = strlen(s->map[i].from);
|
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
|
/* match canonical host if vhosts are enabled and
|
||||||
* the mapping specifies a canonical host */
|
* the mapping specifies a canonical host */
|
||||||
if (s->vhost && s->map[i].chost &&
|
if (s->vhost && s->map[i].chost &&
|
||||||
strcmp(s->map[i].chost, vhostmatch)) {
|
strcmp(s->map[i].chost, vhost->chost)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* swap out target prefix */
|
/* swap out target prefix */
|
||||||
if (esnprintf(tmptarget, sizeof(tmptarget), "%s%s",
|
memmove(realuri, realuri + len, strlen(realuri) + 1);
|
||||||
s->map[i].to, realtarget + len)) {
|
if (prepend(realuri, LEN(realuri), s->map[i].to)) {
|
||||||
return http_send_status(fd, S_REQUEST_TOO_LARGE);
|
return S_REQUEST_TOO_LARGE;
|
||||||
}
|
}
|
||||||
memcpy(realtarget, tmptarget, sizeof(realtarget));
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* normalize target */
|
/* normalize URI again, in case we introduced dirt */
|
||||||
if (normabspath(realtarget)) {
|
if (normabspath(realuri)) {
|
||||||
return http_send_status(fd, S_BAD_REQUEST);
|
return S_BAD_REQUEST;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* stat the target */
|
/* stat the relative path derived from the URI */
|
||||||
if (stat(RELPATH(realtarget), &st) < 0) {
|
if (stat(RELPATH(realuri), &st) < 0) {
|
||||||
return http_send_status(fd, (errno == EACCES) ?
|
return (errno == EACCES) ? S_FORBIDDEN : S_NOT_FOUND;
|
||||||
S_FORBIDDEN : S_NOT_FOUND);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (S_ISDIR(st.st_mode)) {
|
if (S_ISDIR(st.st_mode)) {
|
||||||
/* add / to target if not present */
|
/* append '/' to URI if not present */
|
||||||
len = strlen(realtarget);
|
len = strlen(realuri);
|
||||||
if (len >= PATH_MAX - 2) {
|
if (len + 1 + 1 > PATH_MAX) {
|
||||||
return http_send_status(fd, S_REQUEST_TOO_LARGE);
|
return S_REQUEST_TOO_LARGE;
|
||||||
}
|
}
|
||||||
if (len && realtarget[len - 1] != '/') {
|
if (len > 0 && realuri[len - 1] != '/') {
|
||||||
realtarget[len] = '/';
|
realuri[len] = '/';
|
||||||
realtarget[len + 1] = '\0';
|
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
|
* reject hidden target, except if it is a well-known URI
|
||||||
* according to RFC 8615
|
* according to RFC 8615
|
||||||
*/
|
*/
|
||||||
if (strstr(realtarget, "/.") && strncmp(realtarget,
|
if (strstr(realuri, "/.") && strncmp(realuri,
|
||||||
"/.well-known/", sizeof("/.well-known/") - 1)) {
|
"/.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 &&
|
* redirect if the original URI and the "real" URI differ or if
|
||||||
strcmp(req->field[REQ_HOST], vhostmatch))) {
|
* the requested host is non-canonical
|
||||||
res.status = S_MOVED_PERMANENTLY;
|
*/
|
||||||
|
if (strcmp(req->target, realuri) || (s->vhost && vhost &&
|
||||||
|
strcmp(req->field[REQ_HOST], vhost->chost))) {
|
||||||
|
res->status = S_MOVED_PERMANENTLY;
|
||||||
|
|
||||||
/* encode realtarget */
|
/* encode realuri */
|
||||||
encode(realtarget, tmptarget);
|
encode(realuri, tmpuri);
|
||||||
|
|
||||||
/* determine target location */
|
/* determine target location */
|
||||||
if (s->vhost) {
|
if (s->vhost) {
|
||||||
/* absolute redirection URL */
|
/* absolute redirection URL */
|
||||||
targethost = req->field[REQ_HOST][0] ? vhostmatch ?
|
targethost = req->field[REQ_HOST][0] ? vhost->chost ?
|
||||||
vhostmatch : req->field[REQ_HOST] : s->host ?
|
vhost->chost : req->field[REQ_HOST] : s->host ?
|
||||||
s->host : "localhost";
|
s->host : "localhost";
|
||||||
|
|
||||||
/* do we need to add a port to the Location? */
|
/* 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 */
|
* honor that later when we fill the "Location"-field */
|
||||||
if ((ipv6host = inet_pton(AF_INET6, targethost,
|
if ((ipv6host = inet_pton(AF_INET6, targethost,
|
||||||
&addr)) < 0) {
|
&addr)) < 0) {
|
||||||
return http_send_status(fd,
|
return S_INTERNAL_SERVER_ERROR;
|
||||||
S_INTERNAL_SERVER_ERROR);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* write location to response struct */
|
/* write location to response struct */
|
||||||
if (esnprintf(res.field[RES_LOCATION],
|
if (esnprintf(res->field[RES_LOCATION],
|
||||||
sizeof(res.field[RES_LOCATION]),
|
sizeof(res->field[RES_LOCATION]),
|
||||||
"//%s%s%s%s%s%s",
|
"//%s%s%s%s%s%s",
|
||||||
ipv6host ? "[" : "",
|
ipv6host ? "[" : "",
|
||||||
targethost,
|
targethost,
|
||||||
ipv6host ? "]" : "", hasport ? ":" : "",
|
ipv6host ? "]" : "", hasport ? ":" : "",
|
||||||
hasport ? s->port : "", tmptarget)) {
|
hasport ? s->port : "", tmpuri)) {
|
||||||
return http_send_status(fd, S_REQUEST_TOO_LARGE);
|
return S_REQUEST_TOO_LARGE;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
/* write relative redirection URL to response struct */
|
/* write relative redirection URI to response struct */
|
||||||
if (esnprintf(res.field[RES_LOCATION],
|
if (esnprintf(res->field[RES_LOCATION],
|
||||||
sizeof(res.field[RES_LOCATION]),
|
sizeof(res->field[RES_LOCATION]),
|
||||||
tmptarget)) {
|
"%s", tmpuri)) {
|
||||||
return http_send_status(fd, S_REQUEST_TOO_LARGE);
|
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)) {
|
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)) {
|
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 */
|
/* 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) {
|
if (s->listdirs) {
|
||||||
/* remove index suffix and serve dir */
|
/* serve directory listing */
|
||||||
realtarget[strlen(realtarget) -
|
res->type = RESTYPE_DIRLISTING;
|
||||||
strlen(s->docindex)] = '\0';
|
res->status = (access(res->path, R_OK)) ?
|
||||||
return resp_dir(fd, RELPATH(realtarget), req);
|
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 {
|
} else {
|
||||||
/* reject */
|
/* reject */
|
||||||
if (!S_ISREG(st.st_mode) || errno == EACCES) {
|
return (!S_ISREG(st.st_mode) || errno == EACCES) ?
|
||||||
return http_send_status(fd, S_FORBIDDEN);
|
S_FORBIDDEN : S_NOT_FOUND;
|
||||||
} else {
|
|
||||||
return http_send_status(fd, S_NOT_FOUND);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -714,39 +747,39 @@ http_send_response(int fd, const struct request *req, const struct server *s)
|
||||||
/* parse field */
|
/* parse field */
|
||||||
if (!strptime(req->field[REQ_IF_MODIFIED_SINCE],
|
if (!strptime(req->field[REQ_IF_MODIFIED_SINCE],
|
||||||
"%a, %d %b %Y %T GMT", &tm)) {
|
"%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 */
|
/* 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) {
|
||||||
res.status = S_NOT_MODIFIED;
|
res->status = S_NOT_MODIFIED;
|
||||||
return http_send_header(fd, &res);
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* range */
|
/* range */
|
||||||
if ((returnstatus = parse_range(req->field[REQ_RANGE],
|
if ((returnstatus = parse_range(req->field[REQ_RANGE], st.st_size,
|
||||||
st.st_size, &lower, &upper))) {
|
&(res->file.lower),
|
||||||
|
&(res->file.upper)))) {
|
||||||
if (returnstatus == S_RANGE_NOT_SATISFIABLE) {
|
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],
|
if (esnprintf(res->field[RES_CONTENT_RANGE],
|
||||||
sizeof(res.field[RES_CONTENT_RANGE]),
|
sizeof(res->field[RES_CONTENT_RANGE]),
|
||||||
"bytes */%zu", st.st_size)) {
|
"bytes */%zu", st.st_size)) {
|
||||||
return http_send_status(fd,
|
return S_INTERNAL_SERVER_ERROR;
|
||||||
S_INTERNAL_SERVER_ERROR);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return http_send_header(fd, &res);
|
return 0;
|
||||||
} else {
|
} else {
|
||||||
return http_send_status(fd, returnstatus);
|
return returnstatus;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* mime */
|
/* mime */
|
||||||
mime = "application/octet-stream";
|
mime = "application/octet-stream";
|
||||||
if ((p = strrchr(realtarget, '.'))) {
|
if ((p = strrchr(realuri, '.'))) {
|
||||||
for (i = 0; i < sizeof(mimes) / sizeof(*mimes); i++) {
|
for (i = 0; i < LEN(mimes); i++) {
|
||||||
if (!strcmp(mimes[i].ext, p + 1)) {
|
if (!strcmp(mimes[i].ext, p + 1)) {
|
||||||
mime = mimes[i].type;
|
mime = mimes[i].type;
|
||||||
break;
|
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;
|
||||||
}
|
}
|
||||||
|
|
9
http.h
9
http.h
|
@ -3,7 +3,6 @@
|
||||||
#define HTTP_H
|
#define HTTP_H
|
||||||
|
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#include <sys/stat.h>
|
|
||||||
|
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
|
||||||
|
@ -67,8 +66,9 @@ enum res_field {
|
||||||
extern const char *res_field_str[];
|
extern const char *res_field_str[];
|
||||||
|
|
||||||
enum res_type {
|
enum res_type {
|
||||||
|
RESTYPE_ERROR,
|
||||||
RESTYPE_FILE,
|
RESTYPE_FILE,
|
||||||
RESTYPE_DIR,
|
RESTYPE_DIRLISTING,
|
||||||
NUM_RES_TYPES,
|
NUM_RES_TYPES,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -76,10 +76,9 @@ struct response {
|
||||||
enum res_type type;
|
enum res_type type;
|
||||||
enum status status;
|
enum status status;
|
||||||
char field[NUM_RES_FIELDS][FIELD_MAX];
|
char field[NUM_RES_FIELDS][FIELD_MAX];
|
||||||
|
char uri[PATH_MAX];
|
||||||
char path[PATH_MAX];
|
char path[PATH_MAX];
|
||||||
struct stat st;
|
|
||||||
struct {
|
struct {
|
||||||
char *mime;
|
|
||||||
size_t lower;
|
size_t lower;
|
||||||
size_t upper;
|
size_t upper;
|
||||||
} file;
|
} 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_send_status(int, enum status);
|
||||||
enum status http_recv_header(int, char *, size_t, size_t *);
|
enum status http_recv_header(int, char *, size_t, size_t *);
|
||||||
enum status http_parse_header(const char *, struct request *);
|
enum status http_parse_header(const char *, struct request *);
|
||||||
enum status http_send_response(int fd, const struct request *,
|
enum status http_prepare_response(const struct request *, struct response *,
|
||||||
const struct server *);
|
const struct server *);
|
||||||
|
|
||||||
#endif /* HTTP_H */
|
#endif /* HTTP_H */
|
||||||
|
|
16
main.c
16
main.c
|
@ -16,6 +16,7 @@
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "resp.h"
|
||||||
#include "http.h"
|
#include "http.h"
|
||||||
#include "sock.h"
|
#include "sock.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
@ -37,12 +38,19 @@ serve(int infd, const struct sockaddr_storage *in_sa, const struct server *s)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* handle request */
|
/* handle request */
|
||||||
if ((status = http_recv_header(c.fd, c.header, LEN(c.header), &c.off))) {
|
if ((status = http_recv_header(c.fd, c.header, LEN(c.header), &c.off)) ||
|
||||||
status = http_send_status(c.fd, status);
|
(status = http_parse_header(c.header, &c.req)) ||
|
||||||
} else if ((status = http_parse_header(c.header, &c.req))) {
|
(status = http_prepare_response(&c.req, &c.res, s))) {
|
||||||
status = http_send_status(c.fd, status);
|
status = http_send_status(c.fd, status);
|
||||||
} else {
|
} 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 */
|
/* write output to log */
|
||||||
|
|
142
resp.c
142
resp.c
|
@ -38,85 +38,18 @@ suffix(int t)
|
||||||
return "";
|
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
|
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 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;
|
int dirlen;
|
||||||
char esc[PATH_MAX /* > NAME_MAX */ * 6]; /* strlen("&...;") <= 6 */
|
char esc[PATH_MAX /* > NAME_MAX */ * 6]; /* strlen("&...;") <= 6 */
|
||||||
|
|
||||||
/* read directory */
|
/* read directory */
|
||||||
if ((dirlen = scandir(name, &e, NULL, compareent)) < 0) {
|
if ((dirlen = scandir(res->path, &e, NULL, compareent)) < 0) {
|
||||||
return http_send_status(fd, S_FORBIDDEN);
|
return S_FORBIDDEN;
|
||||||
}
|
|
||||||
|
|
||||||
/* 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) {
|
|
||||||
/* listing header */
|
|
||||||
html_escape(name, esc, sizeof(esc));
|
|
||||||
if (dprintf(fd,
|
|
||||||
"<!DOCTYPE html>\n<html>\n\t<head>"
|
|
||||||
"<title>Index of %s</title></head>\n"
|
|
||||||
"\t<body>\n\t\t<a href=\"..\">..</a>",
|
|
||||||
esc) < 0) {
|
|
||||||
res.status = S_REQUEST_TIMEOUT;
|
|
||||||
goto cleanup;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* listing */
|
/* listing */
|
||||||
|
@ -133,17 +66,16 @@ resp_dir(int fd, const char *name, const struct request *req)
|
||||||
(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) {
|
||||||
res.status = S_REQUEST_TIMEOUT;
|
ret = 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) {
|
||||||
res.status = S_REQUEST_TIMEOUT;
|
ret = S_REQUEST_TIMEOUT;
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
cleanup:
|
cleanup:
|
||||||
while (dirlen--) {
|
while (dirlen--) {
|
||||||
|
@ -151,76 +83,37 @@ cleanup:
|
||||||
}
|
}
|
||||||
free(e);
|
free(e);
|
||||||
|
|
||||||
return res.status;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum status
|
enum status
|
||||||
resp_file(int fd, const char *name, const struct request *req,
|
resp_file(int fd, const struct response *res)
|
||||||
const struct stat *st, const char *mime, size_t lower,
|
|
||||||
size_t upper)
|
|
||||||
{
|
{
|
||||||
FILE *fp;
|
FILE *fp;
|
||||||
enum status sendstatus;
|
enum status ret = 0;
|
||||||
struct response res = {
|
|
||||||
.status = (req->field[REQ_RANGE][0] != '\0') ?
|
|
||||||
S_PARTIAL_CONTENT : S_OK,
|
|
||||||
.field[RES_ACCEPT_RANGES] = "bytes",
|
|
||||||
};
|
|
||||||
ssize_t bread, bwritten;
|
ssize_t bread, bwritten;
|
||||||
size_t remaining;
|
size_t remaining;
|
||||||
static char buf[BUFSIZ], *p;
|
static char buf[BUFSIZ], *p;
|
||||||
|
|
||||||
/* open file */
|
/* open file */
|
||||||
if (!(fp = fopen(name, "r"))) {
|
if (!(fp = fopen(res->path, "r"))) {
|
||||||
res.status = http_send_status(fd, S_FORBIDDEN);
|
ret = S_FORBIDDEN;
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* seek to lower bound */
|
/* seek to lower bound */
|
||||||
if (fseek(fp, lower, SEEK_SET)) {
|
if (fseek(fp, res->file.lower, SEEK_SET)) {
|
||||||
res.status = http_send_status(fd, S_INTERNAL_SERVER_ERROR);
|
ret = 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 (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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 */
|
/* write data until upper bound is hit */
|
||||||
remaining = upper - lower + 1;
|
remaining = res->file.upper - res->file.lower + 1;
|
||||||
|
|
||||||
while ((bread = fread(buf, 1, MIN(sizeof(buf),
|
while ((bread = fread(buf, 1, MIN(sizeof(buf),
|
||||||
remaining), fp))) {
|
remaining), fp))) {
|
||||||
if (bread < 0) {
|
if (bread < 0) {
|
||||||
res.status = S_INTERNAL_SERVER_ERROR;
|
ret = S_INTERNAL_SERVER_ERROR;
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
}
|
}
|
||||||
remaining -= bread;
|
remaining -= bread;
|
||||||
|
@ -228,18 +121,17 @@ resp_file(int fd, const char *name, const struct request *req,
|
||||||
while (bread > 0) {
|
while (bread > 0) {
|
||||||
bwritten = write(fd, p, bread);
|
bwritten = write(fd, p, bread);
|
||||||
if (bwritten <= 0) {
|
if (bwritten <= 0) {
|
||||||
res.status = S_REQUEST_TIMEOUT;
|
ret = S_REQUEST_TIMEOUT;
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
}
|
}
|
||||||
bread -= bwritten;
|
bread -= bwritten;
|
||||||
p += bwritten;
|
p += bwritten;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
cleanup:
|
cleanup:
|
||||||
if (fp) {
|
if (fp) {
|
||||||
fclose(fp);
|
fclose(fp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
5
resp.h
5
resp.h
|
@ -7,8 +7,7 @@
|
||||||
|
|
||||||
#include "http.h"
|
#include "http.h"
|
||||||
|
|
||||||
enum status resp_dir(int, const char *, const struct request *);
|
enum status resp_dir(int, const struct response *);
|
||||||
enum status resp_file(int, const char *, const struct request *,
|
enum status resp_file(int, const struct response *);
|
||||||
const struct stat *, const char *, size_t, size_t);
|
|
||||||
|
|
||||||
#endif /* RESP_H */
|
#endif /* RESP_H */
|
||||||
|
|
60
util.c
60
util.c
|
@ -108,6 +108,66 @@ esnprintf(char *str, size_t size, const char *fmt, ...)
|
||||||
return (ret < 0 || (size_t)ret >= size);
|
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 INVALID 1
|
||||||
#define TOOSMALL 2
|
#define TOOSMALL 2
|
||||||
#define TOOLARGE 3
|
#define TOOLARGE 3
|
||||||
|
|
2
util.h
2
util.h
|
@ -51,6 +51,8 @@ void eunveil(const char *, const char *);
|
||||||
|
|
||||||
int timestamp(char *, size_t, time_t);
|
int timestamp(char *, size_t, time_t);
|
||||||
int esnprintf(char *, size_t, const char *, ...);
|
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);
|
void *reallocarray(void *, size_t, size_t);
|
||||||
long long strtonum(const char *, long long, long long, const char **);
|
long long strtonum(const char *, long long, long long, const char **);
|
||||||
|
|
Loading…
Reference in a new issue