From: Stefan Sperling Subject: gotwebd authentication To: gameoftrees@openbsd.org Date: Wed, 17 Sep 2025 13:22:07 +0200 This patch adds user authentication to gotwebd. See the manual page changes included in the diff for details of how it works. Joint work with op@ over several weeks. This diff is quite large. I have broken it down to a few commits locally: 2025-09-17 main use got_path_basename() instead of hand-rolled pointer offset hack 2025-09-17 3bd38b7 add a 'weblogin' command to gotsh for future use with gotwebd authentication 2025-09-17 b8b41bf add a login process to gotwebd for future use with authentication 2025-09-17 fdd7730 add gotwebd authentication config settings 2025-09-17 4892aca add an auth process to gotwebd which handles web request authentication 2025-09-17 42c8b7a check access rules in gotweb.c 2025-09-17 f91b4d7 implement initial gotwebd authentication regression tests 2025-09-17 gotwebd-auth document gotwebd authentication ok? M gotsh/Makefile | 2+ 1- M gotsh/gotsh.1 | 59+ 1- M gotsh/gotsh.c | 204+ 27- M gotwebd/Makefile | 3+ 3- A gotwebd/auth.c | 976+ 0- M gotwebd/config.c | 95+ 0- M gotwebd/fcgi.c | 55+ 1- M gotwebd/gotweb.c | 150+ 13- M gotwebd/gotwebd.c | 302+ 3- M gotwebd/gotwebd.conf.5 | 179+ 1- M gotwebd/gotwebd.h | 93+ 0- A gotwebd/login.c | 796+ 0- M gotwebd/pages.tmpl | 4+ 0- M gotwebd/parse.y | 288+ 9- M gotwebd/sockets.c | 51+ 29- M include/got_error.h | 2+ 0- M lib/error.c | 2+ 0- M regress/gotsysd/Makefile | 45+ 2- M regress/gotsysd/README | 5+ 0- M regress/gotsysd/test_gotsysd.sh | 2+ 0- A regress/gotsysd/test_gotwebd.sh | 412+ 0- 21 files changed, 3725 insertions(+), 90 deletions(-) commit - 12c1bbcab3809ec364d34a8280dfb318a2968da6 commit + 738000984be1ba0138090c935cdd5fda3cd1123e blob - 9fb50df7e49e232c3ac7791a729a4e6beefe2509 blob + 3d8275dfeef285fef7ab58e4e4300a6ef6c9887e --- gotsh/Makefile +++ gotsh/Makefile @@ -9,7 +9,8 @@ SRCS= gotsh.c error.c pkt.c hash.c serve.c path.c git MAN = ${PROG}.1 -CPPFLAGS = -I${.CURDIR}/../include -I${.CURDIR}/../lib -I${.CURDIR}/../gotd +CPPFLAGS = -I${.CURDIR}/../include -I${.CURDIR}/../lib -I${.CURDIR}/../gotd \ + -I${.CURDIR}/../gotwebd .if defined(PROFILE) LDADD = -lutil_p -lc_p -levent_p blob - 6388edcc1cf538c87de0145789b59d6972dec668 blob + 988e06af44aa3e0a9bb6f100ca2d575ea66e5b66 --- gotsh/gotsh.1 +++ gotsh/gotsh.1 @@ -22,6 +22,7 @@ .Sh SYNOPSIS .Nm Fl c Sq Cm git-receive-pack Ar repository-path .Nm Fl c Sq Cm git-upload-pack Ar repository-path +.Nm Fl c Sq Cm weblogin Oo Ar hostname Oc .Sh DESCRIPTION .Nm is the network-facing interface to @@ -75,6 +76,27 @@ accessing the unix socket of via .Nm . .Pp +The +.Cm weblogin +command provides user authentication for +.Xr gotwebd 8 . +.Nm +will connect to +.Xr gotwebd 8 +and obtain a login URL which allows browsing private repositories the user +has been granted read access to in +.Xr gotwebd.conf 5 . +If multiple servers are declared in +.Xr gotwebd.conf 5 +the +.Ar hostname +parameter is required and indicates the desired virtual host to use in the URL. +If no +.Ar hostname +is specified and only one server is declared in +.Xr gotwebd.conf 5 +then the name of this server will be used in the URL. +.Pp It is recommended to restrict .Xr ssh 1 features available to users of @@ -139,12 +161,48 @@ Match User anonymous DisableForwarding yes PermitTTY no .Ed +.Pp +Obtain a +.Xr gotwebd 8 +login URL for got.example.com: +.Bd -literal -offset indent +$ ssh got.example.com weblogin +.Ed +.Pp +If the web server at got.example.com serves virtual hosts then two +hostnames must be provided. +One for +.Xr ssh 1 +to connect to, and another to identify the virtual host served by +.Xr gotwebd 8 : +.Bd -literal -offset indent +$ ssh got.example.com weblogin got.example.com +.Ed +.Pp +In practice both hostnames will often be the same, but this is not guaranteed. +There is no reliable way determine the desired virtual host automatically. +An +.Xr ssh_config 5 +entry like the following can save some typing: +.Bd -literal -offset indent +Host weblogin + Hostname got.example.com + RemoteCommand weblogin %h +.Ed +.Pp +The following command is now equivalent to the above: +.Bd -literal -offset indent +$ ssh weblogin +.Ed +.Ed .Sh SEE ALSO .Xr gitwrapper 1 , .Xr got 1 , .Xr ssh 1 , .Xr gotd.conf 5 , +.Xr gotwebd.conf 5 , .Xr sshd_config 5 , -.Xr gotd 8 +.Xr gotd 8 , +.Xr gotwebd 8 .Sh AUTHORS .An Stefan Sperling Aq Mt stsp@openbsd.org blob - 13623bc34739f85f1e1611827992c048d0747d09 blob + 68ce43775446512b98a6449313b3da033cbd561c --- gotsh/gotsh.c +++ gotsh/gotsh.c @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -34,10 +35,13 @@ #include "got_object.h" #include "got_serve.h" #include "got_path.h" +#include "got_reference.h" #include "got_lib_dial.h" +#include "got_lib_poll.h" #include "gotd.h" +#include "gotwebd.h" static int chattygot; @@ -46,6 +50,7 @@ usage(void) { fprintf(stderr, "usage: %s -c '%s|%s repository-path'\n", getprogname(), GOT_DIAL_CMD_SEND, GOT_DIAL_CMD_FETCH); + fprintf(stderr, " %s -c 'weblogin [hostname]'\n", getprogname()); exit(1); } @@ -65,46 +70,209 @@ apply_unveil(const char *unix_socket_path) return NULL; } +/* Read session URL from gotwebd's auth socket and send it to the client. */ +static const struct got_error * +weblogin(FILE *out, int sock, const char *hostname) +{ + const struct got_error *err = NULL; + FILE *fp; + int ret; + char *line = NULL; + size_t linesize; + ssize_t linelen; + + fp = fdopen(sock, "w+"); + if (fp == NULL) + return got_error_from_errno("fdopen"); + + ret = fprintf(fp, "login%s%s\n", hostname != NULL ? " " : "", + hostname != NULL ? hostname : ""); + if (ret < 0) { + err = got_error_from_errno("fprintf"); + goto done; + } + + /* + * gotwebd will return "ok URL", likewise terminated by \n, + * or might return an arbitrary error message + \n. + * We don't know how long this line will be, so keep reading + * in chunks until we have read all of it. + * For forward compatibilty, ignore any trailing lines received. + */ + linelen = getline(&line, &linesize, fp); + if (linelen == -1) { + err = got_error(GOT_ERR_EOF); + if (ferror(fp)) + err = got_error_from_errno("getline"); + goto done; + } + + if (strncmp(line, "ok ", 3) == 0) { + fprintf(out, "Login successful. Please visit the following " + "URL within the next %d minutes: %s\n", + GOTWEBD_LOGIN_TIMEOUT / 60, line + 3); + goto done; + } + + if (strncmp(line, "err ", 4) == 0) { + err = got_error_fmt(GOT_ERR_LOGIN_FAILED, "%s", line + 4); + goto done; + } + + err = got_error(GOT_ERR_UNKNOWN_COMMAND); +done: + if (line != NULL) + free(line); + if (fp != NULL && fclose(fp) == EOF && err == NULL) + err = got_error_from_errno("fclose"); + return err; +} + + +static const struct got_error * +parse_weblogin_command(char **hostname, char *cmd) +{ + size_t len, cmdlen; + + *hostname = NULL; + + len = strlen(cmd); + + while (len > 0 && isspace(cmd[len - 1])) + cmd[--len] = '\0'; + + if (len == 0) + return got_error(GOT_ERR_BAD_PACKET); + + if (len >= strlen(GOTWEBD_LOGIN_CMD) && + strncmp(cmd, GOTWEBD_LOGIN_CMD, strlen(GOTWEBD_LOGIN_CMD)) == 0) + cmdlen = strlen(GOTWEBD_LOGIN_CMD); + else + return got_error(GOT_ERR_BAD_PACKET); + + /* The hostname parameter is optional. */ + if (len == cmdlen) + return NULL; + + if (len <= cmdlen + 1 || cmd[cmdlen] != ' ') + return got_error(GOT_ERR_BAD_PACKET); + + if (memchr(&cmd[cmdlen + 1], '\0', len - cmdlen) == NULL) + return got_error(GOT_ERR_BAD_PACKET); + + /* Forbid linefeeds in hostnames. We use \n as internal terminator. */ + if (memchr(&cmd[cmdlen + 1], '\n', len - cmdlen) != NULL) + return got_error(GOT_ERR_BAD_PACKET); + + *hostname = strdup(&cmd[cmdlen + 1]); + if (*hostname == NULL) + return got_error_from_errno("strdup"); + + /* Deny an empty hostname. */ + if ((*hostname)[0] == '\0') { + free(*hostname); + *hostname = NULL; + return got_error(GOT_ERR_BAD_PACKET); + } + + /* Deny overlong hostnames ,*/ + if (len - cmdlen > _POSIX_HOST_NAME_MAX) + return got_error_fmt(GOT_ERR_NO_SPACE, + "hostname length exceeds %d bytes", _POSIX_HOST_NAME_MAX); + + /* + * TODO: More hostname verification? In any case, the provided + * value will have to match a string obtained from gotwebd.conf. + */ + + return NULL; +} + int main(int argc, char *argv[]) { const struct got_error *error; const char *unix_socket_path; - int gotd_sock = -1; + int sock = -1; struct sockaddr_un sun; char *gitcmd = NULL, *command = NULL, *repo_path = NULL; + char *hostname = NULL; + int do_weblogin = 0; #ifndef PROFILE if (pledge("stdio recvfd unix unveil", NULL) == -1) err(1, "pledge"); #endif - - unix_socket_path = getenv("GOTD_UNIX_SOCKET"); - if (unix_socket_path == NULL) - unix_socket_path = GOTD_UNIX_SOCKET; - - error = apply_unveil(unix_socket_path); - if (error) - goto done; - - if (strcmp(argv[0], GOT_DIAL_CMD_SEND) == 0 || + if (strcmp(argv[0], GOTWEBD_LOGIN_CMD) == 0) { + if (argc != 1 && argc != 2) + usage(); + unix_socket_path = getenv("GOTWEBD_LOGIN_SOCKET"); + if (unix_socket_path == NULL) + unix_socket_path = GOTWEBD_LOGIN_SOCKET; + error = apply_unveil(unix_socket_path); + if (error) + goto done; + if (argc == 2) { + hostname = strdup(argv[1]); + if (hostname == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } + do_weblogin = 1; + } else if (strcmp(argv[0], GOT_DIAL_CMD_SEND) == 0 || strcmp(argv[0], GOT_DIAL_CMD_FETCH) == 0) { if (argc != 2) usage(); + unix_socket_path = getenv("GOTD_UNIX_SOCKET"); + if (unix_socket_path == NULL) + unix_socket_path = GOTD_UNIX_SOCKET; + error = apply_unveil(unix_socket_path); + if (error) + goto done; if (asprintf(&gitcmd, "%s %s", argv[0], argv[1]) == -1) err(1, "asprintf"); error = got_dial_parse_command(&command, &repo_path, gitcmd); - } else { - if (argc != 3 || strcmp(argv[1], "-c") != 0) - usage(); - error = got_dial_parse_command(&command, &repo_path, argv[2]); - } - if (error && error->code == GOT_ERR_BAD_PACKET) + if (error) { + if (error->code == GOT_ERR_BAD_PACKET) + usage(); + goto done; + } + } else if (argc == 3 && strcmp(argv[1], "-c") == 0) { + if (strncmp(argv[2], GOTWEBD_LOGIN_CMD, + strlen(GOTWEBD_LOGIN_CMD)) == 0) { + unix_socket_path = getenv("GOTWEBD_LOGIN_SOCKET"); + if (unix_socket_path == NULL) + unix_socket_path = GOTWEBD_LOGIN_SOCKET; + error = apply_unveil(unix_socket_path); + if (error) + goto done; + error = parse_weblogin_command(&hostname, argv[2]); + if (error) { + if (error->code == GOT_ERR_BAD_PACKET) + usage(); + goto done; + } + do_weblogin = 1; + } else { + unix_socket_path = getenv("GOTD_UNIX_SOCKET"); + if (unix_socket_path == NULL) + unix_socket_path = GOTD_UNIX_SOCKET; + error = apply_unveil(unix_socket_path); + if (error) + goto done; + error = got_dial_parse_command(&command, &repo_path, + argv[2]); + if (error) { + if (error->code == GOT_ERR_BAD_PACKET) + usage(); + goto done; + } + } + } else usage(); - if (error) - goto done; - if ((gotd_sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) + if ((sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) err(1, "socket"); memset(&sun, 0, sizeof(sun)); @@ -112,21 +280,30 @@ main(int argc, char *argv[]) if (strlcpy(sun.sun_path, unix_socket_path, sizeof(sun.sun_path)) >= sizeof(sun.sun_path)) errx(1, "gotd socket path too long"); - if (connect(gotd_sock, (struct sockaddr *)&sun, sizeof(sun)) == -1) + if (connect(sock, (struct sockaddr *)&sun, sizeof(sun)) == -1) err(1, "connect: %s", unix_socket_path); + if (do_weblogin) { #ifndef PROFILE - if (pledge("stdio recvfd", NULL) == -1) - err(1, "pledge"); + if (pledge("stdio", NULL) == -1) + err(1, "pledge"); #endif - error = got_serve(STDIN_FILENO, STDOUT_FILENO, command, repo_path, - gotd_sock, chattygot); + error = weblogin(stdout, sock, hostname); + } else { +#ifndef PROFILE + if (pledge("stdio recvfd", NULL) == -1) + err(1, "pledge"); +#endif + error = got_serve(STDIN_FILENO, STDOUT_FILENO, command, + repo_path, sock, chattygot); + } done: free(gitcmd); free(command); free(repo_path); - if (gotd_sock != -1) - close(gotd_sock); + free(hostname); + if (sock != -1) + close(sock); if (error) { fprintf(stderr, "%s: %s\n", getprogname(), error->msg); return 1; blob - 545a13d2e5aba6dc9df486f7300af0acf23c4df7 blob + cb1821eb9160b452f0b06c959c08118f05d37707 --- gotwebd/Makefile +++ gotwebd/Makefile @@ -5,7 +5,7 @@ .include "Makefile.inc" PROG = gotwebd -SRCS = config.c sockets.c gotwebd.c parse.y \ +SRCS = config.c sockets.c auth.c login.c gotwebd.c parse.y \ fcgi.c gotweb.c got_operations.c tmpl.c pages.c SRCS += blame.c commit_graph.c delta.c diff.c \ diffreg.c error.c object.c object_cache.c \ @@ -37,9 +37,9 @@ MAN = ${PROG}.conf.5 ${PROG}.8 CPPFLAGS += -I${.CURDIR}/../include -I${.CURDIR}/../lib -I${.CURDIR} CPPFLAGS += -I${.CURDIR}/../template -LDADD += -lz -levent -lutil -lm +LDADD += -lz -levent -lutil -lm -lcrypto YFLAGS = -DPADD = ${LIBEVENT} ${LIBUTIL} ${LIBM} +DPADD = ${LIBEVENT} ${LIBUTIL} ${LIBM} ${LIBCRYPTO} #CFLAGS += -DGOT_NO_OBJ_CACHE .if ${GOT_RELEASE} != "Yes" blob - 5b6c4f611de441b8dee08ed52fd642dff811ef24 blob + 372a732ea54522c6c2bbf51e0775443fcf1d03c6 --- gotwebd/config.c +++ gotwebd/config.c @@ -56,6 +56,7 @@ config_init(struct gotwebd *env) TAILQ_INIT(&env->servers); TAILQ_INIT(&env->sockets); TAILQ_INIT(&env->addresses); + STAILQ_INIT(&env->access_rules); for (i = 0; i < PRIV_FDS__MAX; i++) env->priv_fd[i] = -1; @@ -89,6 +90,8 @@ config_getserver(struct gotwebd *env, struct imsg *ims fatalx("%s: wrong size", __func__); memcpy(srv, p, sizeof(*srv)); + STAILQ_INIT(&srv->access_rules); + TAILQ_INIT(&srv->repos); /* log server info */ log_debug("%s: server=%s", __func__, srv->name); @@ -209,3 +212,95 @@ config_getfd(struct gotwebd *env, struct imsg *imsg) return 1; } + +void +config_set_access_rules(struct imsgev *iev, + struct gotwebd_access_rule_list *rules) +{ + struct gotwebd_access_rule *rule; + + STAILQ_FOREACH(rule, rules, entry) { + if (imsg_compose_event(iev, GOTWEBD_IMSG_CFG_ACCESS_RULE, + 0, -1, -1, rule, sizeof(*rule)) == -1) + fatal("imsg_compose_event " + "GOTWEBD_IMSG_CFG_ACCESS_RULE"); + } +} + +void +config_get_access_rule(struct gotwebd_access_rule_list *rules, + struct imsg *imsg) +{ + struct gotwebd_access_rule *rule; + size_t len; + + rule = calloc(1, sizeof(*rule)); + if (rule == NULL) + fatal("malloc"); + + if (imsg_get_data(imsg, rule, sizeof(*rule))) + fatalx("%s: invalid CFG_ACCESS_RULE message", __func__); + + switch (rule->access) { + case GOTWEBD_ACCESS_DENIED: + case GOTWEBD_ACCESS_PERMITTED: + break; + default: + fatalx("%s: invalid CFG_ACCESS_RULE message", __func__); + } + + len = strnlen(rule->identifier, sizeof(rule->identifier)); + if (len == 0 || len >= sizeof(rule->identifier)) + fatalx("%s: invalid CFG_ACCESS_RULE message", __func__); + + STAILQ_INSERT_TAIL(rules, rule, entry); +} + +void +config_set_repository(struct imsgev *iev, struct gotwebd_repo *repo) +{ + if (imsg_compose_event(iev, + GOTWEBD_IMSG_CFG_REPO, 0, -1, -1, repo, sizeof(*repo)) == -1) + fatal("imsg_compose_event GOTWEBD_IMSG_CFG_REPO"); +} + +void +config_get_repository(struct gotwebd_repolist *repos, struct imsg *imsg) +{ + struct gotwebd_repo *repo; + size_t len; + + repo = calloc(1, sizeof(*repo)); + if (repo == NULL) + fatal("malloc"); + + if (imsg_get_data(imsg, repo, sizeof(*repo))) + fatalx("%s: invalid CFG_REPO message", __func__); + + switch (repo->auth_config) { + case GOTWEBD_AUTH_DISABLED: + case GOTWEBD_AUTH_SECURE: + case GOTWEBD_AUTH_INSECURE: + break; + default: + fatalx("%s: invalid CFG_REPO message", __func__); + } + + len = strnlen(repo->name, sizeof(repo->name)); + if (len == 0 || len >= sizeof(repo->name)) + fatalx("%s: invalid CFG_REPO message", __func__); + + if (strchr(repo->name, '/') != NULL) { + fatalx("repository names must not contain slashes: %s", + repo->name); + } + + if (strchr(repo->name, '\n') != NULL) { + fatalx("repository names must not contain linefeeds: %s", + repo->name); + } + + STAILQ_INIT(&repo->access_rules); + + TAILQ_INSERT_TAIL(repos, repo, entry); +} blob - /dev/null blob + 2e1bbe106015e73ae9c967bbbcef5b5ac16cfb36 (mode 644) --- /dev/null +++ gotwebd/auth.c @@ -0,0 +1,976 @@ +/* + * Copyright (c) 2025 Stefan Sperling + * Copyright (c) 2025 Omar Polo + * Copyright (c) 2015 Ted Unangst + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "got_error.h" +#include "got_reference.h" +#include "got_object.h" + +#include "gotwebd.h" +#include "log.h" +#include "tmpl.h" + +static char login_token_secret[32]; +static char auth_token_secret[32]; + +static void +auth_shutdown(void) +{ + struct gotwebd *env = gotwebd_env; + + imsgbuf_clear(&env->iev_parent->ibuf); + imsgbuf_clear(&env->iev_sockets->ibuf); + imsgbuf_clear(&env->iev_gotweb->ibuf); + + free(env->iev_parent); + free(env->iev_sockets); + free(env->iev_gotweb); + free(env); + + exit(0); +} + +static void +auth_sighdlr(int sig, short event, void *arg) +{ + switch (sig) { + case SIGHUP: + log_info("%s: ignoring SIGHUP", __func__); + break; + case SIGPIPE: + log_info("%s: ignoring SIGPIPE", __func__); + break; + case SIGUSR1: + log_info("%s: ignoring SIGUSR1", __func__); + break; + case SIGCHLD: + break; + case SIGINT: + case SIGTERM: + auth_shutdown(); + break; + default: + log_warn("unexpected signal %d", sig); + break; + } +} + +static int +parseuid(const char *s, uid_t *uid) +{ + struct passwd *pw; + const char *errstr; + + if ((pw = getpwnam(s)) != NULL) { + *uid = pw->pw_uid; + if (*uid == UID_MAX) + return -1; + return 0; + } + *uid = strtonum(s, 0, UID_MAX - 1, &errstr); + if (errstr) + return -1; + return 0; +} + +static int +uidcheck(const char *s, uid_t desired) +{ + uid_t uid; + + if (parseuid(s, &uid) != 0) + return -1; + if (uid != desired) + return -1; + return 0; +} + +static int +parsegid(const char *s, gid_t *gid) +{ + struct group *gr; + const char *errstr; + + if ((gr = getgrnam(s)) != NULL) { + *gid = gr->gr_gid; + if (*gid == GID_MAX) + return -1; + return 0; + } + *gid = strtonum(s, 0, GID_MAX - 1, &errstr); + if (errstr) + return -1; + return 0; +} + +static int +match_identifier(const char *identifier, gid_t *groups, int ngroups, + uid_t euid, gid_t egid) +{ + int i; + + if (identifier[0] == ':') { + gid_t rgid; + if (parsegid(identifier + 1, &rgid) == -1) + return 0; + if (rgid == egid) + return 1; + for (i = 0; i < ngroups; i++) { + if (rgid == groups[i]) + break; + } + if (i == ngroups) + return 0; + } else if (uidcheck(identifier, euid) != 0) + return 0; + + return 1; +} + +static enum gotwebd_access +auth_check(const char **identifier, uid_t uid, + struct gotwebd_access_rule_list *rules) +{ + struct gotwebd_access_rule *rule; + enum gotwebd_access access = GOTWEBD_ACCESS_NO_MATCH; + struct passwd *pw; + gid_t groups[NGROUPS_MAX]; + int ngroups = NGROUPS_MAX; + gid_t gid; + + if (identifier) + *identifier = NULL; + + pw = getpwuid(uid); + if (pw == NULL) + return GOTWEBD_ACCESS_DENIED; + + gid = pw->pw_gid; + + if (getgrouplist(pw->pw_name, gid, groups, &ngroups) == -1) + log_warnx("group membership list truncated"); + + STAILQ_FOREACH(rule, rules, entry) { + if (!match_identifier(rule->identifier, groups, ngroups, + uid, gid)) + continue; + + access = rule->access; + if (identifier) + *identifier = rule->identifier; + } + + return access; +} + +static void +auth_launch(struct gotwebd *env) +{ + if (env->iev_sockets == NULL) + fatal("sockets process not connected"); + if (env->iev_gotweb == NULL) + fatal("gotweb process not connected"); + + event_add(&env->iev_sockets->ev, NULL); + event_add(&env->iev_gotweb->ev, NULL); +} + +static void +render_error(struct request *c, const struct got_error *error) +{ + int status; + + log_warnx("%s", error->msg); + + c->t->error = error; + + if (error->code == GOT_ERR_LOGIN_FAILED) + status = 401; + else + status = 400; + + if (gotweb_reply(c, status, "text/html", NULL) == -1) + return; + gotweb_render_page(c->tp, gotweb_render_error); +} + +static void +abort_request(uint32_t request_id) +{ + if (imsg_compose_event(gotwebd_env->iev_sockets, GOTWEBD_IMSG_REQ_ABORT, + GOTWEBD_PROC_GOTWEB, -1, -1, &request_id, sizeof(request_id)) == -1) + log_warn("imsg_compose_event"); +} + +static void +forward_request(struct request *c) +{ + struct gotwebd *env = gotwebd_env; + const struct got_error *error; + int ret; + + ret = imsg_compose_event(env->iev_gotweb, GOTWEBD_IMSG_REQ_PROCESS, + GOTWEBD_PROC_AUTH, -1, c->fd, c, sizeof(*c)); + if (ret == -1) { + error = got_error_set_errno(ret, "could not forward request " + "to gotweb process"); + render_error(c, error); + return; + } + + c->fd = -1; +} + +static void +do_login(struct request *c) +{ + const struct got_error *error = NULL; + struct gotwebd *env = gotwebd_env; + uid_t uid; + struct server *srv; + char *hostname = NULL; + char *token = NULL; + const char *identifier = NULL; + const time_t validity = 24 * 60 * 60; /* 1 day */ + struct gotweb_url url; + struct gotwebd_repo *repo; + + int r; + + if (login_check_token(&uid, &hostname, c->fcgi_params.qs.login, + login_token_secret, sizeof(login_token_secret), "login") == -1) { + error = got_error(GOT_ERR_LOGIN_FAILED); + goto err; + } + + /* + * The www user ID represents the case where no authentication + * occurred. This user must not be allowed to log in. + */ + if (uid == env->www_uid) { + error = got_error(GOT_ERR_LOGIN_FAILED); + goto err; + } + + c->client_uid = uid; + if (strcmp(hostname, c->fcgi_params.server_name) != 0) { + error = got_error_msg(GOT_ERR_LOGIN_FAILED, + "wrong server name in login token"); + goto err; + } + + srv = gotweb_get_server(c->fcgi_params.server_name); + if (srv == NULL) { + error = got_error_msg(GOT_ERR_LOGIN_FAILED, + "invalid server name for login"); + goto err; + } + + TAILQ_FOREACH(repo, &srv->repos, entry) { + switch (auth_check(&identifier, uid, &repo->access_rules)) { + case GOTWEBD_ACCESS_PERMITTED: + goto logged_in; + case GOTWEBD_ACCESS_DENIED: + case GOTWEBD_ACCESS_NO_MATCH: + break; + default: + error = got_error_fmt(GOT_ERR_LOGIN_FAILED, + "access check error for uid %u\n", uid); + goto err; + } + } + + switch (auth_check(&identifier, uid, &srv->access_rules)) { + case GOTWEBD_ACCESS_PERMITTED: + goto logged_in; + case GOTWEBD_ACCESS_DENIED: + error = got_error_msg(GOT_ERR_LOGIN_FAILED, + "permission denied"); + goto err; + case GOTWEBD_ACCESS_NO_MATCH: + break; + default: + error = got_error_fmt(GOT_ERR_LOGIN_FAILED, + "access check error for uid %u\n", uid); + goto err; + } + + switch (auth_check(&identifier, uid, &env->access_rules)) { + case GOTWEBD_ACCESS_PERMITTED: + break; + case GOTWEBD_ACCESS_DENIED: + case GOTWEBD_ACCESS_NO_MATCH: + error = got_error_msg(GOT_ERR_LOGIN_FAILED, + "permission denied"); + goto err; + default: + error = got_error_fmt(GOT_ERR_LOGIN_FAILED, + "access check error for uid %u\n", uid); + goto err; + } + +logged_in: + if (gotwebd_env->gotwebd_verbose > 0) { + log_info("successful login of uid %u as %s for server \"%s\"", + uid, identifier, hostname); + } + + /* + * Generate a long-lasting token for the browser cookie. + * TODO: make validity configurable? + */ + token = login_gen_token(uid, hostname, validity, + auth_token_secret, sizeof(auth_token_secret), + "authentication"); + if (token == NULL) { + error = got_error_msg(GOT_ERR_LOGIN_FAILED, + "failed to generate authentication cookie"); + goto err; + } + + r = tp_writef(c->tp, "Set-Cookie: gwdauth=%s;" + " SameSite=Strict;%s Path=/; HttpOnly; Max-Age=%llu\r\n", token, + env->auth_config == GOTWEBD_AUTH_SECURE ? " Secure;" : "", + validity); + explicit_bzero(token, strlen(token)); + free(token); + if (r == -1) { + error = got_error_from_errno("tp_writef"); + goto err; + } + + memset(&url, 0, sizeof(url)); + url.action = INDEX; + gotweb_reply(c, 307, "text/html", &url); + return; + +err: + free(hostname); + hostname = NULL; + + log_warnx("%s: %s", __func__, error->msg); + c->t->error = error; + if (error->code == GOT_ERR_LOGIN_FAILED) { + if (gotweb_reply(c, 401, "text/html", NULL) == -1) + return; + gotweb_render_page(c->tp, gotweb_render_unauthorized); + } else { + if (gotweb_reply(c, 400, "text/html", NULL) == -1) + return; + gotweb_render_page(c->tp, gotweb_render_error); + } +} + +static void +process_request(struct request *c) +{ + const struct got_error *error = NULL; + struct gotwebd *env = gotwebd_env; + uid_t uid; + struct server *srv; + struct gotwebd_repo *repo = NULL; + enum gotwebd_auth_config auth_config; + char *hostname = NULL; + const char *identifier = NULL; + + srv = gotweb_get_server(c->fcgi_params.server_name); + if (srv == NULL) { + log_warnx("request for unknown server name"); + error = got_error(GOT_ERR_BAD_QUERYSTRING); + goto done; + } + + auth_config = srv->auth_config; + if (c->fcgi_params.qs.path[0] != '\0') { + repo = gotweb_get_repository(srv, c->fcgi_params.qs.path); + if (repo) + auth_config = repo->auth_config; + } + + switch (auth_config) { + case GOTWEBD_AUTH_SECURE: + case GOTWEBD_AUTH_INSECURE: + break; + case GOTWEBD_AUTH_DISABLED: + forward_request(c); + return; + default: + fatalx("bad auth_config %d", env->auth_config); + } + + if (login_check_token(&uid, &hostname, c->fcgi_params.auth_cookie, + auth_token_secret, sizeof(auth_token_secret), + "authentication") == -1) { + error = got_error(GOT_ERR_LOGIN_FAILED); + goto done; + } + + /* + * The www user ID represents the case where no authentication + * occurred. This user is not allowed in authentication cookies. + */ + if (uid == env->www_uid) { + error = got_error(GOT_ERR_LOGIN_FAILED); + goto done; + } + + c->client_uid = uid; + if (strcmp(hostname, c->fcgi_params.server_name) != 0) { + error = got_error_msg(GOT_ERR_LOGIN_FAILED, + "bad server name in login token"); + goto done; + } + + if (repo) { + switch (auth_check(&identifier, uid, &repo->access_rules)) { + case GOTWEBD_ACCESS_DENIED: + error = got_error_msg(GOT_ERR_LOGIN_FAILED, + "permission denied"); + goto done; + case GOTWEBD_ACCESS_PERMITTED: + goto permitted; + case GOTWEBD_ACCESS_NO_MATCH: + break; + default: + error = got_error_fmt(GOT_ERR_LOGIN_FAILED, + "access check error for uid %u\n", uid); + goto done; + } + } else if (c->fcgi_params.qs.action == INDEX) { + int have_public_repo = 0; + + /* + * The index page may contain a mix of repositories we have + * access to and/or for which authentication is disabled. + */ + TAILQ_FOREACH(repo, &srv->repos, entry) { + if (repo->auth_config == GOTWEBD_AUTH_DISABLED) + have_public_repo = -1; + + switch (auth_check(&identifier, uid, + &repo->access_rules)) { + case GOTWEBD_ACCESS_PERMITTED: + goto permitted; + case GOTWEBD_ACCESS_DENIED: + case GOTWEBD_ACCESS_NO_MATCH: + break; + default: + error = got_error_fmt(GOT_ERR_LOGIN_FAILED, + "access check error for uid %u\n", uid); + goto done; + } + } + + /* We have access to public repositories only. */ + if (have_public_repo) { + identifier = ""; + goto permitted; + } + } + + switch (auth_check(&identifier, uid, &srv->access_rules)) { + case GOTWEBD_ACCESS_DENIED: + error = got_error_msg(GOT_ERR_LOGIN_FAILED, + "permission denied"); + goto done; + case GOTWEBD_ACCESS_PERMITTED: + goto permitted; + case GOTWEBD_ACCESS_NO_MATCH: + break; + default: + error = got_error_fmt(GOT_ERR_LOGIN_FAILED, + "access check error for uid %u\n", uid); + goto done; + } + + switch (auth_check(&identifier, uid, &env->access_rules)) { + case GOTWEBD_ACCESS_DENIED: + case GOTWEBD_ACCESS_NO_MATCH: + error = got_error_msg(GOT_ERR_LOGIN_FAILED, + "permission denied"); + goto done; + case GOTWEBD_ACCESS_PERMITTED: + goto permitted; + default: + error = got_error_fmt(GOT_ERR_LOGIN_FAILED, + "access check error for uid %u\n", uid); + goto done; + } + +permitted: + /* + * At this point, identifier should either be the empty string (if + * the request is allowed because authentication is partly disabled), + * or a user or group name. + */ + if (identifier == NULL) + fatalx("have no known user identifier"); + + if (strlcpy(c->access_identifier, identifier, + sizeof(c->access_identifier)) >= sizeof(c->access_identifier)) { + error = got_error_msg(GOT_ERR_NO_SPACE, + "identifier too long"); + goto done; + } + + if (gotwebd_env->gotwebd_verbose > 0) { + log_info("authenticated UID %u as %s for server \"%s\"", + uid, identifier, hostname); + } +done: + free(hostname); + if (error) + render_error(c, error); + else + forward_request(c); +} + +static struct request * +recv_request(struct imsg *imsg) +{ + const struct got_error *error = NULL; + struct request *c; + struct server *srv; + size_t datalen = imsg->hdr.len - IMSG_HEADER_SIZE; + int fd = -1; + uint8_t *outbuf = NULL; + + if (datalen != sizeof(*c)) { + log_warnx("bad request size received over imsg"); + return NULL; + } + + fd = imsg_get_fd(imsg); + if (fd == -1) { + log_warnx("no client file descriptor"); + return NULL; + } + + c = calloc(1, sizeof(*c)); + if (c == NULL) { + log_warn("calloc"); + return NULL; + } + + outbuf = calloc(1, GOTWEBD_CACHESIZE); + if (outbuf == NULL) { + log_warn("calloc"); + free(c); + return NULL; + } + + memcpy(c, imsg->data, sizeof(*c)); + + /* Non-NULL pointers, if any, are not from our address space. */ + c->sock = NULL; + c->srv = NULL; + c->t = NULL; + c->tp = NULL; + c->buf = NULL; + c->outbuf = outbuf; + + memset(&c->ev, 0, sizeof(c->ev)); + memset(&c->tmo, 0, sizeof(c->tmo)); + + /* Use our own temporary file descriptors. */ + memcpy(c->priv_fd, gotwebd_env->priv_fd, sizeof(c->priv_fd)); + + c->fd = fd; + + c->client_uid = gotwebd_env->www_uid; + + c->tp = template(c, fcgi_write, c->outbuf, GOTWEBD_CACHESIZE); + if (c->tp == NULL) { + log_warn("gotweb init template"); + fcgi_cleanup_request(c); + return NULL; + } + + /* init the transport */ + error = gotweb_init_transport(&c->t); + if (error) { + log_warnx("gotweb init transport: %s", error->msg); + fcgi_cleanup_request(c); + return NULL; + } + + /* querystring */ + c->t->qs = &c->fcgi_params.qs; + + /* get the gotwebd server */ + srv = gotweb_get_server(c->fcgi_params.server_name); + if (srv == NULL) { + log_warnx("server '%s' not found", c->fcgi_params.server_name); + fcgi_cleanup_request(c); + return NULL; + } + c->srv = srv; + + return c; +} + +static void +auth_dispatch_sockets(int fd, short event, void *arg) +{ + struct imsgev *iev = arg; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + struct request *c; + int shut = 0; + + ibuf = &iev->ibuf; + + if (event & EV_READ) { + if ((n = imsgbuf_read(ibuf)) == -1) + fatal("imsgbuf_read error"); + if (n == 0) /* Connection closed */ + shut = 1; + } + if (event & EV_WRITE) { + if (imsgbuf_write(ibuf) == -1) + fatal("imsgbuf_write"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("imsg_get"); + if (n == 0) /* No more messages. */ + break; + + switch (imsg.hdr.type) { + case GOTWEBD_IMSG_REQ_PROCESS: + c = recv_request(&imsg); + if (c == NULL) + break; + + if (c->fcgi_params.qs.login[0] != '\0') + do_login(c); + else + process_request(c); + + /* + * If we have not forwarded the request to the gotweb + * process we must flush and clean up ourselves. + */ + if (c->fd != -1) { + uint32_t request_id = c->request_id; + + if (template_flush(c->tp) == -1) { + log_warn("request %u flush", + c->request_id); + } + fcgi_create_end_record(c); + fcgi_cleanup_request(c); + abort_request(request_id); + } + break; + default: + fatalx("%s: unknown imsg type %d", __func__, + imsg.hdr.type); + } + + imsg_free(&imsg); + } + + if (!shut) + imsg_event_add(iev); + else { + /* This pipe is dead. Remove its event handler */ + event_del(&iev->ev); + event_loopexit(NULL); + } +} + +static void +recv_sockets_pipe(struct gotwebd *env, struct imsg *imsg) +{ + struct imsgev *iev; + int fd; + + if (env->iev_sockets != NULL) + fatalx("sockets process already connected"); + + fd = imsg_get_fd(imsg); + if (fd == -1) + fatalx("invalid login pipe fd"); + + iev = calloc(1, sizeof(*iev)); + if (iev == NULL) + fatal("calloc"); + + if (imsgbuf_init(&iev->ibuf, fd) == -1) + fatal("imsgbuf_init"); + imsgbuf_allow_fdpass(&iev->ibuf); + imsgbuf_set_maxsize(&iev->ibuf, sizeof(struct request)); + + iev->handler = auth_dispatch_sockets; + iev->data = iev; + event_set(&iev->ev, fd, EV_READ, auth_dispatch_sockets, iev); + imsg_event_add(iev); + + env->iev_sockets = iev; +} + +static void +auth_dispatch_gotweb(int fd, short event, void *arg) +{ + struct imsgev *iev = arg; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + int shut = 0; + + ibuf = &iev->ibuf; + + if (event & EV_READ) { + if ((n = imsgbuf_read(ibuf)) == -1) + fatal("imsgbuf_read error"); + if (n == 0) /* Connection closed */ + shut = 1; + } + if (event & EV_WRITE) { + if (imsgbuf_write(ibuf) == -1) + fatal("imsgbuf_write"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("imsg_get"); + if (n == 0) /* No more messages. */ + break; + + switch (imsg.hdr.type) { + case GOTWEBD_IMSG_REQ_ABORT: { + uint32_t request_id; + + if (imsg_get_data(&imsg, &request_id, + sizeof(request_id)) == -1) + fatalx("invalid REQ_ABORT msg"); + + abort_request(request_id); + break; + } + default: + fatalx("%s: unknown imsg type %d", __func__, + imsg.hdr.type); + } + + imsg_free(&imsg); + } + + if (!shut) + imsg_event_add(iev); + else { + /* This pipe is dead. Remove its event handler */ + event_del(&iev->ev); + event_loopexit(NULL); + } +} + +static void +recv_gotweb_pipe(struct gotwebd *env, struct imsg *imsg) +{ + struct imsgev *iev; + int fd; + + if (env->iev_gotweb != NULL) + fatalx("gotweb process already connected"); + + fd = imsg_get_fd(imsg); + if (fd == -1) + fatalx("invalid login pipe fd"); + + iev = calloc(1, sizeof(*iev)); + if (iev == NULL) + fatal("calloc"); + + if (imsgbuf_init(&iev->ibuf, fd) == -1) + fatal("imsgbuf_init"); + imsgbuf_allow_fdpass(&iev->ibuf); + imsgbuf_set_maxsize(&iev->ibuf, sizeof(struct request)); + + iev->handler = auth_dispatch_gotweb; + iev->data = iev; + event_set(&iev->ev, fd, EV_READ, auth_dispatch_gotweb, iev); + imsg_event_add(iev); + + env->iev_gotweb = iev; +} + +static void +auth_dispatch_main(int fd, short event, void *arg) +{ + struct imsgev *iev = arg; + struct imsgbuf *ibuf; + struct imsg imsg; + struct gotwebd *env = gotwebd_env; + struct server *srv; + struct gotwebd_repo *repo; + ssize_t n; + int shut = 0; + + ibuf = &iev->ibuf; + + if (event & EV_READ) { + if ((n = imsgbuf_read(ibuf)) == -1) + fatal("imsgbuf_read error"); + if (n == 0) /* Connection closed */ + shut = 1; + } + if (event & EV_WRITE) { + if (imsgbuf_write(ibuf) == -1) + fatal("imsgbuf_write"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("imsg_get"); + if (n == 0) /* No more messages. */ + break; + + switch (imsg.hdr.type) { + case GOTWEBD_IMSG_CFG_ACCESS_RULE: + if (TAILQ_EMPTY(&env->servers)) { + /* global access rule */ + config_get_access_rule(&env->access_rules, + &imsg); + } else { + srv = TAILQ_LAST(&env->servers, serverlist); + if (TAILQ_EMPTY(&srv->repos)) { + /* per-server access rule */ + config_get_access_rule( + &srv->access_rules, &imsg); + } else { + /* per-repository access rule */ + repo = TAILQ_LAST(&srv->repos, + gotwebd_repolist); + config_get_access_rule( + &repo->access_rules, &imsg); + } + } + break; + case GOTWEBD_IMSG_CFG_SRV: + config_getserver(gotwebd_env, &imsg); + break; + case GOTWEBD_IMSG_CFG_REPO: + if (TAILQ_EMPTY(&env->servers)) + fatalx("%s: unexpected CFG_REPO msg", __func__); + srv = TAILQ_LAST(&env->servers, serverlist); + config_get_repository(&srv->repos, &imsg); + break; + case GOTWEBD_IMSG_CTL_PIPE: + if (env->iev_sockets == NULL) + recv_sockets_pipe(env, &imsg); + else + recv_gotweb_pipe(env, &imsg); + break; + case GOTWEBD_IMSG_CTL_START: + auth_launch(env); + break; + case GOTWEBD_IMSG_LOGIN_SECRET: + if (imsg_get_data(&imsg, login_token_secret, + sizeof(login_token_secret)) == -1) + fatalx("invalid LOGIN_SECRET msg"); + break; + case GOTWEBD_IMSG_AUTH_SECRET: + if (imsg_get_data(&imsg, auth_token_secret, + sizeof(auth_token_secret)) == -1) + fatalx("invalid AUTH_SECRET msg"); + break; + case GOTWEBD_IMSG_AUTH_CONF: + if (imsg_get_data(&imsg, &env->auth_config, + sizeof(env->auth_config)) == -1) + fatalx("invalid AUTH_CONF msg"); + break; + case GOTWEBD_IMSG_WWW_UID: + if (imsg_get_data(&imsg, &env->www_uid, + sizeof(env->www_uid)) == -1) + fatalx("invalid WWW_UID msg"); + break; + default: + fatalx("%s: unknown imsg type %d", __func__, + imsg.hdr.type); + } + + imsg_free(&imsg); + } + + if (!shut) + imsg_event_add(iev); + else { + /* This pipe is dead. Remove its event handler */ + event_del(&iev->ev); + event_loopexit(NULL); + } +} + +void +gotwebd_auth(struct gotwebd *env, int fd) +{ + struct event sighup, sigint, sigusr1, sigchld, sigterm; + struct event_base *evb; + + evb = event_init(); + + if ((env->iev_parent = malloc(sizeof(*env->iev_parent))) == NULL) + fatal("malloc"); + if (imsgbuf_init(&env->iev_parent->ibuf, fd) == -1) + fatal("imsgbuf_init"); + imsgbuf_allow_fdpass(&env->iev_parent->ibuf); + env->iev_parent->handler = auth_dispatch_main; + env->iev_parent->data = env->iev_parent; + event_set(&env->iev_parent->ev, fd, EV_READ, auth_dispatch_main, + env->iev_parent); + event_add(&env->iev_parent->ev, NULL); + + signal(SIGPIPE, SIG_IGN); + + signal_set(&sighup, SIGHUP, auth_sighdlr, env); + signal_add(&sighup, NULL); + signal_set(&sigint, SIGINT, auth_sighdlr, env); + signal_add(&sigint, NULL); + signal_set(&sigusr1, SIGUSR1, auth_sighdlr, env); + signal_add(&sigusr1, NULL); + signal_set(&sigchld, SIGCHLD, auth_sighdlr, env); + signal_add(&sigchld, NULL); + signal_set(&sigterm, SIGTERM, auth_sighdlr, env); + signal_add(&sigterm, NULL); + +#ifndef PROFILE + if (pledge("stdio getpw recvfd sendfd", NULL) == -1) + fatal("pledge"); +#endif + event_dispatch(); + event_base_free(evb); + auth_shutdown(); +} blob - 30774393e8a5bd14695eeda37cf4105e6c11f907 blob + 72d224172bff2531ebcbbdeb3c28089e8392c9d7 --- gotwebd/fcgi.c +++ gotwebd/fcgi.c @@ -186,6 +186,7 @@ static const struct querystring_keys querystring_keys[ { "headref", HEADREF }, { "index_page", INDEX_PAGE }, { "path", PATH }, + { "login", LOGIN }, }; static const struct action_keys action_keys[] = { @@ -366,7 +367,15 @@ assign_querystring(struct querystring *qs, char *key, goto done; } break; - } + case LOGIN: + if (strlcpy(qs->login, value, sizeof(qs->login)) >= + sizeof(qs->login)) { + error = got_error_msg(GOT_ERR_NO_SPACE, + "login token too long"); + goto done; + } + break; + } /* entry found */ break; @@ -425,6 +434,47 @@ err: return error; } +static void +parse_cookie_hdr(struct gotwebd_fcgi_params *params, char *hdr, size_t len) +{ + size_t l; + char *end; + + memset(params->auth_cookie, 0, sizeof(params->auth_cookie)); + + while (len > 0) { + if (hdr[0] == ' ' || hdr[0] == '\t') { + hdr++; + len--; + continue; + } + + /* looking at the start of name=val */ + + if ((end = memchr(hdr, ' ', len)) == NULL || + (end = memchr(hdr, '\t', len)) == NULL) + end = hdr + len; + l = end - hdr; + + if (len > 8 && !strncmp(hdr, "gwdauth=", 8)) { + hdr += 8; + len -= 8; + l -= 8; + + if (l < MAX_AUTH_COOKIE - 1) { + memcpy(params->auth_cookie, hdr, l); + params->auth_cookie[l] = '\0'; + } + + return; + } + + /* skip to the next one */ + hdr += l; + len -= l; + } +} + int fcgi_parse_params(uint8_t *buf, uint16_t n, struct gotwebd_fcgi_params *params) { @@ -507,6 +557,10 @@ fcgi_parse_params(uint8_t *buf, uint16_t n, struct got strncmp(buf, "HTTPS", 5) == 0) params->https = 1; + if (name_len == 11 && + strncmp(buf, "HTTP_COOKIE", 11) == 0) + parse_cookie_hdr(params, val, val_len); + buf += name_len + val_len; n -= name_len - val_len; } blob - d18feafe16107f9706e6306fc9fe2ad35f69bd37 blob + 40193456fd89445654d027726731bf33676b80b5 --- gotwebd/gotweb.c +++ gotwebd/gotweb.c @@ -66,9 +66,7 @@ static const struct got_error *gotweb_get_clone_url(ch static void gotweb_free_repo_dir(struct repo_dir *); -struct server *gotweb_get_server(const char *); - -static int +int gotweb_reply(struct request *c, int status, const char *ctype, struct gotweb_url *location) { @@ -128,7 +126,7 @@ cleanup_request(struct request *c) fcgi_cleanup_request(c); - if (imsg_compose_event(gotwebd_env->iev_sockets, GOTWEBD_IMSG_REQ_ABORT, + if (imsg_compose_event(gotwebd_env->iev_auth, GOTWEBD_IMSG_REQ_ABORT, GOTWEBD_PROC_GOTWEB, -1, -1, &request_id, sizeof(request_id)) == -1) log_warn("imsg_compose_event"); } @@ -541,6 +539,28 @@ gotweb_init_transport(struct transport **t) return error; } +struct gotwebd_repo * +gotweb_get_repository(struct server *server, const char *name) +{ + struct gotwebd_repo *repo; + + TAILQ_FOREACH(repo, &server->repos, entry) { + if (strncmp(repo->name, name, strlen(repo->name)) != 0) + continue; + + if (strlen(name) == strlen(repo->name)) + return repo; + + if (strlen(name) != strlen(repo->name) + 4) + continue; + + if (strcmp(name + strlen(repo->name), ".git") == 0) + return repo; + } + + return NULL; +} + void gotweb_free_repo_tag(struct repo_tag *rt) { @@ -960,16 +980,48 @@ validate_path(const char *path, const char *parent_pat return error; } +static enum gotwebd_access +auth_check(struct request *c, struct gotwebd_access_rule_list *rules) +{ + struct gotwebd *env = gotwebd_env; + enum gotwebd_access access = GOTWEBD_ACCESS_NO_MATCH; + struct gotwebd_access_rule *rule; + + /* + * The www user ID implies that no user authentication occurred. + * But authentication is enabled so we must deny this request. + */ + if (c->client_uid == env->www_uid) + return GOTWEBD_ACCESS_DENIED; + + /* + * We cannot access /etc/passwd in this process so we cannot + * verify the client's user ID outselves here. + * Match rules against the access identifier which has already + * passed authentication in the auth process. + */ + STAILQ_FOREACH(rule, rules, entry) { + if (strcmp(rule->identifier, c->access_identifier) == 0) + access = rule->access; + } + + return access; +} + static const struct got_error * gotweb_load_got_path(struct repo_dir **rp, const char *dir, struct request *c) { const struct got_error *error = NULL; + struct gotwebd *env = gotwebd_env; struct server *srv = c->srv; struct transport *t = c->t; struct repo_dir *repo_dir; DIR *dt; char *dir_test; + struct gotwebd_repo *repo; + enum gotwebd_auth_config auth_config = 0; + enum gotwebd_access access = GOTWEBD_ACCESS_DENIED; *rp = calloc(1, sizeof(**rp)); if (*rp == NULL) @@ -1013,6 +1065,50 @@ gotweb_load_got_path(struct repo_dir **rp, const char if (error) goto err; + repo = gotweb_get_repository(srv, repo_dir->name); + if (repo) { + auth_config = repo->auth_config; + switch (auth_config) { + case GOTWEBD_AUTH_DISABLED: + access = GOTWEBD_ACCESS_PERMITTED; + break; + case GOTWEBD_AUTH_SECURE: + case GOTWEBD_AUTH_INSECURE: + access = auth_check(c, &repo->access_rules); + if (access == GOTWEBD_ACCESS_NO_MATCH) + access = auth_check(c, &srv->access_rules); + if (access == GOTWEBD_ACCESS_NO_MATCH) + access = auth_check(c, &env->access_rules); + if (access == GOTWEBD_ACCESS_NO_MATCH) + access = GOTWEBD_ACCESS_DENIED; + break; + } + } else { + auth_config = srv->auth_config; + switch (auth_config) { + case GOTWEBD_AUTH_DISABLED: + access = GOTWEBD_ACCESS_PERMITTED; + break; + case GOTWEBD_AUTH_SECURE: + case GOTWEBD_AUTH_INSECURE: + access = auth_check(c, &srv->access_rules); + if (access == GOTWEBD_ACCESS_NO_MATCH) + access = auth_check(c, &env->access_rules); + if (access == GOTWEBD_ACCESS_NO_MATCH) + access = GOTWEBD_ACCESS_DENIED; + break; + } + } + + if (access != GOTWEBD_ACCESS_PERMITTED && + access != GOTWEBD_ACCESS_DENIED) + fatalx("invalid access check result %d", access); + + if (access != GOTWEBD_ACCESS_PERMITTED) { + error = got_error_path(repo_dir->name, GOT_ERR_NOT_GIT_REPO); + goto err; + } + if (srv->respect_exportok && faccessat(dirfd(dt), "git-daemon-export-ok", F_OK, 0) == -1) { error = got_error_path(repo_dir->name, GOT_ERR_NOT_GIT_REPO); @@ -1171,8 +1267,8 @@ gotweb_shutdown(void) imsgbuf_clear(&gotwebd_env->iev_parent->ibuf); free(gotwebd_env->iev_parent); - imsgbuf_clear(&gotwebd_env->iev_sockets->ibuf); - free(gotwebd_env->iev_sockets); + imsgbuf_clear(&gotwebd_env->iev_auth->ibuf); + free(gotwebd_env->iev_auth); while (!TAILQ_EMPTY(&gotwebd_env->servers)) { struct server *srv; @@ -1225,8 +1321,8 @@ gotweb_launch(struct gotwebd *env) struct server *srv; const struct got_error *error; - if (env->iev_sockets == NULL) - fatal("sockets process not connected"); + if (env->iev_auth == NULL) + fatal("auth process not connected"); #ifndef PROFILE if (pledge("stdio rpath recvfd sendfd proc exec unveil", NULL) == -1) @@ -1245,7 +1341,7 @@ gotweb_launch(struct gotwebd *env) if (unveil(NULL, NULL) == -1) fatal("unveil"); - event_add(&env->iev_sockets->ev, NULL); + event_add(&env->iev_auth->ev, NULL); } static void @@ -1314,14 +1410,17 @@ gotweb_dispatch_server(int fd, short event, void *arg) } static void -recv_server_pipe(struct gotwebd *env, struct imsg *imsg) +recv_auth_pipe(struct gotwebd *env, struct imsg *imsg) { struct imsgev *iev; int fd; + if (env->iev_auth != NULL) + fatalx("auth process already connected"); + fd = imsg_get_fd(imsg); if (fd == -1) - fatalx("invalid server pipe fd"); + fatalx("invalid auth pipe fd"); iev = calloc(1, sizeof(*iev)); if (iev == NULL) @@ -1335,7 +1434,7 @@ recv_server_pipe(struct gotwebd *env, struct imsg *ims event_set(&iev->ev, fd, EV_READ, gotweb_dispatch_server, iev); imsg_event_add(iev); - env->iev_sockets = iev; + env->iev_auth = iev; } static void @@ -1345,6 +1444,8 @@ gotweb_dispatch_main(int fd, short event, void *arg) struct imsgbuf *ibuf; struct imsg imsg; struct gotwebd *env = gotwebd_env; + struct server *srv; + struct gotwebd_repo *repo; ssize_t n; int shut = 0; @@ -1368,9 +1469,35 @@ gotweb_dispatch_main(int fd, short event, void *arg) break; switch (imsg.hdr.type) { + case GOTWEBD_IMSG_CFG_ACCESS_RULE: + if (TAILQ_EMPTY(&env->servers)) { + /* global access rule */ + config_get_access_rule(&env->access_rules, + &imsg); + } else { + srv = TAILQ_LAST(&env->servers, serverlist); + if (TAILQ_EMPTY(&srv->repos)) { + /* per-server access rule */ + config_get_access_rule( + &srv->access_rules, &imsg); + } else { + /* per-repository access rule */ + repo = TAILQ_LAST(&srv->repos, + gotwebd_repolist); + config_get_access_rule( + &repo->access_rules, &imsg); + } + } + break; case GOTWEBD_IMSG_CFG_SRV: config_getserver(env, &imsg); break; + case GOTWEBD_IMSG_CFG_REPO: + if (TAILQ_EMPTY(&env->servers)) + fatalx("%s: unexpected CFG_REPO msg", __func__); + srv = TAILQ_LAST(&env->servers, serverlist); + config_get_repository(&srv->repos, &imsg); + break; case GOTWEBD_IMSG_CFG_FD: config_getfd(env, &imsg); break; @@ -1381,8 +1508,18 @@ gotweb_dispatch_main(int fd, short event, void *arg) config_getcfg(env, &imsg); break; case GOTWEBD_IMSG_CTL_PIPE: - recv_server_pipe(env, &imsg); + recv_auth_pipe(env, &imsg); break; + case GOTWEBD_IMSG_AUTH_CONF: + if (imsg_get_data(&imsg, &env->auth_config, + sizeof(env->auth_config)) == -1) + fatalx("%s: invalid AUTH_CONF msg", __func__); + break; + case GOTWEBD_IMSG_WWW_UID: + if (imsg_get_data(&imsg, &env->www_uid, + sizeof(env->www_uid)) == -1) + fatalx("%s: invalid WWW_UID msg", __func__); + break; case GOTWEBD_IMSG_CTL_START: gotweb_launch(env); break; blob - 11f47b8a3f9c368e255247b624b2e2f44eba10d9 blob + bf7d854b0f45a7e8f84a71ad8df9e68c4e1ab4b8 --- gotwebd/gotwebd.c +++ gotwebd/gotwebd.c @@ -57,6 +57,8 @@ void gotwebd_sighdlr(int sig, short event, void *arg) void gotwebd_shutdown(void); void gotwebd_dispatch_server(int, short, void *); void gotwebd_dispatch_fcgi(int, short, void *); +void gotwebd_dispatch_login(int, short, void *); +void gotwebd_dispatch_auth(int, short, void *); void gotwebd_dispatch_gotweb(int, short, void *); struct gotwebd *gotwebd_env; @@ -127,6 +129,13 @@ main_compose_sockets(struct gotwebd *env, uint32_t typ } int +main_compose_login(struct gotwebd *env, uint32_t type, int fd, + const void *data, uint16_t len) +{ + return send_imsg(env->iev_login, type, fd, data, len); +} + +int main_compose_gotweb(struct gotwebd *env, uint32_t type, int fd, const void *data, uint16_t len) { @@ -143,12 +152,74 @@ main_compose_gotweb(struct gotwebd *env, uint32_t type } int +main_compose_auth(struct gotwebd *env, uint32_t type, int fd, + const void *data, uint16_t len) +{ + size_t i; + int ret = 0; + + for (i = 0; i < env->prefork; i++) { + ret = send_imsg(&env->iev_auth[i], type, fd, data, len); + if (ret) + break; + } + + return ret; +} + +int sockets_compose_main(struct gotwebd *env, uint32_t type, const void *d, uint16_t len) { return (imsg_compose_event(env->iev_parent, type, 0, -1, -1, d, len)); } + void +gotwebd_dispatch_login(int fd, short event, void *arg) +{ + struct imsgev *iev = arg; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + int shut = 0; + + ibuf = &iev->ibuf; + + if (event & EV_READ) { + if ((n = imsgbuf_read(ibuf)) == -1) + fatal("imsgbuf_read error"); + if (n == 0) /* Connection closed */ + shut = 1; + } + if (event & EV_WRITE) { + if (imsgbuf_write(ibuf) == -1) + fatal("imsgbuf_write"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("imsg_get"); + if (n == 0) /* No more messages. */ + break; + + switch (imsg.hdr.type) { + default: + fatalx("%s: unknown imsg type %d", __func__, + imsg.hdr.type); + } + + imsg_free(&imsg); + } + + if (!shut) + imsg_event_add(iev); + else { + /* This pipe is dead. Remove its event handler */ + event_del(&iev->ev); + event_loopexit(NULL); + } +} + void gotwebd_dispatch_server(int fd, short event, void *arg) { @@ -250,6 +321,56 @@ gotwebd_dispatch_fcgi(int fd, short event, void *arg) } void +gotwebd_dispatch_auth(int fd, short event, void *arg) +{ + struct imsgev *iev = arg; + struct imsgbuf *ibuf; + struct imsg imsg; + struct gotwebd *env = gotwebd_env; + ssize_t n; + int shut = 0; + + ibuf = &iev->ibuf; + + if (event & EV_READ) { + if ((n = imsgbuf_read(ibuf)) == -1) + fatal("imsgbuf_read error"); + if (n == 0) /* Connection closed */ + shut = 1; + } + if (event & EV_WRITE) { + if (imsgbuf_write(ibuf) == -1) + fatal("imsgbuf_write"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("imsg_get"); + if (n == 0) /* No more messages. */ + break; + + switch (imsg.hdr.type) { + case GOTWEBD_IMSG_CFG_DONE: + gotwebd_configure_done(env); + break; + default: + fatalx("%s: unknown imsg type %d", __func__, + imsg.hdr.type); + } + + imsg_free(&imsg); + } + + if (!shut) + imsg_event_add(iev); + else { + /* This pipe is dead. Remove its event handler */ + event_del(&iev->ev); + event_loopexit(NULL); + } +} + +void gotwebd_dispatch_gotweb(int fd, short event, void *arg) { struct imsgev *iev = arg; @@ -365,9 +486,15 @@ spawn_process(struct gotwebd *env, const char *argv0, if (asprintf(&s, "-S%d", env->prefork) == -1) fatal("asprintf"); argv[argc++] = s; + } else if (proc_type == GOTWEBD_PROC_LOGIN) { + argv[argc++] = "-L"; + argv[argc++] = username; } else if (proc_type == GOTWEBD_PROC_FCGI) { argv[argc++] = "-F"; argv[argc++] = username; + } else if (proc_type == GOTWEBD_PROC_AUTH) { + argv[argc++] = "-A"; + argv[argc++] = username; } else if (proc_type == GOTWEBD_PROC_GOTWEB) { argv[argc++] = "-G"; argv[argc++] = username; @@ -431,8 +558,12 @@ main(int argc, char **argv) fatal("%s: calloc", __func__); config_init(env); - while ((ch = getopt(argc, argv, "D:dG:f:F:nS:vW:")) != -1) { + while ((ch = getopt(argc, argv, "A:D:dG:f:F:L:nS:vW:")) != -1) { switch (ch) { + case 'A': + proc_type = GOTWEBD_PROC_AUTH; + gotwebd_username = optarg; + break; case 'D': if (cmdline_symset(optarg) < 0) log_warnx("could not parse macro definition %s", @@ -452,6 +583,10 @@ main(int argc, char **argv) proc_type = GOTWEBD_PROC_FCGI; gotwebd_username = optarg; break; + case 'L': + proc_type = GOTWEBD_PROC_LOGIN; + gotwebd_username = optarg; + break; case 'n': no_action = 1; break; @@ -504,6 +639,7 @@ main(int argc, char **argv) pw = getpwnam(www_username); if (pw == NULL) fatalx("unknown user %s", www_username); + env->www_uid = pw->pw_uid; www_gid = pw->pw_gid; pw = getpwnam(gotwebd_username); @@ -521,6 +657,17 @@ main(int argc, char **argv) log_setverbose(env->gotwebd_verbose); switch (proc_type) { + case GOTWEBD_PROC_LOGIN: + setproctitle("login"); + log_procinit("login"); + + if (setgroups(1, &pw->pw_gid) == -1 || + setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) == -1 || + setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) == -1) + fatal("failed to drop privileges"); + + gotwebd_login(env, GOTWEBD_SOCK_FILENO); + return 1; case GOTWEBD_PROC_SOCKETS: setproctitle("sockets"); log_procinit("sockets"); @@ -553,6 +700,17 @@ main(int argc, char **argv) gotwebd_fcgi(env, GOTWEBD_SOCK_FILENO); return 1; + case GOTWEBD_PROC_AUTH: + setproctitle("auth"); + log_procinit("auth"); + + if (setgroups(1, &pw->pw_gid) == -1 || + setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) == -1 || + setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) == -1) + fatal("failed to drop privileges"); + + gotwebd_auth(env, GOTWEBD_SOCK_FILENO); + return 1; case GOTWEBD_PROC_GOTWEB: setproctitle("gotweb"); log_procinit("gotweb"); @@ -577,10 +735,18 @@ main(int argc, char **argv) if (env->iev_sockets == NULL) fatal("calloc"); + env->iev_login = calloc(1, sizeof(*env->iev_login)); + if (env->iev_login == NULL) + fatal("calloc"); + env->iev_fcgi = calloc(1, sizeof(*env->iev_fcgi)); if (env->iev_fcgi == NULL) fatal("calloc"); + env->iev_auth = calloc(env->prefork, sizeof(*env->iev_auth)); + if (env->iev_auth == NULL) + fatal("calloc"); + env->iev_gotweb = calloc(env->prefork, sizeof(*env->iev_gotweb)); if (env->iev_gotweb == NULL) fatal("calloc"); @@ -589,11 +755,17 @@ main(int argc, char **argv) GOTWEBD_PROC_SOCKETS, gotwebd_username, gotwebd_dispatch_server); + spawn_process(env, argv0, env->iev_login, GOTWEBD_PROC_LOGIN, + gotwebd_username, gotwebd_dispatch_login); + spawn_process(env, argv0, env->iev_fcgi, GOTWEBD_PROC_FCGI, gotwebd_username, gotwebd_dispatch_fcgi); for (i = 0; i < env->prefork; ++i) { + spawn_process(env, argv0, &env->iev_auth[i], + GOTWEBD_PROC_AUTH, gotwebd_username, + gotwebd_dispatch_auth); spawn_process(env, argv0, &env->iev_gotweb[i], GOTWEBD_PROC_GOTWEB, gotwebd_username, gotwebd_dispatch_gotweb); @@ -653,7 +825,7 @@ main(int argc, char **argv) static void connect_children(struct gotwebd *env) { - struct imsgev *iev_gotweb; + struct imsgev *iev_gotweb, *iev_auth; int pipe[2]; int i; @@ -668,6 +840,7 @@ connect_children(struct gotwebd *env) for (i = 0; i < env->prefork; i++) { iev_gotweb = &env->iev_gotweb[i]; + iev_auth = &env->iev_auth[i]; if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, pipe) == -1) fatal("socketpair"); @@ -676,6 +849,17 @@ connect_children(struct gotwebd *env) pipe[0], NULL, 0)) fatal("send_imsg"); + if (send_imsg(iev_auth, GOTWEBD_IMSG_CTL_PIPE, + pipe[1], NULL, 0)) + fatal("send_imsg"); + + if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, pipe) == -1) + fatal("socketpair"); + + if (send_imsg(iev_auth, GOTWEBD_IMSG_CTL_PIPE, + pipe[0], NULL, 0)) + fatal("send_imsg"); + if (send_imsg(iev_gotweb, GOTWEBD_IMSG_CTL_PIPE, pipe[1], NULL, 0)) fatal("send_imsg"); @@ -687,18 +871,69 @@ gotwebd_configure(struct gotwebd *env, uid_t uid, gid_ { struct server *srv; struct socket *sock; + struct gotwebd_repo *repo; + char secret[32]; + int i; /* gotweb need to reload its config. */ env->gotweb_pending = env->prefork; + env->auth_pending = env->prefork; + /* send global access rules */ + for (i = 0; i < env->prefork; ++i) { + config_set_access_rules(&env->iev_auth[i], + &env->access_rules); + config_set_access_rules(&env->iev_gotweb[i], + &env->access_rules); + } + /* send our gotweb servers */ TAILQ_FOREACH(srv, &env->servers, entry) { if (main_compose_sockets(env, GOTWEBD_IMSG_CFG_SRV, -1, srv, sizeof(*srv)) == -1) fatal("send_imsg GOTWEBD_IMSG_CFG_SRV"); + if (main_compose_auth(env, GOTWEBD_IMSG_CFG_SRV, + -1, srv, sizeof(*srv)) == -1) + fatal("main_compose_gotweb GOTWEBD_IMSG_CFG_SRV"); if (main_compose_gotweb(env, GOTWEBD_IMSG_CFG_SRV, -1, srv, sizeof(*srv)) == -1) fatal("main_compose_gotweb GOTWEBD_IMSG_CFG_SRV"); + if (main_compose_login(env, GOTWEBD_IMSG_CFG_SRV, + -1, srv, sizeof(*srv)) == -1) + fatal("main_compose_gotweb GOTWEBD_IMSG_CFG_SRV"); + + /* send per-server access rules */ + for (i = 0; i < env->prefork; ++i) { + config_set_access_rules(&env->iev_auth[i], + &srv->access_rules); + config_set_access_rules(&env->iev_gotweb[i], + &srv->access_rules); + } + + /* send repositories and per-repository access rules */ + TAILQ_FOREACH(repo, &srv->repos, entry) { + for (i = 0; i < env->prefork; i++) { + config_set_repository(&env->iev_auth[i], + repo); + config_set_repository(&env->iev_gotweb[i], + repo); + + config_set_access_rules(&env->iev_auth[i], + &repo->access_rules); + config_set_access_rules(&env->iev_gotweb[i], + &repo->access_rules); + } + } + + for (i = 0; i < env->prefork; i++) { + if (imsgbuf_flush(&env->iev_auth[i].ibuf) == -1) + fatal("imsgbuf_flush"); + imsg_event_add(&env->iev_auth[i]); + + if (imsgbuf_flush(&env->iev_gotweb[i].ibuf) == -1) + fatal("imsgbuf_flush"); + imsg_event_add(&env->iev_gotweb[i]); + } } /* send our sockets */ @@ -714,6 +949,44 @@ gotwebd_configure(struct gotwebd *env, uid_t uid, gid_ /* Connect servers and gotwebs. */ connect_children(env); + if (main_compose_auth(env, GOTWEBD_IMSG_AUTH_CONF, -1, + &env->auth_config, sizeof(env->auth_config)) == -1) + fatal("send_imsg GOTWEB_IMSG_AUTH_CONF"); + if (main_compose_gotweb(env, GOTWEBD_IMSG_AUTH_CONF, -1, + &env->auth_config, sizeof(env->auth_config)) == -1) + fatal("main_compose_gotweb GOTWEB_IMSG_AUTH_CONF"); + + if (main_compose_auth(env, GOTWEBD_IMSG_WWW_UID, -1, + &env->www_uid, sizeof(env->www_uid)) == -1) + fatal("main_compose_auth GOTWEB_IMSG_WWW_UID"); + if (main_compose_gotweb(env, GOTWEBD_IMSG_WWW_UID, -1, + &env->www_uid, sizeof(env->www_uid)) == -1) + fatal("main_compose_gotweb GOTWEB_IMSG_WWW_UID"); + + arc4random_buf(secret, sizeof(secret)); + + if (main_compose_login(env, GOTWEBD_IMSG_LOGIN_SECRET, -1, + secret, sizeof(secret)) == -1) + fatal("main_compose_login GOTWEB_IMSG_LOGIN_SECRET"); + if (main_compose_auth(env, GOTWEBD_IMSG_LOGIN_SECRET, -1, + secret, sizeof(secret)) == -1) + fatal("main_compose_auth GOTWEB_IMSG_LOGIN_SECRET"); + + arc4random_buf(secret, sizeof(secret)); + + if (main_compose_auth(env, GOTWEBD_IMSG_AUTH_SECRET, -1, + secret, sizeof(secret)) == -1) + fatal("main_compose_auth GOTWEB_IMSG_AUTH_SECRET"); + + explicit_bzero(secret, sizeof(secret)); + + if (login_privinit(env, uid, gid) == -1) + fatalx("cannot open authentication socket"); + + if (main_compose_login(env, GOTWEBD_IMSG_CFG_SOCK, env->login_sock->fd, + NULL, 0) == -1) + fatal("main_compose_login GOTWEBD_IMSG_CFG_SOCK"); + if (main_compose_sockets(env, GOTWEBD_IMSG_CFG_DONE, -1, NULL, 0) == -1) fatal("send_imsg GOTWEBD_IMSG_CFG_DONE"); @@ -733,8 +1006,20 @@ gotwebd_configure_done(struct gotwebd *env) if (env->gotweb_pending == 0 && main_compose_gotweb(env, GOTWEBD_IMSG_CTL_START, -1, NULL, 0) == -1) - fatal("main_compose_gotewb GOTWEBD_IMSG_CTL_START"); + fatal("main_compose_gotweb GOTWEBD_IMSG_CTL_START"); } + + if (env->auth_pending > 0) { + env->auth_pending--; + if (env->auth_pending == 0 && + main_compose_auth(env, GOTWEBD_IMSG_CTL_START, + -1, NULL, 0) == -1) + fatal("main_compose_auth GOTWEBD_IMSG_CTL_START"); + } + + if (main_compose_login(env, GOTWEBD_IMSG_CTL_START, + -1, NULL, 0) == -1) + fatal("send_imsg GOTWEBD_IMSG_CTL_START"); } void @@ -744,6 +1029,12 @@ gotwebd_shutdown(void) pid_t pid; int i, status; + event_del(&env->iev_login->ev); + imsgbuf_clear(&env->iev_login->ibuf); + close(env->iev_login->ibuf.fd); + env->iev_login->ibuf.fd = -1; + free(env->iev_login); + event_del(&env->iev_sockets->ev); imsgbuf_clear(&env->iev_sockets->ibuf); close(env->iev_sockets->ibuf.fd); @@ -757,13 +1048,21 @@ gotwebd_shutdown(void) free(env->iev_fcgi); for (i = 0; i < env->prefork; ++i) { + event_del(&env->iev_auth[i].ev); + imsgbuf_clear(&env->iev_auth[i].ibuf); + close(env->iev_auth[i].ibuf.fd); + env->iev_auth[i].ibuf.fd = -1; + event_del(&env->iev_gotweb[i].ev); imsgbuf_clear(&env->iev_gotweb[i].ibuf); close(env->iev_gotweb[i].ibuf.fd); env->iev_gotweb[i].ibuf.fd = -1; } + free(env->iev_auth); free(env->iev_gotweb); + free(env->login_sock); + do { pid = waitpid(WAIT_ANY, &status, 0); if (pid <= 0) blob - 6069de8ff5ac1b124f1a08c7268cd9d295c98fc3 blob + cf20e0e4735b466b23c1a9afd4c2821fc1054e6d --- gotwebd/gotwebd.conf.5 +++ gotwebd/gotwebd.conf.5 @@ -60,6 +60,37 @@ Setting the to .Pa / effectively disables chroot. +.It Ic disable authentication +Disable authentication, allowing any browser to view any repository +not hidden via the +.Ic respect_exportok +directive. +Authentication can also be configured on a per-server or per-repository basis. +.It Ic enable authentication Oo Ic insecure Oc +Enable authentication, requiring browsers to present a login token cookie +before read-only repository access is granted. +Authentication can also be configured on a per-server or per-repository basis. +.Pp +Browsers presenting a valid login token cookie will be mapped to the +user account which obtained the login token over SSH from the +.Cm weblogin +command of +.Xr gotsh 1 . +.Pp +Unauthenticated browsers will be mapped to the user account which runs +.Xr httpd 8 . +This user account can be set with the +.Ic www user +directive. +Attempts to read repositories as this user will be denied unless +authentication is disabled for the repository. +.Pp +Unless the +.Ic insecure +keyword is used, the login token cookie will be marked as +.Dq Secure , +which causes browsers to only send the cookie when connected to the +web server over a TLS connection. .It Ic listen on Ar address Ic port Ar number Configure an address and port for incoming FastCGI connections. Valid @@ -80,6 +111,18 @@ While the specified .Ar path must be absolute, it should usually point inside the web server's chroot directory such that the web server can access the socket. +.It Ic login socket Ar path +Set the +.Ar path +to the +.Ux Ns -domain +socket for +.Xr gotsh 1 +.Ic weblogin +commands. +By default the path +.Pa /var/run/gotweb-login.sock +will be used. .It Ic prefork Ar number Spawn enough processes such that .Ar number @@ -128,6 +171,9 @@ followed by server-specific configuration directives i .Pp .Ic server Ar name Brq ... .Pp +If more than one server is defined, each +.Ar name +should match the hostname which browsers use to reach the corresponding server. The first server defined is used if the requested hostname is not matched by any server block. .Pp @@ -148,6 +194,22 @@ Defaults to .Pp This path must be valid in the web server's URL space since browsers will attempt to fetch it. +.It Ic disable authentication +Disable authentication for this server, allowing any browser to view any +repository not hidden via the +.Ic respect_exportok +directive. +Authentication can also configured on a per-repository basis. +.Pp +If not specified, the global configuration context determines +whether authentication is disabled. +.It Ic enable authentication Oo Ic insecure Oc +Enable authentication, requiring browsers to present a login token cookie +before read-only repository access is granted. +Authentication can also configured on a per-repository basis. +.Pp +If not specified, the global configuration context determines +whether authentication is enabled. .It Ic logo_url Ar url Set a hyperlink for the logo. Defaults to @@ -173,6 +235,76 @@ The .Cm chroot directive must be used before the server declaration in order to take effect. +.It Ic repository Ar path Brq ... +Set options which apply to a particular repository served by this server. +.Pp +A repository context is declared with a unique +.Ar path , +followed by repository-specific configuration directives inside curly braces. +.Pp +The repository +.Ar path +is relative and will be looked up within the server's +.Ar repos_path , +where the repository +.Ar path +should exist with or without a +.Dq .git +suffix. +.Pp +If a repository exists in the server's +.Ar repos_path +without being mentioned in +.Nm +then the repository will be inaccessible if authentication is enabled. +.Pp +For each repository, access rules can be configured using the +.Ic permit +and +.Ic deny +configuration directives. +Multiple access rules can be specified, and the last matching rule +determines the action taken. +If no rule matches and authentication is enabled, +.Xr gotwebd 8 +will behave as if the repository did not exist to avoid revealing +the existence of secret repositories to unauthorized users. +.Pp +The available repository configuration directives are as follows: +.Bl -tag -width Ds +.It Ic deny Ar identity +Deny repository access to users with the username +.Ar identity . +Group names may be matched by prepending a colon +.Pq Sq \&: +to +.Ar identity . +Numeric IDs are also accepted. +.It Ic permit Ar identity +Permit repository access to users with the username +.Ar identity . +Group names may be matched by prepending a colon +.Pq Sq \&: +to +.Ar identity . +Numeric IDs are also accepted. +.It Ic disable authentication +Disable authentication, allowing any browser to view the repository. +Any access rules configured with +.Ic permit +or +.Ic deny +directives for this repository will be ignored. +.Pp +If not specified, the server context or global context determines +whether authentication is disabled. +.It Ic enable authentication Oo Ic insecure Oc +Enable authentication, requiring browsers to present a login token cookie +before read-only repository access is granted. +.Pp +If not specified, the server context or global context determines +whether authentication is enabled. +.El .It Ic respect_exportok Ar on | off Set whether to display the repository only if it contains the magic .Pa git-daemon-export-ok @@ -249,7 +381,7 @@ Default location for the socket. .El .Sh EXAMPLES -A sample configuration: +A sample configuration which allows public browsing: .Bd -literal -offset indent www user "www" # www username needs quotes since www is a keyword @@ -257,6 +389,7 @@ server "localhost" { site_name "my public repos" site_owner "Flan Hacker" site_link "Flan' Projects" + disable authentication } .Ed .Pp @@ -271,8 +404,53 @@ listen on ::1 port 9000 server "localhost" { site_name "my public repos" repos_path "/var/git" + disable authentication } .Ed +.Pp +The following example illustrates the use of directives related to +authentication: +.Bd -literal -offset indent +# 3 scopes: global, per-server, per-repository + +disable authentication # override the default which is 'enable' + +# Allow user "admin" to read anything unless overridden with a +# "deny" rule later. +permit "admin" + +server "public.example.com" { + # inherit global default, i.e. authentication is disabled + repos_path "/var/www/got/public" +} + +server "secure.example.com" { + enable authentication # override global default + + permit flan_squee # grant access to flan_squee + permit :developers # grant access to developers group + + repos_path "/var/git" + + repository "got" { # /var/git/got and /var/git/got.git + # Grant access to users who have authenticated as + # the anonymous user to gotsh(1), which anyone with + # an SSH client sbould be able to do. + # Dumb web crawlers will remain locked out. + permit anonymous + } + + repository "public" { + # As an exception, allow any web browsers and + # web crawlers to view this repository. + disable authentication + } + + repository "secret" { + deny admin # not even the admin can read this + } +} +.Ed .Sh SEE ALSO .Xr got 1 , .Xr httpd.conf 5 , blob - 58211c1296e9a12d3d7be515900874e2cfc64c6c blob + 458931bb31cdbfc98d734e47e30996ed8da3ac7b --- gotwebd/gotwebd.h +++ gotwebd/gotwebd.h @@ -39,6 +39,10 @@ #define GOTWEBD_WWW_USER "www" #endif +#define GOTWEBD_LOGIN_CMD "weblogin" +#define GOTWEBD_LOGIN_SOCKET "/var/run/gotweb-login.sock" +#define GOTWEBD_LOGIN_TIMEOUT 300 /* in seconds */ + #define GOTWEBD_MAXDESCRSZ 1024 #define GOTWEBD_MAXCLONEURLSZ 1024 #define GOTWEBD_CACHESIZE 1024 @@ -55,6 +59,8 @@ #define MAX_QUERYSTRING 2048 #define MAX_DOCUMENT_URI 255 #define MAX_SERVER_NAME 255 +#define MAX_AUTH_COOKIE 255 +#define MAX_IDENTIFIER_SIZE 32 #define GOTWEB_GIT_DIR ".git" @@ -125,6 +131,8 @@ enum gotwebd_proc_type { GOTWEBD_PROC_PARENT, GOTWEBD_PROC_SOCKETS, GOTWEBD_PROC_FCGI, + GOTWEBD_PROC_LOGIN, + GOTWEBD_PROC_AUTH, GOTWEBD_PROC_GOTWEB, }; @@ -132,11 +140,17 @@ enum imsg_type { GOTWEBD_IMSG_CFG_SRV, GOTWEBD_IMSG_CFG_SOCK, GOTWEBD_IMSG_CFG_FD, + GOTWEBD_IMSG_CFG_ACCESS_RULE, + GOTWEBD_IMSG_CFG_REPO, GOTWEBD_IMSG_CFG_DONE, GOTWEBD_IMSG_CTL_PIPE, GOTWEBD_IMSG_CTL_START, + GOTWEBD_IMSG_LOGIN_SECRET, + GOTWEBD_IMSG_AUTH_SECRET, + GOTWEBD_IMSG_AUTH_CONF, GOTWEBD_IMSG_FCGI_PARSE_PARAMS, GOTWEBD_IMSG_FCGI_PARAMS, + GOTWEBD_IMSG_WWW_UID, GOTWEBD_IMSG_REQ_ABORT, GOTWEBD_IMSG_REQ_PROCESS, }; @@ -275,6 +289,7 @@ struct querystring { char headref[MAX_DOCUMENT_URI]; int index_page; char path[PATH_MAX]; + char login[MAX_AUTH_COOKIE]; }; struct gotwebd_fcgi_params { @@ -282,6 +297,7 @@ struct gotwebd_fcgi_params { struct querystring qs; char document_uri[MAX_DOCUMENT_URI]; char server_name[MAX_SERVER_NAME]; + char auth_cookie[MAX_AUTH_COOKIE]; int https; }; @@ -312,6 +328,9 @@ struct request { int nparams_parsed; int client_status; + + uid_t client_uid; + char access_identifier[MAX_IDENTIFIER_SIZE]; }; TAILQ_HEAD(requestlist, request); @@ -339,6 +358,36 @@ struct address { }; TAILQ_HEAD(addresslist, address); +enum gotwebd_auth_config { + GOTWEBD_AUTH_DISABLED = 0xf00000ff, + GOTWEBD_AUTH_SECURE = 0x00808000, + GOTWEBD_AUTH_INSECURE = 0x0f7f7f00 +}; + +enum gotwebd_access { + GOTWEBD_ACCESS_NO_MATCH = -2, + GOTWEBD_ACCESS_DENIED = -1, + GOTWEBD_ACCESS_PERMITTED = 1 +}; + +struct gotwebd_access_rule { + STAILQ_ENTRY(gotwebd_access_rule) entry; + + enum gotwebd_access access; + char identifier[MAX_IDENTIFIER_SIZE]; +}; +STAILQ_HEAD(gotwebd_access_rule_list, gotwebd_access_rule); + +struct gotwebd_repo { + TAILQ_ENTRY(gotwebd_repo) entry; + + char name[NAME_MAX]; + + enum gotwebd_auth_config auth_config; + struct gotwebd_access_rule_list access_rules; +}; +TAILQ_HEAD(gotwebd_repolist, gotwebd_repo); + struct server { TAILQ_ENTRY(server) entry; @@ -363,6 +412,11 @@ struct server { int show_repo_description; int show_repo_cloneurl; int respect_exportok; + + enum gotwebd_auth_config auth_config; + struct gotwebd_access_rule_list access_rules; + + struct gotwebd_repolist repos; }; TAILQ_HEAD(serverlist, server); @@ -401,6 +455,12 @@ struct gotwebd { struct socketlist sockets; struct addresslist addresses; + struct socket *login_sock; + struct event login_pause_ev; + + enum gotwebd_auth_config auth_config; + struct gotwebd_access_rule_list access_rules; + int pack_fds[GOTWEB_PACK_NUM_TEMPFILES]; int priv_fd[PRIV_FDS__MAX]; @@ -414,13 +474,18 @@ struct gotwebd { struct imsgev *iev_parent; struct imsgev *iev_sockets; struct imsgev *iev_fcgi; + struct imsgev *iev_login; + struct imsgev *iev_gotsh; + struct imsgev *iev_auth; struct imsgev *iev_gotweb; uint16_t prefork; + int auth_pending; int gotweb_pending; int *worker_load; char httpd_chroot[PATH_MAX]; + uid_t www_uid; }; /* @@ -455,6 +520,7 @@ enum querystring_elements { HEADREF, INDEX_PAGE, PATH, + LOGIN, }; extern struct gotwebd *gotwebd_env; @@ -468,8 +534,12 @@ int imsg_compose_event(struct imsgev *, uint16_t, uin pid_t, int, const void *, size_t); int main_compose_sockets(struct gotwebd *, uint32_t, int, const void *, uint16_t); +int main_compose_login(struct gotwebd *, uint32_t, int, + const void *, uint16_t); int sockets_compose_main(struct gotwebd *, uint32_t, const void *, uint16_t); +int main_compose_auth(struct gotwebd *, uint32_t, int, + const void *, uint16_t); int main_compose_gotweb(struct gotwebd *, uint32_t, int, const void *, uint16_t); @@ -477,10 +547,26 @@ int main_compose_gotweb(struct gotwebd *, uint32_t, i void sockets(struct gotwebd *, int); void sockets_parse_sockets(struct gotwebd *); void sockets_socket_accept(int, short, void *); +struct socket *sockets_conf_new_socket(int, struct address *); int sockets_privinit(struct gotwebd *, struct socket *, uid_t, gid_t); void sockets_rlimit(int); +/* login.c */ +char *login_gen_token(uint64_t, const char *, time_t, const char *, size_t, + const char *); +int login_check_token(uid_t *, char **, const char *, const char *, size_t, + const char *); +int login_privinit(struct gotwebd *, uid_t, gid_t); +void gotwebd_login(struct gotwebd *, int); + +/* auth.c */ +void gotwebd_auth(struct gotwebd *, int); + /* gotweb.c */ +struct server *gotweb_get_server(const char *); +struct gotwebd_repo * gotweb_get_repository(struct server *, const char *); +int gotweb_reply(struct request *c, int status, const char *ctype, + struct gotweb_url *); void gotweb_index_navs(struct request *, struct gotweb_url *, int *, struct gotweb_url *, int *); int gotweb_render_age(struct template *, time_t); @@ -512,8 +598,10 @@ int gotweb_render_summary(struct template *); int gotweb_render_blame(struct template *); int gotweb_render_patch(struct template *); int gotweb_render_rss(struct template *); +int gotweb_render_unauthorized(struct template *); /* parse.y */ +struct gotwebd_repo * gotwebd_new_repo(const char *); int parse_config(const char *, struct gotwebd *); int cmdline_symset(char *); @@ -550,4 +638,9 @@ int config_getsock(struct gotwebd *, struct imsg *); int config_setfd(struct gotwebd *); int config_getfd(struct gotwebd *, struct imsg *); int config_getcfg(struct gotwebd *, struct imsg *); +void config_set_access_rules(struct imsgev *, + struct gotwebd_access_rule_list *); +void config_get_access_rule(struct gotwebd_access_rule_list *, struct imsg *); +void config_set_repository(struct imsgev *, struct gotwebd_repo *); +void config_get_repository(struct gotwebd_repolist *, struct imsg *); int config_init(struct gotwebd *); blob - 9ce2f4f4149824c1fbfbbe7bb7aea41082f89a13 blob + c525c3ba19fb057b82ee8fc2ec2d1d80f5f03191 --- gotwebd/pages.tmpl +++ gotwebd/pages.tmpl @@ -1370,3 +1370,7 @@ date: {{ datebuf }} {{ " UTC" }} {{ "\n" }} {{ mail }} {{" "}} ({{ author }}) {{ end }} + +{{ define gotweb_render_unauthorized(struct template *tp) }} +

