File Coverage

blib/lib/Net/Async/HTTP/Server/PSGI.pm
Criterion Covered Total %
statement 96 97 98.9
branch 35 42 83.3
condition 4 8 50.0
subroutine 15 15 100.0
pod 2 2 100.0
total 152 164 92.6


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