Download raw body.
gotwebd authentication
On Wed, Sep 17, 2025 at 01:22:07PM +0200, Stefan Sperling wrote: > This patch adds user authentication to gotwebd. > See the manual page changes included in the diff for details of how it works. Updated diff which fixes some issues, most importantly an issue where login tokens didn't work with some hostnames (depending on hostname length). I have this running on got.g.o where it helps reduce load from crawlers significantly. The anonymous user is currently allowed to log in via: ssh anonymous@got.gameoftrees.org weblogin 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+ 2- M gotwebd/gotwebd.h | 93+ 0- A gotwebd/login.c | 795+ 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, 3724 insertions(+), 91 deletions(-) commit - 12c1bbcab3809ec364d34a8280dfb318a2968da6 commit + 66cf37ecd53dbaa8c0ae161986e5782edb4cc586 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 <sys/socket.h> #include <sys/un.h> +#include <ctype.h> #include <err.h> #include <event.h> #include <imsg.h> @@ -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 <stsp@openbsd.org> + * Copyright (c) 2025 Omar Polo <op@openbsd.org> + * Copyright (c) 2015 Ted Unangst <tedu@openbsd.org> + * + * 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 <sys/queue.h> + +#include <errno.h> +#include <event.h> +#include <imsg.h> +#include <pwd.h> +#include <sha1.h> +#include <sha2.h> +#include <string.h> +#include <signal.h> +#include <stdio.h> +#include <grp.h> +#include <stdlib.h> +#include <unistd.h> + +#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 + 39ddd44a1bd5c5e063b6441001efcca0579a7b4b --- 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,10 +235,79 @@ The .Cm chroot directive must be used before the server declaration in order to take effect. +.It Ic repository Ar name Brq ... +Set options which apply to a particular repository served by this server. +.Pp +A repository context is declared with a unique +.Ar name , +followed by repository-specific configuration directives inside curly braces. +.Pp +The repository will be looked up within the server's +.Ar repos_path , +where the directory +.Ar name +can exist with or without a +.Dq .git +suffix. +.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. +.Pp +If no access rules are set in a repository context, or if a repository exists +in the server's +.Ar repos_path +without being mentioned in +.Nm +at all, then the access rules set in the server and global configuration +contexts apply. +If no rule matches then the repository will be inaccessible if authentication +is enabled. +.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 -file. +file, regardless of whether authentication is enabled and has failed or +succeeded. Disabled by default. .It Ic show_repo_age Ar on | off Toggle display of last repository modification date. @@ -249,7 +380,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 +388,7 @@ server "localhost" { site_name "my public repos" site_owner "Flan Hacker" site_link "Flan' Projects" + disable authentication } .Ed .Pp @@ -271,8 +403,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 }}) </author> {{ end }} + +{{ define gotweb_render_unauthorized(struct template *tp) }} +<p>Wrong or missing authentication code</p> +{{ 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 <v.string> STRING %token <v.number> NUMBER %type <v.number> boolean %type <v.string> listen_addr +%type <v.string> 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 + e42357173c5e19966e56747a4f7d9fc7b01f6d73 (mode 644) --- /dev/null +++ gotwebd/login.c @@ -0,0 +1,795 @@ +/* + * Copyright (c) 2025 Stefan Sperling <stsp@openbsd.org> + * Copyright (c) 2025 Omar Polo <op@openbsd.org> + * + * 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 <sys/queue.h> +#include <sys/stat.h> + +#include <errno.h> +#include <event.h> +#include <imsg.h> +#include <sha1.h> +#include <sha2.h> +#include <signal.h> +#include <siphash.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <openssl/bio.h> +#include <openssl/buffer.h> +#include <openssl/evp.h> +#include <openssl/hmac.h> + +#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; + } + + /* Trim padding. */ + while (len > 0 && (len % 4) != 0) + len--; + + 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, pad; + 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 hostname with trailing NULs for base64 encoding. */ + pad = 0; + while (((3 + 8 + 8 + 8 + hlen + pad) % 4) != 0) { + if (fwrite("", 1, 1, fp) != 1) { + fclose(fp); + free(tok); + return NULL; + } + pad++; + } + + 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 >= 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 <bsd.regress.mk> 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 <<EOF usage: gotsh -c 'git-receive-pack|git-upload-pack repository-path' + gotsh -c 'weblogin [hostname]' EOF cmp -s $testroot/stderr.expected $testroot/stderr ret=$? @@ -268,6 +269,7 @@ EOF cat > $testroot/stderr.expected <<EOF usage: gotsh -c 'git-receive-pack|git-upload-pack repository-path' + gotsh -c 'weblogin [hostname]' EOF cmp -s $testroot/stderr.expected $testroot/stderr ret=$? blob - /dev/null blob + bf2d4849cc5779b21fcf3b9653dfcabef95ea288 (mode 755) --- /dev/null +++ regress/gotsysd/test_gotwebd.sh @@ -0,0 +1,412 @@ +#!/bin/sh +# +# Copyright (c) 2025 Stefan Sperling <stsp@openbsd.org> +# +# 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 <<EOF +[got] +Repos +login failed + +EOF + cmp -s $testroot/stdout.expected $testroot/stdout + ret=$? + if [ $ret -ne 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + # Obtain a login token over ssh. + ssh -q -i ${GOTSYSD_SSH_KEY} ${GOTSYSD_TEST_USER}@${VMIP} \ + 'gotsh -c weblogin' > $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 <<EOF +[got] +Repos +Project +gotsys.git +summary | briefs | commits | tags | tree | rss +------------------------------------------------------------------------------- + +EOF + cmp -s $testroot/stdout.expected $testroot/stdout + ret=$? + if [ $ret -ne 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + # Request a tree page using the stored cookie. + w3m -cookie-jar "$testroot/cookies" \ + "http://${VMIP}/?action=tree&path=gotsys.git" -dump \ + > $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 <<EOF +[got] +Repos / gotsys.git / tree / + +Tree + +Tree: + $tree_id +Date: + $d +Message: + init + +------------------------------------------------------------------------------- + +gotsys.conf commits | blame + +EOF + cmp -s $testroot/stdout.expected $testroot/stdout + ret=$? + if [ $ret -ne 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + # Attempt to access the same page again without the cookie. + w3m "http://${VMIP}/?action=tree&path=gotsys.git" -dump \ + > $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 <<EOF +[got] +Repos / gotsys.git / tree / +login failed + +EOF + cmp -s $testroot/stdout.expected $testroot/stdout + ret=$? + if [ $ret -ne 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + # Attempt to access a non-existent repository without the cookie. + # Observable behaviour should match the case where the name of a + # hidden and existing repository was guessed correctly. + w3m "http://${VMIP}/?action=tree&path=nonexistent.git" -dump \ + > $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 <<EOF +[got] +Repos / nonexistent.git / tree / +login failed + +EOF + cmp -s $testroot/stdout.expected $testroot/stdout + ret=$? + if [ $ret -ne 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + test_done "$testroot" "0" +} + +test_access_rules_index_page() { + local testroot=`test_init access_rules_index_page 1` + + got checkout -q $testroot/${GOTSYS_REPO} $testroot/wt >/dev/null + ret=$? + if [ $ret -ne 0 ]; then + echo "got checkout failed unexpectedly" >&2 + test_done "$testroot" 1 + return 1 + fi + + crypted_vm_pw=`echo ${GOTSYSD_VM_PASSWORD} | encrypt | tr -d '\n'` + crypted_pw=`echo ${GOTSYSD_DEV_PASSWORD} | encrypt | tr -d '\n'` + sshkey=`cat ${GOTSYSD_SSH_PUBKEY}` + cat > ${testroot}/wt/gotsys.conf <<EOF +user ${GOTSYSD_TEST_USER} { + password "${crypted_vm_pw}" + authorized key ${sshkey} +} +user ${GOTSYSD_DEV_USER} { + password "${crypted_pw}" + authorized key ${sshkey} +} +repository gotsys.git { + permit rw ${GOTSYSD_TEST_USER} + permit rw ${GOTSYSD_DEV_USER} +} +repository public.git { + permit rw ${GOTSYSD_TEST_USER} + permit rw ${GOTSYSD_DEV_USER} +} +repository gotdev.git { + permit rw ${GOTSYSD_DEV_USER} +} +repository gottest.git { + permit rw ${GOTSYSD_TEST_USER} +} +EOF + (cd ${testroot}/wt && got commit \ + -m "create user ${GOTSYSD_DEV_USER}" >/dev/null) + local commit_id=`git_show_head $testroot/${GOTSYS_REPO}` + + got send -q -i ${GOTSYSD_SSH_KEY} -r ${testroot}/${GOTSYS_REPO} + ret=$? + if [ $ret -ne 0 ]; then + echo "got send failed unexpectedly" >&2 + test_done "$testroot" 1 + return 1 + fi + + # Wait for gotsysd to apply the new configuration. + echo "$commit_id" > $testroot/stdout.expected + for i in 1 2 3 4 5; do + sleep 1 + ssh -i ${GOTSYSD_SSH_KEY} root@${VMIP} \ + cat /var/db/gotsysd/commit > $testroot/stdout + if cmp -s $testroot/stdout.expected $testroot/stdout; then + break; + fi + done + cmp -s $testroot/stdout.expected $testroot/stdout + ret=$? + if [ $ret -ne 0 ]; then + echo "gotsysd failed to apply configuration" >&2 + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + # Request the index page without being logged in. + # Repositories we do not have access to should not be listed. + w3m "http://${VMIP}/" -dump > $testroot/stdout + cat > $testroot/stdout.expected <<EOF +[got] +Repos +Project +public.git +summary | briefs | commits | tags | tree | rss +------------------------------------------------------------------------------- + +EOF + + # Obtain a login token over ssh. + ssh -q -i ${GOTSYSD_SSH_KEY} ${GOTSYSD_TEST_USER}@${VMIP} \ + 'gotsh -c weblogin' > $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. Repositories we do not have access + # to should not be listed. + 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 <<EOF +[got] +Repos +Project +gotsys.git +summary | briefs | commits | tags | tree | rss +------------------------------------------------------------------------------- +gottest.git +summary | briefs | commits | tags | tree | rss +------------------------------------------------------------------------------- +public.git +summary | briefs | commits | tags | tree | rss +------------------------------------------------------------------------------- + +EOF + cmp -s $testroot/stdout.expected $testroot/stdout + ret=$? + if [ $ret -ne 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + # Obtain a different login token over ssh. + ssh -q -i ${GOTSYSD_SSH_KEY} ${GOTSYSD_DEV_USER}@${VMIP} \ + 'weblogin' > $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. Repositories we do not have access + # to should not be listed. + 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 <<EOF +[got] +Repos +Project +gotdev.git +summary | briefs | commits | tags | tree | rss +------------------------------------------------------------------------------- +public.git +summary | briefs | commits | tags | tree | rss +------------------------------------------------------------------------------- + +EOF + cmp -s $testroot/stdout.expected $testroot/stdout + ret=$? + if [ $ret -ne 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + test_done "$testroot" "0" +} + +test_access_rules_tree_page() { + local testroot=`test_init access_rules_tree_page 1` + + # Attempt to access a public repository's tree + w3m "http://${VMIP}/?action=tree&path=public.git" -dump \ + > $testroot/stdout + + cat > $testroot/stdout.expected <<EOF +[got] +Repos / public.git / tree / +reference refs/heads/main not found + +EOF + cmp -s $testroot/stdout.expected $testroot/stdout + ret=$? + if [ $ret -ne 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + # Attempt to access a private repository's tree + w3m "http://${VMIP}/?action=tree&path=gottest.git" -dump \ + > $testroot/stdout + + cat > $testroot/stdout.expected <<EOF +[got] +Repos / gottest.git / tree / +login failed + +EOF + cmp -s $testroot/stdout.expected $testroot/stdout + ret=$? + if [ $ret -ne 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + # Obtain a login token over ssh. + ssh -q -i ${GOTSYSD_SSH_KEY} ${GOTSYSD_TEST_USER}@${VMIP} \ + 'gotsh -c weblogin' > $testroot/stdout + ret=$? + if [ $ret -ne 0 ]; then + echo "ssh login failed failed unexpectedly" >&2 + test_done "$testroot" 1 + return 1 + fi + + 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 <<EOF +[got] +Repos +Project +gotsys.git +summary | briefs | commits | tags | tree | rss +------------------------------------------------------------------------------- +gottest.git +summary | briefs | commits | tags | tree | rss +------------------------------------------------------------------------------- +public.git +summary | briefs | commits | tags | tree | rss +------------------------------------------------------------------------------- + +EOF + cmp -s $testroot/stdout.expected $testroot/stdout + ret=$? + if [ $ret -ne 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + # Attempt to access the private repository's tree again with cookie + w3m -cookie-jar $testroot/cookies \ + "http://${VMIP}/?action=tree&path=gottest.git" -dump \ + > $testroot/stdout + + cat > $testroot/stdout.expected <<EOF +[got] +Repos / gottest.git / tree / +reference refs/heads/main not found + +EOF + cmp -s $testroot/stdout.expected $testroot/stdout + ret=$? + if [ $ret -ne 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + + test_done "$testroot" "$ret" +} + +test_parseargs "$@" +run_test test_login +run_test test_access_rules_index_page +run_test test_access_rules_tree_page
gotwebd authentication