File Coverage

blib/lib/Mojolicious/Plugin/CGI.pm
Criterion Covered Total %
statement 171 181 94.4
branch 71 94 75.5
condition 41 62 66.1
subroutine 30 30 100.0
pod 1 1 100.0
total 314 368 85.3


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::CGI;
2 34     34   75480 use Mojo::Base 'Mojolicious::Plugin';
  34         90  
  34         234  
3              
4 34     34   6154 use File::Basename;
  34         78  
  34         2105  
5 34     34   185 use File::Spec;
  34         78  
  34         921  
6 34     34   14471 use IO::Pipely 'pipely';
  34         61172  
  34         1991  
7 34     34   247 use Mojo::Util qw(b64_decode encode);
  34         76  
  34         1728  
8 34     34   198 use POSIX 'WNOHANG';
  34         84  
  34         243  
9 34     34   15127 use Perl::OSType 'is_os_type';
  34         11045  
  34         2068  
10 34     34   228 use Socket qw(AF_INET inet_aton);
  34         83  
  34         1464  
11 34     34   12710 use Sys::Hostname;
  34         27702  
  34         1951  
12              
13 34   50 34   233 use constant CHECK_CHILD_INTERVAL => $ENV{CHECK_CHILD_INTERVAL} || 0.01;
  34         69  
  34         2798  
14 34     34   207 use constant DEBUG => $ENV{MOJO_PLUGIN_CGI_DEBUG};
  34         66  
  34         1802  
15 34     34   191 use constant IS_WINDOWS => is_os_type('Windows');
  34         70  
  34         124  
16 34     34   2225 use constant READ => 0;
  34         73  
  34         1337  
17 34     34   177 use constant WRITE => 1;
  34         65  
  34         77290  
