credentials: add "cache" helper

If you access repositories over smart-http using http
authentication, then it can be annoying to have git ask you
for your password repeatedly. We cache credentials in
memory, of course, but git is composed of many small
programs. Having to input your password for each one can be
frustrating.

This patch introduces a credential helper that will cache
passwords in memory for a short period of time.

Signed-off-by: Jeff King <peff@peff.net>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Jeff King
2011-07-18 03:55:12 -04:00
committed by Junio C Hamano
parent b0c3e61f4d
commit 2d6874d83a
11 changed files with 715 additions and 5 deletions

2
.gitignore vendored
View File

@@ -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

View File

@@ -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 <socket>
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 `<socket>`
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

View File

@@ -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 <path>::
Use `<path>` 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 <helper>::
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: <type your username>
Password: <type your 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

View File

@@ -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.

View File

@@ -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

268
credential-cache--daemon.c Normal file
View File

@@ -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 <socket_path>");
check_socket_directory(socket_path);
serve_cache(socket_path);
return 0;
}

163
credential-cache.c Normal file
View File

@@ -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 <helper> 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;
}

View File

@@ -130,6 +130,7 @@
#include <arpa/inet.h>
#include <netdb.h>
#include <pwd.h>
#include <sys/un.h>
#ifndef NO_INTTYPES_H
#include <inttypes.h>
#else

View File

@@ -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

58
unix-socket.c Normal file
View File

@@ -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;
}

7
unix-socket.h Normal file
View File

@@ -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 */