Cheap VPS & Xen Server

Residential Proxy Network - Hourly & Monthly Packages

Serving CGI Scripts With Nginx On Debian Squeeze/Ubuntu 11.04


This tutorial shows how you can serve CGI scripts (Perl scripts) with nginx on Debian Squeeze/Ubuntu 11.04. While nginx itself does not serve CGI, there are several ways to work around this. I will outline three solutions: the first is to proxy requests for CGI scripts to Thttpd, a small web server that has CGI support, while the second and third solution are very similar – both use a CGI wrapper to serve CGI scripts.

I do not issue any guarantee that this will work for you!

 

1 Preliminary Note

I’m using the website www.example.com here with the document root /var/www/www.example.com/web/ and the Nginx vhost configuration file /etc/nginx/sites-enabled/www.example.com.vhost.

A note for Ubuntu users:

Because we must run all the steps from this tutorial with root privileges, we can either prepend all commands in this tutorial with the string sudo, or we become root right now by typing

sudo su

 

2 Using Thttpd

In this chapter I am going to describe how to configure nginx to proxy requests for CGI scripts (extensions .cgi or .pl) to Thttpd. I will configure Thttpd to run on port 8000.

First we install Thttpd:

apt-get install thttpd

The nginx ThttpdCGI page says that Thttpd should be patched – fortunately this patch is already included in Debian’s and Ubuntu’s thttpd package, so we don’t need to do this.

Next we open /etc/default/thttpd

vi /etc/default/thttpd

… and set ENABLED to yes:

[...]
ENABLED=yes

Then we make a backup of the original /etc/thttpd/thttpd.conf file and create a new one as follows:

mv /etc/thttpd/thttpd.conf /etc/thttpd/thttpd.conf_orig
vi /etc/thttpd/thttpd.conf

# BEWARE : No empty lines are allowed!
# This section overrides defaults
# This section _documents_ defaults in effect
# port=80
# nosymlink         # default = !chroot
# novhost
# nocgipat
# nothrottles
# host=0.0.0.0
# charset=iso-8859-1
host=127.0.0.1
port=8000
user=www-data
logfile=/var/log/thttpd.log
pidfile=/var/run/thttpd.pid
dir=/var/www
cgipat=**.cgi|**.pl

This will make Thttpd listen on port 8000 on 127.0.0.1; its document root is /var/www.

Start Thttpd:

/etc/init.d/thttpd start

Next create /etc/nginx/proxy.conf:

vi /etc/nginx/proxy.conf

proxy_redirect          off;
proxy_set_header        Host            $host;
proxy_set_header        X-Real-IP       $remote_addr;
proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size    10m;
client_body_buffer_size 128k;
proxy_connect_timeout   90;
proxy_send_timeout      90;
proxy_read_timeout      90;

Now open your vhost configuration file…

vi /etc/nginx/sites-enabled/www.example.com.vhost

… and add a location /cgi-bin {} section to the server {} container:

server {
[...]
   location /cgi-bin {
      include proxy.conf;
      proxy_pass http://127.0.0.1:8000;
   }
[...]
}

Reload nginx:

/etc/init.d/nginx reload

Because Thttpd’s document root is /var/www, location /cgi-bin translates to the directory /var/www/cgi-bin (this is true for all your vhosts, which means each vhost must place its CGI scripts in /var/www/cgi-bin; this is a drawback for shared hosting environments; the solution is to use a CGI wrapper as described in chapters 3 and 4 instead of Thttpd).

Create the directory…

mkdir /var/www/cgi-bin

… and then place your CGI scripts in it and make them executable. For testing purposes I will create a small Hello World Perl script (instead of hello_world.cgi you can also use the extension .pl -> hello_world.pl):

vi /var/www/cgi-bin/hello_world.cgi

#!/usr/bin/perl -w

     # Tell perl to send a html header.
     # So your browser gets the output
     # rather then <stdout>(command line
     # on the server.)
print "Content-type: text/html\n\n";

     # print your basic html tags.
     # and the content of them.
print "<html><head><title>Hello World!! </title></head>\n";
print "<body><h1>Hello world</h1></body></html>\n";

chmod 755 /var/www/cgi-bin/hello_world.cgi

Open a browser and test the script:

http://www.example.com/cgi-bin/hello_world.cgi

If all goes well, you should get the following output:

1

3 Using Simple CGI

Another approach is to use a CGI wrapper script, as described here: http://wiki.nginx.org/NginxSimpleCGI

The advantage over Thttpd is that each vhost can have its own cgi-bin directory. The CGI wrapper should work well for simple CGI scripts, but you might get problems with more complex scripts.

Let’s install the requirements for the CGI wrapper script:

apt-get install libfcgi-perl libfcgi-procmanager-perl

Now we create the /usr/local/bin/cgiwrap-fcgi.pl wrapper script (the script is taken from http://wiki.nginx.org/NginxSimpleCGI):

vi /usr/local/bin/cgiwrap-fcgi.pl

#!/usr/bin/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";
    }
  }
}

Make the script executable…

chmod 755 /usr/local/bin/cgiwrap-fcgi.pl