18              
19             our $VERSION = '0.38';
20             our %ORIGINAL_ENV = %ENV;
21              
22             has env => sub { +{%ORIGINAL_ENV} };
23              
24             sub register {
25 43     43 1 24022 my ($self, $app, $args) = @_;
26 43   100     269 my $pids = $app->{'mojolicious_plugin_cgi.pids'} ||= {};
27              
28 43 100       210 $args = {route => shift @$args, script => shift @$args} if ref $args eq 'ARRAY';
29 43   66     250 $args->{env} ||= $self->env;
30 43 50       193 $args->{run} = delete $args->{script} if ref $args->{script} eq 'CODE';
31 43         94 $args->{pids} = $pids;
32              
33 43 100   9   243 $app->helper('cgi.run' => sub { _run($args, @_) }) unless $app->renderer->helpers->{'cgi.run'};
  9         62497  
34             $app->{'mojolicious_plugin_cgi.tid'}
35 43   66 1139   1964 ||= Mojo::IOLoop->recurring(CHECK_CHILD_INTERVAL, sub { local ($?, $!); _waitpids($pids); });
  1139         10182947  
  1139         5013  
36              
37 43 100 66     3936 if ($args->{support_semicolon_in_query_string}
38             and !$app->{'mojolicious_plugin_cgi.before_dispatch'}++)
39             {
40             $app->hook(
41             before_dispatch => sub {
42 2     2   17810 $_[0]->stash('cgi.query_string' => $_[0]->req->url->query->to_string);
43             }
44 2         12 );
45             }
46              
47 43 100       183 return unless $args->{route}; # just register the helper
48 37 50 66     231 die "Neither 'run', nor 'script' is specified." unless $args->{run} or $args->{script};
49             $args->{route} = $app->routes->any("$args->{route}/*path_info", {path_info => ''})
50 37 50       230 unless ref $args->{route};
51 37 100 33     16959 $args->{script} = File::Spec->rel2abs($args->{script}) || $args->{script} if $args->{script};
52 37     47   270 $args->{route}->to(cb => sub { _run($args, @_) });
  47         394163  
53             }
54              
55             sub _child {
56 20     20   483 my ($c, $args, $stdin, $stdout, $stderr) = @_;
57 20 100       941 my @STDERR = @$stderr ? ('>&', fileno $stderr->[WRITE]) : ('>>', $args->{errlog});
58              
59 20         1389 Mojo::IOLoop->reset;
60 20         44472 warn "[CGI:$args->{name}:$$] <<< (@{[$stdin->slurp]})\n" if DEBUG;
61 20 100 50     449 open STDIN, '<', $stdin->path or die "STDIN @{[$stdin->path]}: $!" if -s $stdin->path;
  0         0  
62 20 50       2556 open STDERR, $STDERR[0], $STDERR[1] or die "STDERR: @$stderr: $!";
63 20 50       302 open STDOUT, '>&', fileno $stdout->[WRITE] or die "STDOUT: $!";
64 20         223 select STDERR;
65 20         271 $| = 1;
66 20         204 select STDOUT;
67 20         98 $| = 1;
68              
69 20         333 %ENV = _emulate_environment($c, $args);
70             $args->{run} ? $args->{run}->($c) : exec $args->{script}
71 20 50 50     0 || die "Could not execute $args->{script}: $!";
72              
73 0         0 eval { POSIX::_exit($!) } unless IS_WINDOWS;
  0         0  
74 0         0 eval { CORE::kill KILL => $$ };
  0         0  
75 0         0 exit $!;
76             }
77              
78             sub _emulate_environment {
79 20     20   124 my ($c, $args) = @_;
80 20         292 my $tx = $c->tx;
81 20         416 my $req = $tx->req;
82 20         395 my $headers = $req->headers;
83 20 100       319 my $content_length = $req->content->is_multipart ? $req->body_size : $headers->content_length;
84 20         1219 my %env_headers = (HTTP_COOKIE => '', HTTP_REFERER => '');
85 20         95 my ($remote_user, $script_name);
86              
87 20         76 for my $name (@{$headers->names}) {
  20         357  
88 86         2083 my $key = uc "http_$name";
89 86         546 $key =~ s!\W!_!g;
90 86         721 $env_headers{$key} = $headers->header($name);
91             }
92              
93 20 100       473 if (my $userinfo = $c->req->url->to_abs->userinfo) {
    50          
94 2 50       1032 $remote_user = $userinfo =~ /([^:]+)/ ? $1 : '';
95             }
96             elsif (my $authenticate = $headers->authorization) {
97 0 0       0 $remote_user = $authenticate =~ /Basic\s+(.*)/ ? b64_decode $1 : '';
98 0 0       0 $remote_user = $remote_user =~ /([^:]+)/ ? $1 : '';
99             }
100              
101 20 100       9809 if ($args->{route}) {
    50          
102 17         426 $script_name = $c->url_for($args->{route}->name, {path_info => ''})->path->to_string;
103             }
104             elsif (my $name = $c->stash('script_name')) {
105 3         70 my $name = quotemeta $name;
106 3 50       17 $script_name = $c->req->url->path =~ m!^(.*?/$name)! ? $1 : $c->stash('script_name');
107             }
108              
109             return (
110 20         1013 %{$args->{env}},
111             CONTENT_LENGTH => $content_length || 0,
112             CONTENT_TYPE => $headers->content_type || '',
113             GATEWAY_INTERFACE => 'CGI/1.1',
114             HTTPS => $req->is_secure ? 'YES' : 'NO',
115             %env_headers,
116             PATH_INFO => '/' . encode('UTF-8', $c->stash('path_info') // ''),
117             QUERY_STRING => $c->stash('cgi.query_string') || $req->url->query->to_string,
118             REMOTE_ADDR => $tx->remote_address,
119             REMOTE_HOST => gethostbyaddr(inet_aton($tx->remote_address || '127.0.0.1'), AF_INET) || '',
120             REMOTE_PORT => $tx->remote_port,
121             REMOTE_USER => $remote_user || '',
122             REQUEST_METHOD => $req->method,
123             SCRIPT_FILENAME => $args->{script} || '',
124             SCRIPT_NAME => $script_name || $args->{name},
125 20 50 100     16321 SERVER_ADMIN => $ENV{USER} || '',
    50 100        
      50        
      100        
      50        
      100        
      50        
      33        
      50        
126             SERVER_NAME => hostname,
127             SERVER_PORT => $tx->local_port,
128             SERVER_PROTOCOL => $req->is_secure ? 'HTTPS' : 'HTTP', # TODO: Version is missing
129             SERVER_SOFTWARE => __PACKAGE__,
130             );
131             }
132              
133             sub _run {
134 56     56   459 my ($defaults, $c) = (shift, shift);
135 56 50       310 my $args = @_ ? @_ > 1 ? {@_} : {%{$_[0]}} : {};
  0 100       0  
136 56   66     477 my $before = $args->{before} || $defaults->{before};
137 56         228 my $stdin = _stdin($c);
138 56         50096 my @stdout = pipely;
139 56         4018 my ($pid, $log_key, @stderr);
140              
141 56   100     1172 $args->{$_} ||= $defaults->{$_} for qw(env errlog route run script);
142 56 100       3285 $args->{name} = $args->{run} ? "$args->{run}" : basename $args->{script};
143 56 100       263 $c->$before($args) if $before;
144 56 100       537 @stderr = (pipely) unless $args->{errlog};
145 56 50       74620 defined($pid = fork) or die "[CGI:$args->{name}] fork failed: $!";
146 56 100       2020 _child($c, $args, $stdin, \@stdout, \@stderr) unless $pid;
147 36         1376 $args->{pids}{$pid} = $args->{name};
148 36         533 $log_key = "CGI:$args->{name}:$pid";
149 36   66     1641 $c->app->log->debug("[$log_key] START @{[$args->{script} || $args->{run}]}");
  36         2714  
150              
151 36         2144 for my $p (\@stdout, \@stderr) {
152 72 100       9615 next unless $p->[READ];
153 69         721 close $p->[WRITE];
154 69         1414 $p->[READ] = Mojo::IOLoop::Stream->new($p->[READ])->timeout(0);
155 69         5295 Mojo::IOLoop->stream($p->[READ]);
156             }
157              
158             $c->delay(
159             sub {
160 36     36   20151 my ($delay) = @_;
161 36         273 $c->stash('cgi.pid' => $pid, 'cgi.stdin' => $stdin);
162 36 100       1084 $stderr[READ]->on(read => _stderr_cb($c, $log_key)) if $stderr[READ];
163 36         392 $stdout[READ]->on(read => _stdout_cb($c, $log_key));
164 36         297 $stdout[READ]->on(close => $delay->begin);
165             },
166             sub {
167 36     36   59504 my ($delay) = @_;
168 36         110 my $GUARD = 50;
169 36         87 warn "[CGI:$args->{name}:$pid] Child closed STDOUT\n" if DEBUG;
170 36 50 50     382 unlink $stdin->path or die "Could not remove STDIN @{[$stdin->path]}" if -e $stdin->path;
  0         0  
171 36         3638 local ($?, $!);
172             _waitpids({$pid => $args->{pids}{$pid}})
173 36   66     1101 while $args->{pids}{$pid}
      66        
174             and kill 0, $pid
175             and $GUARD--;
176 36 50       185 $defaults->{pids}{$pid} = $args->{pids}{$pid} if kill 0, $pid;
177 36 100       216 return $c->finish if $c->res->code;
178 3         132 return $c->render(text => "Could not run CGI script ($?, $!).\n", status => 500);
179             },
180 36         6666 );
181             }
182              
183             sub _stderr_cb {
184 33     33   147 my ($c, $log_key) = @_;
185 33         161 my $log = $c->app->log;
186 33         419 my $buf = '';
187              
188             return sub {
189 3     3   19074 my ($stream, $chunk) = @_;
190 3         9 warn "[$log_key] !!! ($chunk)\n" if DEBUG;
191 3         27 $buf .= $chunk;
192 3         99 $log->warn("[$log_key] $1") while $buf =~ s!^(.+)[\r\n]+$!!m;
193 33         598 };
194             }
195              
196             sub _stdout_cb {
197 36     36   159 my ($c, $log_key) = @_;
198 36         181 my $buf = '';
199 36         97 my $headers;
200              
201             return sub {
202 33     33   157837 my ($stream, $chunk) = @_;
203 33         148 warn "[$log_key] >>> ($chunk)\n" if DEBUG;
204              
205             # true if HTTP header has been written to client
206 33 50       237 return $c->write($chunk) if $headers;
207              
208 33         204 $buf .= $chunk;
209              
210             # false until all headers has been read from the CGI script
211 33 50       529 $buf =~ s/^(.*?\x0a\x0d?\x0a\x0d?)//s or return;
212 33         309 $headers = $1;
213              
214 33 100       278 if ($headers =~ /^HTTP/) {
215 6 100       42 $c->res->code($headers =~ m!^HTTP (\d\d\d)! ? $1 : 200);
216 6         231 $c->res->parse($headers);
217             }
218             else {
219 27 100       203 $c->res->code($1) if $headers =~ /^Status: (\d\d\d)/m;
220 27 100       350 $c->res->code($headers =~ /Location:/ ? 302 : 200) unless $c->res->code;
    100          
221 27         1518 $c->res->parse($c->res->get_start_line_chunk(0) . $headers);
222             }
223 33 100       25748 $c->write($buf) if length $buf;
224 36         470 };
225             }
226              
227             sub _stdin {
228 56     56   166 my $c = shift;
229 56         124 my $stdin;
230              
231 56 100       232 if ($c->req->content->is_multipart) {
232 2         108 $stdin = Mojo::Asset::File->new;
233 2         38 $stdin->add_chunk($c->req->build_body);
234             }
235             else {
236 54         1046 $stdin = $c->req->content->asset;
237             }
238              
239 56 100       6220 return $stdin if $stdin->isa('Mojo::Asset::File');
240 54         300 return Mojo::Asset::File->new->add_chunk($stdin->slurp);
241             }
242              
243             sub _waitpids {
244 1175     1175   3052 my $pids = shift;
245              
246 1175         6876 for my $pid (keys %$pids) {
247              
248             # no idea why i need to do this, but it seems like waitpid() below return -1 if not
249 36         603 local $SIG{CHLD} = 'DEFAULT';
250 36 50       1243 next unless waitpid $pid, WNOHANG;
251 36   50     230 my $name = delete $pids->{$pid} || 'unknown';
252 36         186 my ($exit_value, $signal) = ($? >> 8, $? & 127);
253 36         485 warn "[CGI:$name:$pid] Child exit_value=$exit_value ($signal)\n" if DEBUG;
254             }
255             }
256              
257             1;
258              
259             =encoding utf8
260              
261             =head1 NAME
262              
263             Mojolicious::Plugin::CGI - Run CGI script from Mojolicious
264              
265             =head1 VERSION
266              
267             0.38
268              
269             =head1 DESCRIPTION
270              
271             This plugin enable L to run Perl CGI scripts. It does so by forking
272             a new process with a modified environment and reads the STDOUT in a non-blocking
273             manner.
274              
275             =head1 SYNOPSIS
276              
277             =head2 Standard usage
278              
279             use Mojolicious::Lite;
280             plugin CGI => [ "/cgi-bin/script" => "/path/to/cgi/script.pl" ];
281              
282             Using the code above is enough to run C when accessing
283             L.
284              
285             =head2 Complex usage
286              
287             plugin CGI => {
288             # Specify the script and mount point
289             script => "/path/to/cgi/script.pl",
290             route => "/some/route",
291              
292             # %ENV variables visible from inside the CGI script
293             env => {}, # default is \%ENV
294              
295             # Path to where STDERR from cgi script goes
296             errlog => "/path/to/file.log",
297              
298             # The "before" hook is called before script start
299             # It receives a Mojolicious::Controller which can be modified
300             before => sub {
301             my $c = shift;
302             $c->req->url->query->param(a => 123);
303             },
304             };
305              
306             The above contains all the options you can pass on to the plugin.
307              
308             =head2 Helper
309              
310             plugin "CGI";
311              
312             # GET /cgi-bin/some-script.cgi/path/info?x=123
313             get "/cgi-bin/#script_name/*path_info" => {path_info => ''}, sub {
314             my $c = shift;
315             my $name = $c->stash("script_name");
316             $c->cgi->run(script => File::Spec->rel2abs("/path/to/cgi/$name"));
317             };
318              
319             The helper can take most the arguments that L takes, with the
320             exception of C.
321              
322             It is critical that "script_name" and "path_info" is present in
323             L. If the values are extracted directly
324             from the path or set manually does not matter.
325              
326             Note that the helper is registered in all of the examples.
327              
328             =head2 Running code refs
329              
330             plugin CGI => {
331             route => "/some/path",
332             run => sub {
333             my $cgi = CGI->new;
334             # ...
335             }
336             };
337              
338             Instead of calling a script, you can run a code block when accessing the route.
339             This is (pretty much) safe, even if the code block modifies global state,
340             since it runs in a separate fork/process.
341              
342             =head2 Support for semicolon in query string
343              
344             plugin CGI => {
345             support_semicolon_in_query_string => 1,
346             ...
347             };
348              
349             The code above need to be added before other plugins or handler which use
350             L. It will inject a C
351             hook which saves the original QUERY_STRING, before it is split on
352             "&" in L.
353              
354             =head1 ATTRIBUTES
355              
356             =head2 env
357              
358             Holds a hash ref containing the environment variables that should be
359             used when starting the CGI script. Defaults to C<%ENV> when this module
360             was loaded.
361              
362             This plugin will create a set of environment variables depenendent on the
363             request passed in which is according to the CGI spec. In addition to L,
364             these dynamic variables are set:
365              
366             CONTENT_LENGTH, CONTENT_TYPE, HTTPS, PATH, PATH_INFO, QUERY_STRING,
367             REMOTE_ADDR, REMOTE_HOST, REMOTE_PORT, REMOTE_USER, REQUEST_METHOD,
368             SCRIPT_NAME, SERVER_PORT, SERVER_PROTOCOL.
369              
370             Additional static variables:
371              
372             GATEWAY_INTERFACE = "CGI/1.1"
373             SERVER_ADMIN = $ENV{USER}
374             SCRIPT_FILENAME = Script name given as argument to register.
375             SERVER_NAME = Sys::Hostname::hostname()
376             SERVER_SOFTWARE = "Mojolicious::Plugin::CGI"
377              
378             Plus all headers are exposed. Examples:
379              
380             .----------------------------------------.
381             | Header | Variable |
382             |-----------------|----------------------|
383             | Referer | HTTP_REFERER |
384             | User-Agent | HTTP_USER_AGENT |
385             | X-Forwarded-For | HTTP_X_FORWARDED_FOR |
386             '----------------------------------------'
387              
388             =head2 register
389              
390             $self->register($app, [ $route => $script ]);
391             $self->register($app, %args);
392             $self->register($app, \%args);
393              
394             C and L need to exist as keys in C<%args> unless given as plain
395             arguments.
396              
397             C<$route> can be either a plain path or a route object.
398              
399             =head1 COPYRIGHT AND LICENSE
400              
401             Copyright (C) 2014, Jan Henning Thorsen
402              
403             This program is free software, you can redistribute it and/or modify it under
404             the terms of the Artistic License version 2.0.
405              
406             =head1 AUTHOR
407              
408             Jan Henning Thorsen - C
409              
410             =cut