| #!/usr/bin/env 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}) { | |
| $num_port = Git::port_num($nentry->{port}); | |
| unless ($num_port) { | |
| printf(STDERR "ignoring invalid port `%s' " . | |
| "from netrc file\n", $nentry->{port}); | |
| } | |
| # Since we've already validated and converted | |
| # the port to its numerical value, do not | |
| # capture it as the `protocol' value, as used | |
| # to be the case for symbolic port names. | |
| 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); | |
| # skip any unknown tokens | |
| next 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"; | |
| } |