File Coverage

blib/lib/Catalyst/Engine/PSGI.pm
Criterion Covered Total %
statement 21 112 18.7
branch 0 38 0.0
condition 0 28 0.0
subroutine 7 19 36.8
pod 6 8 75.0
total 34 205 16.5


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