File Coverage

blib/lib/RPC/ExtDirect/Server.pm
Criterion Covered Total %
statement 33 202 16.3
branch 0 54 0.0
condition 0 34 0.0
subroutine 11 31 35.4
pod 14 14 100.0
total 58 335 17.3


line stmt bran cond sub pod time code
1             package RPC::ExtDirect::Server;
2              
3 7     7   26677 use strict;
  7         7  
  7         161  
4 7     7   22 use warnings;
  7         6  
  7         161  
5 7     7   20 no warnings 'uninitialized'; ## no critic
  7         6  
  7         152  
6              
7 7     7   18 use Carp;
  7         6  
  7         404  
8              
9 7     7   1693 use RPC::ExtDirect::Util::Accessor;
  7         2136  
  7         136  
10 7     7   1631 use RPC::ExtDirect::Config;
  7         66690  
  7         173  
11 7     7   2059 use RPC::ExtDirect::API;
  7         15297  
  7         31  
12 7     7   1642 use RPC::ExtDirect;
  7         18943  
  7         33  
13 7     7   2977 use CGI::ExtDirect;
  7         51893  
  7         153  
14              
15 7     7   2894 use HTTP::Server::Simple::CGI;
  7         56230  
  7         206  
16 7     7   38 use base 'HTTP::Server::Simple::CGI';
  7         7  
  7         13431  
