TcpServerUtils.pm 6.7 KB

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