package HTTP::Engine::Middleware::Static; use HTTP::Engine::Middleware; use HTTP::Engine::Response; use MIME::Types; use Path::Class; use Cwd; use Any::Moose 'X::Types::Path::Class'; use Any::Moose '::Util::TypeConstraints'; use File::Spec::Unix; use HTTP::Date (); # corece of Regexp subtype 'HTTP::Engine::Middleware::Static::Regexp' => as 'RegexpRef'; coerce 'HTTP::Engine::Middleware::Static::Regexp' => from 'Str' => via { qr/$_/ }; has 'regexp' => ( is => 'ro', isa => 'HTTP::Engine::Middleware::Static::Regexp', coerce => 1, required => 1, ); has 'docroot' => ( is => 'ro', isa => 'Path::Class::Dir', coerce => 1, required => 1, ); has directory_index => ( is => 'ro', isa => 'Str|Undef', ); has 'mime_types' => ( is => 'ro', isa => 'MIME::Types', lazy => 1, default => sub { my $mime_types = MIME::Types->new(only_complete => 1); $mime_types->create_type_index; $mime_types; }, ); has 'is_404_handler' => ( is => 'ro', isa => 'Bool', default => 1, ); before_handle { my ( $c, $self, $req ) = @_; my $re = $self->regexp; my $uri_path = $req->uri->path; return $req unless $uri_path && $uri_path =~ /^(?:$re)$/; my $docroot = dir($self->docroot)->absolute; my $file = do { if ($uri_path =~ m{/$} && $self->directory_index) { $docroot->file( File::Spec::Unix->splitpath($uri_path), $self->directory_index ); } else { $docroot->file( File::Spec::Unix->splitpath($uri_path) ) } }; my $realpath = Cwd::realpath($file->absolute->stringify); if ($realpath) { # check directory traversal if realpath found return HTTP::Engine::Response->new( status => 403, body => 'forbidden') unless $docroot->subsumes($realpath); } unless ($realpath && -e $file && !-d _) { return $req unless $self->is_404_handler; return HTTP::Engine::Response->new( status => 404, body => 'not found' ); } my $content_type = 'text/plain'; if ($file =~ /.*\.(\S{1,})$/xms ) { my $mime = $self->mime_types->mimeTypeOf($1); $content_type = $mime->type if $mime; } my $fh = $file->openr; die "Unable to open $file for reading : $!" unless $fh; binmode $fh; my $res = HTTP::Engine::Response->new( body => $fh, content_type => $content_type ); my $stat = $file->stat; $res->header( 'Content-Length' => $stat->size ); $res->header( 'Last-Modified' => HTTP::Date::time2str( $stat->mtime ) ); $res; }; __MIDDLEWARE__ __END__ =head1 NAME HTTP::Engine::Middleware::Static - handler for static files =head1 SYNOPSIS my $mw = HTTP::Engine::Middleware->new; $mw->install( 'HTTP::Engine::Middleware::Static' => { regexp => qr{^/(robots.txt|favicon.ico|(?:css|js|img)/.+)$}, docroot => '/path/to/htdocs/', }); HTTP::Engine->new( interface => { module => 'YourFavoriteInterfaceHere', request_handler => $mw->handler( \&handler ), } )->run(); # $ GET http//localhost/css/foo.css # to get the /path/to/htdocs/css/foo.css # $ GET http//localhost/js/jquery.js # to get the /path/to/htdocs/js/jquery.js # $ GET http//localhost/robots.txt # to get the /path/to/htdocs/robots.txt has multi document root my $mw = HTTP::Engine::Middleware->new; $mw->install( 'HTTP::Engine::Middleware::Static' => { regexp => qr{^/(robots.txt|favicon.ico|(?:css|js|img)/.+)$}, docroot => '/path/to/htdocs/', }, 'HTTP::Engine::Middleware::Static' => { regexp => qr{^/foo(/.+)$}, docroot => '/foo/bar/', }, ); HTTP::Engine->new( interface => { module => 'YourFavoriteInterfaceHere', request_handler => $mw->handler( \&handler ), } )->run(); # $ GET http//localhost/css/foo.css # to get the /path/to/htdocs/css/foo.css # $ GET http//localhost/robots.txt # to get the /path/to/htdocs/robots.txt # $ GET http//localhost/foo/baz.html # to get the /foo/bar/baz.txt through only the specific URL to backend my $mw = HTTP::Engine::Middleware->new; $mw->install( 'HTTP::Engine::Middleware::Static' => { regexp => qr{^/(robots.txt|favicon.ico|(?:css|img)/.+|js/(?!dynamic).+)$}, docroot => '/path/to/htdocs/', }); HTTP::Engine->new( interface => { module => 'YourFavoriteInterfaceHere', request_handler => $mw->handler( \&handler ), } )->run(); # $ GET http//localhost/js/jquery.js # to get the /path/to/htdocs/js/jquery.js # $ GET http//localhost/js/dynamic-json.js # to get the your application response Will you want to set config from yaml? my $mw = HTTP::Engine::Middleware->new; $mw->install( 'HTTP::Engine::Middleware::Static' => { regexp => '^/(robots.txt|favicon.ico|(?:css|img)/.+|js/(?!dynamic).+)$', docroot => '/path/to/htdocs/', }); HTTP::Engine->new( interface => { module => 'YourFavoriteInterfaceHere', request_handler => $mw->handler( \&handler ), } )->run(); # $ GET http//localhost/js/jquery.js # to get the /path/to/htdocs/js/jquery.js # $ GET http//localhost/js/dynamic-json.js # to get the your application response Do you want 404 handle has backend application? my $mw = HTTP::Engine::Middleware->new; $mw->install( 'HTTP::Engine::Middleware::Static' => { regexp => qr{^/css/.+)$}, docroot => '/path/to/htdocs/', is_404_handler => 0, # 404 handling off }); HTTP::Engine->new( interface => { module => 'YourFavoriteInterfaceHere', request_handler => $mw->handler(sub { HTTP::Engine::Response->new( body => 'dynamic daikuma' ); }), } )->run(); # if css has foo.css file only # $ GET http//localhost/css/foo.css # to get the /path/to/htdocs/css/foo.css # $ GET http//localhost/css/bar.css # to get the 'dynamic daikuma' strings =head1 DESCRIPTION On development site, you would feed some static contents from Interface::ServerSimple, or other stuff. This module helps that. =head1 AUTHORS Kazuhiro Osawa typester (is_404_handler support) =cut