Compare commits

..

26 commits

Author SHA1 Message Date
9ce826690d Only build
All checks were successful
continuous-integration/drone/push Build is passing
2020-08-31 21:02:03 +02:00
e8bc00ffd2 Add dirl to README
Some checks failed
continuous-integration/drone/push Build is failing
2020-08-31 21:00:05 +02:00
ab42879bc6 Merge remote-tracking branch 'upstream/master'
Some checks failed
continuous-integration/drone/push Build is failing
2020-08-30 09:42:18 +02:00
Laslo Hunhold
0823ba4c3e
Add logmsg() and refactor connection handling
Also use compound literals for immediate pointers we don't use later
(same as with setsockopt() in 32223c96bd).

Signed-off-by: Laslo Hunhold <dev@frign.de>
2020-08-29 13:02:51 +02:00
Laslo Hunhold
a36b901d40
Add http_send_body() and data_send_error() and refactor
This turns the data-functions into the only functions "allowed"
to send body-data (called with http_send_body()). The previous (hacky)
approach of doing this in http_send_header() is not only out of place,
it's an easy source of bugs given, for instance, the sending of body
data is not expected with HEAD-requests.

Given html_escape() is now only used in data.c, we move it there from
util.c and make it a static method again.

Signed-off-by: Laslo Hunhold <dev@frign.de>
2020-08-29 00:49:16 +02:00
Laslo Hunhold
db127723c6
Rename functions in data.h and adapt ifdef
Signed-off-by: Laslo Hunhold <dev@frign.de>
2020-08-28 23:46:12 +02:00
Laslo Hunhold
a94b15814c
Rename resp.{c,h} to data.{c,h}
The methods in data.h only deal with the actual response data, not
the request handling itself, which has been formalized a bit more
in http.h. To avoid confusion, we rename it to data.h.

Signed-off-by: Laslo Hunhold <dev@frign.de>
2020-08-28 23:29:54 +02:00
Laslo Hunhold
9a95d9183c
Rename status to s in serve()
This is more consistent with the codebase.

Signed-off-by: Laslo Hunhold <dev@frign.de>
2020-08-28 23:19:29 +02:00
Laslo Hunhold
68e4ff3021
Return proper error-status when http_send_header() fails
Explicitly show that we set the status of the response struct to the
returned error status. This makes it clear that we are beyond the point
where the "form" of the response struct matters and it's now only about
the log-output.

Signed-off-by: Laslo Hunhold <dev@frign.de>
2020-08-28 23:16:47 +02:00
Laslo Hunhold
c0909c70e4
Improve http_prepare_response()'s error semantics
I don't like the juggling with status-values in serve. It makes
sense for http_recv_header() and http_parse_header(), because we
don't have a response-struct yet that we can "fill". We could pass
it to them, but that would make the usage a bit messy.

However, in http_prepare_response(), we are already entrusted with
a pointer to a response-struct, and just failing here (by returning
an error value) leaves the response-struct in an invalid state. Instead,
we make it a void function and reflect the status using the status field
in the passed response struct.

This way, there is no case where the response struct is in an
invalid state after calling a http_prepare_*()-method.

Signed-off-by: Laslo Hunhold <dev@frign.de>
2020-08-28 22:52:04 +02:00
Laslo Hunhold
123f168a3b
Replace http_send_status() with http_prepare_error_response()
This approach fits better in line of first initializing the response
struct and then sending the header with http_send_header() later.

Signed-off-by: Laslo Hunhold <dev@frign.de>
2020-08-28 22:34:46 +02:00
670d2ed65c Merge remote-tracking branch 'upstream/master'
Some checks failed
continuous-integration/drone/push Build is failing
2020-08-23 16:31:13 +02:00
Laslo Hunhold
601b56d270
Mention default behaviour in the manual when the host is not given
Signed-off-by: Laslo Hunhold <dev@frign.de>
2020-08-23 13:36:56 +02:00
Laslo Hunhold
27f8bbfac4
Refactor sock_get_uds() a bit
This refines the error messages a bit and makes clearer what went
wrong.

