/*
	$Id$
	
	
*/

#include "sbox-fastcgi.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <time.h>
#include <pwd.h>
#include <grp.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/param.h>
#include <sys/stat.h>
#if SET_LIMITS
#include <sys/resource.h>
#endif

/*
	Description.
	sbox-fastcgi is designed to allow executing FastCGI applications in safe environment.
	It was developed for F12 company to allow executing users php scripts in strict environment with apropriate 
	UID/GID and in closed chroot.
	
	sbox-fastcgi was designed as a replacement for apache suexec.
	
	sbox-fastcgi makes series of tests to confirm that it has been called correctly
*/

/* a few statics from sbox */
#ifndef MAXPATHLEN
  #ifdef PATH_MAX
     #define MAXPATHLEN PATH_MAX
  #else
     #define MAXPATHLEN 8192
  #endif
#endif

#ifndef NAME_MAX
#define NAME_MAX 512
#endif

#define AP_ENVBUF 256


extern char **environ;
static FILE *log;


static void open_log();
static void log_error(const char* fmt, ...);
void debug(const char* fmt, ...);
static void check_consistency(char *, char *);
static char * check_target(char *);
static char * get_vhost_root(char *);
static void clean_env(void);
static char *get_stripped_cmd(char*, char*,char*, char*);

static char *safe_env_lst[] =
{
    "AUTH_TYPE",
    "CONTENT_LENGTH",
    "CONTENT_TYPE",
    "DATE_GMT",
    "DATE_LOCAL",
    "DOCUMENT_NAME",
    "DOCUMENT_PATH_INFO",
    "DOCUMENT_URI",
    "FILEPATH_INFO",
    "GATEWAY_INTERFACE",
    "LAST_MODIFIED",
    "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_URI",
    "SCRIPT_URL",
    "SERVER_ADMIN",
    "SERVER_NAME",
    "SERVER_PORT",
    "SERVER_PROTOCOL",
    "SERVER_SOFTWARE",
    "USER_NAME",
    "TZ",
#if !DO_CHROOT
    "SCRIPT_NAME",
    "PATH_INFO",
    "PATH_TRANSLATED",
    "DOCUMENT_ROOT",
    "SCRIPT_FILENAME",
#endif
    NULL
};


/**
	Main function
*/
int main(int argc, char *argv[]){
	
	char *cmd;
	char *prog;
	char *target_uname;
	char *target_gname;
	char *cwd;
	char *chroot_target;
	char *scmd;	//stripped cmd - under chroot
	uid_t uid;
	gid_t gid;
	
	
	if( argc < 4 ){
		log_error("Not enought parameters\n");
		exit(101);
	}//
	prog = argv[0];
	target_uname = argv[1];
	target_gname = argv[2];
	
	
	cmd = argv[3];
	

	//we open log file for logging
	open_log();	
	
//	log_error("Target UID/GID: (%d/%d)", target_uname, target_gname);
	// check consistency of application
	check_consistency(target_uname, target_gname);
	
	// check target permissions
	cwd = check_target(cmd);
	
	//get directory to chroot to
	chroot_target = get_vhost_root(cwd);
	
	scmd = get_stripped_cmd(cwd, chroot_target, cmd, scmd);
		
	debug("chroot in main(): %s\n", chroot_target);
	
	//fix environment
	clean_env();
	
	//set env for PHP interpreter
	setenv("PHPRC", PHPRC_PATH, 1);
	setenv("PHP_FCGI_MAX_REQUESTS", PHP_FCGI_MAX_REQUESTS, 1);
	setenv("PHP_FCGI_CHILDREN", PHP_FCGI_CHILDREN, 1);

	// do chroot
	if( chdir(chroot_target) != 0 ){
		log_error("couldn't change working directory to vhost root: %s (%s)\n", chroot_target, strerror(errno));
		exit(112);
	}//
	debug("Changed directory to: %s\n", getcwd(NULL, 0));

	
	if( chroot( chroot_target ) != 0 ){
		log_error("couldn't chroot() to %s (%s)\n", chroot_target, strerror(errno));
		
		exit(113);
	}//if chroot error
	
	
	// do setuid && setgid
	uid = (uid_t) atol( target_uname );
	gid = (gid_t) atol( target_gname );
	
	if( setgid(gid) != 0 ){
		log_error("couldn't setgid() to %d (%s)\n", gid, strerror(errno));
		exit(109);
	}//if setgid error
	if( setuid(uid) != 0 ){
		log_error("couldn't setuid() to %d (%s)\n", uid, strerror(errno));
		exit(110);
	}//if setuid error
	
	//execute FastCGI app
	debug("Execute: %s\n", cmd);
	
	execv(scmd, &argv[3]);
	
	
}//main

/**
	Function opens log file
	
	Needs setting LOG_FILE in header file
	
*/
void open_log () {
#ifdef LOG_FILE
  if (strlen(LOG_FILE) > 0) {
   if ((log = fopen(LOG_FILE, "a")) == NULL) {
      fprintf(stderr, "sbox failed to open log file.  Falling back to stderr.\n");
      log = stderr;
    }
  } else {
    log = stderr;
  }
#else
  log = NULL;
#endif
}//open_log

