File Coverage

blib/lib/HTTP/Server/Simple/Static.pm
Criterion Covered Total %
statement 29 86 33.7
branch 0 18 0.0
condition 0 14 0.0
subroutine 10 12 83.3
pod 1 2 50.0
total 40 132 30.3


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