############################################## # $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->{AsyncOutputFn} = "telnet_Output"; $hash->{UndefFn} = "telnet_Undef"; $hash->{AttrFn} = "telnet_Attr"; no warnings 'qw'; my @attrList = qw( SSL allowedCommands allowfrom connectInterval connectTimeout encoding:utf8,latin1 globalpassword password prompt sslCertPrefix sslVersion ); use warnings 'qw'; $hash->{AttrList} = join(" ", @attrList); $hash->{ActivateInformFn} = "telnet_ActivateInform"; $hash->{CanAuthenticate} = 2; $cmds{encoding} = { Fn=>"CommandTelnetEncoding", ClientFilter => "telnet", Hlp=>"[utf8|latin1],query and set the character encoding ". "for the current telnet session" }; $cmds{inform} = { Fn=>"CommandTelnetInform", ClientFilter => "telnet", Hlp=>"{on|onWithState|off|log|raw|timer|status},". "echo all events to this client" }; } 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_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 = (defined($port) && $port =~ m/^\d+$/); my $isClient = ($port && $port =~ m/^(.+):\d+$/); return "Usage: define telnet { [IPV6:] [global] | ". " [IPV6:]serverName:port }" if(!($isServer || $isClient) || ($isClient && $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_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->{canAsyncOutput} = 1; $chash->{encoding} = AttrVal($name, "encoding", "utf8"); $chash->{prompt} = AttrVal($name, "prompt", AttrVal('global','title','fhem')); if($chash->{prompt} =~ m/^{.*}$/s) { $chash->{prompt} = eval $chash->{prompt}; $chash->{prompt} =~ s/\n//; } $chash->{prompt} .= '>'; # Not really nice, but dont know better. syswrite($chash->{CD}, sprintf("%c%c%c", 255, 253, 0) ) if( AttrVal($name, "encoding", "") ); #DO BINARY $chash->{CD}->flush(); my $auth = Authenticate($chash, undef); syswrite($chash->{CD}, sprintf("%c%c%cPassword: ", 255, 251, 1)) # WILL ECHO if($auth); $chash->{Authenticated} = 0 if(!$auth); return; } my $buf; my $ret = sysread($hash->{CD}, $buf, 256); if(!defined($ret) || $ret <= 0) { if($hash->{isClient}) { telnet_ClientDisconnect($hash, 0); } else { delete $hash->{canAsyncOutput}; CommandDelete(undef, $name); } return; } if(ord($buf) == 4) { # EOT / ^D CommandQuit($hash, ""); return; } $buf =~ s/\r//g; my $sname = ($hash->{isClient} ? $name : $hash->{SNAME}); if(!defined($hash->{Authenticated}) || $hash->{Authenticated}) { $buf =~ s/\xff..//g; # Telnet IAC stuff if($buf =~ m/\xfd./) { # Telnet Do ? Wont / ^C handling $buf =~ s/\xfd(.)//; syswrite($hash->{CD}, sprintf("%c%c%c", 0xff, 0xfc, ord($1))) } } $buf = Encode::decode('UTF-8', $buf) if($unicodeEncoding); $hash->{BUF} .= $buf; my @ret; my $gotCmd; while($hash->{BUF} =~ m/\n/) { my ($cmd, $rest) = split("\n", $hash->{BUF}, 2); $hash->{BUF} = $rest; if(!defined($hash->{Authenticated})) { syswrite($hash->{CD}, sprintf("%c%c%c\r\n", 255, 252, 1)); # WONT ECHO if(Authenticate($hash, $cmd) != 2) { $hash->{Authenticated} = 1; next; } else { if($hash->{isClient}) { telnet_ClientDisconnect($hash, 0); } else { delete($hash->{rcvdQuit}); CommandDelete(undef, $name); } return; } } $gotCmd = 1; if($cmd || $hash->{prevlines}) { if($cmd =~ m/\\\s*$/) { # Multi-line $cmd =~ s/\\\s*$//; $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 = AttrVal("global", "motd", ""); my $gie = $defs{global}{init_errors}; if($motd ne "none" && ($motd || $gie)) { push @ret, "$motd\n$gie"; } $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}); $ret =~ s/\n/\r\n/g if($hash->{Authenticated}); # only for DOS telnet telnet_Output($hash, $ret, 1); if($hash->{rcvdQuit}) { if($hash->{isClient}) { delete($hash->{rcvdQuit}); telnet_ClientDisconnect($hash, 0); } else { CommandDelete(undef, $name); } } } sub telnet_Output($$$) { my ($hash,$ret,$nonl) = @_; if($ret && defined($hash->{CD})) { $ret = utf8ToLatin1($ret) if( $hash->{encoding} eq "latin1" ); if(!$nonl) { # AsyncOutput stuff $ret = "\n$ret\n$hash->{prompt} " if( $hash->{showPrompt}); $ret = "$ret\n" if(!$hash->{showPrompt}); } for(;;) { $ret = Encode::encode('UTF-8', $ret) if($unicodeEncoding || utf8::is_utf8($ret) && $ret =~ m/[^\x00-\xFF]/); my $l = syswrite($hash->{CD}, $ret); last if(!$l || $l == length($ret)); $ret = substr($ret, $l); } $hash->{CD}->flush(); } return undef; } ########################## sub telnet_Attr(@) { my ($type, $devName, $attrName, @param) = @_; my @a = @_; my $hash = $defs{$devName}; if($type eq "set" && $attrName eq "SSL") { InternalTimer(1, sub($) { # Wait for sslCertPrefix my ($hash) = @_; TcpServer_SetSSL($hash); if($hash->{CD}) { my $ret = IO::Socket::SSL->start_SSL($hash->{CD}); Log3 $devName, 1, "$hash->{NAME} start_SSL: $ret" if($ret); } }, $hash, 0); # Wait for sslCertPrefix } if(($attrName eq "allowedCommands" || $attrName eq "password" || $attrName eq "globalpassword" ) && $type eq "set") { my $aName = "allowed_$devName"; my $exists = ($defs{$aName} ? 1 : 0); AnalyzeCommand(undef, "defmod $aName allowed"); AnalyzeCommand(undef, "attr $aName validFor $devName"); AnalyzeCommand(undef, "attr $aName $attrName ".join(" ",@param)); return "$devName: ".($exists ? "modifying":"creating"). " device $aName for attribute $attrName"; } return undef; } sub telnet_Undef($$) { my ($hash, $arg) = @_; delete($logInform{$hash->{NAME}}); delete($inform{$hash->{NAME}}); return TcpServer_Close($hash); } ##################################### sub CommandTelnetInform($$) { my ($cl, $param) = @_; return if(!$cl); my $name = $cl->{NAME}; return "Usage: inform {on|onWithState|off|raw|timer|log|status} [regexp]" if($param !~ m/^(on|onWithState|off|raw|timer|log|status)/); if($param eq "status") { my $i = $inform{$name}; return $i ? ($i->{type} . ($i->{regexp} ? " ".$i->{regexp} : "")) : "off"; } if($param eq "off") { delete($logInform{$name}); delete($inform{$name}); } elsif($param eq "log") { $logInform{$name} = sub($$){ my ($me, $msg) = @_; # _NO_ Log3 here! telnet_Output($defs{$me}, $msg."\n", 1); } } elsif($param ne "off") { my ($type, $regexp) = split(" ", $param); $inform{$name}{NR} = $cl->{NR}; $inform{$name}{type} = $type; if($regexp) { eval { "Hallo" =~ m/$regexp/ }; return "Bad regexp: $@" if($@); $inform{$name}{regexp} = $regexp; } Log 4, "Setting inform to $param"; } return undef; } sub telnet_ActivateInform($) { my ($cl) = @_; CommandTelnetInform($cl, "log"); } 1; =pod =item helper =item summary telnet server for FHEM =item summary_DE FHEM telnet Server =begin html

telnet

=end html =begin html_DE

telnet

=end html_DE =cut 1;