From 716b93a85cb7e1a56e99f4152965039f0f62d97d Mon Sep 17 00:00:00 2001 From: "klaus.schauer" <> Date: Thu, 28 Jul 2022 15:45:34 +0000 Subject: [PATCH] 02_HTTPAPI.pm: new module git-svn-id: https://svn.fhem.de/fhem/trunk@26266 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/FHEM/02_HTTPAPI.pm | 474 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 474 insertions(+) create mode 100644 fhem/FHEM/02_HTTPAPI.pm diff --git a/fhem/FHEM/02_HTTPAPI.pm b/fhem/FHEM/02_HTTPAPI.pm new file mode 100644 index 000000000..2e05b7ad8 --- /dev/null +++ b/fhem/FHEM/02_HTTPAPI.pm @@ -0,0 +1,474 @@ +# $Id$ +# HTTP API commands +# set command: http://://set?device=&action= +# get command: http://://get?device=&action= +# read reading: http://://read?device=&reading= +# write reading: http:////write?device=&reading=&value= + +package main; +use Encode qw(decode encode); +use SetExtensions; +use strict; +use TcpServerUtils; +use warnings; + +my $gPath = ''; +BEGIN { + $gPath = substr($0, 0, rindex($0, '/')); +} +if (lc(substr($0, -7)) eq 'fhem.pl') { + $gPath = $attr{global}{modpath}.'/FHEM'; +} +use lib ($gPath.'/lib', $gPath.'/FHEM/lib', './FHEM/lib', './lib', './FHEM', './', '/usr/local/FHEM/share/fhem/FHEM/lib'); + +my $encoding = 'UTF-8'; +my $infix = 'api'; +my $linkPattern = "^\/?(([^\/]*(\/[^\/]+)*)\/?)\$"; +my $tcpServAdr = 'global'; +my $tcpServPort = 8087; + +sub HTTPAPI_Initialize($) { + my ($hash) = @_; + $hash->{AttrFn} = "HTTPAPI_Attr"; + $hash->{DefFn} = "HTTPAPI_Define"; + $hash->{ReadFn} = "HTTPAPI_Read"; + $hash->{UndefFn} = "HTTPAPI_Undef"; + $hash->{AttrList} = "disable:0,1 devicesCtrl " . $readingFnAttributes; + return undef; +} + +sub HTTPAPI_Define($$) { + my ($hash, $def) = @_; + my @param = split("[ \t][ \t]*", $def); + my ($name, $type, $apiName, $pport, $global) = split("[ \t]+", $def); + my $port; + if (defined $pport) { + $port = $pport; + $port =~ s/^IPV6://; + return "Usage: define HTTPAPI [] [[IPV6:]] [global|local|]" if ($port !~ m/^\d+$/); + } else { + $port = $tcpServPort; + $pport = $tcpServPort; + } + $global = $global // $tcpServAdr; + $global = undef if ($global eq 'local'); + $infix = $apiName if (defined $apiName); + $hash->{INFIX} = $infix; + # open TCP server for HTTP API service + my $ret = TcpServer_Open($hash, $pport, $global); + if($ret && !$init_done) { + Log3 $name, 1, "HTTPAPI $ret already exists"; + exit(1); + } + readingsSingleUpdate($hash, "state", "initialized", 1); + Log3 $name, 2, "HTTPAPI $name initialized"; + return $ret; +} + +sub HTTPAPI_Attr(@) { + my ($cmd, $name, $aName, $aVal) = @_; + if ($cmd eq "set") { + if ($aName =~ "devicesCtrl") { + if ($aVal !~ /^[A-Z_a-z0-9\,]+$/) { + Log3 $name, 2, "HTTPAPI $name invalid reading list in attr $name $aName $aVal (only A-Z, a-z, 0-9, _ and , allowed)"; + return "Invalid reading name $aVal (only A-Z, a-z, 0-9, _ and , allowed)"; + } + #addToDevAttrList($name, $aName); + } + } + return undef; +} + +sub HTTPAPI_CGI($$$) { + # execute request to http://:/$infix? + my ($hash, $name, $request) = @_; + my $apiCmd; + my $apiCmdString; + my $fhemDevName; + my $link; + return($hash, 503, 'close', "text/plain; charset=utf-8", encode($encoding, "error=503 Service Unavailable")) if(IsDisabled($name)); + + if($request =~ m/^(\/$infix)\/(set|get|read|write)\?(.*)$/) { + $link = $1; + $apiCmd = $2; + $apiCmdString = $3; + + # url decoding + $request =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg; + #readingsSingleUpdate($defs{$name}, 'request', $request, 0); + + if ($apiCmdString =~ /&device(\=[^&]*)?(?=&|$)|^device(\=[^&]*)?(&|$)/) { + $fhemDevName = substr(($1 // $2), 1); + # url decoding + $fhemDevName =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg; + if (defined $defs{$fhemDevName}) { + # Control of the device allowed? + my $devicesCtrl = AttrVal($name, 'devicesCtrl', undef); + my $allowedDev; + if (defined $devicesCtrl) { + my @devicesCtrl = split(',', $devicesCtrl); + foreach (@devicesCtrl) { + next if($_ ne $fhemDevName); + $allowedDev = $fhemDevName; + last; + } + return($hash, 403, 'close', "text/plain; charset=utf-8", encode($encoding, "error=403 Forbidden, $request > control of the device $fhemDevName not allowed")) if (!defined($allowedDev)) + } + if ($apiCmd eq 'get') { + my $getCmd; + if ($apiCmdString =~ /&action(\=[^&]*)?(?=&|$)|^action(\=[^&]*)?(&|$)/) { + $getCmd = substr(($1 // $2), 1); + # url decoding + $getCmd =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg; + my $ret = CommandGet(undef, "$fhemDevName $getCmd"); + if ($ret) { + return($hash, 200, 'close', "text/plain; charset=utf-8", encode($encoding, "$getCmd=$ret")) + } else { + return($hash, 400, 'close', "text/plain; charset=utf-8", encode($encoding, "error=400 Bad Request, $request > get $fhemDevName $getCmd")) + } + } else { + return($hash, 400, 'close', "text/plain; charset=utf-8", encode($encoding, "error=400 Bad Request, $request > attribute action is missing")) + } + } elsif ($apiCmd eq 'read') { + my $readingName; + if ($apiCmdString =~ /&reading(\=[^&]*)?(?=&|$)|^reading(\=[^&]*)?(&|$)/) { + $readingName = substr(($1 // $2), 1); + # url decoding + $readingName =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg; + my $readingVal = ReadingsVal($fhemDevName, $readingName, undef); + #readingsSingleUpdate($defs{$name}, 'reponse', "$readingName=$readingVal", 1); + if (defined $readingVal) { + return($hash, 200, 'close', "text/plain; charset=utf-8", encode($encoding, "$readingName=$readingVal")); + } else { + return($hash, 400, 'close', "text/plain; charset=utf-8", encode($encoding, "error=400 Bad Request, $request > reading $readingName unknown")) + } + } else { + return($hash, 400, 'close', "text/plain; charset=utf-8", encode($encoding, "error=400 Bad Request, $request > attribute reading is missing")) + } + + } elsif ($apiCmd eq 'set') { + my $setCmd; + if ($apiCmdString =~ /&action(\=[^&]*)?(?=&|$)|^action(\=[^&]*)?(&|$)/) { + $setCmd = substr(($1 // $2), 1); + # url decoding + $setCmd =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg; + #my $ret = CommandSet(undef, "$fhemDevName $setCmd"); + #my $ret = AnalyzeCommand($defs{$hash->{SNAME}}, "set $fhemDevName $setCmd"); + my $ret = AnalyzeCommand($defs{$name}, "set $fhemDevName $setCmd"); + if ($ret) { + return($hash, 400, 'close', "text/plain; charset=utf-8", encode($encoding, "error=400 Bad Request, $request > $ret")) + } else { + return($hash, 200, 'close', "text/plain; charset=utf-8", encode($encoding, "$fhemDevName=$setCmd")) + } + } else { + return($hash, 400, 'close', "text/plain; charset=utf-8", encode($encoding, "error=400 Bad Request, $request > attribute action is missing")) + } + + } elsif ($apiCmd eq 'write') { + my $readingName; + if ($apiCmdString =~ /&reading(\=[^&]*)?(?=&|$)|^reading(\=[^&]*)?(&|$)/) { + $readingName = substr(($1 // $2), 1); + # url decoding + $readingName =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg; + my $readingVal; + + if ($apiCmdString =~ /&value(\=[^&]*)?(?=&|$)|^value(\=[^&]*)?(&|$)/) { + $readingVal = substr(($1 // $2), 1); + # url decoding + $readingVal =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg; + if ($readingVal ne '') { + ($readingName, $readingVal) = split(/:\s?/, readingsSingleUpdate($defs{$fhemDevName}, $readingName, $readingVal, 1)); + return($hash, 200, 'close', "text/plain; charset=utf-8", encode($encoding, "$readingName=$readingVal")) + } else { + readingsDelete($defs{$fhemDevName}, $readingName); + return($hash, 200, 'close', "text/plain; charset=utf-8", encode($encoding, "$readingName=")) + } + } else { + # delete reading if value is not found + readingsDelete($defs{$fhemDevName}, $readingName); + return($hash, 200, 'close', "text/plain; charset=utf-8", encode($encoding, "$readingName=")) + } + } else { + return($hash, 400, 'close', "text/plain; charset=utf-8", encode($encoding, "error=400 Bad Request, $request > attribute reading is missing")) + } + } else { + return($hash, 400, 'close', "text/plain; charset=utf-8", encode($encoding, "error=400 Bad Request, $request > action $apiCmd unknown")) + } + } else { + return($hash, 400, 'close', "text/plain; charset=utf-8", encode($encoding, "error=400 Bad Request, $request > device $fhemDevName unknown")) + } + } else { + return($hash, 400, 'close', "text/plain; charset=utf-8", encode($encoding, "error=400 Bad Request, $request > attribute device missing")) + } + + } else { + return HTTPAPI_CommandRef($hash); + } + return; +} + +sub HTTPAPI_CommandRef($) { + my ($hash) = @_; + my @contents; + my $contents; + my $fileName = $gPath . '/03_HTTPAPI.pm'; + if(open(INPUTFILE, $fileName)) { + binmode(INPUTFILE); + @contents= ; + close(INPUTFILE); + $contents = join("", @contents); + $contents =~ /\n=begin.html([\s\S]*)\n=end.html/gs; + return($hash, 200, 'close', "text/html; charset=utf-8", encode($encoding, $1)); + } else { + return($hash, 404, 'close', "text/plain; charset=utf-8", encode($encoding, "error=404 Not Found, file $fileName not found")); + } + return; +} + +sub HTTPAPI_Read($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + # accept request and create a child + if ($hash->{SERVERSOCKET}) { + my $chash = TcpServer_Accept($hash, "HTTPAPI"); + return if (!$chash); + $chash->{encoding} = $encoding; + $chash->{CD}->blocking(0); + return; + } + + # read data + my $buf; + my $ret = sysread($hash->{CD}, $buf, 2048); + + # connection error, 0=EOF, undef=error + if (!defined($ret) || $ret <= 0) { + TcpServer_Close($hash, 1); + delete $hash->{BUF}; + return; + } + + $hash->{BUF} .= $buf; + my $content = ''; + my $contentType = ''; + + while(exists($hash->{BUF}) && length($hash->{BUF}) > 0) { + if (!$hash->{HDR}) { + last if ($hash->{BUF} !~ m/^(.*?)(\n\n|\r\n\r\n)(.*)$/s); + $hash->{HDR} = $1; + $hash->{BUF} = $3; + if ($hash->{HDR} =~ m/Content-Length:\s*([^\r\n]*)/si) { + $hash->{CONTENT_LENGTH} = $1; + } + if ($hash->{HDR} =~ m/Content-Type:\s*([^\r\n]*)/si) { + $contentType = $1; + } + } + + if ($hash->{CONTENT_LENGTH}) { + return if (length($hash->{BUF}) < $hash->{CONTENT_LENGTH}); + $content = substr($hash->{BUF}, 0, $hash->{CONTENT_LENGTH}); + $hash->{BUF} = substr($hash->{BUF}, $hash->{CONTENT_LENGTH}); + } + my @httpheader = split(/[\r\n]+/, $hash->{HDR}); + my ($method, $url, $httpvers) = split(" ", $httpheader[0], 3) if ($httpheader[0]); + $method = "" if (!$method); + delete ($hash->{HDR}); + + if ($method !~ m/^(GET|POST)$/i) { + $ret = HTTPAPI_TcpServerWrite($hash, 405, 'close', "text/plain; charset=utf-8", "error=405 Method Not Allowed"); + delete $hash->{CONTENT_LENGTH}; + next; + } elsif ($method eq 'POST' && $contentType ne 'application/xml') { + $ret = HTTPAPI_TcpServerWrite($hash, 400, 'close', "text/plain; charset=utf-8", "error=400 Bad Request"); + delete $hash->{CONTENT_LENGTH}; + next; + } elsif ($url eq "/favicon.ico") { + $ret = HTTPAPI_TcpServerWrite($hash, 404, 'close', "text/plain; charset=utf-8", "error=404 Not Found"); + delete $hash->{CONTENT_LENGTH}; + next; + } elsif ($url !~ m/\/$infix\//i) { + $ret = HTTPAPI_TcpServerWrite($hash, 400, 'close', "text/plain; charset=utf-8", "error=400 Bad Request"); + delete $hash->{CONTENT_LENGTH}; + next; + } else { + $url =~ m/\/$infix\/(.*)\?(.*)$/i; + my ($requestCmd, $cmdString) = ($1, $2); + # CGI Aufruf + $ret = HTTPAPI_TcpServerWrite(HTTPAPI_CGI($hash, $name, $url)); + delete $hash->{CONTENT_LENGTH}; + next: + } + + } + TcpServer_Close($hash, 1); + delete $hash->{BUF}; + return; +} + +sub HTTPAPI_TcpServerWrite($$$$$) { + my ($hash, $httpState, $connection, $contentType, $content) = @_; + my ($contentLength, $header) = (0, "HTTP/1.1 "); + my %httpState = ( + 200 => '200 OK', + 400 => '400 Bad Request', + 403 => '403 Forbidden', + 404 => '404 Not Found', + 405 => '405 Method Not Allowed', + 503 => '503 Service Unavailable' + ); + $content = $content // ''; + $contentLength = length($content); + $header .= "$httpState{$httpState}\r\nContent-Length: $contentLength\r\n"; + $header .= "Allow: GET, POST\r\n" if ($httpState == 405); + $header .= "Connection: $connection\r\n" if (defined $connection); + $header .= "Content-Type: $contentType\r\n" if (defined $contentType); + $header .= "\r\n"; + return TcpServer_WriteBlocking($hash, $header . $content); +} + +sub HTTPAPI_Undef($) { + my ($hash) = @_; + return TcpServer_Close($hash, 1); +} + +1; + +=pod +=item device +=item summary HTTP API server that executes set/get commands and sets/reads readings +=item summary_DE HTTP API-Server, der set-/get-Befehle ausführt und Readings setzt/liest +=begin html + + +

