#!/usr/bin/perl

###############################################################################
# This file is part of Ásbrú Connection Manager
#
# Copyright (C) 2017-2020 Ásbrú Connection Manager team (https://asbru-cm.net)
# Copyright (C) 2010-2016 David Torrejón Vaquerizas
#
# Ásbrú Connection Manager is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ásbrú Connection Manager is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License version 3
# along with Ásbrú Connection Manager.
# If not, see <http://www.gnu.org/licenses/gpl-3.0.html>.
###############################################################################

use utf8;

$|++;

###################################################################
# START : Modules import

use strict;
use warnings;

use FindBin qw ($RealBin $Bin $Script);
use lib $RealBin, "$RealBin/ex", "$RealBin/edit";
use Storable qw (dclone thaw retrieve fd_retrieve nstore_fd);
use Expect;
use Glib::IO; # GSettings
use POSIX qw (strftime);
use Encode qw (encode decode);
use IO::Socket::INET;
use Encode;
use KeePass;
use PACUtils;

use Gtk3 '-init';

binmode STDOUT;
binmode STDERR;

# END OF : Modules import
########################################################

########################################################
# START : Variables declaration/initialization
my $APPNAME = $PACUtils::APPNAME;
my $APPICON = "$RealBin/../res/asbru-logo-64.png";
my $CFG_DIR = $ENV{"ASBRU_CFG"};
my $SCRIPTS_DIR = "$CFG_DIR/scripts";

my @CMD;
my $CFG_FILE;
my $CFG;
my $IS_CLUSTER;
my $UUID;
my $GETCMD = 0;
my $CONNECTED = 0;
my $PASSWORD_COUNT = 0;
my $WAS_ASK_PASS = 0;
my $SUDO_PASSWORD_COUNT = 0;
my $USER_COUNT = 0;
my $SOCKET;
my $SOCKET_EXEC;
my $proxy_type = 'socks5';
my $proxy_ip = '';
my $proxy_port = '';
my $proxy_user = '';
my $proxy_pass = '';
my $proxy_cmd = '';
my $proxy_jump = 0;

my @KPXWHERE = ('comment', 'created', 'password', 'title', 'url', 'username');

# Define some ANSI colors
my %COLOR = (
    'norm' => "\033[m",    # black
    'log' => "\033[33m",   # yellow
    'recv' => "\033[35m",  # magenta
    'sent' => "\033[0m\033[32m", # green
    'err' => "\033[31m" # red
);

# Create the Expect object
$Expect::Multiline_Matching = 1;
my $EXP = new Expect;
eval {
    $EXP->slave->clone_winsize_from(\*STDIN);
};

# END OF : Variables declaration/initialization
########################################################

########################################################
# START : Signal handling
sub __disconnect {
    my $signal = shift;
    ctrl("DISCONNECTING");
    $EXP->hard_close;
    ctrl("DISCONNECTED");
    exit 0;
}
$SIG{'INT'} = \&__disconnect;
$SIG{'TERM'} = \&__disconnect;
$SIG{'QUIT'} = \&__disconnect;
# END OF : Signal handling
########################################################

########################################################
# START : Main program

# Retrieve command line options
$CFG_FILE = shift;
$UUID = shift or die "$COLOR{'err'}ERROR: You must provide a profile to connect to!!$COLOR{'norm'}";
$IS_CLUSTER = shift // 0;
$GETCMD = shift // 0;

# Load the 'freezed' configuration
$CFG = retrieve($CFG_FILE) or die "ERROR: Could not load config file '$CFG_FILE': $!";

# Check some command line options
defined $$CFG{'environments'}{$UUID} or die("ERROR: Profile '$UUID' does not exist!!");

`which autossh 1>/dev/null 2>&1`;
my $autossh_bin = ! $?;

