TcpServerUtils.pm 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. ##############################################
  2. # $Id: TcpServerUtils.pm 17529 2018-10-14 12:57:06Z rudolfkoenig $
  3. package main;
  4. use strict;
  5. use warnings;
  6. use IO::Socket;
  7. use vars qw($SSL_ERROR);
  8. sub
  9. TcpServer_Open($$$)
  10. {
  11. my ($hash, $port, $global) = @_;
  12. my $name = $hash->{NAME};
  13. if($port =~ m/^IPV6:(\d+)$/i) {
  14. $port = $1;
  15. eval "require IO::Socket::INET6; use Socket6;";
  16. if($@) {
  17. Log3 $hash, 1, $@;
  18. Log3 $hash, 1, "$name: Can't load INET6, falling back to IPV4";
  19. } else {
  20. $hash->{IPV6} = 1;
  21. }
  22. }
  23. my $lh = ($global ? ($global eq "global"? undef : $global) :
  24. ($hash->{IPV6} ? "::1" : "127.0.0.1"));
  25. my @opts = (
  26. Domain => ($hash->{IPV6} ? AF_INET6() : AF_UNSPEC), # Linux bug
  27. LocalHost => $lh,
  28. LocalPort => $port,
  29. Listen => 32, # For Windows
  30. Blocking => ($^O =~ /Win/ ? 1 : 0), # Needed for .WRITEBUFFER@darwin
  31. ReuseAddr => 1
  32. );
  33. readingsSingleUpdate($hash, "state", "Initialized", 0);
  34. $hash->{SERVERSOCKET} = $hash->{IPV6} ?
  35. IO::Socket::INET6->new(@opts) :
  36. IO::Socket::INET->new(@opts);
  37. if(!$hash->{SERVERSOCKET}) {
  38. return "$name: Can't open server port at $port: $!";
  39. }
  40. $hash->{FD} = $hash->{SERVERSOCKET}->fileno();
  41. $hash->{PORT} = $hash->{SERVERSOCKET}->sockport();
  42. $selectlist{"$name.$port"} = $hash;
  43. Log3 $hash, 3, "$name: port ". $hash->{PORT} ." opened";
  44. return undef;
  45. }
  46. sub
  47. TcpServer_Accept($$)
  48. {
  49. my ($hash, $type) = @_;
  50. my $name = $hash->{NAME};
  51. my @clientinfo = $hash->{SERVERSOCKET}->accept();
  52. if(!@clientinfo) {
  53. Log3 $name, 1, "Accept failed ($name: $!)" if($! != EAGAIN);
  54. return undef;
  55. }
  56. $hash->{CONNECTS}++;
  57. my ($port, $iaddr) = $hash->{IPV6} ?
  58. sockaddr_in6($clientinfo[1]) :
  59. sockaddr_in($clientinfo[1]);
  60. my $caddr = $hash->{IPV6} ?
  61. inet_ntop(AF_INET6(), $iaddr) :
  62. inet_ntoa($iaddr);
  63. my $af = $attr{$name}{allowfrom};
  64. if(!$af) {
  65. my $re ="^(::ffff:)?(127|192.168|172.(1[6-9]|2[0-9]|3[01])|10|169.254)\\.|".
  66. "^(f[cde]|::1)";
  67. if($caddr !~ m/$re/) {
  68. my %empty;
  69. $hash->{SNAME} = $hash->{NAME};
  70. my $auth = Authenticate($hash, \%empty);
  71. delete $hash->{SNAME};
  72. if($auth == 0) {
  73. Log3 $name, 1,
  74. "Connection refused from the non-local address $caddr:$port, ".
  75. "as there is no working allowed instance defined for it";
  76. close($clientinfo[0]);
  77. return undef;
  78. }
  79. }
  80. }
  81. if($af) {
  82. if($caddr !~ m/$af/) {
  83. my $hostname = gethostbyaddr($iaddr, AF_INET);
  84. if(!$hostname || $hostname !~ m/$af/) {
  85. Log3 $name, 1, "Connection refused from $caddr:$port";
  86. close($clientinfo[0]);
  87. return undef;
  88. }
  89. }
  90. }
  91. #$clientinfo[0]->blocking(0); # Forum #24799
  92. if($hash->{SSL}) {
  93. # Forum #27565: SSLv23:!SSLv3:!SSLv2', #35004: TLSv12:!SSLv3
  94. my $sslVersion = AttrVal($hash->{NAME}, "sslVersion",
  95. AttrVal("global", "sslVersion", "TLSv12:!SSLv3"));
  96. # Certs directory must be in the modpath, i.e. at the same level as the
  97. # FHEM directory
  98. my $mp = AttrVal("global", "modpath", ".");
  99. my $certPrefix = AttrVal($name, "sslCertPrefix", "certs/server-");
  100. my $ret;
  101. eval {
  102. $ret = IO::Socket::SSL->start_SSL($clientinfo[0], {
  103. SSL_server => 1,
  104. SSL_key_file => "$mp/${certPrefix}key.pem",
  105. SSL_cert_file => "$mp/${certPrefix}cert.pem",
  106. SSL_version => $sslVersion,
  107. SSL_cipher_list => 'HIGH:!RC4:!eNULL:!aNULL',
  108. Timeout => 4,
  109. });
  110. $! = EINVAL if(!$clientinfo[0]->blocking() && $!==EWOULDBLOCK);
  111. };
  112. my $err = $!;
  113. if( !$ret
  114. && $err != EWOULDBLOCK
  115. && $err ne "Socket is not connected") {
  116. $err = "" if(!$err);
  117. $err .= " ".($SSL_ERROR ? $SSL_ERROR : IO::Socket::SSL::errstr());
  118. Log3 $name, 1, "$type SSL/HTTPS error: $err (peer: $caddr)"
  119. if($err !~ m/error:00000000:lib.0.:func.0.:reason.0./); #Forum 56364
  120. close($clientinfo[0]);
  121. return undef;
  122. }
  123. }
  124. my $cname = "${name}_${caddr}_${port}";
  125. my %nhash;
  126. $nhash{NR} = $devcount++;
  127. $nhash{NAME} = $cname;
  128. $nhash{PEER} = $caddr;
  129. $nhash{PORT} = $port;
  130. $nhash{FD} = $clientinfo[0]->fileno();
  131. $nhash{CD} = $clientinfo[0]; # sysread / close won't work on fileno
  132. $nhash{TYPE} = $type;
  133. $nhash{SSL} = $hash->{SSL};
  134. readingsSingleUpdate(\%nhash, "state", "Connected", 0);
  135. $nhash{SNAME} = $name;
  136. $nhash{TEMPORARY} = 1; # Don't want to save it
  137. $nhash{BUF} = "";
  138. $attr{$cname}{room} = "hidden";
  139. $defs{$cname} = \%nhash;
  140. $selectlist{$nhash{NAME}} = \%nhash;
  141. my $ret = $clientinfo[0]->setsockopt(SOL_SOCKET, SO_KEEPALIVE, 1);
  142. Log3 $name, 4, "Connection accepted from $nhash{NAME}";
  143. return \%nhash;
  144. }
  145. sub
  146. TcpServer_SetSSL($)
  147. {
  148. my ($hash) = @_;
  149. eval "require IO::Socket::SSL";
  150. if($@) {
  151. Log3 $hash, 1, $@;
  152. Log3 $hash, 1, "Can't load IO::Socket::SSL, falling back to HTTP";
  153. } else {
  154. $hash->{SSL} = 1;
  155. }
  156. }
  157. sub
  158. TcpServer_Close($@)
  159. {
  160. my ($hash, $dodel) = @_;
  161. my $name = $hash->{NAME};
  162. if(defined($hash->{CD})) { # Clients
  163. close($hash->{CD});
  164. delete($hash->{CD});
  165. delete($selectlist{$name});
  166. delete($hash->{FD}); # Avoid Read->Close->Write
  167. delete $attr{$name} if($dodel);
  168. delete $defs{$name} if($dodel);
  169. }
  170. if(defined($hash->{SERVERSOCKET})) { # Server
  171. close($hash->{SERVERSOCKET});
  172. $name = $name . "." . $hash->{PORT};
  173. delete($selectlist{$name});
  174. delete($hash->{FD}); # Avoid Read->Close->Write
  175. }
  176. return undef;
  177. }
  178. # close a (SSL-)Socket in local process
  179. # avoids interfering with other processes using it
  180. # this is critical for SSL and helps with other issues, too
  181. sub
  182. TcpServer_Disown($)
  183. {
  184. my ($hash) = @_;
  185. my $name = $hash->{NAME};
  186. if( defined($hash->{CD}) ){
  187. if( $hash->{SSL} ){
  188. $hash->{CD}->close( SSL_no_shutdown => 1);
  189. } else {
  190. close( $hash->{CD} );
  191. }
  192. delete($hash->{CD});
  193. delete($selectlist{$name});
  194. delete($hash->{FD}); # Avoid Read->Close->Write
  195. }
  196. return;
  197. }
  198. # wait for a socket to become ready
  199. # takes IO::Socket::SSL + non-blocking into account
  200. sub
  201. TcpServer_Wait($$)
  202. {
  203. my( $hash, $direction ) = @_;
  204. my $read = '';
  205. my $write ='';
  206. if( $direction eq 'read' || $hash->{wantRead} ){
  207. vec( $read, $hash->{FD}, 1) = 1;
  208. } elsif( $direction eq 'write' || $hash->{wantWrite} ){
  209. vec( $write, $hash->{FD}, 1) = 1;
  210. } else {
  211. return undef;
  212. }
  213. my $ret = select( $read, $write, undef, undef );
  214. return if $ret == -1;
  215. if( vec( $read, $hash->{FD}, 1) ){
  216. delete $hash->{wantRead};
  217. }
  218. if( vec( $write, $hash->{FD}, 1) ){
  219. delete $hash->{wantWrite};
  220. }
  221. # return true on success
  222. return 1;
  223. }
  224. # WantRead/Write: keep ssl constants local
  225. sub
  226. TcpServer_WantRead($)
  227. {
  228. my( $hash ) = @_;
  229. return $hash->{SSL}
  230. && $hash->{CD}
  231. && $hash->{CD}->errstr == &IO::Socket::SSL::SSL_WANT_READ;
  232. }
  233. sub
  234. TcpServer_WantWrite($)
  235. {
  236. my( $hash ) = @_;
  237. return $hash->{SSL}
  238. && $hash->{CD}
  239. && $hash->{CD}->errstr == &IO::Socket::SSL::SSL_WANT_WRITE;
  240. }
  241. # write until all data is done.
  242. # hanldes both, blocking and non-blocking sockets
  243. # ... with or without SSL
  244. sub
  245. TcpServer_WriteBlocking($$)
  246. {
  247. my( $hash, $txt ) = @_;
  248. my $sock = $hash->{CD};
  249. return undef if(!$sock);
  250. my $off = 0;
  251. my $len = length($txt);
  252. while($off < $len) {
  253. if(!TcpServer_Wait($hash, 'write')) {
  254. TcpServer_Close($hash);
  255. return undef;
  256. }
  257. my $ret = syswrite($sock, $txt, $len-$off, $off);
  258. if( defined $ret ){
  259. $off += $ret;
  260. } elsif( $! == EWOULDBLOCK ){
  261. $hash->{wantRead} = 1
  262. if TcpServer_WantRead($hash);
  263. } else {
  264. TcpServer_Close($hash);
  265. return undef; # error
  266. }
  267. }
  268. return 1; # success
  269. }
  270. 1;