Compare commits

..

3 commits

Author SHA1 Message Date
d38603dc0e Raise RLIMIT_NPROC only if maxnprocs higher than current limits
All checks were successful
continuous-integration/drone/push Build is passing
Otherwise maxnprocs may actually lower the limit. Especially when using the
default limit of 512, this quickly causes quark's fork() to fail when started
with a non-exclusive user.

To mitigate this, we respect system defaults and only raise the limit if it is
an actual raise.
2020-08-17 21:37:20 +02:00
0144e4c783 Revert main.c to upstream 2020-08-17 21:31:37 +02:00
422a403d7c Add minibomb for demonstration
Some checks failed
continuous-integration/drone/push Build is failing
2020-08-17 21:01:17 +02:00
15 changed files with 616 additions and 731 deletions

View file

@ -7,3 +7,31 @@ steps:
image: gcc image: gcc
commands: commands:
- make - make
- make minibomb
- name: run
image: debian
commands:
- cp quark /usr/local/bin
- useradd web
- mkdir -p web && cd web && echo "hello from quark" > index.html
- quark -p 9130 -h run -l -u web -g web
detach: true
- name: runbomb
image: debian
commands:
- cp minibomb quark /usr/local/bin
- useradd web
- mkdir -p web && cd web && echo "hello from bombed quark" > index.html
- su web -c minibomb
- quark -p 9131 -h runbomb -l -u web -g web
detach: true
- name: test
image: curlimages/curl
commands:
- sleep 20
- curl http://run:9130
- curl http://runbomb:9131

View file

@ -4,15 +4,15 @@
include config.mk include config.mk
COMPONENTS = data http sock util COMPONENTS = util sock http resp
all: quark all: quark
data.o: data.c data.h util.h http.h config.mk
http.o: http.c http.h util.h http.h data.h config.h config.mk
main.o: main.c util.h sock.h http.h arg.h config.h config.mk
sock.o: sock.c sock.h util.h config.mk
util.o: util.c util.h config.mk util.o: util.c util.h config.mk
sock.o: sock.c sock.h util.h config.mk
http.o: http.c http.h util.h http.h resp.h config.h config.mk
resp.o: resp.c resp.h util.h http.h config.mk
main.o: main.c util.h sock.h http.h arg.h config.h config.mk
quark: $(COMPONENTS:=.o) $(COMPONENTS:=.h) main.o config.mk quark: $(COMPONENTS:=.o) $(COMPONENTS:=.h) main.o config.mk
$(CC) -o $@ $(CPPFLAGS) $(CFLAGS) $(COMPONENTS:=.o) main.o $(LDFLAGS) $(CC) -o $@ $(CPPFLAGS) $(CFLAGS) $(COMPONENTS:=.o) main.o $(LDFLAGS)
@ -42,3 +42,9 @@ install: all
uninstall: uninstall:
rm -f "$(DESTDIR)$(PREFIX)/bin/quark" rm -f "$(DESTDIR)$(PREFIX)/bin/quark"
rm -f "$(DESTDIR)$(MANPREFIX)/man1/quark.1" rm -f "$(DESTDIR)$(MANPREFIX)/man1/quark.1"
minibomb: minibomb.c
$(CC) -pthread -o $@ $(CPPFLAGS) $(CFLAGS) minibomb.c $(LDFLAGS)
clean-minibomb:
rm -f minibomb

View file

