1#!/usr/bin/perl 2 3use strict; 4use warnings; 5 6use Getopt::Long; 7use File::Basename; 8use Git; 9 10my$VERSION="0.2"; 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; 45foreachmy$k(keys%{$options{tmap}}) { 46push@{$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@", 58'gpg|g:s', 59); 60 61if($options{help}) { 62my$shortname= basename($0); 63$shortname=~s/git-credential-//; 64 65print<<EOHIPPUS; 66 67$0[(-f <authfile>)...] [-g <program>] [-d] [-v] [-k] get 68 69Version$VERSIONby tzz\@lifelogs.com. License: BSD. 70 71Options: 72 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). 78 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. 82 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. 86 87 -k|--insecure : ignore bad file ownership or permissions 88 89 -d|--debug : turn on debugging (developer info) 90 91 -v|--verbose : be more verbose (show files and information found) 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 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: 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 130this credential helper will look for the first entry in every <authfile> that 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 147Again, note that only the first matching entry from all the <authfile>s, 148processed in the sequence given on the command line, is used. 149 150Netrc/authinfo tokens can be quoted as 'STRING' or "STRING". 151 152No caching is performed by this credential helper. 153 154EOHIPPUS 155 156exit0; 157} 158 159my$mode=shift@ARGV; 160 161# Credentials must get a parameter, so die if it's missing. 162die"Syntax:$0[(-f <authfile>)...] [-d] get"unlessdefined$mode; 163 164# Only support 'get' mode; with any other unsupported ones we just exit. 165exit0unless$modeeq'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) { 172my@candidates=qw[ 173 ~/.authinfo.gpg 174 ~/.netrc.gpg 175 ~/.authinfo 176 ~/.netrc 177 ]; 178 179$files=$options{file} = [map{glob$_}@candidates]; 180} 181 182load_config(\%options); 183 184my$query= read_credential_data_from_stdin(); 185 186FILE: 187foreachmy$file(@$files) { 188my$gpgmode=$file=~m/\.gpg$/; 189unless(-r $file) { 190 log_verbose("Unable to read$file; skipping it"); 191next 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 :-( 196unless($gpgmode||$options{insecure} || 197$^Oeq'os2' 198||$^Oeq'MSWin32' 199||$^Oeq'MacOS' 200||$^O=~/^cygwin/) { 201my@stat=stat($file); 202 203if(@stat) { 204if($stat[2] &077) { 205 log_verbose("Insecure$file(mode=%04o); skipping it", 206$stat[2] &07777); 207next FILE; 208} 209 210if($stat[4] != $<) { 211 log_verbose("Not owner of$file; skipping it"); 212next FILE; 213} 214} 215} 216 217my@entries= load_netrc($file,$gpgmode); 218 219unless(scalar@entries) { 220if($!) { 221 log_verbose("Unable to open$file:$!"); 222}else{ 223 log_verbose("No netrc entries found in$file"); 224} 225 226next FILE; 227} 228 229my$entry= find_netrc_entry($query,@entries); 230if($entry) { 231 print_credential_data($entry,$query); 232# we're done! 233last FILE; 234} 235} 236 237exit0; 238 239sub load_netrc { 240my$file=shift@_; 241my$gpgmode=shift@_; 242 243my$io; 244if($gpgmode) { 245my@cmd= ($options{'gpg'},qw(--decrypt),$file); 246 log_verbose("Using GPG to open$file: [@cmd]"); 247open$io,"-|",@cmd; 248}else{ 249 log_verbose("Opening$file..."); 250open$io,'<',$file; 251} 252 253# nothing to do if the open failed (we log the error later) 254return 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 258my@netrc_entries= net_netrc_loader($io); 259 260# these entries will use the credential helper protocol token names 261my@entries; 262 263foreachmy$nentry(@netrc_entries) { 264my%entry; 265my$num_port; 266 267if(!defined$nentry->{machine}) { 268next; 269} 270if(defined$nentry->{port} &&$nentry->{port} =~m/^\d+$/) { 271$num_port=$nentry->{port}; 272delete$nentry->{port}; 273} 274 275# create the new entry for the credential helper protocol 276$entry{$options{tmap}->{$_}} =$nentry->{$_}foreachkeys%$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" 280if(defined$entry{host} &&defined$num_port) { 281$entry{host} =join(':',$entry{host},$num_port); 282} 283 284push@entries, \%entry; 285} 286 287return@entries; 288} 289 290sub net_netrc_loader { 291my$fh=shift@_; 292my@entries; 293my($mach,$macdef,$tok,@tok); 294 295 LINE: 296while(<$fh>) { 297undef$macdefif/\A\n\Z/; 298 299if($macdef) { 300next LINE; 301} 302 303s/^\s*//; 304chomp; 305 306while(length&&s/^("((?:[^"]+|\\.)*)"|((?:[^\\\s]+|\\.)*))\s*//) { 307(my$tok=$+) =~s/\\(.)/$1/g; 308push(@tok,$tok); 309} 310 311 TOKEN: 312while(@tok) { 313if($tok[0]eq"default") { 314shift(@tok); 315$mach= { machine =>undef}; 316next TOKEN; 317} 318 319$tok=shift(@tok); 320 321if($tokeq"machine") { 322my$host=shift@tok; 323$mach= { machine =>$host}; 324push@entries,$mach; 325}elsif(exists$options{tmap}->{$tok}) { 326unless($mach) { 327 log_debug("Skipping token$tokbecause no machine was given"); 328next TOKEN; 329} 330 331my$value=shift@tok; 332unless(defined$value) { 333 log_debug("Token$tokhad no value, skipping it."); 334next 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($tokeq"macdef") {# we ignore macros 341next TOKEN unless$mach; 342my$value=shift@tok; 343$macdef=1; 344} 345} 346} 347 348return@entries; 349} 350 351sub read_credential_data_from_stdin { 352# the query: start with every token with no value 353my%q=map{$_=>undef}values(%{$options{tmap}}); 354 355while(<STDIN>) { 356next unlessm/^([^=]+)=(.+)/; 357 358my($token,$value) = ($1,$2); 359die"Unknown search token$token"unlessexists$q{$token}; 360$q{$token} =$value; 361 log_debug("We were given search token$tokenand value$value"); 362} 363 364foreach(sort keys%q) { 365 log_debug("Searching for%s=%s",$_,$q{$_} ||'(any value)'); 366} 367 368return \%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 { 374my$query=shift@_; 375 376 ENTRY: 377foreachmy$entry(@_) 378{ 379my$entry_text=join', ',map{"$_=$entry->{$_}"}keys%$entry; 380foreachmy$check(sort keys%$query) { 381if(!defined$entry->{$check}) { 382 log_debug("OK: entry has no$checktoken, so any value satisfies check$check"); 383}elsif(defined$query->{$check}) { 384 log_debug("compare%s[%s] to [%s] (entry:%s)", 385$check, 386$entry->{$check}, 387$query->{$check}, 388$entry_text); 389unless($query->{$check}eq$entry->{$check}) { 390next ENTRY; 391} 392}else{ 393 log_debug("OK: any value satisfies check$check"); 394} 395} 396 397return$entry; 398} 399 400# nothing was found 401return; 402} 403 404sub print_credential_data { 405my$entry=shift@_; 406my$query=shift@_; 407 408 log_debug("entry has passed all the search checks"); 409 TOKEN: 410foreachmy$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 413next TOKEN unlessexists$query->{$git_token}; 414 415# don't print things asked in the query (the entry matches them) 416next TOKEN ifdefined$query->{$git_token}; 417 418 log_debug("FOUND:$git_token=$entry->{$git_token}"); 419printf"%s=%s\n",$git_token,$entry->{$git_token}; 420} 421} 422sub load_config { 423# load settings from git config 424my$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} 430sub log_verbose { 431return unless$options{verbose}; 432printf STDERR @_; 433printf STDERR "\n"; 434} 435 436sub log_debug { 437return unless$options{debug}; 438printf STDERR @_; 439printf STDERR "\n"; 440}