… and create the /var/run/nginx/ directory where the script will create its socket (that nginx will connect to):

mkdir /var/run/nginx/

By default, nginx runs as the www-data user; nginx must have read and write access to the /var/run/nginx/ directory, therefore we do a chown:

chown www-data:www-data /var/run/nginx/

Because the socket /var/run/nginx/cgiwrap-dispatch.sock (which will be created by the wrapper) must also be owned by www-data, we start the wrapper as the www-data user:

su www-data

/usr/local/bin/cgiwrap-fcgi.pl &> /dev/null &

The wrapper should now run as a daemon in the background, so we can become root again:

exit

Now open your vhost configuration file…

vi /etc/nginx/sites-enabled/www.example.com.vhost

… and add a location /cgi-bin {} section to the server {} container:

server {
[...]
   location /cgi-bin {
      root /var/www/www.example.com;
      gzip off; #gzip makes scripts feel slower since they have to complete before getting gzipped
      fastcgi_pass  unix:/var/run/nginx/cgiwrap-dispatch.sock;
      include /etc/nginx/fastcgi_params;
      fastcgi_index index.cgi;
      fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
   }
[...]
}

Reload nginx:

/etc/init.d/nginx reload

Next we create our cgi-bin directory – /var/www/www.example.com/cgi-bin because we defined root /var/www/www.example.com; in the location /cgi-bin {} container:

mkdir /var/www/www.example.com/cgi-bin

Now we place our CGI scripts in it and make them executable. For testing purposes I will create a small Hello World Perl script (instead of hello_world.cgi you can also use the extension .pl -> hello_world.pl):

vi /var/www/www.example.com/cgi-bin/hello_world.cgi

#!/usr/bin/perl -w

     # Tell perl to send a html header.
     # So your browser gets the output
     # rather then <stdout>(command line
     # on the server.)
print "Content-type: text/html\n\n";

     # print your basic html tags.
     # and the content of them.
print "<html><head><title>Hello World!! </title></head>\n";
print "<body><h1>Hello world</h1></body></html>\n";

chmod 755 /var/www/www.example.com/cgi-bin/hello_world.cgi

Open a browser and test the script:

http://www.example.com/cgi-bin/hello_world.cgi

If all goes well, you should get the following output:

1

4 Using Fcgiwrap

Fcgiwrap is another CGI wrapper that should work also for complex CGI scripts and – like Simple CGI – can be used for shared hosting environments because it allows each vhost to use its own cgi-bin directory.

Install the fcgiwrap package:

apt-get install fcgiwrap

After the installation, the fcgiwrap daemon should already be started; its socket is /var/run/fcgiwrap.socket. If it is not running, you can use the /etc/init.d/fcgiwrap script to start it.

Now open your vhost configuration file…

vi /etc/nginx/sites-enabled/www.example.com.vhost

… and add a location /cgi-bin {} section to the server {} container:

server {
[...]
   location /cgi-bin/ {
     # Disable gzip (it makes scripts feel slower since they have to complete
     # before getting gzipped)
     gzip off;
     # Set the root to /usr/lib (inside this location this means that we are
     # giving access to the files under /usr/lib/cgi-bin)
     root  /var/www/www.example.com;
     # Fastcgi socket
     fastcgi_pass  unix:/var/run/fcgiwrap.socket;
     # Fastcgi parameters, include the standard ones
     include /etc/nginx/fastcgi_params;
     # Adjust non standard parameters (SCRIPT_FILENAME)
     fastcgi_param SCRIPT_FILENAME  $document_root$fastcgi_script_name;
   }
[...]
}

Reload nginx:

/etc/init.d/nginx reload

Next we create our cgi-bin directory – /var/www/www.example.com/cgi-bin because we defined root /var/www/www.example.com; in the location /cgi-bin {} container:

mkdir /var/www/www.example.com/cgi-bin

Now we place our CGI scripts in it and make them executable. For testing purposes I will create a small Hello World Perl script (instead of hello_world.cgi you can also use the extension .pl -> hello_world.pl):

vi /var/www/www.example.com/cgi-bin/hello_world.cgi

#!/usr/bin/perl -w
     # Tell perl to send a html header.
     # So your browser gets the output
     # rather then <stdout>(command line
     # on the server.)
print "Content-type: text/html\n\n";
     # print your basic html tags.
     # and the content of them.
print "<html><head><title>Hello World!! </title></head>\n";
print "<body><h1>Hello world</h1></body></html>\n";

chmod 755 /var/www/www.example.com/cgi-bin/hello_world.cgi

Open a browser and test the script:

http://www.example.com/cgi-bin/hello_world.cgi

If all goes well, you should get the following output:

1

  • Nginx: http://nginx.org/
  • Nginx Wiki: http://wiki.nginx.org/
  • Thttpd: http://acme.com/software/thttpd/
  • nginx ThttpdCGI: http://wiki.nginx.org/ThttpdCGI
  • nginx Simple CGI: http://wiki.nginx.org/NginxSimpleCGI
  • nginx Fcgiwrap: http://wiki.nginx.org/Fcgiwrap

 

Comments

comments