File Coverage

blib/lib/RPC/ExtDirect/Server.pm
Criterion Covered Total %
statement 33 203 16.2
branch 0 54 0.0
condition 0 31 0.0
subroutine 11 31 35.4
pod 14 14 100.0
total 58 333 17.4


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