如何在Nginx中使用CGI

缘起
在探索到CGI到底为何物时,google SimpleCGI的时候,看到这篇文章,粗看了几眼,觉得不错,有几个感兴趣的topic:CGI,FastCGI,Nginx。

正文
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.


大体而言,Nginx中缺乏对CGI的支持不是问题,实际上还带来一些好处:因为Nginx不直接执行外部程序(CGI),心怀不轨的人不能通过上传并执行任意脚本来攻击你的系统。

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).


当然,依然存在一些攻击系统的方法,比如上传PHP脚本到某个目录,但你定向到给目录时,执行PHP FastCGI脚本,但这显然更加困难一些。

Still, sometimes you have a one-off simple CGI program you need to support and here's a recipe for exposing your CGI to Nginx as a FastCGI instead:


有时,需要支持一次性的CGI脚本。这里介绍一个将CGI程序转换为FastCGI然后传递给Nginx的方法:

Let's say this is our CGI program, written in Perl:

下面介绍一下这个Perl编写的CGI程序:


#!/usr/bin/perl

print "Content-type: text/html\n\n";
print "<html><body>Hello, world.</body></html>";
Make sure that the web server can read the file and it is marked as executable.


确保web服务器能够读取并将其看作可执行文件。

Next, we need a Perl wrapper that will run as a FastCGI and run this for us:

接下来,需要一个运行为FastCGI的Perl包装器来运行这个程序:

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. -- [email protected]


下面的这个包装器相当的简陋,但在stdio上大体正确(不需要高效或完美),也可以将标准STDERR读取和输出为fcgi的STDERR。不确定代码中在应用中没有任何输出数据的STDERRS中是否存在竞争条件,这个包装器很好满足了我的目标。包装器中99.9%的部分来自邮件列表和wiki,我(原作者)只添加了一些管道处理从而确保能够通过STDERR进行通信,以及一个接受合适的数据流的选择循环。


#!perl
use FCGI;
use Socket;
use FCGI::ProcManager;
sub shutdown { FCGI::CloseSocket($socket); exit; }
sub restart  { FCGI::CloseSocket($socket); &main; }
use sigtrap 'handler', \&shutdown, 'normal-signals';
use sigtrap 'handler', \&restart,  'HUP';
require 'syscall.ph';
use POSIX qw(setsid);
 
END()  { }
BEGIN() { }
{
  no warnings;
  *CORE::GLOBAL::exit = sub { die "fakeexit\nrc=" . shift() . "\n"; };
};
 
eval q{exit};
if ($@) {
  exit unless $@ =~ /^fakeexit/;
}
&main;
 
sub daemonize() {
  chdir '/' or die "Can't chdir to /: $!";
  defined( my $pid = fork ) or die "Can't fork: $!";
  exit if $pid;
  setsid() or die "Can't start a new session: $!";
  umask 0;
}
 
sub main {
  $proc_manager = FCGI::ProcManager->new( {n_processes => 5} );
  $socket = FCGI::OpenSocket( "/var/run/nginx/cgiwrap-dispatch.sock", 10 )
  ; #use UNIX sockets - user running this script must have w access to the 'nginx' folder!!
  $request =
  FCGI::Request( \*STDIN, \*STDOUT, \*STDERR, \%req_params, $socket,
  &FCGI::FAIL_ACCEPT_ON_INTR );
  $proc_manager->pm_manage();
  if ($request) { request_loop() }
  FCGI::CloseSocket($socket);
}
 
