File Coverage

bin/qmail-deliverabled
Criterion Covered Total %
statement 26 37 70.2
branch 1 18 5.5
condition n/a
subroutine 8 9 88.8
pod 0 1 0.0
total 35 65 53.8


line stmt bran cond sub pod time code
1             #!/usr/bin/perl -Tw
2 7     7   28329 use strict;
  7         33  
  7         748  
3              
4 7     7   118 use IO::Socket::INET;
  7         142  
  7         415  
5 7     7   18475 use POSIX ();
  7         48  
  7         493  
6 7     7   63 use Qmail::Deliverable ':all';
  7         31  
  7         6240  
7 7     7   7549 use Getopt::Long;
  7         133532  
  7         52  
8             Getopt::Long::Configure("bundling");
9              
10             my ( $pidfile, $verbose, $stop, $foreground );
11             my $listen = "127.0.0.1:8998";
12              
13             sub _uri_unescape {
14 19     19   44 my ($value) = @_;
15 19         144 $value =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
  22         168  
16 19         61 return $value;
17             }
18              
19             sub _send_response {
20 23     23   63 my ( $sock, $code, $message, $body ) = @_;
21 23 50       49 $body = '' if not defined $body;
22 23         112 my $response = join "",
23             "HTTP/1.0 $code $message\r\n",
24             "Content-Length: " . length($body) . "\r\n",
25             "Content-Type: text/plain\r\n",
26             "Connection: close\r\n",
27             "\r\n",
28             $body;
29 23         37 print {$sock} $response;
  23         1768  
30             }
31              
32             sub _send_error {
33 6     6   19 my ( $sock, $code, $message ) = @_;
34 6         29 _send_response( $sock, $code, $message, "$code $message\n" );
35             }
36              
37             GetOptions(
38             "help|h" => sub { die "Use 'man qmail-deliverabled' for full documentation.\n" },
39             "verbose|v" => \$verbose,
40             "listen|l=s" => \$listen,
41             "pidfile|p:s" => \$pidfile,
42             "stop" => \$stop,
43             "foreground|f" => \$foreground,
44             ) or exit 255;
45              
46             ($listen) = $listen =~ /^(stop|[0-9.]+:[0-9]+)$/
47             or die "Listen argument must be ip:port!\n";
48              
49             if ($pidfile) {
50             ($pidfile) = $pidfile =~ m[^(/[\x20-\xff]+)$]
51             or die "pidfile must be an absolute path, beginning with a /.\n";
52             }
53              
54             chdir '/';
55              
56             if ( $stop or $listen eq 'stop' ) {
57             die "Cannot --stop without --pidfile.\n" if not $pidfile;
58             open my $fh, '<', $pidfile or die "Could not open pidfile $pidfile: $!\n";
59             my $pid = readline $fh;
60             ($pid) = $pid =~ /^([2-9]|[0-9]{2,})$/
61             or die "Could not read PID from $pidfile\n";
62             close $fh;
63             kill 15, $pid;
64             sleep 1;
65             kill 9, $pid;
66             unlink $pidfile;
67             exit;
68             }
69              
70             # Bind the listener before daemonizing so any "address already in use"
71             # error surfaces in the foreground where the operator can see it.
72             my ( $listen_host, $listen_port ) = $listen =~ /^([0-9.]+):([0-9]+)\z/
73             or die "Listen argument must be ip:port!\n";
74             my $d = IO::Socket::INET->new(
75             LocalAddr => $listen_host,
76             LocalPort => $listen_port,
77             Proto => 'tcp',
78             Listen => 5,
79             ReuseAddr => 1,
80             ) or die "Could not start daemon ($!)\n";
81              
82             daemonize() unless $foreground;
83              
84             # pidfile is written AFTER daemonization so it records the final PID.
85             # Only the surviving (grand)child reaches this point.
86             my $cleanup_pidfile;
87              
88             END {
89             unlink $cleanup_pidfile
90             if defined $cleanup_pidfile && -e $cleanup_pidfile;
91             }
92              
93             if ($pidfile) {
94             open my $fh, '>', $pidfile
95             or die "Could not open pidfile $pidfile: $!\n";
96             print {$fh} $$
97             or die "Could not write to pidfile $pidfile: $!\n";
98             close $fh
99             or die "Could not close pidfile $pidfile: $!\n";
100             $cleanup_pidfile = $pidfile;
101             }
102              
103             $SIG{TERM} = $SIG{INT} = sub { exit 0 };
104             $SIG{HUP} = sub {
105             warn "SIGHUP received.\n";
106             reread_config;
107             warn "Qmail configuration reloaded.\n";
108             };
109             $SIG{PIPE} = 'IGNORE'; # broken client mid-response should not kill us
110              
111             $verbose && print "My PID is $$.\n";
112              
113             my ($base0) = $0 =~ /([\x20-\x7f]+)/;
114             my %counter;
115             $counter{yes} = $counter{no} = 0;
116              
117             $| = 1;
118              
119             for ( ;; ) {
120             $verbose && printf "Listening on %s.\n", $listen;
121             while ( my $c = $d->accept ) {
122             $verbose && printf "Accepted request from %vd.\n", $c->peeraddr;
123             my $request_line = <$c>;
124             if ( not defined $request_line ) {
125             $c->close;
126             undef($c);
127             next;
128             }
129              
130             while ( my $line = <$c> ) {
131             last if $line =~ /^\r?\n\z/;
132             }
133              
134             my ( $method, $target ) = $request_line =~ /^([A-Z]+) ([\x21-\x7e]+) HTTP\/1\.[01]\r?\n\z/;
135             if ( not defined $method or $method ne 'GET' or $target !~ m[^/qd1/] ) {
136             $verbose && print "Not a qd request.\n";
137             _send_error( $c, 403, "Forbidden" );
138             $c->close;
139             undef($c);
140             next;
141             }
142              
143             my ( $command, $raw_query ) = $target =~ m{\A/qd1/([A-Za-z_]+)(?:\?(.*))?\z};
144             if ( not defined $command ) {
145             $verbose && print "Not a qd request.\n";
146             _send_error( $c, 403, "Forbidden" );
147             $c->close;
148             undef($c);
149             next;
150             }
151              
152             my $arg =
153             defined $raw_query && length $raw_query
154             ? _uri_unescape($raw_query)
155             : "\0";
156              
157             ($arg) = $arg =~ /^([\x20-\x7e]*)\z/ or do {
158             $verbose && print "Invalid data received.\n";
159             _send_error( $c, 400, "Bad Request" );
160             $c->close;
161             undef($c);
162             next;
163             };
164              
165             my $rv;
166             if ( $command eq 'qmail_local' ) {
167             $verbose && printf "qmail_local('%s') => ", $arg;
168             $rv = eval { qmail_local($arg) };
169             if ($@) {
170             $verbose && warn "qmail_local error: $@";
171             _send_error( $c, 500, "Internal Server Error" );
172             $c->close;
173             undef($c);
174             next;
175             }
176             $verbose && printf "%s\n", defined $rv ? $rv : '(undef)';
177             }
178             elsif ( $command eq 'deliverable' ) {
179             $verbose && printf "deliverable('%s') => ", $arg;
180             $rv = eval { deliverable($arg) };
181             if ($@) {
182             $verbose && warn "deliverable error: $@";
183             _send_error( $c, 500, "Internal Server Error" );
184             $c->close;
185             undef($c);
186             next;
187             }
188             $verbose && printf "%s\n", defined $rv ? sprintf( "0x%02x", $rv ) : '(undef)';
189             $counter{yes}++ if $rv;
190             $counter{no}++ if !$rv;
191             my $total = $counter{yes} + $counter{no};
192             $0 = sprintf "$base0 yes=%d(%.1f%%), no=%d(%.1f%%), total=%d",
193             $counter{yes}, $counter{yes} / $total * 100,
194             $counter{no}, $counter{no} / $total * 100,
195             $total;
196             }
197             else {
198             $verbose && printf "Unknown command: %s\n", $command;
199             _send_error( $c, 403, "Forbidden" );
200             $c->close;
201             undef($c);
202             next;
203             }
204             if ( defined $rv ) {
205             _send_response( $c, 200, "OK", $rv );
206             }
207             else {
208             _send_response( $c, 204, "UNDEF", '' );
209             }
210             $c->close;
211             undef($c);
212             }
213             sleep 5;
214             }
215              
216             # Standard double-fork daemonization: detaches from the controlling terminal,
217             # becomes its own session leader, and redirects standard handles to /dev/null
218             # so output from libraries doesn't end up on a terminal that no longer cares.
219             sub daemonize {
220 0     0 0   my $pid = fork;
221 0 0         die "fork: $!\n" if not defined $pid;
222 0 0         exit 0 if $pid; # original parent exits
223              
224 0 0         POSIX::setsid() != -1 or die "setsid: $!\n";
225              
226 0           $pid = fork;
227 0 0         die "fork: $!\n" if not defined $pid;
228 0 0         exit 0 if $pid; # session leader exits
229              
230 0 0         open STDIN, '<', '/dev/null' or die "reopen STDIN: $!\n";
231 0 0         open STDOUT, '>', '/dev/null' or die "reopen STDOUT: $!\n";
232 0 0         open STDERR, '>', '/dev/null' or die "reopen STDERR: $!\n";
233 0           umask 0;
234             }
235              
236             __END__