#!/usr/opkg/bin/perl -w
##
## OSSP ac -- Auto Connection
## Copyright (c) 2003-2004 The OSSP Project
## Copyright (c) 2003-2004 Cable & Wireless
## Copyright (c) 2003-2004 Ralf S. Engelschall
##
## This file is part of OSSP ac, a tool for automated login
## sessions which can be found at http://www.ossp.org/pkg/tool/ac/.
##
## This program 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
## 2.0 of the License, or (at your option) any later version.
##
## This program 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
## along with this file; if not, write to the Free Software
## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
## USA, or contact The OSSP Project
##
## ac: program implementation (syntax: Perl 5.x)
##
require 5.008;
use strict;
$|++;
# load a plethora of Perl modules
use IO::File; # from OpenPKG "perl"
use POSIX; # from OpenPKG "perl"
use Getopt::Long; # from OpenPKG "perl"
use Data::Dumper; # from OpenPKG "perl"
use Parse::RecDescent; # from OpenPKG "perl-parse"
#use Term::ReadLine; # from OpenPKG "perl-term" (deferred loaded!)
#use Term::ReadLine::Gnu; # from OpenPKG "perl-term" (deferred loaded!)
#use Curses; # from OpenPKG "perl-curses" (deferred loaded!)
#use Curses::UI; # from OpenPKG "perl-curses" (deferred loaded!)
#use Expect; # from OpenPKG "perl-sys" (deferred loaded!)
# configure optional debugging
$Data::Dumper::Purity = 1;
$Data::Dumper::Indent = 1;
$Data::Dumper::Terse = 1;
# fixed program information
my $my = {
prog_name => "ac",
prog_vers => "2.0.0",
prog_date => "11-Apr-2003",
prog_global => "/u/rse/ac/src/config",
prog_homedir => "$ENV{HOME}/.ac",
prog_config => "$ENV{HOME}/.ac/config",
prog_complete => "$ENV{HOME}/.ac/.complete",
prog_history => "$ENV{HOME}/.ac/.history",
prog_grammar => "$ENV{HOME}/.ac/.grammar",
prog_tmpfile => "$ENV{HOME}/.ac/.tmp.$$",
user_name => ($ENV{USER} || $ENV{LOGNAME} || "unknown"),
};
## _________________________________________________________________________
##
## Command Line Option Parsing
## _________________________________________________________________________
##
# default command line options
my $opt = {
version => 0,
verbose => 0,
debug => 0,
update => 0,
config => "",
help => 0,
define => [],
};
# global exception handling support
$SIG{__DIE__} = sub {
my ($err) = @_;
$err =~ s|\s+at\s+.*||s if (not $opt->{verbose});
print STDERR "$my->{prog_name}:ERROR: $err ". ($! ? "($!)" : "") . "\n";
exit(1);
};
# parse command line options
Getopt::Long::Configure("bundling");
my %getopt_spec = (
'V|version' => \$opt->{version},
'v|verbose' => \$opt->{verbose},
'd|debug' => \$opt->{debug},
'h|help' => \$opt->{help},
'u|update' => \$opt->{update},
'D|define=s' => $opt->{define},
'c|config=s' => \$opt->{config},
);
my $result = GetOptions(%getopt_spec)
|| die "command line option parsing failed";
if ($opt->{help}) {
print "usage: $my->{prog_name} [] \n" .
"available options are:\n" .
" -v,--verbose enable verbose run-time mode\n" .
" -u,--update update Bash command completion cache\n" .
" -V,--version print program version\n" .
" -h,--help print out this usage page\n" .
" -D,--define NAME=VAL set (and overwrite) variable definitions\n" .
" -c,--config FILE read this configuration file only\n";
exit(0);
}
if ($opt->{version}) {
print "$my->{prog_name} $my->{prog_vers} ($my->{prog_date})\n";
exit(0);
}
## _________________________________________________________________________
##
## Configuration Parsing
## _________________________________________________________________________
##
# $cf = {
# user => {
# -order => [ rse, ... ],
# rse => { realname => "...", description => ... },
# ...
# },
# group => {
# -order => [ en, ... ],
# en => { description => "...", members => [...] },
# ...
# },
# host => {
# -order => [ en1, ... ],
# en1 => { hostname => "...", ... },
# ...
# },
# domain => {
# -order => [ en, ... ],
# en => { description => "...", members => [...] },
# ...
# },
# grant => [
# [ [ "...", ... ], [ "...", ... ] ],
# ...
# ],
# method => {
# ssh => [ [], [], ... ],
# ...
# }
# };
# prepare in-core configuration representation
use vars qw($cf);
$cf = {
user => { -order => [] },
group => { -order => [] },
host => { -order => [] },
domain => { -order => [] },
grant => [],
method => {}
};
# parsing utility function: read configuration file
sub config_load {
my ($ctx, $filename) = @_;
if ($filename !~ m|^/|s) {
# make absolute out of relative path
$filename = $ctx->{cwd}->[-1]."/".$filename;
$filename = &path_canonicalize($filename);
}
my $io = new IO::File "<$filename"
or die "cannot open configuration file \"$filename\"";
my $config = '';
$config .= $_ while (<$io>);
$io->close;
$config =~ s|^\s*#.*?$||mg;
return $config;
}
# parsing utility function: expand a token
sub expand_token {
my ($ctx, $token) = @_;
$token =~ s/\$\{([a-zA-Z][a-zA-Z0-9_]*)\}/$ctx->{def}->{$1} || $ENV{$1} || ''/sge;
$token =~ s/\%\{([a-zA-Z][a-zA-Z0-9_]*)\}/$ctx->{def}->{$1} || $ctx->{env}->{$1} || ''/sge;
$token =~ s/\@\{([^\}]+)\}/&base64_decode($1)/sge;
$token =~ s/\\t/\t/sg;
$token =~ s/\\r/\r/sg;
$token =~ s/\\n/\n/sg;
$token =~ s/\\c(.)/eval "\\c$1"/sge;
return $token;
}
# the top-down parsing grammar itself
my $grammar = q[
# create local variable for holding parsing context
{ use vars qw($ctx); $ctx = ""; }
# start rule (for explicit context init and EOF handling)
start: { $ctx = $arg[0]; }
config /^\Z/
|
# whole configuration
config: directive(s)
|
# single configuration directive
directive: include
| scope
| set
| user
| group
| host
| domain
| grant
| method
| ""
|
# directive: "set ;"
set: "set" token token ";" {
$ctx->{env}->{$item[2]} = &::expand_token($ctx, $item[3]);
}
|
# directive: "include ;"
include: "include" token ";" {
my $file = $item[2];
my $dir = $file;
$dir =~ s|/[^/]+$||s;
$dir = $ctx->{cwd}->[-1]."/".$dir if ($dir !~ m|^/|s);
$dir = &::path_canonicalize($dir);
#$text = "scope \"$dir\" { " . &::config_load($ctx, $file) . "}; " . $text;
$text = &::config_load($ctx, $file) . $text;
Parse::RecDescent::LineCounter::resync($thisline);
}
|
# directive: "scope { ... };"
scope: "scope" token "{" {
push(@{$ctx->{cwd}}, $item[2]);
}
config {
pop(@{$ctx->{cwd}});
} "}" ";"
|
# directive: "user ;"
user: "user" token token token ";" {
die "line $thisline (column $thiscolumn): " .
"user \"$item[2]\" already defined"
if (defined($ctx->{cf}->{user}->{$item[2]}));
push(@{$ctx->{cf}->{user}->{-order}}, $item[2]);
$ctx->{cf}->{user}->{$item[2]} = {
'realname' => $item[3],
'email' => $item[4]
};
}
|
# directive: "group { [ ...] };"
group: "group" token token "{" token(s?) "}" ";" {
die "line $thisline (column $thiscolumn): " .
"group \"$item[2]\" already defined"
if (defined($ctx->{cf}->{group}->{$item[2]}));
push(@{$ctx->{cf}->{group}->{-order}}, $item[2]);
$ctx->{cf}->{group}->{$item[2]} = {
'description' => $item[3],
'members' => $item[5]
};
}
|
# directive: "host { ... };"
host:
| "host" token "{" host_param[$param](s) "}" ";" {
die "line $thisline (column $thiscolumn): " .
"host \"$item[2]\" already defined"
if (defined($ctx->{cf}->{host}->{$item[2]}));
push(@{$ctx->{cf}->{host}->{-order}}, $item[2]);
$ctx->{cf}->{host}->{$item[2]} = $param;
}
|
host_param: /(hostname|purpose|system|platform|location)/ token ";" {
$arg[0]->{$item[1]} = $item[2];
}
| /(login|domain)/ token(s) ";" {
$arg[0]->{$item[1]} = $item[2];
}
|
# directive: "domain { [ ...] };"
domain: "domain" token token "{" token(s?) "}" ";" {
die "line $thisline (column $thiscolumn): " .
"domain \"$item[2]\" already defined"
if (defined($ctx->{cf}->{domain}->{$item[2]}));
push(@{$ctx->{cf}->{domain}->{-order}}, $item[2]);
$ctx->{cf}->{domain}->{$item[2]} = {
'description' => $item[3],
'members' => $item[5]
};
}
|
# directive: "grant { [ ...] } { [ ...] };"
grant: "grant" "{" token(s) "}" "{" token(s) "}" ";" {
push(@{$ctx->{cf}->{grant}}, [ $item[3], $item[6] ]);
}
|
# directive: "method { [...] };"
method: "method" token "{" script "}" ";" {
die "line $thisline (column $thiscolumn): " .
"method \"$item[2]\" already defined"
if (defined($ctx->{cf}->{method}->{$item[2]}));
$ctx->{cf}->{method}->{$item[2]} = $item[4];
}
|
# login script
script: command ";" script { unshift(@{$item[3]}, $item[1]);
$return = $item[3]; }
| command { $return = $item[1]; }
|
# login script: scripting directives
command: "system" token { $return = [ $item[1], $item[2] ]; }
| "spawn" token { $return = [ $item[1], $item[2] ]; }
| "timeout" token { $return = [ $item[1], $item[2] ]; }
| "send" token { $return = [ $item[1], $item[2] ]; }
| "expect" expect_args { $return = [ $item[1], $item[2] ]; }
| "sleep" token { $return = [ $item[1], $item[2] ]; }
| "repeat" { $return = [ $item[1] ]; }
| "interact" { $return = [ $item[1] ]; }
| if_clause { $return = $item[1]; }
| "" { $return = [ ]; }
|
# login script: expect argument(s): "regex [ script [ regex script ... ]]"
expect_args: expect_arg_std(1..) { $return = $item[1]; }
| expect_arg_abr { $return = [ $item[1] ]; }
|
expect_arg_std: regex "{" script "}" { $return = [ $item[1], $item[3] ]; }
|
expect_arg_abr: regex { $return = [ $item[1], [] ]; }
|
# if clause
if_clause: "if" "(" expr ")" "{" script "}" ("else" "{" script "}")(?) {
$return = [ $item[1], $item[3], $item[6], $item[10] ];
}
|
# boolean expression
# (clean and obvious, but for top-down parsing
# not usable version because of left-recursion)
#expr: "(" expr ")" { $return = $item[2]; }
# | /[!-]/ expr { $return = [ "op:$item[1]", $item[2] ]; }
# | expr /[.*\/%+-]/ expr { $return = [ "op:$item[2]", $item[1], $item[3] ]; }
# | expr /(==|!=|<|<=|>=|>)/ expr { $return = [ "op:$item[2]", $item[1], $item[3] ]; }
# | expr "=~" regex { $return = [ "op:$item[2]", $item[1], $item[3] ]; }
# | token { $return = [ "tok", $item[1] ]; }
# |
# boolean expression
# (ugly and confusing, but usable version,
# resulted through left-recursion elemination)
expr: "(" expr ")" { $return = $item[2]; }
term[$item[4]] { $return = $item[5]; }
| /[!-]/ expr { $return = [ "op:$item[1]", $item[2] ]; }
term[$item[3]] { $return = $item[4]; }
| token { $return = [ "tok", $item[1] ]; }
term[$item[2]] { $return = $item[3]; }
|
term: /[.*\/%+-]/ expr { $return = [ "op:$item[1]", $arg[0], $item[2] ]; }
term[$item[3]] { $return = $item[4]; }
| /(==|!=|<|<=|>=|>)/ expr { $return = [ "op:$item[1]", $arg[0], $item[2] ]; }
term[$item[3]] { $return = $item[4]; }
| "=~" regex { $return = [ "op:$item[1]", $arg[0], $item[2] ]; }
term[$item[3]] { $return = $item[4]; }
| {1} { $return = $arg[0]; }
|
# regular expression token
regex: m{/((\\\\/|[^/])*)/} { $return = &::expand_token($ctx, $1); }
| token { $return = quotemeta($item[1]); }
|
# plain text token
token: m/\"((\\\"|[^\"])*)\"/ { $return = &::expand_token($ctx, $1); }
| m/'((\\\\'|[^'])*)'/ { $return = &::expand_token($ctx, $1); }
| m/[^ \t\r\n;\{\}\(\)]+/ { $return = &::expand_token($ctx, $item[1]); }
|
];
# create configuration parser
if ($opt->{debug}) {
#$::RD_TRACE = 1;
$::RD_HINT = 1;
$::RD_ERRORS = 1;
$::RD_WARN = 1;
}
my $parser;
if (not $opt->{debug}) {
# speed up run-time processing by pre-compiling grammar
if (! -f "$my->{prog_grammar}") {
# perform pre-compilation operation
my $cwd = POSIX::getcwd();
chdir($my->{prog_homedir});
Parse::RecDescent->Precompile($grammar, "ac::grammar");
my $io = new IO::File "; }
$io->close;
unlink("grammar.pm");
chdir($cwd);
# fixup pre-compilation result (at least necessary for P::RD <= 1.93)
$pm =~ s|(sub\s+new\s+\{\s+)my\s+(bless\()|$1$2|s;
$pm =~ s|$|\n1;\n|s;
# store pre-compilation result for reuse
$io = new IO::File ">$my->{prog_grammar}"
or die "unable to write pre-compiled grammar to \"$my->{prog_grammar}\"";
$io->print($pm);
$io->close;
}
# read pre-compiled grammar
require "$my->{prog_grammar}";
import ac::grammar;
$parser = new ac::grammar;
}
else {
# slow approach: parse from scratch
$parser = new Parse::RecDescent ($grammar)
or die "unable to parse configuration parsing grammar";
}
# prepare parsing context
my $ctx = {};
$ctx->{cf} = $cf;
$ctx->{cwd} = [ POSIX::getcwd() ];
$ctx->{env} = {};
$ctx->{def} = {};
foreach my $define (@{$opt->{define}}) {
if ($define =~ m|^([a-zA-Z][a-zA-Z0-9_]*)(?:[=:](.*))?$|) {
my ($name, $value) = ($1, $2);
$value ||= '';
$ctx->{def}->{$name} = $value;
}
}
# on-the-fly generate top-level configuration
my $config = '';
if ($opt->{config} ne '') {
# explicitly supplied configuration file
$config .= "include \"$opt->{config}\";";
}
else {
# implicitly supplied global and local configuration files
if (-f $my->{prog_global}) {
$config .= "include \"$my->{prog_global}\";";
}
if (-f $my->{prog_config}) {
$config .= "include \"$my->{prog_config}\";";
}
}
# recursively parse configuration
defined $parser->start($config, 1, $ctx)
or die "failed to parse configuration directives";
## _________________________________________________________________________
##
## Configuration Post-Processing
## _________________________________________________________________________
##
# expand group references in community group memberships configuration
sub expand_group {
my ($name) = @_;
my $members = {};
foreach my $member (@{$cf->{group}->{$name}->{members}}) {
if ($member =~ m|^\@(.+)$|s) {
die "reference to unknown group \"$1\" in group \"$name\""
if (not defined($cf->{group}->{$1}));
my $M = &expand_group($1);
map { $members->{$_} = 1 } (@{$M});
}
else {
die "reference to unknown user \"$member\" in group \"$name\""
if (not defined($cf->{user}->{$member}));
$members->{$member} = 1;
}
}
return [ keys(%{$members}) ];
}
foreach my $name (keys(%{$cf->{group}})) {
next if ($name eq '-order');
$cf->{group}->{$name}->{members} = &expand_group($name);
}
# extend domain memberships with reverse/per-host domain specification
foreach my $host (keys(%{$cf->{host}})) {
next if ($host eq '-order');
if (defined($cf->{host}->{$host}->{domain})) {
foreach my $domain (@{$cf->{host}->{$host}->{domain}}) {
die "unknown domain \"$domain\""
if (not defined($cf->{domain}->{$domain}));
push(@{$cf->{domain}->{$domain}->{members}}, $host);
}
}
}
# expand domain references in system domain memberships configuration
sub expand_domain {
my ($name) = @_;
my $members = {};
foreach my $member (@{$cf->{domain}->{$name}->{members}}) {
if ($member =~ m|^\@(.+)$|s) {
die "reference to unknown domain \"$1\" in domain \"$name\""
if (not defined($cf->{domain}->{$1}));
my $M = &expand_domain($1);
map { $members->{$_} = 1 } (@{$M});
}
else {
die "reference to unknown host \"$member\" in domain \"$name\""
if (not defined($cf->{host}->{$member}));
$members->{$member} = 1;
}
}
return [ keys(%{$members}) ];
}
foreach my $name (keys(%{$cf->{domain}})) {
next if ($name eq '-order');
$cf->{domain}->{$name}->{members} = &expand_domain($name);
}
# extend reverse/per-host domain specification with domain memberships
foreach my $host (keys(%{$cf->{host}})) {
next if ($host eq '-order');
if (not defined($cf->{host}->{$host}->{domain})) {
$cf->{host}->{$host}->{domain} = [];
}
foreach my $domain (@{$cf->{domain}->{-order}}) {
next if ($domain eq '-order');
foreach my $member (@{$cf->{domain}->{$domain}->{members}}) {
if ($member eq $host) {
if (not grep { $_ eq $domain } @{$cf->{host}->{$host}->{domain}}) {
push(@{$cf->{host}->{$host}->{domain}}, $domain);
}
}
}
}
}
# expand group/domain references in access grant configuration
foreach my $grant (@{$cf->{grant}}) {
$grant->[0] = [ map { &expand_host($_) } @{$grant->[0]} ];
$grant->[1] = [ map { &expand_user($_) } @{$grant->[1]} ];
sub expand_host {
my ($host) = @_;
my $tag = "";
if ($host =~ m|^(.+)(\[[^\]]+\])$|s) {
($host, $tag) = ($1, $2);
}
if ($host =~ m|^~(.+)$|s) {
# regular expression match
$host = $1;
if ($host =~ m|^\@(.+)$|s) {
my $regex = $1;
my @hosts = ();
foreach my $domain (keys(%{$cf->{domain}})) {
next if ($domain eq '-open');
if ($domain =~ m|$regex|s) {
foreach my $host (@{$cf->{domain}->{$domain}->{members}}) {
push(@hosts, $host.$tag);
}
}
}
die "no domains match regular expression \"$regex\""
if (@hosts == 0);
return @hosts;
}
else {
my $regex = $host;
my @hosts = ();
foreach my $host (keys(%{$cf->{host}})) {
next if ($host eq '-open');
if ($host =~ m|$regex|s) {
push(@hosts, $host.$tag);
}
}
die "no hosts match regular expression \"$regex\""
if (@hosts == 0);
return @hosts;
}
}
else {
# plain text match
if ($host =~ m|^\@(.+)$|s) {
my $domain = $1;
die "unknown domain \"$domain\""
if (not defined($cf->{domain}->{$domain}));
return map { $_.$tag } @{$cf->{domain}->{$domain}->{members}};
}
else {
die "unknown host \"$host\""
if (not defined($cf->{host}->{$host}));
return ($host.$tag);
}
}
}
sub expand_user {
my ($user) = @_;
my @user = ();
if ($user =~ m|^\@(.+)$|s) {
my $group = $1;
die "unknown community group \"$group\""
if (not defined($cf->{group}->{$group}));
return @{$cf->{group}->{$group}->{members}};
}
else {
die "unknown user \"$user\""
if (not defined($cf->{user}->{$user}));
return ($user);
}
}
}
# multiply out access control list
my $acl_by_user = {};
my $acl_by_host = {};
foreach my $grant (@{$cf->{grant}}) {
my ($hosts, $users) = ($grant->[0], $grant->[1]);
foreach my $host (@{$hosts}) {
foreach my $user (@{$users}) {
$acl_by_user->{$user} ||= {};
$acl_by_user->{$user}->{$host} = 1;
$acl_by_host->{$host} ||= {};
$acl_by_host->{$host}->{$user} = 1;
}
}
}
# optional configuration debugging
#print Data::Dumper->Dump([$cf]);
#exit(0);
## _________________________________________________________________________
##
## Main Procedure
## _________________________________________________________________________
##
# operation: update Bash command line completion cache
my $rc = 0;
if ($opt->{update}) {
$rc = &do_update();
}
if (@ARGV == 0) {
$rc = &do_cli();
}
elsif (@ARGV == 1) {
$rc = &do_connect($ARGV[0]);
}
else {
die "invalid number of arguments";
}
exit($rc);
## _________________________________________________________________________
##
## Utility Functions
## _________________________________________________________________________
##
# canonicalize filesystem path
sub path_canonicalize {
my ($path) = @_;
$path =~ s|/{2,}|/|sg;
1 while ($path =~ s|/\./||s);
1 while ($path =~ s|/[^/]+/\.\./|/|s);
return $path;
}
# encode a string into Base64 chunk
sub base64_encode {
my ($obj, $eol) = @_;
my $res = '';
$eol = "\n" unless defined($eol);
pos($obj) = 0;
while ($obj =~ m/(.{1,45})/gs) {
$res .= substr(pack('u', $1), 1);
chop($res);
}
$res =~ tr/` -_/AA-Za-z0-9+\//;
my $padding = (3 - length($obj) % 3) % 3;
$res =~ s/.{$padding}$/'=' x $padding/e if $padding;
$res =~ s/(.{1,64})/$1$eol/g if (length($eol) > 0);
return $res;
}
# decode a string from a Base64 chunk
sub base64_decode {
local($^W) = 0;
my ($str) = @_;
my $res = '';
$str =~ tr|A-Za-z0-9+=/||cd;
die 'invalid length of Base64 chunk' if (length($str) % 4);
$str =~ s/=+$//;
$str =~ tr|A-Za-z0-9+/| -_|;
while ($str =~ m/(.{1,60})/gs) {
my $len = chr(32 + length($1)*3/4);
$res .= unpack('u', $len.$1);
}
return $res;
}
sub text_fit {
my ($col, $str) = @_;
if (length($str) > $col) {
$str = substr($str, 0, $col);
}
return sprintf("%-${col}s", $str);
}
sub text_trim {
my ($col, $str) = @_;
if (length($str) > $col) {
$str = substr($str, 0, $col);
}
return $str;
}
## _________________________________________________________________________
##
## Operation: Update Command Line Completion Cache
## _________________________________________________________________________
##
sub do_update {
my $cache = '';
# command line options
foreach my $opt (keys(%getopt_spec)) {
if ($opt =~ m/^(.)\|(.*)$/s) {
$cache .= "option -$1\n";
$cache .= "option --$2\n";
}
}
# hostnames
foreach my $host (@{$cf->{host}->{-order}}) {
if ($acl_by_user->{$my->{user_name}}->{$host}) {
$cache .= "host ".$cf->{host}->{$host}->{hostname}."\n";
}
}
my $io = new IO::File ">$my->{prog_complete}"
or die "cannot write to completion cache file \"$my->{prog_complete}\"";
$io->print($cache);
$io->close;
exit(0);
}
## _________________________________________________________________________
##
## User Interface: Command Line Interface (CLI)
## _________________________________________________________________________
##
sub do_cli {
my $user = $cf->{user}->{$my->{user_name}}
or die "unknown user \"$my->{user_name}\"";
# FIXME
print " __ _ ___ \n" .
" / _\` |/ __| Welcome ".$user->{realname}." <".$user->{email}.">\n" .
"| (_| | (__ to the Cable & Wireless Auto Connect (AC) frontend\n" .
" \\__,_|\\___| shell running on server ac.de.cw.net\n" .
"\n" .
"In case of any questions or problems please contact:\n" .
"Internet Services, Ralf S. Engelschall \n" .
"\n";
# deferred loading of Term::ReadLine::Gnu
eval {
local $^W = 0;
use Term::ReadLine;
use Term::ReadLine::Gnu;
};
my $rl = new Term::ReadLine $my->{prog_name};
my $out = $rl->OUT || \*STDOUT;
my $prompt = $my->{user_name}."@".$my->{prog_name}."> ";
my $input;
$rl->ornaments("md,me,,");
$rl->MinLine(undef);
$rl->read_history($my->{prog_history});
my $rla = $rl->Attribs;
$rla->{completion_function} = sub {
my ($text, $line, $start) = @_;
my @r = ();
if ($start == 0) {
@r = qw(info connect quit);
}
elsif ($line =~ m|^\s*i(n(f(o?)?)?)?\s+\S*$|s) {
@r = map { $cf->{host}->{$_}->{hostname} }
keys(%{$acl_by_user->{$my->{user_name}}});
}
elsif ($line =~ m|^\s*c(o(n(n(e(ct?)?)?)?)?)?\s+\S*$|s) {
@r = map { $cf->{host}->{$_}->{hostname} }
keys(%{$acl_by_user->{$my->{user_name}}});
}
return @r;
};
while (defined ($input = $rl->readline($prompt))) {
if ($input =~ m|^\s*q(u(it?)?)?\s*$|s) {
last;
}
elsif ($input =~ m|^\s*i(n(f(o?)?)?)?|s) {
if ($input =~ m|^\s*\S+\s*$|s) {
my $rc = &do_info("*");
}
elsif ($input =~ m|^\s*\S+\s+(\S+)\s*$|s) {
my $rc = &do_info($1);
}
else {
print $out $my->{prog_name}.":ERROR: invalid arguments on 'info' command\n";
}
}
elsif ($input =~ m|^\s*c(o(n(n(e(ct?)?)?)?)?)?|s) {
if ($input =~ m|^\s*\S+\s*$|s) {
my $rc = &do_connect("*");
}
elsif ($input =~ m|^\s*\S+\s+(\S+)\s*$|s) {
my $rc = &do_connect($1);
}
else {
print $out $my->{prog_name}.":ERROR: invalid arguments on 'connect' command\n";
}
}
elsif ($input =~ m/^\s*\?\s*$/s) {
print $out
"info ......... output summary information\n" .
"connect ...... connect to a particular host\n" .
"quit .................... quit this session\n";
}
elsif ($input eq '') {
# no operation
}
else {
print $out $my->{prog_name}.":ERROR: invalid command (press '?' for help)\n";
}
$rl->addhistory($input) if ($input !~ m/^\S+$/);
}
print $out "\n" if (not defined($input));
$rl->write_history($my->{prog_history});
return 0;
}
## _________________________________________________________________________
##
## User Interface: Console User Interface (CUI)
## _________________________________________________________________________
##
sub do_cui {
my (@hosts) = @_;
# deferred loading of Curses::UI
if (not defined($Curses::VERSION) and not defined($Curses::UI::VERSION)) {
eval {
local $^W = 0;
use Curses;
use Curses::UI;
};
}
# create the CUI root object
my $cui = new Curses::UI (
-clear_on_exit => 0,
-debug => 0
);
my $W = {};
# determine width of screen
my $max_width = $cui->width();
die "screen width smaller than 80 characters" if ($max_width < 80);
my $cols = [["p", 2], ["s", 22], ["p", 1], ["s", 13], ["p", 1],
["s", 12], ["p", 1], ["s", 12], ["p", 1], ["s", 13], ["p", 2]];
my $width = 0;
foreach my $col (@{$cols}) {
$width += $col->[1];
}
my $i = 0;
while ($width < $max_width) {
while ($cols->[$i]->[0] eq 'p') {
$i = (($i + 1) % ($#{$cols}+1));
}
$cols->[$i]->[1]++;
$width++;
$i = (($i + 1) % ($#{$cols}+1));
}
my $fmt = "";
foreach my $col (@{$cols}) {
if ($col->[0] eq 'p') {
$fmt .= (" " x ($col->[1]));
}
else {
$fmt .= "%-". $col->[1] . "s";
}
}
# determine host list
my $list_values = [];
my $list_labels = {};
my $domains = $cf->{domain}->{-order};
$i = 0;
foreach my $domain (@{$domains}) {
my $headed = 0;
my $hosts = $cf->{host}->{-order};
foreach my $host (@{$hosts}) {
if ((grep { $_ eq $cf->{host}->{$host}->{hostname} } @hosts) == 1
and $cf->{host}->{$host}->{domain}->[0] eq $domain) {
if (not $headed) {
push(@{$list_values}, "::$i");
$list_labels->{"::$i"} = "";
$i++;
push(@{$list_values}, "::$i");
$list_labels->{"::$i"} = " ".$cf->{domain}->{$domain}->{description}."";
$i++;
$headed = 1;
}
my $label = sprintf($fmt,
&text_trim($cols->[1]->[1], $cf->{host}->{$host}->{'hostname'}),
&text_trim($cols->[3]->[1], $cf->{host}->{$host}->{'purpose'}),
&text_trim($cols->[5]->[1], $cf->{host}->{$host}->{'system'}),
&text_trim($cols->[7]->[1], $cf->{host}->{$host}->{'platform'}),
&text_trim($cols->[9]->[1], $cf->{host}->{$host}->{'location'}),
$cf->{host}->{$host}->{'hostname'});
push(@{$list_values}, $cf->{host}->{$host});
$list_labels->{$cf->{host}->{$host}} = $label;
}
}
}
# add main full-size window
$W->{main} = $cui->add(
'main', 'Window',
-title => "",
-border => 0,
-padtop => 0,
-padbottom => 0,
-ipad => 0,
-width => -1,
-height => -1,
);
# add header line
my $header = sprintf($fmt,
"Hostname", "Purpose", "System", "Platform", "Location"
);
$W->{header} = $W->{main}->add(
'header', 'Label',
-y => 0,
-x => 0,
-width => -1,
-height => 1,
-border => 0,
-reverse => 1,
-paddingspaces => 1,
-text => $header,
);
# add scrolling list
$W->{list} = $W->{main}->add(
'list', 'Listbox',
-y => 1,
-x => 0,
-width => $W->{main}->width,
-height => $W->{main}->height - 2,
-border => 0,
-values => $list_values,
-labels => $list_labels,
-multi => 0,
-title => "",
-vscrollbar => 0,
-htmltext => 1,
);
# add footer line
my $footer = sprintf($fmt,
"Hostname", "Purpose", "System", "Platform", "Location"
);
$W->{footer} = $W->{main}->add(
'footer', 'Label',
-y => -1,
-x => 0,
-width => -1,
-height => 1,
-text => $footer,
-border => 0,
-reverse => 1,
-paddingspaces => 1
);
# overwrite Curses::UI::Listbox key bindings
# to provide skipped plain-text list items
$W->{list}->set_binding(sub {
my $w = shift;
$w->do_routine("option-prevpage");
$w->layout_content;
while ($w->get_active_value() =~ m|^::|) {
$w->do_routine("option-next");
}
}, KEY_PPAGE());
$W->{list}->set_binding(sub {
my $w = shift;
$w->do_routine("option-nextpage");
$w->layout_content;
while ($w->get_active_value() =~ m|^::|) {
$w->do_routine("option-prev");
}
}, KEY_NPAGE());
$W->{list}->set_binding(sub {
my $w = shift;
my $n = 0;
my $y = $w->{-ypos};
return if ($y <= 0);
$y--, $n++;
$y--, $n++ while ($y > 0 and $w->{-values}->[$y] =~ m|^::|);
$y++, $n-- while ($w->{-values}->[$y] =~ m|^::|);
$w->do_routine("option-prev") while ($n-- > 0);
}, KEY_UP());
$W->{list}->set_binding(sub {
my $w = shift;
my $n = 0;
my $y = $w->{-ypos};
return if ($y >= $w->{-max_selected});
$y++, $n++;
$y++, $n++ while ($y < $w->{-max_selected} and $w->{-values}->[$y] =~ m|^::|);
$y--, $n-- while ($w->{-values}->[$y] =~ m|^::|);
$w->do_routine("option-next") while ($n-- > 0);
}, KEY_DOWN());
# CUI termination indicator
my $cui_exit = 0;
# terminate with "q" and LEFT
$cui->set_binding(sub {
$cui_exit = 2;
}, "q");
$cui->set_binding(sub {
$cui_exit = 2;
}, KEY_LEFT());
# select entry on RETURN and RIGHT
$W->{list}->set_binding(sub {
my $w = shift;
$w->do_routine("option-select");
$cui_exit = 1;
}, KEY_ENTER());
$W->{list}->set_binding(sub {
my $w = shift;
$w->do_routine("option-select");
$cui_exit = 1;
}, KEY_RIGHT());
# show info box on SPACE
$W->{list}->set_binding(sub {
my $w = shift;
my $host = $w->get_active_value() || return;
my $lastlogin = "NOW";
my $text = sprintf(
" Hostname: %s Purpose: %s \n" .
" Platform: %s Location: %s \n" .
" System: %s Last Login: %s ",
&text_fit(22, $host->{'hostname'}),
&text_fit(22, $host->{'purpose'}),
&text_fit(22, $host->{'platform'}),
&text_fit(22, $host->{'location'}),
&text_fit(22, $host->{'system'}),
&text_fit(22, $lastlogin));
my $box = $W->{main}->add(
'box', 'Dialog::Status',
-message => $text
);
$box->draw;
$box->focus;
$W->{main}->delete('box');
}, " ");
# hack: inject DOWN and UP key sequence in order to get rid of
# special case when first entry is a skipped plain-text entry.
$W->{list}->process_bindings(KEY_DOWN());
$W->{list}->process_bindings(KEY_UP());
# now perform the CUI main loop
$cui->focus(undef, 1);
$cui->draw;
$cui->do_one_event() until ($cui_exit);
# determine result
my $host = ($cui_exit == 1 ? $W->{list}->get_active_value() : undef);
$host = $host->{hostname} if (defined($host) and ref($host));
# shutdown CUI
$cui->DESTROY;
undef $cui;
# install program termination preparation sequence where stdout is
# redirected to get rid of the nasty (screen clearing) after-endwin()
# NCurses program termination cleanup sequence (which cannot be
# disabled from Perl as it looks).
END {
my $fd_stdout_new = POSIX::open("/dev/null", O_RDWR) || die;
my $fd_stdout_old = POSIX::dup(fileno(STDOUT));
dup2($fd_stdout_new, fileno(STDOUT));
}
return $host;
}
## _________________________________________________________________________
##
## Operation: Display Host Information
## _________________________________________________________________________
##
sub do_info {
my ($hostname) = @_;
# create hostname regex out of hostname pattern
my $re_hostname = $hostname;
$re_hostname =~ s|([.\$\@\%])|\\$1|sg;
$re_hostname =~ s|\*|.*|s;
$re_hostname =~ s|\?|.|s;
# iterate over all hosts in configuration order
my $first = 1;
foreach my $host (@{$cf->{host}->{-order}}) {
my $host = $cf->{host}->{$host};
if ($host->{hostname} =~ m/^.*${re_hostname}.*$/) {
# FIXME
my $lastlogin = "NOW";
print "\n" if (not $first);
$first = 0;
printf(" Hostname: %s Purpose: %s\n" .
" Platform: %s Location: %s\n" .
" System: %s Last Login: %s\n",
&text_fit(22, $host->{'hostname'}),
&text_fit(22, $host->{'purpose'}),
&text_fit(22, $host->{'platform'}),
&text_fit(22, $host->{'location'}),
&text_fit(22, $host->{'system'}),
&text_fit(22, $lastlogin));
}
}
return 0;
}
## _________________________________________________________________________
##
## Operation: Connect to a Host
## _________________________________________________________________________
##
# operation: connect to a host
sub do_connect {
my ($host) = @_;
# expand hostname via full hostname list
my @hosts = map { $cf->{host}->{$_}->{hostname} }
keys(%{$acl_by_user->{$my->{user_name}}});
my $re_host = $host;
$re_host =~ s|([.\$\@\%])|\\$1|sg;
$re_host =~ s|\*|.*|s;
$re_host =~ s|\?|.|s;
@hosts = grep { m/^.*${re_host}.*$/ } @hosts;
my $rc = 0;
if (@hosts == 0) {
die "host \"".$host."\" not known";
}
elsif (@hosts == 1) {
$rc = &do_connect_host($hosts[0]);
}
else {
my $hostname = &do_cui(@hosts);
if (defined($hostname)) {
$rc = &do_connect_host($hostname);
}
}
return $rc;
}
sub do_connect_host {
my ($hostname) = @_;
my $host = undef;
foreach my $h (keys(%{$cf->{host}})) {
next if ($h eq '-order');
if ($cf->{host}->{$h}->{hostname} eq $hostname) {
$host = $cf->{host}->{$h};
last;
}
}
# FIXME
my $lastlogin = "NOW";
# FIXME? Really?
if ($opt->{verbose}) {
&do_info($host->{'hostname'});
}
# deferred loading of Term::ReadLine::Gnu
if (not defined($Expect::VERSION)) {
eval {
local $^W = 0;
use Expect;
};
};
print "$my->{prog_name}: connection establishing\n";
# prepare script execution context
my $login = $host->{login};
my $scr = $cf->{method}->{$login->[0]}
or die "invalid login method \"$login->[0]\"";
my $ctx = {};
$ctx->{exp} = new Expect;
$ctx->{exp}->raw_pty(0);
$ctx->{timeout} = [ 10 ];
$ctx->{level} = 0;
$ctx->{args_by_pos} = [ $login->[0] ];
$ctx->{args_by_name} = {};
$ctx->{backref} = [ '' ];
$ctx->{spawned} = 0;
my $i = 1;
$ctx->{args_by_name}->{hostname} = $host->{hostname}; # special
foreach my $arg (@{$login}[1..$#{$login}]) {
if ($arg =~ m|^([a-zA-Z][a-zA-Z0-9_]*)=(.*)$|) {
$ctx->{args_by_name}->{$1} = $2;
}
else {
$ctx->{args_by_pos}->[$i++] = $arg;
}
};
# perform script execution
&execute_script($ctx, $scr);
# shutdown script execution and destroy context
$ctx->{exp}->soft_close() if ($ctx->{spawned});
print "\r"; # fix terminal cursor after scripting
undef $ctx->{exp};
print "$my->{prog_name}: connection droppped\n";
return 0;
}
## _________________________________________________________________________
##
## Operation: Execute Send/Expect Script
## _________________________________________________________________________
##
# expand an $X argument
sub expand_arg {
my ($ctx, $token) = @_;
$token =~ s/\#(\d+)/$ctx->{backref}->[$1] || ''/sge;
$token =~ s/\#\{(\d+)\}/$ctx->{backref}->[$1] || ''/sge;
$token =~ s/\$(\d+)/$ctx->{args_by_pos}->[$1] || ''/sge;
$token =~ s/\$\{(\d+)\}/$ctx->{args_by_pos}->[$1] || ''/sge;
$token =~ s/\&\{([a-zA-Z][a-zA-Z0-9_]*)\}/$ctx->{args_by_name}->{$1} || ''/sge;
return $token;
}
# execute send/expect script
sub execute_script {
my ($ctx, $scr) = @_;
# execute all commands in sequence
foreach my $cmd (@{$scr}) {
if ($cmd->[0] eq 'system') {
# execute a command synchronous
my $shcmd = &expand_arg($ctx, $cmd->[1]);
my $rc = system($shcmd);
if ($rc != 0) {
die "failed to spawn synchronous command: \"$shcmd\": $!";
}
}
elsif ($cmd->[0] eq 'spawn') {
# execute a command asynchronous
my $shcmd = &expand_arg($ctx, $cmd->[1]);
$ctx->{exp}->spawn($shcmd)
or die "failed to spawn asynchronous command \"$shcmd\": $!";
$ctx->{spawned} = 1;
}
elsif ($cmd->[0] eq 'timeout') {
# set timeout (locally scoped)
my $timeout = &expand_arg($ctx, $cmd->[1]);
$timeout =~ m|^\d+$|
or die "invalid timeout \"$timeout\"";
$ctx->{timeout}->[-1] = $timeout;
}
elsif ($cmd->[0] eq 'send') {
# send a string
my $string = &expand_arg($ctx, $cmd->[1]);
$ctx->{exp}->send($string);
}
elsif ($cmd->[0] eq 'expect') {
# expect one ore more strings
# open new scope
push(@{$ctx->{timeout}}, $ctx->{timeout}->[-1]);
$ctx->{level}++;
# assemble expect list
my @expected = ();
sub expect_cb {
my ($exp, $ctx, $scr) = @_;
return '' if (@{$scr} == 0); # short circuiting
$ctx->{backref} = [ '', $exp->matchlist() ];
return &::execute_script($ctx, $scr); # recursion(!)
};
foreach my $rule (@{$cmd->[1]}) {
my $regex = &expand_arg($ctx, $rule->[0]);
my $expect = [
"-re", $regex,
\&expect_cb, $ctx, $rule->[1]
];
push(@expected, $expect);
}
# perform expect operation
$ctx->{exp}->expect($ctx->{timeout}->[-1], @expected);
# close current scope
pop(@{$ctx->{timeout}});
$ctx->{level}--;
}
elsif ($cmd->[0] eq 'sleep') {
# sleep for a while
my $num = &expand_arg($ctx, $cmd->[1]);
die "non-numeric argument to command 'sleep'"
if ($num !~ m/^(\d+|\d*\.\d+)$/);
select(undef, undef, undef, $num);
}
elsif ($cmd->[0] eq 'repeat') {
# indicate to Expect module the repeating of
# current 'expect' operation (see Expect manpage)
if ($ctx->{level} == 0) {
die "command 'repeat' not allowed at outmost level";
}
return "exp_continue";
}
elsif ($cmd->[0] eq 'interact') {
# enter interactive session until EOF
if ($ctx->{level} != 0) {
die "command 'interact' allowed at outmost level only";
}
$ctx->{exp}->interact();
}
elsif ($cmd->[0] eq 'if') {
my ($expr, $true, $false) = ($cmd->[1], $cmd->[2], $cmd->[3]);
my $rv = &execute_expr($ctx, $expr);
if ($rv) {
# open new scope
push(@{$ctx->{timeout}}, $ctx->{timeout}->[-1]);
$ctx->{level}++;
# perform true clause
&execute_script($ctx, $true);
# close current scope
pop(@{$ctx->{timeout}});
$ctx->{level}--;
}
if (not $rv and defined($false)) {
# open new scope
push(@{$ctx->{timeout}}, $ctx->{timeout}->[-1]);
$ctx->{level}++;
# perform true clause
&execute_script($ctx, $false);
# close current scope
pop(@{$ctx->{timeout}});
$ctx->{level}--;
}
}
else {
die "invalid command \"$cmd->[0]\"";
}
}
return '';
}
sub execute_expr {
my ($ctx, $expr) = @_;
my $rv = '';
if ($expr->[0] eq 'tok') {
# simple plain-text token
$rv = &expand_arg($ctx, $expr->[1]);
}
elsif ($expr->[0] =~ m/^op:([.*\/%+-]|==|!=|<|<=|>=|>)$/) {
# standard binary operator expression
my ($op, $e1, $e2) = ($1, $expr->[1], $expr->[2]);
$e1 = &execute_expr($ctx, $e1);
$e2 = &execute_expr($ctx, $e2);
if ( $op =~ m/^(==|!=|<|<=|>=|>)$/
and ($e1 !~ m|^\d+$| or $e2 !~ m|^\d+$|)) {
# use string comparison operators except
# if both operands are really numerical
my %op_map = (qw(== eq != ne < lt <= le > gt >= ge));
$op = $op_map{$op};
}
eval "\$rv = (\$e1 $op \$e2);";
}
elsif ($expr->[0] =~ m/^op:(=~)$/) {
# pattern matching binary operator expression
my ($op, $e1, $re) = ($1, $expr->[1], $expr->[2]);
$e1 = &execute_expr($ctx, $e1);
$re = &expand_arg($ctx, $re);
eval "\$rv = (\$e1 $op m/\$re/);";
}
elsif ($expr->[0] =~ m/^op:([!-])$/) {
# standard unary operator expression
my ($op, $e1) = ($1, $expr->[1]);
$e1 = &execute_expr($ctx, $e1);
$op = '!' if ($op eq '-' and $e1 !~ m|^\d+$|);
$e1 = ($e1 =~ m|^\s*$|s ? 0 : 1) if ($op eq '!');
eval "\$rv = ($op \$e1);";
}
else {
die "invalid expression operator \"$expr->[0]\"";
}
return $rv;
}