/**
	Function writes message to log file
*/
static void write_log(const char* fmt, va_list ap) {
  time_t    timevar;
  char*     timestr;
  
  if (log == NULL) return;

  timevar = time(NULL);
  timestr = ctime(&timevar);
  timestr[strlen(timestr)-1]='\0';
  
  fprintf(log,"[%s] sbox: ",timestr);
  vfprintf(log, fmt, ap);
  fflush(log);

  return;
}//write_log


/**
	Logs error message to log file
*/
void log_error(const char* fmt, ...) {
  va_list ap;

  va_start(ap, fmt);
  write_log(fmt,ap);
  va_end(ap);
}//log_error

void debug(const char* fmt, ...){
#ifdef _DEBUG
	va_list ap;

  	va_start(ap, fmt);
  	write_log(fmt,ap);
  	va_end(ap);
#endif
}//

/**
	Check consistency
	
	Function checks couple conditions to see if wrapper was properly executed
*/
void check_consistency(char * t_uname, char * t_gname) {
	struct passwd *pass;
	struct group *grp;
	uid_t uid;
	gid_t gid;
	
	/* get our group and user ids */
	uid = getuid();
	if( ( pass = getpwuid(uid) ) == NULL ){
		log_error("invalid uid (%ld)\n", uid);
		exit(102);
	}//if no uid info
	
	gid = getgid();
	if( ( grp = getgrgid(gid) ) == NULL ){
		log_error("invalid gid (%ld)\n", gid);
		exit(102);
	}//if no group info
	
	/* exit if executed by root */
	if( uid == 0 ){
		log_error("cannot be run as root\n");
		exit(103);
	}//if uid == 0
	
	if( gid == 0 ){
		log_error("cannot be run as root group\n");
		exit(103);
	}//if gid == 0
	
	/* make sure that we were invoken by the web user and group */
	if( strcmp(WEB_USER, pass->pw_name) ){
		log_error("invoked by %s, but must be run by %s\n", pass->pw_name, WEB_USER);
		exit(103);
	}//if not WEB_USER
	
	if( strcmp(WEB_GROUP, grp->gr_name) ){
		log_error("invoked by group %s, but must be run by group %s\n", grp->gr_name, WEB_GROUP);
		exit(103);
	}//if not WEB_GROUP
	
	
	
	/* switch uid/gid to target values */
	uid = (uid_t)atol(t_uname);
	gid = (gid_t)atol(t_gname);

	debug("Specified UID/GID for (%s/%s): (%d/%d)\n", t_uname, t_gname, uid, gid);	
	
	/* defined userid must be between UID_MIN and UID_MAX */
	if( ( uid < UID_MIN ) || ( uid > UID_MAX ) ){
		log_error("specified uid (%d) out of range (%d - %d)\n", uid, UID_MIN, UID_MAX);
		exit(107);
	}//if uid out of range
	
	/* defined gid must be between GID_MIN and GID_MAX */
	if( (gid < GID_MIN) || (gid > GID_MAX) ){
		log_error("specified gid (%d) out of range (%d - %d)\n", gid, GID_MIN, GID_MAX);
		exit(108);
	}//if gid out of range	
	
}//check_consistency


/**
	Function does some checking about target and other conditions	
*/
char * check_target(char *cmd){
	
	char *cwd = NULL;
	char *root;
	int i, slashes = 0, tmp;
	struct stat sarg, sdir;
	
	/* get FastCGI application path */
	if( (cwd = getcwd(cwd, MAXPATHLEN) ) == NULL ){
		log_error("cannot get current working directory");
		exit(111);
	}//
	
	debug("Current dir %s\n", cwd);
	
	/* check for an attempt to back up of the current directory */
	if( strstr(cwd, "/../") != NULL ){
		log_error("invalid path (%s)\n", cwd);
		exit(104);
	}//

	debug("Application to run: %s\n", cmd);
	
	/* stat target cwd and verify it is a directory */
	if( ( stat(cwd, &sdir) != 0 ) || !( S_ISDIR( sdir.st_mode ) ) ){
		log_error("Dir stat failed. (%s): (%s)\n", cwd, strerror(errno) );
		exit(115);
	}//stat directory and check if it is directory :)
	
	/* check if cwd is writable by others */
	if ( ( sdir.st_mode & S_IWOTH ) || ( sdir.st_mode & S_IWGRP ) ) {
		log_error("error: directory is world writable (%s)\n", cwd);
		exit(116);
	}//if cwd writable by others
	
	/* stat target program */
	if( ( stat( cmd, &sarg) != 0 ) || ( S_ISLNK(sarg.st_mode) ) ){
		log_error("Program stat failed. (%s): (%s)\n", cmd, strerror(errno) );
		exit(117);		
	}//stat program
	
	/* error if program is writable by others */
	if ( ( sarg.st_mode & S_IWOTH ) || ( sarg.st_mode & S_IWGRP ) ) {
		log_error("error: program is writable by others (%s)\n", cmd);
		exit(118);
	}//if cmdpath writable by others
	
	/* error if program is setuid or setgid */
	if (( sarg.st_mode & S_ISUID) || (sarg.st_mode & S_ISGID)) {  
		log_error("error: program is either setuid or setgid");
		exit(119);
	}//if cmdpath setuid or setgid
		
	/* error if program is not executable 
	 otherwise there will be only "Premature end of script headers... */
	if (!(sarg.st_mode & S_IXUSR)) { 
		log_error("error: file has no execute permission (%s/%s)\n", cwd, cmd);
		exit(121);
	}//if program not executable
	
	debug("Defined VHOSTS_ROOT: %s\n", VHOSTS_ROOT);
	
	/* we check if cwd is inside VHOSTS_ROOT */
	if( strncmp(cwd, VHOSTS_ROOT, strlen(VHOSTS_ROOT) ) != 0 ){
		log_error("command path (%s) is not inside vhosts_root (%s)\n", cwd, VHOSTS_ROOT);
		exit(114);
	}//if path not inside VHOSTS_ROOT
	
	
	return cwd;
	
}//check_target