# Shorten some variables
my $DEBUG = $$CFG{'defaults'}{'debug'};
my $COMMAND_PROMPT = $$CFG{'defaults'}{'command prompt'};
my $USERNAME_PROMPT = $$CFG{'defaults'}{'username prompt'};
my $PASSWORD_PROMPT = $$CFG{'defaults'}{'password prompt'};
my $HOSTCHANGE_PROMPT = $$CFG{'defaults'}{'hostkey changed prompt'};
my $ANYKEY_PROMPT = $$CFG{'defaults'}{'press any key prompt'};
my $REMOTEHOST_PROMPT = $$CFG{'defaults'}{'remote host changed prompt'};
my $ACCEPT_KEY = $$CFG{'defaults'}{'auto accept key'};
my $TIMEOUT_CONNECT = $$CFG{'defaults'}{'timeout connect'} || undef;
my $TIMEOUT_CMD = $$CFG{'defaults'}{'timeout command'} || undef;
my $AUTOSSH = $$CFG{'environments'}{$UUID}{'autossh'} && $autossh_bin;
my $METHOD = $AUTOSSH ? 'autossh' : $$CFG{'environments'}{$UUID}{'method'};
my $SUDO = $$CFG{'environments'}{$UUID}{'use sudo'};
my $SUDO_PROMPT = $$CFG{'defaults'}{'sudo prompt'};
my $SUDO_PASSWORD = $$CFG{'defaults'}{'sudo password'};
my $IP = $$CFG{'environments'}{$UUID}{'ip'};
my $PORT = $$CFG{'environments'}{$UUID}{'port'};
my $USER = $$CFG{'environments'}{$UUID}{'user'};
my $PASS = $$CFG{'environments'}{$UUID}{'pass'};
my $AUTH = $$CFG{'environments'}{$UUID}{'auth type'};
my $PUBKEY = $$CFG{'environments'}{$UUID}{'public key'};
my $PASSPHRASE = $$CFG{'environments'}{$UUID}{'passphrase'};
my $PASSPHRASE_USER = $$CFG{'environments'}{$UUID}{'passphrase user'};
my $AUTHFALLBACK = $$CFG{'environments'}{$UUID}{'auth fallback'};
my $NAME = $$CFG{'environments'}{$UUID}{'name'};
my $RESTART = $$CFG{'environments'}{$UUID}{'autoreconnect'} // 0;
my $SEND_SLOW = ($$CFG{'environments'}{$UUID}{'send slow'} // 0) / 1000;
my $TITLE = $$CFG{'environments'}{$UUID}{'title'};
my $EXPECT = defined $$CFG{'environments'}{$UUID}{'expect'};
my $USE_PREPEND_COMMAND = $$CFG{'environments'}{$UUID}{'use prepend command'};
my $PREPEND_COMMAND = $$CFG{'environments'}{$UUID}{'prepend command'};
my $QUOTE_COMMAND = $$CFG{'environments'}{$UUID}{'quote command'} && $USE_PREPEND_COMMAND;
my $MANUAL = $$CFG{'environments'}{$UUID}{'auth type'} eq 'manual';
my $TMP_UUID = $$CFG{'tmp'}{'uuid'};
my $LOG_FILE = $$CFG{'tmp'}{'log file'};
my $SOCK_FILE = $$CFG{'tmp'}{'socket'};
my $SOCK_EXEC_FILE = $$CFG{'tmp'}{'socket exec'};
my $REMOVE_CTRL_CHARS = $$CFG{'environments'}{$UUID}{'save session logs'} ? $$CFG{'environments'}{$UUID}{'remove control chars'} : $$CFG{'defaults'}{'remove control chars'};
my @KEEPASS = @{$$CFG{'keepass'} // []};
my $LPORT;

if (!$RESTART && $IS_CLUSTER) {
    $RESTART = 2;
}

$IP = subst($IP);
$PORT = subst($PORT);
# Use keepass
if ($$CFG{'environments'}{$UUID}{'infer user pass from KPX'} && $$CFG{'defaults'}{'keepass'}{'use_keepass'}) {
    # TODO : I think this will never happen previous if already stated that use_keepass is true
    if (!$$CFG{'defaults'}{'keepass'}{'use_keepass'}) {
        msg("ERROR: KeePassX can not be used because 'KeePassX' is not enabled under 'Preferences->KeePass Options'");
        exit 1;
    }

    my $re = $$CFG{'environments'}{$UUID}{'KPX title regexp'};
    my $where = $KPXWHERE[$$CFG{'environments'}{$UUID}{'infer from KPX where'}];
    my @found = findKP(\@KEEPASS, $where, qr/$re/);
    if (! scalar @found) {
        msg("ERROR: No entry '$where' found on KeePassX matching '$re'");
        exit 1;
    } elsif (((scalar @found) > 1) && $$CFG{'defaults'}{'keepass'}{'ask_user'}) {
        msg("INFO: Found " . (scalar @found) . " entries with '$where' matching '$re' while inferring. Asking user...");
        my $tmp = "<ASK:KeePass '$where' matching '$TITLE':";
        foreach my $hash (@found) {
            $tmp .= "|$$hash{$where}";
        }
        $tmp .= '>';
        my ($str, $pos) = subst($tmp);
        if (! defined $str) {
            msg("INFO: Connection canceled by user");
            exit 1;
        }
        ($USER, $PASS) = ($found[$pos]{username}, $found[$pos]{password});
    } else {
        msg("INFO: Using with $where '$found[0]{$where}' matching '$re' (username: $USER, password: hidden!!) while inferring...");
        ($USER, $PASS) = ($found[0]{username}, $found[0]{password});
    }
} else {
    $USER = subst($USER);
    $PASS = subst($PASS);
}
$PASSPHRASE = subst($PASSPHRASE);
$PASSPHRASE_USER = subst($PASSPHRASE_USER);

# Build initial SSH connection command
my $CONNECT_OPTS = $$CFG{'environments'}{$UUID}{'options'} || '';

if (! $GETCMD) {
    $SOCKET = IO::Socket::UNIX->new(
        Type => SOCK_STREAM,
        Peer => $SOCK_FILE
    ) or die "ERROR: Could not open SOCKET file '$SOCK_FILE' for connecting: $!";
    $SOCKET->autoflush;

    if (!auth($SOCKET)) {
        die "ERROR: Service listening at file '$SOCK_FILE' is not PAC";
    }

    $SOCKET_EXEC = IO::Socket::UNIX->new(
        Type => SOCK_STREAM,
        Peer => $SOCK_EXEC_FILE
    ) or die "ERROR: Could not open SOCKET file '$SOCK_EXEC_FILE' for connecting: $!";
    $SOCKET_EXEC->autoflush;
}

my %_W;
my $INT = 0;

########################################################
# START : Procedures definition

sub msg {
    my $msg = shift or die "$COLOR{'err'}ERROR: You must provide a message to 'msg'!!$COLOR{'norm'}";

    print "$COLOR{'log'}\[$Script($$)][$NAME][$TITLE]: $msg\r\f$COLOR{'norm'}";
    return 1;
}

sub ctrl {
    my $msg = shift or die "ERROR: You must provide a message to 'ctrl'!!";

    if ($DEBUG) {
        msg($msg);
    }
    if (!defined $SOCKET) {
        return 1;
    }

    my $wout = '';
    vec($wout, fileno($SOCKET), 1) = 1;
    select(undef, $wout, undef, 5) or die "ERROR: Could not write to PACMain SOCKET at $SOCK_FILE: $!";

    $SOCKET->send('PAC_MSG_START[' . encode('UTF-16', $msg) . ']PAC_MSG_END');
    return 1;
}
sub auth {
    my $socket = shift;

    &ctrl("!!_PAC_AUTH_[$$CFG{tmp}{uuid}]!!");
    my $auth = '';
    sysread($socket, $auth, 1024);
    return $auth eq "!!_PAC_AUTH_[$$CFG{tmp}{uuid}]!!";
}

sub subst {
    my $string = shift // '';

    my $pos = -1;

    if (!defined $$CFG{'environments'}{$UUID}) {
        return $string;
    }

    $string =~ s/\<\<(proxy|jump)_host\>\>/$proxy_ip/g;
    $string =~ s/\<\<(proxy|jump)_port\>\>/$proxy_port/g;
    $string =~ s/\<\<(proxy|jump)_user\>\>/$proxy_user/g;
    $string =~ s/\<\<(proxy|jump)_pass\>\>/$proxy_pass/g;
    #$string =~ s/\<\<jump_key\>\>/$proxy_key/g;

    my $tstamp = time;
    my ($dy, $dm, $dd, $th, $tm, $ts) = split('_', strftime("%Y_%m_%d_%H_%M_%S", localtime));
    my $name = $$CFG{'environments'}{$UUID}{name};
    my $title = $$CFG{'environments'}{$UUID}{title};
    my $ip = $$CFG{'environments'}{$UUID}{ip};
    my $user = $$CFG{'environments'}{$UUID}{user};
    my $pass = $$CFG{'environments'}{$UUID}{pass};

    while ($string =~ /<UUID>/go) {
        $string =~ s/<UUID>/$UUID/g;
    }
    while ($string =~ /<TIMESTAMP>/go) {
        $string =~ s/<TIMESTAMP>/$tstamp/g;
    }
    while ($string =~ /<DATE_Y>/go) {
        $string =~ s/<DATE_Y>/$dy/g;
    }
    while ($string =~ /<DATE_M>/go) {
        $string =~ s/<DATE_M>/$dm/g;
    }
    while ($string =~ /<DATE_D>/go) {
        $string =~ s/<DATE_D>/$dd/g;
    }
    while ($string =~ /<TIME_H>/go) {
        $string =~ s/<TIME_H>/$th/g;
    }
    while ($string =~ /<TIME_M>/go) {
        $string =~ s/<TIME_M>/$tm/g;
    }
    while ($string =~ /<TIME_S>/go) {
        $string =~ s/<TIME_S>/$ts/g;
    }
    while ($string =~ /<NAME>/go) {
        $string =~ s/<NAME>/$name/g;
    }
    while ($string =~ /<TITLE>/go) {
        $string =~ s/<TITLE>/$title/g;
    }
    while ($string =~ /<IP>/go) {
        $string =~ s/<IP>/$ip/g;
    }
    while ($string =~ /<USER>/go) {
        $string =~ s/<USER>/$user/g;
    }
    while ($string =~ /<PASS>/go) {
        $string =~ s/<PASS>/$pass/g;
    }

    # Replace '<command prompt>' with user defined value for command prompt
    while ($string =~ /<command prompt>/go) {
        $string = $COMMAND_PROMPT;
    }

    # Replace <KPXRE_GET_(title|username|password|url)_WHERE_(title|username|password|url)==(.+?)==> with KeePassX value
    while ($string =~ /<KPXRE_GET_(title|username|password|url)_WHERE_(title|username|password|url)==(.+?)==>/go) {
        my $what = $1;
        my $where = $2;
        my $var = $3;
        my $regexp = qr/$var/;

        if (! $$CFG{'defaults'}{'keepass'}{'use_keepass'}) {
            msg("WARNING: KeePassX variable '$var' can not be used because 'KeePassX' is not enabled under 'Preferences->KeePass Options'");
            next;
        }

        my @found = findKP(\@KEEPASS, $where, $regexp);
        if (! scalar @found) {
            msg("ERROR: No entry '$where' found on KeePassX matching '$var'");
            exit 1;
        } elsif (((scalar @found) > 1) && $$CFG{'defaults'}{'keepass'}{'ask_user'}) {
            msg("INFO: Found more than one entry for '$where' with value '$var'. Asking user...");
            my $tmp = "<ASK:KeePass Passwords matching '$where' like '$var':";
            foreach my $hash (@found) {
                $tmp .= "|$$hash{$what}";
            }
            $tmp .= '>';
            $string =~ s/<KPXRE_GET_${what}_WHERE_${where}==\Q$var\E==>/$tmp/g;
        } else {
            msg("INFO: Found " . (scalar(@found)) ." entries for '$where' with value '$var'. Selected first entry...") if (scalar(@found) > 1) && ! $$CFG{'defaults'}{'keepass'}{'ask_user'};
            $string =~ s/<KPXRE_GET_${what}_WHERE_${where}==\Q$var\E==>/$found[0]{$what}/g;
        }
    }

    # Replace <KPX_(title|username|url):*> with KeePassX password value
    while ($string =~ /<KPX_(title|username|url):(.+?)>/go) {
        my $type = $1;
        my $var = $2;

        if (! $$CFG{'defaults'}{'keepass'}{'use_keepass'}) {
            msg("WARNING: KeePassX variable '$var' can not be used because 'KeePassX' is not enabled under 'Preferences->KeePass Options'");
            next;
        }

        my @found = findKP(\@KEEPASS, $type, $var);
        if (! scalar @found) {
            msg("ERROR: No entry '$type' found on KeePassX matching '$var'"); exit 1;
        } elsif (((scalar @found) > 1) && $$CFG{'defaults'}{'keepass'}{'ask_user'}) {
            msg("WARNING: Found more than one entry for '$type' with value '$var'. Asking user...");
            my $tmp = "<ASK:KeePass Passwords matching '$type' like '$var':";
            foreach my $hash (@found) {
                $tmp .= "|$$hash{password}";
            }
            $tmp .= '>';
            $string =~ s/<KPX_$type:\Q$var\E>/$tmp/g;
        } else {
            $string =~ s/<KPX_$type:\Q$var\E>/$found[0]{password}/g;
        }
    }

    # Replace '<GV:.+>' with user saved global variables for '$connection_cmd' execution
    while ($string =~ /<GV:(.+?)>/go) {
        my $var = $1;
        if (defined $$CFG{'defaults'}{'global variables'}{$var}) {
            my $val = $$CFG{'defaults'}{'global variables'}{$var}{'value'} // '';
            $string =~ s/<GV:$var>/$val/g;
        }
    }

    # Replace '<V:#>' with user saved variables for '$connection_cmd'
    while ($string =~ /<V:(\d+?)>/go) {
        my $var = $1;
        if (defined $$CFG{'environments'}{$UUID}{'variables'}[$var]) {
            my $val = $$CFG{'environments'}{$UUID}{'variables'}[$var]{txt} // '';
            $string =~ s/<V:$var>/$val/g;
        }
    }

    # Replace '<ENV:#>' with environment variables for '$connection_cmd'
    while ($string =~ /<ENV:(.+?)>/go) {
        my $var = $1;
        if (defined $ENV{$var}) {
            $string =~ s/<ENV:\Q$var\E>/$ENV{$var}/g;
        }
    }

    # Replace '<ASK:#>' with user provided data for 'cmd' execution
    while ($string =~ /<ASK:(\d+?)>/go) {
        my $var = $1;
        my $val = wEnterValue(undef, "<b>Variable substitution '$var'</b>" , $string) // return undef;
        if (!defined $val) {
            last;
        }
        $string =~ s/<ASK:\Q$var\E>/$val/g;
    }

    # Replace '<ASK:*>' with user provided data for 'cmd' execution
    while ($string =~ /<ASK:(.+\|.+?)>/go) {
        my $var = $1;
        my @list;
        @list = split('\|', $var);
        my $desc = shift @list;
        my $ret;
        ($ret, $pos) = wEnterValue(undef, "<b>Choose variable value:</b>" , $desc, \@list);
        defined $ret or return undef;
        $string =~ s/<ASK:(.+\|.+?)>/$ret/g;
    }

    # Replace '<ASK:*>' with user provided data for 'cmd' execution
    while ($string =~ /<ASK:(.+?)>/go) {
        my $var = $1;
        my $val = wEnterValue(undef, "<b>Variable substitution</b>" , $var) // return undef;
        if (!defined $val) {
            last;
        }
        $string =~ s/<ASK:\Q$var\E>/$val/g;
    }

    # Replace '<CMD:#>' with the result of executing 'cmd'
    while ($string =~ /<CMD:(.+?)>/go) {
        my $var = $1;
        my $output = `$var 2>&1`;
        chomp $output;
        $string =~ s/<CMD:\Q$var\E>/$output/g;
    }

    return wantarray ? ($string, $pos) : $string;
}

sub wEnterValue {
    my $self = shift;
    my $lblup = shift;
    my $lbldown = shift;
    my $default = shift;
    my $visible = shift // 1;

    my @list;
    my $pos = -1;

    if (! defined $default) {
        $default = '';
    } elsif (ref($default)) {
        @list = @{$default};
    }

    # Create the dialog window,
    $_W{window}{data} = Gtk3::Dialog->new_with_buttons(
        "$APPNAME : Enter data",
        undef,
        'modal',
        'gtk-cancel' => 'cancel',
        'gtk-ok' => 'ok'
    );
    # and setup some dialog properties.
    $_W{window}{data}->set_default_response('ok');
    $_W{window}{data}->set_position('center');
    $_W{window}{data}->set_icon_from_file($APPICON);
    $_W{window}{data}->set_size_request(-1, -1);
    $_W{window}{data}->set_resizable(0);
    $_W{window}{data}->set_border_width(5);

    # Create an HBox to contain a picture and a label
    $_W{window}{gui}{hbox} = Gtk3::HBox->new(0, 0);
    $_W{window}{data}->get_content_area->pack_start($_W{window}{gui}{hbox}, 1, 1, 5);
    $_W{window}{gui}{hbox}->set_border_width(5);

    # Create image
    $_W{window}{gui}{img} = Gtk3::Image->new_from_stock('gtk-edit', 'dialog');
    $_W{window}{gui}{hbox}->pack_start($_W{window}{gui}{img}, 0, 1, 5);

    # Create 1st label
    $_W{window}{gui}{lblup} = Gtk3::Label->new;
    $_W{window}{gui}{hbox}->pack_start($_W{window}{gui}{lblup}, 1, 1, 5);
    $_W{window}{gui}{lblup}->set_markup($lblup);

    if (defined $lbldown) {
        # Create 2nd label
        $_W{window}{gui}{lbldwn} = Gtk3::Label->new;
        $_W{window}{data}->get_content_area->pack_start($_W{window}{gui}{lbldwn}, 1, 1, 5);
        $_W{window}{gui}{lbldwn}->set_text($lbldown);
    }

    if (@list) {
        # Create combobox widget
        $_W{window}{gui}{comboList} = Gtk3::ComboBoxText->new;
        $_W{window}{data}->get_content_area->pack_start($_W{window}{gui}{comboList}, 0, 1, 0);
        $_W{window}{gui}{comboList}->set_property('can_focus', 0);
        foreach my $text (@list) {
            $_W{window}{gui}{comboList}->append_text($text)
        };
        $_W{window}{gui}{comboList}->set_active(0);
    } else {
        # Create the entry widget
        $_W{window}{gui}{entry} = Gtk3::Entry->new;
        $_W{window}{data}->get_content_area->pack_start($_W{window}{gui}{entry}, 0, 1, 5);
        $_W{window}{gui}{entry}->set_text($default);
        $_W{window}{gui}{entry}->set_activates_default(1);
        $_W{window}{gui}{entry}->set_visibility($visible);
    }

    # Show the window (in a modal fashion)
    $_W{window}{data}->show_all;
    my $ok = $_W{window}{data}->run;

    my $val;
    if (@list) {
        $val = ($ok eq 'ok') ? $_W{window}{gui}{comboList}->get_active_text : undef;
        $pos = $_W{window}{gui}{comboList}->get_active;
    } else {
        $val = ($ok eq 'ok') ? $_W{window}{gui}{entry}->get_chars(0, -1) : undef;
    }

    $_W{window}{data}->destroy;
    while (Gtk3::events_pending) {
        Gtk3::main_iteration;
    }
    undef %_W;

    return wantarray ? ($val, $pos) : $val;
}

sub send_slow {
    if ($SEND_SLOW) {
        $_[0]->send_slow($SEND_SLOW, $_[1]);
    } else {
        $_[0]->send($_[1]);
    }
}

sub _getPrompt {
    # Delete any data accumulated before launching the command
    $EXP->clear_accum;
    $EXP->restart_timeout_upon_receive(1);

    send_slow($EXP, "\n");
    my ($matched_pattern_position, $error, $successfully_matching_string, $before_match, $after_match) = $EXP->expect(0.5, [timeout => sub {1;}], [/\R.+\R/ => sub {1;}]);
    $before_match =~ s/\R|\r|\f|\n//go;
    $before_match =~ s/(^\s*)|(\s*$)//go;
    return $before_match;
}

sub _execAndCapture {
    my $tmp = shift;

    my $pipe = $$tmp{pipe};
    my $tee = $$tmp{tee};
    my $cmd = $$tmp{cmd};
    my $pattern = $$tmp{prompt};
    my $capture = $$tmp{capture};
    my $ctrl = $$tmp{ctrl};
    my $lines = $$tmp{lines};
    my $intro = $$tmp{intro};

    $cmd = subst($cmd);
    $cmd ||= $$ctrl{cmd};

    # Delete any data accumulated before launching the command
    $EXP->clear_accum;
    $EXP->restart_timeout_upon_receive(1);

    if ($intro) {
        send_slow($EXP, $cmd . (($METHOD =~ /^.*3270.*$/) || ($METHOD eq 'IBM 3270/5250') ? "\r\f" : "\n"));
    } else {
        send_slow($EXP, $cmd);
    }
    if (! defined $pattern && ($pipe || $tee || $capture || defined $ctrl)) {
        send_slow($EXP, '##__PAC__PIPE__##');
    }

    $TIMEOUT_CMD = $$CFG{'environments'}{$UUID}{'terminal options'}{'use personal settings'} ?
        ($$CFG{'environments'}{$UUID}{'terminal options'}{'timeout command'} || undef)
        :
        ($$CFG{'defaults'}{'timeout command'} || undef);

    $CONNECTED = 1;

    ctrl("PIPE_WAIT[" . ($TIMEOUT_CMD // 'indefinitely') . "][" . ((! defined $pattern && ($pipe || $tee || $capture || defined $ctrl)) ? '##__PAC__PIPE__##' : $pattern // '') . "]");

    my $ok = 0;
    # Wait for pattern prompt before continue...
    my ($matched_pattern_position, $error, $successfully_matching_string, $before_match, $after_match) = $EXP->expect(

        $TIMEOUT_CMD,

        [timeout => sub {ctrl("ERROR:$TIMEOUT_CMD seconds waiting for expected input");}],

        [eof => sub {
            ctrl("ERROR:Connection ended by remote peer!! " . $EXP->set_accum());
            $CONNECTED = 0;
            $EXP->hard_close;
        }],

        [((! defined $pattern && ($pipe || $tee || $capture || defined $ctrl)) ? '##__PAC__PIPE__##' : (defined $pattern ? $pattern : '')) => sub {$ok = 1;}]
    );

    if ($ok && ($pipe || $tee || $capture || defined $ctrl)) {
       if (! defined $pattern && ($pipe || $tee || $capture || defined $ctrl)) {
           send_slow($EXP, "\x08"x17);
       }

        # Separate lines (\R matches *all kind* of new-lines: \n, \f, \r, ...)
        my @out_lines = split(/\R/, $before_match);
        # Remove last (prompt) line from output
        pop @out_lines;
        # Re-join in a single string
        my $out = join("\n", @out_lines);

        if ($capture) {
            return $out;
        } elsif (defined $ctrl) {
            $lines //= 1;
            my $tmp = join("\n", splice(@out_lines, 1, $lines));
            # Line #0 uses to be the executed command; line #1 uses to be the output of that command (one line expected!!)
            ctrl("$$ctrl{ctrl}:$tmp");
        } elsif ($tee) {
            $tee =~ /^>{1,2}(.+)$/go or $tee = '>' . $tee;
            if (!open(F,"<:utf8",$tee)) {
                return 1;
            }
            print F $out;
            close F;
        } else {
            # Advise PAC to receive command output
            ctrl("EXEC:RECEIVE_OUT");
            nstore_fd(\$out, $SOCKET_EXEC) or die "ERROR:$!";
        }
    } else {
        # Advise PAC NOT to receive command output
        ctrl("EXEC:DISCARD_OUT");
    }

    return undef;
}

sub _execScript {
    my $tmp = shift;

    if (!defined $tmp) {
        return 0;
    }

    my $name = $$tmp{name};
    my $script = $$tmp{script};
    if (!defined $script || !defined $name) {
        return 0;
    }

    our %COMMON; undef %COMMON;
    our %PAC; undef %PAC;
    our %TERMINAL; undef %TERMINAL;
    our %SHARED; undef %SHARED;

    $COMMON{subst} = sub {
        my $txt = shift // '';
        ctrl("SCRIPT_SUB_SUBST[NAME:$name][PID:$$][PARAMS:$txt]");
        return subst($txt);
    };
    $COMMON{cfg} = sub {my $ref = shift // 0; return $ref ? $CFG : dclone($CFG);};
    $COMMON{cfg_sanity} = sub {_cfgSanityCheck(shift);};
    $COMMON{del_esc} = sub {return _removeEscapeSeqs(shift // '');};

    $TERMINAL{exp} = $EXP;
    $TERMINAL{name} = $NAME;
    $TERMINAL{method} = $METHOD;
    $TERMINAL{uuid} = $UUID;
    $TERMINAL{tmp_uuid} = $TMP_UUID;
    $TERMINAL{error} = '';
    $TERMINAL{ask} = sub {
        my $txt = shift;
        my $visible = shift // 1;

        ctrl("SCRIPT_SUB_ASK[NAME:$name][PID:$$][PARAMS:$txt, $visible]");

        return wEnterValue(undef, "<b>PAC SCRIPT USER INPUT</b>" , $txt, undef, $visible);
    };
    $TERMINAL{msg} = sub {msg(subst(shift));};
    $TERMINAL{log} = sub {
        my $file = shift // '';
        $file = subst($file);

        ctrl("SCRIPT_SUB_LOG[NAME:$name][PID:$$][PARAMS:$file]");

        $EXP->log_file->flush;
        $EXP->log_file($file);
        return $EXP->log_file;
    };
    $TERMINAL{send} = sub {
        my $txt = shift // '';
        $txt = subst($txt);

        ctrl("SCRIPT_SUB_SEND[NAME:$name][PID:$$][PARAMS:$txt]");

        $EXP->clear_accum;
        send_slow($EXP, $txt);
    };
    $TERMINAL{send_get} = sub {
        my $txt = shift // '';
        my $intro = shift // 1;

        ctrl("SCRIPT_SUB_SEND_GET[NAME:$name][PID:$$][PARAMS:$txt]");

        my $out = _execAndCapture({'capture' => 1, 'cmd' => $txt, 'intro' => $intro});
        if (!defined $out) {
            return undef;
        }
        $out =~ s/^.+?\R//go;
        return _removeEscapeSeqs($out);
    };
    $TERMINAL{get_prompt} = sub {
        my $del_esq = shift // 1;
        ctrl("SCRIPT_SUB_GET_PROMPT[NAME:$name][PID:$$][PARAMS:]");
        my $prompt = _getPrompt();
        if ($del_esq) {
            $prompt = _removeEscapeSeqs($prompt);
        }
        return "\Q$prompt\E";
    };
    $TERMINAL{expect} = sub {
        my $pattern = shift // '';
        my $tmout = shift // 1;

        ctrl("SCRIPT_SUB_EXPECT[NAME:$name][PID:$$][PARAMS:$pattern, $tmout]");

        $pattern = subst($pattern);
        $tmout = subst($tmout);

        $TERMINAL{out1} = undef;
        $TERMINAL{out2} = undef;
        $TERMINAL{error} = '';

        my $wait = $tmout;
        while ($wait > 0) {
            $EXP->expect(
                1,
                [eof => sub {$TERMINAL{error} = "Connection closed while waiting for pattern '$pattern'", exit 1;}],
                [$pattern => sub {
                    $TERMINAL{out1} = $EXP->before;
                    $TERMINAL{out2} = $EXP->after;
                    $TERMINAL{error} = '';
                    last;
                }]
            );
            $wait--;
        }
        if (! $wait) {
            $TERMINAL{error} = "Timeout ($tmout seconds) waiting for pattern '$pattern'";
            return 0;
        }
        return $TERMINAL{error} eq '';
    };

    # Start PAC Script
    ctrl("SCRIPT_START[NAME:$name]");

    no warnings ('redefine');

    *OLDERR = *STDERR;
    open STDERR, ">/dev/null";

    eval $script;
    %SHARED = %{$$tmp{shared}};

    *STDERR = *OLDERR;

    if ($@) {
        return 0;
    }
    use warnings;

    if (!defined &CONNECTION) {
        return 1;
    }
    eval {
        local $SIG{'TERM'} = sub {
            ctrl("SCRIPT_STOPPED_MANUALLY[NAME:$name]");
            msg("PAC Script '$name' terminated by user request (TERM(15) signal)");
            if (defined $_W{window}{data}){
                $_W{window}{data}->destroy;
            }
            undef %_W;
            while (Gtk3::events_pending) {
                Gtk3::main_iteration;
            }
            die;
        };

        &CONNECTION;
    };

    ctrl("SCRIPT_STOP[NAME:$name]");

    return 1;
}

sub findKP {
    my $kp = shift;
    my $where = shift // 'title';
    my $what = shift // qr/.*/;

    my @found;
    foreach my $hash (@{$kp}) {
        if (ref($what) eq 'Regexp') {
            if ($$hash{$where} !~ /^$what$/) {
                next;
            }
        } elsif ($$hash{$where} ne $what) {
            next;
        }
        push(@found, {
            title => $$hash{title},
            url => $$hash{url},
            username => $$hash{username},
            password => $$hash{password},
            created => $$hash{created},
            comment => $$hash{comment}
        });
    }

    return wantarray ? @found : scalar(@found);
}

sub _setProxy {
    my $method = shift;
    my $user = $USER;

    # Set variables in case they are used in function : sub subst()
    my $ssh_config = '';
    if (($$CFG{'environments'}{$UUID}{'use proxy'} == 3) || ($$CFG{'environments'}{$UUID}{'use proxy'} == 0)) {
        if ($AUTH eq 'publickey') {
            $user = $PASSPHRASE_USER;
        }
        if (-e "/etc/ssh/ssh_config") {
            open (SSHC,"<:utf8","/etc/ssh/ssh_config");
            while (my $l = <SSHC>) {
                $l =~ s/\n//g;
                $l =~ s/^[\t ]+//;
                if (!$l) {
                    next;
                } elsif ($l =~ /^#/) {
                    next;
                } elsif ($l =~ /^Host \*/) {
                    next;
                }
                $ssh_config .= "$l\n";
            }
            close SSHC;
        }
    }

    if ($$CFG{'environments'}{$UUID}{'use proxy'} == 1) {
        # Use this connection SOCKS Proxy settings
        $proxy_ip = $$CFG{'environments'}{$UUID}{'proxy ip'};
        $proxy_port = $$CFG{'environments'}{$UUID}{'proxy port'};
        $proxy_user = $$CFG{'environments'}{$UUID}{'proxy user'};
        $proxy_pass = $$CFG{'environments'}{$UUID}{'proxy pass'};
        $proxy_cmd  = _getProxyCmd($proxy_type, $proxy_ip, $proxy_port, $proxy_user, $proxy_pass);
        if ($method =~ /ssh/i) {
            $CONNECT_OPTS .= " -o ProxyCommand='$proxy_cmd %h %p'";
        } else {
            _openSocksTunnel();
        }
    } elsif ($$CFG{'environments'}{$UUID}{'use proxy'} == 3) {
        # Use this connection Jump Server settings
        $proxy_jump = 1;
        $proxy_ip = $$CFG{'environments'}{$UUID}{'jump ip'};
        $proxy_port = $$CFG{'environments'}{$UUID}{'jump port'};
        $proxy_user = $$CFG{'environments'}{$UUID}{'jump user'};
        $proxy_pass = $$CFG{'environments'}{$UUID}{'jump pass'};
        open(CFG,">:utf8","$CFG_DIR/tmp/$UUID.conf");
        print CFG qq“Host *\n$ssh_config\nProtocol 2\nCompression yes\nTCPKeepAlive yes\nServerAliveInterval 60\nPreferredAuthentications publickey,hostbased,keyboard-interactive,password\n“;
        print CFG qq“\nHost jumpserver\nHostName $proxy_ip\nPort $proxy_port\nUser $proxy_user\n“;
        if ($$CFG{'environments'}{$UUID}{'jump key'}) {
            print CFG "IdentityFile $$CFG{'environments'}{$UUID}{'jump key'}\n";
        }
        if ($method =~ /ssh/i) {
            # Second server is only important for SSH JumpHost
            print CFG qq“\nHost server\nHostName $IP\nUser $user\nPort $PORT\n“;
            if (($PUBKEY)&&($$CFG{'environments'}{$UUID}{'auth type'} eq 'publickey')) {
                print CFG "IdentityFile $PUBKEY\n";
            }
        }
        close CFG;
        if ($method =~ /VNC|RDP/i) {
            _openSshTunnel($UUID);
        }
    } elsif ($$CFG{'environments'}{$UUID}{'use proxy'} == 0) {
        # Use global configuration settings
        if ($$CFG{'defaults'}{'proxy'} eq 'Jump') {
            # Jump Server settings
            $proxy_jump = 1;
            $proxy_ip = $$CFG{'defaults'}{'jump ip'};
            $proxy_port = $$CFG{'defaults'}{'jump port'};
            $proxy_user = $$CFG{'defaults'}{'jump user'};
            $proxy_pass = $$CFG{'defaults'}{'jump pass'};
            open(CFG,">:utf8","$CFG_DIR/tmp/$UUID.conf");
            print CFG qq“Host *\n$ssh_config\nProtocol 2\nCompression yes\nTCPKeepAlive yes\nServerAliveInterval 60\nPreferredAuthentications publickey,hostbased,keyboard-interactive,password\n“;
            print CFG qq“\nHost jumpserver\nHostName $proxy_ip\nPort $proxy_port\nUser $proxy_user\n“;
            if ($$CFG{'defaults'}{'jump key'}) {
                print CFG "IdentityFile $$CFG{'defaults'}{'jump key'}\n";
            }
            if ($method =~ /ssh/i) {
                # Second server is only important for SSH JumpHost
                print CFG qq“\nHost server\nHostName $IP\nUser $user\nPort $PORT\n“;
                if (($PUBKEY)&&($$CFG{'environments'}{$UUID}{'auth type'} eq 'publickey')) {
                    print CFG "IdentityFile $PUBKEY\n";
                }
            }
            close CFG;
            if ($method =~ /VNC|RDP/i) {
                _openSshTunnel($UUID);
            }

        } elsif ($$CFG{'defaults'}{'proxy'} eq 'Proxy') {
            # SOCKS Proxy settings
            $proxy_ip = $$CFG{'defaults'}{'proxy ip'};
            $proxy_port = $$CFG{'defaults'}{'proxy port'};
            $proxy_user = $$CFG{'defaults'}{'proxy user'};
            $proxy_pass = $$CFG{'defaults'}{'proxy pass'};
            $proxy_cmd  = _getProxyCmd($proxy_type, $proxy_ip, $proxy_port, $proxy_user, $proxy_pass);
            if ($method =~ /ssh/i) {
                $CONNECT_OPTS .= " -o ProxyCommand='$proxy_cmd  %h %p'";
            } else {
                _openSocksTunnel();
            }
        }
    }
    return 0;
}

sub _getProxyCmd {
    my ($proxy_type, $proxy_ip, $proxy_port, $proxy_user, $proxy_pass) = @_;

    if ($proxy_user ne '') {
        `which ncat 1>/dev/null 2>&1`;
        if (! $?) {
            return "ncat --proxy $proxy_ip:$proxy_port --proxy-type $proxy_type --proxy-auth $proxy_user:$proxy_pass";
        }
        ctrl("UNHIDE_TERMINAL");
        print("$COLOR{'err'}ERROR$COLOR{'norm'}: ncat is required if using proxy user/password.\nncat is part of the nmap project (https://nmap.org/ncat/).\n");
        die();
    } else {
        return "nc -x $proxy_ip:$proxy_port";
    }
}

sub _openSshTunnel {
    my ($uuid) = @_;

    $proxy_jump = 2;

    if (-e "$CFG_DIR/tmp/$uuid.ctl") {
        # Avoid open a second terminal with the same UUID
        my $check = `ssh -F $CFG_DIR/tmp/$uuid.conf -S $CFG_DIR/tmp/$uuid.ctl -T -O check jumpserver 2>&1`;
        if ($check =~ /=\d+/) {
            ctrl("UNHIDE_TERMINAL");
            die "$COLOR{'err'}Can not duplicate VNC/RDP connections$COLOR{'norm'}\nConfigure a different second connection.\n\n";
        }
    }
    # Check if port we want to bind is not alreay in use
    $LPORT = _getLocalPort($PORT);
    system "ssh -F $CFG_DIR/tmp/$uuid.conf -S $CFG_DIR/tmp/$uuid.ctl -f -N -T -M -L $LPORT:$IP:$PORT jumpserver";
    $IP = '127.0.0.1';
    $PORT = $LPORT;
}

sub _openSocksTunnel {
    my $pipe = "$CFG_DIR/tmp/pipe_$UUID";

    $LPORT = _getLocalPort($PORT);
    $proxy_cmd = _getProxyCmd($proxy_type, $proxy_ip, $proxy_port, $proxy_user, $proxy_pass);

    unlink($pipe);
    system("mkfifo $pipe");
    system("nc -l -p $LPORT <$pipe | $proxy_cmd $IP $PORT >$pipe &");
    $IP = '127.0.0.1';
    $PORT = $LPORT;
}

# Find out a free local TCP port
sub _getLocalPort {
    my $LPORT = shift;
    my $break = 100;

    while ($break && (my $msg = `netstat -lnt | grep $LPORT 2>&1`)) {
        if ($msg !~ /:$LPORT/) {
            # netstat not installed?
            last;
        }
        $LPORT++;
        $break--;
    }
    return $LPORT;
}

# Place any thing that has to be done before a hard_close
sub _hardClose {
    my ($uuid)  = @_;
    my $pipe = "$CFG_DIR/tmp/pipe_$uuid";

    if ($proxy_jump == 2) {
        system "ssh -F $CFG_DIR/tmp/$uuid.conf -S $CFG_DIR/tmp/$uuid.ctl -T -O exit jumpserver";
    }
    if (-e $pipe) {
        unlink($pipe);
    }
    if ($IS_CLUSTER && ($RESTART == 2)) {
        ctrl('RESTART');
    }
    return 0;
}

# If we want a perfect fit of an embedded RDP tab, we can use X11::GUITest if available
# We should eval in BEGIN or perl warns "Too late to run INIT block"
my $module_guitest;
BEGIN{
    eval("use X11::GUITest qw (GetWindowPos GetRootWindow)");

    if ($@) {
        $module_guitest="N";
    } else {
        $module_guitest="Y";
    }
}
# END OF : Procedures definition
########################################################

# Prepare Networking

# Choose and prepare the connection method
my $connection_cmd = '';
my $connection_txt = '';

if (defined $METHOD) {
    ##############################################
    # TERMINAL METHODS (ssh, telnet, etc)
    ##############################################
    if (($METHOD =~ /^.*ssh.*$/) || ($METHOD eq 'SSH')) {
        _setProxy('ssh');
        if ($METHOD ne 'autossh') {
            $METHOD = 'ssh';
        }
        my $key = '';
        if ($AUTH eq 'publickey') {
            $key = ($PUBKEY ? "-i \"$PUBKEY\"" : "") . ($AUTHFALLBACK ? '' : ' -o "PreferredAuthentications=publickey"');
            $USER = $PASSPHRASE_USER;
            $PASS = $PASSPHRASE;
        } elsif (! $AUTHFALLBACK) {
            $key = '-o "PreferredAuthentications=password,keyboard-interactive"';
        }
        if ($proxy_jump) {
            $connection_cmd = "$METHOD -F $CFG_DIR/tmp/$UUID.conf $CONNECT_OPTS -J jumpserver server";
        } else {
            $connection_cmd = "$METHOD -p $PORT $key $CONNECT_OPTS " . ($USER ? "-l $USER " : '') . "$IP";
        }
        $connection_txt = $connection_cmd;
    } elsif (($METHOD =~ /^.*mosh.*$/) || ($METHOD eq 'MOSH')) {
        $METHOD = 'mosh';
        if ($PORT != 22) {
            $CONNECT_OPTS .= " --ssh=\"ssh -p $PORT\"";
        }
        my $key = '';
        if ($AUTH eq 'publickey') {
            $key = ($PUBKEY ? "-i \"$PUBKEY\"" : "") . ($AUTHFALLBACK ? '' : ' -o "PreferredAuthentications=publickey"');
            $USER = $PASSPHRASE_USER;
            $PASS = $PASSPHRASE;
            $CONNECT_OPTS = " --ssh=\"ssh -p $PORT $key\"";
        } elsif (! $AUTHFALLBACK) {
            $key = '-o "PreferredAuthentications=password,keyboard-interactive"';
        }
        $connection_cmd = "$METHOD $CONNECT_OPTS ${USER}\@$IP";
        $connection_txt = $connection_cmd;
    } elsif (($METHOD =~ /^.*sftp.*$/) || ($METHOD eq 'SFTP')) {
        _setProxy('ssh');
        $METHOD = 'sftp';
        my $key = '';
        if ($AUTH eq 'publickey') {
            $key = ($PUBKEY ? "-i \"$PUBKEY\"" : "") . ($AUTHFALLBACK ? '' : ' -o "PreferredAuthentications=publickey"');
            $USER = $PASSPHRASE_USER;
            $PASS = $PASSPHRASE;
        } elsif (! $AUTHFALLBACK) {
            $key = '-o "PreferredAuthentications=password,keyboard-interactive"';
        }
        $connection_cmd = "$METHOD $key -P $PORT $CONNECT_OPTS " . ($USER ? "$USER@" : '') . "$IP";
        $connection_txt = $connection_cmd;
    } elsif (($METHOD =~ /^.*telnet.*$/) || ($METHOD eq 'Telnet')) {
        $METHOD = 'telnet';
        my $port = ($PORT == 23) ? '' : $PORT;
        $connection_cmd = "$METHOD $CONNECT_OPTS $IP $port";
        $connection_txt = $connection_cmd;
    } elsif (($METHOD =~ /^.*ftp*$/) || ($METHOD eq 'FTP')) {
        $METHOD = 'ftp';
        my $port = ($PORT == 21) ? '' : $PORT;
        $connection_cmd = "$METHOD $CONNECT_OPTS $IP $port";
        $connection_txt = $connection_cmd;
    } elsif (($METHOD =~ /^.*cadaver*$/) || ($METHOD eq 'WebDAV')) {
        $METHOD = 'cadaver';
        $connection_cmd = "$METHOD $CONNECT_OPTS $IP";
        $connection_txt = $connection_cmd;
    } elsif (($METHOD =~ /^.*cu$/) || ($METHOD eq 'Serial (cu)')) {
        $METHOD = 'cu';
        $connection_cmd = "$METHOD $CONNECT_OPTS $IP";
        $connection_txt = $connection_cmd;
    } elsif (($METHOD =~ /^.*remote-tty$/) || ($METHOD eq 'Serial (remote-tty)')) {
        $METHOD = 'remote-tty';
        $connection_cmd = "$METHOD $CONNECT_OPTS" . ($USER ? " -l $USER" : '') . " $IP";
        $connection_txt = $connection_cmd;
    } elsif (($METHOD =~ /^.*3270.*$/) || ($METHOD eq 'IBM 3270/5250')) {
        $METHOD = 'c3270';
        $CONNECT_OPTS =~ s/\s+-prepend_([P|S|N|L])//go;
        my $modifier = $1;
        $connection_cmd = "$METHOD $CONNECT_OPTS " . ($modifier ? "$modifier:" : '') . "[$IP]:$PORT";
        $connection_txt = $connection_cmd;
    }
    ##############################################
    # TERMINAL DESKTOP METHODS (rdp, vnc, etc)
    ##############################################
    elsif ($METHOD =~ /^.*RDP \((.+)\).*$/) {
        $METHOD = $1;
        _setProxy('RDP');
        if ((defined $$CFG{'tmp'}{'xid'}) && ($METHOD eq 'rdesktop')) {
            if ($module_guitest eq 'Y') {
                my ($xpos, $ypos, $Xwidth, $Xheight, $borderWidth, $_screen) = GetWindowPos($$CFG{'tmp'}{'xid'});
                if ($Xwidth > 3 && $Xheight > 1) {
                    $$CFG{'tmp'}{'width'} = $Xwidth - 2;
                    $$CFG{'tmp'}{'height'} = $Xheight;
                }
            }
            $connection_cmd = "$METHOD -X $$CFG{'tmp'}{'xid'} -g $$CFG{'tmp'}{'width'}x$$CFG{'tmp'}{'height'} $CONNECT_OPTS" . ($MANUAL ? '' : " -u $USER -p -") . " $IP:$PORT";
            $connection_txt = "$METHOD -X $$CFG{'tmp'}{'xid'} -g $$CFG{'tmp'}{'width'}x$$CFG{'tmp'}{'height'} $CONNECT_OPTS" . ($MANUAL ? '' : " -u $USER -p -") . " $IP:$PORT";
        } elsif ((defined $$CFG{'tmp'}{'xid'}) && ($METHOD =~ /^.*freerdp$/)) {
            if ($module_guitest eq 'Y') {
                my ($xpos, $ypos, $Xwidth, $Xheight, $borderWidth, $_screen) = GetWindowPos($$CFG{'tmp'}{'xid'});
                if ($Xwidth > 1 && $Xheight > 1) {
                    $$CFG{'tmp'}{'width'} = $Xwidth;
                    $$CFG{'tmp'}{'height'} = $Xheight;
                }
            }
            $connection_cmd = "$METHOD /parent-window:$$CFG{'tmp'}{'xid'} /size:$$CFG{'tmp'}{'width'}x$$CFG{'tmp'}{'height'} $CONNECT_OPTS" . ($MANUAL ? '' : " /u:'$USER' /p:'$PASS'") . " /v:$IP:$PORT";
            $connection_txt = "$METHOD /parent-window:$$CFG{'tmp'}{'xid'} /size:$$CFG{'tmp'}{'width'}x$$CFG{'tmp'}{'height'} $CONNECT_OPTS" . ($MANUAL ? '' : " /u:'$USER' /p:'<<hidden_password>>'") . " /v:$IP:$PORT";
        } elsif (defined $$CFG{'tmp'}{'xid'}) {
            $connection_cmd = "$METHOD /parent-window:$$CFG{'tmp'}{'xid'} /size:$$CFG{'tmp'}{'width'}x$$CFG{'tmp'}{'height'} $CONNECT_OPTS" . ($MANUAL ? '' : " /u:'$USER' /p:'$PASS'") . " /v:$IP:$PORT";
            $connection_txt = "$METHOD /parent-window:$$CFG{'tmp'}{'xid'} /size:$$CFG{'tmp'}{'width'}x$$CFG{'tmp'}{'height'} $CONNECT_OPTS" . ($MANUAL ? '' : " /u:'$USER' /p:'<<hidden_password>>'") . " /v:$IP:$PORT";
        } elsif ($METHOD eq 'rdesktop') {
            $connection_cmd = "$METHOD $CONNECT_OPTS" . ($MANUAL ? '' : " -u $USER -p -") . " -T \"$TITLE\" $IP:$PORT";
            $connection_txt = "$METHOD $CONNECT_OPTS" . ($MANUAL ? '' : " -u $USER -p -") . " -T \"$TITLE\" $IP:$PORT";
        } elsif (($METHOD =~ /^.*freerdp$/) && (defined $PASS)) {
            $connection_cmd = "$METHOD $CONNECT_OPTS" . ($MANUAL ? '' : " /u:$USER /p:'$PASS'") . " /t:\"$TITLE\" /v:$IP:$PORT";
            $connection_txt = "$METHOD $CONNECT_OPTS" . ($MANUAL ? '' : " /u:$USER /p:'<<hidden_password>>'") . " /t:\"$TITLE\" /v:$IP:$PORT";
        } elsif ($METHOD =~ /^.*freerdp$/) {
            $connection_cmd = "$METHOD $CONNECT_OPTS" . ($MANUAL ? '' : " /u:$USER") . " /t:\"$TITLE\" /v:$IP:$PORT";
            $connection_txt = "$METHOD $CONNECT_OPTS" . ($MANUAL ? '' : " /u:$USER") . " /t:\"$TITLE\" /v:$IP:$PORT";
        } else {
            $connection_cmd = "$METHOD $CONNECT_OPTS" . ($MANUAL ? '' : " -u $USER") . " $IP:$PORT -T \"$TITLE\"";
            $connection_txt = "$METHOD $CONNECT_OPTS" . ($MANUAL ? '' : " -u $USER") . " $IP:$PORT -T \"$TITLE\"";
        }
    }
    elsif (($METHOD =~ /^.*vncviewer$/) || ($METHOD eq 'VNC')) {
        $METHOD = 'vncviewer';
        if (($PASS eq '<<ASK_PASS>>') || ($MANUAL)) {
            ctrl("PASSWORD:Asking user for password...");
            $PASS = wEnterValue(undef, '<b>Manual Password requested</b>', "Enter Password for '$NAME'", '', 0) // '';
        }
        my $method = 'VNC';
        $CONNECT_OPTS =~ s/\s*-embed//go;
        if (`vncviewer --help 2>&1 | /bin/grep RealVNC`) {
            $method = 'RealVNC';
        } elsif (`vncviewer --help 2>&1 | /bin/grep TigerVNC`) {
            $method = 'TigerVNC';
        }
        _setProxy($method);
        if ($method eq 'TigerVNC') {
            my $pfile = "$CFG_DIR/tmp/pac_conn_{$$}_$UUID";
            system("echo \"$PASS\" | vncpasswd -f > $pfile");
            if (defined $$CFG{'tmp'}{'xid'}) {
                $connection_cmd = "$METHOD $CONNECT_OPTS -PasswordFile=$pfile -Parent=$$CFG{'tmp'}{'xid'} $IP:$PORT";
                $connection_txt = "$METHOD $CONNECT_OPTS -PasswordFile=$pfile -Parent=$$CFG{'tmp'}{'xid'} $IP:$PORT";
            } else {
                $connection_cmd = "$METHOD $CONNECT_OPTS -PasswordFile=$pfile $IP:$PORT";
                $connection_txt = "$METHOD $CONNECT_OPTS -PasswordFile=$pfile $IP:$PORT";
            }
        } elsif ($method eq 'RealVNC') {
            my $user = $USER ? "UserName=$USER" : '';

            $connection_cmd = "$METHOD $CONNECT_OPTS $user $IP:$PORT";
            $connection_txt = "$METHOD $CONNECT_OPTS $user $IP:$PORT";
            $METHOD = "Real$METHOD";
        } else {
            my $user = '';

            $connection_cmd = "echo \"$PASS\" | $METHOD $CONNECT_OPTS $user $IP:$PORT";
            $connection_txt = "echo \"<<hidden_password>>\" | $METHOD $CONNECT_OPTS $user $IP:$PORT";
        }
    }
    ##############################################
    # GENERIC METHOD (simple command line)
    ##############################################
    elsif (($METHOD =~ /^.*generic$/) || ($METHOD eq 'Generic Command')) {
        $connection_cmd = "$IP";
        $connection_txt = "$IP";
    }
    elsif ($METHOD eq 'PACShell') {
        $connection_cmd = "$$CFG{'defaults'}{'shell binary'} $$CFG{'defaults'}{'shell options'}";
        $connection_txt = "$$CFG{'defaults'}{'shell binary'} $$CFG{'defaults'}{'shell options'}";
    }
    ##############################################
    # UNKNOWN METHOD (error!)
    ##############################################
    else {
        my $string = "Unsupported connection method '$METHOD'.";
        msg($string);
        ctrl("ERROR:$string");
        ctrl("DISCONNECTED");
        exit 1;
    }
}

# Check for prepend commands
if ($QUOTE_COMMAND) {
    $connection_cmd = qq|"$connection_cmd"|;
}
if ($QUOTE_COMMAND) {
    $connection_txt = qq|"$connection_cmd"|;
}
if ($USE_PREPEND_COMMAND) {
    $connection_cmd = "$PREPEND_COMMAND $connection_cmd";
}
if ($USE_PREPEND_COMMAND) {
    $connection_txt = "$PREPEND_COMMAND $connection_txt";
}

# Check for 'sudo' use
if ($SUDO) {
    $connection_cmd = "sudo -p '$SUDO_PROMPT' $connection_cmd";
    $connection_txt = "sudo -p '$SUDO_PROMPT' $connection_txt";
}

# Check if there are non-generic terminal settings
if ($$CFG{'environments'}{$UUID}{'terminal options'}{'use personal settings'}) {
    $TIMEOUT_CONNECT = $$CFG{'environments'}{$UUID}{'terminal options'}{'timeout connect'} || undef;
    $TIMEOUT_CMD = $$CFG{'environments'}{$UUID}{'terminal options'}{'timeout command'} || undef;
    $COMMAND_PROMPT = $$CFG{'environments'}{$UUID}{'terminal options'}{'command prompt'};
    $USERNAME_PROMPT = $$CFG{'environments'}{$UUID}{'terminal options'}{'username prompt'};
    $PASSWORD_PROMPT = $$CFG{'environments'}{$UUID}{'terminal options'}{'password prompt'};
}

$connection_cmd = subst($connection_cmd);
if ($GETCMD) {
    print $connection_cmd;
    exit 0;
}

# Set log file
$EXP->log_file($LOG_FILE);

# Spawn the session
ctrl("SPAWNING:$connection_txt");
$EXP->spawn("$connection_cmd 2>&1") or die "Cannot spawn '$connection_cmd: $!\n";
ctrl("SPAWNED:'$connection_txt' (PID:$$)");

$EXP->exp_internal($DEBUG); # EXPECT DEBUG!!

if (($METHOD =~ /^.*rdesktop$/) || ($METHOD =~ /^.*freerdp.*/) || ($METHOD eq 'RDP (Windows)')) {
    $CONNECTED = 1;
    ctrl("CONNECTED");

    # No way to expect anything from 'rdesktop' command line...
    $EXP->expect($TIMEOUT_CONNECT,

        [eof => sub {
            $CONNECTED = 0;
            ctrl("CLOSE:Connection ended by remote peer!! " . $EXP->set_accum());
            _hardClose($UUID);
            $EXP->hard_close;
        }],

        ['\[ERROR\]', sub {
            ctrl("UNHIDE_TERMINAL");
        }],

        # Found certificate verification string
        ['^.+WARNING: CERTIFICATE NAME MISMATCH!.+$', sub {
            if ($ACCEPT_KEY) {
                send_slow($EXP, "Y\n");
                ctrl("HOSTKEY:accepted by configuration (sent 'Y')");
                exp_continue;
            } else {
                $CONNECTED = 0;
                send_slow($EXP, "N\n");
                ctrl("UNHIDE_TERMINAL");
                ctrl("CLOSE:HOSTKEY:rejected by configuration (sent 'N')");
            }
        }],

        # Found login string
        [$USERNAME_PROMPT, sub {

            if (($USER eq '') && ($USER_COUNT)) {
                ctrl("CLOSE:LOGIN:Empty username is not valid to login here");
                _hardClose($UUID);
                $EXP->hard_close();
            }
            $USER_COUNT++;
            $USER = subst($USER);
            ctrl("LOGIN:$USER");
            send_slow($EXP, "$USER\n");
            exp_continue;
            }],
        # Found password/passphrase string
        [$PASSWORD_PROMPT, sub {

        # First password attempt...
            if (! $PASSWORD_COUNT) {
                $PASSWORD_COUNT++;
                if (($PASS eq '<<ASK_PASS>>') || ($MANUAL)) {
                    ctrl("PASSWORD:Asking user for password...");
                    $PASS = wEnterValue(undef, '<b>Manual Password requested</b>', "Please, enter Password", '', 0);
                }
                if (defined $PASS) {
                    $EXP->log_stdout(0);
                    $PASS = subst($PASS);
                    send_slow($EXP, "$PASS\n", 'hide');
                    $EXP->log_stdout(1);
                    ctrl("PASSWORD:Sent (not shown)");
                    exp_continue;
                } else {
                    ctrl("UNHIDE_TERMINAL");
                    ctrl("CLOSE:PASSWORD:Password input cancelled by user");
                    $CONNECTED = 0;
                    _hardClose($UUID);
                    $EXP->hard_close;
                }
            }
            # ... second password attempt: provided password is no longer valid!!
            else {
                ctrl("CLOSE:PASSWORD:Provided username/password '$USER/<<hidden_password>>' was rejected");
                _hardClose($UUID);
                $EXP->hard_close;
            }
        }],
    )
}
elsif (($METHOD =~ /^.*vncviewer$/) || ($METHOD eq 'VNC')) {
    # Expect authentication confirmation...
    my ($matched_pattern_position, $error, $successfully_matching_string, $before_match, $after_match) = $EXP->expect($TIMEOUT_CONNECT,
        [timeout => sub {
            if ($METHOD =~ /Real/) {
                # Special case for RealVNC, no information to control timeout, logins, etc.
                $CONNECTED = 1;
                ctrl("CONNECTED");
            } else {
                $CONNECTED = 0;
                ctrl("CLOSE:TIMEOUT:$TIMEOUT_CONNECT seconds trying to connect or get prompt!!");
                _hardClose($UUID);
                $EXP->hard_close;
            }
        }],

        [eof => sub {

            $CONNECTED = 0;
            ctrl("CLOSE:Connection ended by remote peer!! " . $EXP->set_accum());
            _hardClose($UUID);
            $EXP->hard_close;
        }],

        # Found login string
        ['^.*Authentication successful.*$', sub {
            $CONNECTED = 1;
            ctrl("CONNECTED");
        }],

        # No auth is needed
        ['^.*No authentication needed*$', sub {
            $CONNECTED = 1;
            ctrl("CONNECTED");
        }],

        # Found login string
        ['^.*Conn:\s+[cC]onnected to host\s+.+\s+port\s+\d+.*$', sub {
            $CONNECTED = 1;
            ctrl("CONNECTED");
        }],

        # Found login string
        [$USERNAME_PROMPT, sub {

            if (($USER eq '') && ($USER_COUNT)) {
                ctrl("CLOSE:LOGIN:Empty username is not valid to login here");
                _hardClose($UUID);
                $EXP->hard_close();
            }
            $USER_COUNT++;
            $USER = subst($USER);
            ctrl("LOGIN:$USER");
            send_slow($EXP, "$USER\n");
            exp_continue;
        }],

        # Found password/passphrase string
        [$PASSWORD_PROMPT, sub {

            my $user = $EXP->before // '';
            $user =~ s/^(.+?)@.+/$1/go;

            ctrl("PASSWORD:Asking user for gateway's password...");
            $PASS = wEnterValue(undef, "<b>Gateway Password requested</b>", "Enter Password for gateway's user '$user'", '', 0);

            if (defined $PASS) {
                $EXP->log_stdout(0);
                $PASS = subst($PASS);
                send_slow($EXP, "$PASS\n", 'hide');
                $EXP->log_stdout(1);
                ctrl("PASSWORD:Sent (not shown)");
                exp_continue;
            } else {
                ctrl("CLOSE:PASSWORD:Password input cancelled by user");
                _hardClose($UUID);
                $EXP->hard_close;
            }
        }],

        # Found any other string (special case for the '-via' option)
        ['.*((open|connect) failed)|refused|(server closed).*', sub {
            ctrl("DISCONNECTED");
            $CONNECTED = 0;
        }],

    )
}
elsif (($METHOD =~ /^.*generic$/) || ($METHOD eq 'Generic Command') || ($METHOD =~ /^.*3270.*$/) || ($METHOD eq 'IBM 3270/5250')) {
    $CONNECTED = 1;

    # Do we have to do complex expects?
    if ((!$EXPECT || !$CONNECTED)) {
        return 1;
    }

    my $end = 0;
    my $avoid_first_expectation = 0;
    $EXP->restart_timeout_upon_receive(1);
    for(my $i = 0; $i < scalar(@{$$CFG{'environments'}{$UUID}{'expect'}}); $i++) {
        my $hash = $$CFG{'environments'}{$UUID}{'expect'}[$i];
        my $pattern = $$hash{'expect'} // '';
        my $command = $$hash{'send'} // '';
        my $hide = $$hash{'hidden'} // 0;
        my $active = $$hash{'active'} // 0;
        my $return = $$hash{'return'} // 1;
        my $on_match = $$hash{'on_match'} // -1;
        my $on_fail = $$hash{'on_fail'} // -1;
        my $time_out = $$hash{'time_out'} // -1;

        if (!$active) {
            next;
        }
        my $jump = 0;

        if ($time_out >= 0) {
            $TIMEOUT_CMD = $time_out;
        }

        $pattern = subst($pattern);

        ctrl("EXPECT:WAITING:$pattern");

        # Wait for pattern prompt before continue...
        $EXP->expect($TIMEOUT_CMD,

            [timeout => sub {
                if ($on_fail == -1) {
                    ctrl("CLOSE:TIMEOUT:$TIMEOUT_CMD seconds expecting pattern '$pattern'!!");
                    $CONNECTED = 0;
                    $EXP->hard_close;
                } elsif ($on_fail == -2) {
                    ctrl("EXPECT:ON_FAIL:timeout expecting '$pattern'. Finishing Expect");
                    $CONNECTED = 1;
                    $end = 1;
                } else {
                    ctrl("EXPECT:ON_FAIL:timeout expecting '$pattern'. Jumping to '$on_fail'");
                    $i = --$on_fail;
                    $jump = 1;
                }
            }],

            [eof => sub {

                $CONNECTED = 0;
                ctrl("CLOSE:Connection ended by remote peer!! " . $EXP->set_accum());
                $EXP->hard_close();
            }],

            [($avoid_first_expectation) ? '' : $pattern, sub {
                if ($on_match == -1) {
                    $jump = 0;
                } elsif ($on_match == -2) {
                    $end = 1;
                    $CONNECTED = 1;
                    ctrl("EXPECT:ON_MATCH:found pattern '$pattern'. Stopping by config");
                } else {
                    ctrl("EXPECT:ON_MATCH:found pattern '$pattern'. Jumping to '$on_match'");
                    $avoid_first_expectation = 1;
                    $i = --$on_match;
                }
            }]
        );
        last if $end;
        next if $jump;
        $avoid_first_expectation = 0;
        if (!$CONNECTED) {
            last;
        }

        $command = subst($command);

        # ... and launch command
        if ($hide) {
            ctrl("EXPECT:SENDING:<<HIDDEN STRING>>");
            $EXP->log_stdout(0);
        } else {
            my $cmd_str = $command;
            $cmd_str =~ s/\n$//go;
            ctrl("EXPECT:SENDING:$cmd_str" . ($return ? '\n' : ''));
        }

        send_slow($EXP, $command . ($return ? (($METHOD =~ /^.*3270.*$/) || ($METHOD eq 'IBM 3270/5250') ? "\r\f" : "\n") : ''));
        if ($hide) {
            $EXP->log_stdout(1);
        }
    }

    $EXP->restart_timeout_upon_receive(0);

    if ($CONNECTED) {
        ctrl("TITLE:$TITLE");
        ctrl("CONNECTED");
    } else {
        $CONNECTED = 0;
        ctrl("DISCONNECTED:" . ($EXP->error));
    }

}
elsif ($METHOD eq 'PACShell') {
    $CONNECTED = 1;
    ctrl("CONNECTED");
}
elsif (!$MANUAL) {
    my $end = 0;

    # Expect password/confirmation prompt...
    my ($matched_pattern_position, $error, $successfully_matching_string, $before_match, $after_match) = $EXP->expect($TIMEOUT_CONNECT,

        [timeout => sub {
            $CONNECTED = 0;
            ctrl("CLOSE:TIMEOUT:$TIMEOUT_CONNECT seconds trying to connect or get prompt!!");
            _hardClose($UUID);
            $EXP->hard_close();
        }],

        [eof => sub {
            $CONNECTED = 0;
            ctrl("CLOSE:Connection ended by remote peer!! " . $EXP->set_accum());
            _hardClose($UUID);
            $EXP->hard_close();
        }],

        # Found sudo prompt
        ["\Q$SUDO_PROMPT\E", sub {
            if ($CONNECTED) {
                $EXP->exp_continue;
            }
            # First 'sudo' password attempt...
            if (! $SUDO_PASSWORD_COUNT) {
                $SUDO_PASSWORD_COUNT++;
                if ((defined $SUDO_PASSWORD) && ($SUDO_PASSWORD eq '<<ASK_PASS>>')) {
                    ctrl("PASSWORD:Asking user '$ENV{'USER'}' for 'sudo' password...");
                    $SUDO_PASSWORD = wEnterValue(undef, '<b>Password requested</b>', "Enter 'sudo' password for '$ENV{'USER'}'", '', 0);
                }
                if (defined $SUDO_PASSWORD) {
                    $EXP->log_stdout(0);
                    $SUDO_PASSWORD = subst($SUDO_PASSWORD);
                    send_slow($EXP, "$SUDO_PASSWORD\n", 'hide');
                    $EXP->log_stdout(1);
                    ctrl("PASSWORD:'sudo' password sent (not shown)");
                    exp_continue;
                } else {
                    ctrl("CLOSE:PASSWORD:Password for 'sudo' input cancelled by user");
                    $EXP->hard_close;
                }
            }
            # ... second 'sudo' password attempt: provided password is no longer valid!!
            else {
                ctrl("CLOSE:PASSWORD:Provided 'sudo' password was rejected");
                _hardClose($UUID);
                $EXP->hard_close;
            }
        }],

        # Found Host-Key verification string
        [$HOSTCHANGE_PROMPT, sub {
            my $match = $EXP->match;
            $match =~ /$HOSTCHANGE_PROMPT/g;
            my ($yes, $no) = ($1, $2);
            if ($ACCEPT_KEY) {
                send_slow($EXP, "$yes\n");
                ctrl("HOSTKEY:accepted by configuration (sent '$yes')");
                exp_continue;
            } else {
                my $val = wEnterValue(undef, $match, "Answer yes/no") // return undef;
                send_slow($EXP, "$val\n");
                ctrl("HOSTKEY:user value sent '$val')");
                exp_continue;
            }
        }],

        # Found "Press any key to continue" string
        [$ANYKEY_PROMPT, sub {
            send_slow($EXP, "\n");
            ctrl("PRESSKEY:sending 'return' to continue connecting");
            exp_continue;
        }],

        # Found WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED
        [$REMOTEHOST_PROMPT, sub {
            my $match = $EXP->match;
            chomp $match;
            $match =~ /$REMOTEHOST_PROMPT/go;
            my ($known_hosts, $line) = ($1, $2);
            $CONNECTED = 0;
            if ($ACCEPT_KEY) {
                ctrl("HOSTKEY:deleting offending hostkey by configuration and Restarting connection");
                open(F, `echo $known_hosts 2>&1`) or die "ERROR: could not open for reading '$known_hosts' ($!)";
                my @data = <F>;
                close F;
                splice(@data, $line - 1, 1);
                open(F, '>' . `echo $known_hosts 2>&1`) or die "ERROR: could not open for writing '$known_hosts' ($!)";
                print F @data;
                close F;
                ctrl('RESTART');
            } else {
                ctrl("CLOSE:HOSTKEY:ignoring offending hostkey by configuration");
            }
        }],

        # Found login string
        [$USERNAME_PROMPT, sub {

            if (($USER eq '') && ($USER_COUNT)) {
                ctrl("CLOSE:LOGIN:Empty username is not valid to login here");
                $EXP->hard_close();
            }
            $USER_COUNT++;
            $USER = subst($USER);
            ctrl("LOGIN:$USER");
            send_slow($EXP, "$USER\n");
            exp_continue;
        }],

        # Found password/passphrase string
        [$PASSWORD_PROMPT, sub {

        # A few attempts...
            if ($PASSWORD_COUNT < 6) {
                $PASSWORD_COUNT++;
                $WAS_ASK_PASS = 0;
                if ((defined $PASS && $PASS eq '<<ASK_PASS>>') || ((!defined $PASS || $PASS eq '') && $AUTH eq 'publickey')) {
                    my @before = split /\n/, $EXP->before;
                    # Extract the last line and use it is as prompt for the request password dialog
                    my $real_prompt = (scalar @before > 0 ? $before[-1] : '') . $EXP->match;

                    $WAS_ASK_PASS = 1;
                    ctrl("PASSWORD:Asking user for password...");
                    $PASS = wEnterValue(undef, '<b>Manual Password requested</b>', $real_prompt, '', 0);
                }
                if (defined $PASS) {
                    $EXP->log_stdout(0);
                    $PASS = subst($PASS);
                    send_slow($EXP, "$PASS\n", 'hide');
                    $EXP->log_stdout(1);
                    ctrl("PASSWORD:Sent (not shown)");
                    # Next time we will ask the password since the automatic one did not work
                    $PASS = '<<ASK_PASS>>';
                    exp_continue;
                } else {
                    ctrl("CLOSE:PASSWORD:Password input cancelled by user");
                    $EXP->hard_close;
                }
            }
            # ... too many password attempts: provided password is no longer valid!!
            else {
                ctrl("CLOSE:PASSWORD:Provided username/password '$USER/<<hidden_password>>' was rejected");
                _hardClose($UUID);
                $EXP->hard_close;
            }
        }],

        # Found user prompt
        [$COMMAND_PROMPT, sub {
            $CONNECTED = 1;
            my $avoid_first_expectation = 1;

            # Do we have to do complex expects?
            if (!$EXPECT || !$CONNECTED) {
                return 1;
            }

            $EXP->restart_timeout_upon_receive(1);
            for(my $i = 0; $i < scalar(@{$$CFG{'environments'}{$UUID}{'expect'}}); $i++) {
                my $hash = $$CFG{'environments'}{$UUID}{'expect'}[$i];
                my $pattern = $$hash{'expect'} // '';
                my $command = $$hash{'send'} // '';
                my $hide = $$hash{'hidden'} // 0;
                my $active = $$hash{'active'} // 0;
                my $return = $$hash{'return'} // 1;
                my $on_match = $$hash{'on_match'} // -1;
                my $on_fail = $$hash{'on_fail'} // -1;
                my $time_out = $$hash{'time_out'} // -1;

                if (!$active) {
                    next;
                }

                my $jump = 0;

                if ($time_out >= 0) {
                    $TIMEOUT_CMD = $time_out;
                }
                $pattern = subst($pattern);

                ctrl("EXPECT:WAITING:$pattern");

                # Wait for pattern prompt before continue...
                $EXP->expect($TIMEOUT_CMD,

                    [timeout => sub {
                        if ($on_fail == -1) {
                            ctrl("CLOSE:TIMEOUT:$TIMEOUT_CMD seconds expecting pattern '$pattern'!!");
                            $CONNECTED = 0;
                            $EXP->hard_close;
                        } elsif ($on_fail == -2) {
                            ctrl("EXPECT:ON_FAIL:timeout expecting '$pattern'. Finishing Expect");
                            $CONNECTED = 1;
                            $end = 1;
                        } else {
                            ctrl("EXPECT:ON_FAIL:timeout expecting '$pattern'. Jumping to '$on_fail'");
                            $i = --$on_fail;
                            $jump = 1;
                        }
                    }],

                    [eof => sub {
                        $CONNECTED = 0;
                        ctrl("CLOSE:Connection ended by remote peer!! " . $EXP->set_accum());
                        _hardClose($UUID);
                        $EXP->hard_close();
                    }],

                    # Found Host-Key verification string
                    [$HOSTCHANGE_PROMPT, sub {

                        my $match = $EXP->match;
                        $match =~ /$HOSTCHANGE_PROMPT/g;
                        my ($yes, $no) = ($1, $2);
                        if ($ACCEPT_KEY) {
                            send_slow($EXP, "$yes\n");
                            ctrl("EXPECT:HOSTKEY:accepted by configuration (sent '$yes')");
                            exp_continue;
                        } else {
                            send_slow($EXP, "$no\n");
                            $CONNECTED = 0;
                            $EXP->hard_close();
                            ctrl("CLOSE:EXPECT:HOSTKEY:rejected by configuration (sent '$no')");
                        }
                    }],

                    [($avoid_first_expectation) ? '' : $pattern, sub {
                        if ($on_match == -1) {
                            $jump = 0;
                        } elsif ($on_match == -2) {
                            $end = 1;
                            $CONNECTED = 1;
                            ctrl("EXPECT:ON_MATCH:found pattern '$pattern'. Stopping by config");
                        } else {
                            ctrl("EXPECT:ON_MATCH:found pattern '$pattern'. Jumping to '$on_match'");
                            $avoid_first_expectation = 1;
                            $i = --$on_match;
                        }
                    }]
                );
                if ($end) {
                    last;
                }
                if ($jump) {
                    next;
                }
                $avoid_first_expectation = 0;
                if (!$CONNECTED) {
                    last;
                }

                $command = subst($command);

                # Disconnect if an error occured during command substitution (eg: when user cancelled data entry)
                if (not defined($command)) {
                    $CONNECTED = 0;
                    last;
                }

                # otherwise launch command
                if ($hide) {
                    ctrl("EXPECT:SENDING:<<HIDDEN STRING>>" . ($return ? '\n' : ''));
                    $EXP->log_stdout(0);
                } else {
                    my $cmd_str = $command;
                    $cmd_str =~ s/\n$//go;
                    ctrl("EXPECT:SENDING:$cmd_str" . ($return ? '\n' : ''));
                }

                send_slow($EXP, $command);
                if ($return) {
                    send_slow($EXP, "\n");
                }

                if ($hide) {
                    $EXP->log_stdout(1);
                }
            }

            $EXP->restart_timeout_upon_receive(0);

        }],

    ); # EXPECT CLOSE

    if ($end || ! $error) {
        if ($CONNECTED) {
            ctrl("TITLE:$TITLE");
            ctrl("CONNECTED");
        }
    } else {
        $CONNECTED = 0;
        ctrl("DISCONNECTED:$error");
    }
} else {    # TODO this should be an elsif
    if (! defined $EXP->error) {
        $CONNECTED = 0;
        ctrl("DISCONNECTING:" . ($EXP->error));
    } else {
        $CONNECTED = 1;
        ctrl("CONNECTING:Manual authentication requested");
        ctrl("TITLE:$TITLE");
        ctrl("CONNECTED");
    }
}

$SIG{'WINCH'} = sub {
    if (!$CONNECTED) {
        return 1;
    }
    while (! $EXP->slave) {
        select(undef, undef, undef, 0.25);
    };
    $EXP->slave->clone_winsize_from(\*STDIN);
    kill WINCH => $EXP->pid if $EXP->pid;
};

$SIG{'INT'} = undef;
$SIG{'HUP'} = sub {
    # Avoid more interruptions
    local $SIG{'WINCH'} = undef;

    my $chain_uuid = '';
    my $CHAIN_CFG;

    _hardClose($UUID);

    if ($INT) {
        return 1;
    }
    $INT = 1;

    # First, read the file with the configuration to use
    my $rin = '';
    vec($rin, fileno($SOCKET), 1) = 1;
    select($rin, undef, undef, 2) or return 1;
    sysread($SOCKET, $chain_uuid, 1024);
    $chain_uuid =~ s/^!!_PAC_CHAIN_\[(.+)\]!!$/$1/g;
    if (! $chain_uuid) {
        $INT = 0;
        return 1;
    }

    # Second, retrieve the 'serialized' configuration to be used
    $rin = '';
    vec($rin, fileno($SOCKET), 1) = 1;
    select($rin, undef, undef, 2) or return 1;
    eval {$CHAIN_CFG = fd_retrieve($SOCKET);};
    if ($@) {
        $INT = 0;
        return 1;
    }

    # Prepare some progressbar data
    my $chain_name = $$CHAIN_CFG{'environments'}{$chain_uuid}{'name'};
    my $exp_partial = 0;
    my $exp_total = 0;
    foreach my $exp (@{$$CHAIN_CFG{'environments'}{$chain_uuid}{'expect'}}) {
        if ($$exp{'active'} // 0) {
            ++$exp_total;
        }
    }
    if (! $exp_total) {
        $INT = 0;
        return 1;
    }
    ctrl("CHAIN:$chain_name:$chain_uuid:$exp_partial:$exp_total");

    my $TIMEOUT_CMD = $$CHAIN_CFG{'defaults'}{'timeout command'} || undef;
    if ($$CHAIN_CFG{'environments'}{$chain_uuid}{'terminal options'}{'use personal settings'}) {
        $TIMEOUT_CMD = $$CHAIN_CFG{'environments'}{$chain_uuid}{'terminal options'}{'timeout command'} || undef;
    }

    my $end = 0;
    my $avoid_first_expectation = 1;
    $EXP->restart_timeout_upon_receive(1);
    for(my $i = 0; $i < scalar(@{$$CHAIN_CFG{'environments'}{$chain_uuid}{'expect'}}); $i++) {
        my $hash = $$CHAIN_CFG{'environments'}{$chain_uuid}{'expect'}[$i];
        my $pattern = $$hash{'expect'} // '';
        my $command = $$hash{'send'} // '';
        my $hide = $$hash{'hidden'} // 0;
        my $active = $$hash{'active'} // 0;
        my $return = $$hash{'return'} // 1;
        my $on_match = $$hash{'on_match'} // -1;
        my $on_fail = $$hash{'on_fail'} // -1;
        my $time_out = $$hash{'time_out'} // -1;

        if (!$active) {
            next;
        }
        my $jump = 0;

        if ($time_out >= 0) {
            $TIMEOUT_CMD = $time_out;
        }

        $pattern = subst($pattern);

        ctrl("CHAIN:$chain_name:WAITING($pattern):" . ($exp_partial++) . ":$exp_total");

        # Wait for pattern prompt before continue...
        $EXP->expect($TIMEOUT_CMD,

            [timeout => sub {
                if ($on_fail == -1) {
                    ctrl("CLOSE:TIMEOUT:$TIMEOUT_CMD seconds expecting pattern '$pattern'!!");
                    $CONNECTED = 0;
                    $EXP->hard_close;
                } elsif ($on_fail == -2) {
                    ctrl("CHAIN:EXPECT:ON_FAIL:timeout expecting '$pattern'. Finishing Expect");
                    $CONNECTED = 1;
                    $end = 1;
                } else {
                    ctrl("CHAIN:EXPECT:ON_FAIL:timeout expecting '$pattern'. Jumping to '$on_fail'");
                    $i = --$on_fail;
                    $jump = 1;
                }
            }],

            [eof => sub {

                $CONNECTED = 0;
                ctrl("CLOSE:Connection ended by remote peer!! " . $EXP->set_accum());
                $EXP->hard_close();
            }],

            # Found Host-Key verification string
            [$HOSTCHANGE_PROMPT, sub {

                my $match = $EXP->match;
                $match =~ /$HOSTCHANGE_PROMPT/go;
                my ($yes, $no) = ($1, $2);
                if ($ACCEPT_KEY) {
                    send_slow($EXP, $yes . (($METHOD =~ /^.*3270.*$/) || ($METHOD eq 'IBM 3270/5250')) ? "\r\f" : "\n");
                    ctrl("EXPECT:HOSTKEY:accepted by configuration (sent '$yes')");
                    exp_continue;
                } else {
                    send_slow($EXP, $no . (($METHOD =~ /^.*3270.*$/) || ($METHOD eq 'IBM 3270/5250')) ? "\r\f" : "\n");
                    $CONNECTED = 0;
                    $EXP->hard_close();
                    ctrl("CLOSE:EXPECT:HOSTKEY:rejected by configuration (sent '$no')");
                }
            }],

            [($avoid_first_expectation) ? '' : $pattern, sub {
                if ($on_match == -1) {
                    $jump = 0;
                } elsif ($on_match == -2) {
                    $end = 1;
                    $CONNECTED = 1;
                    ctrl("CHAIN:EXPECT:ON_MATCH:found pattern '$pattern'. Stopping by config");
                } else {
                    ctrl("CHAIN:EXPECT:ON_MATCH:found pattern '$pattern'. Jumping to '$on_match'");
                    $avoid_first_expectation = 1;
                    $i = --$on_match;
                }
            }]
        ); # EXPECT CLOSE
        if ($end) {
            last;
        }
        if ($jump) {
            next;
        }
        $avoid_first_expectation = 0;
        if (!$CONNECTED) {
            last;
        }

        $command = subst($command);

        # ... and launch command
        if ($hide) {
            ctrl("CHAIN:$chain_name:SENDING(<<HIDDEN STRING>>):$exp_partial:$exp_total");
            $EXP->log_stdout(0);
        } else {
            my $cmd_str = $command;
            $cmd_str =~ s/\n$//go;
            ctrl("CHAIN:$chain_name:SENDING:$cmd_str" . ($return ? '\n' : '') . ":$exp_partial:$exp_total");
        }

        send_slow($EXP, $command . ($return ? (($METHOD =~ /^.*3270.*$/) || ($METHOD eq 'IBM 3270/5250') ? "\r\f" : "\n") : ''));
    }

    $EXP->log_stdout(0);
    $EXP->restart_timeout_upon_receive(0);

    if ($$CHAIN_CFG{'tmp'}{'set title'}) {
        ctrl("TITLE:$$CHAIN_CFG{'tmp'}{'title'}");
    }
    ctrl("CONNECTED");
    $INT = 0;
    return 1;
};

$SIG{'USR1'} = sub {
    # Avoid more interruptions
    local $SIG{'WINCH'} = undef;

    return 1 if $INT;
    $INT = 1;

    # Now, read the command to execute
    my $rin = '';
    vec($rin, fileno($SOCKET), 1) = 1;
    select($rin, undef, undef, 2) or return 1;
    my $tmp;
    eval {
        $tmp = fd_retrieve($SOCKET);
    };
    if ($@) {
        return 1;
    }
    if (! defined $$tmp{cmd}) {
        $INT = 0;
        return 1;
    }

    _execAndCapture($tmp);

    ctrl($CONNECTED ? "CONNECTED" : "DISCONNECTED");
    $INT = 0;
    return 1;
};

$SIG{'USR2'} = sub {
    # Avoid more interruptions
    local $SIG{'WINCH'} = undef;

    return 1 if $INT;
    $INT = 1;

    my $rin = '';
    vec($rin, fileno($SOCKET), 1) = 1;
    select($rin, undef, undef, 2) or return 1;
    my $tmp;
    $tmp = fd_retrieve($SOCKET) or return 0;
    if (! defined $tmp) {
        $INT = 0;
        return 0;
    }

    _execScript($tmp);
    ctrl($CONNECTED ? "CONNECTED" : "DISCONNECTED");

    $INT = 0;
    return 1;
};

if ($CONNECTED) {
    # Restart only when connecting at startup connection
    if ($IS_CLUSTER && ($RESTART == 2)) {
        $RESTART = 0;
    }
    $EXP->interact(\*STDIN, "__PAC__STOP__${UUID}__${$}__");
}

_hardClose($UUID);

my $why = $?;

if (($why ne 0) && $RESTART) {
    ctrl('RESTART');
}

# Finish this expect session
if (defined $EXP->pid) {
    kill(15, $EXP->pid);
}
$EXP->hard_close;

close_log_file();

ctrl("DISCONNECTED");

exit 0;

sub close_log_file {
    if (!$LOG_FILE || !$REMOVE_CTRL_CHARS) {
        return 1;
    }
    if (!open(F,"<:utf8",$LOG_FILE)) {
        ctrl("ERROR: Could not open file '$LOG_FILE' for reading!! ($!)");
        return 1;
    }
    my @lines = <F>;
    close F;
    if (! open(F,">:utf8","$LOG_FILE.$$")) {
        ctrl("ERROR: Could not open file '$LOG_FILE.$$' for writting!! ($!)");
        return 1;
    }
    print F _removeEscapeSeqs(join('', @lines));
    close F;
    rename("$LOG_FILE.$$", $LOG_FILE) or ctrl("ERROR: $!");
}

END {
    if (!$LOG_FILE || !$REMOVE_CTRL_CHARS) {
        return 1;
    }

    ctrl("LOGFILE:Removing CONTROL characters");

    if (!open(F,"<:utf8",$LOG_FILE)) {
        ctrl("ERROR: Could not open file '$LOG_FILE' for reading!! ($!)");
        return 1;
    }
    my @lines = <F>;
    close F;

    if (! open(F,">:utf8","$LOG_FILE.$$")) {
        ctrl("ERROR: Could not open file '$LOG_FILE.$$' for writting!! ($!)");
        return 1;
    }
    print F _removeEscapeSeqs(join('', @lines));
    close F;

    rename("$LOG_FILE.$$", $LOG_FILE) or ctrl("ERROR: $!");
}

# END : Main program
########################################################

__END__

=encoding utf8

=head1 NAME

pac_conn

=head1 SYNOPSIS

Application to open a connection of type : ssh, vnc, sftp, telnet, ftp, rdp, etc.

=head1 DESCRIPTION

Global Variables

    %COLOR          Colors to use in terminal messages
                    'norm'    black
                    'log'     yellow
                    'recv'    magenta
                    'sent'    green
                    'err'     red
    $EXP            Expect object
    $GSETTINGS      Gnome settings
    $CFG_FILE       Configuration file.freeze passed as shell argument
    $UUID           session UUID as shell argument
    $GETCMD         command to execute  as shell argument
    $DEBUG          0 No , 1 Yes : Set with option "Expect : DEBUG" in Terminal options Advanced
    $CFG            Configuration object "$CFG = retrieve($CFG_FILE)"
    $SEND_SLOW      Time in milliseconds to wait between


=head2 sub __disconnect

Handle disconnection

=head2 sub msg

prints a message to the terminal screen using I<log color>

=head2 sub ctrl

Sends a internal message to the terminal associated with this connection. Using a UNIX socket

=head2 sub auth (socket)

Check that socket is a PAC socket and not other
    return true 1, false 0

=head2 sub subst (line)

B<line> string

    takes "line" and substitutes tags for their values
        password
        username
        uuid
        etc

=head2 sub wEnterValue(undef,title,label,default,visible)

Display a modal input dialog box and wait for answer

B<title> Title to your request

B<label> The question to answer

B<default> if defined, will search for an array with that name and create a Listbox to choose from predefined values, otherwise will draw a textbox for input

B<visible> if the text typed by the user should be visible or hidden (passwords should be hidden)

=head2 sub send_slow(expect object,message)

B<expect object> has attached a spawned connection
B<message> message to send to the spawned connection

    if ($SEND_SLOW) {
        expect->send_slow(message)
    } else {
        expect->send(message)
    }

=head2 sub _getPrompt

Sends a "\n" to the current session and waits for the answer

runs expect expressions over the answer

=head2 sub _execAndCapture

Execute a command in the session and wait for answer

Pipe the command to other terminals if $pipe is active (possibly used in clusters)

=head2 sub _execScript

Parse a user script, run the script.

=head2 sub findKP

Find Keepass user, password

=head2 main

Steps

    Create a connection string depending on:
        Proxy
        Protocol (telnet, ssh, rdp, etc)
        Terminal session options
        Credentials
        tunnels
        ...
    Spawn the session using the Expect object : $EXP
        Inform the terminal that the spawned object has been created
    Depending on the protocol follow user authentication
        Supply user validation data as expect finds the login patterns
    Create signal handlers
    If connected
        Send interaction to Terminal


=head1 Perl particulars

    select(undef, undef, undef, 0.5)        ===>  sleep(500ms);

