Changes since PATCHv3:

- simple tests in Makefile
- support multiple files, code refactored
- documentation and comments updated
- fix IO::File for GPG pipe
- exit peacefully in almost every situation, die on bad invocation or query
- use log_verbose() and -v for logging for the user
- use log_debug() and -d for logging for the developer
- use Net::Netrc parser and `man netrc' to improve parsing
- ignore 'default' and 'macdef' netrc entries
- require 'machine' token in netrc lines
- ignore netrc files with bad permissions or owner (from Net::Netrc)

Signed-off-by: Ted Zlatanov <t...@lifelogs.com>
---
 contrib/credential/netrc/Makefile             |   10 +
 contrib/credential/netrc/git-credential-netrc |  423 +++++++++++++++++++++++++
 2 files changed, 433 insertions(+), 0 deletions(-)
 create mode 100644 contrib/credential/netrc/Makefile
 create mode 100755 contrib/credential/netrc/git-credential-netrc

diff --git a/contrib/credential/netrc/Makefile 
b/contrib/credential/netrc/Makefile
new file mode 100644
index 0000000..ee8c5f0
--- /dev/null
+++ b/contrib/credential/netrc/Makefile
@@ -0,0 +1,10 @@
+test_netrc:
+       @(echo "bad data" | ./git-credential-netrc -f A -d -v) || echo "Bad 
invocation test, ignoring failure"
+       @echo "-> Silent invocation... nothing should show up here with a 
missing file"
+       @echo "bad data" | ./git-credential-netrc -f A get
+       @echo "-> Back to noisy: -v and -d used below, missing file"
+       echo "bad data" | ./git-credential-netrc -f A -d -v get
+       @echo "-> Look for any entry in the default file set"
+       echo "" | ./git-credential-netrc -d -v get
+       @echo "-> Look for github.com in the default file set"
+       echo "host=google.com" | ./git-credential-netrc -d -v get
diff --git a/contrib/credential/netrc/git-credential-netrc 
b/contrib/credential/netrc/git-credential-netrc
new file mode 100755
index 0000000..6946217
--- /dev/null
+++ b/contrib/credential/netrc/git-credential-netrc
@@ -0,0 +1,423 @@
+#!/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,
+              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",
+           "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] 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, and the order is respected.
+
+  -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:
+
+protocol=https
+username=tzz
+
+this credential helper will look for the first entry in every AUTHFILE that
+matches
+
+port https login tzz
+
+OR
+
+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".
+
+Again, note that the first matching entry from all the AUTHFILEs is used.
+
+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)
+{
+       unless (-r $file)
+       {
+               log_verbose("Unable to read $file; skipping it");
+               next FILE;
+       }
+
+       # the following check is copied from Net::Netrc
+       # OS/2 and Win32 do not handle stat in a way compatable with this check 
:-(
+       unless ($^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 $mode = (stat($file))[2];
+       if ($mode & 077)
+       {
+               log_verbose("Insecure $file (mode=%04o); skipping it",
+                           $mode & 07777);
+               next FILE;
+       }
+
+       my @entries = load_netrc($file);
+
+       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 $io;
+       if ($file =~ m/\.gpg$/) {
+               log_verbose("Using GPG to open $file");
+               # GPG doesn't work well with 2- or 3-argument open
+               $io = new IO::File("gpg --decrypt $file|");
+       }
+       else {
+               log_verbose("Opening $file...");
+               $io = new IO::File($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->{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) = (0, 0);
+
+    LINE:
+       while (<$fh>) {
+               undef $macdef if /\A\n\Z/;
+
+               if ($macdef) {
+                       push(@$macdef, $_);
+                       next LINE;
+               }
+
+               s/^\s*//;
+               chomp;
+
+               while (length && 
s/^("((?:[^"]+|\\.)*)"|((?:[^\\\s]+|\\.)*))\s*//) {
+                       (my $tok = $+) =~ s/\\(.)/$1/g;
+                       push(@tok, $tok);
+               }
+
+           TOKEN:
+               while (@tok) {
+                       $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;
+                               $mach->{macdef} = {} unless exists 
$mach->{macdef};
+                               $macdef = $mach->{machdef}{$value} = [];
+                       }
+               }
+       }
+
+       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";
+}
-- 
1.7.9.rc2

--
To unsubscribe from this list: send the line "unsubscribe git" in
the body of a message to majord...@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html

Reply via email to