File Coverage

blib/lib/Net/Async/HTTP/Server/PSGI.pm
Criterion Covered Total %
statement 95 95 100.0
branch 34 38 89.4
condition 4 8 50.0
subroutine 15 15 100.0
pod 2 2 100.0
total 150 158 94.9


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2013 -- leonerd@leonerd.org.uk
5              
6             package Net::Async::HTTP::Server::PSGI;
7              
8 3     3   205357 use strict;
  3         26  
  3         97  
9 3     3   17 use warnings;
  3         6  
  3         77  
10              
11 3     3   23 use Carp;
  3         7  
  3         207  
12              
13 3     3   22 use base qw( Net::Async::HTTP::Server );
  3         5  
  3         1192  
14              
15             our $VERSION = '0.12';
16              
17 3     3   1625 use HTTP::Response;
  3         21976  
  3         3419  
18              
19             my $CRLF = "\x0d\x0a";
20              
21             =head1 NAME
22              
23             C - use C applications with C
24              
25             =head1 SYNOPSIS
26              
27             use Net::Async::HTTP::Server::PSGI;
28             use IO::Async::Loop;
29              
30             my $loop = IO::Async::Loop->new;
31              
32             my $httpserver = Net::Async::HTTP::Server::PSGI->new(
33             app => sub {
34             my $env = shift;
35              
36             return [
37             200,
38             [ "Content-Type" => "text/plain" ],
39             [ "Hello, world!" ],
40             ];
41             },
42             );
43              
44             $loop->add( $httpserver );
45              
46             $httpserver->listen(
47             addr => { family => "inet6", socktype => "stream", port => 8080 },
48             )->get;
49              
50             $loop->run;
51              
52             =head1 DESCRIPTION
53              
54             This subclass of L allows an HTTP server to use a
55             L application to respond to requests. It acts as a gateway between the
56             HTTP connection from the web client, and the C application. Aside from
57             the use of C instead of the C event, this class behaves
58             similarly to C.
59              
60             To handle the content length when sending responses, the PSGI implementation
61             may add a header to the response. When sending a plain C of strings, if
62             a C header is absent, the length will be calculated by taking
63             the total of all the strings in the array, and setting the length header. When
64             sending content from an IO reference or using the streaming responder C
65             reference, the C header will be set to C, and all
66             writes will be performed as C chunks.
67              
68             =cut
69              
70             =head1 PARAMETERS
71              
72             The following named parameters may be passed to C or C:
73              
74             =over 8
75              
76             =item app => CODE
77              
78             Reference to the actual C application to use for responding to requests
79              
80             =back
81              
82             =cut
83              
84             sub configure
85             {
86 12     12 1 195345 my $self = shift;
87 12         48 my %args = @_;
88              
89 12 100       53 if( exists $args{app} ) {
90 10         106 $self->{app} = delete $args{app};
91             }
92              
93 12         62 $self->SUPER::configure( %args );
94             }
95              
96             =head1 PSGI ENVIRONMENT
97              
98             The following extra keys are supplied to the environment of the C app:
99              
100             =over 8
101              
102             =item C
103              
104             The actual L filehandle that the request was received on.
105              
106             If the server is running under SSL for HTTPS, this will be an
107             L instance, so reading from or writing to it will happen in
108             cleartext.
109              
110             =item C
111              
112             The C object serving the request
113              
114             =item C
115              
116             The L object representing this particular
117             request
118              
119             =item C
120              
121             The L object that the C
122             object is a member of.
123              
124             =back
125              
126             =cut
127              
128             sub on_request
129             {
130 9     9 1 150 my $self = shift;
131 9         24 my ( $req ) = @_;
132              
133             # Much of this code stolen fro^W^Winspired by Plack::Handler::Net::FastCGI
134              
135 9     2   57 open my $stdin, "<", \$req->body;
  2         98  
  2         4  
  2         14  
136              
137 9         1941 my $socket = $req->stream->read_handle;
138              
139 9         68 my $path_info = $req->path;
140 9 100       212 $path_info = "" if $path_info eq "/";
141              
142 9   50     43 my %env = (
143             SERVER_PORT => $socket->sockport,
144             SERVER_NAME => $socket->sockhost,
145             SERVER_PROTOCOL => $req->protocol,
146             SCRIPT_NAME => '',
147             PATH_INFO => $path_info,
148             QUERY_STRING => $req->query_string // "",
149             REMOTE_ADDR => $socket->peerhost,
150             REMOTE_PORT => $socket->peerport,
151             REQUEST_METHOD => $req->method,
152             REQUEST_URI => $req->path,
153             'psgi.version' => [1,0],
154             'psgi.url_scheme' => "http",
155             'psgi.input' => $stdin,
156             'psgi.errors' => \*STDERR,
157             'psgi.multithread' => 0,
158             'psgi.multiprocess' => 0,
159             'psgi.run_once' => 0,
160             'psgi.nonblocking' => 1,
161             'psgi.streaming' => 1,
162              
163             # Extensions
164             'psgix.io' => $socket,
165             'psgix.input.buffered' => 1, # we're using a PerlIO scalar handle
166             'net.async.http.server' => $self,
167             'net.async.http.server.req' => $req,
168             'io.async.loop' => $self->get_loop,
169             );
170              
171 9         453 foreach ( $req->headers ) {
172 4         11 my ( $name, $value ) = @$_;
173 4         22 $name =~ s/-/_/g;
174 4         11 $name = uc $name;
175              
176             # Content-Length and Content-Type don't get HTTP_ prefix
177 4 100       21 $name = "HTTP_$name" unless $name =~ m/^CONTENT_(?:LENGTH|TYPE)$/;
178              
179 4         12 $env{$name} = $value;
180             }
181              
182 9         75 my $resp = $self->{app}->( \%env );
183              
184             my $responder = sub {
185 9     9   2202 my ( $status, $headers, $body ) = @{ +shift };
  9         24  
186              
187 9         76 my $response = HTTP::Response->new( $status );
188 9         483 $response->protocol( $req->protocol );
189              
190 9         153 my $has_content_length = 0;
191 9         20 my $use_chunked_transfer;
192 9         48 while( my ( $key, $value ) = splice @$headers, 0, 2 ) {
193 11         55 $response->header( $key, $value );
194              
195 11 100       702 $has_content_length = 1 if $key eq "Content-Length";
196 11 50 33     69 $use_chunked_transfer++ if $key eq "Transfer-Encoding" and $value eq "chunked";
197             }
198              
199 9 100       41 if( !defined $body ) {
    100          
200 2 50       7 croak "Responder given no body in void context" unless defined wantarray;
201              
202 2 100       9 unless( $has_content_length ) {
203 1         4 $response->header( "Transfer-Encoding" => "chunked" );
204 1         48 $use_chunked_transfer++;
205             }
206              
207 2         10 $req->write( $response->as_string( $CRLF ) );
208              
209 2 100       18 return $use_chunked_transfer ?
210             Net::Async::HTTP::Server::PSGI::ChunkWriterStream->new( $req ) :
211             Net::Async::HTTP::Server::PSGI::WriterStream->new( $req );
212             }
213             elsif( ref $body eq "ARRAY" ) {
214 5 50       14 unless( $has_content_length ) {
215 5         8 my $len = 0;
216 5         9 my $found_undef;
217 5   66     21 $len += length( $_ // ( $found_undef++, "" ) ) for @$body;
218 5 100       332 carp "Found undefined value in PSGI body" if $found_undef;
219              
220 5         113 $response->content_length( $len );
221             }
222              
223 5         222 $req->write( $response->as_string( $CRLF ) );
224              
225 5         24 $req->write( $_ ) for @$body;
226 5         35 $req->done;
227             }
228             else {
229 2 100       6 unless( $has_content_length ) {
230 1         6 $response->header( "Transfer-Encoding" => "chunked" );
231 1         54 $use_chunked_transfer++;
232             }
233              
234 2         8 $req->write( $response->as_string( $CRLF ) );
235              
236 2 100       6 if( $use_chunked_transfer ) {
237             $req->write( sub {
238             # We can't return the EOF chunk and set undef in one go
239             # What we'll have to do is send the EOF chunk then clear $body,
240             # which indicates end
241 3 100       733 return unless defined $body;
242              
243 2         14 local $/ = \8192;
244 2         62 my $buffer = $body->getline;
245              
246             # Form HTTP chunks out of it
247 2 100       91 defined $buffer and
248             return sprintf( "%X$CRLF%s$CRLF", length $buffer, $buffer );
249              
250 1         11 $body->close;
251 1         12 undef $body;
252 1         7 return "0$CRLF$CRLF";
253 1         7 } );
254             }
255             else {
256             $req->write( sub {
257 2         488 local $/ = \8192;
258 2         55 my $buffer = $body->getline;
259              
260 2 100       53 defined $buffer and return $buffer;
261              
262 1         5 $body->close;
263 1         9 return undef;
264 1         5 } );
265             }
266              
267 2         8 $req->done;
268             }
269 9         176 };
270              
271 9 100       43 if( ref $resp eq "ARRAY" ) {
    50          
272 6         16 $responder->( $resp );
273             }
274             elsif( ref $resp eq "CODE" ) {
275 3         12 $resp->( $responder );
276             }
277             }
278              
279             # Hide from indexer
280             package
281             Net::Async::HTTP::Server::PSGI::WriterStream;
282              
283             sub new
284             {
285 1     1   3 my $class = shift;
286 1         8 return bless [ @_ ], $class;
287             }
288              
289 2     2   37790 sub write { shift->[0]->write( $_[0] ) }
290 1     1   8 sub close { shift->[0]->done }
291              
292             # Hide from indexer
293             package
294             Net::Async::HTTP::Server::PSGI::ChunkWriterStream;
295              
296             sub new
297             {
298 1     1   3 my $class = shift;
299 1         48 return bless [ @_ ], $class;
300             }
301              
302 2     2   40002 sub write { shift->[0]->write_chunk( $_[0] ) }
303 1     1   9 sub close { shift->[0]->write_chunk_eof }
304              
305             =head1 SEE ALSO
306              
307             =over 4
308              
309             =item *
310              
311             L - Perl Web Server Gateway Interface Specification
312              
313             =item *
314              
315             L - HTTP handler for Plack using
316             L
317              
318             =back
319              
320             =head1 AUTHOR
321              
322             Paul Evans
323              
324             =cut
325              
326             0x55AA;