diff --git a/http.c b/http.c index 49b30dc..b6b1ab7 100644 --- 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, + "\n\n\t" + "Index of %s\n" + "\t\n\t\t..", + esc) < 0) { + return S_REQUEST_TIMEOUT; + } + } + return res->status; } @@ -143,44 +155,52 @@ decode(const char src[PATH_MAX], char dest[PATH_MAX]) dest[i] = '\0'; } -int -http_get_request(int fd, struct request *req) +enum status +http_recv_header(int fd, char *h, size_t hsiz, size_t *off) +{ + ssize_t r; + + if (h == NULL || off == NULL || *off > hsiz) { + return S_INTERNAL_SERVER_ERROR; + } + + while (1) { + if ((r = read(fd, h + *off, hsiz - *off)) <= 0) { + return S_REQUEST_TIMEOUT; + } + *off += r; + + /* check if we are done (header terminated) */ + if (*off >= 4 && !memcmp(h + *off - 4, "\r\n\r\n", 4)) { + break; + } + + /* buffer is full or read over, but header is not terminated */ + if (r == 0 || *off == hsiz) { + return S_REQUEST_TOO_LARGE; + } + } + + /* header is complete, remove last \r\n and null-terminate */ + h[*off - 2] = '\0'; + + /* set *off to 0 to indicate we are finished */ + *off = 0; + + return 0; +} + +enum status +http_parse_header(const char *h, struct request *req) { struct in6_addr addr; - size_t hlen, i, mlen; - ssize_t off; - char h[HEADER_MAX], *p, *q; + size_t i, mlen; + const char *p, *q; + char *m, *n; /* empty all fields */ memset(req, 0, sizeof(*req)); - /* - * receive header - */ - for (hlen = 0; ;) { - if ((off = read(fd, h + hlen, sizeof(h) - hlen)) < 0) { - return http_send_status(fd, S_REQUEST_TIMEOUT); - } else if (off == 0) { - break; - } - hlen += off; - if (hlen >= 4 && !memcmp(h + hlen - 4, "\r\n\r\n", 4)) { - break; - } - if (hlen == sizeof(h)) { - return http_send_status(fd, S_REQUEST_TOO_LARGE); - } - } - - /* remove terminating empty line */ - if (hlen < 2) { - return http_send_status(fd, S_BAD_REQUEST); - } - hlen -= 2; - - /* null-terminate the header */ - h[hlen] = '\0'; - /* * parse request line */ @@ -194,12 +214,12 @@ http_get_request(int fd, struct request *req) } } if (i == NUM_REQ_METHODS) { - return http_send_status(fd, S_METHOD_NOT_ALLOWED); + return S_METHOD_NOT_ALLOWED; } /* a single space must follow the method */ if (h[mlen] != ' ') { - return http_send_status(fd, S_BAD_REQUEST); + return S_BAD_REQUEST; } /* basis for next step */ @@ -207,32 +227,32 @@ http_get_request(int fd, struct request *req) /* TARGET */ if (!(q = strchr(p, ' '))) { - return http_send_status(fd, S_BAD_REQUEST); + return S_BAD_REQUEST; } - *q = '\0'; if (q - p + 1 > PATH_MAX) { - return http_send_status(fd, S_REQUEST_TOO_LARGE); + return S_REQUEST_TOO_LARGE; } - memcpy(req->target, p, q - p + 1); - decode(req->target, req->target); + memcpy(req->uri, p, q - p); + req->uri[q - p] = '\0'; + decode(req->uri, req->uri); /* basis for next step */ p = q + 1; /* HTTP-VERSION */ if (strncmp(p, "HTTP/", sizeof("HTTP/") - 1)) { - return http_send_status(fd, S_BAD_REQUEST); + return S_BAD_REQUEST; } p += sizeof("HTTP/") - 1; if (strncmp(p, "1.0", sizeof("1.0") - 1) && strncmp(p, "1.1", sizeof("1.1") - 1)) { - return http_send_status(fd, S_VERSION_NOT_SUPPORTED); + return S_VERSION_NOT_SUPPORTED; } p += sizeof("1.*") - 1; /* check terminator */ if (strncmp(p, "\r\n", sizeof("\r\n") - 1)) { - return http_send_status(fd, S_BAD_REQUEST); + return S_BAD_REQUEST; } /* basis for next step */ @@ -253,7 +273,7 @@ http_get_request(int fd, struct request *req) if (i == NUM_REQ_FIELDS) { /* unmatched field, skip this line */ if (!(q = strstr(p, "\r\n"))) { - return http_send_status(fd, S_BAD_REQUEST); + return S_BAD_REQUEST; } p = q + (sizeof("\r\n") - 1); continue; @@ -263,7 +283,7 @@ http_get_request(int fd, struct request *req) /* a single colon must follow the field name */ if (*p != ':') { - return http_send_status(fd, S_BAD_REQUEST); + return S_BAD_REQUEST; } /* skip whitespace */ @@ -272,13 +292,13 @@ http_get_request(int fd, struct request *req) /* extract field content */ if (!(q = strstr(p, "\r\n"))) { - return http_send_status(fd, S_BAD_REQUEST); + return S_BAD_REQUEST; } - *q = '\0'; if (q - p + 1 > FIELD_MAX) { - return http_send_status(fd, S_REQUEST_TOO_LARGE); + return S_REQUEST_TOO_LARGE; } - memcpy(req->field[i], p, q - p + 1); + memcpy(req->field[i], p, q - p); + req->field[i][q - p] = '\0'; /* go to next line */ p = q + (sizeof("\r\n") - 1); @@ -288,37 +308,37 @@ http_get_request(int fd, struct request *req) * clean up host */ - p = strrchr(req->field[REQ_HOST], ':'); - q = strrchr(req->field[REQ_HOST], ']'); + m = strrchr(req->field[REQ_HOST], ':'); + n = strrchr(req->field[REQ_HOST], ']'); /* strip port suffix but don't interfere with IPv6 bracket notation * as per RFC 2732 */ - if (p && (!q || p > q)) { + if (m && (!n || m > n)) { /* port suffix must not be empty */ - if (*(p + 1) == '\0') { - return http_send_status(fd, S_BAD_REQUEST); + if (*(m + 1) == '\0') { + return S_BAD_REQUEST; } - *p = '\0'; + *m = '\0'; } /* strip the brackets from the IPv6 notation and validate the address */ - if (q) { + if (n) { /* brackets must be on the outside */ - if (req->field[REQ_HOST][0] != '[' || *(q + 1) != '\0') { - return http_send_status(fd, S_BAD_REQUEST); + if (req->field[REQ_HOST][0] != '[' || *(n + 1) != '\0') { + return S_BAD_REQUEST; } /* remove the right bracket */ - *q = '\0'; - p = req->field[REQ_HOST] + 1; + *n = '\0'; + m = req->field[REQ_HOST] + 1; /* validate the contained IPv6 address */ - if (inet_pton(AF_INET6, p, &addr) != 1) { - return http_send_status(fd, S_BAD_REQUEST); + if (inet_pton(AF_INET6, m, &addr) != 1) { + return S_BAD_REQUEST; } /* copy it into the host field */ - memmove(req->field[REQ_HOST], p, q - p + 1); + memmove(req->field[REQ_HOST], m, n - m + 1); } return 0; @@ -528,171 +548,196 @@ 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) +http_prepare_response(const struct request *req, struct response *res, + const struct server *srv) { 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; - /* make a working copy of the target */ - memcpy(realtarget, req->target, sizeof(realtarget)); + /* empty all response fields */ + memset(res, 0, sizeof(*res)); + + /* make a working copy of the URI and normalize it */ + memcpy(realuri, req->uri, sizeof(realuri)); + if (normabspath(realuri)) { + return S_BAD_REQUEST; + } /* match vhost */ - vhostmatch = 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; + vhost = NULL; + if (srv->vhost) { + for (i = 0; i < srv->vhost_len; i++) { + if (!regexec(&(srv->vhost[i].re), req->field[REQ_HOST], + 0, NULL, 0)) { + /* we have a matching vhost */ + vhost = &(srv->vhost[i]); break; } } - if (i == s.vhost_len) { - return http_send_status(fd, S_NOT_FOUND); + if (i == srv->vhost_len) { + 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)) { + /* apply URI prefix mapping */ + for (i = 0; i < srv->map_len; i++) { + len = strlen(srv->map[i].from); + if (!strncmp(realuri, srv->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)) { + if (srv->vhost && srv->map[i].chost && + strcmp(srv->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); + /* swap out URI prefix */ + memmove(realuri, realuri + len, strlen(realuri) + 1); + if (prepend(realuri, LEN(realuri), srv->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; } - /* reject hidden target */ - if (realtarget[0] == '.' || strstr(realtarget, "/.")) { - return http_send_status(fd, S_FORBIDDEN); - } - - /* 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'; } } - /* 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; + /* + * reject hidden targets, except if it is a well-known URI + * according to RFC 8615 + */ + if (strstr(realuri, "/.") && strncmp(realuri, + "/.well-known/", sizeof("/.well-known/") - 1)) { + return S_FORBIDDEN; + } - /* encode realtarget */ - encode(realtarget, tmptarget); + /* + * redirect if the original URI and the "real" URI differ or if + * the requested host is non-canonical + */ + if (strcmp(req->uri, realuri) || (srv->vhost && vhost && + strcmp(req->field[REQ_HOST], vhost->chost))) { + res->status = S_MOVED_PERMANENTLY; + + /* encode realuri */ + encode(realuri, tmpuri); /* determine target location */ - if (s.vhost) { + if (srv->vhost) { /* absolute redirection URL */ - targethost = req->field[REQ_HOST][0] ? vhostmatch ? - vhostmatch : req->field[REQ_HOST] : s.host ? - s.host : "localhost"; + targethost = req->field[REQ_HOST][0] ? vhost->chost ? + vhost->chost : req->field[REQ_HOST] : + srv->host ? srv->host : "localhost"; /* do we need to add a port to the Location? */ - hasport = s.port && strcmp(s.port, "80"); + hasport = srv->port && strcmp(srv->port, "80"); /* RFC 2732 specifies to use brackets for IPv6-addresses * 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, &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 ? srv->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->uri)) { + return S_REQUEST_TOO_LARGE; + } + if (esnprintf(res->path, sizeof(res->path), "%s%s", + vhost ? vhost->dir : "", RELPATH(req->uri))) { + return S_REQUEST_TOO_LARGE; + } } if (S_ISDIR(st.st_mode)) { - /* append docindex to target */ - if (esnprintf(realtarget, sizeof(realtarget), "%s%s", - req->target, s.docindex)) { - return http_send_status(fd, S_REQUEST_TOO_LARGE); + /* + * check if the directory index exists by appending it to + * the URI + */ + if (esnprintf(tmpuri, sizeof(tmpuri), "%s%s", + req->uri, srv->docindex)) { + 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 (s.listdirs) { - /* remove index suffix and serve dir */ - realtarget[strlen(realtarget) - - strlen(s.docindex)] = '\0'; - return resp_dir(fd, RELPATH(realtarget), req); + if (stat(RELPATH(tmpuri), &st) < 0 || !S_ISREG(st.st_mode)) { + if (srv->listdirs) { + /* 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; } } } @@ -702,39 +747,39 @@ http_send_response(int fd, const struct request *req) /* 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; @@ -742,5 +787,43 @@ http_send_response(int fd, const struct request *req) } } - 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; } diff --git a/http.h b/http.h index b6ba073..a9cf871 100644 --- a/http.h +++ b/http.h @@ -4,6 +4,8 @@ #include +#include "util.h" + #define HEADER_MAX 4096 #define FIELD_MAX 200 @@ -26,7 +28,7 @@ extern const char *req_method_str[]; struct request { enum req_method method; - char target[PATH_MAX]; + char uri[PATH_MAX]; char field[NUM_REQ_FIELDS][FIELD_MAX]; }; @@ -61,14 +63,47 @@ enum res_field { extern const char *res_field_str[]; +enum res_type { + RESTYPE_ERROR, + RESTYPE_FILE, + RESTYPE_DIRLISTING, + NUM_RES_TYPES, +}; + 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 { + size_t lower; + size_t upper; + } file; +}; + +enum conn_state { + C_VACANT, + C_RECV_HEADER, + C_SEND_HEADER, + C_SEND_DATA, + NUM_CONN_STATES, +}; + +struct connection { + enum conn_state state; + int fd; + char header[HEADER_MAX]; /* general req/res-header buffer */ + size_t off; /* general offset (header/file/dir) */ + struct request req; + struct response res; }; 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, const struct request *); +enum status http_recv_header(int, char *, size_t, size_t *); +enum status http_parse_header(const char *, struct request *); +enum status http_prepare_response(const struct request *, struct response *, + const struct server *); #endif /* HTTP_H */ diff --git a/main.c b/main.c index 0542fab..110af1a 100644 --- a/main.c +++ b/main.c @@ -16,6 +16,7 @@ #include #include +#include "resp.h" #include "http.h" #include "sock.h" #include "util.h" @@ -23,22 +24,33 @@ static char *udsname; static void -serve(int infd, const struct sockaddr_storage *in_sa) +serve(int infd, const struct sockaddr_storage *in_sa, const struct server *srv) { - struct request req; + struct connection c = { .fd = infd }; time_t t; enum status status; char inaddr[INET6_ADDRSTRLEN /* > INET_ADDRSTRLEN */]; char tstmp[21]; /* set connection timeout */ - if (sock_set_timeout(infd, 30)) { + if (sock_set_timeout(c.fd, 30)) { goto cleanup; } /* handle request */ - if (!(status = http_get_request(infd, &req))) { - status = http_send_response(infd, &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, srv))) { + status = http_send_status(c.fd, status); + } else { + 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 */ @@ -52,12 +64,12 @@ serve(int infd, const struct sockaddr_storage *in_sa) goto cleanup; } printf("%s\t%s\t%d\t%s\t%s\n", tstmp, inaddr, status, - req.field[REQ_HOST], req.target); + c.req.field[REQ_HOST], c.req.uri); cleanup: /* clean up and finish */ - shutdown(infd, SHUT_RD); - shutdown(infd, SHUT_WR); - close(infd); + shutdown(c.fd, SHUT_RD); + shutdown(c.fd, SHUT_WR); + close(c.fd); } static void @@ -177,6 +189,9 @@ main(int argc, char *argv[]) struct group *grp = NULL; struct passwd *pwd = NULL; struct rlimit rlim; + struct server srv = { + .docindex = "index.html", + }; struct sockaddr_storage in_sa; size_t i; socklen_t in_sa_len; @@ -190,13 +205,6 @@ main(int argc, char *argv[]) char *user = "nobody"; char *group = "nogroup"; - s.host = s.port = NULL; - s.vhost = NULL; - s.map = NULL; - s.vhost_len = s.map_len = 0; - s.docindex = "index.html"; - s.listdirs = 0; - ARGBEGIN { case 'd': servedir = EARGF(usage()); @@ -205,28 +213,28 @@ main(int argc, char *argv[]) group = EARGF(usage()); break; case 'h': - s.host = EARGF(usage()); + srv.host = EARGF(usage()); break; case 'i': - s.docindex = EARGF(usage()); - if (strchr(s.docindex, '/')) { + srv.docindex = EARGF(usage()); + if (strchr(srv.docindex, '/')) { die("The document index must not contain '/'"); } break; case 'l': - s.listdirs = 1; + srv.listdirs = 1; break; case 'm': if (spacetok(EARGF(usage()), tok, 3) || !tok[0] || !tok[1]) { usage(); } - if (!(s.map = reallocarray(s.map, ++s.map_len, + if (!(srv.map = reallocarray(srv.map, ++srv.map_len, sizeof(struct map)))) { die("reallocarray:"); } - s.map[s.map_len - 1].from = tok[0]; - s.map[s.map_len - 1].to = tok[1]; - s.map[s.map_len - 1].chost = tok[2]; + srv.map[srv.map_len - 1].from = tok[0]; + srv.map[srv.map_len - 1].to = tok[1]; + srv.map[srv.map_len - 1].chost = tok[2]; break; case 'n': maxnprocs = strtonum(EARGF(usage()), 1, INT_MAX, &err); @@ -235,7 +243,7 @@ main(int argc, char *argv[]) } break; case 'p': - s.port = EARGF(usage()); + srv.port = EARGF(usage()); break; case 'U': udsname = EARGF(usage()); @@ -248,14 +256,14 @@ main(int argc, char *argv[]) !tok[2]) { usage(); } - if (!(s.vhost = reallocarray(s.vhost, ++s.vhost_len, - sizeof(struct vhost)))) { + if (!(srv.vhost = reallocarray(srv.vhost, ++srv.vhost_len, + sizeof(*srv.vhost)))) { die("reallocarray:"); } - s.vhost[s.vhost_len - 1].chost = tok[0]; - s.vhost[s.vhost_len - 1].regex = tok[1]; - s.vhost[s.vhost_len - 1].dir = tok[2]; - s.vhost[s.vhost_len - 1].prefix = tok[3]; + srv.vhost[srv.vhost_len - 1].chost = tok[0]; + srv.vhost[srv.vhost_len - 1].regex = tok[1]; + srv.vhost[srv.vhost_len - 1].dir = tok[2]; + srv.vhost[srv.vhost_len - 1].prefix = tok[3]; break; default: usage(); @@ -266,7 +274,7 @@ main(int argc, char *argv[]) } /* can't have both host and UDS but must have one of port or UDS*/ - if ((s.host && udsname) || !(s.port || udsname)) { + if ((srv.host && udsname) || !(srv.port || udsname)) { usage(); } @@ -276,11 +284,11 @@ main(int argc, char *argv[]) } /* compile and check the supplied vhost regexes */ - for (i = 0; i < s.vhost_len; i++) { - if (regcomp(&s.vhost[i].re, s.vhost[i].regex, + for (i = 0; i < srv.vhost_len; i++) { + if (regcomp(&srv.vhost[i].re, srv.vhost[i].regex, REG_EXTENDED | REG_ICASE | REG_NOSUB)) { die("regcomp '%s': invalid regex", - s.vhost[i].regex); + srv.vhost[i].regex); } } @@ -309,7 +317,7 @@ main(int argc, char *argv[]) /* bind socket */ insock = udsname ? sock_get_uds(udsname, pwd->pw_uid, grp->gr_gid) : - sock_get_ips(s.host, s.port); + sock_get_ips(srv.host, srv.port); switch (fork()) { case -1: @@ -372,7 +380,7 @@ main(int argc, char *argv[]) /* fork and handle */ switch (fork()) { case 0: - serve(infd, &in_sa); + serve(infd, &in_sa, &srv); exit(0); break; case -1: diff --git a/quark.1 b/quark.1 index a86f8e7..6e0e5f8 100644 --- a/quark.1 +++ b/quark.1 @@ -1,4 +1,4 @@ -.Dd 2019-02-24 +.Dd 2020-08-23 .Dt QUARK 1 .Os suckless.org .Sh NAME @@ -30,6 +30,15 @@ .Sh DESCRIPTION .Nm is a simple HTTP GET/HEAD-only web server for static content. +It supports virtual hosts (see +.Fl v ) , +explicit redirects (see +.Fl m ) , +directory listings (see +.Fl l ) , +conditional "If-Modified-Since"-requests (RFC 7232), range requests +(RFC 7233) and well-known URIs (RFC 8615), while refusing to serve +hidden files and directories. .Sh OPTIONS .Bl -tag -width Ds .It Fl d Ar dir @@ -46,6 +55,7 @@ The default is "nogroup". Use .Ar host as the server hostname. +The default is the loopback interface (i.e. localhost). .It Fl i Ar file Set .Ar file @@ -54,7 +64,7 @@ The default is "index.html". .It Fl l Enable directory listing. .It Fl m Ar map -Add the target prefix mapping rule specified by +Add the URI prefix mapping rule specified by .Ar map , which has the form .Qq Pa from to [chost] , @@ -63,7 +73,7 @@ escaped with '\\'. .Pp The prefix .Pa from -of all matching targets is replaced with +of all matching URIs is replaced with .Pa to , optionally limited to the canonical virtual host .Pa chost . @@ -108,7 +118,7 @@ is redirected to the canonical host .Pa chost , if they differ, using the directory .Pa dir -as the root directory, optionally prefixing the target with +as the root directory, optionally prefixing the URI with .Pa prefix . If any virtual hosts are specified, all requests on non-matching hosts are discarded. diff --git a/resp.c b/resp.c index 2f639e5..b7441dc 100644 --- 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[] = { - { '&', "&" }, - { '<', "<" }, - { '>', ">" }, - { '"', """ }, - { '\'', "'" }, - }; - 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; + /* 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, "
\n\t\t%s%s", + 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\n\n") < 0) { + ret = S_REQUEST_TIMEOUT; goto cleanup; } - if (req->method == M_GET) { - /* listing header */ - html_escape(name, esc, sizeof(esc)); - if (dprintf(fd, - "\n\n\t" - "Index of %s\n" - "\t\n\t\t..", - 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, "
\n\t\t%s%s", - esc, - (e[i]->d_type == DT_DIR) ? "/" : "", - esc, - suffix(e[i]->d_type)) < 0) { - res.status = S_REQUEST_TIMEOUT; - goto cleanup; - } - } - - /* listing footer */ - if (dprintf(fd, "\n\t\n\n") < 0) { - res.status = 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); + /* write data until upper bound is hit */ + remaining = res->file.upper - res->file.lower + 1; + + while ((bread = fread(buf, 1, MIN(sizeof(buf), + remaining), fp))) { + if (bread < 0) { + ret = S_INTERNAL_SERVER_ERROR; + goto cleanup; } - } - 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 */ - remaining = upper - lower + 1; - - while ((bread = fread(buf, 1, MIN(sizeof(buf), - remaining), fp))) { - if (bread < 0) { - res.status = S_INTERNAL_SERVER_ERROR; + 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; } diff --git a/resp.h b/resp.h index 32d4c5d..92e5ad6 100644 --- 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 */ diff --git a/sock.c b/sock.c index dd99bdb..c1fbd43 100644 --- a/sock.c +++ b/sock.c @@ -63,7 +63,7 @@ void sock_rem_uds(const char *udsname) { if (unlink(udsname) < 0) { - die("unlink:"); + die("unlink '%s':", udsname); } } @@ -74,7 +74,8 @@ sock_get_uds(const char *udsname, uid_t uid, gid_t gid) .sun_family = AF_UNIX, }; size_t udsnamelen; - int insock, sockmode = S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH; + int insock, sockmode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | + S_IROTH | S_IWOTH; if ((insock = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) { die("socket:"); @@ -86,7 +87,7 @@ sock_get_uds(const char *udsname, uid_t uid, gid_t gid) memcpy(addr.sun_path, udsname, udsnamelen + 1); if (bind(insock, (const struct sockaddr *)&addr, sizeof(addr)) < 0) { - die("bind %s:", udsname); + die("bind '%s':", udsname); } if (listen(insock, SOMAXCONN) < 0) { @@ -96,12 +97,12 @@ sock_get_uds(const char *udsname, uid_t uid, gid_t gid) if (chmod(udsname, sockmode) < 0) { sock_rem_uds(udsname); - die("chmod:"); + die("chmod '%s':", udsname); } if (chown(udsname, uid, gid) < 0) { sock_rem_uds(udsname); - die("chown:"); + die("chown '%s':", udsname); } return insock; diff --git a/util.c b/util.c index 7ad512f..2b54df1 100644 --- a/util.c +++ b/util.c @@ -16,7 +16,6 @@ #include "util.h" char *argv0; -struct server s; static void verr(const char *fmt, va_list ap) @@ -109,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[] = { + { '&', "&" }, + { '<', "<" }, + { '>', ">" }, + { '"', """ }, + { '\'', "'" }, + }; + 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 diff --git a/util.h b/util.h index b23a192..6b6d17d 100644 --- a/util.h +++ b/util.h @@ -23,7 +23,7 @@ struct map { char *to; }; -extern struct server { +struct server { char *host; char *port; char *docindex; @@ -32,7 +32,7 @@ extern struct server { size_t vhost_len; struct map *map; size_t map_len; -} s; +}; #undef MIN #define MIN(x,y) ((x) < (y) ? (x) : (y)) @@ -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 **);