quark-digestauth-20201101-dff98c0.diff - sites - public wiki contents of suckless.org
(HTM) git clone git://git.suckless.org/sites
(DIR) Log
(DIR) Files
(DIR) 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