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