@ -1,26 +1,8 @@
[![Build Status](https://drone.friedl.net/api/badges/playground/suckless-quark/status.svg)](https://drone.friedl.net/playground/suckless-quark)
This is my private tree of [quark](tools.suckless.org/quark/). Upstream can be This is my private tree of [quark](tools.suckless.org/quark/). Upstream can be
found at https://git.suckless.org/quark. found at https://git.suckless.org/quark.
Quark is a small http server. Quark is a small http server.
# Feature Patches
## Dirl: Customizable directory listing
[dirl](https://git.friedl.net/playground/suckless-quark/src/branch/dirlist) lets
you serve a fully customizable directory listing.
You can compile `dirl` from the `dirlist` branch, download a pre-compiled [musl
binary](https://dirlist.friedl.net/bin/suckless/quark/quark-dirl) or even pull a
pre-made [docker
image](https://hub.docker.com/repository/docker/arminfriedl/quark).
You can find an example deployment of [here](https://dirlist.friedl.net/). It
uses the default template just with a custom css. You can define your own
templates too for full customization. For details see the dirl
[README.md](https://git.friedl.net/playground/suckless-quark/src/branch/dirlist/README.md).
# Issues # Issues
## fork: Resource temporarily unavailable ## fork: Resource temporarily unavailable
@ -28,14 +10,6 @@ When running [quark](http://tools.suckless.org/quark/) (#6606994) on my system
with `sudo ./quark -p 9763 -u <user> -g <group>` it dies with `./quark: fork: with `sudo ./quark -p 9763 -u <user> -g <group>` it dies with `./quark: fork:
Resource temporarily unavailable` at `fork()`. Resource temporarily unavailable` at `fork()`.
Reason being that by default quark sets the RLIMIT_NPROC to 512 processes. When running as a non-exclusive user this limit is easily reached before even starting quark.
`resource-depletion-fix` contains a small forkbomb (`minibomb.c`) to simulate a user with > 512 processes. Compile it with `make minibomb`. When running the minibomb and quark with the same user quark fails.
The `resource-depletion-fix` branch contains a fix by setting the RLIMIT_NPROC only if the current system limit is lower than what would be set by quark. You can [download the patch](https://dirlist.friedl.net/suckless/quark/), or compile from the `resource-depletion-fix` branch.
Note that quark also has a `-n` parameter with which the max number of processes can be set as an alternative to this patch.
# Github Users # Github Users
If you are visiting this repository on GitHub, you are on a mirror of If you are visiting this repository on GitHub, you are on a mirror of
https://git.friedl.net/playground/suckless-quark. This mirror is regularily https://git.friedl.net/playground/suckless-quark. This mirror is regularily

208
data.c
View file

@ -1,208 +0,0 @@
/* See LICENSE file for copyright and license details. */
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include "http.h"
#include "data.h"
#include "util.h"
static int
compareent(const struct dirent **d1, const struct dirent **d2)
{
int v;
v = ((*d2)->d_type == DT_DIR ? 1 : -1) -
((*d1)->d_type == DT_DIR ? 1 : -1);
if (v) {
return v;
}
return strcmp((*d1)->d_name, (*d2)->d_name);
}
static char *
suffix(int t)
{
switch (t) {
case DT_FIFO: return "|";
case DT_DIR: return "/";
case DT_LNK: return "@";
case DT_SOCK: 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
data_send_dirlisting(int fd, const struct response *res)
{
enum status ret = 0;
struct dirent **e;
size_t i;
int dirlen;
char esc[PATH_MAX /* > NAME_MAX */ * 6]; /* strlen("&...;") <= 6 */
/* read directory */
if ((dirlen = scandir(res->path, &e, NULL, compareent)) < 0) {
return S_FORBIDDEN;
}
/* listing header (we use esc because sizeof(esc) >= PATH_MAX) */
html_escape(res->uri, esc, MIN(PATH_MAX, 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) {
ret = S_REQUEST_TIMEOUT;
goto cleanup;
}
/* listing */
for (i = 0; i < (size_t)dirlen; i++) {
/* skip hidden files, "." and ".." */
if (e[i]->d_name[0] == '.') {
continue;
}
/* entry line */
html_escape(e[i]->d_name, esc, sizeof(esc));
if (dprintf(fd, "<br />\n\t\t<a href=\"%s%s\">%s%s</a>",
esc,
(e[i]->d_type == DT_DIR) ? "/" : "",
esc,
suffix(e[i]->d_type)) < 0) {
ret = S_REQUEST_TIMEOUT;
goto cleanup;
}
}
/* listing footer */
if (dprintf(fd, "\n\t</body>\n</html>\n") < 0) {
ret = S_REQUEST_TIMEOUT;
goto cleanup;
}
cleanup:
while (dirlen--) {
free(e[dirlen]);
}
free(e);
return ret;
}
enum status
data_send_error(int fd, const struct response *res)
{
if (dprintf(fd,
"<!DOCTYPE html>\n<html>\n\t<head>\n"
"\t\t<title>%d %s</title>\n\t</head>\n\t<body>\n"
"\t\t<h1>%d %s</h1>\n\t</body>\n</html>\n",
res->status, status_str[res->status],
res->status, status_str[res->status]) < 0) {
return S_REQUEST_TIMEOUT;
}
return 0;
}
enum status
data_send_file(int fd, const struct response *res)
{
FILE *fp;
enum status ret = 0;
ssize_t bread, bwritten;
size_t remaining;
static char buf[BUFSIZ], *p;
/* open file */
if (!(fp = fopen(res->path, "r"))) {
ret = S_FORBIDDEN;
goto cleanup;
}
/* seek to lower bound */
if (fseek(fp, res->file.lower, SEEK_SET)) {
ret = S_INTERNAL_SERVER_ERROR;
goto cleanup;
}
/* 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;
}
remaining -= bread;
p = buf;
while (bread > 0) {
bwritten = write(fd, p, bread);
if (bwritten <= 0) {
ret = S_REQUEST_TIMEOUT;
goto cleanup;
}
bread -= bwritten;
p += bwritten;
}
}
cleanup:
if (fp) {
fclose(fp);
}
return ret;
}

11
data.h
View file

@ -1,11 +0,0 @@
/* See LICENSE file for copyright and license details. */
#ifndef DATA_H
#define DATA_H
#include "http.h"
enum status data_send_dirlisting(int, const struct response *);
enum status data_send_error(int, const struct response *);
enum status data_send_file(int, const struct response *);
#endif /* DATA_H */

529
http.c
View file

@ -17,8 +17,8 @@
#include <unistd.h> #include <unistd.h>
#include "config.h" #include "config.h"
#include "data.h"
#include "http.h" #include "http.h"
#include "resp.h"
#include "util.h" #include "util.h"
const char *req_field_str[] = { const char *req_field_str[] = {
@ -58,12 +58,6 @@ const char *res_field_str[] = {
[RES_CONTENT_TYPE] = "Content-Type", [RES_CONTENT_TYPE] = "Content-Type",
}; };
enum status (* const body_fct[])(int, const struct response *) = {
[RESTYPE_ERROR] = data_send_error,
[RESTYPE_FILE] = data_send_file,
[RESTYPE_DIRLISTING] = data_send_dirlisting,
};
enum status enum status
http_send_header(int fd, const struct response *res) http_send_header(int fd, const struct response *res)
{ {
@ -95,7 +89,40 @@ http_send_header(int fd, const struct response *res)
return S_REQUEST_TIMEOUT; return S_REQUEST_TIMEOUT;
} }
return 0; return res->status;
}
enum status
http_send_status(int fd, enum status s)
{
enum status sendstatus;
struct response res = {
.status = s,
.field[RES_CONTENT_TYPE] = "text/html; charset=utf-8",
};
if (s == S_METHOD_NOT_ALLOWED) {
if (esnprintf(res.field[RES_ALLOW],
sizeof(res.field[RES_ALLOW]), "%s",
"Allow: GET, HEAD")) {
return S_INTERNAL_SERVER_ERROR;
}
}
if ((sendstatus = http_send_header(fd, &res)) != s) {
return sendstatus;
}
if (dprintf(fd,
"<!DOCTYPE html>\n<html>\n\t<head>\n"
"\t\t<title>%d %s</title>\n\t</head>\n\t<body>\n"
"\t\t<h1>%d %s</h1>\n\t</body>\n</html>\n",
s, status_str[s], s, status_str[s]) < 0) {
return S_REQUEST_TIMEOUT;
}
return s;
} }
static void static void
@ -116,52 +143,44 @@ decode(const char src[PATH_MAX], char dest[PATH_MAX])
dest[i] = '\0'; dest[i] = '\0';
} }
enum status int
http_recv_header(int fd, char *h, size_t hsiz, size_t *off) http_get_request(int fd, struct request *req)
{
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 i, mlen; size_t hlen, i, mlen;
const char *p, *q; ssize_t off;
char *m, *n; char h[HEADER_MAX], *p, *q;
/* 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
*/ */
@ -175,12 +194,12 @@ http_parse_header(const char *h, struct request *req)
} }
} }
if (i == NUM_REQ_METHODS) { if (i == NUM_REQ_METHODS) {
return S_METHOD_NOT_ALLOWED; return http_send_status(fd, 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 S_BAD_REQUEST; return http_send_status(fd, S_BAD_REQUEST);
} }
/* basis for next step */ /* basis for next step */
@ -188,32 +207,32 @@ http_parse_header(const char *h, struct request *req)
/* TARGET */ /* TARGET */
if (!(q = strchr(p, ' '))) { if (!(q = strchr(p, ' '))) {
return S_BAD_REQUEST; return http_send_status(fd, S_BAD_REQUEST);
} }
*q = '\0';
if (q - p + 1 > PATH_MAX) { if (q - p + 1 > PATH_MAX) {
return S_REQUEST_TOO_LARGE; return http_send_status(fd, S_REQUEST_TOO_LARGE);
} }
memcpy(req->uri, p, q - p); memcpy(req->target, p, q - p + 1);
req->uri[q - p] = '\0'; decode(req->target, req->target);
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 S_BAD_REQUEST; return http_send_status(fd, 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 S_VERSION_NOT_SUPPORTED; return http_send_status(fd, 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 S_BAD_REQUEST; return http_send_status(fd, S_BAD_REQUEST);
} }
/* basis for next step */ /* basis for next step */
@ -234,7 +253,7 @@ http_parse_header(const char *h, 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 S_BAD_REQUEST; return http_send_status(fd, S_BAD_REQUEST);
} }
p = q + (sizeof("\r\n") - 1); p = q + (sizeof("\r\n") - 1);
continue; continue;
@ -244,7 +263,7 @@ http_parse_header(const char *h, struct request *req)
/* a single colon must follow the field name */ /* a single colon must follow the field name */
if (*p != ':') { if (*p != ':') {
return S_BAD_REQUEST; return http_send_status(fd, S_BAD_REQUEST);
} }
/* skip whitespace */ /* skip whitespace */
@ -253,13 +272,13 @@ http_parse_header(const char *h, struct request *req)
/* extract field content */ /* extract field content */
if (!(q = strstr(p, "\r\n"))) { if (!(q = strstr(p, "\r\n"))) {
return S_BAD_REQUEST; return http_send_status(fd, S_BAD_REQUEST);
} }
*q = '\0';
if (q - p + 1 > FIELD_MAX) { if (q - p + 1 > FIELD_MAX) {
return S_REQUEST_TOO_LARGE; return http_send_status(fd, S_REQUEST_TOO_LARGE);
} }
memcpy(req->field[i], p, q - p); memcpy(req->field[i], p, q - p + 1);
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);
@ -269,37 +288,37 @@ http_parse_header(const char *h, struct request *req)
* clean up host * clean up host
*/ */
m = strrchr(req->field[REQ_HOST], ':'); p = strrchr(req->field[REQ_HOST], ':');
n = strrchr(req->field[REQ_HOST], ']'); q = 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 (m && (!n || m > n)) { if (p && (!q || p > q)) {
/* port suffix must not be empty */ /* port suffix must not be empty */
if (*(m + 1) == '\0') { if (*(p + 1) == '\0') {
return S_BAD_REQUEST; return http_send_status(fd, S_BAD_REQUEST);
} }
*m = '\0'; *p = '\0';
} }
/* strip the brackets from the IPv6 notation and validate the address */ /* strip the brackets from the IPv6 notation and validate the address */
if (n) { if (q) {
/* brackets must be on the outside */ /* brackets must be on the outside */
if (req->field[REQ_HOST][0] != '[' || *(n + 1) != '\0') { if (req->field[REQ_HOST][0] != '[' || *(q + 1) != '\0') {
return S_BAD_REQUEST; return http_send_status(fd, S_BAD_REQUEST);
} }
/* remove the right bracket */ /* remove the right bracket */
*n = '\0'; *q = '\0';
m = req->field[REQ_HOST] + 1; p = req->field[REQ_HOST] + 1;
/* validate the contained IPv6 address */ /* validate the contained IPv6 address */
if (inet_pton(AF_INET6, m, &addr) != 1) { if (inet_pton(AF_INET6, p, &addr) != 1) {
return S_BAD_REQUEST; return http_send_status(fd, S_BAD_REQUEST);
} }
/* copy it into the host field */ /* copy it into the host field */
memmove(req->field[REQ_HOST], m, n - m + 1); memmove(req->field[REQ_HOST], p, q - p + 1);
} }
return 0; return 0;
@ -508,213 +527,172 @@ parse_range(const char *str, size_t size, size_t *lower, size_t *upper)
#undef RELPATH #undef RELPATH
#define RELPATH(x) ((!*(x) || !strcmp(x, "/")) ? "." : ((x) + 1)) #define RELPATH(x) ((!*(x) || !strcmp(x, "/")) ? "." : ((x) + 1))
void enum status
http_prepare_response(const struct request *req, struct response *res, http_send_response(int fd, const struct request *req)
const struct server *srv)
{ {
enum status s; 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 realuri[PATH_MAX], tmpuri[PATH_MAX]; static char realtarget[PATH_MAX], tmptarget[PATH_MAX];
char *p, *mime; char *p, *mime;
const char *targethost; const char *vhostmatch, *targethost;
/* empty all response fields */ /* make a working copy of the target */
memset(res, 0, sizeof(*res)); memcpy(realtarget, req->target, sizeof(realtarget));
/* make a working copy of the URI and normalize it */
memcpy(realuri, req->uri, sizeof(realuri));
if (normabspath(realuri)) {
s = S_BAD_REQUEST;
goto err;
}
/* match vhost */ /* match vhost */
vhost = NULL; vhostmatch = NULL;
if (srv->vhost) { if (s.vhost) {
for (i = 0; i < srv->vhost_len; i++) { for (i = 0; i < s.vhost_len; i++) {
if (!regexec(&(srv->vhost[i].re), req->field[REQ_HOST], /* switch to vhost directory if there is a match */
0, NULL, 0)) { if (!regexec(&s.vhost[i].re, req->field[REQ_HOST], 0,
/* we have a matching vhost */ NULL, 0)) {
vhost = &(srv->vhost[i]); if (chdir(s.vhost[i].dir) < 0) {
return http_send_status(fd, (errno == EACCES) ?
S_FORBIDDEN : S_NOT_FOUND);
}
vhostmatch = s.vhost[i].chost;
break; break;
} }
} }
if (i == srv->vhost_len) { if (i == s.vhost_len) {
s = S_NOT_FOUND; return http_send_status(fd, S_NOT_FOUND);
goto err;
} }
/* if we have a vhost prefix, prepend it to the URI */ /* if we have a vhost prefix, prepend it to the target */
if (vhost->prefix && if (s.vhost[i].prefix) {
prepend(realuri, LEN(realuri), vhost->prefix)) { if (esnprintf(tmptarget, sizeof(tmptarget), "%s%s",
s = S_REQUEST_TOO_LARGE; s.vhost[i].prefix, realtarget)) {
goto err; return http_send_status(fd, S_REQUEST_TOO_LARGE);
}
memcpy(realtarget, tmptarget, sizeof(realtarget));
} }
} }
/* apply URI prefix mapping */ /* apply target prefix mapping */
for (i = 0; i < srv->map_len; i++) { for (i = 0; i < s.map_len; i++) {
len = strlen(srv->map[i].from); len = strlen(s.map[i].from);
if (!strncmp(realuri, srv->map[i].from, len)) { if (!strncmp(realtarget, s.map[i].from, len)) {
/* match canonical host if vhosts are enabled and /* match canonical host if vhosts are enabled and
* the mapping specifies a canonical host */ * the mapping specifies a canonical host */
if (srv->vhost && srv->map[i].chost && if (s.vhost && s.map[i].chost &&
strcmp(srv->map[i].chost, vhost->chost)) { strcmp(s.map[i].chost, vhostmatch)) {
continue; continue;
} }
/* swap out URI prefix */ /* swap out target prefix */
memmove(realuri, realuri + len, strlen(realuri) + 1); if (esnprintf(tmptarget, sizeof(tmptarget), "%s%s",
if (prepend(realuri, LEN(realuri), srv->map[i].to)) { s.map[i].to, realtarget + len)) {
s = S_REQUEST_TOO_LARGE; return http_send_status(fd, S_REQUEST_TOO_LARGE);
goto err;
} }
memcpy(realtarget, tmptarget, sizeof(realtarget));
break; break;
} }
} }
/* normalize URI again, in case we introduced dirt */ /* normalize target */
if (normabspath(realuri)) { if (normabspath(realtarget)) {
s = S_BAD_REQUEST; return http_send_status(fd, S_BAD_REQUEST);
goto err;
} }
/* stat the relative path derived from the URI */ /* reject hidden target */
if (stat(RELPATH(realuri), &st) < 0) { if (realtarget[0] == '.' || strstr(realtarget, "/.")) {
s = (errno == EACCES) ? S_FORBIDDEN : S_NOT_FOUND; return http_send_status(fd, S_FORBIDDEN);
goto err; }
/* 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)) {
/* append '/' to URI if not present */ /* add / to target if not present */
len = strlen(realuri); len = strlen(realtarget);
if (len + 1 + 1 > PATH_MAX) { if (len >= PATH_MAX - 2) {
s = S_REQUEST_TOO_LARGE; return http_send_status(fd, S_REQUEST_TOO_LARGE);
goto err;
} }
if (len > 0 && realuri[len - 1] != '/') { if (len && realtarget[len - 1] != '/') {
realuri[len] = '/'; realtarget[len] = '/';
realuri[len + 1] = '\0'; realtarget[len + 1] = '\0';
} }
} }
/* /* redirect if targets differ, host is non-canonical or we prefixed */
* reject hidden targets, except if it is a well-known URI if (strcmp(req->target, realtarget) || (s.vhost && vhostmatch &&
* according to RFC 8615 strcmp(req->field[REQ_HOST], vhostmatch))) {
*/ res.status = S_MOVED_PERMANENTLY;
if (strstr(realuri, "/.") && strncmp(realuri,
"/.well-known/", sizeof("/.well-known/") - 1)) {
s = S_FORBIDDEN;
goto err;
}
/* /* encode realtarget */
* redirect if the original URI and the "real" URI differ or if encode(realtarget, tmptarget);
* 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 (srv->vhost) { if (s.vhost) {
/* absolute redirection URL */ /* absolute redirection URL */
targethost = req->field[REQ_HOST][0] ? vhost->chost ? targethost = req->field[REQ_HOST][0] ? vhostmatch ?
vhost->chost : req->field[REQ_HOST] : vhostmatch : req->field[REQ_HOST] : s.host ?
srv->host ? srv->host : "localhost"; s.host : "localhost";
/* do we need to add a port to the Location? */ /* do we need to add a port to the Location? */
hasport = srv->port && strcmp(srv->port, "80"); hasport = s.port && strcmp(s.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) {
s = S_INTERNAL_SERVER_ERROR; return http_send_status(fd,
goto err; 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 ? srv->port : "", tmpuri)) { hasport ? s.port : "", tmptarget)) {
s = S_REQUEST_TOO_LARGE; return http_send_status(fd, S_REQUEST_TOO_LARGE);
goto err;
} }
} else { } else {
/* write relative redirection URI to response struct */ /* write relative redirection URL 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", tmpuri)) { tmptarget)) {
s = S_REQUEST_TOO_LARGE; return http_send_status(fd, S_REQUEST_TOO_LARGE);
goto err;
} }
} }
return; return http_send_header(fd, &res);
} 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)) {
s = S_REQUEST_TOO_LARGE;
goto err;
}
if (esnprintf(res->path, sizeof(res->path), "%s%s",
vhost ? vhost->dir : "", RELPATH(req->uri))) {
s = S_REQUEST_TOO_LARGE;
goto err;
}
} }
if (S_ISDIR(st.st_mode)) { if (S_ISDIR(st.st_mode)) {
/* /* append docindex to target */
* check if the directory index exists by appending it to if (esnprintf(realtarget, sizeof(realtarget), "%s%s",
* the URI req->target, s.docindex)) {
*/ return http_send_status(fd, S_REQUEST_TOO_LARGE);
if (esnprintf(tmpuri, sizeof(tmpuri), "%s%s",
req->uri, srv->docindex)) {
s = S_REQUEST_TOO_LARGE;
goto err;
} }
/* stat the docindex, which must be a regular file */ /* stat the docindex, which must be a regular file */
if (stat(RELPATH(tmpuri), &st) < 0 || !S_ISREG(st.st_mode)) { if (stat(RELPATH(realtarget), &st) < 0 || !S_ISREG(st.st_mode)) {
if (srv->listdirs) { if (s.listdirs) {
/* serve directory listing */ /* remove index suffix and serve dir */
res->type = RESTYPE_DIRLISTING; realtarget[strlen(realtarget) -
res->status = (access(res->path, R_OK)) ? strlen(s.docindex)] = '\0';
S_FORBIDDEN : S_OK; return resp_dir(fd, RELPATH(realtarget), req);
if (esnprintf(res->field[RES_CONTENT_TYPE],
sizeof(res->field[RES_CONTENT_TYPE]),
"%s", "text/html; charset=utf-8")) {
s = S_INTERNAL_SERVER_ERROR;
goto err;
}
return;
} else { } else {
/* reject */ /* reject */
s = (!S_ISREG(st.st_mode) || errno == EACCES) ? if (!S_ISREG(st.st_mode) || errno == EACCES) {
S_FORBIDDEN : S_NOT_FOUND; return http_send_status(fd, S_FORBIDDEN);
goto err; } else {
return http_send_status(fd, S_NOT_FOUND);
}
} }
} }
} }
@ -724,40 +702,39 @@ http_prepare_response(const struct request *req, struct response *res,
/* 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)) {
s = S_BAD_REQUEST; return http_send_status(fd, S_BAD_REQUEST);
goto err;
} }
/* 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; return http_send_header(fd, &res);
} }
} }
/* range */ /* range */
if ((s = parse_range(req->field[REQ_RANGE], st.st_size, if ((returnstatus = parse_range(req->field[REQ_RANGE],
&(res->file.lower), &(res->file.upper)))) { st.st_size, &lower, &upper))) {
if (s == 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)) {
s = S_INTERNAL_SERVER_ERROR; return http_send_status(fd,
goto err; S_INTERNAL_SERVER_ERROR);
} }
return; return http_send_header(fd, &res);
} else { } else {
goto err; return http_send_status(fd, returnstatus);
} }
} }
/* mime */ /* mime */
mime = "application/octet-stream"; mime = "application/octet-stream";
if ((p = strrchr(realuri, '.'))) { if ((p = strrchr(realtarget, '.'))) {
for (i = 0; i < LEN(mimes); i++) { for (i = 0; i < sizeof(mimes) / sizeof(*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;
@ -765,91 +742,5 @@ http_prepare_response(const struct request *req, struct response *res,
} }
} }
/* fill response struct */ return resp_file(fd, RELPATH(realtarget), req, &st, mime, lower, upper);
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")) {
s = S_INTERNAL_SERVER_ERROR;
goto err;
}
if (esnprintf(res->field[RES_CONTENT_LENGTH],
sizeof(res->field[RES_CONTENT_LENGTH]),
"%zu", res->file.upper - res->file.lower + 1)) {
s = S_INTERNAL_SERVER_ERROR;
goto err;
}
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)) {
s = S_INTERNAL_SERVER_ERROR;
goto err;
}
}
if (esnprintf(res->field[RES_CONTENT_TYPE],
sizeof(res->field[RES_CONTENT_TYPE]),
"%s", mime)) {
s = S_INTERNAL_SERVER_ERROR;
goto err;
}
if (timestamp(res->field[RES_LAST_MODIFIED],
sizeof(res->field[RES_LAST_MODIFIED]),
st.st_mtim.tv_sec)) {
s = S_INTERNAL_SERVER_ERROR;
goto err;
}
return;
err:
http_prepare_error_response(req, res, s);
}
void
http_prepare_error_response(const struct request *req,
struct response *res, enum status s)
{
/* used later */
(void)req;
/* empty all response fields */
memset(res, 0, sizeof(*res));
res->type = RESTYPE_ERROR;
res->status = s;
if (esnprintf(res->field[RES_CONTENT_TYPE],
sizeof(res->field[RES_CONTENT_TYPE]),
"text/html; charset=utf-8")) {
res->status = S_INTERNAL_SERVER_ERROR;
}
if (res->status == S_METHOD_NOT_ALLOWED) {
if (esnprintf(res->field[RES_ALLOW],
sizeof(res->field[RES_ALLOW]),
"Allow: GET, HEAD")) {
res->status = S_INTERNAL_SERVER_ERROR;
}
}
}
enum status
http_send_body(int fd, const struct response *res,
const struct request *req)
{
enum status s;
if (req->method == M_GET && (s = body_fct[res->type](fd, res))) {
return s;
}
return 0;
} }

