2
0
mirror of https://github.com/fhem/fhem-mirror.git synced 2025-02-11 13:12:00 +00:00
fhem-mirror/fhem/FHEM/TcpServerUtils.pm
2022-03-21 09:01:16 +00:00

633 lines
17 KiB
Perl

##############################################
# $Id$
package main;
use strict;
use warnings;
use IO::Socket;
use vars qw($SSL_ERROR);
my ($joinGroup, $leaveGroup, $multiCastLoop, $addMembership, $dropMembership);
# Perl 5.16 / wheezy compatibility mode / #126290
eval "Socket::IPV6_JOIN_GROUP";
if($@) {
$joinGroup = 20;
$leaveGroup = 21;
$multiCastLoop = 34;
$addMembership = 35;
$dropMembership = 36;
} else {
$joinGroup = eval "Socket::IPV6_JOIN_GROUP";
$leaveGroup = eval "Socket::IPV6_LEAVE_GROUP";
$multiCastLoop = eval "Socket::IP_MULTICAST_LOOP";
$addMembership = eval "Socket::IP_ADD_MEMBERSHIP";
$dropMembership = eval "Socket::IP_DROP_MEMBERSHIP";
}
sub
TcpServer_Open($$$;$)
{
my ($hash, $port, $global, $multicast) = @_;
my $name = $hash->{NAME};
return 'multicast not supported without SO_REUSEPORT'
if($multicast && !defined(&SO_REUSEPORT));
if($port =~ m/^IPV6:(\d+)$/i) {
$port = $1;
eval "require IO::Socket::INET6; use Socket6;";
if($@) {
Log3 $hash, 1, $@;
Log3 $hash, 1, "$name: Can't load INET6, falling back to IPV4";
} else {
$hash->{IPV6} = 1;
}
}
my $lh = ($global ? ($global eq "global"? undef : $global) :
($hash->{IPV6} ? "::1" : "127.0.0.1"));
my %opts = (
Domain => ($hash->{IPV6} ? AF_INET6() : AF_UNSPEC), # Linux bug / #126448
LocalHost => $lh,
LocalPort => $port,
Listen => 32, # For Windows
Blocking => ($^O =~ /Win/ ? 1 : 0), # Needed for .WRITEBUFFER@darwin
ReuseAddr => 1
);
if($multicast) {
$opts{ReusePort} = 1;
$opts{Proto} = 'udp';
delete($opts{Listen});
$hash->{MULTICAST} = $multicast;
}
readingsSingleUpdate($hash, "state", "Initialized", 0);
$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} = $hash->{SERVERSOCKET}->sockport();
$selectlist{"$name.$port"} = $hash;
Log3 $hash, 3, "$name: port ". $hash->{PORT} ." opened";
return undef;
}
sub
TcpServer_SetLoopbackMode($$)
{
my ($hash, $loopback) = @_;
my $name = $hash->{NAME};
my $sock = $hash->{SERVERSOCKET};
my $old;
if( !$hash->{IPV6} && $sock->sockdomain() == AF_INET ) {
my $packed = getsockopt($sock, Socket::IPPROTO_IP, $multiCastLoop);
if( !$packed ) {
Log3 $name, 1, "$name: failed to get loopback mode: $!";
return undef;
}
$old = unpack("I", $packed);
if( !setsockopt($sock, Socket::IPPROTO_IP,
$multiCastLoop, pack("I", $loopback ) ) ) {
Log3 $name, 1, "$name: could not set loopback mode: $!";
return undef;
}
} elsif( !$hash->{IPV6} && $sock->sockdomain() == AF_INET6 ) {
my $packed = getsockopt($sock, Socket::IPPROTO_IPV6,
Socket::IPV6_MULTICAST_LOOP);
if( !$packed ) {
Log3 $name, 1, "$name: failed to get loopback mode: $!";
return undef;
}
$old = unpack("I", $packed);
if( setsockopt($sock, Socket::IPPROTO_IPV6,
Socket::IPV6_MULTICAST_LOOP, pack("I", $loopback ) ) ) {
Log3 $name, 1, "$name: could not set loopback mode: $!";
return undef;
}
} else {
Log3 $name, 1,
"$name: TcpServer_SetLoopbackMode failed: unsupported socket family";
return undef;
}
return $old;
}
sub
TcpServer_MCastAdd($$)
{
my ($hash, $addr) = @_;
my $name = $hash->{NAME};
my $sock = $hash->{SERVERSOCKET};
$hash->{ADDR} = $addr;
# disable loopback
TcpServer_SetLoopbackMode($hash, 0);
# add multicast address
if(!$hash->{IPV6} && $sock->sockdomain() == AF_INET) {
my $ip_mreq = Socket::pack_ip_mreq( inet_aton( $addr ), INADDR_ANY );
setsockopt($sock, Socket::IPPROTO_IP, $addMembership, $ip_mreq )
or return "$name: could not set IP_ADD_MEMBERSHIP socket option: $!";
} elsif($hash->{IPV6} && $sock->sockdomain() == AF_INET6) {
my $ipv6_mreq = Socket::pack_ipv6_mreq( inet_pton( AF_INET6, $addr ), 0 );
setsockopt($sock, Socket::IPPROTO_IPV6, $joinGroup, $ipv6_mreq )
or return "$name: could not set IPV6_JOIN_GROUP socket option: $!";
} else {
return("$name: TcpServer_MCastAdd failed: unsupported socket family" );
}
readingsSingleUpdate($hash, "state", "Multicast listen", 0);
return undef;
}
sub
TcpServer_MCastRecv($$$;$)
{
my ($hash, undef, $length, $flags) = @_;
my $name = $hash->{NAME};
my $sock = $hash->{SERVERSOCKET};
my $sockaddr = $sock->recv($_[1], $length, $flags);
if(!$hash->{IPV6} && $sock->sockdomain() == AF_INET) {
my ($peer_port, $addr) = Socket::unpack_sockaddr_in($sockaddr);
my $peer_host = inet_ntoa($addr);
return $peer_host, $peer_port;
} elsif($hash->{IPV6} && $sock->sockdomain() == AF_INET6) {
my ($peer_port, $addr) = Socket::unpack_sockaddr_in6($sockaddr);
my $peer_host = inet_ntop(AF_INET6(),$addr);
return $peer_host, $peer_port;
} else {
Log3 $name, 1,"$name: TcpServer_MCastRecv failed: unsupported socket family";
return undef;
}
}
sub
TcpServer_MCastSend($$;$$)
{
my ($hash, $data, $addr, $port) = @_;
my $name = $hash->{NAME};
my $sock = $hash->{SERVERSOCKET};
$addr = $hash->{ADDR} if( !$addr );
$port = $hash->{PORT} if( !$port );
if( !$addr ) {
Log3 $name, 1, "$name: TcpServer_MCastSend failed: address unknown";
return undef;
}
if( !$port ) {
Log3 $name, 1, "$name: TcpServer_MCastSend failed: port unknown";
return undef;
}
if(!$hash->{IPV6} && $sock->sockdomain() == AF_INET) {
my $sockaddr = Socket::pack_sockaddr_in($port, inet_aton($addr));
return $sock->send($data,0,$sockaddr);
} elsif($hash->{IPV6} && $sock->sockdomain() == AF_INET6) {
my $sockaddr = Socket::pack_sockaddr_in6($port, inet_pton($addr));
return $sock->send($data,0,$sockaddr);
} else {
Log3 $name, 1,"$name: TcpServer_MCastSend failed: unsupported socket family";
return undef;
}
}
sub
TcpServer_MCastRemove($$)
{
my ($hash, $addr) = @_;
my $name = $hash->{NAME};
my $sock = $hash->{SERVERSOCKET};
delete $hash->{ADDR};
if(!$hash->{IPV6} && $sock->sockdomain() == AF_INET) {
my $ip_mreq = Socket::pack_ip_mreq( inet_aton( $addr ), INADDR_ANY );
setsockopt($sock, Socket::IPPROTO_IP, $dropMembership, $ip_mreq )
or return "$name: could not set IP_DROP_MEMBERSHIP socket option: $!";
} elsif($hash->{IPV6} && $sock->sockdomain() == AF_INET6) {
my $ipv6_mreq = Socket::pack_ipv6_mreq( inet_pton( AF_INET6, $addr ), 0 );
setsockopt($sock, Socket::IPPROTO_IPV6, $leaveGroup, $ipv6_mreq)
or return "$name: could not set IPV6_LEAVE_GROUP socket option: $!";
} else {
return("$name: TcpServer_MCastRemove failed: unsupported socket family" );
}
readingsSingleUpdate($hash, "state", "Multicast listen stopped", 0);
return undef;
}
sub
TcpServer_Accept($$)
{
my ($hash, $type) = @_;
my $name = $hash->{NAME};
if($hash->{MULTICAST}) {
Log3 $name, 1, "$name: can't accept on a mutlicast socket";
return undef;
}
my @clientinfo = $hash->{SERVERSOCKET}->accept();
if(!@clientinfo) {
Log3 $name, 1, "Accept failed ($name: $!)" if($! != EAGAIN);
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) {
my $re ="^(::ffff:)?(127|192.168|172.(1[6-9]|2[0-9]|3[01])|10|169.254)\\.|".
"^(f[cde]|::1)";
if($caddr !~ m/$re/) {
my %empty;
$hash->{SNAME} = $hash->{NAME};
my $auth = Authenticate($hash, \%empty);
delete $hash->{SNAME};
if($auth == 0) {
Log3 $name, 1,
"Connection refused from the non-local address $caddr:$port, ".
"as there is no working allowed instance defined for it";
close($clientinfo[0]);
return undef;
}
}
}
if($af) {
if($caddr !~ m/$af/) {
my $hostname = gethostbyaddr($iaddr, AF_INET);
if(!$hostname || $hostname !~ m/$af/) {
Log3 $name, 1, "Connection refused from $caddr:$port";
close($clientinfo[0]);
return undef;
}
}
}
#$clientinfo[0]->blocking(0); # Forum #24799
if($hash->{SSL}) {
# Forum #27565: SSLv23:!SSLv3:!SSLv2', #35004: TLSv12:!SSLv3
my $sslVersion = AttrVal($hash->{NAME}, "sslVersion",
AttrVal("global", "sslVersion", undef));
# Certs directory must be in the modpath, i.e. at the same level as the
# FHEM directory
my $mp = AttrVal("global", "modpath", ".");
my $certPrefix = AttrVal($name, "sslCertPrefix", "certs/server-");
my $ret;
eval {
$ret = IO::Socket::SSL->start_SSL($clientinfo[0], {
SSL_server => 1,
SSL_key_file => "$mp/${certPrefix}key.pem",
SSL_cert_file => "$mp/${certPrefix}cert.pem",
SSL_version => $sslVersion,
SSL_cipher_list => 'HIGH:!RC4:!eNULL:!aNULL',
Timeout => 4,
});
$! = EINVAL if(!$clientinfo[0]->blocking() && $!==EWOULDBLOCK);
};
my $err = $!;
if( !$ret
&& $err != EWOULDBLOCK
&& $err ne "Socket is not connected") {
$err = "" if(!$err);
$err .= " ".($SSL_ERROR ? $SSL_ERROR : IO::Socket::SSL::errstr());
my $errLevel = ($err =~ m/error:14094416:SSL/ ? 5 : 1); # 61511
Log3 $name, $errLevel, "$type SSL/HTTPS error: $err (peer: $caddr)"
if($err !~ m/error:00000000:lib.0.:func.0.:reason.0./); #Forum 56364
close($clientinfo[0]);
return undef;
}
}
my $cname = "${name}_${caddr}_${port}";
my %nhash;
$nhash{NR} = $devcount++;
$nhash{NAME} = $cname;
$nhash{PEER} = $caddr;
$nhash{PORT} = $port;
$nhash{FD} = $clientinfo[0]->fileno();
$nhash{CD} = $clientinfo[0]; # sysread / close won't work on fileno
$nhash{TYPE} = $type;
$nhash{SSL} = $hash->{SSL};
readingsSingleUpdate(\%nhash, "state", "Connected", 0);
$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;
my $ret = $clientinfo[0]->setsockopt(SOL_SOCKET, SO_KEEPALIVE, 1);
Log3 $name, 4, "Connection accepted from $nhash{NAME}";
return \%nhash;
}
sub
TcpServer_SetSSL($)
{
my ($hash) = @_;
eval "require IO::Socket::SSL";
if($@) {
Log3 $hash, 1, $@;
Log3 $hash, 1, "Can't load IO::Socket::SSL, falling back to HTTP";
return;
}
my $name = $hash->{NAME};
my $cp = AttrVal("global", "modpath", ".")."/".
AttrVal($name, "sslCertPrefix", "certs/server-");
if(! -r "${cp}key.pem") {
Log 1, "$name: Server certificate missing, trying to create one";
if($cp =~ m,^(.*)/(.*?), && ! -d $1 && !mkdir($1)) {
Log 1, "$name: failed to create $1: $!, falling back to HTTP";
return;
}
if(!open(FH,">certreq.txt")) {
Log 1, "$name: failed to create certreq.txt: $!, falling back to HTTP";
return;
}
my $hostname = `hostname`;
chomp($hostname);
print FH << "EOF";
[ req ]
prompt = no
distinguished_name = dn
x509_extensions = ext
[ dn ]
CN = $hostname
O = FHEM
OU = localhost
[ ext ]
basicConstraints=CA:TRUE
extendedKeyUsage = serverAuth
subjectAltName=\@san
[san]
DNS.1=localhost
DNS.2=$hostname
IP.1=127.0.0.1
IP.2=::1
EOF
close(FH);
my $cmd = "openssl req -new -x509 -days 3650 -nodes -newkey rsa:2048 ".
"-config certreq.txt -out ${cp}cert.pem -keyout ${cp}key.pem";
Log 1, "Executing $cmd";
`$cmd`;
unlink("certreq.txt");
}
$hash->{SSL} = 1;
}
sub
TcpServer_Close($@)
{
my ($hash, $dodel, $ignoreNtfy) = @_;
my $name = $hash->{NAME};
if(defined($hash->{CD})) { # Clients
close($hash->{CD});
delete($hash->{CD});
delete($selectlist{$name});
delete($hash->{FD}); # Avoid Read->Close->Write
removeFromNtfyHash($name) if(!$ignoreNtfy); # can be expensive
}
if(defined($hash->{SERVERSOCKET})) { # Server
close($hash->{SERVERSOCKET});
$name = $name . "." . $hash->{PORT};
delete($selectlist{$name});
delete($hash->{FD}); # Avoid Read->Close->Write
}
if($dodel) {
delete $attr{$name};
delete $defs{$name};
} else {
$hash->{stacktrace} = stacktraceAsString(1);
}
return undef;
}
# close a (SSL-)Socket in local process
# avoids interfering with other processes using it
# this is critical for SSL and helps with other issues, too
sub
TcpServer_Disown($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
if( defined($hash->{CD}) ){
if( $hash->{SSL} ){
$hash->{CD}->close( SSL_no_shutdown => 1);
} else {
close( $hash->{CD} );
}
delete($hash->{CD});
delete($selectlist{$name});
delete($hash->{FD}); # Avoid Read->Close->Write
$hash->{stacktrace} = stacktraceAsString(1);
}
return;
}
# wait for a socket to become ready
# takes IO::Socket::SSL + non-blocking into account
sub
TcpServer_Wait($$)
{
my( $hash, $direction ) = @_;
my $read = '';
my $write ='';
if( $direction eq 'read' || $hash->{wantRead} ){
vec( $read, $hash->{FD}, 1) = 1;
} elsif( $direction eq 'write' || $hash->{wantWrite} ){
vec( $write, $hash->{FD}, 1) = 1;
} else {
return undef;
}
my $ret = select( $read, $write, undef, undef );
return if $ret == -1;
if( vec( $read, $hash->{FD}, 1) ){
delete $hash->{wantRead};
}
if( vec( $write, $hash->{FD}, 1) ){
delete $hash->{wantWrite};
}
# return true on success
return 1;
}
# WantRead/Write: keep ssl constants local
sub
TcpServer_WantRead($)
{
my( $hash ) = @_;
return $hash->{SSL}
&& $hash->{CD}
&& $hash->{CD}->errstr == &IO::Socket::SSL::SSL_WANT_READ;
}
sub
TcpServer_WantWrite($)
{
my( $hash ) = @_;
return $hash->{SSL}
&& $hash->{CD}
&& $hash->{CD}->errstr == &IO::Socket::SSL::SSL_WANT_WRITE;
}
# write until all data is done.
# hanldes both, blocking and non-blocking sockets
# ... with or without SSL
sub
TcpServer_WriteBlocking($$)
{
my( $hash, $txt ) = @_;
if($hash->{WriteFn}) { # FWTP needs it
no strict "refs";
return &{$hash->{WriteFn}}($hash, \$txt);
use strict "refs";
}
my $sock = $hash->{CD};
return undef if(!$sock);
my $off = 0;
my $len = length($txt);
while($off < $len) {
if(!TcpServer_Wait($hash, 'write')) {
TcpServer_Close($hash);
return undef;
}
my $ret;
eval { $ret = syswrite($sock, $txt, $len-$off, $off); }; # Wide character
if($@) {
Log 1, $@;
Log 1, "txt:".join(":",unpack("C*",$txt)).",len:$len,off:$off";
stacktrace();
}
if( defined $ret ){
$off += $ret;
my $sh = $defs{$hash->{SNAME}};
$sh->{BYTES_WRITTEN} += $ret if(defined($sh->{BYTES_WRITTEN}));
} elsif( $! == EWOULDBLOCK ){
$hash->{wantRead} = 1
if TcpServer_WantRead($hash);
} else {
TcpServer_Close($hash);
return undef; # error
}
}
return 1; # success
}
=pod
Multicast:
(https://forum.fhem.de/index.php/topic,126290.msg1209591.html#msg1209591)
verwendet wird es so:
- das socket mit gesetztem optionalen vierten paramter von TcpServer_Open
erzeugen: d.h. im define oder sonst wo mit my $ret = TcpServer_Open($hash,
'5353', '0.0.0.0', 1); initialisieren. statt der 0.0.0.0 kann man auch die
multicast adresse angeben, ich bin mir nicht sicher was richtiger ist und ob
es einen unterschied macht. funktionieren tut bei mir beides.
- TcpServer_Open schaltet automatisch den loopback mode aus. d.h. man empfängt
seine eigenen daten nicht. ich vermute das ist der normalfall. mit
TcpServer_SetLoopbackMode($hash,[0|1]); kann man das ändern wenn man es
braucht.
- mit TcpServer_MCastAdd die multicast adresse zum socket explizit hinzufügen:
z.b.: TcpServer_MCastAdd($hash,'224.0.0.251'); erst ab jetzt bekommt man auch
tatsächlich daten.
- im (device) hash wird für jedes udp packet die ReadFn aufgerufen. dort kann
man mit TcpServer_MCastRecv dann die empfangenen daten abholen:
my($peer_host, $peer_port) = TcpServer_MCastRecv($hash,$data,$length);
alternativ kann man auch die low leven routinen direkt aufrufen:
- my $sockaddr = $hash->{SERVERSOCKET}->recv($data, 4096);
der empfang geht über SERVERSOCKET, nicht wie bei tcp über CD, es wird auch
kein accept verwendet
- aus $sockaddr kann man sich die gegenstelle zum udp packet holen falls man
die braucht:
my ($peer_port, $addr) = Socket::sockaddr_in($sockaddr);
my $peer_host = inet_ntoa($addr);
- eigene daten sendet man mit TcpServer_MCastSend:
TcpServer_MCastSend($hash,$data[,$host[,$port]]); hier bei wird der port aus
dem TcpServer_Open und die adresse aus TcpServer_MCastAdd verwendet.
alternativ kann man beides auch beim aufruf mitgeben.
- mit TcpServer_MCastRemove kann man die multicast adresse auch wieder vom
socket entfernen:
z.b.: TcpServer_MCastRemove($hash,'224.0.0.251');
das socket empfängt dann keine daten für diese adresse mehr.
- mit wechselweisem TcpServer_MCastAdd und TcpServer_MCastRemove kann man auch
zeitweise zwischen empfangen und ignorieren hin und her wechseln. z.b. als
reaktion auf disable.
- mit TcpServer_Close($hash); das ganze am ende z.b. in der UndefFn wieder zu
machen.
=cut
1;