admin.cgi 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  1. #!/usr/bin/perl -w
  2. ################################################################
  3. # $Id:$
  4. # vim: ts=2:et
  5. #
  6. # (c) 2012 Copyright: Martin Fischer (m_fischer at gmx dot de)
  7. # All rights reserved
  8. #
  9. # This script free software; you can redistribute it and/or modify
  10. # it under the terms of the GNU General Public License as published by
  11. # the Free Software Foundation; either version 2 of the License, or
  12. # any later version.
  13. #
  14. # The GNU General Public License can be found at
  15. # http://www.gnu.org/copyleft/gpl.html.
  16. # A copy is found in the textfile GPL.txt and important notices to the license
  17. # from the author is found in LICENSE.txt distributed with these scripts.
  18. #
  19. # This script is distributed in the hope that it will be useful,
  20. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  21. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  22. # GNU General Public License for more details.
  23. #
  24. ################################################################
  25. use CGI qw(:standard :html3 :header Vars);
  26. use CGI::Carp qw(warningsToBrowser fatalsToBrowser carpout);
  27. use CGI::Session;
  28. use DBI; #requires libdbd-sqlite3-perl
  29. use File::Copy;
  30. use LWP::Simple;
  31. use IO::Uncompress::Gunzip qw(gunzip $GunzipError);
  32. use lib "./lib";
  33. use Geo::IP;
  34. use strict;
  35. use warnings;
  36. no warnings 'uninitialized';
  37. # directory cointains databases
  38. my $datadir = "./data";
  39. # geo ip database file from http://www.maxmind.com/download/geoip/database/
  40. # should be updated once per month
  41. my $geoIPDat = "$datadir/GeoLiteCity.dat";
  42. # database
  43. my $dbf = "$datadir/fhem_statistics_db.sqlite";
  44. my $dsn = "dbi:SQLite:dbname=$dbf";
  45. my $sth;
  46. # requirements for housekeeping;
  47. my $controlFileURL = "http://fhem.de/fhemupdate4/svn/controls_fhem.txt";
  48. # fhem node
  49. my $ua = $ENV{HTTP_USER_AGENT};
  50. my $ip = $ENV{REMOTE_ADDR};
  51. # cascading style sheets
  52. my $css = "http://fhem.de/../css/style.css";
  53. my $myStyle=<<END;
  54. ul.menu {
  55. margin: 0;
  56. padding: 0;
  57. }
  58. ul.menu li {
  59. list-style: none;
  60. display: inline;
  61. margin: 0;
  62. padding-right: 2px;
  63. }
  64. END
  65. my $dbh = DBI->connect($dsn,"","", { RaiseError => 1, ShowErrorStatement => 1 }) ||
  66. die "Cannot connect: $DBI::errstr";
  67. my $cgi = new CGI;
  68. my $session = new CGI::Session(undef, $cgi, {Directory=>'/tmp'});
  69. my $cookie = $cgi->cookie(CGISESSID => $session->id );
  70. &init($cgi,$session);
  71. if($session->param("~login-trials") >= 3) {
  72. print $cgi->header(),
  73. $cgi->start_html(
  74. -title => 'fhem.de - Statistics Maintainance mode',
  75. -author => 'm_fischer@gmx.de',
  76. -base => 'true',
  77. -style => {-src => $css,-code => $myStyle},
  78. ),
  79. $cgi->p("You failed 3 times in a row.<br>" .
  80. "Your session is blocked. Please contact us with the details of your action"
  81. ),
  82. $cgi->end_html;
  83. exit(0);
  84. }
  85. unless($session->param("~logged-in")) {
  86. print login_page($cgi,$session);
  87. exit(0);
  88. }
  89. &maintainance($cgi,$session);
  90. exit(0);
  91. ########################################
  92. sub login_page {
  93. my ($cgi,$session) = @_;
  94. print $cgi->header(-cookie=>$cookie),
  95. $cgi->start_html(
  96. -title => 'fhem.de - Statistics Maintainance mode',
  97. -author => 'm_fischer@gmx.de',
  98. -base => 'true',
  99. -style => {-src => $css,-code => $myStyle},
  100. ),
  101. $cgi->h3("fhem.de - Statistics Maintainance mode"),
  102. $cgi->start_form,
  103. $cgi->hidden(-name=>'_cmd',-value=>$cgi->param('_cmd')),
  104. $cgi->hidden(-name=>'_act',-value=>$cgi->param('_act')),
  105. $cgi->strong("<code>Username: </code>"),
  106. $cgi->textfield(-name=>'username'),br,
  107. $cgi->strong("<code>Password: </code>"),
  108. $cgi->password_field(-name=>'password'),br,
  109. $cgi->submit(-value=>'Login'),
  110. $cgi->end_form,
  111. $cgi->end_html;
  112. }
  113. ########################################
  114. sub init($$) {
  115. my ($cgi,$session) = @_;
  116. if($session->param("~logged-in")) {
  117. return 1;
  118. }
  119. my $username = $cgi->param("username") or return;
  120. my $password = $cgi->param("password") or return;
  121. if(my $profile = authUser($username,$password)) {
  122. $session->param("~profile", $profile);
  123. $session->param("~logged-in", 1);
  124. $session->clear(["~login-trials"]);
  125. return 1;
  126. }
  127. my $trials = $session->param("~login-trials") || 0;
  128. return $session->param("~login-trials", ++$trials);
  129. }
  130. ########################################
  131. sub authUser($$) {
  132. my ($username,$password) = @_;
  133. my %credentials;
  134. my $fh;
  135. if(open($fh,"<$datadir/.maintainance.pwd")) {
  136. while (my $line = <$fh>) {
  137. chomp $line;
  138. my ($user,$pass) = split(":",$line);
  139. $credentials{$user} = $pass;
  140. }
  141. close $fh;
  142. }
  143. if(exists $credentials{$username} &&
  144. crypt($password,"Fhem") eq $credentials{$username}) {
  145. my $p_mask = "x" . length($credentials{$username});
  146. return {username=>$username, password=>$p_mask};
  147. }
  148. return undef;
  149. }
  150. ########################################
  151. sub maintainance($$) {
  152. my ($cgi,$session) = @_;
  153. my $url = url(-path_info=>1);
  154. my $profile = $session->param("~profile");
  155. my @geo = getLocation($ip);
  156. if($cgi->param("_file")) {
  157. &cmdDownload($cgi,$session,param("_file"));
  158. }
  159. print $cgi->header(),
  160. $cgi->start_html(
  161. -title => 'fhem.de - Statistics Maintainance mode',
  162. -author => 'm_fischer@gmx.de',
  163. -base => 'true',
  164. -style => {-src => $css,-code => $myStyle},
  165. ),
  166. $cgi->h3("fhem.de - Statistics Maintainance mode");
  167. print $cgi->p("Welcome $profile->{username} ..."),
  168. $cgi->p("IP: $ip, countryname:$geo[2] city:$geo[5] lat:$geo[6] lon:$geo[7]"),
  169. $cgi->ul({-class=>'menu'},
  170. $cgi->li([
  171. "<span>[</span>",
  172. $cgi->a({href=>$url},"home"),
  173. "<span>|</span>",
  174. $cgi->a({href=>$url."?_cmd=backup"},"backup"),
  175. "<span>|</span>",
  176. $cgi->a({href=>$url."?_cmd=dir"},"dir"),
  177. "<span>|</span>",
  178. $cgi->a({href=>$url."?_cmd=housekeeping"},"housekeeping"),
  179. "<span>|</span>",
  180. $cgi->a({href=>$url."?_cmd=update"},"update"),
  181. "<span>|</span>",
  182. $cgi->a({href=>$url."?_cmd=help"},"help"),
  183. "<span>|</span>",
  184. $cgi->a({href=>"http://fhem.de/stats/statistics.cgi",-target=>'_blank'},"view statistics"),
  185. "<span>|</span>",
  186. $cgi->a({href=>$url."?_cmd=logout"},"logout"),
  187. "<span>]</span>",
  188. ])
  189. ),
  190. $cgi->hr;
  191. my $cmd = $cgi->param("_cmd");
  192. my $act = $cgi->param("_act");
  193. if($cmd) {
  194. my $error;
  195. my @t = localtime;
  196. my $timeNow = sprintf("%04d%02d%02d-%02d%02d%02d",$t[5]+1900, $t[4]+1, $t[3], $t[2], $t[1], $t[0]);
  197. my $ret;
  198. if($cmd eq "help") {
  199. &cmdHelp($cgi,$session);
  200. } elsif($cmd eq "backup") {
  201. &cmdBackup($cgi,$session,$act);
  202. } elsif($cmd eq "dir") {
  203. &cmdDir($cgi,$session);
  204. } elsif($cmd eq "housekeeping") {
  205. &cmdHousekeeping($cgi,$session,$act);
  206. } elsif($cmd eq "update") {
  207. &cmdUpdate($cgi,$session,$act);
  208. } elsif($cmd eq "logout") {
  209. $session->clear(["~logged-in"]);
  210. print "<META HTTP-EQUIV=refresh CONTENT=\"1;URL=$url\">\n";
  211. }
  212. if($error) {
  213. print $cgi->p("Error: $error");
  214. }
  215. }
  216. print end_html;
  217. }
  218. ########################################
  219. sub cmdHelp($$) {
  220. my ($cgi,$session) = @_;
  221. print $cgi->h4("Help"),
  222. $cgi->table({-border=>0,-cellpadding=>'5'},
  223. $cgi->Tr({-align=>'left',-valign=>'top'},
  224. [
  225. $cgi->th([
  226. "command",
  227. "action",
  228. "short description"
  229. ]),
  230. $cgi->td([
  231. "<code>help</code>",
  232. "",
  233. "<code>show this info.</code>"
  234. ]),
  235. $cgi->td([
  236. "<code>backup</code>",
  237. "<code>statistics</code>",
  238. "<code>backup statisitc database with timestamp extension</code>"
  239. ]),
  240. $cgi->td([
  241. "<code>backup</code>",
  242. "<code>geoip</code>",
  243. "<code>backup geoip databae with timestamp extension</code>"
  244. ]),
  245. $cgi->td([
  246. "<code>dir</code>",
  247. "",
  248. "<code>show content of datadir '$datadir'</code>"
  249. ]),
  250. $cgi->td([
  251. "<code>housekeeping</code>",
  252. "<code>modules</code>",
  253. "<code>get controlfile from '$controlFileURL' and remove inofficial modules from table 'modules'</code>"
  254. ]),
  255. $cgi->td([
  256. "<code>update</code>",
  257. "<code>geoip</code>",
  258. "<code>get new version of geoip database 'GeoLiteCity.dat', unzip and install it.</code>"
  259. ]),
  260. ]
  261. )
  262. );
  263. return undef;
  264. }
  265. ########################################
  266. sub cmdBackup($$$) {
  267. my ($cgi,$session,$act) = @_;
  268. my $url = url(-path_info=>1);
  269. my $timeNow = TimeNow();
  270. my $error;
  271. print $cgi->h4("Backup"),
  272. $cgi->ul({-class=>'menu'},
  273. $cgi->li([
  274. "<span>[</span>",
  275. $cgi->a({href=>$url."?_cmd=backup;_act=statistics"},"statistics database "),
  276. "<span>|</span>",
  277. $cgi->a({href=>$url."?_cmd=backup;_act=geoip"},"geoip database "),
  278. "<span>|</span>",
  279. $cgi->a({href=>$url."?_cmd=backup;_act=download;_file=$dbf"},"download statistics database "),
  280. "<span>]</span>",
  281. ])
  282. );
  283. if($act eq "statistics") {
  284. print $cgi->h5("backup $dbf");
  285. copy($dbf,$dbf."-".$timeNow) or $error = "Copy failed: $!";
  286. print $cgi->p("<code>copy $dbf to $dbf-$timeNow done.</code>");
  287. }
  288. if($act eq "geoip") {
  289. print $cgi->h5("backup $geoIPDat");
  290. copy($geoIPDat,$geoIPDat."-".$timeNow) or $error = "Copy failed: $!";
  291. print $cgi->p("<code>copy $geoIPDat to $geoIPDat-$timeNow done.</code>");
  292. }
  293. if($error) {
  294. print $cgi->p("Error: $error");
  295. }
  296. return undef;
  297. }
  298. ########################################
  299. sub cmdDownload($$$) {
  300. my ($cgi,$session,$file) = @_;
  301. my $error;
  302. my $filename = substr $file,rindex($file,'/')+1;
  303. open(my $DLFILE,"<$file") or $error = "Open failed: $!";
  304. print $cgi->header(-type => 'application/x-download',
  305. -attachment => $filename,
  306. -Content_length => -s "$file",
  307. );
  308. binmode $DLFILE;
  309. print while <$DLFILE>;
  310. undef ($DLFILE);
  311. if($error) {
  312. print $cgi->p("Error: $error");
  313. }
  314. }
  315. ########################################
  316. sub cmdDir($$$) {
  317. my ($cgi,$session,$act) = @_;
  318. my $error;
  319. print $cgi->h4("Content of directory $datadir");
  320. opendir(my $dh, $datadir) or $error = "Can't opendir $datadir: $!";
  321. my @dir = grep { !/^\./ && -f "$datadir/$_" } readdir($dh);
  322. closedir $dh;
  323. for my $file (sort @dir) {
  324. print $cgi->code($file),$cgi->br;
  325. }
  326. if($error) {
  327. print $cgi->p("Error: $error");
  328. }
  329. return undef;
  330. }
  331. ########################################
  332. sub cmdUpdate($$$) {
  333. my ($cgi,$session,$act) = @_;
  334. my $url = url(-path_info=>1);
  335. my $timeNow = TimeNow();
  336. my $error;
  337. print $cgi->h4("Update"),
  338. $cgi->ul({-class=>'menu'},
  339. $cgi->li([
  340. "<span>[</span>",
  341. $cgi->a({href=>$url."?_cmd=update;_act=geoip"},"GeoLiteCity.dat"),
  342. "<span>]</span>",
  343. ])
  344. );
  345. if($act eq "geoip") {
  346. print $cgi->h5("update GeoLiteCity.dat");
  347. my $url = "http://geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz";
  348. my $infile = "$datadir/GeoLiteCity.dat.gz";
  349. my $outfile = "$datadir/GeoLiteCity.dat";
  350. my $data = getstore($url,$infile);
  351. if($data == "200") {
  352. copy($geoIPDat,$geoIPDat."-".$timeNow) or $error = "Copy failed: $!";
  353. print $cgi->p("<code>copy $geoIPDat to $geoIPDat-$timeNow done.</code>");
  354. gunzip $infile => $outfile or $error = "gunzip failed: $GunzipError";
  355. print $cgi->p("<code>New $outfile installed.</code>");
  356. } else {
  357. $error = "response for $infile: $data";
  358. }
  359. }
  360. if($error) {
  361. print $cgi->p("Error: $error");
  362. }
  363. return undef;
  364. }
  365. ########################################
  366. sub cmdHousekeeping($$$) {
  367. my ($cgi,$session,$act) = @_;
  368. my $url = url(-path_info=>1);
  369. my $error;
  370. print $cgi->h4("Housekeeping"),
  371. $cgi->ul({-class=>'menu'},
  372. $cgi->li([
  373. "<span>[</span>",
  374. $cgi->a({href=>$url."?_cmd=housekeeping;_act=modules"},"remove inofficial modules"),
  375. "<span>]</span>",
  376. ])
  377. );
  378. if($act eq "modules") {
  379. my $control = get($controlFileURL);
  380. my $control_ref = {};
  381. ($error,$control_ref) = parseControlFile("fhem",$control,$control_ref,0);
  382. print $cgi->h5("Housekeeping for table 'modules'");
  383. my @ignoreColumns = qw(Global uniqueID);
  384. my %columnOld = %{ $dbh->column_info(undef, undef, 'modules', undef)->fetchall_hashref('COLUMN_NAME') };
  385. my %columnNew = %columnOld;
  386. my $removeColumns;
  387. foreach my $col (sort keys %columnOld) {
  388. if(!exists $control_ref->{$col} && !grep {/$col/} @ignoreColumns) {
  389. delete $columnNew{$col};
  390. $removeColumns .= "$col ";
  391. }
  392. }
  393. if(!$removeColumns) {
  394. print $cgi->p("<p><code>inofficial modules found:<br />none</code>");
  395. } else {
  396. print $cgi->p("<p><code>inofficial modules found:<br />$removeColumns</code>");
  397. copy($dbf,$dbf."-".TimeNow()) or $error = "Copy of $dbf failed: $!";
  398. if(!$error) {
  399. delete $columnNew{uniqueID};
  400. my $createTable = "CREATE TABLE modules (uniqueID VARCHAR(32) PRIMARY KEY UNIQUE";
  401. my $selectColumns = "uniqueID";
  402. foreach my $col (sort keys %columnNew) {
  403. $createTable .= ", $col INTEGER DEFAULT 0";
  404. $selectColumns .= ", $col";
  405. }
  406. $createTable .= ");";
  407. my $sql;
  408. $sql = "ALTER TABLE 'modules' RENAME TO 'modules_old';";
  409. print $cgi->p("<code>sql:<br />$sql</code>");
  410. $dbh->do($sql);
  411. $sql = $createTable;
  412. print $cgi->p("<code>sql:<br />$sql</code>");
  413. $dbh->do($sql);
  414. $sql = "INSERT INTO 'modules' ($selectColumns) SELECT $selectColumns FROM 'modules_old';";
  415. print $cgi->p("<code>sql:<br />$sql</code>");
  416. $dbh->do($sql);
  417. $sql = "DROP TABLE 'modules_old';";
  418. print $cgi->p("<code>sql:<br />$sql</code>");
  419. $dbh->do($sql);
  420. }
  421. }
  422. }
  423. if($error) {
  424. print $cgi->p("Error: $error");
  425. }
  426. return undef;
  427. }
  428. ########################################
  429. sub parseControlFile($$$$) {
  430. my ($pack,$controlFile,$control_ref,$local) = @_;
  431. my %control = %$control_ref if ($control_ref && ref($control_ref) eq "HASH");
  432. my $from = ($local ? "local" : "remote");
  433. my $ret;
  434. if ($local) {
  435. my $str = "";
  436. # read local controlfile in string
  437. if (open FH, "$controlFile") {
  438. $str = do { local $/; <FH> };
  439. }
  440. close(FH);
  441. $controlFile = $str
  442. }
  443. # parse file
  444. if ($controlFile) {
  445. foreach my $l (split("[\r\n]", $controlFile)) {
  446. chomp($l);
  447. my ($ctrl,$date,$size,$file,$move) = "";
  448. if ($l =~ m/^(UPD) (20\d\d-\d\d-\d\d_\d\d:\d\d:\d\d) (\d+) (\S+)$/) {
  449. $ctrl = $1;
  450. $date = $2;
  451. $size = $3;
  452. $file = $4;
  453. } elsif ($l =~ m/^(DIR) (\S+)$/) {
  454. $ctrl = $1;
  455. $file = $2;
  456. } elsif ($l =~ m/^(MOV) (\S+) (\S+)$/) {
  457. $ctrl = $1;
  458. $file = $2;
  459. $move = $3;
  460. } elsif ($l =~ m/^(DEL) (\S+)$/) {
  461. $ctrl = $1;
  462. $file = $2;
  463. } else {
  464. $ctrl = "ESC"
  465. }
  466. if ($ctrl eq "ESC") {
  467. $ret = "File 'controls_".lc($pack).".txt' ($from) is corrupt";
  468. }
  469. last if ($ret);
  470. if ($l =~ m/^UPD/ && $file =~ m/^FHEM/) {
  471. if ($file =~ m/^.*(\d\d_)(.*).pm$/) {
  472. my $modName = $2;
  473. $control{$modName} = $file;
  474. }
  475. }
  476. }
  477. }
  478. return ($ret, \%control);
  479. }
  480. ########################################
  481. sub getLocation($) {
  482. my ($ip) = shift;
  483. my $gi = Geo::IP->open($geoIPDat, GEOIP_STANDARD);
  484. my $rec = $gi->record_by_addr($ip);
  485. if(!$rec) {
  486. return;
  487. } else {
  488. return (
  489. $rec->country_code,$rec->country_code3,$rec->country_name,$rec->region,$rec->region_name,$rec->city,
  490. $rec->latitude,$rec->longitude,$rec->time_zone,$rec->continent_code
  491. );
  492. }
  493. }
  494. ########################################
  495. sub TimeNow() {
  496. my @t = localtime;
  497. return sprintf("%04d%02d%02d-%02d%02d%02d",$t[5]+1900, $t[4]+1, $t[3], $t[2], $t[1], $t[0]);
  498. }
  499. 1;