17              
18             ### PACKAGE GLOBAL VARIABLE ###
19             #
20             # Version of this module.
21             #
22              
23             our $VERSION = '1.21';
24              
25             # We're trying hard not to depend on any non-core modules,
26             # but there's no reason not to use them if they're available
27             my ($have_http_date, $have_cgi_simple);
28              
29             {
30             local $@;
31             $have_http_date = eval "require HTTP::Date";
32             $have_cgi_simple = eval "require CGI::Simple";
33             }
34              
35             # CGI.pm < 3.36 does not support HTTP_COOKIE environment variable
36             # with multiple values separated by commas instead of semicolons,
37             # which is exactly what HTTP::Server::Simple::CGI::Environment
38             # does in version <= 0.51. The module below will fix that.
39              
40             if ( $CGI::VERSION < 3.36 && $HTTP::Server::Simple::VERSION <= 0.51 ) {
41             local $@;
42              
43             require RPC::ExtDirect::Server::Patch::HTTPServerSimple;
44             }
45              
46             # We assume that HTTP::Date::time2str is better maintained,
47             # so use it if we can. If HTTP::Date is not installed,
48             # fall back to our own time2str - which was shamelessly copied
49             # from HTTP::Date anyway.
50             if ( $have_http_date ) {
51             *time2str = *HTTP::Date::time2str;
52             *str2time = *HTTP::Date::str2time;
53             }
54             else {
55             eval <<'END_SUB';
56             my @DoW = qw(Sun Mon Tue Wed Thu Fri Sat);
57             my @MoY = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
58            
59             sub time2str {
60             my $time = shift;
61            
62             $time = time unless defined $time;
63            
64             my ($sec, $min, $hour, $mday, $mon, $year, $wday)
65             = gmtime($time);
66            
67             return sprintf "%s, %02d %s %04d %02d:%02d:%02d GMT",
68             $DoW[$wday],
69             $mday,
70             $MoY[$mon],
71             $year + 1900,
72             $hour,
73             $min,
74             $sec
75             ;
76             }
77             END_SUB
78             }
79              
80             my %DEFAULTS = (
81             index_file => 'index.html',
82             expires_after => 259200, # 3 days in seconds
83             buffer_size => 262144, # 256kb in bytes
84             );
85              
86             ### PUBLIC CLASS METHOD (CONSTRUCTOR) ###
87             #
88             # Instantiate a new HTTPServer
89             #
90              
91             sub new {
92 0     0 1   my ($class, %arg) = @_;
93              
94 0   0       my $api = delete $arg{api} || RPC::ExtDirect->get_api();
95 0   0       my $config = delete $arg{config} || $api->config;
96 0   0       my $host = delete $arg{host} || '127.0.0.1';
97 0   0       my $port = delete $arg{port} || 8080;
98 0   0       my $cust_disp = delete $arg{dispatch} || [];
99 0   0       my $static_dir = delete $arg{static_dir} || '/tmp';
100 0           my $cgi_class = delete $arg{cgi_class};
101              
102 0           $config->set_options(%arg);
103              
104 0           my $self = $class->SUPER::new($port);
105            
106 0           $self->_init_cgi_class($cgi_class);
107            
108 0           $self->api($api);
109 0           $self->config($config);
110 0           $self->host($host);
111              
112 0           $self->static_dir($static_dir);
113 0           $self->logit("Using static directory ". $self->static_dir);
114            
115 0           while ( my ($k, $v) = each %DEFAULTS ) {
116 0 0         my $value = exists $arg{ $k } ? delete $arg{ $k } : $v;
117            
118 0           $self->$k($value);
119             }
120              
121 0           $self->_init_dispatch($cust_disp);
122            
123 0           return bless $self, $class;
124             }
125              
126             ### PUBLIC INSTANCE METHOD ###
127             #
128             # Find matching method by URI and dispatch it.
129             # This is an entry point for HTTP::Server::Simple API, and is called
130             # by the underlying module (in fact HTTP::Server::Simple::CGI).
131             #
132              
133             sub handle_request {
134 0     0 1   my ($self, $cgi) = @_;
135            
136 0           my $path_info = $cgi->path_info();
137            
138 0           my $debug = $self->config->debug;
139            
140 0 0         $self->logit("Handling request: $path_info") if $debug;
141            
142 0           $cgi->nph(1);
143            
144             HANDLER:
145 0           for my $handler ( @{ $self->dispatch } ) {
  0            
146 0           my $match = $handler->{match};
147            
148 0 0         $self->logit("Matching '$path_info' against $match") if $debug;
149            
150 0 0         next HANDLER unless $path_info =~ $match;
151            
152 0 0         $self->logit("Got specific handler with match '$match'") if $debug;
153            
154 0           my $code = $handler->{code};
155            
156             # Handlers are always called as if they were ref($self)
157             # instance methods
158 0           return $code->($self, $cgi);
159             }
160              
161 0 0         $self->logit("No specific handlers found, serving default") if $debug;
162            
163 0           return $self->handle_default($cgi, $path_info);
164             }
165              
166             ### PUBLIC INSTANCE METHOD ###
167             #
168             # Default request handler
169             #
170              
171             sub handle_default {
172 0     0 1   my ($self, $cgi, $path) = @_;
173              
174             # Lame security measure
175 0 0         return $self->handle_403($cgi, $path) if $path =~ m{/\.\.};
176              
177 0           my $static = $self->static_dir();
178 0 0         $static .= '/' unless $path =~ m{^/};
179              
180 0           my $file_name = $static . $path;
181            
182 0           my $file_exists = -f $file_name;
183 0           my $file_readable = -r $file_name;
184              
185 0 0 0       if ( -d $file_name ) {
    0 0        
    0          
186 0           $self->logit("Got directory request");
187 0           return $self->handle_directory($cgi, $path);
188             }
189             elsif ( $file_exists && !$file_readable ) {
190 0           $self->logit("File exists but no permissions to read it (403)");
191 0           return $self->handle_403($cgi, $path);
192             }
193             elsif ( $file_exists && $file_readable ) {
194 0           $self->logit("Got readable file, serving as static content");
195 0           return $self->handle_static(
196             cgi => $cgi,
197             path => $path,
198             file_name => $file_name,
199             );
200             }
201             else {
202 0           return $self->handle_404($cgi, $path);
203             };
204              
205 0           return 1;
206             }
207              
208             ### PUBLIC INSTANCE METHOD ###
209             #
210             # Handle directory request. Usually results in a redirect
211             # but can be overridden to do something fancier.
212             #
213              
214             sub handle_directory {
215 0     0 1   my ($self, $cgi, $path) = @_;
216            
217             # Directory requested, redirecting to index.html
218 0           $path =~ s{/+$}{};
219            
220 0           my $index_file = $self->index_file;
221            
222 0           $self->logit("Redirecting to $path/$index_file");
223            
224 0           my $out = $self->stdio_handle;
225              
226 0           print $out $cgi->redirect(
227             -uri => "$path/$index_file",
228             -status => '301 Moved Permanently'
229             );
230            
231 0           return 1;
232             }
233              
234             ### PUBLIC INSTANCE METHOD ###
235             #
236             # Handle static content
237             #
238              
239             sub handle_static {
240 0     0 1   my ($self, %arg) = @_;
241              
242 0           my $cgi = $arg{cgi};
243 0           my $file_name = $arg{file_name};
244              
245 0           $self->logit("Handling static request for $file_name");
246              
247 0           my ($fsize, $fmtime) = (stat $file_name)[7, 9];
248 0           my ($type, $charset) = $self->_guess_mime_type($file_name);
249            
250 0           $self->logit("Got MIME type $type");
251            
252 0           my $out = $self->stdio_handle;
253            
254             # We're only processing If-Modified-Since if HTTP::Date is installed.
255             # That's because str2time is not trivial and there's no point in
256             # copying that much code. The feature is not worth it.
257 0 0         if ( $have_http_date ) {
258 0           my $ims = $cgi->http('If-Modified-Since');
259            
260 0 0 0       if ( $ims && $fmtime <= str2time($ims) ) {
261 0           $self->logit("File has not changed, serving 304");
262 0           print $out $cgi->header(
263             -type => $type,
264             -status => '304 Not Modified',
265             );
266            
267 0           return 1;
268             };
269             }
270            
271 0           my ($in, $buf);
272              
273 0 0         if ( not open $in, '<', $file_name ) {
274 0           $self->logit("File is unreadable, serving 403");
275 0           return $self->handle_403($cgi);
276             };
277              
278 0           $self->logit("Serving file content with 200");
279            
280 0           my $expires = $self->expires_after;
281              
282 0 0 0       print $out $cgi->header(
283             -type => $type,
284             -status => '200 OK',
285             -charset => ($charset || ($type !~ /image|octet/ ? 'utf-8' : '')),
286             ( $expires ? ( -Expires => time2str(time + $expires) ) : () ),
287             -Content_Length => $fsize,
288             -Last_Modified => time2str($fmtime),
289             );
290              
291 0           my $bufsize = $self->buffer_size;
292            
293 0           binmode $in;
294 0           binmode $out;
295            
296             # Making the out handle hot helps in older Perls
297             {
298 0           my $orig_fh = select $out;
  0            
299 0           $| = 1;
300 0           select $orig_fh;
301             }
302              
303 0           print $out $buf while sysread $in, $buf, $bufsize;
304              
305 0           return 1;
306             }
307              
308             ### PUBLIC INSTANCE METHOD ###
309             #
310             # Return Ext.Direct API declaration JavaScript
311             #
312              
313             sub handle_extdirect_api {
314 0     0 1   my ($self, $cgi) = @_;
315              
316 0           $self->logit("Got Ext.Direct API request");
317              
318 0           return $self->_handle_extdirect($cgi, 'api');
319             }
320              
321             ### PUBLIC INSTANCE METHOD ###
322             #
323             # Route Ext.Direct method calls
324             #
325              
326             sub handle_extdirect_router {
327 0     0 1   my ($self, $cgi) = @_;
328              
329 0           $self->logit("Got Ext.Direct route request");
330              
331 0           return $self->_handle_extdirect($cgi, 'route');
332             }
333              
334             ### PUBLIC INSTANCE METHOD ###
335             #
336             # Poll Ext.Direct event providers for events
337             #
338              
339             sub handle_extdirect_poll {
340 0     0 1   my ($self, $cgi) = @_;
341              
342 0           $self->logit("Got Ext.Direct event poll request");
343              
344 0           return $self->_handle_extdirect($cgi, 'poll');
345             }
346              
347             ### PUBLIC INSTANCE METHOD ###
348             #
349             # Return 403 header without a body.
350             #
351              
352             sub handle_403 {
353 0     0 1   my ($self, $cgi, $uri) = @_;
354            
355 0           $self->logit("Handling 403 for URI $uri");
356            
357 0           my $out = $self->stdio_handle;
358            
359 0           print $out $cgi->header(-status => '403 Forbidden');
360            
361 0           return 1;
362             }
363              
364             ### PUBLIC INSTANCE METHOD ###
365             #
366             # Return 404 header without a body.
367             #
368              
369             sub handle_404 {
370 0     0 1   my ($self, $cgi, $uri) = @_;
371              
372 0           $self->logit("Handling 404 for URI $uri");
373            
374 0           my $out = $self->stdio_handle;
375              
376 0           print $out $cgi->header(-status => '404 Not Found');
377              
378 0           return 1;
379             }
380              
381             ### PUBLIC INSTANCE METHOD ###
382             #
383             # Log debugging info to STDERR
384             #
385              
386             sub logit {
387 0     0 1   my $self = shift;
388            
389 0 0         print STDERR @_, "\n" if $self->config->debug;
390             }
391              
392             ### PUBLIC PACKAGE SUBROUTINE ###
393             #
394             # Prints banner, but only if debugging is on
395             #
396              
397             sub print_banner {
398 0     0 1   my $self = shift;
399              
400 0 0         $self->SUPER::print_banner if $self->config->debug;
401             }
402              
403             ### PUBLIC INSTANCE METHODS ###
404             #
405             # Read-write accessors
406             #
407              
408             RPC::ExtDirect::Util::Accessor->mk_accessors(
409             simple => [qw/
410             api
411             config
412             dispatch
413             static_dir
414             index_file
415             expires_after
416             buffer_size
417             /],
418             );
419              
420             ############## PRIVATE METHODS BELOW ##############
421              
422             ### PRIVATE INSTANCE METHOD ###
423             #
424             # Parse HTTP request line. Returns three values: request method,
425             # URI and protocol.
426             #
427             # This method is overridden to improve parsing speed. The original
428             # method is reading characters from STDIN one by one, which
429             # results in abysmal performance. Not sure what was the intent
430             # there but I haven't encountered any problems so far with the
431             # faster implementation below.
432             #
433             # The same is applicable to the parse_headers() below.
434             #
435              
436             sub parse_request {
437 0     0 1   my $self = shift;
438              
439 0           my $io_handle = $self->stdio_handle;
440 0           my $input = <$io_handle>;
441              
442 0 0         return unless $input;
443              
444 0 0         $input =~ /^(\w+)\s+(\S+)(?:\s+(\S+))?\r?$/ and
445             return ( $1.'', $2.'', $3.'' );
446             }
447              
448             ### PRIVATE INSTANCE METHOD ###
449             #
450             # Parse incoming HTTP headers from input file handle and return
451             # an arrayref of header/value pairs.
452             #
453              
454             sub parse_headers {
455 0     0 1   my $self = shift;
456              
457 0           my $io_handle = $self->stdio_handle;
458              
459 0           my @headers;
460              
461 0           while ( my $input = <$io_handle> ) {
462 0           $input =~ s/[\r\l\n\s]+$//;
463 0 0         last if !$input;
464              
465 0 0         push @headers, $1 => $2
466             if $input =~ /^([^()<>\@,;:\\"\/\[\]?={} \t]+):\s*(.*)/i;
467             };
468              
469 0           return \@headers;
470             }
471              
472             ### PRIVATE INSTANCE METHOD ###
473             #
474             # Initialize CGI class. Used by constructor.
475             #
476              
477             sub _init_cgi_class {
478 0     0     my ($self, $cgi_class) = @_;
479            
480             # Default to CGI::Simple > 1.113 if it's available, unless the user
481             # overrode cgi_class to do something else. CGI::Simple 1.113 and
482             # earlier has a bug with form/multipart file upload handling, so
483             # we don't use it even if it is installed.
484 0 0 0       if ( $cgi_class ) {
    0 0        
485 0           $self->cgi_class($cgi_class);
486            
487 0 0         if ( $cgi_class eq 'CGI' ) {
488             $self->cgi_init(sub {
489 0     0     local $@;
490            
491 0           eval {
492 0           require CGI;
493 0           CGI::initialize_globals();
494             }
495 0           });
496             }
497             else {
498             $self->cgi_init(sub {
499 0     0     eval "require $cgi_class";
500 0           });
501             }
502             }
503             elsif ( $have_cgi_simple && $CGI::Simple::VERSION > 1.113 &&
504             $self->cgi_class eq 'CGI' )
505             {
506 0           $self->cgi_class('CGI::Simple');
507 0           $self->cgi_init(undef);
508             }
509             }
510              
511             ### PRIVATE INSTANCE METHOD ###
512             #
513             # Initialize dispatch table. Used by constructor.
514             #
515              
516             sub _init_dispatch {
517 0     0     my ($self, $cust_disp) = @_;
518            
519 0           my $config = $self->config;
520            
521 0           my @dispatch;
522              
523             # Set the custom handlers so they would come first served.
524             # Format:
525             # [ qr{URI} => \&method, ... ]
526             # [ { match => qr{URI}, code => \&method, } ]
527 0           while ( my $uri = shift @$cust_disp ) {
528 0           $self->logit("Installing custom handler for URI: $uri");
529 0           push @dispatch, {
530             match => qr{$uri},
531             code => shift @$cust_disp,
532             };
533             };
534            
535             # The default Ext.Direct handlers always come last
536 0           for my $type ( qw/ api router poll / ) {
537 0           my $uri_getter = "${type}_path";
538 0           my $handler = "handle_extdirect_${type}";
539 0           my $uri = $config->$uri_getter;
540            
541 0 0         if ( $uri ) {
542             push @dispatch, {
543 0           match => qr/^\Q$uri\E$/, code => \&{ $handler },
  0            
544             }
545             }
546             }
547              
548 0           $self->dispatch(\@dispatch);
549             }
550              
551             ### PRIVATE INSTANCE METHOD ###
552             #
553             # Do the actual heavy lifting for Ext.Direct calls
554             #
555              
556             sub _handle_extdirect {
557 0     0     my ($self, $cgi, $what) = @_;
558              
559 0           my $exd = CGI::ExtDirect->new({
560             api => $self->api,
561             config => $self->config,
562             cgi => $cgi,
563             });
564              
565             # Standard CGI headers for this handler
566 0           my %std_cgi = ( '-nph' => 1, '-charset' => 'utf-8' );
567            
568 0           my $out = $self->stdio_handle;
569              
570 0           print $out $exd->$what( %std_cgi );
571              
572 0           return 1;
573             }
574              
575             # Popular MIME types, taken from http://lwp.interglacial.com/appc_01.htm
576             my %MIME_TYPES = (
577             au => 'audio/basic',
578             avi => 'vide/avi',
579             bmp => 'image/bmp',
580             bz2 => 'application/x-bzip2',
581             css => 'text/css',
582             dtd => 'application/xml-dtd',
583             doc => 'application/msword',
584             gif => 'image/gif',
585             gz => 'application/x-gzip',
586             ico => 'image/x-icon',
587             hqx => 'application/mac-binhex40',
588             htm => 'text/html',
589             html => 'text/html',
590             jar => 'application/java-archive',
591             jpg => 'image/jpeg',
592             jpeg => 'image/jpeg',
593             js => 'text/javascript',
594             json => 'application/json',
595             midi => 'audio/x-midi',
596             mp3 => 'audio/mpeg',
597             mpeg => 'video/mpeg',
598             ogg => 'audio/vorbis',
599             pdf => 'application/pdf',
600             pl => 'application/x-perl',
601             png => 'image/png',
602             ppt => 'application/vnd.ms-powerpoint',
603             ps => 'application/postscript',
604             qt => 'video/quicktime',
605             rdf => 'application/rdf',
606             rtf => 'application/rtf',
607             sgml => 'text/sgml',
608             sit => 'application/x-stuffit',
609             svg => 'image/svg+xml',
610             swf => 'application/x-shockwave-flash',
611             tgz => 'application/x-tar',
612             tiff => 'image/tiff',
613             tsv => 'text/tab-separated-values',
614             txt => 'text/plain',
615             wav => 'audio/wav',
616             xls => 'application/excel',
617             xml => 'application/xml',
618             zip => 'application/zip',
619             );
620              
621             ### PRIVATE INSTANCE METHOD ###
622             #
623             # Return the guessed MIME type for a file name
624             #
625              
626             # We try to use File::LibMagic or File::MimeInfo if available
627             {
628             local $@;
629            
630             my $have_libmagic = $ENV{DEBUG_NO_FILE_LIBMAGIC}
631             ? !1
632             : eval "require File::LibMagic";
633            
634             #
635             # File::MimeInfo is a bit kludgy: it depends on shared-mime-info database
636             # being installed, and when said database is missing it will do only
637             # very basic guessing that is not very useful. Not only that, it will
638             # also complain loudly into STDERR about the missing database, which is
639             # definitely not helping either. So in addition to checking if the module
640             # itself is available we poke a bit deeper and decide if it's worth using.
641             #
642             my $have_mimeinfo = !$ENV{DEBUG_NO_FILE_MIMEINFO} &&
643             eval {
644             require File::MimeInfo;
645             # This is a dependency of File::MimeInfo
646             require File::BaseDir;
647              
648             # When both arrays are empty the module is essentially useless
649             @File::MimeInfo::DIRS || File::BaseDir::data_files('mime/globs');
650             };
651            
652             sub _guess_mime_type {
653 0     0     my ($self, $file_name) = @_;
654            
655 0           my ($type, $charset);
656            
657 0 0         if ( $have_libmagic ) {
    0          
658 0           my $magic = File::LibMagic->new();
659 0           my $mime = $magic->checktype_filename($file_name);
660            
661 0           ($type, $charset) = $mime =~ m{^([^;]+);\s*charset=(.*)$};
662             }
663             elsif ( $have_mimeinfo ) {
664 0           my $mimeinfo = File::MimeInfo->new();
665 0           $type = $mimeinfo->mimetype($file_name);
666             }
667            
668             # If none of the advanced modules are present, resort to
669             # guesstimating by file extension
670             else {
671 0           my ($suffix) = $file_name =~ /.*\.(\w+)$/;
672            
673 0           $type = $MIME_TYPES{ $suffix };
674             }
675            
676 0   0       return ($type || 'application/octet-stream', $charset);
677             }
678             }
679              
680             1;