File Coverage

blib/lib/Net/Async/UWSGI/Server/Connection.pm
Criterion Covered Total %
statement 30 121 24.7
branch 0 44 0.0
condition 0 21 0.0
subroutine 10 30 33.3
pod 13 17 76.4
total 53 233 22.7


line stmt bran cond sub pod time code
1             package Net::Async::UWSGI::Server::Connection;
2             $Net::Async::UWSGI::Server::Connection::VERSION = '0.004';
3 2     2   8 use strict;
  2         2  
  2         62  
4 2     2   8 use warnings;
  2         2  
  2         62  
5              
6 2     2   6 use parent qw(IO::Async::Stream);
  2         2  
  2         8  
7              
8             =head1 NAME
9              
10             Net::Async::UWSGI::Server::Connection - represents an incoming connection to a server
11              
12             =head1 VERSION
13              
14             version 0.004
15              
16             =head1 DESCRIPTION
17              
18             =cut
19              
20 2     2   30349 use JSON::MaybeXS;
  2         9368  
  2         108  
21              
22 2     2   906 use URI::QueryParam;
  2         1034  
  2         46  
23 2     2   1044 use IO::Async::Timer::Countdown;
  2         2770  
  2         66  
24              
25 2     2   11 use Encode qw(encode);
  2         4  
  2         98  
26 2     2   927 use Protocol::UWSGI qw(:server);
  2         9457  
  2         287  
27 2     2   16 use List::UtilsBy qw(bundle_by);
  2         2  
  2         2088  
