sites

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

quark-digestauth-20200916-5d0221d.diff (25577B)


      1 From e0efcece3647fad31ca2750aaf59dd39dd192496 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: Thu, 29 Oct 2020 10:05:27 +0000
      5 Subject: [PATCH] Add Digest auth support
      6 
      7 This follows RFC 7616, but only MD5 algorithm and auth qop is supported.
      8 ---
      9  Makefile     |   3 +-
     10  config.def.h |   2 +-
     11  http.c       | 291 +++++++++++++++++++++++++++++++++++++++++++++++++--
     12  http.h       |  28 ++++-
     13  main.c       |  77 ++++++++++++--
     14  md5.c        | 148 ++++++++++++++++++++++++++
     15  md5.h        |  18 ++++
     16  quark.1      |  26 +++++
     17  util.h       |  14 +++
     18  9 files changed, 584 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 548e6aa..6c9e442 100644
     24 --- a/Makefile
     25 +++ b/Makefile
     26 @@ -4,13 +4,14 @@
     27  
     28  include config.mk
     29  
     30 -COMPONENTS = data http sock util
     31 +COMPONENTS = data http md5 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 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 f1e15a4..1862dc4 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 @@ -527,21 +533,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 @@ -787,14 +969,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 @@ -832,17 +1063,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 @@ -861,4 +1097,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 d64774b..b45ad15 100644
    530 --- a/main.c
    531 +++ b/main.c
    532 @@ -60,11 +60,17 @@ serve(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 @@ -72,7 +78,7 @@ serve(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 @@ -82,16 +88,16 @@ serve(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 @@ -146,6 +152,20 @@ response:
    582  err:
    583  	logmsg(c);
    584  
    585 +	/* don't cleanup if we keep the connection alive */
    586 +	if (c->res.keep_alive) {
    587 +		/*
    588 +		 * if the length is unspecified, a keep-alive connection will
    589 +		 * wait timeout: kill the connection to avoid it
    590 +		 */
    591 +		if (c->res.field[RES_CONTENT_LENGTH][0] == '\0') {
    592 +			c->res.status = S_INTERNAL_SERVER_ERROR;
    593 +		} else {
    594 +			c->state = C_START;
    595 +			return;
    596 +		}
    597 +	}
    598 +
    599  	/* clean up and finish */
    600  	shutdown(c->fd, SHUT_RD);
    601  	shutdown(c->fd, SHUT_WR);
    602 @@ -257,7 +277,8 @@ static void
    603  usage(void)
    604  {
    605  	const char *opts = "[-u user] [-g group] [-n num] [-d dir] [-l] "
    606 -	                   "[-i file] [-v vhost] ... [-m map] ...";
    607 +	                   "[-i file] [-v vhost] ... [-m map] ... "
    608 +			   "[-r realm] ... [-a account] ...";
    609  
    610  	die("usage: %s -p port [-h host] %s\n"
    611  	    "       %s -U file [-p port] %s", argv0,
    612 @@ -273,6 +294,7 @@ main(int argc, char *argv[])
    613  	struct server srv = {
    614  		.docindex = "index.html",
    615  	};
    616 +	struct realm *realm;
    617  	size_t i;
    618  	int insock, status = 0;
    619  	const char *err;
    620 @@ -285,6 +307,29 @@ main(int argc, char *argv[])
    621  	char *group = "nogroup";
    622  
    623  	ARGBEGIN {
    624 +	case 'a':
    625 +		if (spacetok(EARGF(usage()), tok, 3) || !tok[0] || !tok[1] ||
    626 +		    !tok[2]) {
    627 +			usage();
    628 +		}
    629 +		realm = NULL;
    630 +		for (i = 0; i < srv.realm_len; i++) {
    631 +			if (!strcmp(srv.realm[i].name, tok[0])) {
    632 +				realm = &(srv.realm[i]);
    633 +				break;
    634 +			}
    635 +		}
    636 +		if (!realm) {
    637 +			die("Realm '%s' not found", tok[0]);
    638 +		}
    639 +		if (!(realm->account = reallocarray(realm->account,
    640 +		                                 ++realm->account_len,
    641 +		                                 sizeof(struct account)))) {
    642 +				die("reallocarray:");
    643 +		}
    644 +		realm->account[realm->account_len - 1].username = tok[1];
    645 +		realm->account[realm->account_len - 1].crypt    = tok[2];
    646 +		break;
    647  	case 'd':
    648  		servedir = EARGF(usage());
    649  		break;
    650 @@ -324,6 +369,24 @@ main(int argc, char *argv[])
    651  	case 'p':
    652  		srv.port = EARGF(usage());
    653  		break;
    654 +	case 'r':
    655 +		if (spacetok(EARGF(usage()), tok, 2) || !tok[0] || !tok[1]) {
    656 +			usage();
    657 +		}
    658 +		errno = 0;
    659 +		if (!(grp = getgrnam(tok[0]))) {
    660 +			die("getgrnam '%s': %s", tok[0] ? tok[0] : "null",
    661 +			    errno ? strerror(errno) : "Entry not found");
    662 +		}
    663 +		if (!(srv.realm = reallocarray(srv.realm, ++srv.realm_len,
    664 +		                           sizeof(struct realm)))) {
    665 +			die("reallocarray:");
    666 +		}
    667 +		srv.realm[srv.realm_len - 1].gid         = grp->gr_gid;
    668 +		srv.realm[srv.realm_len - 1].name        = tok[1];
    669 +		srv.realm[srv.realm_len - 1].account     = NULL;
    670 +		srv.realm[srv.realm_len - 1].account_len = 0;
    671 +		break;
    672  	case 'U':
    673  		udsname = EARGF(usage());
    674  		break;
    675 diff --git a/md5.c b/md5.c
    676 new file mode 100644
    677 index 0000000..f56a501
    678 --- /dev/null
    679 +++ b/md5.c
    680 @@ -0,0 +1,148 @@
    681 +/* public domain md5 implementation based on rfc1321 and libtomcrypt */
    682 +#include <stdint.h>
    683 +#include <string.h>
    684 +
    685 +#include "md5.h"
    686 +
    687 +static uint32_t rol(uint32_t n, int k) { return (n << k) | (n >> (32-k)); }
    688 +#define F(x,y,z) (z ^ (x & (y ^ z)))
    689 +#define G(x,y,z) (y ^ (z & (y ^ x)))
    690 +#define H(x,y,z) (x ^ y ^ z)
    691 +#define I(x,y,z) (y ^ (x | ~z))
    692 +#define FF(a,b,c,d,w,s,t) a += F(b,c,d) + w + t; a = rol(a,s) + b
    693 +#define GG(a,b,c,d,w,s,t) a += G(b,c,d) + w + t; a = rol(a,s) + b
    694 +#define HH(a,b,c,d,w,s,t) a += H(b,c,d) + w + t; a = rol(a,s) + b
    695 +#define II(a,b,c,d,w,s,t) a += I(b,c,d) + w + t; a = rol(a,s) + b
    696 +
    697 +static const uint32_t tab[64] = {
    698 +	0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
    699 +	0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
    700 +	0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
    701 +	0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
    702 +	0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
    703 +	0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
    704 +	0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
    705 +	0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391
    706 +};
    707 +
    708 +static void
    709 +processblock(struct md5 *s, const uint8_t *buf)
    710 +{
    711 +	uint32_t i, W[16], a, b, c, d;
    712 +
    713 +	for (i = 0; i < 16; i++) {
    714 +		W[i] = buf[4*i];
    715 +		W[i] |= (uint32_t)buf[4*i+1]<<8;
    716 +		W[i] |= (uint32_t)buf[4*i+2]<<16;
    717 +		W[i] |= (uint32_t)buf[4*i+3]<<24;
    718 +	}
    719 +
    720 +	a = s->h[0];
    721 +	b = s->h[1];
    722 +	c = s->h[2];
    723 +	d = s->h[3];
    724 +
    725 +	i = 0;
    726 +	while (i < 16) {
    727 +		FF(a,b,c,d, W[i],  7, tab[i]); i++;
    728 +		FF(d,a,b,c, W[i], 12, tab[i]); i++;
    729 +		FF(c,d,a,b, W[i], 17, tab[i]); i++;
    730 +		FF(b,c,d,a, W[i], 22, tab[i]); i++;
    731 +	}
    732 +	while (i < 32) {
    733 +		GG(a,b,c,d, W[(5*i+1)%16],  5, tab[i]); i++;
    734 +		GG(d,a,b,c, W[(5*i+1)%16],  9, tab[i]); i++;
    735 +		GG(c,d,a,b, W[(5*i+1)%16], 14, tab[i]); i++;
    736 +		GG(b,c,d,a, W[(5*i+1)%16], 20, tab[i]); i++;
    737 +	}
    738 +	while (i < 48) {
    739 +		HH(a,b,c,d, W[(3*i+5)%16],  4, tab[i]); i++;
    740 +		HH(d,a,b,c, W[(3*i+5)%16], 11, tab[i]); i++;
    741 +		HH(c,d,a,b, W[(3*i+5)%16], 16, tab[i]); i++;
    742 +		HH(b,c,d,a, W[(3*i+5)%16], 23, tab[i]); i++;
    743 +	}
    744 +	while (i < 64) {
    745 +		II(a,b,c,d, W[7*i%16],  6, tab[i]); i++;
    746 +		II(d,a,b,c, W[7*i%16], 10, tab[i]); i++;
    747 +		II(c,d,a,b, W[7*i%16], 15, tab[i]); i++;
    748 +		II(b,c,d,a, W[7*i%16], 21, tab[i]); i++;
    749 +	}
    750 +
    751 +	s->h[0] += a;
    752 +	s->h[1] += b;
    753 +	s->h[2] += c;
    754 +	s->h[3] += d;
    755 +}
    756 +
    757 +static void
    758 +pad(struct md5 *s)
    759 +{
    760 +	unsigned r = s->len % 64;
    761 +
    762 +	s->buf[r++] = 0x80;
    763 +	if (r > 56) {
    764 +		memset(s->buf + r, 0, 64 - r);
    765 +		r = 0;
    766 +		processblock(s, s->buf);
    767 +	}
    768 +	memset(s->buf + r, 0, 56 - r);
    769 +	s->len *= 8;
    770 +	s->buf[56] = s->len;
    771 +	s->buf[57] = s->len >> 8;
    772 +	s->buf[58] = s->len >> 16;
    773 +	s->buf[59] = s->len >> 24;
    774 +	s->buf[60] = s->len >> 32;
    775 +	s->buf[61] = s->len >> 40;
    776 +	s->buf[62] = s->len >> 48;
    777 +	s->buf[63] = s->len >> 56;
    778 +	processblock(s, s->buf);
    779 +}
    780 +
    781 +void
    782 +md5_init(void *ctx)
    783 +{
    784 +	struct md5 *s = ctx;
    785 +	s->len = 0;
    786 +	s->h[0] = 0x67452301;
    787 +	s->h[1] = 0xefcdab89;
    788 +	s->h[2] = 0x98badcfe;
    789 +	s->h[3] = 0x10325476;
    790 +}
    791 +
    792 +void
    793 +md5_sum(void *ctx, uint8_t md[MD5_DIGEST_LENGTH])
    794 +{
    795 +	struct md5 *s = ctx;
    796 +	int i;
    797 +
    798 +	pad(s);
    799 +	for (i = 0; i < 4; i++) {
    800 +		md[4*i] = s->h[i];
    801 +		md[4*i+1] = s->h[i] >> 8;
    802 +		md[4*i+2] = s->h[i] >> 16;
    803 +		md[4*i+3] = s->h[i] >> 24;
    804 +	}
    805 +}
    806 +
    807 +void
    808 +md5_update(void *ctx, const void *m, unsigned long len)
    809 +{
    810 +	struct md5 *s = ctx;
    811 +	const uint8_t *p = m;
    812 +	unsigned r = s->len % 64;
    813 +
    814 +	s->len += len;
    815 +	if (r) {
    816 +		if (len < 64 - r) {
    817 +			memcpy(s->buf + r, p, len);
    818 +			return;
    819 +		}
    820 +		memcpy(s->buf + r, p, 64 - r);
    821 +		len -= 64 - r;
    822 +		p += 64 - r;
    823 +		processblock(s, s->buf);
    824 +	}
    825 +	for (; len >= 64; len -= 64, p += 64)
    826 +		processblock(s, p);
    827 +	memcpy(s->buf, p, len);
    828 +}
    829 diff --git a/md5.h b/md5.h
    830 new file mode 100644
    831 index 0000000..0b5005e
    832 --- /dev/null
    833 +++ b/md5.h
    834 @@ -0,0 +1,18 @@
    835 +/* public domain md5 implementation based on rfc1321 and libtomcrypt */
    836 +
    837 +struct md5 {
    838 +	uint64_t len;    /* processed message length */
    839 +	uint32_t h[4];   /* hash state */
    840 +	uint8_t buf[64]; /* message block buffer */
    841 +};
    842 +
    843 +enum { MD5_DIGEST_LENGTH = 16 };
    844 +
    845 +/* reset state */
    846 +void md5_init(void *ctx);
    847 +/* process message */
    848 +void md5_update(void *ctx, const void *m, unsigned long len);
    849 +/* get message digest */
    850 +/* state is ruined after sum, keep a copy if multiple sum is needed */
    851 +/* part of the message might be left in s, zero it if secrecy is needed */
    852 +void md5_sum(void *ctx, uint8_t md[MD5_DIGEST_LENGTH]);
    853 diff --git a/quark.1 b/quark.1
    854 index 6e0e5f8..3394639 100644
    855 --- a/quark.1
    856 +++ b/quark.1
    857 @@ -16,6 +16,8 @@
    858  .Op Fl i Ar file
    859  .Oo Fl v Ar vhost Oc ...
    860  .Oo Fl m Ar map Oc ...
    861 +.Oo Fl r Ar realm Oc ...
    862 +.Oo Fl a Ar account Oc ...
    863  .Nm
    864  .Fl U Ar file
    865  .Op Fl p Ar port
    866 @@ -27,6 +29,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  .Sh DESCRIPTION
    873  .Nm
    874  is a simple HTTP GET/HEAD-only web server for static content.
    875 @@ -36,11 +40,26 @@ explicit redirects (see
    876  .Fl m ) ,
    877  directory listings (see
    878  .Fl l ) ,
    879 +Digest authentication (RFC 7616, see
    880 +.Fl r
    881 +and
    882 +.Fl a ) ,
    883  conditional "If-Modified-Since"-requests (RFC 7232), range requests
    884  (RFC 7233) and well-known URIs (RFC 8615), while refusing to serve
    885  hidden files and directories.
    886  .Sh OPTIONS
    887  .Bl -tag -width Ds
    888 +.It Fl a Ar account
    889 +Add the account specified by
    890 +.Ar account ,
    891 +which has the form
    892 +.Qq Pa realm username crypt ,
    893 +where each element is separated with spaces (0x20) that can be
    894 +escaped with '\\'. The
    895 +.Pa crypt
    896 +parameter can be generated as follows:
    897 +.Pp
    898 +echo -n 'username:realm:password' | md5sum | awk '{ print $1 }'
    899  .It Fl d Ar dir
    900  Serve
    901  .Ar dir
    902 @@ -92,6 +111,13 @@ In socket mode, use
    903  .Ar port
    904  for constructing proper virtual host
    905  redirects on non-standard ports.
    906 +.It Fl r Ar realm
    907 +Add mapping from group to realm as specified by
    908 +.Ar realm ,
    909 +which has the form
    910 +.Qq Pa group name ,
    911 +where each element is separated with spaces (0x20) that can be
    912 +escaped with '\\'.
    913  .It Fl U Ar file
    914  Create the UNIX-domain socket
    915  .Ar file ,
    916 diff --git a/util.h b/util.h
    917 index 983abd2..0307a34 100644
    918 --- a/util.h
    919 +++ b/util.h
    920 @@ -23,6 +23,18 @@ struct map {
    921  	char *to;
    922  };
    923  
    924 +struct account {
    925 +	char *username;
    926 +	char *crypt;
    927 +};
    928 +
    929 +struct realm {
    930 +	gid_t gid;
    931 +	char *name;
    932 +	struct account *account;
    933 +	size_t account_len;
    934 +};
    935 +
    936  struct server {
    937  	char *host;
    938  	char *port;
    939 @@ -32,6 +44,8 @@ struct server {
    940  	size_t vhost_len;
    941  	struct map *map;
    942  	size_t map_len;
    943 +	struct realm *realm;
    944 +	size_t realm_len;
    945  };
    946  
    947  /* general purpose buffer */
    948 -- 
    949 2.29.0
    950