Commit | Line | Data |
---|---|---|
54829209 TZ |
1 | #!/usr/bin/perl |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | ||
6 | use Getopt::Long; | |
7 | use File::Basename; | |
786ef50a | 8 | use Git; |
54829209 | 9 | |
786ef50a | 10 | my $VERSION = "0.2"; |
54829209 TZ |
11 | |
12 | my %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. | |
31 | foreach (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 | ||
44 | my %rmap; | |
45 | foreach my $k (keys %{$options{tmap}}) { | |
46 | push @{$rmap{$options{tmap}->{$k}}}, $k; | |
47 | } | |
48 | ||
49 | Getopt::Long::Configure("bundling"); | |
50 | ||
51 | # TODO: maybe allow the token map $options{tmap} to be configurable. | |
52 | GetOptions(\%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 | ||
61 | if ($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 | |
69 | Version $VERSION by tzz\@lifelogs.com. License: BSD. | |
70 | ||
71 | Options: | |
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 | |
93 | To 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 | |
98 | in 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 |
108 | Only "get" mode is supported by this credential helper. It opens every |
109 | <authfile> and looks for the first entry that matches the requested search | |
110 | criteria: | |
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 | ||
124 | Thus, when we get this query on STDIN: | |
125 | ||
126 | host=github.com | |
127 | protocol=https | |
128 | username=tzz | |
129 | ||
786ef50a | 130 | this credential helper will look for the first entry in every <authfile> that |
54829209 TZ |
131 | matches |
132 | ||
133 | machine github.com port https login tzz | |
134 | ||
135 | OR | |
136 | ||
137 | machine github.com protocol https login tzz | |
138 | ||
139 | OR... etc. acceptable tokens as listed above. Any unknown tokens are | |
140 | simply ignored. | |
141 | ||
142 | Then, 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 | |
144 | back to "protocol". Any redundant entry tokens (part of the original query) are | |
145 | skipped. | |
146 | ||
786ef50a LM |
147 | Again, note that only the first matching entry from all the <authfile>s, |
148 | processed in the sequence given on the command line, is used. | |
54829209 TZ |
149 | |
150 | Netrc/authinfo tokens can be quoted as 'STRING' or "STRING". | |
151 | ||
152 | No caching is performed by this credential helper. | |
153 | ||
154 | EOHIPPUS | |
155 | ||
156 | exit 0; | |
157 | } | |
158 | ||
159 | my $mode = shift @ARGV; | |
160 | ||
161 | # Credentials must get a parameter, so die if it's missing. | |
786ef50a | 162 | die "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. | |
165 | exit 0 unless $mode eq 'get'; | |
166 | ||
167 | my $files = $options{file}; | |
168 | ||
169 | # if no files were given, use a predefined list. | |
170 | # note that .gpg files come first | |
171 | unless (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 |
182 | load_config(\%options); |
183 | ||
54829209 TZ |
184 | my $query = read_credential_data_from_stdin(); |
185 | ||
186 | FILE: | |
187 | foreach 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 | ||
237 | exit 0; | |
238 | ||
239 | sub 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 | ||
290 | sub 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 | ||
351 | sub 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 | |
373 | sub 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 | ||
404 | sub 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 |
422 | sub 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 |
430 | sub log_verbose { |
431 | return unless $options{verbose}; | |
432 | printf STDERR @_; | |
433 | printf STDERR "\n"; | |
434 | } | |
435 | ||
436 | sub log_debug { | |
437 | return unless $options{debug}; | |
438 | printf STDERR @_; | |
439 | printf STDERR "\n"; | |
440 | } |