From: Jeff King <hidden> Date: 2016-06-15 22:52:31
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
From: Jeff King <hidden> Date: 2016-06-15 22:52:31
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 <redacted>
---
t/test-lib.sh | 5 +++++
1 files changed, 5 insertions(+), 0 deletions(-)
@@ -379,6 +379,11 @@ test_config () {gitconfig"$@"}+test_config_global(){+test_when_finished"test_unconfig --global '$1'"&&+gitconfig--global"$@"+}+# Use test_set_prereq to tell that a particular prerequisite is available.# The prerequisite can later be checked for in two ways:#
From: Jeff King <hidden> Date: 2016-06-15 22:52:31
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 <redacted>
---
t/t5550-http-fetch.sh | 2 +-
1 files changed, 1 insertions(+), 1 deletions(-)
@@ -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&&-echowrong>askpass-reponse&&+echowrong>askpass-response&&gitclone"$HTTPD_URL_USER_PASS/auth/repo.git"clone-auth-none&&test_cmpaskpass-expect-noneaskpass-query'
From: Jeff King <hidden> Date: 2016-06-15 22:52:31
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 <redacted>
---
.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
@@ -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).
@@ -0,0 +1,242 @@+#include"cache.h"+#include"credential.h"+#include"string-list.h"+#include"run-command.h"++voidcredential_init(structcredential*c)+{+memset(c,0,sizeof(*c));+c->helpers.strdup_strings=1;+}++voidcredential_clear(structcredential*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);+}++staticvoidcredential_describe(structcredential*c,structstrbuf*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);+}++staticchar*credential_ask_one(constchar*what,structcredential*c)+{+structstrbufdesc=STRBUF_INIT;+structstrbufprompt=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+*actuallyechoesthecharacters.However,weneedtoreadfrom+*/dev/ttyandnotstdio,whichisnotportable(butgetpasswilldo+*itforus).http.cusesthesameworkaround.*/+r=git_getpass(prompt.buf);++strbuf_release(&desc);+strbuf_release(&prompt);+returnxstrdup(r);+}++staticvoidcredential_getpass(structcredential*c)+{+if(!c->username)+c->username=credential_ask_one("Username",c);+if(!c->password)+c->password=credential_ask_one("Password",c);+}++intcredential_read(structcredential*c,FILE*fp)+{+structstrbufline=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);+}+elseif(!strcmp(key,"password")){+free(c->password);+c->password=xstrdup(value);+}+elseif(!strcmp(key,"protocol")){+free(c->protocol);+c->protocol=xstrdup(value);+}+elseif(!strcmp(key,"host")){+free(c->host);+c->host=xstrdup(value);+}+elseif(!strcmp(key,"path")){+free(c->path);+c->path=xstrdup(value);+}+/* ignore other lines; we don't know what they mean, but+*thisfuture-proofsuswhenlaterversionsofgitdo+*learnnewlines,andthehelpersareupdatedtomatch*/+}++strbuf_release(&line);+return0;+}++staticvoidcredential_write_item(FILE*fp,constchar*key,constchar*value)+{+if(!value)+return;+fprintf(fp,"%s=%s\n",key,value);+}++staticvoidcredential_write(conststructcredential*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);+}++staticintfirst_word_is_alnum(constchar*s)+{+for(;*s&&*s!=' ';s++)+if(!isalnum(*s))+return0;+return1;+}++staticintrun_credential_helper(structcredential*c,+constchar*cmd,+intwant_output)+{+structchild_processhelper;+constchar*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){+intr;+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;+return0;+}++staticintcredential_do(structcredential*c,constchar*method,+constchar*operation)+{+structstrbufcmd=STRBUF_INIT;+intr;++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);+returnr;+}++voidcredential_fill(structcredential*c)+{+inti;++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");+}++voidcredential_approve(structcredential*c)+{+inti;++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;+}++voidcredential_reject(structcredential*c)+{+inti;++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;+}
@@ -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>stdout2>stderr&&+test_cmpexpect-stdoutstdout&&+test_cmpexpect-stderrstderr+}++read_chunk(){+whilereadline;do+case"$line"in+--)break;;+*)echo"$line";;+esac+done+}+++cat>askpass<<\EOF+#!/bin/sh+echo>&2askpass:$*+what=`echo$1|cut-d" "-f1|trA-Za-z|tr-cda-z`+echo"askpass-$what"+EOF+chmod+xaskpass+GIT_ASKPASS="$PWD/askpass"+exportGIT_ASKPASS
@@ -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|seds/.*git-credential-//`+echo>&2"$whoami: $*"+whileIFS==readkeyvalue;do+echo>&2"$whoami: $key=$value"+eval"$key=$value"+done+EOF++cat>git-credential-useless<<-\EOF&&+#!/bin/sh+../dump+exit0+EOF+chmod+xgit-credential-useless&&++cat>git-credential-verbatim<<-\EOF&&+#!/bin/sh+user=$1;shift+pass=$1;shift+../dump+test-z"$user"||echousername=$user+test-z"$pass"||echopassword=$pass+EOF+chmod+xgit-credential-verbatim&&++PATH="$PWD:$PATH"+'++test_expect_success'credential_fill invokes helper''+checkfill"verbatim foo bar"<<-\EOF+--+username=foo+password=bar+--+verbatim:get+EOF+'++test_expect_success'credential_fill invokes multiple helpers''+checkfilluseless"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''+checkfill"verbatim one two""verbatim three four"<<-\EOF+--+username=one+password=two+--+verbatim:get+EOF+'++test_expect_success'credential_fill continues through partial response''+checkfill"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''+checkfill"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''+checkapproveuseless"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''+checkapproveuseless<<-\EOF+username=foo+--+--+EOF+'+++test_expect_success'credential_reject calls all helpers''+checkrejectuseless"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''+checkfill"verbatim \"\" three"<<-\EOF+username=one+--+username=one+password=three+--+verbatim:get+verbatim:username=one+EOF+'++test_expect_success'usernames can be overridden''+checkfill"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''+checkfill"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''+checkfill<<-\EOF+--+username=askpass-username+password=askpass-password+--+askpass:Username:+askpass:Password:+EOF+'++test_expect_success'internal getpass does not ask for known username''+checkfill<<-\EOF+username=foo+--+username=foo+password=askpass-password+--+askpass:Password:+EOF+'++test_done
From: Jeff King <hidden> Date: 2016-06-15 22:52:31
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 <redacted>
---
Documentation/technical/api-credentials.txt | 3 ++
credential.c | 52 +++++++++++++++++++++++++++
credential.h | 1 +
3 files changed, 56 insertions(+), 0 deletions(-)
@@ -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 --------------------------
@@ -240,3 +241,54 @@ void credential_reject(struct credential *c)c->password=NULL;c->approved=0;}++voidcredential_from_url(structcredential*c,constchar*url)+{+constchar*at,*colon,*cp,*slash,*host,*proto_end;++credential_clear(c);++/*+*Matchoneof:+*(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;+}+elseif(!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';+}+}
From: Jeff King <hidden> Date: 2016-06-15 22:52:31
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 <redacted>
---
http.c | 113 +++++++++++--------------------------------------
t/t5550-http-fetch.sh | 38 ++++++++++++-----
2 files changed, 52 insertions(+), 99 deletions(-)
@@ -244,18 +224,14 @@ static void init_curl_http_auth(CURL *result)staticinthas_cert_password(void){-if(ssl_cert_password!=NULL)-return1;if(ssl_cert==NULL||ssl_cert_password_required!=1)return0;-/* 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);-return1;-}else-return0;+if(!cert_auth.password){+cert_auth.protocol=xstrdup("cert");+cert_auth.path=xstrdup(ssl_cert);+credential_fill(&cert_auth);+}+return1;}staticCURL*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 >= 0x070903if(ssl_key!=NULL)curl_easy_setopt(result,CURLOPT_SSLKEY,ssl_key);
@@ -324,42 +300,6 @@ static int has_cert_password(void)returnresult;}-staticvoidhttp_auth_init(constchar*url)-{-constchar*at,*colon,*cp,*slash,*host;--cp=strstr(url,"://");-if(!cp)-return;--/*-*Ok,theURLlookslike"proto://something".Whichone?-*"proto://<user>:<pass>@<host>/...",-*"proto://<user>@<host>/...",orjust-*"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;-}-elseif(!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);-}-staticvoidset_from_env(constchar**var,constchar*envname){constchar*val=getenv(envname);
@@ -836,17 +776,11 @@ static int http_request(const char *url, void *result, int target, int options)elseif(missing_target(&results))ret=HTTP_MISSING_TARGET;elseif(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_getpassisneededherebecauseitsverylikelystdin/stdoutare-*pipestoourparentprocess.Soweinsteadneedtouse/dev/tty,-*butthatisnon-portable.Usinggit_getpass()canatleastbestubbed-*onotherplatformswithadifferentimplementationif/whennecessary.-*/-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);+returnret;}
@@ -49,40 +49,56 @@ test_expect_success 'setup askpass helpers' 'EOFchmod+xaskpass&&GIT_ASKPASS="$PWD/askpass"&&-exportGIT_ASKPASS&&->askpass-expect-none&&-echo"askpass: Password for '\''$HTTPD_DEST'\'': ">askpass-expect-pass&&-{echo"askpass: Username for '\''$HTTPD_DEST'\'': "&&-cataskpass-expect-pass-}>askpass-expect-both-'+exportGIT_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_cmpaskpass-expectaskpass-query+} test_expect_success'cloning password-protected repository can fail''>askpass-query&&echowrong>askpass-response&&test_must_failgitclone"$HTTPD_URL/auth/repo.git"clone-auth-fail&&-test_cmpaskpass-expect-bothaskpass-query+expect_askpassbothwrong' test_expect_success'http auth can use user/pass in URL''>askpass-query&&echowrong>askpass-response&&gitclone"$HTTPD_URL_USER_PASS/auth/repo.git"clone-auth-none&&-test_cmpaskpass-expect-noneaskpass-query+expect_askpassnone' test_expect_success'http auth can use just user in URL''>askpass-query&&echouser@host>askpass-response&&gitclone"$HTTPD_URL_USER/auth/repo.git"clone-auth-pass&&-test_cmpaskpass-expect-passaskpass-query+expect_askpasspassuser@host' test_expect_success'http auth can request both user and pass''>askpass-query&&echouser@host>askpass-response&&gitclone"$HTTPD_URL/auth/repo.git"clone-auth-both&&-test_cmpaskpass-expect-bothaskpass-query+expect_askpassbothuser@host' test_expect_success'fetch changes via http''
From: Jeff King <hidden> Date: 2016-06-15 22:52:31
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 <redacted>
---
credential.c | 61 ++++++++++++++++++++++++++++++++++++++++++++++++
credential.h | 5 +++-
t/t0300-credentials.sh | 42 +++++++++++++++++++++++++++++++++
t/t5550-http-fetch.sh | 12 +++++++++
4 files changed, 119 insertions(+), 1 deletions(-)
@@ -192,4 +192,46 @@ test_expect_success 'internal getpass does not ask for known username' 'EOF'+HELPER="f() {+cat>/dev/null+echousername=foo+echopassword=bar+};f"+test_expect_success'respect configured credentials''+test_configcredential.helper"$HELPER"&&+checkfill<<-\EOF+--+username=foo+password=bar+--+EOF+'++test_expect_success'match configured credential''+test_configcredential.https://example.com.helper"$HELPER"&&+checkfill<<-\EOF+protocol=https+host=example.com+path=repo.git+--+username=foo+password=bar+--+EOF+'++test_expect_success'do not match configured credential''+test_configcredential.https://foo.helper"$HELPER"&&+checkfill<<-\EOF+protocol=https+host=bar+--+username=askpass-username+password=askpass-password+--+askpass:Usernamefor'\''https://bar'\'':+askpass:Passwordfor'\''https://askpass-username@bar'\'':+EOF+'+ test_done
@@ -101,6 +101,18 @@ test_expect_success 'http auth can request both user and pass' 'expect_askpassbothuser@host'+test_expect_success'http auth respects credential helper config''+test_config_globalcredential.helper"f() {+cat>/dev/null+echousername=user@host+echopassword=user@host+};f" &&+>askpass-query&&+echowrong>askpass-response&&+gitclone"$HTTPD_URL/auth/repo.git"clone-auth-helper&&+expect_askpassnone+'+ test_expect_success'fetch changes via http''echocontent>>file&&gitcommit-a-mtwo&&
From: Jeff King <hidden> Date: 2016-06-15 22:52:31
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 <redacted>
---
credential.c | 4 ++++
t/t0300-credentials.sh | 13 +++++++++++++
t/t5550-http-fetch.sh | 16 ++++++++++++++++
3 files changed, 33 insertions(+), 0 deletions(-)
@@ -234,4 +234,17 @@ test_expect_success 'do not match configured credential' 'EOF'+test_expect_success'pull username from config''+test_configcredential.https://example.com.usernamefoo&&+checkfill<<-\EOF+protocol=https+host=example.com+--+username=foo+password=askpass-password+--+askpass:Passwordfor'\''https://foo@example.com'\'':+EOF+'+ test_done
@@ -113,6 +113,22 @@ test_expect_success 'http auth respects credential helper config' 'expect_askpassnone'+test_expect_success'http auth can get username from config''+test_config_global"credential.$HTTPD_URL.username"user@host&&+>askpass-query&&+echouser@host>askpass-response&&+gitclone"$HTTPD_URL/auth/repo.git"clone-auth-user&&+expect_askpasspassuser@host+'++test_expect_success'configured username does not override URL''+test_config_global"credential.$HTTPD_URL.username"wrong&&+>askpass-query&&+echouser@host>askpass-response&&+gitclone"$HTTPD_URL_USER/auth/repo.git"clone-auth-user2&&+expect_askpasspassuser@host+'+ test_expect_success'fetch changes via http''echocontent>>file&&gitcommit-a-mtwo&&
From: Jeff King <hidden> Date: 2016-06-15 22:52:31
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 <redacted>
---
credential.c | 14 ++++++++++++++
credential.h | 3 ++-
t/t0300-credentials.sh | 29 +++++++++++++++++++++++++++++
t/t5550-http-fetch.sh | 2 +-
4 files changed, 46 insertions(+), 2 deletions(-)
@@ -247,4 +247,33 @@ test_expect_success 'pull username from config' 'EOF'+test_expect_success'http paths can be part of context''+checkfill"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_configcredential.https://example.com.useHttpPathtrue&&+checkfill"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
From: Jeff King <hidden> Date: 2016-06-15 22:52:31
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 <redacted>
---
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
@@ -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::
@@ -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
From: Jeff King <hidden> Date: 2016-06-15 22:52:31
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 <redacted>
---
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
@@ -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
@@ -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
@@ -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.
@@ -0,0 +1,269 @@+#include"cache.h"+#include"credential.h"+#include"unix-socket.h"+#include"sigchain.h"++staticconstchar*socket_path;++staticvoidcleanup_socket(void)+{+if(socket_path)+unlink(socket_path);+}++staticvoidcleanup_socket_on_signal(intsig)+{+cleanup_socket();+sigchain_pop(sig);+raise(sig);+}++structcredential_cache_entry{+structcredentialitem;+unsignedlongexpiration;+};+staticstructcredential_cache_entry*entries;+staticintentries_nr;+staticintentries_alloc;++staticvoidcache_credential(structcredential*c,inttimeout)+{+structcredential_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;+}++staticstructcredential_cache_entry*lookup_credential(conststructcredential*c)+{+inti;+for(i=0;i<entries_nr;i++){+structcredential*e=&entries[i].item;+if(credential_match(c,e))+return&entries[i];+}+returnNULL;+}++staticvoidremove_credential(conststructcredential*c)+{+structcredential_cache_entry*e;++e=lookup_credential(c);+if(e)+e->expiration=0;+}++staticintcheck_expirations(void)+{+staticunsignedlongwait_for_entry_until;+inti=0;+unsignedlongnow=time(NULL);+unsignedlongnext=(unsignedlong)-1;++/*+*Initiallygivetheclient30secondstoactuallycontactus+*andstoreacredentialbeforewedecidethere'snopointin+*keepingthedaemonaround.+*/+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));+/*+*Stickaround30secondsincaseanewcredential+*showsup(e.g.,becausewejustremovedafailed+*one,andwewillsoongetthecorrectone).+*/+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)+return0;+next=wait_for_entry_until;+}++returnnext-now;+}++staticintread_request(FILE*fh,structcredential*c,+structstrbuf*action,int*timeout){+staticstructstrbufitem=STRBUF_INIT;+constchar*p;++strbuf_getline(&item,fh,'\n');+p=skip_prefix(item.buf,"action=");+if(!p)+returnerror("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)+returnerror("client sent bogus timeout line: %s",item.buf);+*timeout=atoi(p);++if(credential_read(c,fh)<0)+return-1;+return0;+}++staticvoidserve_one_client(FILE*in,FILE*out)+{+structcredentialc=CREDENTIAL_INIT;+structstrbufaction=STRBUF_INIT;+inttimeout=-1;++if(read_request(in,&c,&action,&timeout)<0)+/* ignore error */;+elseif(!strcmp(action.buf,"get")){+structcredential_cache_entry*e=lookup_credential(&c);+if(e){+fprintf(out,"username=%s\n",e->item.username);+fprintf(out,"password=%s\n",e->item.password);+}+}+elseif(!strcmp(action.buf,"exit"))+exit(0);+elseif(!strcmp(action.buf,"erase"))+remove_credential(&c);+elseif(!strcmp(action.buf,"store")){+if(timeout<0)+warning("cache client didn't specify a timeout");+elseif(!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);+}++staticintserve_cache_loop(intfd)+{+structpollfdpfd;+unsignedlongwakeup;++wakeup=check_expirations();+if(!wakeup)+return0;++pfd.fd=fd;+pfd.events=POLLIN;+if(poll(&pfd,1,1000*wakeup)<0){+if(errno!=EINTR)+die_errno("poll failed");+return1;+}++if(pfd.revents&POLLIN){+intclient,client2;+FILE*in,*out;++client=accept(fd,NULL,NULL);+if(client<0){+warning("accept failed: %s",strerror(errno));+return1;+}+client2=dup(client);+if(client2<0){+warning("dup failed: %s",strerror(errno));+close(client);+return1;+}++in=xfdopen(client,"r");+out=xfdopen(client2,"w");+serve_one_client(in,out);+fclose(in);+fclose(out);+}+return1;+}++staticvoidserve_cache(constchar*socket_path)+{+intfd;++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);+}++staticconstcharpermissions_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";+staticvoidcheck_socket_directory(constchar*path)+{+structstatst;+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;+}++/*+*Wemustbesuretocreatethedirectorywiththecorrectmode,+*notjustchmoditafterthefact;otherwise,thereisarace+*conditioninwhichsomebodycanchdirtoit,sleep,thentrytoopen+*ourprotectedsocket.+*/+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);+}++intmain(intargc,constchar**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);++return0;+}
@@ -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++staticintsend_request(constchar*socket,conststructstrbuf*out)+{+intgot_data=0;+intfd=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){+charin[1024];+intr;++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;+}+returngot_data;+}++staticvoidspawn_daemon(constchar*socket)+{+structchild_processdaemon;+constchar*argv[]={NULL,NULL,NULL};+charbuf[128];+intr;++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);+}++staticvoiddo_cache(constchar*socket,constchar*action,inttimeout,+intflags)+{+structstrbufbuf=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);+}++intmain(intargc,constchar**argv)+{+char*socket_path=NULL;+inttimeout=900;+constchar*op;+constchar*constusage[]={+"git credential-cache [options] <action>",+NULL+};+structoptionoptions[]={+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);+elseif(!strcmp(op,"get")||!strcmp(op,"erase"))+do_cache(socket_path,op,timeout,FLAG_RELAY);+elseif(!strcmp(op,"store"))+do_cache(socket_path,op,timeout,FLAG_RELAY|FLAG_SPAWN);+else+die("unknown operation: %s",op);++return0;+}
@@ -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$1httpsexample.comstore-user+reject$1httpsexample.comuser1+reject$1httpsexample.comuser2+reject$1ftpother.tlduser+reject$1httpstimeout.tlduser+}++reject(){+(+echoprotocol=$2+echohost=$3+echousername=$4+)|test-credentialreject$1+}++helper_test(){+HELPER=$1++test_expect_success"helper ($HELPER) has no existing data"'+checkfill$HELPER<<-\EOF+protocol=https+host=example.com+--+username=askpass-username+password=askpass-password+--+askpass:Usernamefor'\''https://example.com'\'':+askpass:Passwordfor'\''https://askpass-username@example.com'\'':+EOF+'++test_expect_success"helper ($HELPER) stores password"'+checkapprove$HELPER<<-\EOF+protocol=https+host=example.com+username=store-user+password=store-pass+EOF+'++test_expect_success"helper ($HELPER) can retrieve password"'+checkfill$HELPER<<-\EOF+protocol=https+host=example.com+--+username=store-user+password=store-pass+--+EOF+'++test_expect_success"helper ($HELPER) requires matching protocol"'+checkfill$HELPER<<-\EOF+protocol=http+host=example.com+--+username=askpass-username+password=askpass-password+--+askpass:Usernamefor'\''http://example.com'\'':+askpass:Passwordfor'\''http://askpass-username@example.com'\'':+EOF+'++test_expect_success"helper ($HELPER) requires matching host"'+checkfill$HELPER<<-\EOF+protocol=https+host=other.tld+--+username=askpass-username+password=askpass-password+--+askpass:Usernamefor'\''https://other.tld'\'':+askpass:Passwordfor'\''https://askpass-username@other.tld'\'':+EOF+'++test_expect_success"helper ($HELPER) requires matching username"'+checkfill$HELPER<<-\EOF+protocol=https+host=example.com+username=other+--+username=other+password=askpass-password+--+askpass:Passwordfor'\''https://other@example.com'\'':+EOF+'++test_expect_success"helper ($HELPER) requires matching path"'+checkapprove$HELPER<<-\EOF&&+protocol=ftp+host=other.tld+path=foo.git+username=user+password=pass+EOF+checkfill$HELPER<<-\EOF+protocol=ftp+host=other.tld+path=bar.git+--+username=askpass-username+password=askpass-password+--+askpass:Usernamefor'\''ftp://other.tld/bar.git'\'':+askpass:Passwordfor'\''ftp://askpass-username@other.tld/bar.git'\'':+EOF+'++test_expect_success"helper ($HELPER) can forget host"'+checkreject$HELPER<<-\EOF&&+protocol=https+host=example.com+EOF+checkfill$HELPER<<-\EOF+protocol=https+host=example.com+--+username=askpass-username+password=askpass-password+--+askpass:Usernamefor'\''https://example.com'\'':+askpass:Passwordfor'\''https://askpass-username@example.com'\'':+EOF+'++test_expect_success"helper ($HELPER) can store multiple users"'+checkapprove$HELPER<<-\EOF&&+protocol=https+host=example.com+username=user1+password=pass1+EOF+checkapprove$HELPER<<-\EOF&&+protocol=https+host=example.com+username=user2+password=pass2+EOF+checkfill$HELPER<<-\EOF&&+protocol=https+host=example.com+username=user1+--+username=user1+password=pass1+EOF+checkfill$HELPER<<-\EOF+protocol=https+host=example.com+username=user2+--+username=user2+password=pass2+EOF+'++test_expect_success"helper ($HELPER) can forget user"'+checkreject$HELPER<<-\EOF&&+protocol=https+host=example.com+username=user1+EOF+checkfill$HELPER<<-\EOF+protocol=https+host=example.com+username=user1+--+username=user1+password=askpass-password+--+askpass:Passwordfor'\''https://user1@example.com'\'':+'++test_expect_success"helper ($HELPER) remembers other user"'+checkfill$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"'+checkapprove"$HELPER"<<-\EOF&&+protocol=https+host=timeout.tld+username=user+password=pass+EOF+sleep2&&+checkfill"$HELPER"<<-\EOF+protocol=https+host=timeout.tld+--+username=askpass-username+password=askpass-password+--+askpass:Usernamefor'\''https://timeout.tld'\'':+askpass:Passwordfor'\''https://askpass-username@timeout.tld'\'':+EOF+'+} cat>askpass<<\EOF#!/bin/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_testcache+helper_test_timeoutcache--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.+gitcredential-cacheexit++test_done
@@ -0,0 +1,56 @@+#include"cache.h"+#include"unix-socket.h"++staticintunix_stream_socket(void)+{+intfd=socket(AF_UNIX,SOCK_STREAM,0);+if(fd<0)+die_errno("unable to create socket");+returnfd;+}++staticvoidunix_sockaddr_init(structsockaddr_un*sa,constchar*path)+{+intsize=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);+}++intunix_stream_connect(constchar*path)+{+intfd;+structsockaddr_unsa;++unix_sockaddr_init(&sa,path);+fd=unix_stream_socket();+if(connect(fd,(structsockaddr*)&sa,sizeof(sa))<0){+close(fd);+return-1;+}+returnfd;+}++intunix_stream_listen(constchar*path)+{+intfd;+structsockaddr_unsa;++unix_sockaddr_init(&sa,path);+fd=unix_stream_socket();++unlink(path);+if(bind(fd,(structsockaddr*)&sa,sizeof(sa))<0){+close(fd);+return-1;+}++if(listen(fd,5)<0){+close(fd);+return-1;+}++returnfd;+}
From: Jeff King <hidden> Date: 2016-06-15 22:52:31
This just follows the rfc3986 rules for percent-encoding
url data into a strbuf.
Signed-off-by: Jeff King <redacted>
---
strbuf.c | 37 +++++++++++++++++++++++++++++++++++++
strbuf.h | 5 +++++
2 files changed, 42 insertions(+), 0 deletions(-)
From: Jeff King <hidden> Date: 2016-06-15 22:52:31
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 <redacted>
---
.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
@@ -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
@@ -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,
@@ -0,0 +1,144 @@+#include"cache.h"+#include"credential.h"+#include"string-list.h"+#include"parse-options.h"++staticstructlock_filecredential_lock;++staticvoidparse_credential_file(constchar*fn,+structcredential*c,+void(*match_cb)(structcredential*),+void(*other_cb)(structstrbuf*))+{+FILE*fh;+structstrbufline=STRBUF_INIT;+structcredentialentry=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;+}+}+elseif(other_cb)+other_cb(&line);+}++credential_clear(&entry);+strbuf_release(&line);+fclose(fh);+}++staticvoidprint_entry(structcredential*c)+{+printf("username=%s\n",c->username);+printf("password=%s\n",c->password);+}++staticvoidprint_line(structstrbuf*buf)+{+strbuf_addch(buf,'\n');+write_or_die(credential_lock.fd,buf->buf,buf->len);+}++staticvoidrewrite_credential_file(constchar*fn,structcredential*c,+structstrbuf*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");+}++staticvoidstore_credential(constchar*fn,structcredential*c)+{+structstrbufbuf=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);+}++staticvoidremove_credential(constchar*fn,structcredential*c)+{+if(!c->protocol||!(c->host||c->path))+return;+rewrite_credential_file(fn,c,NULL);+}++staticintlookup_credential(constchar*fn,structcredential*c)+{+if(!c->protocol||!(c->host||c->path))+return0;+parse_credential_file(fn,c,print_entry,NULL);+returnc->username&&c->password;+}++intmain(intargc,constchar**argv)+{+constchar*constusage[]={+"git credential-store [options] <action>",+NULL+};+constchar*op;+structcredentialc=CREDENTIAL_INIT;+char*store=NULL;+structoptionoptions[]={+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);+elseif(!strcmp(op,"erase"))+remove_credential(store,&c);+elseif(!strcmp(op,"store"))+store_credential(store,&c);+else+die("unknown operation: %s",op);++return0;+}
From: Jeff King <hidden> Date: 2016-06-15 22:52:31
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 <redacted>
---
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
[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,
@@ -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
From: Jeff King <hidden> Date: 2016-06-15 22:52:32
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
From: Jeff King <hidden> Date: 2016-06-15 22:52:32
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 <redacted>
---
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
@@ -619,47 +619,3 @@ int finish_connect(struct child_process *conn)free(conn);returncode;}--char*git_getpass(constchar*prompt)-{-constchar*askpass;-structchild_processpass;-constchar*args[3];-staticstructstrbufbuffer=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");-returnresult;-}--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"));--returnbuffer.buf;-}
@@ -0,0 +1,48 @@+#include"cache.h"+#include"run-command.h"+#include"strbuf.h"+#include"prompt.h"++char*git_getpass(constchar*prompt)+{+constchar*askpass;+structchild_processpass;+constchar*args[3];+staticstructstrbufbuffer=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");+returnresult;+}++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"));++returnbuffer.buf;+}
From: Jeff King <hidden> Date: 2016-06-15 22:52:32
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 <redacted>
---
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(-)
@@ -3,26 +3,13 @@#include"strbuf.h"#include"prompt.h"-char*git_getpass(constchar*prompt)+staticchar*do_askpass(constchar*cmd,constchar*prompt,constchar*name){-constchar*askpass;structchild_processpass;constchar*args[3];staticstructstrbufbuffer=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");-returnresult;-}--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);
From: Jeff King <hidden> Date: 2016-06-15 22:52:32
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 <redacted>
---
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
From: Jeff King <hidden> Date: 2016-06-15 22:52:32
This will use getpass_echo when set.
Signed-off-by: Jeff King <redacted>
---
prompt.c | 5 ++++-
prompt.h | 1 +
2 files changed, 5 insertions(+), 1 deletions(-)
From: Jeff King <hidden> Date: 2016-06-15 22:52:32
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 <redacted>
---
credential.c | 15 +++++++--------
1 files changed, 7 insertions(+), 8 deletions(-)
@@ -121,11 +122,7 @@ static void credential_describe(struct credential *c, struct strbuf *out)elsestrbuf_addf(&prompt,"%s: ",what);-/* FIXME: for usernames, we should do something less magical that-*actuallyechoesthecharacters.However,weneedtoreadfrom-*/dev/ttyandnotstdio,whichisnotportable(butgetpasswilldo-*itforus).http.cusesthesameworkaround.*/-r=git_getpass(prompt.buf);+r=git_prompt(prompt.buf,what,flags);strbuf_release(&desc);strbuf_release(&prompt);
From: Jeff King <hidden> Date: 2016-06-15 22:52:32
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 <redacted>
---
Makefile | 8 ++++++++
compat/getpass.c | 35 +++++++++++++++++++++++++++++++++++
2 files changed, 43 insertions(+), 0 deletions(-)
@@ -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
From: Erik Faye-Lund <hidden> Date: 2016-06-15 22:52:32
On Sun, Nov 27, 2011 at 9:27 AM, Jeff King [off-list ref] wrote:
On Thu, Nov 24, 2011 at 05:58:01AM -0500, Jeff King wrote:
quoted
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)...
From: Jeff King <hidden> Date: 2016-06-15 22:52:32
On Sun, Nov 27, 2011 at 10:17:26AM +0100, Erik Faye-Lund wrote:
quoted
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:
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.
+ 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
From: Erik Faye-Lund <hidden> Date: 2016-06-15 22:52:32
On Mon, Nov 28, 2011 at 4:53 AM, Jeff King [off-list ref] wrote:
On Sun, Nov 27, 2011 at 10:17:26AM +0100, Erik Faye-Lund wrote:
quoted
quoted
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 ;)
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 ;)
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:
quoted
+ 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)...
quoted
+ 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).
From: Jeff King <hidden> Date: 2016-06-15 22:52:32
On Mon, Nov 28, 2011 at 10:36:21AM +0100, Erik Faye-Lund wrote:
quoted
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.
quoted
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.
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
From: Frans Klaver <hidden> Date: 2016-06-15 22:52:32
On Mon, Nov 28, 2011 at 12:31 PM, Jeff King [off-list ref] wrote:
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
From: Erik Faye-Lund <hidden> Date: 2016-06-15 22:52:32
On Mon, Nov 28, 2011 at 12:31 PM, Jeff King [off-list ref] wrote:
On Mon, Nov 28, 2011 at 10:36:21AM +0100, Erik Faye-Lund wrote:
quoted
quoted
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.
quoted
quoted
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.
quoted
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...
From: Jeff King <hidden> Date: 2016-06-15 22:52:32
On Mon, Nov 28, 2011 at 01:59:34PM +0100, Erik Faye-Lund wrote:
quoted
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
From: Junio C Hamano <hidden> Date: 2016-06-15 22:52:32
Jeff King [off-list ref] writes:
quoted hunk
This just follows the rfc3986 rules for percent-encoding
url data into a strbuf.
Signed-off-by: Jeff King <redacted>
---
strbuf.c | 37 +++++++++++++++++++++++++++++++++++++
strbuf.h | 5 +++++
2 files changed, 42 insertions(+), 0 deletions(-)
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.
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")?
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?
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.
+ 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?
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?
From: Jeff King <hidden> Date: 2016-06-15 22:52:32
On Tue, Nov 29, 2011 at 10:19:00AM -0800, Junio C Hamano wrote:
quoted
+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).
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
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.
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.
quoted
+ 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.
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
From: René Scharfe <hidden> Date: 2016-06-15 22:52:32
Am 29.11.2011 22:19, schrieb Jeff King:
On Tue, Nov 29, 2011 at 10:19:00AM -0800, Junio C Hamano wrote:
quoted
quoted
+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é
From: Jeff King <hidden> Date: 2016-06-15 22:52:32
On Wed, Nov 30, 2011 at 12:26:20AM +0100, René Scharfe wrote:
quoted
quoted
quoted
+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
From: René Scharfe <hidden> Date: 2016-06-15 22:52:32
Am 30.11.2011 04:20, schrieb Jeff King:
On Wed, Nov 30, 2011 at 12:26:20AM +0100, René Scharfe wrote:
quoted
quoted
quoted
quoted
+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é