File Coverage

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


line stmt bran cond sub pod time code
1             package RPC::ExtDirect::Server;
2              
3 7     7   30647 use strict;
  7         10  
  7         261  
4 7     7   27 use warnings;
  7         8  
  7         231  
5 7     7   24 no warnings 'uninitialized'; ## no critic
  7         8  
  7         207  
6              
7 7     7   26 use Carp;
  7         7  
  7         497  
8              
9 7     7   2039 use RPC::ExtDirect::Util::Accessor;
  7         2532  
  7         176  
10 7     7   1905 use RPC::ExtDirect::Config;
  7         80724  
  7         226  
11 7     7   2848 use RPC::ExtDirect::API;
  7         18601  
  7         42  
12 7     7   2292 use RPC::ExtDirect;
  7         23202  
  7         50  
13 7     7   3698 use CGI::ExtDirect;
  7         63953  
  7         192  
14              
15 7     7   4082 use HTTP::Server::Simple::CGI;
  7         70641  
  7         237  
16 7     7   49 use base 'HTTP::Server::Simple::CGI';
  7         7  
  7         14920  
17              
18             ### PACKAGE GLOBAL VARIABLE ###
19             #
20             # Version of this module.
21             #
22              
23             our $VERSION = '1.20';
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.44. The module below will fix that.
39              
40             if ( $CGI::VERSION < 3.36 && $HTTP::Server::Simple::VERSION <= 0.44 ) {
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 0           HANDLER:
145 0           for my $handler ( @{ $self->dispatch } ) {
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             file_name => $file_name,
198             );
199             }
200             else {
201 0           return $self->handle_404($cgi, $path);
202             };
203              
204 0           return 1;
205             }
206              
207             ### PUBLIC INSTANCE METHOD ###
208             #
209             # Handle directory request. Usually results in a redirect
210             # but can be overridden to do something fancier.
211             #
212              
213             sub handle_directory {
214 0     0 1   my ($self, $cgi, $path) = @_;
215            
216             # Directory requested, redirecting to index.html
217 0           $path =~ s{/+$}{};
218            
219 0           my $index_file = $self->index_file;
220            
221 0           $self->logit("Redirecting to $path/$index_file");
222            
223 0           my $out = $self->stdio_handle;
224              
225 0           print $out $cgi->redirect(
226             -uri => "$path/$index_file",
227             -status => '301 Moved Permanently'
228             );
229            
230 0           return 1;
231             }
232              
233             ### PUBLIC INSTANCE METHOD ###
234             #
235             # Handle static content
236             #
237              
238             sub handle_static {
239 0     0 1   my ($self, %arg) = @_;
240              
241 0           my $cgi = $arg{cgi};
242 0           my $file_name = $arg{file_name};
243              
244 0           $self->logit("Handling static request for $file_name");
245              
246 0           my ($fino, $fsize, $fmtime) = (stat $file_name)[1, 7, 9];
247 0 0         return $self->handle_404() unless $fino;
248              
249 0           my ($type, $charset) = $self->_guess_mime_type($file_name);
250            
251 0           $self->logit("Got MIME type $type");
252            
253 0           my $out = $self->stdio_handle;
254            
255             # We're only processing If-Modified-Since if HTTP::Date is installed.
256             # That's because str2time is not trivial and there's no point in
257             # copying that much code. The feature is not worth it.
258 0 0         if ( $have_http_date ) {
259 0           my $ims = $cgi->http('If-Modified-Since');
260            
261 0 0 0       if ( $ims && $fmtime <= str2time($ims) ) {
262 0           $self->logit("File has not changed, serving 304");
263 0           print $out $cgi->header(
264             -type => $type,
265             -status => '304 Not Modified',
266             );
267            
268 0           return 1;
269             };
270             }
271            
272 0           my ($in, $buf);
273              
274 0 0         if ( not open $in, '<', $file_name ) {
275 0           $self->logit("File is unreadable, serving 403");
276 0           return $self->handle_403($cgi);
277             };
278              
279 0           $self->logit("Serving file content with 200");
280            
281 0           my $expires = $self->expires_after;
282              
283 0 0 0       print $out $cgi->header(
284             -type => $type,
285             -status => '200 OK',
286             -charset => ($charset || ($type !~ /image|octet/ ? 'utf-8' : '')),
287             ( $expires ? ( -Expires => time2str(time + $expires) ) : () ),
288             -Content_Length => $fsize,
289             -Last_Modified => time2str($fmtime),
290             );
291              
292 0           my $bufsize = $self->buffer_size;
293            
294 0           binmode $in;
295 0           binmode $out;
296            
297             # Making the out handle hot helps in older Perls
298             {
299 0           my $orig_fh = select $out;
  0            
300 0           $| = 1;
301 0           select $orig_fh;
302             }
303              
304 0           print $out $buf while sysread $in, $buf, $bufsize;
305              
306 0           return 1;
307             }
308              
309             ### PUBLIC INSTANCE METHOD ###
310             #
311             # Return Ext.Direct API declaration JavaScript
312             #
313              
314             sub handle_extdirect_api {
315 0     0 1   my ($self, $cgi) = @_;
316              
317 0           $self->logit("Got Ext.Direct API request");
318              
319 0           return $self->_handle_extdirect($cgi, 'api');
320             }
321              
322             ### PUBLIC INSTANCE METHOD ###
323             #
324             # Route Ext.Direct method calls
325             #
326              
327             sub handle_extdirect_router {
328 0     0 1   my ($self, $cgi) = @_;
329              
330 0           $self->logit("Got Ext.Direct route request");
331              
332 0           return $self->_handle_extdirect($cgi, 'route');
333             }
334              
335             ### PUBLIC INSTANCE METHOD ###
336             #
337             # Poll Ext.Direct event providers for events
338             #
339              
340             sub handle_extdirect_poll {
341 0     0 1   my ($self, $cgi) = @_;
342              
343 0           $self->logit("Got Ext.Direct event poll request");
344              
345 0           return $self->_handle_extdirect($cgi, 'poll');
346             }
347              
348             ### PUBLIC INSTANCE METHOD ###
349             #
350             # Return 403 header without a body.
351             #
352              
353             sub handle_403 {
354 0     0 1   my ($self, $cgi, $uri) = @_;
355            
356 0           $self->logit("Handling 403 for URI $uri");
357            
358 0           my $out = $self->stdio_handle;
359            
360 0           print $out $cgi->header(-status => '403 Forbidden');
361            
362 0           return 1;
363             }
364              
365             ### PUBLIC INSTANCE METHOD ###
366             #
367             # Return 404 header without a body.
368             #
369              
370             sub handle_404 {
371 0     0 1   my ($self, $cgi, $uri) = @_;
372              
373 0           $self->logit("Handling 404 for URI $uri");
374            
375 0           my $out = $self->stdio_handle;
376              
377 0           print $out $cgi->header(-status => '404 Not Found');
378              
379 0           return 1;
380             }
381              
382             ### PUBLIC INSTANCE METHOD ###
383             #
384             # Log debugging info to STDERR
385             #
386              
387             sub logit {
388 0     0 1   my $self = shift;
389            
390 0 0         print STDERR @_, "\n" if $self->config->debug;
391             }
392              
393             ### PUBLIC PACKAGE SUBROUTINE ###
394             #
395             # Prints banner, but only if debugging is on
396             #
397              
398             sub print_banner {
399 0     0 1   my $self = shift;
400              
401 0 0         $self->SUPER::print_banner if $self->config->debug;
402             }
403              
404             ### PUBLIC INSTANCE METHODS ###
405             #
406             # Read-write accessors
407             #
408              
409             RPC::ExtDirect::Util::Accessor->mk_accessors(
410             simple => [qw/
411             api
412             config
413             dispatch
414             static_dir
415             index_file
416             expires_after
417             buffer_size
418             /],
419             );
420              
421             ############## PRIVATE METHODS BELOW ##############
422              
423             ### PRIVATE INSTANCE METHOD ###
424             #
425             # Parse HTTP request line. Returns three values: request method,
426             # URI and protocol.
427             #
428             # This method is overridden to improve parsing speed. The original
429             # method is reading characters from STDIN one by one, which
430             # results in abysmal performance. Not sure what was the intent
431             # there but I haven't encountered any problems so far with the
432             # faster implementation below.
433             #
434             # The same is applicable to the parse_headers() below.
435             #
436              
437             sub parse_request {
438 0     0 1   my $self = shift;
439              
440 0           my $io_handle = $self->stdio_handle;
441 0           my $input = <$io_handle>;
442              
443 0 0         return unless $input;
444              
445 0 0         $input =~ /^(\w+)\s+(\S+)(?:\s+(\S+))?\r?$/ and
446             return ( $1.'', $2.'', $3.'' );
447             }
448              
449             ### PRIVATE INSTANCE METHOD ###
450             #
451             # Parse incoming HTTP headers from input file handle and return
452             # an arrayref of header/value pairs.
453             #
454              
455             sub parse_headers {
456 0     0 1   my $self = shift;
457              
458 0           my $io_handle = $self->stdio_handle;
459              
460 0           my @headers;
461              
462 0           while ( my $input = <$io_handle> ) {
463 0           $input =~ s/[\r\l\n\s]+$//;
464 0 0         last if !$input;
465              
466 0 0         push @headers, $1 => $2
467             if $input =~ /^([^()<>\@,;:\\"\/\[\]?={} \t]+):\s*(.*)/i;
468             };
469              
470 0           return \@headers;
471             }
472              
473             ### PRIVATE INSTANCE METHOD ###
474             #
475             # Initialize CGI class. Used by constructor.
476             #
477              
478             sub _init_cgi_class {
479 0     0     my ($self, $cgi_class) = @_;
480            
481             # Default to CGI::Simple > 1.113 if it's available, unless the user
482             # overrode cgi_class to do something else. CGI::Simple 1.113 and
483             # earlier has a bug with form/multipart file upload handling, so
484             # we don't use it even if it is installed.
485 0 0 0       if ( $cgi_class ) {
    0 0        
486 0           $self->cgi_class($cgi_class);
487            
488 0 0         if ( $cgi_class eq 'CGI' ) {
489             $self->cgi_init(sub {
490 0     0     local $@;
491            
492 0           eval {
493 0           require CGI;
494 0           CGI::initialize_globals();
495             }
496 0           });
497             }
498             else {
499             $self->cgi_init(sub {
500 0     0     eval "require $cgi_class";
501 0           });
502             }
503             }
504             elsif ( $have_cgi_simple && $CGI::Simple::VERSION > 1.113 &&
505             $self->cgi_class eq 'CGI' )
506             {
507 0           $self->cgi_class('CGI::Simple');
508 0           $self->cgi_init(undef);
509             }
510             }
511              
512             ### PRIVATE INSTANCE METHOD ###
513             #
514             # Initialize dispatch table. Used by constructor.
515             #
516              
517             sub _init_dispatch {
518 0     0     my ($self, $cust_disp) = @_;
519            
520 0           my $config = $self->config;
521            
522 0           my @dispatch;
523              
524             # Set the custom handlers so they would come first served.
525             # Format:
526             # [ qr{URI} => \&method, ... ]
527             # [ { match => qr{URI}, code => \&method, } ]
528 0           while ( my $uri = shift @$cust_disp ) {
529 0           $self->logit("Installing custom handler for URI: $uri");
530 0           push @dispatch, {
531             match => qr{$uri},
532             code => shift @$cust_disp,
533             };
534             };
535            
536             # The default Ext.Direct handlers always come last
537 0           for my $type ( qw/ api router poll / ) {
538 0           my $uri_getter = "${type}_path";
539 0           my $handler = "handle_extdirect_${type}";
540 0           my $uri = $config->$uri_getter;
541            
542 0 0         if ( $uri ) {
543 0           push @dispatch, {
544 0           match => qr/^\Q$uri\E$/, code => \&{ $handler },
545             }
546             }
547             }
548              
549 0           $self->dispatch(\@dispatch);
550             }
551              
552             ### PRIVATE INSTANCE METHOD ###
553             #
554             # Do the actual heavy lifting for Ext.Direct calls
555             #
556              
557             sub _handle_extdirect {
558 0     0     my ($self, $cgi, $what) = @_;
559              
560 0           my $exd = CGI::ExtDirect->new({
561             api => $self->api,
562             config => $self->config,
563             cgi => $cgi,
564             });
565              
566             # Standard CGI headers for this handler
567 0           my %std_cgi = ( '-nph' => 1, '-charset' => 'utf-8' );
568            
569 0           my $out = $self->stdio_handle;
570              
571 0           print $out $exd->$what( %std_cgi );
572              
573 0           return 1;
574             }
575              
576             # Popular MIME types, taken from http://lwp.interglacial.com/appc_01.htm
577             my %MIME_TYPES = (
578             au => 'audio/basic',
579             avi => 'vide/avi',
580             bmp => 'image/bmp',
581             bz2 => 'application/x-bzip2',
582             css => 'text/css',
583             dtd => 'application/xml-dtd',
584             doc => 'application/msword',
585             gif => 'image/gif',
586             gz => 'application/x-gzip',
587             ico => 'image/x-icon',
588             hqx => 'application/mac-binhex40',
589             htm => 'text/html',
590             html => 'text/html',
591             jar => 'application/java-archive',
592             jpg => 'image/jpeg',
593             jpeg => 'image/jpeg',
594             js => 'text/javascript',
595             json => 'application/json',
596             midi => 'audio/x-midi',
597             mp3 => 'audio/mpeg',
598             mpeg => 'video/mpeg',
599             ogg => 'audio/vorbis',
600             pdf => 'application/pdf',
601             pl => 'application/x-perl',
602             png => 'image/png',
603             ppt => 'application/vnd.ms-powerpoint',
604             ps => 'application/postscript',
605             qt => 'video/quicktime',
606             rdf => 'application/rdf',
607             rtf => 'application/rtf',
608             sgml => 'text/sgml',
609             sit => 'application/x-stuffit',
610             svg => 'image/svg+xml',
611             swf => 'application/x-shockwave-flash',
612             tgz => 'application/x-tar',
613             tiff => 'image/tiff',
614             tsv => 'text/tab-separated-values',
615             txt => 'text/plain',
616             wav => 'audio/wav',
617             xls => 'application/excel',
618             xml => 'application/xml',
619             zip => 'application/zip',
620             );
621              
622             ### PRIVATE INSTANCE METHOD ###
623             #
624             # Return the guessed MIME type for a file name
625             #
626              
627             # We try to use File::LibMagic or File::MimeInfo if available
628             {
629             local $@;
630            
631             my $have_libmagic = $ENV{DEBUG_NO_FILE_LIBMAGIC}
632             ? !1
633             : eval "require File::LibMagic";
634            
635             my $have_mimeinfo = $ENV{DEBUG_NO_FILE_MIMEINFO}
636             ? !1
637             : eval "require File::MimeInfo";
638            
639             sub _guess_mime_type {
640 0     0     my ($self, $file_name) = @_;
641            
642 0           my ($type, $charset);
643            
644 0 0         if ( $have_libmagic ) {
    0          
645 0           my $magic = File::LibMagic->new();
646 0           my $mime = $magic->checktype_filename($file_name);
647            
648 0           ($type, $charset) = $mime =~ m{^([^;]+);\s*charset=(.*)$};
649             }
650             elsif ( $have_mimeinfo ) {
651 0           my $mimeinfo = File::MimeInfo->new();
652 0           $type = $mimeinfo->mimetype($file_name);
653             }
654            
655             # If none of the advanced modules are present, resort to
656             # guesstimating by file extension
657             else {
658 0           my ($suffix) = $file_name =~ /.*\.(\w+)$/;
659            
660 0           $type = $MIME_TYPES{ $suffix };
661             }
662            
663 0   0       return ($type || 'application/octet-stream', $charset);
664             }
665             }
666              
667             1;