File Coverage

lib/UR/Service/WebServer.pm
Criterion Covered Total %
statement 79 81 97.5
branch 16 18 88.8
condition 7 10 70.0
subroutine 19 19 100.0
pod 6 6 100.0
total 127 134 94.7


line stmt bran cond sub pod time code
1             package UR::Service::WebServer;
2              
3 1     1   92 use strict;
  1         1  
  1         29  
4 1     1   3 use warnings;
  1         1  
  1         27  
5              
6 1     1   4 use UR;
  1         1  
  1         10  
7 1     1   342 use UR::Service::WebServer::Server;
  1         3  
  1         8  
8 1     1   26 use IO::File;
  1         1  
  1         119  
9 1     1   4 use IO::Socket::INET;
  1         1  
  1         8  
10 1     1   464 use Sys::Hostname;
  1         2  
  1         637  
11              
12             class UR::Service::WebServer {
13             has => [
14             host => { is => 'String',
15             default_value => 'localhost',
16             doc => 'IP address to listen on' },
17             port => { is => 'Integer',
18             default_value => undef,
19             doc => 'TCP port to listen on' },
20             ],
21             has_optional => [
22             server => { is => 'HTTP::Server::PSGI', calculate_from => ['__host','__port'], is_constant => 1,
23             calculate => q(
24             return UR::Service::WebServer::Server->new(
25             host => $__host,
26             port => $__port,
27             timeout => $self->timeout,
28             server_ready => sub { $self->announce() },
29             );
30             ), },
31             timeout => { is => 'Integer',
32             default_value => undef,
33             doc => 'Timeout for read and write events' },
34             idle_timeout => { is => 'Integer', default_value => undef,
35             doc => 'Exit the event loop after being idle for this many seconds' },
36             cb => { is => 'CODE', doc => 'callback for handling requests' },
37             ],
38             };
39              
40             # Override port and host so they can auto-fill when needed
41             sub _port_host_override {
42 9     9   10 my $self = shift;
43 9         8 my $methodname = shift;
44 9         12 my $method = '__'.$methodname;
45 9         12 my $socket_method = 'sock'.$methodname;
46 9 100       12 if (@_) {
47 6 100       15 if ($self->{server}) {
48 2         31 die "Cannot change $methodname after it has created the listen socket";
49             }
50 4         18 $self->$method(@_);
51              
52             } else {
53             # if (!defined($self->$method) && !defined($self->{server})) {
54 3 50       13 unless (defined $self->$method) {
55 3 100       8 unless (defined $self->{server}) {
56             # not connected yet - start the server's listen socket and get its port
57 2         8 $self->server->setup_listener();
58             }
59 3         17 $self->$method( $self->server->listen_sock->$socket_method() );
60             }
61             }
62 7         14 return $self->$method;
63             }
64              
65             sub port {
66 5     5 1 398 my $self = shift;
67 5         13 $self->_port_host_override('port', @_);
68             }
69              
70             sub host {
71 4     4 1 300 my $self = shift;
72 4         8 $self->_port_host_override('host', @_);
73             }
74              
75              
76             sub announce {
77 7     7 1 10 my $self = shift;
78              
79 7         22 my $sock = $self->server->listen_sock;
80 7 100       19 my $host = ($sock->sockhost eq '0.0.0.0') ? Sys::Hostname::hostname() : gethostbyaddr($sock->sockaddr, AF_INET);
81 7         403 $self->status_message(sprintf('Listening on http://%s:%d/', $host, $sock->sockport));
82 7         29 return 1;
83             }
84              
85             sub run {
86 3     3 1 22 my $self = shift;
87              
88 3   66     11 my $cb = shift || $self->cb;
89              
90 3 50       8 unless ($cb) {
91 0         0 $self->warning_message("No callback for run()... returning");
92 0         0 return;
93             }
94              
95 3   100     9 my $timeout = $self->idle_timeout || 0;
96 3     1   44 local $SIG{'ALRM'} = sub { die "alarm\n" };
  1         999504  
97 3         5 eval {
98 3         11 alarm($timeout);
99 3         8 $self->server->run($cb);
100             };
101 3         111 alarm(0);
102 3 100       36 die $@ unless $@ eq "alarm\n";
103             }
104              
105             my %mime_types = (
106             'js' => 'application/javascript',
107             'html' => 'text/html',
108             'css' => 'text/css',
109             '*' => 'text/plain',
110             );
111             sub _mime_type_for_filename {
112 6     6   8 my($self, $pathname) = @_;
113 6         20 my($ext) = ($pathname =~ m/\.(\w+)$/);
114 6   100     15 $ext ||= '*';
115 6   33     18 return $mime_types{$ext} || $mime_types{'*'};
116             }
117             sub _file_opener_for_directory {
118 2     2   3 my($self, $dir) = @_;
119             return sub {
120 8     8   14 (my $pathname = shift) =~ s#/?\.\.##g; # Remove .. - don't want them escaping the given directory tree
121 8         47 return IO::File->new( join('/', $dir, $pathname), 'r');
122 2         11 };
123            
124             }
125             sub file_handler_for_directory {
126 2     2 1 3432 my($self, $dir) = @_;
127              
128 2         6 my $opener = $self->_file_opener_for_directory($dir);
129              
130             return sub {
131 8     8   5287 my($env, $pathname) = @_;
132              
133 8         14 my $fh = $opener->($pathname);
134 8 100       507 unless($fh) {
135 2         9 return [ 404, [ 'Content-Type' => 'text/plain'], ['Not Found']];
136             }
137 6         28 my $type = $self->_mime_type_for_filename($pathname);
138 6 100       12 if ($env->{'psgi.streaming'}) {
139 3         10 return [ 200, ['Content-Type' => $type], $fh];
140             } else {
141 3         7 local $/;
142 3         37 my $buffer = <$fh>;
143 3         32 return [ 200, ['Content-Type' => $type], [$buffer]];
144             }
145 2         7 };
146             }
147              
148             sub delete {
149 3     3 1 3000 my $self = shift;
150 3         14 $self->server->listen_sock->close();
151 3         96 $self->{server} = undef;
152 3         18 $self->SUPER::delete(@_);
153             }
154              
155             1;
156              
157             =pod
158              
159             =head1 NAME
160              
161             UR::Service::WebServer - A PSGI-based web server
162              
163             =head1 SYNOPSIS
164              
165             my $s = UR::Service::WebServer(port => 4321);
166             $s->run( \&handle_request );
167              
168             =head1 DESCRIPTION
169              
170             Implements a simple, standalone web server based on HTTP::Server::PSGI. The
171             event loop is entered by calling the run() method.
172              
173             =head2 Properties
174              
175             =over 4
176              
177             =item host
178              
179             The IP address to listen on for connections. The default value is
180             'localhost'. host can be changed any time before the server is created,
181             usually the first time run() is called.
182              
183             =item port
184              
185             The TCP port to listen on for connections. The detault value is undef,
186             meaning that the system will pick an unused port. port can be changed any
187             time before the server is created, usually the first time run() is called.
188              
189             =item server
190              
191             Holds a reference to an object that isa HTTP::Server::PSGI. This will be
192             automatically created the first time run() is called.
193              
194             =item cb
195              
196             Holds a CODE reference used as the default request handler within run().
197              
198             =back
199              
200             =head2 Methods
201              
202             =over 4
203              
204             =item $self->announce()
205              
206             This method is called when the PSGI server is ready to accept requests.
207             The base-class behavior is to print the listening URL on STDOUT. Subclasses
208             can override it to implement their own behavior.
209              
210             =item my $code = $self->file_handler_for_directory($path)
211              
212             A helper method used for implementing server for files located in the
213             directory $path. It returns a CODE ref that takes 2 arguments, $env (the
214             standard PSGI env hashref) and $pathname (a path relative to $path). It
215             returns the standard tuple a PSGI server expects.
216              
217             $pathname is pre-processed by removing all occurrences of ".." to keep requests
218             within the provided $path. If the requested file is not found, then it
219             returns a 404.
220              
221             =item $self->run(<$cb>)
222              
223             Enter the request loop. If a callback is not provided to run(), then the
224             object's cb property is used instead. If neither have a value, then run()
225             returns immediately.
226              
227             For each request $cb is called with one argument, the standard PSGI env
228             hashref.
229              
230             =back
231              
232             =head1 SEE ALSO
233              
234             L
235              
236             =cut