File Coverage

blib/lib/Any/Daemon/FCGI/ClientConn.pm
Criterion Covered Total %
statement 33 181 18.2
branch 0 82 0.0
condition 0 35 0.0
subroutine 11 27 40.7
pod 3 6 50.0
total 47 331 14.2


line stmt bran cond sub pod time code
1             # Copyrights 2013-2019 by [Mark Overmeer].
2             # For other contributors see ChangeLog.
3             # See the manual pages for details on the licensing terms.
4             # Pod stripped from pm file by OODoc 2.02.
5             # This code is part of distribution Any-Daemon-HTTP. Meta-POD processed
6             # with OODoc into POD and HTML manual-pages. See README.md
7             # Copyright Mark Overmeer. Licensed under the same terms as Perl itself.
8              
9             package Any::Daemon::FCGI::ClientConn;
10 1     1   6 use vars '$VERSION';
  1         2  
  1         46  
11             $VERSION = '0.29';
12              
13              
14 1     1   5 use warnings;
  1         2  
  1         19  
15 1     1   5 use strict;
  1         9  
  1         21  
16              
17 1     1   5 use Log::Report 'any-daemon-http';
  1         2  
  1         4  
18              
19 1     1   619 use HTTP::Request ();
  1         16678  
  1         23  
20 1     1   440 use Time::HiRes qw(usleep);
  1         1083  
  1         4  
21 1     1   165 use Errno qw(EAGAIN EINTR EWOULDBLOCK);
  1         3  
  1         50  
22 1     1   393 use IO::Select ();
  1         1349  
  1         24  
23 1     1   6 use Socket qw/inet_aton PF_INET AF_INET SHUT_RD SHUT_WR/;
  1         1  
  1         55  
24              
25 1     1   394 use Any::Daemon::FCGI::Request ();
  1         3  
  1         37  
26              
27             use constant
28 1         1722 { FCGI_VERSION => 1
29             , FCGI_KEEP_CONN => 1 # flag bit
30             , MAX_FRAME_SEND => 32 * 1024 # may have 65535 bytes content
31             , MAX_READ_CHUNKS => 16 * 1024
32             , CRLF => "\x0D\x0A"
33             , RESERVED => 0
34 1     1   5 };
  1         2  