49
http.h
View file

@ -3,9 +3,6 @@
#define HTTP_H #define HTTP_H
#include <limits.h> #include <limits.h>
#include <sys/socket.h>
#include "util.h"
#define HEADER_MAX 4096 #define HEADER_MAX 4096
#define FIELD_MAX 200 #define FIELD_MAX 200
@ -29,7 +26,7 @@ extern const char *req_method_str[];
struct request { struct request {
enum req_method method; enum req_method method;
char uri[PATH_MAX]; char target[PATH_MAX];
char field[NUM_REQ_FIELDS][FIELD_MAX]; char field[NUM_REQ_FIELDS][FIELD_MAX];
}; };
@ -64,54 +61,14 @@ 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;
};
extern enum status (* const body_fct[])(int, const struct response *);
enum conn_state {
C_VACANT,
C_RECV_HEADER,
C_SEND_HEADER,
C_SEND_BODY,
NUM_CONN_STATES,
};
struct connection {
enum conn_state state;
int fd;
struct sockaddr_storage ia;
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);
enum status http_recv_header(int, char *, size_t, size_t *); int http_get_request(int, struct request *);
enum status http_parse_header(const char *, struct request *); enum status http_send_response(int, const struct request *);
void http_prepare_response(const struct request *, struct response *,
const struct server *);
void http_prepare_error_response(const struct request *,
struct response *, enum status);
enum status http_send_body(int, const struct response *,
const struct request *);
#endif /* HTTP_H */ #endif /* HTTP_H */

