From 72c147593235480cf5231b5a671922ff96f84f4f Mon Sep 17 00:00:00 2001 From: rudolfkoenig <> Date: Wed, 4 Jan 2017 21:31:55 +0000 Subject: [PATCH] 01_FHEMWEB.pm: first version of websocket support (Forum #59713) git-svn-id: https://svn.fhem.de/fhem/trunk@12957 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/FHEM/01_FHEMWEB.pm | 157 +++++++++++++++++++++++++-------------- fhem/www/pgm2/console.js | 86 +++++++++++++++------ fhem/www/pgm2/fhemweb.js | 61 ++++++++++----- 3 files changed, 208 insertions(+), 96 deletions(-) diff --git a/fhem/FHEM/01_FHEMWEB.pm b/fhem/FHEM/01_FHEMWEB.pm index af64b92a4..44647e6bc 100755 --- a/fhem/FHEM/01_FHEMWEB.pm +++ b/fhem/FHEM/01_FHEMWEB.pm @@ -11,6 +11,7 @@ use Time::HiRes qw(gettimeofday); ######################### # Forward declaration sub FW_IconURL($); +sub FW_addToWritebuffer($$@); sub FW_answerCall($); sub FW_dev2image($;$); sub FW_devState($$@); @@ -156,7 +157,7 @@ FHEMWEB_Initialize($) hiddengroup hiddenroom iconPath - longpoll:0,1 + longpoll:0,1,websocket longpollSVG:1,0 menuEntries mainInputLength @@ -376,7 +377,6 @@ FW_Read($$) } @FW_httpheader; delete($hash->{HDR}); - $FW_userAgent = $FW_httpheader{"User-Agent"}; my @origin = grep /Origin/, @FW_httpheader; $FW_headerlines = (AttrVal($FW_wname, "CORS", 0) ? (($#origin<0) ? "": "Access-Control-Allow-".$origin[0]."\r\n"). @@ -422,6 +422,25 @@ FW_Read($$) delete $hash->{CONTENT_LENGTH}; $hash->{LASTACCESS} = $now; + if( $method eq 'GET' && $FW_httpheader{Connection} =~ /Upgrade/i ) { + use Digest::SHA1 qw(sha1_base64); + TcpServer_WriteBlocking($FW_chash, + "HTTP/1.1 101 Switching Protocols\r\n" . + "Upgrade: websocket\r\n" . + "Connection: Upgrade\r\n" . + "Sec-WebSocket-Accept:". + sha1_base64($FW_httpheader{'Sec-WebSocket-Key'}. + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")."=\r\n" . + "\r\n" ); + $FW_chash->{websocket} = 1; + + my $me = $FW_chash; + my ($cmd, $cmddev) = FW_digestCgi($arg); + FW_initInform($me, 0) if($FW_inform); + return -1; + } + + $FW_userAgent = $FW_httpheader{"User-Agent"}; $arg = "" if(!defined($arg)); Log3 $FW_wname, 4, "$name $method $arg; BUFLEN:".length($hash->{BUF}); $FW_ME = "/" . AttrVal($FW_wname, "webname", "fhem"); @@ -482,7 +501,7 @@ FW_Read($$) ("Expires: ".FmtDateTimeRFC1123($now+900)."\r\n") : ""); Log3 $FW_wname, 4, "name: $arg / RL:$length / $FW_RETTYPE / $compressed / $expires"; - if( ! addToWritebuffer($hash, + if( ! FW_addToWritebuffer($hash, "HTTP/1.1 200 OK\r\n" . "Content-Length: $length\r\n" . $expires . $compressed . $FW_headerlines . @@ -496,6 +515,77 @@ FW_Read($$) } } +sub +FW_initInform($$) +{ + my ($me, $longpoll) = @_; + + if($FW_inform =~ /type=/) { + foreach my $kv (split(";", $FW_inform)) { + my ($key,$value) = split("=", $kv, 2); + $me->{inform}{$key} = $value; + } + + } else { # Compatibility mode + $me->{inform}{type} = ($FW_room ? "status" : "raw"); + $me->{inform}{filter} = ($FW_room ? $FW_room : ".*"); + } + my $filter = $me->{inform}{filter}; + $filter = "NAME=.*" if($filter eq "room=all"); + $filter = "room!=.+" if($filter eq "room=Unsorted"); + + my %h = map { $_ => 1 } devspec2array($filter); + $h{global} = 1 if( $me->{inform}{addglobal} ); + $h{"#FHEMWEB:$FW_wname"} = 1; + $me->{inform}{devices} = \%h; + %FW_visibleDeviceHash = FW_visibleDevices(); + + # NTFY_ORDER is larger than the normal order (50-) + $me->{NTFY_ORDER} = $FW_cname; # else notifyfn won't be called + %ntfyHash = (); + $me->{inform}{since} = time()-5 + if(!defined($me->{inform}{since}) || $me->{inform}{since} !~ m/^\d+$/); + + if($longpoll) { + my $sinceTimestamp = FmtDateTime($me->{inform}{since}); + TcpServer_WriteBlocking($me, + "HTTP/1.1 200 OK\r\n". + $FW_headerlines. + "Content-Type: application/octet-stream; charset=$FW_encoding\r\n\r\n". + FW_roomStatesForInform($me, $sinceTimestamp)); + } + + if($FW_id && $defs{$FW_wname}{asyncOutput}) { + my $data = $defs{$FW_wname}{asyncOutput}{$FW_id}; + if($data) { + FW_addToWritebuffer($me, $data."\n"); + delete $defs{$FW_wname}{asyncOutput}{$FW_id}; + } + } + if($me->{inform}{withLog}) { + $logInform{$me->{NAME}} = "FW_logInform"; + } else { + delete($logInform{$me->{NAME}}); + } +} + + +sub +FW_addToWritebuffer($$@) +{ + my ($hash, $txt, $callback, $nolimit) = @_; + + if( $hash->{websocket} ) { + my $len = length($txt); + if( $len < 126 ) { + $txt = chr(0x81) . chr(0xFF) . chr($len) . $txt; + } else { + $txt = chr(0x81) . chr(0x7E) . pack( 'n', $len ) . $txt; + } + } + return addToWritebuffer($hash, $txt, $callback, $nolimit); +} + sub FW_AsyncOutput($$) { @@ -524,7 +614,7 @@ FW_AsyncOutput($$) next if( $chash->{TYPE} ne 'FHEMWEB' ); next if( !$chash->{inform} ); next if( !$chash->{FW_ID} || $chash->{FW_ID} ne $hash->{FW_ID} ); - addToWritebuffer($chash, $data."\n"); + FW_addToWritebuffer($chash, $data."\n"); $found = 1; last; } @@ -670,51 +760,7 @@ FW_answerCall($) } if($FW_inform) { # Longpoll header - if($FW_inform =~ /type=/) { - foreach my $kv (split(";", $FW_inform)) { - my ($key,$value) = split("=", $kv, 2); - $me->{inform}{$key} = $value; - } - - } else { # Compatibility mode - $me->{inform}{type} = ($FW_room ? "status" : "raw"); - $me->{inform}{filter} = ($FW_room ? $FW_room : ".*"); - } - my $filter = $me->{inform}{filter}; - $filter = "NAME=.*" if($filter eq "room=all"); - $filter = "room!=.+" if($filter eq "room=Unsorted"); - - my %h = map { $_ => 1 } devspec2array($filter); - $h{global} = 1 if( $me->{inform}{addglobal} ); - $h{"#FHEMWEB:$FW_wname"} = 1; - $me->{inform}{devices} = \%h; - %FW_visibleDeviceHash = FW_visibleDevices(); - - # NTFY_ORDER is larger than the normal order (50-) - $me->{NTFY_ORDER} = $FW_cname; # else notifyfn won't be called - %ntfyHash = (); - $me->{inform}{since} = time()-5 - if(!defined($me->{inform}{since}) || $me->{inform}{since} !~ m/^\d+$/); - my $sinceTimestamp = FmtDateTime($me->{inform}{since}); - TcpServer_WriteBlocking($me, - "HTTP/1.1 200 OK\r\n". - $FW_headerlines. - "Content-Type: application/octet-stream; charset=$FW_encoding\r\n\r\n". - FW_roomStatesForInform($me, $sinceTimestamp)); - - if($FW_id && $defs{$FW_wname}{asyncOutput}) { - my $data = $defs{$FW_wname}{asyncOutput}{$FW_id}; - if($data) { - addToWritebuffer($me, $data."\n"); - delete $defs{$FW_wname}{asyncOutput}{$FW_id}; - } - } - if($me->{inform}{withLog}) { - $logInform{$me->{NAME}} = "FW_logInform"; - } else { - delete($logInform{$me->{NAME}}); - } - + FW_initInform($me, 1); return -1; } @@ -871,7 +917,7 @@ FW_answerCall($) my $csrf= ($FW_CSRF ? "fwcsrf='$defs{$FW_wname}{CSRFTOKEN}'" : ""); my $gen = 'generated="'.(time()-1).'"'; - my $lp = 'longpoll="'.AttrVal($FW_wname,"longpoll",1).'"'; + my $lp = 'longpoll="'.AttrVal($FW_wname,"longpoll",1).'"'; $FW_id = $FW_chash->{NR} if( !$FW_id ); FW_pO "\n
"; @@ -2575,7 +2621,7 @@ FW_logInform($$) return; } $msg = FW_htmlEscape($msg); - if(!addToWritebuffer($ntfy, "