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