96_allowed.pm 16 KB

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