96_allowed.pm 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  1. ##############################################
  2. # $Id: 96_allowed.pm 14888 2017-08-13 12:07:12Z rudolfkoenig $
  3. package main;
  4. use strict;
  5. use warnings;
  6. use vars qw(@FW_httpheader); # HTTP header, line by line
  7. my $allowed_haveSha;
  8. #####################################
  9. sub
  10. allowed_Initialize($)
  11. {
  12. my ($hash) = @_;
  13. $hash->{DefFn} = "allowed_Define";
  14. $hash->{AuthorizeFn} = "allowed_Authorize";
  15. $hash->{AuthenticateFn} = "allowed_Authenticate";
  16. $hash->{SetFn} = "allowed_Set";
  17. $hash->{AttrFn} = "allowed_Attr";
  18. $hash->{AttrList} = "disable:0,1 validFor allowedCommands allowedDevices ".
  19. "basicAuth basicAuthMsg password globalpassword ".
  20. "basicAuthExpiry";
  21. $hash->{UndefFn} = "allowed_Undef";
  22. $hash->{FW_detailFn} = "allowed_fhemwebFn";
  23. eval { require Digest::SHA; };
  24. if($@) {
  25. Log3 $hash, 4, $@;
  26. $allowed_haveSha = 0;
  27. } else {
  28. $allowed_haveSha = 1;
  29. }
  30. }
  31. #####################################
  32. sub
  33. allowed_Define($$)
  34. {
  35. my ($hash, $def) = @_;
  36. my @l = split(" ", $def);
  37. if(@l > 2) {
  38. my %list;
  39. for(my $i=2; $i<@l; $i++) {
  40. $list{$l[$i]} = 1;
  41. }
  42. $hash->{devices} = \%list;
  43. }
  44. $auth_refresh = 1;
  45. readingsSingleUpdate($hash, "state", "validFor:", 0);
  46. return undef;
  47. }
  48. sub
  49. allowed_Undef($$)
  50. {
  51. $auth_refresh = 1;
  52. return undef;
  53. }
  54. #####################################
  55. # Return 0 for don't care, 1 for Allowed, 2 for forbidden.
  56. sub
  57. allowed_Authorize($$$$)
  58. {
  59. my ($me, $cl, $type, $arg) = @_;
  60. return 0 if($me->{disabled});
  61. if( $cl->{SNAME} ) {
  62. return 0 if(!$me->{validFor} || $me->{validFor} !~ m/\b$cl->{SNAME}\b/);
  63. } else {
  64. return 0 if(!$me->{validFor} || $me->{validFor} !~ m/\b$cl->{NAME}\b/);
  65. }
  66. if($type eq "cmd") {
  67. return 0 if(!$me->{allowedCommands});
  68. # Return 0: allow stacking with other instances, see Forum#46380
  69. return ($me->{allowedCommands} =~ m/\b\Q$arg\E\b/) ? 0 : 2;
  70. }
  71. if($type eq "devicename") {
  72. return 0 if(!$me->{allowedDevices});
  73. return ($me->{allowedDevices} =~ m/\b\Q$arg\E\b/) ? 0 : 2;
  74. }
  75. return 0;
  76. }
  77. #####################################
  78. # Return 0 for authentication not needed, 1 for auth-ok, 2 for wrong password
  79. sub
  80. allowed_Authenticate($$$$)
  81. {
  82. my ($me, $cl, $param) = @_;
  83. return 0 if($me->{disabled});
  84. return 0 if(!$me->{validFor} || $me->{validFor} !~ m/\b$cl->{SNAME}\b/);
  85. my $aName = $me->{NAME};
  86. if($cl->{TYPE} eq "FHEMWEB") {
  87. my $basicAuth = AttrVal($aName, "basicAuth", undef);
  88. delete $cl->{".httpAuthHeader"};
  89. return 0 if(!$basicAuth);
  90. my $FW_httpheader = $param;
  91. my $secret = $FW_httpheader->{Authorization};
  92. $secret =~ s/^Basic //i if($secret);
  93. # Check for Cookie in headers if no basicAuth header is set
  94. my $authcookie;
  95. if (!$secret && $FW_httpheader->{Cookie}) {
  96. if(AttrVal($aName, "basicAuthExpiry", 0)) {
  97. my $cookie = "; ".$FW_httpheader->{Cookie}.";";
  98. $authcookie = $1 if ( $cookie =~ /; AuthToken=([^;]+);/ );
  99. $secret = $authcookie;
  100. }
  101. }
  102. my $pwok = ($secret && $secret eq $basicAuth); # Base64
  103. my ($user, $password) = split(":", decode_base64($secret)) if($secret);
  104. ($user,$password) = ("","") if(!defined($user) || !defined($password));
  105. if($secret && $basicAuth =~ m/^{.*}$/) {
  106. eval "use MIME::Base64";
  107. if($@) {
  108. Log3 $aName, 1, $@;
  109. } else {
  110. $pwok = eval $basicAuth;
  111. Log3 $aName, 1, "basicAuth expression: $@" if($@);
  112. }
  113. } elsif($basicAuth =~ m/^SHA256:(.{8}):(.*)$/) {
  114. if($allowed_haveSha) {
  115. $pwok = Digest::SHA::sha256_base64("$1:$user:$password") eq $2;
  116. } else {
  117. Log3 $me, 3, "Cant load Digest::SHA to decode $me->{NAME} beiscAuth";
  118. }
  119. }
  120. # Add Cookie header ONLY if authentication with basicAuth was succesful
  121. if($pwok && (!defined($authcookie) || $secret ne $authcookie)) {
  122. my $time = AttrVal($aName, "basicAuthExpiry", 0);
  123. if ( $time ) {
  124. $time = int($time*86400+time());
  125. # generate timestamp according to RFC-1130 in Expires
  126. my $expires = FmtDateTimeRFC1123($time);
  127. readingsBeginUpdate($me);
  128. readingsBulkUpdate($me,'lastAuthUser', $user, 1);
  129. readingsBulkUpdate($me,'lastAuthExpires', $time, 1);
  130. readingsBulkUpdate($me,'lastAuthExpiresFmt', $expires, 1);
  131. readingsEndUpdate($me, 1);
  132. # set header with expiry
  133. $cl->{".httpAuthHeader"} = "Set-Cookie: AuthToken=".$secret.
  134. "; Path=/ ; Expires=$expires\r\n" ;
  135. }
  136. }
  137. return 1 if($pwok);
  138. my $msg = AttrVal($aName, "basicAuthMsg", "FHEM: login required");
  139. $cl->{".httpAuthHeader"} = "HTTP/1.1 401 Authorization Required\r\n".
  140. "WWW-Authenticate: Basic realm=\"$msg\"\r\n";
  141. return 2;
  142. }
  143. if($cl->{TYPE} eq "telnet") {
  144. my $pw = AttrVal($aName, "password", undef);
  145. if(!$pw) {
  146. $pw = AttrVal($aName, "globalpassword", undef);
  147. $pw = undef if($pw && $cl->{NAME} =~ m/_127.0.0.1_/);
  148. }
  149. return 0 if(!$pw);
  150. return 2 if(!defined($param));
  151. if($pw =~ m/^{.*}$/) {
  152. my $password = $param;
  153. my $ret = eval $pw;
  154. Log3 $aName, 1, "password expression: $@" if($@);
  155. return ($ret ? 1 : 2);
  156. } elsif($pw =~ m/^SHA256:(.{8}):(.*)$/) {
  157. if($allowed_haveSha) {
  158. return (Digest::SHA::sha256_base64("$1:$param") eq $2) ? 1 : 2;
  159. } else {
  160. Log3 $me, 3, "Cant load Digest::SHA to decode $me->{NAME} beiscAuth";
  161. }
  162. }
  163. return ($pw eq $param) ? 1 : 2;
  164. }
  165. return 0;
  166. }
  167. sub
  168. allowed_Set(@)
  169. {
  170. my ($hash, @a) = @_;
  171. my %sets = (globalpassword=>1, password=>1, basicAuth=>2);
  172. return "no set argument specified" if(int(@a) < 2);
  173. return "Unknown argument $a[1], choose one of ".join(" ",sort keys %sets)
  174. if(!defined($sets{$a[1]}));
  175. return "$a[1] needs $sets{$a[1]} parameters"
  176. if(@a-2 != $sets{$a[1]});
  177. return "Cannot load Digest::SHA" if(!$allowed_haveSha);
  178. my $plain = ($a[1] eq "basicAuth" ? "$a[2]:$a[3]" : $a[2]);
  179. my ($x,$y) = gettimeofday();
  180. my $salt = substr(sprintf("%08X", rand($y)*rand($x)),0,8);
  181. CommandAttr($hash->{CL}, "$a[0] $a[1] SHA256:$salt:".
  182. Digest::SHA::sha256_base64("$salt:$plain"));
  183. }
  184. sub
  185. allowed_Attr(@)
  186. {
  187. my ($type, $devName, $attrName, @param) = @_;
  188. my $hash = $defs{$devName};
  189. my $set = ($type eq "del" ? 0 : (!defined($param[0]) || $param[0]) ? 1 : 0);
  190. if($attrName eq "disable") {
  191. readingsSingleUpdate($hash, "state", $set ? "disabled" : "active", 1);
  192. if($set) {
  193. $hash->{disabled} = 1;
  194. } else {
  195. delete($hash->{disabled});
  196. }
  197. } elsif($attrName eq "allowedCommands" || # hoping for some speedup
  198. $attrName eq "allowedDevices" ||
  199. $attrName eq "validFor") {
  200. if($set) {
  201. $hash->{$attrName} = join(" ", @param);
  202. } else {
  203. delete($hash->{$attrName});
  204. }
  205. readingsSingleUpdate($hash, "state", "validFor:".join(",",@param), 1)
  206. if($attrName eq "validFor");
  207. } elsif(($attrName eq "basicAuth" ||
  208. $attrName eq "password" || $attrName eq "globalpassword") &&
  209. $type eq "set") {
  210. foreach my $d (devspec2array("TYPE=(FHEMWEB|telnet)")) {
  211. delete $defs{$d}{Authenticated} if($defs{$d});
  212. }
  213. }
  214. return undef;
  215. }
  216. #########################
  217. sub
  218. allowed_fhemwebFn($$$$)
  219. {
  220. my ($FW_wname, $d, $room, $pageHash) = @_; # pageHash is set for summaryFn.
  221. my $hash = $defs{$d};
  222. my $vf = $defs{$d}{validFor} ? $defs{$d}{validFor} : "";
  223. my @arr = map { "<input type='checkbox' ".($vf =~ m/\b$_\b/ ? "checked ":"").
  224. "name='$_' class='vfAttr'><label>$_</label>" }
  225. grep { !$defs{$_}{SNAME} }
  226. devspec2array("TYPE=(FHEMWEB|telnet)");
  227. return "<input id='vfAttr' type='button' value='attr'> $d validFor <ul>".
  228. join("<br>",@arr)."</ul><script>var dev='$d';".<<'EOF';
  229. $("#vfAttr").click(function(){
  230. var names=[];
  231. $("input.vfAttr:checked").each(function(){names.push($(this).attr("name"))});
  232. FW_cmd(FW_root+"?cmd=attr "+dev+" validFor "+names.join(",")+"&XHR=1");
  233. });
  234. </script>
  235. EOF
  236. }
  237. 1;
  238. =pod
  239. =item helper
  240. =item summary authorize command execution based on frontend
  241. =item summary_DE authorisiert Befehlsausf&uuml;hrung basierend auf dem Frontend
  242. =begin html
  243. <a name="allowed"></a>
  244. <h3>allowed</h3>
  245. <ul>
  246. <br>
  247. <a name="alloweddefine"></a>
  248. <b>Define</b>
  249. <ul>
  250. <code>define &lt;name&gt; allowed &lt;deviceList&gt;</code>
  251. <br><br>
  252. Authorize execution of commands and modification of devices based on the
  253. frontend used and/or authenticate users.<br><br>
  254. If there are multiple instances defined which are valid for a given
  255. frontend device, then all authorizations must succeed. For authentication
  256. it is sufficient when one of the instances succeeds. The checks are
  257. executed in alphabetical order of the allowed instance names.<br><br>
  258. <b>Note:</b> this module should work as intended, but no guarantee
  259. can be given that there is no way to circumvent it.<br><br>
  260. Examples:
  261. <ul><code>
  262. define allowedWEB allowed<br>
  263. attr allowedWEB validFor WEB,WEBphone,WEBtablet<br>
  264. attr allowedWEB basicAuth { "$user:$password" eq "admin:secret" }<br>
  265. attr allowedWEB allowedCommands set,get<br><br>
  266. define allowedTelnet allowed<br>
  267. attr allowedTelnet validFor telnetPort<br>
  268. attr allowedTelnet password secret<br>
  269. </code></ul>
  270. <br>
  271. </ul>
  272. <a name="allowedset"></a>
  273. <b>Set</b>
  274. <ul>
  275. <li>basicAuth &lt;username&gt; &lt;password&gt;</li>
  276. <li>password &lt;password&gt;</li>
  277. <li>globalpassword &lt;password&gt;<br>
  278. these commands set the corresponding attribute, by computing an SHA256
  279. hash from the arguments and a salt. Note: the perl module Device::SHA is
  280. needed.
  281. </li>
  282. </ul><br>
  283. <a name="allowedget"></a>
  284. <b>Get</b> <ul>N/A</ul><br>
  285. <a name="allowedattr"></a>
  286. <b>Attributes</b>
  287. <ul>
  288. <li><a href="#disable">disable</a></li><br>
  289. <a name="allowedCommands"></a>
  290. <li>allowedCommands<br>
  291. A comma separated list of commands allowed from the matching frontend
  292. (see validFor).<br>
  293. If set to an empty list <code>, (i.e. comma only)</code>
  294. then no comands are allowed. If set to <code>get,set</code>, then only
  295. a "regular" usage is allowed via set and get, but changing any
  296. configuration is forbidden.<br>
  297. </li><br>
  298. <a name="allowedDevices"></a>
  299. <li>allowedDevices<br>
  300. A comma separated list of device names which can be manipulated via the
  301. matching frontend (see validFor).
  302. </li><br>
  303. <a name="basicAuth"></a>
  304. <li>basicAuth, basicAuthMsg<br>
  305. request a username/password authentication for FHEMWEB access.
  306. It can be a base64 encoded string of user:password, an SHA256 hash
  307. (which should be set via the corresponding set command) or a perl
  308. expression if enclosed in {}, where $user and $password are set, and
  309. which returns true if accepted or false if not. Examples:
  310. <ul><code>
  311. attr allowed basicAuth ZmhlbXVzZXI6c2VjcmV0<br>
  312. attr allowed basicAuth SHA256:F87740B5:q8dHeiClaPLaWVsR/rqkzcBhw/JvvwVi4bEwKmJc/Is<br>
  313. attr allowed basicAuth {"$user:$password" eq "fhemuser:secret"}<br>
  314. </code></ul>
  315. If basicAuthMsg is set, it will be displayed in the popup window when
  316. requesting the username/password. Note: not all browsers support this
  317. feature.<br>
  318. </li><br>
  319. <a name="basicAuthExpiry"></a>
  320. <li>basicAuthExpiry<br>
  321. allow the basicAuth to be kept valid for a given number of days.
  322. So username/password as specified in basicAuth are only requested
  323. after a certain period.
  324. This is achieved by sending a cookie to the browser that will expire
  325. after the given period.
  326. Only valid if basicAuth is set.
  327. </li><br>
  328. <a name="password"></a>
  329. <li>password<br>
  330. Specify a password for telnet instances, which has to be entered as the
  331. very first string after the connection is established. The same rules
  332. apply as for basicAuth, with the expception that there is no user to be
  333. specified.<br>
  334. Note: if this attribute is set, you have to specify a password as the
  335. first argument when using fhem.pl in client mode:
  336. <ul>
  337. perl fhem.pl localhost:7072 secret "set lamp on"
  338. </ul>
  339. </li><br>
  340. <a name="globalpassword"></a>
  341. <li>globalpassword<br>
  342. Just like the attribute password, but a password will only required for
  343. non-local connections.
  344. </li><br>
  345. <a name="validFor"></a>
  346. <li>validFor<br>
  347. A comma separated list of frontend names. Currently supported frontends
  348. are all devices connected through the FHEM TCP/IP library, e.g. telnet
  349. and FHEMWEB. The allowed instance is only active, if this attribute is
  350. set.
  351. </li>
  352. </ul>
  353. <br>
  354. </ul>
  355. =end html
  356. =begin html_DE
  357. <a name="allowed"></a>
  358. <h3>allowed</h3>
  359. <ul>
  360. <br>
  361. <a name="alloweddefine"></a>
  362. <b>Define</b>
  363. <ul>
  364. <code>define &lt;name&gt; allowed &lt;deviceList&gt;</code>
  365. <br><br>
  366. Authorisiert das Ausf&uuml;hren von Kommandos oder das &Auml;ndern von
  367. Ger&auml;ten abh&auml;ngig vom verwendeten Frontend.<br>
  368. Falls man mehrere allowed Instanzen definiert hat, die f&uuml;r dasselbe
  369. Frontend verantwortlich sind, dann m&uuml;ssen alle Authorisierungen
  370. genehmigt sein, um das Befehl ausf&uuml;hren zu k&ouml;nnen. Auf der
  371. anderen Seite reicht es, wenn einer der Authentifizierungen positiv
  372. entschieden wird. Die Pr&uuml;fungen werden in alphabetischer Reihenfolge
  373. der Instanznamen ausgef&uuml;hrt. <br><br>
  374. <b>Achtung:</b> das Modul sollte wie hier beschrieben funktionieren,
  375. allerdings k&ouml;nnen wir keine Garantie geben, da&szlig; man sie nicht
  376. &uuml;berlisten, und Schaden anrichten kann.<br><br>
  377. Beispiele:
  378. <ul><code>
  379. define allowedWEB allowed<br>
  380. attr allowedWEB validFor WEB,WEBphone,WEBtablet<br>
  381. attr allowedWEB basicAuth { "$user:$password" eq "admin:secret" }<br>
  382. attr allowedWEB allowedCommands set,get<br><br>
  383. define allowedTelnet allowed<br>
  384. attr allowedTelnet validFor telnetPort<br>
  385. attr allowedTelnet password secret<br>
  386. </code></ul>
  387. <br>
  388. </ul>
  389. <a name="allowedset"></a>
  390. <b>Set</b>
  391. <ul>
  392. <li>basicAuth &lt;username&gt; &lt;password&gt;</li>
  393. <li>password &lt;password&gt;</li>
  394. <li>globalpassword &lt;password&gt;<br>
  395. diese Befehle setzen das entsprechende Attribut, indem sie aus den
  396. Parameter und ein Salt ein SHA256 Hashwert berechnen. Achtung: das perl
  397. Modul Device::SHA wird ben&ouml;tigt.
  398. </li>
  399. </ul><br>
  400. <a name="allowedget"></a>
  401. <b>Get</b> <ul>N/A</ul><br>
  402. <a name="allowedattr"></a>
  403. <b>Attribute</b>
  404. <ul>
  405. <li><a href="#disable">disable</a>
  406. </li><br>
  407. <a name="allowedCommands"></a>
  408. <li>allowedCommands<br>
  409. Eine Komma getrennte Liste der erlaubten Befehle des passenden
  410. Frontends (siehe validFor). Bei einer leeren Liste (, dh. nur ein
  411. Komma) wird dieser Frontend "read-only".
  412. Falls es auf <code>get,set</code> gesetzt ist, dann sind in dieser
  413. Frontend keine Konfigurations&auml;nderungen m&ouml;glich, nur
  414. "normale" Bedienung der Schalter/etc.
  415. </li><br>
  416. <a name="allowedDevices"></a>
  417. <li>allowedDevices<br>
  418. Komma getrennte Liste von Ger&auml;tenamen, die mit dem passenden
  419. Frontend (siehe validFor) ge&auml;ndert werden k&ouml;nnen.
  420. </li><br>
  421. <a name="basicAuth"></a>
  422. <li>basicAuth, basicAuthMsg<br>
  423. Erzwingt eine Authentifizierung mit Benutzername/Passwort f&uuml;r die
  424. zugerdnete FHEMWEB Instanzen. Der Wert kann entweder das base64
  425. kodierte Benutzername:Passwort sein, ein SHA256 hash (was man am besten
  426. mit dem passenden set Befehl erzeugt), oder, falls er in {}
  427. eingeschlossen ist, ein Perl Ausdruck. F&uuml;r Letzteres wird
  428. $user und $passwort gesetzt, und muss wahr zur&uuml;ckliefern, falls
  429. Benutzername und Passwort korrekt sind. Beispiele:
  430. <ul><code>
  431. attr allowed basicAuth ZmhlbXVzZXI6c2VjcmV0<br>
  432. attr allowed basicAuth SHA256:F87740B5:q8dHeiClaPLaWVsR/rqkzcBhw/JvvwVi4bEwKmJc/Is<br>
  433. attr allowed basicAuth {"$user:$password" eq "fhemuser:secret"}<br>
  434. </code></ul>
  435. basicAuthMsg wird (in manchen Browsern) in dem Passwort Dialog als
  436. &Uuml;berschrift angezeigt.<br>
  437. </li><br>
  438. <a name="password"></a>
  439. <li>password<br>
  440. Betrifft nur telnet Instanzen (siehe validFor): Bezeichnet ein
  441. Passwort, welches als allererster String eingegeben werden muss,
  442. nachdem die Verbindung aufgebaut wurde. F&uuml;r die Werte gelten die
  443. Regeln von basicAuth, mit der Ausnahme, dass nur Passwort und kein
  444. Benutzername spezifiziert wird.<br> Falls dieser Parameter gesetzt
  445. wird, sendet FHEM telnet IAC Requests, um ein Echo w&auml;hrend der
  446. Passworteingabe zu unterdr&uuml;cken. Ebenso werden alle
  447. zur&uuml;ckgegebenen Zeilen mit \r\n abgeschlossen.<br>
  448. Falls dieses Attribut gesetzt wird, muss als erstes Argument ein
  449. Passwort angegeben werden, wenn fhem.pl im Client-mode betrieben wird:
  450. <ul><code>
  451. perl fhem.pl localhost:7072 secret "set lamp on"
  452. </code></ul>
  453. </li><br>
  454. <a name="globalpassword"></a>
  455. <li>globalpassword<br>
  456. Betrifft nur telnet Instanzen (siehe validFor): Entspricht dem
  457. Attribut password; ein Passwort wird aber ausschlie&szlig;lich f&uuml;r
  458. nicht-lokale Verbindungen verlangt.
  459. </li><br>
  460. <a name="validFor"></a>
  461. <li>validFor<br>
  462. Komma separierte Liste von Frontend-Instanznamen. Aktuell werden nur
  463. Frontends unterst&uuml;tzt, die das FHEM TCP/IP Bibliothek verwenden,
  464. z.Bsp. telnet und FHEMWEB. Falls nicht gesetzt, ist die allowed Instanz
  465. nicht aktiv.
  466. </li>
  467. </ul>
  468. <br>
  469. </ul>
  470. =end html_DE
  471. =cut