#!/usr/bin/perl

# Various SSL read and write related tests:
# - SSL_read, SSL_peek, SSL_read_ex, SSL_peek_ex,
# - SSL_write_ex, SSL_pending and SSL_has_pending.

use strict;
use warnings;
use Test::More;
use Socket;
use File::Spec;
use Net::SSLeay;
use Config;
use IO::Socket::INET;
use Storable;

BEGIN {
  plan skip_all => "fork() not supported on $^O" unless $Config{d_fork};
}

my $tests = 53;
plan tests => $tests;

my $pid;
alarm(30);
END { kill 9,$pid if $pid }

my $server;
Net::SSLeay::initialize();

# See that lengths differ for all msgs
my $msg1 = "1 first message from server";
my $msg2 = "2 second message from server";
my $msg3 = "3 third message from server: pad";

my @rounds = qw(openssl openssl-1.1.0 openssl-1.1.1);

sub server
{
    # SSL server - just handle connections, send to client and exit
    my $cert_pem = File::Spec->catfile('t', 'data', 'testcert_wildcard.crt.pem');
    my $key_pem = File::Spec->catfile('t', 'data', 'testcert_key_2048.pem');

    $server = IO::Socket::INET->new( LocalAddr => '127.0.0.1', Listen => 3)
	or BAIL_OUT("failed to create server socket: $!");

    defined($pid = fork()) or BAIL_OUT("failed to fork: $!");
    if ($pid == 0) {
	foreach my $round (@rounds)
	{
	    my ($ctx, $ssl, $cl);

	    next if skip_round($round);

	    $cl = $server->accept or BAIL_OUT("accept failed: $!");

	    $ctx = Net::SSLeay::CTX_new();
	    Net::SSLeay::set_cert_and_key($ctx, $cert_pem, $key_pem);

	    $ssl = Net::SSLeay::new($ctx);
	    Net::SSLeay::set_fd($ssl, fileno($cl));
	    Net::SSLeay::accept($ssl);

	    Net::SSLeay::write($ssl, $msg1);
	    Net::SSLeay::write($ssl, $msg2);

	    my $msg = Net::SSLeay::read($ssl);
	    Net::SSLeay::write($ssl, $msg);
	}
	exit(0);
    }
}

sub client
{
    my $saddr = $server->sockhost.':'.$server->sockport;
    foreach my $round (@rounds)
    {
	my ($ctx, $ssl, $cl);

	$cl = IO::Socket::INET->new($saddr)
	    or BAIL_OUT("failed to connect to server: $!");

	$ctx = Net::SSLeay::CTX_new();
	$ssl = Net::SSLeay::new($ctx);

	my ($reason, $num_tests) = skip_round($round);
	if ($reason) {
	  SKIP: {
	      skip($reason, $num_tests);
	    }
	    next;
	}

	round_openssl($ctx, $ssl, $cl) if $round eq 'openssl';
	round_openssl_1_1_0($ctx, $ssl, $cl) if $round eq 'openssl-1.1.0';
	round_openssl_1_1_1($ctx, $ssl, $cl) if $round eq 'openssl-1.1.1';

	Net::SSLeay::shutdown($ssl);
	Net::SSLeay::free($ssl);
    }
    return;
}

