File Coverage

blib/lib/HTTP/Server/Simple/Static.pm
Criterion Covered Total %
statement 29 85 34.1
branch 0 18 0.0
condition 0 14 0.0
subroutine 10 12 83.3
pod 1 2 50.0
total 40 131 30.5


line stmt bran cond sub pod time code
1             package HTTP::Server::Simple::Static;
2 1     1   466 use strict;
  1         1  
  1         23  
3 1     1   4 use warnings;
  1         1  
  1         18  
4              
5 1     1   9 use v5.10;
  1         3  
6              
7 1     1   4 use Cwd ();
  1         1  
  1         15  
8 1     1   230 use File::Spec::Functions qw(catfile);
  1         570  
  1         45  
9 1     1   297 use HTTP::Date ();
  1         2908  
  1         20  
10 1     1   237 use IO::File ();
  1         5468  
  1         19  
11 1     1   253 use URI::Escape ();
  1         1037  
  1         21  
12              
13 1     1   6 use base qw(Exporter);
  1         1  
  1         449  
14             our @EXPORT = qw(serve_static);
15              
16             our $VERSION = '0.13';
17              
18             my $line_end = "\015\012";
19              
20             my $magic;
21              
22             my @mime_types = (
23             [ qr/\.htm(l)?$/ => 'text/html' ],
24             [ qr/\.txt$/ => 'text/plain' ],
25             [ qr/\.css$/ => 'text/css' ],
26             [ qr/\.js$/ => 'application/javascript' ],
27             );
28              
29             sub get_mimetype {
30 0     0 0   my ($path) = @_;
31              
32 0           for my $type (@mime_types) {
33 0 0         if ( $path =~ $type->[0] ) {
34 0           return $type->[1];
35             }
36             }
37              
38 0 0         if ( !defined $magic ) {
39 0           require File::LibMagic;
40 0           $magic = File::LibMagic->new();
41             }
42              
43 0           return $magic->checktype_filename($path);
44             }
45              
46             sub serve_static {
47 0     0 1   my ( $self, $cgi, $base ) = @_;
48 0   0       $base //= q{.};
49              
50 0           my $path = $cgi->url( -absolute => 1, -path_info => 1 );
51              
52             # Internet Explorer provides the full URI in the GET section
53             # of the request header, so remove the protocol, domain name,
54             # and port if they exist.
55 0           $path =~ s{^https?://([^/:]+)(:\d+)?/}{/};
56              
57 0           $path = URI::Escape::uri_unescape($path);
58              
59             # The splitting of the URL path and then concatenating with the
60             # base ensures the correct directory separators are used for the
61             # file path.
62              
63 0           my @parts = split q{/+}, $path;
64 0           my $fullpath = catfile( $base, @parts );
65              
66             # Sanitize the path and try it.
67              
68 0           my $realpath = Cwd::realpath($fullpath);
69 0 0 0       if ( !$realpath || $realpath !~ m/^\Q$base\E/ ) {
70             # directory traversal attack!
71 0           return 0;
72             }
73              
74 0           my $fh = IO::File->new();
75 0 0 0       if ( -f $realpath && $fh->open($realpath) ) {
76 0           my $mtime = ( stat $realpath )[9];
77 0           my $now = time;
78              
79             # RFC-2616 Section 14.25 "If-Modified-Since":
80              
81 0           my $if_modified_since = $cgi->http('If-Modified-Since');
82 0 0         if ($if_modified_since) {
83 0           $if_modified_since = HTTP::Date::str2time($if_modified_since);
84              
85 0 0 0       if ( defined $if_modified_since && # parsed ok
      0        
86             $if_modified_since <= $now && # not in future
87             $mtime <= $if_modified_since ) { # not changed
88              
89 0           print 'HTTP/1.1 304 Not Modified' . $line_end;
90 0           print $line_end;
91              
92 0           return 1;
93             }
94             }
95              
96             # Read the file contents and get the length in bytes
97              
98 0           binmode $fh;
99 0           binmode $self->stdout_handle;
100              
101 0           my $content;
102             {
103 0           local $/;
  0            
104 0           $content = <$fh>;
105             }
106 0           $fh->close;
107              
108 0           my $content_length;
109 0 0         if ( defined $content ) {
110 1     1   341 use bytes; # Content-Length in bytes, not characters
  1         11  
  1         4  
111 0           $content_length = length $content;
112             }
113             else {
114 0           $content_length = 0;
115 0           $content = q{};
116             }
117              
118             # Find MIME type
119              
120 0           my $mimetype = get_mimetype($realpath);
121              
122             # RFC-2616 Section 14.29 "Last-Modified":
123             #
124             # An origin server MUST NOT send a Last-Modified date which is
125             # later than the server's time of message origination. In such
126             # cases, where the resource's last modification would indicate
127             # some time in the future, the server MUST replace that date
128             # with the message origination date.
129              
130 0 0         if ( $mtime > $now ) {
131 0           $mtime = $now;
132             }
133              
134 0           my $last_modified = HTTP::Date::time2str($mtime);
135 0           my $date = HTTP::Date::time2str($now);
136              
137 0           print 'HTTP/1.1 200 OK' . $line_end;
138 0           print 'Date: ' . $date . $line_end;
139 0           print 'Last-Modified: ' . $last_modified . $line_end;
140 0           print 'Content-Type: ' . $mimetype . $line_end;
141 0           print 'Content-Length: ' . $content_length . $line_end;
142 0           print $line_end;
143              
144 0 0         if ( $cgi->request_method() ne 'HEAD' ) {
145 0           print $content;
146             }
147              
148 0           return 1;
149             }
150 0           return 0;
151             }
152              
153             1;
154             __END__