File Coverage

blib/lib/Mojolicious/Plugin/CGI.pm
Criterion Covered Total %
statement 169 179 94.4
branch 71 94 75.5
condition 41 62 66.1
subroutine 29 29 100.0
pod 1 1 100.0
total 311 365 85.2


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::CGI;
2 34     34   130244 use Mojo::Base 'Mojolicious::Plugin';
  34         72  
  34         245  
3              
4 34     34   7173 use File::Basename;
  34         73  
  34         2409  
5 34     34   207 use File::Spec;
  34         66  
  34         837  
6 34     34   17922 use IO::Pipely 'pipely';
  34         84740  
  34         2357  
7 34     34   267 use Mojo::Util qw(b64_decode encode);
  34         83  
  34         1789  
8 34     34   208 use POSIX 'WNOHANG';
  34         74  
  34         279  
9 34     34   17641 use Perl::OSType 'is_os_type';
  34         14314  
  34         2210  
10 34     34   255 use Socket qw(AF_INET inet_aton);
  34         76  
  34         1963  
11 34     34   16705 use Sys::Hostname;
  34         37548  
  34         2918  
12              
13 34   50 34   274 use constant CHECK_CHILD_INTERVAL => $ENV{CHECK_CHILD_INTERVAL} || 0.01;
  34         76  
  34         3518  
14 34     34   237 use constant DEBUG => $ENV{MOJO_PLUGIN_CGI_DEBUG};
  34         86  
  34         2057  
15 34     34   204 use constant IS_WINDOWS => is_os_type('Windows');
  34         71  
  34         139  
16 34     34   2262 use constant READ => 0;
  34         318  
  34         1623  
17 34     34   199 use constant WRITE => 1;
  34         64  
  34         103259  
18              
19             our $VERSION = '0.40';
20             our %ORIGINAL_ENV = %ENV;
21              
22             has env => sub { +{%ORIGINAL_ENV} };
23              
24             sub register {
25 43     43 1 50759 my ($self, $app, $args) = @_;
26 43   100     328 my $pids = $app->{'mojolicious_plugin_cgi.pids'} ||= {};
27              
28 43 100       281 $args = {route => shift @$args, script => shift @$args} if ref $args eq 'ARRAY';
29 43   66     259 $args->{env} ||= $self->env;
30 43 50       157 $args->{run} = delete $args->{script} if ref $args->{script} eq 'CODE';
31 43         107 $args->{pids} = $pids;
32              
33 43 100   9   278 $app->helper('cgi.run' => sub { _run($args, @_) }) unless $app->renderer->helpers->{'cgi.run'};
  9         101274  
34             $app->{'mojolicious_plugin_cgi.tid'}
35 43   66 1684   16677 ||= Mojo::IOLoop->recurring(CHECK_CHILD_INTERVAL, sub { local ($?, $!); _waitpids($pids); });
  1684         15349676  
  1684         6754  
36              
37 43 100 66     4632 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   4578 $_[0]->stash('cgi.query_string' => $_[0]->req->url->query->to_string);
43             }
44 2         18 );
45             }
46              
47 43 100       197 return unless $args->{route}; # just register the helper
48 37 50 66     207 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       240 unless ref $args->{route};
51 37 100 33     21916 $args->{script} = File::Spec->rel2abs($args->{script}) || $args->{script} if $args->{script};
52 37     47   363 $args->{route}->to(cb => sub { _run($args, @_) });
  47         273282  
