File Coverage

blib/lib/MHFS/HTTP/Server/Client.pm
Criterion Covered Total %
statement 44 232 18.9
branch 0 94 0.0
condition 0 18 0.0
subroutine 15 31 48.3
pod 0 13 0.0
total 59 388 15.2


line stmt bran cond sub pod time code
1             package MHFS::HTTP::Server::Client v0.7.0;
2 1     1   48 use 5.014;
  1         5  
3 1     1   7 use strict; use warnings;
  1     1   2  
  1         29  
  1         5  
  1         2  
  1         57  
4 1     1   6 use feature 'say';
  1         2  
  1         149  
5 1     1   8 use Time::HiRes qw( usleep clock_gettime CLOCK_REALTIME CLOCK_MONOTONIC);
  1         1  
  1         9  
6 1     1   136 use IO::Socket::INET;
  1         2  
  1         10  
7 1     1   809 use Errno qw(EINTR EIO :POSIX);
  1         3  
  1         533  
8 1     1   9 use Fcntl qw(:seek :mode);
  1         3  
  1         333  
9 1     1   623 use File::stat;
  1         9060  
  1         90  
10 1     1   35 use IO::Poll qw(POLLIN POLLOUT POLLHUP);
  1         5  
  1         85  
11 1     1   9 use Scalar::Util qw(looks_like_number weaken);
  1         2  
  1         61  
12 1     1   6 use Data::Dumper;
  1         2  
  1         54  
13 1     1   6 use Carp;
  1         1  
  1         56  
14 1     1   775 use MHFS::HTTP::Server::Client::Request;
  1         5  
  1         732  
15              
16             sub new {
17 0     0 0   my ($class, $sock, $server, $serverhostinfo, $ip) = @_;
18 0           $sock->blocking(0);
19 0           my %self = ('sock' => $sock, 'server' => $server, 'time' => clock_gettime(CLOCK_MONOTONIC), 'inbuf' => '', 'serverhostname' => $serverhostinfo->{'hostname'}, 'absurl' => $serverhostinfo->{'absurl'}, 'ip' => $ip, 'X-MHFS-PROXY-KEY' => $serverhostinfo->{'X-MHFS-PROXY-KEY'});
20 0           $self{'CONN-ID'} = int($self{'time'} * rand()); # insecure uid
21 0           $self{'outheaders'}{'X-MHFS-CONN-ID'} = sprintf("%X", $self{'CONN-ID'});
22 0           bless \%self, $class;
23 0           $self{'request'} = MHFS::HTTP::Server::Client::Request->new(\%self);
24 0           return \%self;
25             }
26              
27             # add a connection timeout timer
28             sub AddClientCloseTimer {
29 0     0 0   my ($self, $timelength, $id, $is_requesttimeout) = @_;
30 0           weaken($self); #don't allow this timer to keep the client object alive
31 0           my $server = $self->{'server'};
32 0           say "CCT | add timer: $id";
33             $server->{'evp'}->add_timer($timelength, 0, sub {
34 0 0   0     if(! defined $self) {
35 0           say "CCT | $id self undef";
36 0           return undef;
37             }
38             # Commented out as with connection reuse on, Apache 2.4.10 seems sometimes
39             # pass 408 on to the next client.
40             #if($is_requesttimeout) {
41             # say "CCT | \$timelength ($timelength) exceeded, sending 408";
42             # $self->{request}->Send408;
43             # CT_WRITE($self);
44             #}
45 0           say "CCT | \$timelength ($timelength) exceeded, closing CONN $id";
46 0           say "-------------------------------------------------";
47 0           $server->{'evp'}->remove($self->{'sock'});
48 0           say "poll has " . scalar ( $server->{'evp'}{'poll'}->handles) . " handles";
49 0           return undef;
50 0           }, $id);
51 0           return $id;
52             }
53              
54             sub KillClientCloseTimer {
55 0     0 0   my ($self, $id) = @_;
56 0           my $server = $self->{'server'};
57 0           say "CCT | removing timer: $id";
58 0           $server->{'evp'}->remove_timer_by_id($id);
59             }
60              
61             sub SetEvents {
62 0     0 0   my ($self, $events) = @_;
63 0           $self->{'server'}{'evp'}->set($self->{'sock'}, $self, $events);
64             }
65              
66             use constant {
67 1         3787 RECV_SIZE => 65536,
68             CT_YIELD => 1,
69             CT_DONE => undef,
70             #CT_READ => 1,
71             #CT_PROCESS = 2,
72             #CT_WRITE => 3
73 1     1   12 };
  1         3  
