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 **);