File Coverage

blib/lib/Mail/MtPolicyd.pm
Criterion Covered Total %
statement 55 265 20.7
branch 0 72 0.0
condition 0 27 0.0
subroutine 17 35 48.5
pod 6 12 50.0
total 78 411 18.9


line stmt bran cond sub pod time code
1             package Mail::MtPolicyd;
2              
3 2     2   301153 use strict;
  2         2  
  2         44  
4 2     2   6 use warnings;
  2         3  
  2         47  
5 2     2   6 use base qw(Net::Server::PreFork);
  2         2  
  2         950  
6              
7             our $VERSION = '2.02'; # VERSION
8             # ABSTRACT: a modular policy daemon for postfix
9              
10 2     2   87228 use Data::Dumper;
  2         9115  
  2         125  
11 2     2   626 use Mail::MtPolicyd::Profiler;
  2         7  
  2         82  
12 2     2   972 use Mail::MtPolicyd::Request;
  2         5  
  2         71  
13 2     2   967 use Mail::MtPolicyd::VirtualHost;
  2         7  
  2         78  
14 2     2   934 use Mail::MtPolicyd::ConnectionPool;
  2         6  
  2         81  
15 2     2   979 use Mail::MtPolicyd::SessionCache;
  2         4  
  2         68  
16 2     2   2942 use DBI;
  2         24285  
  2         141  
17 2     2   18 use Time::HiRes qw( usleep tv_interval gettimeofday );
  2         2  
  2         17  
18 2     2   1818 use Getopt::Long;
  2         15606  
  2         11  
19 2     2   1312 use Tie::IxHash;
  2         3956  
  2         60  
20 2     2   1371 use Config::General qw(ParseConfig);
  2         25940  
  2         156  
