############################################## # $Id$ package main; use strict; use warnings; use vars qw(@FW_httpheader); # HTTP header, line by line use MIME::Base64; my $allowed_haveSha; sub allowed_CheckBasicAuth($$$$); ##################################### 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"; no warnings 'qw'; my @attrList = qw( allowedCommands allowedDevices allowedDevicesRegexp allowedIfAuthenticatedByMe:1,0 basicAuth basicAuthExpiry basicAuthMsg disable:1,0 globalpassword password validFor ); use warnings 'qw'; $hash->{AttrList} = join(" ", @attrList)." ".$readingFnAttributes; $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, $silent) = @_; 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/); } return 0 if(AttrVal($me->{NAME}, "allowedIfAuthenticatedByMe", 0) && (!$cl->{AuthenticatedBy} || $cl->{AuthenticatedBy} ne $me->{NAME})); 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} && !$me->{allowedDevicesRegexp}); return 1 if($me->{allowedDevices} && $me->{allowedDevices} =~ m/\b\Q$arg\E\b/); return 1 if($me->{allowedDevicesRegexp} && $arg =~ m/^$me->{allowedDevicesRegexp}$/); if(!$silent) { 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) = @_; my $doReturn = sub($$){ my ($r,$a) = @_; $cl->{AuthenticatedBy} = $me->{NAME} if($r == 1); $cl->{AuthenticationDeniedBy} = $me->{NAME} if($r == 2 && $a); return $r; }; 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 = (allowed_CheckBasicAuth($me, $cl, $secret, $basicAuth) == 1); # 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 ) { my ($user, $password) = split(":", decode_base64($secret)) if($secret); $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 &$doReturn(1, 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 &$doReturn(2, $secret); } elsif($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 &$doReturn($ret ? 1 : 2, $param); } elsif($pw =~ m/^SHA256:(.{8}):(.*)$/) { if($allowed_haveSha) { return &$doReturn(Digest::SHA::sha256_base64("$1:$param") eq $2 ? 1 : 2, $param); } else { Log3 $me, 3, "Cant load Digest::SHA to decode $me->{NAME} beiscAuth"; } } return &$doReturn(($pw eq $param) ? 1 : 2, $param); } else { $param =~ m/^basicAuth:(.*)/ if($param); return &$doReturn(allowed_CheckBasicAuth($me, $cl, $1, AttrVal($aName,"basicAuth",undef)), $param); } } sub allowed_CheckBasicAuth($$$$) { my ($me, $cl, $secret, $basicAuth) = @_; return 0 if(!$basicAuth); my $aName = $me->{NAME}; my $pwok = ($secret && $secret eq $basicAuth) ? 1 : 2; # Base64 my ($user, $password) = split(":", decode_base64($secret)) if($secret); ($user,$password) = ("","") if(!defined($user) || !defined($password)); if($secret && $basicAuth =~ m/^{.*}$/) { $pwok = eval $basicAuth; if($@) { Log3 $aName, 1, "basicAuth expression: $@"; $pwok = 2; } else { $pwok = ($pwok ? 1 : 2); } } elsif($basicAuth =~ m/^SHA256:(.{8}):(.*)$/) { if($allowed_haveSha) { $pwok = (Digest::SHA::sha256_base64("$1:$user:$password") eq $2 ? 1 : 2); } else { Log3 $me, 3, "Cannot load Digest::SHA to decode $aName basicAuth"; $pwok = 2; } } $cl->{AuthenticatedUser} = $user if($user); return $pwok; } 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 "allowedDevicesRegexp" || $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 { my $ca = $modules{$defs{$_}{TYPE}}{CanAuthenticate}; push(@F_arr, $_) if($ca == 1); push(@t_arr, $_) if($ca == 2); "" } grep { !$defs{$_}{SNAME} && $modules{$defs{$_}{TYPE}}{CanAuthenticate} } sort keys %defs; my $r = " $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"