diff --git a/.gitignore b/.gitignore index 7d2fefce96..a6b0bd4035 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,8 @@ /git-commit-tree /git-config /git-count-objects +/git-credential-cache +/git-credential-cache--daemon /git-cvsexportcommit /git-cvsimport /git-cvsserver diff --git a/Documentation/git-credential-cache--daemon.txt b/Documentation/git-credential-cache--daemon.txt new file mode 100644 index 0000000000..11edc5a173 --- /dev/null +++ b/Documentation/git-credential-cache--daemon.txt @@ -0,0 +1,26 @@ +git-credential-cache--daemon(1) +=============================== + +NAME +---- +git-credential-cache--daemon - temporarily store user credentials in memory + +SYNOPSIS +-------- +[verse] +git credential-cache--daemon + +DESCRIPTION +----------- + +NOTE: You probably don't want to invoke this command yourself; it is +started automatically when you use linkgit:git-credential-cache[1]. + +This command listens on the Unix domain socket specified by `` +for `git-credential-cache` clients. Clients may store and retrieve +credentials. Each credential is held for a timeout specified by the +client; once no credentials are held, the daemon exits. + +GIT +--- +Part of the linkgit:git[1] suite diff --git a/Documentation/git-credential-cache.txt b/Documentation/git-credential-cache.txt new file mode 100644 index 0000000000..563fdae6fb --- /dev/null +++ b/Documentation/git-credential-cache.txt @@ -0,0 +1,84 @@ +git-credential-cache(1) +======================= + +NAME +---- +git-credential-cache - helper to temporarily store passwords in memory + +SYNOPSIS +-------- +----------------------------- +git config credential.helper 'cache [options]' +----------------------------- + +DESCRIPTION +----------- + +This command requests credentials from the user and caches them in +memory for use by future git programs. The stored credentials never +touch the disk, and are forgotten after a configurable timeout. The +cache is accessible over a Unix domain socket, restricted to the current +user by filesystem permissions. + +You probably don't want to invoke this command directly; it is meant to +be used as a credential helper by other parts of git. See +linkgit:gitcredentials[7] or `EXAMPLES` below. + +OPTIONS +------- + +--timeout:: + + Number of seconds to cache credentials (default: 900). + +--socket :: + + Use `` to contact a running cache daemon (or start a new + cache daemon if one is not started). Defaults to + `~/.git-credential-cache/socket`. If your home directory is on a + network-mounted filesystem, you may need to change this to a + local filesystem. + +--chain :: + + Specify an external helper to use for retrieving credentials + from the user, instead of the default method. The resulting + credentials are then cached as normal. This option can be + given multiple times; each chained helper will be tried until + credentials are received. + +--exit:: + + Tell a running daemon to exit, forgetting all cached + credentials. + +Git may provide other options to the program when it is called as a +credential helper; see linkgit:gitcredentials[7]. + +EXAMPLES +-------- + +The point of this helper is to reduce the number of times you must type +your username or password. For example: + +------------------------------------ +$ git config credential.helper cache +$ git push http://example.com/repo.git +Username: +Password: + +[work for 5 more minutes] +$ git push http://example.com/repo.git +[your credentials are used automatically] +------------------------------------ + +You can provide options via the credential.helper configuration +variable (this example drops the cache time to 5 minutes): + +------------------------------------ +$ git config credential.helper 'cache --timeout=300' +------------------------------------ + +GIT +--- +Part of the linkgit:git[1] suite diff --git a/Documentation/gitcredentials.txt b/Documentation/gitcredentials.txt index 74136ee03a..bd1a3b66f3 100644 --- a/Documentation/gitcredentials.txt +++ b/Documentation/gitcredentials.txt @@ -80,11 +80,18 @@ CREDENTIAL HELPERS Credential helpers are external programs from which git can request usernames and passwords. -To use a helper, you must first select one to use. Git does not yet -include any credential helpers, but you may have third-party helpers -installed; search for `credential-*` in the output of `git help -a`, and -consult the documentation of individual helpers. Once you have selected -a helper, you can tell git to use it by putting its name into the +To use a helper, you must first select one to use. Git currently +includes the following helpers: + +cache:: + + Cache credentials in memory for a short period of time. See + linkgit:git-credential-cache[1] for details. + +You may may also have third-party helpers installed; search for +`credential-*` in the output of `git help -a`, and consult the +documentation of individual helpers. Once you have selected a helper, +you can tell git to use it by putting its name into the credential.helper variable. 1. Find a helper. diff --git a/Makefile b/Makefile index 5da42d025f..442e249b08 100644 --- a/Makefile +++ b/Makefile @@ -420,6 +420,8 @@ PROGRAM_OBJS += show-index.o PROGRAM_OBJS += upload-pack.o PROGRAM_OBJS += http-backend.o PROGRAM_OBJS += sh-i18n--envsubst.o +PROGRAM_OBJS += credential-cache.o +PROGRAM_OBJS += credential-cache--daemon.o PROGRAMS += $(patsubst %.o,git-%$X,$(PROGRAM_OBJS)) @@ -680,6 +682,7 @@ LIB_OBJS += transport-helper.o LIB_OBJS += tree-diff.o LIB_OBJS += tree.o LIB_OBJS += tree-walk.o +LIB_OBJS += unix-socket.o LIB_OBJS += unpack-trees.o LIB_OBJS += url.o LIB_OBJS += usage.o diff --git a/credential-cache--daemon.c b/credential-cache--daemon.c new file mode 100644 index 0000000000..f520347f60 --- /dev/null +++ b/credential-cache--daemon.c @@ -0,0 +1,268 @@ +#include "cache.h" +#include "credential.h" +#include "unix-socket.h" + +struct credential_cache_entry { + struct credential item; + unsigned long expiration; +}; +static struct credential_cache_entry *entries; +static int entries_nr; +static int entries_alloc; + +static void cache_credential(const struct credential *c, int timeout) +{ + struct credential_cache_entry *e; + + ALLOC_GROW(entries, entries_nr + 1, entries_alloc); + e = &entries[entries_nr++]; + + memcpy(&e->item, c, sizeof(*c)); + e->expiration = time(NULL) + timeout; +} + +static struct credential_cache_entry *lookup_credential(const struct credential *c) +{ + int i; + for (i = 0; i < entries_nr; i++) { + struct credential *e = &entries[i].item; + + /* We must either both have the same unique token, + * or we must not be using unique tokens at all. */ + if (e->unique) { + if (!c->unique || strcmp(e->unique, c->unique)) + continue; + } + else if (c->unique) + continue; + + /* If we have a username, it must match. Otherwise, + * we will fill in the username. */ + if (c->username && strcmp(e->username, c->username)) + continue; + + return &entries[i]; + } + return NULL; +} + +static void remove_credential(const struct credential *c) +{ + struct credential_cache_entry *e; + + e = lookup_credential(c); + if (e) + e->expiration = 0; +} + +static int check_expirations(void) +{ + int i = 0; + unsigned long now = time(NULL); + unsigned long next = (unsigned long)-1; + + while (i < entries_nr) { + if (entries[i].expiration <= now) { + entries_nr--; + if (!entries_nr) + return 0; + free(entries[i].item.description); + free(entries[i].item.unique); + free(entries[i].item.username); + free(entries[i].item.password); + memcpy(&entries[i], &entries[entries_nr], sizeof(*entries)); + } + else { + if (entries[i].expiration < next) + next = entries[i].expiration; + i++; + } + } + + return next - now; +} + +static int read_credential_request(FILE *fh, struct credential *c, + char **action, int *timeout) { + struct strbuf item = STRBUF_INIT; + + while (strbuf_getline(&item, fh, '\0') != EOF) { + char *key = item.buf; + char *value = strchr(key, '='); + + if (!value) { + warning("cache client sent bogus input: %s", key); + strbuf_release(&item); + return -1; + } + *value++ = '\0'; + + if (!strcmp(key, "action")) + *action = xstrdup(value); + else if (!strcmp(key, "unique")) + c->unique = xstrdup(value); + else if (!strcmp(key, "username")) + c->username = xstrdup(value); + else if (!strcmp(key, "password")) + c->password = xstrdup(value); + else if (!strcmp(key, "timeout")) + *timeout = atoi(value); + else { + warning("cache client sent bogus key: %s", key); + strbuf_release(&item); + return -1; + } + } + strbuf_release(&item); + return 0; +} + +static void serve_one_client(FILE *in, FILE *out) +{ + struct credential c = { NULL }; + int timeout = -1; + char *action = NULL; + + if (read_credential_request(in, &c, &action, &timeout) < 0) + return; + + if (!action) { + warning("cache client didn't specify an action"); + return; + } + + if (!strcmp(action, "exit")) + exit(0); + + if (!strcmp(action, "get")) { + struct credential_cache_entry *e = lookup_credential(&c); + if (e) { + fprintf(out, "username=%s\n", e->item.username); + fprintf(out, "password=%s\n", e->item.password); + } + return; + } + + if (!strcmp(action, "erase")) { + remove_credential(&c); + return; + } + + if (!strcmp(action, "store")) { + if (timeout < 0) { + warning("cache client didn't specify a timeout"); + return; + } + + remove_credential(&c); + cache_credential(&c, timeout); + return; + } + + warning("cache client sent unknown action: %s", action); + return; +} + +static int serve_cache_loop(int fd) +{ + struct pollfd pfd; + unsigned long wakeup; + + wakeup = check_expirations(); + if (!wakeup) + return 0; + + pfd.fd = fd; + pfd.events = POLLIN; + if (poll(&pfd, 1, 1000 * wakeup) < 0) { + if (errno != EINTR) + die_errno("poll failed"); + return 1; + } + + if (pfd.revents & POLLIN) { + int client, client2; + FILE *in, *out; + + client = accept(fd, NULL, NULL); + if (client < 0) { + warning("accept failed: %s", strerror(errno)); + return 1; + } + client2 = dup(client); + if (client2 < 0) { + warning("dup failed: %s", strerror(errno)); + close(client); + return 1; + } + + in = xfdopen(client, "r"); + out = xfdopen(client2, "w"); + serve_one_client(in, out); + fclose(in); + fclose(out); + } + return 1; +} + +static void serve_cache(const char *socket_path) +{ + int fd; + + fd = unix_stream_listen(socket_path); + if (fd < 0) + die_errno("unable to bind to '%s'", socket_path); + + printf("ok\n"); + fclose(stdout); + + while (serve_cache_loop(fd)) + ; /* nothing */ + + close(fd); + unlink(socket_path); +} + +static const char permissions_advice[] = +"The permissions on your socket directory are too loose; other\n" +"users may be able to read your cached credentials. Consider running:\n" +"\n" +" chmod 0700 %s"; +static void check_socket_directory(const char *path) +{ + struct stat st; + char *path_copy = xstrdup(path); + char *dir = dirname(path_copy); + + if (!stat(dir, &st)) { + if (st.st_mode & 077) + die(permissions_advice, dir); + free(path_copy); + return; + } + + /* + * We must be sure to create the directory with the correct mode, + * not just chmod it after the fact; otherwise, there is a race + * condition in which somebody can chdir to it, sleep, then try to open + * our protected socket. + */ + if (safe_create_leading_directories_const(dir) < 0) + die_errno("unable to create directories for '%s'", dir); + if (mkdir(dir, 0700) < 0) + die_errno("unable to mkdir '%s'", dir); + free(path_copy); +} + +int main(int argc, const char **argv) +{ + const char *socket_path = argv[1]; + + if (!socket_path) + die("usage: git-credential-cache--daemon "); + check_socket_directory(socket_path); + + serve_cache(socket_path); + + return 0; +} diff --git a/credential-cache.c b/credential-cache.c new file mode 100644 index 0000000000..f495043ad6 --- /dev/null +++ b/credential-cache.c @@ -0,0 +1,163 @@ +#include "cache.h" +#include "credential.h" +#include "string-list.h" +#include "parse-options.h" +#include "unix-socket.h" +#include "run-command.h" + +static int send_request(const char *socket, const struct strbuf *out) +{ + int got_data = 0; + int fd = unix_stream_connect(socket); + + if (fd < 0) + return -1; + + if (write_in_full(fd, out->buf, out->len) < 0) + die_errno("unable to write to cache daemon"); + shutdown(fd, SHUT_WR); + + while (1) { + char in[1024]; + int r; + + r = read_in_full(fd, in, sizeof(in)); + if (r == 0) + break; + if (r < 0) + die_errno("read error from cache daemon"); + write_or_die(1, in, r); + got_data = 1; + } + return got_data; +} + +static void out_str(struct strbuf *out, const char *key, const char *value) +{ + if (!value) + return; + strbuf_addf(out, "%s=%s", key, value); + strbuf_addch(out, '\0'); +} + +static void out_int(struct strbuf *out, const char *key, int value) +{ + strbuf_addf(out, "%s=%d", key, value); + strbuf_addch(out, '\0'); +} + +static int do_cache(const char *socket, const char *action, + const struct credential *c, int timeout) +{ + struct strbuf buf = STRBUF_INIT; + int ret; + + out_str(&buf, "action", action); + if (c) { + out_str(&buf, "unique", c->unique); + out_str(&buf, "username", c->username); + out_str(&buf, "password", c->password); + } + if (timeout > 0) + out_int(&buf, "timeout", timeout); + + ret = send_request(socket, &buf); + + strbuf_release(&buf); + return ret; +} + +static void spawn_daemon(const char *socket) +{ + struct child_process daemon; + const char *argv[] = { NULL, NULL, NULL }; + char buf[128]; + int r; + + memset(&daemon, 0, sizeof(daemon)); + argv[0] = "git-credential-cache--daemon"; + argv[1] = socket; + daemon.argv = argv; + daemon.no_stdin = 1; + daemon.out = -1; + + if (start_command(&daemon)) + die_errno("unable to start cache daemon"); + r = read_in_full(daemon.out, buf, sizeof(buf)); + if (r < 0) + die_errno("unable to read result code from cache daemon"); + if (r != 3 || memcmp(buf, "ok\n", 3)) + die("cache daemon did not start: %.*s", r, buf); + close(daemon.out); +} + +int main(int argc, const char **argv) +{ + struct credential c = { NULL }; + char *socket_path = NULL; + int timeout = 900; + struct string_list chain = STRING_LIST_INIT_NODUP; + int exit_mode = 0; + int reject_mode = 0; + const char * const usage[] = { + "git credential-cache [options]", + NULL + }; + struct option options[] = { + OPT_BOOLEAN(0, "exit", &exit_mode, + "tell a running daemon to exit"), + OPT_BOOLEAN(0, "reject", &reject_mode, + "reject a cached credential"), + OPT_INTEGER(0, "timeout", &timeout, + "number of seconds to cache credentials"), + OPT_STRING(0, "socket", &socket_path, "path", + "path of cache-daemon socket"), + OPT_STRING_LIST(0, "chain", &chain, "helper", + "use to get non-cached credentials"), + OPT_STRING(0, "username", &c.username, "name", + "an existing username"), + OPT_STRING(0, "description", &c.description, "desc", + "human-readable description of the credential"), + OPT_STRING(0, "unique", &c.unique, "token", + "a unique context for the credential"), + OPT_END() + }; + + argc = parse_options(argc, argv, NULL, options, usage, 0); + if (argc) + usage_with_options(usage, options); + /* credential_reject wants to free() these */ + if (c.username) + c.username = xstrdup(c.username); + if (c.password) + c.password = xstrdup(c.password); + + if (!socket_path) + socket_path = expand_user_path("~/.git-credential-cache/socket"); + if (!socket_path) + die("unable to find a suitable socket path; use --socket"); + + if (exit_mode) { + do_cache(socket_path, "exit", NULL, -1); + return 0; + } + + if (reject_mode) { + do_cache(socket_path, "erase", &c, -1); + credential_reject(&c, &chain); + return 0; + } + + if (do_cache(socket_path, "get", &c, -1) > 0) + return 0; + + credential_fill(&c, &chain); + printf("username=%s\n", c.username); + printf("password=%s\n", c.password); + + if (do_cache(socket_path, "store", &c, timeout) < 0) { + spawn_daemon(socket_path); + do_cache(socket_path, "store", &c, timeout); + } + return 0; +} diff --git a/git-compat-util.h b/git-compat-util.h index ddfbf77149..1dfa4c5ec2 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -130,6 +130,7 @@ #include #include #include +#include #ifndef NO_INTTYPES_H #include #else diff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh index 68d838c26d..994a0aae91 100755 --- a/t/t0300-credentials.sh +++ b/t/t0300-credentials.sh @@ -182,4 +182,95 @@ test_expect_success 'internal getpass can pull from config' ' EOF ' +test_expect_success 'credential-cache caches password' ' + test_when_finished "git credential-cache --exit" && + check --unique=host cache <<-\EOF && + username=askpass-result + password=askpass-result + -- + askpass: Username: + askpass: Password: + EOF + check --unique=host cache <<-\EOF + username=askpass-result + password=askpass-result + -- + EOF +' + +test_expect_success 'credential-cache requires matching unique token' ' + test_when_finished "git credential-cache --exit" && + check --unique=host cache <<-\EOF && + username=askpass-result + password=askpass-result + -- + askpass: Username: + askpass: Password: + EOF + check --unique=host2 cache <<-\EOF + username=askpass-result + password=askpass-result + -- + askpass: Username: + askpass: Password: + EOF +' + +test_expect_success 'credential-cache requires matching usernames' ' + test_when_finished "git credential-cache --exit" && + check --unique=host cache <<-\EOF && + username=askpass-result + password=askpass-result + -- + askpass: Username: + askpass: Password: + EOF + check --unique=host --username=other cache <<-\EOF + username=other + password=askpass-result + -- + askpass: Password: + EOF +' + +test_expect_success 'credential-cache times out' ' + test_when_finished "git credential-cache --exit || true" && + check --unique=host "cache --timeout=1" <<-\EOF && + username=askpass-result + password=askpass-result + -- + askpass: Username: + askpass: Password: + EOF + sleep 2 && + check --unique=host cache <<-\EOF + username=askpass-result + password=askpass-result + -- + askpass: Username: + askpass: Password: + EOF +' + +test_expect_success 'credential-cache removes rejected credentials' ' + test_when_finished "git credential-cache --exit || true" && + check --unique=host cache <<-\EOF && + username=askpass-result + password=askpass-result + -- + askpass: Username: + askpass: Password: + EOF + check --reject --unique=host --username=askpass-result cache <<-\EOF && + -- + EOF + check --unique=host cache <<-\EOF + username=askpass-result + password=askpass-result + -- + askpass: Username: + askpass: Password: + EOF +' + test_done diff --git a/unix-socket.c b/unix-socket.c new file mode 100644 index 0000000000..cf9890f57c --- /dev/null +++ b/unix-socket.c @@ -0,0 +1,58 @@ +#include "cache.h" +#include "unix-socket.h" + +static int unix_stream_socket(void) +{ + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) + die_errno("unable to create socket"); + return fd; +} + +static void unix_sockaddr_init(struct sockaddr_un *sa, const char *path) +{ + int size = strlen(path) + 1; + if (size > sizeof(sa->sun_path)) + die("socket path is too long to fit in sockaddr"); + memset(sa, 0, sizeof(*sa)); + sa->sun_family = AF_UNIX; + memcpy(sa->sun_path, path, size); +} + +int unix_stream_connect(const char *path) +{ + int fd; + struct sockaddr_un sa; + + unix_sockaddr_init(&sa, path); + fd = unix_stream_socket(); + if (connect(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) { + close(fd); + return -1; + } + return fd; +} + +int unix_stream_listen(const char *path) +{ + int fd; + struct sockaddr_un sa; + + unix_sockaddr_init(&sa, path); + fd = unix_stream_socket(); + + if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) { + unlink(path); + if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) { + close(fd); + return -1; + } + } + + if (listen(fd, 5) < 0) { + close(fd); + return -1; + } + + return fd; +} diff --git a/unix-socket.h b/unix-socket.h new file mode 100644 index 0000000000..e271aeec5a --- /dev/null +++ b/unix-socket.h @@ -0,0 +1,7 @@ +#ifndef UNIX_SOCKET_H +#define UNIX_SOCKET_H + +int unix_stream_connect(const char *path); +int unix_stream_listen(const char *path); + +#endif /* UNIX_SOCKET_H */