# Returns list for skip() if we should skip this round, false if we
# shouldn't
sub skip_round
{
    my ($round) = @_;

    return if $round eq 'openssl';

    if ($round eq 'openssl-1.1.0') {
	if (Net::SSLeay::constant("OPENSSL_VERSION_NUMBER") < 0x1010000f ||
	    Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER"))
	{
	    return ("Need OpenSSL 1.1.0 or later", 6);
	} else {
	    return;
	}
    }

    if ($round eq 'openssl-1.1.1') {
	if (Net::SSLeay::constant("OPENSSL_VERSION_NUMBER") < 0x1010100f ||
	    Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER"))
	{
	    return ("Need OpenSSL 1.1.1 or later", 26);
	} else {
	    return;
	}
    }

    diag("Unknown round: $round");
    return;
}

sub round_openssl
{
    my ($ctx, $ssl, $cl) = @_;

    my ($peek_msg, $read_msg, $len, $err, $ret);

    # ssl is not connected yet
    $peek_msg = Net::SSLeay::peek($ssl);
    is($peek_msg, undef, "scalar: peek returns undef for closed ssl");

    ($peek_msg, $len) = Net::SSLeay::peek($ssl);
    is($peek_msg, undef, "list: peek returns undef for closed ssl");
    cmp_ok($len, '<=', 0, 'list: peek returns length <=0 for closed ssl');
    $err = Net::SSLeay::get_error($ssl, $len);
    isnt($err, Net::SSLeay::ERROR_WANT_READ(), "peek err $err is not retryable WANT_READ");
    isnt($err, Net::SSLeay::ERROR_WANT_WRITE(), "peek err $err is not retryable WANT_WRITE");

    $read_msg = Net::SSLeay::read($ssl);
    is($read_msg, undef, "scalar: read returns undef for closed ssl");

    ($read_msg, $len) = Net::SSLeay::read($ssl);
    is($read_msg, undef, "list: read returns undef for closed ssl");
    cmp_ok($len, '<=', 0, 'list: read returns length <=0 for closed ssl');
    $err = Net::SSLeay::get_error($ssl, $len);
    isnt($err, Net::SSLeay::ERROR_WANT_READ(), "read err $err is not retryable WANT_READ");
    isnt($err, Net::SSLeay::ERROR_WANT_WRITE(), "read err $err is not retryable WANT_WRITE");

    $ret = Net::SSLeay::pending($ssl);
    is($ret, 0, "pending returns 0 for closed ssl");

    Net::SSLeay::set_fd($ssl, $cl);
    Net::SSLeay::connect($ssl);

    # msg1
    $ret = Net::SSLeay::pending($ssl);
    is($ret, 0, "pending returns 0");

    $peek_msg = Net::SSLeay::peek($ssl);
    is($peek_msg, $msg1, "scalar: peek returns msg1");

    # processing was triggered by peek
    $ret = Net::SSLeay::pending($ssl);
    is($ret, length($msg1), "pending returns msg1 length");

    ($peek_msg, $len) = Net::SSLeay::peek($ssl);
    is($peek_msg, $msg1, "list: peek returns msg1");
    is($len, length($msg1), "list: peek returns msg1 length");

    $read_msg = Net::SSLeay::read($ssl);
    is($peek_msg, $read_msg, "scalar: read and peek agree about msg1");

    # msg2
    $peek_msg = Net::SSLeay::peek($ssl);
    is($peek_msg, $msg2, "scalar: peek returns msg2");

    ($read_msg, $len) = Net::SSLeay::read($ssl);
    is($peek_msg, $read_msg, "list: read and peek agree about msg2");
    is($len, length($msg2), "list: read returns msg2 length");

    # msg3
    Net::SSLeay::write($ssl, $msg3);
    is(Net::SSLeay::read($ssl), $msg3, "ping with msg3");

    return;
}

# Test has_pending and other functionality added in 1.1.0.
# Revisit: Better tests for has_pending
sub round_openssl_1_1_0
{
    my ($ctx, $ssl, $cl) = @_;

    my ($peek_msg, $read_msg, $len, $err, $ret);

    # ssl is not connected yet
    $ret = Net::SSLeay::has_pending($ssl);
    is($ret, 0, "1.1.0: has_pending returns 0 for closed ssl");

    Net::SSLeay::set_fd($ssl, $cl);
    Net::SSLeay::connect($ssl);

    # msg1
    $ret = Net::SSLeay::has_pending($ssl);
    is($ret, 0, "1.1.0: has_pending returns 0");

    # This triggers processing after which we have pending data
    $peek_msg = Net::SSLeay::peek($ssl);
    is($peek_msg, $msg1, "1.1.0: peek returns msg1");

    $ret = Net::SSLeay::has_pending($ssl);
    is($ret, 1, "1.1.0: has_pending returns 1");

    Net::SSLeay::read($ssl); # Read and discard

    $ret = Net::SSLeay::has_pending($ssl);
    is($ret, 0, "1.1.0: has_pending returns 0 after read");

    # msg2
    Net::SSLeay::read($ssl); # Read and discard

    # msg3
    Net::SSLeay::write($ssl, $msg3);
    is(Net::SSLeay::read($ssl), $msg3, "1.1.0: ping with msg3");

    return;
}

sub round_openssl_1_1_1
{
    my ($ctx, $ssl, $cl) = @_;

    my ($peek_msg, $read_msg, $len, $err, $err_ex, $ret);

    # ssl is not connected yet
    ($peek_msg, $ret) = Net::SSLeay::peek_ex($ssl);
    is($peek_msg, undef, "1.1.1: list: peek_ex returns undef message for closed ssl");
    is($ret, 0, '1.1.1: list: peek_ex returns 0 for closed ssl');
    $err = Net::SSLeay::get_error($ssl, $ret);
    isnt($err, Net::SSLeay::ERROR_WANT_READ(), "1.1.1: peek_ex err $err is not retryable WANT_READ");
    isnt($err, Net::SSLeay::ERROR_WANT_WRITE(), "1.1.1: peek_ex err $err is not retryable WANT_WRITE");

    ($read_msg, $len) = Net::SSLeay::read($ssl);
    is($read_msg, undef, "1.1.1: list: read returns undef message for closed ssl");
    cmp_ok($len, '<=', 0, '1.1.1: list: read returns length <=0 for closed ssl');
    $err = Net::SSLeay::get_error($ssl, $len);
    isnt($err, Net::SSLeay::ERROR_WANT_READ(), "1.1.1: read err $err is not retryable WANT_READ");
    isnt($err, Net::SSLeay::ERROR_WANT_WRITE(), "1.1.1: read err $err is not retryable WANT_WRITE");

    ($read_msg, $ret) = Net::SSLeay::read_ex($ssl);
    is($read_msg, undef, "1.1.1: list: read_ex returns undef message for closed sssl");
    is($ret, 0, "1.1.1: list: read_ex returns 0 for closed sssl");
    $err_ex = Net::SSLeay::get_error($ssl, $ret);
    is ($err_ex, $err, '1.1.1: read_ex and read err are equal');

    Net::SSLeay::set_fd($ssl, $cl);
    Net::SSLeay::connect($ssl);

    # msg1
    $ret = Net::SSLeay::has_pending($ssl);
    is($ret, 0, "1.1.1: has_pending returns 0");

    # This triggers processing after which we have pending data
    ($peek_msg, $ret) = Net::SSLeay::peek_ex($ssl);
    is($peek_msg, $msg1, "1.1.1: list: peek_ex returns msg1");
    is($ret, 1, "1.1.1: list: peek_ex returns 1");

    $len = Net::SSLeay::pending($ssl);
    is($len, length($msg1), "1.1.1: pending returns msg1 length");

    $ret = Net::SSLeay::has_pending($ssl);
    is($ret, 1, "1.1.1: has_pending returns 1");

    ($read_msg, $ret) = Net::SSLeay::read_ex($ssl);
    is($read_msg, $msg1, "1.1.1: list: read_ex returns msg1");
    is($ret, 1, "1.1.1: list: read_ex returns 1");

    $len = Net::SSLeay::pending($ssl);
    is($len, 0, "1.1.1: pending returns 0 after read_ex");

    $ret = Net::SSLeay::has_pending($ssl);
    is($ret, 0, "1.1.1: has_pending returns 0 after read_ex");

    # msg2
    Net::SSLeay::read($ssl); # Read and discard

    # msg3
    ($len, $ret) = Net::SSLeay::write_ex($ssl, $msg3);
    is($len, length($msg3), "1.1.1: write_ex wrote all");
    is($ret, 1, "1.1.1: write_ex returns 1");

    my ($read_msg1, $ret1) = Net::SSLeay::read_ex($ssl, 5);
    my ($read_msg2, $ret2) = Net::SSLeay::read_ex($ssl, (length($msg3) - 5));

    is($ret1, 1, '1.1.1: ping with msg3 part1 ok');
    is($ret2, 1, '1.1.1: ping with msg3 part2 ok');
    is(length($read_msg1), 5, '1.1.1: ping with msg3, part1 length was 5');
    is($read_msg1 . $read_msg2, $msg3, "1.1.1: ping with msg3 in two parts");

    return;
}

server();
client();
waitpid $pid, 0;
exit(0);
