File Coverage

blib/lib/MHFS/HTTP/Server/Client/Request.pm
Criterion Covered Total %
statement 82 566 14.4
branch 2 192 1.0
condition 0 70 0.0
subroutine 29 70 41.4
pod 0 27 0.0
total 113 925 12.2


line stmt bran cond sub pod time code
1             package MHFS::HTTP::Server::Client::Request v0.7.0;
2 1     1   22 use 5.014;
  1         4  
3 1     1   8 use strict; use warnings;
  1     1   2  
  1         42  
  1         7  
  1         2  
  1         60  
4 1     1   7 use feature 'say';
  1         2  
  1         164  
5 1     1   9 use Time::HiRes qw( usleep clock_gettime CLOCK_REALTIME CLOCK_MONOTONIC);
  1         2  
  1         10  
6 1     1   787 use URI::Escape;
  1         2362  
  1         86  
7 1     1   8 use Cwd qw(abs_path getcwd);
  1         2  
  1         56  
8 1     1   6 use Feature::Compat::Try;
  1         2  
  1         10  
9 1     1   74 use File::Basename;
  1         2  
  1         93  
10 1     1   41 use File::stat;
  1         3  
  1         77  
11 1     1   6 use IO::Poll qw(POLLIN POLLOUT POLLHUP);
  1         2  
  1         72  
12 1     1   6 use Data::Dumper;
  1         2  
  1         65  
13 1     1   7 use Scalar::Util qw(weaken);
  1         2  
  1         60  
14 1     1   7 use List::Util qw[min max];
  1         2  
  1         76  
15 1     1   6 use Symbol 'gensym';
  1         2  
  1         61  
16 1     1   6 use Devel::Peek;
  1         3  
  1         7  
17 1     1   769 use Encode qw(decode encode);
  1         20496  
  1         129  
18             use constant {
19 1         127 MAX_REQUEST_SIZE => 8192,
20 1     1   9 };
  1         3  
21 1     1   1097 use FindBin;
  1         1578  
  1         72  
22 1     1   10 use File::Spec;
  1         2  
  1         26  
23 1     1   7 use MHFS::EventLoop::Poll;
  1         2  
  1         33  
24 1     1   657 use MHFS::Process;
  1         5  
  1         65  
25 1     1   604 use MHFS::Util qw(get_printable_utf8 LOCK_GET_LOCKDATA getMIME shell_escape escape_html_noquote parse_ipv4);
  1         4  
  1         250  