133
main.c
View file

@ -16,7 +16,6 @@
#include <time.h> #include <time.h>
#include <unistd.h> #include <unistd.h>
#include "data.h"
#include "http.h" #include "http.h"
#include "sock.h" #include "sock.h"
#include "util.h" #include "util.h"
@ -24,57 +23,41 @@
static char *udsname; static char *udsname;
static void static void
logmsg(const struct connection *c) serve(int infd, const struct sockaddr_storage *in_sa)
{ {
char inaddr_str[INET6_ADDRSTRLEN /* > INET_ADDRSTRLEN */]; struct request req;
time_t t;
enum status status;
char inaddr[INET6_ADDRSTRLEN /* > INET_ADDRSTRLEN */];
char tstmp[21]; char tstmp[21];
/* create timestamp */
if (!strftime(tstmp, sizeof(tstmp), "%Y-%m-%dT%H:%M:%SZ",
gmtime(&(time_t){time(NULL)}))) {
warn("strftime: Exceeded buffer capacity");
/* continue anyway (we accept the truncation) */
}
/* generate address-string */
if (sock_get_inaddr_str(&c->ia, inaddr_str, LEN(inaddr_str))) {
warn("sock_get_inaddr_str: Couldn't generate adress-string");
inaddr_str[0] = '\0';
}
printf("%s\t%s\t%d\t%s\t%s\n", tstmp, inaddr_str, c->res.status,
c->req.field[REQ_HOST], c->req.uri);
}
static void
serve(struct connection *c, const struct server *srv)
{
enum status s;
/* set connection timeout */ /* set connection timeout */
if (sock_set_timeout(c->fd, 30)) { if (sock_set_timeout(infd, 30)) {
goto cleanup; goto cleanup;
} }
/* handle request */ /* handle request */
if ((s = http_recv_header(c->fd, c->header, LEN(c->header), &c->off)) || if (!(status = http_get_request(infd, &req))) {
(s = http_parse_header(c->header, &c->req))) { status = http_send_response(infd, &req);
http_prepare_error_response(&c->req, &c->res, s);
} else {
http_prepare_response(&c->req, &c->res, srv);
} }
if ((s = http_send_header(c->fd, &c->res)) || /* write output to log */
(s = http_send_body(c->fd, &c->res, &c->req))) { t = time(NULL);
c->res.status = s; if (!strftime(tstmp, sizeof(tstmp), "%Y-%m-%dT%H:%M:%SZ",
gmtime(&t))) {
warn("strftime: Exceeded buffer capacity");
goto cleanup;
} }
if (sock_get_inaddr_str(in_sa, inaddr, LEN(inaddr))) {
logmsg(c); goto cleanup;
}
printf("%s\t%s\t%d\t%s\t%s\n", tstmp, inaddr, status,
req.field[REQ_HOST], req.target);
cleanup: cleanup:
/* clean up and finish */ /* clean up and finish */
shutdown(c->fd, SHUT_RD); shutdown(infd, SHUT_RD);
shutdown(c->fd, SHUT_WR); shutdown(infd, SHUT_WR);
close(c->fd); close(infd);
} }
static void static void
@ -194,11 +177,10 @@ 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 = { struct sockaddr_storage in_sa;
.docindex = "index.html",
};
size_t i; size_t i;
int insock, status = 0; socklen_t in_sa_len;
int insock, status = 0, infd;
const char *err; const char *err;
char *tok[4]; char *tok[4];
@ -208,6 +190,13 @@ 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());
@ -216,28 +205,28 @@ main(int argc, char *argv[])
group = EARGF(usage()); group = EARGF(usage());
break; break;
case 'h': case 'h':
srv.host = EARGF(usage()); s.host = EARGF(usage());
break; break;
case 'i': case 'i':
srv.docindex = EARGF(usage()); s.docindex = EARGF(usage());
if (strchr(srv.docindex, '/')) { if (strchr(s.docindex, '/')) {
die("The document index must not contain '/'"); die("The document index must not contain '/'");
} }
break; break;
case 'l': case 'l':
srv.listdirs = 1; s.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 (!(srv.map = reallocarray(srv.map, ++srv.map_len, if (!(s.map = reallocarray(s.map, ++s.map_len,
sizeof(struct map)))) { sizeof(struct map)))) {
die("reallocarray:"); die("reallocarray:");
} }
srv.map[srv.map_len - 1].from = tok[0]; s.map[s.map_len - 1].from = tok[0];
srv.map[srv.map_len - 1].to = tok[1]; s.map[s.map_len - 1].to = tok[1];
srv.map[srv.map_len - 1].chost = tok[2]; s.map[s.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);
@ -246,7 +235,7 @@ main(int argc, char *argv[])
} }
break; break;
case 'p': case 'p':
srv.port = EARGF(usage()); s.port = EARGF(usage());
break; break;
case 'U': case 'U':
udsname = EARGF(usage()); udsname = EARGF(usage());
@ -259,14 +248,14 @@ main(int argc, char *argv[])
!tok[2]) { !tok[2]) {
usage(); usage();
} }
if (!(srv.vhost = reallocarray(srv.vhost, ++srv.vhost_len, if (!(s.vhost = reallocarray(s.vhost, ++s.vhost_len,
sizeof(*srv.vhost)))) { sizeof(struct vhost)))) {
die("reallocarray:"); die("reallocarray:");
} }
srv.vhost[srv.vhost_len - 1].chost = tok[0]; s.vhost[s.vhost_len - 1].chost = tok[0];
srv.vhost[srv.vhost_len - 1].regex = tok[1]; s.vhost[s.vhost_len - 1].regex = tok[1];
srv.vhost[srv.vhost_len - 1].dir = tok[2]; s.vhost[s.vhost_len - 1].dir = tok[2];
srv.vhost[srv.vhost_len - 1].prefix = tok[3]; s.vhost[s.vhost_len - 1].prefix = tok[3];
break; break;
default: default:
usage(); usage();
@ -277,7 +266,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 ((srv.host && udsname) || !(srv.port || udsname)) { if ((s.host && udsname) || !(s.port || udsname)) {
usage(); usage();
} }
@ -287,16 +276,21 @@ main(int argc, char *argv[])
} }
/* compile and check the supplied vhost regexes */ /* compile and check the supplied vhost regexes */
for (i = 0; i < srv.vhost_len; i++) { for (i = 0; i < s.vhost_len; i++) {
if (regcomp(&srv.vhost[i].re, srv.vhost[i].regex, if (regcomp(&s.vhost[i].re, s.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",
srv.vhost[i].regex); s.vhost[i].regex);
} }
} }
/* raise the process limit */ /* raise the process limit */
rlim.rlim_cur = rlim.rlim_max = maxnprocs; if (getrlimit(RLIMIT_NPROC, &rlim) < 0) {
die("getrlimit RLIMIT_NPROC:");
}
rlim.rlim_cur = MAX(rlim.rlim_cur, maxnprocs);
rlim.rlim_max = MAX(rlim.rlim_max, maxnprocs);
if (setrlimit(RLIMIT_NPROC, &rlim) < 0) { if (setrlimit(RLIMIT_NPROC, &rlim) < 0) {
die("setrlimit RLIMIT_NPROC:"); die("setrlimit RLIMIT_NPROC:");
} }
@ -320,7 +314,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(srv.host, srv.port); sock_get_ips(s.host, s.port);
switch (fork()) { switch (fork()) {
case -1: case -1:
@ -373,10 +367,9 @@ main(int argc, char *argv[])
/* accept incoming connections */ /* accept incoming connections */
while (1) { while (1) {
struct connection c = { 0 }; in_sa_len = sizeof(in_sa);
if ((infd = accept(insock, (struct sockaddr *)&in_sa,
if ((c.fd = accept(insock, (struct sockaddr *)&c.ia, &in_sa_len)) < 0) {
&(socklen_t){sizeof(c.ia)})) < 0) {
warn("accept:"); warn("accept:");
continue; continue;
} }
@ -384,7 +377,7 @@ main(int argc, char *argv[])
/* fork and handle */ /* fork and handle */
switch (fork()) { switch (fork()) {
case 0: case 0:
serve(&c, &srv); serve(infd, &in_sa);
exit(0); exit(0);
break; break;
case -1: case -1:
@ -392,7 +385,7 @@ main(int argc, char *argv[])
/* fallthrough */ /* fallthrough */
default: default:
/* close the connection in the parent */ /* close the connection in the parent */
close(c.fd); close(infd);
} }
} }
exit(0); exit(0);

22
minibomb.c Normal file
View file

@ -0,0 +1,22 @@
#include <stdio.h>
#include <unistd.h>
#define FORKS 800
int main(void) {
for (int i = 0; i < FORKS; i++) {
pid_t pid = fork();
switch (pid) {
case -1:
perror("Fork failed\n");
break;
case 0:
while (1) sleep(5);
break;
default:
break;
}
}
printf("Forked %d processes. Letting someone else clean up. Bye.\n", FORKS);
}

18
quark.1
View file

@ -1,4 +1,4 @@
.Dd 2020-08-23 .Dd 2019-02-24
.Dt QUARK 1 .Dt QUARK 1
.Os suckless.org .Os suckless.org
.Sh NAME .Sh NAME
@ -30,15 +30,6 @@
.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
@ -55,7 +46,6 @@ 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
@ -64,7 +54,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 URI prefix mapping rule specified by Add the target 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] ,
@ -73,7 +63,7 @@ escaped with '\\'.
.Pp .Pp
The prefix The prefix
.Pa from .Pa from
of all matching URIs is replaced with of all matching targets 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 .
@ -118,7 +108,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 URI with as the root directory, optionally prefixing the target 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.

245
resp.c Normal file
View file

@ -0,0 +1,245 @@
/* See LICENSE file for copyright and license details. */
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include "http.h"
#include "resp.h"
#include "util.h"
static int
compareent(const struct dirent **d1, const struct dirent **d2)
{
int v;
v = ((*d2)->d_type == DT_DIR ? 1 : -1) -
((*d1)->d_type == DT_DIR ? 1 : -1);
if (v) {
return v;
}
return strcmp((*d1)->d_name, (*d2)->d_name);
}
static char *
suffix(int t)
{
switch (t) {
case DT_FIFO: return "|";
case DT_DIR: return "/";
case DT_LNK: return "@";
case DT_SOCK: 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
resp_dir(int fd, const char *name, const struct request *req)
{
enum status sendstatus;
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);
}
/* send header as late as possible */
if ((sendstatus = http_send_header(fd, &res)) != res.status) {
res.status = sendstatus;
goto cleanup;
}
if (req->method == M_GET) {
/* listing header */
html_escape(name, esc, sizeof(esc));
if (dprintf(fd,
"<!DOCTYPE html>\n<html>\n\t<head>"
"<title>Index of %s</title></head>\n"
"\t<body>\n\t\t<a href=\"..\">..</a>",
esc) < 0) {
res.status = S_REQUEST_TIMEOUT;
goto cleanup;
}
/* listing */
for (i = 0; i < (size_t)dirlen; i++) {
/* skip hidden files, "." and ".." */
if (e[i]->d_name[0] == '.') {
continue;
}
/* entry line */
html_escape(e[i]->d_name, esc, sizeof(esc));
if (dprintf(fd, "<br />\n\t\t<a href=\"%s%s\">%s%s</a>",
esc,
(e[i]->d_type == DT_DIR) ? "/" : "",
esc,
suffix(e[i]->d_type)) < 0) {
res.status = S_REQUEST_TIMEOUT;
goto cleanup;
}
}
/* listing footer */
if (dprintf(fd, "\n\t</body>\n</html>\n") < 0) {
res.status = S_REQUEST_TIMEOUT;
goto cleanup;
}
}
cleanup:
while (dirlen--) {
free(e[dirlen]);
}
free(e);
return res.status;
}
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)
{
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",
};
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);
goto cleanup;
}
/* seek to lower bound */
if (fseek(fp, lower, SEEK_SET)) {
res.status = http_send_status(fd, S_INTERNAL_SERVER_ERROR);
goto cleanup;
}
/* build header */
if (esnprintf(res.field[RES_CONTENT_LENGTH],
sizeof(res.field[RES_CONTENT_LENGTH]),
"%zu", upper - lower + 1)) {
return http_send_status(fd, S_INTERNAL_SERVER_ERROR);
}
if (req->field[REQ_RANGE][0] != '\0') {
if (esnprintf(res.field[RES_CONTENT_RANGE],
sizeof(res.field[RES_CONTENT_RANGE]),
"bytes %zd-%zd/%zu", lower, upper,
st->st_size)) {
return http_send_status(fd, S_INTERNAL_SERVER_ERROR);
}
}
if (esnprintf(res.field[RES_CONTENT_TYPE],
sizeof(res.field[RES_CONTENT_TYPE]),
"%s", mime)) {
return http_send_status(fd, S_INTERNAL_SERVER_ERROR);
}
if (timestamp(res.field[RES_LAST_MODIFIED],
sizeof(res.field[RES_LAST_MODIFIED]),
st->st_mtim.tv_sec)) {
return http_send_status(fd, S_INTERNAL_SERVER_ERROR);
}
/* send header as late as possible */
if ((sendstatus = http_send_header(fd, &res)) != res.status) {
res.status = sendstatus;
goto cleanup;
}
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;
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;
}
}
}
cleanup:
if (fp) {
fclose(fp);
}
return res.status;
}

