############################################## # $Id$ # 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} = "globalpassword password prompt ". "allowfrom SSL connectTimeout connectInterval ". "encoding:utf8,latin1 sslVersion"; $hash->{ActivateInformFn} = "telnet_ActivateInform"; my %lhash = ( Fn=>"CommandTelnetEncoding", ClientFilter => "telnet", Hlp=>"[utf8|latin1],query and set the character encoding for the current telnet session" ); $cmds{encoding} = \%lhash; } sub CommandTelnetEncoding($$) { my ($hash, $param) = @_; my $ret = ""; if( !$param ) { $ret = "current encoding is $hash->{encoding}"; } elsif( $param eq "utf8" || $param eq "latin1" ) { $hash->{encoding} = $param; syswrite($hash->{CD}, sprintf("%c%c%c", 255, 253, 0) ); $ret = "encoding changed to $param"; } else { $ret = "unknown encoding >>$param<<"; } return $ret; } ##################################### 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_ClientConnect($) { my ($hash) = @_; my $name = $hash->{NAME}; $hash->{DEF} =~ m/^(IPV6:)?(.*):(\d+)$/; my ($isIPv6, $server, $port) = ($1, $2, $3); Log3 $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); Log3 $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) { Log3 $name, 4, "$name: Connect failed."; } else { Log3 $name, 3, "$name: Disconnected"; } } ########################## sub telnet_Define($$$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); my ($name, $type, $pport, $global) = split("[ \t]+", $def); my $port = $pport; $port =~ s/^IPV6://; my $isServer = 1 if(defined($port) && $port =~ m/^\d+$/); my $isClient = 1 if($port && $port =~ m/^(.+):\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($isServer) { my $ret = TcpServer_Open($hash, $pport, $global); if($ret && !$init_done) { Log3 $name, 1, "$ret. Exiting."; exit(1); } return $ret; } if($isClient) { $hash->{isClient} = 1; telnet_ClientConnect($hash); } } 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); $chash->{encoding} = AttrVal($name, "encoding", "utf8"); $chash->{prompt} = AttrVal($name, "prompt", "fhem>"); syswrite($chash->{CD}, sprintf("%c%c%c", 255, 253, 0) ) if( AttrVal($name, "encoding", "") ); #DO BINARY $chash->{CD}->flush(); 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) { 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 $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 ? 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; Log3 $name, 1, "password expression: $@" if($@); } if($ret) { $hash->{pwEntered} = 1; next; } else { if($hash->{isClient}) { telnet_ClientDisconnect($hash, 0); } else { delete($hash->{rcvdQuit}); 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}); } $cmd = latin1ToUtf8($cmd) if( $hash->{encoding} eq "latin1" ); $ret = AnalyzeCommandChain($hash, $cmd); push @ret, $ret if(defined($ret)); } } else { $hash->{showPrompt} = 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} ? "> " : $hash->{prompt}." ") if($gotCmd && $hash->{showPrompt} && !$hash->{rcvdQuit}); if($ret) { $ret = utf8ToLatin1($ret) if( $hash->{encoding} eq "latin1" ); $ret =~ s/\n/\r\n/g if($pw); # only for DOS telnet for(;;) { my $l = syswrite($hash->{CD}, $ret); 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); } } } ########################## sub telnet_Attr(@) { my @a = @_; my $hash = $defs{$a[1]}; if($a[0] eq "set" && $a[2] eq "SSL") { TcpServer_SetSSL($hash); if($hash->{CD}) { my $ret = IO::Socket::SSL->start_SSL($hash->{CD}); Log3 $a[1], 1, "$hash->{NAME} start_SSL: $ret" if($ret); } } return undef; } sub telnet_Undef($$) { my ($hash, $arg) = @_; return TcpServer_Close($hash); } sub telnet_ActivateInform($;$) { my ($cl, $arg) = @_; my $name = $cl->{NAME}; $arg = "" if(!defined($arg)); CommandInform($cl, "timer $arg") if(!$inform{$name}); } 1; =pod =begin html

telnet

=end html =begin html_DE

telnet

=end html_DE =cut 1;