# You may distribute under the terms of either the GNU General Public License
# or the Artistic License (the same terms as Perl itself)
#
# (C) Paul Evans, 2010-2023 -- leonerd@leonerd.org.uk
package IO::Async::SSL 0.25;
use v5.14;
use warnings;
use Carp;
use POSIX qw( EAGAIN EWOULDBLOCK );
use IO::Socket::SSL 2.003 qw( $SSL_ERROR SSL_WANT_READ SSL_WANT_WRITE ); # default_ca
# require >= 2.003 for bugfixes - see RT#125220
use Future 0.33; # ->catch_with_f
use IO::Async::Handle 0.29;
use IO::Async::Loop 0.61; # new Listen API
=head1 NAME
C<IO::Async::SSL> - use SSL/TLS with L<IO::Async>
=head1 SYNOPSIS
use IO::Async::Loop;
use IO::Async::SSL;
my $loop = IO::Async::Loop->new();
$loop->SSL_connect(
host => "www.example.com",
service => "https",
on_stream => sub {
my ( $stream ) = @_;
$stream->configure(
on_read => sub {
...
},
);
$loop->add( $stream );
...
},
on_resolve_error => sub { print STDERR "Cannot resolve - $_[0]\n"; },
on_connect_error => sub { print STDERR "Cannot connect\n"; },
on_ssl_error => sub { print STDERR "Cannot negotiate SSL - $_[-1]\n"; },
);
=head1 DESCRIPTION
This module extends existing L<IO::Async> classes with extra methods to allow
the use of SSL or TLS-based connections using L<IO::Socket::SSL>. It does not
directly provide any methods or functions of its own.
Primarily, it provides C<SSL_connect> and C<SSL_listen>, which yield
C<IO::Socket::SSL>-upgraded socket handles or L<IO::Async::Stream>
instances, and two forms of C<SSL_upgrade> to upgrade an existing TCP
connection to use SSL.
As an additional convenience, if the C<SSL_verify_mode> and C<SSL_ca_*>
options are omitted, the module will attempt to provide them by querying the
result of L<IO::Socket::SSL>'s C<default_ca> function. Otherwise, the module
will print a warning and set C<SSL_VERIFY_NONE> instead.
=cut
my %SSL_ca_args = IO::Socket::SSL::default_ca();
sub _SSL_args
{
my %args = @_;
# SSL clients (i.e. non-server) require a verify mode
if( !$args{SSL_server} and !defined $args{SSL_verify_mode} and
!defined $args{SSL_ca_file} and !defined $args{SSL_ca_path} ) {
unless( %SSL_ca_args ) {
carp "Unable to set SSL_VERIFY_PEER because IO::Socket::SSL::default_ca() gives nothing";
$SSL_ca_args{SSL_verify_mode} = IO::Socket::SSL::SSL_VERIFY_NONE();
}
%args = ( %SSL_ca_args, %args );
}
return %args;
}
sub sslread
{
my $stream = shift;
my ( $fh, undef, $len ) = @_;
my $ret = $stream->_sysread( $fh, $_[1], $len );
my $read_wants_write = !defined $ret &&
( $! == EAGAIN or $! == EWOULDBLOCK ) &&
$SSL_ERROR == SSL_WANT_WRITE;
$stream->want_writeready_for_read( $read_wants_write );
# It's possible SSL_read took all the data out of the filehandle, thus
# making it not appear read-ready any more.
if( $fh->pending ) {
$stream->loop->later( sub { $stream->on_read_ready } );
}
return $ret;
}
sub sslwrite
{
my $stream = shift;
my ( $fh, undef, $len ) = @_;
# Placate RT98372
utf8::downgrade( $_[1] ) or
carp "Wide character in sslwrite";
my $ret = $stream->_syswrite( $fh, $_[1], $len );
my $write_wants_read = !defined $ret &&
( $! == EAGAIN or $! == EWOULDBLOCK ) &&
$SSL_ERROR == SSL_WANT_READ;
$stream->want_readready_for_write( $write_wants_read );
# If write wants read, there's no point waiting on writereadiness either
$stream->want_writeready_for_write( !$write_wants_read );
return $ret;
}
=head1 LOOP METHODS
The following extra methods are added to L<IO::Async::Loop>.
=cut
=head2 SSL_upgrade
( $stream or $socket ) = $loop->SSL_upgrade( %params )->get;
This method upgrades a given stream filehandle into an SSL-wrapped stream,
returning a future which will yield the given stream object or socket.
Takes the following parameters:
=over 8
=item handle => IO::Async::Stream | IO
The C<IO::Async::Stream> object containing the IO handle of an
already-established connection to act as the transport for SSL; or the plain
IO socket handle itself.
If an C<IO::Async::Stream> is passed it will have the C<reader> and C<writer>
functions set on it suitable for SSL use, and will be returned as the result
from the future.
If a plain socket handle is passed, that will be returned from the future
instead.
=item SSL_server => BOOL
If true, indicates this is the server side of the connection.
=back
In addition, any parameter whose name starts C<SSL_> will be passed to the
C<IO::Socket::SSL> constructor.
The following legacy callback arguments are also supported, in case the
returned future is not used:
=over 8
=item on_upgraded => CODE
A continuation that is invoked when the socket has been successfully upgraded
to SSL. It will be passed an instance of an C<IO::Socket::SSL>, which will
have appropriate SSL-compatible reader/writer functions attached.
$on_upgraded->( $sslsocket )
=item on_error => CODE
A continuation that is invoked if C<IO::Socket::SSL> detects an error while
negotiating the upgrade.
$on_error->( $! )
=back
=cut
sub IO::Async::Loop::SSL_upgrade
{
my $loop = shift;
my %params = @_;
my $f = $loop->new_future;
$params{handle} or croak "Expected 'handle'";
my $stream;
my $socket;
if( $params{handle}->isa( "IO::Async::Stream" ) ) {
$stream = delete $params{handle};
$socket = $stream->read_handle;
}
else {
$socket = delete $params{handle};
}
{
my $on_upgraded = delete $params{on_upgraded} or defined wantarray
or croak "Expected 'on_upgraded' or to return a Future";
my $on_error = delete $params{on_error} or defined wantarray
or croak "Expected 'on_error' or to return a Future";
$f->on_done( $on_upgraded ) if $on_upgraded;
$f->on_fail( $on_error ) if $on_error;
}
my %ssl_params = map { $_ => delete $params{$_} } grep m/^SSL_/, keys %params;
eval {
$socket = IO::Socket::SSL->start_SSL( $socket, _SSL_args
SSL_startHandshake => 0,
# Required to make IO::Socket::SSL not ->close before we have a chance to remove it from the loop
SSL_error_trap => sub { },
%ssl_params,
) or die IO::Socket::SSL->errstr;
} or do {
chomp( my $e = $@ );
return $f->fail( $e, "ssl" );
};
my $ready_method = $ssl_params{SSL_server} ? "accept_SSL" : "connect_SSL";
my $ready = sub {
my ( $self ) = @_;
if( $socket->$ready_method ) {
$loop->remove( $self );
if( $stream ) {
$stream->configure(
handle => $socket,
reader => \&sslread,
writer => \&sslwrite,
);
}
$f->done( $stream || $socket );
return;
}
if( $! != EAGAIN and $! != EWOULDBLOCK ) {
my $errstr = IO::Socket::SSL::errstr();
$loop->remove( $self );
$f->fail( $errstr, "ssl" );
return;
}
$self->want_readready ( $SSL_ERROR == SSL_WANT_READ );
$self->want_writeready( $SSL_ERROR == SSL_WANT_WRITE );
};
# We're going to steal the IO handle from $stream, so we'll have to
# temporarily deconfigure it
$stream->configure( handle => undef ) if $stream;
$loop->add( my $handle = IO::Async::Handle->new(
handle => $socket,
on_read_ready => $ready,
on_write_ready => $ready,
) );
$ready->( $handle );
return $f if defined wantarray;
# Caller is not going to keep hold of the Future, so we have to ensure it
# stays alive somehow
$f->on_ready( sub { undef $f } ); # intentional cycle
}
=head2 SSL_connect
$stream = $loop->SSL_connect( %params )->get;
This method performs a non-blocking connection to a given address or set of
addresses, upgrades the socket to SSL, then yields a C<IO::Async::Stream>
object when the SSL handshake is complete.
It takes all the same arguments as C<IO::Async::Loop::connect()>. Any argument
whose name starts C<SSL_> will be passed on to the L<IO::Socket::SSL>
constructor rather than the Loop's C<connect> method. It is not required to
pass the C<socktype> option, as SSL implies this will be C<stream>.
This method can also upgrade an existing C<IO::Async::Stream> or subclass
instance given as the C<handle> argument, by setting the C<reader> and
C<writer> functions.
=head2 SSL_connect (void)
$loop->SSL_connect( %params,
on_connected => sub { ... },
on_stream => sub { ... },
);
When not returning a future, this method also supports the C<on_connected> and
C<on_stream> continuations.
In addition, the following arguments are then required:
=over 8
=item on_ssl_error => CODE
A continuation that is invoked if C<IO::Socket::SSL> detects an SSL-based
error once the actual stream socket is connected.
=back
If the C<on_connected> continuation is used, the socket handle it yields will
be a C<IO::Socket::SSL>, which must be wrapped in C<IO::Async::SSLStream> to
be used by C<IO::Async>. The C<on_stream> continuation will already yield such
an instance.
=cut
sub IO::Async::Loop::SSL_connect
{
my $loop = shift;
my %params = @_;
my %ssl_params = map { $_ => delete $params{$_} } grep m/^SSL_/, keys %params;
my $on_done;
if( exists $params{on_connected} ) {
my $on_connected = delete $params{on_connected};
$on_done = sub {
my ( $stream ) = @_;
$on_connected->( $stream->read_handle );
};
}
elsif( exists $params{on_stream} ) {
my $on_stream = delete $params{on_stream};
$on_done = $on_stream;
}
else {
croak "Expected 'on_connected' or 'on_stream' or to return a Future" unless defined wantarray;
}
my $on_ssl_error = delete $params{on_ssl_error} or defined wantarray or
croak "Expected 'on_ssl_error' or to return a Future";
my $stream = delete $params{handle} || do {
require IO::Async::Stream;
IO::Async::Stream->new;
};
$stream->isa( "IO::Async::Stream" ) or
croak "Can only SSL_connect a handle instance of IO::Async::Stream";
# Don't ->connect with the handle yet, because we'll first have to use the
# socket to perform SSL_upgrade on. We don't want to confuse the loop by
# giving it the same fd twice.
my $f = $loop->connect(
socktype => 'stream', # SSL over DGRAM or RAW makes no sense
%params,
)->then( sub {
my ( $socket ) = @_;
$stream->configure( handle => $socket );
$loop->SSL_upgrade(
_SSL_args( %ssl_params ),
handle => $stream,
)
});
$f->on_done( $on_done ) if $on_done;
$f->on_fail( sub {
$on_ssl_error->( $_[0] ) if defined $_[1] and $_[1] eq "ssl";
}) if $on_ssl_error;
return $f if defined wantarray;
# Caller is not going to keep hold of the Future, so we have to ensure it
# stays alive somehow
$f->on_ready( sub { undef $f } ); # intentional cycle
}
=head2 SSL_listen
$loop->SSL_listen( %params )->get;
This method sets up a listening socket using the addresses given, and will
invoke the callback each time a new connection is accepted on the socket and
the SSL handshake has been completed. This can be either the C<on_accept> or
C<on_stream> continuation; C<on_socket> is not supported.
It takes all the same arguments as C<IO::Async::Loop::listen()>. Any argument
whose name starts C<SSL_> will be passed on to the L<IO::Socket::SSL>
constructor rather than the Loop's C<listen> method. It is not required to
pass the C<socktype> option, as SSL implies this will be C<stream>.
In addition, the following arguments are rquired:
=over 8
=item on_ssl_error => CODE
A continuation that is invoked if C<IO::Socket::SSL> detects an SSL-based
error once the actual stream socket is connected.
=back
The underlying L<IO::Socket::SSL> socket will also require the server key and
certificate for a server-mode socket. See its documentation for more details.
If the C<on_accept> continuation is used, the socket handle it yields will be
a C<IO::Socket::SSL>, which must be wrapped in C<IO::Async::SSLStream> to be
used by C<IO::Async>. The C<on_stream> continuation will already yield such an
instance.
=cut
sub IO::Async::Loop::SSL_listen
{
my $loop = shift;
my %params = @_;
my %ssl_params = map { $_ => delete $params{$_} } grep m/^SSL_/, keys %params;
my $on_ssl_error = delete $params{on_ssl_error} or defined wantarray
or croak "Expected 'on_ssl_error'";
my $f = $loop->listen(
socktype => 'stream',
%params,
)->on_done( sub {
my $listener = shift;
my $cleartext_acceptor = $listener->acceptor;
my $ssl_acceptor = sub {
my $listener = shift;
my ( $listen_sock, %params ) = @_;
my $stream = $params{handle};
!defined $stream or $stream->isa( "IO::Async::Stream" ) or
croak "Can only accept SSL on IO::Async::Stream handles";
$listener->$cleartext_acceptor( $listen_sock )->then( sub {
my ( $socket ) = @_;
return Future->done() unless $socket; # EAGAIN
$stream->configure( handle => $socket ) if $stream;
$loop->SSL_upgrade(
_SSL_args( SSL_server => 1, %ssl_params ),
handle => ( $stream || $socket ),
)->catch_with_f( ssl => sub {
my ( $f, $failure ) = @_;
if( $on_ssl_error ) {
$on_ssl_error->( $failure );
return Future->done; # eat it
}
return $f;
});
});
};
$listener->configure( acceptor => $ssl_acceptor );
});
return $f if defined wantarray;
# Caller is not going to keep hold of the Future, so we have to ensure it
# stays alive somehow
$f->on_ready( sub { undef $f } ); # intentional cycle
}
=head1 STREAM PROTOCOL METHODS
The following extra methods are added to L<IO::Async::Protocol::Stream>.
=cut
=head2 SSL_upgrade
$protocol->SSL_upgrade( %params )->get;
A shortcut to calling C<< $loop->SSL_upgrade >>. This method will unconfigure
the C<transport> of the Protocol, upgrade its underlying filehandle to SSL,
then reconfigure it again with SSL reader and writer functions on it. It takes
the same arguments as C<< $loop->SSL_upgrade >>, except that the C<handle>
argument is not required as it's taken from the Protocol's C<transport>.
=cut
sub IO::Async::Protocol::Stream::SSL_upgrade
{
my $protocol = shift;
my %params = @_;
my $on_upgraded = delete $params{on_upgraded} or croak "Expected 'on_upgraded'";
my $loop = $protocol->get_loop or croak "Expected to be a member of a Loop";
my $transport = $protocol->transport;
$protocol->configure( transport => undef );
$loop->SSL_upgrade(
handle => $transport,
on_upgraded => sub {
my ( $transport ) = @_;
$protocol->configure( transport => $transport );
$on_upgraded->();
},
%params,
);
}
=head1 AUTHOR
Paul Evans <leonerd@leonerd.org.uk>
=cut
0x55AA;