Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Armin Friedl 2020-08-23 16:31:13 +02:00
commit 670d2ed65c
9 changed files with 475 additions and 386 deletions

429
http.c
View file

@ -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;
} }
@ -143,44 +155,52 @@ decode(const char src[PATH_MAX], char dest[PATH_MAX])
dest[i] = '\0'; dest[i] = '\0';
} }
int enum status
http_get_request(int fd, struct request *req) 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; struct in6_addr addr;
size_t hlen, i, mlen; size_t i, mlen;
ssize_t off; const char *p, *q;
char h[HEADER_MAX], *p, *q; char *m, *n;
/* empty all fields */ /* empty all fields */
memset(req, 0, sizeof(*req)); 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 * parse request line
*/ */
@ -194,12 +214,12 @@ http_get_request(int fd, struct request *req)
} }
} }
if (i == NUM_REQ_METHODS) { 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 */ /* a single space must follow the method */
if (h[mlen] != ' ') { if (h[mlen] != ' ') {
return http_send_status(fd, S_BAD_REQUEST); return S_BAD_REQUEST;
} }
/* basis for next step */ /* basis for next step */
@ -207,32 +227,32 @@ http_get_request(int fd, struct request *req)
/* TARGET */ /* TARGET */
if (!(q = strchr(p, ' '))) { if (!(q = strchr(p, ' '))) {
return http_send_status(fd, S_BAD_REQUEST); return S_BAD_REQUEST;
} }
*q = '\0';
if (q - p + 1 > PATH_MAX) { 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); memcpy(req->uri, p, q - p);
decode(req->target, req->target); req->uri[q - p] = '\0';
decode(req->uri, req->uri);
/* basis for next step */ /* basis for next step */
p = q + 1; p = q + 1;
/* HTTP-VERSION */ /* HTTP-VERSION */
if (strncmp(p, "HTTP/", sizeof("HTTP/") - 1)) { if (strncmp(p, "HTTP/", sizeof("HTTP/") - 1)) {
return http_send_status(fd, S_BAD_REQUEST); return S_BAD_REQUEST;
} }
p += sizeof("HTTP/") - 1; p += sizeof("HTTP/") - 1;
if (strncmp(p, "1.0", sizeof("1.0") - 1) && if (strncmp(p, "1.0", sizeof("1.0") - 1) &&
strncmp(p, "1.1", sizeof("1.1") - 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; p += sizeof("1.*") - 1;
/* check terminator */ /* check terminator */
if (strncmp(p, "\r\n", sizeof("\r\n") - 1)) { 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 */ /* basis for next step */
@ -253,7 +273,7 @@ http_get_request(int fd, struct request *req)
if (i == NUM_REQ_FIELDS) { if (i == NUM_REQ_FIELDS) {
/* unmatched field, skip this line */ /* unmatched field, skip this line */
if (!(q = strstr(p, "\r\n"))) { if (!(q = strstr(p, "\r\n"))) {
return http_send_status(fd, S_BAD_REQUEST); return S_BAD_REQUEST;
} }
p = q + (sizeof("\r\n") - 1); p = q + (sizeof("\r\n") - 1);
continue; continue;
@ -263,7 +283,7 @@ http_get_request(int fd, struct request *req)
/* a single colon must follow the field name */ /* a single colon must follow the field name */
if (*p != ':') { if (*p != ':') {
return http_send_status(fd, S_BAD_REQUEST); return S_BAD_REQUEST;
} }
/* skip whitespace */ /* skip whitespace */
@ -272,13 +292,13 @@ http_get_request(int fd, struct request *req)
/* extract field content */ /* extract field content */
if (!(q = strstr(p, "\r\n"))) { 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) { 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 */ /* go to next line */
p = q + (sizeof("\r\n") - 1); p = q + (sizeof("\r\n") - 1);
@ -288,37 +308,37 @@ http_get_request(int fd, struct request *req)
* clean up host * clean up host
*/ */
p = strrchr(req->field[REQ_HOST], ':'); m = strrchr(req->field[REQ_HOST], ':');
q = strrchr(req->field[REQ_HOST], ']'); n = strrchr(req->field[REQ_HOST], ']');
/* strip port suffix but don't interfere with IPv6 bracket notation /* strip port suffix but don't interfere with IPv6 bracket notation
* as per RFC 2732 */ * as per RFC 2732 */
if (p && (!q || p > q)) { if (m && (!n || m > n)) {
/* port suffix must not be empty */ /* port suffix must not be empty */
if (*(p + 1) == '\0') { if (*(m + 1) == '\0') {
return http_send_status(fd, S_BAD_REQUEST); return S_BAD_REQUEST;
} }
*p = '\0'; *m = '\0';
} }
/* strip the brackets from the IPv6 notation and validate the address */ /* strip the brackets from the IPv6 notation and validate the address */
if (q) { if (n) {
/* brackets must be on the outside */ /* brackets must be on the outside */
if (req->field[REQ_HOST][0] != '[' || *(q + 1) != '\0') { if (req->field[REQ_HOST][0] != '[' || *(n + 1) != '\0') {
return http_send_status(fd, S_BAD_REQUEST); return S_BAD_REQUEST;
} }
/* remove the right bracket */ /* remove the right bracket */
*q = '\0'; *n = '\0';
p = req->field[REQ_HOST] + 1; m = req->field[REQ_HOST] + 1;
/* validate the contained IPv6 address */ /* validate the contained IPv6 address */
if (inet_pton(AF_INET6, p, &addr) != 1) { if (inet_pton(AF_INET6, m, &addr) != 1) {
return http_send_status(fd, S_BAD_REQUEST); return S_BAD_REQUEST;
} }
/* copy it into the host field */ /* 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; 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)) #define RELPATH(x) ((!*(x) || !strcmp(x, "/")) ? "." : ((x) + 1))
enum status 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; 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->uri, sizeof(realuri));
if (normabspath(realuri)) {
return S_BAD_REQUEST;
}
/* match vhost */ /* match vhost */
vhostmatch = NULL; vhost = NULL;
if (s.vhost) { if (srv->vhost) {
for (i = 0; i < s.vhost_len; i++) { for (i = 0; i < srv->vhost_len; i++) {
/* switch to vhost directory if there is a match */ if (!regexec(&(srv->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 = &(srv->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 == srv->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 URI prefix mapping */
for (i = 0; i < s.map_len; i++) { for (i = 0; i < srv->map_len; i++) {
len = strlen(s.map[i].from); len = strlen(srv->map[i].from);
if (!strncmp(realtarget, s.map[i].from, len)) { if (!strncmp(realuri, srv->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 (srv->vhost && srv->map[i].chost &&
strcmp(s.map[i].chost, vhostmatch)) { strcmp(srv->map[i].chost, vhost->chost)) {
continue; continue;
} }
/* swap out target prefix */ /* swap out URI 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), srv->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;
} }
/* reject hidden target */ /* stat the relative path derived from the URI */
if (realtarget[0] == '.' || strstr(realtarget, "/.")) { if (stat(RELPATH(realuri), &st) < 0) {
return http_send_status(fd, S_FORBIDDEN); return (errno == EACCES) ? S_FORBIDDEN : S_NOT_FOUND;
}
/* stat the target */
if (stat(RELPATH(realtarget), &st) < 0) {
return http_send_status(fd, (errno == EACCES) ?
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';
} }
} }
/* redirect if targets differ, host is non-canonical or we prefixed */ /*
if (strcmp(req->target, realtarget) || (s.vhost && vhostmatch && * reject hidden targets, except if it is a well-known URI
strcmp(req->field[REQ_HOST], vhostmatch))) { * according to RFC 8615
res.status = S_MOVED_PERMANENTLY; */
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 */ /* determine target location */
if (s.vhost) { if (srv->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 : "localhost"; srv->host ? srv->host : "localhost";
/* do we need to add a port to the Location? */ /* 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 /* RFC 2732 specifies to use brackets for IPv6-addresses
* in URLs, so we need to check if our host is one and * in URLs, so we need to check if our host is one and
* 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 ? srv->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->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)) { 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
req->target, s.docindex)) { * the URI
return http_send_status(fd, S_REQUEST_TOO_LARGE); */
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 */ /* 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 (srv->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);
}
} }
} }
} }
@ -702,39 +747,39 @@ http_send_response(int fd, const struct request *req)
/* 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;
@ -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;
} }

41
http.h
View file

@ -4,6 +4,8 @@
#include <limits.h> #include <limits.h>
#include "util.h"
#define HEADER_MAX 4096 #define HEADER_MAX 4096
#define FIELD_MAX 200 #define FIELD_MAX 200
@ -26,7 +28,7 @@ extern const char *req_method_str[];
struct request { struct request {
enum req_method method; enum req_method method;
char target[PATH_MAX]; char uri[PATH_MAX];
char field[NUM_REQ_FIELDS][FIELD_MAX]; char field[NUM_REQ_FIELDS][FIELD_MAX];
}; };
@ -61,14 +63,47 @@ enum res_field {
extern const char *res_field_str[]; extern const char *res_field_str[];
enum res_type {
RESTYPE_ERROR,
RESTYPE_FILE,
RESTYPE_DIRLISTING,
NUM_RES_TYPES,
};
struct response { struct response {
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];
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_header(int, const struct response *);
enum status http_send_status(int, enum status); enum status http_send_status(int, enum status);
int http_get_request(int, struct request *); enum status http_recv_header(int, char *, size_t, size_t *);
enum status http_send_response(int, const struct request *); 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 */ #endif /* HTTP_H */

82
main.c
View file

@ -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"
@ -23,22 +24,33 @@
static char *udsname; static char *udsname;
static void 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; time_t t;
enum status status; enum status status;
char inaddr[INET6_ADDRSTRLEN /* > INET_ADDRSTRLEN */]; char inaddr[INET6_ADDRSTRLEN /* > INET_ADDRSTRLEN */];
char tstmp[21]; char tstmp[21];
/* set connection timeout */ /* set connection timeout */
if (sock_set_timeout(infd, 30)) { if (sock_set_timeout(c.fd, 30)) {
goto cleanup; goto cleanup;
} }
/* handle request */ /* handle request */
if (!(status = http_get_request(infd, &req))) { if ((status = http_recv_header(c.fd, c.header, LEN(c.header), &c.off)) ||
status = http_send_response(infd, &req); (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 */ /* write output to log */
@ -52,12 +64,12 @@ serve(int infd, const struct sockaddr_storage *in_sa)
goto cleanup; goto cleanup;
} }
printf("%s\t%s\t%d\t%s\t%s\n", tstmp, inaddr, status, 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: cleanup:
/* clean up and finish */ /* clean up and finish */
shutdown(infd, SHUT_RD); shutdown(c.fd, SHUT_RD);
shutdown(infd, SHUT_WR); shutdown(c.fd, SHUT_WR);
close(infd); close(c.fd);
} }
static void static void
@ -177,6 +189,9 @@ main(int argc, char *argv[])
struct group *grp = NULL; struct group *grp = NULL;
struct passwd *pwd = NULL; struct passwd *pwd = NULL;
struct rlimit rlim; struct rlimit rlim;
struct server srv = {
.docindex = "index.html",
};
struct sockaddr_storage in_sa; struct sockaddr_storage in_sa;
size_t i; size_t i;
socklen_t in_sa_len; socklen_t in_sa_len;
@ -190,13 +205,6 @@ main(int argc, char *argv[])
char *user = "nobody"; char *user = "nobody";
char *group = "nogroup"; 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 { ARGBEGIN {
case 'd': case 'd':
servedir = EARGF(usage()); servedir = EARGF(usage());
@ -205,28 +213,28 @@ main(int argc, char *argv[])
group = EARGF(usage()); group = EARGF(usage());
break; break;
case 'h': case 'h':
s.host = EARGF(usage()); srv.host = EARGF(usage());
break; break;
case 'i': case 'i':
s.docindex = EARGF(usage()); srv.docindex = EARGF(usage());
if (strchr(s.docindex, '/')) { if (strchr(srv.docindex, '/')) {
die("The document index must not contain '/'"); die("The document index must not contain '/'");
} }
break; break;
case 'l': case 'l':
s.listdirs = 1; srv.listdirs = 1;
break; break;
case 'm': case 'm':
if (spacetok(EARGF(usage()), tok, 3) || !tok[0] || !tok[1]) { if (spacetok(EARGF(usage()), tok, 3) || !tok[0] || !tok[1]) {
usage(); usage();
} }
if (!(s.map = reallocarray(s.map, ++s.map_len, if (!(srv.map = reallocarray(srv.map, ++srv.map_len,
sizeof(struct map)))) { sizeof(struct map)))) {
die("reallocarray:"); die("reallocarray:");
} }
s.map[s.map_len - 1].from = tok[0]; srv.map[srv.map_len - 1].from = tok[0];
s.map[s.map_len - 1].to = tok[1]; srv.map[srv.map_len - 1].to = tok[1];
s.map[s.map_len - 1].chost = tok[2]; srv.map[srv.map_len - 1].chost = tok[2];
break; break;
case 'n': case 'n':
maxnprocs = strtonum(EARGF(usage()), 1, INT_MAX, &err); maxnprocs = strtonum(EARGF(usage()), 1, INT_MAX, &err);
@ -235,7 +243,7 @@ main(int argc, char *argv[])
} }
break; break;
case 'p': case 'p':
s.port = EARGF(usage()); srv.port = EARGF(usage());
break; break;
case 'U': case 'U':
udsname = EARGF(usage()); udsname = EARGF(usage());
@ -248,14 +256,14 @@ main(int argc, char *argv[])
!tok[2]) { !tok[2]) {
usage(); usage();
} }
if (!(s.vhost = reallocarray(s.vhost, ++s.vhost_len, if (!(srv.vhost = reallocarray(srv.vhost, ++srv.vhost_len,
sizeof(struct vhost)))) { sizeof(*srv.vhost)))) {
die("reallocarray:"); die("reallocarray:");
} }
s.vhost[s.vhost_len - 1].chost = tok[0]; srv.vhost[srv.vhost_len - 1].chost = tok[0];
s.vhost[s.vhost_len - 1].regex = tok[1]; srv.vhost[srv.vhost_len - 1].regex = tok[1];
s.vhost[s.vhost_len - 1].dir = tok[2]; srv.vhost[srv.vhost_len - 1].dir = tok[2];
s.vhost[s.vhost_len - 1].prefix = tok[3]; srv.vhost[srv.vhost_len - 1].prefix = tok[3];
break; break;
default: default:
usage(); 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*/ /* 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(); usage();
} }
@ -276,11 +284,11 @@ main(int argc, char *argv[])
} }
/* compile and check the supplied vhost regexes */ /* compile and check the supplied vhost regexes */
for (i = 0; i < s.vhost_len; i++) { for (i = 0; i < srv.vhost_len; i++) {
if (regcomp(&s.vhost[i].re, s.vhost[i].regex, if (regcomp(&srv.vhost[i].re, srv.vhost[i].regex,
REG_EXTENDED | REG_ICASE | REG_NOSUB)) { REG_EXTENDED | REG_ICASE | REG_NOSUB)) {
die("regcomp '%s': invalid regex", die("regcomp '%s': invalid regex",
s.vhost[i].regex); srv.vhost[i].regex);
} }
} }
@ -309,7 +317,7 @@ main(int argc, char *argv[])
/* bind socket */ /* bind socket */
insock = udsname ? sock_get_uds(udsname, pwd->pw_uid, grp->gr_gid) : 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()) { switch (fork()) {
case -1: case -1:
@ -372,7 +380,7 @@ main(int argc, char *argv[])
/* fork and handle */ /* fork and handle */
switch (fork()) { switch (fork()) {
case 0: case 0:
serve(infd, &in_sa); serve(infd, &in_sa, &srv);
exit(0); exit(0);
break; break;
case -1: case -1:

18
quark.1
View file

@ -1,4 +1,4 @@
.Dd 2019-02-24 .Dd 2020-08-23
.Dt QUARK 1 .Dt QUARK 1
.Os suckless.org .Os suckless.org
.Sh NAME .Sh NAME
@ -30,6 +30,15 @@
.Sh DESCRIPTION .Sh DESCRIPTION
.Nm .Nm
is a simple HTTP GET/HEAD-only web server for static content. 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 .Sh OPTIONS
.Bl -tag -width Ds .Bl -tag -width Ds
.It Fl d Ar dir .It Fl d Ar dir
@ -46,6 +55,7 @@ The default is "nogroup".
Use Use
.Ar host .Ar host
as the server hostname. as the server hostname.
The default is the loopback interface (i.e. localhost).
.It Fl i Ar file .It Fl i Ar file
Set Set
.Ar file .Ar file
@ -54,7 +64,7 @@ The default is "index.html".
.It Fl l .It Fl l
Enable directory listing. Enable directory listing.
.It Fl m Ar map .It Fl m Ar map
Add the target prefix mapping rule specified by Add the URI prefix mapping rule specified by
.Ar map , .Ar map ,
which has the form which has the form
.Qq Pa from to [chost] , .Qq Pa from to [chost] ,
@ -63,7 +73,7 @@ escaped with '\\'.
.Pp .Pp
The prefix The prefix
.Pa from .Pa from
of all matching targets is replaced with of all matching URIs is replaced with
.Pa to , .Pa to ,
optionally limited to the canonical virtual host optionally limited to the canonical virtual host
.Pa chost . .Pa chost .
@ -108,7 +118,7 @@ is redirected to the canonical host
.Pa chost , .Pa chost ,
if they differ, using the directory if they differ, using the directory
.Pa dir .Pa dir
as the root directory, optionally prefixing the target with as the root directory, optionally prefixing the URI with
.Pa prefix . .Pa prefix .
If any virtual hosts are specified, all requests on non-matching If any virtual hosts are specified, all requests on non-matching
hosts are discarded. hosts are discarded.

142
resp.c
View file

@ -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[] = {
{ '&', "&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 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
View file

@ -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 */

11
sock.c
View file

@ -63,7 +63,7 @@ void
sock_rem_uds(const char *udsname) sock_rem_uds(const char *udsname)
{ {
if (unlink(udsname) < 0) { 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, .sun_family = AF_UNIX,
}; };
size_t udsnamelen; 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) { if ((insock = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
die("socket:"); 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); memcpy(addr.sun_path, udsname, udsnamelen + 1);
if (bind(insock, (const struct sockaddr *)&addr, sizeof(addr)) < 0) { if (bind(insock, (const struct sockaddr *)&addr, sizeof(addr)) < 0) {
die("bind %s:", udsname); die("bind '%s':", udsname);
} }
if (listen(insock, SOMAXCONN) < 0) { 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) { if (chmod(udsname, sockmode) < 0) {
sock_rem_uds(udsname); sock_rem_uds(udsname);
die("chmod:"); die("chmod '%s':", udsname);
} }
if (chown(udsname, uid, gid) < 0) { if (chown(udsname, uid, gid) < 0) {
sock_rem_uds(udsname); sock_rem_uds(udsname);
die("chown:"); die("chown '%s':", udsname);
} }
return insock; return insock;

61
util.c
View file

@ -16,7 +16,6 @@
#include "util.h" #include "util.h"
char *argv0; char *argv0;
struct server s;
static void static void
verr(const char *fmt, va_list ap) 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); 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 INVALID 1
#define TOOSMALL 2 #define TOOSMALL 2
#define TOOLARGE 3 #define TOOLARGE 3

6
util.h
View file

@ -23,7 +23,7 @@ struct map {
char *to; char *to;
}; };
extern struct server { struct server {
char *host; char *host;
char *port; char *port;
char *docindex; char *docindex;
@ -32,7 +32,7 @@ extern struct server {
size_t vhost_len; size_t vhost_len;
struct map *map; struct map *map;
size_t map_len; size_t map_len;
} s; };
#undef MIN #undef MIN
#define MIN(x,y) ((x) < (y) ? (x) : (y)) #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 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 **);