diff --git a/.gitignore b/.gitignore index 8572c8c0b0..a26ad005ff 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,10 @@ /git-commit-tree /git-config /git-count-objects +/git-credential-cache +/git-credential-cache--daemon +/git-credential-store +/git-credential-getpass /git-cvsexportcommit /git-cvsimport /git-cvsserver @@ -167,6 +171,7 @@ /gitweb/static/gitweb.js /gitweb/static/gitweb.min.* /test-chmtime +/test-credential /test-ctype /test-date /test-delta diff --git a/Documentation/Makefile b/Documentation/Makefile index 2004fbe630..f6211c5961 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 gitnamespaces.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 0658ffb889..0ecef9d7b0 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -825,6 +825,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/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/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/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 new file mode 100644 index 0000000000..33ea56cb7d --- /dev/null +++ b/Documentation/gitcredentials.txt @@ -0,0 +1,151 @@ +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 currently +includes the following helpers: + +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, +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 diff --git a/Documentation/technical/api-credentials.txt b/Documentation/technical/api-credentials.txt new file mode 100644 index 0000000000..335a007354 --- /dev/null +++ b/Documentation/technical/api-credentials.txt @@ -0,0 +1,114 @@ +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 + 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. + + +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 78040b9e03..56f7c57982 100644 --- a/Makefile +++ b/Makefile @@ -424,10 +424,15 @@ 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 +PROGRAM_OBJS += credential-store.o +PROGRAM_OBJS += credential-getpass.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 @@ -518,6 +523,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 @@ -597,6 +603,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 @@ -681,6 +688,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/config.c b/config.c index 4183f80262..e31d805eac 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) @@ -794,6 +795,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-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/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; +} 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/credential.c b/credential.c new file mode 100644 index 0000000000..7a0c751363 --- /dev/null +++ b/credential.c @@ -0,0 +1,251 @@ +#include "cache.h" +#include "credential.h" +#include "quote.h" +#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) +{ + 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; + 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) +{ + credential_from_config(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 (!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->nr == 1) + strbuf_addf(&err, "; tried '%s'", methods->items[0].string); + else { + 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 = &default_methods; + + if (!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) + 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"); + } + } + + free(c->username); + c->username = NULL; + 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 new file mode 100644 index 0000000000..788ed8e8bf --- /dev/null +++ b/credential.h @@ -0,0 +1,22 @@ +#ifndef CREDENTIAL_H +#define CREDENTIAL_H + +struct credential { + char *description; + char *username; + char *password; + char *unique; +}; + +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); +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/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/http.c b/http.c index a1ea3db499..d6b2d783ff 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,11 +309,11 @@ static CURL *get_curl_handle(void) static void http_auth_init(const char *url) { - char *at, *colon, *cp, *slash, *decoded; - int len; + const char *at, *colon, *cp, *slash, *host, *proto_end; + struct strbuf unique = STRBUF_INIT; - cp = strstr(url, "://"); - if (!cp) + proto_end = strstr(url, "://"); + if (!proto_end) return; /* @@ -320,38 +322,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 */ - 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_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; + + 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; + } + + http_auth.description = url_decode_mem(host, slash - host); + + strbuf_add(&unique, url, proto_end - url); + strbuf_addch(&unique, ':'); + strbuf_addstr(&unique, http_auth.description); + http_auth.unique = strbuf_detach(&unique, NULL); } static void set_from_env(const char **var, const char *envname) @@ -475,10 +470,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; } @@ -838,16 +833,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; } @@ -865,13 +855,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); } /* @@ -894,7 +889,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)) diff --git a/remote-curl.c b/remote-curl.c index 5798aa57b6..4319102791 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); 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/t0300-credentials.sh b/t/t0300-credentials.sh new file mode 100755 index 0000000000..5d5497619a --- /dev/null +++ b/t/t0300-credentials.sh @@ -0,0 +1,331 @@ +#!/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_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_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_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 diff --git a/t/t5550-http-fetch.sh b/t/t5550-http-fetch.sh index a1883ca6b6..b04261cfb5 100755 --- a/t/t5550-http-fetch.sh +++ b/t/t5550-http-fetch.sh @@ -35,11 +35,75 @@ 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 for '\''$HTTPD_DEST'\'': " >askpass-expect-pass && + { echo "askpass: Username for '\''$HTTPD_DEST'\'': " && + 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-response && + 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 '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 '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' ' 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; +} 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 */ 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);