Signed-off-by: Laslo Hunhold <dev@frign.de>
2020-08-23 13:35:49 +02:00
Laslo Hunhold
1ccaac023c
Rename s to srv
This improves readability a bit and helps iron out confusions with
status-variables called s in other methods.

Signed-off-by: Laslo Hunhold <dev@frign.de>
2020-08-23 11:03:18 +02:00
Laslo Hunhold
50c85ec642
Rename "target" to "URI" where appropriate
Of course URIs point at "targets", but the URIs themselves should
be called what they are, not only in the interest of clarity in terms
of nomenclature.

Signed-off-by: Laslo Hunhold <dev@frign.de>
2020-08-22 23:38:38 +02:00
Laslo Hunhold
68be64e2c1
Remove unused field in the request-struct
Signed-off-by: Laslo Hunhold <dev@frign.de>
2020-08-22 23:31:42 +02:00
Laslo Hunhold
58d0f44e03
Refactor http_send_response() into http_prepare_response()
The function http_send_response() did too much. It not only took
the request fields and built them together into a response, it
delegated too little and many functions were "hacked" into it, for
instance shady directory-changes for vhosts and hand-construction
of response structs.

The preparations for a rework were already made in previous commits,
including a tighter focus on the response-struct itself. Instead of
doing everything locally in the http_send_response() function, the
new http_prepare_response() only really takes the request-struct and
builds a response-struct. The response-struct is expanded such that
it's possible to do the data-sending simply with the response-struct
itself and not any other magic parameters that just drop out of the
function.

Another matter are the http_send_status()-calls. Because the
aforementioned function is so central, this refactoring has included
many areas. Instead of calling http_send_status() in every error-case,
which makes little sense now given we first delegate everything through
a response struct, errors are just sent as a return value and caught
centrally (in serve() in main.c), which centralizes the error handling
a bit.

It might look a bit strange now and it might not be clear in which
direction this is going, but subsequent commits will hopefully give
clarity in this regard.

Signed-off-by: Laslo Hunhold <dev@frign.de>
2020-08-22 23:20:00 +02:00
Laslo Hunhold
a5163d0813
Split up http_get_request()
The function has become too long and basically did two things: Receiving
the header and parsing it. To better reflect this, we split it up into
the two functions http_recv_header() and http_parse_header(). This way,
we also obtain a better separation of concerns and can further reduce
the scope of each parameter-list.

http_recv_header() has been written in such a way that it can be
reentered and fill up the header-buffer bit by bit using a pointer to
an offset value.

The error handling was improved by only returning the immediate error
status codes and letting the caller do the error-handling with
http_send_status().

Signed-off-by: Laslo Hunhold <dev@frign.de>
2020-08-22 11:05:20 +02:00
Laslo Hunhold
c1b242e405
Add connection struct
This struct contains the request and response structs, represents a state
and has some utility-buffers.

Signed-off-by: Laslo Hunhold <dev@frign.de>
2020-08-22 09:24:57 +02:00
Laslo Hunhold
6d2fe7f29e
Move infd and header into request-struct
This compacts the connection state into one struct.

Signed-off-by: Laslo Hunhold <dev@frign.de>
2020-08-21 19:38:29 +02:00
f1fed77826 Add patch to README
All checks were successful
continuous-integration/drone/push Build is passing
2020-08-18 21:27:53 +02:00
ac056a792d Add drone build to README
All checks were successful
continuous-integration/drone/push Build is passing
2020-08-18 19:51:06 +02:00
Laslo Hunhold
ce77dd7962
Update manpage to list capabilities and behaviour
Signed-off-by: Laslo Hunhold <dev@frign.de>
2020-08-18 08:46:52 +02:00
Laslo Hunhold
65600ffe7a
Reduce global state by localizing the server-struct
The server-struct variable s was global, which made it readable and
modifiable from any point in the code. Making it a local variable in
main() instead and passing it as a pointer to constant memory to each
function needing it makes much more sense and allows the compiler to
warn us if we do try to modify it, which it wouldn't have before.

Signed-off-by: Laslo Hunhold <dev@frign.de>
2020-08-17 11:37:25 +02:00
Laslo Hunhold
3bd49b2456
Implement RFC 8615 (Well-Known URIs) and refine access errors
We generally rejected any URI that had a path component beginning
with a '.', i.e. a hidden file. RFC 8615 specifies the well-known URI,
which is used, for instance, with the "http-01" challenge type in
acme-client(1) and will probably see more usage in the future.

