#!/usr/bin/perl

###############################################################################
# This file is part of Ásbrú Connection Manager
#
# Copyright (C) 2017-2019 Á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;
binmode STDOUT,':utf8';
binmode STDERR,':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';

# 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 %PROXY;
my $PROXYPID;
my $SOCKET;
my $SOCKET_EXEC;
my $PROXYFIED = 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);
};

# Connect to Gnome's GSettings
my $GSETTINGS = Glib::IO::Settings -> new('org.gnome.system.proxy.http');

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

########################################################
# START : Signal handling
sub __disconnect {
    my $signal = shift;
    ctrl("DISCONNECTING");
    if (defined $PROXYPID) {
        kill(15, $PROXYPID);
    }
    $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;

# Get PROXY from environment
$PROXY{'env'}{'ip'} = $GSETTINGS -> get_string('host');
$PROXY{'env'}{'port'} = $GSETTINGS -> get_int('port');
$PROXY{'env'}{'user'} = $GSETTINGS -> get_string('authentication-user');
$PROXY{'env'}{'pass'} = $GSETTINGS -> get_string('authentication-password');

# 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_PROXY = $$CFG{'defaults'}{'use proxy'};
my $USE_SYSTEM_PROXY = $$CFG{'defaults'}{'use system proxy'};
my $FORCE_USE_PROXY = $$CFG{'environments'}{$UUID}{'use proxy'} || '0';
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'} // []};
$PROXY{'cfg'}{'ip'} = $$CFG{'defaults'}{'proxy ip'};
$PROXY{'cfg'}{'port'} = $$CFG{'defaults'}{'proxy port'};
$PROXY{'cfg'}{'user'} = $$CFG{'defaults'}{'proxy user'};
$PROXY{'cfg'}{'pass'} = $$CFG{'defaults'}{'proxy pass'};

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);
$PROXY{'cfg'}{'ip'} = subst($PROXY{'cfg'}{'ip'});
$PROXY{'cfg'}{'port'} = subst($PROXY{'cfg'}{'port'});
$PROXY{'cfg'}{'user'} = subst($PROXY{'cfg'}{'user'});
$PROXY{'cfg'}{'pass'} = subst($PROXY{'cfg'}{'pass'});

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

my $proxy_ip = $PROXY{($USE_SYSTEM_PROXY) ? 'env' : 'cfg'}{'ip'};
my $proxy_port = $PROXY{($USE_SYSTEM_PROXY) ? 'env' : 'cfg'}{'port'};
my $proxy_user = $PROXY{($USE_SYSTEM_PROXY) ? 'env' : 'cfg'}{'user'} // '';
my $proxy_pass = $PROXY{($USE_SYSTEM_PROXY) ? 'env' : 'cfg'}{'pass'} // '';
if ($$CFG{'environments'}{$UUID}{'use proxy'} == 1) {
    $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'};
}

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_host\>\>/$proxy_ip/g;
    $string =~ s/\<\<proxy_port\>\>/$proxy_port/g;
    $string =~ s/\<\<proxy_user\>\>/$proxy_user/g;
    $string =~ s/\<\<proxy_pass\>\>/$proxy_pass/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);
}

# 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
########################################################

# User wants to use a proxy, check that Net::Proxy is available
if ((($USE_PROXY && $FORCE_USE_PROXY < 2 || ($FORCE_USE_PROXY == 1))) && $proxy_ip && $proxy_port) {

    # https://stackoverflow.com/questions/32608504/how-to-check-if-perl-module-is-available/32608860#32608860
    my $rc =  eval{require Net::Proxy};
    if (! $rc) {
        $rc = do {
            no strict;
            *stash = *{"Net::Proxy::"};
            scalar keys %stash;
        }
    }

    if (! $rc) {
        msg("ERROR: To use proxied connection, Ásbrú requires perl module Net::Proxy (see https://metacpan.org/pod/Net::Proxy).\n");
        msg("Please install this module on your system or disable proxy settings for this connection.\n");
        exit 1;
    }
}

