############################################## # $Id$ 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); 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 ($me->{allowedCommands} =~ m/\b\Q$arg\E\b/) ? 0 : 2; } if($type eq "devicename") { return 0 if(!$me->{allowedDevices}); return ($me->{allowedDevices} =~ m/\b\Q$arg\E\b/) ? 0 : 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); 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"; } } # 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}); } readingsSingleUpdate($hash, "state", "validFor:".join(",",@param), 1) if($attrName eq "validFor"); } 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}); } } 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 @arr = map { "" } grep { !$defs{$_}{SNAME} } devspec2array("TYPE=(FHEMWEB|telnet)"); return " $d validFor
define <name> allowed <deviceList>
define allowedWEB allowed
attr allowedWEB validFor WEB,WEBphone,WEBtablet
attr allowedWEB basicAuth { "$user:$password" eq "admin:secret" }
attr allowedWEB allowedCommands set,get
define allowedTelnet allowed
attr allowedTelnet validFor telnetPort
attr allowedTelnet password secret
, (i.e. comma only)
then no comands are allowed. If set to get,set
, then only
a "regular" usage is allowed via set and get, but changing any
configuration is forbidden.
attr allowed basicAuth ZmhlbXVzZXI6c2VjcmV0
attr allowed basicAuth SHA256:F87740B5:q8dHeiClaPLaWVsR/rqkzcBhw/JvvwVi4bEwKmJc/Is
attr allowed basicAuth {"$user:$password" eq "fhemuser:secret"}
define <name> allowed <deviceList>
define allowedWEB allowed
attr allowedWEB validFor WEB,WEBphone,WEBtablet
attr allowedWEB basicAuth { "$user:$password" eq "admin:secret" }
attr allowedWEB allowedCommands set,get
define allowedTelnet allowed
attr allowedTelnet validFor telnetPort
attr allowedTelnet password secret
get,set
gesetzt ist, dann sind in dieser
Frontend keine Konfigurationsänderungen möglich, nur
"normale" Bedienung der Schalter/etc.
attr allowed basicAuth ZmhlbXVzZXI6c2VjcmV0
attr allowed basicAuth SHA256:F87740B5:q8dHeiClaPLaWVsR/rqkzcBhw/JvvwVi4bEwKmJc/Is
attr allowed basicAuth {"$user:$password" eq "fhemuser:secret"}
perl fhem.pl localhost:7072 secret "set lamp on"