To support it, we move the hidden target check after the stat(), so we
don't have to worry about canonicalization of dir-URIs (i.e. missing
trailing '/'). This changes the behaviour a bit, as now quark won't
only send out a 403 whenever a hidden target is requested, but only
if it actually exists, and a 404 otherwise.

Given the earlier call to normabspath() ensures that our path begins
with a '/', we don't need the first check "realtarget[0] == '.'"
anymore, so it can be removed.

Thanks to Robert Russell <robertrussell.72001@gmail.com> for reporting
the lack of support of the RFC 8615 in quark.

Signed-off-by: Laslo Hunhold <dev@frign.de>
2020-08-17 10:39:54 +02:00
15 changed files with 731 additions and 616 deletions

View file

@ -7,31 +7,3 @@ 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 = util sock http resp COMPONENTS = data http sock util
all: quark all: quark
util.o: util.c util.h config.mk data.o: data.c data.h util.h http.h config.mk
sock.o: sock.c sock.h util.h config.mk http.o: http.c http.h util.h http.h data.h config.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 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
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,9 +42,3 @@ 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,8 +1,26 @@
[![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
@ -10,6 +28,14 @@ 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 Normal file
View file

@ -0,0 +1,208 @@
/* 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 Normal file
View file

@ -0,0 +1,11 @@
/* 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,6 +58,12 @@ 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)
{ {
@ -89,40 +95,7 @@ http_send_header(int fd, const struct response *res)
return S_REQUEST_TIMEOUT; return S_REQUEST_TIMEOUT;
} }
return res->status; return 0;
}
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
@ -143,44 +116,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 +175,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 +188,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 +234,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 +244,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 +253,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 +269,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;
@ -527,172 +508,213 @@ 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))
enum status void
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 s;
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)) {
s = S_BAD_REQUEST;
goto err;
}
/* 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); s = S_NOT_FOUND;
goto err;
} }
/* 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)) { s = S_REQUEST_TOO_LARGE;
return http_send_status(fd, S_REQUEST_TOO_LARGE); goto err;
}
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); s = S_REQUEST_TOO_LARGE;
goto err;
} }
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); s = S_BAD_REQUEST;
goto err;
} }
/* 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); s = (errno == EACCES) ? S_FORBIDDEN : S_NOT_FOUND;
} 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)) {
/* 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); s = S_REQUEST_TOO_LARGE;
goto err;
} }
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)) {
s = S_FORBIDDEN;
goto err;
}
/* 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, s = S_INTERNAL_SERVER_ERROR;
S_INTERNAL_SERVER_ERROR); goto err;
} }
/* 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); s = S_REQUEST_TOO_LARGE;
goto err;
} }
} 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); s = S_REQUEST_TOO_LARGE;
goto err;
} }
} }
return http_send_header(fd, &res); return;
} 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 */ /*
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)) {
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(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")) {
s = S_INTERNAL_SERVER_ERROR;
goto err;
}
return;
} else { } else {
/* reject */ /* reject */
if (!S_ISREG(st.st_mode) || errno == EACCES) { s = (!S_ISREG(st.st_mode) || errno == EACCES) ?
return http_send_status(fd, S_FORBIDDEN); S_FORBIDDEN : S_NOT_FOUND;
} else { goto err;
return http_send_status(fd, S_NOT_FOUND);
}
} }
} }
} }
@ -702,39 +724,40 @@ 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); s = 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 http_send_header(fd, &res); return;
} }
} }
/* range */ /* range */
if ((returnstatus = parse_range(req->field[REQ_RANGE], if ((s = 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 (s == 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, s = S_INTERNAL_SERVER_ERROR;
S_INTERNAL_SERVER_ERROR); goto err;
} }
return http_send_header(fd, &res); return;
} else { } else {
return http_send_status(fd, returnstatus); goto err;
} }
} }
/* 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 +765,91 @@ 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")) {
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,6 +3,9 @@
#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
@ -26,7 +29,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 +64,54 @@ 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);
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 *);
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 */

