Merge branch 'tg/doc-sec-list'
[git] / contrib / credential / netrc / git-credential-netrc
1 #!/usr/bin/perl
2
3 use strict;
4 use warnings;
5 use autodie;
6
7 use Getopt::Long;
8 use File::Basename;
9 use Git;
10
11 my $VERSION = "0.2";
12
13 my %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.
32 foreach (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
45 my %rmap;
46 foreach my $k (keys %{$options{tmap}}) {
47         push @{$rmap{$options{tmap}->{$k}}}, $k;
48 }
49
50 Getopt::Long::Configure("bundling");
51
52 # TODO: maybe allow the token map $options{tmap} to be configurable.
53 GetOptions(\%options,
54            "help|h",
55            "debug|d",
56            "insecure|k",
57            "verbose|v",
58            "file|f=s@",
59            'gpg|g:s',
60           );
61
62 if ($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
70 Version $VERSION by tzz\@lifelogs.com.  License: BSD.
71
72 Options:
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
94 To 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
99 in 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
109 Only "get" mode is supported by this credential helper.  It opens every
110 <authfile> and looks for the first entry that matches the requested search
111 criteria:
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
125 Thus, when we get this query on STDIN:
126
127 host=github.com
128 protocol=https
129 username=tzz
130
131 this credential helper will look for the first entry in every <authfile> that
132 matches
133
134 machine github.com port https login tzz
135
136 OR
137
138 machine github.com protocol https login tzz
139
140 OR... etc. acceptable tokens as listed above.  Any unknown tokens are
141 simply ignored.
142
143 Then, 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
145 back to "protocol".  Any redundant entry tokens (part of the original query) are
146 skipped.
147
148 Again, note that only the first matching entry from all the <authfile>s,
149 processed in the sequence given on the command line, is used.
150
151 Netrc/authinfo tokens can be quoted as 'STRING' or "STRING".
152
153 No caching is performed by this credential helper.
154
155 EOHIPPUS
156
157         exit 0;
158 }
159
160 my $mode = shift @ARGV;
161
162 # Credentials must get a parameter, so die if it's missing.
163 die "Syntax: $0 [(-f <authfile>)...] [-d] get" unless defined $mode;
164
165 # Only support 'get' mode; with any other unsupported ones we just exit.
166 exit 0 unless $mode eq 'get';
167
168 my $files = $options{file};
169
170 # if no files were given, use a predefined list.
171 # note that .gpg files come first
172 unless (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
183 load_config(\%options);
184
185 my $query = read_credential_data_from_stdin();
186
187 FILE:
188 foreach 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
238 exit 0;
239
240 sub 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
291 sub 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
352 sub 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
374 sub 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
405 sub 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 }
423 sub 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 }
431 sub log_verbose {
432         return unless $options{verbose};
433         printf STDERR @_;
434         printf STDERR "\n";
435 }
436
437 sub log_debug {
438         return unless $options{debug};
439         printf STDERR @_;
440         printf STDERR "\n";
441 }