1package HTTP::Engine::Middleware::Static;
2use HTTP::Engine::Middleware;
3use HTTP::Engine::Response;
4
5use MIME::Types;
6use Path::Class;
7use Cwd;
8use Any::Moose 'X::Types::Path::Class';
9use Any::Moose '::Util::TypeConstraints';
10use File::Spec::Unix;
11use HTTP::Date ();
12
13# corece of Regexp
14subtype 'HTTP::Engine::Middleware::Static::Regexp'
15    => as 'RegexpRef';
16coerce 'HTTP::Engine::Middleware::Static::Regexp'
17    => from 'Str' => via { qr/$_/ };
18
19has 'regexp' => (
20    is       => 'ro',
21    isa      => 'HTTP::Engine::Middleware::Static::Regexp',
22    coerce   => 1,
23    required => 1,
24);
25
26has 'docroot' => (
27    is       => 'ro',
28    isa      => 'Path::Class::Dir',
29    coerce   => 1,
30    required => 1,
31);
32
33has directory_index => (
34    is  => 'ro',
35    isa => 'Str|Undef',
36);
37
38has 'mime_types' => (
39    is  => 'ro',
40    isa => 'MIME::Types',
41    lazy => 1,
42    default => sub {
43        my $mime_types = MIME::Types->new(only_complete => 1);
44        $mime_types->create_type_index;
45        $mime_types;
46    },
47);
48
49has 'is_404_handler' => (
50    is      => 'ro',
51    isa     => 'Bool',
52    default => 1,
53);
54
55before_handle {
56    my ( $c, $self, $req ) = @_;
57
58    my $re   = $self->regexp;
59    my $uri_path = $req->uri->path;
60    return $req unless $uri_path && $uri_path =~ /^(?:$re)$/;
61
62    my $docroot = dir($self->docroot)->absolute;
63    my $file = do {
64        if ($uri_path =~ m{/$} && $self->directory_index) {
65            $docroot->file(
66                File::Spec::Unix->splitpath($uri_path),
67                $self->directory_index
68            );
69        } else {
70            $docroot->file(
71                File::Spec::Unix->splitpath($uri_path)
72            )
73        }
74    };
75
76    my $realpath = Cwd::realpath($file->absolute->stringify);
77    if ($realpath) {
78        # check directory traversal if realpath found
79        return HTTP::Engine::Response->new( status => 403, body => 'forbidden') unless $docroot->subsumes($realpath);
80    }
81
82    unless ($realpath && -e $file && !-d _) {
83        return $req unless $self->is_404_handler;
84        return HTTP::Engine::Response->new( status => 404, body => 'not found' );
85    }
86
87    my $content_type = 'text/plain';
88    if ($file =~ /.*\.(\S{1,})$/xms ) {
89        my $mime = $self->mime_types->mimeTypeOf($1);
90        $content_type = $mime->type if $mime;
91    }
92
93    my $fh = $file->openr;
94    die "Unable to open $file for reading : $!" unless $fh;
95    binmode $fh;
96
97    my $res = HTTP::Engine::Response->new( body => $fh, content_type => $content_type );
98    my $stat = $file->stat;
99    $res->header( 'Content-Length' => $stat->size );
100    $res->header( 'Last-Modified'  => HTTP::Date::time2str( $stat->mtime ) );
101    $res;
102};
103
104
105__MIDDLEWARE__
106
107__END__
108
109=head1 NAME
110
111HTTP::Engine::Middleware::Static - handler for static files
112
113=head1 SYNOPSIS
114
115    my $mw = HTTP::Engine::Middleware->new;
116    $mw->install( 'HTTP::Engine::Middleware::Static' => {
117        regexp  => qr{^/(robots.txt|favicon.ico|(?:css|js|img)/.+)$},
118        docroot => '/path/to/htdocs/',
119    });
120    HTTP::Engine->new(
121        interface => {
122            module => 'YourFavoriteInterfaceHere',
123            request_handler => $mw->handler( \&handler ),
124        }
125    )->run();
126
127    # $ GET http//localhost/css/foo.css
128    # to get the /path/to/htdocs/css/foo.css
129
130    # $ GET http//localhost/js/jquery.js
131    # to get the /path/to/htdocs/js/jquery.js
132
133    # $ GET http//localhost/robots.txt
134    # to get the /path/to/htdocs/robots.txt
135
136has multi document root
137
138    my $mw = HTTP::Engine::Middleware->new;
139    $mw->install(
140        'HTTP::Engine::Middleware::Static' => {
141            regexp  => qr{^/(robots.txt|favicon.ico|(?:css|js|img)/.+)$},
142            docroot => '/path/to/htdocs/',
143        },
144        'HTTP::Engine::Middleware::Static' => {
145            regexp  => qr{^/foo(/.+)$},
146            docroot => '/foo/bar/',
147        },
148    );
149    HTTP::Engine->new(
150        interface => {
151            module => 'YourFavoriteInterfaceHere',
152            request_handler => $mw->handler( \&handler ),
153        }
154    )->run();
155
156    # $ GET http//localhost/css/foo.css
157    # to get the /path/to/htdocs/css/foo.css
158
159    # $ GET http//localhost/robots.txt
160    # to get the /path/to/htdocs/robots.txt
161
162    # $ GET http//localhost/foo/baz.html
163    # to get the /foo/bar/baz.txt
164
165through only the specific URL to backend
166
167    my $mw = HTTP::Engine::Middleware->new;
168    $mw->install( 'HTTP::Engine::Middleware::Static' => {
169        regexp  => qr{^/(robots.txt|favicon.ico|(?:css|img)/.+|js/(?!dynamic).+)$},
170        docroot => '/path/to/htdocs/',
171    });
172    HTTP::Engine->new(
173        interface => {
174            module => 'YourFavoriteInterfaceHere',
175            request_handler => $mw->handler( \&handler ),
176        }
177    )->run();
178
179    # $ GET http//localhost/js/jquery.js
180    # to get the /path/to/htdocs/js/jquery.js
181
182    # $ GET http//localhost/js/dynamic-json.js
183    # to get the your application response
184
185Will you want to set config from yaml?
186
187    my $mw = HTTP::Engine::Middleware->new;
188    $mw->install( 'HTTP::Engine::Middleware::Static' => {
189        regexp  => '^/(robots.txt|favicon.ico|(?:css|img)/.+|js/(?!dynamic).+)$',
190        docroot => '/path/to/htdocs/',
191    });
192    HTTP::Engine->new(
193        interface => {
194            module => 'YourFavoriteInterfaceHere',
195            request_handler => $mw->handler( \&handler ),
196        }
197    )->run();
198
199    # $ GET http//localhost/js/jquery.js
200    # to get the /path/to/htdocs/js/jquery.js
201
202    # $ GET http//localhost/js/dynamic-json.js
203    # to get the your application response
204
205Do you want 404 handle has backend application?
206
207    my $mw = HTTP::Engine::Middleware->new;
208    $mw->install( 'HTTP::Engine::Middleware::Static' => {
209        regexp         => qr{^/css/.+)$},
210        docroot        => '/path/to/htdocs/',
211        is_404_handler => 0, # 404 handling off
212    });
213    HTTP::Engine->new(
214        interface => {
215            module => 'YourFavoriteInterfaceHere',
216            request_handler => $mw->handler(sub {
217                HTTP::Engine::Response->new( body => 'dynamic daikuma' );
218            }),
219        }
220    )->run();
221
222    # if css has foo.css file only
223
224    # $ GET http//localhost/css/foo.css
225    # to get the /path/to/htdocs/css/foo.css
226
227    # $ GET http//localhost/css/bar.css
228    # to get the 'dynamic daikuma' strings
229
230
231=head1 DESCRIPTION
232
233On development site, you would feed some static contents from Interface::ServerSimple, or other stuff.
234This module helps that.
235
236=head1 AUTHORS
237
238Kazuhiro Osawa
239
240typester (is_404_handler support)
241
242=cut
243