Merge branch 'jk/http-auth-keyring' into next

* jk/http-auth-keyring:
  credentials: add "getpass" helper
  credentials: add "store" helper
  credentials: add "cache" helper
  docs: end-user documentation for the credential subsystem
  http: use hostname in credential description
  allow the user to configure credential helpers
  look for credentials in config before prompting
  http: use credential API to get passwords
  introduce credentials API
  http: retry authentication failures for all http requests
  remote-curl: don't retry auth failures with dumb protocol
  improve httpd auth tests
  url: decode buffers that are not NUL-terminated

Conflicts:
	Documentation/Makefile
This commit is contained in:
Junio C Hamano
2011-08-03 15:25:53 -07:00
28 changed files with 1959 additions and 84 deletions

5
.gitignore vendored
View File

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

View File

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

View File

@@ -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.<context>.username::
Specify a default username to be used instead of prompting the
user when getting credentials for `<context>`. See
linkgit:gitcredentials[7] for details.
include::diff-config.txt[]
difftool.<tool>.path::

View File

@@ -0,0 +1,26 @@
git-credential-cache--daemon(1)
===============================
NAME
----
git-credential-cache--daemon - temporarily store user credentials in memory
SYNOPSIS
--------
[verse]
git credential-cache--daemon <socket>
DESCRIPTION
-----------
NOTE: You probably don't want to invoke this command yourself; it is
started automatically when you use linkgit:git-credential-cache[1].
This command listens on the Unix domain socket specified by `<socket>`
for `git-credential-cache` clients. Clients may store and retrieve
credentials. Each credential is held for a timeout specified by the
client; once no credentials are held, the daemon exits.
GIT
---
Part of the linkgit:git[1] suite

View File

@@ -0,0 +1,84 @@
git-credential-cache(1)
=======================
NAME
----
git-credential-cache - helper to temporarily store passwords in memory
SYNOPSIS
--------
-----------------------------
git config credential.helper 'cache [options]'
-----------------------------
DESCRIPTION
-----------
This command requests credentials from the user and caches them in
memory for use by future git programs. The stored credentials never
touch the disk, and are forgotten after a configurable timeout. The
cache is accessible over a Unix domain socket, restricted to the current
user by filesystem permissions.
You probably don't want to invoke this command directly; it is meant to
be used as a credential helper by other parts of git. See
linkgit:gitcredentials[7] or `EXAMPLES` below.
OPTIONS
-------
--timeout::
Number of seconds to cache credentials (default: 900).
--socket <path>::
Use `<path>` to contact a running cache daemon (or start a new
cache daemon if one is not started). Defaults to
`~/.git-credential-cache/socket`. If your home directory is on a
network-mounted filesystem, you may need to change this to a
local filesystem.
--chain <helper>::
Specify an external helper to use for retrieving credentials
from the user, instead of the default method. The resulting
credentials are then cached as normal. This option can be
given multiple times; each chained helper will be tried until
credentials are received.
--exit::
Tell a running daemon to exit, forgetting all cached
credentials.
Git may provide other options to the program when it is called as a
credential helper; see linkgit:gitcredentials[7].
EXAMPLES
--------
The point of this helper is to reduce the number of times you must type
your username or password. For example:
------------------------------------
$ git config credential.helper cache
$ git push http://example.com/repo.git
Username: <type your username>
Password: <type your password>
[work for 5 more minutes]
$ git push http://example.com/repo.git
[your credentials are used automatically]
------------------------------------
You can provide options via the credential.helper configuration
variable (this example drops the cache time to 5 minutes):
------------------------------------
$ git config credential.helper 'cache --timeout=300'
------------------------------------
GIT
---
Part of the linkgit:git[1] suite

View File

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

View File

@@ -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=<path>::
Use `<path>` 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 <helper>::
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: <type your username>
Password: <type your password>
[several days later]
$ git push http://example.com/repo.git
[your credentials are used automatically]
------------------------------------
GIT
---
Part of the linkgit:git[1] suite

View File

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

View File

@@ -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=<X>`::
`<X>` will contain a human-readable description of the
credential being requested. If this option is not given, no
description is available.
`--unique=<X>`::
`<X>` 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=<X>`::
`<X>` 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.

View File

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

View File

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

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