26             BEGIN {
27 1 50   1   136 if( ! (eval "use JSON; 1")) {
  1     1   550  
  0         0  
  0         0  
28 1 50   1   119 eval "use JSON::PP; 1" or die "No implementation of JSON available";
  1         940  
  1         23063  
  1         93  
29 1         156 warn __PACKAGE__.": Using PurePerl version of JSON (JSON::PP)";
30             }
31             }
32              
33             # Optional dependency, Alien::Tar::Size
34             BEGIN {
35 1     1   9 use constant HAS_Alien_Tar_Size => (eval "use Alien::Tar::Size; 1");
  1     1   3  
  1         110  
  1         742  
  1         58749  
  1         14  
36 1     1   12454 if(! HAS_Alien_Tar_Size) {
37             warn "Alien::Tar::Size is not available";
38             }
39             }
40              
41             sub new {
42 0     0 0   my ($class, $client) = @_;
43 0           my %self = ( 'client' => $client);
44 0           bless \%self, $class;
45 0           weaken($self{'client'}); #don't allow Request to keep client alive
46 0           $self{'on_read_ready'} = \&want_request_line;
47 0           $self{'outheaders'}{'X-MHFS-CONN-ID'} = $client->{'outheaders'}{'X-MHFS-CONN-ID'};
48 0           $self{'rl'} = 0;
49             # we want the request
50 0           $client->SetEvents(POLLIN | MHFS::EventLoop::Poll->ALWAYSMASK );
51 0           $self{'recvrequesttimerid'} = $client->AddClientCloseTimer($client->{'server'}{'settings'}{'recvrequestimeout'}, $client->{'CONN-ID'}, 1);
52 0           return \%self;
53             }
54              
55             # on ready ready handlers
56             sub want_request_line {
57 0     0 0   my ($self) = @_;
58              
59 0           my $ipos = index($self->{'client'}{'inbuf'}, "\r\n");
60 0 0         if($ipos != -1) {
    0          
61 0 0         if(substr($self->{'client'}{'inbuf'}, 0, $ipos+2, '') =~ /^(([^\s]+)\s+([^\s]+)\s+(?:HTTP\/1\.([0-1])))\r\n/) {
62 0           my $rl = $1;
63 0           $self->{'method'} = $2;
64 0           $self->{'uri'} = $3;
65 0           $self->{'httpproto'} = $4;
66 0           my $rid = int(clock_gettime(CLOCK_MONOTONIC) * rand()); # insecure uid
67 0           $self->{'outheaders'}{'X-MHFS-REQUEST-ID'} = sprintf("%X", $rid);
68 0           say "X-MHFS-CONN-ID: " . $self->{'outheaders'}{'X-MHFS-CONN-ID'} . " X-MHFS-REQUEST-ID: " . $self->{'outheaders'}{'X-MHFS-REQUEST-ID'};
69 0           say "RECV: $rl";
70 0 0 0       if(($self->{'method'} ne 'GET') && ($self->{'method'} ne 'HEAD') && ($self->{'method'} ne 'PUT')) {
      0        
71 0           say "X-MHFS-CONN-ID: " . $self->{'outheaders'}{'X-MHFS-CONN-ID'} . 'Invalid method: ' . $self->{'method'}. ', closing conn';
72 0           return undef;
73             }
74 0           my ($path, $querystring) = ($self->{'uri'} =~ /^([^\?]+)(?:\?)?(.*)$/g);
75 0           say("raw path: $path\nraw querystring: $querystring");
76              
77             # transformations
78             ## Path
79 0           $path = uri_unescape($path);
80 0           my %pathStruct = ( 'unescapepath' => $path );
81              
82             # collapse slashes
83 0           $path =~ s/\/{2,}/\//g;
84 0           say "collapsed: $path";
85 0           $pathStruct{'unsafecollapse'} = $path;
86              
87             # without trailing slash
88 0 0         if(index($pathStruct{'unsafecollapse'}, '/', length($pathStruct{'unsafecollapse'})-1) != -1) {
89 0           chop($path);
90 0           say "no slash path: $path ";
91             }
92 0           $pathStruct{'unsafepath'} = $path;
93              
94             ## Querystring
95 0           my %qsStruct;
96             # In the querystring spaces are sometimes encoded as + for legacy reasons unfortunately
97 0           $querystring =~ s/\+/%20/g;
98 0           my @qsPairs = split('&', $querystring);
99 0           foreach my $pair (@qsPairs) {
100 0           my($key, $value) = split('=', $pair);
101 0 0         if(defined $value) {
102 0 0         if(!defined $qsStruct{$key}) {
103 0           $qsStruct{$key} = uri_unescape($value);
104             }
105             else {
106 0 0         if(ref($qsStruct{$key}) ne 'ARRAY') {
107 0           $qsStruct{$key} = [$qsStruct{$key}];
108             };
109 0           push @{$qsStruct{$key}}, uri_unescape($value);
  0            
110             }
111             }
112             }
113              
114 0           $self->{'path'} = \%pathStruct;
115 0           $self->{'qs'} = \%qsStruct;
116 0           $self->{'on_read_ready'} = \&want_headers;
117             #return want_headers($self);
118 0           goto &want_headers;
119             }
120             else {
121 0           say "X-MHFS-CONN-ID: " . $self->{'outheaders'}{'X-MHFS-CONN-ID'} . ' Invalid Request line, closing conn';
122 0           return undef;
123             }
124             }
125             elsif(length($self->{'client'}{'inbuf'}) > MAX_REQUEST_SIZE) {
126 0           say "X-MHFS-CONN-ID: " . $self->{'outheaders'}{'X-MHFS-CONN-ID'} . ' No Request line, closing conn';
127 0           return undef;
128             }
129 0           return 1;
130             }
131              
132             sub want_headers {
133 0     0 0   my ($self) = @_;
134 0           my $ipos;
135 0           while($ipos = index($self->{'client'}{'inbuf'}, "\r\n")) {
136 0 0         if($ipos == -1) {
    0          
137 0 0         if(length($self->{'client'}{'inbuf'}) > MAX_REQUEST_SIZE) {
138 0           say "X-MHFS-CONN-ID: " . $self->{'outheaders'}{'X-MHFS-CONN-ID'} . ' Headers too big, closing conn';
139 0           return undef;
140             }
141 0           return 1;
142             }
143             elsif(substr($self->{'client'}{'inbuf'}, 0, $ipos+2, '') =~ /^(([^:]+):\s*(.*))\r\n/) {
144 0           say "RECV: $1";
145 0           $self->{'header'}{$2} = $3;
146             }
147             else {
148 0           say "X-MHFS-CONN-ID: " . $self->{'outheaders'}{'X-MHFS-CONN-ID'} . ' Invalid header, closing conn';
149 0           return undef;
150             }
151             }
152             # when $ipos is 0 we recieved the end of the headers: \r\n\r\n
153              
154             # verify correct host is specified when required
155 0 0         if($self->{'client'}{'serverhostname'}) {
156 0 0 0       if((! $self->{'header'}{'Host'}) ||
157             ($self->{'header'}{'Host'} ne $self->{'client'}{'serverhostname'})) {
158 0   0       my $printhostname = $self->{'header'}{'Host'} // '';
159 0           say "Host: $printhostname does not match ". $self->{'client'}{'serverhostname'};
160 0           return undef;
161             }
162             }
163              
164 0           $self->{'ip'} = $self->{'client'}{'ip'};
165              
166             # check if we're trusted (we can trust the headers such as from reverse proxy)
167 0           my $trusted;
168 0 0 0       if($self->{'client'}{'X-MHFS-PROXY-KEY'} && $self->{'header'}{'X-MHFS-PROXY-KEY'}) {
169 0           $trusted = $self->{'client'}{'X-MHFS-PROXY-KEY'} eq $self->{'header'}{'X-MHFS-PROXY-KEY'};
170             }
171             # drops conns for naughty client's using forbidden headers
172 0 0         if(!$trusted) {
173 0           my @absolutelyforbidden = ('X-MHFS-PROXY-KEY', 'X-Forwarded-For');
174 0           foreach my $forbidden (@absolutelyforbidden) {
175 0 0         if( exists $self->{'header'}{$forbidden}) {
176 0           say "header $forbidden is forbidden!";
177 0           return undef;
178             }
179             }
180             }
181             # process reverse proxy headers
182             else {
183 0           delete $self->{'header'}{'X-MHFS-PROXY-KEY'};
184 0 0         try { $self->{'ip'} = parse_ipv4($self->{'header'}{'X-Forwarded-For'}) if($self->{'header'}{'X-Forwarded-For'}); }
  0            
185 0           catch ($e) { say "ip not updated, unable to parse X-Forwarded-For: " . $self->{'header'}{'X-Forwarded-For'}; }
186             }
187 0           my $netmap = $self->{'client'}{'server'}{'settings'}{'NETMAP'};
188 0 0 0       if($netmap && (($self->{'ip'} >> 24) == $netmap->[0])) {
189 0           say "HACK for netmap converting to local ip";
190 0           $self->{'ip'} = ($self->{'ip'} & 0xFFFFFF) | ($netmap->[1] << 24);
191             }
192              
193             # remove the final \r\n
194 0           substr($self->{'client'}{'inbuf'}, 0, 2, '');
195 0 0 0       if((defined $self->{'header'}{'Range'}) && ($self->{'header'}{'Range'} =~ /^bytes=([0-9]+)\-([0-9]*)$/)) {
196 0           $self->{'header'}{'_RangeStart'} = $1;
197 0 0         $self->{'header'}{'_RangeEnd'} = ($2 ne '') ? $2 : undef;
198             }
199 0           $self->{'on_read_ready'} = undef;
200 0           $self->{'client'}->SetEvents(MHFS::EventLoop::Poll->ALWAYSMASK );
201 0           $self->{'client'}->KillClientCloseTimer($self->{'recvrequesttimerid'});
202 0           $self->{'recvrequesttimerid'} = undef;
203              
204             # finally handle the request
205 0           foreach my $route (@{$self->{'client'}{'server'}{'routes'}}) {
  0            
206 0 0         if($self->{'path'}{'unsafecollapse'} eq $route->[0]) {
207 0           $route->[1]($self);
208 0           return 1;
209             }
210             else {
211             # wildcard ending
212 0 0         next if(index($route->[0], '*', length($route->[0])-1) == -1);
213 0 0         next if(rindex($self->{'path'}{'unsafecollapse'}, substr($route->[0], 0, -1), 0) != 0);
214 0           $route->[1]($self);
215 0           return 1;
216             }
217             }
218 0           $self->{'client'}{'server'}{'route_default'}($self);
219 0           return 1;
220             }
221              
222             # unfortunately the absolute url of the server is required for stuff like m3u playlist generation
223             sub getAbsoluteURL {
224 0     0 0   my ($self) = @_;
225 0 0 0       return $self->{'client'}{'absurl'} // (defined($self->{'header'}{'Host'}) ? 'http://'.$self->{'header'}{'Host'} : undef);
226             }
227              
228             sub _ReqDataLength {
229 0     0     my ($self, $datalength) = @_;
230 0   0       $datalength //= 99999999999;
231 0   0       my $end = $self->{'header'}{'_RangeEnd'} // ($datalength-1);
232 0           my $dl = $end+1;
233 0           say "_ReqDataLength returning: $dl";
234 0           return $dl;
235             }
236              
237             sub _SendResponse {
238 0     0     my ($self, $fileitem) = @_;
239 0 0         if(Encode::is_utf8($fileitem->{'buf'})) {
240 0           warn "_SendResponse: UTF8 flag is set, turning off";
241 0           Encode::_utf8_off($fileitem->{'buf'});
242             }
243 0 0 0       if($self->{'outheaders'}{'Transfer-Encoding'} && ($self->{'outheaders'}{'Transfer-Encoding'} eq 'chunked')) {
244 0           say "chunked response";
245 0           $fileitem->{'is_chunked'} = 1;
246             }
247              
248 0           $self->{'response'} = $fileitem;
249 0           $self->{'client'}->SetEvents(POLLOUT | MHFS::EventLoop::Poll->ALWAYSMASK );
250             }
251              
252             sub _SendDataItem {
253 0     0     my ($self, $dataitem, $opt) = @_;
254 0           my $size = $opt->{'size'};
255 0           my $code = $opt->{'code'};
256              
257 0 0         if(! $code) {
258             # if start is defined it's a range request
259 0 0         if(defined $self->{'header'}{'_RangeStart'}) {
260 0           $code = 206;
261             }
262             else {
263 0           $code = 200;
264             }
265             }
266              
267 0           my $contentlength;
268             # range request
269 0 0         if($code == 206) {
270 0           my $start = $self->{'header'}{'_RangeStart'};
271 0           my $end = $self->{'header'}{'_RangeEnd'};
272 0 0         if(defined $end) {
    0          
273 0           $contentlength = $end - $start + 1;
274             }
275             elsif(defined $size) {
276 0           say 'Implicitly setting end to size';
277 0           $end = $size - 1;
278 0           $contentlength = $end - $start + 1;
279             }
280             # no end and size unknown. we have 4 choices:
281             # set end to the current end (the satisfiable range on RFC 7233 2.1). Dumb clients don't attempt to request the rest of the data ...
282             # send non partial response (200). This will often disable range requests.
283             # send multipart. "A server MUST NOT generate a multipart response to a request for a single range"(RFC 7233 4.1) guess not
284              
285             # LIE, use a large value to signify infinite size. RFC 8673 suggests doing so when client signifies it can.
286             # Current clients don't however, so lets hope they can.
287             else {
288 0           say 'Implicitly setting end to 999999999999 to signify unknown end';
289 0           $end = 999999999999;
290             }
291              
292 0 0         if($end < $start) {
293 0           say "_SendDataItem, end < start";
294 0           $self->Send403();
295 0           return;
296             }
297 0   0       $self->{'outheaders'}{'Content-Range'} = "bytes $start-$end/" . ($size // '*');
298             }
299             # everybody else
300             else {
301 0           $contentlength = $size;
302             }
303              
304             # if the CL isn't known we need to send chunked
305 0 0         if(! defined $contentlength) {
306 0           $self->{'outheaders'}{'Transfer-Encoding'} = 'chunked';
307             }
308             else {
309 0           $self->{'outheaders'}{'Content-Length'} = "$contentlength";
310             }
311              
312              
313              
314 0           my %lookup = (
315             200 => "HTTP/1.1 200 OK\r\n",
316             206 => "HTTP/1.1 206 Partial Content\r\n",
317             301 => "HTTP/1.1 301 Moved Permanently\r\n",
318             307 => "HTTP/1.1 307 Temporary Redirect\r\n",
319             403 => "HTTP/1.1 403 Forbidden\r\n",
320             404 => "HTTP/1.1 404 File Not Found\r\n",
321             408 => "HTTP/1.1 408 Request Timeout\r\n",
322             416 => "HTTP/1.1 416 Range Not Satisfiable\r\n",
323             503 => "HTTP/1.1 503 Service Unavailable\r\n"
324             );
325              
326 0           my $headtext = $lookup{$code};
327 0 0         if(!$headtext) {
328 0           say "_SendDataItem, bad code $code";
329 0           $self->Send403();
330 0           return;
331             }
332 0           my $mime = $opt->{'mime'};
333 0           $headtext .= "Content-Type: $mime\r\n";
334              
335 0           my $filename = $opt->{'filename'};
336 0           my $disposition = 'inline';
337 0 0         if($opt->{'attachment'}) {
    0          
338 0           $disposition = 'attachment';
339 0           $filename = $opt->{'attachment'};
340             }
341             elsif($opt->{'inline'}) {
342 0           $filename = $opt->{'inline'};
343             }
344 0 0         if($filename) {
345 0           my $sendablebytes = encode('UTF-8', get_printable_utf8($filename));
346 0           $headtext .= "Content-Disposition: $disposition; filename*=UTF-8''".uri_escape($sendablebytes)."; filename=\"$sendablebytes\"\r\n";
347             }
348              
349 0   0       $self->{'outheaders'}{'Accept-Ranges'} //= 'bytes';
350 0   0       $self->{'outheaders'}{'Connection'} //= $self->{'header'}{'Connection'};
351 0   0       $self->{'outheaders'}{'Connection'} //= 'keep-alive';
352              
353             # SharedArrayBuffer
354 0 0         if($opt->{'allowSAB'}) {
355 0           say "sending SAB headers";
356 0           $self->{'outheaders'}{'Cross-Origin-Opener-Policy'} = 'same-origin';
357 0           $self->{'outheaders'}{'Cross-Origin-Embedder-Policy'} = 'require-corp';
358             }
359              
360             # serialize the outgoing headers
361 0           foreach my $header (keys %{$self->{'outheaders'}}) {
  0            
362 0           $headtext .= "$header: " . $self->{'outheaders'}{$header} . "\r\n";
363             }
364              
365 0           $headtext .= "\r\n";
366 0           $dataitem->{'buf'} = $headtext;
367              
368 0 0         if($dataitem->{'fh'}) {
369 0           $dataitem->{'fh_pos'} = tell($dataitem->{'fh'});
370 0   0 0     $dataitem->{'get_current_length'} //= sub { return undef };
  0            
371             }
372              
373 0           $self->_SendResponse($dataitem);
374             }
375              
376             sub Send400 {
377 0     0 0   my ($self) = @_;
378 0           my $msg = "400 Bad Request\r\n";
379 0           $self->SendHTML($msg, {'code' => 403});
380             }
381              
382             sub Send403 {
383 0     0 0   my ($self) = @_;
384 0           my $msg = "403 Forbidden\r\n";
385 0           $self->SendHTML($msg, {'code' => 403});
386             }
387              
388             sub Send404 {
389 0     0 0   my ($self) = @_;
390 0           my $msg = "404 Not Found";
391 0           $self->SendHTML($msg, {'code' => 404});
392             }
393              
394             sub Send408 {
395 0     0 0   my ($self) = @_;
396 0           my $msg = "408 Request Timeout";
397 0           $self->{'outheaders'}{'Connection'} = 'close';
398 0           $self->SendHTML($msg, {'code' => 408});
399             }
400              
401             sub Send416 {
402 0     0 0   my ($self, $cursize) = @_;
403 0           $self->{'outheaders'}{'Content-Range'} = "*/$cursize";
404 0           $self->SendHTML('', {'code' => 416});
405             }
406              
407             sub Send503 {
408 0     0 0   my ($self) = @_;
409 0           $self->{'outheaders'}{'Retry-After'} = 5;
410 0           my $msg = "503 Service Unavailable";
411 0           $self->SendHTML($msg, {'code' => 503});
412             }
413              
414             # requires already encoded url
415             sub SendRedirectRawURL {
416 0     0 0   my ($self, $code, $url) = @_;
417              
418 0           $self->{'outheaders'}{'Location'} = $url;
419 0           my $msg = "UNKNOWN REDIRECT MSG";
420 0 0         if($code == 301) {
    0          
421 0           $msg = "301 Moved Permanently";
422             }
423             elsif($code == 307) {
424 0           $msg = "307 Temporary Redirect";
425             }
426 0           $msg .= "\r\n\r\n";
427 0           $self->SendHTML($msg, {'code' => $code});
428             }
429              
430             # encodes path and querystring
431             # path and query string keys and values must be bytes not unicode string
432             sub SendRedirect {
433 0     0 0   my ($self, $code, $path, $qs) = @_;
434 0           my $url;
435             # encode the path component
436 0           while(length($path)) {
437 0           my $slash = index($path, '/');
438 0 0         my $len = ($slash != -1) ? $slash : length($path);
439 0           my $pathcomponent = substr($path, 0, $len, '');
440 0           $url .= uri_escape($pathcomponent);
441 0 0         if($slash != -1) {
442 0           substr($path, 0, 1, '');
443 0           $url .= '/';
444             }
445             }
446             # encode the querystring
447 0 0         if($qs) {
448 0           $url .= '?';
449 0           foreach my $key (keys %{$qs}) {
  0            
450 0           my @values;
451 0 0         if(ref($qs->{$key}) ne 'ARRAY') {
452 0           push @values, $qs->{$key};
453             }
454             else {
455 0           @values = @{$qs->{$key}};
  0            
456             }
457 0           foreach my $value (@values) {
458 0           $url .= uri_escape($key).'='.uri_escape($value) . '&';
459             }
460             }
461 0           chop $url;
462             }
463              
464 0           @_ = ($self, $code, $url);
465 0           goto &SendRedirectRawURL;
466             }
467              
468             sub SendLocalFile {
469 0     0 0   my ($self, $requestfile) = @_;
470 0           my $start = $self->{'header'}{'_RangeStart'};
471 0           my $client = $self->{'client'};
472              
473             # open the file and get the size
474 0           my %fileitem = ('requestfile' => $requestfile);
475 0           my $currentsize;
476 0 0         if($self->{'method'} ne 'HEAD') {
477 0           my $FH;
478 0 0         if(! open($FH, "<", $requestfile)) {
479 0           say "SLF: open failed";
480 0           $self->Send404;
481 0           return;
482             }
483 0           binmode($FH);
484 0           my $st = stat($FH);
485 0 0         if(! $st) {
486 0           $self->Send404();
487 0           return;
488             }
489 0           $currentsize = $st->size;
490 0           $fileitem{'fh'} = $FH;
491             }
492             else {
493 0           $currentsize = (-s $requestfile);
494             }
495              
496             # seek if a start is specified
497 0 0         if(defined $start) {
498 0 0         if($start >= $currentsize) {
    0          
499 0           $self->Send416($currentsize);
500 0           return;
501             }
502             elsif($fileitem{'fh'}) {
503 0           seek($fileitem{'fh'}, $start, 0);
504             }
505             }
506              
507             # get the maximumly possible file size. 99999999999 signfies unknown
508             my $get_current_size = sub {
509 0     0     return $currentsize;
510 0           };
511 0           my $done;
512             my $ts;
513             my $get_max_size = sub {
514 0 0   0     if($done) {
515 0           return $ts;
516             }
517 0           my $locksz = LOCK_GET_LOCKDATA($requestfile);
518 0 0         if(defined($locksz)) {
519 0   0       $ts = ($locksz || 0);
520             }
521             else {
522 0           $done = 1;
523 0   0       $ts = ($get_current_size->() || 0);
524             }
525 0           };
526 0           my $filelength = $get_max_size->();
527              
528             # truncate to the [potentially] satisfiable end
529 0 0         if(defined $self->{'header'}{'_RangeEnd'}) {
530 0           $self->{'header'}{'_RangeEnd'} = min($filelength-1, $self->{'header'}{'_RangeEnd'});
531             }
532              
533             # setup callback for retrieving current file size if we are following the file
534 0 0         if($fileitem{'fh'}) {
535 0 0         if(! $done) {
536             $get_current_size = sub {
537 0     0     return stat($fileitem{'fh'})
538 0           };
539             }
540              
541             my $get_read_filesize = sub {
542 0     0     my $maxsize = $get_max_size->();
543 0 0         if(defined $self->{'header'}{'_RangeEnd'}) {
544 0           my $rangesize = $self->{'header'}{'_RangeEnd'}+1;
545 0 0         return $rangesize if($rangesize <= $maxsize);
546             }
547 0           return $maxsize;
548 0           };
549 0           $fileitem{'get_current_length'} = $get_read_filesize;
550             }
551              
552             # flag to add SharedArrayBuffer headers
553 0           my @SABwhitelist = ('static/music_worklet_inprogress/index.html');
554 0           my $allowSAB;
555 0           foreach my $allowed (@SABwhitelist) {
556 0 0         if(index($requestfile, $allowed, length($requestfile)-length($allowed)) != -1) {
557 0           $allowSAB = 1;
558 0           last;
559             }
560             }
561              
562             # finally build headers and send
563 0 0         if($filelength == 99999999999) {
564 0           $filelength = undef;
565             }
566 0           my $mime = getMIME($requestfile);
567              
568 0           my $opt = {
569             'size' => $filelength,
570             'mime' => $mime,
571             'allowSAB' => $allowSAB
572             };
573 0 0         if($self->{'responseopt'}{'cd_file'}) {
574 0           $opt->{$self->{'responseopt'}{'cd_file'}} = basename($requestfile);
575             }
576              
577 0           $self->_SendDataItem(\%fileitem, $opt);
578             }
579              
580             # currently only supports fixed filelength
581             sub SendPipe {
582 0     0 0   my ($self, $FH, $filename, $filelength, $mime) = @_;
583 0 0         if(! defined $filelength) {
584 0           $self->Send404();
585             }
586              
587 0   0       $mime //= getMIME($filename);
588 0           binmode($FH);
589 0           my %fileitem;
590 0           $fileitem{'fh'} = $FH;
591             $fileitem{'get_current_length'} = sub {
592 0 0   0     my $tocheck = defined $self->{'header'}{'_RangeEnd'} ? $self->{'header'}{'_RangeEnd'}+1 : $filelength;
593 0           return min($filelength, $tocheck);
594 0           };
595              
596 0           $self->_SendDataItem(\%fileitem, {
597             'size' => $filelength,
598             'mime' => $mime,
599             'filename' => $filename
600             });
601             }
602              
603             # to do get rid of shell escape, launch ssh without blocking
604             sub SendFromSSH {
605 0     0 0   my ($self, $sshsource, $filename, $node) = @_;
606 0           my @sshcmd = ('ssh', $sshsource->{'userhost'}, '-p', $sshsource->{'port'});
607 0           my $fullescapedname = "'" . shell_escape($filename) . "'";
608 0           my $folder = $sshsource->{'folder'};
609 0           my $size = $node->[1];
610 0           my @cmd;
611 0 0         if(defined $self->{'header'}{'_RangeStart'}) {
612 0           my $start = $self->{'header'}{'_RangeStart'};
613 0   0       my $end = $self->{'header'}{'_RangeEnd'} // ($size - 1);
614 0           my $bytestoskip = $start;
615 0           my $count = $end - $start + 1;
616 0           @cmd = (@sshcmd, 'dd', 'skip='.$bytestoskip, 'count='.$count, 'bs=1', 'if='.$fullescapedname);
617             }
618             else{
619 0           @cmd = (@sshcmd, 'cat', $fullescapedname);
620             }
621 0           say "SendFromSSH (BLOCKING)";
622 0 0         open(my $cmdh, '-|', @cmd) or die("SendFromSSH $!");
623              
624 0           $self->SendPipe($cmdh, basename($filename), $size);
625 0           return 1;
626             }
627              
628             # ENOTIMPLEMENTED
629             sub Proxy {
630 0     0 0   my ($self, $proxy, $node) = @_;
631 0           die;
632 0           return 1;
633             }
634              
635             # buf is a bytes scalar
636             sub SendBytes {
637 0     0 0   my ($self, $mime, $buf, $options) = @_;
638              
639             # we want to sent in increments of bytes not characters
640 0 0         if(Encode::is_utf8($buf)) {
641 0           warn "SendBytes: UTF8 flag is set, turning off";
642 0           Encode::_utf8_off($buf);
643             }
644              
645 0           my $bytesize = length($buf);
646              
647             # only truncate buf if responding to a range request
648 0 0 0       if((!$options->{'code'}) || ($options->{'code'} == 206)) {
649 0   0       my $start = $self->{'header'}{'_RangeStart'} // 0;
650 0   0       my $end = $self->{'header'}{'_RangeEnd'} // $bytesize-1;
651 0           $buf = substr($buf, $start, ($end-$start) + 1);
652             }
653              
654             # Use perlio to read from the buf
655 0           my $fh;
656 0 0         if(!open($fh, '<', \$buf)) {
657 0           $self->Send404;
658 0           return;
659             }
660             my %fileitem = (
661             'fh' => $fh,
662 0     0     'get_current_length' => sub { return undef }
663 0           );
664             $self->_SendDataItem(\%fileitem, {
665             'size' => $bytesize,
666             'mime' => $mime,
667             'filename' => $options->{'filename'},
668 0           'code' => $options->{'code'}
669             });
670             }
671              
672             # expects unicode string (not bytes)
673             sub SendText {
674 0     0 0   my ($self, $mime, $buf, $options) = @_;
675 0           @_ = ($self, $mime, encode('UTF-8', $buf), $options);
676 0           goto &SendBytes;
677             }
678              
679             # expects unicode string (not bytes)
680             sub SendHTML {
681 0     0 0   my ($self, $buf, $options) = @_;;
682 0           @_ = ($self, 'text/html; charset=utf-8', encode('UTF-8', $buf), $options);
683 0           goto &SendBytes;
684             }
685              
686             # expects perl data structure
687             sub SendAsJSON {
688 0     0 0   my ($self, $obj, $options) = @_;
689 0           @_ = ($self, 'application/json', encode_json($obj), $options);
690 0           goto &SendBytes;
691             }
692              
693             sub SendCallback {
694 0     0 0   my ($self, $callback, $options) = @_;
695 0           my %fileitem;
696 0           $fileitem{'cb'} = $callback;
697              
698             $self->_SendDataItem(\%fileitem, {
699             'size' => $options->{'size'},
700             'mime' => $options->{'mime'},
701 0           'filename' => $options->{'filename'}
702             });
703             }
704              
705             sub SendAsTar {
706 0     0 0   my ($self, $requestfile) = @_;
707              
708 0           if(!HAS_Alien_Tar_Size) {
709             warn("Cannot send tar without Alien::Tar::Size");
710             $self->Send404();
711             return;
712             }
713 0           my ($libtarsize) = Alien::Tar::Size->dynamic_libs;
714 0 0         if(!$libtarsize) {
715 0           warn("Cannot find libtarsize");
716 0           $self->Send404();
717 0           return;
718             }
719              
720             # HACK, use LD_PRELOAD to hook tar to calculate the size quickly
721 0           my @tarcmd = ('tar', '-C', dirname($requestfile), basename($requestfile), '-c', '--owner=0', '--group=0');
722             $self->{'process'} = MHFS::Process->new(\@tarcmd, $self->{'client'}{'server'}{'evp'}, {
723             'SIGCHLD' => sub {
724 0     0     my $out = $self->{'process'}{'fd'}{'stdout'}{'fd'};
725 0           my $size;
726 0           read($out, $size, 50);
727 0           chomp $size;
728 0           say "size: $size";
729             $self->{'process'} = MHFS::Process->new(\@tarcmd, $self->{'client'}{'server'}{'evp'}, {
730             'STDOUT' => sub {
731 0           my($out) = @_;
732 0           say "tar sending response";
733 0           $self->{'outheaders'}{'Accept-Ranges'} = 'none';
734 0           my %fileitem = ('fh' => $out, 'get_current_length' => sub { return undef });
  0            
735 0           $self->_SendDataItem(\%fileitem, {
736             'size' => $size,
737             'mime' => 'application/x-tar',
738             'code' => 200,
739             'attachment' => basename($requestfile).'.tar'
740             });
741 0           return 0;
742             }
743 0           });
744             },
745             },
746             undef, # fd settings
747             {
748 0           'LD_PRELOAD' => $libtarsize
749             });
750             }
751              
752             sub SendDirectory {
753 0     0 0   my ($request, $droot) = @_;
754              
755             # otherwise attempt to send a file from droot
756 0           my $requestfile = abs_path($droot . $request->{'path'}{'unsafecollapse'});
757 0 0         say "abs requestfile: $requestfile" if(defined $requestfile);
758              
759             # not a file or is outside of the document root
760 0 0 0       if(( ! defined $requestfile) ||
    0          
    0          
761             (rindex($requestfile, $droot, 0) != 0)){
762 0           $request->Send404;
763             }
764             # is regular file
765             elsif (-f $requestfile) {
766 0 0         if(index($request->{'path'}{'unsafecollapse'}, '/', length($request->{'path'}{'unsafecollapse'})-1) == -1) {
767 0           $request->SendFile($requestfile);
768             }
769             else {
770 0           $request->Send404;
771             }
772             }
773             # is directory
774             elsif (-d _) {
775             # ends with slash
776 0 0         if(index($request->{'path'}{'unescapepath'}, '/', length($request->{'path'}{'unescapepath'})-1) != -1) {
777 0           my $index = $requestfile.'/index.html';
778 0 0         if(-f $index) {
779 0           $request->SendFile($index);
780 0           return;
781             }
782 0           $request->Send404;
783             }
784             else {
785             # redirect to slash path
786 0           my $bn = basename($requestfile);
787 0           $request->SendRedirect(301, $bn.'/');
788             }
789             }
790             else {
791 0           $request->Send404;
792             }
793             }
794              
795             sub SendDirectoryListing {
796 0     0 0   my ($self, $absdir, $urldir) = @_;
797 0           my $urf = $absdir .'/'.substr($self->{'path'}{'unsafepath'}, length($urldir));
798 0           my $requestfile = abs_path($urf);
799 0           my $ml = $absdir;
800 0 0         say "rf $requestfile " if(defined $requestfile);
801 0 0 0       if (( ! defined $requestfile) || (rindex($requestfile, $ml, 0) != 0)){
802 0           $self->Send404;
803 0           return;
804             }
805              
806 0 0         if(-f $requestfile) {
    0          
807 0 0         if(index($self->{'path'}{'unsafecollapse'}, '/', length($self->{'path'}{'unsafecollapse'})-1) == -1) {
808 0           $self->SendFile($requestfile);
809             }
810             else {
811 0           $self->Send404;
812             }
813 0           return;
814             }
815             elsif(-d _) {
816             # ends with slash
817 0 0         if((substr $self->{'path'}{'unescapepath'}, -1) eq '/') {
818 0 0         opendir ( my $dh, $requestfile ) or die "Error in opening dir $requestfile\n";
819 0           my $buf;
820             my $filename;
821 0           while( ($filename = readdir($dh))) {
822 0 0 0       next if(($filename eq '.') || ($filename eq '..'));
823 0 0         next if(!(-s "$requestfile/$filename"));
824 0           my $url = uri_escape($filename);
825 0 0         $url .= '/' if(-d _);
826 0           $buf .= ''.${escape_html_noquote(decode('UTF-8', $filename, Encode::LEAVE_SRC))} .'

';
  0            
827             }
828 0           closedir($dh);
829 0           $self->SendHTML($buf);
830 0           return;
831             }
832             # redirect to slash path
833             else {
834 0           $self->SendRedirect(301, basename($requestfile).'/');
835 0           return;
836             }
837             }
838 0           $self->Send404;
839             }
840              
841             sub PUTBuf_old {
842 0     0 0   my ($self, $handler) = @_;
843 0 0         if(length($self->{'client'}{'inbuf'}) < $self->{'header'}{'Content-Length'}) {
844 0           $self->{'client'}->SetEvents(POLLIN | MHFS::EventLoop::Poll->ALWAYSMASK );
845             }
846 0           my $sdata;
847             $self->{'on_read_ready'} = sub {
848 0     0     my $contentlength = $self->{'header'}{'Content-Length'};
849 0           $sdata .= $self->{'client'}{'inbuf'};
850 0           my $dlength = length($sdata);
851 0 0         if($dlength >= $contentlength) {
852 0           say 'PUTBuf datalength ' . $dlength;
853 0           my $data;
854 0 0         if($dlength > $contentlength) {
855 0           $data = substr($sdata, 0, $contentlength);
856 0           $self->{'client'}{'inbuf'} = substr($sdata, $contentlength);
857 0           $dlength = length($data)
858             }
859             else {
860 0           $data = $sdata;
861 0           $self->{'client'}{'inbuf'} = '';
862             }
863 0           $self->{'on_read_ready'} = undef;
864 0           $handler->($data);
865             }
866             else {
867 0           $self->{'client'}{'inbuf'} = '';
868             }
869             #return '';
870 0           return 1;
871 0           };
872 0           $self->{'on_read_ready'}->();
873             }
874              
875             sub PUTBuf {
876 0     0 0   my ($self, $handler) = @_;
877 0 0         if($self->{'header'}{'Content-Length'} > 20000000) {
878 0           say "PUTBuf too big";
879 0           $self->{'client'}->SetEvents(POLLIN | MHFS::EventLoop::Poll->ALWAYSMASK );
880 0     0     $self->{'on_read_ready'} = sub { return undef };
  0            
881 0           return;
882             }
883 0 0         if(length($self->{'client'}{'inbuf'}) < $self->{'header'}{'Content-Length'}) {
884 0           $self->{'client'}->SetEvents(POLLIN | MHFS::EventLoop::Poll->ALWAYSMASK );
885             }
886             $self->{'on_read_ready'} = sub {
887 0     0     my $contentlength = $self->{'header'}{'Content-Length'};
888 0           my $dlength = length($self->{'client'}{'inbuf'});
889 0 0         if($dlength >= $contentlength) {
890 0           say 'PUTBuf datalength ' . $dlength;
891 0           my $data;
892 0 0         if($dlength > $contentlength) {
893 0           $data = substr($self->{'client'}{'inbuf'}, 0, $contentlength, '');
894             }
895             else {
896 0           $data = $self->{'client'}{'inbuf'};
897 0           $self->{'client'}{'inbuf'} = '';
898             }
899 0           $self->{'on_read_ready'} = undef;
900 0           $handler->($data);
901             }
902 0           return 1;
903 0           };
904 0           $self->{'on_read_ready'}->();
905             }
906              
907             sub SendFile {
908 0     0 0   my ($self, $requestfile) = @_;
909 0           foreach my $uploader (@{$self->{'client'}{'server'}{'uploaders'}}) {
  0            
910 0 0         return if($uploader->($self, $requestfile));
911             }
912 0           say "SendFile - SendLocalFile $requestfile";
913 0           return $self->SendLocalFile($requestfile);
914             }
915              
916             1;