mirror of
https://github.com/git/git.git
synced 2026-03-14 10:53:25 +01:00
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:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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::
|
||||
|
||||
26
Documentation/git-credential-cache--daemon.txt
Normal file
26
Documentation/git-credential-cache--daemon.txt
Normal 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
|
||||
84
Documentation/git-credential-cache.txt
Normal file
84
Documentation/git-credential-cache.txt
Normal 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
|
||||
58
Documentation/git-credential-getpass.txt
Normal file
58
Documentation/git-credential-getpass.txt
Normal 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
|
||||
69
Documentation/git-credential-store.txt
Normal file
69
Documentation/git-credential-store.txt
Normal 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
|
||||
151
Documentation/gitcredentials.txt
Normal file
151
Documentation/gitcredentials.txt
Normal 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
|
||||
114
Documentation/technical/api-credentials.txt
Normal file
114
Documentation/technical/api-credentials.txt
Normal 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.
|
||||
8
Makefile
8
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
|
||||
|
||||
4
config.c
4
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;
|
||||
}
|
||||
|
||||
268
credential-cache--daemon.c
Normal file
268
credential-cache--daemon.c
Normal 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
163
credential-cache.c
Normal 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
37
credential-getpass.c
Normal 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
87
credential-store.c
Normal 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
251
credential.c
Normal 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
22
credential.h
Normal 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 */
|
||||
@@ -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
129
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://<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))
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
331
t/t0300-credentials.sh
Executable 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
|
||||
@@ -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
47
test-credential.c
Normal 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
58
unix-socket.c
Normal 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
7
unix-socket.h
Normal 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
26
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)
|
||||
|
||||
1
url.h
1
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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user