@@ -0,0 +1,268 @@
#include "cache.h"
#include "credential.h"
#include "unix-socket.h"
struct credential_cache_entry {
struct credential item;
unsigned long expiration;
};
static struct credential_cache_entry *entries;
static int entries_nr;
static int entries_alloc;
static void cache_credential(const struct credential *c, int timeout)
{
struct credential_cache_entry *e;
ALLOC_GROW(entries, entries_nr + 1, entries_alloc);
e = &entries[entries_nr++];
memcpy(&e->item, c, sizeof(*c));
e->expiration = time(NULL) + timeout;
}
static struct credential_cache_entry *lookup_credential(const struct credential *c)
{
int i;
for (i = 0; i < entries_nr; i++) {
struct credential *e = &entries[i].item;
/* We must either both have the same unique token,
* or we must not be using unique tokens at all. */
if (e->unique) {
if (!c->unique || strcmp(e->unique, c->unique))
continue;
}
else if (c->unique)
continue;
/* If we have a username, it must match. Otherwise,
* we will fill in the username. */
if (c->username && strcmp(e->username, c->username))
continue;
return &entries[i];
}
return NULL;
}
static void remove_credential(const struct credential *c)
{
struct credential_cache_entry *e;
e = lookup_credential(c);
if (e)
e->expiration = 0;
}
static int check_expirations(void)
{
int i = 0;
unsigned long now = time(NULL);
unsigned long next = (unsigned long)-1;
while (i < entries_nr) {
if (entries[i].expiration <= now) {
entries_nr--;
if (!entries_nr)
return 0;
free(entries[i].item.description);
free(entries[i].item.unique);
free(entries[i].item.username);
free(entries[i].item.password);
memcpy(&entries[i], &entries[entries_nr], sizeof(*entries));
}
else {
if (entries[i].expiration < next)
next = entries[i].expiration;
i++;
}
}
return next - now;
}
static int read_credential_request(FILE *fh, struct credential *c,
char **action, int *timeout) {
struct strbuf item = STRBUF_INIT;
while (strbuf_getline(&item, fh, '\0') != EOF) {
char *key = item.buf;
char *value = strchr(key, '=');
if (!value) {
warning("cache client sent bogus input: %s", key);
strbuf_release(&item);
return -1;
}
*value++ = '\0';
if (!strcmp(key, "action"))
*action = xstrdup(value);
else if (!strcmp(key, "unique"))
c->unique = xstrdup(value);
else if (!strcmp(key, "username"))
c->username = xstrdup(value);
else if (!strcmp(key, "password"))
c->password = xstrdup(value);
else if (!strcmp(key, "timeout"))
*timeout = atoi(value);
else {
warning("cache client sent bogus key: %s", key);
strbuf_release(&item);
return -1;
}
}
strbuf_release(&item);
return 0;
}
static void serve_one_client(FILE *in, FILE *out)
{
struct credential c = { NULL };
int timeout = -1;
char *action = NULL;
if (read_credential_request(in, &c, &action, &timeout) < 0)
return;
if (!action) {
warning("cache client didn't specify an action");
return;
}
if (!strcmp(action, "exit"))
exit(0);
if (!strcmp(action, "get")) {
struct credential_cache_entry *e = lookup_credential(&c);
if (e) {
fprintf(out, "username=%s\n", e->item.username);
fprintf(out, "password=%s\n", e->item.password);
}
return;
}
if (!strcmp(action, "erase")) {
remove_credential(&c);
return;
}
if (!strcmp(action, "store")) {
if (timeout < 0) {
warning("cache client didn't specify a timeout");
return;
}
remove_credential(&c);
cache_credential(&c, timeout);
return;
}
warning("cache client sent unknown action: %s", action);
return;
}
static int serve_cache_loop(int fd)
{
struct pollfd pfd;
unsigned long wakeup;
wakeup = check_expirations();
if (!wakeup)
return 0;
pfd.fd = fd;
pfd.events = POLLIN;
if (poll(&pfd, 1, 1000 * wakeup) < 0) {
if (errno != EINTR)
die_errno("poll failed");
return 1;
}
if (pfd.revents & POLLIN) {
int client, client2;
FILE *in, *out;
client = accept(fd, NULL, NULL);
if (client < 0) {
warning("accept failed: %s", strerror(errno));
return 1;
}
client2 = dup(client);
if (client2 < 0) {
warning("dup failed: %s", strerror(errno));
close(client);
return 1;
}
in = xfdopen(client, "r");
out = xfdopen(client2, "w");
serve_one_client(in, out);
fclose(in);
fclose(out);
}
return 1;
}
static void serve_cache(const char *socket_path)
{
int fd;
fd = unix_stream_listen(socket_path);
if (fd < 0)
die_errno("unable to bind to '%s'", socket_path);
printf("ok\n");
fclose(stdout);
while (serve_cache_loop(fd))
; /* nothing */
close(fd);
unlink(socket_path);
}
static const char permissions_advice[] =
"The permissions on your socket directory are too loose; other\n"
"users may be able to read your cached credentials. Consider running:\n"
"\n"
" chmod 0700 %s";
static void check_socket_directory(const char *path)
{
struct stat st;
char *path_copy = xstrdup(path);
char *dir = dirname(path_copy);
if (!stat(dir, &st)) {
if (st.st_mode & 077)
die(permissions_advice, dir);
free(path_copy);
return;
}
/*
* We must be sure to create the directory with the correct mode,
* not just chmod it after the fact; otherwise, there is a race
* condition in which somebody can chdir to it, sleep, then try to open
* our protected socket.
*/
if (safe_create_leading_directories_const(dir) < 0)
die_errno("unable to create directories for '%s'", dir);
if (mkdir(dir, 0700) < 0)
die_errno("unable to mkdir '%s'", dir);
free(path_copy);
}
int main(int argc, const char **argv)
{
const char *socket_path = argv[1];
if (!socket_path)
die("usage: git-credential-cache--daemon <socket_path>");
check_socket_directory(socket_path);
serve_cache(socket_path);
return 0;
}