149
main.c
View file

@ -16,6 +16,7 @@
#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"
@ -23,41 +24,57 @@
static char *udsname; static char *udsname;
static void static void
serve(int infd, const struct sockaddr_storage *in_sa) logmsg(const struct connection *c)
{ {
struct request req; char inaddr_str[INET6_ADDRSTRLEN /* > INET_ADDRSTRLEN */];
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(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 ((s = http_recv_header(c->fd, c->header, LEN(c->header), &c->off)) ||
status = http_send_response(infd, &req); (s = http_parse_header(c->header, &c->req))) {
http_prepare_error_response(&c->req, &c->res, s);
} else {
http_prepare_response(&c->req, &c->res, srv);
} }
/* write output to log */ if ((s = http_send_header(c->fd, &c->res)) ||
t = time(NULL); (s = http_send_body(c->fd, &c->res, &c->req))) {
if (!strftime(tstmp, sizeof(tstmp), "%Y-%m-%dT%H:%M:%SZ", c->res.status = s;
gmtime(&t))) {
warn("strftime: Exceeded buffer capacity");
goto cleanup;
} }
if (sock_get_inaddr_str(in_sa, inaddr, LEN(inaddr))) {
goto cleanup; logmsg(c);
}
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(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,10 +194,11 @@ 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 sockaddr_storage in_sa; struct server srv = {
.docindex = "index.html",
};
size_t i; size_t i;
socklen_t in_sa_len; int insock, status = 0;
int insock, status = 0, infd;
const char *err; const char *err;
char *tok[4]; char *tok[4];
@ -190,13 +208,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 +216,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 +246,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 +259,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 +277,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,30 +287,25 @@ 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);
} }
} }
/* raise the process limit */ /* raise the process limit */
if (getrlimit(RLIMIT_NPROC, &rlim) < 0) { rlim.rlim_cur = rlim.rlim_max = maxnprocs;
die("getrlimit RLIMIT_NPROC:"); if (setrlimit(RLIMIT_NPROC, &rlim) < 0) {
} die("setrlimit RLIMIT_NPROC:");
}
rlim.rlim_cur = MAX(rlim.rlim_cur, maxnprocs); /* validate user and group */
rlim.rlim_max = MAX(rlim.rlim_max, maxnprocs); errno = 0;
if (setrlimit(RLIMIT_NPROC, &rlim) < 0) { if (!user || !(pwd = getpwnam(user))) {
die("setrlimit RLIMIT_NPROC:"); die("getpwnam '%s': %s", user ? user : "null",
} errno ? strerror(errno) : "Entry not found");
/* validate user and group */
errno = 0;
if (!user || !(pwd = getpwnam(user))) {
die("getpwnam '%s': %s", user ? user : "null",
errno ? strerror(errno) : "Entry not found");
} }
errno = 0; errno = 0;
if (!group || !(grp = getgrnam(group))) { if (!group || !(grp = getgrnam(group))) {
@ -314,7 +320,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:
@ -367,9 +373,10 @@ main(int argc, char *argv[])
/* accept incoming connections */ /* accept incoming connections */
while (1) { while (1) {
in_sa_len = sizeof(in_sa); struct connection c = { 0 };
if ((infd = accept(insock, (struct sockaddr *)&in_sa,
&in_sa_len)) < 0) { if ((c.fd = accept(insock, (struct sockaddr *)&c.ia,
&(socklen_t){sizeof(c.ia)})) < 0) {
warn("accept:"); warn("accept:");
continue; continue;
} }
@ -377,7 +384,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(&c, &srv);
exit(0); exit(0);
break; break;
case -1: case -1:
@ -385,7 +392,7 @@ main(int argc, char *argv[])
/* fallthrough */ /* fallthrough */
default: default:
/* close the connection in the parent */ /* close the connection in the parent */
close(infd); close(c.fd);
} }
} }
exit(0); exit(0);

View file

@ -1,22 +0,0 @@
#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 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.

245
resp.c
View file

@ -1,245 +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 "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
View file

@ -1,14 +0,0 @@
/* 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:"); 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;

16
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,21 @@ 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;
}; };
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,7 @@ 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 **);