You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

rspamd.cgi 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. #!/usr/bin/perl -w
  2. use strict;
  3. use warnings;
  4. {
  5. package RspamdWebInterface;
  6. use strict;
  7. use Mail::Rspamd::Client;
  8. use CGI qw/:standard -debug/;
  9. use IO::Socket::INET;
  10. use IO::String;
  11. use Data::Dumper;
  12. my %cfg = (
  13. 'hosts' => ['localhost:11333'],
  14. 'timeout' => 1,
  15. 'password' => '',
  16. 'statfiles' => ['WINNOW_HAM', 'WINNOW_SPAM'],
  17. );
  18. sub new {
  19. my ($class, $args) = @_;
  20. $class = ref($class) || $class;
  21. my $self = {
  22. addr => 'localhost',
  23. port => 8080,
  24. standalone => 0,
  25. };
  26. if ($args->{'standalone'}) {
  27. $self->{'standalone'} = 1;
  28. }
  29. if ($args->{'port'}) {
  30. $self->{'port'} = $args->{'port'};
  31. }
  32. if ($args->{'addr'}) {
  33. $self->{'addr'} = $args->{'addr'};
  34. }
  35. if ($args->{'config'}) {
  36. open CFG, "< $args->{'config'}";
  37. my $cf;
  38. $cfg{'hosts'} = [];
  39. while (<CFG>) {
  40. chomp;
  41. push (@{$cfg{'hosts'}}, $_);
  42. }
  43. close CFG;
  44. }
  45. bless($self, $class);
  46. $self;
  47. }
  48. sub _handle_ping {
  49. my $self = shift;
  50. my (@servers_alive, @servers_dead);
  51. my $rspamd = Mail::Rspamd::Client->new({timeout=>$cfg{timeout}});
  52. my $number = 0;
  53. # Walk throught selection of servers
  54. foreach (@{ $cfg{'hosts'} }) {
  55. if ($rspamd->ping($_)) {
  56. push(@servers_alive, $_);
  57. }
  58. else {
  59. push(@servers_dead, $_);
  60. }
  61. $number ++;
  62. }
  63. print header;
  64. print qq!<select multiple="multiple" id="id_servers" name="servers" size="$number">!;
  65. foreach (@servers_alive) {
  66. print qq!<option value="$_" style="color:#8CC543">$_</option>!;
  67. }
  68. foreach (@servers_dead) {
  69. print qq!<option value="$_" style="color:#C51111" disable="disable">$_</option>!;
  70. }
  71. print "</select>";
  72. }
  73. sub _show_html {
  74. my $self = shift;
  75. print header,
  76. start_html(-title=>'Rspamd control', -script=>[{-type=>'JAVASCRIPT', -src=>'http://www.google.com/jsapi'},
  77. {-type=>'JAVASCRIPT', -code=>'google.load("jquery", "1");'}]),
  78. h1('Manage rspamd cluster'),
  79. start_form(-method=>'POST', -enctype=>&CGI::MULTIPART, -action=>$ENV{PATH_INFO}),
  80. "<label for=\"id_servers\">Servers:</label>",
  81. "<div id=\"servers_div\">",
  82. scrolling_list(-name => 'servers',
  83. -multiple=>'true',
  84. -values=>$cfg{'hosts'},
  85. -id=>'id_servers',
  86. ),
  87. "</div>",
  88. button(-name=>'ping',
  89. -value=>'Ping',
  90. -onClick=>'$.ajax({
  91. url: \'/ajax\',
  92. success: function(data) {
  93. $(\'#servers_div\').html(data);
  94. }
  95. });'),
  96. br,
  97. "<label for='id_command'>Command:</label>",
  98. popup_menu (-name=>'command',
  99. -values=>['symbols', 'check', 'stat', 'learn', 'fuzzy_add', 'fuzzy_del', 'weights', 'uptime'],
  100. -labels=> {
  101. 'symbols'=>'Scan symbols for message',
  102. 'check'=>'Check if message is spam',
  103. 'stat'=>'Check statistics',
  104. 'learn'=>'Learn rspamd with message',
  105. 'fuzzy_add'=>'Add fuzzy hash',
  106. 'fuzzy_del'=>'Delete fuzzy hash',
  107. 'weights'=>'Check weights of message',
  108. 'uptime'=>'Get uptime',
  109. },
  110. -id=>'id_command',
  111. ),
  112. br,
  113. "<label for=\"id_statfile\">Statfile:</label>",
  114. popup_menu(-name=>'statfile', -id=>'id_statfile', -values=>$cfg{'statfiles'}),
  115. br,
  116. "<label for=\"id_file\">File:</label>",
  117. filefield(-name=>'upload_file', -id=>'id_file'),
  118. br,
  119. "<label for=\"id_message\">Message text:</label>",
  120. textarea(-name=>'message', -id=>'id_message', -rows=>10, -columns=>80),
  121. br,
  122. "<label for=\"id_weight\">Weight of learn:</label>",
  123. textfield(-name=>'weight', -id=>'id_weight'),
  124. br,
  125. submit,
  126. end_form;
  127. print end_html;
  128. }
  129. sub _get_file {
  130. my $self = shift;
  131. my $fh = shift;
  132. my $output;
  133. my $buffer;
  134. if (! $fh) {
  135. return undef;
  136. }
  137. my $io_handle = $fh->handle;
  138. while (my $bytesread = $io_handle->read($buffer,1024)) {
  139. $output .= $buffer;
  140. }
  141. return $output;
  142. }
  143. sub _make_rfc822_message {
  144. my $self = shift;
  145. my $msg = shift;
  146. # Check whether first line is a header line
  147. if ($msg =~ /^[^:]+:\s*\S+$/) {
  148. # Assume that message has all headers
  149. return $msg;
  150. }
  151. else {
  152. my $output = <<EOT;
  153. Received: from localhost (localhost [127.0.0.1])
  154. by localhost (Postfix) with ESMTP id 5EC0D146;
  155. MIME-Version: 1.0
  156. Content-Type: text/plain; charset=UTF-8; format=flowed
  157. Content-Transfer-Encoding: 8bit
  158. Date: Thu, 1 Jan 1970 00:00:00 +0000
  159. From: auto\@non-existent.com
  160. Message-Id: <auto\@non-existent.com>
  161. $msg
  162. EOT
  163. return $output;
  164. }
  165. }
  166. sub _get_message {
  167. my $self = shift;
  168. my $cgi = shift;
  169. if ($cgi->param('upload_file')) {
  170. return $self->_get_file($cgi->upload('upload_file'));
  171. }
  172. elsif (my $msg = $cgi->param('message')) {
  173. return $self->_make_rfc822_message ($msg);
  174. }
  175. undef;
  176. }
  177. sub _show_rspamc_result {
  178. my $self = shift;
  179. my $host = shift;
  180. my $res = shift;
  181. if (defined($res->{error})) {
  182. print "<p><strong>Error occured:</strong>&nbsp;$res->{error}</p>";
  183. }
  184. else {
  185. while (my ($metric, $result) = each (%{ $res })) {
  186. print "<p><strong>Metric:</strong>&nbsp;$metric</p>";
  187. print "<p><strong>Summary:</strong>&nbsp;$result->{isspam}, [ $result->{score} / $result->{threshold} ]</p>";
  188. print "<p><strong>Symbols:</strong>&nbsp;";
  189. print join("; ", @{ $result->{symbols} }) . "</p>";
  190. print "<p><strong>Urls:</strong>&nbsp;" . join(", ", @{ $result->{urls} }) . "</p>";
  191. foreach my $msg (@{ $result->{messages} }) {
  192. print "<p><strong>Message:</strong>&nbsp;$msg</p>";
  193. }
  194. print br;
  195. }
  196. }
  197. }
  198. sub _show_error {
  199. my $self = shift;
  200. my $error = shift;
  201. print header,
  202. start_html(-title=>'Rspamd control', -script=>[{-type=>'JAVASCRIPT', -src=>'http://www.google.com/jsapi'},
  203. {-type=>'JAVASCRIPT', -code=>'google.load("jquery", "1");'}]),
  204. h1('Results for rspamd command'),
  205. "<p><strong>Error occured:</strong>&nbsp;$error</p>",
  206. '<a href="javascript:history.back()">Back to manage</a>',
  207. end_html;
  208. }
  209. sub _show_control_result {
  210. my $self = shift;
  211. my $host = shift;
  212. my $res = shift;
  213. if ($res->{error_code} == 0) {
  214. print "<p><pre>$res->{error}</pre></p>";
  215. }
  216. else {
  217. print "<p><strong>Error occured:</strong>&nbsp;$res->{error}</p>";
  218. }
  219. }
  220. sub _show_results {
  221. my $self = shift;
  222. my $rspamd = shift;
  223. my $res = shift;
  224. if (defined ($res->{error})) {
  225. $self->_print_error($res->{error});
  226. return;
  227. }
  228. print header,
  229. start_html(-title=>'Rspamd control', -script=>[{-type=>'JAVASCRIPT', -src=>'http://www.google.com/jsapi'},
  230. {-type=>'JAVASCRIPT', -code=>'google.load("jquery", "1");'}]),
  231. h1('Results for rspamd command: ' . $rspamd->{command});
  232. while (my ($host, $result) = each (%{ $res })) {
  233. print h2('Results for host: ' . $host);
  234. if ($rspamd->{control}) {
  235. $self->_show_control_result ($host, $result);
  236. }
  237. else {
  238. $self->_show_rspamc_result ($host, $result);
  239. }
  240. print hr;
  241. }
  242. print '<a href="javascript:history.back()">Back to manage</a>';
  243. print end_html;
  244. }
  245. sub _handle_form {
  246. my $self = shift;
  247. my $cgi = shift;
  248. my @servers = $cgi->param('servers');
  249. if (!@servers || scalar(@servers) == 0) {
  250. @servers = @{ $cfg{'hosts'} };
  251. }
  252. my $rspamd = Mail::Rspamd::Client->new({hosts => \@servers, timeout=>$cfg{timeout}, password=>$cfg{password}});
  253. my $cmd = $cgi->param('command');
  254. if (!$cmd) {
  255. return undef;
  256. }
  257. my $results;
  258. if($cmd eq 'symbols' || $cmd eq 'check') {
  259. my $msg = $self->_get_message($cgi);
  260. return undef unless $msg;
  261. $results = $rspamd->$cmd($msg);
  262. }
  263. elsif ($cmd eq 'learn') {
  264. my $statfile = $cgi->param('statfile');
  265. return undef unless $statfile;
  266. my $msg = $self->_get_message($cgi);
  267. return undef unless $msg;
  268. $rspamd->{'statfile'} = $statfile;
  269. if (my $weight = $cgi->param('weight')) {
  270. $rspamd->{'weight'} = int($weight);
  271. }
  272. $results = $rspamd->learn($msg);
  273. }
  274. elsif ($cmd eq 'fuzzy_add' || $cmd eq 'fuzzy_del') {
  275. my $msg = $self->_get_message($cgi);
  276. return undef unless $msg;
  277. if (my $weight = $cgi->param('weight')) {
  278. $rspamd->{'weight'} = int($weight);
  279. }
  280. $results = $rspamd->$cmd($msg);
  281. }
  282. elsif ($cmd eq 'stat' || $cmd eq 'uptime') {
  283. $results = $rspamd->$cmd();
  284. }
  285. $self->_show_results($rspamd, $results);
  286. }
  287. sub _handle_request {
  288. my $self = shift;
  289. my $cgi = shift;
  290. my $path = $cgi->path_info();
  291. unless ($path) {
  292. print "CGI environment missing\n";
  293. return undef;
  294. }
  295. print "HTTP/1.0 200 OK\r\n";
  296. if ($cgi->request_method() eq 'POST') {
  297. if (!$self->_handle_form($cgi)) {
  298. $self->_show_error("invalid command");
  299. }
  300. }
  301. else {
  302. if ($path =~ '^/ajax$') {
  303. $self->_handle_ping();
  304. }
  305. else {
  306. $self->_show_html();
  307. }
  308. }
  309. }
  310. sub _run_standalone {
  311. my $self = shift;
  312. my $listen = IO::Socket::INET->new(
  313. Listen => 5,
  314. LocalAddr => $self->{addr},
  315. LocalPort => $self->{port},
  316. Proto => 'tcp',
  317. ReuseAddr => 1
  318. );
  319. unless ($listen) {
  320. warn "unable to listen on port $self->{port}: $!\n";
  321. return undef;
  322. };
  323. print STDERR "waiting for connection on port $self->{port}\n";
  324. while (1) {
  325. my $s = $listen->accept();
  326. open(STDOUT, ">&=".fileno($s));
  327. open(STDIN, "<&=".fileno($s));
  328. my ($req, $content);
  329. delete $ENV{CONTENT_LENGTH};
  330. {
  331. local ($/) = "\r\n";
  332. while (<STDIN>) {
  333. $req .= $_;
  334. chomp;
  335. last unless /\S/;
  336. if (/^GET\s*(\S+)/) {
  337. $ENV{REQUEST_METHOD} = 'GET';
  338. my ($pi, $qs) = split /\?/, $1, 2;
  339. $ENV{'PATH_INFO'} = $pi;
  340. $ENV{'QUERY_STRING'} = $qs;
  341. } elsif (/^POST\s*(\S+)/) {
  342. $ENV{REQUEST_METHOD} = 'POST';
  343. my ($pi, $qs) = split /\?/, $1, 2;
  344. $ENV{'PATH_INFO'} = $pi;
  345. $ENV{'QUERY_STRING'} = $qs;
  346. } elsif (/^Content-Type:\s*(.*)/) {
  347. $ENV{CONTENT_TYPE} = $1;
  348. } elsif (/^Content-Length:\s*(.*)/) {
  349. $ENV{CONTENT_LENGTH} = $1;
  350. }
  351. }
  352. }
  353. $ENV{SERVER_PORT} = $self->{port};
  354. $ENV{SERVER_NAME} = $self->{addr};
  355. if (my $size = $ENV{CONTENT_LENGTH}) {
  356. $content = '';
  357. while (length($content) < $size) {
  358. my $nr = read(STDIN, $content, $size-length($content),
  359. length($content));
  360. warn "read error" unless $nr;
  361. }
  362. }
  363. close(STDIN); # n.b.: does not close socket
  364. tie *STDIN, 'IO::String', $content;
  365. undef @CGI::QUERY_PARAM;
  366. my $q = new CGI();
  367. $self->_handle_request($q);
  368. untie *STDIN;
  369. close(STDOUT);
  370. close($s);
  371. }
  372. }
  373. sub run {
  374. my $self = shift;
  375. if ($self->{'standalone'} != 0) {
  376. $self->_run_standalone();
  377. }
  378. else {
  379. my $q = new CGI();
  380. $self->_handle_request($q);
  381. }
  382. }
  383. }
  384. # Parse arguments
  385. my ($port, $standalone, $cfg, $host);
  386. while (my $arg = shift @ARGV) {
  387. if ($arg =~ /^-port$/i) {
  388. $port = shift @ARGV;
  389. }
  390. elsif ($arg =~ /^-standalone$/i) {
  391. $standalone = 1;
  392. }
  393. elsif ($arg =~ /^-cfg$/i) {
  394. $cfg = shift @ARGV;
  395. }
  396. elsif ($arg =~ /^-host$/i) {
  397. $host = shift @ARGV;
  398. }
  399. else {
  400. print STDERR <<EOT;
  401. Rspamd.cgi is a simple web intraface for managing rspamd cluster.
  402. Usage: rspamd.cgi [-standalone] [-host hostname] [-port number] [-cfg config_file]
  403. Options allowed:
  404. -standalone Start rspamd.cgi as standalone http server (for testing)
  405. -port Port to run standalone server
  406. -host Host to run standalone server
  407. -cfg Config file (in perl) that redefines defaults
  408. EOT
  409. exit;
  410. }
  411. }
  412. $port = 8080 unless int($port);
  413. $host = 'localhost' unless $host;
  414. RspamdWebInterface->new({port=>$port, standalone=>$standalone, config=>$cfg, addr=>$host})->run();