URI: 
       Refactor http_send_response() into http_prepare_response() - quark - quark web server
  HTML git clone git://git.suckless.org/quark
   DIR Log
   DIR Files
   DIR Refs
   DIR LICENSE
       ---
   DIR commit 58d0f44e0395fe37b3575da35992b3d3e7f262d7
   DIR parent a5163d08135b81b271b1d7ba36b24e342b57961f
  HTML Author: Laslo Hunhold <dev@frign.de>
       Date:   Sat, 22 Aug 2020 23:20:00 +0200
       
       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>
       
       Diffstat:
         M http.c                              |     261 ++++++++++++++++++++-----------
         M http.h                              |      11 +++++------
         M main.c                              |      16 ++++++++++++----
         M resp.c                              |     202 +++++++------------------------
         M resp.h                              |       5 ++---
         M util.c                              |      60 +++++++++++++++++++++++++++++++
         M util.h                              |       2 ++
       
       7 files changed, 294 insertions(+), 263 deletions(-)
       ---
   DIR diff --git 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,
       +                            "<!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;
        }
        
       @@ -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;
       +
       +        /* empty all response fields */
       +        memset(res, 0, sizeof(*res));
        
       -        /* make a working copy of the target */
       -        memcpy(realtarget, req->target, sizeof(realtarget));
       +        /* 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;
        }
   DIR diff --git a/http.h b/http.h
       @@ -3,7 +3,6 @@
        #define HTTP_H
        
        #include <limits.h>
       -#include <sys/stat.h>
        
        #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 */
   DIR diff --git a/main.c b/main.c
       @@ -16,6 +16,7 @@
        #include <time.h>
        #include <unistd.h>
        
       +#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 */
   DIR diff --git 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[] = {
       -                { '&',  "&amp;"  },
       -                { '<',  "&lt;"   },
       -                { '>',  "&gt;"   },
       -                { '"',  "&quot;" },
       -                { '\'', "&#x27;" },
       -        };
       -        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;
       -                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 */
       -                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, "<br />\n\t\t<a href=\"%s%s\">%s%s</a>",
       -                                    esc,
       -                                    (e[i]->d_type == DT_DIR) ? "/" : "",
       -                                    esc,
       -                                    suffix(e[i]->d_type)) < 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;
                        }
        
       -                /* listing footer */
       -                if (dprintf(fd, "\n\t</body>\n</html>\n") < 0) {
       -                        res.status = S_REQUEST_TIMEOUT;
       +                /* entry line */
       +                html_escape(e[i]->d_name, esc, sizeof(esc));
       +                if (dprintf(fd, "<br />\n\t\t<a href=\"%s%s\">%s%s</a>",
       +                            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</body>\n</html>\n") < 0) {
       +                ret = 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);
       -                }
       -        }
       -        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;
       -        }
       +        /* write data until upper bound is hit */
       +        remaining = res->file.upper - res->file.lower + 1;
        
       -        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;
       +        while ((bread = fread(buf, 1, MIN(sizeof(buf),
       +                              remaining), fp))) {
       +                if (bread < 0) {
       +                        ret = S_INTERNAL_SERVER_ERROR;
       +                        goto cleanup;
       +                }
       +                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;
        }
   DIR diff --git 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 */
   DIR diff --git 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[] = {
       +                { '&',  "&amp;"  },
       +                { '<',  "&lt;"   },
       +                { '>',  "&gt;"   },
       +                { '"',  "&quot;" },
       +                { '\'', "&#x27;" },
       +        };
       +        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
   DIR diff --git 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 **);