sites

public wiki contents of suckless.org
git clone git://git.suckless.org/sites
Log | Files | Refs

quark-digestauth-20201101-dff98c0.diff (25809B)


      1 From 2d855b934bf0ba2bdcaf7c818a4a9b1a836b2c59 Mon Sep 17 00:00:00 2001
      2 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20S=C3=A1nchez=20Garc=C3=ADa?=
      3  <soy.jmi2k@gmail.com>
      4 Date: Sun, 1 Nov 2020 00:57:31 +0000
      5 Subject: [PATCH] Add Digest auth support
      6 
      7 This follows RFC 7616, but only MD5 algorithm and auth qop are supported.
      8 ---
      9  Makefile     |   3 +-
     10  config.def.h |   2 +-
     11  http.c       | 291 +++++++++++++++++++++++++++++++++++++++++++++++++--
     12  http.h       |  28 ++++-
     13  main.c       |  79 ++++++++++++--
     14  md5.c        | 148 ++++++++++++++++++++++++++
     15  md5.h        |  18 ++++
     16  quark.1      |  26 +++++
     17  util.h       |  14 +++
     18  9 files changed, 586 insertions(+), 23 deletions(-)
     19  create mode 100644 md5.c
     20  create mode 100644 md5.h
     21 
     22 diff --git a/Makefile b/Makefile
     23 index da0e458..52fc3db 100644
     24 --- a/Makefile
     25 +++ b/Makefile
     26 @@ -4,13 +4,14 @@
     27  
     28  include config.mk
     29  
     30 -COMPONENTS = data http queue sock util
     31 +COMPONENTS = data http md5 queue sock util
     32  
     33  all: quark
     34  
     35  data.o: data.c data.h http.h util.h config.mk
     36  http.o: http.c config.h http.h util.h config.mk
     37  main.o: main.c arg.h data.h http.h queue.h sock.h util.h config.mk
     38 +md5.o: md5.c md5.h config.mk
     39  sock.o: sock.c sock.h util.h config.mk
     40  util.o: util.c util.h config.mk
     41  
     42 diff --git a/config.def.h b/config.def.h
     43 index 56f62aa..a322e7a 100644
     44 --- a/config.def.h
     45 +++ b/config.def.h
     46 @@ -2,7 +2,7 @@
     47  #define CONFIG_H
     48  
     49  #define BUFFER_SIZE 4096
     50 -#define FIELD_MAX   200
     51 +#define FIELD_MAX   500
     52  
     53  /* mime-types */
     54  static const struct {
     55 diff --git a/http.c b/http.c
     56 index dc32290..1f99722 100644
     57 --- a/http.c
     58 +++ b/http.c
     59 @@ -17,13 +17,16 @@
     60  #include <unistd.h>
     61  
     62  #include "config.h"
     63 +#include "data.h"
     64  #include "http.h"
     65 +#include "md5.h"
     66  #include "util.h"
     67  
     68  const char *req_field_str[] = {
     69  	[REQ_HOST]              = "Host",
     70  	[REQ_RANGE]             = "Range",
     71  	[REQ_IF_MODIFIED_SINCE] = "If-Modified-Since",
     72 +	[REQ_AUTHORIZATION]     = "Authorization",
     73  };
     74  
     75  const char *req_method_str[] = {
     76 @@ -37,6 +40,7 @@ const char *status_str[] = {
     77  	[S_MOVED_PERMANENTLY]     = "Moved Permanently",
     78  	[S_NOT_MODIFIED]          = "Not Modified",
     79  	[S_BAD_REQUEST]           = "Bad Request",
     80 +	[S_UNAUTHORIZED]          = "Unauthorized",
     81  	[S_FORBIDDEN]             = "Forbidden",
     82  	[S_NOT_FOUND]             = "Not Found",
     83  	[S_METHOD_NOT_ALLOWED]    = "Method Not Allowed",
     84 @@ -55,6 +59,7 @@ const char *res_field_str[] = {
     85  	[RES_CONTENT_LENGTH] = "Content-Length",
     86  	[RES_CONTENT_RANGE]  = "Content-Range",
     87  	[RES_CONTENT_TYPE]   = "Content-Type",
     88 +	[RES_AUTHENTICATE]   = "WWW-Authenticate",
     89  };
     90  
     91  enum status
     92 @@ -75,8 +80,9 @@ http_prepare_header_buf(const struct response *res, struct buffer *buf)
     93  	if (buffer_appendf(buf,
     94  	                   "HTTP/1.1 %d %s\r\n"
     95  	                   "Date: %s\r\n"
     96 -	                   "Connection: close\r\n",
     97 -	                   res->status, status_str[res->status], tstmp)) {
     98 +	                   "Connection: %s\r\n",
     99 +	                   res->status, status_str[res->status], tstmp,
    100 +			   res->keep_alive ? "keep-alive" : "close")) {
    101  		goto err;
    102  	}
    103  
    104 @@ -549,21 +555,197 @@ parse_range(const char *str, size_t size, size_t *lower, size_t *upper)
    105  	return 0;
    106  }
    107  
    108 +static enum status
    109 +parse_auth(const char *str, struct auth *auth)
    110 +{
    111 +	const char *p;
    112 +	char *q;
    113 +
    114 +	/* done if no range-string is given */
    115 +	if (str == NULL || *str == '\0') {
    116 +		return 0;
    117 +	}
    118 +
    119 +	/* skip method authentication statement */
    120 +	if (strncmp(str, "Digest", sizeof("Digest") - 1)) {
    121 +		return S_BAD_REQUEST;
    122 +	}
    123 +
    124 +	p = str + (sizeof("Digest") - 1);
    125 +
    126 +	/*
    127 +	 * Almost all the fields are quoted-string with no restrictions.
    128 +	 *
    129 +	 * However, some of them require special parsing, which is done inline
    130 +	 * and continue the loop early, before reaching the quoted-string
    131 +	 * parser.
    132 +	 */
    133 +	while (*p) {
    134 +		/* skip leading whitespace */
    135 +		for (++p; *p == ' ' || *p == '\t'; p++)
    136 +			;
    137 +
    138 +		if (!strncmp("qop=", p,
    139 +		             sizeof("qop=") - 1)) {
    140 +			p += sizeof("qop=") - 1;
    141 +			q = auth->qop;
    142 +			/* "qop" is handled differently */
    143 +			while (*p && *p != ',') {
    144 +				if (*p == '\\') {
    145 +					++p;
    146 +				}
    147 +				*q++ = *p++;
    148 +			}
    149 +			*q = '\0';
    150 +
    151 +			continue;
    152 +		} else if (!strncmp("algorithm=", p,
    153 +		                    sizeof("algorithm=") - 1)) {
    154 +			p += sizeof("algorithm=") - 1;
    155 +			q = auth->algorithm;
    156 +			/* "algorithm" is handled differently */
    157 +			while (*p && *p != ',') {
    158 +				if (*p == '\\') {
    159 +					++p;
    160 +				}
    161 +				*q++ = *p++;
    162 +			}
    163 +			*q = '\0';
    164 +
    165 +			continue;
    166 +		} else if (!strncmp("nc=", p,
    167 +		                    sizeof("nc=") - 1)) {
    168 +			p += sizeof("nc=") - 1;
    169 +			q = auth->nc;
    170 +			/* "nc" is handled differently */
    171 +			while (*p && *p != ',') {
    172 +				if (*p < '0' || *p > '9') {
    173 +					return S_BAD_REQUEST;
    174 +				}
    175 +				*q++ = *p++;
    176 +			}
    177 +			*q = '\0';
    178 +
    179 +			continue;
    180 +		/* these all are quoted-string */
    181 +		} else if (!strncmp("response=\"", p,
    182 +		                    sizeof("response=\"") - 1)) {
    183 +			p += sizeof("response=\"") - 1;
    184 +			q = auth->response;
    185 +		} else if (!strncmp("username=\"", p,
    186 +		                    sizeof("username=\"") - 1)) {
    187 +			p += sizeof("username=\"") - 1;
    188 +			q = auth->username;
    189 +		} else if (!strncmp("realm=\"", p,
    190 +		                    sizeof("realm=\"") - 1)) {
    191 +			p += sizeof("realm=\"") - 1;
    192 +			q = auth->realm;
    193 +		} else if (!strncmp("uri=\"", p,
    194 +		                    sizeof("uri=\"") - 1)) {
    195 +			p += sizeof("uri=\"") - 1;
    196 +			q = auth->uri;
    197 +		} else if (!strncmp("cnonce=\"", p,
    198 +		                    sizeof("cnonce=\"") - 1)) {
    199 +			p += sizeof("cnonce=\"") - 1;
    200 +			q = auth->cnonce;
    201 +		} else if (!strncmp("nonce=\"", p,
    202 +		                    sizeof("nonce=\"") - 1)) {
    203 +			p += sizeof("nonce=\"") - 1;
    204 +			q = auth->nonce;
    205 +		} else {
    206 +			return S_BAD_REQUEST;
    207 +		}
    208 +
    209 +		/* parse quoted-string */
    210 +		while (*p != '"') {
    211 +			if (*p == '\\') {
    212 +				++p;
    213 +			}
    214 +			if (!*p) {
    215 +				return S_BAD_REQUEST;
    216 +			}
    217 +			*q++ = *p++;
    218 +		}
    219 +		*q = '\0';
    220 +		++p;
    221 +	}
    222 +
    223 +	/* skip trailing whitespace */
    224 +	for (++p; *p == ' ' || *p == '\t'; p++)
    225 +		;
    226 +
    227 +	if (*p) {
    228 +		return S_BAD_REQUEST;
    229 +	}
    230 +
    231 +	return 0;
    232 +}
    233 +
    234 +static enum status
    235 +prepare_digest(char response[MD5_DIGEST_LENGTH * 2 + 1],
    236 +               enum req_method method, const char *uri, const uint8_t *a1,
    237 +               const char *nonce, const char *nc, const char *cnonce,
    238 +               const char *qop)
    239 +{
    240 +	uint8_t a2[MD5_DIGEST_LENGTH], kdr[MD5_DIGEST_LENGTH];
    241 +	char scratch[FIELD_MAX];
    242 +	struct md5 md5;
    243 +	unsigned int i;
    244 +	char *p;
    245 +
    246 +	/* calculate H(A2) */
    247 +	if (esnprintf(scratch, sizeof(scratch), "%s:%s",
    248 +	              req_method_str[method], uri)) {
    249 +		return S_INTERNAL_SERVER_ERROR;
    250 +	}
    251 +
    252 +	md5_init(&md5);
    253 +	md5_update(&md5, scratch, strlen(scratch));
    254 +	md5_sum(&md5, a2);
    255 +
    256 +	/* calculate response */
    257 +	if (esnprintf(scratch, sizeof(scratch), "%s:%s:%s:%s:%s:%-*x",
    258 +	              a1, nonce, nc, cnonce, qop,
    259 +	              MD5_DIGEST_LENGTH * 2 + 1, 0)) {
    260 +		return S_INTERNAL_SERVER_ERROR;
    261 +	}
    262 +
    263 +	/* replace trailing string of '-' inside scratch with actual H(A2) */
    264 +	p = &scratch[strlen(scratch) - (MD5_DIGEST_LENGTH * 2 + 1)];
    265 +	for (i = 0; i < sizeof(a2); i++) {
    266 +		sprintf(&p[i << 1], "%02x", a2[i]);
    267 +	}
    268 +
    269 +	md5_init(&md5);
    270 +	md5_update(&md5, scratch, strlen(scratch));
    271 +	md5_sum(&md5, kdr);
    272 +
    273 +	for (i = 0; i < sizeof(kdr); i++) {
    274 +		sprintf(&response[i << 1], "%02x", kdr[i]);
    275 +	}
    276 +
    277 +	return 0;
    278 +}
    279 +
    280  #undef RELPATH
    281  #define RELPATH(x) ((!*(x) || !strcmp(x, "/")) ? "." : ((x) + 1))
    282  
    283  void
    284 -http_prepare_response(const struct request *req, struct response *res,
    285 -                      const struct server *srv)
    286 +http_prepare_response(struct request *req, struct response *res,
    287 +                      char nonce[FIELD_MAX], const struct server *srv)
    288  {
    289  	enum status s;
    290  	struct in6_addr addr;
    291  	struct stat st;
    292  	struct tm tm = { 0 };
    293 +	struct auth auth = { 0 };
    294  	struct vhost *vhost;
    295 +	struct realm *realm;
    296 +	struct account *account;
    297  	size_t len, i;
    298  	int hasport, ipv6host;
    299  	static char realuri[PATH_MAX], tmpuri[PATH_MAX];
    300 +	char response[MD5_DIGEST_LENGTH * 2 + 1];
    301  	char *p, *mime;
    302  	const char *targethost;
    303  
    304 @@ -809,14 +991,63 @@ http_prepare_response(const struct request *req, struct response *res,
    305  		}
    306  	}
    307  
    308 -	/* fill response struct */
    309 -	res->type = RESTYPE_FILE;
    310 -
    311  	/* check if file is readable */
    312  	res->status = (access(res->path, R_OK)) ? S_FORBIDDEN :
    313  	              (req->field[REQ_RANGE][0] != '\0') ?
    314  	              S_PARTIAL_CONTENT : S_OK;
    315  
    316 +	/* check if the client is authorized */
    317 +	realm = NULL;
    318 +	if (srv->realm) {
    319 +		for (i = 0; i < srv->realm_len; i++) {
    320 +			if (srv->realm[i].gid == st.st_gid) {
    321 +				realm = &(srv->realm[i]);
    322 +				break;
    323 +			}
    324 +		}
    325 +		req->realm = realm;
    326 +		/* if the file belongs to a realm */
    327 +		if (i < srv->realm_len) {
    328 +			if (req->field[REQ_AUTHORIZATION][0] == '\0') {
    329 +				s = S_UNAUTHORIZED;
    330 +				goto err;
    331 +			}
    332 +			if ((s = parse_auth(req->field[REQ_AUTHORIZATION],
    333 +			                    &auth))) {
    334 +				goto err;
    335 +			}
    336 +			/* look for the requested user */
    337 +			for (i = 0; i < realm->account_len; i++) {
    338 +				if (!strcmp(auth.username,
    339 +					    realm->account[i].username)) {
    340 +					account = &(realm->account[i]);
    341 +					break;
    342 +				}
    343 +			}
    344 +			if (i == realm->account_len) {
    345 +				s = S_UNAUTHORIZED;
    346 +				goto err;
    347 +			}
    348 +			if ((s = prepare_digest(response, req->method,
    349 +			                        auth.uri,
    350 +			                        (uint8_t *)account->crypt,
    351 +			                        nonce, auth.nc,
    352 +			                        auth.cnonce, auth.qop))) {
    353 +				goto err;
    354 +			}
    355 +			if (strcmp(auth.nonce, nonce)) {
    356 +				req->stale = 1;
    357 +			}
    358 +			if (strncmp(response, auth.response, sizeof(response))) {
    359 +				s = S_UNAUTHORIZED;
    360 +				goto err;
    361 +			}
    362 +		}
    363 +	}
    364 +
    365 +	/* fill response struct */
    366 +	res->type = RESTYPE_FILE;
    367 +
    368  	if (esnprintf(res->field[RES_ACCEPT_RANGES],
    369  	              sizeof(res->field[RES_ACCEPT_RANGES]),
    370  		      "%s", "bytes")) {
    371 @@ -854,17 +1085,22 @@ http_prepare_response(const struct request *req, struct response *res,
    372  
    373  	return;
    374  err:
    375 -	http_prepare_error_response(req, res, s);
    376 +	http_prepare_error_response(req, res, nonce, s);
    377  }
    378  
    379  void
    380  http_prepare_error_response(const struct request *req,
    381 -                            struct response *res, enum status s)
    382 +                            struct response *res, char nonce[FIELD_MAX],
    383 +                            enum status s)
    384  {
    385 +	struct timespec ts;
    386 +	struct buffer buf;
    387 +	size_t progress;
    388 +
    389  	/* used later */
    390  	(void)req;
    391  
    392 -	/* empty all response fields */
    393 +	/* empty all fields */
    394  	memset(res, 0, sizeof(*res));
    395  
    396  	res->type = RESTYPE_ERROR;
    397 @@ -883,4 +1119,39 @@ http_prepare_error_response(const struct request *req,
    398  			res->status = S_INTERNAL_SERVER_ERROR;
    399  		}
    400  	}
    401 +
    402 +	if (res->status == S_UNAUTHORIZED) {
    403 +		clock_gettime(CLOCK_MONOTONIC, &ts);
    404 +		if (esnprintf(nonce, FIELD_MAX,
    405 +		              "%lus, %luns, %s",
    406 +		              ts.tv_sec, ts.tv_nsec,
    407 +		              req->realm->name)) {
    408 +			res->status = S_INTERNAL_SERVER_ERROR;
    409 +		}
    410 +		if (esnprintf(res->field[RES_AUTHENTICATE],
    411 +			      sizeof(res->field[RES_AUTHENTICATE]),
    412 +			      "Digest "
    413 +			      "realm=\"%s\", "
    414 +			      "qop=\"auth\", "
    415 +			      "algorithm=MD5, "
    416 +			      "stale=%s, "
    417 +			      "nonce=\"%s\"",
    418 +			      req->realm->name,
    419 +			      req->stale ? "true" : "false",
    420 +			      nonce)) {
    421 +			res->status = S_INTERNAL_SERVER_ERROR;
    422 +		} else {
    423 +			res->keep_alive = 1;
    424 +		}
    425 +	}
    426 +
    427 +	progress = 0;
    428 +	if (data_prepare_error_buf(res, &buf, &progress)
    429 +	    || esnprintf(res->field[RES_CONTENT_LENGTH],
    430 +	              sizeof(res->field[RES_CONTENT_LENGTH]),
    431 +	              "%zu", buf.len)) {
    432 +		res->field[RES_CONTENT_LENGTH][0] = '\0';
    433 +		res->keep_alive = 0;
    434 +		s = S_INTERNAL_SERVER_ERROR;
    435 +	}
    436  }
    437 diff --git a/http.h b/http.h
    438 index bfaa807..215bb8f 100644
    439 --- a/http.h
    440 +++ b/http.h
    441 @@ -12,6 +12,7 @@ enum req_field {
    442  	REQ_HOST,
    443  	REQ_RANGE,
    444  	REQ_IF_MODIFIED_SINCE,
    445 +	REQ_AUTHORIZATION,
    446  	NUM_REQ_FIELDS,
    447  };
    448  
    449 @@ -28,6 +29,8 @@ extern const char *req_method_str[];
    450  struct request {
    451  	enum req_method method;
    452  	char uri[PATH_MAX];
    453 +	struct realm *realm;
    454 +	int stale;
    455  	char field[NUM_REQ_FIELDS][FIELD_MAX];
    456  };
    457  
    458 @@ -37,6 +40,7 @@ enum status {
    459  	S_MOVED_PERMANENTLY     = 301,
    460  	S_NOT_MODIFIED          = 304,
    461  	S_BAD_REQUEST           = 400,
    462 +	S_UNAUTHORIZED          = 401,
    463  	S_FORBIDDEN             = 403,
    464  	S_NOT_FOUND             = 404,
    465  	S_METHOD_NOT_ALLOWED    = 405,
    466 @@ -57,6 +61,7 @@ enum res_field {
    467  	RES_CONTENT_LENGTH,
    468  	RES_CONTENT_RANGE,
    469  	RES_CONTENT_TYPE,
    470 +	RES_AUTHENTICATE,
    471  	NUM_RES_FIELDS,
    472  };
    473  
    474 @@ -72,6 +77,7 @@ enum res_type {
    475  struct response {
    476  	enum res_type type;
    477  	enum status status;
    478 +	int keep_alive;
    479  	char field[NUM_RES_FIELDS][FIELD_MAX];
    480  	char uri[PATH_MAX];
    481  	char path[PATH_MAX];
    482 @@ -83,6 +89,7 @@ struct response {
    483  
    484  enum conn_state {
    485  	C_VACANT,
    486 +	C_START,
    487  	C_RECV_HEADER,
    488  	C_SEND_HEADER,
    489  	C_SEND_BODY,
    490 @@ -91,6 +98,7 @@ enum conn_state {
    491  
    492  struct connection {
    493  	enum conn_state state;
    494 +	char nonce[FIELD_MAX];
    495  	int fd;
    496  	struct sockaddr_storage ia;
    497  	struct request req;
    498 @@ -99,13 +107,25 @@ struct connection {
    499  	size_t progress;
    500  };
    501  
    502 +struct auth {
    503 +	char response[FIELD_MAX];
    504 +	char username[FIELD_MAX];
    505 +	char realm[FIELD_MAX];
    506 +	char uri[FIELD_MAX];
    507 +	char qop[FIELD_MAX];
    508 +	char cnonce[FIELD_MAX];
    509 +	char nonce[FIELD_MAX];
    510 +	char algorithm[FIELD_MAX];
    511 +	char nc[FIELD_MAX];
    512 +};
    513 +
    514  enum status http_prepare_header_buf(const struct response *, struct buffer *);
    515  enum status http_send_buf(int, struct buffer *);
    516  enum status http_recv_header(int, struct buffer *, int *);
    517  enum status http_parse_header(const char *, struct request *);
    518 -void http_prepare_response(const struct request *, struct response *,
    519 -                           const struct server *);
    520 -void http_prepare_error_response(const struct request *,
    521 -                                 struct response *, enum status);
    522 +void http_prepare_response(struct request *, struct response *,
    523 +                           char nonce[FIELD_MAX], const struct server *);
    524 +void http_prepare_error_response(const struct request *, struct response *,
    525 +                                 char nonce[FIELD_MAX], enum status);
    526  
    527  #endif /* HTTP_H */
    528 diff --git a/main.c b/main.c
    529 index e26cc77..65dacf4 100644
    530 --- a/main.c
    531 +++ b/main.c
    532 @@ -66,11 +66,17 @@ serve_connection(struct connection *c, const struct server *srv)
    533  
    534  	switch (c->state) {
    535  	case C_VACANT:
    536 +		/* we were passed a "fresh" connection, reset all state */
    537 +
    538 +		c->state = C_START;
    539 +		/* fallthrough */
    540 +	case C_START:
    541  		/*
    542 -		 * we were passed a "fresh" connection which should now
    543 -		 * try to receive the header, reset buf beforehand
    544 +		 * we start handling a request, so we first must try to
    545 +		 * receive the header, reset buf beforehand
    546  		 */
    547  		memset(&c->buf, 0, sizeof(c->buf));
    548 +		c->progress = 0;
    549  
    550  		c->state = C_RECV_HEADER;
    551  		/* fallthrough */
    552 @@ -78,7 +84,7 @@ serve_connection(struct connection *c, const struct server *srv)
    553  		/* receive header */
    554  		done = 0;
    555  		if ((s = http_recv_header(c->fd, &c->buf, &done))) {
    556 -			http_prepare_error_response(&c->req, &c->res, s);
    557 +			http_prepare_error_response(&c->req, &c->res, c->nonce, s);
    558  			goto response;
    559  		}
    560  		if (!done) {
    561 @@ -88,16 +94,16 @@ serve_connection(struct connection *c, const struct server *srv)
    562  
    563  		/* parse header */
    564  		if ((s = http_parse_header(c->buf.data, &c->req))) {
    565 -			http_prepare_error_response(&c->req, &c->res, s);
    566 +			http_prepare_error_response(&c->req, &c->res, c->nonce, s);
    567  			goto response;
    568  		}
    569  
    570  		/* prepare response struct */
    571 -		http_prepare_response(&c->req, &c->res, srv);
    572 +		http_prepare_response(&c->req, &c->res, c->nonce, srv);
    573  response:
    574  		/* generate response header */
    575  		if ((s = http_prepare_header_buf(&c->res, &c->buf))) {
    576 -			http_prepare_error_response(&c->req, &c->res, s);
    577 +			http_prepare_error_response(&c->req, &c->res, c->nonce, s);
    578  			if ((s = http_prepare_header_buf(&c->res, &c->buf))) {
    579  				/* couldn't generate the header, we failed for good */
    580  				c->res.status = s;
    581 @@ -151,6 +157,21 @@ response:
    582  	}
    583  err:
    584  	logmsg(c);
    585 +
    586 +	/* don't cleanup if we keep the connection alive */
    587 +	if (c->res.keep_alive) {
    588 +		/*
    589 +		 * if the length is unspecified, a keep-alive connection will
    590 +		 * wait timeout: kill the connection to avoid it
    591 +		 */
    592 +		if (c->res.field[RES_CONTENT_LENGTH][0] == '\0') {
    593 +			c->res.status = S_INTERNAL_SERVER_ERROR;
    594 +		} else {
    595 +			c->state = C_START;
    596 +			return;
    597 +		}
    598 +	}
    599 +
    600  	close_connection(c);
    601  }
    602  
    603 @@ -296,6 +317,7 @@ thread_method(void *data)
    604  				 * we are "stuck" at
    605  				 */
    606  				switch(c->state) {
    607 +				case C_START:
    608  				case C_RECV_HEADER:
    609  					if (queue_mod_fd(qfd, c->fd,
    610  					                 QUEUE_EVENT_IN,
    611 @@ -463,7 +485,8 @@ static void
    612  usage(void)
    613  {
    614  	const char *opts = "[-u user] [-g group] [-n num] [-d dir] [-l] "
    615 -	                   "[-i file] [-v vhost] ... [-m map] ...";
    616 +	                   "[-i file] [-v vhost] ... [-m map] ... "
    617 +			   "[-r realm] ... [-a account] ...";
    618  
    619  	die("usage: %s -p port [-h host] %s\n"
    620  	    "       %s -U file [-p port] %s", argv0,
    621 @@ -479,6 +502,7 @@ main(int argc, char *argv[])
    622  	struct server srv = {
    623  		.docindex = "index.html",
    624  	};
    625 +	struct realm *realm;
    626  	size_t i;
    627  	int *insock = NULL, status = 0;
    628  	const char *err;
    629 @@ -492,6 +516,29 @@ main(int argc, char *argv[])
    630  	char *group = "nogroup";
    631  
    632  	ARGBEGIN {
    633 +	case 'a':
    634 +		if (spacetok(EARGF(usage()), tok, 3) || !tok[0] || !tok[1] ||
    635 +		    !tok[2]) {
    636 +			usage();
    637 +		}
    638 +		realm = NULL;
    639 +		for (i = 0; i < srv.realm_len; i++) {
    640 +			if (!strcmp(srv.realm[i].name, tok[0])) {
    641 +				realm = &(srv.realm[i]);
    642 +				break;
    643 +			}
    644 +		}
    645 +		if (!realm) {
    646 +			die("Realm '%s' not found", tok[0]);
    647 +		}
    648 +		if (!(realm->account = reallocarray(realm->account,
    649 +		                                 ++realm->account_len,
    650 +		                                 sizeof(struct account)))) {
    651 +				die("reallocarray:");
    652 +		}
    653 +		realm->account[realm->account_len - 1].username = tok[1];
    654 +		realm->account[realm->account_len - 1].crypt    = tok[2];
    655 +		break;
    656  	case 'd':
    657  		servedir = EARGF(usage());
    658  		break;
    659 @@ -539,6 +586,24 @@ main(int argc, char *argv[])
    660  	case 'p':
    661  		srv.port = EARGF(usage());
    662  		break;
    663 +	case 'r':
    664 +		if (spacetok(EARGF(usage()), tok, 2) || !tok[0] || !tok[1]) {
    665 +			usage();
    666 +		}
    667 +		errno = 0;
    668 +		if (!(grp = getgrnam(tok[0]))) {
    669 +			die("getgrnam '%s': %s", tok[0] ? tok[0] : "null",
    670 +			    errno ? strerror(errno) : "Entry not found");
    671 +		}
    672 +		if (!(srv.realm = reallocarray(srv.realm, ++srv.realm_len,
    673 +		                           sizeof(struct realm)))) {
    674 +			die("reallocarray:");
    675 +		}
    676 +		srv.realm[srv.realm_len - 1].gid         = grp->gr_gid;
    677 +		srv.realm[srv.realm_len - 1].name        = tok[1];
    678 +		srv.realm[srv.realm_len - 1].account     = NULL;
    679 +		srv.realm[srv.realm_len - 1].account_len = 0;
    680 +		break;
    681  	case 'U':
    682  		udsname = EARGF(usage());
    683  		break;
    684 diff --git a/md5.c b/md5.c
    685 new file mode 100644
    686 index 0000000..f56a501
    687 --- /dev/null
    688 +++ b/md5.c
    689 @@ -0,0 +1,148 @@
    690 +/* public domain md5 implementation based on rfc1321 and libtomcrypt */
    691 +#include <stdint.h>
    692 +#include <string.h>
    693 +
    694 +#include "md5.h"
    695 +
    696 +static uint32_t rol(uint32_t n, int k) { return (n << k) | (n >> (32-k)); }
    697 +#define F(x,y,z) (z ^ (x & (y ^ z)))
    698 +#define G(x,y,z) (y ^ (z & (y ^ x)))
    699 +#define H(x,y,z) (x ^ y ^ z)
    700 +#define I(x,y,z) (y ^ (x | ~z))
    701 +#define FF(a,b,c,d,w,s,t) a += F(b,c,d) + w + t; a = rol(a,s) + b
    702 +#define GG(a,b,c,d,w,s,t) a += G(b,c,d) + w + t; a = rol(a,s) + b
    703 +#define HH(a,b,c,d,w,s,t) a += H(b,c,d) + w + t; a = rol(a,s) + b
    704 +#define II(a,b,c,d,w,s,t) a += I(b,c,d) + w + t; a = rol(a,s) + b
    705 +
    706 +static const uint32_t tab[64] = {
    707 +	0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
    708 +	0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
    709 +	0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
    710 +	0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
    711 +	0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
    712 +	0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
    713 +	0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
    714 +	0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391
    715 +};
    716 +
    717 +static void
    718 +processblock(struct md5 *s, const uint8_t *buf)
    719 +{
    720 +	uint32_t i, W[16], a, b, c, d;
    721 +
    722 +	for (i = 0; i < 16; i++) {
    723 +		W[i] = buf[4*i];
    724 +		W[i] |= (uint32_t)buf[4*i+1]<<8;
    725 +		W[i] |= (uint32_t)buf[4*i+2]<<16;
    726 +		W[i] |= (uint32_t)buf[4*i+3]<<24;
    727 +	}
    728 +
    729 +	a = s->h[0];
    730 +	b = s->h[1];
    731 +	c = s->h[2];
    732 +	d = s->h[3];
    733 +
    734 +	i = 0;
    735 +	while (i < 16) {
    736 +		FF(a,b,c,d, W[i],  7, tab[i]); i++;
    737 +		FF(d,a,b,c, W[i], 12, tab[i]); i++;
    738 +		FF(c,d,a,b, W[i], 17, tab[i]); i++;
    739 +		FF(b,c,d,a, W[i], 22, tab[i]); i++;
    740 +	}
    741 +	while (i < 32) {
    742 +		GG(a,b,c,d, W[(5*i+1)%16],  5, tab[i]); i++;
    743 +		GG(d,a,b,c, W[(5*i+1)%16],  9, tab[i]); i++;
    744 +		GG(c,d,a,b, W[(5*i+1)%16], 14, tab[i]); i++;
    745 +		GG(b,c,d,a, W[(5*i+1)%16], 20, tab[i]); i++;
    746 +	}
    747 +	while (i < 48) {
    748 +		HH(a,b,c,d, W[(3*i+5)%16],  4, tab[i]); i++;
    749 +		HH(d,a,b,c, W[(3*i+5)%16], 11, tab[i]); i++;
    750 +		HH(c,d,a,b, W[(3*i+5)%16], 16, tab[i]); i++;
    751 +		HH(b,c,d,a, W[(3*i+5)%16], 23, tab[i]); i++;
    752 +	}
    753 +	while (i < 64) {
    754 +		II(a,b,c,d, W[7*i%16],  6, tab[i]); i++;
    755 +		II(d,a,b,c, W[7*i%16], 10, tab[i]); i++;
    756 +		II(c,d,a,b, W[7*i%16], 15, tab[i]); i++;
    757 +		II(b,c,d,a, W[7*i%16], 21, tab[i]); i++;
    758 +	}
    759 +
    760 +	s->h[0] += a;
    761 +	s->h[1] += b;
    762 +	s->h[2] += c;
    763 +	s->h[3] += d;
    764 +}
    765 +
    766 +static void
    767 +pad(struct md5 *s)
    768 +{
    769 +	unsigned r = s->len % 64;
    770 +
    771 +	s->buf[r++] = 0x80;
    772 +	if (r > 56) {
    773 +		memset(s->buf + r, 0, 64 - r);
    774 +		r = 0;
    775 +		processblock(s, s->buf);
    776 +	}
    777 +	memset(s->buf + r, 0, 56 - r);
    778 +	s->len *= 8;
    779 +	s->buf[56] = s->len;
    780 +	s->buf[57] = s->len >> 8;
    781 +	s->buf[58] = s->len >> 16;
    782 +	s->buf[59] = s->len >> 24;
    783 +	s->buf[60] = s->len >> 32;
    784 +	s->buf[61] = s->len >> 40;
    785 +	s->buf[62] = s->len >> 48;
    786 +	s->buf[63] = s->len >> 56;
    787 +	processblock(s, s->buf);
    788 +}
    789 +
    790 +void
    791 +md5_init(void *ctx)
    792 +{
    793 +	struct md5 *s = ctx;
    794 +	s->len = 0;
    795 +	s->h[0] = 0x67452301;
    796 +	s->h[1] = 0xefcdab89;
    797 +	s->h[2] = 0x98badcfe;
    798 +	s->h[3] = 0x10325476;
    799 +}
    800 +
    801 +void
    802 +md5_sum(void *ctx, uint8_t md[MD5_DIGEST_LENGTH])
    803 +{
    804 +	struct md5 *s = ctx;
    805 +	int i;
    806 +
    807 +	pad(s);
    808 +	for (i = 0; i < 4; i++) {
    809 +		md[4*i] = s->h[i];
    810 +		md[4*i+1] = s->h[i] >> 8;
    811 +		md[4*i+2] = s->h[i] >> 16;
    812 +		md[4*i+3] = s->h[i] >> 24;
    813 +	}
    814 +}
    815 +
    816 +void
    817 +md5_update(void *ctx, const void *m, unsigned long len)
    818 +{
    819 +	struct md5 *s = ctx;
    820 +	const uint8_t *p = m;
    821 +	unsigned r = s->len % 64;
    822 +
    823 +	s->len += len;
    824 +	if (r) {
    825 +		if (len < 64 - r) {
    826 +			memcpy(s->buf + r, p, len);
    827 +			return;
    828 +		}
    829 +		memcpy(s->buf + r, p, 64 - r);
    830 +		len -= 64 - r;
    831 +		p += 64 - r;
    832 +		processblock(s, s->buf);
    833 +	}
    834 +	for (; len >= 64; len -= 64, p += 64)
    835 +		processblock(s, p);
    836 +	memcpy(s->buf, p, len);
    837 +}
    838 diff --git a/md5.h b/md5.h
    839 new file mode 100644
    840 index 0000000..0b5005e
    841 --- /dev/null
    842 +++ b/md5.h
    843 @@ -0,0 +1,18 @@
    844 +/* public domain md5 implementation based on rfc1321 and libtomcrypt */
    845 +
    846 +struct md5 {
    847 +	uint64_t len;    /* processed message length */
    848 +	uint32_t h[4];   /* hash state */
    849 +	uint8_t buf[64]; /* message block buffer */
    850 +};
    851 +
    852 +enum { MD5_DIGEST_LENGTH = 16 };
    853 +
    854 +/* reset state */
    855 +void md5_init(void *ctx);
    856 +/* process message */
    857 +void md5_update(void *ctx, const void *m, unsigned long len);
    858 +/* get message digest */
    859 +/* state is ruined after sum, keep a copy if multiple sum is needed */
    860 +/* part of the message might be left in s, zero it if secrecy is needed */
    861 +void md5_sum(void *ctx, uint8_t md[MD5_DIGEST_LENGTH]);
    862 diff --git a/quark.1 b/quark.1
    863 index d752cc7..2e79661 100644
    864 --- a/quark.1
    865 +++ b/quark.1
    866 @@ -17,6 +17,8 @@
    867  .Op Fl i Ar file
    868  .Oo Fl v Ar vhost Oc ...
    869  .Oo Fl m Ar map Oc ...
    870 +.Oo Fl r Ar realm Oc ...
    871 +.Oo Fl a Ar account Oc ...
    872  .Nm
    873  .Fl U Ar file
    874  .Op Fl p Ar port
    875 @@ -29,6 +31,8 @@
    876  .Op Fl i Ar file
    877  .Oo Fl v Ar vhost Oc ...
    878  .Oo Fl m Ar map Oc ...
    879 +.Oo Fl r Ar realm Oc ...
    880 +.Oo Fl a Ar account Oc ...
    881  .Sh DESCRIPTION
    882  .Nm
    883  is a simple HTTP GET/HEAD-only web server for static content.
    884 @@ -38,11 +42,26 @@ explicit redirects (see
    885  .Fl m ) ,
    886  directory listings (see
    887  .Fl l ) ,
    888 +Digest authentication (RFC 7616, see
    889 +.Fl r
    890 +and
    891 +.Fl a ) ,
    892  conditional "If-Modified-Since"-requests (RFC 7232), range requests
    893  (RFC 7233) and well-known URIs (RFC 8615), while refusing to serve
    894  hidden files and directories.
    895  .Sh OPTIONS
    896  .Bl -tag -width Ds
    897 +.It Fl a Ar account
    898 +Add the account specified by
    899 +.Ar account ,
    900 +which has the form
    901 +.Qq Pa realm username crypt ,
    902 +where each element is separated with spaces (0x20) that can be
    903 +escaped with '\\'. The
    904 +.Pa crypt
    905 +parameter can be generated as follows:
    906 +.Pp
    907 +echo -n 'username:realm:password' | md5sum | awk '{ print $1 }'
    908  .It Fl d Ar dir
    909  Serve
    910  .Ar dir
    911 @@ -90,6 +109,13 @@ In socket mode, use
    912  .Ar port
    913  for constructing proper virtual host
    914  redirects on non-standard ports.
    915 +.It Fl r Ar realm
    916 +Add mapping from group to realm as specified by
    917 +.Ar realm ,
    918 +which has the form
    919 +.Qq Pa group name ,
    920 +where each element is separated with spaces (0x20) that can be
    921 +escaped with '\\'.
    922  .It Fl U Ar file
    923  Create the UNIX-domain socket
    924  .Ar file ,
    925 diff --git a/util.h b/util.h
    926 index 983abd2..0307a34 100644
    927 --- a/util.h
    928 +++ b/util.h
    929 @@ -23,6 +23,18 @@ struct map {
    930  	char *to;
    931  };
    932  
    933 +struct account {
    934 +	char *username;
    935 +	char *crypt;
    936 +};
    937 +
    938 +struct realm {
    939 +	gid_t gid;
    940 +	char *name;
    941 +	struct account *account;
    942 +	size_t account_len;
    943 +};
    944 +
    945  struct server {
    946  	char *host;
    947  	char *port;
    948 @@ -32,6 +44,8 @@ struct server {
    949  	size_t vhost_len;
    950  	struct map *map;
    951  	size_t map_len;
    952 +	struct realm *realm;
    953 +	size_t realm_len;
    954  };
    955  
    956  /* general purpose buffer */
    957 -- 
    958 2.29.2
    959