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