threads / patch / 29017

patch, 13 partscredential helpers, take two

Subject: [PATCH 0/13] credential helpers, take two

## tl;dr

49 messages between Nov 24, 2011 and Nov 30, 2011. Diffs are folded; open one to read it.

replies: 48people: 6as markdown or json

Jeff King· Nov 24, 2011, 10:58 UTC · lore
Here's a revised version of the http-auth / credential-helper series.

It has the same basic premise as the first series (git contacts external programs to access secure storage, thus enabling secure password caching), but there are a number of tweaks in the implementation.

The main departures are:
  1. Helpers now _only_ act as storage; they never ask for credentials
     directly from the user.  This makes writing them much simpler.
     It also means that "askpass" remains the only way to get input from
     the user. However, I've left room in the helper protocol to add an
     "ask" action. If people really want something like a classy
     username/password dialog from the helpers, it will be easy to add.
  2. The helper protocol now happens completely over pipes. In the first
     round, we gave information to helpers on the command line. That was
     OK, since we never gave them a password; they only gave them to us
     (and cached them if they wanted to). But now that git is asking for
     the password itself, it has to send the password to the helper to
     store. And that definitely shouldn't go on the command line.
     The parsing load on the helper is therefore a little higher.
     However, it's still really quite easy.
  3. The old "unique" token has been broken into components. That means
     less parsing for most helpers, which wanted the broken-down fields.
     Helpers that want a single token can pretty easily reassemble.
  4. I dropped the "description" field. I noticed that all of the
     components of a credential context are actually parts of a URL. So
     we can just show the URL (or a subset thereof) to the user in the
     prompt. See the discussion in patches 05 and 08.
  5. Config handling happens at the right place (before helpers) now.

If you want an overview without reading the patches too carefully, I recommend reading the documentation added in patches 03 and 09, which contain the API and end-user documentation respectively.

Helper writers may want to look at t0303 added in patch 13; it's an adaptation of the test script I posted earlier for testing new external helpers.

  [01/13]: test-lib: add test_config_global variant
  [02/13]: t5550: fix typo
  [03/13]: introduce credentials API
  [04/13]: credential: add function for parsing url components
  [05/13]: http: use credential API to get passwords
  [06/13]: credential: apply helper config
  [07/13]: credential: add credential.*.username
  [08/13]: credential: make relevance of http path configurable
  [09/13]: docs: end-user documentation for the credential subsystem
  [10/13]: credentials: add "cache" helper
  [11/13]: strbuf: add strbuf_add*_urlencode
  [12/13]: credentials: add "store" helper
  [13/13]: t: add test harness for external credential helpers

I've been running with this for a few days, so I think the most horrible bugs are shaken out. But I had to rewrite almost all of the code from the first series, so I wouldn't be surprised if there are some new bugs lurking in there. If you are reviewing, please read from scratch and don't assume that something that worked in the first series is still working. :)

-Peff
Jeff King· Nov 24, 2011, 10:58 UTC · re: Jeff King · lore

[PATCH 01/13] test-lib: add test_config_global variant

The point of test_config is to simultaneously set a config variable and register its cleanup handler, like:

  test_config core.foo bar

However, it stupidly assumes that $1 contained the name of the variable, which means it won't work for:

  test_config --global core.foo bar

We could try to parse the command-line ourselves and figure out which parts need to be fed to test_unconfig. But since this is likely the most common variant, it's much simpler and less error-prone to simply add a new function.

Signed-off-by: Jeff King <peff@peff.net>
---
 t/test-lib.sh |    5 +++++
 1 files changed, 5 insertions(+), 0 deletions(-)
Show changes to t/test-lib.sh +5 −0
diff --git a/t/test-lib.sh b/t/test-lib.sh
index bdd9513..160479b 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -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:
 #
-- 
1.7.7.4.5.gb32a5
Jeff King· Nov 24, 2011, 10:59 UTC · re: Jeff King · lore

[PATCH 02/13] t5550: fix typo

This didn't have an impact, because it was just setting up an "expect" file that happened to be identical to the one in the test before it.

Signed-off-by: Jeff King <peff@peff.net>
---
 t/t5550-http-fetch.sh |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)
Show changes to t/t5550-http-fetch.sh +1 −1
diff --git a/t/t5550-http-fetch.sh b/t/t5550-http-fetch.sh
index 311a33c..3d6e871 100755
--- a/t/t5550-http-fetch.sh
+++ b/t/t5550-http-fetch.sh
@@ -66,7 +66,7 @@ test_expect_success 'cloning password-protected repository can fail' '
 
 test_expect_success 'http auth can use user/pass in URL' '
 	>askpass-query &&
-	echo wrong >askpass-reponse &&
+	echo wrong >askpass-response &&
 	git clone "$HTTPD_URL_USER_PASS/auth/repo.git" clone-auth-none &&
 	test_cmp askpass-expect-none askpass-query
 '
-- 
1.7.7.4.5.gb32a5
Jeff King· Nov 24, 2011, 11:01 UTC · re: Jeff King · lore

[PATCH 03/13] introduce credentials API

There are a few places in git that need to get a username and password credential from the user; the most notable one is HTTP authentication for smart-http pushing.

Right now the only choices for providing credentials are to put them plaintext into your ~/.netrc, or to have git prompt you (either on the terminal or via an askpass program). The former is not very secure, and the latter is not very convenient.

Unfortunately, there is no "always best" solution for password management. The details will depend on the tradeoff you want between security and convenience, as well as how git can integrate with other security systems (e.g., many operating systems provide a keychain or password wallet for single sign-on).

This patch provides an abstract notion of credentials as a data item, and provides three basic operations:

  - fill (i.e., acquire from external storage or from the
    user)
  - approve (mark a credential as "working" for further
    storage)
  - reject (mark a credential as "not working", so it can
    be removed from storage)

These operations can be backed by external helper processes that interact with system- or user-specific secure storage.

Signed-off-by: Jeff King <peff@peff.net>
---
 .gitignore                                  |    1 +
 Documentation/technical/api-credentials.txt |  148 ++++++++++++++++
 Makefile                                    |    3 +
 credential.c                                |  242 +++++++++++++++++++++++++++
 credential.h                                |   28 +++
 t/lib-credential.sh                         |   33 ++++
 t/t0300-credentials.sh                      |  195 +++++++++++++++++++++
 test-credential.c                           |   38 ++++
 8 files changed, 688 insertions(+), 0 deletions(-)
 create mode 100644 Documentation/technical/api-credentials.txt
 create mode 100644 credential.c
 create mode 100644 credential.h
 create mode 100755 t/lib-credential.sh
 create mode 100755 t/t0300-credentials.sh
 create mode 100644 test-credential.c
Show changes to 8 files +688 −0

.gitignore, Documentation/technical/api-credentials.txt, Makefile, credential.c, credential.h, t/lib-credential.sh, t/t0300-credentials.sh, test-credential.c

diff --git a/.gitignore b/.gitignore
index 8572c8c..7d2fefc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -167,6 +167,7 @@
 /gitweb/static/gitweb.js
 /gitweb/static/gitweb.min.*
 /test-chmtime
+/test-credential
 /test-ctype
 /test-date
 /test-delta
