From 9e8332ae5ab5a07b5085a117310344a1f5a33c78 Mon Sep 17 00:00:00 2001 From: rudolfkoenig <> Date: Thu, 10 Jan 2013 14:40:57 +0000 Subject: [PATCH] telnet client mode git-svn-id: https://svn.fhem.de/fhem/trunk@2463 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/CHANGED | 1 + fhem/FHEM/98_telnet.pm | 160 ++++++++++++++++++++++++++++++++++++----- fhem/contrib/tcptee.pl | 93 ++++++++++++++++-------- 3 files changed, 205 insertions(+), 49 deletions(-) diff --git a/fhem/CHANGED b/fhem/CHANGED index d4689f5ff..6500c8157 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -48,6 +48,7 @@ OWFS - feature: stateFormat (readingsFn modules) and showInternalValues attributes - feature: new readingsFn modules: FS20 CUL_WS HMS CUL_EM CUL_TX EnOcean ZWave + - feature: telnet client mode - 2012-10-28 (5.3) - feature: added functions trim, ltrim, rtrim, UntoggleDirect, diff --git a/fhem/FHEM/98_telnet.pm b/fhem/FHEM/98_telnet.pm index 9435763cf..bc3b072c1 100644 --- a/fhem/FHEM/98_telnet.pm +++ b/fhem/FHEM/98_telnet.pm @@ -21,7 +21,7 @@ telnet_Initialize($) $hash->{AttrFn} = "telnet_Attr"; $hash->{NotifyFn}= "telnet_SecurityCheck"; $hash->{AttrList} = "loglevel:0,1,2,3,4,5,6 globalpassword password ". - "allowfrom SSL"; + "allowfrom SSL connectTimeout connectInterval"; } ##################################### @@ -45,6 +45,63 @@ telnet_SecurityCheck($$) return; } +########################## +sub +telnet_ClientConnect($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + $hash->{DEF} =~ m/^(IPV6:)?(.*):(\d+)$/; + my ($isIPv6, $server, $port) = ($1, $2, $3); + + Log GetLogLevel($name,4), "$name: Connecting to $server:$port..."; + my @opts = ( + PeerAddr => "$server:$port", + Timeout => AttrVal($name, "connectTimeout", 2), + ); + + my $client; + if($hash->{SSL}) { + $client = IO::Socket::SSL->new(@opts); + } else { + $client = IO::Socket::INET->new(@opts); + } + if($client) { + $hash->{FD} = $client->fileno(); + $hash->{CD} = $client; # sysread / close won't work on fileno + $hash->{BUF} = ""; + $hash->{CONNECTS}++; + $selectlist{$name} = $hash; + $hash->{STATE} = "Connected"; + RemoveInternalTimer($hash); + Log(GetLogLevel($name,3), "$name: connected to $server:$port"); + + } else { + telnet_ClientDisconnect($hash, 1); + + } +} + +########################## +sub +telnet_ClientDisconnect($$) +{ + my ($hash, $connect) = @_; + my $name = $hash->{NAME}; + close($hash->{CD}) if($hash->{CD}); + delete($hash->{FD}); + delete($hash->{CD}); + delete($selectlist{$name}); + $hash->{STATE} = "Disconnected"; + InternalTimer(gettimeofday()+AttrVal($name, "connectInterval", 60), + "telnet_ClientConnect", $hash, 0); + if($connect) { + Log GetLogLevel($name,4), "$name: Connect failed."; + } else { + Log GetLogLevel($name,3), "$name: Disconnected"; + } +} + ########################## sub telnet_Define($$$) @@ -53,18 +110,30 @@ telnet_Define($$$) my @a = split("[ \t][ \t]*", $def); my ($name, $type, $port, $global) = split("[ \t]+", $def); - return "Usage: define telnet [IPV6:] [global]" - if($port !~ m/^(IPV6:)?\d+$/ || ($global && $global ne "global")); - my $ret = TcpServer_Open($hash, $port, $global); + my $isServer = 1 if($port && $port =~ m/^(IPV6:)?\d+$/); + my $isClient = 1 if($port && $port =~ m/^(IPV6:)?.*:\d+$/); + + return "Usage: define telnet { [IPV6:] [global] | ". + " [IPV6:]serverName:port }" + if(!($isServer || $isClient) || + ($isClient && $global) || + ($global && $global ne "global")); # Make sure that fhem only runs once - if($ret && !$init_done) { - Log 1, "$ret. Exiting."; - exit(1); + if($isServer) { + my $ret = TcpServer_Open($hash, $port, $global); + if($ret && !$init_done) { + Log 1, "$ret. Exiting."; + exit(1); + } + return $ret; } - return $ret; + if($isClient) { + $hash->{isClient} = 1; + telnet_ClientConnect($hash); + } } sub @@ -97,16 +166,22 @@ telnet_Read($) my $buf; my $ret = sysread($hash->{CD}, $buf, 256); if(!defined($ret) || $ret <= 0) { - CommandDelete(undef, $name); + if($hash->{isClient}) { + telnet_ClientDisconnect($hash, 0); + } else { + CommandDelete(undef, $name); + } return; } + if(ord($buf) == 4) { # EOT / ^D CommandQuit($hash, ""); return; } $buf =~ s/\r//g; - my $pw = telnet_pw($hash->{SNAME}, $name); + my $sname = ($hash->{isClient} ? $name : $hash->{SNAME}); + my $pw = telnet_pw($sname, $name); if($pw) { $buf =~ s/\xff..//g; # Telnet IAC stuff $buf =~ s/\xfd(.)//; # Telnet Do ? @@ -136,7 +211,12 @@ telnet_Read($) $hash->{pwEntered} = 1; next; } else { - CommandDelete(undef, $name); + if($hash->{isClient}) { + telnet_ClientDisconnect($hash, 0); + } else { + delete($hash->{rcvdQuit}); + CommandDelete(undef, $name); + } return; } } @@ -175,8 +255,17 @@ telnet_Read($) last if(!$l || $l == length($ret)); $ret = substr($ret, $l); } + $hash->{CD}->flush(); + + } + if($hash->{rcvdQuit}) { + if($hash->{isClient}) { + delete($hash->{rcvdQuit}); + telnet_ClientDisconnect($hash, 0); + } else { + CommandDelete(undef, $name); + } } - CommandDelete(undef, $name) if($hash->{rcvdQuit}); } ########################## @@ -188,6 +277,10 @@ telnet_Attr(@) if($a[0] eq "set" && $a[2] eq "SSL") { TcpServer_SetSSL($hash); + if($hash->{CD}) { + my $ret = IO::Socket::SSL->start_SSL($hash->{CD}); + Log 1, "$hash->{NAME} start_SSL: $ret" if($ret); + } } return undef; } @@ -211,30 +304,48 @@ telnet_Undef($$) Define
    - define <name> telnet <portNumber> [global] + define <name> telnet <portNumber> [global]
    + or
    + define <name> telnet <servername>:<portNumber>

    + First form, server mode:
    Listen on the TCP/IP port <portNumber> for incoming connections. If the second parameter global is not specified, the server will only listen to localhost connections. -

    - +
    To use IPV6, specify the portNumber as IPV6:<number>, in this case the perl module IO::Socket:INET6 will be requested. On Linux you may have to install it with cpan -i IO::Socket::INET6 or apt-get libio-socket-inet6-perl; OSX and the FritzBox-7390 perl already has - this module. -

    + this module.
    Examples:
      define tPort telnet 7072 global
      attr tPort globalpassword mySecret
      attr tPort SSL
    -
    Note: The old global attribute port is automatically converted to a telnet instance with the name telnetPort. The global allowfrom attibute is lost in this conversion. + +

    + Second form, client mode:
    + Connect to the specified server port, and execute commands received from + there just like in server mode. This can be used to connect to a fhem + instance sitting behind a firewall, when installing exceptions in the + firewall is not desired or possible. Note: this client mode supprts SSL, + but not IPV6.
    + Example: +
      + Start tcptee first on publicly reachable host outside the firewall.
        + perl contrib/tcptee.pl --bidi 3000
      + Configure fhem inside the firewall:
        + define tClient telnet <tcptee_host>:3000
      + Connect to the fhem from outside of the firewall:
        + telnet <tcptee_host> 3000
      +
    +

