TcpServerUtils.pm 7.4 KB

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