2020-01-12 00:36:56 +01:00
|
|
|
|
#!/usr/bin/perl
|
|
|
|
|
|
|
|
|
|
use strict;
|
|
|
|
|
use warnings;
|
|
|
|
|
|
|
|
|
|
use Getopt::Long;
|
|
|
|
|
use File::Basename;
|
|
|
|
|
use Git;
|
|
|
|
|
|
|
|
|
|
my $VERSION = "0.2";
|
|
|
|
|
|
|
|
|
|
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@",
|
|
|
|
|
'gpg|g:s',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if ($options{help}) {
|
|
|
|
|
my $shortname = basename($0);
|
|
|
|
|
$shortname =~ s/git-credential-//;
|
|
|
|
|
|
|
|
|
|
print <<EOHIPPUS;
|
|
|
|
|
|
|
|
|
|
$0 [(-f <authfile>)...] [-g <program>] [-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.
|
|
|
|
|
|
|
|
|
|
-g|--gpg <program> : specify the program for GPG. By default, this is the
|
|
|
|
|
value of gpg.program in the git repository or global
|
|
|
|
|
option or gpg.
|
|
|
|
|
|
|
|
|
|
-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 <authfile>s,
|
|
|
|
|
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 <authfile>)...] [-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 ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
load_config(\%options);
|
|
|
|
|
|
|
|
|
|
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 = ($options{'gpg'}, qw(--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 load_config {
|
|
|
|
|
# load settings from git config
|
|
|
|
|
my $options = shift;
|
|
|
|
|
# set from command argument, gpg.program option, or default to gpg
|
2020-11-21 19:20:35 +01:00
|
|
|
|
$options->{'gpg'} //= Git::config('gpg.program')
|
2020-01-12 00:36:56 +01:00
|
|
|
|
// 'gpg';
|
|
|
|
|
log_verbose("using $options{'gpg'} for GPG operations");
|
|
|
|
|
}
|
|
|
|
|
sub log_verbose {
|
|
|
|
|
return unless $options{verbose};
|
|
|
|
|
printf STDERR @_;
|
|
|
|
|
printf STDERR "\n";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub log_debug {
|
|
|
|
|
return unless $options{debug};
|
|
|
|
|
printf STDERR @_;
|
|
|
|
|
printf STDERR "\n";
|
|
|
|
|
}
|