#!/usr/bin/perl # # conmux -- the main console multiplexor daemon # # Main console multiplexor daemon. There is one of these daemons for # each open console supported in the system. Clients are directed to # this daemon via the conmux-registry deamon. # # (C) Copyright IBM Corp. 2004, 2005, 2006 # Author: Andy Whitcroft # # The Console Multiplexor is released under the GNU Public License V2 # use strict; use FindBin; use Symbol qw(gensym); use IO::Socket; use IO::Multiplex; use IPC::Open3; use URI::Escape; use Net::Domain; # Find our internal libraries. use lib $FindBin::Bin; use lib "$FindBin::Bin/../lib/"; use lib "$FindBin::Bin/lib/"; use Conmux; our $P = 'conmux'; our $debug = 0; $SIG{'CHLD'} = "IGNORE"; $| = 1; # # CALLBACK: this class is used to provide a timed callback. The multiplexor # libarary allows us to set a timeout on any open file we have registered. # So, we open a new file descriptor to /dev/null and set a timeout on that. # package Callback; sub new { my ($class, $mux, $who, $time) = @_; my $self = bless { 'who' => $who }, $class; my ($fh); print "Callback::new [$self] mux<$mux> who<$who> time<$time>\n" if ($main::debug); # Open a file handle to nothing, we need this to hang the timeout # on in the multiplexor. It will fail with a mux_eof, which we ignore. open($fh, "add($fh); $mux->set_callback_object($self, $fh); $mux->set_timeout($fh, $time); $self; } sub mux_timeout { my ($self, $mux, $fh) = @_; print "Callback::mux_timeout [$self] mux<$mux> fh<$fh>\n" if ($main::debug); $self->{'who'}->callback_timeout(); $mux->close($fh); } sub DESTROY { my ($self) = @_; print "Callback::DESTROY [$self]\n" if ($main::debug); } # # LISTENER SOCKET: creates an intenet listener for new clients and # connects them to the junction provided. # package ListenerSocket; sub new { my ($class, $mux, $port) = @_; my $self = bless { 'mux' => $mux }, $class; print "ListenerSocket::new [$self] mux<$mux> port<$port>\n" if ($main::debug); $self->initialise($port); $self; } sub initialise { my ($self, $port) = @_; my ($sock); print "ListenerSocket::initialise [$self] port<$port> " if ($main::debug); # Create a listening socket and add it to the multiplexor. my $sock = new IO::Socket::INET(Proto => 'tcp', LocalPort => $port, Listen => 4, ReuseAddr => 1) or die "socket: $@"; print " adding $self $sock\n" if ($main::debug); $self->mux->listen($sock); $self->mux->set_callback_object($self, $sock); $self->listener($sock); } # DATA accessors. sub mux { my $self = shift; if (@_) { $self->{'mux'} = shift } return $self->{'mux'}; } sub listener { my $self = shift; if (@_) { $self->{'listener'} = shift } return $self->{'listener'}; } sub address { my ($self) = @_; Net::Domain::hostfqdn() . ':' . $self->{'listener'}->sockport(); } # JUNCTION: callbacks. ##sub junctionInput { ##} ##sub junctionEOF { ## my ($self) = @_; ## ## $self->{'junction'}->junctionRemove($self, 'console-client'); ## $self->{'mux'}->close($self->{'listener'}); ##} # Handle new connections by instantiating a new client class. sub mux_connection { my ($self, $mux, $fh) = @_; my ($client); print "ListenerSocket::mux_connection [$self] mux<$mux> fh<$fh>\n" if ($main::debug); # Make a new client connection. $client = ClientCmd->new($mux, $fh); print " new connection $self $client\n" if ($main::debug); } sub DESTROY { my ($self) = @_; print "ListenerSocket::DESTROY [$self]\n" if ($main::debug); close($self->listener); } # # JUNCTION: generic junction box object, connects names groups of objects # to other named groups. # # Expects the following callbacks to be defined on each object registered: # junctionInput($from, $data) # junctionEOF($from, $to) # package Junction; sub new { my ($class) = @_; my $self = bless { }, $class; print "Junction::new [$self]\n" if ($main::debug); $self; } sub junctionAdd { my ($self, $client) = @_; print "Junction::junctionAdd [$self] client<$client>\n" if ($main::debug); # Add ourselves to the list of recipients. $self->{$client} = $client; } sub junctionInput { my ($self, $client, $data) = @_; my ($c); print "Junction::junctionInput [$self] client<$client> " . "data<$data>\n" if ($main::debug); # Send this data on to the clients listed in the output list. for $c (values %{$self}) { print " sending to $c\n" if ($main::debug); $c->junctionInput($client, $data); } } sub junctionEOF { my ($self, $client) = @_; my ($c); print "Junction::junctionEOF [$self] client<$client>\n" if ($main::debug); # Send this eof on to the clients listed in the output list. for $c (values %{$self}) { print " sending to $c\n" if ($main::debug); $c->junctionEOF($client); } } sub junctionRemove { my ($self, $client) = @_; print "Junction::junctionRemove [$self] client<$client>\n" if ($main::debug); # Drop this client from our lists. delete $self->{$client}; } # # PAYLOAD: generic payload object, connects itself to the requisite junction. # package Payload; my %payloads = (); my $payloads = 0; sub lookup { my ($class, $name) = @_; $payloads{$name}; } sub found { my ($class, $name, $self) = @_; print "Payloads::found name<$name> self<$self>\n" if ($main::debug); $payloads{$name} = $self; $payloads++; } sub lost { my ($class, $name, $self) = @_; print "Payloads::lost name<$name> self<$self>\n" if ($main::debug); undef $payloads{$name}; if (--$payloads == 0) { exit(0); } } sub new { my ($class, $name, $title, $mux, @a) = @_; my $self = bless { }, $class; print "Payload::new [$self] name<$name> title<$title> mux<$mux>\n" if ($main::debug); Payload->found($name, $self); $self->name($name); $self->title($title); $self->mux($mux); $self->enabled(1); $self->cin(Junction->new); $self->cout(Junction->new); $self->initialise(@a); $self; } # Data accessors. sub name { my $self = shift; if (@_) { $self->{'name'} = shift } return $self->{'name'}; } sub title { my $self = shift; if (@_) { $self->{'title'} = shift } return $self->{'title'}; } sub mux { my $self = shift; if (@_) { $self->{'mux'} = shift } return $self->{'mux'}; } sub cin { my $self = shift; if (@_) { $self->{'cin'} = shift } return $self->{'cin'}; } sub cout { my $self = shift; if (@_) { $self->{'cout'} = shift } return $self->{'cout'}; } sub enabled { my $self = shift; if (@_) { $self->{'enabled'} = shift } return $self->{'enabled'}; } sub connected { my $self = shift; if (@_) { $self->{'connected'} = shift } $self->transition(); return $self->{'connected'}; } sub transition { my $self = shift; my $time = time; if (($time - $self->{'trans_minor'}) > 30) { $self->{'trans_major'} = $time; } $self->{'trans_minor'} = $time; } sub retry_timeout { my $self = shift; my $time = time - $self->{'trans_major'}; if ($time < 60) { return 1; } elsif ($time < 120) { return 10; } else { return 30; } } sub state { my $self = shift; my $ctime = $self->{'connected'}; my $ttime = $self->{'trans_major'}; my $time = time; if ($ctime && ($time - $ctime) > 30) { "connected"; } elsif ($ttime && ($time - $ttime) < 60) { "transition"; } else { "disconnected"; } } sub initialise { my ($self) = @_; my ($sock); print "Payload::initialise [$self]\n" if ($main::debug); # Ensure we recieve client input. $self->cin->junctionAdd($self); $self->connected(time); } # Telnet constants. my $TN_IAC = sprintf("%c", 255); my $TN_DONT = sprintf("%c", 254); my $TN_DO = sprintf("%c", 253); my $TN_WONT = sprintf("%c", 252); my $TN_WILL = sprintf("%c", 251); my $TN_SB = sprintf("%c", 250); my $TN_SE = sprintf("%c", 240); my $TN_BREAK = sprintf("%c", 243); my $TNOPT_ECHO = sprintf("%c", 1); my $TNOPT_SGA = sprintf("%c", 3); # # If we get here then we have accumulated a complete telnet # negotiation string. # # Telnet negotiation protocol - RFC#854: # # DO We are being asked to DO an option # DONT We are being asked to NOT DO an option # WILL We are being told they will DO an option # WONT We are being told they will NOT DO an option # # DO/DONT requests indicate we should {en,dis}able a mode. # We are expected to respond with WILL or WONT. To prevent # loops, we should not respond if the request matches our # current mode. # # WILL/WONT requests indicate the other end would like to # {en,dis}able a mode. We are expected to respond with # DO/DONT. # # If we want a particular mode {en,dis}abled then we may start # negotiation of that mode with a WILL/WONT. # # We want the other end to perform echo by default so we will # DO any request for ECHO and DONT all other requests. # sub mux_input { my ($self, $mux, $fh, $input) = @_; my ($client); print "Payload::mux_input [$self] mux<$mux> fh<$fh> input<$$input>\n" if ($main::debug); while ($$input ne "") { # Ordinary text. if ($$input =~ s/^([^$TN_IAC]+)//) { # Data coming in from the payload, this needs to go to # all of the clients. $self->cout->junctionInput($self, $1); next; } # IAC,SB,...,SE if ($$input =~ s/^$TN_IAC$TN_SB([^$TN_SE]+)$TN_SE//) { print "SB\n" if ($main::debug); next; } # IAC,[DO|DONT|WILL|WONT], if ($$input =~ s/^$TN_IAC$TN_DO(.)//) { my $c = unpack("C", $1); print "DO<$c:$1>\n" if ($main::debug); # We are DONT on all options so WONT all requests. $self->junctionInput($self, "$TN_IAC$TN_WONT$1"); next; } if ($$input =~ s/^$TN_IAC$TN_DONT(.)//) { my $c = unpack("C", $1); print "DONT<$c:$1>\n" if ($main::debug); # We are already DONT on all options, no reply. next; } if ($$input =~ s/^$TN_IAC$TN_WILL(.)//) { my $c = unpack("C", $1); print "WILL<$c:$1>\n" if ($main::debug); my $reply = $TN_DONT; if ($1 == $TNOPT_ECHO || $1 == $TNOPT_SGA) { $reply = $TN_DO; } $self->junctionInput($self, "$TN_IAC$reply$1"); next; } if ($$input =~ s/^$TN_IAC$TN_WONT(.)//) { my $c = unpack("C", $1); print "WONT<$c:$1>\n" if ($main::debug); $self->junctionInput($self, "$TN_IAC$TN_DONT$1"); next; } # IAC,