File Coverage

blib/lib/Catalyst/Engine/PSGI.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Catalyst::Engine::PSGI;
2              
3 1     1   6 use strict;
  1         2  
  1         123  
4 1     1   32 use 5.008_001;
  1         5  
  1         59  
5             our $VERSION = '0.13';
6              
7 1     1   10947 use Moose;
  0            
  0            
8             extends 'Catalyst::Engine';
9              
10             {
11             # Temporary hack to see if there are better ways like TraitFor,
12             # but without requiring downstream changes.
13             sub Catalyst::Request::env {
14             my $req = shift;
15             $req->{_psgi_env} = shift if @_;
16             $req->{_psgi_env};
17             }
18             }
19              
20             use Scalar::Util qw(blessed);
21             use URI;
22             use Catalyst::Controller::Metal;
23              
24             # This is what Catalyst does to decode path. Not compatible to CGI RFC 3875
25             my %reserved = map { sprintf('%02x', ord($_)) => 1 } split //, $URI::reserved;
26             sub _uri_safe_unescape {
27             my ($s) = @_;
28             $s =~ s/%([a-fA-F0-9]{2})/$reserved{lc($1)} ? "%$1" : pack('C', hex($1))/ge;
29             $s
30             }
31              
32             sub prepare_connection {
33             my ( $self, $c ) = @_;
34              
35             my $request = $c->request;
36             my $env = $self->env;
37              
38             $request->env($env);
39             $request->address( $env->{REMOTE_ADDR} );
40             $request->hostname( $env->{REMOTE_HOST} ) if exists $env->{REMOTE_HOST};
41             $request->protocol( $env->{SERVER_PROTOCOL} );
42             $request->user( $env->{REMOTE_USER} ); # XXX: Deprecated. See Catalyst::Request for removal information
43             $request->remote_user( $env->{REMOTE_USER} );
44             $request->method( $env->{REQUEST_METHOD} );
45              
46             $request->secure( $env->{'psgi.url_scheme'} eq 'https' );
47             }
48              
49             sub prepare_headers {
50             my ( $self, $c ) = @_;
51              
52             my $env = $c->request->env;
53             my $headers = $c->request->headers;
54             foreach my $header ( keys %$env ) {
55             next unless $header =~ /^(HTTP|CONTENT|COOKIE)/i;
56             ( my $field = $header ) =~ s/^HTTPS?_//;
57             $field =~ tr/_/-/;
58             $headers->header( $field => $env->{$header} );
59             }
60             }
61              
62             sub prepare_path {
63             my ( $self, $c ) = @_;
64              
65             my $env = $c->request->env;
66              
67             my $scheme = $c->request->secure ? 'https' : 'http';
68             my $host = $env->{HTTP_HOST} || $env->{SERVER_NAME};
69             my $port = $env->{SERVER_PORT} || 80;
70             my $base_path = $env->{SCRIPT_NAME} || "/";
71              
72             # set the request URI
73             my $req_uri = $env->{REQUEST_URI};
74             $req_uri =~ s/\?.*$//;
75             my $path = _uri_safe_unescape($req_uri);
76             if ($path eq $base_path) {
77             $path .= "/"; # To fool catalyst a bit
78             }
79             $path =~ s{^/+}{};
80              
81             # Using URI directly is way too slow, so we construct the URLs manually
82             my $uri_class = "URI::$scheme";
83              
84             # HTTP_HOST will include the port even if it's 80/443
85             $host =~ s/:(?:80|443)$//;
86              
87             if ( $port !~ /^(?:80|443)$/ && $host !~ /:/ ) {
88             $host .= ":$port";
89             }
90              
91             # Escape the path
92             $path =~ s/([^$URI::uric])/$URI::Escape::escapes{$1}/go;
93             $path =~ s/\?/%3F/g; # STUPID STUPID SPECIAL CASE
94              
95             my $query = $env->{QUERY_STRING} ? '?' . $env->{QUERY_STRING} : '';
96             my $uri = $scheme . '://' . $host . '/' . $path . $query;
97              
98             $c->request->uri( bless \$uri, $uri_class );
99              
100             # set the base URI
101             # base must end in a slash
102             $base_path .= '/' unless $base_path =~ m{/$};
103              
104             my $base_uri = $scheme . '://' . $host . $base_path;
105              
106             $c->request->base( bless \$base_uri, $uri_class );
107             }
108              
109             around prepare_query_parameters => sub {
110             my $orig = shift;
111             my ( $self, $c ) = @_;
112              
113             if ( my $qs = $c->request->env->{QUERY_STRING} ) {
114             $self->$orig( $c, $qs );
115             }
116             };
117              
118             sub prepare_request {
119             my ( $self, $c, %args ) = @_;
120              
121             if ( $args{env} ) {
122             $self->env( $args{env} );
123             }
124              
125             $self->{buffer} = '';
126             }
127              
128             sub write {
129             my($self, $c, $buffer) = @_;
130             $self->{buffer} .= $buffer if defined $buffer;
131             }
132              
133             sub finalize_body {
134             # do nothing since we serve content
135             }
136              
137             sub read_chunk {
138             my($self, $c) = (shift, shift);
139             $self->env->{'psgi.input'}->read(@_);
140             }
141              
142             sub run {
143             my($self, $class, $env) = @_;
144              
145             # short circuit with Metal
146             for my $metal (Catalyst::Controller::Metal->metals_for($class)) {
147             my $res = $metal->call($env);
148             if (defined $res && !(ref $res eq 'ARRAY' && $res->[0] == 404)) {
149             return $res;
150             }
151             }
152              
153             # what Catalyst->handle_request does
154             my $status = -1;
155             my $c;
156             eval {
157             $c = $class->prepare(env => $env);
158             $c->dispatch;
159             $status = $c->finalize;
160             };
161              
162             # clear the $env ref to avoid leaks
163             $self->env(undef);
164              
165             if (my $error = $@) {
166             chomp $error;
167             $class->log->error(qq/Caught exception in engine "$error"/);
168             }
169              
170             if (my $coderef = $class->log->can('_flush')){
171             $class->log->$coderef();
172             }
173              
174             return [ 500, [ 'Content-Type' => 'text/plain', 'Content-Length' => 11 ], [ 'Bad request' ] ]
175             unless $c;
176              
177             my $body = $c->res->body;
178             $body = '' unless defined $body;
179             if (!ref $body && $body eq '' && $self->{buffer}) {
180             $body = [ $self->{buffer} ];
181             } elsif (ref($body) eq 'GLOB' || (Scalar::Util::blessed($body) && $body->can('getline'))) {
182             # $body is FH
183             } elsif (ref($body) eq 'CODE') {
184             return $body;
185             } else {
186             $body = [ $body ];
187             }
188              
189             my $headers = [];
190             $c->res->headers->scan(sub { my($k, $v) = @_; push @$headers, $k, _escape($v) });
191             return [ $c->res->status, $headers, $body ];
192             }
193              
194             sub _escape {
195             local $_ = shift;
196             s/(\r?\n)+/ /g;
197             s/ +/ /g;
198             return $_;
199             }
200              
201             no Moose;
202              
203             1;
204             __END__
205              
206             =encoding utf-8
207              
208             =for stopwords
209              
210             =head1 NAME
211              
212             Catalyst::Engine::PSGI - PSGI engine for Catalyst
213              
214             =head1 SYNOPSIS
215              
216             # app.psgi
217             use strict;
218             use MyApp;
219              
220             MyApp->setup_engine('PSGI');
221             my $app = sub { MyApp->run(@_) };
222              
223             =head1 DESCRIPTION
224              
225             Catalyst::Engine::PSGI is a Catalyst Engine that adapts Catalyst into the PSGI gateway protocol.
226              
227             =head1 COMPATIBILITY
228              
229             =over 4
230              
231             =item *
232              
233             Currently this engine works with Catalyst 5.8 (Catamoose) or newer.
234              
235             =item *
236              
237             Your application is supposed to work with any PSGI servers without any
238             code modifications, but if your application uses C<< $c->res->write >>
239             to do streaming write, this engine will buffer the ouput until your
240             app finishes.
241              
242             To do real streaming with this engine, you should implement an
243             IO::Handle-like object that responds to C<getline> method that returns
244             chunk or undef when done, and set that object to C<< $c->res->body >>.
245              
246             Alternatively, it is possible to set the body to a code reference,
247             which will be used to stream content as documented in the
248             L<PSGI spec|PSGI/Delayed_Reponse_and_Streaming_Body>.
249              
250             =item *
251              
252             When your application runs behind the frontend proxy like nginx or
253             lighttpd, this Catalyst engine doesn't automatically recognize the
254             incoming headers like C<X-Forwarded-For>, because respecting these
255             headers by default causes a potential security issue.
256              
257             You have to enable L<Plack::Middleware::ReverseProxy> or
258             L<Plack::Middleware::ForwardedHeaders> to automatically promote those
259             forwarded headers into C<REMOTE_ADDR> hence IP address of the request.
260              
261             ReverseProxy middleware is pretty simple and has no configuration
262             while ForwardedHeaders allows you to configure which upstream host to
263             trust, etc.
264              
265             =back
266              
267             =head1 AUTHOR
268              
269             Tatsuhiko Miyagawa E<lt>miyagawa@bulknews.netE<gt>
270              
271             Most of the code is taken and modified from Catalyst::Engine::CGI.
272              
273             =head1 LICENSE
274              
275             This library is free software; you can redistribute it and/or modify
276             it under the same terms as Perl itself.
277              
278             =head1 SEE ALSO
279              
280             I<Catalyst::Engine> L<PSGI> I<Plack>
281              
282             =cut