28              
29             =head2 CONTENT_TYPE_HANDLER
30              
31             =cut
32              
33             our %CONTENT_TYPE_HANDLER = (
34             'application/javascript' => 'json',
35             );
36              
37             =head1 METHODS
38              
39             =cut
40              
41             =head2 configure
42              
43             Applies configuration parameters.
44              
45             =over 4
46              
47             =item * bus - the event bus
48              
49             =item * on_request - callback when we get an incoming request
50              
51             =back
52              
53             =cut
54              
55             sub configure {
56 0     0 1   my ($self, %args) = @_;
57 0           for(qw(bus on_request default_content_handler)) {
58 0 0         $self->{$_} = delete $args{$_} if exists $args{$_};
59             }
60 0           $self->SUPER::configure(%args);
61             }
62              
63 0     0 0   sub default_content_handler { shift->{default_content_handler} }
64              
65             =head2 json
66              
67             Accessor for the current JSON state
68              
69             =cut
70              
71 0   0 0 1   sub json { shift->{json} ||= JSON::MaybeXS->new(utf8 => 1) }
72              
73             =head2 on_read
74              
75             Base read handler for incoming traffic.
76              
77             Attempts to delegate to L as soon as we get the UWSGI
78             frame.
79              
80             =cut
81              
82             sub on_read {
83 0     0 1   my ( $self, $buffref, $eof ) = @_;
84 0 0         if(my $pkt = extract_frame($buffref)) {
    0          
85 0           $self->{env} = $pkt;
86             # We have a request, start processing
87 0           return $self->can('dispatch_request');
88             } elsif($eof) {
89             # EOF before a valid request? Bail out immediately
90 0           $self->cancel;
91             }
92 0           return 0;
93             }
94              
95             =head2 cancel
96              
97             Cancels any request in progress.
98              
99             If there's still a connection to the client,
100             they'll receive a 500 response.
101              
102             It's far more likely that the client has gone
103             away, in which case there's no response to send.
104              
105             =cut
106              
107             sub cancel {
108 0     0 1   my ($self) = @_;
109 0 0         $self->response->cancel unless $self->response->is_ready
110             }
111              
112             =head2 env
113              
114             Accessor for the UWSGI environment.
115              
116             =cut
117              
118 0     0 1   sub env { shift->{env} }
119              
120             =head2 response
121              
122             Resolves when the response is complete.
123              
124             =cut
125              
126             sub response {
127 0   0 0 1   $_[0]->{response} ||= $_[0]->loop->new_future;
128             }
129              
130             =head2 dispatch_request
131              
132             At this point we have a request including headers,
133             and we should know whether there's a body involved
134             somewhere.
135              
136             =cut
137              
138             sub dispatch_request {
139 0     0 1   my ($self, $buffref, $eof) = @_;
140              
141             # Plain GET request? We might be able to bail out here
142 0 0         return $self->finish_request unless $self->has_body;
143              
144 0           my $env = $self->env;
145 0   0       my $handler = $self->default_content_handler || 'raw';
146 0 0         if(my $type = $env->{CONTENT_TYPE}) {
147 0 0         $handler = $CONTENT_TYPE_HANDLER{$type} if exists $CONTENT_TYPE_HANDLER{$type};
148             }
149 0           $handler = 'content_handler_' . $handler;
150 0           $self->{input_handler} = $self->${\"curry::weak::$handler"};
  0            
151              
152             # Try to read N bytes if we have content length. Most UWSGI implementations seem
153             # to set this.
154 0 0         if(exists $env->{CONTENT_LENGTH}) {
155 0           $self->{remaining} = $env->{CONTENT_LENGTH};
156 0           return $self->can('read_to_length');
157             }
158              
159             # Streaming might be nice, but nginx has no support for this
160 0 0 0       if(exists $env->{HTTP_TRANSFER_ENCODING} && $env->{HTTP_TRANSFER_ENCODING} eq 'chunked') {
161 0           return $self->can('read_chunked');
162             }
163 0           die "no idea how to handle this, missing length and not chunked";
164             }
165              
166             sub finish_request {
167 0     0 0   my ($self) = @_;
168 0 0         $self->{request_body} = $self->{input_handler}->()
169             if $self->has_body;
170             $self->{completion} = $self->{on_request}->($self)
171             ->then($self->curry::write_response)
172             ->on_fail(sub {
173 0     0     $self->debug_printf("Failed while attempting to handle request: %s (%s)", @_);
174 0           })->on_ready($self->curry::close_now);
175             return sub {
176 0     0     my ($self, $buffref, $eof) = @_;
177 0 0 0       $self->{completion}->cancel if $eof && !$self->{completion}->is_ready;
178 0           0
179             }
180 0           }
181              
182             {
183             my %methods_with_body = (
184             PUT => 1,
185             POST => 1,
186             PROPPATCH => 1,
187             );
188              
189             =head2 has_body
190              
191             Returns true if we're expecting a request body
192             for the current request method.
193              
194             =cut
195              
196             sub has_body {
197 0     0 1   my ($self, $env) = @_;
198 0 0         return 1 if $methods_with_body{$self->env->{REQUEST_METHOD}};
199 0           return 0;
200             }
201             }
202              
203             =head2 read_chunked
204              
205             Read handler for chunked data. Unlikely to be used by any real implementations.
206              
207             =cut
208              
209             sub read_chunked {
210 0     0 1   my ($self, $buffref, $eof) = @_;
211 0           $self->debug_printf("Body read: $self, $buffref, $eof: [%s]", $$buffref);
212 0 0         if(defined $self->{chunk_remaining}) {
213 0           my $data = substr $$buffref, 0, $self->{chunk_remaining}, '';
214 0           $self->{chunk_remaining} -= length $data;
215 0           $self->debug_printf("Had %d bytes, %d left in chunk", length($data), $self->{chunk_remaining});
216 0           $self->{input_handler}->($data);
217 0 0         return 0 if $self->{chunk_remaining};
218 0           $self->debug_printf("Look for next chunk");
219 0           delete $self->{chunk_remaining};
220 0           return 1;
221             } else {
222 0 0         return 0 if -1 == (my $size_len = index($$buffref, "\x0D\x0A"));
223 0           $self->{chunk_remaining} = hex substr $$buffref, 0, $size_len, '';
224 0           substr $$buffref, 0, 2, '';
225 0           $self->debug_printf("Have %d bytes in this chunk", $self->{chunk_remaining});
226 0 0         return 1 if $self->{chunk_remaining};
227 0           $self->debug_printf("End of chunked data, looking for trailing headers");
228 0           return $self->can('on_trailing_header');
229             }
230             }
231              
232             =head2 on_trailing_header
233              
234             Deal with trailing headers. Not yet implemented.
235              
236             =cut
237              
238             sub on_trailing_header {
239 0     0 1   my ($self, $buffref, $eof) = @_;
240             # FIXME not yet implemented
241 0           $$buffref = '';
242 0           return $self->finish_request;
243             }
244              
245             =head2 read_to_length
246              
247             Read up to the expected fixed length of data.
248              
249             =cut
250              
251             sub read_to_length {
252 0     0 1   my ($self, $buffref, $eof) = @_;
253 0           $self->{remaining} -= length $$buffref;
254 0           $self->debug_printf("Body read: $self, $buffref, $eof: %s with %d remaining", $$buffref, $self->{remaining});
255 0           $self->{input_handler}->($$buffref);
256 0           $$buffref = '';
257 0 0         return $self->finish_request unless $self->{remaining};
258 0           return 0;
259             }
260              
261             =head2 request_body
262              
263             Accessor for the request body, available to the L callback.
264              
265             =cut
266              
267 0     0 1   sub request_body { shift->{request_body} }
268              
269             sub content_handler_raw {
270 0     0 0   my ($self, $data) = @_;
271 0 0         if(defined $data) {
272 0           $self->{data} .= $data;
273             } else {
274 0           return $self->{data}
275             }
276             }
277              
278             =head2 content_handler_json
279              
280             Handle JSON content.
281              
282             =cut
283              
284             sub content_handler_json {
285 0     0 1   my ($self, $data) = @_;
286 0 0         if(defined $data) {
287             eval {
288 0           $self->json->incr_parse($data);
289 0           1
290 0 0         } or do {
291 0           $self->debug_printf("Invalid JSON received: %s", $@);
292             };
293             } else {
294             return eval {
295 0           $self->json->incr_parse
296 0   0       } // do {
297 0           $self->debug_printf("Invalid JSON from incr_parse: %s", $@);
298             }
299             }
300             }
301              
302             my %status = (
303             200 => 'OK',
304             204 => 'No content',
305             400 => 'Bad request',
306             404 => 'Not found',
307             500 => 'Internal server error',
308             );
309 2     2   12 use constant USE_HTTP_RESPONSE => 0;
  2         3  
  2         453  
310             sub write_response {
311 0     0 0   my ($self, $code, $hdr, $body) = @_;
312 0 0         my $type = ref($body) ? 'text/javascript' : 'text/plain';
313 0 0         my $content = ref($body) ? encode_json($body) : encode(
314             'UTF-8' => $body
315             );
316 0   0       $hdr ||= [];
317 0           if(USE_HTTP_RESPONSE) {
318             return $self->write(
319             'HTTP/1.1 ' . HTTP::Response->new(
320             $code => ($status{$code} // 'Unknown'), [
321             'Content-Type' => $type,
322             'Content-Length' => length $content,
323             @$hdr
324             ],
325             $content
326             )->as_string("\x0D\x0A")
327             )
328             } else {
329             return $self->write(
330             join "\015\012", (
331             'HTTP/1.1 ' . $code . ' ' . ($status{$code} // 'Unknown'),
332             'Content-Type: ' . $type,
333             'Content-Length: ' . length($content),
334 0   0 0     (bundle_by { join ': ', @_ } 2, @$hdr),
  0            
335             '',
336             $content
337             )
338             )
339             }
340             }
341              
342             1;
343              
344             __END__