File Coverage

blib/lib/Mail/MtPolicyd.pm
Criterion Covered Total %
statement 55 321 17.1
branch 0 96 0.0
condition 0 33 0.0
subroutine 17 39 43.5
pod 6 16 37.5
total 78 505 15.4


line stmt bran cond sub pod time code
1             package Mail::MtPolicyd;
2              
3 2     2   508398 use strict;
  2         5  
  2         56  
4 2     2   10 use base qw(Net::Server::PreFork);
  2         3  
  2         1733  
5              
6             our $VERSION = '1.23'; # VERSION
7             # ABSTRACT: a modular policy daemon for postfix
8              
9 2     2   138280 use Data::Dumper;
  2         13934  
  2         125  
10 2     2   1025 use Mail::MtPolicyd::Profiler;
  2         8  
  2         83  
11 2     2   1372 use Mail::MtPolicyd::Request;
  2         7  
  2         84  
12 2     2   1423 use Mail::MtPolicyd::VirtualHost;
  2         7  
  2         76  
13 2     2   1369 use Mail::MtPolicyd::SqlConnection;
  2         7  
  2         86  
14 2     2   1264 use Mail::MtPolicyd::LdapConnection;
  2         9  
  2         96  
15 2     2   19 use DBI;
  2         5  
  2         89  
16 2     2   2226 use Cache::Memcached;
  2         210707  
  2         84  
17 2     2   18 use Time::HiRes qw( usleep tv_interval gettimeofday );
  2         4  
  2         19  
18 2     2   2790 use Getopt::Long;
  2         23633  
  2         13  
19 2     2   1946 use Tie::IxHash;
  2         5183  
  2         68  
20 2     2   2289 use Config::General qw(ParseConfig);
  2         34805  
  2         152  
21 2     2   17 use IO::Handle;
  2         5  
  2         7088  
