Merge branch 'mn/send-email-works-with-credential'
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
This commit is contained in:
@ -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.
|
||||||
|
@ -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.
|
||||||
@ -1155,9 +1196,7 @@ X-Mailer: git-send-email $gitversion
|
|||||||
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) {
|
||||||
@ -1185,31 +1224,7 @@ X-Mailer: git-send-email $gitversion
|
|||||||
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;
|
||||||
|
198
perl/Git.pm
198
perl/Git.pm
@ -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);
|
||||||
|
|
||||||
|
|
||||||
@ -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;
|
||||||
@ -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;
|
||||||
@ -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;
|
||||||
@ -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... ] )
|
||||||
@ -420,7 +421,7 @@ 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);
|
||||||
|
|
||||||
@ -428,23 +429,26 @@ 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);
|
||||||
}
|
}
|
||||||
@ -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);
|
||||||
@ -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) {
|
||||||
|
Reference in New Issue
Block a user