You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
423 lines
10 KiB
423 lines
10 KiB
#!/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 $entry->{$check}) { |
|
log_debug("OK: entry has no $check token, so any value satisfies check $check"); |
|
} elsif (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"; |
|
}
|
|
|