22            
23              
24             sub _preload_modules {
25             # PRELOAD some modules
26 0     0   0 my @modules = (
27             'DBI',
28             'Moose',
29             'Moose::Role',
30             'MooseX::Getopt',
31             'MooseX::Role::Parameterized',
32             'namespace::autoclean',
33             );
34              
35 0         0 foreach my $module (@modules) {
36 0         0 $module =~ s/::/\//g;
37 0         0 $module .= '.pm';
38 0         0 require $module;
39             }
40             }
41              
42             sub _apply_values_from_config {
43 0     0   0 my ( $self, $target, $config ) = ( shift, shift, shift );
44              
45 0         0 while ( my $key = shift ) {
46 0 0       0 if(! defined $config->{$key} ) {
47 0         0 next;
48             }
49 0         0 $target->{$key} = $config->{$key};
50             }
51              
52 0         0 return;
53             }
54              
55             sub _apply_array_from_config {
56 0     0   0 my ( $self, $target, $config ) = ( shift, shift, shift );
57              
58 0         0 while ( my $key = shift ) {
59 0 0       0 if(! defined $config->{$key} ) {
60 0         0 next;
61             }
62 0         0 $target->{$key} = [ split(/\s*,\s*/, $config->{$key}) ];
63             }
64              
65 0         0 return;
66             }
67              
68             sub print_usage {
69 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";
70 0         0 return;
71             }
72              
73             sub configure {
74 0     0 1 0 my $self = shift;
75 0         0 my $server = $self->{'server'};
76 0         0 my $cmdline;
77            
78 0 0       0 return if(@_);
79              
80 0 0       0 if( ! defined $server->{'config_file'} ) {
81 0         0 $server->{'config_file'} = '/etc/mtpolicyd/mtpolicyd.conf';
82             }
83 0         0 $server->{'background'} = 1;
84 0         0 $server->{'setsid'} = 1;
85 0         0 $server->{'no_close_by_child'} = 1;
86              
87             # Parse command line params
88 0         0 %{$cmdline} = ();
  0         0  
89             GetOptions(
90 0         0 \%{$cmdline},
  0         0  
91             "help|h",
92             "dump_config|d",
93             "config|c:s",
94             "foreground|f",
95             "loglevel|l:i",
96             "cron|t:s",
97             );
98 0 0       0 if ($cmdline->{'help'}) {
99 0         0 $self->print_usage;
100 0         0 exit 0;
101             }
102 0 0 0     0 if (defined($cmdline->{'config'}) && $cmdline->{'config'} ne "") {
103 0         0 $server->{'config_file'} = $cmdline->{'config'};
104             }
105 0 0       0 if( ! -f $server->{'config_file'} ) {
106 0         0 print(STDERR 'configuration file '.$server->{'config_file'}.' does not exist!\n');
107 0         0 exit 1;
108             }
109              
110             # DEFAULTS
111 0 0       0 if( ! defined $server->{'log_level'} ) {
112 0         0 $server->{'log_level'} = 2;
113             }
114 0 0 0     0 if( ! defined $server->{'log_file'} && ! $cmdline->{'foreground'} ) {
115 0         0 $server->{'log_file'} = 'Sys::Syslog';
116             }
117 0         0 $server->{'syslog_ident'} = 'mtpolicyd';
118 0         0 $server->{'syslog_facility'} = 'mail';
119              
120 0         0 $server->{'proto'} = 'tcp';
121 0         0 $server->{'host'} = '127.0.0.1';
122 0 0       0 if( ! defined $server->{'port'} ) {
123 0         0 $server->{'port'} = [ '127.0.0.1:12345' ];
124             }
125              
126 0         0 $server->{'min_servers'} = 4;
127 0         0 $server->{'min_spare_servers'} = 4;
128 0         0 $server->{'max_spare_servers'} = 12;
129 0         0 $server->{'max_servers'} = 25;
130 0         0 $server->{'max_requests'} = 1000;
131              
132 0         0 $self->{'request_timeout'} = 20;
133              
134 0         0 $self->{'keepalive_timeout'} = 60;
135 0         0 $self->{'max_keepalive'} = 0;
136              
137 0         0 $self->{'vhost_by_policy_context'} = 0;
138              
139 0         0 $self->{'db_dsn'} = undef;
140 0         0 $self->{'db_user'} = '';
141 0         0 $self->{'db_password'} = '';
142              
143             $self->{'ldap_host'} = undef,
144             $self->{'ldap_port'} = 389,
145             $self->{'ldap_keepalive'} = 1,
146             $self->{'ldap_timeout'} = 120,
147             $self->{'ldap_binddn'} = undef,
148             $self->{'ldap_password'} = undef,
149             $self->{'ldap_starttls'} = 1,
150              
151 0         0 $self->{'memcached_servers'} = [ '127.0.0.1:11211' ];
152 0         0 $self->{'memcached_namespace'} = 'mt-';
153 0         0 $self->{'memcached_expire'} = 5 * 60;
154              
155             # will be incremented in linear steps 50, 100, 150...
156 0         0 $self->{'session_lock_wait'} = 50; # usec
157 0         0 $self->{'session_lock_max_retry'} = 50; # times
158 0         0 $self->{'session_lock_timeout'} = 10; # sec
159              
160 0         0 $self->{'program_name'} = $0;
161              
162             # APPLY values from configuration file
163 0         0 tie my %config_hash, "Tie::IxHash";
164             %config_hash = ParseConfig(
165             -AllowMultiOptions => 'no',
166 0         0 -ConfigFile => $server->{'config_file'},
167             -Tie => "Tie::IxHash"
168             );
169 0         0 my $config = \%config_hash;
170              
171 0         0 $self->_apply_values_from_config($server, $config,
172             'user', 'group', 'pid_file',
173             'log_level', 'log_file', 'syslog_ident', 'syslog_facility',
174             'host',
175             'min_servers', 'min_spare_servers', 'max_spare_servers',
176             'max_servers', 'max_requests',
177             'chroot',
178             );
179 0         0 $self->_apply_array_from_config($server, $config, 'port');
180              
181 0         0 $self->_apply_values_from_config($self, $config,
182             'request_timeout', 'keepalive_timeout', 'max_keepalive',
183             'vhost_by_policy_context', 'db_dsn', 'db_user', 'db_password',
184             'memcached_namespace', 'memcached_expire',
185             'session_lock_wait', 'session_lock_max_retry', 'session_lock_timeout',
186             'program_name',
187             'ldap_host', 'ldap_port', 'ldap_keepalive', 'ldap_timeout', 'ldap_binddn',
188             'ldap_password', 'ldap_starttls',
189             );
190 0         0 $self->_apply_array_from_config($self, $config, 'memcached_servers');
191              
192             # Initialize DB connection before load vhosts
193 0 0 0     0 if( defined $self->{'db_dsn'} && $self->{'db_dsn'} !~ /^\s*$/ ) {
194             Mail::MtPolicyd::SqlConnection->initialize(
195             dsn => $self->{'db_dsn'},
196             user => $self->{'db_user'},
197 0         0 password => $self->{'db_password'},
198             );
199             }
200              
201 0 0 0     0 if( defined $self->{'ldap_host'} && $self->{'ldap_host'} !~ /^\s*$/ ) {
202             Mail::MtPolicyd::LdapConnection->initialize(
203             host => $self->{'ldap_host'},
204             port => $self->{'ldap_port'},
205             keepalive => $self->{'ldap_keepalive'},
206             timeout => $self->{'ldap_timeout'},
207             binddn => $self->{'ldap_binddn'},
208             password => $self->{'ldap_password'},
209 0         0 starttls => $self->{'ldap_starttls'},
210             );
211             }
212              
213             # LOAD VirtualHosts
214 0 0       0 if( ! defined $config->{'VirtualHost'} ) {
215 0         0 print(STDERR 'no virtual hosts configured!\n');
216 0         0 exit 1;
217             }
218 0         0 my $vhosts = $config->{'VirtualHost'};
219              
220 0         0 $self->{'virtual_hosts'} = {};
221 0         0 foreach my $vhost_port (keys %$vhosts) {
222 0         0 my $vhost = $vhosts->{$vhost_port};
223 0         0 $self->{'virtual_hosts'}->{$vhost_port} =
224             Mail::MtPolicyd::VirtualHost->new_from_config($vhost_port, $vhost)
225             }
226 0 0       0 if ($cmdline->{'dump_config'}) {
227 0         0 print "----- Virtual Hosts -----\n";
228 0         0 print Dumper( $self->{'virtual_hosts'} );
229 0         0 exit 0;
230             }
231              
232             # foreground mode (cmdline)
233 0 0       0 if ($cmdline->{'foreground'}) {
234 0         0 $server->{'background'} = undef;
235 0         0 $server->{'setsid'} = undef;
236             }
237 0 0       0 if( $cmdline->{'loglevel'} ) {
238 0         0 $server->{'log_level'} = $cmdline->{'loglevel'};
239             }
240              
241             # if running in cron mode execute cronjobs and exit
242 0 0 0     0 if( $cmdline->{'cron'} && $cmdline->{'cron'} !~ /^\s*$/ ) {
243 0         0 my @tasks = split(/\s*,\s*/, $cmdline->{'cron'});
244 0         0 $self->cron( @tasks );
245 0         0 exit 0;
246             }
247              
248             # change processname in top/ps
249 0         0 $self->_set_process_stat('master');
250              
251 0         0 return;
252             }
253              
254             sub cron {
255 2     2 0 1079 my $self = shift;
256 2         3 foreach my $vhost ( keys %{$self->{'virtual_hosts'}} ) {
  2         8  
257 2         11 $self->{'virtual_hosts'}->{$vhost}->cron( $self, @_ );
258             }
259 2         8 return;
260             }
261              
262             sub pre_loop_hook {
263 0     0 1 0 my $self = shift;
264              
265 0         0 $self->_preload_modules;
266              
267 0         0 return;
268             }
269              
270             sub child_init_hook {
271 0     0 1 0 my $self = shift;
272              
273 0         0 $self->_set_process_stat('virgin child');
274              
275             # close parent database connection
276 0 0       0 if( Mail::MtPolicyd::SqlConnection->is_initialized ) {
277 0         0 eval { Mail::MtPolicyd::SqlConnection->reconnect; };
  0         0  
278 0 0       0 if($@) {
279 0         0 $self->log(0, 'failed to enstablish sql connection: '.$@);
280             }
281             }
282 0 0       0 if( Mail::MtPolicyd::LdapConnection->is_initialized ) {
283 0         0 eval { Mail::MtPolicyd::LdapConnection->reconnect; };
  0         0  
284 0 0       0 if($@) {
285 0         0 $self->log(0, 'failed to enstablish ldap connection: '.$@);
286             }
287             }
288              
289             $self->{'memcached'} = Cache::Memcached->new( {
290             'servers' => $self->{'memcached_servers'},
291             'debug' => 0,
292 0         0 'namespace' => $self->{'memcached_namespace'},
293             } );
294              
295 0         0 return;
296             }
297              
298             sub child_finish_hook {
299 0     0 1 0 my $self = shift;
300 0         0 $self->_set_process_stat('finish');
301              
302 0 0       0 if( Mail::MtPolicyd::SqlConnection->is_initialized ) {
303 0         0 eval { Mail::MtPolicyd::SqlConnection->instance->disconnect };
  0         0  
304             }
305 0 0       0 if( Mail::MtPolicyd::LdapConnection->is_initialized ) {
306 0         0 eval { Mail::MtPolicyd::LdapConnection->instance->disconnect };
  0         0  
307             }
308              
309 0         0 return;
310             }
311              
312             sub memcached {
313 0     0 0 0 my $self = shift;
314 0 0       0 if( ! defined $self->{'memcached'} ) {
315 0         0 die('no memcached connection available!');
316             }
317 0         0 return( $self->{'memcached'} );
318             }
319              
320             sub acquire_session_lock {
321 0     0 0 0 my ( $self, $instance ) = @_;
322 0         0 my $lock = 'lock_'.$instance;
323 0         0 my $wait = $self->{'session_lock_wait'};
324 0         0 my $max_retry = $self->{'session_lock_max_retry'};
325 0         0 my $lock_ttl = $self->{'session_lock_timeout'};
326              
327 0         0 for( my $try = 1 ; $try < $max_retry ; $try++ ) {
328 0 0       0 if( $self->{'memcached'}->add($lock, 1, $lock_ttl) ) {
329 0         0 return; # lock created
330             }
331 0         0 usleep( $wait * $try );
332             }
333              
334 0         0 die('could not acquire lock for session '.$instance);
335 0         0 return;
336             }
337              
338             sub release_session_lock {
339 0     0 0 0 my ( $self, $instance ) = @_;
340 0         0 my $lock = 'lock_'.$instance;
341              
342 0         0 $self->{'memcached'}->delete($lock);
343              
344 0         0 return;
345             }
346              
347             sub retrieve_session {
348 0     0 0 0 my ($self, $instance ) = @_;
349              
350 0 0       0 if( ! defined $instance ) {
351 0         0 return;
352             }
353              
354 0         0 $self->acquire_session_lock( $instance );
355              
356 0 0       0 if( my $session = $self->{'memcached'}->get($instance) ) {
357 0         0 return($session);
358             }
359            
360 0         0 return( { '_instance' => $instance } );
361             }
362              
363             sub store_session {
364 0     0 0 0 my ($self, $session ) = @_;
365 0         0 my $instance = $session->{'_instance'};
366 0 0       0 my $expire = defined $self->{'memcached_expire'} ? $self->{'memcached_expire'} : 300;
367              
368 0 0 0     0 if( ! defined $session || ! defined $instance ) {
369 0         0 return;
370             }
371            
372 0         0 $self->{'memcached'}->set($instance, $session, $expire);
373              
374 0         0 $self->release_session_lock($instance);
375              
376 0         0 return;
377             }
378              
379             sub get_conn_port {
380 0     0 0 0 my $self = shift;
381 0         0 my $server = $self->{server};
382 0         0 my $client = $server->{client};
383 0         0 my $port;
384 0   0     0 my $is_socket = $client && $client->UNIVERSAL::can('NS_proto') &&
385             $client->NS_proto eq 'UNIX';
386              
387 0 0       0 if( $is_socket ) {
388 0 0       0 $port = Net::Server->VERSION >= 2 ? $client->NS_port
389             : $client->NS_unix_path;
390             } else {
391 0         0 $port = $self->{'server'}->{'sockport'};
392             }
393 0         0 return($port);
394             }
395              
396             sub get_virtual_host {
397 0     0 0 0 my ( $self, $conn_port, $r ) = @_;
398 0         0 my $vhost;
399 0         0 my $policy_context = $r->attr('policy_context');
400              
401 0 0 0     0 if( $self->{'vhost_by_policy_context'}
      0        
402             && defined $policy_context
403             && $policy_context ne '' ) {
404 0         0 foreach my $vhost_port ( keys %{$self->{'virtual_hosts'}} ) {
  0         0  
405 0         0 $vhost = $self->{'virtual_hosts'}->{$vhost_port};
406 0 0       0 if( $policy_context eq $vhost->name ) {
407 0         0 return( $vhost );
408             }
409             }
410             }
411              
412 0         0 $vhost = $self->{'virtual_hosts'}->{$conn_port};
413 0 0       0 if( ! defined $vhost ) {
414 0         0 die('no virtual host defined for port '.$conn_port);
415             }
416 0         0 return($vhost);
417             }
418              
419             sub get_dbh {
420 0     0 0 0 my $self = shift;
421 0 0       0 if( ! Mail::MtPolicyd::SqlConnection->is_initialized ) {
422 0         0 die('no database connection available (no configured?)');
423             }
424 0         0 return( Mail::MtPolicyd::SqlConnection->instance->dbh );
425             }
426              
427             sub _is_loglevel {
428 0     0   0 my ( $self, $level ) = @_;
429 0 0 0     0 if( $self->{'server'}->{'log_level'} &&
430             $self->{'server'}->{'log_level'} >= $level ) {
431 0         0 return(1);
432             }
433 0         0 return(0);
434             }
435              
436             our %_LOG_ESCAPE_MAP = (
437             "\0" => '\0',
438             "\r" => '\r',
439             "\n" => '\n',
440             "\\" => '\\\\',
441             );
442              
443             our $_LOG_ESCAPE_MAP_RE = '['.join('',
444             map {
445             sprintf('\\x%02x', ord($_))
446             } keys %_LOG_ESCAPE_MAP
447             ).']';
448              
449             sub log {
450 5     5 1 24 my ( $self, $level, $msg, @params ) = @_;
451 5         40 $msg =~ s/($_LOG_ESCAPE_MAP_RE)/
452 1         6 $_LOG_ESCAPE_MAP{$1} /gse;
453 5         11 $msg =~ s/([\x01-\x08\x0b-\x0c\x0e-\x1f\x7f])/
454 0         0 sprintf('\\x%02X', ord($1)) /gse;
455 5         20 return $self->SUPER::log( $level, $msg, @params );
456             }
457              
458             sub _process_one_request {
459 0     0     my ( $self, $conn, $vhost, $r ) = @_;
460 0           my $port = $vhost->port;
461 0           my $s;
462             my $error;
463              
464 0           eval {
465 0           my $start_t = [gettimeofday];
466 0     0     local $SIG{'ALRM'} = sub { die "Request timeout!" };
  0            
467 0           my $timeout = $self->{'request_timeout'};
468 0           alarm($timeout);
469              
470 0 0         if( $self->_is_loglevel(4) ) { $self->log(4, 'request: '.$r->dump_attr); }
  0            
471 0           my $instance = $r->attr('instance');
472              
473 0           Mail::MtPolicyd::Profiler->tick('retrieve session');
474 0           $s = $self->retrieve_session($instance);
475 0 0         if( $self->_is_loglevel(4) ) { $self->log(4, 'session: '.Dumper($s)); }
  0            
476 0           $r->session($s);
477              
478 0           Mail::MtPolicyd::Profiler->tick('run vhost');
479 0           my $result = $vhost->run($r);
480              
481 0           my $response = $result->as_policyd_response;
482 0           $conn->print($response);
483 0           $conn->flush;
484              
485             # convert to ms and round by 0.5/int
486 0           my $elapsed = int(tv_interval( $start_t, [gettimeofday] ) * 100 + 0.5);
487 0 0         my $matched = defined $result->last_match ? $result->last_match : '';
488 0           $self->log(1, $vhost->name.': instance='.$instance.', type='.$r->type.', t='.$elapsed.'ms, plugin='.$matched.', result='.$result->as_log);
489             };
490 0 0         if ( $@ ) { $error = $@; }
  0            
491              
492 0 0         if( defined $s ) {
493 0           $self->store_session($s);
494             }
495              
496 0 0         if( defined $error ) { die( $error ); }
  0            
497              
498 0           return;
499             }
500              
501             sub process_request {
502 0     0 1   my ( $self, $conn ) = @_;
503 0           my $max_keepalive = $self->{'max_keepalive'};
504              
505 0           my $port = $self->get_conn_port;
506 0           $self->log(4, 'accepted connection on port '.$port );
507              
508 0   0       for( my $alive_count = 0
509             ; $max_keepalive == 0 || $alive_count < $max_keepalive
510             ; $alive_count++ ) {
511 0           my $r;
512 0           $self->_set_process_stat('waiting request');
513 0           Mail::MtPolicyd::Profiler->reset;
514 0           eval {
515 0     0     local $SIG{'ALRM'} = sub { die "Keepalive connection timeout" };
  0            
516 0           my $timeout = $self->{'keepalive_timeout'};
517 0           alarm($timeout);
518 0           Mail::MtPolicyd::Profiler->tick('parsing request');
519 0           $r = Mail::MtPolicyd::Request->new_from_fh( $conn, 'server' => $self );
520             };
521 0 0         if ( $@ =~ /Keepalive connection timeout/ ) {
    0          
    0          
522 0           $self->log(3, '['.$port.']: keepalive timeout: closing connection');
523 0           last;
524             } elsif($@ =~ /connection closed by peer/) {
525 0           $self->log(3, '['.$port.']: connection closed by peer');
526 0           last;
527             } elsif($@) {
528 0           $self->log(0, '['.$port.']: error while reading request: '.$@);
529 0           last;
530            
531             }
532 0           Mail::MtPolicyd::Profiler->tick('processing request');
533 0           my $vhost = $self->get_virtual_host($port, $r);
534 0           $self->_set_process_stat($vhost->name.', processing request');
535 0           eval {
536 0           $self->_process_one_request( $conn, $vhost, $r );
537             };
538 0 0         if ( $@ =~ /Request timeout!/ ) {
    0          
539 0           $self->log(1, '['.$port.']: request timed out');
540 0           last;
541             } elsif($@) {
542 0           $self->log(0, 'error while processing request: '.$@);
543 0           last;
544             }
545 0           Mail::MtPolicyd::Profiler->stop_current_timer;
546 0 0         if( $self->_is_loglevel(4) ) {
547 0           $self->log(4, Mail::MtPolicyd::Profiler->to_string);
548             }
549             }
550              
551 0           $self->log(3, '['.$port.']: closing connection');
552 0           $self->_set_process_stat('idle');
553              
554 0           return;
555             }
556              
557             sub _set_process_stat {
558 0     0     my ( $self, $stat ) = @_;
559 0           $0 = $self->{'program_name'}.' ('.$stat.')'
560             };
561              
562             1;
563              
564             __END__
565              
566             =pod
567              
568             =encoding UTF-8
569              
570             =head1 NAME
571              
572             Mail::MtPolicyd - a modular policy daemon for postfix
573              
574             =head1 VERSION
575              
576             version 1.23
577              
578             =head1 DESCRIPTION
579              
580             Mail::MtPolicyd is the Net::Server class of the mtpolicyd daemon.
581              
582             =head2 SYNOPSIS
583              
584             use Mail::MtPolicyd;
585             Mail::MtPolicyd->run;
586              
587             =head1 AUTHOR
588              
589             Markus Benning <ich@markusbenning.de>
590              
591             =head1 COPYRIGHT AND LICENSE
592              
593             This software is Copyright (c) 2014 by Markus Benning <ich@markusbenning.de>.
594              
595             This is free software, licensed under:
596              
597             The GNU General Public License, Version 2, June 1991
598              
599             =cut