Skip to content

Commit

Permalink
Merge branch 'mn/send-email-works-with-credential' into next
Browse files Browse the repository at this point in the history
Hooks the credential system to send-email.

* mn/send-email-works-with-credential:
  git-send-email: use git credential to obtain password
  Git.pm: add interface for git credential command
  Git.pm: allow pipes to be closed prior to calling command_close_bidi_pipe
  Git.pm: refactor command_close_bidi_pipe to use _cmd_close
  Git.pm: fix example in command_close_bidi_pipe documentation
  Git.pm: allow command_close_bidi_pipe to be called as method
  • Loading branch information
gitster committed Mar 19, 2013
2 parents 1a68953 + 4d31a44 commit 2daad63
Show file tree
Hide file tree
Showing 3 changed files with 222 additions and 51 deletions.
4 changes: 2 additions & 2 deletions Documentation/git-send-email.txt
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -164,8 +164,8 @@ Sending
Furthermore, passwords need not be specified in configuration files Furthermore, passwords need not be specified in configuration files
or on the command line. If a username has been specified (with or on the command line. If a username has been specified (with
'--smtp-user' or a 'sendemail.smtpuser'), but no password has been '--smtp-user' or a 'sendemail.smtpuser'), but no password has been
specified (with '--smtp-pass' or 'sendemail.smtppass'), then the specified (with '--smtp-pass' or 'sendemail.smtppass'), then
user is prompted for a password while the input is masked for privacy. a password is obtained using 'git-credential'.


