// $Id: cgi-wrap.c,v 1.3 2003/07/02 22:20:57 ensc Exp $    --*- c -*--

// Copyright (C) 2003 Enrico Scholz <enrico.scholz@informatik.tu-chemnitz.de>
//  
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; version 2 of the License.
//  
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//  
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
//  

  // \file cgi-wrap.c
  // \brief Wrapper around the various big-sister perl-scripts
  //
  // This program is a wrapper to execute the big-sister scripts in a chroot as
  // a certain user.  In particularly it:
  //   # will be executed as setuid root
  //   # drops privilegies and becomes a special user/group
  //   # executes the command pointed by argv[0] in a special directory inside a
  //     chroot environment

  // CGI_USERFILE is a file containing the following information in the given
  // order:
  // #  <uid>
  // #  <gid>+ (space-separated)
  // #  <username>
  // #  <chroot-dir>
  // #  <bin-dir>


#ifdef HAVE_CONFIG_H
#  include <config.h>
#endif

#ifndef _GNU_SOURCE
#  define _GNU_SOURCE
#endif

#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <grp.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <assert.h>

#ifndef NGROUPS
#  define NGROUPS 32
#endif

#ifndef CGI_USERFILE
#  error CGI_USERFILE not defined
#endif

#ifdef __GNUC__
#  define __unused__	__attribute__((__unused__))
#else
#  define __unused__
#endif

static char const __unused__	VERSION[]  = "version 0.2";
static char const __unused__	REVISION[] = "$Id: cgi-wrap.c,v 1.3 2003/07/02 22:20:57 ensc Exp $";

  // This list is stolen from httpd's suexec.c
static char const * const	SAFE_ENV[] = {
    /* variable name starts with */
  "HTTP_", "SSL_",

    /* variable name is */
  "AUTH_TYPE=", "CONTENT_LENGTH=", "CONTENT_TYPE=", "DATE_GMT=", "DATE_LOCAL=",
  "DOCUMENT_NAME=", "DOCUMENT_PATH_INFO=", "DOCUMENT_ROOT=", "DOCUMENT_URI=",
  "FILEPATH_INFO=", "GATEWAY_INTERFACE=", "HTTPS=", "LAST_MODIFIED=",
  "PATH_INFO=", "PATH_TRANSLATED=", "QUERY_STRING=", "QUERY_STRING_UNESCAPED=",
  "REMOTE_ADDR=", "REMOTE_HOST=", "REMOTE_IDENT=", "REMOTE_PORT=",
  "REMOTE_USER=", "REDIRECT_QUERY_STRING=", "REDIRECT_STATUS=",
  "REDIRECT_URL=", "REQUEST_METHOD=", "REQUEST_URI=", "SCRIPT_FILENAME=",
  "SCRIPT_NAME=", "SCRIPT_URI=", "SCRIPT_URL=", "SERVER_ADMIN=",
  "SERVER_NAME=", "SERVER_ADDR=", "SERVER_PORT=", "SERVER_PROTOCOL=",
  "SERVER_SOFTWARE=", "UNIQUE_ID=", "USER_NAME=", "TZ=",
  0
};
  
static struct {
    uid_t		uid;
    gid_t		gids[NGROUPS];
    size_t		gid_count;
    char const *	user_name;
    char const *	chroot_dir;
    char const *	bin_dir;
}			configuration;

#define WRITEMSG(X)	(void)write(2,X,sizeof(X)-1);

static void
writeStr(int fd, char const *str)
{
  (void)write(fd, str, strlen(str));
}

static int
checkLinkStat(char const *file, struct stat *buf, char const *file_type)
{
  if (lstat(file, buf)!=0) {
    perror("lstat()");
    return EXIT_FAILURE;
  }

  if ( (buf->st_mode & (S_ISUID|S_ISGID)) ||
       (buf->st_uid!=0) ) {
    WRITEMSG("Invalid permissions and/or ownership of ");
    writeStr(2, file_type);
    WRITEMSG("\n");

    return EXIT_FAILURE;
  }

  return 0;
}

static int
checkFileStat(char const *file, struct stat *buf, char const *file_type)
{
  if (stat(file, buf)!=0) {
    perror("stat()");
    return EXIT_FAILURE;
  }

  if ( (buf->st_mode & (S_IWGRP|S_IWOTH|S_ISUID|S_ISGID)) ||
       (buf->st_uid!=0) ) {
    WRITEMSG("Invalid permissions and/or ownership of ");
    writeStr(2, file_type);
    WRITEMSG("\n");
    
    return EXIT_FAILURE;
  }

  return 0;
}

