TcpServerUtils.pm 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. ##############################################
  2. # $Id: TcpServerUtils.pm 15707 2017-12-27 14:41:21Z 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 => 10,
  30. Blocking => ($^O =~ /Win/ ? 1 : 0), # Needed for .WRITEBUFFER@darwin
  31. ReuseAddr => 1
  32. );
  33. $hash->{STATE} = "Initialized";
  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 $ret;
  100. eval {
  101. $ret = IO::Socket::SSL->start_SSL($clientinfo[0], {
  102. SSL_server => 1,
  103. SSL_key_file => "$mp/certs/server-key.pem",
  104. SSL_cert_file => "$mp/certs/server-cert.pem",
  105. SSL_version => $sslVersion,
  106. SSL_cipher_list => 'HIGH:!RC4:!eNULL:!aNULL',
  107. Timeout => 4,
  108. });
  109. };
  110. my $err = $!;
  111. if( !$ret
  112. && $err != EWOULDBLOCK
  113. && $err ne "Socket is not connected") {
  114. $err = "" if(!$err);
  115. $err .= " ".($SSL_ERROR ? $SSL_ERROR : IO::Socket::SSL::errstr());
  116. Log3 $name, 1, "$type SSL/HTTPS error: $err (peer: $caddr)"
  117. if($err !~ m/error:00000000:lib.0.:func.0.:reason.0./); #Forum 56364
  118. close($clientinfo[0]);
  119. return undef;
  120. }
  121. }
  122. my $cname = "${name}_${caddr}_${port}";
  123. my %nhash;
  124. $nhash{NR} = $devcount++;
  125. $nhash{NAME} = $cname;
  126. $nhash{PEER} = $caddr;
  127. $nhash{PORT} = $port;
  128. $nhash{FD} = $clientinfo[0]->fileno();
  129. $nhash{CD} = $clientinfo[0]; # sysread / close won't work on fileno
  130. $nhash{TYPE} = $type;
  131. $nhash{SSL} = $hash->{SSL};
  132. $nhash{STATE} = "Connected";
  133. $nhash{SNAME} = $name;
  134. $nhash{TEMPORARY} = 1; # Don't want to save it
  135. $nhash{BUF} = "";
  136. $attr{$cname}{room} = "hidden";
  137. $defs{$cname} = \%nhash;
  138. $selectlist{$nhash{NAME}} = \%nhash;
  139. my $ret = $clientinfo[0]->setsockopt(SOL_SOCKET, SO_KEEPALIVE, 1);
  140. Log3 $name, 4, "Connection accepted from $nhash{NAME}";
  141. return \%nhash;
  142. }
  143. sub
  144. TcpServer_SetSSL($)
  145. {
  146. my ($hash) = @_;
  147. eval "require IO::Socket::SSL";
  148. if($@) {
  149. Log3 $hash, 1, $@;
  150. Log3 $hash, 1, "Can't load IO::Socket::SSL, falling back to HTTP";
  151. } else {
  152. $hash->{SSL} = 1;
  153. }
  154. }
  155. sub
  156. TcpServer_Close($@)
  157. {
  158. my ($hash, $dodel) = @_;
  159. my $name = $hash->{NAME};
  160. if(defined($hash->{CD})) { # Clients
  161. close($hash->{CD});
  162. delete($hash->{CD});
  163. delete($selectlist{$name});
  164. delete($hash->{FD}); # Avoid Read->Close->Write
  165. delete $attr{$name} if($dodel);
  166. delete $defs{$name} if($dodel);
  167. }
  168. if(defined($hash->{SERVERSOCKET})) { # Server
  169. close($hash->{SERVERSOCKET});
  170. $name = $name . "." . $hash->{PORT};
  171. delete($selectlist{$name});
  172. delete($hash->{FD}); # Avoid Read->Close->Write
  173. }
  174. return undef;
  175. }
  176. # close a (SSL-)Socket in local process
  177. # avoids interfering with other processes using it
  178. # this is critical for SSL and helps with other issues, too
  179. sub
  180. TcpServer_Disown($)
  181. {
  182. my ($hash) = @_;
  183. my $name = $hash->{NAME};
  184. if( defined($hash->{CD}) ){
  185. if( $hash->{SSL} ){
  186. $hash->{CD}->close( SSL_no_shutdown => 1);
  187. } else {
  188. close( $hash->{CD} );
  189. }
  190. delete($hash->{CD});
  191. delete($selectlist{$name});
  192. delete($hash->{FD}); # Avoid Read->Close->Write
  193. }
  194. return;
  195. }
  196. # wait for a socket to become ready
  197. # takes IO::Socket::SSL + non-blocking into account
  198. sub
  199. TcpServer_Wait($$)
  200. {
  201. my( $hash, $direction ) = @_;
  202. my $read = '';
  203. my $write ='';
  204. if( $direction eq 'read' || $hash->{wantRead} ){
  205. vec( $read, $hash->{FD}, 1) = 1;
  206. } elsif( $direction eq 'write' || $hash->{wantWrite} ){
  207. vec( $write, $hash->{FD}, 1) = 1;
  208. } else {
  209. return undef;
  210. }
  211. my $ret = select( $read, $write, undef, undef );
  212. return if $ret == -1;
  213. if( vec( $read, $hash->{FD}, 1) ){
  214. delete $hash->{wantRead};
  215. }
  216. if( vec( $write, $hash->{FD}, 1) ){
  217. delete $hash->{wantWrite};
  218. }
  219. # return true on success
  220. return 1;
  221. }
  222. # WantRead/Write: keep ssl constants local
  223. sub
  224. TcpServer_WantRead($)
  225. {
  226. my( $hash ) = @_;
  227. return $hash->{SSL}
  228. && $hash->{CD}
  229. && $hash->{CD}->errstr == &IO::Socket::SSL::SSL_WANT_READ;
  230. }
  231. sub
  232. TcpServer_WantWrite($)
  233. {
  234. my( $hash ) = @_;
  235. return $hash->{SSL}
  236. && $hash->{CD}
  237. && $hash->{CD}->errstr == &IO::Socket::SSL::SSL_WANT_WRITE;
  238. }
  239. # write until all data is done.
  240. # hanldes both, blocking and non-blocking sockets
  241. # ... with or without SSL
  242. sub
  243. TcpServer_WriteBlocking($$)
  244. {
  245. my( $hash, $txt ) = @_;
  246. my $sock = $hash->{CD};
  247. return undef if(!$sock);
  248. my $off = 0;
  249. my $len = length($txt);
  250. while($off < $len) {
  251. if(!TcpServer_Wait($hash, 'write')) {
  252. TcpServer_Close($hash);
  253. return undef;
  254. }
  255. my $ret = syswrite($sock, $txt, $len-$off, $off);
  256. if( defined $ret ){
  257. $off += $ret;
  258. } elsif( $! == EWOULDBLOCK ){
  259. $hash->{wantRead} = 1
  260. if TcpServer_WantRead($hash);
  261. } else {
  262. TcpServer_Close($hash);
  263. return undef; # error
  264. }
  265. }
  266. return 1; # success
  267. }
  268. 1;