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   101173 use Mojo::Base 'Mojolicious::Plugin';
  34         69  
  34         258  
3              
4 34     34   6641 use File::Basename;
  34         72  
  34         2284  
5 34     34   199 use File::Spec;
  34         75  
  34         822  
6 34     34   16555 use IO::Pipely 'pipely';
  34         85934  
  34         2192  
7 34     34   240 use Mojo::Util qw(b64_decode encode);
  34         75  
  34         1739  
8 34     34   221 use POSIX 'WNOHANG';
  34         62  
  34         279  
9 34     34   16751 use Perl::OSType 'is_os_type';
  34         13604  
  34         2036  
10 34     34   255 use Socket qw(AF_INET inet_aton);
  34         74  
  34         1764  
11 34     34   14367 use Sys::Hostname;
  34         34338  
  34         2449  
12              
13 34   50 34   350 use constant CHECK_CHILD_INTERVAL => $ENV{CHECK_CHILD_INTERVAL} || 0.01;
  34         74  
  34         3230  
14 34     34   202 use constant DEBUG => $ENV{MOJO_PLUGIN_CGI_DEBUG};
  34         66  
  34         2029  
15 34     34   194 use constant IS_WINDOWS => is_os_type('Windows');
  34         64  
  34         123  
16 34     34   2115 use constant READ => 0;
  34         290  
  34         1573  
17 34     34   187 use constant WRITE => 1;
  34         53  
  34         96580  
18              
19             our $VERSION = '0.39';
20             our %ORIGINAL_ENV = %ENV;
21              
22             has env => sub { +{%ORIGINAL_ENV} };
23              
24             sub register {
25 43     43 1 27172 my ($self, $app, $args) = @_;
26 43   100     288 my $pids = $app->{'mojolicious_plugin_cgi.pids'} ||= {};
27              
28 43 100       199 $args = {route => shift @$args, script => shift @$args} if ref $args eq 'ARRAY';
29 43   66     253 $args->{env} ||= $self->env;
30 43 50       146 $args->{run} = delete $args->{script} if ref $args->{script} eq 'CODE';
31 43         87 $args->{pids} = $pids;
32              
33 43 100   9   248 $app->helper('cgi.run' => sub { _run($args, @_) }) unless $app->renderer->helpers->{'cgi.run'};
  9         101488  
34             $app->{'mojolicious_plugin_cgi.tid'}
35 43   66 1478   2114 ||= Mojo::IOLoop->recurring(CHECK_CHILD_INTERVAL, sub { local ($?, $!); _waitpids($pids); });
  1478         13531649  
  1478         5548  
36              
37 43 100 66     4235 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   4502 $_[0]->stash('cgi.query_string' => $_[0]->req->url->query->to_string);
43             }
44 2         24 );
45             }
46              
47 43 100       190 return unless $args->{route}; # just register the helper
48 37 50 66     269 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       404 unless ref $args->{route};
51 37 100 33     19563 $args->{script} = File::Spec->rel2abs($args->{script}) || $args->{script} if $args->{script};
52 37     47   336 $args->{route}->to(cb => sub { _run($args, @_) });
  47         437285  