static char *
readUID(uid_t *res, char *str)
{
  char		*endptr;

  assert(res!=0 && str!=0);

  *res = strtol(str, &endptr, 0);
  if (*str=='\0' || *endptr!='\0') {
    *res = -1;
    WRITEMSG("Failed to parse uid\n");
    return 0;
  }

  return endptr;
}

static char *
readGIDs(gid_t *gids, size_t *gid_len, char *str)
{
  char		*endptr = str;
  size_t	idx = 0;

  assert(gids!=0 && gid_len!=0 && str!=0);

  for (;idx<*gid_len;++idx) {
    gids[idx] = strtol(str, &endptr, 0);

    if (*str=='\0' || (*endptr!='\0' && *endptr!=' ')) {
      gids[idx] = -1;
      *gid_len  =  0;
      WRITEMSG("Failed to parse gid\n");
      return 0;
    }

    if (*endptr=='\0') break;
    str = endptr+1;
  }

  if (*endptr!='\0') {
    WRITEMSG("Too much groups given\n");
    *gid_len = 0;
    return 0;
  }
    
  *gid_len = idx+1;

  return endptr;
}

static int
readConfiguration()
{
  struct stat	stat_name, stat_link, stat_file;
  int		res;
  int		fd;
  char		buf[8192];
  ssize_t	len;
  char		*ptr;
  char		*ptr_tok;
  int		state = 0;

  if ( (res=checkLinkStat(CGI_USERFILE, &stat_link, "configfile")) ||
       (res=checkFileStat(CGI_USERFILE, &stat_name, "configfile")) )
    return res;
  
  if ( (fd=open(CGI_USERFILE, O_RDONLY))==-1) {
    perror("open()");
    return EXIT_FAILURE;
  }

  if (fstat(fd, &stat_file)==-1) {
    perror("fstat()");
    return EXIT_FAILURE;
  }

  stat_file.st_atime = 0;
  stat_name.st_atime = 0;

  if ( stat_file.st_dev   != stat_name.st_dev   ||
       stat_file.st_ino   != stat_name.st_ino   ||
       stat_file.st_mode  != stat_name.st_mode  ||
       stat_file.st_size  != stat_name.st_size  ||
       stat_file.st_ctime != stat_name.st_ctime ||
       stat_file.st_mtime != stat_name.st_mtime ) {
    WRITEMSG("RACE detected while checking configfile\n");
    return EXIT_FAILURE;
  }

  again:
  len = read(fd, buf, sizeof(buf)-1);
  if (len==-1 && (errno==EAGAIN || errno==EINTR)) goto again;

  if (len==-1) {
    perror("read()");
    return EXIT_FAILURE;
  }
  if (len+1==sizeof buf) {
    WRITEMSG("configfile contains too much information\n");
    return EXIT_FAILURE;
  }

  buf[len] = '\0';
  ptr      = strtok_r(buf, "\n", &ptr_tok);
  
  while (ptr!=0 && *ptr!='\0' && state<=6) {
    switch (state) {
      case 0	:
	ptr = readUID(&configuration.uid, ptr);
	if (ptr==0) return EXIT_FAILURE;
	break;
	
      case 1	:
	configuration.gid_count = NGROUPS;
	ptr = readGIDs(configuration.gids, &configuration.gid_count, ptr);
	if (ptr==0) return EXIT_FAILURE;
	break;

      case 2	:
	configuration.user_name  = strdup(ptr);
	break;

      case 3	:
	configuration.chroot_dir = strdup(ptr);
	break;

      case 4	:
	configuration.bin_dir    = strdup(ptr);
	break;

      default	:
	break;
    }

    ptr = strtok_r(0, "\n", &ptr_tok);
    ++state;
  }

  if (state!=5) {
    WRITEMSG("Failed to parse configfile\n");
    return EXIT_FAILURE;
  }

  if (configuration.gid_count==0) {
    WRITEMSG("Unexpected internal error: gid_count is still 0\n");
    assert(0);
    return EXIT_FAILURE;
  }

  return 0;
}