/**
	Get virtual host directory

	FastCGI application is defined within apache VirtualHost directive and it is different
	for every virtualhost. Function gets app path i strip it to virtualhost "filesystem".
	All applications must be placed inside /home/vhosts directory.
*/
char * get_vhost_root( char * cwd){

	char * root;
	int i, slashes = 0;
	
	
	/* we check if cwd is inside VHOSTS_ROOT */
	if( strncmp(cwd, VHOSTS_ROOT, strlen(VHOSTS_ROOT) ) != 0 ){
		log_error("command path (%s) is not inside vhosts_root (%s)\n", cwd, VHOSTS_ROOT);
		exit(114);
	}//if path not inside VHOSTS_ROOT
	
	/* allocating memory for vhosts "filesystem" root */	
	if( ( root = (char *) calloc( sizeof(char), MAXPATHLEN) ) == NULL ){
		log_error("cannot allocate memory while calculating virtual host docroot");
		exit(105);
	}//

	/* get VHOSTS_ROOT and append Virtual Host directory */
	for( i = 0; i < strlen(cwd) && slashes < 4; i ++ ){
		root[i] = cwd[i];
		if( cwd[i] == '/' )
			slashes ++;
	}//for cwd len
	
	root[--i] = 0;		//without trailing slash
	
	debug("Calculated root: %s\n", root);
	
	return root;
	
}//get_vhost_root

/**
	Cleaning environment - code from Apache group suexec
*/
static void clean_env(void)
{
    char pathbuf[512];
    char **cleanenv;
    char **ep;
    int cidx = 0;
    int idx;

    /* While cleaning the environment, the environment should be clean.
     * (e.g. malloc() may get the name of a file for writing debugging info.
     * Bad news if MALLOC_DEBUG_FILE is set to /etc/passwd.  Sprintf() may be
     * susceptible to bad locale settings....)
     * (from PR 2790)
     */
    char **envp = environ;
    char *empty_ptr = NULL;

    environ = &empty_ptr; /* VERY safe environment */

    if ((cleanenv = (char **) calloc(AP_ENVBUF, sizeof(char *))) == NULL) {
        log_error("emerg: failed to malloc memory for environment\n");
	exit(120);
    }

    sprintf(pathbuf, "PATH=%s", SAFE_PATH);
    cleanenv[cidx] = strdup(pathbuf);
    cidx++;

    for (ep = envp; *ep && cidx < AP_ENVBUF-1; ep++) {
        for (idx = 0; safe_env_lst[idx]; idx++) {
            if (!strncmp(*ep, safe_env_lst[idx],
                         strlen(safe_env_lst[idx]))) {
		cleanenv[cidx] = *ep;
		cidx++;
                break;
            }
        }
    }

    cleanenv[cidx] = NULL;

    environ = cleanenv;
}//clean_env


/**
	Function calculates stripped path to FastCGI application inside chrooted
	filesystem.
*/
char * get_stripped_cmd(char *cwd, char *vhost, char *cmd, char *scmd){
	
	int i;
	// we look for offset to start copying from cwd
	
	i = strlen(cwd) - strlen(vhost); //vhost is without trailing slash
		
	if( ( scmd = (char*)calloc(sizeof(char), i + strlen(cmd) ) ) == NULL ){
		log_error("Cannot allocate memory for application command\n");
		exit(123);
	}//
	
	//copying from cwd what is longer than vhost gives as relative root
	scmd = strncat(scmd, cwd + strlen(vhost), i);	
	i = strlen(scmd);
	scmd[i] = '/';	//we have to put / because neither cmd nor vhost has one
	scmd[++i] = 0;	//and we add null for strncat
	
	//finaly put cmd at the end and voila ;)
	scmd = strncat(scmd, cmd, strlen(cmd));
	
	
	debug("SCMD: %s\n", scmd);
	
	return scmd;
	
	
}//
