/*
 * $Id: cgi-wrapper.c,v 1.4 2011/04/22 23:11:23 sra Exp $
 *
 * This is a small wrapper program intended for use when running php-cgi
 * in FastCGI mode under mod_fcgid.  It may be useful for other purposes
 * as well.
 *
 * Usage scenario is an Apache httpd running a number of different
 * virtual hosts, some of which run complicated PHP packages like Drupal.
 * We want to get the PHP interpreter out of httpd and into a separate
 * process with its own uid/gid, both to protect httpd from bugs in the
 * PHP code or interpreter and also to protect the several virtual hosts
 * from each other.
 *
 * In theory one can use Apache's suexec mechanism for this, but it's
 * hard to configure, is way over the top for this purpose, and some of
 * the restrictions it imposes (such as insisting that the target program
 * must be under httpd's document root) are potential vulnerabilities
 * given this particular kind of setup.   Hence this wrapper.
 *
 * The model here is that all configuration of this wrapper is controlled
 * by the UID and GID of the wrapper binary.  Each virtual host runs as a
 * separate UID/GID, which you create in the usual way in /etc/passwd and
 * /etc/group.  The wrapper expects to be run setuid and setgid to the
 * target UID and GID.  The wrapper binary should be have permissions
 * 6550, and the user running httpd (www on FreeBSD) should be a member
 * of the target group.  The filename of the php-cgi interpreter is
 * compiled in.  You need a separate copy of the binary for each UID/GID.
 *
 * On startup, the wrapper does a few simple checks, then forks.  The
 * child process sets its UID and GID, then execs the php-cgi
 * interpreter.  The original wrapper process sticks around to act as a
 * relay for Unix signals; without this, a non-root httpd can't send
 * signals to its setuid children, which causes problems when httpd
 * attempts a "graceful restart" operation.
 *
 * Sample httpd.conf for use with this wrapper:
 *
 *     <VirtualHost *>
 * 	DocumentRoot		/usr/local/www/data/vhost1
 * 	ServerName		vhost1.example.org
 * 	DirectoryIndex		index.php
 * 	AddType			application/x-httpd-php .php
 * 	PHP_Fix_Pathinfo_Enable 1
 * 	MaxRequestsPerProcess   500
 * 	<Location />
 * 	    AddHandler		fcgid-script .php
 * 	    Options		+ExecCGI
 * 	    FCGIWrapper		/usr/local/www/wrappers/php-cgi.vhost1 .php
 * 	</Location>
 *     </VirtualHost>
 *
 * where the wrapper binary for this particular virtual host is
 * php-cgi.vhost1.
 *
 * Remember to enable FastCGI support when you build PHP.
 *
 * Commands needed to convert the data files for an existing setup that
 * was based on mod_php will be very dependent on precise details of your
 * setup, but the following sufficed for my existing Drupal sites:
 *
 *     mtree -c -p $docroot >/some/place/safe/before.mtree
 *     find $docroot -user www -group www -print0 | xargs -0 chmod g=u
 *     find $docroot -user www -print0 | xargs -0 chown -h $newuser
 *     find $docroot -group www -print0 | xargs -0 chgrp -h $newgroup
 *     mtree -c -p $docroot >/some/place/safe/after.mtree
 *
 * This code was tested on FreeBSD 6.4-STABLE, with Apache 2.0.63,
 * mod_fcgid 2.2, and PHP 5.2.9.  YMMV.
 *
 * In theory this wrapper could be used with other CGI or FastCGI
 * programs, there's nothing about it other than the compiled in filename
 * that's specific to PHP.  I haven't needed it with anything else yet.
 *
 * In theory I could call getpwuid() to get the user's default group from
 * /etc/passwd, and it should be possible to setgid() to that group after
 * going setuid() to the target user (the other way around only works for
 * root).  In theory this would make it a bit easier to protect the
 * wrapped interpreter from httpd.  In practice, I haven't bothered, both
 * because I'm not particularly worried about attacks on PHP by httpd,
 * but also because, for the Drupal sites that caused me to write this,
 * httpd needs group read access to some of the virtual host's files
 * anyway, so there wouldn't have been much point.
 *
 * This program is hereby explictly placed in the public domain as
 * Beer-Ware.  If we meet some day and you think this program is worth
 * it, you can buy me a beer.  Your mileage may vary.  We decline
 * responsibilities, all shapes, all sizes, all colors.  If this
 * program breaks, you get to keep both pieces.
 */

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/resource.h>
#include <signal.h>
#include <errno.h>
#include <string.h>
#include <time.h>

#ifndef WRAPPED_PROGRAM
#define	WRAPPED_PROGRAM		"/usr/local/bin/php-cgi"
#endif

#ifndef EXIT_TIMEOUT
#define EXIT_TIMEOUT		30
#endif

static const char rcsid[] = "$Id: cgi-wrapper.c,v 1.4 2011/04/22 23:11:23 sra Exp $";

static /* const */ char prog[] = WRAPPED_PROGRAM;

static const int sigs[] = {
  SIGHUP, SIGUSR1, SIGUSR2
};

static const int kill_sigs[] = {
  SIGINT, SIGQUIT, SIGABRT, SIGTERM
};

static pid_t pid = 0;

static char *jane;

static int wait_flags = 0;

static time_t exit_time;

static void handler (int sig)
{
  if (pid)
    kill(pid, sig);
}

static void kill_handler(int sig)
{
  handler(sig);
  if (pid) {
    wait_flags = WNOHANG;
    exit_time = time(0) + EXIT_TIMEOUT;
  }
}

static int lose(const char *msg)
{
  if (!msg)
    msg = strerror(errno);
  fprintf(stderr, "%s: %s [%s]\n", jane, msg, rcsid);
  return 1;
}

int main (int argc, char *argv[])
{
  const uid_t uid = getuid(), euid = geteuid();
  const gid_t gid = getgid(), egid = getegid();
  pid_t p;
  int i, status;
  time_t now;

  jane = argv[0];
  argv[0] = prog;

  if (euid == 0)
    return lose("Must not be setuid root, aborting");

  if (uid == euid || gid == egid)
    return lose("Must be setuid & setgid, aborting");

  for (i = 0; i < sizeof(sigs)/sizeof(*sigs); ++i)
    signal(sigs[i], handler);

  for (i = 0; i < sizeof(kill_sigs)/sizeof(*kill_sigs); ++i)
    signal(kill_sigs[i], kill_handler);

  switch ((pid = vfork())) {

  case -1:
    return lose(NULL);

  case 0:
    if (!setgid(egid) && !setuid(euid))
      execv(argv[0], argv);
    lose(NULL);
    _exit(2);

  default:
    while ((p = waitpid(pid, &status, wait_flags)) == -1 && errno == EINTR)
      if (wait_flags && (now = time(0)) < exit_time)
	sleep(exit_time - now);
      else if (wait_flags)
	break;

    if (p != pid || (!WIFEXITED(status) && !WIFSIGNALED(status)))
      kill(pid, SIGKILL);

    return p == pid && WIFEXITED(status) ? WEXITSTATUS(status) : 3;

  }
}
