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