File Coverage

blib/lib/Plack/Handler/Net/FastCGI.pm
Criterion Covered Total %
statement 42 155 27.1
branch 1 58 1.7
condition 1 18 5.5
subroutine 13 26 50.0
pod 0 3 0.0
total 57 260 21.9


line stmt bran cond sub pod time code
1             package Plack::Handler::Net::FastCGI;
2 1     1   25952 use strict;
  1         2  
  1         24  
3 1     1   867 use IO::Socket qw[];
  1         350875  
  1         31  
4 1     1   1026 use Net::FastCGI 0.12;
  1         11109  
  1         42  
5 1     1   18 use Net::FastCGI::Constant qw[:common :type :flag :role :protocol_status];
  1         2  
  1         309  
6 1     1   1106 use Net::FastCGI::IO qw[:all];
  1         5992  
  1         211  
7 1     1   10 use Net::FastCGI::Protocol qw[:all];
  1         2  
  1         323  
8 1     1   1171 use Plack::TempBuffer qw[];
  1         12809  
  1         29  
9 1     1   997 use Plack::Util qw[];
  1         7402  
  1         27  
10 1     1   1319 use URI qw[];
  1         4956  
  1         26  
11 1     1   10 use URI::Escape qw[uri_unescape];
  1         3  
  1         175  
