HttpUtils.pm 28 KB

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