From 2500cdcd33ddec6043b402079688061fa72562e1 Mon Sep 17 00:00:00 2001 From: rudolfkoenig <> Date: Sat, 23 Jun 2012 16:22:28 +0000 Subject: [PATCH] New telnet module and its consequences git-svn-id: https://svn.fhem.de/fhem/trunk@1638 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/CHANGED | 1 + fhem/FHEM/98_telnet.pm | 190 ++++++++++++++++++++ fhem/FHEM/TcpServerUtils.pm | 150 ++++++++++++++++ fhem/TODO | 2 - fhem/docs/commandref.html | 153 +++++++++++----- fhem/fhem.pl | 269 ++++++---------------------- fhem/webfrontend/pgm2/01_FHEMWEB.pm | 137 ++------------ 7 files changed, 525 insertions(+), 377 deletions(-) create mode 100644 fhem/FHEM/98_telnet.pm create mode 100644 fhem/FHEM/TcpServerUtils.pm diff --git a/fhem/CHANGED b/fhem/CHANGED index 291764b70..f7e715e37 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -49,6 +49,7 @@ *Utils.pm files from fhem.pl - feature: portpassword and basicAuth may use evaluated functions - feature: motd with SecurityCheck added + - feature: telnet module added, attr global port moved. allowfrom changed. - 2011-12-31 (5.2) - bugfix: applying smallscreen attributes to firefox/opera diff --git a/fhem/FHEM/98_telnet.pm b/fhem/FHEM/98_telnet.pm new file mode 100644 index 000000000..58f5ee6cf --- /dev/null +++ b/fhem/FHEM/98_telnet.pm @@ -0,0 +1,190 @@ +############################################## +# $Id: 98_telnet.pm 1098 2011-11-12 07:51:08Z rudolfkoenig $ + +# Note: this is not really a telnet server, but a TCP server with slight telnet +# features (disable echo on password) + +package main; +use strict; +use warnings; +use TcpServerUtils; + +########################## +sub +telnet_Initialize($) +{ + my ($hash) = @_; + + $hash->{DefFn} = "telnet_Define"; + $hash->{ReadFn} = "telnet_Read"; + $hash->{UndefFn} = "telnet_Undef"; + $hash->{AttrFn} = "telnet_Attr"; + $hash->{NotifyFn}= "telnet_SecurityCheck"; + $hash->{AttrList} = "loglevel:0,1,2,3,4,5,6 globalpassword password ". + "allowfrom SSL"; +} + +##################################### +sub +telnet_SecurityCheck($$) +{ + my ($ntfy, $dev) = @_; + return if($dev->{NAME} ne "global" || + !grep(m/^INITIALIZED$/, @{$dev->{CHANGED}})); + my $motd = AttrVal("global", "motd", ""); + if($motd =~ "^SecurityCheck") { + my @list = grep { !(AttrVal($_, "password", undef) || + AttrVal($_, "globalpassword", undef)) } + devspec2array("TYPE=telnet"); + $motd .= (join(",", sort @list). + " has no password/globalpassword attribute\n") + if(@list); + $attr{global}{motd} = $motd; + } + delete $modules{telnet}{NotifyFn}; + return; +} + +########################## +sub +telnet_Define($$$) +{ + my ($hash, $def) = @_; + + 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")); + + return TcpServer_Open($hash, $port, $global); +} + +sub +telnet_pw($$) +{ + my ($sname, $cname) = @_; + my $pw = $attr{$sname}{password}; + return $pw if($pw); + + $pw = $attr{$sname}{globalpassword}; + return $pw if($pw && $cname !~ m/^telnet:127.0.0.1/); + + return undef; +} + +########################## +sub +telnet_Read($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + if($hash->{SERVERSOCKET}) { # Accept and create a child + my $chash = TcpServer_Accept($hash, "telnet"); + return if(!$chash); + syswrite($chash->{CD}, sprintf("%c%c%cPassword: ", 255, 251, 1)) # WILL ECHO + if(telnet_pw($name, $chash->{NAME})); + return; + } + + my $buf; + my $ret = sysread($hash->{CD}, $buf, 256); + if(!defined($ret) || $ret <= 0) { + CommandDelete(undef, $name); + return; + } + if(ord($buf) == 4) { # EOT / ^D + CommandQuit($hash, ""); + next; + } + + $buf =~ s/\r//g; + my $pw = telnet_pw($hash->{SNAME}, $name); + if($pw) { + $buf =~ s/\xff..//g; # Telnet IAC stuff + $buf =~ s/\xfd(.)//; # Telnet Do ? + syswrite($hash->{CD}, sprintf("%c%c%c", 0xff, 0xfc, ord($1))) + if(defined($1)) # Wont / ^C handling + } + $hash->{BUF} .= $buf; + my @ret; + my $gotCmd; + + while($hash->{BUF} =~ m/\n/) { + my ($cmd, $rest) = split("\n", $hash->{BUF}, 2); + $hash->{BUF} = $rest; + + if(!$hash->{pwEntered}) { + if($pw) { + syswrite($hash->{CD}, sprintf("%c%c%c\r\n", 255, 252, 1)); # WONT ECHO + + $ret = ($pw eq $cmd); + if($pw =~ m/^{.*}$/) { # Expression as pw + my $password = $cmd; + $ret = eval $pw; + Log 1, "password expression: $@" if($@); + } + + if($ret) { + $hash->{pwEntered} = 1; + next; + } else { + CommandDelete(undef, $name); + return; + } + } + } + $gotCmd = 1; + if($cmd) { + if($cmd =~ m/\\ *$/) { # Multi-line + $hash->{prevlines} .= $cmd . "\n"; + } else { + if($hash->{prevlines}) { + $cmd = $hash->{prevlines} . $cmd; + undef($hash->{prevlines}); + } + $ret = AnalyzeCommandChain($hash, $cmd); + push @ret, $ret if(defined($ret)); + } + } else { + $hash->{prompt} = 1; # Empty return + if(!$hash->{motdDisplayed}) { + my $motd = $attr{global}{motd}; + push @ret, $motd if($motd && $motd ne "none"); + $hash->{motdDisplayed} = 1; + } + } + next if($rest); + } + + $ret = ""; + $ret .= (join("\n", @ret) . "\n") if(@ret); + $ret .= ($hash->{prevlines} ? "> " : "fhem> ") + if($gotCmd && $hash->{prompt} && !$hash->{rcvdQuit}); + if($ret) { + $ret =~ s/\n/\r\n/g if($pw); # only for DOS telnet + syswrite($hash->{CD}, $ret); + } + CommandDelete(undef, $name) if($hash->{rcvdQuit}); +} + +########################## +sub +telnet_Attr(@) +{ + my @a = @_; + my $hash = $defs{$a[1]}; + + if($a[0] eq "set" && $a[2] eq "SSL") { + TcpServer_SetSSL($hash); + } + return undef; +} + +sub +telnet_Undef($$) +{ + my ($hash, $arg) = @_; + return TcpServer_Close($hash); +} + +1; diff --git a/fhem/FHEM/TcpServerUtils.pm b/fhem/FHEM/TcpServerUtils.pm new file mode 100644 index 000000000..75d7e6c87 --- /dev/null +++ b/fhem/FHEM/TcpServerUtils.pm @@ -0,0 +1,150 @@ +############################################## +# $Id: TcpServerUtils.pm 1098 2011-11-12 07:51:08Z rudolfkoenig $ + +package main; +use strict; +use warnings; +use IO::Socket; + +sub +TcpServer_Open($$$) +{ + my ($hash, $port, $global) = @_; + my $name = $hash->{NAME}; + + if($port =~ m/^IPV6:(\d+)$/i) { + $port = $1; + eval "require IO::Socket::INET6; use Socket6;"; + if($@) { + Log 1, $@; + Log 1, "$name: Can't load INET6, falling back to IPV4"; + } else { + $hash->{IPV6} = 1; + } + } + + my @opts = ( + Domain => ($hash->{IPV6} ? AF_INET6() : AF_UNSPEC), # Linux bug + LocalHost => ($global ? undef : "localhost"), + LocalPort => $port, + Listen => 10, + ReuseAddr => 1 + ); + $hash->{STATE} = "Initialized"; + $hash->{SERVERSOCKET} = $hash->{IPV6} ? + IO::Socket::INET6->new(@opts) : + IO::Socket::INET->new(@opts); + + if(!$hash->{SERVERSOCKET}) { + return "$name: Can't open server port at $port: $!"; + } + + $hash->{FD} = $hash->{SERVERSOCKET}->fileno(); + $hash->{PORT} = $port; + + $selectlist{"$name.$port"} = $hash; + Log(3, "$name: port $port opened"); + return undef; +} + +sub +TcpServer_Accept($$) +{ + my ($hash, $type) = @_; + + my $name = $hash->{NAME}; + my $ll = GetLogLevel($name,4); + my @clientinfo = $hash->{SERVERSOCKET}->accept(); + if(!@clientinfo) { + Log 1, "Accept failed ($name: $!)"; + return undef; + } + $hash->{CONNECTS}++; + + my ($port, $iaddr) = $hash->{IPV6} ? + sockaddr_in6($clientinfo[1]) : + sockaddr_in($clientinfo[1]); + my $caddr = $hash->{IPV6} ? + inet_ntop(AF_INET6(), $iaddr) : + inet_ntoa($iaddr); + + my $af = $attr{$name}{allowfrom}; + if($af) { + if($caddr !~ m/$af/) { + my $hostname = gethostbyaddr($iaddr, AF_INET); + if(!$hostname || $hostname !~ m/$af/) { + Log 1, "Connection refused from $caddr:$port"; + close($clientinfo[0]); + return undef; + } + } + } + + if($hash->{SSL}) { + # Certs directory must be in the modpath, i.e. at the same level as the + # FHEM directory + my $mp = AttrVal("global", "modpath", "."); + my $ret = IO::Socket::SSL->start_SSL($clientinfo[0], { + SSL_server => 1, + SSL_key_file => "$mp/certs/server-key.pem", + SSL_cert_file => "$mp/certs/server-cert.pem", + }); + if(!$ret && $! ne "Socket is not connected") { + Log 1, "$type SSL/HTTPS error: $!"; + close($clientinfo[0]); + return undef; + } + } + + my $cname = "$type:$caddr:$port"; + my %nhash; + $nhash{NR} = $devcount++; + $nhash{NAME} = $cname; + $nhash{FD} = $clientinfo[0]->fileno(); + $nhash{CD} = $clientinfo[0]; # sysread / close won't work on fileno + $nhash{TYPE} = $type; + $nhash{STATE} = "Connected"; + $nhash{SNAME} = $name; + $nhash{TEMPORARY} = 1; # Don't want to save it + $nhash{BUF} = ""; + $attr{$cname}{room} = "hidden"; + $defs{$cname} = \%nhash; + $selectlist{$nhash{NAME}} = \%nhash; + + + Log($ll, "Connection accepted from $nhash{NAME}"); + return \%nhash; +} + +sub +TcpServer_SetSSL($) +{ + my ($hash) = @_; + eval "require IO::Socket::SSL"; + if($@) { + Log 1, $@; + Log 1, "Can't load IO::Socket::SSL, falling back to HTTP"; + } else { + $hash->{SSL} = 1; + } +} + + +sub +TcpServer_Close($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + + if(defined($hash->{CD})) { # Clients + close($hash->{CD}); + delete($selectlist{$name}); + } + if(defined($hash->{SERVERSOCKET})) { # Server + close($hash->{SERVERSOCKET}); + $name = $name . "." . $hash->{PORT}; + delete($selectlist{$name}); + } + return undef; +} +1; diff --git a/fhem/TODO b/fhem/TODO index 1a0cf76fc..85d9e1a18 100644 --- a/fhem/TODO +++ b/fhem/TODO @@ -1,8 +1,6 @@ FHEM: -- FHEMWEB warning - finish updatefhem - autoload commands -> rename updatefhem, CULflash, etc - - FHEM2FHEM reconnect - HomeMatic set log 2 - implement wiki decisions diff --git a/fhem/docs/commandref.html b/fhem/docs/commandref.html index bab664c06..586d26a93 100644 --- a/fhem/docs/commandref.html +++ b/fhem/docs/commandref.html @@ -28,7 +28,6 @@
fhem commands