21 2     2   17 use IO::Handle;
  2         4  
  2         3383  
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->{'program_name'} = $0;
140              
141             # APPLY values from configuration file
142 0         0 tie my %config_hash, "Tie::IxHash";
143             %config_hash = ParseConfig(
144             -AllowMultiOptions => 'no',
145 0         0 -ConfigFile => $server->{'config_file'},
146             -Tie => "Tie::IxHash"
147             );
148 0         0 my $config = \%config_hash;
149              
150 0         0 $self->_apply_values_from_config($server, $config,
151             'user', 'group', 'pid_file',
152             'log_level', 'log_file', 'syslog_ident', 'syslog_facility',
153             'host',
154             'min_servers', 'min_spare_servers', 'max_spare_servers',
155             'max_servers', 'max_requests',
156             'chroot',
157             );
158 0         0 $self->_apply_array_from_config($server, $config, 'port');
159              
160 0         0 $self->_apply_values_from_config($self, $config,
161             'request_timeout', 'keepalive_timeout', 'max_keepalive',
162             'vhost_by_policy_context',
163             'program_name',
164             );
165              
166             # initialize connection pool
167 0         0 Mail::MtPolicyd::ConnectionPool->initialize;
168 0 0       0 if( defined $config->{'Connection'} ) {
169 0         0 Mail::MtPolicyd::ConnectionPool->load_config( $config->{'Connection'} );
170             }
171 0         0 $self->{'session_cache_config'} = $config->{'SessionCache'};
172              
173             # LOAD VirtualHosts
174 0 0       0 if( ! defined $config->{'VirtualHost'} ) {
175 0         0 print(STDERR 'no virtual hosts configured!\n');
176 0         0 exit 1;
177             }
178 0         0 my $vhosts = $config->{'VirtualHost'};
179              
180 0         0 $self->{'virtual_hosts'} = {};
181 0         0 foreach my $vhost_port (keys %$vhosts) {
182 0         0 my $vhost = $vhosts->{$vhost_port};
183 0         0 $self->{'virtual_hosts'}->{$vhost_port} =
184             Mail::MtPolicyd::VirtualHost->new_from_config($vhost_port, $vhost)
185             }
186 0 0       0 if ($cmdline->{'dump_config'}) {
187 0         0 print "----- Virtual Hosts -----\n";
188 0         0 print Dumper( $self->{'virtual_hosts'} );
189 0         0 exit 0;
190             }
191              
192             # foreground mode (cmdline)
193 0 0       0 if ($cmdline->{'foreground'}) {
194 0         0 $server->{'background'} = undef;
195 0         0 $server->{'setsid'} = undef;
196             }
197 0 0       0 if( $cmdline->{'loglevel'} ) {
198 0         0 $server->{'log_level'} = $cmdline->{'loglevel'};
199             }
200              
201             # if running in cron mode execute cronjobs and exit
202 0 0 0     0 if( $cmdline->{'cron'} && $cmdline->{'cron'} !~ /^\s*$/ ) {
203 0         0 my @tasks = split(/\s*,\s*/, $cmdline->{'cron'});
204 0         0 $self->cron( @tasks );
205 0         0 exit 0;
206             }
207              
208             # change processname in top/ps
209 0         0 $self->_set_process_stat('master');
210              
211 0         0 return;
212             }
213              
214             sub cron {
215 2     2 0 659 my $self = shift;
216 2         2 foreach my $vhost ( keys %{$self->{'virtual_hosts'}} ) {
  2         6  
217 2         8 $self->{'virtual_hosts'}->{$vhost}->cron( $self, @_ );
218             }
219 2         7 return;
220             }
221              
222             sub pre_loop_hook {
223 0     0 1 0 my $self = shift;
224              
225 0         0 $self->_preload_modules;
226              
227 0         0 return;
228             }
229              
230             sub child_init_hook {
231 0     0 1 0 my $self = shift;
232              
233 0         0 $self->_set_process_stat('virgin child');
234              
235             # recreate connection in child process
236 0         0 Mail::MtPolicyd::ConnectionPool->reconnect;
237              
238             # initialize session cache
239 0         0 $self->{'session_cache'} = Mail::MtPolicyd::SessionCache->new(
240             server => $self,
241             );
242 0 0 0     0 if( defined $self->{'session_cache_config'} &&
243             ref($self->{'session_cache_config'}) eq 'HASH') {
244 0         0 $self->{'session_cache'}->load_config( $self->{'session_cache_config'} );
245             }
246              
247 0         0 return;
248             }
249              
250             sub child_finish_hook {
251 0     0 1 0 my $self = shift;
252 0         0 $self->_set_process_stat('finish');
253              
254 0         0 Mail::MtPolicyd::ConnectionPool->shutdown;
255 0 0       0 if( defined $self->{'session_cache'} ) {
256 0         0 $self->{'session_cache'}->shutdown;
257             }
258              
259 0         0 return;
260             }
261              
262             sub get_conn_port {
263 0     0 0 0 my $self = shift;
264 0         0 my $server = $self->{server};
265 0         0 my $client = $server->{client};
266 0         0 my $port;
267 0   0     0 my $is_socket = $client && $client->UNIVERSAL::can('NS_proto') &&
268             $client->NS_proto eq 'UNIX';
269              
270 0 0       0 if( $is_socket ) {
271 0 0       0 $port = Net::Server->VERSION >= 2 ? $client->NS_port
272             : $client->NS_unix_path;
273             } else {
274 0         0 $port = $self->{'server'}->{'sockport'};
275             }
276 0         0 return($port);
277             }
278              
279             sub get_virtual_host {
280 0     0 0 0 my ( $self, $conn_port, $r ) = @_;
281 0         0 my $vhost;
282 0         0 my $policy_context = $r->attr('policy_context');
283              
284 0 0 0     0 if( $self->{'vhost_by_policy_context'}
      0        
285             && defined $policy_context
286             && $policy_context ne '' ) {
287 0         0 foreach my $vhost_port ( keys %{$self->{'virtual_hosts'}} ) {
  0         0  
288 0         0 $vhost = $self->{'virtual_hosts'}->{$vhost_port};
289 0 0       0 if( $policy_context eq $vhost->name ) {
290 0         0 return( $vhost );
291             }
292             }
293             }
294              
295 0         0 $vhost = $self->{'virtual_hosts'}->{$conn_port};
296 0 0       0 if( ! defined $vhost ) {
297 0         0 die('no virtual host defined for port '.$conn_port);
298             }
299 0         0 return($vhost);
300             }
301              
302             sub _is_loglevel {
303 0     0   0 my ( $self, $level ) = @_;
304 0 0 0     0 if( $self->{'server'}->{'log_level'} &&
305             $self->{'server'}->{'log_level'} >= $level ) {
306 0         0 return(1);
307             }
308 0         0 return(0);
309             }
310              
311             our %_LOG_ESCAPE_MAP = (
312             "\0" => '\0',
313             "\r" => '\r',
314             "\n" => '\n',
315             "\\" => '\\\\',
316             );
317              
318             our $_LOG_ESCAPE_MAP_RE = '['.join('',
319             map {
320             sprintf('\\x%02x', ord($_))
321             } keys %_LOG_ESCAPE_MAP
322             ).']';
323              
324             sub log {
325 5     5 1 22 my ( $self, $level, $msg, @params ) = @_;
326 5         26 $msg =~ s/($_LOG_ESCAPE_MAP_RE)/
327 1         5 $_LOG_ESCAPE_MAP{$1} /gse;
328 5         5 $msg =~ s/([\x01-\x08\x0b-\x0c\x0e-\x1f\x7f])/
329 0         0 sprintf('\\x%02X', ord($1)) /gse;
330 5         15 return $self->SUPER::log( $level, $msg, @params );
331             }
332              
333             sub _process_one_request {
334 0     0     my ( $self, $conn, $vhost, $r ) = @_;
335 0           my $port = $vhost->port;
336 0           my $s;
337             my $error;
338              
339 0           eval {
340 0           my $start_t = [gettimeofday];
341 0     0     local $SIG{'ALRM'} = sub { die "Request timeout!" };
  0            
342 0           my $timeout = $self->{'request_timeout'};
343 0           alarm($timeout);
344              
345 0 0         if( $self->_is_loglevel(4) ) { $self->log(4, 'request: '.$r->dump_attr); }
  0            
346 0           my $instance = $r->attr('instance');
347              
348 0           Mail::MtPolicyd::Profiler->tick('retrieve session');
349 0           $s = $self->{'session_cache'}->retrieve_session($instance);
350 0 0         if( $self->_is_loglevel(4) ) { $self->log(4, 'session: '.Dumper($s)); }
  0            
351 0           $r->session($s);
352              
353 0           Mail::MtPolicyd::Profiler->tick('run vhost');
354 0           my $result = $vhost->run($r);
355              
356 0           my $response = $result->as_policyd_response;
357 0           $conn->print($response);
358 0           $conn->flush;
359              
360             # convert to ms and round by 0.5/int
361 0           my $elapsed = int(tv_interval( $start_t, [gettimeofday] ) * 100 + 0.5);
362 0 0         my $matched = defined $result->last_match ? $result->last_match : '';
363 0           $self->log(1, $vhost->name.': instance='.$instance.', type='.$r->type.', t='.$elapsed.'ms, plugin='.$matched.', result='.$result->as_log);
364             };
365 0 0         if ( $@ ) { $error = $@; }
  0            
366              
367 0 0         if( defined $s ) {
368 0           $self->{'session_cache'}->store_session($s);
369             }
370              
371 0 0         if( defined $error ) { die( $error ); }
  0            
372              
373 0           return;
374             }
375              
376             sub process_request {
377 0     0 1   my ( $self, $conn ) = @_;
378 0           my $max_keepalive = $self->{'max_keepalive'};
379              
380 0           my $port = $self->get_conn_port;
381 0           $self->log(4, 'accepted connection on port '.$port );
382              
383 0   0       for( my $alive_count = 0
384             ; $max_keepalive == 0 || $alive_count < $max_keepalive
385             ; $alive_count++ ) {
386 0           my $r;
387 0           $self->_set_process_stat('waiting request');
388 0           Mail::MtPolicyd::Profiler->reset;
389 0           eval {
390 0     0     local $SIG{'ALRM'} = sub { die "Keepalive connection timeout" };
  0            
391 0           my $timeout = $self->{'keepalive_timeout'};
392 0           alarm($timeout);
393 0           Mail::MtPolicyd::Profiler->tick('parsing request');
394 0           $r = Mail::MtPolicyd::Request->new_from_fh( $conn, 'server' => $self );
395             };
396 0 0         if ( $@ =~ /Keepalive connection timeout/ ) {
    0          
    0          
397 0           $self->log(3, '['.$port.']: keepalive timeout: closing connection');
398 0           last;
399             } elsif($@ =~ /connection closed by peer/) {
400 0           $self->log(3, '['.$port.']: connection closed by peer');
401 0           last;
402             } elsif($@) {
403 0           $self->log(0, '['.$port.']: error while reading request: '.$@);
404 0           last;
405            
406             }
407 0           Mail::MtPolicyd::Profiler->tick('processing request');
408 0           my $vhost = $self->get_virtual_host($port, $r);
409 0           $self->_set_process_stat($vhost->name.', processing request');
410 0           eval {
411 0           $self->_process_one_request( $conn, $vhost, $r );
412             };
413 0 0         if ( $@ =~ /Request timeout!/ ) {
    0          
414 0           $self->log(1, '['.$port.']: request timed out');
415 0           last;
416             } elsif($@) {
417 0           $self->log(0, 'error while processing request: '.$@);
418 0           last;
419             }
420 0           Mail::MtPolicyd::Profiler->stop_current_timer;
421 0 0         if( $self->_is_loglevel(4) ) {
422 0           $self->log(4, Mail::MtPolicyd::Profiler->to_string);
423             }
424             }
425              
426 0           $self->log(3, '['.$port.']: closing connection');
427 0           $self->_set_process_stat('idle');
428              
429 0           return;
430             }
431              
432             sub _set_process_stat {
433 0     0     my ( $self, $stat ) = @_;
434 0           $0 = $self->{'program_name'}.' ('.$stat.')'
435             };
436              
437             sub memcached {
438 0     0 0   die('the global memcached connection does no longer exist in mtpolicyd >= 2.00');
439             }
440              
441             sub get_dbh {
442 0     0 0   die('the global dbh handle is no longer available in mtpolicyd >= 2.00');
443             }
444              
445             1;
446              
447             __END__
448              
449             =pod
450              
451             =encoding UTF-8
452              
453             =head1 NAME
454              
455             Mail::MtPolicyd - a modular policy daemon for postfix
456              
457             =head1 VERSION
458              
459             version 2.02
460              
461             =head1 DESCRIPTION
462              
463             Mail::MtPolicyd is the Net::Server class of the mtpolicyd daemon.
464              
465             =head2 SYNOPSIS
466              
467             use Mail::MtPolicyd;
468             Mail::MtPolicyd->run;
469              
470             =head1 AUTHOR
471              
472             Markus Benning <ich@markusbenning.de>
473              
474             =head1 COPYRIGHT AND LICENSE
475              
476             This software is Copyright (c) 2014 by Markus Benning <ich@markusbenning.de>.
477              
478             This is free software, licensed under:
479              
480             The GNU General Public License, Version 2, June 1991
481              
482             =cut