File Coverage

blib/lib/Mail/MtPolicyd.pm
Criterion Covered Total %
statement 52 262 19.8
branch 0 72 0.0
condition 0 27 0.0
subroutine 16 34 47.0
pod 6 12 50.0
total 74 407 18.1


line stmt bran cond sub pod time code
1             package Mail::MtPolicyd;
2              
3 2     2   330175 use strict;
  2         3  
  2         51  
4 2     2   5 use base qw(Net::Server::PreFork);
  2         2  
  2         912  
5              
6             our $VERSION = '2.01'; # VERSION
7             # ABSTRACT: a modular policy daemon for postfix
8              
9 2     2   85502 use Data::Dumper;
  2         8922  
  2         120  
10 2     2   603 use Mail::MtPolicyd::Profiler;
  2         6  
  2         74  
11 2     2   802 use Mail::MtPolicyd::Request;
  2         4  
  2         62  
12 2     2   817 use Mail::MtPolicyd::VirtualHost;
  2         6  
  2         59  
13 2     2   863 use Mail::MtPolicyd::ConnectionPool;
  2         6  
  2         76  
14 2     2   887 use Mail::MtPolicyd::SessionCache;
  2         7  
  2         72  
15 2     2   2847 use DBI;
  2         23014  
  2         114  
16 2     2   13 use Time::HiRes qw( usleep tv_interval gettimeofday );
  2         3  
  2         16  
17 2     2   1748 use Getopt::Long;
  2         14196  
  2         8  
18 2     2   1057 use Tie::IxHash;
  2         3936  
  2         52  
19 2     2   1228 use Config::General qw(ParseConfig);
  2         24706  
  2         116  
20 2     2   12 use IO::Handle;
  2         3  
  2         3546  