static int
getBasename(char *dst, char const *src)
{
  char const *		start = src;
  char *		ptr   = dst+2;

  {
    char const *	i;
    
    for (i=src; *i!='\0'; ++i) {
      if (*i=='/') start=i+1;
    }
  }

  dst[0] = '.';
  dst[1] = '/';
  strcpy(ptr, start);
  
  switch (*ptr) {
    case '\0'	:
      WRITEMSG("basename must not be empty\n");
      return EXIT_FAILURE;
      
    case '.'	:
      WRITEMSG("basename must not begin with a period\n");
      return EXIT_FAILURE;

    case '/'	:	// Can not happen
      WRITEMSG("internal error\n");
      return EXIT_FAILURE;
  }
  
  for (; *ptr!='\0'; ++ptr) {
    if ( (*ptr>='A' && *ptr<='Z') ||
	 (*ptr>='a' && *ptr<='z') ||
	 (*ptr>='0' && *ptr<='9') ) { /* all ok */ }
    else {
      switch (*ptr) {
	case '_'	:
	case '-'	:
	case '.'	:  break;  // period '.' was checked already
	default		:
	  WRITEMSG("basename contains invalid characters\n");
	  return EXIT_FAILURE;
      }
    }
  }

  return 0;
}

static int
doChroot()
{
  if (chdir(configuration.chroot_dir)!=0) {
    perror("chdir()");
    return EXIT_FAILURE;
  }

  if (chroot(configuration.chroot_dir)!=0) {
    perror("chroot()");
    return EXIT_FAILURE;
  }
  
  return 0;
}

static int
changeIDs()
{
  uid_t const		uid = configuration.uid;
  gid_t const		gid = configuration.gids[0];

  if (uid==0 || gid==0) {
    WRITEMSG("UID/GID must not be zero\n");
    return EXIT_FAILURE;
  }

  if (setgroups(configuration.gid_count,configuration.gids)!=0) {
    perror("setgroups()");
    return EXIT_FAILURE;
  }

  if (setregid(gid,gid)!=0 || setreuid(uid,uid)!=0) {
    perror("setregid()/setreuid()");
    return EXIT_FAILURE;
  }

  if (getgid()!=gid || getegid()!=gid ||
      getuid()!=uid || geteuid()!=uid) {
    WRITEMSG("unexpected GID/UID\n");
    return EXIT_FAILURE;
  }

  return 0;
}

static int
isValidEnv(char const *ptr)
{
  char const * const *	i;
  
  for (i=SAFE_ENV; *i!=0; ++i) {
    if (strncmp(ptr, *i, strlen(*i))==0) return 1;
  }
  return 0;
}

static int
setEnv(char *buf)
{
  char	**out_ptr = environ;
  char	**in_ptr  = environ;

  if (in_ptr!=0) {
    for (; *in_ptr!=0; ++in_ptr) {
      if (isValidEnv(*in_ptr)) {
	*out_ptr = *in_ptr;
	++out_ptr;
      }
    }
    *out_ptr = 0;
  }

  if ( putenv("IFS= \t\n")==-1 ||
       putenv("PATH=/bin:/usr/bin")==-1 ||
       putenv("TERM=dump")==-1 ) {
    perror("putenv()");
    return EXIT_FAILURE;
  }

  strcpy(buf, "USER=");
  strcat(buf, configuration.user_name);
  if ( (putenv(buf)==-1) ) {
    perror("setenv()");
    return EXIT_FAILURE;
  }
  

  return 0;
}

static int
checkPerm(char const *file)
{
  struct stat	buf;
  int		res;
  
  if (stat(".", &buf)!=0) {
    perror("stat()");
    return EXIT_FAILURE;
  }

  if ( (buf.st_mode & (S_IWGRP|S_IWOTH)) ||
       (buf.st_uid!=0) || !S_ISDIR(buf.st_mode) ) {
    WRITEMSG("Invalid permissions and/or ownership of bindir\n");
    return EXIT_FAILURE;
  }

  if ( (res=checkLinkStat(file, &buf, "executable")) ||
       (res=checkFileStat(file, &buf, "executable")) )
    return res;

  return 0;
}

int
main(int __unused__ argc,
     char *argv[])
{
  int			res;
  char *		basename;
  char *		user_env;

  if ( (res = readConfiguration()) ||
       (res = doChroot()) ||
       (res = changeIDs()))
    return res;

  user_env = alloca(strlen(configuration.user_name) + sizeof("USER=") + 1);
  if ( (res = setEnv(user_env)) )
    return res;

  if (chdir(configuration.bin_dir)!=0) {
    perror("chdir()");
    return EXIT_FAILURE;
  }

  basename	=  alloca(strlen(argv[0])+3);
  if ( (res = getBasename(basename, argv[0])) )
    return res;

  if ( (res = checkPerm(basename)) )
    return res;


  (void)execv(basename, argv+0);
  perror("execv()");
  return EXIT_FAILURE;
}

  // Local Variables:
  // compile-command: "make cgi-wrap CFLAGS='-O0 -g3 -std=c99 -Wall -W -pedantic' CONFFILE=/tmp/cgi-wrap.conf STRIP=:"
  // fill-column: 80
  // End:
