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; 46foreachmy$k(keys%{$options{tmap}}) { 47push@{$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}) { 63my$shortname= basename($0); 64$shortname=~s/git-credential-//; 65 66print<<EOHIPPUS; 67 68$0[(-f <authfile>)...] [-g <program>] [-d] [-v] [-k] get 69 70Version$VERSIONby 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 157exit0; 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"unlessdefined$mode; 164 165# Only support 'get' mode; with any other unsupported ones we just exit. 166exit0unless$modeeq'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) { 173my@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: 188foreachmy$file(@$files) { 189my$gpgmode=$file=~m/\.gpg$/; 190unless(-r $file) { 191 log_verbose("Unable to read$file; skipping it"); 192next 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 :-( 197unless($gpgmode||$options{insecure} || 198$^Oeq'os2' 199||$^Oeq'MSWin32' 200||$^Oeq'MacOS' 201||$^O=~/^cygwin/) { 202my@stat=stat($file); 203 204if(@stat) { 205if($stat[2] &077) { 206 log_verbose("Insecure$file(mode=%04o); skipping it", 207$stat[2] &07777); 208next FILE; 209} 210 211if($stat[4] != $<) { 212 log_verbose("Not owner of$file; skipping it"); 213next FILE; 214} 215} 216} 217 218my@entries= load_netrc($file,$gpgmode); 219 220unless(scalar@entries) { 221if($!) { 222 log_verbose("Unable to open$file:$!"); 223}else{ 224 log_verbose("No netrc entries found in$file"); 225} 226 227next FILE; 228} 229 230my$entry= find_netrc_entry($query,@entries); 231if($entry) { 232 print_credential_data($entry,$query); 233# we're done! 234last FILE; 235} 236} 237 238exit0; 239 240sub load_netrc { 241my$file=shift@_; 242my$gpgmode=shift@_; 243 244my$io; 245if($gpgmode) { 246my@cmd= ($options{'gpg'},qw(--decrypt),$file); 247 log_verbose("Using GPG to open$file: [@cmd]"); 248open$io,"-|",@cmd; 249}else{ 250 log_verbose("Opening$file..."); 251open$io,'<',$file; 252} 253 254# nothing to do if the open failed (we log the error later) 255return 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 259my@netrc_entries= net_netrc_loader($io); 260 261# these entries will use the credential helper protocol token names 262my@entries; 263 264foreachmy$nentry(@netrc_entries) { 265my%entry; 266my$num_port; 267 268if(!defined$nentry->{machine}) { 269next; 270} 271if(defined$nentry->{port} &&$nentry->{port} =~m/^\d+$/) { 272$num_port=$nentry->{port}; 273delete$nentry->{port}; 274} 275 276# create the new entry for the credential helper protocol 277$entry{$options{tmap}->{$_}} =$nentry->{$_}foreachkeys%$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" 281if(defined$entry{host} &&defined$num_port) { 282$entry{host} =join(':',$entry{host},$num_port); 283} 284 285push@entries, \%entry; 286} 287 288return@entries; 289} 290 291sub net_netrc_loader { 292my$fh=shift@_; 293my@entries; 294my($mach,$macdef,$tok,@tok); 295 296 LINE: 297while(<$fh>) { 298undef$macdefif/\A\n\Z/; 299 300if($macdef) { 301next LINE; 302} 303 304s/^\s*//; 305chomp; 306 307while(length&&s/^("((?:[^"]+|\\.)*)"|((?:[^\\\s]+|\\.)*))\s*//) { 308(my$tok=$+) =~s/\\(.)/$1/g; 309push(@tok,$tok); 310} 311 312 TOKEN: 313while(@tok) { 314if($tok[0]eq"default") { 315shift(@tok); 316$mach= { machine =>undef}; 317next TOKEN; 318} 319 320$tok=shift(@tok); 321 322if($tokeq"machine") { 323my$host=shift@tok; 324$mach= { machine =>$host}; 325push@entries,$mach; 326}elsif(exists$options{tmap}->{$tok}) { 327unless($mach) { 328 log_debug("Skipping token$tokbecause no machine was given"); 329next TOKEN; 330} 331 332my$value=shift@tok; 333unless(defined$value) { 334 log_debug("Token$tokhad no value, skipping it."); 335next 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($tokeq"macdef") {# we ignore macros 342next TOKEN unless$mach; 343my$value=shift@tok; 344$macdef=1; 345} 346} 347} 348 349return@entries; 350} 351 352sub read_credential_data_from_stdin { 353# the query: start with every token with no value 354my%q=map{$_=>undef}values(%{$options{tmap}}); 355 356while(<STDIN>) { 357next unlessm/^([^=]+)=(.+)/; 358 359my($token,$value) = ($1,$2); 360die"Unknown search token$token"unlessexists$q{$token}; 361$q{$token} =$value; 362 log_debug("We were given search token$tokenand value$value"); 363} 364 365foreach(sort keys%q) { 366 log_debug("Searching for%s=%s",$_,$q{$_} ||'(any value)'); 367} 368 369return \%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 { 375my$query=shift@_; 376 377 ENTRY: 378foreachmy$entry(@_) 379{ 380my$entry_text=join', ',map{"$_=$entry->{$_}"}keys%$entry; 381foreachmy$check(sort keys%$query) { 382if(!defined$entry->{$check}) { 383 log_debug("OK: entry has no$checktoken, 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); 390unless($query->{$check}eq$entry->{$check}) { 391next ENTRY; 392} 393}else{ 394 log_debug("OK: any value satisfies check$check"); 395} 396} 397 398return$entry; 399} 400 401# nothing was found 402return; 403} 404 405sub print_credential_data { 406my$entry=shift@_; 407my$query=shift@_; 408 409 log_debug("entry has passed all the search checks"); 410 TOKEN: 411foreachmy$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 414next TOKEN unlessexists$query->{$git_token}; 415 416# don't print things asked in the query (the entry matches them) 417next TOKEN ifdefined$query->{$git_token}; 418 419 log_debug("FOUND:$git_token=$entry->{$git_token}"); 420printf"%s=%s\n",$git_token,$entry->{$git_token}; 421} 422} 423sub load_config { 424# load settings from git config 425my$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 { 432return unless$options{verbose}; 433printf STDERR @_; 434printf STDERR "\n"; 435} 436 437sub log_debug { 438return unless$options{debug}; 439printf STDERR @_; 440printf STDERR "\n"; 441}