163
credential-cache.c Normal file
View File

@@ -0,0 +1,163 @@
#include "cache.h"
#include "credential.h"
#include "string-list.h"
#include "parse-options.h"
#include "unix-socket.h"
#include "run-command.h"
static int send_request(const char *socket, const struct strbuf *out)
{
int got_data = 0;
int fd = unix_stream_connect(socket);
if (fd < 0)
return -1;
if (write_in_full(fd, out->buf, out->len) < 0)
die_errno("unable to write to cache daemon");
shutdown(fd, SHUT_WR);
while (1) {
char in[1024];
int r;
r = read_in_full(fd, in, sizeof(in));
if (r == 0)
break;
if (r < 0)
die_errno("read error from cache daemon");
write_or_die(1, in, r);
got_data = 1;
}
return got_data;
}
static void out_str(struct strbuf *out, const char *key, const char *value)
{
if (!value)
return;
strbuf_addf(out, "%s=%s", key, value);
strbuf_addch(out, '\0');
}
static void out_int(struct strbuf *out, const char *key, int value)
{
strbuf_addf(out, "%s=%d", key, value);
strbuf_addch(out, '\0');
}
static int do_cache(const char *socket, const char *action,
const struct credential *c, int timeout)
{
struct strbuf buf = STRBUF_INIT;
int ret;
out_str(&buf, "action", action);
if (c) {
out_str(&buf, "unique", c->unique);
out_str(&buf, "username", c->username);
out_str(&buf, "password", c->password);
}
if (timeout > 0)
out_int(&buf, "timeout", timeout);
ret = send_request(socket, &buf);
strbuf_release(&buf);
return ret;
}
static void spawn_daemon(const char *socket)
{
struct child_process daemon;
const char *argv[] = { NULL, NULL, NULL };
char buf[128];
int r;
memset(&daemon, 0, sizeof(daemon));
argv[0] = "git-credential-cache--daemon";
argv[1] = socket;
daemon.argv = argv;
daemon.no_stdin = 1;
daemon.out = -1;
if (start_command(&daemon))
die_errno("unable to start cache daemon");
r = read_in_full(daemon.out, buf, sizeof(buf));
if (r < 0)
die_errno("unable to read result code from cache daemon");
if (r != 3 || memcmp(buf, "ok\n", 3))
die("cache daemon did not start: %.*s", r, buf);
close(daemon.out);
}
int main(int argc, const char **argv)
{
struct credential c = { NULL };
char *socket_path = NULL;
int timeout = 900;
struct string_list chain = STRING_LIST_INIT_NODUP;
int exit_mode = 0;
int reject_mode = 0;
const char * const usage[] = {
"git credential-cache [options]",
NULL
};
struct option options[] = {
OPT_BOOLEAN(0, "exit", &exit_mode,
"tell a running daemon to exit"),
OPT_BOOLEAN(0, "reject", &reject_mode,
"reject a cached credential"),
OPT_INTEGER(0, "timeout", &timeout,
"number of seconds to cache credentials"),
OPT_STRING(0, "socket", &socket_path, "path",
"path of cache-daemon socket"),
OPT_STRING_LIST(0, "chain", &chain, "helper",
"use <helper> to get non-cached credentials"),
OPT_STRING(0, "username", &c.username, "name",
"an existing username"),
OPT_STRING(0, "description", &c.description, "desc",
"human-readable description of the credential"),
OPT_STRING(0, "unique", &c.unique, "token",
"a unique context for the credential"),
OPT_END()
};
argc = parse_options(argc, argv, NULL, options, usage, 0);
if (argc)
usage_with_options(usage, options);
/* credential_reject wants to free() these */
if (c.username)
c.username = xstrdup(c.username);
if (c.password)
c.password = xstrdup(c.password);
if (!socket_path)
socket_path = expand_user_path("~/.git-credential-cache/socket");
if (!socket_path)
die("unable to find a suitable socket path; use --socket");
if (exit_mode) {
do_cache(socket_path, "exit", NULL, -1);
return 0;
}
if (reject_mode) {
do_cache(socket_path, "erase", &c, -1);
credential_reject(&c, &chain);
return 0;
}
if (do_cache(socket_path, "get", &c, -1) > 0)
return 0;
credential_fill(&c, &chain);
printf("username=%s\n", c.username);
printf("password=%s\n", c.password);
if (do_cache(socket_path, "store", &c, timeout) < 0) {
spawn_daemon(socket_path);
do_cache(socket_path, "store", &c, timeout);
}
return 0;
}