53             }
54              
55             sub _child {
56 20     20   794 my ($c, $args, $stdin, $stdout, $stderr) = @_;
57 20 100       1283 my @STDERR = @$stderr ? ('>&', fileno $stderr->[WRITE]) : ('>>', $args->{errlog});
58              
59 20         2018 Mojo::IOLoop->reset;
60 20         62975 warn "[CGI:$args->{name}:$$] <<< (@{[$stdin->slurp]})\n" if DEBUG;
61 20 100 50     810 open STDIN, '<', $stdin->path or die "STDIN @{[$stdin->path]}: $!" if -s $stdin->path;
  0         0  
62 20 50       3621 open STDERR, $STDERR[0], $STDERR[1] or die "STDERR: @$stderr: $!";
63 20 50       775 open STDOUT, '>&', fileno $stdout->[WRITE] or die "STDOUT: $!";
64 20         362 select STDERR;
65 20         426 $| = 1;
66 20         381 select STDOUT;
67 20         188 $| = 1;
68              
69 20         349 %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   193 my ($c, $args) = @_;
80 20         524 my $tx = $c->tx;
81 20         693 my $req = $tx->req;
82 20         453 my $headers = $req->headers;
83 20 100       654 my $content_length = $req->content->is_multipart ? $req->body_size : $headers->content_length;
84 20         1773 my %env_headers = (HTTP_COOKIE => '', HTTP_REFERER => '');
85 20         160 my ($remote_user, $script_name);
86              
87 20         110 for my $name (@{$headers->names}) {
  20         646  
88 86         2601 my $key = uc "http_$name";
89 86         690 $key =~ s!\W!_!g;
90 86         558 $env_headers{$key} = $headers->header($name);
91             }
92              
93 20 100       765 if (my $userinfo = $c->req->url->to_abs->userinfo) {
    50          
94 2 50       1718 $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       13862 if ($args->{route}) {
    50          
102 17         671 $script_name = $c->url_for($args->{route}->name, {path_info => ''})->path->to_string;
103             }
104             elsif (my $name = $c->stash('script_name')) {
105 3         125 my $name = quotemeta $name;
106 3 50       28 $script_name = $c->req->url->path =~ m!^(.*?/$name)! ? $1 : $c->stash('script_name');
107             }
108              
109             return (
110 20         1688 %{$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     28893 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   206 my ($defaults, $c) = (shift, shift);
135 56 50       290 my $args = @_ ? @_ > 1 ? {@_} : {%{$_[0]}} : {};
  0 100       0  
136 56   66     617 my $before = $args->{before} || $defaults->{before};
137 56         267 my $stdin = _stdin($c);
138 56         58624 my @stdout = pipely;
139 56         5369 my ($pid, $log_key, @stderr);
140              
141 56   100     1150 $args->{$_} ||= $defaults->{$_} for qw(env errlog route run script);
142 56 100       4706 $args->{name} = $args->{run} ? "$args->{run}" : basename $args->{script};
143 56 100       266 $c->$before($args) if $before;
144 56 100       556 @stderr = (pipely) unless $args->{errlog};
145 56 50       104116 defined($pid = fork) or die "[CGI:$args->{name}] fork failed: $!";
146 56 100       3250 _child($c, $args, $stdin, \@stdout, \@stderr) unless $pid;
147 36         2283 $args->{pids}{$pid} = $args->{name};
148 36         915 $log_key = "CGI:$args->{name}:$pid";
149 36   66     2737 $c->app->log->debug("[$log_key] START @{[$args->{script} || $args->{run}]}");
  36         4647  
150              
151 36         3686 for my $p (\@stdout, \@stderr) {
152 72 100       13403 next unless $p->[READ];
153 69         1265 close $p->[WRITE];
154 69         2265 $p->[READ] = Mojo::IOLoop::Stream->new($p->[READ])->timeout(0);
155 69         8619 Mojo::IOLoop->stream($p->[READ]);
156             }
157              
158 36         4935 $c->stash('cgi.pid' => $pid, 'cgi.stdin' => $stdin);
159 36         3235 $c->render_later;
160              
161 36 100       1725 $stderr[READ]->on(read => _stderr_cb($c, $log_key)) if $stderr[READ];
162 36         875 $stdout[READ]->on(read => _stdout_cb($c, $log_key));
163             $stdout[READ]->on(close => sub {
164 36     36   54574 my $GUARD = 50;
165 36         106 warn "[CGI:$args->{name}:$pid] Child closed STDOUT\n" if DEBUG;
166 36 50 50     622 unlink $stdin->path or die "Could not remove STDIN @{[$stdin->path]}" if -e $stdin->path;
  0         0  
167 36         3651 local ($?, $!);
168             _waitpids({$pid => $args->{pids}{$pid}})
169 36   66     1925 while $args->{pids}{$pid}
      66        
170             and kill 0, $pid
171             and $GUARD--;
172 36 50       362 $defaults->{pids}{$pid} = $args->{pids}{$pid} if kill 0, $pid;
173 36 100       395 return $c->finish if $c->res->code;
174 3         180 return $c->render(text => "Could not run CGI script ($?, $!).\n", status => 500);
175             }
176 36         1108 );
177             }
178              
179             sub _stderr_cb {
180 33     33   309 my ($c, $log_key) = @_;
181 33         663 my $log = $c->app->log;
182 33         575 my $buf = '';
183              
184             return sub {
185 3     3   12234 my ($stream, $chunk) = @_;
186 3         33 warn "[$log_key] !!! ($chunk)\n" if DEBUG;
187 3         24 $buf .= $chunk;
188 3         96 $log->warn("[$log_key] $1") while $buf =~ s!^(.+)[\r\n]+$!!m;
189 33         1586 };
190             }
191              
192             sub _stdout_cb {
193 36     36   276 my ($c, $log_key) = @_;
194 36         281 my $buf = '';
195 36         192 my $headers;
196              
197             return sub {
198 33     33   208561 my ($stream, $chunk) = @_;
199 33         182 warn "[$log_key] >>> ($chunk)\n" if DEBUG;
200              
201             # true if HTTP header has been written to client
202 33 50       316 return $c->write($chunk) if $headers;
203              
204 33         268 $buf .= $chunk;
205              
206             # false until all headers has been read from the CGI script
207 33 50       813 $buf =~ s/^(.*?\x0a\x0d?\x0a\x0d?)//s or return;
208 33         601 $headers = $1;
209              
210 33 100       456 if ($headers =~ /^HTTP/) {
211 6 100       100 $c->res->code($headers =~ m!^HTTP (\d\d\d)! ? $1 : 200);
212 6         434 $c->res->parse($headers);
213             }
214             else {
215 27 100       337 $c->res->code($1) if $headers =~ /^Status: (\d\d\d)/m;
216 27 100       591 $c->res->code($headers =~ /Location:/ ? 302 : 200) unless $c->res->code;
    100          
217 27         2302 $c->res->parse($c->res->get_start_line_chunk(0) . $headers);
218             }
219 33 100       39842 $c->write($buf) if length $buf;
220 36         1281 };
221             }
222              
223             sub _stdin {
224 56     56   158 my $c = shift;
225 56         143 my $stdin;
226              
227 56 100       263 if ($c->req->content->is_multipart) {
228 2         52 $stdin = Mojo::Asset::File->new;
229 2         48 $stdin->add_chunk($c->req->build_body);
230             }
231             else {
232 54         1013 $stdin = $c->req->content->asset;
233             }
234              
235 56 100       5496 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 1514     1514   3301 my $pids = shift;
241              
242 1514         8682 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         1542 local $SIG{CHLD} = 'DEFAULT';
246 36 50       1175 next unless waitpid $pid, WNOHANG;
247 36   50     249 my $name = delete $pids->{$pid} || 'unknown';
248 36         299 my ($exit_value, $signal) = ($? >> 8, $? & 127);
249 36         894 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.39
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