21            
22              
23             sub _preload_modules {
24             # PRELOAD some modules
25 0     0   0 my @modules = (
26             'DBI',
27             'Moose',
28             'Moose::Role',
29             'MooseX::Getopt',
30             'MooseX::Role::Parameterized',
31             'namespace::autoclean',
32             );
33              
34 0         0 foreach my $module (@modules) {
35 0         0 $module =~ s/::/\//g;
36 0         0 $module .= '.pm';
37 0         0 require $module;
38             }
39             }
40              
41             sub _apply_values_from_config {
42 0     0   0 my ( $self, $target, $config ) = ( shift, shift, shift );
43              
44 0         0 while ( my $key = shift ) {
45 0 0       0 if(! defined $config->{$key} ) {
46 0         0 next;
47             }
48 0         0 $target->{$key} = $config->{$key};
49             }
50              
51 0         0 return;
52             }
53              
54             sub _apply_array_from_config {
55 0     0   0 my ( $self, $target, $config ) = ( shift, shift, shift );
56              
57 0         0 while ( my $key = shift ) {
58 0 0       0 if(! defined $config->{$key} ) {
59 0         0 next;
60             }
61 0         0 $target->{$key} = [ split(/\s*,\s*/, $config->{$key}) ];
62             }
63              
64 0         0 return;
65             }
66              
67             sub print_usage {
68 0     0 0 0 print "mtpolicyd [-h|--help] [-c|--config=<file>] [-f|--foreground] [-l|--loglevel=<level>] [-d|--dump_vhosts] [-t|--cron=<task1,hourly,daily,...>]\n";
69 0         0 return;
70             }
71              
72             sub configure {
73 0     0 1 0 my $self = shift;
74 0         0 my $server = $self->{'server'};
75 0         0 my $cmdline;
76            
77 0 0       0 return if(@_);
78              
79 0 0       0 if( ! defined $server->{'config_file'} ) {
80 0         0 $server->{'config_file'} = '/etc/mtpolicyd/mtpolicyd.conf';
81             }
82 0         0 $server->{'background'} = 1;
83 0         0 $server->{'setsid'} = 1;
84 0         0 $server->{'no_close_by_child'} = 1;
85              
86             # Parse command line params
87 0         0 %{$cmdline} = ();
  0         0  
88             GetOptions(
89 0         0 \%{$cmdline},
  0         0  
90             "help|h",
91             "dump_config|d",
92             "config|c:s",
93             "foreground|f",
94             "loglevel|l:i",
95             "cron|t:s",
96             );
97 0 0       0 if ($cmdline->{'help'}) {
98 0         0 $self->print_usage;
99 0         0 exit 0;
100             }
101 0 0 0     0 if (defined($cmdline->{'config'}) && $cmdline->{'config'} ne "") {
102 0         0 $server->{'config_file'} = $cmdline->{'config'};
103             }
104 0 0       0 if( ! -f $server->{'config_file'} ) {
105 0         0 print(STDERR 'configuration file '.$server->{'config_file'}.' does not exist!\n');
106 0         0 exit 1;
107             }
108              
109             # DEFAULTS
110 0 0       0 if( ! defined $server->{'log_level'} ) {
111 0         0 $server->{'log_level'} = 2;
112             }
113 0 0 0     0 if( ! defined $server->{'log_file'} && ! $cmdline->{'foreground'} ) {
114 0         0 $server->{'log_file'} = 'Sys::Syslog';
115             }
116 0         0 $server->{'syslog_ident'} = 'mtpolicyd';
117 0         0 $server->{'syslog_facility'} = 'mail';
118              
119 0         0 $server->{'proto'} = 'tcp';
120 0         0 $server->{'host'} = '127.0.0.1';
121 0 0       0 if( ! defined $server->{'port'} ) {
122 0         0 $server->{'port'} = [ '127.0.0.1:12345' ];
123             }
124              
125 0         0 $server->{'min_servers'} = 4;
126 0         0 $server->{'min_spare_servers'} = 4;
127 0         0 $server->{'max_spare_servers'} = 12;
128 0         0 $server->{'max_servers'} = 25;
129 0         0 $server->{'max_requests'} = 1000;
130              
131 0         0 $self->{'request_timeout'} = 20;
132              
133 0         0 $self->{'keepalive_timeout'} = 60;
134 0         0 $self->{'max_keepalive'} = 0;
135              
136 0         0 $self->{'vhost_by_policy_context'} = 0;
137              
138 0         0 $self->{'program_name'} = $0;
139              
140             # APPLY values from configuration file
141 0         0 tie my %config_hash, "Tie::IxHash";
142             %config_hash = ParseConfig(
143             -AllowMultiOptions => 'no',
144 0         0 -ConfigFile => $server->{'config_file'},
145             -Tie => "Tie::IxHash"
146             );
147 0         0 my $config = \%config_hash;
148              
149 0         0 $self->_apply_values_from_config($server, $config,
150             'user', 'group', 'pid_file',
151             'log_level', 'log_file', 'syslog_ident', 'syslog_facility',
152             'host',
153             'min_servers', 'min_spare_servers', 'max_spare_servers',
154             'max_servers', 'max_requests',
155             'chroot',
156             );
157 0         0 $self->_apply_array_from_config($server, $config, 'port');
158              
159 0         0 $self->_apply_values_from_config($self, $config,
160             'request_timeout', 'keepalive_timeout', 'max_keepalive',
161             'vhost_by_policy_context',
162             'program_name',
163             );
164              
165             # initialize connection pool
166 0         0 Mail::MtPolicyd::ConnectionPool->initialize;
167 0 0       0 if( defined $config->{'Connection'} ) {
168 0         0 Mail::MtPolicyd::ConnectionPool->load_config( $config->{'Connection'} );
169             }
170 0         0 $self->{'session_cache_config'} = $config->{'SessionCache'};
171              
172             # LOAD VirtualHosts
173 0 0       0 if( ! defined $config->{'VirtualHost'} ) {
174 0         0 print(STDERR 'no virtual hosts configured!\n');
175 0         0 exit 1;
176             }
177 0         0 my $vhosts = $config->{'VirtualHost'};
178              
179 0         0 $self->{'virtual_hosts'} = {};
180 0         0 foreach my $vhost_port (keys %$vhosts) {
181 0         0 my $vhost = $vhosts->{$vhost_port};
182 0         0 $self->{'virtual_hosts'}->{$vhost_port} =
183             Mail::MtPolicyd::VirtualHost->new_from_config($vhost_port, $vhost)
184             }
185 0 0       0 if ($cmdline->{'dump_config'}) {
186 0         0 print "----- Virtual Hosts -----\n";
187 0         0 print Dumper( $self->{'virtual_hosts'} );
188 0         0 exit 0;
189             }
190              
191             # foreground mode (cmdline)
192 0 0       0 if ($cmdline->{'foreground'}) {
193 0         0 $server->{'background'} = undef;
194 0         0 $server->{'setsid'} = undef;
195             }
196 0 0       0 if( $cmdline->{'loglevel'} ) {
197 0         0 $server->{'log_level'} = $cmdline->{'loglevel'};
198             }
199              
200             # if running in cron mode execute cronjobs and exit
201 0 0 0     0 if( $cmdline->{'cron'} && $cmdline->{'cron'} !~ /^\s*$/ ) {
202 0         0 my @tasks = split(/\s*,\s*/, $cmdline->{'cron'});
203 0         0 $self->cron( @tasks );
204 0         0 exit 0;
205             }
206              
207             # change processname in top/ps
208 0         0 $self->_set_process_stat('master');
209              
210 0         0 return;
211             }
212              
213             sub cron {
214 2     2 0 1096 my $self = shift;
215 2         2 foreach my $vhost ( keys %{$self->{'virtual_hosts'}} ) {
  2         8  
216 2         8 $self->{'virtual_hosts'}->{$vhost}->cron( $self, @_ );
217             }
218 2         7 return;
219             }
220              
221             sub pre_loop_hook {
222 0     0 1 0 my $self = shift;
223              
224 0         0 $self->_preload_modules;
225              
226 0         0 return;
227             }
228              
229             sub child_init_hook {
230 0     0 1 0 my $self = shift;
231              
232 0         0 $self->_set_process_stat('virgin child');
233              
234             # recreate connection in child process
235 0         0 Mail::MtPolicyd::ConnectionPool->reconnect;
236              
237             # initialize session cache
238 0         0 $self->{'session_cache'} = Mail::MtPolicyd::SessionCache->new(
239             server => $self,
240             );
241 0 0 0     0 if( defined $self->{'session_cache_config'} &&
242             ref($self->{'session_cache_config'}) eq 'HASH') {
243 0         0 $self->{'session_cache'}->load_config( $self->{'session_cache_config'} );
244             }
245              
246 0         0 return;
247             }
248              
249             sub child_finish_hook {
250 0     0 1 0 my $self = shift;
251 0         0 $self->_set_process_stat('finish');
252              
253 0         0 Mail::MtPolicyd::ConnectionPool->shutdown;
254 0 0       0 if( defined $self->{'session_cache'} ) {
255 0         0 $self->{'session_cache'}->shutdown;
256             }
257              
258 0         0 return;
259             }
260              
261             sub get_conn_port {
262 0     0 0 0 my $self = shift;
263 0         0 my $server = $self->{server};
264 0         0 my $client = $server->{client};
265 0         0 my $port;
266 0   0     0 my $is_socket = $client && $client->UNIVERSAL::can('NS_proto') &&
267             $client->NS_proto eq 'UNIX';
268              
269 0 0       0 if( $is_socket ) {
270 0 0       0 $port = Net::Server->VERSION >= 2 ? $client->NS_port
271             : $client->NS_unix_path;
272             } else {
273 0         0 $port = $self->{'server'}->{'sockport'};
274             }
275 0         0 return($port);
276             }
277              
278             sub get_virtual_host {
279 0     0 0 0 my ( $self, $conn_port, $r ) = @_;
280 0         0 my $vhost;
281 0         0 my $policy_context = $r->attr('policy_context');
282              
283 0 0 0     0 if( $self->{'vhost_by_policy_context'}
      0        
284             && defined $policy_context
285             && $policy_context ne '' ) {
286 0         0 foreach my $vhost_port ( keys %{$self->{'virtual_hosts'}} ) {
  0         0  
287 0         0 $vhost = $self->{'virtual_hosts'}->{$vhost_port};
288 0 0       0 if( $policy_context eq $vhost->name ) {
289 0         0 return( $vhost );
290             }
291             }
292             }
293              
294 0         0 $vhost = $self->{'virtual_hosts'}->{$conn_port};
295 0 0       0 if( ! defined $vhost ) {
296 0         0 die('no virtual host defined for port '.$conn_port);
297             }
298 0         0 return($vhost);
299             }
300              
301             sub _is_loglevel {
302 0     0   0 my ( $self, $level ) = @_;
303 0 0 0     0 if( $self->{'server'}->{'log_level'} &&
304             $self->{'server'}->{'log_level'} >= $level ) {
305 0         0 return(1);
306             }
307 0         0 return(0);
308             }
309              
310             our %_LOG_ESCAPE_MAP = (
311             "\0" => '\0',
312             "\r" => '\r',
313             "\n" => '\n',
314             "\\" => '\\\\',
315             );
316              
317             our $_LOG_ESCAPE_MAP_RE = '['.join('',
318             map {
319             sprintf('\\x%02x', ord($_))
320             } keys %_LOG_ESCAPE_MAP
321             ).']';
322              
323             sub log {
324 5     5 1 17 my ( $self, $level, $msg, @params ) = @_;
325 5         33 $msg =~ s/($_LOG_ESCAPE_MAP_RE)/
326 1         4 $_LOG_ESCAPE_MAP{$1} /gse;
327 5         7 $msg =~ s/([\x01-\x08\x0b-\x0c\x0e-\x1f\x7f])/
328 0         0 sprintf('\\x%02X', ord($1)) /gse;
329 5         14 return $self->SUPER::log( $level, $msg, @params );
330             }
331              
332             sub _process_one_request {
333 0     0     my ( $self, $conn, $vhost, $r ) = @_;
334 0           my $port = $vhost->port;
335 0           my $s;
336             my $error;
337              
338 0           eval {
339 0           my $start_t = [gettimeofday];
340 0     0     local $SIG{'ALRM'} = sub { die "Request timeout!" };
  0            
341 0           my $timeout = $self->{'request_timeout'};
342 0           alarm($timeout);
343              
344 0 0         if( $self->_is_loglevel(4) ) { $self->log(4, 'request: '.$r->dump_attr); }
  0            
345 0           my $instance = $r->attr('instance');
346              
347 0           Mail::MtPolicyd::Profiler->tick('retrieve session');
348 0           $s = $self->{'session_cache'}->retrieve_session($instance);
349 0 0         if( $self->_is_loglevel(4) ) { $self->log(4, 'session: '.Dumper($s)); }
  0            
350 0           $r->session($s);
351              
352 0           Mail::MtPolicyd::Profiler->tick('run vhost');
353 0           my $result = $vhost->run($r);
354              
355 0           my $response = $result->as_policyd_response;
356 0           $conn->print($response);
357 0           $conn->flush;
358              
359             # convert to ms and round by 0.5/int
360 0           my $elapsed = int(tv_interval( $start_t, [gettimeofday] ) * 100 + 0.5);
361 0 0         my $matched = defined $result->last_match ? $result->last_match : '';
362 0           $self->log(1, $vhost->name.': instance='.$instance.', type='.$r->type.', t='.$elapsed.'ms, plugin='.$matched.', result='.$result->as_log);
363             };
364 0 0         if ( $@ ) { $error = $@; }
  0            
365              
366 0 0         if( defined $s ) {
367 0           $self->{'session_cache'}->store_session($s);
368             }
369              
370 0 0         if( defined $error ) { die( $error ); }
  0            
371              
372 0           return;
373             }
374              
375             sub process_request {
376 0     0 1   my ( $self, $conn ) = @_;
377 0           my $max_keepalive = $self->{'max_keepalive'};
378              
379 0           my $port = $self->get_conn_port;
380 0           $self->log(4, 'accepted connection on port '.$port );
381              
382 0   0       for( my $alive_count = 0
383             ; $max_keepalive == 0 || $alive_count < $max_keepalive
384             ; $alive_count++ ) {
385 0           my $r;
386 0           $self->_set_process_stat('waiting request');
387 0           Mail::MtPolicyd::Profiler->reset;
388 0           eval {
389 0     0     local $SIG{'ALRM'} = sub { die "Keepalive connection timeout" };
  0            
390 0           my $timeout = $self->{'keepalive_timeout'};
391 0           alarm($timeout);
392 0           Mail::MtPolicyd::Profiler->tick('parsing request');
393 0           $r = Mail::MtPolicyd::Request->new_from_fh( $conn, 'server' => $self );
394             };
395 0 0         if ( $@ =~ /Keepalive connection timeout/ ) {
    0          
    0          
396 0           $self->log(3, '['.$port.']: keepalive timeout: closing connection');
397 0           last;
398             } elsif($@ =~ /connection closed by peer/) {
399 0           $self->log(3, '['.$port.']: connection closed by peer');
400 0           last;
401             } elsif($@) {
402 0           $self->log(0, '['.$port.']: error while reading request: '.$@);
403 0           last;
404            
405             }
406 0           Mail::MtPolicyd::Profiler->tick('processing request');
407 0           my $vhost = $self->get_virtual_host($port, $r);
408 0           $self->_set_process_stat($vhost->name.', processing request');
409 0           eval {
410 0           $self->_process_one_request( $conn, $vhost, $r );
411             };
412 0 0         if ( $@ =~ /Request timeout!/ ) {
    0          
413 0           $self->log(1, '['.$port.']: request timed out');
414 0           last;
415             } elsif($@) {
416 0           $self->log(0, 'error while processing request: '.$@);
417 0           last;
418             }
419 0           Mail::MtPolicyd::Profiler->stop_current_timer;
420 0 0         if( $self->_is_loglevel(4) ) {
421 0           $self->log(4, Mail::MtPolicyd::Profiler->to_string);
422             }
423             }
424              
425 0           $self->log(3, '['.$port.']: closing connection');
426 0           $self->_set_process_stat('idle');
427              
428 0           return;
429             }
430              
431             sub _set_process_stat {
432 0     0     my ( $self, $stat ) = @_;
433 0           $0 = $self->{'program_name'}.' ('.$stat.')'
434             };
435              
436             sub memcached {
437 0     0 0   die('the global memcached connection does no longer exist in mtpolicyd >= 2.00');
438             }
439              
440             sub get_dbh {
441 0     0 0   die('the global dbh handle is no longer available in mtpolicyd >= 2.00');
442             }
443              
444             1;
445              
446             __END__
447              
448             =pod
449              
450             =encoding UTF-8
451              
452             =head1 NAME
453              
454             Mail::MtPolicyd - a modular policy daemon for postfix
455              
456             =head1 VERSION
457              
458             version 2.01
459              
460             =head1 DESCRIPTION
461              
462             Mail::MtPolicyd is the Net::Server class of the mtpolicyd daemon.
463              
464             =head2 SYNOPSIS
465              
466             use Mail::MtPolicyd;
467             Mail::MtPolicyd->run;
468              
469             =head1 AUTHOR
470              
471             Markus Benning <ich@markusbenning.de>
472              
473             =head1 COPYRIGHT AND LICENSE
474              
475             This software is Copyright (c) 2014 by Markus Benning <ich@markusbenning.de>.
476              
477             This is free software, licensed under:
478              
479             The GNU General Public License, Version 2, June 1991
480              
481             =cut