12              
13             BEGIN {
14 1     1   3 our $VERSION = '0.01';
15              
16 1         1 eval {
17 1         412 require PerlIO::code;
18             };
19 1 50       7 my $mode = $@ ? ">:via(@{[__PACKAGE__]})" : '>:Code';
  1         6  
20 1         150 *PERLIO_MODE = sub () { $mode };
  0         0  
21             }
22              
23             sub DEBUG () { 0 }
24              
25             sub new {
26 0     0 0   my $class = shift;
27 0           my $self = bless { @_ }, $class;
28 0 0 0       $self->{listen} ||= [ ":$self->{port}" ] if $self->{port};
29 0   0       $self->{values} ||= {
30             FCGI_MAX_CONNS => 1, # maximum number of concurrent transport connections this application will accept
31             FCGI_MAX_REQS => 1, # maximum number of concurrent requests this application will accept
32             FCGI_MPXS_CONNS => 0, # this implementation can't multiplex
33             };
34 0           $self;
35             }
36              
37             BEGIN {
38 1     1   6 require Socket;
39 1   33     1 my $HAS_AF_UNIX = eval { Socket->import('AF_UNIX'); defined(my $v = &AF_UNIX) } && !$@;
40 1         764 *HAS_AF_UNIX = sub () { $HAS_AF_UNIX };
  0         0  
41             }
42              
43             sub run {
44 0     0 0   my ($self, $app) = @_;
45 0           $self->{app} = $app;
46              
47 0           my $socket;
48             my $proto;
49 0           my $port;
50              
51 0 0         if ($self->{listen}) {
52 0           $port = $self->{listen}->[0];
53 0 0         if ($port =~ s/^://) {
54 0           $proto = 'tcp';
55 0 0         $socket = IO::Socket::INET->new(
56             Listen => 5,
57             LocalPort => $port,
58             Reuse => 1
59             ) or die "Couldn't create listener socket: $!";
60             } else {
61 0           $proto = 'unix';
62 0 0         $socket = IO::Socket::UNIX->new(
63             Listen => 5,
64             Local => $port,
65             ) or die "Couldn't create UNIX listener socket: $!";
66             }
67             }
68             else {
69 0 0         (-S STDIN)
70             || die "Standard input is not a socket: specify a listen location";
71              
72 0           $socket = \*STDIN;
73 0           $proto = 'tcp';
74              
75 0           if (HAS_AF_UNIX) {
76 0           my $sockaddr = getsockname(*STDIN);
77 0 0         if (unpack('S', $sockaddr) == &Socket::AF_UNIX) {
78 0           $proto = 'unix';
79             }
80             }
81             }
82              
83 0 0 0       $self->{server_ready}->({
84             host => 'localhost',
85             port => $port,
86             proto => $proto,
87             server_software => 'Plack::Handler::Net::FastCGI',
88             }) if $self->{server_ready} && $proto;
89              
90 0           while (accept(my $connection, $socket)) {
91 0           $self->_handle_connection($connection);
92             }
93             }
94              
95             sub _handle_request {
96 0     0     my($self, $env, $stdin, $stdout, $stderr) = @_;
97              
98 0 0 0       $env = {
99             %$env,
100             'psgi.version' => [1,1],
101             'psgi.url_scheme' => ($env->{HTTPS}||'off') =~ /^(?:on|1)$/i ? 'https' : 'http',
102             'psgi.input' => $stdin,
103             'psgi.errors' => $stderr,
104             'psgi.multithread' => Plack::Util::FALSE,
105             'psgi.multiprocess' => Plack::Util::FALSE,
106             'psgi.run_once' => Plack::Util::FALSE,
107             'psgi.streaming' => Plack::Util::TRUE,
108             'psgi.nonblocking' => Plack::Util::FALSE,
109             };
110              
111 0           delete $env->{HTTP_CONTENT_TYPE};
112 0           delete $env->{HTTP_CONTENT_LENGTH};
113              
114             # lighttpd munges multiple slashes in PATH_INFO into one. Try recovering it
115 0           my $uri = URI->new("http://localhost" . $env->{REQUEST_URI});
116 0           $env->{PATH_INFO} = uri_unescape($uri->path);
117 0           $env->{PATH_INFO} =~ s/^\Q$env->{SCRIPT_NAME}\E//;
118              
119             # root access for mod_fastcgi
120 0 0         if (!exists $env->{PATH_INFO}) {
121 0           $env->{PATH_INFO} = '';
122             }
123              
124 0           my $res = Plack::Util::run_app $self->{app}, $env;
125              
126 0 0         if (ref $res eq 'ARRAY') {
    0          
127 0           $self->_handle_response($res, $stdout);
128             } elsif (ref $res eq 'CODE') {
129             $res->(sub {
130 0     0     $self->_handle_response($_[0], $stdout);
131 0           });
132             } else {
133 0           die "Bad response $res";
134             }
135             }
136              
137             sub _handle_response {
138 0     0     my ($self, $res, $stdout) = @_;
139              
140 0           my $hdrs;
141 0           $hdrs = "Status: $res->[0]\015\012";
142              
143 0           my $headers = $res->[1];
144 0           while (my ($k, $v) = splice @$headers, 0, 2) {
145 0           $hdrs .= "$k: $v\015\012";
146             }
147 0           $hdrs .= "\015\012";
148              
149 0           print {$stdout} $hdrs;
  0            
150              
151 0     0     my $cb = sub { print {$stdout} $_[0] };
  0            
  0            
152 0           my $body = $res->[2];
153 0 0         if (defined $body) {
154 0           Plack::Util::foreach($body, $cb);
155             }
156             else {
157             return Plack::Util::inline_object
158             write => $cb,
159 0     0     close => sub { };
  0            
160             }
161             }
162              
163             our $STDOUT_BUFFER_SIZE = 8192;
164             our $STDERR_BUFFER_SIZE = 0;
165              
166 1     1   6 use warnings FATAL => 'Net::FastCGI::IO';
  1         1  
  1         1001  
