From 66c8448543432308e8fce5e3e04076e875410f67 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Mon, 18 Jul 2011 03:48:51 -0400 Subject: [PATCH 01/13] url: decode buffers that are not NUL-terminated The url_decode function needs only minor tweaks to handle arbitrary buffers. Let's do those tweaks, which cleans up an unreadable mess of temporary strings in http.c. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- http.c | 27 ++++----------------------- url.c | 26 ++++++++++++++++++-------- url.h | 1 + 3 files changed, 23 insertions(+), 31 deletions(-) diff --git a/http.c b/http.c index a1ea3db499..c93716c72f 100644 --- a/http.c +++ b/http.c @@ -307,8 +307,7 @@ static CURL *get_curl_handle(void) static void http_auth_init(const char *url) { - char *at, *colon, *cp, *slash, *decoded; - int len; + char *at, *colon, *cp, *slash; cp = strstr(url, "://"); if (!cp) @@ -328,29 +327,11 @@ static void http_auth_init(const char *url) return; /* No credentials */ if (!colon || at <= colon) { /* Only username */ - len = at - cp; - user_name = xmalloc(len + 1); - memcpy(user_name, cp, len); - user_name[len] = '\0'; - decoded = url_decode(user_name); - free(user_name); - user_name = decoded; + user_name = url_decode_mem(cp, at - cp); user_pass = NULL; } else { - len = colon - cp; - user_name = xmalloc(len + 1); - memcpy(user_name, cp, len); - user_name[len] = '\0'; - decoded = url_decode(user_name); - free(user_name); - user_name = decoded; - len = at - (colon + 1); - user_pass = xmalloc(len + 1); - memcpy(user_pass, colon + 1, len); - user_pass[len] = '\0'; - decoded = url_decode(user_pass); - free(user_pass); - user_pass = decoded; + user_name = url_decode_mem(cp, colon - cp); + user_pass = url_decode_mem(colon + 1, at - (colon + 1)); } } diff --git a/url.c b/url.c index 3e06fd34c4..389d9dab10 100644 --- a/url.c +++ b/url.c @@ -68,18 +68,20 @@ static int url_decode_char(const char *q) return val; } -static char *url_decode_internal(const char **query, const char *stop_at, - struct strbuf *out, int decode_plus) +static char *url_decode_internal(const char **query, int len, + const char *stop_at, struct strbuf *out, + int decode_plus) { const char *q = *query; - do { + while (len) { unsigned char c = *q; if (!c) break; if (stop_at && strchr(stop_at, c)) { q++; + len--; break; } @@ -88,6 +90,7 @@ static char *url_decode_internal(const char **query, const char *stop_at, if (0 <= val) { strbuf_addch(out, val); q += 3; + len -= 3; continue; } } @@ -97,34 +100,41 @@ static char *url_decode_internal(const char **query, const char *stop_at, else strbuf_addch(out, c); q++; - } while (1); + len--; + } *query = q; return strbuf_detach(out, NULL); } char *url_decode(const char *url) +{ + return url_decode_mem(url, strlen(url)); +} + +char *url_decode_mem(const char *url, int len) { struct strbuf out = STRBUF_INIT; - const char *colon = strchr(url, ':'); + const char *colon = memchr(url, ':', len); /* Skip protocol part if present */ if (colon && url < colon) { strbuf_add(&out, url, colon - url); + len -= colon - url; url = colon; } - return url_decode_internal(&url, NULL, &out, 0); + return url_decode_internal(&url, len, NULL, &out, 0); } char *url_decode_parameter_name(const char **query) { struct strbuf out = STRBUF_INIT; - return url_decode_internal(query, "&=", &out, 1); + return url_decode_internal(query, -1, "&=", &out, 1); } char *url_decode_parameter_value(const char **query) { struct strbuf out = STRBUF_INIT; - return url_decode_internal(query, "&", &out, 1); + return url_decode_internal(query, -1, "&", &out, 1); } void end_url_with_slash(struct strbuf *buf, const char *url) diff --git a/url.h b/url.h index 7100e3215a..abdaf6fa30 100644 --- a/url.h +++ b/url.h @@ -4,6 +4,7 @@ extern int is_url(const char *url); extern int is_urlschemechar(int first_flag, int ch); extern char *url_decode(const char *url); +extern char *url_decode_mem(const char *url, int len); extern char *url_decode_parameter_name(const char **query); extern char *url_decode_parameter_value(const char **query); From 5232586c7985e6a420ee741e19e7fd6d040d43f6 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Mon, 18 Jul 2011 03:49:12 -0400 Subject: [PATCH 02/13] improve httpd auth tests These just checked that we could clone a repository when the username and password were given in the URL; we should also check that git will prompt when no or partial credentials are given. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- t/lib-httpd.sh | 10 +++++---- t/t5550-http-fetch.sh | 51 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/t/lib-httpd.sh b/t/lib-httpd.sh index b8996a373a..f7dc0781d5 100644 --- a/t/lib-httpd.sh +++ b/t/lib-httpd.sh @@ -81,8 +81,7 @@ prepare_httpd() { if test -n "$LIB_HTTPD_SSL" then - HTTPD_URL=https://127.0.0.1:$LIB_HTTPD_PORT - AUTH_HTTPD_URL=https://user%40host:user%40host@127.0.0.1:$LIB_HTTPD_PORT + HTTPD_PROTO=https RANDFILE_PATH="$HTTPD_ROOT_PATH"/.rnd openssl req \ -config "$TEST_PATH/ssl.cnf" \ @@ -93,9 +92,12 @@ prepare_httpd() { export GIT_SSL_NO_VERIFY HTTPD_PARA="$HTTPD_PARA -DSSL" else - HTTPD_URL=http://127.0.0.1:$LIB_HTTPD_PORT - AUTH_HTTPD_URL=http://user%40host:user%40host@127.0.0.1:$LIB_HTTPD_PORT + HTTPD_PROTO=http fi + HTTPD_DEST=127.0.0.1:$LIB_HTTPD_PORT + HTTPD_URL=$HTTPD_PROTO://$HTTPD_DEST + HTTPD_URL_USER=$HTTPD_PROTO://user%40host@$HTTPD_DEST + HTTPD_URL_USER_PASS=$HTTPD_PROTO://user%40host:user%40host@$HTTPD_DEST if test -n "$LIB_HTTPD_DAV" -o -n "$LIB_HTTPD_SVN" then diff --git a/t/t5550-http-fetch.sh b/t/t5550-http-fetch.sh index a1883ca6b6..ed4db09bed 100755 --- a/t/t5550-http-fetch.sh +++ b/t/t5550-http-fetch.sh @@ -35,11 +35,54 @@ test_expect_success 'clone http repository' ' test_cmp file clone/file ' -test_expect_success 'clone http repository with authentication' ' +test_expect_success 'create password-protected repository' ' mkdir "$HTTPD_DOCUMENT_ROOT_PATH/auth/" && - cp -Rf "$HTTPD_DOCUMENT_ROOT_PATH/repo.git" "$HTTPD_DOCUMENT_ROOT_PATH/auth/repo.git" && - git clone $AUTH_HTTPD_URL/auth/repo.git clone-auth && - test_cmp file clone-auth/file + cp -Rf "$HTTPD_DOCUMENT_ROOT_PATH/repo.git" \ + "$HTTPD_DOCUMENT_ROOT_PATH/auth/repo.git" +' + +test_expect_success 'setup askpass helpers' ' + cat >askpass <<-EOF && + #!/bin/sh + echo >>"$PWD/askpass-query" "askpass: \$*" && + cat "$PWD/askpass-response" + EOF + chmod +x askpass && + GIT_ASKPASS="$PWD/askpass" && + export GIT_ASKPASS && + >askpass-expect-none && + echo "askpass: Password: " >askpass-expect-pass && + { echo "askpass: Username: " && + cat askpass-expect-pass + } >askpass-expect-both +' + +test_expect_success 'cloning password-protected repository can fail' ' + >askpass-query && + echo wrong >askpass-response && + test_must_fail git clone "$HTTPD_URL/auth/repo.git" clone-auth-fail && + test_cmp askpass-expect-both askpass-query +' + +test_expect_success 'http auth can use user/pass in URL' ' + >askpass-query && + echo wrong >askpass-reponse && + git clone "$HTTPD_URL_USER_PASS/auth/repo.git" clone-auth-none && + test_cmp askpass-expect-none askpass-query +' + +test_expect_success 'http auth can use just user in URL' ' + >askpass-query && + echo user@host >askpass-response && + git clone "$HTTPD_URL_USER/auth/repo.git" clone-auth-pass && + test_cmp askpass-expect-pass askpass-query +' + +test_expect_success 'http auth can request both user and pass' ' + >askpass-query && + echo user@host >askpass-response && + git clone "$HTTPD_URL/auth/repo.git" clone-auth-both && + test_cmp askpass-expect-both askpass-query ' test_expect_success 'fetch changes via http' ' From 28d0c1017a10a93ce165a2d4e9fb6a691a933bd3 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Mon, 18 Jul 2011 03:49:56 -0400 Subject: [PATCH 03/13] remote-curl: don't retry auth failures with dumb protocol When fetching an http URL, we first try fetching info/refs with an extra "service" parameter. This will work for a smart-http server, or a dumb server which ignores extra parameters when fetching files. If that fails, we retry without the extra parameter to remain compatible with dumb servers which didn't like our first request. If the server returned a "401 Unauthorized", indicating that the credentials we provided were not good, there is not much point in retrying. With the current code, we just waste an extra round trip to the HTTP server before failing. But as the http code becomes smarter about throwing away rejected credentials and re-prompting the user for new ones (which it will later in this series), this will become more confusing. At some point we will stop asking for credentials to retry smart http, and will be asking for credentials to retry dumb http. So now we're not only wasting an extra HTTP round trip for something that is unlikely to work, but we're making the user re-type their password for it. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- remote-curl.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/remote-curl.c b/remote-curl.c index faaeda44a9..6c24ab157c 100644 --- a/remote-curl.c +++ b/remote-curl.c @@ -115,7 +115,7 @@ static struct discovery* discover_refs(const char *service) http_ret = http_get_strbuf(refs_url, &buffer, HTTP_NO_CACHE); /* try again with "plain" url (no ? or & appended) */ - if (http_ret != HTTP_OK) { + if (http_ret != HTTP_OK && http_ret != HTTP_NOAUTH) { free(refs_url); strbuf_reset(&buffer); From 8d677edc4fa3fd1fe12b49bf279aaad5be89b81c Mon Sep 17 00:00:00 2001 From: Jeff King Date: Mon, 18 Jul 2011 03:50:14 -0400 Subject: [PATCH 04/13] http: retry authentication failures for all http requests Commit 42653c0 (Prompt for a username when an HTTP request 401s, 2010-04-01) changed http_get_strbuf to prompt for credentials when we receive a 401, but didn't touch http_get_file. The latter is called only for dumb http; while it's usually the case that people don't use authentication on top of dumb http, there is no reason not to allow both types of requests to use this feature. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- http.c | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/http.c b/http.c index c93716c72f..89e3cf4bd8 100644 --- a/http.c +++ b/http.c @@ -846,13 +846,18 @@ static int http_request(const char *url, void *result, int target, int options) return ret; } +static int http_request_reauth(const char *url, void *result, int target, + int options) +{ + int ret = http_request(url, result, target, options); + if (ret != HTTP_REAUTH) + return ret; + return http_request(url, result, target, options); +} + int http_get_strbuf(const char *url, struct strbuf *result, int options) { - int http_ret = http_request(url, result, HTTP_REQUEST_STRBUF, options); - if (http_ret == HTTP_REAUTH) { - http_ret = http_request(url, result, HTTP_REQUEST_STRBUF, options); - } - return http_ret; + return http_request_reauth(url, result, HTTP_REQUEST_STRBUF, options); } /* @@ -875,7 +880,7 @@ static int http_get_file(const char *url, const char *filename, int options) goto cleanup; } - ret = http_request(url, result, HTTP_REQUEST_FILE, options); + ret = http_request_reauth(url, result, HTTP_REQUEST_FILE, options); fclose(result); if ((ret == HTTP_OK) && move_temp_to_file(tmpfile.buf, filename)) From 59f522602894c4a291ff56d9308b2981cf1c99ed Mon Sep 17 00:00:00 2001 From: Jeff King Date: Mon, 18 Jul 2011 03:50:34 -0400 Subject: [PATCH 05/13] introduce credentials API There are a few places in git that need to get a username and password credential from the user; the most notable one is HTTP authentication for smart-http pushing. Right now the only choices for providing credentials are to put them plaintext into your ~/.netrc, or to have git prompt you (either on the terminal or via an askpass program). The former is not very secure, and the latter is not very convenient. Unfortunately, there is no "always best" solution for password management. The details will depend on the tradeoff you want between security and convenience, as well as how git can integrate with other security systems (e.g., many operating systems provide a keychain or password wallet for single sign-on). This patch abstracts the notion of gathering user credentials into a few simple functions. These functions can be backed by our internal git_getpass implementation (which just prompts the user), or by external helpers which are free to consult system-specific password wallets, make custom policy decisions on password caching and storage, or prompt the user in a non-traditional manner. The helper protocol aims for simplicity of helper implementation; see the newly added documentation for details. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- .gitignore | 1 + Documentation/technical/api-credentials.txt | 113 ++++++++++++ Makefile | 3 + credential.c | 190 ++++++++++++++++++++ credential.h | 19 ++ t/t0300-credentials.sh | 175 ++++++++++++++++++ test-credential.c | 47 +++++ 7 files changed, 548 insertions(+) create mode 100644 Documentation/technical/api-credentials.txt create mode 100644 credential.c create mode 100644 credential.h create mode 100755 t/t0300-credentials.sh create mode 100644 test-credential.c diff --git a/.gitignore b/.gitignore index 8572c8c0b0..7d2fefce96 100644 --- a/.gitignore +++ b/.gitignore @@ -167,6 +167,7 @@ /gitweb/static/gitweb.js /gitweb/static/gitweb.min.* /test-chmtime +/test-credential /test-ctype /test-date /test-delta diff --git a/Documentation/technical/api-credentials.txt b/Documentation/technical/api-credentials.txt new file mode 100644 index 0000000000..880db92c69 --- /dev/null +++ b/Documentation/technical/api-credentials.txt @@ -0,0 +1,113 @@ +credentials API +=============== + +The credentials API provides an abstracted way of gathering username and +password credentials from the user (even though credentials in the wider +world can take many forms, in this document the word "credential" always +refers to a username and password pair). + +Data Structures +--------------- + +`struct credential`:: + + This struct represents a single username/password combination. + The `username` and `password` fields should be heap-allocated + strings (or NULL if they are not yet known). The `unique` field, + if non-NULL, should be a heap-allocated string indicating a + unique context for this credential (e.g., a protocol and server + name for a remote credential). The `description` field, if + non-NULL, should point to a string containing a human-readable + description of this credential. + +`struct string_list methods`:: + + The credential functions take a `string_list` of methods for + acquiring credentials. Each string specifies an external + helper which will be run, in order, to acquire credentials, + until both a username and password have been acquired. A NULL or + empty methods list indicates that the internal + `credential_getpass` function should be used. + + +Functions +--------- + +`credential_fill_gently`:: + + Attempt to fill the username and password fields of the passed + credential struct. If they cannot be filled after trying each + available method, returns -1. Otherwise, returns 0. + +`credential_fill`:: + + Like `credential_fill_gently`, but `die()` if credentials cannot + be gathered. + +`credential_reject`:: + + Inform the credential subsystem that the provided credentials + have been rejected. This will clear the username and password + fields in `struct credential`, as well as notify any helpers of + the rejection (which may, for example, purge the invalid + credentials from storage). + +`credential_getpass`:: + + Fetch credentials from the user either using an "askpass" helper + (see the discussion of core.askpass and GIT_ASKPASS in + linkgit:git-config[1] and linkgit:git[1], respectively) or by + prompting the user via the terminal. + + +Credential Helpers +------------------ + +Credential helpers are programs executed by git to fetch credentials +from storage or from the user. The default behavior when no helpers are +defined is to use the internal `credential_askpass` function. + +When a helper is executed, it may receive the following options on the +command line: + +`--reject`:: + + Specify that the provided credential has been rejected; the + helper may take appropriate action to purge any credential + storage or cache. If this option is not given, the helper should + assume a credential is being requested. + +`--description=`:: + + `` will contain a human-readable description of the + credential being requested. If this option is not given, no + description is available. + +`--unique=`:: + + `` will contain a token to uniquely identify the context of + the credential (e.g., a host name for network authentication). + If this option is not given, no context is available. + +`--username=`:: + + `` will contain the username requested by the user. If this + option is not given, no username is available, and the helper + should provide both a username and password. + +The helper should produce a list of items on stdout, each followed by a +newline character. Each item should consist of a key-value pair, separated +by an `=` (equals) sign. The value may contain any bytes except a +newline. When reading the response, git understands the following keys: + +`username`:: + + The username part of the credential. If a username was given to + the helper via `--username`, the new value will override it. + +`password`:: + + The password part of the credential. + +It is perfectly acceptable for a helper to provide only part of a +credential, or nothing at all. diff --git a/Makefile b/Makefile index 4ed7996f7b..5da42d025f 100644 --- a/Makefile +++ b/Makefile @@ -424,6 +424,7 @@ PROGRAM_OBJS += sh-i18n--envsubst.o PROGRAMS += $(patsubst %.o,git-%$X,$(PROGRAM_OBJS)) TEST_PROGRAMS_NEED_X += test-chmtime +TEST_PROGRAMS_NEED_X += test-credential TEST_PROGRAMS_NEED_X += test-ctype TEST_PROGRAMS_NEED_X += test-date TEST_PROGRAMS_NEED_X += test-delta @@ -514,6 +515,7 @@ LIB_H += compat/win32/pthread.h LIB_H += compat/win32/syslog.h LIB_H += compat/win32/sys/poll.h LIB_H += compat/win32/dirent.h +LIB_H += credential.h LIB_H += csum-file.h LIB_H += decorate.h LIB_H += delta.h @@ -593,6 +595,7 @@ LIB_OBJS += config.o LIB_OBJS += connect.o LIB_OBJS += convert.o LIB_OBJS += copy.o +LIB_OBJS += credential.o LIB_OBJS += csum-file.o LIB_OBJS += ctype.o LIB_OBJS += date.o diff --git a/credential.c b/credential.c new file mode 100644 index 0000000000..f33c66f126 --- /dev/null +++ b/credential.c @@ -0,0 +1,190 @@ +#include "cache.h" +#include "credential.h" +#include "quote.h" +#include "string-list.h" +#include "run-command.h" + +static char *credential_ask_one(const char *what, const char *desc) +{ + struct strbuf prompt = STRBUF_INIT; + char *r; + + if (desc) + strbuf_addf(&prompt, "%s for '%s': ", what, desc); + else + strbuf_addf(&prompt, "%s: ", what); + + /* FIXME: for usernames, we should do something less magical that + * actually echoes the characters. However, we need to read from + * /dev/tty and not stdio, which is not portable (but getpass will do + * it for us). http.c uses the same workaround. */ + r = git_getpass(prompt.buf); + + strbuf_release(&prompt); + return xstrdup(r); +} + +int credential_getpass(struct credential *c) +{ + + if (!c->username) + c->username = credential_ask_one("Username", c->description); + if (!c->password) + c->password = credential_ask_one("Password", c->description); + return 0; +} + +static int read_credential_response(struct credential *c, FILE *fp) +{ + struct strbuf response = STRBUF_INIT; + + while (strbuf_getline(&response, fp, '\n') != EOF) { + char *key = response.buf; + char *value = strchr(key, '='); + + if (!value) { + warning("bad output from credential helper: %s", key); + strbuf_release(&response); + return -1; + } + *value++ = '\0'; + + if (!strcmp(key, "username")) { + free(c->username); + c->username = xstrdup(value); + } + else if (!strcmp(key, "password")) { + free(c->password); + c->password = xstrdup(value); + } + /* ignore other responses; we don't know what they mean */ + } + + strbuf_release(&response); + return 0; +} + +static int run_credential_helper(struct credential *c, const char *cmd) +{ + struct child_process helper; + const char *argv[] = { NULL, NULL }; + FILE *fp; + int r; + + memset(&helper, 0, sizeof(helper)); + argv[0] = cmd; + helper.argv = argv; + helper.use_shell = 1; + helper.no_stdin = 1; + helper.out = -1; + + if (start_command(&helper)) + return -1; + fp = xfdopen(helper.out, "r"); + + r = read_credential_response(c, fp); + + fclose(fp); + if (finish_command(&helper)) + r = -1; + + return r; +} + +static void add_item(struct strbuf *out, const char *key, const char *value) +{ + if (!value) + return; + strbuf_addf(out, " --%s=", key); + sq_quote_buf(out, value); +} + +static int first_word_is_alnum(const char *s) +{ + for (; *s && *s != ' '; s++) + if (!isalnum(*s)) + return 0; + return 1; +} + +static int credential_do(struct credential *c, const char *method, + const char *extra) +{ + struct strbuf cmd = STRBUF_INIT; + int r; + + if (first_word_is_alnum(method)) + strbuf_addf(&cmd, "git credential-%s", method); + else + strbuf_addstr(&cmd, method); + + if (extra) + strbuf_addf(&cmd, " %s", extra); + + add_item(&cmd, "description", c->description); + add_item(&cmd, "unique", c->unique); + add_item(&cmd, "username", c->username); + + r = run_credential_helper(c, cmd.buf); + + strbuf_release(&cmd); + return r; +} + +void credential_fill(struct credential *c, const struct string_list *methods) +{ + struct strbuf err = STRBUF_INIT; + + if (!credential_fill_gently(c, methods)) + return; + + strbuf_addstr(&err, "unable to get credentials"); + if (c->description) + strbuf_addf(&err, "for '%s'", c->description); + if (methods && methods->nr == 1) + strbuf_addf(&err, "; tried '%s'", methods->items[0].string); + else if (methods) { + int i; + strbuf_addstr(&err, "; tried:"); + for (i = 0; i < methods->nr; i++) + strbuf_addf(&err, "\n %s", methods->items[i].string); + } + die("%s", err.buf); +} + +int credential_fill_gently(struct credential *c, + const struct string_list *methods) +{ + int i; + + if (c->username && c->password) + return 0; + + if (!methods || !methods->nr) + return credential_getpass(c); + + for (i = 0; i < methods->nr; i++) { + if (!credential_do(c, methods->items[i].string, NULL) && + c->username && c->password) + return 0; + } + + return -1; +} + +void credential_reject(struct credential *c, const struct string_list *methods) +{ + int i; + + if (methods && c->username) { + for (i = 0; i < methods->nr; i++) { + /* ignore errors, there's nothing we can do */ + credential_do(c, methods->items[i].string, "--reject"); + } + } + + free(c->username); + c->username = NULL; + free(c->password); + c->password = NULL; +} diff --git a/credential.h b/credential.h new file mode 100644 index 0000000000..383b720bb5 --- /dev/null +++ b/credential.h @@ -0,0 +1,19 @@ +#ifndef CREDENTIAL_H +#define CREDENTIAL_H + +struct credential { + char *description; + char *username; + char *password; + char *unique; +}; + +struct string_list; + +int credential_getpass(struct credential *); + +int credential_fill_gently(struct credential *, const struct string_list *methods); +void credential_fill(struct credential *, const struct string_list *methods); +void credential_reject(struct credential *, const struct string_list *methods); + +#endif /* CREDENTIAL_H */ diff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh new file mode 100755 index 0000000000..447e98339e --- /dev/null +++ b/t/t0300-credentials.sh @@ -0,0 +1,175 @@ +#!/bin/sh + +test_description='basic credential helper tests' +. ./test-lib.sh + +# Try a set of credential helpers; the expected +# stdout and stderr should be provided on stdin, +# separated by "--". +check() { + while read line; do + case "$line" in + --) break ;; + *) echo "$line" ;; + esac + done >expect-stdout && + cat >expect-stderr && + test-credential "$@" >stdout 2>stderr && + test_cmp expect-stdout stdout && + test_cmp expect-stderr stderr +} + +test_expect_success 'setup helper scripts' ' + cat >dump <<-\EOF && + whoami=$1; shift + if test $# = 0; then + echo >&2 "$whoami: " + else + for i in "$@"; do + echo >&2 "$whoami: $i" + done + fi + EOF + chmod +x dump && + + cat >git-credential-useless <<-\EOF && + #!/bin/sh + dump useless "$@" + exit 0 + EOF + chmod +x git-credential-useless && + + cat >git-credential-verbatim <<-\EOF && + #!/bin/sh + user=$1; shift + pass=$1; shift + dump verbatim "$@" + test -z "$user" || echo username=$user + test -z "$pass" || echo password=$pass + EOF + chmod +x git-credential-verbatim && + + cat >askpass <<-\EOF && + #!/bin/sh + echo >&2 askpass: $* + echo askpass-result + EOF + chmod +x askpass && + GIT_ASKPASS=askpass && + export GIT_ASKPASS && + + PATH="$PWD:$PATH" +' + +test_expect_success 'credential_fill invokes helper' ' + check "verbatim foo bar" <<-\EOF + username=foo + password=bar + -- + verbatim: + EOF +' + +test_expect_success 'credential_fill invokes multiple helpers' ' + check useless "verbatim foo bar" <<-\EOF + username=foo + password=bar + -- + useless: + verbatim: + EOF +' + +test_expect_success 'credential_fill stops when we get a full response' ' + check "verbatim one two" "verbatim three four" <<-\EOF + username=one + password=two + -- + verbatim: + EOF +' + +test_expect_success 'credential_fill continues through partial response' ' + check "verbatim one \"\"" "verbatim two three" <<-\EOF + username=two + password=three + -- + verbatim: + verbatim: --username=one + EOF +' + +test_expect_success 'credential_fill passes along metadata' ' + check --description=foo --unique=bar "verbatim one two" <<-\EOF + username=one + password=two + -- + verbatim: --description=foo + verbatim: --unique=bar + EOF +' + +test_expect_success 'credential_reject calls all helpers' ' + check --reject --username=foo useless "verbatim one two" <<-\EOF + -- + useless: --reject + useless: --username=foo + verbatim: --reject + verbatim: --username=foo + EOF +' + +test_expect_success 'do not bother rejecting empty credential' ' + check --reject useless <<-\EOF + -- + EOF +' + +test_expect_success 'usernames can be preserved' ' + check --username=one "verbatim \"\" three" <<-\EOF + username=one + password=three + -- + verbatim: --username=one +' + +test_expect_success 'usernames can be overridden' ' + check --username=one "verbatim two three" <<-\EOF + username=two + password=three + -- + verbatim: --username=one + EOF +' + +test_expect_success 'do not bother completing already-full credential' ' + check --username=one --password=two "verbatim three four" <<-\EOF + username=one + password=two + -- + EOF +' + +# We can't test the basic terminal password prompt here because +# getpass() tries too hard to find the real terminal. But if our +# askpass helper is run, we know the internal getpass is working. +test_expect_success 'empty methods falls back to internal getpass' ' + check <<-\EOF + username=askpass-result + password=askpass-result + -- + askpass: Username: + askpass: Password: + EOF +' + +test_expect_success 'internal getpass does not ask for known username' ' + check --username=foo <<-\EOF + username=foo + password=askpass-result + -- + askpass: Password: + EOF +' + +test_done diff --git a/test-credential.c b/test-credential.c new file mode 100644 index 0000000000..3929efd9b0 --- /dev/null +++ b/test-credential.c @@ -0,0 +1,47 @@ +#include "cache.h" +#include "credential.h" +#include "string-list.h" +#include "parse-options.h" + +int main(int argc, const char **argv) +{ + int reject = 0; + struct credential c = { NULL }; + struct string_list methods = STRING_LIST_INIT_NODUP; + const char *const usage[] = { + "test-credential [options] [method...]", + NULL + }; + struct option options[] = { + OPT_BOOLEAN(0, "reject", &reject, "reject"), + OPT_STRING(0, "description", &c.description, "desc", + "description"), + OPT_STRING(0, "unique", &c.unique, "token", + "unique"), + OPT_STRING(0, "username", &c.username, "name", "username"), + OPT_STRING(0, "password", &c.password, "pass", "password"), + OPT_END() + }; + int i; + + argc = parse_options(argc, argv, NULL, options, usage, 0); + for (i = 0; i < argc; i++) + string_list_append(&methods, argv[i]); + /* credential_reject will try to free() */ + if (c.username) + c.username = xstrdup(c.username); + if (c.password) + c.password = xstrdup(c.password); + + if (reject) + credential_reject(&c, &methods); + else + credential_fill(&c, &methods); + + if (c.username) + printf("username=%s\n", c.username); + if (c.password) + printf("password=%s\n", c.password); + + return 0; +} From 41b870190386a5419df7c55d3b3f8722e8744dc4 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Mon, 18 Jul 2011 03:50:53 -0400 Subject: [PATCH 06/13] http: use credential API to get passwords This patch converts the http code to use the new credential API, both for http authentication as well as for getting certificate passwords. Most of the code change is simply variable naming (the passwords are now contained inside a struct). The biggest change is determining a "unique" context to pass to the credential API. This patch uses "http:$host" for http authentication and "cert:$file" for opening certificate files. We pass an empty list of methods to the credential API, which means that we will use the internal credential_getpass function. This should yield no behavior change, except that we now print "Password for 'certificate':" instead of "Certificate Password:" when asking for certificate passwords. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- http.c | 94 ++++++++++++++++++++++++------------------- t/t5550-http-fetch.sh | 2 +- 2 files changed, 53 insertions(+), 43 deletions(-) diff --git a/http.c b/http.c index 89e3cf4bd8..4c047be802 100644 --- a/http.c +++ b/http.c @@ -3,6 +3,7 @@ #include "sideband.h" #include "run-command.h" #include "url.h" +#include "credential.h" int data_received; int active_requests; @@ -42,7 +43,7 @@ static long curl_low_speed_time = -1; static int curl_ftp_no_epsv; static const char *curl_http_proxy; static const char *curl_cookie_file; -static char *user_name, *user_pass; +static struct credential http_auth; static const char *user_agent; #if LIBCURL_VERSION_NUM >= 0x071700 @@ -53,7 +54,7 @@ static const char *user_agent; #define CURLOPT_KEYPASSWD CURLOPT_SSLCERTPASSWD #endif -static char *ssl_cert_password; +static struct credential cert_auth; static int ssl_cert_password_required; static struct curl_slist *pragma_header; @@ -211,11 +212,11 @@ static int http_options(const char *var, const char *value, void *cb) static void init_curl_http_auth(CURL *result) { - if (user_name) { + if (http_auth.username) { struct strbuf up = STRBUF_INIT; - if (!user_pass) - user_pass = xstrdup(git_getpass("Password: ")); - strbuf_addf(&up, "%s:%s", user_name, user_pass); + credential_fill(&http_auth, NULL); + strbuf_addf(&up, "%s:%s", + http_auth.username, http_auth.password); curl_easy_setopt(result, CURLOPT_USERPWD, strbuf_detach(&up, NULL)); } @@ -223,18 +224,19 @@ static void init_curl_http_auth(CURL *result) static int has_cert_password(void) { - if (ssl_cert_password != NULL) - return 1; if (ssl_cert == NULL || ssl_cert_password_required != 1) return 0; - /* Only prompt the user once. */ - ssl_cert_password_required = -1; - ssl_cert_password = git_getpass("Certificate Password: "); - if (ssl_cert_password != NULL) { - ssl_cert_password = xstrdup(ssl_cert_password); - return 1; - } else - return 0; + if (!cert_auth.description) + cert_auth.description = "certificate"; + if (!cert_auth.unique) { + struct strbuf unique = STRBUF_INIT; + strbuf_addf(&unique, "cert:%s", ssl_cert); + cert_auth.unique = strbuf_detach(&unique, NULL); + } + if (!cert_auth.username) + cert_auth.username = xstrdup(""); + credential_fill(&cert_auth, NULL); + return 1; } static CURL *get_curl_handle(void) @@ -263,7 +265,7 @@ static CURL *get_curl_handle(void) if (ssl_cert != NULL) curl_easy_setopt(result, CURLOPT_SSLCERT, ssl_cert); if (has_cert_password()) - curl_easy_setopt(result, CURLOPT_KEYPASSWD, ssl_cert_password); + curl_easy_setopt(result, CURLOPT_KEYPASSWD, cert_auth.password); #if LIBCURL_VERSION_NUM >= 0x070903 if (ssl_key != NULL) curl_easy_setopt(result, CURLOPT_SSLKEY, ssl_key); @@ -307,10 +309,12 @@ static CURL *get_curl_handle(void) static void http_auth_init(const char *url) { - char *at, *colon, *cp, *slash; + const char *at, *colon, *cp, *slash, *host, *proto_end; + char *decoded; + struct strbuf unique = STRBUF_INIT; - cp = strstr(url, "://"); - if (!cp) + proto_end = strstr(url, "://"); + if (!proto_end) return; /* @@ -319,20 +323,31 @@ static void http_auth_init(const char *url) * "proto://@/...", or just * "proto:///..."? */ - cp += 3; + cp = proto_end + 3; at = strchr(cp, '@'); colon = strchr(cp, ':'); slash = strchrnul(cp, '/'); - if (!at || slash <= at) - return; /* No credentials */ - if (!colon || at <= colon) { - /* Only username */ - user_name = url_decode_mem(cp, at - cp); - user_pass = NULL; - } else { - user_name = url_decode_mem(cp, colon - cp); - user_pass = url_decode_mem(colon + 1, at - (colon + 1)); + + if (!at || slash <= at) { + /* No credentials, but we may have to ask for some later */ + host = cp; } + else if (!colon || at <= colon) { + /* Only username */ + http_auth.username = url_decode_mem(cp, at - cp); + host = at + 1; + } else { + http_auth.username = url_decode_mem(cp, colon - cp); + http_auth.password = url_decode_mem(colon + 1, at - (colon + 1)); + host = at + 1; + } + + strbuf_add(&unique, url, proto_end - url); + strbuf_addch(&unique, ':'); + decoded = url_decode_mem(host, slash - host); + strbuf_addstr(&unique, decoded); + free(decoded); + http_auth.unique = strbuf_detach(&unique, NULL); } static void set_from_env(const char **var, const char *envname) @@ -456,10 +471,10 @@ void http_cleanup(void) curl_http_proxy = NULL; } - if (ssl_cert_password != NULL) { - memset(ssl_cert_password, 0, strlen(ssl_cert_password)); - free(ssl_cert_password); - ssl_cert_password = NULL; + if (cert_auth.password) { + memset(cert_auth.password, 0, strlen(cert_auth.password)); + free(cert_auth.password); + cert_auth.password = NULL; } ssl_cert_password_required = 0; } @@ -819,16 +834,11 @@ static int http_request(const char *url, void *result, int target, int options) else if (missing_target(&results)) ret = HTTP_MISSING_TARGET; else if (results.http_code == 401) { - if (user_name) { + if (http_auth.username) { + credential_reject(&http_auth, NULL); ret = HTTP_NOAUTH; } else { - /* - * git_getpass is needed here because its very likely stdin/stdout are - * pipes to our parent process. So we instead need to use /dev/tty, - * but that is non-portable. Using git_getpass() can at least be stubbed - * on other platforms with a different implementation if/when necessary. - */ - user_name = xstrdup(git_getpass("Username: ")); + credential_fill(&http_auth, NULL); init_curl_http_auth(slot->curl); ret = HTTP_REAUTH; } diff --git a/t/t5550-http-fetch.sh b/t/t5550-http-fetch.sh index ed4db09bed..af3bc6bad4 100755 --- a/t/t5550-http-fetch.sh +++ b/t/t5550-http-fetch.sh @@ -66,7 +66,7 @@ test_expect_success 'cloning password-protected repository can fail' ' test_expect_success 'http auth can use user/pass in URL' ' >askpass-query && - echo wrong >askpass-reponse && + echo wrong >askpass-response && git clone "$HTTPD_URL_USER_PASS/auth/repo.git" clone-auth-none && test_cmp askpass-expect-none askpass-query ' From 20a93d32fa7de2f097cb0bc1acfc5a6182db9cd3 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Mon, 18 Jul 2011 03:51:08 -0400 Subject: [PATCH 07/13] look for credentials in config before prompting When an http request receives a 401, we ask the user for both a username and password. While it's generally not a good idea for us to store the password in plaintext, having to input the username each time is annoying, and can be easily solved with a config variable. This patch teaches the credential subsystem to look up items in the git config file before prompting. Items are indexed by the "unique" token passed to the credential system. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- credential.c | 38 ++++++++++++++++++++++++++++++++++++++ credential.h | 1 + t/t0300-credentials.sh | 10 ++++++++++ t/t5550-http-fetch.sh | 8 ++++++++ 4 files changed, 57 insertions(+) diff --git a/credential.c b/credential.c index f33c66f126..907b90bad1 100644 --- a/credential.c +++ b/credential.c @@ -4,6 +4,43 @@ #include "string-list.h" #include "run-command.h" +static int credential_config_callback(const char *var, const char *value, + void *data) +{ + struct credential *c = data; + + if (!value) + return 0; + + var = skip_prefix(var, "credential."); + if (!var) + return 0; + + var = skip_prefix(var, c->unique); + if (!var) + return 0; + + if (*var != '.') + return 0; + var++; + + if (!strcmp(var, "username")) { + if (!c->username) + c->username = xstrdup(value); + } + else if (!strcmp(var, "password")) { + free(c->password); + c->password = xstrdup(value); + } + return 0; +} + +void credential_from_config(struct credential *c) +{ + if (c->unique) + git_config(credential_config_callback, c); +} + static char *credential_ask_one(const char *what, const char *desc) { struct strbuf prompt = STRBUF_INIT; @@ -26,6 +63,7 @@ static char *credential_ask_one(const char *what, const char *desc) int credential_getpass(struct credential *c) { + credential_from_config(c); if (!c->username) c->username = credential_ask_one("Username", c->description); diff --git a/credential.h b/credential.h index 383b720bb5..30a0156c0f 100644 --- a/credential.h +++ b/credential.h @@ -11,6 +11,7 @@ struct credential { struct string_list; int credential_getpass(struct credential *); +void credential_from_config(struct credential *); int credential_fill_gently(struct credential *, const struct string_list *methods); void credential_fill(struct credential *, const struct string_list *methods); diff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh index 447e98339e..68d838c26d 100755 --- a/t/t0300-credentials.sh +++ b/t/t0300-credentials.sh @@ -172,4 +172,14 @@ test_expect_success 'internal getpass does not ask for known username' ' EOF ' +test_expect_success 'internal getpass can pull from config' ' + git config credential.foo.username configured-username + check --unique=foo <<-\EOF + username=configured-username + password=askpass-result + -- + askpass: Password: + EOF +' + test_done diff --git a/t/t5550-http-fetch.sh b/t/t5550-http-fetch.sh index af3bc6bad4..c78baaf345 100755 --- a/t/t5550-http-fetch.sh +++ b/t/t5550-http-fetch.sh @@ -85,6 +85,14 @@ test_expect_success 'http auth can request both user and pass' ' test_cmp askpass-expect-both askpass-query ' +test_expect_success 'http auth can pull user from config' ' + >askpass-query && + echo user@host >askpass-response && + git config --global credential.$HTTPD_PROTO:$HTTPD_DEST.username user@host && + git clone "$HTTPD_URL/auth/repo.git" clone-auth-config && + test_cmp askpass-expect-pass askpass-query +' + test_expect_success 'fetch changes via http' ' echo content >>file && git commit -a -m two && From 00b1daa2c109e1d4ccf67b6f92736dbd422d2ba8 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Mon, 18 Jul 2011 03:51:26 -0400 Subject: [PATCH 08/13] allow the user to configure credential helpers The functionality for helpers is already there; we just need to give the users a way to turn it on. The new functionality is enabled whenever a caller of the credentials API passes a NULL method list. This will enable it for all current callers (i.e., the http code). Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- Documentation/technical/api-credentials.txt | 5 ++-- config.c | 4 +++ credential.c | 31 ++++++++++++++++++--- credential.h | 2 ++ t/t5550-http-fetch.sh | 13 +++++++++ 5 files changed, 49 insertions(+), 6 deletions(-) diff --git a/Documentation/technical/api-credentials.txt b/Documentation/technical/api-credentials.txt index 880db92c69..335a007354 100644 --- a/Documentation/technical/api-credentials.txt +++ b/Documentation/technical/api-credentials.txt @@ -25,8 +25,9 @@ Data Structures The credential functions take a `string_list` of methods for acquiring credentials. Each string specifies an external helper which will be run, in order, to acquire credentials, - until both a username and password have been acquired. A NULL or - empty methods list indicates that the internal + until both a username and password have been acquired. A NULL + parameter means to use the default list (as configured by + `credential.helper`); an empty list indicates that the internal `credential_getpass` function should be used. diff --git a/config.c b/config.c index 6b61a849d2..cd2d61e3cb 100644 --- a/config.c +++ b/config.c @@ -9,6 +9,7 @@ #include "exec_cmd.h" #include "strbuf.h" #include "quote.h" +#include "credential.h" #define MAXNAME (256) @@ -791,6 +792,9 @@ int git_default_config(const char *var, const char *value, void *dummy) return 0; } + if (!prefixcmp(var, "credential.")) + return git_default_credential_config(var, value); + /* Add other config variables here and to Documentation/config.txt. */ return 0; } diff --git a/credential.c b/credential.c index 907b90bad1..7a0c751363 100644 --- a/credential.c +++ b/credential.c @@ -4,6 +4,8 @@ #include "string-list.h" #include "run-command.h" +static struct string_list default_methods; + static int credential_config_callback(const char *var, const char *value, void *data) { @@ -173,15 +175,18 @@ void credential_fill(struct credential *c, const struct string_list *methods) { struct strbuf err = STRBUF_INIT; + if (!methods) + methods = &default_methods; + if (!credential_fill_gently(c, methods)) return; strbuf_addstr(&err, "unable to get credentials"); if (c->description) strbuf_addf(&err, "for '%s'", c->description); - if (methods && methods->nr == 1) + if (methods->nr == 1) strbuf_addf(&err, "; tried '%s'", methods->items[0].string); - else if (methods) { + else { int i; strbuf_addstr(&err, "; tried:"); for (i = 0; i < methods->nr; i++) @@ -198,7 +203,10 @@ int credential_fill_gently(struct credential *c, if (c->username && c->password) return 0; - if (!methods || !methods->nr) + if (!methods) + methods = &default_methods; + + if (!methods->nr) return credential_getpass(c); for (i = 0; i < methods->nr; i++) { @@ -214,7 +222,10 @@ void credential_reject(struct credential *c, const struct string_list *methods) { int i; - if (methods && c->username) { + if (!methods) + methods = &default_methods; + + if (c->username) { for (i = 0; i < methods->nr; i++) { /* ignore errors, there's nothing we can do */ credential_do(c, methods->items[i].string, "--reject"); @@ -226,3 +237,15 @@ void credential_reject(struct credential *c, const struct string_list *methods) free(c->password); c->password = NULL; } + +int git_default_credential_config(const char *var, const char *value) +{ + if (!strcmp(var, "credential.helper")) { + if (!value) + return config_error_nonbool(var); + string_list_append(&default_methods, xstrdup(value)); + return 0; + } + + return 0; +} diff --git a/credential.h b/credential.h index 30a0156c0f..788ed8e8bf 100644 --- a/credential.h +++ b/credential.h @@ -17,4 +17,6 @@ int credential_fill_gently(struct credential *, const struct string_list *method void credential_fill(struct credential *, const struct string_list *methods); void credential_reject(struct credential *, const struct string_list *methods); +int git_default_credential_config(const char *var, const char *value); + #endif /* CREDENTIAL_H */ diff --git a/t/t5550-http-fetch.sh b/t/t5550-http-fetch.sh index c78baaf345..407e1cb3c6 100755 --- a/t/t5550-http-fetch.sh +++ b/t/t5550-http-fetch.sh @@ -93,6 +93,19 @@ test_expect_success 'http auth can pull user from config' ' test_cmp askpass-expect-pass askpass-query ' +test_expect_success 'http auth respects credential helpers' ' + cat >credential-helper <<-\EOF && + #!/bin/sh + echo username=user@host + echo password=user@host + EOF + chmod +x credential-helper && + git config --global credential.helper "\"$PWD/credential-helper\"" && + >askpass-query && + git clone "$HTTPD_URL/auth/repo.git" clone-auth-helper && + test_cmp askpass-expect-none askpass-query +' + test_expect_success 'fetch changes via http' ' echo content >>file && git commit -a -m two && From dad8534f864120bb47dce6e7e6a6d523144f5216 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Mon, 18 Jul 2011 03:52:32 -0400 Subject: [PATCH 09/13] http: use hostname in credential description Until now, a request for an http password looked like: Username: Password: Now it will look like: Username for 'example.com': Password for 'example.com': Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- http.c | 7 +++---- t/t5550-http-fetch.sh | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/http.c b/http.c index 4c047be802..d6b2d783ff 100644 --- a/http.c +++ b/http.c @@ -310,7 +310,6 @@ static CURL *get_curl_handle(void) static void http_auth_init(const char *url) { const char *at, *colon, *cp, *slash, *host, *proto_end; - char *decoded; struct strbuf unique = STRBUF_INIT; proto_end = strstr(url, "://"); @@ -342,11 +341,11 @@ static void http_auth_init(const char *url) host = at + 1; } + http_auth.description = url_decode_mem(host, slash - host); + strbuf_add(&unique, url, proto_end - url); strbuf_addch(&unique, ':'); - decoded = url_decode_mem(host, slash - host); - strbuf_addstr(&unique, decoded); - free(decoded); + strbuf_addstr(&unique, http_auth.description); http_auth.unique = strbuf_detach(&unique, NULL); } diff --git a/t/t5550-http-fetch.sh b/t/t5550-http-fetch.sh index 407e1cb3c6..b04261cfb5 100755 --- a/t/t5550-http-fetch.sh +++ b/t/t5550-http-fetch.sh @@ -51,8 +51,8 @@ test_expect_success 'setup askpass helpers' ' GIT_ASKPASS="$PWD/askpass" && export GIT_ASKPASS && >askpass-expect-none && - echo "askpass: Password: " >askpass-expect-pass && - { echo "askpass: Username: " && + echo "askpass: Password for '\''$HTTPD_DEST'\'': " >askpass-expect-pass && + { echo "askpass: Username for '\''$HTTPD_DEST'\'': " && cat askpass-expect-pass } >askpass-expect-both ' From b0c3e61f4d53b59dbf48f76ed730fc4d4c004cc6 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Mon, 18 Jul 2011 03:52:47 -0400 Subject: [PATCH 10/13] docs: end-user documentation for the credential subsystem The credential API and helper format is already defined in technical/api-credentials.txt. This presents the end-user view. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- Documentation/Makefile | 1 + Documentation/config.txt | 11 +++ Documentation/gitcredentials.txt | 139 +++++++++++++++++++++++++++++++ 3 files changed, 151 insertions(+) create mode 100644 Documentation/gitcredentials.txt diff --git a/Documentation/Makefile b/Documentation/Makefile index 36989b7f65..88e7f4774f 100644 --- a/Documentation/Makefile +++ b/Documentation/Makefile @@ -7,6 +7,7 @@ MAN5_TXT=gitattributes.txt gitignore.txt gitmodules.txt githooks.txt \ MAN7_TXT=gitcli.txt gittutorial.txt gittutorial-2.txt \ gitcvs-migration.txt gitcore-tutorial.txt gitglossary.txt \ gitdiffcore.txt gitrevisions.txt gitworkflows.txt +MAN7_TXT += gitcredentials.txt MAN_TXT = $(MAN1_TXT) $(MAN5_TXT) $(MAN7_TXT) MAN_XML=$(patsubst %.txt,%.xml,$(MAN_TXT)) diff --git a/Documentation/config.txt b/Documentation/config.txt index b56959b5dc..13c13f4b84 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -823,6 +823,17 @@ commit.template:: "{tilde}/" is expanded to the value of `$HOME` and "{tilde}user/" to the specified user's home directory. +credential.helper:: + Specify an external helper to be called when a username or + password credential is needed; the helper may consult external + storage to avoid prompting the user for the credentials. See + linkgit:gitcredentials[7] for details. + +credential..username:: + Specify a default username to be used instead of prompting the + user when getting credentials for ``. See + linkgit:gitcredentials[7] for details. + include::diff-config.txt[] difftool..path:: diff --git a/Documentation/gitcredentials.txt b/Documentation/gitcredentials.txt new file mode 100644 index 0000000000..74136ee03a --- /dev/null +++ b/Documentation/gitcredentials.txt @@ -0,0 +1,139 @@ +gitcredentials(7) +================= + +NAME +---- +gitcredentials - providing usernames and passwords to git + +SYNOPSIS +-------- +------------------ +git config credential.https:example.com.username myusername +git config credential.helper "$helper $options" +------------------ + +DESCRIPTION +----------- + +Git will sometimes need credentials from the user in order to perform +operations; for example, it may need to ask for a username and password +in order to access a remote repository over HTTP. This manual describes +the mechanisms git uses to request these credentials, as well as some +features to avoid inputting these credentials repeatedly. + +REQUESTING CREDENTIALS +---------------------- + +Without any credential helpers defined, git will try the following +strategies to ask the user for usernames and passwords: + +1. If the `GIT_ASKPASS` environment variable is set, the program + specified by the variable is invoked. A suitable prompt is provided + to the program on the command line, and the user's input is read + from its standard output. + +2. Otherwise, if the `core.askpass` configuration variable is set, its + value is used as above. + +3. Otherwise, if the `SSH_ASKPASS` environment variable is set, its + value is used as above. + +4. Otherwise, the user is prompted on the terminal. + +AVOIDING REPETITION +------------------- + +It can be cumbersome to input the same credentials over and over. Git +provides two methods to reduce this annoyance: + +1. Static configuration of usernames for a given authentication context. + +2. Credential helpers to cache or store passwords, or to interact with + a system password wallet or keychain. + +STATIC CONFIGURATION +-------------------- + +Git can look for credential information in your git config files. Note +that it only makes sense to store usernames, not passwords, as git +config files are not encrypted or usually even protected by filesystem +permissions. + +For a given credential request, git uses a unique token to represent the +context of a request. For example, a request to +`https://example.com/repo.git` would have the context +`https:example.com`. See `CONTEXT TOKENS` below for a full list. + +To statically configure a username, set the configuration variable +`credential.$token.username`. For example, in this instance git will +prompt only for the password, not the username: + +-------------------------------------------------------------- +$ git config --global credential.https:example.com.username me +$ git push https://example.com/repo.git +Password: +-------------------------------------------------------------- + +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 +credential.helper variable. + +1. Find a helper. ++ +------------------------------------------- +$ git help -a | grep credential- +credential-foo +------------------------------------------- + +2. Read its description. ++ +------------------------------------------- +$ git help credential-foo +------------------------------------------- + +3. Tell git to use it. ++ +------------------------------------------- +$ git config --global credential.helper foo +------------------------------------------- + +If there are multiple instances of the `credential.helper` configuration +variable, each helper will be tried in turn, and may provide a username, +password, or nothing. Once git has acquired both a username and a +password, no more helpers will be tried. + +CUSTOM HELPERS +-------------- + +You can write your own custom helpers to interface with any system in +which you keep credentials. See the documentation for git's +link:technical/api-credentials.html[credentials API] for details. + +CONTEXT TOKENS +-------------- + +The full set of unique context tokens provided by git to credential +helpers is: + +`$protocol:$hostname`:: + + A network request to a specific host. `$protocol` is + either `http` or `https`, and `$hostname` is the hostname + provided to git (which may not be fully qualified). + +`cert:$filename`:: + + A password to decrypt a certificate on disk. + +GIT +--- +Part of the linkgit:git[1] suite From 2d6874d83a71b330e51475fd2f6dafedcfeecb44 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Mon, 18 Jul 2011 03:55:12 -0400 Subject: [PATCH 11/13] 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 Signed-off-by: Junio C Hamano --- .gitignore | 2 + .../git-credential-cache--daemon.txt | 26 ++ Documentation/git-credential-cache.txt | 84 ++++++ Documentation/gitcredentials.txt | 17 +- Makefile | 3 + credential-cache--daemon.c | 268 ++++++++++++++++++ credential-cache.c | 163 +++++++++++ git-compat-util.h | 1 + t/t0300-credentials.sh | 91 ++++++ unix-socket.c | 58 ++++ unix-socket.h | 7 + 11 files changed, 715 insertions(+), 5 deletions(-) create mode 100644 Documentation/git-credential-cache--daemon.txt create mode 100644 Documentation/git-credential-cache.txt create mode 100644 credential-cache--daemon.c create mode 100644 credential-cache.c create mode 100644 unix-socket.c create mode 100644 unix-socket.h 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 */ From 3fc3ee73327b955d9759e802ca3e3d9c08429925 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Mon, 18 Jul 2011 03:58:14 -0400 Subject: [PATCH 12/13] credentials: add "store" helper This is like "cache", except that we actually put the credentials on disk. This can be terribly insecure, of course, but we do what we can to protect them by filesystem permissions, and we warn the user in the documentation. This is not unlike using .netrc to store entries, but it's a little more user-friendly. Instead of putting credentials in place ahead of time, we transparently store them after prompting the user for them once. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- .gitignore | 1 + Documentation/git-credential-store.txt | 69 ++++++++++++++++++++ Documentation/gitcredentials.txt | 5 ++ Makefile | 1 + credential-store.c | 87 ++++++++++++++++++++++++++ t/t0300-credentials.sh | 55 ++++++++++++++++ 6 files changed, 218 insertions(+) create mode 100644 Documentation/git-credential-store.txt create mode 100644 credential-store.c diff --git a/.gitignore b/.gitignore index a6b0bd4035..2b7a3f9876 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ /git-count-objects /git-credential-cache /git-credential-cache--daemon +/git-credential-store /git-cvsexportcommit /git-cvsimport /git-cvsserver diff --git a/Documentation/git-credential-store.txt b/Documentation/git-credential-store.txt new file mode 100644 index 0000000000..9fc0f764a1 --- /dev/null +++ b/Documentation/git-credential-store.txt @@ -0,0 +1,69 @@ +git-credential-store(1) +======================= + +NAME +---- +git-credential-store - helper to store credentials on disk + +SYNOPSIS +-------- +------------------- +git config credential.helper 'store [options]' +------------------- + +DESCRIPTION +----------- + +NOTE: Using this helper will store your passwords unencrypted on disk, +protected only by filesystem permissions. If this is not an acceptable +security tradeoff, try linkgit:git-credential-cache[1], or find a helper +that integrates with secure storage provided by your operating system. + +This command requests credentials from the user and stores them +indefinitely on disk for use by future git programs. + +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 +------- + +--store=:: + + Use `` to store credentials. The file will have its + filesystem permissions set to prevent other users on the system + from reading it, but will not be encrypted or otherwise + protected. + +--chain :: + + Specify an external helper to use for retrieving credentials + from the user, instead of the default method. The resulting + credentials are then stored as normal. This option can be + given multiple times; each chained helper will be tried until + credentials are received. + +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 store +$ git push http://example.com/repo.git +Username: +Password: + +[several days later] +$ git push http://example.com/repo.git +[your credentials are used automatically] +------------------------------------ + +GIT +--- +Part of the linkgit:git[1] suite diff --git a/Documentation/gitcredentials.txt b/Documentation/gitcredentials.txt index bd1a3b66f3..33ea56cb7d 100644 --- a/Documentation/gitcredentials.txt +++ b/Documentation/gitcredentials.txt @@ -88,6 +88,11 @@ cache:: Cache credentials in memory for a short period of time. See linkgit:git-credential-cache[1] for details. +store:: + + Store credentials indefinitely on disk. See + linkgit:git-credential-store[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, diff --git a/Makefile b/Makefile index 442e249b08..22e2afc946 100644 --- a/Makefile +++ b/Makefile @@ -422,6 +422,7 @@ PROGRAM_OBJS += http-backend.o PROGRAM_OBJS += sh-i18n--envsubst.o PROGRAM_OBJS += credential-cache.o PROGRAM_OBJS += credential-cache--daemon.o +PROGRAM_OBJS += credential-store.o PROGRAMS += $(patsubst %.o,git-%$X,$(PROGRAM_OBJS)) diff --git a/credential-store.c b/credential-store.c new file mode 100644 index 0000000000..8ab858215b --- /dev/null +++ b/credential-store.c @@ -0,0 +1,87 @@ +#include "cache.h" +#include "credential.h" +#include "string-list.h" +#include "parse-options.h" + +static int lookup_credential(const char *fn, struct credential *c) +{ + config_exclusive_filename = fn; + credential_from_config(c); + return c->username && c->password; +} + +static void store_item(const char *fn, const char *unique, + const char *item, const char *value) +{ + struct strbuf key = STRBUF_INIT; + + if (!unique) + return; + + config_exclusive_filename = fn; + umask(077); + + strbuf_addf(&key, "credential.%s.%s", unique, item); + git_config_set(key.buf, value); + strbuf_release(&key); +} + +static void store_credential(const char *fn, struct credential *c) +{ + store_item(fn, c->unique, "username", c->username); + store_item(fn, c->unique, "password", c->password); +} + +static void remove_credential(const char *fn, struct credential *c) +{ + store_item(fn, c->unique, "username", NULL); + store_item(fn, c->unique, "password", NULL); +} + +int main(int argc, const char **argv) +{ + const char * const usage[] = { + "git credential-store [options]", + NULL + }; + struct credential c = { NULL }; + struct string_list chain = STRING_LIST_INIT_NODUP; + char *store = NULL; + int reject = 0; + struct option options[] = { + OPT_STRING_LIST(0, "store", &store, "file", + "fetch and store credentials in "), + OPT_STRING_LIST(0, "chain", &chain, "helper", + "use to get non-cached credentials"), + OPT_BOOLEAN(0, "reject", &reject, + "reject a stored credential"), + 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); + + if (!store) + store = expand_user_path("~/.git-credentials"); + if (!store) + die("unable to set up default store; use --store"); + + if (reject) + remove_credential(store, &c); + else { + if (!lookup_credential(store, &c)) { + credential_fill(&c, &chain); + store_credential(store, &c); + } + printf("username=%s\n", c.username); + printf("password=%s\n", c.password); + } + return 0; +} diff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh index 994a0aae91..5d5497619a 100755 --- a/t/t0300-credentials.sh +++ b/t/t0300-credentials.sh @@ -273,4 +273,59 @@ test_expect_success 'credential-cache removes rejected credentials' ' EOF ' +test_expect_success 'credential-store stores password' ' + test_when_finished "rm -f .git-credentials" && + check --unique=host store <<-\EOF && + username=askpass-result + password=askpass-result + -- + askpass: Username: + askpass: Password: + EOF + check --unique=host store <<-\EOF + username=askpass-result + password=askpass-result + -- + EOF +' + +test_expect_success 'credential-store requires matching unique token' ' + test_when_finished "rm -f .git-credentials" && + check --unique=host store <<-\EOF && + username=askpass-result + password=askpass-result + -- + askpass: Username: + askpass: Password: + EOF + check --unique=host2 store <<-\EOF + username=askpass-result + password=askpass-result + -- + askpass: Username: + askpass: Password: + EOF +' + +test_expect_success 'credential-store removes rejected credentials' ' + test_when_finished "rm -f .git-credentials" && + check --unique=host store <<-\EOF && + username=askpass-result + password=askpass-result + -- + askpass: Username: + askpass: Password: + EOF + check --reject --unique=host --username=askpass-result store <<-\EOF && + -- + EOF + check --unique=host store <<-\EOF + username=askpass-result + password=askpass-result + -- + askpass: Username: + askpass: Password: + EOF +' + test_done From 1e481b38ac77c9ab32a54bd419d0e60e6d5ee5d1 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Mon, 18 Jul 2011 03:58:59 -0400 Subject: [PATCH 13/13] credentials: add "getpass" helper This just does the normal "ask on the terminal, or use GIT_ASKPASS" logic that we already do. But it's useful for writers of third-party helpers. See the documentation for an example. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- .gitignore | 1 + Documentation/git-credential-getpass.txt | 58 ++++++++++++++++++++++++ Makefile | 1 + credential-getpass.c | 37 +++++++++++++++ 4 files changed, 97 insertions(+) create mode 100644 Documentation/git-credential-getpass.txt create mode 100644 credential-getpass.c diff --git a/.gitignore b/.gitignore index 2b7a3f9876..a26ad005ff 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ /git-credential-cache /git-credential-cache--daemon /git-credential-store +/git-credential-getpass /git-cvsexportcommit /git-cvsimport /git-cvsserver diff --git a/Documentation/git-credential-getpass.txt b/Documentation/git-credential-getpass.txt new file mode 100644 index 0000000000..a37c7a2f97 --- /dev/null +++ b/Documentation/git-credential-getpass.txt @@ -0,0 +1,58 @@ +git-credential-getpass(1) +========================= + +NAME +---- +git-credential-getpass - helper to request credentials from a user + +SYNOPSIS +-------- +[verse] +git credential-getpass + +DESCRIPTION +----------- + +This command requests credentials from the user using git's "default" +scheme, including asking via the terminal and respecting the +`GIT_ASKPASS` environment variable; see linkgit:gitcredentials[7] for a +complete description. The helpers are provided on stdout using git's +credential helper protocol. + +There is no point in using this program as a credential helper by +itself; it is exactly equivalent to git's behavior when no helper is +configured. + +However, writers of third-party helpers may want to invoke this program +to simulate git's behavior. + +EXAMPLES +-------- + +Here's a simple, silly example of a helper that stores credentials on +disk (similar to linkgit:git-credential-store[1]), and how it could use +the `getpass` helper. + +------------------------------------------- +#!/bin/sh + +STORAGE=$HOME/.credentials + +for i in "$@"; do + case "$i" in + --unique=*) + unique=${i#--unique=} ;; + esac +done + +if ! test -e "$STORAGE/$unique"; then + mkdir -m 0700 "$STORAGE" + git credential-getpass "$@" >"$STORAGE/$unique" +fi + +cat "$STORAGE/$unique" +------------------------------------------- + +GIT +--- +Part of the linkgit:git[1] suite diff --git a/Makefile b/Makefile index 22e2afc946..1af45895b2 100644 --- a/Makefile +++ b/Makefile @@ -423,6 +423,7 @@ PROGRAM_OBJS += sh-i18n--envsubst.o PROGRAM_OBJS += credential-cache.o PROGRAM_OBJS += credential-cache--daemon.o PROGRAM_OBJS += credential-store.o +PROGRAM_OBJS += credential-getpass.o PROGRAMS += $(patsubst %.o,git-%$X,$(PROGRAM_OBJS)) diff --git a/credential-getpass.c b/credential-getpass.c new file mode 100644 index 0000000000..6bfb8f8c72 --- /dev/null +++ b/credential-getpass.c @@ -0,0 +1,37 @@ +#include "cache.h" +#include "credential.h" +#include "parse-options.h" +#include "string-list.h" + +int main(int argc, const char **argv) +{ + const char * const usage[] = { + "git credential-getpass [options]", + NULL + }; + struct credential c = { NULL }; + int reject = 0; + struct option options[] = { + OPT_BOOLEAN(0, "reject", &reject, + "reject a stored credential"), + 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); + + if (reject) + return 0; + + credential_getpass(&c); + printf("username=%s\n", c.username); + printf("password=%s\n", c.password); + return 0; +}