53             }
54              
55             sub _child {
56 20     20   911 my ($c, $args, $stdin, $stdout, $stderr) = @_;
57 20 100       1515 my @STDERR = @$stderr ? ('>&', fileno $stderr->[WRITE]) : ('>>', $args->{errlog});
58              
59 20         2161 Mojo::IOLoop->reset;
60 20         68650 warn "[CGI:$args->{name}:$$] <<< (@{[$stdin->slurp]})\n" if DEBUG;
61 20 100 50     965 open STDIN, '<', $stdin->path or die "STDIN @{[$stdin->path]}: $!" if -s $stdin->path;
  0         0  
62 20 50       4367 open STDERR, $STDERR[0], $STDERR[1] or die "STDERR: @$stderr: $!";
63 20 50       799 open STDOUT, '>&', fileno $stdout->[WRITE] or die "STDOUT: $!";
64 20         411 select STDERR;
65 20         458 $| = 1;
66 20         329 select STDOUT;
67 20         164 $| = 1;
68              
69 20         425 %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   178 my ($c, $args) = @_;
80 20         609 my $tx = $c->tx;
81 20         503 my $req = $tx->req;
82 20         473 my $headers = $req->headers;
83 20 100       647 my $content_length = $req->content->is_multipart ? $req->body_size : $headers->content_length;
84 20         1724 my %env_headers = (HTTP_COOKIE => '', HTTP_REFERER => '');
85 20         154 my ($remote_user, $script_name);
86              
87 20         96 for my $name (@{$headers->names}) {
  20         716  
88 86         2829 my $key = uc "http_$name";
89 86         804 $key =~ s!\W!_!g;
90 86         523 $env_headers{$key} = $headers->header($name);
91             }
92              
93 20 100       947 if (my $userinfo = $c->req->url->to_abs->userinfo) {
    50          
94 2 50       1686 $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       14852 if ($args->{route}) {
    50          
102 17         771 $script_name = $c->url_for($args->{route}->name, {path_info => ''})->path->to_string;
103             }
104             elsif (my $name = $c->stash('script_name')) {
105 3         181 my $name = quotemeta $name;
106 3 50       42 $script_name = $c->req->url->path =~ m!^(.*?/$name)! ? $1 : $c->stash('script_name');
107             }
108              
109             return (
110 20         1904 %{$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     30647 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   243 my ($defaults, $c) = (shift, shift);
135 56 50       305 my $args = @_ ? @_ > 1 ? {@_} : {%{$_[0]}} : {};
  0 100       0  
136 56   66     527 my $before = $args->{before} || $defaults->{before};
137 56         274 my $stdin = _stdin($c);
138 56         61619 my @stdout = pipely;
139 56         5565 my ($pid, $log_key, @stderr);
140              
141 56   100     1123 $args->{$_} ||= $defaults->{$_} for qw(env errlog route run script);
142 56 100       5164 $args->{name} = $args->{run} ? "$args->{run}" : basename $args->{script};
143 56 100       270 $c->$before($args) if $before;
144 56 100       576 @stderr = (pipely) unless $args->{errlog};
145 56 50       102798 defined($pid = fork) or die "[CGI:$args->{name}] fork failed: $!";
146 56 100       3506 _child($c, $args, $stdin, \@stdout, \@stderr) unless $pid;
147 36         2461 $args->{pids}{$pid} = $args->{name};
148 36         879 $log_key = "CGI:$args->{name}:$pid";
149 36   66     2720 $c->app->log->debug("[$log_key] START @{[$args->{script} || $args->{run}]}");
  36         4915  
150              
151 36         1663 for my $p (\@stdout, \@stderr) {
152 72 100       14425 next unless $p->[READ];
153 69         1368 close $p->[WRITE];
154 69         2508 $p->[READ] = Mojo::IOLoop::Stream->new($p->[READ])->timeout(0);
155 69         12977 Mojo::IOLoop->stream($p->[READ]);
156             }
157              
158 36         5011 $c->stash('cgi.pid' => $pid, 'cgi.stdin' => $stdin);
159 36         3249 $c->render_later;
160              
161 36 100       1905 $stderr[READ]->on(read => _stderr_cb($c, $log_key)) if $stderr[READ];
162 36         939 $stdout[READ]->on(read => _stdout_cb($c, $log_key));
163             $stdout[READ]->on(close => sub {
164 36     36   69434 my $GUARD = 50;
165 36         182 warn "[CGI:$args->{name}:$pid] Child closed STDOUT\n" if DEBUG;
166 36 50 50     784 unlink $stdin->path or die "Could not remove STDIN @{[$stdin->path]}" if -e $stdin->path;
  0         0  
167 36         4706 local ($?, $!);
168             _waitpids({$pid => $args->{pids}{$pid}})
169 36   66     2403 while $args->{pids}{$pid}
      66        
170             and kill 0, $pid
171             and $GUARD--;
172 36 50       473 $defaults->{pids}{$pid} = $args->{pids}{$pid} if kill 0, $pid;
173 36 100       317 return $c->finish if $c->res->code;
174 3         234 return $c->render(text => "Could not run CGI script ($?, $!).\n", status => 500);
175             }
176 36         1315 );
177             }
178              
179             sub _stderr_cb {
180 33     33   274 my ($c, $log_key) = @_;
181 33         704 my $log = $c->app->log;
182 33         663 my $buf = '';
183              
184             return sub {
185 3     3   11499 my ($stream, $chunk) = @_;
186 3         24 warn "[$log_key] !!! ($chunk)\n" if DEBUG;
187 3         21 $buf .= $chunk;
188 3         189 $log->warn("[$log_key] $1") while $buf =~ s!^(.+)[\r\n]+$!!m;
189 33         1552 };
190             }
191              
192             sub _stdout_cb {
193 36     36   281 my ($c, $log_key) = @_;
194 36         277 my $buf = '';
195 36         237 my $headers;
196              
197             return sub {
198 33     33   177563 my ($stream, $chunk) = @_;
199 33         573 warn "[$log_key] >>> ($chunk)\n" if DEBUG;
200              
201             # true if HTTP header has been written to client
202 33 50       376 return $c->write($chunk) if $headers;
203              
204 33         351 $buf .= $chunk;
205              
206             # false until all headers has been read from the CGI script
207 33 50       1016 $buf =~ s/^(.*?\x0a\x0d?\x0a\x0d?)//s or return;
208 33         610 $headers = $1;
209              
210 33 100       503 if ($headers =~ /^HTTP/) {
211 6 100       96 $c->res->code($headers =~ m!^HTTP (\d\d\d)! ? $1 : 200);
212 6         428 $c->res->parse($headers);
213             }
214             else {
215 27 100       365 $c->res->code($1) if $headers =~ /^Status: (\d\d\d)/m;
216 27 100       654 $c->res->code($headers =~ /Location:/ ? 302 : 200) unless $c->res->code;
    100          
217 27         2961 $c->res->parse($c->res->get_start_line_chunk(0) . $headers);
218             }
219 33 100       47129 $c->write($buf) if length $buf;
220 36         1197 };
221             }
222              
223             sub _stdin {
224 56     56   166 my $c = shift;
225 56         132 my $stdin;
226              
227 56 100       242 if ($c->req->content->is_multipart) {
228 2         60 $stdin = Mojo::Asset::File->new;
229 2         34 $stdin->add_chunk($c->req->build_body);
230             }
231             else {
232 54         1029 $stdin = $c->req->content->asset;
233             }
234              
235 56 100       5797 return $stdin if $stdin->isa('Mojo::Asset::File');
236 54         435 return Mojo::Asset::File->new->add_chunk($stdin->slurp);
237             }
238              
239             sub _waitpids {
240 1720     1720   4030 my $pids = shift;
241              
242 1720         10972 for my $pid (keys %$pids) {
243              
244             # no idea why i need to do this, but it seems like waitpid() below return -1 if not
245 36         1841 local $SIG{CHLD} = 'DEFAULT';
246 36 50       1493 next unless waitpid $pid, WNOHANG;
247 36   50     325 my $name = delete $pids->{$pid} || 'unknown';
248 36         259 my ($exit_value, $signal) = ($? >> 8, $? & 127);
249 36         1012 warn "[CGI:$name:$pid] Child exit_value=$exit_value ($signal)\n" if DEBUG;
250             }
251             }
252              
253             1;
254              
255             =encoding utf8
256              
257             =head1 NAME
258              
259             Mojolicious::Plugin::CGI - Run CGI script from Mojolicious
260              
261             =head1 VERSION
262              
263             0.40
264              
265             =head1 DESCRIPTION
266              
267             This plugin enables L to run Perl CGI scripts. It does so by forking
268             a new process with a modified environment and reads the STDOUT in a non-blocking
269             manner.
270              
271             =head1 SYNOPSIS
272              
273             =head2 Standard usage
274              
275             use Mojolicious::Lite;
276             plugin CGI => [ "/cgi-bin/script" => "/path/to/cgi/script.pl" ];
277              
278             Using the code above is enough to run C when accessing
279             L.
280              
281             =head2 Complex usage
282              
283             plugin CGI => {
284             # Specify the script and mount point
285             script => "/path/to/cgi/script.pl",
286             route => "/some/route",
287              
288             # %ENV variables visible from inside the CGI script
289             env => {}, # default is \%ENV
290              
291             # Path to where STDERR from cgi script goes
292             errlog => "/path/to/file.log",
293              
294             # The "before" hook is called before script start
295             # It receives a Mojolicious::Controller which can be modified
296             before => sub {
297             my $c = shift;
298             $c->req->url->query->param(a => 123);
299             },
300             };
301              
302             The above contains all the options you can pass on to the plugin.
303              
304             =head2 Helper
305              
306             plugin "CGI";
307              
308             # GET /cgi-bin/some-script.cgi/path/info?x=123
309             get "/cgi-bin/#script_name/*path_info" => {path_info => ''}, sub {
310             my $c = shift;
311             my $name = $c->stash("script_name");
312             $c->cgi->run(script => File::Spec->rel2abs("/path/to/cgi/$name"));
313             };
314              
315             The helper can take most of the arguments that L takes, with the
316             exception of C.
317              
318             It is critical that "script_name" and "path_info" is present in
319             L. Whether the values are extracted directly
320             from the path or set manually does not matter.
321              
322             Note that the helper is registered in all of the examples.
323              
324             =head2 Running code refs
325              
326             plugin CGI => {
327             route => "/some/path",
328             run => sub {
329             my $cgi = CGI->new;
330             # ...
331             }
332             };
333              
334             Instead of calling a script, you can run a code block when accessing the route.
335             This is (pretty much) safe, even if the code block modifies global state,
336             since it runs in a separate fork/process.
337              
338             =head2 Support for semicolon in query string
339              
340             plugin CGI => {
341             support_semicolon_in_query_string => 1,
342             ...
343             };
344              
345             The code above needs to be added before other plugins or handlers which use
346             L. It will inject a C
347             hook which saves the original QUERY_STRING, before it is split on
348             "&" in L.
349              
350             =head1 ATTRIBUTES
351              
352             =head2 env
353              
354             Holds a hash ref containing the environment variables that should be
355             used when starting the CGI script. Defaults to C<%ENV> when this module
356             was loaded.
357              
358             This plugin will create a set of environment variables depenendent on the
359             request passed in which is according to the CGI spec. In addition to L,
360             these dynamic variables are set:
361              
362             CONTENT_LENGTH, CONTENT_TYPE, HTTPS, PATH, PATH_INFO, QUERY_STRING,
363             REMOTE_ADDR, REMOTE_HOST, REMOTE_PORT, REMOTE_USER, REQUEST_METHOD,
364             SCRIPT_NAME, SERVER_PORT, SERVER_PROTOCOL.
365              
366             Additional static variables:
367              
368             GATEWAY_INTERFACE = "CGI/1.1"
369             SERVER_ADMIN = $ENV{USER}
370             SCRIPT_FILENAME = Script name given as argument to register.
371             SERVER_NAME = Sys::Hostname::hostname()
372             SERVER_SOFTWARE = "Mojolicious::Plugin::CGI"
373              
374             Plus all headers are exposed. Examples:
375              
376             .----------------------------------------.
377             | Header | Variable |
378             |-----------------|----------------------|
379             | Referer | HTTP_REFERER |
380             | User-Agent | HTTP_USER_AGENT |
381             | X-Forwarded-For | HTTP_X_FORWARDED_FOR |
382             '----------------------------------------'
383              
384             =head2 register
385              
386             $self->register($app, [ $route => $script ]);
387             $self->register($app, %args);
388             $self->register($app, \%args);
389              
390             C and L need to exist as keys in C<%args> unless given as plain
391             arguments.
392              
393             C<$route> can be either a plain path or a route object.
394              
395             =head1 COPYRIGHT AND LICENSE
396              
397             Copyright (C) 2014, Jan Henning Thorsen
398              
399             This program is free software, you can redistribute it and/or modify it under
400             the terms of the Artistic License version 2.0.
401              
402             =head1 AUTHOR
403              
404             Jan Henning Thorsen - C
405              
406             =cut