Browse Source
This credential helper supports multiple files, returning the first one that matches. It checks file permissions and owner. For *.gpg files, it will run GPG to decrypt the file. Signed-off-by: Ted Zlatanov <tzz@lifelogs.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>maint
Ted Zlatanov
12 years ago
committed by
Junio C Hamano
4 changed files with 545 additions and 0 deletions
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
test: |
||||
./test.pl |
||||
|
||||
testverbose: |
||||
./test.pl -d -v |
@ -0,0 +1,421 @@
@@ -0,0 +1,421 @@
|
||||
#!/usr/bin/perl |
||||
|
||||
use strict; |
||||
use warnings; |
||||
|
||||
use Getopt::Long; |
||||
use File::Basename; |
||||
|
||||
my $VERSION = "0.1"; |
||||
|
||||
my %options = ( |
||||
help => 0, |
||||
debug => 0, |
||||
verbose => 0, |
||||
insecure => 0, |
||||
file => [], |
||||
|
||||
# identical token maps, e.g. host -> host, will be inserted later |
||||
tmap => { |
||||
port => 'protocol', |
||||
machine => 'host', |
||||
path => 'path', |
||||
login => 'username', |
||||
user => 'username', |
||||
password => 'password', |
||||
} |
||||
); |
||||
|
||||
# Map each credential protocol token to itself on the netrc side. |
||||
foreach (values %{$options{tmap}}) { |
||||
$options{tmap}->{$_} = $_; |
||||
} |
||||
|
||||
# Now, $options{tmap} has a mapping from the netrc format to the Git credential |
||||
# helper protocol. |
||||
|
||||
# Next, we build the reverse token map. |
||||
|
||||
# When $rmap{foo} contains 'bar', that means that what the Git credential helper |
||||
# protocol calls 'bar' is found as 'foo' in the netrc/authinfo file. Keys in |
||||
# %rmap are what we expect to read from the netrc/authinfo file. |
||||
|
||||
my %rmap; |
||||
foreach my $k (keys %{$options{tmap}}) { |
||||
push @{$rmap{$options{tmap}->{$k}}}, $k; |
||||
} |
||||
|
||||
Getopt::Long::Configure("bundling"); |
||||
|
||||
# TODO: maybe allow the token map $options{tmap} to be configurable. |
||||
GetOptions(\%options, |
||||
"help|h", |
||||
"debug|d", |
||||
"insecure|k", |
||||
"verbose|v", |
||||
"file|f=s@", |
||||
); |
||||
|
||||
if ($options{help}) { |
||||
my $shortname = basename($0); |
||||
$shortname =~ s/git-credential-//; |
||||
|
||||
print <<EOHIPPUS; |
||||
|
||||
$0 [-f AUTHFILE1] [-f AUTHFILEN] [-d] [-v] [-k] get |
||||
|
||||
Version $VERSION by tzz\@lifelogs.com. License: BSD. |
||||
|
||||
Options: |
||||
|
||||
-f|--file AUTHFILE : specify netrc-style files. Files with the .gpg extension |
||||
will be decrypted by GPG before parsing. Multiple -f |
||||
arguments are OK. They are processed in order, and the |
||||
first matching entry found is returned via the credential |
||||
helper protocol (see below). |
||||
|
||||
When no -f option is given, .authinfo.gpg, .netrc.gpg, |
||||
.authinfo, and .netrc files in your home directory are used |
||||
in this order. |
||||
|
||||
-k|--insecure : ignore bad file ownership or permissions |
||||
|
||||
-d|--debug : turn on debugging (developer info) |
||||
|
||||
-v|--verbose : be more verbose (show files and information found) |
||||
|
||||
To enable this credential helper: |
||||
|
||||
git config credential.helper '$shortname -f AUTHFILE1 -f AUTHFILE2' |
||||
|
||||
(Note that Git will prepend "git-credential-" to the helper name and look for it |
||||
in the path.) |
||||
|
||||
...and if you want lots of debugging info: |
||||
|
||||
git config credential.helper '$shortname -f AUTHFILE -d' |
||||
|
||||
...or to see the files opened and data found: |
||||
|
||||
git config credential.helper '$shortname -f AUTHFILE -v' |
||||
|
||||
Only "get" mode is supported by this credential helper. It opens every AUTHFILE |
||||
and looks for the first entry that matches the requested search criteria: |
||||
|
||||
'port|protocol': |
||||
The protocol that will be used (e.g., https). (protocol=X) |
||||
|
||||
'machine|host': |
||||
The remote hostname for a network credential. (host=X) |
||||
|
||||
'path': |
||||
The path with which the credential will be used. (path=X) |
||||
|
||||
'login|user|username': |
||||
The credential’s username, if we already have one. (username=X) |
||||
|
||||
Thus, when we get this query on STDIN: |
||||
|
||||
host=github.com |
||||
protocol=https |
||||
username=tzz |
||||
|
||||
this credential helper will look for the first entry in every AUTHFILE that |
||||
matches |
||||
|
||||
machine github.com port https login tzz |
||||
|
||||
OR |
||||
|
||||
machine github.com protocol https login tzz |
||||
|
||||
OR... etc. acceptable tokens as listed above. Any unknown tokens are |
||||
simply ignored. |
||||
|
||||
Then, the helper will print out whatever tokens it got from the entry, including |
||||
"password" tokens, mapping back to Git's helper protocol; e.g. "port" is mapped |
||||
back to "protocol". Any redundant entry tokens (part of the original query) are |
||||
skipped. |
||||
|
||||
Again, note that only the first matching entry from all the AUTHFILEs, processed |
||||
in the sequence given on the command line, is used. |
||||
|
||||
Netrc/authinfo tokens can be quoted as 'STRING' or "STRING". |
||||
|
||||
No caching is performed by this credential helper. |
||||
|
||||
EOHIPPUS |
||||
|
||||
exit 0; |
||||
} |
||||
|
||||
my $mode = shift @ARGV; |
||||
|
||||
# Credentials must get a parameter, so die if it's missing. |
||||
die "Syntax: $0 [-f AUTHFILE1] [-f AUTHFILEN] [-d] get" unless defined $mode; |
||||
|
||||
# Only support 'get' mode; with any other unsupported ones we just exit. |
||||
exit 0 unless $mode eq 'get'; |
||||
|
||||
my $files = $options{file}; |
||||
|
||||
# if no files were given, use a predefined list. |
||||
# note that .gpg files come first |
||||
unless (scalar @$files) { |
||||
my @candidates = qw[ |
||||
~/.authinfo.gpg |
||||
~/.netrc.gpg |
||||
~/.authinfo |
||||
~/.netrc |
||||
]; |
||||
|
||||
$files = $options{file} = [ map { glob $_ } @candidates ]; |
||||
} |
||||
|
||||
my $query = read_credential_data_from_stdin(); |
||||
|
||||
FILE: |
||||
foreach my $file (@$files) { |
||||
my $gpgmode = $file =~ m/\.gpg$/; |
||||
unless (-r $file) { |
||||
log_verbose("Unable to read $file; skipping it"); |
||||
next FILE; |
||||
} |
||||
|
||||
# the following check is copied from Net::Netrc, for non-GPG files |
||||
# OS/2 and Win32 do not handle stat in a way compatible with this check :-( |
||||
unless ($gpgmode || $options{insecure} || |
||||
$^O eq 'os2' |
||||
|| $^O eq 'MSWin32' |
||||
|| $^O eq 'MacOS' |
||||
|| $^O =~ /^cygwin/) { |
||||
my @stat = stat($file); |
||||
|
||||
if (@stat) { |
||||
if ($stat[2] & 077) { |
||||
log_verbose("Insecure $file (mode=%04o); skipping it", |
||||
$stat[2] & 07777); |
||||
next FILE; |
||||
} |
||||
|
||||
if ($stat[4] != $<) { |
||||
log_verbose("Not owner of $file; skipping it"); |
||||
next FILE; |
||||
} |
||||
} |
||||
} |
||||
|
||||
my @entries = load_netrc($file, $gpgmode); |
||||
|
||||
unless (scalar @entries) { |
||||
if ($!) { |
||||
log_verbose("Unable to open $file: $!"); |
||||
} else { |
||||
log_verbose("No netrc entries found in $file"); |
||||
} |
||||
|
||||
next FILE; |
||||
} |
||||
|
||||
my $entry = find_netrc_entry($query, @entries); |
||||
if ($entry) { |
||||
print_credential_data($entry, $query); |
||||
# we're done! |
||||
last FILE; |
||||
} |
||||
} |
||||
|
||||
exit 0; |
||||
|
||||
sub load_netrc { |
||||
my $file = shift @_; |
||||
my $gpgmode = shift @_; |
||||
|
||||
my $io; |
||||
if ($gpgmode) { |
||||
my @cmd = (qw(gpg --decrypt), $file); |
||||
log_verbose("Using GPG to open $file: [@cmd]"); |
||||
open $io, "-|", @cmd; |
||||
} else { |
||||
log_verbose("Opening $file..."); |
||||
open $io, '<', $file; |
||||
} |
||||
|
||||
# nothing to do if the open failed (we log the error later) |
||||
return unless $io; |
||||
|
||||
# Net::Netrc does this, but the functionality is merged with the file |
||||
# detection logic, so we have to extract just the part we need |
||||
my @netrc_entries = net_netrc_loader($io); |
||||
|
||||
# these entries will use the credential helper protocol token names |
||||
my @entries; |
||||
|
||||
foreach my $nentry (@netrc_entries) { |
||||
my %entry; |
||||
my $num_port; |
||||
|
||||
if (!defined $nentry->{machine}) { |
||||
next; |
||||
} |
||||
if (defined $nentry->{port} && $nentry->{port} =~ m/^\d+$/) { |
||||
$num_port = $nentry->{port}; |
||||
delete $nentry->{port}; |
||||
} |
||||
|
||||
# create the new entry for the credential helper protocol |
||||
$entry{$options{tmap}->{$_}} = $nentry->{$_} foreach keys %$nentry; |
||||
|
||||
# for "host X port Y" where Y is an integer (captured by |
||||
# $num_port above), set the host to "X:Y" |
||||
if (defined $entry{host} && defined $num_port) { |
||||
$entry{host} = join(':', $entry{host}, $num_port); |
||||
} |
||||
|
||||
push @entries, \%entry; |
||||
} |
||||
|
||||
return @entries; |
||||
} |
||||
|
||||
sub net_netrc_loader { |
||||
my $fh = shift @_; |
||||
my @entries; |
||||
my ($mach, $macdef, $tok, @tok); |
||||
|
||||
LINE: |
||||
while (<$fh>) { |
||||
undef $macdef if /\A\n\Z/; |
||||
|
||||
if ($macdef) { |
||||
next LINE; |
||||
} |
||||
|
||||
s/^\s*//; |
||||
chomp; |
||||
|
||||
while (length && s/^("((?:[^"]+|\\.)*)"|((?:[^\\\s]+|\\.)*))\s*//) { |
||||
(my $tok = $+) =~ s/\\(.)/$1/g; |
||||
push(@tok, $tok); |
||||
} |
||||
|
||||
TOKEN: |
||||
while (@tok) { |
||||
if ($tok[0] eq "default") { |
||||
shift(@tok); |
||||
$mach = { machine => undef }; |
||||
next TOKEN; |
||||
} |
||||
|
||||
$tok = shift(@tok); |
||||
|
||||
if ($tok eq "machine") { |
||||
my $host = shift @tok; |
||||
$mach = { machine => $host }; |
||||
push @entries, $mach; |
||||
} elsif (exists $options{tmap}->{$tok}) { |
||||
unless ($mach) { |
||||
log_debug("Skipping token $tok because no machine was given"); |
||||
next TOKEN; |
||||
} |
||||
|
||||
my $value = shift @tok; |
||||
unless (defined $value) { |
||||
log_debug("Token $tok had no value, skipping it."); |
||||
next TOKEN; |
||||
} |
||||
|
||||
# Following line added by rmerrell to remove '/' escape char in .netrc |
||||
$value =~ s/\/\\/\\/g; |
||||
$mach->{$tok} = $value; |
||||
} elsif ($tok eq "macdef") { # we ignore macros |
||||
next TOKEN unless $mach; |
||||
my $value = shift @tok; |
||||
$macdef = 1; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return @entries; |
||||
} |
||||
|
||||
sub read_credential_data_from_stdin { |
||||
# the query: start with every token with no value |
||||
my %q = map { $_ => undef } values(%{$options{tmap}}); |
||||
|
||||
while (<STDIN>) { |
||||
next unless m/^([^=]+)=(.+)/; |
||||
|
||||
my ($token, $value) = ($1, $2); |
||||
die "Unknown search token $token" unless exists $q{$token}; |
||||
$q{$token} = $value; |
||||
log_debug("We were given search token $token and value $value"); |
||||
} |
||||
|
||||
foreach (sort keys %q) { |
||||
log_debug("Searching for %s = %s", $_, $q{$_} || '(any value)'); |
||||
} |
||||
|
||||
return \%q; |
||||
} |
||||
|
||||
# takes the search tokens and then a list of entries |
||||
# each entry is a hash reference |
||||
sub find_netrc_entry { |
||||
my $query = shift @_; |
||||
|
||||
ENTRY: |
||||
foreach my $entry (@_) |
||||
{ |
||||
my $entry_text = join ', ', map { "$_=$entry->{$_}" } keys %$entry; |
||||
foreach my $check (sort keys %$query) { |
||||
if (defined $query->{$check}) { |
||||
log_debug("compare %s [%s] to [%s] (entry: %s)", |
||||
$check, |
||||
$entry->{$check}, |
||||
$query->{$check}, |
||||
$entry_text); |
||||
unless ($query->{$check} eq $entry->{$check}) { |
||||
next ENTRY; |
||||
} |
||||
} else { |
||||
log_debug("OK: any value satisfies check $check"); |
||||
} |
||||
} |
||||
|
||||
return $entry; |
||||
} |
||||
|
||||
# nothing was found |
||||
return; |
||||
} |
||||
|
||||
sub print_credential_data { |
||||
my $entry = shift @_; |
||||
my $query = shift @_; |
||||
|
||||
log_debug("entry has passed all the search checks"); |
||||
TOKEN: |
||||
foreach my $git_token (sort keys %$entry) { |
||||
log_debug("looking for useful token $git_token"); |
||||
# don't print unknown (to the credential helper protocol) tokens |
||||
next TOKEN unless exists $query->{$git_token}; |
||||
|
||||
# don't print things asked in the query (the entry matches them) |
||||
next TOKEN if defined $query->{$git_token}; |
||||
|
||||
log_debug("FOUND: $git_token=$entry->{$git_token}"); |
||||
printf "%s=%s\n", $git_token, $entry->{$git_token}; |
||||
} |
||||
} |
||||
sub log_verbose { |
||||
return unless $options{verbose}; |
||||
printf STDERR @_; |
||||
printf STDERR "\n"; |
||||
} |
||||
|
||||
sub log_debug { |
||||
return unless $options{debug}; |
||||
printf STDERR @_; |
||||
printf STDERR "\n"; |
||||
} |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
machine imap login tzz@lifelogs.com port imaps password letmeknow |
||||
machine imap login bob port imaps password bobwillknow |
||||
|
||||
# comment test |
||||
|
||||
machine imap2 login tzz port 1099 password tzzknow |
||||
machine imap2 login bob password bobwillknow |
||||
|
||||
# another command |
||||
|
||||
machine github.com |
||||
multilinetoken anothervalue |
||||
login carol password carolknows |
@ -0,0 +1,106 @@
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/perl |
||||
|
||||
use warnings; |
||||
use strict; |
||||
use Test; |
||||
use IPC::Open2; |
||||
|
||||
BEGIN { plan tests => 15 } |
||||
|
||||
my @global_credential_args = @ARGV; |
||||
my $netrc = './test.netrc'; |
||||
print "# Testing insecure file, nothing should be found\n"; |
||||
chmod 0644, $netrc; |
||||
my $cred = run_credential(['-f', $netrc, 'get'], |
||||
{ host => 'github.com' }); |
||||
|
||||
ok(scalar keys %$cred, 0, "Got 0 keys from insecure file"); |
||||
|
||||
print "# Testing missing file, nothing should be found\n"; |
||||
chmod 0644, $netrc; |
||||
$cred = run_credential(['-f', '///nosuchfile///', 'get'], |
||||
{ host => 'github.com' }); |
||||
|
||||
ok(scalar keys %$cred, 0, "Got 0 keys from missing file"); |
||||
|
||||
chmod 0600, $netrc; |
||||
|
||||
print "# Testing with invalid data\n"; |
||||
$cred = run_credential(['-f', $netrc, 'get'], |
||||
"bad data"); |
||||
ok(scalar keys %$cred, 4, "Got first found keys with bad data"); |
||||
|
||||
print "# Testing netrc file for a missing corovamilkbar entry\n"; |
||||
$cred = run_credential(['-f', $netrc, 'get'], |
||||
{ host => 'corovamilkbar' }); |
||||
|
||||
ok(scalar keys %$cred, 0, "Got no corovamilkbar keys"); |
||||
|
||||
print "# Testing netrc file for a github.com entry\n"; |
||||
$cred = run_credential(['-f', $netrc, 'get'], |
||||
{ host => 'github.com' }); |
||||
|
||||
ok(scalar keys %$cred, 2, "Got 2 Github keys"); |
||||
|
||||
ok($cred->{password}, 'carolknows', "Got correct Github password"); |
||||
ok($cred->{username}, 'carol', "Got correct Github username"); |
||||
|
||||
print "# Testing netrc file for a username-specific entry\n"; |
||||
$cred = run_credential(['-f', $netrc, 'get'], |
||||
{ host => 'imap', username => 'bob' }); |
||||
|
||||
ok(scalar keys %$cred, 2, "Got 2 username-specific keys"); |
||||
|
||||
ok($cred->{password}, 'bobwillknow', "Got correct user-specific password"); |
||||
ok($cred->{protocol}, 'imaps', "Got correct user-specific protocol"); |
||||
|
||||
print "# Testing netrc file for a host:port-specific entry\n"; |
||||
$cred = run_credential(['-f', $netrc, 'get'], |
||||
{ host => 'imap2:1099' }); |
||||
|
||||
ok(scalar keys %$cred, 2, "Got 2 host:port-specific keys"); |
||||
|
||||
ok($cred->{password}, 'tzzknow', "Got correct host:port-specific password"); |
||||
ok($cred->{username}, 'tzz', "Got correct host:port-specific username"); |
||||
|
||||
print "# Testing netrc file that 'host:port kills host' entry\n"; |
||||
$cred = run_credential(['-f', $netrc, 'get'], |
||||
{ host => 'imap2' }); |
||||
|
||||
ok(scalar keys %$cred, 2, "Got 2 'host:port kills host' keys"); |
||||
|
||||
ok($cred->{password}, 'bobwillknow', "Got correct 'host:port kills host' password"); |
||||
ok($cred->{username}, 'bob', "Got correct 'host:port kills host' username"); |
||||
|
||||
sub run_credential |
||||
{ |
||||
my $args = shift @_; |
||||
my $data = shift @_; |
||||
my $pid = open2(my $chld_out, my $chld_in, |
||||
'./git-credential-netrc', @global_credential_args, |
||||
@$args); |
||||
|
||||
die "Couldn't open pipe to netrc credential helper: $!" unless $pid; |
||||
|
||||
if (ref $data eq 'HASH') |
||||
{ |
||||
print $chld_in "$_=$data->{$_}\n" foreach sort keys %$data; |
||||
} |
||||
else |
||||
{ |
||||
print $chld_in "$data\n"; |
||||
} |
||||
|
||||
close $chld_in; |
||||
my %ret; |
||||
|
||||
while (<$chld_out>) |
||||
{ |
||||
chomp; |
||||
next unless m/^([^=]+)=(.+)/; |
||||
|
||||
$ret{$1} = $2; |
||||
} |
||||
|
||||
return \%ret; |
||||
} |
Loading…
Reference in new issue