| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587 |
- ##############################################
- # $Id: 96_allowed.pm 16295 2018-02-28 22:11:09Z rudolfkoenig $
- package main;
- use strict;
- use warnings;
- use vars qw(@FW_httpheader); # HTTP header, line by line
- my $allowed_haveSha;
- #####################################
- sub
- allowed_Initialize($)
- {
- my ($hash) = @_;
- $hash->{DefFn} = "allowed_Define";
- $hash->{AuthorizeFn} = "allowed_Authorize";
- $hash->{AuthenticateFn} = "allowed_Authenticate";
- $hash->{SetFn} = "allowed_Set";
- $hash->{AttrFn} = "allowed_Attr";
- $hash->{AttrList} = "disable:0,1 validFor allowedCommands allowedDevices ".
- "basicAuth basicAuthMsg password globalpassword ".
- "basicAuthExpiry";
- $hash->{UndefFn} = "allowed_Undef";
- $hash->{FW_detailFn} = "allowed_fhemwebFn";
- eval { require Digest::SHA; };
- if($@) {
- Log3 $hash, 4, $@;
- $allowed_haveSha = 0;
- } else {
- $allowed_haveSha = 1;
- }
- }
- #####################################
- sub
- allowed_Define($$)
- {
- my ($hash, $def) = @_;
- my @l = split(" ", $def);
- if(@l > 2) {
- my %list;
- for(my $i=2; $i<@l; $i++) {
- $list{$l[$i]} = 1;
- }
- $hash->{devices} = \%list;
- }
- $auth_refresh = 1;
- readingsSingleUpdate($hash, "state", "validFor:", 0);
- SecurityCheck() if($init_done);
- return undef;
- }
- sub
- allowed_Undef($$)
- {
- $auth_refresh = 1;
- return undef;
- }
- #####################################
- # Return 0 for don't care, 1 for Allowed, 2 for forbidden.
- sub
- allowed_Authorize($$$$)
- {
- my ($me, $cl, $type, $arg) = @_;
- return 0 if($me->{disabled});
- if( $cl->{SNAME} ) {
- return 0 if(!$me->{validFor} || $me->{validFor} !~ m/\b$cl->{SNAME}\b/);
- } else {
- return 0 if(!$me->{validFor} || $me->{validFor} !~ m/\b$cl->{NAME}\b/);
- }
- if($type eq "cmd") {
- return 0 if(!$me->{allowedCommands});
- # Return 0: allow stacking with other instances, see Forum#46380
- return 0 if($me->{allowedCommands} =~ m/\b\Q$arg\E\b/);
- Log3 $me, 3, "Forbidden command $arg for $cl->{NAME}";
- stacktrace() if(AttrVal($me, "verbose", 5));
- return 2;
- }
- if($type eq "devicename") {
- return 0 if(!$me->{allowedDevices});
- return 1 if($me->{allowedDevices} =~ m/\b\Q$arg\E\b/);
- Log3 $me, 3, "Forbidden device $arg for $cl->{NAME}";
- stacktrace() if(AttrVal($me, "verbose", 5));
- return 2;
- }
- return 0;
- }
- #####################################
- # Return 0 for authentication not needed, 1 for auth-ok, 2 for wrong password
- sub
- allowed_Authenticate($$$$)
- {
- my ($me, $cl, $param) = @_;
- return 0 if($me->{disabled});
- return 0 if(!$me->{validFor} || $me->{validFor} !~ m/\b$cl->{SNAME}\b/);
- my $aName = $me->{NAME};
- if($cl->{TYPE} eq "FHEMWEB") {
- my $basicAuth = AttrVal($aName, "basicAuth", undef);
- delete $cl->{".httpAuthHeader"};
- return 0 if(!$basicAuth);
- return 2 if(!$param);
- my $FW_httpheader = $param;
- my $secret = $FW_httpheader->{Authorization};
- $secret =~ s/^Basic //i if($secret);
- # Check for Cookie in headers if no basicAuth header is set
- my $authcookie;
- if (!$secret && $FW_httpheader->{Cookie}) {
- if(AttrVal($aName, "basicAuthExpiry", 0)) {
- my $cookie = "; ".$FW_httpheader->{Cookie}.";";
- $authcookie = $1 if ( $cookie =~ /; AuthToken=([^;]+);/ );
- $secret = $authcookie;
- }
- }
- my $pwok = ($secret && $secret eq $basicAuth); # Base64
- my ($user, $password) = split(":", decode_base64($secret)) if($secret);
- ($user,$password) = ("","") if(!defined($user) || !defined($password));
- if($secret && $basicAuth =~ m/^{.*}$/) {
- eval "use MIME::Base64";
- if($@) {
- Log3 $aName, 1, $@;
- } else {
- $pwok = eval $basicAuth;
- Log3 $aName, 1, "basicAuth expression: $@" if($@);
- }
- } elsif($basicAuth =~ m/^SHA256:(.{8}):(.*)$/) {
- if($allowed_haveSha) {
- $pwok = Digest::SHA::sha256_base64("$1:$user:$password") eq $2;
- } else {
- Log3 $me, 3, "Cant load Digest::SHA to decode $me->{NAME} beiscAuth";
- }
- }
- Log3 $me, 3, "Login denied by $aName for $user via $cl->{NAME}"
- if(!$pwok && $user);
- # Add Cookie header ONLY if authentication with basicAuth was succesful
- if($pwok && (!defined($authcookie) || $secret ne $authcookie)) {
- my $time = AttrVal($aName, "basicAuthExpiry", 0);
- if ( $time ) {
- $time = int($time*86400+time());
- # generate timestamp according to RFC-1130 in Expires
- my $expires = FmtDateTimeRFC1123($time);
- readingsBeginUpdate($me);
- readingsBulkUpdate($me,'lastAuthUser', $user, 1);
- readingsBulkUpdate($me,'lastAuthExpires', $time, 1);
- readingsBulkUpdate($me,'lastAuthExpiresFmt', $expires, 1);
- readingsEndUpdate($me, 1);
- # set header with expiry
- $cl->{".httpAuthHeader"} = "Set-Cookie: AuthToken=".$secret.
- "; Path=/ ; Expires=$expires\r\n" ;
- }
- }
- return 1 if($pwok);
- my $msg = AttrVal($aName, "basicAuthMsg", "FHEM: login required");
- $cl->{".httpAuthHeader"} = "HTTP/1.1 401 Authorization Required\r\n".
- "WWW-Authenticate: Basic realm=\"$msg\"\r\n";
- return 2;
- }
- if($cl->{TYPE} eq "telnet") {
- my $pw = AttrVal($aName, "password", undef);
- if(!$pw) {
- $pw = AttrVal($aName, "globalpassword", undef);
- $pw = undef if($pw && $cl->{NAME} =~ m/_127.0.0.1_/);
- }
- return 0 if(!$pw);
- return 2 if(!defined($param));
- if($pw =~ m/^{.*}$/) {
- my $password = $param;
- my $ret = eval $pw;
- Log3 $aName, 1, "password expression: $@" if($@);
- return ($ret ? 1 : 2);
- } elsif($pw =~ m/^SHA256:(.{8}):(.*)$/) {
- if($allowed_haveSha) {
- return (Digest::SHA::sha256_base64("$1:$param") eq $2) ? 1 : 2;
- } else {
- Log3 $me, 3, "Cant load Digest::SHA to decode $me->{NAME} beiscAuth";
- }
- }
- return ($pw eq $param) ? 1 : 2;
- }
- return 0;
- }
- sub
- allowed_Set(@)
- {
- my ($hash, @a) = @_;
- my %sets = (globalpassword=>1, password=>1, basicAuth=>2);
- return "no set argument specified" if(int(@a) < 2);
- return "Unknown argument $a[1], choose one of ".join(" ",sort keys %sets)
- if(!defined($sets{$a[1]}));
- return "$a[1] needs $sets{$a[1]} parameters"
- if(@a-2 != $sets{$a[1]});
- return "Cannot load Digest::SHA" if(!$allowed_haveSha);
- my $plain = ($a[1] eq "basicAuth" ? "$a[2]:$a[3]" : $a[2]);
- my ($x,$y) = gettimeofday();
- my $salt = substr(sprintf("%08X", rand($y)*rand($x)),0,8);
- CommandAttr($hash->{CL}, "$a[0] $a[1] SHA256:$salt:".
- Digest::SHA::sha256_base64("$salt:$plain"));
- }
- sub
- allowed_Attr(@)
- {
- my ($type, $devName, $attrName, @param) = @_;
- my $hash = $defs{$devName};
- my $set = ($type eq "del" ? 0 : (!defined($param[0]) || $param[0]) ? 1 : 0);
- if($attrName eq "disable") {
- readingsSingleUpdate($hash, "state", $set ? "disabled" : "active", 1);
- if($set) {
- $hash->{disabled} = 1;
- } else {
- delete($hash->{disabled});
- }
- } elsif($attrName eq "allowedCommands" || # hoping for some speedup
- $attrName eq "allowedDevices" ||
- $attrName eq "validFor") {
- if($set) {
- $hash->{$attrName} = join(" ", @param);
- } else {
- delete($hash->{$attrName});
- }
- if($attrName eq "validFor") {
- readingsSingleUpdate($hash, "state", "validFor:".join(",",@param), 1);
- InternalTimer(1, "SecurityCheck", 0) if($init_done);
- }
- } elsif(($attrName eq "basicAuth" ||
- $attrName eq "password" || $attrName eq "globalpassword") &&
- $type eq "set") {
- foreach my $d (devspec2array("TYPE=(FHEMWEB|telnet)")) {
- delete $defs{$d}{Authenticated} if($defs{$d});
- }
- InternalTimer(1, "SecurityCheck", 0) if($init_done);
- }
- return undef;
- }
- #########################
- sub
- allowed_fhemwebFn($$$$)
- {
- my ($FW_wname, $d, $room, $pageHash) = @_; # pageHash is set for summaryFn.
- my $hash = $defs{$d};
- my $vf = $defs{$d}{validFor} ? $defs{$d}{validFor} : "";
- my (@F_arr, @t_arr);
- my @arr = map {
- push(@F_arr, $_) if($defs{$_}{TYPE} eq "FHEMWEB");
- push(@t_arr, $_) if($defs{$_}{TYPE} eq "telnet");
- "<input type='checkbox' ".($vf =~ m/\b$_\b/ ? "checked ":"").
- "name='$_' class='vfAttr'><label>$_</label>"
- }
- grep { !$defs{$_}{SNAME} }
- devspec2array("TYPE=(FHEMWEB|telnet)");
- my $r = "<input id='vfAttr' type='button' value='attr'> $d validFor <ul>".
- join("<br>",@arr)."</ul><script>var dev='$d';".<<'EOF';
- $("#vfAttr").click(function(){
- var names=[];
- $("input.vfAttr:checked").each(function(){names.push($(this).attr("name"))});
- FW_cmd(FW_root+"?cmd=attr "+dev+" validFor "+names.join(",")+"&XHR=1");
- });
- </script>
- EOF
- $r .= "For ".join(",",@F_arr).
- ": \"set $d basicAuth <username> <password>\"<br>"
- if(@F_arr);
- $r .= "For ".join(",",@t_arr).
- ": \"set $d password <password>\" or".
- " \"set $d globalpassword <password>\"<br>"
- if(@t_arr);
- return $r;
- }
- 1;
- =pod
- =item helper
- =item summary authorize command execution based on frontend
- =item summary_DE authorisiert Befehlsausführung basierend auf dem Frontend
- =begin html
- <a name="allowed"></a>
- <h3>allowed</h3>
- <ul>
- <br>
- <a name="alloweddefine"></a>
- <b>Define</b>
- <ul>
- <code>define <name> allowed <deviceList></code>
- <br><br>
- Authorize execution of commands and modification of devices based on the
- frontend used and/or authenticate users.<br><br>
- If there are multiple instances defined which are valid for a given
- frontend device, then all authorizations must succeed. For authentication
- it is sufficient when one of the instances succeeds. The checks are
- executed in alphabetical order of the allowed instance names.<br><br>
- <b>Note:</b> this module should work as intended, but no guarantee
- can be given that there is no way to circumvent it.<br><br>
- Examples:
- <ul><code>
- define allowedWEB allowed<br>
- attr allowedWEB validFor WEB,WEBphone,WEBtablet<br>
- attr allowedWEB basicAuth { "$user:$password" eq "admin:secret" }<br>
- attr allowedWEB allowedCommands set,get<br><br>
- define allowedTelnet allowed<br>
- attr allowedTelnet validFor telnetPort<br>
- attr allowedTelnet password secret<br>
- </code></ul>
- <br>
- </ul>
- <a name="allowedset"></a>
- <b>Set</b>
- <ul>
- <li>basicAuth <username> <password></li>
- <li>password <password></li>
- <li>globalpassword <password><br>
- these commands set the corresponding attribute, by computing an SHA256
- hash from the arguments and a salt. Note: the perl module Device::SHA is
- needed.
- </li>
- </ul><br>
- <a name="allowedget"></a>
- <b>Get</b> <ul>N/A</ul><br>
- <a name="allowedattr"></a>
- <b>Attributes</b>
- <ul>
- <li><a href="#disable">disable</a></li><br>
- <a name="allowedCommands"></a>
- <li>allowedCommands<br>
- A comma separated list of commands allowed from the matching frontend
- (see validFor).<br>
- If set to an empty list <code>, (i.e. comma only)</code>
- then no comands are allowed. If set to <code>get,set</code>, then only
- a "regular" usage is allowed via set and get, but changing any
- configuration is forbidden.<br>
- </li><br>
- <a name="allowedDevices"></a>
- <li>allowedDevices<br>
- A comma separated list of device names which can be manipulated via the
- matching frontend (see validFor).
- </li><br>
- <a name="basicAuth"></a>
- <li>basicAuth, basicAuthMsg<br>
- request a username/password authentication for FHEMWEB access.
- It can be a base64 encoded string of user:password, an SHA256 hash
- (which should be set via the corresponding set command) or a perl
- expression if enclosed in {}, where $user and $password are set, and
- which returns true if accepted or false if not. Examples:
- <ul><code>
- attr allowed basicAuth ZmhlbXVzZXI6c2VjcmV0<br>
- attr allowed basicAuth SHA256:F87740B5:q8dHeiClaPLaWVsR/rqkzcBhw/JvvwVi4bEwKmJc/Is<br>
- attr allowed basicAuth {"$user:$password" eq "fhemuser:secret"}<br>
- </code></ul>
- If basicAuthMsg is set, it will be displayed in the popup window when
- requesting the username/password. Note: not all browsers support this
- feature.<br>
- </li><br>
- <a name="basicAuthExpiry"></a>
- <li>basicAuthExpiry<br>
- allow the basicAuth to be kept valid for a given number of days.
- So username/password as specified in basicAuth are only requested
- after a certain period.
- This is achieved by sending a cookie to the browser that will expire
- after the given period.
- Only valid if basicAuth is set.
- </li><br>
- <a name="password"></a>
- <li>password<br>
- Specify a password for telnet instances, which has to be entered as the
- very first string after the connection is established. The same rules
- apply as for basicAuth, with the expception that there is no user to be
- specified.<br>
- Note: if this attribute is set, you have to specify a password as the
- first argument when using fhem.pl in client mode:
- <ul>
- perl fhem.pl localhost:7072 secret "set lamp on"
- </ul>
- </li><br>
- <a name="globalpassword"></a>
- <li>globalpassword<br>
- Just like the attribute password, but a password will only required for
- non-local connections.
- </li><br>
- <a name="validFor"></a>
- <li>validFor<br>
- A comma separated list of frontend names. Currently supported frontends
- are all devices connected through the FHEM TCP/IP library, e.g. telnet
- and FHEMWEB. The allowed instance is only active, if this attribute is
- set.
- </li>
- </ul>
- <br>
- </ul>
- =end html
- =begin html_DE
- <a name="allowed"></a>
- <h3>allowed</h3>
- <ul>
- <br>
- <a name="alloweddefine"></a>
- <b>Define</b>
- <ul>
- <code>define <name> allowed <deviceList></code>
- <br><br>
- Authorisiert das Ausführen von Kommandos oder das Ändern von
- Geräten abhängig vom verwendeten Frontend.<br>
- Falls man mehrere allowed Instanzen definiert hat, die für dasselbe
- Frontend verantwortlich sind, dann müssen alle Authorisierungen
- genehmigt sein, um das Befehl ausführen zu können. Auf der
- anderen Seite reicht es, wenn einer der Authentifizierungen positiv
- entschieden wird. Die Prüfungen werden in alphabetischer Reihenfolge
- der Instanznamen ausgeführt. <br><br>
- <b>Achtung:</b> das Modul sollte wie hier beschrieben funktionieren,
- allerdings können wir keine Garantie geben, daß man sie nicht
- überlisten, und Schaden anrichten kann.<br><br>
- Beispiele:
- <ul><code>
- define allowedWEB allowed<br>
- attr allowedWEB validFor WEB,WEBphone,WEBtablet<br>
- attr allowedWEB basicAuth { "$user:$password" eq "admin:secret" }<br>
- attr allowedWEB allowedCommands set,get<br><br>
- define allowedTelnet allowed<br>
- attr allowedTelnet validFor telnetPort<br>
- attr allowedTelnet password secret<br>
- </code></ul>
- <br>
- </ul>
- <a name="allowedset"></a>
- <b>Set</b>
- <ul>
- <li>basicAuth <username> <password></li>
- <li>password <password></li>
- <li>globalpassword <password><br>
- diese Befehle setzen das entsprechende Attribut, indem sie aus den
- Parameter und ein Salt ein SHA256 Hashwert berechnen. Achtung: das perl
- Modul Device::SHA wird benötigt.
- </li>
- </ul><br>
- <a name="allowedget"></a>
- <b>Get</b> <ul>N/A</ul><br>
- <a name="allowedattr"></a>
- <b>Attribute</b>
- <ul>
- <li><a href="#disable">disable</a>
- </li><br>
- <a name="allowedCommands"></a>
- <li>allowedCommands<br>
- Eine Komma getrennte Liste der erlaubten Befehle des passenden
- Frontends (siehe validFor). Bei einer leeren Liste (, dh. nur ein
- Komma) wird dieser Frontend "read-only".
- Falls es auf <code>get,set</code> gesetzt ist, dann sind in dieser
- Frontend keine Konfigurationsänderungen möglich, nur
- "normale" Bedienung der Schalter/etc.
- </li><br>
- <a name="allowedDevices"></a>
- <li>allowedDevices<br>
- Komma getrennte Liste von Gerätenamen, die mit dem passenden
- Frontend (siehe validFor) geändert werden können.
- </li><br>
- <a name="basicAuth"></a>
- <li>basicAuth, basicAuthMsg<br>
- Erzwingt eine Authentifizierung mit Benutzername/Passwort für die
- zugerdnete FHEMWEB Instanzen. Der Wert kann entweder das base64
- kodierte Benutzername:Passwort sein, ein SHA256 hash (was man am besten
- mit dem passenden set Befehl erzeugt), oder, falls er in {}
- eingeschlossen ist, ein Perl Ausdruck. Für Letzteres wird
- $user und $passwort gesetzt, und muss wahr zurückliefern, falls
- Benutzername und Passwort korrekt sind. Beispiele:
- <ul><code>
- attr allowed basicAuth ZmhlbXVzZXI6c2VjcmV0<br>
- attr allowed basicAuth SHA256:F87740B5:q8dHeiClaPLaWVsR/rqkzcBhw/JvvwVi4bEwKmJc/Is<br>
- attr allowed basicAuth {"$user:$password" eq "fhemuser:secret"}<br>
- </code></ul>
- basicAuthMsg wird (in manchen Browsern) in dem Passwort Dialog als
- Überschrift angezeigt.<br>
- </li><br>
- <a name="password"></a>
- <li>password<br>
- Betrifft nur telnet Instanzen (siehe validFor): Bezeichnet ein
- Passwort, welches als allererster String eingegeben werden muss,
- nachdem die Verbindung aufgebaut wurde. Für die Werte gelten die
- Regeln von basicAuth, mit der Ausnahme, dass nur Passwort und kein
- Benutzername spezifiziert wird.<br> Falls dieser Parameter gesetzt
- wird, sendet FHEM telnet IAC Requests, um ein Echo während der
- Passworteingabe zu unterdrücken. Ebenso werden alle
- zurückgegebenen Zeilen mit \r\n abgeschlossen.<br>
- Falls dieses Attribut gesetzt wird, muss als erstes Argument ein
- Passwort angegeben werden, wenn fhem.pl im Client-mode betrieben wird:
- <ul><code>
- perl fhem.pl localhost:7072 secret "set lamp on"
- </code></ul>
- </li><br>
- <a name="globalpassword"></a>
- <li>globalpassword<br>
- Betrifft nur telnet Instanzen (siehe validFor): Entspricht dem
- Attribut password; ein Passwort wird aber ausschließlich für
- nicht-lokale Verbindungen verlangt.
- </li><br>
- <a name="validFor"></a>
- <li>validFor<br>
- Komma separierte Liste von Frontend-Instanznamen. Aktuell werden nur
- Frontends unterstützt, die das FHEM TCP/IP Bibliothek verwenden,
- z.Bsp. telnet und FHEMWEB. Falls nicht gesetzt, ist die allowed Instanz
- nicht aktiv.
- </li>
- </ul>
- <br>
- </ul>
- =end html_DE
- =cut
|