35              
36             # Implementation heavily based on Net::Async::FastCGI::Request and
37             # Mojo::Server::FastCGI
38              
39             my %server_role_name2id =
40             ( RESPONDER => 1
41             , AUTHORIZER => 2
42             , FILTER => 3
43             );
44              
45             my %frame_name2id =
46             ( BEGIN_REQUEST => 1
47             , ABORT_REQUEST => 2
48             , END_REQUEST => 3
49             , PARAMS => 4
50             , STDIN => 5
51             , STDOUT => 6
52             , STDERR => 7
53             , DATA => 8
54             , GET_VALUES => 9
55             , GET_VALUES_RESULT => 10
56             , UNKNOWN_TYPE => 11
57             );
58              
59             my %end_status2id =
60             ( REQUEST_COMPLETE => 0
61             , CANT_MPX_CONN => 1
62             , OVERLOADED => 2
63             , UNKNOWN_ROLE => 3
64             );
65              
66             my %server_role_id2name = reverse %server_role_name2id;
67             my %frame_id2name = reverse %frame_name2id;
68              
69              
70 0     0 1   sub new($%) { (bless {}, $_[0])->init($_[1]) }
71              
72             sub init($)
73 0     0 0   { my ($self, $args) = @_;
74 0           $self->{ADFC_requests} = {};
75 0 0         $self->{ADFC_max_conns} = $args->{max_childs} or panic;
76 0           $self->{ADFC_max_reqs} = $args->{max_childs};
77              
78 0           $self->{ADFC_select} = my $select = IO::Select->new;
79 0 0         $self->{ADFC_socket} = my $socket = $args->{socket} or panic;
80 0           $self->{ADFC_stdin} = \my $stdin;
81 0           $self->{ADFC_keep_conn} = 0;
82 0           $select->add($socket);
83              
84 0           $self;
85             }
86              
87             #----------------
88              
89 0     0 1   sub socket() { shift->{ADFC_socket} }
90              
91             #----------------
92              
93             sub _next_record()
94 0     0     { my $self = shift;
95 0           my $leader = $self->_read_chunk(8);
96 0 0         length $leader==8 or return;
97              
98 0           my ($version, $type_id, $req_id, $clen, $plen) = unpack 'CCnnC', $leader;
99 0           my $body = $self->_read_chunk($clen + $plen);
100              
101 0 0         substr $body, -$plen, $plen, '' if $plen; # remove padding bytes
102 0 0         length $body==$clen or return;
103              
104 0   0       ($frame_id2name{$type_id} || 'UNKNOWN_TYPE', $req_id, \$body);
105             }
106              
107             sub _reply_record($$$)
108 0     0     { my ($self, $type, $req_id, $body) = @_;
109 0 0         my $type_id = $frame_name2id{$type} or panic $type;
110 0           my $empty = ! length $body; # write one empty frame
111              
112 0   0       while(length $body || $empty)
113 0           { my $chunk = substr $body, 0, MAX_FRAME_SEND, '';
114 0           my $size = length $chunk;
115 0           my $pad = (-$size) % 8; # advise to pad on 8 bytes
116 0           my $frame = pack "CCnnCxa${size}x${pad}"
117             , FCGI_VERSION, $type_id, $req_id, $size, $pad, $chunk;
118              
119 0           while(length $frame)
120 0           { my $wrote = syswrite $self->socket, $frame;
121 0 0         if(defined $wrote)
122 0           { substr $frame, 0, $wrote, '';
123 0           next;
124             }
125              
126 0 0 0       return unless $! == EAGAIN || $! == EINTR || $! == EWOULDBLOCK;
      0        
127 0           usleep 1000; # 1 ms
128             }
129              
130 0 0         last if $empty;
131             }
132             }
133              
134              
135             sub get_request()
136 0     0 1   { my $self = shift;
137 0           my $requests = $self->{ADFC_requests};
138 0           my $reqdata;
139              
140             ### At the moment, we will only support processing of whole requests
141             # and full replies: no chunking inside the server.
142              
143 0           while(1)
144 0 0         { my ($type, $req_id, $body) = $self->_next_record
145             or return;
146              
147 0 0         if($req_id==0)
148 0           { $self->_management_record($body);
149 0           next;
150             }
151              
152 0 0         if($type eq 'BEGIN_REQUEST')
153 0           { my ($role_id, $flags) = unpack 'nC', $$body;
154 0 0         my $role = $server_role_id2name{$role_id}
155             or $self->_fcgi_end_request(UNKNOWN_ROLE => $req_id);
156              
157 0           $requests->{$req_id} =
158             { request_id => $req_id
159             , data_complete => $role ne 'FILTER'
160             , stdin_complete => $role eq 'AUTHORIZER'
161             , params_complete => 0
162             , role => $role
163             , params => undef,
164             , stdin => undef,
165             , data => undef,
166             };
167              
168 0 0         unless($flags & FCGI_KEEP_CONN)
169             { # Actually, this flag is incorrectly: more threads may still be
170             # active. So, let's close when they all have ceased to exist.
171 0           info __x"fcgi {id} is last request", id => $req_id;
172 0           $self->{ADFC_keep_conn} = 0;
173             }
174              
175 0           next;
176             }
177              
178 0 0         defined $req_id or panic;
179 0           $reqdata = $requests->{$req_id};
180 0 0         unless($reqdata)
181 0           { notice __x"fcgi received {type} for {id} which does not exist now"
182             , type => $type, id => $req_id;
183 0           next;
184             }
185              
186 0 0         if($type eq 'ABORT_REQUEST')
    0          
    0          
    0          
187 0           { delete $requests->{$req_id};
188             }
189             elsif($type eq 'PARAMS')
190 0 0         { if(length $$body) { $reqdata->{params} .= $$body }
  0            
191 0           else { $reqdata->{params_complete} = 1 }
192             }
193             elsif($type eq 'STDIN') # Not for Authorizer
194 0 0         { if(length $$body) { $reqdata->{stdin} .= $$body }
  0            
195 0           else { $reqdata->{stdin_complete} = 1 }
196             }
197             elsif($type eq 'DATA') # Filter only
198 0 0         { if(length $$body) { $reqdata->{data} .= $$body }
  0            
199 0           else { $reqdata->{data_complete} = 1 }
200             }
201              
202             last if $reqdata->{params_complete}
203             && $reqdata->{stdin_complete}
204 0 0 0       && $reqdata->{data_complete};
      0        
205             }
206              
207             # We still have this record in $reqdata
208 0           my $req_id = $reqdata->{request_id};
209 0           delete $requests->{$req_id};
210              
211 0           my $enc_params = delete $reqdata->{params};
212 0           my $p = $reqdata->{params} = eval { $self->_body2hash(\$enc_params) };
  0            
213 0 0         if($@)
214 0           { notice __x"fcgi {id} params error: {err}", id => $req_id, err => $@;
215 0           delete $requests->{$req_id};
216 0           return $self->get_request;
217             }
218              
219 0   0       my $expected_stdin = $p->{CONTENT_LENGTH} || 0;
220             $expected_stdin == length $reqdata->{stdin}
221             or error __x"fcgi {id} received {got} bytes on stdin, expected {need}"
222             , id => $req_id
223             , got => length $reqdata->{stdin}
224 0 0         , need => $expected_stdin;
225              
226 0   0       my $expected_data = $p->{FCGI_DATA_LENGTH} || 0;
227             $expected_data == length $reqdata->{data}
228             or error __x"fcgi {id} received {got} bytes for data, expected {need}"
229             , id => $req_id
230             , got => length $reqdata->{data}
231 0 0         , need => $expected_data;
232              
233 0           my $request = Any::Daemon::FCGI::Request->new($reqdata);
234              
235 0           my $remote_ip = $request->param('REMOTE_ADDR');
236 0           my $remote_host = gethostbyaddr inet_aton($remote_ip), AF_INET;
237 0   0       info __x"fcgi {id} request from {host}"
238             , id => $req_id
239             , host => $remote_host || $remote_ip;
240              
241 0 0         $self->keep_connection
242             or $self->socket->shutdown(SHUT_RD);
243              
244 0           $request;
245             }
246              
247             sub send_response($;$)
248 0     0 0   { my ($self, $response, $stderr) = @_;
249              
250             #XXX Net::Async::FastCGI::Request demonstrates how to catch stdout and
251             #XXX stderr via ties. We don't use that here: cleanly work with
252             #XXX HTTP::Message objects... errors are logged locally.
253              
254 0           my $req_id = $response->request->request_id;
255              
256             # Simply "Status: " in front of the Response header will make the whole
257             # message HTTP::Response into a valid CGI response.
258 0           $self->_reply_record(STDOUT => $req_id
259             , 'Status: '.$response->as_string(CRLF));
260 0           $self->_reply_record(STDOUT => $req_id, '');
261              
262 0 0 0       if($stderr && length $$stderr)
263 0           { $self->_reply_record(STDERR => $req_id, $$stderr);
264 0           $self->_reply_record(STDERR => $req_id, '');
265             }
266              
267 0           $self->_fcgi_end_request(REQUEST_COMPLETE => $req_id);
268              
269 0 0         $self->keep_connection
270             or $self->socket->shutdown(SHUT_WR);
271              
272 0           $self;
273             }
274              
275             sub keep_connection()
276 0     0 0   { my $self = shift;
277 0 0         $self->{ADFC_keep_conn} || keys %{$self->{ADFC_requests}}
  0            
278             }
279              
280             #### MANAGEMENT RECORDS
281             # have req_id==0
282              
283             sub _management_record($$)
284 0     0     { my ($self, $type, $body) = @_;
285 0 0         $type eq 'GET_VALUES' ? $self->_fcgi_get_values($body)
286             : $self->_fcgi_unknown($body);
287             }
288              
289             # Request record FCGI_GET_VALUES may be used by the front-end server to
290             # collect back_end settings. In Apache, you have to configure it manually.
291              
292             sub _fcgi_get_values($)
293 0     0     { my $self = shift;
294 0           my %need = $self->_body2hash(shift);
295              
296             # The maximum number of concurrent transport connections this
297             # application will accept.
298             $need{FCGI_MAX_CONNS} = $self->{ADFC_max_conns}
299 0 0         if exists $need{FCGI_MAX_CONNS};
300              
301             # The maximum number of concurrent requests this application will accept.
302             $need{FCGI_MAX_REQS} = $self->{ADFC_max_reqs}
303 0 0         if exists $need{FCGI_MAX_REQS};
304              
305             # "0" if this application does not multiplex connections (i.e. handle
306             # concurrent requests over each connection), "1" otherwise.
307             $need{FCGI_MPXS_CONNS} = 0
308 0 0         if exists $need{FCGI_MPXS_CONNS};
309              
310 0           $self->_reply_record(GET_VALUES_RESULT => 0, $self->hash2body(\%need));
311             }
312              
313             # Reply record FCGI_UNKNOWN_TYPE is designed for protocol upgrades: to
314             # respond to unknown record types.
315              
316             sub _fcgi_unknown($)
317 0     0     { my ($self, $body) = @_;
318 0           $self->_reply_record(UNKNOWN_TYPE => 0, '');
319             }
320              
321             # Reply END_REQUEST is used for all ways to close a BEGIN_REQUEST session.
322             # It depends on the $status code which additionals fields were sent.
323              
324             sub _fcgi_end_request($$;$)
325 0     0     { my ($self, $status, $req_id, $rc) = @_;
326 0   0       my $body = pack "nCCCC", $rc || 0, $end_status2id{$status}
327             , RESERVED, RESERVED, RESERVED;
328              
329 0           $self->_reply_record(END_REQUEST => $req_id, $body);
330             }
331              
332             # Convert the FGCI request into a full HTTP::Request object
333             sub _body2hash($$)
334 0     0     { my ($self, $body) = @_;
335 0           my %h;
336              
337 0           while(length $$body)
338 0           { my $name_len = $self->_take_encoded_nv($body);
339 0           my $value_len = $self->_take_encoded_nv($body);
340            
341 0           my $name = substr $$body, 0, $name_len, '';
342 0           $h{$name} = substr $$body, 0, $value_len, '';
343             }
344              
345 0           \%h;
346             }
347              
348             sub _hash2body($)
349 0     0     { my ($self, $h) = @_;
350 0           my @params;
351 0           foreach my $name (sort keys %$h)
352 0           { my $name_len = length $name;
353 0           my $val_len = length $h->{$name};
354             push @params, pack "NNxa{$name_len}xa{$val_len}"
355 0           , $name_len, $val_len, $name, $h->{$name};
356             }
357 0           join '', @params;
358             }
359              
360             # Numerical values are 1 or 4 bytes. Long when first bit == 1
361             sub _take_encoded_nv($)
362 0     0     { my ($self, $body) = @_;
363 0           my $short = unpack 'C', substr $$body, 0, 1, '';
364 0 0         $short & 0x80 or return $short;
365              
366 0           my $long = pack('C', $short & 0x7F) . substr($$body, 0, 3, '');
367 0           unpack 'N', $long;
368             }
369              
370             sub _read_chunk($)
371 0     0     { my ($self, $need) = @_;
372 0           my $stdin = $self->{ADFC_stdin};
373              
374 0 0         return substr $$stdin, 0, $need, ''
375             if length $$stdin > $need;
376              
377 0           my $select = $self->{ADFC_select};
378              
379 0           while(length $$stdin < $need)
380 0 0         { $select->can_read or next;
381              
382 0           my $bytes_read = sysread $self->socket, my $more, MAX_READ_CHUNKS, 0;
383 0 0         if(defined $bytes_read)
384 0 0         { $bytes_read or last;
385 0           $$stdin .= $more;
386 0           next;
387             }
388              
389 0 0 0       last unless $! == EAGAIN || $! == EINTR || $! == EWOULDBLOCK;
      0        
390              
391 0           usleep 1000; # 1 ms
392             }
393              
394 0           substr $$stdin, 0, $need, '';
395             }
396              
397             1;