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