Net-LibSSH

 view release on metacpan or  search on metacpan

t/09-connect-after-disconnect.t  view on Meta::CPAN

use strict;
use warnings;
use lib 't/lib';
use Test::More;
use TestSSHD;
use Net::LibSSH;
use Time::HiRes qw(time);

# libssh 0.10.6 cannot reconnect an ssh_session it has already disconnected.
# ssh_connect() does not fail fast on such a session -- it sits out the whole
# SSH_OPTIONS_TIMEOUT and then reports "Timeout connecting to 127.0.0.1",
# measured at 5.005s against a live sshd with timeout => 5. A caller who tries
# to reuse a session therefore waits out a timeout to be told, wrongly, that
# the host is unreachable.
#
# The session now knows it is spent and connect() refuses straight away. What
# it must NOT do is croak: connect()'s contract is 1 or 0 with the message on
# error() (t/01-session.t pins that for a refused port, and Rex::LibSSH relies
# on it), so this is a fast 0 plus an error() that names the real state.
#
# Being spent is not the same as "disconnect() was called". Measured on the
# same libssh: disconnect() on a session that was never connected leaves it
# fully connectable -- connect() afterwards succeeds and authenticates. Only
# the connect -> disconnect transition is terminal, and the last case below is
# what keeps the guard from over-refusing.
#
# This is its own file rather than more cases in t/01-session.t because it
# needs a real successful connection first: folding it in would make that
# whole file -- the option() coverage and the refused-port contract with it --
# skip on any machine without sshd.

my $srv = TestSSHD->start;
plan skip_all => 'sshd or ssh-keygen not available' unless $srv;

# timeout => 5 is what bounds this test: it is the timeout ssh_connect() would
# wait out on a spent session, so a regression costs five seconds and a failed
# assertion rather than hanging the suite.
sub session {
    my $ssh = Net::LibSSH->new;
    $ssh->option(host       => $srv->host);
    $ssh->option(port       => $srv->port);
    $ssh->option(user       => scalar getpwuid($<));
    $ssh->option(knownhosts => '/dev/null');
    $ssh->option(timeout    => 5);
    return $ssh;
}

sub timed {
    my ($code) = @_;
    my $t0  = time;
    my $rc  = eval { $code->() };
    my $err = $@;
    return ($rc, $err, time - $t0);
}

# --- 1. connect() after disconnect(): fast 0, no croak ----------------------
{
    my $ssh = session();
    ok $ssh->connect, 'connect() succeeds on a fresh session'
        or diag('connect: ' . ($ssh->error // '(no error)'));
    ok $ssh->auth_publickey($srv->client_key), 'auth_publickey() succeeds'
        or diag('auth: ' . ($ssh->error // '(no error)'));

    $ssh->disconnect;

    my ($rc, $err, $elapsed) = timed(sub { $ssh->connect });
    is $err, '', 'connect() on a disconnected session does not croak';
    is $rc,  0,  'connect() on a disconnected session returns 0';

    # The whole point of the ticket: without the guard this is the full
    # SSH_OPTIONS_TIMEOUT (5.005s measured). 1s leaves room for a loaded
    # machine while still being nowhere near the timeout.
    cmp_ok $elapsed, '<', 1,
        sprintf('connect() refuses immediately (%.3fs, was ~5s of timeout)', $elapsed);

    my $msg = $ssh->error;
    ok defined($msg) && length($msg), 'error() is set after the refusal';
    like $msg, qr/disconnect/i, 'error() names the session state';
    unlike $msg, qr/timeout/i,
        'error() no longer blames a timeout for a state we know about';

    # Nothing un-spends the session, and asking twice must not start behaving
    # differently.
    my ($rc2, $err2, $elapsed2) = timed(sub { $ssh->connect });
    is $err2, '', 'a second connect() attempt still does not croak';
    is $rc2,  0,  'and still returns 0';
    cmp_ok $elapsed2, '<', 1, 'and is still immediate';
    like $ssh->error, qr/disconnect/i,
        'and still names the state, not whatever libssh made of the retry';
}

# --- 2. disconnect() on a never-connected session is not terminal -----------
#
# Measured, not assumed: libssh lets this session connect afterwards, so the
# binding must not refuse it. This is the case that decides the guard hangs
# off "was connected", not off "disconnect() was called".
{
    my $ssh = session();
    $ssh->disconnect;

    my ($rc, $err, $elapsed) = timed(sub { $ssh->connect });
    is $err, '', 'connect() after disconnect() on a never-connected session does not croak';



( run in 0.517 second using v1.01-cache-2.11-cpan-7f9471e7e0a )