HttpUtils.pm 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951
  1. ##############################################
  2. # $Id: HttpUtils.pm 16343 2018-03-06 20:44:37Z rudolfkoenig $
  3. package main;
  4. use strict;
  5. use warnings;
  6. use MIME::Base64;
  7. use Digest::MD5 qw(md5_hex);
  8. use vars qw($SSL_ERROR);
  9. # Note: video does not work for every browser (Forum #73214)
  10. my %ext2MIMEType= qw{
  11. bmp image/bmp
  12. css text/css
  13. gif image/gif
  14. html text/html
  15. ico image/x-icon
  16. jpg image/jpeg
  17. js text/javascript
  18. mp4 video/mp4
  19. pdf application/pdf
  20. png image/png
  21. svg image/svg+xml
  22. txt text/plain
  23. };
  24. my $HU_use_zlib;
  25. sub
  26. ext2MIMEType($) {
  27. my ($ext)= @_;
  28. return "text/plain" if(!$ext);
  29. my $MIMEType = $ext2MIMEType{lc($ext)};
  30. return ($MIMEType ? $MIMEType : "text/$ext");
  31. }
  32. sub
  33. filename2MIMEType($) {
  34. my ($filename)= @_;
  35. $filename =~ m/^.*\.([^\.]*)$/;
  36. return ext2MIMEType($1);
  37. }
  38. ##################
  39. sub
  40. urlEncode($) {
  41. $_= $_[0];
  42. s/([\x00-\x2F \x3A-\x40 \x5B-\x60 \x7B-\xFF])/sprintf("%%%02x",ord($1))/eg;
  43. return $_;
  44. }
  45. sub
  46. urlEncodePath($) {
  47. $_= $_[0];
  48. s/([\x00-\x20 \x25 \x3F \x7B-\xFF])/sprintf("%%%02x",ord($1))/eg;
  49. return $_;
  50. }
  51. ##################
  52. sub
  53. urlDecode($) {
  54. $_= $_[0];
  55. s/%([0-9A-F][0-9A-F])/chr(hex($1))/egi;
  56. return $_;
  57. }
  58. sub
  59. HttpUtils_Close($)
  60. {
  61. my ($hash) = @_;
  62. delete($hash->{FD});
  63. delete($selectlist{$hash});
  64. $hash->{conn}->close() if(defined($hash->{conn}));
  65. delete($hash->{conn});
  66. delete($hash->{hu_sslAdded});
  67. delete($hash->{hu_filecount});
  68. delete($hash->{hu_blocking});
  69. delete($hash->{hu_portSfx});
  70. delete($hash->{hu_proxy});
  71. delete($hash->{hu_port});
  72. delete($hash->{directReadFn});
  73. delete($hash->{directWriteFn});
  74. delete($hash->{compress});
  75. }
  76. sub
  77. HttpUtils_Err($)
  78. {
  79. my ($lhash, $errtxt) = @_;
  80. my $hash = $lhash->{hash};
  81. if($lhash->{sts} && $lhash->{sts} == $selectTimestamp) { # busy loop check
  82. Log 4, "extending '$lhash->{msg} $hash->{addr}' timeout due to busy loop";
  83. InternalTimer(gettimeofday()+1, "HttpUtils_Err", $lhash);
  84. return;
  85. }
  86. return if(!defined($hash->{FD})); # Already closed
  87. HttpUtils_Close($hash);
  88. $hash->{callback}($hash, "$lhash->{msg} $hash->{addr} timed out", "");
  89. }
  90. sub
  91. HttpUtils_File($)
  92. {
  93. my ($hash) = @_;
  94. return 0 if($hash->{url} !~ m+^file://(.*)$+);
  95. my $fName = $1;
  96. return (1, "Absolute URL is not supported") if($fName =~ m+^/+);
  97. return (1, ".. in URL is not supported") if($fName =~ m+\.\.+);
  98. open(FH, $fName) || return(1, "$fName: $!");
  99. my $data = join("", <FH>);
  100. close(FH);
  101. return (1, undef, $data);
  102. }
  103. sub
  104. ip2str($)
  105. {
  106. my ($addr) = @_;
  107. return sprintf("%d.%d.%d.%d", unpack("C*", $addr)) if(length($addr) == 4);
  108. my $h = join(":",map { sprintf("%x",$_) } unpack("n*",$addr));
  109. $h =~ s/(:0)+/:/g;
  110. $h =~ s/^0://g;
  111. return "[$h]";
  112. }
  113. # http://www.ccs.neu.edu/home/amislove/teaching/cs4700/fall09/handouts/project1-primer.pdf
  114. my %HU_dnsCache;
  115. sub
  116. HttpUtils_dnsParse($$$)
  117. {
  118. my ($a, $ql,$try6) = @_; # $ql: avoid hardcoding query length
  119. return "wrong message ID" if(unpack("H*",substr($a,0,2)) ne "7072");
  120. while(length($a) >= $ql+16) {
  121. my $l = unpack("C",substr($a,$ql, 1));
  122. if(($l & 0xC0) == 0xC0) { # DNS packed compression
  123. $ql += 2;
  124. } else {
  125. while($l != 0) {
  126. $ql += $l+1;
  127. $l = unpack("C",substr($a,$ql,2));
  128. }
  129. $ql++;
  130. }
  131. return (undef, substr($a,$ql+10,16),unpack("N",substr($a,$ql+4,4)))
  132. if(unpack("N",substr($a,$ql,4)) == 0x1c0001 && $try6);
  133. return (undef, substr($a,$ql+10,4), unpack("N",substr($a,$ql+4,4)))
  134. if(unpack("N",substr($a,$ql,4)) == 0x10001 && !$try6);
  135. $ql += 10+unpack("n",substr($a,$ql+8)) if(length($a) >= $ql+10);
  136. }
  137. return "No A record found";
  138. }
  139. # { HttpUtils_gethostbyname({timeout=>4}, "google.com", 1,
  140. # sub(){my($h,$e,$a)=@_;; Log 1, $e ? "ERR:$e": ("IP:".ip2str($a)) }) }
  141. sub
  142. HttpUtils_gethostbyname($$$$)
  143. {
  144. my ($hash, $host, $try6, $fn) = @_;
  145. if($host =~ m/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ && # IP-Address
  146. $1<256 && $2<256 && $3<256 && $4<256) {
  147. $fn->($hash, undef, pack("CCCC", $1, $2, $3, $4));
  148. return;
  149. }
  150. my $dnsServer = AttrVal("global", "dnsServer", undef);
  151. if(!$dnsServer) { # use the blocking libc to get the IP
  152. if($haveInet6) {
  153. $host = $1 if($host =~ m/^\[([a-f0-9:]+)\]+$/);
  154. my $iaddr = Socket6::inet_pton(AF_INET6, $host);
  155. return $fn->($hash, undef, $iaddr) if($iaddr);
  156. $iaddr = Socket6::inet_pton(AF_INET , $host);
  157. return $fn->($hash, undef, $iaddr) if($iaddr);
  158. my ($s4, $s6);
  159. my @res = Socket6::getaddrinfo($host, 80);
  160. for(my $i=0; $i+5<=@res; $i+=5) {
  161. $s4 = $res[$i+3] if($res[$i] == AF_INET && !$s4);
  162. $s6 = $res[$i+3] if($res[$i] == AF_INET6 && !$s6);
  163. }
  164. if($s6) {
  165. (undef, $iaddr) = Socket6::unpack_sockaddr_in6($s6);
  166. return $fn->($hash, undef, $iaddr);
  167. }
  168. if($s4) {
  169. (undef, $iaddr) = sockaddr_in($s4);
  170. return $fn->($hash, undef, $iaddr);
  171. }
  172. $fn->($hash, "gethostbyname $host failed", undef);
  173. } else {
  174. my $iaddr = inet_aton($host);
  175. my $err;
  176. if(!defined($iaddr)) {
  177. $iaddr = gethostbyname($host); # This is still blocking
  178. $err = (($iaddr && length($iaddr)==4) ?
  179. undef : "gethostbyname $host failed");
  180. }
  181. $fn->($hash, $err, $iaddr);
  182. }
  183. return;
  184. }
  185. return $fn->($hash, undef, $HU_dnsCache{$host}{addr}) # check the cache
  186. if($HU_dnsCache{$host} &&
  187. $HU_dnsCache{$host}{TS}+$HU_dnsCache{$host}{TTL} > gettimeofday());
  188. my $dh = AttrVal("global", "dnsHostsFile", "undef");
  189. if($dh) {
  190. my $fh;
  191. if(open($fh, $dh)) {
  192. while(my $line = <$fh>) {
  193. if($line =~ m/^([^# \t]+).*\b\Q$host\E\b/) {
  194. if($1 =~ m/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ && # IP-Address
  195. $1<256 && $2<256 && $3<256 && $4<256) {
  196. $fn->($hash, undef, pack("CCCC", $1, $2, $3, $4));
  197. close($fh);
  198. return;
  199. }
  200. }
  201. }
  202. close($fh);
  203. }
  204. }
  205. # Direct DNS Query via UDP
  206. my $c = IO::Socket::INET->new(Proto=>'udp', PeerAddr=>"$dnsServer:53");
  207. return $fn->($hash, "Cant create UDP socket:$!", undef) if(!$c);
  208. my %dh = ( conn=>$c, FD=>$c->fileno(), NAME=>"DNS", origHash=>$hash,
  209. addr=>$dnsServer, callback=>$fn );
  210. my %timerHash = ( hash=>\%dh, msg=>"DNS" );
  211. my $bhost = join("", map { pack("CA*",length($_),$_) } split(/\./, $host));
  212. my $qry = pack("nnnnnn", 0x7072,0x0100,1,0,0,0) .
  213. $bhost . pack("Cnn", 0,$try6 ? 28:1,1);
  214. my $ql = length($qry);
  215. Log 5, "DNS QUERY ".unpack("H*", $qry);
  216. $dh{directReadFn} = sub() { # Parse the answer
  217. RemoveInternalTimer(\%timerHash);
  218. my $buf;
  219. my $len = sysread($dh{conn},$buf,65536);
  220. HttpUtils_Close(\%dh);
  221. Log 5, "DNS ANSWER ".($len?$len:0).":".($buf ? unpack("H*", $buf):"N/A");
  222. my ($err, $addr, $ttl) = HttpUtils_dnsParse($buf,$ql,$try6);
  223. return HttpUtils_gethostbyname($hash, $host, 0, $fn) if($err && $try6);
  224. return $fn->($hash, "DNS: $err", undef) if($err);
  225. Log 4, "DNS result for $host: ".ip2str($addr).", ttl:$ttl";
  226. $HU_dnsCache{$host}{TS} = gettimeofday();
  227. $HU_dnsCache{$host}{TTL} = $ttl;
  228. $HU_dnsCache{$host}{addr} = $addr;
  229. return $fn->($hash, undef, $addr);
  230. };
  231. $selectlist{\%dh} = \%dh;
  232. my $dnsQuery;
  233. my $dnsTo = 0.25;
  234. my $lSelectTs = $selectTimestamp;
  235. $dnsQuery = sub()
  236. {
  237. $dnsTo *= 2 if($lSelectTs != $selectTimestamp);
  238. $lSelectTs = $selectTimestamp;
  239. return HttpUtils_Err(\%timerHash) if($dnsTo > $hash->{timeout}/2);
  240. my $ret = syswrite $dh{conn}, $qry;
  241. if(!$ret || $ret != $ql) {
  242. my $err = $!;
  243. HttpUtils_Close(\%dh);
  244. return $fn->($hash, "DNS write error: $err", undef);
  245. }
  246. InternalTimer(gettimeofday()+$dnsTo, $dnsQuery, \%timerHash);
  247. };
  248. $dnsQuery->();
  249. }
  250. sub
  251. HttpUtils_Connect($)
  252. {
  253. my ($hash) = @_;
  254. $hash->{timeout} = 4 if(!defined($hash->{timeout}));
  255. $hash->{loglevel} = 4 if(!$hash->{loglevel});
  256. $hash->{redirects} = 0 if(!$hash->{redirects});
  257. $hash->{displayurl} = $hash->{hideurl} ? "<hidden>" : $hash->{url};
  258. $hash->{sslargs} = {} if(!defined($hash->{sslargs}));
  259. Log3 $hash, $hash->{loglevel}+1, "HttpUtils url=$hash->{displayurl}";
  260. if($hash->{url} !~ /
  261. ^(http|https):\/\/ # $1: proto
  262. (([^:\/]+):([^:\/]+)@)? # $2: auth, $3:user, $4:password
  263. ([^:\/]+|\[[0-9a-f:]+\]) # $5: host or IPv6 address
  264. (:\d+)? # $6: port
  265. (\/.*)$ # $7: path
  266. /xi) {
  267. return "$hash->{displayurl}: malformed or unsupported URL";
  268. }
  269. my ($authstring,$user,$pwd,$port,$host);
  270. ($hash->{protocol},$authstring,$user,$pwd,$host,$port,$hash->{path})
  271. = (lc($1),$2,$3,$4,$5,$6,$7);
  272. $hash->{host} = $host;
  273. if(defined($port)) {
  274. $port =~ s/^://;
  275. } else {
  276. $port = ($hash->{protocol} eq "https" ? 443: 80);
  277. }
  278. $hash->{hu_portSfx} = ($port =~ m/^(80|443)$/ ? "" : ":$port");
  279. $hash->{hu_port} = $port;
  280. $hash->{path} = '/' unless defined($hash->{path});
  281. $hash->{addr} = "$hash->{protocol}://$host:$port";
  282. if($authstring) {
  283. $hash->{auth} = 1;
  284. $hash->{user} = urlDecode("$user");
  285. $hash->{pwd} = urlDecode("$pwd");
  286. } elsif(defined($hash->{user}) && defined($hash->{pwd})) {
  287. $hash->{auth} = 1;
  288. } else {
  289. $hash->{auth} = 0;
  290. }
  291. my $proxy = AttrVal("global", "proxy", undef);
  292. if($proxy) {
  293. my $pe = AttrVal("global", "proxyExclude", undef);
  294. if(!$pe || $host !~ m/$pe/) {
  295. my @hp = split(":", $proxy);
  296. $host = $hp[0];
  297. $port = $hp[1] if($hp[1]);
  298. $hash->{hu_proxy} = 1;
  299. }
  300. }
  301. if((!defined($hash->{compress}) || $hash->{compress}) &&
  302. AttrVal("global", "httpcompress", 1)) {
  303. if(!defined($HU_use_zlib)) {
  304. $HU_use_zlib = 1;
  305. eval { require Compress::Zlib; };
  306. $HU_use_zlib = 0 if($@);
  307. }
  308. $hash->{compress} = $HU_use_zlib;
  309. }
  310. return HttpUtils_Connect2($hash) if($hash->{conn} && $hash->{keepalive});
  311. if($hash->{callback}) { # Nonblocking staff
  312. HttpUtils_gethostbyname($hash, $host, $haveInet6, sub($$$) {
  313. my ($hash, $err, $iaddr) = @_;
  314. $hash = $hash->{origHash} if($hash->{origHash});
  315. if($err) {
  316. HttpUtils_Close($hash);
  317. return $hash->{callback}($hash, $err, "") ;
  318. }
  319. Log 5, "IP: $host -> ".ip2str($iaddr);
  320. $hash->{conn} = length($iaddr) == 4 ?
  321. IO::Socket::INET ->new(Proto=>'tcp', Blocking=>0) :
  322. IO::Socket::INET6->new(Proto=>'tcp', Blocking=>0);
  323. return $hash->{callback}($hash, "Creating socket: $!", "")
  324. if(!$hash->{conn});
  325. my $sa = length($iaddr)==4 ? sockaddr_in($port, $iaddr) :
  326. Socket6::pack_sockaddr_in6($port, $iaddr);
  327. my $ret = connect($hash->{conn}, $sa);
  328. if(!$ret) {
  329. if($!{EINPROGRESS} || int($!)==10035 ||
  330. (int($!)==140 && $^O eq "MSWin32")) { # Nonblocking connect
  331. $hash->{FD} = $hash->{conn}->fileno();
  332. my %timerHash=(hash=>$hash,sts=>$selectTimestamp,msg=>"connect to");
  333. $hash->{directWriteFn} = sub() {
  334. delete($hash->{FD});
  335. delete($hash->{directWriteFn});
  336. delete($selectlist{$hash});
  337. RemoveInternalTimer(\%timerHash);
  338. my $packed = getsockopt($hash->{conn}, SOL_SOCKET, SO_ERROR);
  339. my $errno = unpack("I",$packed);
  340. if($errno) {
  341. HttpUtils_Close($hash);
  342. return $hash->{callback}($hash, "$host: ".strerror($errno), "");
  343. }
  344. my $err = HttpUtils_Connect2($hash);
  345. $hash->{callback}($hash, $err, "") if($err);
  346. return $err;
  347. };
  348. $hash->{NAME}="" if(!defined($hash->{NAME}));#Delete might check it
  349. $selectlist{$hash} = $hash;
  350. InternalTimer(gettimeofday()+$hash->{timeout},
  351. "HttpUtils_Err", \%timerHash);
  352. return undef;
  353. } else {
  354. HttpUtils_Close($hash);
  355. $hash->{callback}($hash, "connect to $hash->{addr}: $!", "");
  356. return undef;
  357. }
  358. }
  359. });
  360. return;
  361. } else {
  362. $hash->{conn} = $haveInet6 ?
  363. IO::Socket::INET6->new(PeerAddr=>"$host:$port",Timeout=>$hash->{timeout}):
  364. IO::Socket::INET ->new(PeerAddr=>"$host:$port",Timeout=>$hash->{timeout});
  365. return "$hash->{displayurl}: Can't connect(1) to $hash->{addr}: $@"
  366. if(!$hash->{conn});
  367. }
  368. return HttpUtils_Connect2($hash);
  369. }
  370. sub
  371. HttpUtils_Connect2($)
  372. {
  373. my ($hash) = @_;
  374. my $usingSSL;
  375. $hash->{host} =~ s/:.*//;
  376. if($hash->{protocol} eq "https" && $hash->{conn} && !$hash->{hu_sslAdded}) {
  377. eval "use IO::Socket::SSL";
  378. if($@) {
  379. my $errstr = "$hash->{addr}: $@";
  380. Log3 $hash, $hash->{loglevel}, $errstr;
  381. HttpUtils_Close($hash);
  382. return $errstr;
  383. } else {
  384. $hash->{conn}->blocking(1);
  385. $usingSSL = 1;
  386. if($hash->{hu_proxy}) { # can block!
  387. my $pw = AttrVal("global", "proxyAuth", "");
  388. $pw = "Proxy-Authorization: Basic $pw\r\n" if($pw);
  389. my $hdr = "CONNECT $hash->{host}:$hash->{hu_port} HTTP/1.0\r\n".
  390. "User-Agent: fhem\r\n$pw\r\n";
  391. syswrite $hash->{conn}, $hdr;
  392. my $buf;
  393. my $len = sysread($hash->{conn},$buf,65536);
  394. if(!defined($len) || $len <= 0 || $buf !~ m/HTTP.*200/) {
  395. HttpUtils_Close($hash);
  396. return "Proxy denied CONNECT";
  397. }
  398. }
  399. my $sslVersion = AttrVal("global", "sslVersion", "SSLv23:!SSLv3:!SSLv2");
  400. $sslVersion = AttrVal($hash->{NAME}, "sslVersion", $sslVersion)
  401. if($hash->{NAME});
  402. my %par = %{$hash->{sslargs}};
  403. $par{Timeout} = $hash->{timeout};
  404. $par{SSL_version} = $sslVersion if(!$par{SSL_version});
  405. $par{SSL_hostname} = $hash->{host}
  406. if(IO::Socket::SSL->can('can_client_sni') &&
  407. IO::Socket::SSL->can_client_sni() &&
  408. (!$hash->{sslargs} || !defined($hash->{sslargs}{SSL_hostname})));
  409. $par{SSL_verify_mode} = 0
  410. if(!$hash->{sslargs} || !defined($hash->{sslargs}{SSL_verify_mode}));
  411. eval {
  412. IO::Socket::SSL->start_SSL($hash->{conn}, \%par) || undef $hash->{conn};
  413. };
  414. if($@) {
  415. Log3 $hash, $hash->{loglevel}, $@;
  416. HttpUtils_Close($hash);
  417. return $@;
  418. }
  419. $hash->{hu_sslAdded} = 1 if($hash->{keepalive});
  420. }
  421. }
  422. if(!$hash->{conn}) {
  423. undef $hash->{conn};
  424. my $err = $@;
  425. if($hash->{protocol} eq "https") {
  426. $err = "" if(!$err);
  427. $err .= " ".($SSL_ERROR ? $SSL_ERROR : IO::Socket::SSL::errstr());
  428. }
  429. return "$hash->{displayurl}: Can't connect(2) to $hash->{addr}: $err";
  430. }
  431. if($hash->{noConn2}) {
  432. $hash->{callback}($hash);
  433. return undef;
  434. }
  435. my $data;
  436. if(defined($hash->{data})) {
  437. if( ref($hash->{data}) eq 'HASH' ) {
  438. foreach my $key (keys %{$hash->{data}}) {
  439. $data .= "&" if( $data );
  440. $data .= "$key=". urlEncode($hash->{data}{$key});
  441. }
  442. } else {
  443. $data = $hash->{data};
  444. }
  445. }
  446. if(defined($hash->{header})) {
  447. if( ref($hash->{header}) eq 'HASH' ) {
  448. $hash->{header} = join("\r\n",
  449. map(($_.': '.$hash->{header}{$_}), keys %{$hash->{header}}));
  450. }
  451. }
  452. my $method = $hash->{method};
  453. $method = ($data ? "POST" : "GET") if( !$method );
  454. my $httpVersion = $hash->{httpversion} ? $hash->{httpversion} : "1.0";
  455. my $path = $hash->{path};
  456. $path = "$hash->{protocol}://$hash->{host}$hash->{hu_portSfx}$path"
  457. if($hash->{hu_proxy});
  458. my $hdr = "$method $path HTTP/$httpVersion\r\n";
  459. $hdr .= "Host: $hash->{host}$hash->{hu_portSfx}\r\n";
  460. $hdr .= "User-Agent: fhem\r\n"
  461. if(!$hash->{header} || $hash->{header} !~ "User-Agent:");
  462. $hdr .= "Accept-Encoding: gzip,deflate\r\n" if($hash->{compress});
  463. $hdr .= "Connection: keep-alive\r\n" if($hash->{keepalive});
  464. $hdr .= "Connection: Close\r\n"
  465. if($httpVersion ne "1.0" && !$hash->{keepalive});
  466. $hdr .= "Authorization: Basic ".
  467. encode_base64($hash->{user}.":".$hash->{pwd}, "")."\r\n"
  468. if($hash->{auth} && !$hash->{digest} &&
  469. !($hash->{header} &&
  470. $hash->{header} =~ /^Authorization:\s*Digest/mi));
  471. $hdr .= $hash->{header}."\r\n" if($hash->{header});
  472. if(defined($data)) {
  473. $hdr .= "Content-Length: ".length($data)."\r\n";
  474. $hdr .= "Content-Type: application/x-www-form-urlencoded\r\n"
  475. if ($hdr !~ "Content-Type:");
  476. }
  477. if(!$usingSSL) {
  478. my $pw = AttrVal("global", "proxyAuth", "");
  479. $hdr .= "Proxy-Authorization: Basic $pw\r\n" if($pw);
  480. }
  481. Log3 $hash, $hash->{loglevel}+1, "HttpUtils request header:\n$hdr";
  482. $hdr .= "\r\n";
  483. my $s = $hash->{shutdown};
  484. $s =(defined($hash->{noshutdown}) && $hash->{noshutdown}==0) if(!defined($s));
  485. $s = 0 if($hash->{protocol} eq "https");
  486. if($hash->{callback}) { # Nonblocking read
  487. $hash->{FD} = $hash->{conn}->fileno();
  488. $hash->{buf} = "";
  489. delete($hash->{httpdatalen});
  490. delete($hash->{httpheader});
  491. $hash->{NAME} = "" if(!defined($hash->{NAME}));
  492. my %timerHash = (hash=>$hash, checkSTS=>$selectTimestamp, msg=>"write to");
  493. $hash->{directReadFn} = sub() {
  494. my $buf;
  495. my $len = sysread($hash->{conn},$buf,65536);
  496. $hash->{buf} .= $buf if(defined($len) && $len > 0);
  497. if(!defined($len) || $len <= 0 ||
  498. HttpUtils_DataComplete($hash)) {
  499. delete($hash->{FD});
  500. delete($hash->{directReadFn});
  501. delete($selectlist{$hash});
  502. RemoveInternalTimer(\%timerHash);
  503. my ($err, $ret, $redirect) = HttpUtils_ParseAnswer($hash);
  504. $hash->{callback}($hash, $err, $ret) if(!$redirect);
  505. } elsif($hash->{incrementalTimeout}) { # Forum #85307
  506. RemoveInternalTimer(\%timerHash);
  507. InternalTimer(gettimeofday()+$hash->{timeout},
  508. "HttpUtils_Err", \%timerHash);
  509. }
  510. };
  511. $data = $hdr.(defined($data) ? $data:"");
  512. $hash->{directWriteFn} = sub($) { # Nonblocking write
  513. my $ret = syswrite $hash->{conn}, $data;
  514. if($ret <= 0) {
  515. my $err = $!;
  516. RemoveInternalTimer(\%timerHash);
  517. HttpUtils_Close($hash);
  518. return $hash->{callback}($hash, "write error: $err", undef)
  519. }
  520. $data = substr($data,$ret);
  521. if(length($data) == 0) {
  522. shutdown($hash->{conn}, 1) if($s);
  523. delete($hash->{directWriteFn});
  524. RemoveInternalTimer(\%timerHash);
  525. $timerHash{msg} = "read from";
  526. InternalTimer(gettimeofday()+$hash->{timeout},
  527. "HttpUtils_Err", \%timerHash);
  528. }
  529. };
  530. $selectlist{$hash} = $hash;
  531. InternalTimer(gettimeofday()+$hash->{timeout}, "HttpUtils_Err",\%timerHash);
  532. return undef;
  533. } else {
  534. syswrite $hash->{conn}, $hdr;
  535. syswrite $hash->{conn}, $data if(defined($data));
  536. shutdown($hash->{conn}, 1) if($s);
  537. }
  538. return undef;
  539. }
  540. sub
  541. HttpUtils_DataComplete($)
  542. {
  543. my ($hash) = @_;
  544. my $hl = $hash->{httpdatalen};
  545. if(!defined($hl)) {
  546. return 0 if($hash->{buf} !~ m/^(.*?)\r?\n\r?\n(.*)$/s);
  547. my ($hdr, $data) = ($1, $2);
  548. if($hdr =~ m/Transfer-Encoding:\s*chunked/si) {
  549. $hash->{httpheader} = $hdr;
  550. $hash->{httpdata} = "";
  551. $hash->{buf} = $data;
  552. $hash->{httpdatalen} = -1;
  553. } elsif($hdr =~ m/Content-Length:\s*(\d+)/si) {
  554. $hash->{httpdatalen} = $1;
  555. $hash->{httpheader} = $hdr;
  556. $hash->{httpdata} = $data;
  557. $hash->{buf} = "";
  558. } else {
  559. $hash->{httpdatalen} = -2;
  560. }
  561. $hl = $hash->{httpdatalen};
  562. }
  563. return 0 if($hl == -2);
  564. if($hl == -1) { # chunked
  565. while($hash->{buf} =~ m/^[\r\n]*([0-9A-F]+)\r?\n(.*)$/si) {
  566. my ($l, $r) = (hex($1), $2);
  567. if($l == 0) {
  568. $hash->{buf} = "";
  569. return 1;
  570. }
  571. return 0 if(length($r) < $l);
  572. $hash->{httpdata} .= substr($r, 0, $l);
  573. $hash->{buf} = substr($r, $l);
  574. }
  575. return 0;
  576. } else {
  577. $hash->{httpdata} .= $hash->{buf};
  578. $hash->{buf} = "";
  579. return 0 if(length($hash->{httpdata}) < $hash->{httpdatalen});
  580. return 1;
  581. }
  582. }
  583. sub
  584. HttpUtils_DigestHeader($$)
  585. {
  586. my ($hash, $header) = @_;
  587. my %digdata;
  588. while($header =~ /(\w+)="?([^"]+?)"?(?:\s*,\s*|$)/gc) {
  589. $digdata{$1} = $2;
  590. }
  591. my ($ha1, $ha2, $response);
  592. my ($user,$passwd) = ($hash->{user}, $hash->{pwd});
  593. if(exists($digdata{qop})) {
  594. $digdata{nc} = "00000001";
  595. $digdata{cnonce} = md5_hex(rand().time());
  596. }
  597. $digdata{uri} = $hash->{path};
  598. $digdata{username} = $user;
  599. if(exists($digdata{algorithm}) && $digdata{algorithm} eq "MD5-sess") {
  600. $ha1 = md5_hex(md5_hex($user.":".$digdata{realm}.":".$passwd).
  601. ":".$digdata{nonce}.":".$digdata{cnonce});
  602. } else {
  603. $ha1 = md5_hex($user.":".$digdata{realm}.":".$passwd);
  604. }
  605. # forcing qop=auth as qop=auth-int is not implemented
  606. $digdata{qop} = "auth" if($digdata{qop});
  607. my $method = $hash->{method};
  608. $method = ($hash->{data} ? "POST" : "GET") if( !$method );
  609. $ha2 = md5_hex($method.":".$hash->{path});
  610. if(exists($digdata{qop}) && $digdata{qop} =~ /(auth-int|auth)/) {
  611. $digdata{response} = md5_hex($ha1.":".
  612. $digdata{nonce}.":".
  613. $digdata{nc}.":".
  614. $digdata{cnonce}.":".
  615. $digdata{qop}.":".
  616. $ha2);
  617. } else {
  618. $digdata{response} = md5_hex($ha1.":".$digdata{nonce}.":".$ha2)
  619. }
  620. return "Authorization: Digest ".
  621. join(", ", map(($_.'='.($_ ne "nc" ? '"' :'').
  622. $digdata{$_}.($_ ne "nc" ? '"' :'')), keys(%digdata)));
  623. }
  624. sub
  625. HttpUtils_ParseAnswer($)
  626. {
  627. my ($hash) = @_;
  628. if(!$hash->{keepalive}) {
  629. $hash->{conn}->close();
  630. undef $hash->{conn};
  631. }
  632. if(!$hash->{buf} && !$hash->{httpheader}) {
  633. # Server answer: Keep-Alive: timeout=2, max=200
  634. if($hash->{keepalive} && $hash->{hu_filecount}) {
  635. my $bc = $hash->{hu_blocking};
  636. HttpUtils_Close($hash);
  637. if($bc) {
  638. return HttpUtils_BlockingGet($hash);
  639. } else {
  640. return HttpUtils_NonblockingGet($hash);
  641. }
  642. }
  643. return ("$hash->{displayurl}: empty answer received", "");
  644. }
  645. $hash->{hu_filecount} = 0 if(!$hash->{hu_filecount});
  646. $hash->{hu_filecount}++;
  647. if(!defined($hash->{httpheader})) { # response without Content-Length
  648. if($hash->{buf} =~ m/^(HTTP.*?)\r?\n\r?\n(.*)$/s) {
  649. $hash->{httpheader} = $1;
  650. $hash->{httpdata} = $2;
  651. delete($hash->{buf});
  652. } else {
  653. my $ret = $hash->{buf};
  654. delete($hash->{buf});
  655. return ("", $ret);
  656. }
  657. }
  658. my $ret = $hash->{httpdata};
  659. $ret = "" if(!defined($ret));
  660. delete $hash->{httpdata};
  661. delete $hash->{httpdatalen};
  662. my @header= split("\r\n", $hash->{httpheader});
  663. my @header0= split(" ", shift @header);
  664. my $code= $header0[1];
  665. # Close if server doesn't support keepalive
  666. HttpUtils_Close($hash)
  667. if($hash->{keepalive} &&
  668. $hash->{httpheader} =~ m/^Connection:\s*close\s*$/mi);
  669. if(!defined($code) || $code eq "") {
  670. return ("$hash->{displayurl}: empty answer received", "");
  671. }
  672. Log3 $hash,$hash->{loglevel}, "$hash->{displayurl}: HTTP response code $code";
  673. $hash->{code} = $code;
  674. # if servers requests digest authentication
  675. if($code==401 && $hash->{auth} &&
  676. !($hash->{header} && $hash->{header} =~ /^Authorization:\s*Digest/mi) &&
  677. $hash->{httpheader} =~ /^WWW-Authenticate:\s*Digest\s*(.+?)\s*$/mi) {
  678. $hash->{header} .= "\r\n".
  679. HttpUtils_DigestHeader($hash, $1) if($hash->{header});
  680. $hash->{header} = HttpUtils_DigestHeader($hash, $1) if(!$hash->{header});
  681. # Request the URL with the Digest response
  682. if($hash->{callback}) {
  683. HttpUtils_NonblockingGet($hash);
  684. return ("", "", 1);
  685. } else {
  686. return HttpUtils_BlockingGet($hash);
  687. }
  688. } elsif($code==401 && $hash->{auth}) {
  689. return ("$hash->{displayurl}: wrong authentication", "")
  690. }
  691. if(($code==301 || $code==302 || $code==303)
  692. && !$hash->{ignoreredirects}) { # redirect
  693. if(++$hash->{redirects} > 5) {
  694. return ("$hash->{displayurl}: Too many redirects", "");
  695. } else {
  696. my $ra;
  697. map { $ra=$1 if($_ =~ m/Location:\s*(\S+)$/) } @header;
  698. $ra = "/$ra" if($ra !~ m/^http/ && $ra !~ m/^\//);
  699. $hash->{url} = ($ra =~ m/^http/) ? $ra: $hash->{addr}.$ra;
  700. Log3 $hash, $hash->{loglevel}, "HttpUtils $hash->{displayurl}: ".
  701. "Redirect to ".($hash->{hideurl} ? "<hidden>" : $hash->{url});
  702. if($hash->{callback}) {
  703. HttpUtils_NonblockingGet($hash);
  704. return ("", "", 1);
  705. } else {
  706. return HttpUtils_BlockingGet($hash);
  707. }
  708. }
  709. }
  710. if($HU_use_zlib) {
  711. if($hash->{httpheader} =~ /^Content-Encoding: gzip/mi) {
  712. eval { $ret = Compress::Zlib::memGunzip($ret) };
  713. return ($@, $ret) if($@);
  714. }
  715. if($hash->{httpheader} =~ /^Content-Encoding: deflate/mi) {
  716. eval { my $i = Compress::Zlib::inflateInit();
  717. my $out = $i->inflate($ret);
  718. $ret = $out if($out) };
  719. return ($@, $ret) if($@);
  720. }
  721. }
  722. # Debug
  723. Log3 $hash, $hash->{loglevel}+1,
  724. "HttpUtils $hash->{displayurl}: Got data, length: ". length($ret);
  725. Log3 $hash, $hash->{loglevel}+1,
  726. "HttpUtils response header:\n$hash->{httpheader}" if($hash->{httpheader});
  727. return ("", $ret);
  728. }
  729. # Parameters in the hash:
  730. # mandatory:
  731. # url, callback
  732. # optional(default):
  733. # digest(0),hideurl(0),timeout(4),data(""),loglevel(4),header("" or HASH),
  734. # noshutdown(1),shutdown(0),httpversion("1.0"),ignoreredirects(0)
  735. # method($data?"POST":"GET"),keepalive(0),sslargs({}),user(),pwd()
  736. # compress(1), incrementalTimeout(0)
  737. # Example:
  738. # { HttpUtils_NonblockingGet({ url=>"http://fhem.de/MAINTAINER.txt",
  739. # callback=>sub($$$){ Log 1,"ERR:$_[1] DATA:".length($_[2]) } }) }
  740. sub
  741. HttpUtils_NonblockingGet($)
  742. {
  743. my ($hash) = @_;
  744. $hash->{hu_blocking} = 0;
  745. my ($isFile, $fErr, $fContent) = HttpUtils_File($hash);
  746. return $hash->{callback}($hash, $fErr, $fContent) if($isFile);
  747. my $err = HttpUtils_Connect($hash);
  748. $hash->{callback}($hash, $err, "") if($err);
  749. }
  750. #################
  751. # Parameters same as HttpUtils_NonblockingGet up to callback
  752. # Returns (err,data)
  753. sub
  754. HttpUtils_BlockingGet($)
  755. {
  756. my ($hash) = @_;
  757. delete $hash->{callback}; # Forum #80712
  758. $hash->{hu_blocking} = 1;
  759. my ($isFile, $fErr, $fContent) = HttpUtils_File($hash);
  760. return ($fErr, $fContent) if($isFile);
  761. my $err = HttpUtils_Connect($hash);
  762. return ($err, undef) if($err);
  763. my $buf = "";
  764. $hash->{conn}->timeout($hash->{timeout});
  765. $hash->{buf} = "";
  766. delete($hash->{httpdatalen});
  767. delete($hash->{httpheader});
  768. for(;;) {
  769. my ($rout, $rin) = ('', '');
  770. vec($rin, $hash->{conn}->fileno(), 1) = 1;
  771. my $nfound = select($rout=$rin, undef, undef, $hash->{timeout});
  772. if($nfound <= 0) {
  773. undef $hash->{conn};
  774. return ("$hash->{displayurl}: Select timeout/error: $!", undef);
  775. }
  776. my $len = sysread($hash->{conn},$buf,65536);
  777. last if(!defined($len) || $len <= 0);
  778. $hash->{buf} .= $buf;
  779. last if(HttpUtils_DataComplete($hash));
  780. }
  781. return HttpUtils_ParseAnswer($hash);
  782. }
  783. # Deprecated, use GetFileFromURL/GetFileFromURLQuiet
  784. sub
  785. CustomGetFileFromURL($$@)
  786. {
  787. my ($hideurl, $url, $timeout, $data, $noshutdown, $loglevel) = @_;
  788. $loglevel = 4 if(!defined($loglevel));
  789. my $hash = { hideurl => $hideurl,
  790. url => $url,
  791. timeout => $timeout,
  792. data => $data,
  793. noshutdown=> $noshutdown,
  794. loglevel => $loglevel,
  795. };
  796. my ($err, $ret) = HttpUtils_BlockingGet($hash);
  797. if($err) {
  798. Log3 undef, $hash->{loglevel}, "CustomGetFileFromURL $err";
  799. return undef;
  800. }
  801. return $ret;
  802. }
  803. ##################
  804. # Parameter: $url, $timeout, $data, $noshutdown, $loglevel
  805. # - if data (which is urlEncoded) is set, then a POST is performed, else a GET.
  806. # - noshutdown must be set e.g. if calling the Fritz!Box Webserver
  807. sub
  808. GetFileFromURL($@)
  809. {
  810. my ($url, @a)= @_;
  811. return CustomGetFileFromURL(0, $url, @a);
  812. }
  813. ##################
  814. # Same as GetFileFromURL, but the url is not displayed in the log.
  815. sub
  816. GetFileFromURLQuiet($@)
  817. {
  818. my ($url, @a)= @_;
  819. return CustomGetFileFromURL(1, $url, @a);
  820. }
  821. sub
  822. GetHttpFile($$)
  823. {
  824. my ($host,$file) = @_;
  825. return GetFileFromURL("http://$host$file");
  826. }
  827. 1;