File Coverage

blib/lib/Net/Async/HTTP/Server/PSGI.pm
Criterion Covered Total %
statement 93 94 98.9
branch 35 42 83.3
condition 4 8 50.0
subroutine 14 14 100.0
pod 2 2 100.0
total 148 160 92.5


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-2024 -- leonerd@leonerd.org.uk
5              
6             package Net::Async::HTTP::Server::PSGI 0.15;
7              
8 4     4   642643 use v5.14;
  4         14  
9 4     4   20 use warnings;
  4         8  
  4         201  
10              
11 4     4   19 use Carp;
  4         7  
  4         300  
12              
13 4     4   39 use base qw( Net::Async::HTTP::Server );
  4         20  
  4         1791  
14              
15 4     4   1867 use HTTP::Response;
  4         28063  
  4         5065  
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 259080 my $self = shift;
85 14         69 my %args = @_;
86              
87 14 100       119 if( exists $args{app} ) {
88 11         191 $self->{app} = delete $args{app};
89             }
90              
91 14         114 $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 215 my $self = shift;
129 10         26 my ( $req ) = @_;
130              
131             # Much of this code stolen fro^W^Winspired by Plack::Handler::Net::FastCGI
132              
133 10         83 open my $stdin, "<", \$req->body;
134              
135 10         321 my $socket = $req->stream->read_handle;
136              
137 10         81 my $path_info = $req->path;
138 10 100       263 $path_info = "" if $path_info eq "/";
139              
140 10   50     44 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       676 if( $socket->can( "sockport" ) ) { # INET or IP
    0          
166 10         101 %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         2097 foreach ( $req->headers ) {
182 4         13 my ( $name, $value ) = @$_;
183 4         22 $name =~ s/-/_/g;
184 4         11 $name = uc $name;
185              
186             # Content-Length and Content-Type don't get HTTP_ prefix
187 4 100       22 $name = "HTTP_$name" unless $name =~ m/^CONTENT_(?:LENGTH|TYPE)$/;
188              
189 4         47 $env{$name} = $value;
190             }
191              
192 10         81 my $resp = $self->{app}->( \%env );
193              
194             my $responder = sub {
195 10     10   3727 my ( $status, $headers, $body ) = @{ +shift };
  10         43  
196              
197 10         110 my $response = HTTP::Response->new( $status );
198 10         812 $response->protocol( $req->protocol );
199              
200 10         213 my $has_content_length = 0;
201 10         18 my $use_chunked_transfer;
202 10         86 while( my ( $key, $value ) = splice @$headers, 0, 2 ) {
203 14         92 $response->push_header( $key, $value );
204              
205 14 100       744 $has_content_length = 1 if $key eq "Content-Length";
206 14 50 33     87 $use_chunked_transfer++ if $key eq "Transfer-Encoding" and $value eq "chunked";
207             }
208              
209 10 100       62 if( !defined $body ) {
    100          
210 2 50       9 croak "Responder given no body in void context" unless defined wantarray;
211              
212 2 100       9 unless( $has_content_length ) {
213 1         11 $response->header( "Transfer-Encoding" => "chunked" );
214 1         73 $use_chunked_transfer++;
215             }
216              
217 2         12 $req->write( $response->as_string( $CRLF ) );
218              
219 2 100       33 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       21 unless( $has_content_length ) {
225 6         11 my $len = 0;
226 6         9 my $found_undef;
227 6   66     45 $len += length( $_ // ( $found_undef++, "" ) ) for @$body;
228 6 100       248 carp "Found undefined value in PSGI body" if $found_undef;
229              
230 6         40 $response->content_length( $len );
231             }
232              
233 6         316 $req->write( $response->as_string( $CRLF ) );
234              
235 6         42 $req->write( $_ ) for @$body;
236 6         49 $req->done;
237             }
238             else {
239 2 100       8 unless( $has_content_length ) {
240 1         12 $response->header( "Transfer-Encoding" => "chunked" );
241 1         133 $use_chunked_transfer++;
242             }
243              
244 2         11 $req->write( $response->as_string( $CRLF ) );
245              
246 2 100       23 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       1311 return unless defined $body;
252              
253 2         13 local $/ = \8192;
254 2         57 my $buffer = $body->getline;
255              
256             # Form HTTP chunks out of it
257 2 100       22 defined $buffer and
258             return sprintf( "%X$CRLF%s$CRLF", length $buffer, $buffer );
259              
260 1         13 $body->close;
261 1         13 undef $body;
262 1         7 return "0$CRLF$CRLF";
263 1         12 } );
264             }
265             else {
266             $req->write( sub {
267 2         842 local $/ = \8192;
268 2         49 my $buffer = $body->getline;
269              
270 2 100       15 defined $buffer and return $buffer;
271              
272 1         7 $body->close;
273 1         14 return undef;
274 1         9 } );
275             }
276              
277 2         10 $req->done;
278             }
279 10         221 };
280              
281 10 100       77 if( ref $resp eq "ARRAY" ) {
    50          
282 7         18 $responder->( $resp );
283             }
284             elsif( ref $resp eq "CODE" ) {
285 3         11 $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   4 my $class = shift;
296 1         42 return bless [ @_ ], $class;
297             }
298              
299 2     2   41388 sub write { shift->[0]->write( $_[0] ) }
300 1     1   10 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   3 my $class = shift;
309 1         11 return bless [ @_ ], $class;
310             }
311              
312 2     2   41527 sub write { shift->[0]->write_chunk( $_[0] ) }
313 1     1   11 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;