440 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Perl
		
	
	
		
			Executable file
		
	
	
	
	
			
		
		
	
	
			440 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Perl
		
	
	
		
			Executable file
		
	
	
	
	
#!/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
 | 
						||
	$options->{'gpg'} //= Git::config('gpg.program')
 | 
						||
	                  // '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";
 | 
						||
}
 |