74              
75             # The "client_thread" consists of 5 states, CT_READ, CT_PROCESS, CT_WRITE, CT_YIELD, and CT_DONE
76             # CT_READ reads input data from the socket
77             ## on data read transitions to CT_PROCESS
78             ## on error transitions to CT_DONE
79             ## otherwise CT_YIELD
80              
81             # CT_PROCESS processes the input data
82             ## on processing done, switches to CT_WRITE or CT_READ to read more data to process
83             ## on error transitions to CT_DONE
84             ## otherwise CT_YIELD
85              
86             # CT_WRITE outputs data to the socket
87             ## on all data written transitions to CT_PROCESS unless Connection: close is set.
88             ## on error transitions to CT_DONE
89             ## otherwise CT_YIELD
90              
91             # CT_YIELD just returns control to the poll loop to wait for IO or allow another client thread to run
92              
93             # CT_DONE also returns control to the poll loop, it is called on error or when the client connection should be closed or is closed
94              
95             sub CT_READ {
96 0     0 0   my ($self) = @_;
97 0           my $tempdata;
98 0 0         if(!defined($self->{'sock'}->recv($tempdata, RECV_SIZE))) {
99 0 0 0       if(! ($!{EAGAIN} || $!{EWOULDBLOCK})) {
100 0           print ("CT_READ RECV errno: $!\n");
101 0           return CT_DONE;
102             }
103 0           say "CT_YIELD: $!";
104 0           return CT_YIELD;
105             }
106 0 0         if(length($tempdata) == 0) {
107 0           say 'Server::Client read 0 bytes, client read closed';
108 0           return CT_DONE;
109             }
110 0           $self->{'inbuf'} .= $tempdata;
111 0           goto &CT_PROCESS;
112             }
113              
114             sub CT_PROCESS {
115 0     0 0   my ($self) = @_;
116 0   0       $self->{'request'} //= MHFS::HTTP::Server::Client::Request->new($self);
117 0 0         if(!defined($self->{'request'}{'on_read_ready'})) {
118 0           die("went into CT_PROCESS in bad state");
119 0           return CT_YIELD;
120             }
121 0           my $res = $self->{'request'}{'on_read_ready'}->($self->{'request'});
122 0 0         if(!$res) {
123 0           return $res;
124             }
125 0 0         if(defined $self->{'request'}{'response'}) {
    0          
126 0           goto &CT_WRITE;
127             }
128             elsif(defined $self->{'request'}{'on_read_ready'}) {
129 0           goto &CT_READ;
130             }
131 0           return $res;
132             }
133              
134             sub CT_WRITE {
135 0     0 0   my ($self) = @_;
136 0 0         if(!defined $self->{'request'}{'response'}) {
137 0           die("went into CT_WRITE in bad state");
138 0           return CT_YIELD;
139             }
140             # TODO only TrySendResponse if there is data in buf or to be read
141 0           my $tsrRet = $self->TrySendResponse;
142 0 0         if(!defined($tsrRet)) {
    0          
143 0           say "-------------------------------------------------";
144 0           return CT_DONE;
145             }
146             elsif($tsrRet ne '') {
147 0 0 0       if($self->{'request'}{'outheaders'}{'Connection'} && ($self->{'request'}{'outheaders'}{'Connection'} eq 'close')) {
148 0           say "Connection close header set closing conn";
149 0           say "-------------------------------------------------";
150 0           return CT_DONE;
151             }
152 0           $self->{'request'} = undef;
153 0           goto &CT_PROCESS;
154             }
155 0           return CT_YIELD;
156             }
157              
158             sub do_on_data {
159 0     0 0   my ($self) = @_;
160 0           my $res = $self->{'request'}{'on_read_ready'}->($self->{'request'});
161 0 0         if($res) {
162 0 0         if(defined $self->{'request'}{'response'}) {
    0          
163             #say "do_on_data: goto onWriteReady";
164 0           goto &onWriteReady;
165             #return onWriteReady($self);
166             }
167             #else {
168             elsif(defined $self->{'request'}{'on_read_ready'}) {
169             #say "do_on_data: goto onReadReady inbuf " . length($self->{'inbuf'});
170 0           goto &onReadReady;
171             #return onReadReady($self);
172             }
173             else {
174 0           say "do_on_data: response and on_read_ready not defined, response by timer or poll?";
175             }
176             }
177 0           return $res;
178             }
179              
180              
181             sub onReadReady {
182 0     0 0   goto &CT_READ;
183 0           my ($self) = @_;
184 0           my $tempdata;
185 0 0         if(defined($self->{'sock'}->recv($tempdata, RECV_SIZE))) {
186 0 0         if(length($tempdata) == 0) {
187 0           say 'Server::Client read 0 bytes, client read closed';
188 0           return undef;
189             }
190 0           $self->{'inbuf'} .= $tempdata;
191 0           goto &do_on_data;
192             }
193 0 0         if(! $!{EAGAIN}) {
194 0           print ("MHFS::HTTP::Server::Client onReadReady RECV errno: $!\n");
195 0           return undef;
196             }
197 0           return '';
198             }
199              
200             sub onWriteReady {
201 0     0 0   goto &CT_WRITE;
202 0           my ($client) = @_;
203             # send the response
204 0 0         if(defined $client->{'request'}{'response'}) {
205             # TODO only TrySendResponse if there is data in buf or to be read
206 0           my $tsrRet = $client->TrySendResponse;
207 0 0         if(!defined($tsrRet)) {
    0          
208 0           say "-------------------------------------------------";
209 0           return undef;
210             }
211             elsif($tsrRet ne '') {
212 0 0 0       if($client->{'request'}{'outheaders'}{'Connection'} && ($client->{'request'}{'outheaders'}{'Connection'} eq 'close')) {
213 0           say "Connection close header set closing conn";
214 0           say "-------------------------------------------------";
215 0           return undef;
216             }
217 0           $client->{'request'} = MHFS::HTTP::Server::Client::Request->new($client);
218             # handle possible existing read data
219 0           goto &do_on_data;
220             }
221             }
222             else {
223 0           say "response not defined, probably set later by a timer or poll";
224             }
225 0           return 1;
226             }
227              
228             sub _TSRReturnPrint {
229 0     0     my ($sentthiscall) = @_;
230 0 0         if($sentthiscall > 0) {
231 0           say "wrote $sentthiscall bytes";
232             }
233             }
234              
235             sub TrySendResponse {
236 0     0 0   my ($client) = @_;
237 0           my $csock = $client->{'sock'};
238 0           my $dataitem = $client->{'request'}{'response'};
239 0 0         defined($dataitem->{'buf'}) or die("dataitem must always have a buf");
240 0           my $sentthiscall = 0;
241             do {
242             # Try to send the buf if set
243 0 0         if(length($dataitem->{'buf'})) {
244 0           my $sret = TrySendItem($csock, \$dataitem->{'buf'});
245             # critical conn error
246 0 0         if(! defined($sret)) {
247 0           _TSRReturnPrint($sentthiscall);
248 0           return undef;
249             }
250 0 0         if($sret) {
251 0           $sentthiscall += $sret;
252             # if we sent data, kill the send timer
253 0 0         if(defined $client->{'sendresponsetimerid'}) {
254 0           $client->KillClientCloseTimer($client->{'sendresponsetimerid'});
255 0           $client->{'sendresponsetimerid'} = undef;
256             }
257             }
258             # not all data sent, add timer
259 0 0         if(length($dataitem->{'buf'}) > 0) {
260 0   0       $client->{'sendresponsetimerid'} //= $client->AddClientCloseTimer($client->{'server'}{'settings'}{'sendresponsetimeout'}, $client->{'CONN-ID'});
261 0           _TSRReturnPrint($sentthiscall);
262 0           return '';
263             }
264              
265             #we sent the full buf
266             }
267              
268             # read more data
269 0           my $newdata;
270 0 0         if(defined $dataitem->{'fh'}) {
    0          
271 0           my $FH = $dataitem->{'fh'};
272 0           my $req_length = $dataitem->{'get_current_length'}->();
273 0           my $filepos = $dataitem->{'fh_pos'};
274             # TODO, remove this assert
275 0 0         if($filepos != tell($FH)) {
276 0           die('tell mismatch');
277             }
278 0 0 0       if($req_length && ($filepos >= $req_length)) {
279 0 0         if($filepos > $req_length) {
280 0           say "Reading too much tell: $filepos req_length: $req_length";
281             }
282 0           say "file read done";
283 0           close($FH);
284             }
285             else {
286 0           my $readamt = 24000;
287 0 0         if($req_length) {
288 0           my $tmpsend = $req_length - $filepos;
289 0 0         $readamt = $tmpsend if($tmpsend < $readamt);
290             }
291             # this is blocking, it shouldn't block for long but it could if it's a pipe especially
292 0           my $bytesRead = read($FH, $newdata, $readamt);
293 0 0         if(! defined($bytesRead)) {
    0          
294 0           $newdata = undef;
295 0           say "READ ERROR: $!";
296             }
297             elsif($bytesRead == 0) {
298             # read EOF, better remove the error
299 0 0         if(! $req_length) {
300 0           say '$req_length not set and read 0 bytes, treating as EOF';
301 0           $newdata = undef;
302             }
303             else {
304 0           say 'FH EOF ' .$filepos;
305 0           seek($FH, 0, 1);
306 0           _TSRReturnPrint($sentthiscall);
307 0           return '';
308             }
309             }
310             else {
311 0           $dataitem->{'fh_pos'} += $bytesRead;
312             }
313             }
314             }
315             elsif(defined $dataitem->{'cb'}) {
316 0           $newdata = $dataitem->{'cb'}->($dataitem);
317             }
318              
319 0           my $encode_chunked = $dataitem->{'is_chunked'};
320             # if we got to here and there's no data, fetching newdata is done
321 0 0         if(! $newdata) {
322 0           $dataitem->{'fh'} = undef;
323 0           $dataitem->{'cb'} = undef;
324 0           $dataitem->{'is_chunked'} = undef;
325 0           $newdata = '';
326             }
327              
328             # encode chunked encoding if needed
329 0 0         if($encode_chunked) {
330 0           my $sizeline = sprintf "%X\r\n", length($newdata);
331 0           $newdata = $sizeline.$newdata."\r\n";
332             }
333              
334             # add the new data to the dataitem buffer
335 0           $dataitem->{'buf'} .= $newdata;
336              
337 0           } while(length($dataitem->{'buf'}));
338 0           $client->{'request'}{'response'} = undef;
339              
340 0           _TSRReturnPrint($sentthiscall);
341 0           say "DONE Sending Data";
342 0           return 'RequestDone'; # not undef because keep-alive
343             }
344              
345             sub TrySendItem {
346 0     0 0   my ($csock, $dataref) = @_;
347 0           my $sret = send($csock, $$dataref, 0);
348 0 0         if(! defined($sret)) {
    0          
349 0 0         if($!{EAGAIN}) {
    0          
    0          
350             #say "SEND EAGAIN\n";
351 0           return 0;
352             }
353             elsif($!{ECONNRESET}) {
354 0           print "ECONNRESET\n";
355             }
356             elsif($!{EPIPE}) {
357 0           print "EPIPE\n";
358             }
359             else {
360 0           print "send errno $!\n";
361             }
362 0           return undef;
363             }
364             elsif($sret) {
365 0           substr($$dataref, 0, $sret, '');
366             }
367 0           return $sret;
368             }
369              
370             sub onHangUp {
371 0     0 0   my ($client) = @_;
372 0           return undef;
373             }
374              
375             sub DESTROY {
376 0     0     my $self = shift;
377 0           say "$$ MHFS::HTTP::Server::Client destructor: ";
378 0           say "$$ ".'X-MHFS-CONN-ID: ' . $self->{'outheaders'}{'X-MHFS-CONN-ID'};
379 0 0         if($self->{'sock'}) {
380             #shutdown($self->{'sock'}, 2);
381 0           close($self->{'sock'});
382             }
383             }
384              
385             1;