contrib / credential / netrc / git-credential-netrcon commit Merge branch 'cc/tests-without-assuming-ref-files-backend' (d6e5484)
   1#!/usr/bin/perl
   2
   3use strict;
   4use warnings;
   5use autodie;
   6
   7use Getopt::Long;
   8use File::Basename;
   9use Git;
  10
  11my $VERSION = "0.2";
  12
  13my %options = (
  14               help => 0,
  15               debug => 0,
  16               verbose => 0,
  17               insecure => 0,
  18               file => [],
  19
  20               # identical token maps, e.g. host -> host, will be inserted later
  21               tmap => {
  22                        port => 'protocol',
  23                        machine => 'host',
  24                        path => 'path',
  25                        login => 'username',
  26                        user => 'username',
  27                        password => 'password',
  28                       }
  29              );
  30
  31# Map each credential protocol token to itself on the netrc side.
  32foreach (values %{$options{tmap}}) {
  33        $options{tmap}->{$_} = $_;
  34}
  35
  36# Now, $options{tmap} has a mapping from the netrc format to the Git credential
  37# helper protocol.
  38
  39# Next, we build the reverse token map.
  40
  41# When $rmap{foo} contains 'bar', that means that what the Git credential helper
  42# protocol calls 'bar' is found as 'foo' in the netrc/authinfo file.  Keys in
  43# %rmap are what we expect to read from the netrc/authinfo file.
  44
  45my %rmap;
  46foreach my $k (keys %{$options{tmap}}) {
  47        push @{$rmap{$options{tmap}->{$k}}}, $k;
  48}
  49
  50Getopt::Long::Configure("bundling");
  51
  52# TODO: maybe allow the token map $options{tmap} to be configurable.
  53GetOptions(\%options,
  54           "help|h",
  55           "debug|d",
  56           "insecure|k",
  57           "verbose|v",
  58           "file|f=s@",
  59           'gpg|g:s',
  60          );
  61
  62if ($options{help}) {
  63        my $shortname = basename($0);
  64        $shortname =~ s/git-credential-//;
  65
  66        print <<EOHIPPUS;
  67
  68$0 [(-f <authfile>)...] [-g <program>] [-d] [-v] [-k] get
  69
  70Version $VERSION by tzz\@lifelogs.com.  License: BSD.
  71
  72Options:
  73
  74  -f|--file <authfile>: specify netrc-style files.  Files with the .gpg
  75                        extension will be decrypted by GPG before parsing.
  76                        Multiple -f arguments are OK.  They are processed in
  77                        order, and the first matching entry found is returned
  78                        via the credential helper protocol (see below).
  79
  80                        When no -f option is given, .authinfo.gpg, .netrc.gpg,
  81                        .authinfo, and .netrc files in your home directory are
  82                        used in this order.
  83
  84  -g|--gpg <program>  : specify the program for GPG. By default, this is the
  85                        value of gpg.program in the git repository or global
  86                        option or gpg.
  87
  88  -k|--insecure       : ignore bad file ownership or permissions
  89
  90  -d|--debug          : turn on debugging (developer info)
  91
  92  -v|--verbose        : be more verbose (show files and information found)
  93
  94To enable this credential helper:
  95
  96  git config credential.helper '$shortname -f AUTHFILE1 -f AUTHFILE2'
  97
  98(Note that Git will prepend "git-credential-" to the helper name and look for it
  99in the path.)
 100
 101...and if you want lots of debugging info:
 102
 103  git config credential.helper '$shortname -f AUTHFILE -d'
 104
 105...or to see the files opened and data found:
 106
 107  git config credential.helper '$shortname -f AUTHFILE -v'
 108
 109Only "get" mode is supported by this credential helper.  It opens every
 110<authfile> and looks for the first entry that matches the requested search
 111criteria:
 112
 113 'port|protocol':
 114   The protocol that will be used (e.g., https). (protocol=X)
 115
 116 'machine|host':
 117   The remote hostname for a network credential. (host=X)
 118
 119 'path':
 120   The path with which the credential will be used. (path=X)
 121
 122 'login|user|username':
 123   The credential’s username, if we already have one. (username=X)
 124
 125Thus, when we get this query on STDIN:
 126
 127host=github.com
 128protocol=https
 129username=tzz
 130
 131this credential helper will look for the first entry in every <authfile> that
 132matches
 133
 134machine github.com port https login tzz
 135
 136OR
 137
 138machine github.com protocol https login tzz
 139
 140OR... etc. acceptable tokens as listed above.  Any unknown tokens are
 141simply ignored.
 142
 143Then, the helper will print out whatever tokens it got from the entry, including
 144"password" tokens, mapping back to Git's helper protocol; e.g. "port" is mapped
 145back to "protocol".  Any redundant entry tokens (part of the original query) are
 146skipped.
 147
 148Again, note that only the first matching entry from all the <authfile>s,
 149processed in the sequence given on the command line, is used.
 150
 151Netrc/authinfo tokens can be quoted as 'STRING' or "STRING".
 152
 153No caching is performed by this credential helper.
 154
 155EOHIPPUS
 156
 157        exit 0;
 158}
 159
 160my $mode = shift @ARGV;
 161
 162# Credentials must get a parameter, so die if it's missing.
 163die "Syntax: $0 [(-f <authfile>)...] [-d] get" unless defined $mode;
 164
 165# Only support 'get' mode; with any other unsupported ones we just exit.
 166exit 0 unless $mode eq 'get';
 167
 168my $files = $options{file};
 169
 170# if no files were given, use a predefined list.
 171# note that .gpg files come first
 172unless (scalar @$files) {
 173        my @candidates = qw[
 174                                   ~/.authinfo.gpg
 175                                   ~/.netrc.gpg
 176                                   ~/.authinfo
 177                                   ~/.netrc
 178                          ];
 179
 180        $files = $options{file} = [ map { glob $_ } @candidates ];
 181}
 182
 183load_config(\%options);
 184
 185my $query = read_credential_data_from_stdin();
 186
 187FILE:
 188foreach my $file (@$files) {
 189        my $gpgmode = $file =~ m/\.gpg$/;
 190        unless (-r $file) {
 191                log_verbose("Unable to read $file; skipping it");
 192                next FILE;
 193        }
 194
 195        # the following check is copied from Net::Netrc, for non-GPG files
 196        # OS/2 and Win32 do not handle stat in a way compatible with this check :-(
 197        unless ($gpgmode || $options{insecure} ||
 198                $^O eq 'os2'
 199                || $^O eq 'MSWin32'
 200                || $^O eq 'MacOS'
 201                || $^O =~ /^cygwin/) {
 202                my @stat = stat($file);
 203
 204                if (@stat) {
 205                        if ($stat[2] & 077) {
 206                                log_verbose("Insecure $file (mode=%04o); skipping it",
 207                                            $stat[2] & 07777);
 208                                next FILE;
 209                        }
 210
 211                        if ($stat[4] != $<) {
 212                                log_verbose("Not owner of $file; skipping it");
 213                                next FILE;
 214                        }
 215                }
 216        }
 217
 218        my @entries = load_netrc($file, $gpgmode);
 219
 220        unless (scalar @entries) {
 221                if ($!) {
 222                        log_verbose("Unable to open $file: $!");
 223                } else {
 224                        log_verbose("No netrc entries found in $file");
 225                }
 226
 227                next FILE;
 228        }
 229
 230        my $entry = find_netrc_entry($query, @entries);
 231        if ($entry) {
 232                print_credential_data($entry, $query);
 233                # we're done!
 234                last FILE;
 235        }
 236}
 237
 238exit 0;
 239
 240sub load_netrc {
 241        my $file = shift @_;
 242        my $gpgmode = shift @_;
 243
 244        my $io;
 245        if ($gpgmode) {
 246                my @cmd = ($options{'gpg'}, qw(--decrypt), $file);
 247                log_verbose("Using GPG to open $file: [@cmd]");
 248                open $io, "-|", @cmd;
 249        } else {
 250                log_verbose("Opening $file...");
 251                open $io, '<', $file;
 252        }
 253
 254        # nothing to do if the open failed (we log the error later)
 255        return unless $io;
 256
 257        # Net::Netrc does this, but the functionality is merged with the file
 258        # detection logic, so we have to extract just the part we need
 259        my @netrc_entries = net_netrc_loader($io);
 260
 261        # these entries will use the credential helper protocol token names
 262        my @entries;
 263
 264        foreach my $nentry (@netrc_entries) {
 265                my %entry;
 266                my $num_port;
 267
 268                if (!defined $nentry->{machine}) {
 269                        next;
 270                }
 271                if (defined $nentry->{port} && $nentry->{port} =~ m/^\d+$/) {
 272                        $num_port = $nentry->{port};
 273                        delete $nentry->{port};
 274                }
 275
 276                # create the new entry for the credential helper protocol
 277                $entry{$options{tmap}->{$_}} = $nentry->{$_} foreach keys %$nentry;
 278
 279                # for "host X port Y" where Y is an integer (captured by
 280                # $num_port above), set the host to "X:Y"
 281                if (defined $entry{host} && defined $num_port) {
 282                        $entry{host} = join(':', $entry{host}, $num_port);
 283                }
 284
 285                push @entries, \%entry;
 286        }
 287
 288        return @entries;
 289}
 290
 291sub net_netrc_loader {
 292        my $fh = shift @_;
 293        my @entries;
 294        my ($mach, $macdef, $tok, @tok);
 295
 296    LINE:
 297        while (<$fh>) {
 298                undef $macdef if /\A\n\Z/;
 299
 300                if ($macdef) {
 301                        next LINE;
 302                }
 303
 304                s/^\s*//;
 305                chomp;
 306
 307                while (length && s/^("((?:[^"]+|\\.)*)"|((?:[^\\\s]+|\\.)*))\s*//) {
 308                        (my $tok = $+) =~ s/\\(.)/$1/g;
 309                        push(@tok, $tok);
 310                }
 311
 312            TOKEN:
 313                while (@tok) {
 314                        if ($tok[0] eq "default") {
 315                                shift(@tok);
 316                                $mach = { machine => undef };
 317                                next TOKEN;
 318                        }
 319
 320                        $tok = shift(@tok);
 321
 322                        if ($tok eq "machine") {
 323                                my $host = shift @tok;
 324                                $mach = { machine => $host };
 325                                push @entries, $mach;
 326                        } elsif (exists $options{tmap}->{$tok}) {
 327                                unless ($mach) {
 328                                        log_debug("Skipping token $tok because no machine was given");
 329                                        next TOKEN;
 330                                }
 331
 332                                my $value = shift @tok;
 333                                unless (defined $value) {
 334                                        log_debug("Token $tok had no value, skipping it.");
 335                                        next TOKEN;
 336                                }
 337
 338                                # Following line added by rmerrell to remove '/' escape char in .netrc
 339                                $value =~ s/\/\\/\\/g;
 340                                $mach->{$tok} = $value;
 341                        } elsif ($tok eq "macdef") { # we ignore macros
 342                                next TOKEN unless $mach;
 343                                my $value = shift @tok;
 344                                $macdef = 1;
 345                        }
 346                }
 347        }
 348
 349        return @entries;
 350}
 351
 352sub read_credential_data_from_stdin {
 353        # the query: start with every token with no value
 354        my %q = map { $_ => undef } values(%{$options{tmap}});
 355
 356        while (<STDIN>) {
 357                next unless m/^([^=]+)=(.+)/;
 358
 359                my ($token, $value) = ($1, $2);
 360                die "Unknown search token $token" unless exists $q{$token};
 361                $q{$token} = $value;
 362                log_debug("We were given search token $token and value $value");
 363        }
 364
 365        foreach (sort keys %q) {
 366                log_debug("Searching for %s = %s", $_, $q{$_} || '(any value)');
 367        }
 368
 369        return \%q;
 370}
 371
 372# takes the search tokens and then a list of entries
 373# each entry is a hash reference
 374sub find_netrc_entry {
 375        my $query = shift @_;
 376
 377    ENTRY:
 378        foreach my $entry (@_)
 379        {
 380                my $entry_text = join ', ', map { "$_=$entry->{$_}" } keys %$entry;
 381                foreach my $check (sort keys %$query) {
 382                        if (!defined $entry->{$check}) {
 383                                log_debug("OK: entry has no $check token, so any value satisfies check $check");
 384                        } elsif (defined $query->{$check}) {
 385                                log_debug("compare %s [%s] to [%s] (entry: %s)",
 386                                          $check,
 387                                          $entry->{$check},
 388                                          $query->{$check},
 389                                          $entry_text);
 390                                unless ($query->{$check} eq $entry->{$check}) {
 391                                        next ENTRY;
 392                                }
 393                        } else {
 394                                log_debug("OK: any value satisfies check $check");
 395                        }
 396                }
 397
 398                return $entry;
 399        }
 400
 401        # nothing was found
 402        return;
 403}
 404
 405sub print_credential_data {
 406        my $entry = shift @_;
 407        my $query = shift @_;
 408
 409        log_debug("entry has passed all the search checks");
 410 TOKEN:
 411        foreach my $git_token (sort keys %$entry) {
 412                log_debug("looking for useful token $git_token");
 413                # don't print unknown (to the credential helper protocol) tokens
 414                next TOKEN unless exists $query->{$git_token};
 415
 416                # don't print things asked in the query (the entry matches them)
 417                next TOKEN if defined $query->{$git_token};
 418
 419                log_debug("FOUND: $git_token=$entry->{$git_token}");
 420                printf "%s=%s\n", $git_token, $entry->{$git_token};
 421        }
 422}
 423sub load_config {
 424        # load settings from git config
 425        my $options = shift;
 426        # set from command argument, gpg.program option, or default to gpg
 427        $options->{'gpg'} //= Git->repository()->config('gpg.program')
 428                          // 'gpg';
 429        log_verbose("using $options{'gpg'} for GPG operations");
 430}
 431sub log_verbose {
 432        return unless $options{verbose};
 433        printf STDERR @_;
 434        printf STDERR "\n";
 435}
 436
 437sub log_debug {
 438        return unless $options{debug};
 439        printf STDERR @_;
 440        printf STDERR "\n";
 441}