HTTPAPI

+
    + HTTPAPI is a compact HTML API server that performs http requests to execute set and get commands + and reads and writes readings.

    + + + Define +
      + define <name> HTTPAPI [<infix>] [[IPV6:]<ip-port>] [global|local|<hostname>]

      + + Defines the HTTP API server.
      +
        +
      • + <infix> is the portion behind the base URL (usually http://<hostname>:<ip-port>/<infix>).
        + [<infix>] = api is default. +
      • +
      • + [[IPV6:]<ip-port>] = 8087 is default.
        + To use IPV6, specify the portNumber as IPV6:<number>, in this case the perl module IO::Socket:INET6 will be requested. + On Linux you may have to install it with cpan -i IO::Socket::INET6 or apt-get libio-socket-inet6-perl. +
      • +
      • + [global|local|<hostname>] = global is default.
        + If the parameter is set to local, the server will only listen to localhost connections. If is set to global, the server + will listen on all interfaces, else it wil try to resolve the parameter as a hostname, and listen only on this interface. +
      • +
      +
      + + Example: +
        + define httpapi HTTPAPI api 8087 global
        +
      +
    +

    + + + Get +
      +
    • API command line for executing a get command
      + Request: +
        + http://<ip-addr>:<port>/<apiName>/get?device=<devname>&action=<cmd>
        +
      + Response: +
        + <action>=<response>|<error>=<error message>
        +
      +
    • +
    +

    + + + Set +
      +
    • API command line for executing a set command
      + Request: +
        + http://<ip-addr>:<port>/<apiName>/set?device=<devname>&action=<cmd>
        +
      + Response: +
        + <device>=<cmd>|<error>=<error message>
        +
      +
    • +
    +

    + + + Generated events +
      +
    • API command line for setting a reading
      + Request: +
        + http://<ip-addr>:<port>/<apiName>/write?device=<devname>&reading=<name>&value=<val>
        +
      + Response: +
        + <reading name>=<val>|<error>=<error message>
        +
      +
    • +
    • API command line for querying a reading
      + Request: +
        + http://<ip-addr>:<port>/<apiName>/read?device=<devname>&reading=<name>
        +
      + Response: +
        + <reading name>=<val>|<error>=<error message>
        +
      +
    • +
    • API command line for deleting a reading
      + Request: +
        + http://<ip-addr>:<port>/<apiName>/write?device=<devname>&reading=<name>&value=
        + http://<ip-addr>:<port>/<apiName>/write?device=<devname>&reading=<name>
        +
      + Response: +
        + <reading name>=|<error>=<error message>
        +
      +
    • +
    +

    + + Usage information +
      + All links are relative to http://<ip-addr>:<ip-port>/.
      + The http://<ip-addr>:<ip-port>/<apiName>/ command displays the module-specific commandref.
      + The response message is encoded to UTF-8. +
    +

    + + + Attributes +
      +
    • devicesCtrl <device_1>,...,<device_n>, + A comma separated list all devices to be controlled.
      + [devicesCtrl] = < > is default, all devices can be controlled. +
    • +
    • disable 0|1
      + If applied commands will not be executed. +
    • +
    • verbose 0...5 +
    • +
    +
+ +=end html +=cut