Help language development. Donate to The Perl Foundation

IO::Socket::Async::SSL cpan:JNTHN last updated on 2020-11-27

README.md
# IO::Socket::Async::SSL [![Build Status](https://travis-ci.org/jnthn/p6-io-socket-async-ssl.svg?branch=master)](https://travis-ci.org/jnthn/p6-io-socket-async-ssl)

This module provides a secure sockets implementation with an API very much
like that of the Perl 6 built-in `IO::Socket::Async` class. For the client
case, provided the standard certificate and host verification are sufficient,
it is drop-in replacement. The server case only needs two extra arguments to
`listen`, specifying the server key and certificate.

As with `IO::Socket::Async`, it is safe to have concurrent connections and to
share them across threads.

## Synopsis

Client:

    use IO::Socket::Async::SSL;

    my $conn = await IO::Socket::Async::SSL.connect('www.perl6.org', 443);
    $conn.print: "GET / HTTP/1.0\r\nHost: www.perl6.org\r\n\r\n";
    react {
        whenever $conn {
            .print
        }
    }
    $conn.close;

Server (assumes certificate and key files `server-crt.pem` and `server-key.pem`):

    use IO::Socket::Async::SSL;

    react {
        my %ssl-config =
            certificate-file => 'server-crt.pem',
            private-key-file => 'server-key.pem';
        whenever IO::Socket::Async::SSL.listen('localhost', 4433, |%ssl-config) -> $conn {
            my $req = '';
            whenever $conn {
                $req ~= $_;
                if $req.contains("\r\n\r\n") {
                    say $req.lines[0];
                    await $conn.print(
                        "HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n" ~
                        "<strong>Hello from a Perl 6 HTTP server</strong>\n");
                    $conn.close;
                }
            }
        }
    }

## Client

The `connect` method on `IO::Socket::Async::SSL` is used to establish a SSL
connection to a server. It requies two positional arguments, which specify the
`host` and `port` to connect to. It returns a `Promise`, which will be kept
with an `IO::Socket::Async::SSL` instance when the connection is established
and the SSL handshake completed.

    my $conn = await IO::Socket::Async::SSL.connect($host, $port);

By default, the SSL certificate will be verified, using the default set of
accepted Certificate Authorities. The `Promise` return by `conenct` will be
broken if verification fails.

Sometimes it is convenient to create a CA and use it to sign certificates for
internal use, for example to secure communications between a set of services
on an internal network. In this case, the `ca-file` named argument can be
passed to specify the certificate authority certificate file:

    my $ca-file = '/config/ca-crt.pem';
    my $conn = await IO::Socket::Async::SSL.connect('user-service', 443, :$ca-file);

Alternatively, a `ca-path` argument can be specified, indicating a directory
where one or more certificates may be found.

It is possible to disable certificate verification by passing the `insecure`
named argument a true value. As the name suggests, **this is not a secure
configuration**, since there is no way for the client to be sure that it is
communicating with the intended server. Therefore, it is vulnerable to
man-in-the-middle attacks.

For control over the ciphers that may be used, pass the `ciphers` argument to
`connect`. It should be a string in [OpenSSL cipher list format](https://www.openssl.org/docs/man1.0.2/apps/ciphers.html).

## Server

The `listen` method returns a `Supply` that, when tapped, will start an SSL
server. The server can be shut down by closing the tap. Whenever a connection
is made to the server, the `Supply` will emit an `IO::Socket::Async::SSL`
instance. The `listen` method requires two positional arguments, specifying
the `host` and `port` to listen on. Two named arguments are also required,
providing the `certificate-file` and `private-key-file`.

    my %ssl-config =
        certificate-file => 'server-crt.pem',
        private-key-file => 'server-key.pem';
    my $connections = IO::Socket::Async::SSL.listen('localhost', 4433, |%ssl-config);
    react {
        my $listener = do whenever $connections -> $conn {
            say "Got a connection!";
            $conn.close;
        }

        whenever signal(SIGINT) {
            say "Shutting down...";
            $listener.close;
            exit;
        }
    }

For control over the ciphers that may be used, pass the `ciphers` argument to
`connect`. It should be a string in [OpenSSL cipher list format](https://www.openssl.org/docs/man1.0.2/apps/ciphers.html). The following boolean options are also accepted:

* `prefer-server-ciphers` - indicates that the order of the ciphers list as
  configured on the server should be preferred over that of the one presented
  by the client
* `no-compression` - disables compression
* `no-session-resumption-on-renegotiation`

## Common client and server functionality

Both the `connect` and `listen` methods take the following optional named
arguments:

* `enc`, which specifies the encoding to use when the socket is used in
  character mode. Defaults to `utf-8`.
* `scheduler`, which specifies the scheduler to use for processing events from
  the underlying `IO::Socket::Async` instance. The default is `$*SCHEDULER`.
  There is rarely a need to change this.

The `Supply`, `print`, `write`, and `close` methods have the same semantics as
in [IO::Socket::Async](https://docs.perl6.org/type/IO$COLON$COLONSocket$COLON$COLONAsync).

## Upgrading connections

Some protocols use [opportunistic TLS](https://en.wikipedia.org/wiki/Opportunistic_TLS),
where the decision to use transport layer security is first negotiated using
a non-encrypted protocol - provided negotiation is successful - a TLS handshake
is then performed. This functionality is provided by the `upgrade-server` and
`upgrade-client` methods. Note that the socket to upgrade must be an instance
of `IO::Socket::Async`. Further, it is important to **stop reading from the
socket after initiating the upgrade**.

Here is an example of using `upgrade-server`. Note the use of `last` in order
to terminate the `whenever` that reads from the plain connection.

    react whenever IO::Socket::Async.listen('localhost', TEST_PORT) -> $plain-conn {
        whenever $plain-conn.Supply -> $start {
            if $start eq "Psst, let's talk securely!\n" {
                my $enc-conn-handshake = IO::Socket::Async::SSL.upgrade-server(
                    $plain-conn,
                    private-key-file => 't/certs-and-keys/server.key',
                    certificate-file => 't/certs-and-keys/server-bundle.crt'
                );
                whenever $enc-conn-handshake -> $enc-conn {
                    uc-service($enc-conn);
                }
                $plain-conn.print("OK, let's talk securely!\n");
                last;
            }
            else {
                $plain-conn.print("OK, let's talk insecurely\n");
                uc-service($plain-conn);
            }
        }

        sub uc-service($conn) {
            whenever $conn -> $crypt-text {
                whenever $conn.print($crypt-text.uc) {
                    $conn.close;
                }
            }
        }
    }

Here's an example using `upgrade-client`; again, take note of the `last`.

    my $plain-conn = await IO::Socket::Async.connect('localhost', TEST_PORT);
    await $plain-conn.print("Psst, let's talk securely!\n");
    react whenever $plain-conn -> $msg {
        my $enc-conn-handshake = IO::Socket::Async::SSL.upgrade-client(
            $plain-conn,
            host => 'localhost',
            ca-file => 't/certs-and-keys/ca.crt');
        whenever $enc-conn-handshake -> $enc-conn {
            await $enc-conn.print("hello!\n");
            whenever $enc-conn.head -> $got {
                print $got; # HELLO!
                done;
            }
        }

        last;
    }

## Bugs, feature requests, and contributions

Please use [GitHub Issues](https://github.com/jnthn/p6-io-socket-async-ssl/issues)
to file bug reports and feature requests. If you wish to contribute to this
module, please file a GitHub Pull Request, or email a Git patch (produced using
`format-patch`) to [[email protected]](mailto:[email protected]). Please also use
this email address to report security vulnerabilities.