commit e39d79e3c6d16ad1f982e9c9a8c2f99d368bd15c from: Stefan Sperling date: Thu Apr 17 16:19:21 2025 UTC implement gotsh weblogin command commit - 71313859002afca93f40060d925f90bf11fb7734 commit + e39d79e3c6d16ad1f982e9c9a8c2f99d368bd15c 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 + 1772346892c041ad7f5ea59596a15be2077c6044 --- gotsh/gotsh.c +++ gotsh/gotsh.c @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -34,18 +35,22 @@ #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; __dead static void usage(void) { - fprintf(stderr, "usage: %s -c '%s|%s repository-path'\n", + 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,265 @@ 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(int outfd, int sock, const char *hostname) +{ + const struct got_error *err = NULL; + char *url = NULL, *greeting = NULL; + char buf[_POSIX_HOST_NAME_MAX + 2]; + size_t size, remain; + int ret; + + if (hostname) { + /* + * Send the desired hostname to gotwebd. + * gotwebd reads up to _POSIX_HOST_NAME_MAX + 1 bytes, terminated + * by a linefeed, \n. + */ + ret = snprintf(buf, sizeof(buf), "login %s\n", hostname); + } else + ret = snprintf(buf, sizeof(buf), "login\n"); + if (ret == -1) + return got_error_from_errno("snprintf"); + if ((size_t)ret >= sizeof(buf)) + return got_error(GOT_ERR_NO_SPACE); + + err = got_poll_write_full(sock, buf, ret); + if (err) + return err; + + /* + * 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. + */ + url = calloc(1, sizeof(buf)); + if (url == NULL) + return got_error_from_errno("calloc"); + + size = sizeof(buf); + remain = size; + + memset(buf, 0, sizeof(buf)); /* reusing buf with strlcat below */ + + for (;;) { + size_t len; + char *eol; + + err = got_poll_read_full(sock, &len, buf, sizeof(buf) - 1, 1); + if (err) + goto done; + + if (len == 0) { + err = got_error(GOT_ERR_EOF); + goto done; + } else if (len >= remain) { /* should not happen */ + err = got_error(GOT_ERR_NO_SPACE); + goto done; + } + + buf[len] = '\0'; + + eol = memchr(buf, '\n', len); + if (eol) + *eol = '\0'; + + if (strlcat(url, buf, size) >= size) { + err = got_error(GOT_ERR_NO_SPACE); + goto done; + } + + if (eol) + break; + + remain -= len; + if (remain < sizeof(buf)) { + size_t newsize = size + sizeof(buf); + char *p; + + p = realloc(url, newsize); + if (p == NULL) { + err = got_error_from_errno("realloc"); + goto done; + } + url = p; + size = newsize; + remain += sizeof(buf); + } + } + + if (strncmp(url, "ok ", 3) == 0) { + if (asprintf(&greeting, "Login successful. Please visit the " + "following URL within the next %d minutes: ", + GOTWEBD_AUTH_TIMEOUT / 60) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + err = got_poll_write_full(outfd, greeting, strlen(greeting)); + if (err) + goto done; + err = got_poll_write_full(outfd, url + 3, strlen(url) - 3); + } else + err = got_poll_write_full(outfd, url, strlen(url)); +done: + if (err && err->code != GOT_ERR_EOF) { + const struct got_error *err2; + + err2 = got_error_fmt(err->code, "%s", getprogname()); + got_poll_write_full(outfd, err2->msg, strlen(err2->msg)); + } + free(url); + free(greeting); + 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_AUTH_CMD) && + strncmp(cmd, GOTWEBD_AUTH_CMD, strlen(GOTWEBD_AUTH_CMD)) == 0) + cmdlen = strlen(GOTWEBD_AUTH_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_AUTH_CMD) == 0) { + if (argc != 1 && argc != 2) + usage(); + unix_socket_path = getenv("GOTWEBD_AUTH_SOCKET"); + if (unix_socket_path == NULL) + unix_socket_path = GOTWEBD_AUTH_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 (strcmp(argv[2], GOTWEBD_AUTH_CMD) == 0) { + unix_socket_path = getenv("GOTWEBD_AUTH_SOCKET"); + if (unix_socket_path == NULL) + unix_socket_path = GOTWEBD_AUTH_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 +336,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_FILENO, 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 - 1973199772dbd6e269f7b884a5607f7e7d7f0414 blob + 432a79667a3d6deab012c3bd82c9d857be510a0f --- gotwebd/gotwebd.h +++ gotwebd/gotwebd.h @@ -39,6 +39,10 @@ #define GOTWEBD_WWW_USER "www" #endif +#define GOTWEBD_AUTH_CMD "weblogin" +#define GOTWEBD_AUTH_SOCKET "/var/run/gotweb-auth.sock" +#define GOTWEBD_AUTH_TIMEOUT 300 /* in seconds */ + #define GOTWEBD_MAXDESCRSZ 1024 #define GOTWEBD_MAXCLONEURLSZ 1024 #define GOTWEBD_CACHESIZE 1024 @@ -538,3 +542,6 @@ int config_setfd(struct gotwebd *); int config_getfd(struct gotwebd *, struct imsg *); int config_getcfg(struct gotwebd *, struct imsg *); int config_init(struct gotwebd *); + +/* ../lib/gotweb_auth.c */ +const struct got_error * gotweb_auth(int, int, int);