diff --git a/Documentation/technical/api-credentials.txt b/Documentation/technical/api-credentials.txt
new file mode 100644
index 0000000..3061077
--- /dev/null
+++ b/Documentation/technical/api-credentials.txt
@@ -0,0 +1,148 @@
+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
+storage 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`::
+
+	Attempt to fill the username and password fields of the passed
+	credential struct, first consulting storage helpers, then asking
+	the user. Guarantees that the username and password fields will
+	be filled afterwards (or die() will be called).
+
+`credential_reject`::
+
+	Inform the credential subsystem that the provided credentials
+	have been rejected. This will notify any storage helpers of the
+	rejection (which allows them to, for example, purge the invalid
+	credentials from storage), and then clear the username and
+	password fields in `struct credential`. It can then be
+	`credential_fill`-ed again.
+
+`credential_approve`::
+
+	Inform the credential subsystem that the provided credentials
+	were successfully used for authentication. This will notify any
+	storage helpers of the approval, so that they can store the
+	result to be used again.
+
+
+Credential Storage Helpers
+--------------------------
+
+Credential storage 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).
+
+Helper scripts should generally be found in the PATH, and have names of
+the form "git-credential-$HELPER". When the helper string "$HELPER" is
+passed to credential functions, they will run "git-credential-$HELPER"
+via the shell. If the first word of $HELPER contains non-alphanumeric
+characters, then $HELPER is executed as a shell command. This makes it
+possible to specify individual scripts by their full path (e.g.,
+`/path/to/helper`) or even shell snippets (`f() { do_whatever; }; f`).
+
+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 `=` or
+newline. The value may contain any bytes except a newline.  In both
+cases, all bytes are treated as-is (i.e., there is no quoting, and one
+cannot transmit a value with newline 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).
diff --git a/Makefile b/Makefile
index b1c80a6..5ca363b 100644
--- a/Makefile
+++ b/Makefile
@@ -431,6 +431,7 @@ PROGRAM_OBJS += sh-i18n--envsubst.o
 PROGRAMS += $(patsubst %.o,git-%$X,$(PROGRAM_OBJS))
 
 TEST_PROGRAMS_NEED_X += test-chmtime
+TEST_PROGRAMS_NEED_X += test-credential
 TEST_PROGRAMS_NEED_X += test-ctype
 TEST_PROGRAMS_NEED_X += test-date
 TEST_PROGRAMS_NEED_X += test-delta
@@ -525,6 +526,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
@@ -609,6 +611,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
diff --git a/credential.c b/credential.c
new file mode 100644
index 0000000..d50a611
--- /dev/null
+++ b/credential.c
@@ -0,0 +1,242 @@
+#include "cache.h"
+#include "credential.h"
+#include "string-list.h"
+#include "run-command.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);
+}
+
+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 first_word_is_alnum(const char *s)
+{
+	for (; *s && *s != ' '; s++)
+		if (!isalnum(*s))
+			return 0;
+	return 1;
+}
+
+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 *method,
+			 const char *operation)
+{
+	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);
+	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;
+
+	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;
+
+	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;
+
+	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;
+}
diff --git a/credential.h b/credential.h
new file mode 100644
index 0000000..2ea7d49
--- /dev/null
+++ b/credential.h
@@ -0,0 +1,28 @@
+#ifndef CREDENTIAL_H
+#define CREDENTIAL_H
+
+#include "string-list.h"
+
+struct credential {
+	struct string_list helpers;
+	unsigned approved: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 *);
+
+#endif /* CREDENTIAL_H */
diff --git a/t/lib-credential.sh b/t/lib-credential.sh
new file mode 100755
index 0000000..54ae1f4
--- /dev/null
+++ b/t/lib-credential.sh
@@ -0,0 +1,33 @@
+#!/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
+}
+
+
+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
diff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh
new file mode 100755
index 0000000..81a455f
--- /dev/null
+++ b/t/t0300-credentials.sh
@@ -0,0 +1,195 @@
+#!/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
+'
+
+test_done
diff --git a/test-credential.c b/test-credential.c
new file mode 100644
index 0000000..dee200e
--- /dev/null
+++ b/test-credential.c
@@ -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;
+}
-- 
1.7.7.4.5.gb32a5
Junio C Hamano· Nov 28, 2011, 21:46 UTC · re: Jeff King · lore

Re: [PATCH 03/13] introduce credentials API

Jeff King <peff@peff.net> writes:
Show 29 quoted lines
> diff --git a/Documentation/technical/api-credentials.txt b/Documentation/technical/api-credentials.txt
> new file mode 100644
> index 0000000..3061077
> --- /dev/null
> +++ b/Documentation/technical/api-credentials.txt
> @@ -0,0 +1,148 @@
> + ...
> +`credential_fill`::
> +
> +	Attempt to fill the username and password fields of the passed
> +	credential struct, first consulting storage helpers, then asking
> +	the user. Guarantees that the username and password fields will
> +	be filled afterwards (or die() will be called).
> +
> +`credential_reject`::
> +
> +	Inform the credential subsystem that the provided credentials
> +	have been rejected. This will notify any storage helpers of the
> +	rejection (which allows them to, for example, purge the invalid
> +	credentials from storage), and then clear the username and
> +	password fields in `struct credential`. It can then be
> +	`credential_fill`-ed again.
> +
> +`credential_approve`::
> +
> +	Inform the credential subsystem that the provided credentials
> +	were successfully used for authentication. This will notify any
> +	storage helpers of the approval, so that they can store the
> +	result to be used again.

It's a bit hard to read and understand which part of the system calls these and which other part of the system is responsible for implementing them, and how "helper" fits into the picture (perhaps calling some of these interfaces will result in "helper" getting called?).

Show 10 quoted lines
> +Credential Storage Helpers
> +--------------------------
> +
> +Credential storage 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).
> +
> +Helper scripts should generally be found in the PATH, and have names of
> +the form "git-credential-$HELPER".
Is this normal PATH or can a helper be moved away into $GIT_EXEC_PATH?

I briefly wondered if they want to be git-credential--$HELPER; I do not deeply care either way, though.

Show 6 quoted lines
> When the helper string "$HELPER" is
> +passed to credential functions, they will run "git-credential-$HELPER"
> +via the shell. If the first word of $HELPER contains non-alphanumeric
> +characters, then $HELPER is executed as a shell command. This makes it
> +possible to specify individual scripts by their full path (e.g.,
> +`/path/to/helper`) or even shell snippets (`f() { do_whatever; }; f`).

The definition of "the first word" above is not specified but it seems to be "space separated". In other words, 'f() { do_whatever; }; f' would be OK but 'f () { do_whatever; }; f' would not be. Am I reading and guessing your intention correctly?

Funnily enough, 'f<TAB>() { do_whatever; }; f' would qualify as the first word having a non alphanumeric.

Show 8 quoted lines
> +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 `=` or
> +newline. The value may contain any bytes except a newline.  In both
> +cases, all bytes are treated as-is (i.e., there is no quoting, and one
> +cannot transmit a value with newline in it).

Can k or v contain a NUL? The literal reading of the above implies they could, but I do not think you meant to.

Show 10 quoted lines
> +int credential_read(struct credential *c, FILE *fp)
> +{
> ...
> +			c->host = xstrdup(value);
> +		}
> +		else if (!strcmp(key, "path")) {
> ...
> +		/* 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 */
Two style nits.
Jeff King· Nov 29, 2011, 05:04 UTC · re: Junio C Hamano · lore

Re: [PATCH 03/13] introduce credentials API

On Mon, Nov 28, 2011 at 01:46:35PM -0800, Junio C Hamano wrote:
Show 34 quoted lines
> > diff --git a/Documentation/technical/api-credentials.txt b/Documentation/technical/api-credentials.txt
> > new file mode 100644
> > index 0000000..3061077
> > --- /dev/null
> > +++ b/Documentation/technical/api-credentials.txt
> > @@ -0,0 +1,148 @@
> > + ...
> > +`credential_fill`::
> > +
> > +	Attempt to fill the username and password fields of the passed
> > +	credential struct, first consulting storage helpers, then asking
> > +	the user. Guarantees that the username and password fields will
> > +	be filled afterwards (or die() will be called).
> > +
> > +`credential_reject`::
> > +
> > +	Inform the credential subsystem that the provided credentials
> > +	have been rejected. This will notify any storage helpers of the
> > +	rejection (which allows them to, for example, purge the invalid
> > +	credentials from storage), and then clear the username and
> > +	password fields in `struct credential`. It can then be
> > +	`credential_fill`-ed again.
> > +
> > +`credential_approve`::
> > +
> > +	Inform the credential subsystem that the provided credentials
> > +	were successfully used for authentication. This will notify any
> > +	storage helpers of the approval, so that they can store the
> > +	result to be used again.
> 
> It's a bit hard to read and understand which part of the system calls
> these and which other part of the system is responsible for implementing
> them, and how "helper" fits into the picture (perhaps calling some of
> these interfaces will result in "helper" getting called?).

How about following it with a rough example of how they would be used, like:

  /*
   * Create a credential with some context; we don't yet know the
   * username or password.
   */
  struct credential c = CREDENTIAL_INIT;
  c.protocol = xstrdup("https");
  c.host = xstrdup("example.com");
  c.path = xstrdup("foo.git");
  /*
   * 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);
  /*
   * And then finally make our request, reporting back to the credential
   * system on whether it succeeded or failed.
   */
  if (make_http_request(c.host, c.path, c.username, c.password) < 0)
          credential_reject(&c);
  else
          credential_accept(&c);
Does that make it more clear?
Show 12 quoted lines
> > +Credential Storage Helpers
> > +--------------------------
> > +
> > +Credential storage 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).
> > +
> > +Helper scripts should generally be found in the PATH, and have names of
> > +the form "git-credential-$HELPER".
> 
> Is this normal PATH or can a helper be moved away into $GIT_EXEC_PATH?

They are executed as normal git commands, so $GIT_EXEC_PATH. Actually, they could even be aliases or builtins, for that matter. I'll update the documentation to reflect that.

> I briefly wondered if they want to be git-credential--$HELPER; I do not
> deeply care either way, though.

Yeah, I thought about that. In some ways they are implementation details that you don't care about, and that makes the "--" separator make sense. But some of them may want to expose other actions to the user. For example, git-credential-cache has an "exit" action that you can call to ask the cache daemon to exit prematurely, forgetting all credentials. Git will never use it; it's purely an end-user interface.

I think it might also make sense to ask helpers to provide a "clear" operation which will delete any stored items. Git wouldn't use it, but it would be helpful for testing.

Show 11 quoted lines
> > When the helper string "$HELPER" is
> > +passed to credential functions, they will run "git-credential-$HELPER"
> > +via the shell. If the first word of $HELPER contains non-alphanumeric
> > +characters, then $HELPER is executed as a shell command. This makes it
> > +possible to specify individual scripts by their full path (e.g.,
> > +`/path/to/helper`) or even shell snippets (`f() { do_whatever; }; f`).
> 
> The definition of "the first word" above is not specified but it seems to
> be "space separated". In other words, 'f() { do_whatever; }; f' would be
> OK but 'f () { do_whatever; }; f' would not be. Am I reading and guessing
> your intention correctly?

Yes, that is my intent, though I am not altogether happy with it, as there are too many confusing corner cases (e.g., your "f ()" example above). My thinking was guided by these requirements:

  1. Users who are using one of the included helpers or a drop-in
     third-party helper should be able to just do:
        git config credential.helper cache
     which is extremely readable and intuitive. Thus, I want to prepend
     "git credential-" at least for simple cases.
  2. You should be able to add arguments to a helper, if it takes them.
     For example, with the current code you can do:
       git config credential.helper 'cache --timeout=300'
     or
       git config credential.helper 'store --store=/some/path'
     and that's all interpreted by the shell, and the helpers receive
     your configuration (the "--store" name is a holdover from version 1
     of the series, which had many more options. Probably "--path" or
     "--file" would be better name).
     So that means that the rule for when to prepend "git credential-"
     is not simply "when the helper is a single token". It can be
     arbitrary arguments, and should be interpreted by the shell (to
     handle things like quoting).
  3. You should be able to use a full path, like:
       git config credential.helper '/path/to/git-credential-foo'
     This works with the isalnum thing. But you could also use
     is_absolute_path.
  4. You should be able to insert full shell snippets if you really want
     to. This is handy for testing, and I could even see somebody doing
     something like:
       git config credential.helper '
       f() {
         case "`uname -s`" in
         Linux) git credential-gnome "$@" ;;
         Darwin) git credential-osx "$@" ;;
         esac
       }
       f'
     if they have a multi-platform config file (or they could check
     $DISPLAY, or `hostname`, or any of a zillion other things).  You
     could put it in a shell script, of course, but sometimes it is
     nice to carry little snippets like that in the config file.
     You could do something like "prepend ! to execute via the shell",
     like we do for aliases. But that is perhaps a little confusing, in
     that we are _always_ executing via the shell. It is just that
     sometimes we prepend some magic to the command name. I dunno.
So one possible rule would be:
  1. If it starts with "!", clip off the "!" and hand it to the shell.
  2. Otherwise, if is_absolute_path(), hand it to the shell directly.
  3. Otherwise, prepend "git credential-" and hand it to the shell.

I think that is slightly less confusing than the "first word is alnum" thing.

> Funnily enough, 'f<TAB>() { do_whatever; }; f' would qualify as the first
> word having a non alphanumeric.
Yeah, arguably a bug in my code. ;)
Show 11 quoted lines
> > +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 `=` or
> > +newline. The value may contain any bytes except a newline.  In both
> > +cases, all bytes are treated as-is (i.e., there is no quoting, and one
> > +cannot transmit a value with newline in it).
> 
> Can k or v contain a NUL? The literal reading of the above implies they
> could, but I do not think you meant to.
No, they cannot. I'll make that more explicit in the next version.

How do you feel about the "values cannot contain a newline" requirement? It feels wrong to me to design a protocol with no escape hatch for quoting. But I really didn't want helper implementations to have to deal with unquoting or other parsing headaches. And terminating with NUL instead of newline makes things annoying in some languages (e.g., shell). So I considered it "good enough", but I'd be curious to hear other opinions.

Show 12 quoted lines
> > +int credential_read(struct credential *c, FILE *fp)
> > +{
> > ...
> > +			c->host = xstrdup(value);
> > +		}
> > +		else if (!strcmp(key, "path")) {
> > ...
> > +		/* 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 */
> 
> Two style nits.
I'm supposed to guess? ;P

I know you like blank lines at the top and bottom of multi-line comments; I often forget that, as they are not part of my usual style (I find them no easier to read).

I'm guessing the other is an uncuddled else? I can never remember which we prefer, as we are horribly inconsistent in the current code.

-Peff
Junio C Hamano· Nov 29, 2011, 17:34 UTC · re: Jeff King · lore

Re: [PATCH 03/13] introduce credentials API

Jeff King <peff@peff.net> writes:
Show 65 quoted lines
> On Mon, Nov 28, 2011 at 01:46:35PM -0800, Junio C Hamano wrote:
>
>> > diff --git a/Documentation/technical/api-credentials.txt b/Documentation/technical/api-credentials.txt
>> > new file mode 100644
>> > index 0000000..3061077
>> > --- /dev/null
>> > +++ b/Documentation/technical/api-credentials.txt
>> > @@ -0,0 +1,148 @@
>> > + ...
>> > +`credential_fill`::
>> > +
>> > +	Attempt to fill the username and password fields of the passed
>> > +	credential struct, first consulting storage helpers, then asking
>> > +	the user. Guarantees that the username and password fields will
>> > +	be filled afterwards (or die() will be called).
>> > +
>> > +`credential_reject`::
>> > +
>> > +	Inform the credential subsystem that the provided credentials
>> > +	have been rejected. This will notify any storage helpers of the
>> > +	rejection (which allows them to, for example, purge the invalid
>> > +	credentials from storage), and then clear the username and
>> > +	password fields in `struct credential`. It can then be
>> > +	`credential_fill`-ed again.
>> > +
>> > +`credential_approve`::
>> > +
>> > +	Inform the credential subsystem that the provided credentials
>> > +	were successfully used for authentication. This will notify any
>> > +	storage helpers of the approval, so that they can store the
>> > +	result to be used again.
>> 
>> It's a bit hard to read and understand which part of the system calls
>> these and which other part of the system is responsible for implementing
>> them, and how "helper" fits into the picture (perhaps calling some of
>> these interfaces will result in "helper" getting called?).
>
> How about following it with a rough example of how they would be used,
> like:
>
>   /*
>    * Create a credential with some context; we don't yet know the
>    * username or password.
>    */
>   struct credential c = CREDENTIAL_INIT;
>   c.protocol = xstrdup("https");
>   c.host = xstrdup("example.com");
>   c.path = xstrdup("foo.git");
>
>   /*
>    * 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);
>
>   /*
>    * And then finally make our request, reporting back to the credential
>    * system on whether it succeeded or failed.
>    */
>   if (make_http_request(c.host, c.path, c.username, c.password) < 0)
>           credential_reject(&c);
>   else
>           credential_accept(&c);
>
> Does that make it more clear?

Immensely, at least to me. From the perspective of a user of the API, a call to credential_fill() is to "fill in the credential" in the sense that "call the function to fill in the credential" but I find it easier to understand if it were explained to me as "ask the API to fill in the credential, which may involve helpers to interact with the user--the point of the API is that the caller does not care how it is done". Same for the reject/accept calls---the example makes it clear that they are to tell the decision to reject/accept made by the application to the credential API, and it is up to the API layer what it does using that decision (like removing the cached and now stale password).

The above example is a bit too simplistic and misleading, though. You would call reject only on authentication failure (do not trash stored and good password upon network being unreachable temporarily or the server being overloaded).

Show 10 quoted lines
> So one possible rule would be:
>
>   1. If it starts with "!", clip off the "!" and hand it to the shell.
>
>   2. Otherwise, if is_absolute_path(), hand it to the shell directly.
>
>   3. Otherwise, prepend "git credential-" and hand it to the shell.
>
> I think that is slightly less confusing than the "first word is alnum"
> thing.
Simpler and easier to explain. Good ;-)
> How do you feel about the "values cannot contain a newline" requirement?

In the context of asking username, password, or passphrase, I think "LF is the end of the line and you cannot have that byte in your response" is perfectly reasonable. I've yet to find a way to use LF in a passphrase to unlock my Gnome keychain ;-).

Show 14 quoted lines
>> > +int credential_read(struct credential *c, FILE *fp)
>> > +{
>> > ...
>> > +			c->host = xstrdup(value);
>> > +		}
>> > +		else if (!strcmp(key, "path")) {
>> > ...
>> > +		/* 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 */
>> 
>> Two style nits.
>
> I'm supposed to guess? ;P
Sorry, but you guessed right.
Jeff King· Nov 29, 2011, 21:14 UTC · re: Junio C Hamano · lore

Re: [PATCH 03/13] introduce credentials API

On Tue, Nov 29, 2011 at 09:34:54AM -0800, Junio C Hamano wrote:
Show 6 quoted lines
> >> > +`credential_fill`::
> >> > +
> >> > +	Attempt to fill the username and password fields of the passed
> >> > +	credential struct, first consulting storage helpers, then asking
> >> > +	the user. Guarantees that the username and password fields will
> >> > +	be filled afterwards (or die() will be called).
Show 10 quoted lines
> Immensely, at least to me. From the perspective of a user of the API, a
> call to credential_fill() is to "fill in the credential" in the sense that
> "call the function to fill in the credential" but I find it easier to
> understand if it were explained to me as "ask the API to fill in the
> credential, which may involve helpers to interact with the user--the point
> of the API is that the caller does not care how it is done".  Same for the
> reject/accept calls---the example makes it clear that they are to tell the
> decision to reject/accept made by the application to the credential API,
> and it is up to the API layer what it does using that decision (like
> removing the cached and now stale password).

Ahh, I see your confusion now. It is not that the description is necessarily lacking any content, but that the tense of the sentences is misleading. I'll fix that.

> The above example is a bit too simplistic and misleading, though. You
> would call reject only on authentication failure (do not trash stored and
> good password upon network being unreachable temporarily or the server
> being overloaded).
Good point. I'll fix that and add the example to the documentation.
Show 12 quoted lines
> > So one possible rule would be:
> >
> >   1. If it starts with "!", clip off the "!" and hand it to the shell.
> >
> >   2. Otherwise, if is_absolute_path(), hand it to the shell directly.
> >
> >   3. Otherwise, prepend "git credential-" and hand it to the shell.
> >
> > I think that is slightly less confusing than the "first word is alnum"
> > thing.
> 
> Simpler and easier to explain. Good ;-)
OK, I'll implement that, then.
Show 6 quoted lines
> > How do you feel about the "values cannot contain a newline" requirement?
> 
> In the context of asking username, password, or passphrase, I think "LF is
> the end of the line and you cannot have that byte in your response" is
> perfectly reasonable. I've yet to find a way to use LF in a passphrase to
> unlock my Gnome keychain ;-).

The potential issue is that other values get that, too. So if you have a URL with "\n" in the path, it cannot be transmitted verbatim. We can url-encode, of course, but I didn't want the helpers to have to deal with quoting issues.

Show 5 quoted lines
> >> Two style nits.
> >
> > I'm supposed to guess? ;P
> 
> Sorry, but you guessed right.
OK, will fix.
-Peff
Jeff King· Nov 24, 2011, 11:01 UTC · re: Jeff King · lore

[PATCH 04/13] credential: add function for parsing url components

All of the components of a credential struct can be found in a URL. For example, the URL:

  http://foo:bar@example.com/repo.git
contains:
  protocol=http
  host=example.com
  path=repo.git
  username=foo
  password=bar

We want to be able to turn URLs into broken-down credential structs so that we know two things:

  1. Which parts of the username/password we still need
  2. What the context of the request is (for prompting or
     as a key for storing credentials).

This code is based on http_auth_init in http.c, but needed a few modifications in order to get all of the components that the credential object is interested in.

Once the http code is switched over to the credential API, then http_auth_init can just go away.

Signed-off-by: Jeff King <peff@peff.net>
---
 Documentation/technical/api-credentials.txt |    3 ++
 credential.c                                |   52 +++++++++++++++++++++++++++
 credential.h                                |    1 +
 3 files changed, 56 insertions(+), 0 deletions(-)
Show changes to 3 files +56 −0

Documentation/technical/api-credentials.txt, credential.c, credential.h

diff --git a/Documentation/technical/api-credentials.txt b/Documentation/technical/api-credentials.txt
index 3061077..0dca0bf 100644
--- a/Documentation/technical/api-credentials.txt
+++ b/Documentation/technical/api-credentials.txt
@@ -62,6 +62,9 @@ Functions
 	storage helpers of the approval, so that they can store the
 	result to be used again.
 
+`credential_from_url`::
+
+	Parse a URL into broken-down credential fields.
 
 Credential Storage Helpers
 --------------------------
diff --git a/credential.c b/credential.c
index d50a611..5cc4319 100644
--- a/credential.c
+++ b/credential.c
@@ -2,6 +2,7 @@
 #include "credential.h"
 #include "string-list.h"
 #include "run-command.h"
+#include "url.h"
 
 void credential_init(struct credential *c)
 {
@@ -240,3 +241,54 @@ void credential_reject(struct credential *c)
 	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';
+	}
+}
diff --git a/credential.h b/credential.h
index 2ea7d49..8a6d162 100644
--- a/credential.h
+++ b/credential.h
@@ -24,5 +24,6 @@ struct credential {
 void credential_reject(struct credential *);
 
 int credential_read(struct credential *, FILE *);
+void credential_from_url(struct credential *, const char *url);
 
 #endif /* CREDENTIAL_H */
-- 
1.7.7.4.5.gb32a5
Jeff King· Nov 24, 2011, 11:01 UTC · re: Jeff King · lore

[PATCH 05/13] http: use credential API to get passwords

This patch converts the http code to use the new credential API, both for http authentication as well as for getting certificate passwords.

Most of the code change is simply variable naming (the passwords are now contained inside the credential struct) or deletion of obsolete code (the credential code handles URL parsing and prompting for us).

The behavior should be the same, with one exception: the credential code will prompt with a description based on the credential components. Therefore, the old prompt of:

  Username for 'example.com':
  Password for 'example.com':
now looks like:
  Username for 'https://example.com/repo.git':
  Password for 'https://user@example.com/repo.git':

Note that we include more information in each line, specifically:

  1. We now include the protocol. While more noisy, this is
     an important part of knowing what you are accessing
     (especially if you care about http vs https).
  2. We include the username in the password prompt. This is
     not a big deal when you have just been prompted for it,
     but the username may also come from the remote's URL
     (and after future patches, from configuration or
     credential helpers).  In that case, it's a nice
     reminder of the user for which you're giving the
     password.
  3. We include the path component of the URL. In many
     cases, the user won't care about this and it's simply
     noise (i.e., they'll use the same credential for a
     whole site). However, that is part of a larger
     question, which is whether path components should be
     part of credential context, both for prompting and for
     lookup by storage helpers. That issue will be addressed
     as a whole in a future patch.
Similarly, for unlocking certificates, we used to say:
  Certificate Password for 'example.com':
and we now say:
  Password for 'cert:///path/to/certificate':

Showing the path to the client certificate makes more sense, as that is what you are unlocking, not "example.com".

Signed-off-by: Jeff King <peff@peff.net>
---
 http.c                |  113 +++++++++++--------------------------------------
 t/t5550-http-fetch.sh |   38 ++++++++++++-----
 2 files changed, 52 insertions(+), 99 deletions(-)
Show changes to 2 files +52 −99

http.c, t/t5550-http-fetch.sh

diff --git a/http.c b/http.c
index e6c7597..917a1ae 100644
--- a/http.c
+++ b/http.c
@@ -3,6 +3,7 @@
 #include "sideband.h"
 #include "run-command.h"
 #include "url.h"
+#include "credential.h"
 
 int data_received;
 int active_requests;
@@ -42,7 +43,7 @@
 static 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
@@ -53,7 +54,7 @@
 #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;
@@ -139,27 +140,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)) {
@@ -232,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_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));
 	}
@@ -244,18 +224,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)
@@ -282,7 +258,7 @@ static int has_cert_password(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);
@@ -324,42 +300,6 @@ static int has_cert_password(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);
@@ -432,7 +372,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://"))
@@ -481,10 +421,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;
 }
@@ -836,17 +776,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;
 }
 
diff --git a/t/t5550-http-fetch.sh b/t/t5550-http-fetch.sh
index 3d6e871..398a2d2 100755
--- a/t/t5550-http-fetch.sh
+++ b/t/t5550-http-fetch.sh
@@ -49,40 +49,56 @@ 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/auth/repo.git
+	{
+		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-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 'fetch changes via http' '
-- 
1.7.7.4.5.gb32a5
Jeff King· Nov 24, 2011, 11:02 UTC · re: Jeff King · lore

[PATCH 06/13] credential: apply helper config

The functionality for credential storage helpers is already there; we just need to give the users a way to turn it on. This patch provides a "credential.helper" configuration variable which allows the user to provide one or more helper strings.

Rather than simply matching credential.helper, we will also compare URLs in subsection headings to the current context. This means you can apply configuration to a subset of credentials. For example:

  [credential "https://example.com"]
	helper = foo

would match a request for "https://example.com/foo.git", but not one for "https://kernel.org/foo.git".

This is overkill for the "helper" variable, since users are unlikely to want different helpers for different sites (and since helpers run arbitrary code, they could do the matching themselves anyway).

However, future patches will add new config variables where this extra feature will be more useful.

Signed-off-by: Jeff King <peff@peff.net>
---
 credential.c           |   61 ++++++++++++++++++++++++++++++++++++++++++++++++
 credential.h           |    5 +++-
 t/t0300-credentials.sh |   42 +++++++++++++++++++++++++++++++++
 t/t5550-http-fetch.sh  |   12 +++++++++
 4 files changed, 119 insertions(+), 1 deletions(-)
Show changes to 4 files +119 −1

credential.c, credential.h, t/t0300-credentials.sh, t/t5550-http-fetch.sh

diff --git a/credential.c b/credential.c
index 5cc4319..120ea62 100644
--- a/credential.c
+++ b/credential.c
@@ -22,6 +22,61 @@ void credential_clear(struct credential *c)
 	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);
+
+	return 0;
+}
+
+static void credential_apply_config(struct credential *c)
+{
+	if (c->configured)
+		return;
+	git_config(credential_config_callback, c);
+	c->configured = 1;
+}
+
 static void credential_describe(struct credential *c, struct strbuf *out)
 {
 	if (!c->protocol)
@@ -203,6 +258,8 @@ void credential_fill(struct credential *c)
 	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)
@@ -223,6 +280,8 @@ void credential_approve(struct credential *c)
 	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;
@@ -232,6 +291,8 @@ 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");
 
diff --git a/credential.h b/credential.h
index 8a6d162..e504272 100644
--- a/credential.h
+++ b/credential.h
@@ -5,7 +5,8 @@
 
 struct credential {
 	struct string_list helpers;
-	unsigned approved:1;
+	unsigned approved:1,
+		 configured:1;
 
 	char *username;
 	char *password;
@@ -25,5 +26,7 @@ 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 */
diff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh
index 81a455f..e3f61f4 100755
--- a/t/t0300-credentials.sh
+++ b/t/t0300-credentials.sh
@@ -192,4 +192,46 @@ test_expect_success 'internal getpass does not ask for known username' '
 	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_done
diff --git a/t/t5550-http-fetch.sh b/t/t5550-http-fetch.sh
index 398a2d2..21e2f5d 100755
--- a/t/t5550-http-fetch.sh
+++ b/t/t5550-http-fetch.sh
@@ -101,6 +101,18 @@ test_expect_success 'http auth can request both user and pass' '
 	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 'fetch changes via http' '
 	echo content >>file &&
 	git commit -a -m two &&
-- 
1.7.7.4.5.gb32a5
Jeff King· Nov 24, 2011, 11:02 UTC · re: Jeff King · lore

[PATCH 07/13] credential: add credential.*.username

Credential helpers can help users avoid having to type their username and password over and over. However, some users may not want a helper for their password, or they may be running a helper which caches for a short time. In this case, it is convenient to provide the non-secret username portion of their credential via config.

Signed-off-by: Jeff King <peff@peff.net>
---
 credential.c           |    4 ++++
 t/t0300-credentials.sh |   13 +++++++++++++
 t/t5550-http-fetch.sh  |   16 ++++++++++++++++
 3 files changed, 33 insertions(+), 0 deletions(-)
Show changes to 3 files +33 −0

credential.c, t/t0300-credentials.sh, t/t5550-http-fetch.sh

diff --git a/credential.c b/credential.c
index 120ea62..06e4e15 100644
--- a/credential.c
+++ b/credential.c
@@ -65,6 +65,10 @@ static int credential_config_callback(const char *var, const char *value,
 
 	if (!strcmp(key, "helper"))
 		string_list_append(&c->helpers, value);
+	else if (!strcmp(key, "username")) {
+		if (!c->username)
+			c->username = xstrdup(value);
+	}
 
 	return 0;
 }
diff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh
index e3f61f4..57751a1 100755
--- a/t/t0300-credentials.sh
+++ b/t/t0300-credentials.sh
@@ -234,4 +234,17 @@ test_expect_success 'do not match configured credential' '
 	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_done
diff --git a/t/t5550-http-fetch.sh b/t/t5550-http-fetch.sh
index 21e2f5d..18f4d59 100755
--- a/t/t5550-http-fetch.sh
+++ b/t/t5550-http-fetch.sh
@@ -113,6 +113,22 @@ test_expect_success 'http auth respects credential helper config' '
 	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' '
 	echo content >>file &&
 	git commit -a -m two &&
-- 
1.7.7.4.5.gb32a5
Jeff King· Nov 24, 2011, 11:03 UTC · re: Jeff King · lore

[PATCH 08/13] credential: make relevance of http path configurable

When parsing a URL into a credential struct, we carefully record each part of the URL, including the path on the remote host, and use the result as part of the credential context.

This had two practical implications:
  1. Credential helpers which store a credential for later
     access are likely to use the "path" portion as part of
     the storage key. That means that a request to
       https://example.com/foo.git
     would not use the same credential that was stored in an
     earlier request for:
       https://example.com/bar.git
  2. The prompt shown to the user includes all relevant
     context, including the path.

In most cases, however, users will have a single password per host. The behavior in (1) will be inconvenient, and the prompt in (2) will be overly long.

This patch introduces a config option to toggle the relevance of http paths. When turned on, we use the path as before. When turned off, we drop the path component from the context: helpers don't see it, and it does not appear in the prompt.

This is nothing you couldn't do with a clever credential helper at the start of your stack, like:

  [credential "http://"]
	helper = "f() { grep -v ^path= ; }; f"
	helper = your_real_helper
But doing this:
  [credential]
	useHttpPath = false

is way easier and more readable. Furthermore, since most users will want the "off" behavior, that is the new default. Users who want it "on" can set the variable (either for all credentials, or just for a subset using credential.*.useHttpPath).

Signed-off-by: Jeff King <peff@peff.net>
---
 credential.c           |   14 ++++++++++++++
 credential.h           |    3 ++-
 t/t0300-credentials.sh |   29 +++++++++++++++++++++++++++++
 t/t5550-http-fetch.sh  |    2 +-
 4 files changed, 46 insertions(+), 2 deletions(-)
Show changes to 4 files +46 −2

credential.c, credential.h, t/t0300-credentials.sh, t/t5550-http-fetch.sh

diff --git a/credential.c b/credential.c
index 06e4e15..36ff0ec 100644
--- a/credential.c
+++ b/credential.c
@@ -69,16 +69,30 @@ static int credential_config_callback(const char *var, const char *value,
 		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)
diff --git a/credential.h b/credential.h
index e504272..96ea41b 100644
--- a/credential.h
+++ b/credential.h
@@ -6,7 +6,8 @@
 struct credential {
 	struct string_list helpers;
 	unsigned approved:1,
-		 configured:1;
+		 configured:1,
+		 use_http_path:1;
 
 	char *username;
 	char *password;
diff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh
index 57751a1..89a8531 100755
--- a/t/t0300-credentials.sh
+++ b/t/t0300-credentials.sh
@@ -247,4 +247,33 @@ test_expect_success 'pull username from config' '
 	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
diff --git a/t/t5550-http-fetch.sh b/t/t5550-http-fetch.sh
index 18f4d59..dd073d1 100755
--- a/t/t5550-http-fetch.sh
+++ b/t/t5550-http-fetch.sh
@@ -53,7 +53,7 @@ test_expect_success 'setup askpass helpers' '
 '
 
 expect_askpass() {
-	dest=$HTTPD_DEST/auth/repo.git
+	dest=$HTTPD_DEST
 	{
 		case "$1" in
 		none)
-- 
1.7.7.4.5.gb32a5
Jeff King· Nov 24, 2011, 11:05 UTC · re: Jeff King · lore

[PATCH 09/13] docs: end-user documentation for the credential subsystem

The credential API and helper format is already defined in technical/api-credentials.txt. This presents the end-user view.

Signed-off-by: Jeff King <peff@peff.net>
---
I'm hopelessly far from being the target audience of this document.
Comments welcome on any spots where I forgot for a moment that the
audience hasn't been reading the underlying code for the past 8 hours.
 Documentation/Makefile           |    1 +
 Documentation/config.txt         |   23 +++++
 Documentation/gitcredentials.txt |  170 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 194 insertions(+), 0 deletions(-)
 create mode 100644 Documentation/gitcredentials.txt
Show changes to 3 files +194 −0

Documentation/Makefile, Documentation/config.txt, Documentation/gitcredentials.txt

diff --git a/Documentation/Makefile b/Documentation/Makefile
index 304b31e..116f175 100644
--- a/Documentation/Makefile
+++ b/Documentation/Makefile
@@ -7,6 +7,7 @@ MAN5_TXT=gitattributes.txt gitignore.txt gitmodules.txt githooks.txt \
 MAN7_TXT=gitcli.txt gittutorial.txt gittutorial-2.txt \
 	gitcvs-migration.txt gitcore-tutorial.txt gitglossary.txt \
 	gitdiffcore.txt gitnamespaces.txt gitrevisions.txt gitworkflows.txt
+MAN7_TXT += gitcredentials.txt
 
 MAN_TXT = $(MAN1_TXT) $(MAN5_TXT) $(MAN7_TXT)
 MAN_XML=$(patsubst %.txt,%.xml,$(MAN_TXT))
diff --git a/Documentation/config.txt b/Documentation/config.txt
index 5a841da..36bcdf2 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -832,6 +832,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::
diff --git a/Documentation/gitcredentials.txt b/Documentation/gitcredentials.txt
new file mode 100644
index 0000000..1e54117
--- /dev/null
+++ b/Documentation/gitcredentials.txt
@@ -0,0 +1,170 @@
+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 does not yet
+include any credential helpers, but you may have third-party helpers
+installed; search for `credential-*` in the output of `git help -a`, and
+consult the documentation of individual helpers.  Once you have selected
+a helper, you can tell git to use it by putting its name into the
+credential.helper variable.
+
+1. Find a helper.
++
+-------------------------------------------
+$ git help -a | grep credential-
+credential-foo
+-------------------------------------------
+
+2. Read its description.
++
+-------------------------------------------
+$ git help credential-foo
+-------------------------------------------
+
+3. Tell git to use it.
++
+-------------------------------------------
+$ git config --global credential.helper foo
+-------------------------------------------
+
+If there are multiple instances of the `credential.helper` configuration
+variable, each helper will be tried in turn, and may provide a username,
+password, or nothing. Once git has acquired both a username and a
+password, no more helpers will be tried.
+
+
+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.
+	The value is executed by the shell.  If the first word of the helper
+	string is alphanumeric, then `git-credential-` is prepended (so if you
+	want to use the `git-credential-foo` helper, you would typically set
+	this to just `foo`). See the manual for specific helpers for examples.
+
+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
-- 
1.7.7.4.5.gb32a5
Jeff King· Nov 24, 2011, 11:07 UTC · re: Jeff King · lore

[PATCH 10/13] credentials: add "cache" helper

If you access repositories over smart-http using http authentication, then it can be annoying to have git ask you for your password repeatedly. We cache credentials in memory, of course, but git is composed of many small programs. Having to input your password for each one can be frustrating.

This patch introduces a credential helper that will cache passwords in memory for a short period of time.

Signed-off-by: Jeff King <peff@peff.net>
---
This required some rewriting to handle the new helper protocol. I also
incorporated suggestions from Ramsay Jones to handle cleanup of the
socket better.

I suspect this will all have to get turned off for msysgit, unless somebody feels like writing a unix-sockets abstraction layer.

 .gitignore                                     |    2 +
 Documentation/git-credential-cache--daemon.txt |   26 +++
 Documentation/git-credential-cache.txt         |   77 +++++++
 Documentation/gitcredentials.txt               |   17 +-
 Makefile                                       |    3 +
 credential-cache--daemon.c                     |  269 ++++++++++++++++++++++++
 credential-cache.c                             |  120 +++++++++++
 git-compat-util.h                              |    1 +
 t/lib-credential.sh                            |  219 +++++++++++++++++++
 t/t0301-credential-cache.sh                    |   18 ++
 unix-socket.c                                  |   56 +++++
 unix-socket.h                                  |    7 +
 12 files changed, 810 insertions(+), 5 deletions(-)
 create mode 100644 Documentation/git-credential-cache--daemon.txt
 create mode 100644 Documentation/git-credential-cache.txt
 create mode 100644 credential-cache--daemon.c
 create mode 100644 credential-cache.c
 create mode 100755 t/t0301-credential-cache.sh
 create mode 100644 unix-socket.c
 create mode 100644 unix-socket.h
Show changes to 12 files +810 −5

.gitignore, Documentation/git-credential-cache--daemon.txt, Documentation/git-credential-cache.txt, Documentation/gitcredentials.txt, Makefile, credential-cache--daemon.c, credential-cache.c, git-compat-util.h, t/lib-credential.sh, t/t0301-credential-cache.sh, unix-socket.c, unix-socket.h

diff --git a/.gitignore b/.gitignore
index 7d2fefc..a6b0bd4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,6 +30,8 @@
 /git-commit-tree
 /git-config
 /git-count-objects
+/git-credential-cache
+/git-credential-cache--daemon
 /git-cvsexportcommit
 /git-cvsimport
 /git-cvsserver
diff --git a/Documentation/git-credential-cache--daemon.txt b/Documentation/git-credential-cache--daemon.txt
new file mode 100644
index 0000000..11edc5a
--- /dev/null
+++ b/Documentation/git-credential-cache--daemon.txt
@@ -0,0 +1,26 @@
+git-credential-cache--daemon(1)
+===============================
+
+NAME
+----
+git-credential-cache--daemon - temporarily store user credentials in memory
+
+SYNOPSIS
+--------
+[verse]
+git credential-cache--daemon <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
diff --git a/Documentation/git-credential-cache.txt b/Documentation/git-credential-cache.txt
new file mode 100644
index 0000000..f3d09c5
--- /dev/null
+++ b/Documentation/git-credential-cache.txt
@@ -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
diff --git a/Documentation/gitcredentials.txt b/Documentation/gitcredentials.txt
index 1e54117..a1592ed 100644
--- a/Documentation/gitcredentials.txt
+++ b/Documentation/gitcredentials.txt
@@ -63,11 +63,18 @@ 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 does not yet
-include any credential helpers, but you may have third-party helpers
-installed; search for `credential-*` in the output of `git help -a`, and
-consult the documentation of individual helpers.  Once you have selected
-a helper, you can tell git to use it by putting its name into the
+To use a helper, you must first select one to use. Git currently
+includes the following helpers:
+
+cache::
+
+	Cache credentials in memory for a short period of time. See
+	linkgit:git-credential-cache[1] for details.
+
+You may may also have third-party helpers installed; search for
+`credential-*` in the output of `git help -a`, and consult the
+documentation of individual helpers.  Once you have selected a helper,
+you can tell git to use it by putting its name into the
 credential.helper variable.
 
 1. Find a helper.
diff --git a/Makefile b/Makefile
index 5ca363b..5d41c29 100644
--- a/Makefile
+++ b/Makefile
@@ -427,6 +427,8 @@ PROGRAM_OBJS += show-index.o
 PROGRAM_OBJS += upload-pack.o
 PROGRAM_OBJS += http-backend.o
 PROGRAM_OBJS += sh-i18n--envsubst.o
+PROGRAM_OBJS += credential-cache.o
+PROGRAM_OBJS += credential-cache--daemon.o
 
 PROGRAMS += $(patsubst %.o,git-%$X,$(PROGRAM_OBJS))
 
@@ -699,6 +701,7 @@ LIB_OBJS += transport-helper.o
 LIB_OBJS += tree-diff.o
 LIB_OBJS += tree.o
 LIB_OBJS += tree-walk.o
+LIB_OBJS += unix-socket.o
 LIB_OBJS += unpack-trees.o
 LIB_OBJS += url.o
 LIB_OBJS += usage.o
diff --git a/credential-cache--daemon.c b/credential-cache--daemon.c
new file mode 100644
index 0000000..390f194
--- /dev/null
+++ b/credential-cache--daemon.c
@@ -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;
+}
diff --git a/credential-cache.c b/credential-cache.c
new file mode 100644
index 0000000..482c4b1
--- /dev/null
+++ b/credential-cache.c
@@ -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
+		die("unknown operation: %s", op);
+
+	return 0;
+}
diff --git a/git-compat-util.h b/git-compat-util.h
index 8b4dd5c..5c238bd 100644
--- a/git-compat-util.h
+++ b/git-compat-util.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
diff --git a/t/lib-credential.sh b/t/lib-credential.sh
index 54ae1f4..ac54b38 100755
--- a/t/lib-credential.sh
+++ b/t/lib-credential.sh
@@ -21,6 +21,225 @@ read_chunk() {
 	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 ftp other.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" '
+		check approve $HELPER <<-\EOF &&
+		protocol=ftp
+		host=other.tld
+		path=foo.git
+		username=user
+		password=pass
+		EOF
+		check fill $HELPER <<-\EOF
+		protocol=ftp
+		host=other.tld
+		path=bar.git
+		--
+		username=askpass-username
+		password=askpass-password
+		--
+		askpass: Username for '\''ftp://other.tld/bar.git'\'':
+		askpass: Password for '\''ftp://askpass-username@other.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'\'':
+	'
+
+	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
diff --git a/t/t0301-credential-cache.sh b/t/t0301-credential-cache.sh
new file mode 100755
index 0000000..3a65f99
--- /dev/null
+++ b/t/t0301-credential-cache.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+test_description='credential-cache tests'
+. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-credential.sh
+
+# 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
diff --git a/unix-socket.c b/unix-socket.c
new file mode 100644
index 0000000..84b1509
--- /dev/null
+++ b/unix-socket.c
@@ -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;
+}
diff --git a/unix-socket.h b/unix-socket.h
new file mode 100644
index 0000000..e271aee
--- /dev/null
+++ b/unix-socket.h
@@ -0,0 +1,7 @@
+#ifndef UNIX_SOCKET_H
+#define UNIX_SOCKET_H
+
+int unix_stream_connect(const char *path);
+int unix_stream_listen(const char *path);
+
+#endif /* UNIX_SOCKET_H */
-- 
1.7.7.4.5.gb32a5
Eric Sunshine· Nov 24, 2011, 14:36 UTC · re: Jeff King · lore

Re: [PATCH 10/13] credentials: add "cache" helper

On Thu, Nov 24, 2011 at 6:07 AM, Jeff King <peff@peff.net> wrote:
Show 12 quoted lines
> diff --git a/Documentation/gitcredentials.txt b/Documentation/gitcredentials.txt
> index 1e54117..a1592ed 100644
> --- a/Documentation/gitcredentials.txt
> +++ b/Documentation/gitcredentials.txt
> @@ -63,11 +63,18 @@ Credential helpers, on the other hand, are external programs from which git can
>  [snip]
> +cache::
> +
> +       Cache credentials in memory for a short period of time. See
> +       linkgit:git-credential-cache[1] for details.
> +
> +You may may also have third-party helpers installed; search for
s/may may/may/
-- ES
Junio C Hamano· Nov 29, 2011, 00:42 UTC · re: Jeff King · lore

Re: [PATCH 10/13] credentials: add "cache" helper

Jeff King <peff@peff.net> writes:
Show 22 quoted lines
> diff --git a/t/lib-credential.sh b/t/lib-credential.sh
> index 54ae1f4..ac54b38 100755
> --- a/t/lib-credential.sh
> +++ b/t/lib-credential.sh
> @@ -21,6 +21,225 @@ read_chunk() {
> ...
> +	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'\'':
> +	'

Missing EOF for here document; I fixed this already in 'pu', but please squash it in when rerolling after 1.7.8 final.

Jeff King· Nov 29, 2011, 05:04 UTC · re: Junio C Hamano · lore

Re: [PATCH 10/13] credentials: add "cache" helper

On Mon, Nov 28, 2011 at 04:42:00PM -0800, Junio C Hamano wrote:
Show 5 quoted lines
> > +		askpass: Password for '\''https://user1@example.com'\'':
> > +	'
> 
> Missing EOF for here document; I fixed this already in 'pu', but please
> squash it in when rerolling after 1.7.8 final.
Thanks, will do.
-Peff
Jeff King· Nov 24, 2011, 11:07 UTC · re: Jeff King · lore

[PATCH 11/13] strbuf: add strbuf_add*_urlencode

This just follows the rfc3986 rules for percent-encoding url data into a strbuf.

Signed-off-by: Jeff King <peff@peff.net>
---
 strbuf.c |   37 +++++++++++++++++++++++++++++++++++++
 strbuf.h |    5 +++++
 2 files changed, 42 insertions(+), 0 deletions(-)
Show changes to 2 files +42 −0

strbuf.c, strbuf.h

diff --git a/strbuf.c b/strbuf.c
index 3ad2cc0..60e5e59 100644
--- a/strbuf.c
+++ b/strbuf.c
@@ -397,3 +397,40 @@ int strbuf_read_file(struct strbuf *sb, const char *path, size_t hint)
 
 	return len;
 }
+
+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);
+}
diff --git a/strbuf.h b/strbuf.h
index 46a33f8..cecd48c 100644
--- a/strbuf.h
+++ b/strbuf.h
@@ -115,4 +115,9 @@ struct strbuf_expand_dict_entry {
 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 */
-- 
1.7.7.4.5.gb32a5
Junio C Hamano· Nov 29, 2011, 18:19 UTC · re: Jeff King · lore

Re: [PATCH 11/13] strbuf: add strbuf_add*_urlencode

Jeff King <peff@peff.net> writes:
Show 28 quoted lines
> This just follows the rfc3986 rules for percent-encoding
> url data into a strbuf.
>
> Signed-off-by: Jeff King <peff@peff.net>
> ---
>  strbuf.c |   37 +++++++++++++++++++++++++++++++++++++
>  strbuf.h |    5 +++++
>  2 files changed, 42 insertions(+), 0 deletions(-)
>
> diff --git a/strbuf.c b/strbuf.c
> index 3ad2cc0..60e5e59 100644
> --- a/strbuf.c
> +++ b/strbuf.c
> @@ -397,3 +397,40 @@ int strbuf_read_file(struct strbuf *sb, const char *path, size_t hint)
>  
>  	return len;
>  }
> +
> +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;
> +}

Part of me wonders if we still have extra bits in sane_ctype[] array but that one is cumbersome to update, and the above should be easier to read and maintain.

> +void strbuf_add_urlencode(struct strbuf *sb, const char *s, size_t len,
> +			  int reserved)

Does "reserved" parameter mean "must-encode-reserved", or "may-encode-reserved" (the latter would be more like "if set to 0, per-cent encoding the result would be an error")?

Jeff King· Nov 29, 2011, 21:19 UTC · re: Junio C Hamano · lore

Re: [PATCH 11/13] strbuf: add strbuf_add*_urlencode

On Tue, Nov 29, 2011 at 10:19:00AM -0800, Junio C Hamano wrote:
Show 14 quoted lines
> > +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;
> > +}
> 
> Part of me wonders if we still have extra bits in sane_ctype[] array but
> that one is cumbersome to update, and the above should be easier to read
> and maintain.

We have 2 bits left. I did consider it, but it just seemed excessively cumbersome for something that really doesn't need to be that fast (if it is indeed any faster than this case statement).

Show 6 quoted lines
> > +void strbuf_add_urlencode(struct strbuf *sb, const char *s, size_t len,
> > +			  int reserved)
> 
> Does "reserved" parameter mean "must-encode-reserved", or
> "may-encode-reserved" (the latter would be more like "if set to 0,
> per-cent encoding the result would be an error")?

It is "must-encode-reserved". The difference, from my reading of the rfc, is that we can relax our encoding in the path-name portion of the URI. For example, in:

  https://user@host/path/to/repo.git

You definitely want to quote "/" in the user or hostname, but doing so in path/to/repo.git is just annoying.

-Peff
René Scharfe· Nov 29, 2011, 23:26 UTC · re: Jeff King · lore

Re: [PATCH 11/13] strbuf: add strbuf_add*_urlencode

Am 29.11.2011 22:19, schrieb Jeff King:
Show 20 quoted lines
> On Tue, Nov 29, 2011 at 10:19:00AM -0800, Junio C Hamano wrote:
> 
>>> +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;
>>> +}
>>
>> Part of me wonders if we still have extra bits in sane_ctype[] array but
>> that one is cumbersome to update, and the above should be easier to read
>> and maintain.
> 
> We have 2 bits left. I did consider it, but it just seemed excessively
> cumbersome for something that really doesn't need to be that fast (if it
> is indeed any faster than this case statement).
Sorry for my bikeshedding, but I'd paint it like this:
	return !!strchr("!*'();:@&=+$,/?#[]", ch);
René
Jeff King· Nov 30, 2011, 03:20 UTC · re: René Scharfe · lore

Re: [PATCH 11/13] strbuf: add strbuf_add*_urlencode

On Wed, Nov 30, 2011 at 12:26:20AM +0100, René Scharfe wrote:
Show 12 quoted lines
> >>> +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;
> >>> +	}
> [...]
> Sorry for my bikeshedding, but I'd paint it like this:
> 
> 	return !!strchr("!*'();:@&=+$,/?#[]", ch);

I was always under the impression that computed jumps via "switch" would out-perform even an optimized strchr. Of course, I never tested. And I doubt performance is even relevant here, and I admit I don't care overly much. I find them both equally readable.

I'm going to leave it as-is unless somebody else wants to say "I strongly prefer version X".

-Peff
Junio C Hamano· Nov 30, 2011, 05:40 UTC · re: Jeff King · lore

Re: [PATCH 11/13] strbuf: add strbuf_add*_urlencode

Jeff King <peff@peff.net> writes:
Show 22 quoted lines
> On Wed, Nov 30, 2011 at 12:26:20AM +0100, René Scharfe wrote:
>
>> >>> +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;
>> >>> +	}
>> [...]
>> Sorry for my bikeshedding, but I'd paint it like this:
>> 
>> 	return !!strchr("!*'();:@&=+$,/?#[]", ch);
>
> I was always under the impression that computed jumps via "switch" would
> out-perform even an optimized strchr. Of course, I never tested. And I
> doubt performance is even relevant here, and I admit I don't care overly
> much. I find them both equally readable.
>
> I'm going to leave it as-is unless somebody else wants to say "I
> strongly prefer version X".

I find the switch/case one much easier to read and count, especially since all the choices are essentially line-noise characters.

Just make sure you indent it correctly ;-)
René Scharfe· Nov 30, 2011, 05:41 UTC · re: Jeff King · lore

Re: [PATCH 11/13] strbuf: add strbuf_add*_urlencode

Am 30.11.2011 04:20, schrieb Jeff King:
Show 22 quoted lines
> On Wed, Nov 30, 2011 at 12:26:20AM +0100, René Scharfe wrote:
> 
>>>>> +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;
>>>>> +	}
>> [...]
>> Sorry for my bikeshedding, but I'd paint it like this:
>>
>> 	return !!strchr("!*'();:@&=+$,/?#[]", ch);
> 
> I was always under the impression that computed jumps via "switch" would
> out-perform even an optimized strchr. Of course, I never tested. And I
> doubt performance is even relevant here, and I admit I don't care overly
> much. I find them both equally readable.
> 
> I'm going to leave it as-is unless somebody else wants to say "I
> strongly prefer version X".

Sure, the second one is significantly slower than the first one. I just prefer it based one its looks in case performance doesn't matter, but that's probably just me being (sometimes too) fond of terseness. :)

René
Jeff King· Nov 24, 2011, 11:07 UTC · re: Jeff King · lore

[PATCH 12/13] credentials: add "store" helper

This is like "cache", except that we actually put the credentials on disk. This can be terribly insecure, of course, but we do what we can to protect them by filesystem permissions, and we warn the user in the documentation.

This is not unlike using .netrc to store entries, but it's a little more user-friendly. Instead of putting credentials in place ahead of time, we transparently store them after prompting the user for them once.

Signed-off-by: Jeff King <peff@peff.net>
---
 .gitignore                             |    1 +
 Documentation/git-credential-store.txt |   75 +++++++++++++++++
 Documentation/gitcredentials.txt       |    5 +
 Makefile                               |    1 +
 credential-store.c                     |  144 ++++++++++++++++++++++++++++++++
 t/t0302-credential-store.sh            |    9 ++
 6 files changed, 235 insertions(+), 0 deletions(-)
 create mode 100644 Documentation/git-credential-store.txt
 create mode 100644 credential-store.c
 create mode 100755 t/t0302-credential-store.sh
Show changes to 6 files +235 −0

.gitignore, Documentation/git-credential-store.txt, Documentation/gitcredentials.txt, Makefile, credential-store.c, t/t0302-credential-store.sh

diff --git a/.gitignore b/.gitignore
index a6b0bd4..2b7a3f9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,6 +32,7 @@
 /git-count-objects
 /git-credential-cache
 /git-credential-cache--daemon
+/git-credential-store
 /git-cvsexportcommit
 /git-cvsimport
 /git-cvsserver
diff --git a/Documentation/git-credential-store.txt b/Documentation/git-credential-store.txt
new file mode 100644
index 0000000..3dcffbd
--- /dev/null
+++ b/Documentation/git-credential-store.txt
@@ -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 context 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
diff --git a/Documentation/gitcredentials.txt b/Documentation/gitcredentials.txt
index a1592ed..519d2bb 100644
--- a/Documentation/gitcredentials.txt
+++ b/Documentation/gitcredentials.txt
@@ -71,6 +71,11 @@ cache::
 	Cache credentials in memory for a short period of time. See
 	linkgit:git-credential-cache[1] for details.
 
+store::
+
+	Store credentials indefinitely on disk. See
+	linkgit:git-credential-store[1] for details.
+
 You may may also have third-party helpers installed; search for
 `credential-*` in the output of `git help -a`, and consult the
 documentation of individual helpers.  Once you have selected a helper,
diff --git a/Makefile b/Makefile
index 5d41c29..2537128 100644
--- a/Makefile
+++ b/Makefile
@@ -429,6 +429,7 @@ PROGRAM_OBJS += http-backend.o
 PROGRAM_OBJS += sh-i18n--envsubst.o
 PROGRAM_OBJS += credential-cache.o
 PROGRAM_OBJS += credential-cache--daemon.o
+PROGRAM_OBJS += credential-store.o
 
 PROGRAMS += $(patsubst %.o,git-%$X,$(PROGRAM_OBJS))
 
diff --git a/credential-store.c b/credential-store.c
new file mode 100644
index 0000000..a6ddb0c
--- /dev/null
+++ b/credential-store.c
@@ -0,0 +1,144 @@
+#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)
+{
+	umask(077);
+	if (hold_lock_file_for_update(&credential_lock, fn, 0) < 0)
+		die_errno("unable to get credential storage lock");
+	parse_credential_file(fn, c, NULL, print_line);
+	if (extra)
+		print_line(extra);
+	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;
+
+	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)
+{
+	if (!c->protocol || !(c->host || c->path))
+		return;
+	rewrite_credential_file(fn, c, NULL);
+}
+
+static int lookup_credential(const char *fn, struct credential *c)
+{
+	if (!c->protocol || !(c->host || c->path))
+		return 0;
+	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 *store = NULL;
+	struct option options[] = {
+		OPT_STRING_LIST(0, "store", &store, "file",
+				"fetch and store credentials in <file>"),
+		OPT_END()
+	};
+
+	argc = parse_options(argc, argv, NULL, options, usage, 0);
+	if (argc != 1)
+		usage_with_options(usage, options);
+	op = argv[0];
+
+	if (!store)
+		store = expand_user_path("~/.git-credentials");
+	if (!store)
+		die("unable to set up default store; use --store");
+
+	if (credential_read(&c, stdin) < 0)
+		die("unable to read credential");
+
+	if (!strcmp(op, "get"))
+		lookup_credential(store, &c);
+	else if (!strcmp(op, "erase"))
+		remove_credential(store, &c);
+	else if (!strcmp(op, "store"))
+		store_credential(store, &c);
+	else
+		die("unknown operation: %s", op);
+
+	return 0;
+}
diff --git a/t/t0302-credential-store.sh b/t/t0302-credential-store.sh
new file mode 100755
index 0000000..f61b40c
--- /dev/null
+++ b/t/t0302-credential-store.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+test_description='credential-store tests'
+. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-credential.sh
+
+helper_test store
+
+test_done
-- 
1.7.7.4.5.gb32a5
Eric Sunshine· Nov 24, 2011, 14:29 UTC · re: Jeff King · lore

Re: [PATCH 12/13] credentials: add "store" helper

On Thu, Nov 24, 2011 at 6:07 AM, Jeff King <peff@peff.net> wrote:
Show 15 quoted lines
> diff --git a/Documentation/git-credential-store.txt b/Documentation/git-credential-store.txt
> new file mode 100644
> index 0000000..3dcffbd
> --- /dev/null
> +++ b/Documentation/git-credential-store.txt
> @@ -0,0 +1,75 @@
> [snip]
> +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 context URL context,
"context URL context"?
-- ES
Jeff King· Nov 24, 2011, 20:09 UTC · re: Eric Sunshine · lore

Re: [PATCH 12/13] credentials: add "store" helper

On Thu, Nov 24, 2011 at 09:29:36AM -0500, Eric Sunshine wrote:
> > +When git needs authentication for a particular context URL context,
> 
> "context URL context"?
Thanks. That is what I get for proofreading right before bed. :)
I squashed this and your other typo fix into the appropriate patches.
-Peff
Junio C Hamano· Nov 29, 2011, 18:19 UTC · re: Jeff King · lore

Re: [PATCH 12/13] credentials: add "store" helper

Jeff King <peff@peff.net> writes:
Show 20 quoted lines
> +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)) {

This looks curious; isn't checking .username and .password part of the responsibility of credential_match()? And even if entry lacks password (which won't happen in the context of this program, given the implementation of store_credential() below) shouldn't it still be considered a match?

> +			if (match_cb) {
> +				match_cb(&entry);
> +				break;
Multiple matches not allowed, which sounds fine.
Show 27 quoted lines
> +			}
> +		}
> +		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)
> +{
> +	umask(077);

Curious placement of umask(). I would expect a function that has its own call to umask() restore it before it returns, and a stand-alone program whose sole purpose is to work with a private file, setting a tight umask upfront at the beginning of main() may be easier to understand.

Show 5 quoted lines
> +	if (hold_lock_file_for_update(&credential_lock, fn, 0) < 0)
> +		die_errno("unable to get credential storage lock");
> +	parse_credential_file(fn, c, NULL, print_line);
> +	if (extra)
> +		print_line(extra);

An entry for a newly updated password comes at the end of the file, instead of replacing an entry already in the file in-place? Given that parse_credential_file() when processing a look-up request (which is the majority of the case) stops upon finding a match, it might make more sense to have the new one (which may be expected to be used often) at the beginning instead, no?

Show 32 quoted lines
> +	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;
> +
> +	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)
> +{
> +	if (!c->protocol || !(c->host || c->path))
> +		return;

The choice of the fields looks rather arbitrary. I cannot say "remove all the credentials whose username is 'gitster' at 'github.com' no matter what protocol is used", but I can say "remove all credentials under any name for any host as long as the transfer goes over 'https' and accesses a repository at 'if/xyzzy' path", it seems.

This filtering matches what lookup_credential() does but shouldn't it be implemented at a single place in any case?

Show 10 quoted lines
> +	rewrite_credential_file(fn, c, NULL);
> +}
> +
> +static int lookup_credential(const char *fn, struct credential *c)
> +{
> +	if (!c->protocol || !(c->host || c->path))
> +		return 0;
> +	parse_credential_file(fn, c, print_entry, NULL);
> +	return c->username && c->password;
> +}
Jeff King· Nov 29, 2011, 21:38 UTC · re: Junio C Hamano · lore

Re: [PATCH 12/13] credentials: add "store" helper

On Tue, Nov 29, 2011 at 10:19:06AM -0800, Junio C Hamano wrote:
Show 10 quoted lines
> > +	while (strbuf_getline(&line, fh, '\n') != EOF) {
> > +		credential_from_url(&entry, line.buf);
> > +		if (entry.username && entry.password &&
> > +		    credential_match(c, &entry)) {
> 
> This looks curious; isn't checking .username and .password part of the
> responsibility of credential_match()? And even if entry lacks password
> (which won't happen in the context of this program, given the
> implementation of store_credential() below) shouldn't it still be
> considered a match?

credential_match will check .username, if the pattern mentions it. It will never check .password. My intent here was to enforce well-formed entries in the credential file. So you could add:

  http://example.com/

to the credential file, but it's just meaningless noise. It doesn't actually tell us a username or password.

The helper won't add such an entry itself, but given the simplicity of the format, I wanted to leave the door open for curious hackers to populate it manually if they choose.

I think you're right that:
  http://user@example.com/

is potentially meaningful, and this would skip that. OTOH, you would be much better served to just do:

  git config credential.http://example.com.username user
So I consider it a slight abuse of this helper in the first place.
Show 9 quoted lines
> > +static void rewrite_credential_file(const char *fn, struct credential *c,
> > +				    struct strbuf *extra)
> > +{
> > +	umask(077);
> 
> Curious placement of umask(). I would expect a function that has its own
> call to umask() restore it before it returns, and a stand-alone program
> whose sole purpose is to work with a private file, setting a tight umask
> upfront at the beginning of main() may be easier to understand.

I think that is largely a holdover from the original implementation, which set the umask and did other black magic before calling git_config_set. I agree it would make more sense at the beginning of the program. Will change.

Show 12 quoted lines
> > +	if (hold_lock_file_for_update(&credential_lock, fn, 0) < 0)
> > +		die_errno("unable to get credential storage lock");
> > +	parse_credential_file(fn, c, NULL, print_line);
> > +	if (extra)
> > +		print_line(extra);
> 
> An entry for a newly updated password comes at the end of the file,
> instead of replacing an entry already in the file in-place? Given that
> parse_credential_file() when processing a look-up request (which is the
> majority of the case) stops upon finding a match, it might make more sense
> to have the new one (which may be expected to be used often) at the
> beginning instead, no?

Yeah. It's a linear search. Your worst-case is always going to be O(n), but I just assumed n would remain relatively small and we wouldn't care (if it isn't, the right solution is probably a smarter data structure).

But your optimization is trivial to implement, so it's probably worth doing.

Show 22 quoted lines
> > +	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;
> > +
> > +	if (!c->protocol || !(c->host || c->path) ||
> > +	    !c->username || !c->password)
> > +		return;
> [...]
> > +static void remove_credential(const char *fn, struct credential *c)
> > +{
> > +	if (!c->protocol || !(c->host || c->path))
> > +		return;
> 
> The choice of the fields looks rather arbitrary. I cannot say "remove all
> the credentials whose username is 'gitster' at 'github.com' no matter what
> protocol is used", but I can say "remove all credentials under any name
> for any host as long as the transfer goes over 'https' and accesses a
> repository at 'if/xyzzy' path", it seems.

It is kind of arbitrary. The storage format is URLs, which is why store_credential is a little pedantic. We can't store something that doesn't have a protocol part, as that is a required part of the URL (actually, in URL-speak this is the "scheme"; I wonder if we should use the same term here).

I was thinking we need a protocol for the same reason in remove_credential, but I think you are right. We never actually convert it to a URL, so in theory you could do:

  git credential-store erase <<\EOF
  username=gitster
  host=github.com
  EOF

Again, not an operation that git will ever perform, but I guess something that people might want to do (I had always assumed the "$EDITOR ~/.git-credentials" was going to be the preferred way of doing such operations :) ).

I don't think there's any harm in loosening that condition.
-Peff
Jeff King· Nov 24, 2011, 11:09 UTC · re: Jeff King · lore

[PATCH 13/13] t: add test harness for external credential helpers

We already have tests for the internal helpers, but it's nice to give authors of external tools an easy way to sanity-check their helpers.

If you have written the "git-credential-foo" helper, you can do so with:

  GIT_TEST_CREDENTIAL_HELPER=foo \
  make t0303-credential-external.sh

This assumes that your helper is capable of both storing and retrieving credentials (some helpers may be read-only, and they will fail these tests).

If your helper supports time-based expiration with a configurable timeout, you can test that feature like this:

  GIT_TEST_CREDENTIAL_HELPER_TIMEOUT="foo --timeout=1" \
  make t0303-credential-external.sh
Signed-off-by: Jeff King <peff@peff.net>
---
I haven't tried porting or testing any of the older helpers to the new
interface. So this script has only been lightly tested with:
  GIT_TEST_CREDENTIAL_HELPER=cache
 t/t0303-credential-external.sh |   19 +++++++++++++++++++
 1 files changed, 19 insertions(+), 0 deletions(-)
 create mode 100755 t/t0303-credential-external.sh
Show changes to t/t0303-credential-external.sh +19 −0
diff --git a/t/t0303-credential-external.sh b/t/t0303-credential-external.sh
new file mode 100755
index 0000000..f3953d4
--- /dev/null
+++ b/t/t0303-credential-external.sh
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+test_description='external credential helper tests'
+. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-credential.sh
+
+if test -z "$GIT_TEST_CREDENTIAL_HELPER"; then
+	say "# skipping external helper tests (set GIT_TEST_CREDENTIAL_HELPER)"
+else
+	helper_test "$GIT_TEST_CREDENTIAL_HELPER"
+fi
+
+if test -z "$GIT_TEST_CREDENTIAL_HELPER_TIMEOUT"; then
+	say "# skipping external helper timeout tests"
+else
+	helper_test_timeout "$GIT_TEST_CREDENTIAL_HELPER_TIMEOUT"
+fi
+
+test_done
-- 
1.7.7.4.5.gb32a5
Erik Faye-Lund· Nov 24, 2011, 11:45 UTC · re: Jeff King · lore

Re: [PATCH 0/13] credential helpers, take two

On Thu, Nov 24, 2011 at 11:58 AM, Jeff King <peff@peff.net> wrote:
> Here's a revised version of the http-auth / credential-helper series.
>
Nice. Do you have the branch somewhere public I can pull it from?
Jeff King· Nov 24, 2011, 11:53 UTC · re: Erik Faye-Lund · lore

Re: [PATCH 0/13] credential helpers, take two

On Thu, Nov 24, 2011 at 12:45:39PM +0100, Erik Faye-Lund wrote:
Show 5 quoted lines
> On Thu, Nov 24, 2011 at 11:58 AM, Jeff King <peff@peff.net> wrote:
> > Here's a revised version of the http-auth / credential-helper series.
> >
> 
> Nice. Do you have the branch somewhere public I can pull it from?
  git://github.com/peff/git.git jk/http-auth-simple
But be aware that I do rebase my topic branches frequently.
-Peff
Erik Faye-Lund· Nov 24, 2011, 12:08 UTC · re: Jeff King · lore

Re: [PATCH 0/13] credential helpers, take two

On Thu, Nov 24, 2011 at 12:53 PM, Jeff King <peff@peff.net> wrote:
Show 10 quoted lines
> On Thu, Nov 24, 2011 at 12:45:39PM +0100, Erik Faye-Lund wrote:
>
>> On Thu, Nov 24, 2011 at 11:58 AM, Jeff King <peff@peff.net> wrote:
>> > Here's a revised version of the http-auth / credential-helper series.
>> >
>>
>> Nice. Do you have the branch somewhere public I can pull it from?
>
>  git://github.com/peff/git.git jk/http-auth-simple
>
Thanks.
> But be aware that I do rebase my topic branches frequently.
>

That's fine by me, I just didn't want to have to download each patch as a raw e-mail and apply it manually ;)

Jeff King· Nov 27, 2011, 08:27 UTC · re: Jeff King · lore

[PATCH 0/6] echo usernames as they are typed

On Thu, Nov 24, 2011 at 05:58:01AM -0500, Jeff King wrote:
> Here's a revised version of the http-auth / credential-helper series.

And here's something I've been meaning to do on top: actually echo characters at the username prompt. We can't do this portably, but we can at least stub out a compatibility layer and let each system do something sensible.

  [1/6]: move git_getpass to its own source file
  [2/6]: refactor git_getpass into generic prompt function
  [3/6]: stub out getpass_echo function
  [4/6]: prompt: add PROMPT_ECHO flag
  [5/6]: credential: use git_prompt instead of git_getpass
  [6/6]: compat/getpass: add a /dev/tty implementation
-Peff
Jeff King· Nov 27, 2011, 08:28 UTC · re: Jeff King · lore

[PATCH 1/6] move git_getpass to its own source file

This is currently in connect.c, but really has nothing to do with the git protocol itself. Let's make a new source file all about prompting the user, which will make it cleaner to refactor.

Signed-off-by: Jeff King <peff@peff.net>
---
 Makefile     |    2 ++
 cache.h      |    1 -
 connect.c    |   44 --------------------------------------------
 credential.c |    1 +
 imap-send.c  |    1 +
 prompt.c     |   48 ++++++++++++++++++++++++++++++++++++++++++++++++
 prompt.h     |    6 ++++++
 7 files changed, 58 insertions(+), 45 deletions(-)
 create mode 100644 prompt.c
 create mode 100644 prompt.h
Show changes to 7 files +58 −45

Makefile, cache.h, connect.c, credential.c, imap-send.c, prompt.c, prompt.h

diff --git a/Makefile b/Makefile
index 2537128..14c6480 100644
--- a/Makefile
+++ b/Makefile
@@ -563,6 +563,7 @@ LIB_H += parse-options.h
 LIB_H += patch-ids.h
 LIB_H += pkt-line.h
 LIB_H += progress.h
+LIB_H += prompt.h
 LIB_H += quote.h
 LIB_H += reflog-walk.h
 LIB_H += refs.h
@@ -669,6 +670,7 @@ LIB_OBJS += pkt-line.o
 LIB_OBJS += preload-index.o
 LIB_OBJS += pretty.o
 LIB_OBJS += progress.o
+LIB_OBJS += prompt.o
 LIB_OBJS += quote.o
 LIB_OBJS += reachable.o
 LIB_OBJS += read-cache.o
diff --git a/cache.h b/cache.h
index 2e6ad36..f320c98 100644
--- a/cache.h
+++ b/cache.h
@@ -1024,7 +1024,6 @@ struct ref {
 extern struct ref *find_ref_by_name(const struct ref *list, const char *name);
 
 #define CONNECT_VERBOSE       (1u << 0)
-extern char *git_getpass(const char *prompt);
 extern struct child_process *git_connect(int fd[2], const char *url, const char *prog, int flags);
 extern int finish_connect(struct child_process *conn);
 extern int git_connection_is_socket(struct child_process *conn);
diff --git a/connect.c b/connect.c
index 51990fa..519e527 100644
--- a/connect.c
+++ b/connect.c
@@ -619,47 +619,3 @@ int finish_connect(struct child_process *conn)
 	free(conn);
 	return code;
 }
-
-char *git_getpass(const char *prompt)
-{
-	const char *askpass;
-	struct child_process pass;
-	const char *args[3];
-	static struct strbuf buffer = STRBUF_INIT;
-
-	askpass = getenv("GIT_ASKPASS");
-	if (!askpass)
-		askpass = askpass_program;
-	if (!askpass)
-		askpass = getenv("SSH_ASKPASS");
-	if (!askpass || !(*askpass)) {
-		char *result = getpass(prompt);
-		if (!result)
-			die_errno("Could not read password");
-		return result;
-	}
-
-	args[0] = askpass;
-	args[1]	= prompt;
-	args[2] = NULL;
-
-	memset(&pass, 0, sizeof(pass));
-	pass.argv = args;
-	pass.out = -1;
-
-	if (start_command(&pass))
-		exit(1);
-
-	strbuf_reset(&buffer);
-	if (strbuf_read(&buffer, pass.out, 20) < 0)
-		die("failed to read password from %s\n", askpass);
-
-	close(pass.out);
-
-	if (finish_command(&pass))
-		exit(1);
-
-	strbuf_setlen(&buffer, strcspn(buffer.buf, "\r\n"));
-
-	return buffer.buf;
-}
diff --git a/credential.c b/credential.c
index 36ff0ec..d918ba5 100644
--- a/credential.c
+++ b/credential.c
@@ -3,6 +3,7 @@
 #include "string-list.h"
 #include "run-command.h"
 #include "url.h"
+#include "prompt.h"
 
 void credential_init(struct credential *c)
 {
diff --git a/imap-send.c b/imap-send.c
index e1ad1a4..8fc2e2d 100644
--- a/imap-send.c
+++ b/imap-send.c
@@ -25,6 +25,7 @@
 #include "cache.h"
 #include "exec_cmd.h"
 #include "run-command.h"
+#include "prompt.h"
 #ifdef NO_OPENSSL
 typedef void *SSL;
 #else
diff --git a/prompt.c b/prompt.c
new file mode 100644
index 0000000..42a1c9f
--- /dev/null
+++ b/prompt.c
@@ -0,0 +1,48 @@
+#include "cache.h"
+#include "run-command.h"
+#include "strbuf.h"
+#include "prompt.h"
+
+char *git_getpass(const char *prompt)
+{
+	const char *askpass;
+	struct child_process pass;
+	const char *args[3];
+	static struct strbuf buffer = STRBUF_INIT;
+
+	askpass = getenv("GIT_ASKPASS");
+	if (!askpass)
+		askpass = askpass_program;
+	if (!askpass)
+		askpass = getenv("SSH_ASKPASS");
+	if (!askpass || !(*askpass)) {
+		char *result = getpass(prompt);
+		if (!result)
+			die_errno("Could not read password");
+		return result;
+	}
+
+	args[0] = askpass;
+	args[1]	= prompt;
+	args[2] = NULL;
+
+	memset(&pass, 0, sizeof(pass));
+	pass.argv = args;
+	pass.out = -1;
+
+	if (start_command(&pass))
+		exit(1);
+
+	strbuf_reset(&buffer);
+	if (strbuf_read(&buffer, pass.out, 20) < 0)
+		die("failed to read password from %s\n", askpass);
+
+	close(pass.out);
+
+	if (finish_command(&pass))
+		exit(1);
+
+	strbuf_setlen(&buffer, strcspn(buffer.buf, "\r\n"));
+
+	return buffer.buf;
+}
diff --git a/prompt.h b/prompt.h
new file mode 100644
index 0000000..0fd7bd9
--- /dev/null
+++ b/prompt.h
@@ -0,0 +1,6 @@
+#ifndef PROMPT_H
+#define PROMPT_H
+
+char *git_getpass(const char *prompt);
+
+#endif /* PROMPT_H */
-- 
1.7.7.4.7.g24824
Jeff King· Nov 27, 2011, 08:29 UTC · re: Jeff King · lore

[PATCH 2/6] refactor git_getpass into generic prompt function

This will allow callers to specify more options (e.g., leaving echo on). The original git_getpass becomes a slim wrapper around the new function.

Signed-off-by: Jeff King <peff@peff.net>
---
Making askpass optional isn't necessary for this series, but splitting
it actually makes the code a little cleaner, imho, and some callers
might eventually want to turn it off.
 prompt.c |   41 +++++++++++++++++++++++++----------------
 prompt.h |    3 +++
 2 files changed, 28 insertions(+), 16 deletions(-)
Show changes to 2 files +28 −16

prompt.c, prompt.h

diff --git a/prompt.c b/prompt.c
index 42a1c9f..7c8f9aa 100644
--- a/prompt.c
+++ b/prompt.c
@@ -3,26 +3,13 @@
 #include "strbuf.h"
 #include "prompt.h"
 
-char *git_getpass(const char *prompt)
+static char *do_askpass(const char *cmd, const char *prompt, const char *name)
 {
-	const char *askpass;
 	struct child_process pass;
 	const char *args[3];
 	static struct strbuf buffer = STRBUF_INIT;
 
-	askpass = getenv("GIT_ASKPASS");
-	if (!askpass)
-		askpass = askpass_program;
-	if (!askpass)
-		askpass = getenv("SSH_ASKPASS");
-	if (!askpass || !(*askpass)) {
-		char *result = getpass(prompt);
-		if (!result)
-			die_errno("Could not read password");
-		return result;
-	}
-
-	args[0] = askpass;
+	args[0] = cmd;
 	args[1]	= prompt;
 	args[2] = NULL;
 
@@ -35,7 +22,7 @@
 
 	strbuf_reset(&buffer);
 	if (strbuf_read(&buffer, pass.out, 20) < 0)
-		die("failed to read password from %s\n", askpass);
+		die("failed to read %s from %s\n", name, cmd);
 
 	close(pass.out);
 
@@ -46,3 +33,25 @@
 
 	return buffer.buf;
 }
+
+char *git_prompt(const char *prompt, const char *name, int flags)
+{
+	if (flags & PROMPT_ASKPASS) {
+		const char *askpass;
+
+		askpass = getenv("GIT_ASKPASS");
+		if (!askpass)
+			askpass = askpass_program;
+		if (!askpass)
+			askpass = getenv("SSH_ASKPASS");
+		if (askpass && *askpass)
+			return do_askpass(askpass, prompt, name);
+	}
+
+	return getpass(prompt);
+}
+
+char *git_getpass(const char *prompt)
+{
+	return git_prompt(prompt, "password", PROMPT_ASKPASS);
+}
diff --git a/prompt.h b/prompt.h
index 0fd7bd9..18868c2 100644
--- a/prompt.h
+++ b/prompt.h
@@ -1,6 +1,9 @@
 #ifndef PROMPT_H
 #define PROMPT_H
 
+#define PROMPT_ASKPASS (1<<0)
+
+char *git_prompt(const char *prompt, const char *name, int flags);
 char *git_getpass(const char *prompt);
 
 #endif /* PROMPT_H */
-- 
1.7.7.4.7.g24824
Jeff King· Nov 27, 2011, 08:30 UTC · re: Jeff King · lore

[PATCH 3/6] stub out getpass_echo function

We can't implement getpass_echo portably, but we can at least put in the infrastructure so that builds can provide a system-specific way of accomplishing this.

Right now we just fall back on calling getpass (which doesn't echo, but is available almost everywhere).

Signed-off-by: Jeff King <peff@peff.net>
---
 Makefile         |    2 ++
 compat/getpass.c |    6 ++++++
 compat/getpass.h |    6 ++++++
 3 files changed, 14 insertions(+), 0 deletions(-)
 create mode 100644 compat/getpass.c
 create mode 100644 compat/getpass.h
Show changes to 3 files +14 −0

Makefile, compat/getpass.c, compat/getpass.h

diff --git a/Makefile b/Makefile
index 14c6480..d133e2b 100644
--- a/Makefile
+++ b/Makefile
@@ -521,6 +521,7 @@ LIB_H += color.h
 LIB_H += commit.h
 LIB_H += compat/bswap.h
 LIB_H += compat/cygwin.h
+LIB_H += compat/getpass.h
 LIB_H += compat/mingw.h
 LIB_H += compat/obstack.h
 LIB_H += compat/win32/pthread.h
@@ -609,6 +610,7 @@ LIB_OBJS += cache-tree.o
 LIB_OBJS += color.o
 LIB_OBJS += combine-diff.o
 LIB_OBJS += commit.o
+LIB_OBJS += compat/getpass.o
 LIB_OBJS += compat/obstack.o
 LIB_OBJS += config.o
 LIB_OBJS += connect.o
diff --git a/compat/getpass.c b/compat/getpass.c
new file mode 100644
index 0000000..8ae82f0
--- /dev/null
+++ b/compat/getpass.c
@@ -0,0 +1,6 @@
+#include "../git-compat-util.h"
+
+char *getpass_echo(const char *prompt)
+{
+	return getpass(prompt);
+}
diff --git a/compat/getpass.h b/compat/getpass.h
new file mode 100644
index 0000000..5f1986b
--- /dev/null
+++ b/compat/getpass.h
@@ -0,0 +1,6 @@
+#ifndef GETPASS_H
+#define GETPASS_H
+
+char *getpass_echo(const char *prompt);
+
+#endif /* GETPASS_H */
-- 
1.7.7.4.7.g24824
Jeff King· Nov 27, 2011, 08:30 UTC · re: Jeff King · lore

[PATCH 4/6] prompt: add PROMPT_ECHO flag

This will use getpass_echo when set.
Signed-off-by: Jeff King <peff@peff.net>
---
 prompt.c |    5 ++++-
 prompt.h |    1 +
 2 files changed, 5 insertions(+), 1 deletions(-)
Show changes to 2 files +5 −1

prompt.c, prompt.h

diff --git a/prompt.c b/prompt.c
index 7c8f9aa..c46227f 100644
--- a/prompt.c
+++ b/prompt.c
@@ -2,6 +2,7 @@
 #include "run-command.h"
 #include "strbuf.h"
 #include "prompt.h"
+#include "compat/getpass.h"
 
 static char *do_askpass(const char *cmd, const char *prompt, const char *name)
 {
@@ -48,7 +49,9 @@
 			return do_askpass(askpass, prompt, name);
 	}
 
-	return getpass(prompt);
+	return flags & PROMPT_ECHO ?
+		getpass_echo(prompt) :
+		getpass(prompt);
 }
 
 char *git_getpass(const char *prompt)
diff --git a/prompt.h b/prompt.h
index 18868c2..7201cae 100644
--- a/prompt.h
+++ b/prompt.h
@@ -2,6 +2,7 @@
 #define PROMPT_H
 
 #define PROMPT_ASKPASS (1<<0)
+#define PROMPT_ECHO    (1<<1)
 
 char *git_prompt(const char *prompt, const char *name, int flags);
 char *git_getpass(const char *prompt);
-- 
1.7.7.4.7.g24824
Jeff King· Nov 27, 2011, 08:31 UTC · re: Jeff King · lore

[PATCH 5/6] credential: use git_prompt instead of git_getpass

When we request a username and password from the user on the terminal, we can't use stdin/stdout, because they may be connected to pipes to other git processes.

Instead, we use getpass() (via git_getpass), which will generally open /dev/tty and read and write from that. The only problem is that getpass is meant to take passwords, and therefore will not echo characters from the username, which is annoying.

Now that git_prompt understand the "echo" flag, we can use that to let the user see their username as they type it.

Signed-off-by: Jeff King <peff@peff.net>
---
 credential.c |   15 +++++++--------
 1 files changed, 7 insertions(+), 8 deletions(-)
Show changes to credential.c +7 −8
diff --git a/credential.c b/credential.c
index d918ba5..9ad580d 100644
--- a/credential.c
+++ b/credential.c
@@ -109,7 +109,8 @@ static void credential_describe(struct credential *c, struct strbuf *out)
 		strbuf_addf(out, "/%s", c->path);
 }
 
-static char *credential_ask_one(const char *what, struct credential *c)
+static char *credential_ask_one(const char *what, struct credential *c,
+				int flags)
 {
 	struct strbuf desc = STRBUF_INIT;
 	struct strbuf prompt = STRBUF_INIT;
@@ -121,11 +122,7 @@ static void credential_describe(struct credential *c, struct strbuf *out)
 	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);
+	r = git_prompt(prompt.buf, what, flags);
 
 	strbuf_release(&desc);
 	strbuf_release(&prompt);
@@ -135,9 +132,11 @@ static void credential_describe(struct credential *c, struct strbuf *out)
 static void credential_getpass(struct credential *c)
 {
 	if (!c->username)
-		c->username = credential_ask_one("Username", c);
+		c->username = credential_ask_one("Username", c,
+						 PROMPT_ASKPASS|PROMPT_ECHO);
 	if (!c->password)
-		c->password = credential_ask_one("Password", c);
+		c->password = credential_ask_one("Password", c,
+						 PROMPT_ASKPASS);
 }
 
 int credential_read(struct credential *c, FILE *fp)
-- 
1.7.7.4.7.g24824
Jeff King· Nov 27, 2011, 08:31 UTC · re: Jeff King · lore

[PATCH 6/6] compat/getpass: add a /dev/tty implementation

This is more or less what regular getpass() does, but without turning off character echoing. You have to set HAVE_DEV_TTY to enable it.

For now, only Linux enables this by default. People on other /dev/tty-enabled systems can submit patches to turn it on once they have tested it.

Signed-off-by: Jeff King <peff@peff.net>
---
 Makefile         |    8 ++++++++
 compat/getpass.c |   35 +++++++++++++++++++++++++++++++++++
 2 files changed, 43 insertions(+), 0 deletions(-)
Show changes to 2 files +43 −0

Makefile, compat/getpass.c

diff --git a/Makefile b/Makefile
index d133e2b..a2afe31 100644
--- a/Makefile
+++ b/Makefile
@@ -227,6 +227,9 @@ all::
 #
 # Define NO_REGEX if you have no or inferior regex support in your C library.
 #
+# Define HAVE_DEV_TTY if your system can open /dev/tty to interact with the
+# user.
+#
 # Define GETTEXT_POISON if you are debugging the choice of strings marked
 # for translation.  In a GETTEXT_POISON build, you can turn all strings marked
 # for translation into gibberish by setting the GIT_GETTEXT_POISON variable
@@ -835,6 +838,7 @@ ifeq ($(uname_S),Linux)
 	NO_STRLCPY = YesPlease
 	NO_MKSTEMPS = YesPlease
 	HAVE_PATHS_H = YesPlease
+	HAVE_DEV_TTY = YesPlease
 endif
 ifeq ($(uname_S),GNU/kFreeBSD)
 	NO_STRLCPY = YesPlease
@@ -1641,6 +1645,10 @@ ifdef HAVE_PATHS_H
 	BASIC_CFLAGS += -DHAVE_PATHS_H
 endif
 
+ifdef HAVE_DEV_TTY
+	BASIC_CFLAGS += -DHAVE_DEV_TTY
+endif
+
 ifdef DIR_HAS_BSD_GROUP_SEMANTICS
 	COMPAT_CFLAGS += -DDIR_HAS_BSD_GROUP_SEMANTICS
 endif
diff --git a/compat/getpass.c b/compat/getpass.c
index 8ae82f0..b5bd1dd 100644
--- a/compat/getpass.c
+++ b/compat/getpass.c
@@ -1,6 +1,41 @@
 #include "../git-compat-util.h"
 
+#ifdef HAVE_DEV_TTY
+
+char *getpass_echo(const char *prompt)
+{
+	static char buf[1024];
+	FILE *fh;
+	int i;
+
+	fh = fopen("/dev/tty", "w+");
+	if (!fh)
+		return NULL;
+
+	fputs(prompt, fh);
+	fflush(fh);
+	for (i = 0; i < sizeof(buf) - 1; i++) {
+		int ch = getc(fh);
+		if (ch == EOF || ch == '\n')
+			break;
+		buf[i] = ch;
+	}
+	buf[i] = '\0';
+
+	if (ferror(fh)) {
+		fclose(fh);
+		return NULL;
+	}
+
+	fclose(fh);
+	return buf;
+}
+
+#else
+
 char *getpass_echo(const char *prompt)
 {
 	return getpass(prompt);
 }
+
+#endif
-- 
1.7.7.4.7.g24824
Junio C Hamano· Nov 27, 2011, 08:56 UTC · re: Jeff King · lore

Re: [PATCH 0/6] echo usernames as they are typed

Jeff King <peff@peff.net> writes:
Show 8 quoted lines
> On Thu, Nov 24, 2011 at 05:58:01AM -0500, Jeff King wrote:
>
>> Here's a revised version of the http-auth / credential-helper series.
>
> And here's something I've been meaning to do on top: actually echo
> characters at the username prompt. We can't do this portably, but we can
> at least stub out a compatibility layer and let each system do something
> sensible.
Yay ;-)
Erik Faye-Lund· Nov 27, 2011, 09:17 UTC · re: Jeff King · lore

Re: [PATCH 0/6] echo usernames as they are typed

On Sun, Nov 27, 2011 at 9:27 AM, Jeff King <peff@peff.net> wrote:
Show 17 quoted lines
> On Thu, Nov 24, 2011 at 05:58:01AM -0500, Jeff King wrote:
>
>> Here's a revised version of the http-auth / credential-helper series.
>
> And here's something I've been meaning to do on top: actually echo
> characters at the username prompt. We can't do this portably, but we can
> at least stub out a compatibility layer and let each system do something
> sensible.
>
>  [1/6]: move git_getpass to its own source file
>  [2/6]: refactor git_getpass into generic prompt function
>  [3/6]: stub out getpass_echo function
>  [4/6]: prompt: add PROMPT_ECHO flag
>  [5/6]: credential: use git_prompt instead of git_getpass
>  [6/6]: compat/getpass: add a /dev/tty implementation
>
> -Peff

Interesting, I've been working on something pretty similar: getting rid of getpass usage all together:

https://github.com/kusma/git/tree/work/askpass

My reason to write a getpass replacement was to avoid capping input to PASS_MAX, which can be as low as 8 characters (and AFAIK is just that on Solaris)...

Jeff King· Nov 28, 2011, 03:53 UTC · re: Erik Faye-Lund · lore

Re: [PATCH 0/6] echo usernames as they are typed

On Sun, Nov 27, 2011 at 10:17:26AM +0100, Erik Faye-Lund wrote:
Show 13 quoted lines
> > And here's something I've been meaning to do on top: actually echo
> > characters at the username prompt. We can't do this portably, but we can
> > at least stub out a compatibility layer and let each system do something
> > sensible.
>
> Interesting, I've been working on something pretty similar: getting
> rid of getpass usage all together:
> 
> https://github.com/kusma/git/tree/work/askpass
> 
> My reason to write a getpass replacement was to avoid capping input to
> PASS_MAX, which can be as low as 8 characters (and AFAIK is just that
> on Solaris)...

Yeah, if there are really bad getpass implementations, we would want to work around them. If we are going to do so, it might make sense to combine the effort with my getpass_echo wrapper, as they are really the same function, modulo tweaking the echo settings.

It would also be nice to make getpass a little more predictable. If /dev/tty can't be opened, glibc's getpass will fall back to writing the prompt to stderr and reading the password from stdin. But we definitely don't want to do that in git-remote-curl, where stdin is already talking a special protocol with the parent fetch process.

So I think it might be best to just write our own getpass. However, your implementation looks wrong to me:

Show 21 quoted lines
> diff --git a/password.c b/password.c
> new file mode 100644
> index 0000000..727ed84
> --- /dev/null
> +++ b/password.c
> @@ -0,0 +1,94 @@
> +#include "cache.h"
> +#include "git-compat-util.h"
> +#include "strbuf.h"
> +#include "run-command.h"
> +
> +#ifndef WIN32
> +
> +static void ask_password(struct strbuf *sb, const char *prompt)
> +{
> +	struct termios attr;
> +	tcflag_t c_lflag;
> +
> +	if (tcgetattr(1, &attr) < 0)
> +		die("tcgetattr failed!");
> +	c_lflag = attr.c_lflag;

This is getting the terminal attributes for stdout. But in many cases, stdout will not be connected to the terminal (in particular, remote-curl, as I mentioned above, will have its stdio connected to the parent fetch process). Stderr is a better guess, as you do here:

> +	fputs(prompt, stderr);
but even that is not foolproof. With getpass(), this should work:
  git clone ... 2>errors
with the prompt going to the terminal. But it doesn't with your patch.

You really want to open "/dev/tty" on most Unix systems (which is what getpass() does). I have no idea what would be appropriate on Windows.

Show 6 quoted lines
> +	for (;;) {
> +		int c = getchar();
> +		if (c == EOF || c == '\n')
> +			break;
> +		strbuf_addch(sb, c);
> +	}

And this is even worse. You're reading from stdin, which will get whatever cruft is in the pipe coming from the parent process (or may even cause a hang, as the parent is probably blocking waiting to read from the child helper).

-Peff
Erik Faye-Lund· Nov 28, 2011, 09:36 UTC · re: Jeff King · lore

Re: [PATCH 0/6] echo usernames as they are typed

On Mon, Nov 28, 2011 at 4:53 AM, Jeff King <peff@peff.net> wrote:
Show 21 quoted lines
> On Sun, Nov 27, 2011 at 10:17:26AM +0100, Erik Faye-Lund wrote:
>
>> > And here's something I've been meaning to do on top: actually echo
>> > characters at the username prompt. We can't do this portably, but we can
>> > at least stub out a compatibility layer and let each system do something
>> > sensible.
>>
>> Interesting, I've been working on something pretty similar: getting
>> rid of getpass usage all together:
>>
>> https://github.com/kusma/git/tree/work/askpass
>>
>> My reason to write a getpass replacement was to avoid capping input to
>> PASS_MAX, which can be as low as 8 characters (and AFAIK is just that
>> on Solaris)...
>
> Yeah, if there are really bad getpass implementations, we would want to
> work around them. If we are going to do so, it might make sense to
> combine the effort with my getpass_echo wrapper, as they are really the
> same function, modulo tweaking the echo settings.
>
My thinking exactly ;)
Show 9 quoted lines
> It would also be nice to make getpass a little more predictable. If
> /dev/tty can't be opened, glibc's getpass will fall back to writing the
> prompt to stderr and reading the password from stdin. But we definitely
> don't want to do that in git-remote-curl, where stdin is already talking
> a special protocol with the parent fetch process.
>
> So I think it might be best to just write our own getpass. However,
> your implementation looks wrong to me:
>
It probably is, yes. It was a very naive attempt ;)
Show 37 quoted lines
>> diff --git a/password.c b/password.c
>> new file mode 100644
>> index 0000000..727ed84
>> --- /dev/null
>> +++ b/password.c
>> @@ -0,0 +1,94 @@
>> +#include "cache.h"
>> +#include "git-compat-util.h"
>> +#include "strbuf.h"
>> +#include "run-command.h"
>> +
>> +#ifndef WIN32
>> +
>> +static void ask_password(struct strbuf *sb, const char *prompt)
>> +{
>> +     struct termios attr;
>> +     tcflag_t c_lflag;
>> +
>> +     if (tcgetattr(1, &attr) < 0)
>> +             die("tcgetattr failed!");
>> +     c_lflag = attr.c_lflag;
>
> This is getting the terminal attributes for stdout. But in many
> cases, stdout will not be connected to the terminal (in particular,
> remote-curl, as I mentioned above, will have its stdio connected to the
> parent fetch process).  Stderr is a better guess, as you do here:
>
>> +     fputs(prompt, stderr);
>
> but even that is not foolproof. With getpass(), this should work:
>
>  git clone ... 2>errors
>
> with the prompt going to the terminal. But it doesn't with your patch.
>
> You really want to open "/dev/tty" on most Unix systems (which is what
> getpass() does).

Yes, you're right. Opening "/dev/tty" is much better. But what happens for processes started by GUI applications (with no easily observable tty, if any)? Does open simply fail? If so, is it desirable for us to fail in that case?

> I have no idea what would be appropriate on Windows.
>

It's pretty similar, but not exactly: CreateFile("CONIN$", ...) or CreateFile("CONOUT$", ...), depending on if you want the read-handle or the write-handle... I can probably cook up something a bit more concrete, though.

But _getch() that we already use always reads from the console (according to MSDN, I haven't actually tested this myself: http://msdn.microsoft.com/en-us/library/078sfkak%28v=VS.80%29.aspx). But I don't think this allows us to fail when no console is attached. Question is, should we fail in such cases? Windows does have an API to prompt for passwords in a GUI window... Perhaps fallbacking to those are the way to go? Something like:

if (GetConsoleWindow()) {
	/*  normal console-stuff */
} else {
	/* call CredUIPromptForWindowsCredentials(...) instead */
}

This might be nicer towards GUI tools, but it requires us to explicitly ask for a password (and possibly a username at the same time)...

Show 12 quoted lines
>> +     for (;;) {
>> +             int c = getchar();
>> +             if (c == EOF || c == '\n')
>> +                     break;
>> +             strbuf_addch(sb, c);
>> +     }
>
> And this is even worse. You're reading from stdin, which will get
> whatever cruft is in the pipe coming from the parent process (or may
> even cause a hang, as the parent is probably blocking waiting to read
> from the child helper).
>
Yeah, thanks for pointing these glitches out ;)
Jeff King· Nov 28, 2011, 11:31 UTC · re: Erik Faye-Lund · lore

Re: [PATCH 0/6] echo usernames as they are typed

On Mon, Nov 28, 2011 at 10:36:21AM +0100, Erik Faye-Lund wrote:
Show 7 quoted lines
> > You really want to open "/dev/tty" on most Unix systems (which is what
> > getpass() does).
> 
> Yes, you're right. Opening "/dev/tty" is much better. But what happens
> for processes started by GUI applications (with no easily observable
> tty, if any)? Does open simply fail? If so, is it desirable for us to
> fail in that case?
Yes, the open will fail (on Linux, I get ENXIO).

And yes, we should fail in that case. getpass() will generally return NULL in that instance, and the current implementation of git_getpass() will die(), explaining that we could not get the password.

Show 6 quoted lines
> > I have no idea what would be appropriate on Windows.
> 
> It's pretty similar, but not exactly: CreateFile("CONIN$", ...) or
> CreateFile("CONOUT$", ...), depending on if you want the read-handle
> or the write-handle... I can probably cook up something a bit more
> concrete, though.

OK, that maps to the /dev/tty concept quite well. Though I suspect the code for turning off character echo-ing is going to also be different.

Show 13 quoted lines
> But _getch() that we already use always reads from the console
> (according to MSDN, I haven't actually tested this myself:
> http://msdn.microsoft.com/en-us/library/078sfkak%28v=VS.80%29.aspx).
> But I don't think this allows us to fail when no console is attached.
> Question is, should we fail in such cases? Windows does have an API to
> prompt for passwords in a GUI window... Perhaps fallbacking to those
> are the way to go? Something like:
> 
> if (GetConsoleWindow()) {
> 	/*  normal console-stuff */
> } else {
> 	/* call CredUIPromptForWindowsCredentials(...) instead */
> }

Certainly on non-Windows something like that would not be welcome. The user can already have specified GIT_ASKPASS if they don't have a terminal. And once the credential-helper code is in, they can use a platform-specific helper that provides a nice dialog if they want it.

So I would say trying to do something graphical would be surprising and unwelcome. But then, I am a very Unix-y kind of guy. Maybe on Windows something like that would be more favorable. I'll leave that decision to people who know more.

-Peff
Frans Klaver· Nov 28, 2011, 11:49 UTC · re: Jeff King · lore

Re: [PATCH 0/6] echo usernames as they are typed

On Mon, Nov 28, 2011 at 12:31 PM, Jeff King <peff@peff.net> wrote:
Show 9 quoted lines
> Certainly on non-Windows something like that would not be welcome. The
> user can already have specified GIT_ASKPASS if they don't have a
> terminal. And once the credential-helper code is in, they can use a
> platform-specific helper that provides a nice dialog if they want it.
>
> So I would say trying to do something graphical would be surprising and
> unwelcome. But then, I am a very Unix-y kind of guy. Maybe on Windows
> something like that would be more favorable. I'll leave that decision to
> people who know more.

I would say that also on windows it would be surprising if you are working on the command line and suddenly a pop-up appears asking for input. So even on windows you should probably keep away from gui stuff in a cli tool, although there are tools that don't.

Frans
Erik Faye-Lund· Nov 28, 2011, 12:59 UTC · re: Jeff King · lore

Re: [PATCH 0/6] echo usernames as they are typed

On Mon, Nov 28, 2011 at 12:31 PM, Jeff King <peff@peff.net> wrote:
Show 45 quoted lines
> On Mon, Nov 28, 2011 at 10:36:21AM +0100, Erik Faye-Lund wrote:
>
>> > You really want to open "/dev/tty" on most Unix systems (which is what
>> > getpass() does).
>>
>> Yes, you're right. Opening "/dev/tty" is much better. But what happens
>> for processes started by GUI applications (with no easily observable
>> tty, if any)? Does open simply fail? If so, is it desirable for us to
>> fail in that case?
>
> Yes, the open will fail (on Linux, I get ENXIO).
>
> And yes, we should fail in that case. getpass() will generally return
> NULL in that instance, and the current implementation of git_getpass()
> will die(), explaining that we could not get the password.
>
>> > I have no idea what would be appropriate on Windows.
>>
>> It's pretty similar, but not exactly: CreateFile("CONIN$", ...) or
>> CreateFile("CONOUT$", ...), depending on if you want the read-handle
>> or the write-handle... I can probably cook up something a bit more
>> concrete, though.
>
> OK, that maps to the /dev/tty concept quite well. Though I suspect the
> code for turning off character echo-ing is going to also be different.
>
>> But _getch() that we already use always reads from the console
>> (according to MSDN, I haven't actually tested this myself:
>> http://msdn.microsoft.com/en-us/library/078sfkak%28v=VS.80%29.aspx).
>> But I don't think this allows us to fail when no console is attached.
>> Question is, should we fail in such cases? Windows does have an API to
>> prompt for passwords in a GUI window... Perhaps fallbacking to those
>> are the way to go? Something like:
>>
>> if (GetConsoleWindow()) {
>>       /*  normal console-stuff */
>> } else {
>>       /* call CredUIPromptForWindowsCredentials(...) instead */
>> }
>
> Certainly on non-Windows something like that would not be welcome. The
> user can already have specified GIT_ASKPASS if they don't have a
> terminal. And once the credential-helper code is in, they can use a
> platform-specific helper that provides a nice dialog if they want it.
>

Yes, that's certainly cleaner implementation-wise. But didn't you change it to only do the storage-part in the last round, or did I misunderstand the updated series?

> So I would say trying to do something graphical would be surprising and
> unwelcome. But then, I am a very Unix-y kind of guy. Maybe on Windows
> something like that would be more favorable. I'll leave that decision to
> people who know more.

Windows doesn't really have that strict norms when it comes to console applications, but it'd be nice if it didn't do anything obviously wrong when the GUI isn't available (non-interactive sessions, PowerShell remote commands, CopSSH, etc). So I guess this is yet another argument to stay with the credential-helper instead, if possible...

Jeff King· Nov 28, 2011, 18:59 UTC · re: Erik Faye-Lund · lore

Re: [PATCH 0/6] echo usernames as they are typed

On Mon, Nov 28, 2011 at 01:59:34PM +0100, Erik Faye-Lund wrote:
Show 9 quoted lines
> > Certainly on non-Windows something like that would not be welcome. The
> > user can already have specified GIT_ASKPASS if they don't have a
> > terminal. And once the credential-helper code is in, they can use a
> > platform-specific helper that provides a nice dialog if they want it.
> >
> 
> Yes, that's certainly cleaner implementation-wise. But didn't you
> change it to only do the storage-part in the last round, or did I
> misunderstand the updated series?

Yeah, sorry, I'm getting ahead of myself. I left room in the spec for an "ask" operation on helpers, but I haven't implemented it yet.

-Peff

← back to recent threads