sub request_loop {
  while ( $request->Accept() >= 0 ) {
    $proc_manager->pm_pre_dispatch();
 
    #processing any STDIN input from WebServer (for CGI-POST actions)
    $stdin_passthrough = '';
    { no warnings; $req_len = 0 + $req_params{'CONTENT_LENGTH'}; };
    if ( ( $req_params{'REQUEST_METHOD'} eq 'POST' ) && ( $req_len != 0 ) ) {
      my $bytes_read = 0;
      while ( $bytes_read < $req_len ) {
        my $data = '';
        my $bytes = read( STDIN, $data, ( $req_len - $bytes_read ) );
        last if ( $bytes == 0 || !defined($bytes) );
        $stdin_passthrough .= $data;
        $bytes_read += $bytes;
      }
    }
 
    #running the cgi app
    if (
      ( -x $req_params{SCRIPT_FILENAME} ) &&    #can I execute this?
      ( -s $req_params{SCRIPT_FILENAME} ) &&    #Is this file empty?
      ( -r $req_params{SCRIPT_FILENAME} )      #can I read this file?
    ) {
      pipe( CHILD_RD,  PARENT_WR );
      pipe( PARENT_ERR, CHILD_ERR );
      my $pid = open( CHILD_O, "-|" );
      unless ( defined($pid) ) {
        print("Content-type: text/plain\r\n\r\n");
        print "Error: CGI app returned no output - Executing $req_params{SCRIPT_FILENAME} failed !\n";
        next;
      }
      $oldfh = select(PARENT_ERR);
      $|    = 1;
      select(CHILD_O);
      $| = 1;
      select($oldfh);
      if ( $pid > 0 ) {
        close(CHILD_RD);
        close(CHILD_ERR);
        print PARENT_WR $stdin_passthrough;
        close(PARENT_WR);
        $rin = $rout = $ein = $eout = '';
        vec( $rin, fileno(CHILD_O),    1 ) = 1;
        vec( $rin, fileno(PARENT_ERR), 1 ) = 1;
        $ein    = $rin;
        $nfound = 0;
 
        while ( $nfound = select( $rout = $rin, undef, $ein = $eout, 10 ) ) {
          die "$!" unless $nfound != -1;
          $r1 = vec( $rout, fileno(PARENT_ERR), 1 ) == 1;
          $r2 = vec( $rout, fileno(CHILD_O),    1 ) == 1;
          $e1 = vec( $eout, fileno(PARENT_ERR), 1 ) == 1;
          $e2 = vec( $eout, fileno(CHILD_O),    1 ) == 1;
 
          if ($r1) {
            while ( $bytes = read( PARENT_ERR, $errbytes, 4096 ) ) {
              print STDERR $errbytes;
            }
            if ($!) {
              $err = $!;
              die $!;
              vec( $rin, fileno(PARENT_ERR), 1 ) = 0
              unless ( $err == EINTR or $err == EAGAIN );
            }
          }
          if ($r2) {
            while ( $bytes = read( CHILD_O, $s, 4096 ) ) {
              print $s;
            }
            if ( !defined($bytes) ) {
              $err = $!;
              die $!;
              vec( $rin, fileno(CHILD_O), 1 ) = 0
              unless ( $err == EINTR or $err == EAGAIN );
            }
          }
          last if ( $e1 || $e2 );
        }
        close CHILD_RD;
        close PARENT_ERR;
        waitpid( $pid, 0 );
      } else {
        foreach $key ( keys %req_params ) {
          $ENV{$key} = $req_params{$key};
        }
 
        # cd to the script's local directory
        if ( $req_params{SCRIPT_FILENAME} =~ /^(.*)\/[^\/] +$/ ) {
          chdir $1;
        }
        close(PARENT_WR);
        #close(PARENT_ERR);
        close(STDIN);
        close(STDERR);
 
        #fcntl(CHILD_RD, F_DUPFD, 0);
        syscall( &SYS_dup2, fileno(CHILD_RD),  0 );
        syscall( &SYS_dup2, fileno(CHILD_ERR), 2 );
 
        #open(STDIN, "<&CHILD_RD");
        exec( $req_params{SCRIPT_FILENAME} );
        die("exec failed");
      }
    } else {
      print("Content-type: text/plain\r\n\r\n");
      print "Error: No such CGI app - $req_params{SCRIPT_FILENAME} may not exist or is not executable by this process.\n";
    }
  }
}
Save the above script as /usr/local/bin/cgiwrap-fcgi.pl.
将上述文件保存为/usr/local/bin/cgiwrap-fcgi.pl。

Just running the program as above will bind it to a unix socket at /var/run/nginx/cgiwrap-dispatch.sock . Be sure your nginx worker process user has read/write access to this file. 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).


运行上述程序将会将其绑定到位于/var/run/nginx/cgiwrap-dispatch.sock的Unix套接字。取保Nginx工作进程具有文件的读写权限。该脚本不会自我终结(?),需要将其运行为后台程序(Bash中在执行命令后添加&)

If this all works, then the next part is to setup Nginx:

如果工作了,接下来就是启动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.
重启Nginx并将浏览器定向到CGI程序的URL。上面的简单配置将会使用cgiwrap-fcgi.pl包装执行位于cgi-bin所有.cgi后缀的文件,请谨记这一点,这可能会很危险。

I've been able to run Python, Perl, and C++ cgi apps with this - apps that use GET or POST.

可以使用GET或POST方法的C++,Perl,Python的cgi应用都可运行。

You may find that $document_root does not point to your actual document root (hardcoded upon build), so you can replace the fastcgi param DOCUMENT_ROOT with "/the/real/path". Also replace the SCRIPT_FILENAME param if that is used by your CGI wrapper (the one above does)


可以看到,$document_root没有指向实际的文档root(依赖构建的硬编码),需要将fastcgi参数DOCUMENT_ROOT替代为真实可用的参数,并将SCRIPT_FILENAME参书替代为CGI包装器使用的真实值。

后记
虽然,没有写过CGI代码,也没有配置安装过Nginx,但我觉的这些东西都很有趣,等我学完Rails,一定要好好研究研究Nginx,和Apahce一起比对学习。

更多Nginx相关教程见以下内容

Nginx 的详细介绍:请点这里
Nginx 的下载地址:请点这里

相关推荐