--smtp-server=<host>:: --smtp-server=<host>::
If set, specifies the outgoing SMTP server to use (e.g. If set, specifies the outgoing SMTP server to use (e.g.
Expand Down
71 changes: 43 additions & 28 deletions git-send-email.perl
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -1045,6 +1045,47 @@ sub maildomain {
return maildomain_net() || maildomain_mta() || 'localhost.localdomain'; return maildomain_net() || maildomain_mta() || 'localhost.localdomain';
} }


sub smtp_host_string {
if (defined $smtp_server_port) {
return "$smtp_server:$smtp_server_port";
} else {
return $smtp_server;
}
}

# Returns 1 if authentication succeeded or was not necessary
# (smtp_user was not specified), and 0 otherwise.

sub smtp_auth_maybe {
if (!defined $smtp_authuser || $auth) {
return 1;
}

# Workaround AUTH PLAIN/LOGIN interaction defect
# with Authen::SASL::Cyrus
eval {
require Authen::SASL;
Authen::SASL->import(qw(Perl));
};

# TODO: Authentication may fail not because credentials were
# invalid but due to other reasons, in which we should not
# reject credentials.
$auth = Git::credential({
'protocol' => 'smtp',
'host' => smtp_host_string(),
'username' => $smtp_authuser,
# if there's no password, "git credential fill" will
# give us one, otherwise it'll just pass this one.
'password' => $smtp_authpass
}, sub {
my $cred = shift;
return !!$smtp->auth($cred->{'username'}, $cred->{'password'});
});

return $auth;
}

# Returns 1 if the message was sent, and 0 otherwise. # Returns 1 if the message was sent, and 0 otherwise.
# In actuality, the whole program dies when there # In actuality, the whole program dies when there
# is an error sending a message. # is an error sending a message.
Expand Down Expand Up @@ -1155,9 +1196,7 @@ sub send_message {
else { else {
require Net::SMTP; require Net::SMTP;
$smtp_domain ||= maildomain(); $smtp_domain ||= maildomain();
$smtp ||= Net::SMTP->new((defined $smtp_server_port) $smtp ||= Net::SMTP->new(smtp_host_string(),
? "$smtp_server:$smtp_server_port"
: $smtp_server,
Hello => $smtp_domain, Hello => $smtp_domain,
Debug => $debug_net_smtp); Debug => $debug_net_smtp);
if ($smtp_encryption eq 'tls' && $smtp) { if ($smtp_encryption eq 'tls' && $smtp) {
Expand Down Expand Up @@ -1185,31 +1224,7 @@ sub send_message {
defined $smtp_server_port ? " port=$smtp_server_port" : ""; defined $smtp_server_port ? " port=$smtp_server_port" : "";
} }


if (defined $smtp_authuser) { smtp_auth_maybe or die $smtp->message;
# Workaround AUTH PLAIN/LOGIN interaction defect
# with Authen::SASL::Cyrus
eval {
require Authen::SASL;
Authen::SASL->import(qw(Perl));
};

if (!defined $smtp_authpass) {

system "stty -echo";

do {
print "Password: ";
$_ = <STDIN>;
print "\n";
} while (!defined $_);

chomp($smtp_authpass = $_);

system "stty echo";
}

$auth ||= $smtp->auth( $smtp_authuser, $smtp_authpass ) or die $smtp->message;
}


$smtp->mail( $raw_from ) or die $smtp->message; $smtp->mail( $raw_from ) or die $smtp->message;
$smtp->to( @recipients ) or die $smtp->message; $smtp->to( @recipients ) or die $smtp->message;
Expand Down
198 changes: 177 additions & 21 deletions perl/Git.pm
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ require Exporter;
version exec_path html_path hash_object git_cmd_try version exec_path html_path hash_object git_cmd_try
remote_refs prompt remote_refs prompt
get_tz_offset get_tz_offset
credential credential_read credential_write
temp_acquire temp_release temp_reset temp_path); temp_acquire temp_release temp_reset temp_path);




Expand Down Expand Up @@ -269,13 +270,13 @@ sub command {


if (not defined wantarray) { if (not defined wantarray) {
# Nothing to pepper the possible exception with. # Nothing to pepper the possible exception with.
_cmd_close($fh, $ctx); _cmd_close($ctx, $fh);


} elsif (not wantarray) { } elsif (not wantarray) {
local $/; local $/;
my $text = <$fh>; my $text = <$fh>;
try { try {
_cmd_close($fh, $ctx); _cmd_close($ctx, $fh);
} catch Git::Error::Command with { } catch Git::Error::Command with {
# Pepper with the output: # Pepper with the output:
my $E = shift; my $E = shift;
Expand All @@ -288,7 +289,7 @@ sub command {
my @lines = <$fh>; my @lines = <$fh>;
defined and chomp for @lines; defined and chomp for @lines;
try { try {
_cmd_close($fh, $ctx); _cmd_close($ctx, $fh);
} catch Git::Error::Command with { } catch Git::Error::Command with {
my $E = shift; my $E = shift;
$E->{'-outputref'} = \@lines; $E->{'-outputref'} = \@lines;
Expand All @@ -315,7 +316,7 @@ sub command_oneline {
my $line = <$fh>; my $line = <$fh>;
defined $line and chomp $line; defined $line and chomp $line;
try { try {
_cmd_close($fh, $ctx); _cmd_close($ctx, $fh);
} catch Git::Error::Command with { } catch Git::Error::Command with {
# Pepper with the output: # Pepper with the output:
my $E = shift; my $E = shift;
Expand Down Expand Up @@ -383,7 +384,7 @@ have more complicated structure.
sub command_close_pipe { sub command_close_pipe {
my ($self, $fh, $ctx) = _maybe_self(@_); my ($self, $fh, $ctx) = _maybe_self(@_);
$ctx ||= '<unknown>'; $ctx ||= '<unknown>';
_cmd_close($fh, $ctx); _cmd_close($ctx, $fh);
} }


=item command_bidi_pipe ( COMMAND [, ARGUMENTS... ] ) =item command_bidi_pipe ( COMMAND [, ARGUMENTS... ] )
Expand Down Expand Up @@ -420,31 +421,34 @@ and it is the fourth value returned by C<command_bidi_pipe()>. The call idiom
is: is:
my ($pid, $in, $out, $ctx) = $r->command_bidi_pipe('cat-file --batch-check'); my ($pid, $in, $out, $ctx) = $r->command_bidi_pipe('cat-file --batch-check');
print "000000000\n" $out; print $out "000000000\n";
while (<$in>) { ... } while (<$in>) { ... }
$r->command_close_bidi_pipe($pid, $in, $out, $ctx); $r->command_close_bidi_pipe($pid, $in, $out, $ctx);
Note that you should not rely on whatever actually is in C<CTX>; Note that you should not rely on whatever actually is in C<CTX>;
currently it is simply the command name but in future the context might currently it is simply the command name but in future the context might
have more complicated structure. have more complicated structure.
C<PIPE_IN> and C<PIPE_OUT> may be C<undef> if they have been closed prior to
calling this function. This may be useful in a query-response type of
commands where caller first writes a query and later reads response, eg:
my ($pid, $in, $out, $ctx) = $r->command_bidi_pipe('cat-file --batch-check');
print $out "000000000\n";
close $out;
while (<$in>) { ... }
$r->command_close_bidi_pipe($pid, $in, undef, $ctx);
This idiom may prevent potential dead locks caused by data sent to the output
pipe not being flushed and thus not reaching the executed command.
=cut =cut


sub command_close_bidi_pipe { sub command_close_bidi_pipe {
local $?; local $?;
my ($pid, $in, $out, $ctx) = @_; my ($self, $pid, $in, $out, $ctx) = _maybe_self(@_);
foreach my $fh ($in, $out) { _cmd_close($ctx, (grep { defined } ($in, $out)));
unless (close $fh) {
if ($!) {
carp "error closing pipe: $!";
} elsif ($? >> 8) {
throw Git::Error::Command($ctx, $? >>8);
}
}
}

waitpid $pid, 0; waitpid $pid, 0;

if ($? >> 8) { if ($? >> 8) {
throw Git::Error::Command($ctx, $? >>8); throw Git::Error::Command($ctx, $? >>8);
} }
Expand Down Expand Up @@ -1020,6 +1024,156 @@ sub _close_cat_blob {
} }




=item credential_read( FILEHANDLE )
Reads credential key-value pairs from C<FILEHANDLE>. Reading stops at EOF or
when an empty line is encountered. Each line must be of the form C<key=value>
with a non-empty key. Function returns hash with all read values. Any white
space (other than new-line character) is preserved.
=cut

sub credential_read {
my ($self, $reader) = _maybe_self(@_);
my %credential;
while (<$reader>) {
chomp;
if ($_ eq '') {
last;
} elsif (!/^([^=]+)=(.*)$/) {
throw Error::Simple("unable to parse git credential data:\n$_");
}
$credential{$1} = $2;
}
return %credential;
}

=item credential_write( FILEHANDLE, CREDENTIAL_HASHREF )
Writes credential key-value pairs from hash referenced by
C<CREDENTIAL_HASHREF> to C<FILEHANDLE>. Keys and values cannot contain
new-lines or NUL bytes characters, and key cannot contain equal signs nor be
empty (if they do Error::Simple is thrown). Any white space is preserved. If
value for a key is C<undef>, it will be skipped.
If C<'url'> key exists it will be written first. (All the other key-value
pairs are written in sorted order but you should not depend on that). Once
all lines are written, an empty line is printed.
=cut

sub credential_write {
my ($self, $writer, $credential) = _maybe_self(@_);
my ($key, $value);

# Check if $credential is valid prior to writing anything
while (($key, $value) = each %$credential) {
if (!defined $key || !length $key) {
throw Error::Simple("credential key empty or undefined");
} elsif ($key =~ /[=\n\0]/) {
throw Error::Simple("credential key contains invalid characters: $key");
} elsif (defined $value && $value =~ /[\n\0]/) {
throw Error::Simple("credential value for key=$key contains invalid characters: $value");
}
}

for $key (sort {
# url overwrites other fields, so it must come first
return -1 if $a eq 'url';
return 1 if $b eq 'url';
return $a cmp $b;
} keys %$credential) {
if (defined $credential->{$key}) {
print $writer $key, '=', $credential->{$key}, "\n";
}
}
print $writer "\n";
}

sub _credential_run {
my ($self, $credential, $op) = _maybe_self(@_);
my ($pid, $reader, $writer, $ctx) = command_bidi_pipe('credential', $op);

credential_write $writer, $credential;
close $writer;

if ($op eq "fill") {
%$credential = credential_read $reader;
}
if (<$reader>) {
throw Error::Simple("unexpected output from git credential $op response:\n$_\n");
}

command_close_bidi_pipe($pid, $reader, undef, $ctx);
}

=item credential( CREDENTIAL_HASHREF [, OPERATION ] )
=item credential( CREDENTIAL_HASHREF, CODE )
Executes C<git credential> for a given set of credentials and specified
operation. In both forms C<CREDENTIAL_HASHREF> needs to be a reference to
a hash which stores credentials. Under certain conditions the hash can
change.
In the first form, C<OPERATION> can be C<'fill'>, C<'approve'> or C<'reject'>,
and function will execute corresponding C<git credential> sub-command. If
it's omitted C<'fill'> is assumed. In case of C<'fill'> the values stored in
C<CREDENTIAL_HASHREF> will be changed to the ones returned by the C<git
credential fill> command. The usual usage would look something like:
my %cred = (
'protocol' => 'https',
'host' => 'example.com',
'username' => 'bob'
);
Git::credential \%cred;
if (try_to_authenticate($cred{'username'}, $cred{'password'})) {
Git::credential \%cred, 'approve';
... do more stuff ...
} else {
Git::credential \%cred, 'reject';
}
In the second form, C<CODE> needs to be a reference to a subroutine. The
function will execute C<git credential fill> to fill the provided credential
hash, then call C<CODE> with C<CREDENTIAL_HASHREF> as the sole argument. If
C<CODE>'s return value is defined, the function will execute C<git credential
approve> (if return value yields true) or C<git credential reject> (if return
value is false). If the return value is undef, nothing at all is executed;
this is useful, for example, if the credential could neither be verified nor
rejected due to an unrelated network error. The return value is the same as
what C<CODE> returns. With this form, the usage might look as follows:
if (Git::credential {
'protocol' => 'https',
'host' => 'example.com',
'username' => 'bob'
}, sub {
my $cred = shift;
return !!try_to_authenticate($cred->{'username'},
$cred->{'password'});
}) {
... do more stuff ...
}
=cut

sub credential {
my ($self, $credential, $op_or_code) = (_maybe_self(@_), 'fill');

if ('CODE' eq ref $op_or_code) {
_credential_run $credential, 'fill';
my $ret = $op_or_code->($credential);
if (defined $ret) {
_credential_run $credential, $ret ? 'approve' : 'reject';
}
return $ret;
} else {
_credential_run $credential, $op_or_code;
}
}

{ # %TEMP_* Lexical Context { # %TEMP_* Lexical Context


my (%TEMP_FILEMAP, %TEMP_FILES); my (%TEMP_FILEMAP, %TEMP_FILES);
Expand Down Expand Up @@ -1375,9 +1529,11 @@ sub _execv_git_cmd { exec('git', @_); }


# Close pipe to a subprocess. # Close pipe to a subprocess.
sub _cmd_close { sub _cmd_close {
my ($fh, $ctx) = @_; my $ctx = shift @_;
if (not close $fh) { foreach my $fh (@_) {
if ($!) { if (close $fh) {
# nop
} elsif ($!) {
# It's just close, no point in fatalities # It's just close, no point in fatalities
carp "error closing pipe: $!"; carp "error closing pipe: $!";
} elsif ($? >> 8) { } elsif ($? >> 8) {
Expand Down

0 comments on commit 2daad63

Please sign in to comment.