Merge branch 'jk/credentials'
* jk/credentials: t: add test harness for external credential helpers credentials: add "store" helper strbuf: add strbuf_add*_urlencode Makefile: unix sockets may not available on some platforms credentials: add "cache" helper docs: end-user documentation for the credential subsystem credential: make relevance of http path configurable credential: add credential.*.username credential: apply helper config http: use credential API to get passwords credential: add function for parsing url components introduce credentials API t5550: fix typo test-lib: add test_config_global variant Conflicts: strbuf.c
This commit is contained in:
commit
367d20ec6b
4
.gitignore
vendored
4
.gitignore
vendored
@ -30,6 +30,9 @@
|
||||
/git-commit-tree
|
||||
/git-config
|
||||
/git-count-objects
|
||||
/git-credential-cache
|
||||
/git-credential-cache--daemon
|
||||
/git-credential-store
|
||||
/git-cvsexportcommit
|
||||
/git-cvsimport
|
||||
/git-cvsserver
|
||||
@ -167,6 +170,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))
|
||||
|
@ -834,6 +834,29 @@ 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.useHttpPath::
|
||||
When acquiring credentials, consider the "path" component of an http
|
||||
or https URL to be important. Defaults to false. See
|
||||
linkgit:gitcredentials[7] for more information.
|
||||
|
||||
credential.username::
|
||||
If no username is set for a network authentication, use this username
|
||||
by default. See credential.<context>.* below, and
|
||||
linkgit:gitcredentials[7].
|
||||
|
||||
credential.<url>.*::
|
||||
Any of the credential.* options above can be applied selectively to
|
||||
some credentials. For example "credential.https://example.com.username"
|
||||
would set the default username only for https connections to
|
||||
example.com. See linkgit:gitcredentials[7] for details on how URLs are
|
||||
matched.
|
||||
|
||||
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
|
77
Documentation/git-credential-cache.txt
Normal file
77
Documentation/git-credential-cache.txt
Normal file
@ -0,0 +1,77 @@
|
||||
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 caches credentials 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 <seconds>::
|
||||
|
||||
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.
|
||||
|
||||
CONTROLLING THE DAEMON
|
||||
----------------------
|
||||
|
||||
If you would like the daemon to exit early, forgetting all cached
|
||||
credentials before their timeout, you can issue an `exit` action:
|
||||
|
||||
--------------------------------------
|
||||
git credential-cache exit
|
||||
--------------------------------------
|
||||
|
||||
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
|
75
Documentation/git-credential-store.txt
Normal file
75
Documentation/git-credential-store.txt
Normal file
@ -0,0 +1,75 @@
|
||||
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 stores credentials 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. Defaults to `~/.git-credentials`.
|
||||
|
||||
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]
|
||||
------------------------------------------
|
||||
|
||||
STORAGE FORMAT
|
||||
--------------
|
||||
|
||||
The `.git-credentials` file is stored in plaintext. Each credential is
|
||||
stored on its own line as a URL like:
|
||||
|
||||
------------------------------
|
||||
https://user:pass@example.com
|
||||
------------------------------
|
||||
|
||||
When git needs authentication for a particular URL context,
|
||||
credential-store will consider that context a pattern to match against
|
||||
each entry in the credentials file. If the protocol, hostname, and
|
||||
username (if we already have one) match, then the password is returned
|
||||
to git. See the discussion of configuration in linkgit:gitcredentials[7]
|
||||
for more information.
|
||||
|
||||
GIT
|
||||
---
|
||||
Part of the linkgit:git[1] suite
|
183
Documentation/gitcredentials.txt
Normal file
183
Documentation/gitcredentials.txt
Normal file
@ -0,0 +1,183 @@
|
||||
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.
|
||||
|
||||
The first is simple and appropriate if you do not have secure storage available
|
||||
for a password. It is generally configured by adding this to your config:
|
||||
|
||||
---------------------------------------
|
||||
[credential "https://example.com"]
|
||||
username = me
|
||||
---------------------------------------
|
||||
|
||||
Credential helpers, on the other hand, are external programs from which git can
|
||||
request both usernames and passwords; they typically interface with secure
|
||||
storage provided by the OS or other programs.
|
||||
|
||||
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 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.
|
||||
|
||||
|
||||
CREDENTIAL CONTEXTS
|
||||
-------------------
|
||||
|
||||
Git considers each credential to have a context defined by a URL. This context
|
||||
is used to look up context-specific configuration, and is passed to any
|
||||
helpers, which may use it as an index into secure storage.
|
||||
|
||||
For instance, imagine we are accessing `https://example.com/foo.git`. When git
|
||||
looks into a config file to see if a section matches this context, it will
|
||||
consider the two a match if the context is a more-specific subset of the
|
||||
pattern in the config file. For example, if you have this in your config file:
|
||||
|
||||
--------------------------------------
|
||||
[credential "https://example.com"]
|
||||
username = foo
|
||||
--------------------------------------
|
||||
|
||||
then we will match: both protocols are the same, both hosts are the same, and
|
||||
the "pattern" URL does not care about the path component at all. However, this
|
||||
context would not match:
|
||||
|
||||
--------------------------------------
|
||||
[credential "https://kernel.org"]
|
||||
username = foo
|
||||
--------------------------------------
|
||||
|
||||
because the hostnames differ. Nor would it match `foo.example.com`; git
|
||||
compares hostnames exactly, without considering whether two hosts are part of
|
||||
the same domain. Likewise, a config entry for `http://example.com` would not
|
||||
match: git compares the protocols exactly.
|
||||
|
||||
|
||||
CONFIGURATION OPTIONS
|
||||
---------------------
|
||||
|
||||
Options for a credential context can be configured either in
|
||||
`credential.\*` (which applies to all credentials), or
|
||||
`credential.<url>.\*`, where <url> matches the context as described
|
||||
above.
|
||||
|
||||
The following options are available in either location:
|
||||
|
||||
helper::
|
||||
|
||||
The name of an external credential helper, and any associated options.
|
||||
If the helper name is not an absolute path, then the string `git
|
||||
credential-` is prepended. The resulting string is executed by the
|
||||
shell (so, for example, setting this to `foo --option=bar` will execute
|
||||
`git credential-foo --option=bar` via the shell. See the manual of
|
||||
specific helpers for examples of their use.
|
||||
|
||||
username::
|
||||
|
||||
A default username, if one is not provided in the URL.
|
||||
|
||||
useHttpPath::
|
||||
|
||||
By default, git does not consider the "path" component of an http URL
|
||||
to be worth matching via external helpers. This means that a credential
|
||||
stored for `https://example.com/foo.git` will also be used for
|
||||
`https://example.com/bar.git`. If you do want to distinguish these
|
||||
cases, set this option to `true`.
|
||||
|
||||
|
||||
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.
|
||||
|
||||
GIT
|
||||
---
|
||||
Part of the linkgit:git[1] suite
|
245
Documentation/technical/api-credentials.txt
Normal file
245
Documentation/technical/api-credentials.txt
Normal file
@ -0,0 +1,245 @@
|
||||
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
|
||||
along with any associated context. All string fields should be
|
||||
heap-allocated (or NULL if they are not known or not applicable).
|
||||
The meaning of the individual context fields is the same as
|
||||
their counterparts in the helper protocol; see the section below
|
||||
for a description of each field.
|
||||
+
|
||||
The `helpers` member of the struct is a `string_list` of helpers. Each
|
||||
string specifies an external helper which will be run, in order, to
|
||||
either acquire or store credentials. See the section on credential
|
||||
helpers below.
|
||||
+
|
||||
This struct should always be initialized with `CREDENTIAL_INIT` or
|
||||
`credential_init`.
|
||||
|
||||
|
||||
Functions
|
||||
---------
|
||||
|
||||
`credential_init`::
|
||||
|
||||
Initialize a credential structure, setting all fields to empty.
|
||||
|
||||
`credential_clear`::
|
||||
|
||||
Free any resources associated with the credential structure,
|
||||
returning it to a pristine initialized state.
|
||||
|
||||
`credential_fill`::
|
||||
|
||||
Instruct the credential subsystem to fill the username and
|
||||
password fields of the passed credential struct by first
|
||||
consulting helpers, then asking the user. After this function
|
||||
returns, the username and password fields of the credential are
|
||||
guaranteed to be non-NULL. If an error occurs, the function will
|
||||
die().
|
||||
|
||||
`credential_reject`::
|
||||
|
||||
Inform the credential subsystem that the provided credentials
|
||||
have been rejected. This will cause the credential subsystem to
|
||||
notify any helpers of the rejection (which allows them, for
|
||||
example, to purge the invalid credentials from storage). It
|
||||
will also free() the username and password fields of the
|
||||
credential and set them to NULL (readying the credential for
|
||||
another call to `credential_fill`). Any errors from helpers are
|
||||
ignored.
|
||||
|
||||
`credential_approve`::
|
||||
|
||||
Inform the credential subsystem that the provided credentials
|
||||
were successfully used for authentication. This will cause the
|
||||
credential subsystem to notify any helpers of the approval, so
|
||||
that they may store the result to be used again. Any errors
|
||||
from helpers are ignored.
|
||||
|
||||
`credential_from_url`::
|
||||
|
||||
Parse a URL into broken-down credential fields.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
The example below shows how the functions of the credential API could be
|
||||
used to login to a fictitious "foo" service on a remote host:
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
int foo_login(struct foo_connection *f)
|
||||
{
|
||||
int status;
|
||||
/*
|
||||
* Create a credential with some context; we don't yet know the
|
||||
* username or password.
|
||||
*/
|
||||
|
||||
struct credential c = CREDENTIAL_INIT;
|
||||
c.protocol = xstrdup("foo");
|
||||
c.host = xstrdup(f->hostname);
|
||||
|
||||
/*
|
||||
* Fill in the username and password fields by contacting
|
||||
* helpers and/or asking the user. The function will die if it
|
||||
* fails.
|
||||
*/
|
||||
credential_fill(&c);
|
||||
|
||||
/*
|
||||
* Otherwise, we have a username and password. Try to use it.
|
||||
*/
|
||||
status = send_foo_login(f, c.username, c.password);
|
||||
switch (status) {
|
||||
case FOO_OK:
|
||||
/* It worked. Store the credential for later use. */
|
||||
credential_accept(&c);
|
||||
break;
|
||||
case FOO_BAD_LOGIN:
|
||||
/* Erase the credential from storage so we don't try it
|
||||
* again. */
|
||||
credential_reject(&c);
|
||||
break;
|
||||
default:
|
||||
/*
|
||||
* Some other error occured. We don't know if the
|
||||
* credential is good or bad, so report nothing to the
|
||||
* credential subsystem.
|
||||
*/
|
||||
}
|
||||
|
||||
/* Free any associated resources. */
|
||||
credential_clear(&c);
|
||||
|
||||
return status;
|
||||
}
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
|
||||
Credential Helpers
|
||||
------------------
|
||||
|
||||
Credential helpers are programs executed by git to fetch or save
|
||||
credentials from and to long-term storage (where "long-term" is simply
|
||||
longer than a single git process; e.g., credentials may be stored
|
||||
in-memory for a few minutes, or indefinitely on disk).
|
||||
|
||||
Each helper is specified by a single string. The string is transformed
|
||||
by git into a command to be executed using these rules:
|
||||
|
||||
1. If the helper string begins with "!", it is considered a shell
|
||||
snippet, and everything after the "!" becomes the command.
|
||||
|
||||
2. Otherwise, if the helper string begins with an absolute path, the
|
||||
verbatim helper string becomes the command.
|
||||
|
||||
3. Otherwise, the string "git credential-" is prepended to the helper
|
||||
string, and the result becomes the command.
|
||||
|
||||
The resulting command then has an "operation" argument appended to it
|
||||
(see below for details), and the result is executed by the shell.
|
||||
|
||||
Here are some example specifications:
|
||||
|
||||
----------------------------------------------------
|
||||
# run "git credential-foo"
|
||||
foo
|
||||
|
||||
# same as above, but pass an argument to the helper
|
||||
foo --bar=baz
|
||||
|
||||
# the arguments are parsed by the shell, so use shell
|
||||
# quoting if necessary
|
||||
foo --bar="whitespace arg"
|
||||
|
||||
# you can also use an absolute path, which will not use the git wrapper
|
||||
/path/to/my/helper --with-arguments
|
||||
|
||||
# or you can specify your own shell snippet
|
||||
!f() { echo "password=`cat $HOME/.secret`"; }; f
|
||||
----------------------------------------------------
|
||||
|
||||
Generally speaking, rule (3) above is the simplest for users to specify.
|
||||
Authors of credential helpers should make an effort to assist their
|
||||
users by naming their program "git-credential-$NAME", and putting it in
|
||||
the $PATH or $GIT_EXEC_PATH during installation, which will allow a user
|
||||
to enable it with `git config credential.helper $NAME`.
|
||||
|
||||
When a helper is executed, it will have one "operation" argument
|
||||
appended to its command line, which is one of:
|
||||
|
||||
`get`::
|
||||
|
||||
Return a matching credential, if any exists.
|
||||
|
||||
`store`::
|
||||
|
||||
Store the credential, if applicable to the helper.
|
||||
|
||||
`erase`::
|
||||
|
||||
Remove a matching credential, if any, from the helper's storage.
|
||||
|
||||
The details of the credential will be provided on the helper's stdin
|
||||
stream. The credential is split into a set of named attributes.
|
||||
Attributes are provided to the helper, one per line. Each attribute is
|
||||
specified by a key-value pair, separated by an `=` (equals) sign,
|
||||
followed by a newline. The key may contain any bytes except `=`,
|
||||
newline, or NUL. The value may contain any bytes except newline or NUL.
|
||||
In both cases, all bytes are treated as-is (i.e., there is no quoting,
|
||||
and one cannot transmit a value with newline or NUL in it). The list of
|
||||
attributes is terminated by a blank line or end-of-file.
|
||||
|
||||
Git will send the following attributes (but may not send all of
|
||||
them for a given credential; for example, a `host` attribute makes no
|
||||
sense when dealing with a non-network protocol):
|
||||
|
||||
`protocol`::
|
||||
|
||||
The protocol over which the credential will be used (e.g.,
|
||||
`https`).
|
||||
|
||||
`host`::
|
||||
|
||||
The remote hostname for a network credential.
|
||||
|
||||
`path`::
|
||||
|
||||
The path with which the credential will be used. E.g., for
|
||||
accessing a remote https repository, this will be the
|
||||
repository's path on the server.
|
||||
|
||||
`username`::
|
||||
|
||||
The credential's username, if we already have one (e.g., from a
|
||||
URL, from the user, or from a previously run helper).
|
||||
|
||||
`password`::
|
||||
|
||||
The credential's password, if we are asking it to be stored.
|
||||
|
||||
For a `get` operation, the helper should produce a list of attributes
|
||||
on stdout in the same format. A helper is free to produce a subset, or
|
||||
even no values at all if it has nothing useful to provide. Any provided
|
||||
attributes will overwrite those already known about by git.
|
||||
|
||||
For a `store` or `erase` operation, the helper's output is ignored.
|
||||
If it fails to perform the requested operation, it may complain to
|
||||
stderr to inform the user. If it does not support the requested
|
||||
operation (e.g., a read-only store), it should silently ignore the
|
||||
request.
|
||||
|
||||
If a helper receives any other operation, it should silently ignore the
|
||||
request. This leaves room for future operations to be added (older
|
||||
helpers will just ignore the new requests).
|
15
Makefile
15
Makefile
@ -143,6 +143,8 @@ all::
|
||||
#
|
||||
# Define NO_IPV6 if you lack IPv6 support and getaddrinfo().
|
||||
#
|
||||
# Define NO_UNIX_SOCKETS if your system does not offer unix sockets.
|
||||
#
|
||||
# Define NO_SOCKADDR_STORAGE if your platform does not have struct
|
||||
# sockaddr_storage.
|
||||
#
|
||||
@ -427,10 +429,12 @@ PROGRAM_OBJS += show-index.o
|
||||
PROGRAM_OBJS += upload-pack.o
|
||||
PROGRAM_OBJS += http-backend.o
|
||||
PROGRAM_OBJS += sh-i18n--envsubst.o
|
||||
PROGRAM_OBJS += credential-store.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
|
||||
@ -526,6 +530,7 @@ LIB_H += compat/win32/poll.h
|
||||
LIB_H += compat/win32/dirent.h
|
||||
LIB_H += connected.h
|
||||
LIB_H += convert.h
|
||||
LIB_H += credential.h
|
||||
LIB_H += csum-file.h
|
||||
LIB_H += decorate.h
|
||||
LIB_H += delta.h
|
||||
@ -613,6 +618,7 @@ LIB_OBJS += connect.o
|
||||
LIB_OBJS += connected.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
|
||||
@ -1103,6 +1109,7 @@ ifeq ($(uname_S),Windows)
|
||||
NO_SYS_POLL_H = YesPlease
|
||||
NO_SYMLINK_HEAD = YesPlease
|
||||
NO_IPV6 = YesPlease
|
||||
NO_UNIX_SOCKETS = YesPlease
|
||||
NO_SETENV = YesPlease
|
||||
NO_UNSETENV = YesPlease
|
||||
NO_STRCASESTR = YesPlease
|
||||
@ -1196,6 +1203,7 @@ ifneq (,$(findstring MINGW,$(uname_S)))
|
||||
NO_LIBGEN_H = YesPlease
|
||||
NO_SYS_POLL_H = YesPlease
|
||||
NO_SYMLINK_HEAD = YesPlease
|
||||
NO_UNIX_SOCKETS = YesPlease
|
||||
NO_SETENV = YesPlease
|
||||
NO_UNSETENV = YesPlease
|
||||
NO_STRCASESTR = YesPlease
|
||||
@ -1573,6 +1581,12 @@ ifdef NO_INET_PTON
|
||||
LIB_OBJS += compat/inet_pton.o
|
||||
BASIC_CFLAGS += -DNO_INET_PTON
|
||||
endif
|
||||
ifndef NO_UNIX_SOCKETS
|
||||
LIB_OBJS += unix-socket.o
|
||||
LIB_H += unix-socket.h
|
||||
PROGRAM_OBJS += credential-cache.o
|
||||
PROGRAM_OBJS += credential-cache--daemon.o
|
||||
endif
|
||||
|
||||
ifdef NO_ICONV
|
||||
BASIC_CFLAGS += -DNO_ICONV
|
||||
@ -2208,6 +2222,7 @@ ifdef GIT_TEST_CMP_USE_COPIED_CONTEXT
|
||||
@echo GIT_TEST_CMP_USE_COPIED_CONTEXT=YesPlease >>$@
|
||||
endif
|
||||
@echo GETTEXT_POISON=\''$(subst ','\'',$(subst ','\'',$(GETTEXT_POISON)))'\' >>$@
|
||||
@echo NO_UNIX_SOCKETS=\''$(subst ','\'',$(subst ','\'',$(NO_UNIX_SOCKETS)))'\' >>$@
|
||||
|
||||
### Detect Tck/Tk interpreter path changes
|
||||
ifndef NO_TCLTK
|
||||
|
269
credential-cache--daemon.c
Normal file
269
credential-cache--daemon.c
Normal file
@ -0,0 +1,269 @@
|
||||
#include "cache.h"
|
||||
#include "credential.h"
|
||||
#include "unix-socket.h"
|
||||
#include "sigchain.h"
|
||||
|
||||
static const char *socket_path;
|
||||
|
||||
static void cleanup_socket(void)
|
||||
{
|
||||
if (socket_path)
|
||||
unlink(socket_path);
|
||||
}
|
||||
|
||||
static void cleanup_socket_on_signal(int sig)
|
||||
{
|
||||
cleanup_socket();
|
||||
sigchain_pop(sig);
|
||||
raise(sig);
|
||||
}
|
||||
|
||||
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(struct credential *c, int timeout)
|
||||
{
|
||||
struct credential_cache_entry *e;
|
||||
|
||||
ALLOC_GROW(entries, entries_nr + 1, entries_alloc);
|
||||
e = &entries[entries_nr++];
|
||||
|
||||
/* take ownership of pointers */
|
||||
memcpy(&e->item, c, sizeof(*c));
|
||||
memset(c, 0, 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;
|
||||
if (credential_match(c, e))
|
||||
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)
|
||||
{
|
||||
static unsigned long wait_for_entry_until;
|
||||
int i = 0;
|
||||
unsigned long now = time(NULL);
|
||||
unsigned long next = (unsigned long)-1;
|
||||
|
||||
/*
|
||||
* Initially give the client 30 seconds to actually contact us
|
||||
* and store a credential before we decide there's no point in
|
||||
* keeping the daemon around.
|
||||
*/
|
||||
if (!wait_for_entry_until)
|
||||
wait_for_entry_until = now + 30;
|
||||
|
||||
while (i < entries_nr) {
|
||||
if (entries[i].expiration <= now) {
|
||||
entries_nr--;
|
||||
credential_clear(&entries[i].item);
|
||||
if (i != entries_nr)
|
||||
memcpy(&entries[i], &entries[entries_nr], sizeof(*entries));
|
||||
/*
|
||||
* Stick around 30 seconds in case a new credential
|
||||
* shows up (e.g., because we just removed a failed
|
||||
* one, and we will soon get the correct one).
|
||||
*/
|
||||
wait_for_entry_until = now + 30;
|
||||
}
|
||||
else {
|
||||
if (entries[i].expiration < next)
|
||||
next = entries[i].expiration;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!entries_nr) {
|
||||
if (wait_for_entry_until <= now)
|
||||
return 0;
|
||||
next = wait_for_entry_until;
|
||||
}
|
||||
|
||||
return next - now;
|
||||
}
|
||||
|
||||
static int read_request(FILE *fh, struct credential *c,
|
||||
struct strbuf *action, int *timeout) {
|
||||
static struct strbuf item = STRBUF_INIT;
|
||||
const char *p;
|
||||
|
||||
strbuf_getline(&item, fh, '\n');
|
||||
p = skip_prefix(item.buf, "action=");
|
||||
if (!p)
|
||||
return error("client sent bogus action line: %s", item.buf);
|
||||
strbuf_addstr(action, p);
|
||||
|
||||
strbuf_getline(&item, fh, '\n');
|
||||
p = skip_prefix(item.buf, "timeout=");
|
||||
if (!p)
|
||||
return error("client sent bogus timeout line: %s", item.buf);
|
||||
*timeout = atoi(p);
|
||||
|
||||
if (credential_read(c, fh) < 0)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void serve_one_client(FILE *in, FILE *out)
|
||||
{
|
||||
struct credential c = CREDENTIAL_INIT;
|
||||
struct strbuf action = STRBUF_INIT;
|
||||
int timeout = -1;
|
||||
|
||||
if (read_request(in, &c, &action, &timeout) < 0)
|
||||
/* ignore error */ ;
|
||||
else if (!strcmp(action.buf, "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);
|
||||
}
|
||||
}
|
||||
else if (!strcmp(action.buf, "exit"))
|
||||
exit(0);
|
||||
else if (!strcmp(action.buf, "erase"))
|
||||
remove_credential(&c);
|
||||
else if (!strcmp(action.buf, "store")) {
|
||||
if (timeout < 0)
|
||||
warning("cache client didn't specify a timeout");
|
||||
else if (!c.username || !c.password)
|
||||
warning("cache client gave us a partial credential");
|
||||
else {
|
||||
remove_credential(&c);
|
||||
cache_credential(&c, timeout);
|
||||
}
|
||||
}
|
||||
else
|
||||
warning("cache client sent unknown action: %s", action.buf);
|
||||
|
||||
credential_clear(&c);
|
||||
strbuf_release(&action);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
socket_path = argv[1];
|
||||
|
||||
if (!socket_path)
|
||||
die("usage: git-credential-cache--daemon <socket_path>");
|
||||
check_socket_directory(socket_path);
|
||||
|
||||
atexit(cleanup_socket);
|
||||
sigchain_push_common(cleanup_socket_on_signal);
|
||||
|
||||
serve_cache(socket_path);
|
||||
|
||||
return 0;
|
||||
}
|
120
credential-cache.c
Normal file
120
credential-cache.c
Normal file
@ -0,0 +1,120 @@
|
||||
#include "cache.h"
|
||||
#include "credential.h"
|
||||
#include "string-list.h"
|
||||
#include "parse-options.h"
|
||||
#include "unix-socket.h"
|
||||
#include "run-command.h"
|
||||
|
||||
#define FLAG_SPAWN 0x1
|
||||
#define FLAG_RELAY 0x2
|
||||
|
||||
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 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);
|
||||
}
|
||||
|
||||
static void do_cache(const char *socket, const char *action, int timeout,
|
||||
int flags)
|
||||
{
|
||||
struct strbuf buf = STRBUF_INIT;
|
||||
|
||||
strbuf_addf(&buf, "action=%s\n", action);
|
||||
strbuf_addf(&buf, "timeout=%d\n", timeout);
|
||||
if (flags & FLAG_RELAY) {
|
||||
if (strbuf_read(&buf, 0, 0) < 0)
|
||||
die_errno("unable to relay credential");
|
||||
}
|
||||
|
||||
if (!send_request(socket, &buf))
|
||||
return;
|
||||
if (flags & FLAG_SPAWN) {
|
||||
spawn_daemon(socket);
|
||||
send_request(socket, &buf);
|
||||
}
|
||||
strbuf_release(&buf);
|
||||
}
|
||||
|
||||
int main(int argc, const char **argv)
|
||||
{
|
||||
char *socket_path = NULL;
|
||||
int timeout = 900;
|
||||
const char *op;
|
||||
const char * const usage[] = {
|
||||
"git credential-cache [options] <action>",
|
||||
NULL
|
||||
};
|
||||
struct option options[] = {
|
||||
OPT_INTEGER(0, "timeout", &timeout,
|
||||
"number of seconds to cache credentials"),
|
||||
OPT_STRING(0, "socket", &socket_path, "path",
|
||||
"path of cache-daemon socket"),
|
||||
OPT_END()
|
||||
};
|
||||
|
||||
argc = parse_options(argc, argv, NULL, options, usage, 0);
|
||||
if (!argc)
|
||||
usage_with_options(usage, options);
|
||||
op = argv[0];
|
||||
|
||||
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 (!strcmp(op, "exit"))
|
||||
do_cache(socket_path, op, timeout, 0);
|
||||
else if (!strcmp(op, "get") || !strcmp(op, "erase"))
|
||||
do_cache(socket_path, op, timeout, FLAG_RELAY);
|
||||
else if (!strcmp(op, "store"))
|
||||
do_cache(socket_path, op, timeout, FLAG_RELAY|FLAG_SPAWN);
|
||||
else
|
||||
; /* ignore unknown operation */
|
||||
|
||||
return 0;
|
||||
}
|
157
credential-store.c
Normal file
157
credential-store.c
Normal file
@ -0,0 +1,157 @@
|
||||
#include "cache.h"
|
||||
#include "credential.h"
|
||||
#include "string-list.h"
|
||||
#include "parse-options.h"
|
||||
|
||||
static struct lock_file credential_lock;
|
||||
|
||||
static void parse_credential_file(const char *fn,
|
||||
struct credential *c,
|
||||
void (*match_cb)(struct credential *),
|
||||
void (*other_cb)(struct strbuf *))
|
||||
{
|
||||
FILE *fh;
|
||||
struct strbuf line = STRBUF_INIT;
|
||||
struct credential entry = CREDENTIAL_INIT;
|
||||
|
||||
fh = fopen(fn, "r");
|
||||
if (!fh) {
|
||||
if (errno != ENOENT)
|
||||
die_errno("unable to open %s", fn);
|
||||
return;
|
||||
}
|
||||
|
||||
while (strbuf_getline(&line, fh, '\n') != EOF) {
|
||||
credential_from_url(&entry, line.buf);
|
||||
if (entry.username && entry.password &&
|
||||
credential_match(c, &entry)) {
|
||||
if (match_cb) {
|
||||
match_cb(&entry);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (other_cb)
|
||||
other_cb(&line);
|
||||
}
|
||||
|
||||
credential_clear(&entry);
|
||||
strbuf_release(&line);
|
||||
fclose(fh);
|
||||
}
|
||||
|
||||
static void print_entry(struct credential *c)
|
||||
{
|
||||
printf("username=%s\n", c->username);
|
||||
printf("password=%s\n", c->password);
|
||||
}
|
||||
|
||||
static void print_line(struct strbuf *buf)
|
||||
{
|
||||
strbuf_addch(buf, '\n');
|
||||
write_or_die(credential_lock.fd, buf->buf, buf->len);
|
||||
}
|
||||
|
||||
static void rewrite_credential_file(const char *fn, struct credential *c,
|
||||
struct strbuf *extra)
|
||||
{
|
||||
if (hold_lock_file_for_update(&credential_lock, fn, 0) < 0)
|
||||
die_errno("unable to get credential storage lock");
|
||||
if (extra)
|
||||
print_line(extra);
|
||||
parse_credential_file(fn, c, NULL, print_line);
|
||||
if (commit_lock_file(&credential_lock) < 0)
|
||||
die_errno("unable to commit credential store");
|
||||
}
|
||||
|
||||
static void store_credential(const char *fn, struct credential *c)
|
||||
{
|
||||
struct strbuf buf = STRBUF_INIT;
|
||||
|
||||
/*
|
||||
* Sanity check that what we are storing is actually sensible.
|
||||
* In particular, we can't make a URL without a protocol field.
|
||||
* Without either a host or pathname (depending on the scheme),
|
||||
* we have no primary key. And without a username and password,
|
||||
* we are not actually storing a credential.
|
||||
*/
|
||||
if (!c->protocol || !(c->host || c->path) ||
|
||||
!c->username || !c->password)
|
||||
return;
|
||||
|
||||
strbuf_addf(&buf, "%s://", c->protocol);
|
||||
strbuf_addstr_urlencode(&buf, c->username, 1);
|
||||
strbuf_addch(&buf, ':');
|
||||
strbuf_addstr_urlencode(&buf, c->password, 1);
|
||||
strbuf_addch(&buf, '@');
|
||||
if (c->host)
|
||||
strbuf_addstr_urlencode(&buf, c->host, 1);
|
||||
if (c->path) {
|
||||
strbuf_addch(&buf, '/');
|
||||
strbuf_addstr_urlencode(&buf, c->path, 0);
|
||||
}
|
||||
|
||||
rewrite_credential_file(fn, c, &buf);
|
||||
strbuf_release(&buf);
|
||||
}
|
||||
|
||||
static void remove_credential(const char *fn, struct credential *c)
|
||||
{
|
||||
/*
|
||||
* Sanity check that we actually have something to match
|
||||
* against. The input we get is a restrictive pattern,
|
||||
* so technically a blank credential means "erase everything".
|
||||
* But it is too easy to accidentally send this, since it is equivalent
|
||||
* to empty input. So explicitly disallow it, and require that the
|
||||
* pattern have some actual content to match.
|
||||
*/
|
||||
if (c->protocol || c->host || c->path || c->username)
|
||||
rewrite_credential_file(fn, c, NULL);
|
||||
}
|
||||
|
||||
static int lookup_credential(const char *fn, struct credential *c)
|
||||
{
|
||||
parse_credential_file(fn, c, print_entry, NULL);
|
||||
return c->username && c->password;
|
||||
}
|
||||
|
||||
int main(int argc, const char **argv)
|
||||
{
|
||||
const char * const usage[] = {
|
||||
"git credential-store [options] <action>",
|
||||
NULL
|
||||
};
|
||||
const char *op;
|
||||
struct credential c = CREDENTIAL_INIT;
|
||||
char *file = NULL;
|
||||
struct option options[] = {
|
||||
OPT_STRING(0, "file", &file, "path",
|
||||
"fetch and store credentials in <path>"),
|
||||
OPT_END()
|
||||
};
|
||||
|
||||
umask(077);
|
||||
|
||||
argc = parse_options(argc, argv, NULL, options, usage, 0);
|
||||
if (argc != 1)
|
||||
usage_with_options(usage, options);
|
||||
op = argv[0];
|
||||
|
||||
if (!file)
|
||||
file = expand_user_path("~/.git-credentials");
|
||||
if (!file)
|
||||
die("unable to set up default path; use --file");
|
||||
|
||||
if (credential_read(&c, stdin) < 0)
|
||||
die("unable to read credential");
|
||||
|
||||
if (!strcmp(op, "get"))
|
||||
lookup_credential(file, &c);
|
||||
else if (!strcmp(op, "erase"))
|
||||
remove_credential(file, &c);
|
||||
else if (!strcmp(op, "store"))
|
||||
store_credential(file, &c);
|
||||
else
|
||||
; /* Ignore unknown operation. */
|
||||
|
||||
return 0;
|
||||
}
|
365
credential.c
Normal file
365
credential.c
Normal file
@ -0,0 +1,365 @@
|
||||
#include "cache.h"
|
||||
#include "credential.h"
|
||||
#include "string-list.h"
|
||||
#include "run-command.h"
|
||||
#include "url.h"
|
||||
|
||||
void credential_init(struct credential *c)
|
||||
{
|
||||
memset(c, 0, sizeof(*c));
|
||||
c->helpers.strdup_strings = 1;
|
||||
}
|
||||
|
||||
void credential_clear(struct credential *c)
|
||||
{
|
||||
free(c->protocol);
|
||||
free(c->host);
|
||||
free(c->path);
|
||||
free(c->username);
|
||||
free(c->password);
|
||||
string_list_clear(&c->helpers, 0);
|
||||
|
||||
credential_init(c);
|
||||
}
|
||||
|
||||
int credential_match(const struct credential *want,
|
||||
const struct credential *have)
|
||||
{
|
||||
#define CHECK(x) (!want->x || (have->x && !strcmp(want->x, have->x)))
|
||||
return CHECK(protocol) &&
|
||||
CHECK(host) &&
|
||||
CHECK(path) &&
|
||||
CHECK(username);
|
||||
#undef CHECK
|
||||
}
|
||||
|
||||
static int credential_config_callback(const char *var, const char *value,
|
||||
void *data)
|
||||
{
|
||||
struct credential *c = data;
|
||||
const char *key, *dot;
|
||||
|
||||
key = skip_prefix(var, "credential.");
|
||||
if (!key)
|
||||
return 0;
|
||||
|
||||
if (!value)
|
||||
return config_error_nonbool(var);
|
||||
|
||||
dot = strrchr(key, '.');
|
||||
if (dot) {
|
||||
struct credential want = CREDENTIAL_INIT;
|
||||
char *url = xmemdupz(key, dot - key);
|
||||
int matched;
|
||||
|
||||
credential_from_url(&want, url);
|
||||
matched = credential_match(&want, c);
|
||||
|
||||
credential_clear(&want);
|
||||
free(url);
|
||||
|
||||
if (!matched)
|
||||
return 0;
|
||||
key = dot + 1;
|
||||
}
|
||||
|
||||
if (!strcmp(key, "helper"))
|
||||
string_list_append(&c->helpers, value);
|
||||
else if (!strcmp(key, "username")) {
|
||||
if (!c->username)
|
||||
c->username = xstrdup(value);
|
||||
}
|
||||
else if (!strcmp(key, "usehttppath"))
|
||||
c->use_http_path = git_config_bool(var, value);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int proto_is_http(const char *s)
|
||||
{
|
||||
if (!s)
|
||||
return 0;
|
||||
return !strcmp(s, "https") || !strcmp(s, "http");
|
||||
}
|
||||
|
||||
static void credential_apply_config(struct credential *c)
|
||||
{
|
||||
if (c->configured)
|
||||
return;
|
||||
git_config(credential_config_callback, c);
|
||||
c->configured = 1;
|
||||
|
||||
if (!c->use_http_path && proto_is_http(c->protocol)) {
|
||||
free(c->path);
|
||||
c->path = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static void credential_describe(struct credential *c, struct strbuf *out)
|
||||
{
|
||||
if (!c->protocol)
|
||||
return;
|
||||
strbuf_addf(out, "%s://", c->protocol);
|
||||
if (c->username && *c->username)
|
||||
strbuf_addf(out, "%s@", c->username);
|
||||
if (c->host)
|
||||
strbuf_addstr(out, c->host);
|
||||
if (c->path)
|
||||
strbuf_addf(out, "/%s", c->path);
|
||||
}
|
||||
|
||||
static char *credential_ask_one(const char *what, struct credential *c)
|
||||
{
|
||||
struct strbuf desc = STRBUF_INIT;
|
||||
struct strbuf prompt = STRBUF_INIT;
|
||||
char *r;
|
||||
|
||||
credential_describe(c, &desc);
|
||||
if (desc.len)
|
||||
strbuf_addf(&prompt, "%s for '%s': ", what, desc.buf);
|
||||
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(&desc);
|
||||
strbuf_release(&prompt);
|
||||
return xstrdup(r);
|
||||
}
|
||||
|
||||
static void credential_getpass(struct credential *c)
|
||||
{
|
||||
if (!c->username)
|
||||
c->username = credential_ask_one("Username", c);
|
||||
if (!c->password)
|
||||
c->password = credential_ask_one("Password", c);
|
||||
}
|
||||
|
||||
int credential_read(struct credential *c, FILE *fp)
|
||||
{
|
||||
struct strbuf line = STRBUF_INIT;
|
||||
|
||||
while (strbuf_getline(&line, fp, '\n') != EOF) {
|
||||
char *key = line.buf;
|
||||
char *value = strchr(key, '=');
|
||||
|
||||
if (!line.len)
|
||||
break;
|
||||
|
||||
if (!value) {
|
||||
warning("invalid credential line: %s", key);
|
||||
strbuf_release(&line);
|
||||
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);
|
||||
} else if (!strcmp(key, "protocol")) {
|
||||
free(c->protocol);
|
||||
c->protocol = xstrdup(value);
|
||||
} else if (!strcmp(key, "host")) {
|
||||
free(c->host);
|
||||
c->host = xstrdup(value);
|
||||
} else if (!strcmp(key, "path")) {
|
||||
free(c->path);
|
||||
c->path = xstrdup(value);
|
||||
}
|
||||
/*
|
||||
* Ignore other lines; we don't know what they mean, but
|
||||
* this future-proofs us when later versions of git do
|
||||
* learn new lines, and the helpers are updated to match.
|
||||
*/
|
||||
}
|
||||
|
||||
strbuf_release(&line);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void credential_write_item(FILE *fp, const char *key, const char *value)
|
||||
{
|
||||
if (!value)
|
||||
return;
|
||||
fprintf(fp, "%s=%s\n", key, value);
|
||||
}
|
||||
|
||||
static void credential_write(const struct credential *c, FILE *fp)
|
||||
{
|
||||
credential_write_item(fp, "protocol", c->protocol);
|
||||
credential_write_item(fp, "host", c->host);
|
||||
credential_write_item(fp, "path", c->path);
|
||||
credential_write_item(fp, "username", c->username);
|
||||
credential_write_item(fp, "password", c->password);
|
||||
}
|
||||
|
||||
static int run_credential_helper(struct credential *c,
|
||||
const char *cmd,
|
||||
int want_output)
|
||||
{
|
||||
struct child_process helper;
|
||||
const char *argv[] = { NULL, NULL };
|
||||
FILE *fp;
|
||||
|
||||
memset(&helper, 0, sizeof(helper));
|
||||
argv[0] = cmd;
|
||||
helper.argv = argv;
|
||||
helper.use_shell = 1;
|
||||
helper.in = -1;
|
||||
if (want_output)
|
||||
helper.out = -1;
|
||||
else
|
||||
helper.no_stdout = 1;
|
||||
|
||||
if (start_command(&helper) < 0)
|
||||
return -1;
|
||||
|
||||
fp = xfdopen(helper.in, "w");
|
||||
credential_write(c, fp);
|
||||
fclose(fp);
|
||||
|
||||
if (want_output) {
|
||||
int r;
|
||||
fp = xfdopen(helper.out, "r");
|
||||
r = credential_read(c, fp);
|
||||
fclose(fp);
|
||||
if (r < 0) {
|
||||
finish_command(&helper);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (finish_command(&helper))
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int credential_do(struct credential *c, const char *helper,
|
||||
const char *operation)
|
||||
{
|
||||
struct strbuf cmd = STRBUF_INIT;
|
||||
int r;
|
||||
|
||||
if (helper[0] == '!')
|
||||
strbuf_addstr(&cmd, helper + 1);
|
||||
else if (is_absolute_path(helper))
|
||||
strbuf_addstr(&cmd, helper);
|
||||
else
|
||||
strbuf_addf(&cmd, "git credential-%s", helper);
|
||||
|
||||
strbuf_addf(&cmd, " %s", operation);
|
||||
r = run_credential_helper(c, cmd.buf, !strcmp(operation, "get"));
|
||||
|
||||
strbuf_release(&cmd);
|
||||
return r;
|
||||
}
|
||||
|
||||
void credential_fill(struct credential *c)
|
||||
{
|
||||
int i;
|
||||
|
||||
if (c->username && c->password)
|
||||
return;
|
||||
|
||||
credential_apply_config(c);
|
||||
|
||||
for (i = 0; i < c->helpers.nr; i++) {
|
||||
credential_do(c, c->helpers.items[i].string, "get");
|
||||
if (c->username && c->password)
|
||||
return;
|
||||
}
|
||||
|
||||
credential_getpass(c);
|
||||
if (!c->username && !c->password)
|
||||
die("unable to get password from user");
|
||||
}
|
||||
|
||||
void credential_approve(struct credential *c)
|
||||
{
|
||||
int i;
|
||||
|
||||
if (c->approved)
|
||||
return;
|
||||
if (!c->username || !c->password)
|
||||
return;
|
||||
|
||||
credential_apply_config(c);
|
||||
|
||||
for (i = 0; i < c->helpers.nr; i++)
|
||||
credential_do(c, c->helpers.items[i].string, "store");
|
||||
c->approved = 1;
|
||||
}
|
||||
|
||||
void credential_reject(struct credential *c)
|
||||
{
|
||||
int i;
|
||||
|
||||
credential_apply_config(c);
|
||||
|
||||
for (i = 0; i < c->helpers.nr; i++)
|
||||
credential_do(c, c->helpers.items[i].string, "erase");
|
||||
|
||||
free(c->username);
|
||||
c->username = NULL;
|
||||
free(c->password);
|
||||
c->password = NULL;
|
||||
c->approved = 0;
|
||||
}
|
||||
|
||||
void credential_from_url(struct credential *c, const char *url)
|
||||
{
|
||||
const char *at, *colon, *cp, *slash, *host, *proto_end;
|
||||
|
||||
credential_clear(c);
|
||||
|
||||
/*
|
||||
* Match one of:
|
||||
* (1) proto://<host>/...
|
||||
* (2) proto://<user>@<host>/...
|
||||
* (3) proto://<user>:<pass>@<host>/...
|
||||
*/
|
||||
proto_end = strstr(url, "://");
|
||||
if (!proto_end)
|
||||
return;
|
||||
cp = proto_end + 3;
|
||||
at = strchr(cp, '@');
|
||||
colon = strchr(cp, ':');
|
||||
slash = strchrnul(cp, '/');
|
||||
|
||||
if (!at || slash <= at) {
|
||||
/* Case (1) */
|
||||
host = cp;
|
||||
}
|
||||
else if (!colon || at <= colon) {
|
||||
/* Case (2) */
|
||||
c->username = url_decode_mem(cp, at - cp);
|
||||
host = at + 1;
|
||||
} else {
|
||||
/* Case (3) */
|
||||
c->username = url_decode_mem(cp, colon - cp);
|
||||
c->password = url_decode_mem(colon + 1, at - (colon + 1));
|
||||
host = at + 1;
|
||||
}
|
||||
|
||||
if (proto_end - url > 0)
|
||||
c->protocol = xmemdupz(url, proto_end - url);
|
||||
if (slash - host > 0)
|
||||
c->host = url_decode_mem(host, slash - host);
|
||||
/* Trim leading and trailing slashes from path */
|
||||
while (*slash == '/')
|
||||
slash++;
|
||||
if (*slash) {
|
||||
char *p;
|
||||
c->path = url_decode(slash);
|
||||
p = c->path + strlen(c->path) - 1;
|
||||
while (p > c->path && *p == '/')
|
||||
*p-- = '\0';
|
||||
}
|
||||
}
|
33
credential.h
Normal file
33
credential.h
Normal file
@ -0,0 +1,33 @@
|
||||
#ifndef CREDENTIAL_H
|
||||
#define CREDENTIAL_H
|
||||
|
||||
#include "string-list.h"
|
||||
|
||||
struct credential {
|
||||
struct string_list helpers;
|
||||
unsigned approved:1,
|
||||
configured:1,
|
||||
use_http_path:1;
|
||||
|
||||
char *username;
|
||||
char *password;
|
||||
char *protocol;
|
||||
char *host;
|
||||
char *path;
|
||||
};
|
||||
|
||||
#define CREDENTIAL_INIT { STRING_LIST_INIT_DUP }
|
||||
|
||||
void credential_init(struct credential *);
|
||||
void credential_clear(struct credential *);
|
||||
|
||||
void credential_fill(struct credential *);
|
||||
void credential_approve(struct credential *);
|
||||
void credential_reject(struct credential *);
|
||||
|
||||
int credential_read(struct credential *, FILE *);
|
||||
void credential_from_url(struct credential *, const char *url);
|
||||
int credential_match(const struct credential *have,
|
||||
const struct credential *want);
|
||||
|
||||
#endif /* CREDENTIAL_H */
|
@ -135,6 +135,7 @@
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
#include <pwd.h>
|
||||
#include <sys/un.h>
|
||||
#ifndef NO_INTTYPES_H
|
||||
#include <inttypes.h>
|
||||
#else
|
||||
|
113
http.c
113
http.c
@ -3,6 +3,7 @@
|
||||
#include "sideband.h"
|
||||
#include "run-command.h"
|
||||
#include "url.h"
|
||||
#include "credential.h"
|
||||
|
||||
int active_requests;
|
||||
int http_is_verbose;
|
||||
@ -41,7 +42,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, *description;
|
||||
static struct credential http_auth = CREDENTIAL_INIT;
|
||||
static const char *user_agent;
|
||||
|
||||
#if LIBCURL_VERSION_NUM >= 0x071700
|
||||
@ -52,7 +53,7 @@ static const char *user_agent;
|
||||
#define CURLOPT_KEYPASSWD CURLOPT_SSLCERTPASSWD
|
||||
#endif
|
||||
|
||||
static char *ssl_cert_password;
|
||||
static struct credential cert_auth = CREDENTIAL_INIT;
|
||||
static int ssl_cert_password_required;
|
||||
|
||||
static struct curl_slist *pragma_header;
|
||||
@ -136,27 +137,6 @@ static void process_curl_messages(void)
|
||||
}
|
||||
#endif
|
||||
|
||||
static char *git_getpass_with_description(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);
|
||||
/*
|
||||
* NEEDSWORK: 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);
|
||||
}
|
||||
|
||||
static int http_options(const char *var, const char *value, void *cb)
|
||||
{
|
||||
if (!strcmp("http.sslverify", var)) {
|
||||
@ -229,11 +209,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_with_description("Password", description));
|
||||
strbuf_addf(&up, "%s:%s", user_name, user_pass);
|
||||
credential_fill(&http_auth);
|
||||
strbuf_addf(&up, "%s:%s",
|
||||
http_auth.username, http_auth.password);
|
||||
curl_easy_setopt(result, CURLOPT_USERPWD,
|
||||
strbuf_detach(&up, NULL));
|
||||
}
|
||||
@ -241,18 +221,14 @@ 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_with_description("Certificate Password", description);
|
||||
if (ssl_cert_password != NULL) {
|
||||
ssl_cert_password = xstrdup(ssl_cert_password);
|
||||
return 1;
|
||||
} else
|
||||
return 0;
|
||||
if (!cert_auth.password) {
|
||||
cert_auth.protocol = xstrdup("cert");
|
||||
cert_auth.path = xstrdup(ssl_cert);
|
||||
credential_fill(&cert_auth);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static CURL *get_curl_handle(void)
|
||||
@ -279,7 +255,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);
|
||||
@ -321,42 +297,6 @@ static CURL *get_curl_handle(void)
|
||||
return result;
|
||||
}
|
||||
|
||||
static void http_auth_init(const char *url)
|
||||
{
|
||||
const char *at, *colon, *cp, *slash, *host;
|
||||
|
||||
cp = strstr(url, "://");
|
||||
if (!cp)
|
||||
return;
|
||||
|
||||
/*
|
||||
* Ok, the URL looks like "proto://something". Which one?
|
||||
* "proto://<user>:<pass>@<host>/...",
|
||||
* "proto://<user>@<host>/...", or just
|
||||
* "proto://<host>/..."?
|
||||
*/
|
||||
cp += 3;
|
||||
at = strchr(cp, '@');
|
||||
colon = strchr(cp, ':');
|
||||
slash = strchrnul(cp, '/');
|
||||
if (!at || slash <= at) {
|
||||
/* No credentials, but we may have to ask for some later */
|
||||
host = cp;
|
||||
}
|
||||
else if (!colon || at <= colon) {
|
||||
/* Only username */
|
||||
user_name = url_decode_mem(cp, at - cp);
|
||||
user_pass = NULL;
|
||||
host = at + 1;
|
||||
} else {
|
||||
user_name = url_decode_mem(cp, colon - cp);
|
||||
user_pass = url_decode_mem(colon + 1, at - (colon + 1));
|
||||
host = at + 1;
|
||||
}
|
||||
|
||||
description = url_decode_mem(host, slash - host);
|
||||
}
|
||||
|
||||
static void set_from_env(const char **var, const char *envname)
|
||||
{
|
||||
const char *val = getenv(envname);
|
||||
@ -429,7 +369,7 @@ void http_init(struct remote *remote, const char *url)
|
||||
curl_ftp_no_epsv = 1;
|
||||
|
||||
if (url) {
|
||||
http_auth_init(url);
|
||||
credential_from_url(&http_auth, url);
|
||||
if (!ssl_cert_password_required &&
|
||||
getenv("GIT_SSL_CERT_PASSWORD_PROTECTED") &&
|
||||
!prefixcmp(url, "https://"))
|
||||
@ -478,10 +418,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 != NULL) {
|
||||
memset(cert_auth.password, 0, strlen(cert_auth.password));
|
||||
free(cert_auth.password);
|
||||
cert_auth.password = NULL;
|
||||
}
|
||||
ssl_cert_password_required = 0;
|
||||
}
|
||||
@ -837,17 +777,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 && user_pass) {
|
||||
if (http_auth.username && http_auth.password) {
|
||||
credential_reject(&http_auth);
|
||||
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.
|
||||
*/
|
||||
if (!user_name)
|
||||
user_name = xstrdup(git_getpass_with_description("Username", description));
|
||||
credential_fill(&http_auth);
|
||||
init_curl_http_auth(slot->curl);
|
||||
ret = HTTP_REAUTH;
|
||||
}
|
||||
@ -866,6 +800,9 @@ static int http_request(const char *url, void *result, int target, int options)
|
||||
curl_slist_free_all(headers);
|
||||
strbuf_release(&buf);
|
||||
|
||||
if (ret == HTTP_OK)
|
||||
credential_approve(&http_auth);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
37
strbuf.c
37
strbuf.c
@ -411,3 +411,40 @@ void strbuf_add_lines(struct strbuf *out, const char *prefix,
|
||||
}
|
||||
strbuf_complete_line(out);
|
||||
}
|
||||
|
||||
static int is_rfc3986_reserved(char ch)
|
||||
{
|
||||
switch (ch) {
|
||||
case '!': case '*': case '\'': case '(': case ')': case ';':
|
||||
case ':': case '@': case '&': case '=': case '+': case '$':
|
||||
case ',': case '/': case '?': case '#': case '[': case ']':
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int is_rfc3986_unreserved(char ch)
|
||||
{
|
||||
return isalnum(ch) ||
|
||||
ch == '-' || ch == '_' || ch == '.' || ch == '~';
|
||||
}
|
||||
|
||||
void strbuf_add_urlencode(struct strbuf *sb, const char *s, size_t len,
|
||||
int reserved)
|
||||
{
|
||||
strbuf_grow(sb, len);
|
||||
while (len--) {
|
||||
char ch = *s++;
|
||||
if (is_rfc3986_unreserved(ch) ||
|
||||
(!reserved && is_rfc3986_reserved(ch)))
|
||||
strbuf_addch(sb, ch);
|
||||
else
|
||||
strbuf_addf(sb, "%%%02x", ch);
|
||||
}
|
||||
}
|
||||
|
||||
void strbuf_addstr_urlencode(struct strbuf *sb, const char *s,
|
||||
int reserved)
|
||||
{
|
||||
strbuf_add_urlencode(sb, s, strlen(s), reserved);
|
||||
}
|
||||
|
5
strbuf.h
5
strbuf.h
@ -123,4 +123,9 @@ extern int launch_editor(const char *path, struct strbuf *buffer, const char *co
|
||||
extern int strbuf_branchname(struct strbuf *sb, const char *name);
|
||||
extern int strbuf_check_branch_ref(struct strbuf *sb, const char *name);
|
||||
|
||||
extern void strbuf_add_urlencode(struct strbuf *, const char *, size_t,
|
||||
int reserved);
|
||||
extern void strbuf_addstr_urlencode(struct strbuf *, const char *,
|
||||
int reserved);
|
||||
|
||||
#endif /* STRBUF_H */
|
||||
|
254
t/lib-credential.sh
Executable file
254
t/lib-credential.sh
Executable file
@ -0,0 +1,254 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Try a set of credential helpers; the expected stdin,
|
||||
# stdout and stderr should be provided on stdin,
|
||||
# separated by "--".
|
||||
check() {
|
||||
read_chunk >stdin &&
|
||||
read_chunk >expect-stdout &&
|
||||
read_chunk >expect-stderr &&
|
||||
test-credential "$@" <stdin >stdout 2>stderr &&
|
||||
test_cmp expect-stdout stdout &&
|
||||
test_cmp expect-stderr stderr
|
||||
}
|
||||
|
||||
read_chunk() {
|
||||
while read line; do
|
||||
case "$line" in
|
||||
--) break ;;
|
||||
*) echo "$line" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# Clear any residual data from previous tests. We only
|
||||
# need this when testing third-party helpers which read and
|
||||
# write outside of our trash-directory sandbox.
|
||||
#
|
||||
# Don't bother checking for success here, as it is
|
||||
# outside the scope of tests and represents a best effort to
|
||||
# clean up after ourselves.
|
||||
helper_test_clean() {
|
||||
reject $1 https example.com store-user
|
||||
reject $1 https example.com user1
|
||||
reject $1 https example.com user2
|
||||
reject $1 http path.tld user
|
||||
reject $1 https timeout.tld user
|
||||
}
|
||||
|
||||
reject() {
|
||||
(
|
||||
echo protocol=$2
|
||||
echo host=$3
|
||||
echo username=$4
|
||||
) | test-credential reject $1
|
||||
}
|
||||
|
||||
helper_test() {
|
||||
HELPER=$1
|
||||
|
||||
test_expect_success "helper ($HELPER) has no existing data" '
|
||||
check fill $HELPER <<-\EOF
|
||||
protocol=https
|
||||
host=example.com
|
||||
--
|
||||
username=askpass-username
|
||||
password=askpass-password
|
||||
--
|
||||
askpass: Username for '\''https://example.com'\'':
|
||||
askpass: Password for '\''https://askpass-username@example.com'\'':
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success "helper ($HELPER) stores password" '
|
||||
check approve $HELPER <<-\EOF
|
||||
protocol=https
|
||||
host=example.com
|
||||
username=store-user
|
||||
password=store-pass
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success "helper ($HELPER) can retrieve password" '
|
||||
check fill $HELPER <<-\EOF
|
||||
protocol=https
|
||||
host=example.com
|
||||
--
|
||||
username=store-user
|
||||
password=store-pass
|
||||
--
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success "helper ($HELPER) requires matching protocol" '
|
||||
check fill $HELPER <<-\EOF
|
||||
protocol=http
|
||||
host=example.com
|
||||
--
|
||||
username=askpass-username
|
||||
password=askpass-password
|
||||
--
|
||||
askpass: Username for '\''http://example.com'\'':
|
||||
askpass: Password for '\''http://askpass-username@example.com'\'':
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success "helper ($HELPER) requires matching host" '
|
||||
check fill $HELPER <<-\EOF
|
||||
protocol=https
|
||||
host=other.tld
|
||||
--
|
||||
username=askpass-username
|
||||
password=askpass-password
|
||||
--
|
||||
askpass: Username for '\''https://other.tld'\'':
|
||||
askpass: Password for '\''https://askpass-username@other.tld'\'':
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success "helper ($HELPER) requires matching username" '
|
||||
check fill $HELPER <<-\EOF
|
||||
protocol=https
|
||||
host=example.com
|
||||
username=other
|
||||
--
|
||||
username=other
|
||||
password=askpass-password
|
||||
--
|
||||
askpass: Password for '\''https://other@example.com'\'':
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success "helper ($HELPER) requires matching path" '
|
||||
test_config credential.usehttppath true &&
|
||||
check approve $HELPER <<-\EOF &&
|
||||
protocol=http
|
||||
host=path.tld
|
||||
path=foo.git
|
||||
username=user
|
||||
password=pass
|
||||
EOF
|
||||
check fill $HELPER <<-\EOF
|
||||
protocol=http
|
||||
host=path.tld
|
||||
path=bar.git
|
||||
--
|
||||
username=askpass-username
|
||||
password=askpass-password
|
||||
--
|
||||
askpass: Username for '\''http://path.tld/bar.git'\'':
|
||||
askpass: Password for '\''http://askpass-username@path.tld/bar.git'\'':
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success "helper ($HELPER) can forget host" '
|
||||
check reject $HELPER <<-\EOF &&
|
||||
protocol=https
|
||||
host=example.com
|
||||
EOF
|
||||
check fill $HELPER <<-\EOF
|
||||
protocol=https
|
||||
host=example.com
|
||||
--
|
||||
username=askpass-username
|
||||
password=askpass-password
|
||||
--
|
||||
askpass: Username for '\''https://example.com'\'':
|
||||
askpass: Password for '\''https://askpass-username@example.com'\'':
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success "helper ($HELPER) can store multiple users" '
|
||||
check approve $HELPER <<-\EOF &&
|
||||
protocol=https
|
||||
host=example.com
|
||||
username=user1
|
||||
password=pass1
|
||||
EOF
|
||||
check approve $HELPER <<-\EOF &&
|
||||
protocol=https
|
||||
host=example.com
|
||||
username=user2
|
||||
password=pass2
|
||||
EOF
|
||||
check fill $HELPER <<-\EOF &&
|
||||
protocol=https
|
||||
host=example.com
|
||||
username=user1
|
||||
--
|
||||
username=user1
|
||||
password=pass1
|
||||
EOF
|
||||
check fill $HELPER <<-\EOF
|
||||
protocol=https
|
||||
host=example.com
|
||||
username=user2
|
||||
--
|
||||
username=user2
|
||||
password=pass2
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success "helper ($HELPER) can forget user" '
|
||||
check reject $HELPER <<-\EOF &&
|
||||
protocol=https
|
||||
host=example.com
|
||||
username=user1
|
||||
EOF
|
||||
check fill $HELPER <<-\EOF
|
||||
protocol=https
|
||||
host=example.com
|
||||
username=user1
|
||||
--
|
||||
username=user1
|
||||
password=askpass-password
|
||||
--
|
||||
askpass: Password for '\''https://user1@example.com'\'':
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success "helper ($HELPER) remembers other user" '
|
||||
check fill $HELPER <<-\EOF
|
||||
protocol=https
|
||||
host=example.com
|
||||
username=user2
|
||||
--
|
||||
username=user2
|
||||
password=pass2
|
||||
EOF
|
||||
'
|
||||
}
|
||||
|
||||
helper_test_timeout() {
|
||||
HELPER="$*"
|
||||
|
||||
test_expect_success "helper ($HELPER) times out" '
|
||||
check approve "$HELPER" <<-\EOF &&
|
||||
protocol=https
|
||||
host=timeout.tld
|
||||
username=user
|
||||
password=pass
|
||||
EOF
|
||||
sleep 2 &&
|
||||
check fill "$HELPER" <<-\EOF
|
||||
protocol=https
|
||||
host=timeout.tld
|
||||
--
|
||||
username=askpass-username
|
||||
password=askpass-password
|
||||
--
|
||||
askpass: Username for '\''https://timeout.tld'\'':
|
||||
askpass: Password for '\''https://askpass-username@timeout.tld'\'':
|
||||
EOF
|
||||
'
|
||||
}
|
||||
|
||||
cat >askpass <<\EOF
|
||||
#!/bin/sh
|
||||
echo >&2 askpass: $*
|
||||
what=`echo $1 | cut -d" " -f1 | tr A-Z a-z | tr -cd a-z`
|
||||
echo "askpass-$what"
|
||||
EOF
|
||||
chmod +x askpass
|
||||
GIT_ASKPASS="$PWD/askpass"
|
||||
export GIT_ASKPASS
|
279
t/t0300-credentials.sh
Executable file
279
t/t0300-credentials.sh
Executable file
@ -0,0 +1,279 @@
|
||||
#!/bin/sh
|
||||
|
||||
test_description='basic credential helper tests'
|
||||
. ./test-lib.sh
|
||||
. "$TEST_DIRECTORY"/lib-credential.sh
|
||||
|
||||
test_expect_success 'setup helper scripts' '
|
||||
cat >dump <<-\EOF &&
|
||||
whoami=`echo $0 | sed s/.*git-credential-//`
|
||||
echo >&2 "$whoami: $*"
|
||||
while IFS== read key value; do
|
||||
echo >&2 "$whoami: $key=$value"
|
||||
eval "$key=$value"
|
||||
done
|
||||
EOF
|
||||
|
||||
cat >git-credential-useless <<-\EOF &&
|
||||
#!/bin/sh
|
||||
. ./dump
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x git-credential-useless &&
|
||||
|
||||
cat >git-credential-verbatim <<-\EOF &&
|
||||
#!/bin/sh
|
||||
user=$1; shift
|
||||
pass=$1; shift
|
||||
. ./dump
|
||||
test -z "$user" || echo username=$user
|
||||
test -z "$pass" || echo password=$pass
|
||||
EOF
|
||||
chmod +x git-credential-verbatim &&
|
||||
|
||||
PATH="$PWD:$PATH"
|
||||
'
|
||||
|
||||
test_expect_success 'credential_fill invokes helper' '
|
||||
check fill "verbatim foo bar" <<-\EOF
|
||||
--
|
||||
username=foo
|
||||
password=bar
|
||||
--
|
||||
verbatim: get
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'credential_fill invokes multiple helpers' '
|
||||
check fill useless "verbatim foo bar" <<-\EOF
|
||||
--
|
||||
username=foo
|
||||
password=bar
|
||||
--
|
||||
useless: get
|
||||
verbatim: get
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'credential_fill stops when we get a full response' '
|
||||
check fill "verbatim one two" "verbatim three four" <<-\EOF
|
||||
--
|
||||
username=one
|
||||
password=two
|
||||
--
|
||||
verbatim: get
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'credential_fill continues through partial response' '
|
||||
check fill "verbatim one \"\"" "verbatim two three" <<-\EOF
|
||||
--
|
||||
username=two
|
||||
password=three
|
||||
--
|
||||
verbatim: get
|
||||
verbatim: get
|
||||
verbatim: username=one
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'credential_fill passes along metadata' '
|
||||
check fill "verbatim one two" <<-\EOF
|
||||
protocol=ftp
|
||||
host=example.com
|
||||
path=foo.git
|
||||
--
|
||||
username=one
|
||||
password=two
|
||||
--
|
||||
verbatim: get
|
||||
verbatim: protocol=ftp
|
||||
verbatim: host=example.com
|
||||
verbatim: path=foo.git
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'credential_approve calls all helpers' '
|
||||
check approve useless "verbatim one two" <<-\EOF
|
||||
username=foo
|
||||
password=bar
|
||||
--
|
||||
--
|
||||
useless: store
|
||||
useless: username=foo
|
||||
useless: password=bar
|
||||
verbatim: store
|
||||
verbatim: username=foo
|
||||
verbatim: password=bar
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'do not bother storing password-less credential' '
|
||||
check approve useless <<-\EOF
|
||||
username=foo
|
||||
--
|
||||
--
|
||||
EOF
|
||||
'
|
||||
|
||||
|
||||
test_expect_success 'credential_reject calls all helpers' '
|
||||
check reject useless "verbatim one two" <<-\EOF
|
||||
username=foo
|
||||
password=bar
|
||||
--
|
||||
--
|
||||
useless: erase
|
||||
useless: username=foo
|
||||
useless: password=bar
|
||||
verbatim: erase
|
||||
verbatim: username=foo
|
||||
verbatim: password=bar
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'usernames can be preserved' '
|
||||
check fill "verbatim \"\" three" <<-\EOF
|
||||
username=one
|
||||
--
|
||||
username=one
|
||||
password=three
|
||||
--
|
||||
verbatim: get
|
||||
verbatim: username=one
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'usernames can be overridden' '
|
||||
check fill "verbatim two three" <<-\EOF
|
||||
username=one
|
||||
--
|
||||
username=two
|
||||
password=three
|
||||
--
|
||||
verbatim: get
|
||||
verbatim: username=one
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'do not bother completing already-full credential' '
|
||||
check fill "verbatim three four" <<-\EOF
|
||||
username=one
|
||||
password=two
|
||||
--
|
||||
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 helper list falls back to internal getpass' '
|
||||
check fill <<-\EOF
|
||||
--
|
||||
username=askpass-username
|
||||
password=askpass-password
|
||||
--
|
||||
askpass: Username:
|
||||
askpass: Password:
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'internal getpass does not ask for known username' '
|
||||
check fill <<-\EOF
|
||||
username=foo
|
||||
--
|
||||
username=foo
|
||||
password=askpass-password
|
||||
--
|
||||
askpass: Password:
|
||||
EOF
|
||||
'
|
||||
|
||||
HELPER="!f() {
|
||||
cat >/dev/null
|
||||
echo username=foo
|
||||
echo password=bar
|
||||
}; f"
|
||||
test_expect_success 'respect configured credentials' '
|
||||
test_config credential.helper "$HELPER" &&
|
||||
check fill <<-\EOF
|
||||
--
|
||||
username=foo
|
||||
password=bar
|
||||
--
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'match configured credential' '
|
||||
test_config credential.https://example.com.helper "$HELPER" &&
|
||||
check fill <<-\EOF
|
||||
protocol=https
|
||||
host=example.com
|
||||
path=repo.git
|
||||
--
|
||||
username=foo
|
||||
password=bar
|
||||
--
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'do not match configured credential' '
|
||||
test_config credential.https://foo.helper "$HELPER" &&
|
||||
check fill <<-\EOF
|
||||
protocol=https
|
||||
host=bar
|
||||
--
|
||||
username=askpass-username
|
||||
password=askpass-password
|
||||
--
|
||||
askpass: Username for '\''https://bar'\'':
|
||||
askpass: Password for '\''https://askpass-username@bar'\'':
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'pull username from config' '
|
||||
test_config credential.https://example.com.username foo &&
|
||||
check fill <<-\EOF
|
||||
protocol=https
|
||||
host=example.com
|
||||
--
|
||||
username=foo
|
||||
password=askpass-password
|
||||
--
|
||||
askpass: Password for '\''https://foo@example.com'\'':
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'http paths can be part of context' '
|
||||
check fill "verbatim foo bar" <<-\EOF &&
|
||||
protocol=https
|
||||
host=example.com
|
||||
path=foo.git
|
||||
--
|
||||
username=foo
|
||||
password=bar
|
||||
--
|
||||
verbatim: get
|
||||
verbatim: protocol=https
|
||||
verbatim: host=example.com
|
||||
EOF
|
||||
test_config credential.https://example.com.useHttpPath true &&
|
||||
check fill "verbatim foo bar" <<-\EOF
|
||||
protocol=https
|
||||
host=example.com
|
||||
path=foo.git
|
||||
--
|
||||
username=foo
|
||||
password=bar
|
||||
--
|
||||
verbatim: get
|
||||
verbatim: protocol=https
|
||||
verbatim: host=example.com
|
||||
verbatim: path=foo.git
|
||||
EOF
|
||||
'
|
||||
|
||||
test_done
|
23
t/t0301-credential-cache.sh
Executable file
23
t/t0301-credential-cache.sh
Executable file
@ -0,0 +1,23 @@
|
||||
#!/bin/sh
|
||||
|
||||
test_description='credential-cache tests'
|
||||
. ./test-lib.sh
|
||||
. "$TEST_DIRECTORY"/lib-credential.sh
|
||||
|
||||
test -z "$NO_UNIX_SOCKETS" || {
|
||||
skip_all='skipping credential-cache tests, unix sockets not available'
|
||||
test_done
|
||||
}
|
||||
|
||||
# don't leave a stale daemon running
|
||||
trap 'code=$?; git credential-cache exit; (exit $code); die' EXIT
|
||||
|
||||
helper_test cache
|
||||
helper_test_timeout cache --timeout=1
|
||||
|
||||
# we can't rely on our "trap" above working after test_done,
|
||||
# as test_done will delete the trash directory containing
|
||||
# our socket, leaving us with no way to access the daemon.
|
||||
git credential-cache exit
|
||||
|
||||
test_done
|
9
t/t0302-credential-store.sh
Executable file
9
t/t0302-credential-store.sh
Executable file
@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
|
||||
test_description='credential-store tests'
|
||||
. ./test-lib.sh
|
||||
. "$TEST_DIRECTORY"/lib-credential.sh
|
||||
|
||||
helper_test store
|
||||
|
||||
test_done
|
39
t/t0303-credential-external.sh
Executable file
39
t/t0303-credential-external.sh
Executable file
@ -0,0 +1,39 @@
|
||||
#!/bin/sh
|
||||
|
||||
test_description='external credential helper tests'
|
||||
. ./test-lib.sh
|
||||
. "$TEST_DIRECTORY"/lib-credential.sh
|
||||
|
||||
pre_test() {
|
||||
test -z "$GIT_TEST_CREDENTIAL_HELPER_SETUP" ||
|
||||
eval "$GIT_TEST_CREDENTIAL_HELPER_SETUP"
|
||||
|
||||
# clean before the test in case there is cruft left
|
||||
# over from a previous run that would impact results
|
||||
helper_test_clean "$GIT_TEST_CREDENTIAL_HELPER"
|
||||
}
|
||||
|
||||
post_test() {
|
||||
# clean afterwards so that we are good citizens
|
||||
# and don't leave cruft in the helper's storage, which
|
||||
# might be long-term system storage
|
||||
helper_test_clean "$GIT_TEST_CREDENTIAL_HELPER"
|
||||
}
|
||||
|
||||
if test -z "$GIT_TEST_CREDENTIAL_HELPER"; then
|
||||
say "# skipping external helper tests (set GIT_TEST_CREDENTIAL_HELPER)"
|
||||
else
|
||||
pre_test
|
||||
helper_test "$GIT_TEST_CREDENTIAL_HELPER"
|
||||
post_test
|
||||
fi
|
||||
|
||||
if test -z "$GIT_TEST_CREDENTIAL_HELPER_TIMEOUT"; then
|
||||
say "# skipping external helper timeout tests"
|
||||
else
|
||||
pre_test
|
||||
helper_test_timeout "$GIT_TEST_CREDENTIAL_HELPER_TIMEOUT"
|
||||
post_test
|
||||
fi
|
||||
|
||||
test_done
|
@ -49,40 +49,84 @@ test_expect_success 'setup askpass helpers' '
|
||||
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
|
||||
export GIT_ASKPASS
|
||||
'
|
||||
|
||||
expect_askpass() {
|
||||
dest=$HTTPD_DEST
|
||||
{
|
||||
case "$1" in
|
||||
none)
|
||||
;;
|
||||
pass)
|
||||
echo "askpass: Password for 'http://$2@$dest': "
|
||||
;;
|
||||
both)
|
||||
echo "askpass: Username for 'http://$dest': "
|
||||
echo "askpass: Password for 'http://$2@$dest': "
|
||||
;;
|
||||
*)
|
||||
false
|
||||
;;
|
||||
esac
|
||||
} >askpass-expect &&
|
||||
test_cmp askpass-expect askpass-query
|
||||
}
|
||||
|
||||
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
|
||||
expect_askpass both wrong
|
||||
'
|
||||
|
||||
test_expect_success 'http auth can use user/pass in URL' '
|
||||
>askpass-query &&
|
||||
echo wrong >askpass-reponse &&
|
||||
echo wrong >askpass-response &&
|
||||
git clone "$HTTPD_URL_USER_PASS/auth/repo.git" clone-auth-none &&
|
||||
test_cmp askpass-expect-none askpass-query
|
||||
expect_askpass none
|
||||
'
|
||||
|
||||
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
|
||||
expect_askpass pass user@host
|
||||
'
|
||||
|
||||
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
|
||||
expect_askpass both user@host
|
||||
'
|
||||
|
||||
test_expect_success 'http auth respects credential helper config' '
|
||||
test_config_global credential.helper "!f() {
|
||||
cat >/dev/null
|
||||
echo username=user@host
|
||||
echo password=user@host
|
||||
}; f" &&
|
||||
>askpass-query &&
|
||||
echo wrong >askpass-response &&
|
||||
git clone "$HTTPD_URL/auth/repo.git" clone-auth-helper &&
|
||||
expect_askpass none
|
||||
'
|
||||
|
||||
test_expect_success 'http auth can get username from config' '
|
||||
test_config_global "credential.$HTTPD_URL.username" user@host &&
|
||||
>askpass-query &&
|
||||
echo user@host >askpass-response &&
|
||||
git clone "$HTTPD_URL/auth/repo.git" clone-auth-user &&
|
||||
expect_askpass pass user@host
|
||||
'
|
||||
|
||||
test_expect_success 'configured username does not override URL' '
|
||||
test_config_global "credential.$HTTPD_URL.username" wrong &&
|
||||
>askpass-query &&
|
||||
echo user@host >askpass-response &&
|
||||
git clone "$HTTPD_URL_USER/auth/repo.git" clone-auth-user2 &&
|
||||
expect_askpass pass user@host
|
||||
'
|
||||
|
||||
test_expect_success 'fetch changes via http' '
|
||||
|
@ -379,6 +379,11 @@ test_config () {
|
||||
git config "$@"
|
||||
}
|
||||
|
||||
test_config_global () {
|
||||
test_when_finished "test_unconfig --global '$1'" &&
|
||||
git config --global "$@"
|
||||
}
|
||||
|
||||
# Use test_set_prereq to tell that a particular prerequisite is available.
|
||||
# The prerequisite can later be checked for in two ways:
|
||||
#
|
||||
|
38
test-credential.c
Normal file
38
test-credential.c
Normal file
@ -0,0 +1,38 @@
|
||||
#include "cache.h"
|
||||
#include "credential.h"
|
||||
#include "string-list.h"
|
||||
|
||||
static const char usage_msg[] =
|
||||
"test-credential <fill|approve|reject> [helper...]";
|
||||
|
||||
int main(int argc, const char **argv)
|
||||
{
|
||||
const char *op;
|
||||
struct credential c = CREDENTIAL_INIT;
|
||||
int i;
|
||||
|
||||
op = argv[1];
|
||||
if (!op)
|
||||
usage(usage_msg);
|
||||
for (i = 2; i < argc; i++)
|
||||
string_list_append(&c.helpers, argv[i]);
|
||||
|
||||
if (credential_read(&c, stdin) < 0)
|
||||
die("unable to read credential from stdin");
|
||||
|
||||
if (!strcmp(op, "fill")) {
|
||||
credential_fill(&c);
|
||||
if (c.username)
|
||||
printf("username=%s\n", c.username);
|
||||
if (c.password)
|
||||
printf("password=%s\n", c.password);
|
||||
}
|
||||
else if (!strcmp(op, "approve"))
|
||||
credential_approve(&c);
|
||||
else if (!strcmp(op, "reject"))
|
||||
credential_reject(&c);
|
||||
else
|
||||
usage(usage_msg);
|
||||
|
||||
return 0;
|
||||
}
|
56
unix-socket.c
Normal file
56
unix-socket.c
Normal file
@ -0,0 +1,56 @@
|
||||
#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();
|
||||
|
||||
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 */
|
Loading…
Reference in New Issue
Block a user