# Prepare the threaded proxy tunnel
if ((($USE_PROXY && $FORCE_USE_PROXY < 2 || ($FORCE_USE_PROXY == 1))) && $proxy_ip && $proxy_port) {
    require IO::Socket;
    require IO::Handle; # Just for autoflush... :(

    socketpair(CHILD, PARENT, AF_UNIX, SOCK_STREAM, PF_UNSPEC) or  die "socketpair: $!";
    CHILD -> autoflush(1);
    PARENT -> autoflush(1);

    my $LOCAL_PORT = 0;

    if ($PROXYPID = fork) {
        # Parent
        close PARENT;

        # Wait until read $LOCAL_PORT
        ctrl("PROXY_THREAD:Waiting for the thread to start the proxy tunnel...");
        chomp($LOCAL_PORT = <CHILD>);
        ctrl("PROXY_THREAD:Built tunnel to $IP:$PORT through proxy $proxy_ip:$proxy_port ($proxy_user/$proxy_pass) using local port $LOCAL_PORT...");

        select(undef, undef, undef, 0.5);

        # If every thing went ok, modify IP and PORT to map the tunneled connection
        $IP = 'localhost';
        $PORT = $LOCAL_PORT;

    } elsif (defined $PROXYPID) {
        # Children
        close CHILD;

        local $SIG{'INT'} = sub {exit;};
        local $SIG{'TERM'} = $SIG{'INT'};
        local $SIG{'QUIT'} = $SIG{'INT'};
        local $SIG{'HUP'} = $SIG{'INT'};
        local $SIG{'USR1'} = $SIG{'INT'};
        local $SIG{'USR2'} = $SIG{'INT'};

        require Net::Proxy;
        my $proxy;

        do {
            $proxy = Net::Proxy -> new({
                in => {
                    type => 'tcp',
                    port => $LOCAL_PORT = 60000 + int(rand(1000)),
                },
                out => {
                    type => 'tcp', # 'tcp' method allows using SOCKs proxies!! :)
                    host => $IP,
                    port => $PORT,
                    proxy_host => $proxy_ip,
                    proxy_port => $proxy_port,
                    proxy_user => $proxy_user,
                    proxy_pass => $proxy_pass,
                    proxy_agent => "Ásbrú Connection Manager (https://www.asbru-cm.net/)"
                }
            });
        } until $proxy;

        $proxy -> set_verbosity(0);
        $proxy -> register;

        # Send the parent process the new created local port
        print PARENT "$LOCAL_PORT\n";

        Net::Proxy -> mainloop(1);

        exit 0;
    } else {
        $CONNECTED = 0;
        ctrl("ERROR:Could not 'fork' to create proxy object ($!)");
        ctrl("CLOSE");
        exit 1;
    }
}

# 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')) {
        if ($METHOD ne 'autossh') {
            $METHOD = 'ssh';
        }
        my $key = '';
        if ($AUTH eq 'publickey') {
            $key = "-i \"$PUBKEY\"" . ($AUTHFALLBACK ? '' : ' -o "PreferredAuthentications=publickey"');
            $USER = $PASSPHRASE_USER;
            $PASS = $PASSPHRASE;
        } elsif (! $AUTHFALLBACK) {
            $key = '-o "PreferredAuthentications=password,keyboard-interactive"';
        }
        $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 = "-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;
        print "*$connection_cmd*\n";
    } elsif (($METHOD =~ /^.*sftp.*$/) || ($METHOD eq 'SFTP')) {
        $METHOD = 'sftp';
        my $key = '';
        if ($AUTH eq 'publickey') {
            $key = "-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;
        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) // '';
        }

        $CONNECT_OPTS =~ s/\s*-embed//go;
        if (`vncviewer --help 2>&1 | /bin/grep 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";
            }
        } 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;
    if ($PROXYPID) {
        kill(9, $PROXYPID);
    }
    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());
            $EXP -> hard_close;
        }],

        # 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("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");
                $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("CLOSE:PASSWORD:Password input cancelled by user");
                    $CONNECTED = 0;
                    $EXP -> hard_close;
                }
            }
            # ... second password attempt: provided password is no longer valid!!
            else {
                ctrl("CLOSE:PASSWORD:Provided username/password '$USER/<<hidden_password>>' was rejected");
                $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 {
            $CONNECTED = 0;
            ctrl("CLOSE:TIMEOUT:$TIMEOUT_CONNECT seconds trying to connect or get prompt!!");
            $EXP -> hard_close;
        }],

        [eof => sub {

            $CONNECTED = 0;
            ctrl("CLOSE:Connection ended by remote peer!! " . $EXP -> set_accum());
            $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");
                $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");
                $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!!");
            if ($RESTART) {
                ctrl('RESTART');
            }
            $EXP -> hard_close();
        }],

        [eof => sub {

            $CONNECTED = 0;
            ctrl("CLOSE:Connection ended by remote peer!! " . $EXP -> set_accum());
            if ($RESTART) {
                ctrl('RESTART');
            }
            $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");
                $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");
                $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());
                        $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;

    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 on connection start, not at the end of the session
    if ($IS_CLUSTER && ($RESTART == 2)) {
        $RESTART = 0;
    }
    $EXP -> interact(\*STDIN, "__PAC__STOP__${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;

if (defined $PROXYPID && $PROXYPID) {
    kill(15, $PROXYPID);
}

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);

