For the most part, lack of CGI support in Nginx is not an issue and actually has an important side-benefit:
"Because Nginx cannot directly execute external programs (CGI), a malicious person can't trick your system into uploading and executing an arbitrary script."
There are still ways, of course (uploading a PHP script into a directory where you've got a directive to execute PHP FastCGI scripts for example), but it does in fact make it a bit more difficult (or conversely, easier to secure).
Still, sometimes you have a one-off simple CGI program you need to support and here's a recipe for exposing your CGI to Ngnix as a FastCGI instead:
Let's say this is our CGI program, written in Perl:
1 #!/usr/bin/perl
2
3 print "Content-type: text/html\n\n";
4 print "<html><body>Hello, world.</body></html>";
Make sure that the web server can read the file and it is marked as executable.
Next, we need a Perl wrapper that will run as a FastCGI and run this for us:
Would someone merge the script below with the previous version that handled POST requests properly: http://wiki.codemongers.com/NginxSimpleCGI?action=recall&rev=10 --I really wish MoinMoin had Talk features. The old POST code worked fine for me but try Denis's and see it it works for you... -joey
I see that you merged the part that reads data from the client but not the part that feeds in to the CGI script's stdin, which actually makes a difference. Some scripts do require POST'ed data to arrive through stdin, e.g., those that process multipart forms (attachments), so it matters. The script that I've submitted has been in production for over a month without any problems. -- denis
This one is still pretty ugly, but forwards stdio around correctly (not necessarily efficiently or prettily), and also reads and spits back out STDERR into fcgi STDERR. I am not entirely confident there is no race condition in the event that an app STDERRS out without any output data, but it works for my purposes. This is for the most part, not my own work; 99.9% of this came from the various improvements on the spawner script worked on in the mailing list and wiki here.. I just added some more pipe handling to enable communicating through STDERR, and a select loop to properly recieve both data streams. -- bryon@spideroak.com
1 #!/usr/bin/perl -w
2 use FCGI;
3 use Socket;
4 use FCGI::ProcManager;
5 sub shutdown { FCGI::CloseSocket($socket); exit; }
6 sub restart { FCGI::CloseSocket($socket); &main; }
7 use sigtrap 'handler', \&shutdown, 'normal-signals';
8 use sigtrap 'handler', \&restart, 'HUP';
9 require 'syscall.ph';
10 use POSIX qw(setsid);
11
12 #&daemonize; we don't daemonize when running under runsv
13 #this keeps the program alive or something after exec'ing perl scripts
14 END() { }
15 BEGIN() { }
16 {
17 no warnings;
18 *CORE::GLOBAL::exit = sub { die "fakeexit\nrc=" . shift() . "\n"; };
19 };
20 eval q{exit};
21 if ($@) {
22 exit unless $@ =~ /^fakeexit/;
23 }
24 &main;
25
26 sub daemonize() {
27 chdir '/' or die "Can't chdir to /: $!";
28 defined( my $pid = fork ) or die "Can't fork: $!";
29 exit if $pid;
30 setsid() or die "Can't start a new session: $!";
31 umask 0;
32 }
33
34 sub main {
35
36 #$socket = FCGI::OpenSocket( "127.0.0.1:8999", 10 ); #use IP sockets
37 #$socket = FCGI::OpenSocket( "/var/run/nginx/perl_cgi-dispatch.sock", 10 ); #use UNIX sockets - user running this script must have w access to the 'nginx' folder!!
38 #foreach $item (keys %ENV) { delete $ENV{$item}; }
39 $proc_manager = FCGI::ProcManager->new( {n_processes => 5} );
40 $socket = FCGI::OpenSocket( "/opt/nginx/fcgi/cgi.sock", 10 )
41 ; #use UNIX sockets - user running this script must have w access to the 'nginx' folder!!
42 $request =
43 FCGI::Request( \*STDIN, \*STDOUT, \*STDERR, \%req_params, $socket,
44 &FCGI::FAIL_ACCEPT_ON_INTR );
45 $proc_manager->pm_manage();
46 if ($request) { request_loop() }
47 FCGI::CloseSocket($socket);
48 }
49
50 sub request_loop {
51 while ( $request->Accept() >= 0 ) {
52 $proc_manager->pm_pre_dispatch();
53
54 #processing any STDIN input from WebServer (for CGI-POST actions)
55 $stdin_passthrough = '';
56 { no warnings; $req_len = 0 + $req_params{'CONTENT_LENGTH'}; };
57 if ( ( $req_params{'REQUEST_METHOD'} eq 'POST' ) && ( $req_len != 0 ) )
58 {
59 my $bytes_read = 0;
60 while ( $bytes_read < $req_len ) {
61 my $data = '';
62 my $bytes = read( STDIN, $data, ( $req_len - $bytes_read ) );
63 last if ( $bytes == 0 || !defined($bytes) );
64 $stdin_passthrough .= $data;
65 $bytes_read += $bytes;
66 }
67 }
68
69 #running the cgi app
70 if (
71 ( -x $req_params{SCRIPT_FILENAME} ) && #can I execute this?
72 ( -s $req_params{SCRIPT_FILENAME} ) && #Is this file empty?
73 ( -r $req_params{SCRIPT_FILENAME} ) #can I read this file?
74 )
75 {
76 pipe( CHILD_RD, PARENT_WR );
77 pipe( PARENT_ERR, CHILD_ERR );
78 my $pid = open( CHILD_O, "-|" );
79 unless ( defined($pid) ) {
80 print("Content-type: text/plain\r\n\r\n");
81 print
82 "Error: CGI app returned no output - Executing $req_params{SCRIPT_FILENAME} failed !\n";
83 next;
84 }
85 $oldfh = select(PARENT_ERR);
86 $| = 1;
87 select(CHILD_O);
88 $| = 1;
89 select($oldfh);
90 if ( $pid > 0 ) {
91 close(CHILD_RD);
92 close(CHILD_ERR);
93 print PARENT_WR $stdin_passthrough;
94 close(PARENT_WR);
95 $rin = $rout = $ein = $eout = '';
96 vec( $rin, fileno(CHILD_O), 1 ) = 1;
97 vec( $rin, fileno(PARENT_ERR), 1 ) = 1;
98 $ein = $rin;
99 $nfound = 0;
100
101 while ( $nfound =
102 select( $rout = $rin, undef, $ein = $eout, 10 ) )
103 {
104 die "$!" unless $nfound != -1;
105 $r1 = vec( $rout, fileno(PARENT_ERR), 1 ) == 1;
106 $r2 = vec( $rout, fileno(CHILD_O), 1 ) == 1;
107 $e1 = vec( $eout, fileno(PARENT_ERR), 1 ) == 1;
108 $e2 = vec( $eout, fileno(CHILD_O), 1 ) == 1;
109
110 if ($r1) {
111 while ( $bytes = read( PARENT_ERR, $errbytes, 4096 ) ) {
112 print STDERR $errbytes;
113 }
114 if ($!) {
115 $err = $!;
116 die $!;
117 vec( $rin, fileno(PARENT_ERR), 1 ) = 0
118 unless ( $err == EINTR or $err == EAGAIN );
119 }
120 }
121 if ($r2) {
122 while ( $bytes = read( CHILD_O, $s, 4096 ) ) {
123 print $s;
124 }
125 if ( !defined($bytes) ) {
126 $err = $!;
127 die $!;
128 vec( $rin, fileno(CHILD_O), 1 ) = 0
129 unless ( $err == EINTR or $err == EAGAIN );
130 }
131 }
132 last if ( $e1 || $e2 );
133 }
134 close CHILD_RD;
135 close PARENT_ERR;
136 waitpid( $pid, 0 );
137 } else {
138 foreach $key ( keys %req_params ) {
139 $ENV{$key} = $req_params{$key};
140 }
141
142 # cd to the script's local directory
143 if ( $req_params{SCRIPT_FILENAME} =~ /^(.*)\/[^\/]+$/ ) {
144 chdir $1;
145 }
146 close(PARENT_WR);
147
148 #close(PARENT_ERR);
149 close(STDIN);
150 close(STDERR);
151
152 #fcntl(CHILD_RD, F_DUPFD, 0);
153 syscall( &SYS_dup2, fileno(CHILD_RD), 0 );
154 syscall( &SYS_dup2, fileno(CHILD_ERR), 2 );
155
156 #open(STDIN, "<&CHILD_RD");
157 exec( $req_params{SCRIPT_FILENAME} );
158 die("exec failed");
159 }
160 } else {
161 print("Content-type: text/plain\r\n\r\n");
162 print
163 "Error: No such CGI app - $req_params{SCRIPT_FILENAME} may not exist or is not executable by this process.\n";
164 }
165 }
166 }
Save the above script as /usr/local/bin/cgiwrap-fcgi.pl.
Just running the program as above will bind it to a unix socket at /var/run/nginx/perl_cgi-dispatch.sock . The script does not fork itself, so you will need to background it somehow (with Bash add an ampersand "&" at the end of your command to execute it).
If this all works, then the next part is to setup Nginx:
http {
root /var/www/htdocs;
index index.html;
location ~ ^/cgi-bin/.*\.cgi$ {
gzip off; #gzip makes scripts feel slower since they have to complete before getting gzipped
fastcgi_pass unix:/var/run/nginx/cgiwrap-dispatch.sock;
fastcgi_index index.cgi;
fastcgi_param SCRIPT_FILENAME /var/www/cgi-bin$fastcgi_script_name;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param DOCUMENT_URI $document_uri;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;
}
}
Restart Nginx and point your browser at your CGI program. The above sample config will execute any .cgi file in cgi-bin with the cgiwrap-fcgi.pl wrapper, tweak this to your heart's content.
I've been able to run Python, Perl, and C++ cgi apps with this - apps that use GET or POST.