37
credential-getpass.c Normal file
View File

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

87
credential-store.c Normal file
View File

@@ -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 <file>"),
OPT_STRING_LIST(0, "chain", &chain, "helper",
"use <helper> 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;
}

251
credential.c Normal file
View File

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

22
credential.h Normal file
View File

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

View File

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

129
http.c
View File

@@ -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://<user>@<host>/...", or just
* "proto://<host>/..."?
*/
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))

View File

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

View File

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

331
t/t0300-credentials.sh Executable file
View File

@@ -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: <empty>"
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: <empty>
EOF
'
test_expect_success 'credential_fill invokes multiple helpers' '
check useless "verbatim foo bar" <<-\EOF
username=foo
password=bar
--
useless: <empty>
verbatim: <empty>
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: <empty>
EOF
'
test_expect_success 'credential_fill continues through partial response' '
check "verbatim one \"\"" "verbatim two three" <<-\EOF
username=two
password=three
--
verbatim: <empty>
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

View File

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

47
test-credential.c Normal file
View File

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

58
unix-socket.c Normal file
View File

@@ -0,0 +1,58 @@
#include "cache.h"
#include "unix-socket.h"
static int unix_stream_socket(void)
{
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0)
die_errno("unable to create socket");
return fd;
}
static void unix_sockaddr_init(struct sockaddr_un *sa, const char *path)
{
int size = strlen(path) + 1;
if (size > sizeof(sa->sun_path))
die("socket path is too long to fit in sockaddr");
memset(sa, 0, sizeof(*sa));
sa->sun_family = AF_UNIX;
memcpy(sa->sun_path, path, size);
}
int unix_stream_connect(const char *path)
{
int fd;
struct sockaddr_un sa;
unix_sockaddr_init(&sa, path);
fd = unix_stream_socket();
if (connect(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
close(fd);
return -1;
}
return fd;
}
int unix_stream_listen(const char *path)
{
int fd;
struct sockaddr_un sa;
unix_sockaddr_init(&sa, path);
fd = unix_stream_socket();
if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
unlink(path);
if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
close(fd);
return -1;
}
}
if (listen(fd, 5) < 0) {
close(fd);
return -1;
}
return fd;
}

7
unix-socket.h Normal file
View File

@@ -0,0 +1,7 @@
#ifndef UNIX_SOCKET_H
#define UNIX_SOCKET_H
int unix_stream_connect(const char *path);
int unix_stream_listen(const char *path);
#endif /* UNIX_SOCKET_H */

26
url.c
View File

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

1
url.h
View File

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