Wrong or missing authentication code

+{{ end }} blob - 49402203bcfefd1783a8fe05ceee15fb09a60d9e blob + 33f11d6b75cf3899d8bb8312b7ea9ddbe58fe466 --- gotwebd/parse.y +++ gotwebd/parse.y @@ -99,10 +99,17 @@ int getservice(const char *); int n; int get_addrs(const char *, const char *); -int get_unix_addr(const char *); +struct address *get_unix_addr(const char *); int addr_dup_check(struct addresslist *, struct address *); void add_addr(struct address *); +static struct gotwebd_repo *new_repo; +static struct gotwebd_repo *conf_new_repo(struct server *, const char *); +static void conf_new_access_rule( + struct gotwebd_access_rule_list *, + enum gotwebd_access, char *); + + typedef struct { union { long long number; @@ -113,17 +120,19 @@ typedef struct { %} -%token LISTEN WWW SITE_NAME SITE_OWNER SITE_LINK LOGO +%token LISTEN LOGIN WWW SITE_NAME SITE_OWNER SITE_LINK LOGO %token LOGO_URL SHOW_REPO_OWNER SHOW_REPO_AGE SHOW_REPO_DESCRIPTION %token MAX_REPOS_DISPLAY REPOS_PATH MAX_COMMITS_DISPLAY ON ERROR %token SHOW_SITE_OWNER SHOW_REPO_CLONEURL PORT PREFORK RESPECT_EXPORTOK %token SERVER CHROOT CUSTOM_CSS SOCKET -%token SUMMARY_COMMITS_DISPLAY SUMMARY_TAGS_DISPLAY USER +%token SUMMARY_COMMITS_DISPLAY SUMMARY_TAGS_DISPLAY USER AUTHENTICATION +%token ENABLE DISABLE INSECURE REPOSITORY PERMIT DENY %token STRING %token NUMBER %type boolean %type listen_addr +%type numberstring %% @@ -153,6 +162,16 @@ varset : STRING '=' STRING { } ; +numberstring : STRING + | NUMBER { + if (asprintf(&$$, "%lld", (long long)$1) == -1) { + yyerror("asprintf: %s", strerror(errno)); + YYERROR; + } + } + ; + + boolean : STRING { if (strcasecmp($1, "1") == 0 || strcasecmp($1, "on") == 0) @@ -237,11 +256,15 @@ main : PREFORK NUMBER { free($3); } | LISTEN ON SOCKET STRING { - if (get_unix_addr($4) == -1) { + struct address *h; + + h = get_unix_addr($4); + if (h == NULL) { yyerror("can't listen on %s", $4); free($4); YYERROR; } + add_addr(h); free($4); } | USER STRING { @@ -256,6 +279,51 @@ main : PREFORK NUMBER { free(gotwebd->www_user); gotwebd->www_user = $3; } + | DISABLE AUTHENTICATION { + if (gotwebd->auth_config != 0) { + yyerror("ambiguous global authentication " + "setting"); + YYERROR; + } + gotwebd->auth_config = GOTWEBD_AUTH_DISABLED; + } + | ENABLE AUTHENTICATION { + if (gotwebd->auth_config != 0) { + yyerror("ambiguous global authentication " + "setting"); + YYERROR; + } + gotwebd->auth_config = GOTWEBD_AUTH_SECURE; + } + | ENABLE AUTHENTICATION INSECURE { + if (gotwebd->auth_config != 0) { + yyerror("ambiguous global authentication " + "setting"); + YYERROR; + } + gotwebd->auth_config = GOTWEBD_AUTH_INSECURE; + } + | PERMIT numberstring { + conf_new_access_rule(&gotwebd->access_rules, + GOTWEBD_ACCESS_PERMITTED, $2); + } + | DENY numberstring { + conf_new_access_rule(&gotwebd->access_rules, + GOTWEBD_ACCESS_DENIED, $2); + } + | LOGIN SOCKET STRING { + struct address *h; + h = get_unix_addr($3); + if (h == NULL) { + yyerror("can't listen on %s", $3); + free($3); + YYERROR; + } + if (gotwebd->login_sock != NULL) + free(gotwebd->login_sock); + gotwebd->login_sock = sockets_conf_new_socket(-1, h); + free($3); + } ; server : SERVER STRING { @@ -410,12 +478,108 @@ serveropts1 : REPOS_PATH STRING { } new_srv->summary_tags_display = $2; } + | DISABLE AUTHENTICATION { + if (new_srv->auth_config != 0) { + yyerror("ambiguous authentication " + "setting for server %s", + new_srv->name); + YYERROR; + } + new_srv->auth_config = GOTWEBD_AUTH_DISABLED; + } + | ENABLE AUTHENTICATION { + if (new_srv->auth_config != 0) { + yyerror("ambiguous authentication " + "setting for server %s", + new_srv->name); + YYERROR; + } + new_srv->auth_config = GOTWEBD_AUTH_SECURE; + } + | ENABLE AUTHENTICATION INSECURE { + if (new_srv->auth_config != 0) { + yyerror("ambiguous authentication " + "setting for server %s", + new_srv->name); + YYERROR; + } + new_srv->auth_config = GOTWEBD_AUTH_INSECURE; + } + | PERMIT numberstring { + conf_new_access_rule(&new_srv->access_rules, + GOTWEBD_ACCESS_PERMITTED, $2); + } + | DENY numberstring { + conf_new_access_rule(&new_srv->access_rules, + GOTWEBD_ACCESS_DENIED, $2); + } + | repository ; serveropts2 : serveropts2 serveropts1 nl | serveropts1 optnl ; +repository : REPOSITORY STRING { + struct gotwebd_repo *repo; + + TAILQ_FOREACH(repo, &new_srv->repos, entry) { + if (strcmp(repo->name, $2) == 0) { + yyerror("duplicate repository " + "'%s' in server '%s'", $2, + new_srv->name); + free($2); + YYERROR; + } + } + + new_repo = conf_new_repo(new_srv, $2); + free($2); + } '{' optnl repoopts2 '}' { + } + ; + +repoopts2 : repoopts2 repoopts1 nl + | repoopts1 optnl + ; + +repoopts1 : DISABLE AUTHENTICATION { + if (new_repo->auth_config != 0) { + yyerror("ambiguous authentication " + "setting for repository %s", + new_repo->name); + YYERROR; + } + new_repo->auth_config = GOTWEBD_AUTH_DISABLED; + } + | ENABLE AUTHENTICATION { + if (new_repo->auth_config != 0) { + yyerror("ambiguous authentication " + "setting for repository %s", + new_repo->name); + YYERROR; + } + new_repo->auth_config = GOTWEBD_AUTH_SECURE; + } + | ENABLE AUTHENTICATION INSECURE { + if (new_repo->auth_config != 0) { + yyerror("ambiguous authentication " + "setting for repository %s", + new_repo->name); + YYERROR; + } + new_repo->auth_config = GOTWEBD_AUTH_INSECURE; + } + | PERMIT numberstring { + conf_new_access_rule(&new_repo->access_rules, + GOTWEBD_ACCESS_PERMITTED, $2); + } + | DENY numberstring { + conf_new_access_rule(&new_repo->access_rules, + GOTWEBD_ACCESS_DENIED, $2); + } + ; + nl : '\n' optnl ; @@ -457,17 +621,25 @@ lookup(char *s) { /* This has to be sorted always. */ static const struct keywords keywords[] = { + { "authentication", AUTHENTICATION }, { "chroot", CHROOT }, { "custom_css", CUSTOM_CSS }, + { "deny", DENY }, + { "disable", DISABLE }, + { "enable", ENABLE }, + { "insecure", INSECURE }, { "listen", LISTEN }, + { "login", LOGIN }, { "logo", LOGO }, { "logo_url", LOGO_URL }, { "max_commits_display", MAX_COMMITS_DISPLAY }, { "max_repos_display", MAX_REPOS_DISPLAY }, { "on", ON }, + { "permit", PERMIT }, { "port", PORT }, { "prefork", PREFORK }, { "repos_path", REPOS_PATH }, + { "repository", REPOSITORY }, { "respect_exportok", RESPECT_EXPORTOK }, { "server", SERVER }, { "show_repo_age", SHOW_REPO_AGE }, @@ -815,6 +987,8 @@ int parse_config(const char *filename, struct gotwebd *env) { struct sym *sym, *next; + struct server *srv; + struct gotwebd_repo *repo; if (config_init(env) == -1) fatalx("failed to initialize configuration"); @@ -849,6 +1023,7 @@ parse_config(const char *filename, struct gotwebd *env /* add the implicit listen on socket */ if (TAILQ_EMPTY(&gotwebd->addresses)) { char path[_POSIX_PATH_MAX]; + struct address *h; if (strlcpy(path, gotwebd->httpd_chroot, sizeof(path)) >= sizeof(path)) { @@ -860,8 +1035,11 @@ parse_config(const char *filename, struct gotwebd *env yyerror("chroot path too long: %s", gotwebd->httpd_chroot); } - if (get_unix_addr(path) == -1) + h = get_unix_addr(path); + if (h == NULL) yyerror("can't listen on %s", path); + else + add_addr(h); } if (errors) @@ -870,6 +1048,39 @@ parse_config(const char *filename, struct gotwebd *env /* setup our listening sockets */ sockets_parse_sockets(env); + /* Add implicit login socket */ + if (gotwebd->login_sock == NULL) { + struct address *h; + h = get_unix_addr(GOTWEBD_LOGIN_SOCKET); + if (h == NULL) { + fprintf(stderr, "cannot listen on %s", + GOTWEBD_LOGIN_SOCKET); + return (-1); + } + gotwebd->login_sock = sockets_conf_new_socket(-1, h); + } + + /* Enable authentication if not explicitly configured. */ + switch (env->auth_config) { + case GOTWEBD_AUTH_SECURE: + case GOTWEBD_AUTH_INSECURE: + case GOTWEBD_AUTH_DISABLED: + break; + default: + env->auth_config = GOTWEBD_AUTH_SECURE; + break; + } + + /* Inherit implicit authentication config from parent scope. */ + TAILQ_FOREACH(srv, &env->servers, entry) { + if (srv->auth_config == 0) + srv->auth_config = env->auth_config; + TAILQ_FOREACH(repo, &srv->repos, entry) { + if (repo->auth_config == 0) + repo->auth_config = srv->auth_config; + } + } + return (0); } @@ -928,6 +1139,9 @@ conf_new_server(const char *name) srv->summary_commits_display = D_MAXSLCOMMDISP; srv->summary_tags_display = D_MAXSLTAGDISP; + STAILQ_INIT(&srv->access_rules); + TAILQ_INIT(&srv->repos); + TAILQ_INSERT_TAIL(&gotwebd->servers, srv, entry); return srv; @@ -1070,7 +1284,7 @@ get_addrs(const char *hostname, const char *servname) return (0); } -int +struct address * get_unix_addr(const char *path) { struct address *h; @@ -1089,11 +1303,10 @@ get_unix_addr(const char *path) if (strlcpy(sun->sun_path, path, sizeof(sun->sun_path)) >= sizeof(sun->sun_path)) { log_warnx("socket path too long: %s", sun->sun_path); - return (-1); + return NULL; } - add_addr(h); - return (0); + return h; } int @@ -1124,3 +1337,69 @@ add_addr(struct address *h) free(h); } + + +struct gotwebd_repo * +gotwebd_new_repo(const char *name) +{ + struct gotwebd_repo *repo; + + repo = calloc(1, sizeof(*repo)); + if (repo == NULL) + return NULL; + + STAILQ_INIT(&repo->access_rules); + + if (strlcpy(repo->name, name, sizeof(repo->name)) >= + sizeof(repo->name)) { + free(repo); + errno = ENOSPC; + return NULL; + } + + return repo; +} + +static struct gotwebd_repo * +conf_new_repo(struct server *server, const char *name) +{ + struct gotwebd_repo *repo; + + if (name[0] == '\0') { + fatalx("syntax error: empty repository name found in %s", + file->name); + } + + if (strchr(name, '/') != NULL) + fatalx("repository names must not contain slashes: %s", name); + + if (strchr(name, '\n') != NULL) + fatalx("repository names must not contain linefeeds: %s", name); + + repo = gotwebd_new_repo(name); + if (repo == NULL) + fatal("gotwebd_new_repo"); + + TAILQ_INSERT_TAIL(&server->repos, repo, entry); + + return repo; +}; + +static void +conf_new_access_rule(struct gotwebd_access_rule_list *rules, + enum gotwebd_access access, char *identifier) +{ + struct gotwebd_access_rule *rule; + + rule = calloc(1, sizeof(*rule)); + if (rule == NULL) + fatal("calloc"); + + rule->access = access; + if (strlcpy(rule->identifier, identifier, + sizeof(rule->identifier)) >= sizeof(rule->identifier)) + fatalx("identifier too long (max %zu bytes): %s", + sizeof(rule->identifier) - 1, identifier); + + STAILQ_INSERT_TAIL(rules, rule, entry); +} blob - /dev/null blob + 86706a2c4a5297def4342a9b94707e460d95f074 (mode 644) --- /dev/null +++ gotwebd/login.c @@ -0,0 +1,796 @@ +/* + * Copyright (c) 2025 Stefan Sperling + * Copyright (c) 2025 Omar Polo + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "got_error.h" +#include "got_reference.h" +#include "got_object.h" + +#include "gotwebd.h" +#include "log.h" + +#define LOGIN_SOCKET_BACKLOG 4 + +struct gotwebd_login_client { + int fd; + int cmd_done; + uid_t euid; + struct bufferevent *bev; +}; + +static volatile int client_cnt; +static int inflight; + +static char login_token_secret[32]; + +/* + * The token format is: + * + * "v1\0"[issued at/64bit][expire/64bit][uid/64bit][host]"\0" + * + * Padded with additional \0 to a length divisible by 4, and then + * followed by the HMAC-SHA256 of it, all encoded in base64. + */ + +/* checks whether the token's signature matches, i.e. if it looks good. */ +int +login_check_token(uid_t *euid, char **hostname, + const char *token, const char *secret, size_t secret_len, + const char *purpose) +{ + time_t now; + uint64_t issued, expire, uid; + uint8_t *data; + int len; + char hmac[32], exp[32]; + unsigned int explen; + size_t used = 0; + + /* + * We get called in several processes which should all have + * a copy of the same secret. + */ + if (secret_len != sizeof(login_token_secret)) + fatalx("%s token secret length mismatch", purpose); + + /* xxx check for overflow */ + len = (strlen(token) / 4) * 3; + + data = malloc(len); + if (data == NULL) + return -1; + + len = EVP_DecodeBlock(data, token, strlen(token)); + if (len == -1) { + free(data); + return -1; + } + if ((len % 4) != 0) { + log_warnx("bad %s token length: %d", purpose, len); + free(data); + return -1; + } + + if (len < 28 + 32) { /* min length assuming empty hostname */ + log_warnx("%s token too short: %d", purpose, len); + free(data); + return -1; + } + + if (memcmp(data, "v1", 3) != 0) { + log_warnx("unknown %s token format version", purpose); + free(data); + return -1; + } + used = 3; + + if (HMAC(EVP_sha256(), secret, secret_len, data, len - 32, + exp, &explen) == NULL) { + log_warnx("HMAC computation failed\n"); + free(data); + return -1; + } + + if (explen != 32) { + log_warnx("unexpected HMAC length: %u\n", explen); + free(data); + return -1; + } + + memcpy(hmac, data + len - explen, explen); + + if (memcmp(hmac, exp, explen) != 0) { + log_warnx("HMAC check failed\n"); + free(data); + return -1; + } + + memcpy(&issued, data + used, sizeof(issued)); + used += sizeof(issued); + + memcpy(&expire, data + used, sizeof(expire)); + used += sizeof(expire); + + memcpy(&uid, data + used, sizeof(uid)); + used += sizeof(uid); + + now = time(NULL); + if (expire < now) { + log_warnx("uid %llu: %s token has expired\n", uid, purpose); + free(data); + return -1; + } + + if (euid) + *euid = (uid_t)uid; + + if (hostname) { + if (used < len - explen) { + *hostname = strndup(data + used, + len - explen - used); + if (*hostname == NULL) { + log_warn("strndup"); + free(data); + return -1; + } + } else { + *hostname = strdup(""); + if (*hostname == NULL) { + log_warn("strdup"); + free(data); + return -1; + } + } + } + + free(data); + return 0; +} + +char * +login_gen_token(uint64_t uid, const char *hostname, time_t validity, + const char *secret, size_t secret_len, const char *purpose) +{ + BIO *bmem, *b64; + BUF_MEM *bufm; + char hmac[EVP_MAX_MD_SIZE]; + char *enc; + FILE *fp; + char *tok; + time_t now; + uint64_t issued, expire; + size_t siz, hlen; + unsigned int hmaclen; /* openssl... */ + + /* + * We get called in several processes which should all have + * a copy of the same secret. + */ + if (secret_len != sizeof(login_token_secret)) + fatalx("%s token secret length mismatch", purpose); + + now = time(NULL); + issued = (uint64_t)now; + expire = issued + validity; + + fp = open_memstream(&tok, &siz); + if (fp == NULL) + return NULL; + + /* include NUL */ + hlen = strlen(hostname) + 1; + + if (fwrite("v1", 1, 3, fp) != 3 || + fwrite(&issued, 1, 8, fp) != 8 || + fwrite(&expire, 1, 8, fp) != 8 || + fwrite(&uid, 1, 8, fp) != 8 || + fwrite(hostname, 1, hlen, fp) != hlen) { + fclose(fp); + free(tok); + return NULL; + } + + /* Pad token with trailing NULs for base64 encoding. */ + while (((3 + 8 + 8 + 8 + hlen) % 4) != 0) { + if (fwrite("", 1, 1, fp) != 1) { + fclose(fp); + free(tok); + return NULL; + } + hlen++; + } + + if (fclose(fp) == EOF) { + free(tok); + return NULL; + } + + /* Base64 encoder expects a length divisible by 4. */ + if ((siz % 4) != 0) + fatalx("generated %s token with bad size %zu", purpose, siz); + + if (siz > INT_MAX) { + /* + * can't really happen, isn't it? yet, openssl + * sometimes likes to take ints so I'd prefer to + * assert. + */ + free(tok); + return NULL; + } + + if (HMAC(EVP_sha256(), secret, secret_len, tok, siz, + hmac, &hmaclen) == NULL) { + free(tok); + return NULL; + } + + bmem = BIO_new(BIO_s_mem()); + if (bmem == NULL) { + free(tok); + return NULL; + } + + b64 = BIO_new(BIO_f_base64()); + if (b64 == NULL) { + BIO_free(bmem); + free(tok); + return NULL; + } + + BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); + b64 = BIO_push(b64, bmem); + + if (BIO_write(b64, tok, siz) != (int)siz || + BIO_write(b64, hmac, hmaclen) != hmaclen || + BIO_flush(b64) <= 0) { + free(tok); + BIO_free_all(b64); + return NULL; + } + + BIO_get_mem_ptr(b64, &bufm); + enc = strndup(bufm->data, bufm->length); + + free(tok); + BIO_free_all(b64); + + if (login_check_token(NULL, NULL, enc, secret, secret_len, + purpose) == -1) + fatalx("generated %s token that won't pass validation", + purpose); + + return enc; +} + +static int +login_socket_listen(struct gotwebd *env, struct socket *sock, + uid_t uid, gid_t gid) +{ + int u_fd = -1; + mode_t old_umask, mode; + + u_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK| SOCK_CLOEXEC, 0); + if (u_fd == -1) { + log_warn("%s: socket", __func__); + return -1; + } + + if (unlink(sock->conf.unix_socket_name) == -1) { + if (errno != ENOENT) { + log_warn("%s: unlink %s", __func__, + sock->conf.unix_socket_name); + close(u_fd); + return -1; + } + } + + old_umask = umask(S_IXUSR|S_IXGRP|S_IWOTH|S_IROTH|S_IXOTH); + mode = S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH; + + if (bind(u_fd, (struct sockaddr *)&sock->conf.addr.ss, + sock->conf.addr.slen) == -1) { + log_warn("%s: bind: %s", __func__, sock->conf.unix_socket_name); + close(u_fd); + (void)umask(old_umask); + return -1; + } + + (void)umask(old_umask); + + if (chmod(sock->conf.unix_socket_name, mode) == -1) { + log_warn("%s: chmod", __func__); + close(u_fd); + (void)unlink(sock->conf.unix_socket_name); + return -1; + } + + if (chown(sock->conf.unix_socket_name, uid, gid) == -1) { + log_warn("%s: chown", __func__); + close(u_fd); + (void)unlink(sock->conf.unix_socket_name); + return -1; + } + + if (listen(u_fd, LOGIN_SOCKET_BACKLOG) == -1) { + log_warn("%s: listen", __func__); + return -1; + } + + return u_fd; +} + +int +login_privinit(struct gotwebd *env, uid_t uid, gid_t gid) +{ + struct socket *sock = env->login_sock; + + if (sock == NULL) + fatalx("no login socket configured"); + + log_info("initializing login socket %s", + sock->conf.unix_socket_name); + + sock->fd = login_socket_listen(env, sock, uid, gid); + if (sock->fd == -1) + return -1; + + return 0; +} + +static void +login_shutdown(void) +{ + struct gotwebd *env = gotwebd_env; + + imsgbuf_clear(&env->iev_parent->ibuf); + free(env->iev_parent); + if (env->iev_gotsh) { + imsgbuf_clear(&env->iev_gotsh->ibuf); + free(env->iev_gotsh); + } + free(env); + + exit(0); +} + +static void +login_sighdlr(int sig, short event, void *arg) +{ + switch (sig) { + case SIGHUP: + log_info("%s: ignoring SIGHUP", __func__); + break; + case SIGPIPE: + log_info("%s: ignoring SIGPIPE", __func__); + break; + case SIGUSR1: + log_info("%s: ignoring SIGUSR1", __func__); + break; + case SIGCHLD: + break; + case SIGINT: + case SIGTERM: + login_shutdown(); + break; + default: + log_warn("unexpected signal %d", sig); + break; + } +} + +static int +accept_reserve(int fd, struct sockaddr *addr, socklen_t *addrlen, + int reserve, volatile int *counter) +{ + int ret; + + if (getdtablecount() + reserve + + ((*counter + 1) * FD_NEEDED) >= getdtablesize()) { + log_debug("inflight fds exceeded"); + errno = EMFILE; + return -1; + } + + if ((ret = accept4(fd, addr, addrlen, + SOCK_NONBLOCK | SOCK_CLOEXEC)) > -1) { + (*counter)++; + } + + return ret; +} + +static void +client_err(struct bufferevent *bev, short error, void *d) +{ + struct gotwebd_login_client *client = d; + + log_debug("closing connection with client fd=%d; error=%d", + client->fd, error); + + bufferevent_free(client->bev); + close(client->fd); + free(client); + + inflight--; + client_cnt--; +} + +static void +client_read(struct bufferevent *bev, void *d) +{ + struct gotwebd_login_client *client = d; + struct evbuffer *in = EVBUFFER_INPUT(bev); + struct evbuffer *out = EVBUFFER_OUTPUT(bev); + char *line, *cmd, *code; + size_t linelen; + const char *hostname; + + if (client->cmd_done) { + log_warnx("%s: client sent data even though login command " + "has already completed", __func__); + client_err(bev, EVBUFFER_READ, client); + return; + } + + line = evbuffer_readln(in, &linelen, EVBUFFER_EOL_LF); + if (line == NULL) { + /* + * there is no line yet to read. however, error if we + * have too much data buffered without a newline + * character. + */ + if (EVBUFFER_LENGTH(in) > LINE_MAX) + client_err(bev, EVBUFFER_READ, client); + return; + } + + cmd = line; + if (strncmp(cmd, "login", 5) == 0) { + cmd += 5; + cmd += strspn(cmd, " \t"); + hostname = cmd; + if (hostname[0] == '\0') { + struct server *srv; + + /* + * In a multi-server setup we do not want to leak our + * first server's hostname to random people. But if + * we only have a single server, we'll expose it. + */ + srv = TAILQ_FIRST(&gotwebd_env->servers); + if (TAILQ_NEXT(srv, entry) == NULL) + hostname = srv->name; + } + + code = login_gen_token(client->euid, hostname, + 5 * 60 /* 5 minutes */, + login_token_secret, sizeof(login_token_secret), "login"); + if (code == NULL) { + log_warn("%s: login_gen_token failed", __func__); + client_err(bev, EVBUFFER_READ, client); + return; + } + + if (evbuffer_add_printf(out, "ok https://%s/?login=%s\n", + hostname, code) == -1) { + log_warnx("%s: evbuffer_add_printf failed", __func__); + client_err(bev, EVBUFFER_READ, client); + free(code); + return; + } + free(code); + + client->cmd_done = 1; + bufferevent_enable(client->bev, EV_READ|EV_WRITE); + return; + } + + if (evbuffer_add_printf(out, "err unknown command\n") == -1) { + log_warnx("%s: evbuffer_add_printf failed", __func__); + client_err(bev, EVBUFFER_READ, client); + return; + } + + client->cmd_done = 1; + return; +} + +static void +client_write(struct bufferevent *bev, void *d) +{ + struct gotwebd_login_client *client = d; + struct evbuffer *out = EVBUFFER_OUTPUT(bev); + + if (client->cmd_done && EVBUFFER_LENGTH(out) == 0) { + /* reply sent */ + client_err(bev, EVBUFFER_WRITE, client); + return; + } +} + +static void +login_accept(int fd, short event, void *arg) +{ + struct imsgev *iev = arg; + struct gotwebd *env = gotwebd_env; + struct sockaddr_storage ss; + struct timeval backoff; + socklen_t len; + int s = -1; + struct gotwebd_login_client *client = NULL; + uid_t euid; + gid_t egid; + + backoff.tv_sec = 1; + backoff.tv_usec = 0; + + if (event_add(&iev->ev, NULL) == -1) { + log_warn("event_add"); + return; + } + if (event & EV_TIMEOUT) + return; + + len = sizeof(ss); + + /* Other backoff conditions apart from EMFILE/ENFILE? */ + s = accept_reserve(fd, (struct sockaddr *)&ss, &len, FD_RESERVE, + &inflight); + if (s == -1) { + switch (errno) { + case EINTR: + case EWOULDBLOCK: + case ECONNABORTED: + return; + case EMFILE: + case ENFILE: + event_del(&iev->ev); + evtimer_add(&env->login_pause_ev, &backoff); + return; + default: + log_warn("accept"); + return; + } + } + + if (client_cnt >= GOTWEBD_MAXCLIENTS) + goto err; + + if (getpeereid(s, &euid, &egid) == -1) { + log_warn("getpeerid"); + goto err; + } + + client = calloc(1, sizeof(*client)); + if (client == NULL) { + log_warn("%s: calloc", __func__); + goto err; + } + client->fd = s; + client->euid = euid; + s = -1; + + client->bev = bufferevent_new(client->fd, client_read, client_write, + client_err, client); + if (client->bev == NULL) { + log_warn("%s: bufferevent_new failed", __func__); + goto err; + } + bufferevent_enable(client->bev, EV_READ|EV_WRITE); + + /* + * undocumented; but these are seconds. 10s should be plenty + * for both receiving a request and sending the reply. + */ + bufferevent_settimeout(client->bev, 10, 10); + + log_debug("%s: new client connected on fd %d uid %d gid %d", __func__, + client->fd, euid, egid); + client_cnt++; + return; +err: + inflight--; + if (client) { + if (client->bev != NULL) + bufferevent_free(client->bev); + close(client->fd); + free(client); + } + if (s != -1) + close(s); +} + +static void +get_login_sock(struct gotwebd *env, struct imsg *imsg) +{ + const struct got_error *err; + struct imsgev *iev; + int fd; + + if (env->iev_gotsh != NULL) { + err = got_error(GOT_ERR_PRIVSEP_MSG); + fatalx("%s", err->msg); + } + + if (IMSG_DATA_SIZE(imsg) != 0) { + err = got_error(GOT_ERR_PRIVSEP_LEN); + fatalx("%s", err->msg); + } + + fd = imsg_get_fd(imsg); + if (fd == -1) { + err = got_error(GOT_ERR_PRIVSEP_NO_FD); + fatalx("%s", err->msg); + } + + iev = calloc(1, sizeof(*iev)); + if (iev == NULL) + fatal("calloc"); + + if (imsgbuf_init(&iev->ibuf, fd) == -1) + fatal("imsgbuf_init"); + + iev->handler = login_accept; + iev->data = iev; + event_set(&iev->ev, fd, EV_READ, login_accept, iev); + imsg_event_add(iev); + + env->iev_gotsh = iev; +} + +static void +login_launch(struct gotwebd *env) +{ +#ifndef PROFILE + if (pledge("stdio unix", NULL) == -1) + fatal("pledge"); +#endif + event_add(&env->iev_gotsh->ev, NULL); +} + +static void +login_dispatch_main(int fd, short event, void *arg) +{ + struct imsgev *iev = arg; + struct imsgbuf *ibuf; + struct imsg imsg; + struct gotwebd *env = gotwebd_env; + ssize_t n; + int shut = 0; + + ibuf = &iev->ibuf; + + if (event & EV_READ) { + if ((n = imsgbuf_read(ibuf)) == -1) + fatal("imsgbuf_read error"); + if (n == 0) /* Connection closed */ + shut = 1; + } + if (event & EV_WRITE) { + if (imsgbuf_write(ibuf) == -1) + fatal("imsgbuf_write"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("imsg_get"); + if (n == 0) /* No more messages. */ + break; + + switch (imsg.hdr.type) { + case GOTWEBD_IMSG_CFG_SRV: + config_getserver(env, &imsg); + break; + case GOTWEBD_IMSG_CFG_SOCK: + get_login_sock(env, &imsg); + break; + case GOTWEBD_IMSG_CTL_START: + login_launch(env); + break; + case GOTWEBD_IMSG_LOGIN_SECRET: + if (imsg_get_data(&imsg, login_token_secret, + sizeof(login_token_secret)) == -1) + fatalx("invalid LOGIN_SECRET msg"); + break; + default: + fatalx("%s: unknown imsg type %d", __func__, + imsg.hdr.type); + } + + imsg_free(&imsg); + } + + if (!shut) + imsg_event_add(iev); + else { + /* This pipe is dead. Remove its event handler */ + event_del(&iev->ev); + event_loopexit(NULL); + } +} + +static void +accept_paused(int fd, short event, void *arg) +{ + struct gotwebd *env = gotwebd_env; + + event_add(&env->iev_gotsh->ev, NULL); +} + +void +gotwebd_login(struct gotwebd *env, int fd) +{ + struct event sighup, sigint, sigusr1, sigchld, sigterm; + struct event_base *evb; + + evb = event_init(); + + if ((env->iev_parent = malloc(sizeof(*env->iev_parent))) == NULL) + fatal("malloc"); + if (imsgbuf_init(&env->iev_parent->ibuf, fd) == -1) + fatal("imsgbuf_init"); + imsgbuf_allow_fdpass(&env->iev_parent->ibuf); + env->iev_parent->handler = login_dispatch_main; + env->iev_parent->data = env->iev_parent; + event_set(&env->iev_parent->ev, fd, EV_READ, login_dispatch_main, + env->iev_parent); + event_add(&env->iev_parent->ev, NULL); + evtimer_set(&env->login_pause_ev, accept_paused, NULL); + + signal(SIGPIPE, SIG_IGN); + + signal_set(&sighup, SIGHUP, login_sighdlr, env); + signal_add(&sighup, NULL); + signal_set(&sigint, SIGINT, login_sighdlr, env); + signal_add(&sigint, NULL); + signal_set(&sigusr1, SIGUSR1, login_sighdlr, env); + signal_add(&sigusr1, NULL); + signal_set(&sigchld, SIGCHLD, login_sighdlr, env); + signal_add(&sigchld, NULL); + signal_set(&sigterm, SIGTERM, login_sighdlr, env); + signal_add(&sigterm, NULL); + +#ifndef PROFILE + if (pledge("stdio recvfd unix", NULL) == -1) + fatal("pledge"); +#endif + + event_dispatch(); + event_base_free(evb); + login_shutdown(); +} + blob - ab9b70f72224ae1422ba89b2c55c42cd3bcd29fb blob + 683166c215dd2b45e82b2a5dc448b9f48b7f1ac7 --- gotwebd/sockets.c +++ gotwebd/sockets.c @@ -77,9 +77,6 @@ static int sockets_create_socket(struct address *); static int sockets_accept_reserve(int, struct sockaddr *, socklen_t *, int, volatile int *); -static struct socket *sockets_conf_new_socket(struct gotwebd *, - int, struct address *); - int cgi_inflight = 0; /* Request hash table needs some spare room to avoid collisions. */ @@ -237,10 +234,10 @@ sockets(struct gotwebd *env, int fd) if (env->prefork <= 0) fatalx("invalid prefork count: %d", env->prefork); - env->iev_gotweb = calloc(env->prefork, sizeof(*env->iev_gotweb)); - if (env->iev_gotweb == NULL) + env->iev_auth = calloc(env->prefork, sizeof(*env->iev_auth)); + if (env->iev_auth == NULL) fatal("calloc"); - env->gotweb_pending = env->prefork; + env->auth_pending = env->prefork; signal(SIGPIPE, SIG_IGN); @@ -273,7 +270,7 @@ sockets_parse_sockets(struct gotwebd *env) int sock_id = 1; TAILQ_FOREACH(a, &env->addresses, entry) { - new_sock = sockets_conf_new_socket(env, sock_id, a); + new_sock = sockets_conf_new_socket(sock_id, a); if (new_sock) { sock_id++; TAILQ_INSERT_TAIL(&env->sockets, @@ -282,8 +279,8 @@ sockets_parse_sockets(struct gotwebd *env) } } -static struct socket * -sockets_conf_new_socket(struct gotwebd *env, int id, struct address *a) +struct socket * +sockets_conf_new_socket(int id, struct address *a) { struct socket *sock; struct address *acp; @@ -335,8 +332,8 @@ sockets_launch(struct gotwebd *env) if (env->iev_fcgi == NULL) fatalx("fcgi process not connected"); - if (env->gotweb_pending != 0) - fatal("gotweb process not connected"); + if (env->auth_pending != 0) + fatal("auth process not connected"); TAILQ_FOREACH(sock, &gotwebd_env->sockets, entry) { if (sock->conf.af_type == AF_UNIX) { @@ -379,7 +376,7 @@ sockets_launch(struct gotwebd *env) #endif event_add(&env->iev_fcgi->ev, NULL); for (i = 0; i < env->prefork; i++) - event_add(&env->iev_gotweb[i].ev, NULL); + event_add(&env->iev_auth[i].ev, NULL); } static void @@ -401,7 +398,7 @@ abort_request(struct imsg *imsg) } static void -server_dispatch_gotweb(int fd, short event, void *arg) +server_dispatch_auth(int fd, short event, void *arg) { struct imsgev *iev = arg; struct imsgbuf *ibuf; @@ -450,32 +447,32 @@ server_dispatch_gotweb(int fd, short event, void *arg) } static void -recv_gotweb_pipe(struct gotwebd *env, struct imsg *imsg) +recv_auth_pipe(struct gotwebd *env, struct imsg *imsg) { struct imsgev *iev; int fd; - if (env->gotweb_pending <= 0) { - log_warn("gotweb pipe already received"); + if (env->auth_pending <= 0) { + log_warn("all auth pipes already received"); return; } fd = imsg_get_fd(imsg); if (fd == -1) - fatalx("invalid gotweb pipe fd"); + fatalx("invalid auth pipe fd"); - iev = &env->iev_gotweb[env->gotweb_pending - 1]; + iev = &env->iev_auth[env->auth_pending - 1]; if (imsgbuf_init(&iev->ibuf, fd) == -1) fatal("imsgbuf_init"); imsgbuf_allow_fdpass(&iev->ibuf); imsgbuf_set_maxsize(&iev->ibuf, sizeof(struct request)); - iev->handler = server_dispatch_gotweb; + iev->handler = server_dispatch_auth; iev->data = iev; - event_set(&iev->ev, fd, EV_READ, server_dispatch_gotweb, iev); + event_set(&iev->ev, fd, EV_READ, server_dispatch_auth, iev); imsg_event_add(iev); - env->gotweb_pending--; + env->auth_pending--; } static struct imsgev * @@ -498,15 +495,16 @@ select_worker(struct request *c) c->request_id, least_busy_worker_idx); c->worker_idx = least_busy_worker_idx; - return &env->iev_gotweb[least_busy_worker_idx]; + return &env->iev_auth[least_busy_worker_idx]; } static int process_request(struct request *c) { struct gotwebd *env = gotwebd_env; - struct querystring *qs = &c->fcgi_params.qs; - struct imsgev *iev_gotweb; + struct gotwebd_fcgi_params *params = &c->fcgi_params; + struct querystring *qs = ¶ms->qs; + struct imsgev *iev_auth; int ret, i; struct request ic; @@ -522,7 +520,17 @@ process_request(struct request *c) return -1; } } + if (params->server_name[0] == '\0') { + struct server *srv = TAILQ_FIRST(&env->servers); + if (strlcpy(params->server_name, srv->name, + sizeof(params->server_name)) >= + sizeof(params->server_name)) { + log_warnx("server name buffer too small"); + return -1; + } + } + memcpy(&ic, c, sizeof(ic)); /* Don't leak pointers from our address space to another process. */ @@ -538,8 +546,8 @@ process_request(struct request *c) ic.priv_fd[i] = -1; ic.fd = -1; - iev_gotweb = select_worker(c); - ret = imsg_compose_event(iev_gotweb, GOTWEBD_IMSG_REQ_PROCESS, + iev_auth = select_worker(c); + ret = imsg_compose_event(iev_auth, GOTWEBD_IMSG_REQ_PROCESS, GOTWEBD_PROC_SOCKETS, -1, c->fd, &ic, sizeof(ic)); if (ret == -1) { log_warn("imsg_compose_event"); @@ -617,6 +625,13 @@ recv_parsed_params(struct imsg *imsg) goto fail; } + if (params.qs.login[0] != '\0' && + strlcpy(p->qs.login, params.qs.login, + sizeof(p->qs.login)) >= sizeof(p->qs.login)) { + log_warnx("login token too long"); + goto fail; + } + if (params.document_uri[0] != '\0' && strlcpy(p->document_uri, params.document_uri, sizeof(p->document_uri)) >= sizeof(p->document_uri)) { @@ -631,6 +646,13 @@ recv_parsed_params(struct imsg *imsg) goto fail; } + if (params.auth_cookie[0] != '\0' && + strlcpy(p->auth_cookie, params.auth_cookie, + sizeof(p->auth_cookie)) >= sizeof(p->auth_cookie)) { + log_warnx("auth cookie too long"); + goto fail; + } + if (params.https && !p->https) p->https = 1; @@ -774,7 +796,7 @@ sockets_dispatch_main(int fd, short event, void *arg) if (env->iev_fcgi == NULL) recv_fcgi_pipe(env, &imsg); else - recv_gotweb_pipe(env, &imsg); + recv_auth_pipe(env, &imsg); break; case GOTWEBD_IMSG_CTL_START: sockets_launch(env); @@ -860,8 +882,8 @@ sockets_shutdown(void) free(gotwebd_env->iev_fcgi); for (i = 0; i < gotwebd_env->prefork; i++) - imsgbuf_clear(&gotwebd_env->iev_gotweb[i].ibuf); - free(gotwebd_env->iev_gotweb); + imsgbuf_clear(&gotwebd_env->iev_auth[i].ibuf); + free(gotwebd_env->iev_auth); free(gotwebd_env->worker_load); free(gotwebd_env); blob - d9cb7ba02a04c7708ffd8b434a1ff6d2b567e681 blob + 6a8c2beac69184d483845a613349d8c35c5d7bff --- include/got_error.h +++ include/got_error.h @@ -197,6 +197,8 @@ #define GOT_ERR_AUTHORIZED_KEY 180 #define GOT_ERR_CONNECTION_LIMIT 190 #define GOT_ERR_ON_SERVER_SIDE 191 +#define GOT_ERR_LOGIN_FAILED 192 +#define GOT_ERR_UNKNOWN_COMMAND 193 struct got_error { int code; blob - 8140daf14f5dd531f79f071964fa9ecc20349c17 blob + 641aa3dc732ea1dcca0e663e75295368e54b0684 --- lib/error.c +++ lib/error.c @@ -248,6 +248,8 @@ static const struct got_error got_errors[] = { { GOT_ERR_AUTHORIZED_KEY, "no authorized key found" }, { GOT_ERR_CONNECTION_LIMIT, "connection limit exceeded" }, { GOT_ERR_ON_SERVER_SIDE, "see server-side logs for error details" }, + { GOT_ERR_LOGIN_FAILED, "login failed" }, + { GOT_ERR_UNKNOWN_COMMAND, "command not found" }, }; static struct got_custom_error { blob - 7575ba1143c38b29a5fe45fff0e1fb0d08ac66fe blob + 11ebb5f52fd40f16c8bafd8cdb372f50a8359d30 --- regress/gotsysd/Makefile +++ regress/gotsysd/Makefile @@ -1,6 +1,6 @@ .include "../../got-version.mk" -REGRESS_TARGETS=test_gotsysd +REGRESS_TARGETS=test_gotsysd test_gotwebd REGRESS_SETUP_ONCE=setup_test_vm REGRESS_CLEANUP=stop_test_vm @@ -220,8 +220,27 @@ $(HTTPD_CONF): @${UNPRIV} 'echo } >> $@' $(GOTWEBD_CONF): - @${UNPRIV} 'echo server "VMIP" { > $@' + @${UNPRIV} 'echo prefork 1 > $@' + @${UNPRIV} 'echo enable authentication insecure >> $@' + @${UNPRIV} 'echo permit ${GOTSYSD_TEST_USER} >> $@' + @${UNPRIV} 'echo deny ${GOTSYSD_DEV_USER} >> $@' + @${UNPRIV} 'echo server \"VMIP\" { >> $@' @${UNPRIV} 'echo \ \ repos_path \"/git\" >> $@' + @${UNPRIV} 'echo \ \ show_repo_age off >> $@' + @${UNPRIV} 'echo \ \ show_repo_description off >> $@' + @${UNPRIV} 'echo \ \ show_repo_owner off >> $@' + @${UNPRIV} 'echo \ \ show_site_owner off >> $@' + @${UNPRIV} 'echo \ \ repository \"public\" { >> $@' + @${UNPRIV} 'echo \ \ \ \ disable authentication >> $@' + @${UNPRIV} 'echo \ \ } >> $@' + @${UNPRIV} 'echo \ \ repository \"gotdev\" { >> $@' + @${UNPRIV} 'echo \ \ \ \ permit ${GOTSYSD_DEV_USER} >> $@' + @${UNPRIV} 'echo \ \ \ \ deny ${GOTSYSD_TEST_USER} >> $@' + @${UNPRIV} 'echo \ \ } >> $@' + @${UNPRIV} 'echo \ \ repository \"gottest\" { >> $@' + @${UNPRIV} 'echo \ \ \ \ permit ${GOTSYSD_TEST_USER} >> $@' + @${UNPRIV} 'echo \ \ \ \ deny ${GOTSYSD_DEV_USER} >> $@' + @${UNPRIV} 'echo \ \ } >> $@' @${UNPRIV} 'echo } >> $@' build_got: @@ -313,5 +332,29 @@ test_gotsysd: GWIP="100.64.$$VMID.2"; \ ${UNPRIV} "env ${GOTSYSD_TEST_ENV} VMIP=$${VMIP} GWIP=$${GWIP} \ sh ./test_gotsysd.sh" +test_gotwebd: + @set -e; \ + VMID=`vmctl status ${GOTSYSD_VM_NAME} | tail -n1 | \ + awk '{print $$1}'`; \ + VMIP="100.64.$$VMID.3"; \ + GWIP="100.64.$$VMID.2"; \ + ${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \ + 'rm -rf /git/gotsys.git /git/foo.git /tmp/gotsys'"; \ + ${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \ + got init /git/${GOTSYS_REPO}"; \ + ${UNPRIV} "${GOTSYSD_SCP_CMD} \ + ${GOT_CONF} root@$${VMIP}:/git/${GOTSYS_REPO}/got.conf"; \ + ${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} mkdir /tmp/gotsys "; \ + ${UNPRIV} "${GOTSYSD_SCP_CMD} \ + ${GOTSYS_CONF} root@$${VMIP}:/tmp/gotsys/"; \ + ${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \ + got import -m init -r /git/${GOTSYS_REPO} \ + /tmp/gotsys >/dev/null"; \ + ${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \ + chown -R _gotd:_gotd /git"; \ + ${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \ + gotsys apply -w > /dev/null"; \ + ${UNPRIV} "env ${GOTSYSD_TEST_ENV} VMIP=$${VMIP} GWIP=$${GWIP} \ + sh ./test_gotwebd.sh" .include blob - 0d5b799a0279c9df6ddd6a9f06c4e851be7721f0 blob + a328ed29230e4a342dcb5e9b6d2a623b2201ca3d --- regress/gotsysd/README +++ regress/gotsysd/README @@ -44,6 +44,11 @@ the VM can be shut down from the host: # vmctl stop gotsysd-test +The gotsysd test suite also tests gotwebd user authentication. These tests +require the w3m browser to be installed: + + # pkg_add w3m + Now the actual test suite can be run with: # make blob - 1cd41413224f10c3733c7889db4c13367beed25b blob + 3d49255fa3386b79e02d4fe6d51fe4ad6caf88cd --- regress/gotsysd/test_gotsysd.sh +++ regress/gotsysd/test_gotsysd.sh @@ -230,6 +230,7 @@ EOF cat > $testroot/stderr.expected < $testroot/stderr.expected < +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +. ../cmdline/common.sh +. ./common.sh + +test_login() { + local testroot=`test_init login 1` + + # Attempt unauthorized access. + w3m "http://${VMIP}/" -dump > $testroot/stdout + cat > $testroot/stdout.expected < $testroot/stdout + ret=$? + if [ $ret -ne 0 ]; then + echo "ssh login failed failed unexpectedly" >&2 + test_done "$testroot" 1 + return 1 + fi + + # Request the index page again using the login token and + # storing the cookie sent by gotwebd. + url=$(cut -d: -f 2,3 < $testroot/stdout | sed -e 's/ https:/http:/') + w3m -cookie-jar "$testroot/cookies" "$url" -dump > $testroot/stdout + cat > $testroot/stdout.expected < $testroot/stdout + + local commit_id=`git_show_head $testroot/${GOTSYS_REPO}` + local commit_time=`git_show_author_time $testroot/${GOTSYS_REPO} $commit_id` + local d=$(env LC_ALL=C date -u -r "$commit_time" \ + +"%a %b %e %H:%M:%S %Y UTC") + local tree_id=$(got cat -r $testroot/${GOTSYS_REPO} $commit_id | \ + grep 'tree ' | cut -d ' ' -f2) + + cat > $testroot/stdout.expected < $testroot/stdout + + local commit_id=`git_show_head $testroot/${GOTSYS_REPO}` + local tree_id=$(got cat -r $testroot/${GOTSYS_REPO} $commit_id | \ + grep 'tree ' | cut -d ' ' -f2) + + cat > $testroot/stdout.expected <