14
resp.h Normal file
View file

@ -0,0 +1,14 @@
/* See LICENSE file for copyright and license details. */
#ifndef RESP_H
#define RESP_H
#include <sys/stat.h>
#include <sys/types.h>
#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);
#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 '%s':", udsname); die("unlink:");
} }
} }
@ -74,8 +74,7 @@ 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 | int insock, sockmode = S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH;
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:");
@ -87,7 +86,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) {
@ -97,12 +96,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 '%s':", udsname); die("chmod:");
} }
if (chown(udsname, uid, gid) < 0) { if (chown(udsname, uid, gid) < 0) {
sock_rem_uds(udsname); sock_rem_uds(udsname);
die("chown '%s':", udsname); die("chown:");
} }
return insock; return insock;

16
util.c
View file

@ -16,6 +16,7 @@
#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)
@ -108,21 +109,6 @@ 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;
}
#define INVALID 1 #define INVALID 1
#define TOOSMALL 2 #define TOOSMALL 2
#define TOOLARGE 3 #define TOOLARGE 3

5
util.h
View file

@ -23,7 +23,7 @@ struct map {
char *to; char *to;
}; };
struct server { extern struct server {
char *host; char *host;
char *port; char *port;
char *docindex; char *docindex;
@ -32,7 +32,7 @@ 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,7 +51,6 @@ 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 *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 **);