File Coverage

blib/lib/POEx/Role/PSGIServer.pm
Criterion Covered Total %
statement 16 16 100.0
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 22 100.0


line stmt bran cond sub pod time code
1             package POEx::Role::PSGIServer;
2             $POEx::Role::PSGIServer::VERSION = '1.150280';
3             #ABSTRACT: (DEPRECATED) Encapsulates core PSGI server behavior
4 1     1   89433 use MooseX::Declare;
  1         2  
  1         18  
5              
6              
7 1     1   134225 role POEx::Role::PSGIServer {
  1     1   3  
  1     1   32  
  1     1   7970  
  1         3  
  1         10  
  1         3545  
  1         2  
  1         9  
  1         84  
8 1     1   38 use aliased 'POEx::Role::Event';
  1         1  
  1         17  
9              
10             use MooseX::Types::Moose(':all');
11             use POEx::Types::PSGIServer(':all');
12             use POEx::Types(':all');
13             use Moose::Autobox;
14             use HTTP::Message::PSGI;
15             use HTTP::Status qw(status_message);
16             use Plack::Util;
17             use POE::Filter::HTTP::Parser;
18             use POE::Filter::Stream;
19             use POEx::Role::PSGIServer::Streamer;
20             use POEx::Role::PSGIServer::ProxyWriter;
21              
22              
23             has psgi_app => (
24             is => 'ro',
25             isa => CodeRef,
26             writer => 'register_service',
27             );
28              
29              
30             has wheel_flushers => (
31             is => 'ro',
32             traits => ['Hash'],
33             isa => 'HashRef',
34             default => sub { {} },
35             handles => {
36             has_wheel_flusher => 'exists',
37             get_wheel_flusher => 'get',
38             set_wheel_flusher => 'set',
39             clear_wheel_flusher => 'delete',
40             }
41             );
42              
43              
44              
45             around BUILDARGS(ClassName $class: @args) {
46             my $hash = $class->$orig(@args);
47              
48             $hash->{listen_port} ||= delete $hash->{port} || 5000;
49             $hash->{listen_ip} ||= delete $hash->{host} || '0.0.0.0';
50             $hash;
51             }
52              
53              
54             after _start is Event {
55             $self->input_filter(POE::Filter::HTTP::Parser->new(type => 'server'));
56             $self->output_filter(POE::Filter::Stream->new());
57             }
58              
59              
60             method write(PSGIServerContext $c, Str $data) {
61             if($c->{chunked}) {
62             my $len = sprintf "%X", do { use bytes; length($data) };
63             $self->_write($c, "$len\r\n$data\r\n");
64             }
65             else {
66             $self->_write($c, $data);
67             }
68            
69             }
70              
71              
72             method _write(PSGIServerContext $c, Str $data) {
73             $c->{wheel}->put($data);
74             }
75              
76              
77             method close(PSGIServerContext $c) {
78             if($c->{chunked}) {
79             $self->_write($c, "0\r\n\r\n");
80             }
81              
82             $c->{wheel}->flush() while $c->{wheel}->get_driver_out_octets();
83             $c->{wheel}->shutdown_output();
84             $self->delete_wheel((delete $c->{wheel})->ID);
85             }
86              
87              
88             method handle_socket_error(Str $action, Int $code, Str $message, WheelID $id) is Event {
89             $self->delete_wheel($id);
90             }
91              
92              
93             method handle_listen_error(Str $action, Int $code, Str $message, WheelID $id) is Event {
94             die "Failed to '$action' to the specified port. Code: $code, Message: $message";
95             }
96              
97              
98              
99             method process_headers(PSGIServerContext $c, PSGIResponse $response) {
100             my $headers = $response->[1];
101             $headers->keys
102             ->each(
103             sub {
104             my $index = shift;
105             return if $index == $#$headers;
106             my ($k, $v) = ($headers->[$index], $headers->[$index+1]) ;
107             $c->{keep_alive} = 0 if $k eq 'Connection' && $v eq 'close';
108             $c->{explicit_length} = 1 if $k eq 'Content-Length';
109             $self->_write($c, "$k:$v\r\n");
110             }
111             );
112            
113             $c->{chunked} = ($c->{keep_alive} && !$c->{explicit_length});
114             }
115              
116              
117             method http_preamble(PSGIServerContext $c, PSGIResponse $response) {
118             $self->_write($c, "${\ $c->{protocol}} ${\ $response->[0] } ${ \status_message($response->[0]) }\r\n");
119             }
120              
121              
122             method http_body_allowed(PSGIServerContext $c, PSGIResponse $response) returns (Bool) {
123             my $code = $response->[0];
124              
125             my $no_body_allowed = ($c->{request}->method =~ /^head$/i)
126             || ($code < 200)
127             || ($code == 204)
128             || ($code == 304);
129              
130             if ($no_body_allowed) {
131             $self->_write($c, "\r\n");
132             $self->close($c);
133             return Plack::Util::FALSE;
134             }
135              
136             return Plack::Util::TRUE;
137             }
138              
139              
140             method respond(PSGIServerContext $c, PSGIResponse $response) is Event {
141             $self->http_preamble($c, $response);
142             $self->process_headers($c, $response);
143             return unless ($self->http_body_allowed($c, $response));
144              
145            
146             $self->_write($c, "Transfer-Encoding: chunked\r\n") if $c->{chunked};
147             $self->_write($c, "\r\n");
148            
149             my $body = $response->[2];
150             if ($body) {
151             # If we have a real filehandle, build a Streamer
152             if (Plack::Util::is_real_fh($body)) {
153             # flush and destroy the old wheel, since the Streamer will build a new one
154             $c->{wheel}->flush();
155             $self->delete_wheel($c->{wheel}->ID);
156             my $handle = (delete $c->{wheel})->get_input_handle();
157             my $streamer = POEx::Role::PSGIServer::Streamer->new(
158             input_handle => $body,
159             output_handle => $handle,
160             server_context => $c,
161             );
162             }
163             # If we don't just iterate the lines
164             else {
165             Plack::Util::foreach($body, sub{$self->write($c, @_)});
166             $self->close($c);
167             }
168              
169             return;
170             }
171              
172             # If there was no body, we need to build a push writer
173             return $self->generate_push_writer($c);
174             }
175              
176              
177             method generate_push_writer(PSGIServerContext $c) returns (Object) {
178             return POEx::Role::PSGIServer::ProxyWriter->new(server_context => $c, proxied => $self);
179             }
180              
181              
182             method generate_psgi_env(PSGIServerContext $c) returns (HashRef) {
183             return req_to_psgi(
184             $c->{request},
185             SERVER_NAME => $self->listen_ip,
186             SERVER_PORT => $self->listen_port,
187             SERVER_PROTOCOL => $c->{protocol},
188             'psgi.streaming' => Plack::Util::TRUE,
189             'psgi.nonblocking' => Plack::Util::TRUE,
190             'psgi.runonce' => Plack::Util::FALSE,
191             );
192             }
193              
194              
195             method build_server_context(HTTPRequest $req, WheelID $wheel_id) returns (PSGIServerContext) {
196             my $version = $req->header('X-HTTP-Version') || '0.9';
197             my $protocol = "HTTP/$version";
198             my $connection = $req->header('Connection') || '';
199             my $keep_alive = ($version eq '1.1' && $connection ne 'close');
200            
201             my $context = {
202             request => $req,
203             wheel => $self->get_wheel($wheel_id),
204             version => $version,
205             protocol => $protocol,
206             connection => $connection,
207             keep_alive => $keep_alive,
208             explicit_length => 0,
209             };
210              
211             return $context;
212             }
213              
214              
215             method handle_inbound_data(HTTPRequest $req, WheelID $wheel_id) is Event {
216             my $context = $self->build_server_context($req, $wheel_id);
217             my $env = $self->generate_psgi_env($context);
218             my $response = Plack::Util::run_app($self->psgi_app, $env);
219              
220             if (ref($response) eq 'CODE') {
221             $response->(sub { $self->respond($context, @_) });
222             }
223             else {
224             $self->yield('respond', $context, $response);
225             }
226             }
227              
228              
229             method run(CodeRef $app) {
230             $self->register_service($app);
231             POE::Kernel->run();
232             }
233              
234             method handle_on_flushed(WheelID $id) is Event {
235             if ($self->has_wheel_flusher($id)) {
236             $self->get_wheel_flusher($id)->();
237             }
238             1;
239             }
240              
241             after delete_wheel(WheelID $id) {
242             $self->clear_wheel_flusher($id);
243             }
244              
245             with 'POEx::Role::TCPServer' => {
246             -excludes => [
247             qw/handle_socket_error handle_listen_error handle_on_flushed/
248             ]
249             };
250             }
251              
252             __END__
253              
254             =pod
255              
256             =head1 NAME
257              
258             POEx::Role::PSGIServer - (DEPRECATED) Encapsulates core PSGI server behavior
259              
260             =head1 VERSION
261              
262             version 1.150280
263              
264             =head1 SYNOPSIS
265              
266             use MooseX::Declare;
267             class MyServer with POEx::Role::PSGIServer { }
268              
269             MyServer->new()->run($some_psgi_app);
270              
271             =head1 DESCRIPTION
272              
273             This module has been deprecated.
274              
275             POEx::Role::PSGIServer encapsulates the core L<PSGI> server behaviors into an easy to consume and extend role. It is based on previous POEx work such as POEx::Role::TCPServer which provides basic TCP socket multiplexing via POE::Wheel::SocketFactory and POE::Wheel::ReadWrite, and POEx::Role::SessionInstantiation which transforms plain Moose objects into POE sessions.
276              
277             =head2 RATIONALE
278              
279             This Role has its roots firmly planted in POE::Component::Server::PSGI which provided the initial seed with the layout and logic of the basic server. Unfortunately, POE::Component::Server::PSGI didn't provide any mechnism for extension. The main goal of this Role is to provide as many extension points as possible. The secondary goal is to provide a more reasonable abstraction for several key pieces of the stack for streaming, and push writing.
280              
281             =head1 CLASS_METHODS
282              
283             =head2 around BUILDARGS
284              
285             (ClassName $class: @args)
286              
287             BUILDARGS is wrapped to translate from the expected Plack::Handler interface to POEx::Role::TCPServer's expected interface.
288              
289             =head1 PUBLIC_ATTRIBUTES
290              
291             =head2 psgi_app
292              
293             is: ro, isa: CodeRef, writer: register_service
294              
295             This attribute stores the PSGI application to be run from this server. A writer method is provided to match the expected Plack::Handler interface
296              
297             =head1 PROTECTED_ATTRIBUTES
298              
299             =head2 wheel_flushers
300              
301             is: ro, isa: HashRef,
302             exists : has_wheel_flusher,
303             get : get_wheel_flusher,
304             set : set_wheel_flusher,
305             delete : clear_wheel_flusher
306              
307             This attribute stores coderefs to be called on a wheel's flush event
308             (necessary to properly handle poll_cb)
309              
310             =head1 PUBLIC_METHODS
311              
312             =head2 run
313              
314             (CodeRef $app)
315              
316             run is provided to complete the Plack::Handler interface and allow the server to be executed with the provided psgi app
317              
318             =head1 PROTECTED_METHODS
319              
320             =head2 after _start
321              
322             is Event
323              
324             _start is advised to supply the proper input (HTTP::Parser) and output (Stream) filters.
325              
326             =head2 write
327              
328             (PSGIServerContext $c, Str $data)
329              
330             write will alter the data if necessary for a chunked transfer encoded response and send it to the output buffer for the current context
331              
332             =head2 close
333              
334             (PSGIServerContext $c)
335              
336             close will close the connection for the current context, but flushing the output buffer first
337              
338             =head2 handle_socket_error
339              
340             (Str $action, Int $code, Str $message, WheelID $id) is Event
341              
342             handle_socket_error overridden from POEx::Role::TCPServer to delete the wheel when a socket level error happens. If more intelligent actions are required, please feel free to exclude this method and provide your own implementation
343              
344             =head2 handle_listen_error
345              
346             (Str $action, Int $code, Str $message, WheelID $id) is Event
347              
348             handle_listen_error is overridden from POEx::Role::TCPServer to die when the SocketFactory fails to listen to the provided address/port. If more intelligent actions are required, please feel free to exclude this method and provide your own implementation
349              
350             =head2 process_headers
351              
352             (PSGIServerContext $c, PSGIResponse $response)
353              
354             process_headers takes the headers from the PSGIResponse, and sends it to the output buffer for the current context. This method also determines if the response body should be transfer encoded as chunked based on the Connection and Content-Length headers.
355              
356             =head2 http_preamble
357              
358             (PSGIServerContext $c, PSGIResponse $response)
359              
360             http_preamble sends the first line of the HTTP response to the output buffer of the current context
361              
362             =head2 http_body_allowed
363              
364             (PSGIServerContext $c, PSGIResponse $response) returns (Bool)
365              
366             http_body_allowed checks the result code from the PSGIResponse to determine if a body should be allowed to be returned. Returns true if a body is allowed, false otherwise.
367              
368             =head2 respond
369              
370             (PSGIServerContext $c, PSGIResponse $response) is Event
371              
372             respond processes the PSGIResponse to write out a valid HTTP response. If the body of the response is a real filehandle, it will be streamed appropriately via L<POEx::Role::PSGIServer::Streamer>. If not, it will be iterated with which ever appropriate interface to the output buffer. If no body is provided, L</generate_push_writer> is called to generate an appropriate object for use in push responses.
373              
374             =head2 generate_push_writer
375              
376             (PSGIServerContext $c) returns (Object)
377              
378             generate_push_writer by default constructs and returns a L<POEx::Role::PSGIServer::ProxyWriter> object that implements the push-object interface defined in L<PSGI>
379              
380             =head2 generate_psgi_env
381              
382             (PSGIServerContext $c) returns (HashRef)
383              
384             generate_psgi_env returns a suitable HashRef as defined by L<PSGI> for application use. If additional application specific items need to be added to the hash, please feel free to advise this method
385              
386             =head2 build_server_context
387              
388             (HTTPRequest $req, WheelID $wheel_id) returns (PSGIServerContext)
389              
390             build_server_context constructs and returns a L<POEx::Types::PSGIServer/PSGIServerContext> for the current connection
391              
392             =head2 handle_inbound_data
393              
394             (HTTPRequest $req, WheelID $wheel_id) is Event
395              
396             handle_inbound_data implements the required method for POEx::Role::TCPServer. It builds a server context, generates a psgi env hash, runs the psgi app, and then responds to the client
397              
398             =head1 PRIVATE_METHODS
399              
400             =head2 _write
401              
402             (PSGIServerContext $c, Str $data)
403              
404             _write accesses the proper wheel for this context and puts the supplied data into the output buffer
405              
406             =head1 AUTHOR
407              
408             Nicholas Perez <nperez@cpan.org>
409              
410             =head1 COPYRIGHT AND LICENSE
411              
412             This software is copyright (c) 2015 by Infinity Interactive.
413              
414             This is free software; you can redistribute it and/or modify it under
415             the same terms as the Perl 5 programming language system itself.
416              
417             =cut