############################################## # $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 EOF } 1; =pod =item helper =item summary authorize command execution based on frontend =item summary_DE authorisiert Befehlsausführung basierend auf dem Frontend =begin html

allowed

=end html =begin html_DE

allowed

=end html_DE =cut