167              
168             sub _handle_connection {
169 0     0     my($self, $socket) = @_;
170              
171 0           my ( $current_id, # id of the request we are currently processing
172             $stdin, # buffer for stdin
173             $stdout, # buffer for stdout
174             $stderr, # buffer for stderr
175             $params, # buffer for params (environ)
176             $done, # done with connection?
177             $keep_conn ); # more requests on this connection?
178              
179 0           ($current_id, $stdin, $stdout, $stderr) = (0, undef, '', '');
180              
181 0           while (!$done) {
182 0 0         my ($type, $request_id, $content) = read_record($socket)
183             or last;
184              
185 0           if (DEBUG) {
186             warn '< ', dump_record($type, $request_id, $content), "\n";
187             }
188              
189 0 0 0       if ($request_id == FCGI_NULL_REQUEST_ID) {
    0          
    0          
    0          
    0          
    0          
190 0 0         if ($type == FCGI_GET_VALUES) {
191 0           my $query = parse_params($content);
192 0           my %reply = map { $_ => $self->{values}->{$_} }
  0            
193 0           grep { exists $self->{values}->{$_} }
194             keys %$query;
195 0           write_record($socket, FCGI_GET_VALUES_RESULT,
196             FCGI_NULL_REQUEST_ID, build_params(\%reply));
197             }
198             else {
199 0           write_record($socket, FCGI_UNKNOWN_TYPE,
200             FCGI_NULL_REQUEST_ID, build_unknown_type($type));
201             }
202             }
203             elsif ($request_id != $current_id && $type != FCGI_BEGIN_REQUEST) {
204             # ignore inactive requests (FastCGI Specification 3.3)
205             }
206             elsif ($type == FCGI_ABORT_REQUEST) {
207 0           $current_id = 0;
208 0           ($stdin, $stdout, $stderr, $params) = (undef, '', '', '');
209             }
210             elsif ($type == FCGI_BEGIN_REQUEST) {
211 0           my ($role, $flags) = parse_begin_request_body($content);
212 0 0 0       if ($current_id || $role != FCGI_RESPONDER) {
213 0 0         my $status = $current_id ? FCGI_CANT_MPX_CONN : FCGI_UNKNOWN_ROLE;
214 0           write_record($socket, FCGI_END_REQUEST, $request_id,
215             build_end_request_body(0, $status));
216             }
217             else {
218 0           $current_id = $request_id;
219 0           $stdin = Plack::TempBuffer->new;
220 0           $keep_conn = ($flags & FCGI_KEEP_CONN);
221             }
222             }
223             elsif ($type == FCGI_PARAMS) {
224 0           $params .= $content;
225             }
226             elsif ($type == FCGI_STDIN) {
227 0           $stdin->print($content);
228              
229 0 0         unless (length $content) {
230 0           my $in = $stdin->rewind;
231              
232             my $stdout_cb = sub {
233 0     0     $stdout .= $_[0];
234 0 0         if (length $stdout >= $STDOUT_BUFFER_SIZE) {
235 0           write_stream($socket, FCGI_STDOUT, $current_id, $stdout, 0);
236 0           $stdout = '';
237             }
238 0           };
239              
240 0 0         open(my $out, PERLIO_MODE, $stdout_cb)
241             || die(qq/Couldn't open sub as fh: $!/);
242              
243             my $stderr_cb = sub {
244 0     0     $stderr .= $_[0];
245 0 0         if (length $stderr >= $STDERR_BUFFER_SIZE) {
246 0           write_stream($socket, FCGI_STDERR, $current_id, $stderr, 0);
247 0           $stderr = '';
248             }
249 0           };
250              
251 0 0         open(my $err, PERLIO_MODE, $stderr_cb)
252             || die(qq/Couldn't open sub as fh: $!/);
253              
254 0           $self->_handle_request(parse_params($params), $in, $out, $err);
255              
256 0           write_stream($socket, FCGI_STDOUT, $current_id, $stdout, 1);
257 0           write_stream($socket, FCGI_STDERR, $current_id, $stderr, 1);
258 0           write_record($socket, FCGI_END_REQUEST, $current_id,
259             build_end_request_body(0, FCGI_REQUEST_COMPLETE));
260              
261             # prepare for next request
262 0           $current_id = 0;
263 0           ($stdin, $stdout, $stderr, $params) = (undef, '', '', '');
264             }
265             }
266             else {
267 0           warn(qq/Received an unknown record type '$type'/);
268             }
269             }
270             }
271              
272             sub PUSHED {
273 0     0 0   my ($class) = @_;
274 0           return bless \(my $self), $class;
275             }
276              
277             sub OPEN {
278 0     0     my ($self, $sub) = @_;
279 0           $$self = $sub;
280             }
281              
282             sub WRITE {
283 0     0     my ($self) = @_;
284 0           $$self->($_[1]);
285 0           return length $_[1];
286             }
287              
288             1;
289              
290             __END__