@@ -291,6 +402,19 @@ telnet_Undef($$) only connections from these addresses are allowed.
+ +
  • connectTimeout
    + Wait at maximum this many seconds for the connection to be established. + Default is 2. +

  • + + +
  • connectInterval
    + After closing a connection, or if a connection cannot be estblished, + try to connect again after this many seconds. Default is 60. +

  • + + diff --git a/fhem/contrib/tcptee.pl b/fhem/contrib/tcptee.pl index 2bb16870c..665ad6d09 100644 --- a/fhem/contrib/tcptee.pl +++ b/fhem/contrib/tcptee.pl @@ -10,10 +10,11 @@ my $bidi; my $loop; my $myIp; my $myPort; +my $ssl; my $serverHost; my $serverPort; -my $usage = "Usage: tcptee.pl [--bidi] [--loop] " . - "[myIp:]myPort:serverHost:serverPort\n"; +my $usage = "Usage: tcptee.pl [--bidi] [--loop] [--ssl] " . + "[myIp:]myPort[:serverHost:serverPort]\n"; while(@ARGV) { my $opt = shift @ARGV; @@ -24,13 +25,19 @@ while(@ARGV) { } elsif($opt =~ m/^--loop$/i) { $loop = 1 - } elsif($opt =~ m/^(.*):(\d+):(.*):(\d+)/) { + } elsif($opt =~ m/^--ssl$/i) { + $ssl = 1 + + } elsif($opt =~ m/^(\d+)$/) { + $myPort = $opt; + + } elsif($opt =~ m/^(.*):(\d+):(.*):(\d+)$/) { $myIp = $1; $myPort = $2; $serverHost = $3; $serverPort = $4; - } elsif($opt =~ m/^(\d+):(.*):(\d+)/) { + } elsif($opt =~ m/^(\d+):(.*):(\d+)$/) { $myPort = $1; $serverHost = $2; $serverPort = $3; @@ -41,10 +48,10 @@ while(@ARGV) { } } -die $usage if(!$serverHost); - my ($sfd, $myfd, %clients, $discoMsg); +die $usage if(!$myPort); + sub tPrint($) { @@ -58,16 +65,18 @@ tPrint($) for(;;) { # Open the server first - $sfd = IO::Socket::INET->new(PeerAddr => "$serverHost:$serverPort"); - if(!$sfd) { - tPrint "Cannot connect to $serverHost:$serverPort : $!" if(!$discoMsg); + if($serverHost) { + $sfd = IO::Socket::INET->new(PeerAddr => "$serverHost:$serverPort"); + if(!$sfd) { + tPrint "Cannot connect to $serverHost:$serverPort : $!" if(!$discoMsg); + $discoMsg = 1; + last if(!$loop); + sleep(5); + next; + } $discoMsg = 1; - last if(!$loop); - sleep(5); - next; + tPrint "Connected to $serverHost:$serverPort"; } - $discoMsg = 1; - tPrint "Connected to $serverHost:$serverPort"; # Now open our listener @@ -85,7 +94,7 @@ for(;;) { # Data loop for(;;) { my ($rin,$rout) = ('',''); - vec($rin, $sfd->fileno(), 1) = 1; + vec($rin, $sfd->fileno(), 1) = 1 if($sfd); vec($rin, $myfd->fileno(), 1) = 1; foreach my $c (keys %clients) { vec($rin, fileno($clients{$c}{fd}), 1) = 1; @@ -110,11 +119,29 @@ for(;;) { $clients{$fd}{addr} = inet_ntoa($iaddr) . ":$port"; tPrint "Connection accepted from $clients{$fd}{addr}"; + if($ssl) { + tPrint "Attaching SSL"; + eval "require IO::Socket::SSL"; + if($@) { + tPrint "Can't load IO::Socket::SSL, falling back to plain"; + } else { + my $ret = IO::Socket::SSL->start_SSL($fd, { + SSL_server => 1, + SSL_key_file => "certs/server-key.pem", + SSL_cert_file => "certs/server-cert.pem", + }); + if(!$ret && $! ne "Socket is not connected") { + die "SSL/HTTPS error: $!"; + } + } + } + + syswrite($fd, $firstmsg) if($firstmsg); } # Data from the server - if(vec($rout, $sfd->fileno(), 1)) { + if($sfd && vec($rout, $sfd->fileno(), 1)) { my $buf; my $ret = sysread($sfd, $buf, 256); if(!defined($ret) || $ret <= 0) { @@ -123,33 +150,37 @@ for(;;) { } foreach my $c (keys %clients) { syswrite($clients{$c}{fd}, $buf); + $clients{$c}{fd}->flush(); } $firstmsg = $buf if(!$firstmsg); } # Data from one of the clients - foreach my $c (keys %clients) { +CLIENT:foreach my $c (keys %clients) { next if(!vec($rout, fileno($clients{$c}{fd}), 1)); - my $buf; - my $ret = sysread($clients{$c}{fd}, $buf, 256); - if(!defined($ret) || $ret <= 0) { - close($clients{$c}{fd}); - tPrint "Client $clients{$c}{addr} left us"; - delete($clients{$c}); - next; - } - - syswrite($sfd, $buf); - if($bidi) { - foreach my $c2 (keys %clients) { - syswrite($clients{$c2}{fd}, $buf) if($c2 ne $c); + for(;;) { + my $buf; + my $ret = sysread($clients{$c}{fd}, $buf, 256); + if(!defined($ret) || $ret <= 0) { + close($clients{$c}{fd}); + tPrint "Client $clients{$c}{addr} left us"; + delete($clients{$c}); + next CLIENT; } + + syswrite($sfd, $buf) if($sfd); + if($bidi) { + foreach my $c2 (keys %clients) { + syswrite($clients{$c2}{fd}, $buf) if($c2 ne $c); + } + } + last if(!$ssl || !$clients{$c}{fd}->pending()); } } } - close($sfd); + close($sfd) if($sfd); close($myfd); foreach my $c (keys %clients) { close($clients{$c}{fd});