/* This is a modified version of Apache suexec to prevent script kiddies on
  sdf from LOICing the Internet over Gopher. It is under the same Apache
  License.

  Cameron Kaiser (cdkaiser) and Stephen Jones (smj) @ sdf.org */

/* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements.  See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License.  You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/*
* suexec.c -- "Wrapper" support program for suEXEC behaviour for Apache
*
***********************************************************************
*
* NOTE! : DO NOT edit this code!!!  Unless you know what you are doing,
*         editing this code might open up your system in unexpected
*         ways to would-be crackers.  Every precaution has been taken
*         to make this code as safe as possible; alter it at your own
*         risk.
*
***********************************************************************
*
*
*/

/*
* GOPHERD_USER -- Define as the username under which Bucktooth normally
*               runs.  This is the only user allowed to execute
*               this program.
*/
#ifndef AP_GOPHERD_USER
#define AP_GOPHERD_USER "nobody"
#endif

/*
* UID_MIN -- Define this as the lowest UID allowed to be a target user
*            for suEXEC.  For most systems, 500 or 100 is common.
*/
#ifndef AP_UID_MIN
#define AP_UID_MIN 100
#endif

/*
* GID_MIN -- Define this as the lowest GID allowed to be a target group
*            for suEXEC.  For most systems, 100 is common.
*/
#ifndef AP_GID_MIN
#define AP_GID_MIN 100
#endif

/* Define the type of user dir you want, relative to ~ or a root path. */
/* #define AP_UP_RELATIVE_TO_USER_DIR 1 */
#define AP_UP_ROOT 1
#if defined(AP_UP_ROOT) && defined(AP_UP_RELATIVE_TO_USER_DIR)
#error pick one: _ROOT or _RELATIVE_TO_USER_DIR
#endif

/*
* USERDIR_SUFFIX -- Define to be the subdirectory under users'
*                   home directories where goEXEC access should
*                   be allowed.  All executables under this directory
*                   will be executable by goEXEC as the user so
*                   they should be "safe" programs. The directory
*                   should match with getpwent().
*/
#if !defined(AP_USERDIR_SUFFIX) && defined(AP_UP_RELATIVE_TO_USER_DIR)
#define AP_USERDIR_SUFFIX "public_html"
#endif

/*
* USERDIR_ROOT -- If users can have files under a master tree and
*                 not (just?) their own home directories, define this.
*                 Same security constraints as the above. This should
*                 be fully qualified.
*/
#if !defined(AP_USERDIR_ROOT) && defined(AP_UP_ROOT)
#define AP_USERDIR_ROOT "/ftp/pub/users"
#endif

/*
* LOG_EXEC -- Define this as a filename if you want all goEXEC
*             transactions and errors logged for auditing and
*             debugging purposes.
*
*/
/* Set this to where your Bucktooth should be logging. */
#if(0)
#define DEFAULT_EXP_LOGFILEDIR "/arpa/af/c/cdkaiser"
#define AP_LOG_EXEC DEFAULT_EXP_LOGFILEDIR "/goexec_log"
#else
#define DEFAULT_EXP_LOGFILEDIR "/var/log"
#define AP_LOG_EXEC DEFAULT_EXP_LOGFILEDIR "/sugopher.log"
#endif


/*
* DOC_ROOT -- Define as the Bucktooth mountpoint. This
*             will be the only hierarchy (aside from user home dirs)
*             that can be used for goEXEC behavior.
*/
#define AP_DOC_ROOT "/ftp/pub"

/*
* SAFE_PATH -- Define a safe PATH environment to pass to CGI executables.
*
*/
#ifndef AP_SAFE_PATH
#define AP_SAFE_PATH "/bin:/usr/bin:/usr/pkg/bin:/usr/local/bin"
#endif

/* Change if you are not POSIX. */
#define _OSD_POSIX
/* Use this if your system has ufork(), or you are compiling with ufork.c. */
/* #define USE_UFORK */
/* Use this if your system can't reliably run #! scripts. This is rare now. */
/* #define NEED_HASHBANG_EMUL */
/* Comment out or define depending on your system. This suffices for most. */
#define HAVE_UNISTD_H
#define HAVE_FCNTL_H
#define HAVE_PWD_H
#define HAVE_GRP_H

/********************************************\
* NO USER SERVICEABLE PARTS BELOW THIS POINT *
\********************************************/

#include <sys/param.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif

#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#ifdef HAVE_FCNTL_H
#include <fcntl.h>
#endif

#ifdef HAVE_PWD_H
#include <pwd.h>
#endif

#ifdef HAVE_GRP_H
#include <grp.h>
#endif

#if defined(PATH_MAX)
#define AP_MAXPATH PATH_MAX
#elif defined(MAXPATHLEN)
#define AP_MAXPATH MAXPATHLEN
#else
#define AP_MAXPATH 8192
#endif

#define AP_ENVBUF 256

extern char **environ;
static FILE *log = NULL;

static const char *const safe_env_lst[] =
{
   /* variable name starts with */
   "GOPHER_",

   /* variable name is */
   "AUTH_TYPE=",
   "CONTENT_LENGTH=",
   "CONTENT_TYPE=",
   "CONTEXT_DOCUMENT_ROOT=",
   "CONTEXT_PREFIX=",
   "DATE_GMT=",
   "DATE_LOCAL=",
   "DOCUMENT_NAME=",
   "DOCUMENT_PATH_INFO=",
   "DOCUMENT_ROOT=",
   "DOCUMENT_URI=",
   "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_ERROR_NOTES=",
   "REDIRECT_HANDLER=",
   "REDIRECT_QUERY_STRING=",
   "REDIRECT_REMOTE_USER=",
   "REDIRECT_SCRIPT_FILENAME=",
   "REDIRECT_STATUS=",
   "REDIRECT_URL=",
   "REQUEST_METHOD=",
   "REQUEST_URI=",
   "REQUEST_SCHEME=",
   "SCRIPT_FILENAME=",
   "SCRIPT_NAME=",
   "SCRIPT_URI=",
   "SCRIPT_URL=",
   "SERVER_ADMIN=",
   "SERVER_NAME=",
   "SERVER_ADDR=",
   "SERVER_PORT=",
   "SERVER_PROTOCOL=",
   "SERVER_SIGNATURE=",
   "SERVER_SOFTWARE=",
   "UNIQUE_ID=",
   "USER_NAME=",
   "TZ=",
   NULL
};


static void err_output(int is_error, const char *fmt, va_list ap)
{
#ifdef AP_LOG_EXEC
   time_t timevar;
   struct tm *lt;

   if (!log) {
#if defined(_LARGEFILE64_SOURCE) && HAVE_FOPEN64
       if ((log = fopen64(AP_LOG_EXEC, "a")) == NULL) {
#else
       if ((log = fopen(AP_LOG_EXEC, "a")) == NULL) {
#endif
           fprintf(stderr, "suexec failure: could not open log file\n");
           perror("fopen");
           exit(1);
       }
   }

   if (is_error) {
       fprintf(stderr, "suexec policy violation: see suexec log for more "
                       "details\n");
   }

   time(&timevar);
   lt = localtime(&timevar);

   fprintf(log, "[%d-%.2d-%.2d %.2d:%.2d:%.2d]: ",
           lt->tm_year + 1900, lt->tm_mon + 1, lt->tm_mday,
           lt->tm_hour, lt->tm_min, lt->tm_sec);

   vfprintf(log, fmt, ap);

   fflush(log);
#endif /* AP_LOG_EXEC */
   return;
}

static void log_err(const char *fmt,...)
{
#ifdef AP_LOG_EXEC
   va_list ap;

   va_start(ap, fmt);
   err_output(1, fmt, ap); /* 1 == is_error */
   va_end(ap);
#endif /* AP_LOG_EXEC */
   return;
}

static void log_no_err(const char *fmt,...)
{
#ifdef AP_LOG_EXEC
   va_list ap;

   va_start(ap, fmt);
   err_output(0, fmt, ap); /* 0 == !is_error */
   va_end(ap);
#endif /* AP_LOG_EXEC */
   return;
}

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 */

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

   sprintf(pathbuf, "PATH=%s", AP_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;
}

int main(int argc, char *argv[])
{
   int userdir = 0;        /* ~userdir flag             */
   uid_t uid;              /* user information          */
   gid_t gid;              /* target group placeholder  */
   char *target_uname;     /* target user name          */
   char *target_gname;     /* target group name         */
   char *target_homedir;   /* target home directory     */
   char *actual_uname;     /* actual user name          */
   char *actual_gname;     /* actual group name         */
   char *cmd;              /* command to be executed    */
   char cwd[AP_MAXPATH];   /* current working directory */
   char dwd[AP_MAXPATH];   /* docroot working directory */
   struct passwd *pw;      /* password entry holder     */
   struct group *gr;       /* group entry holder        */
   struct stat dir_info;   /* directory info holder     */
   struct stat prg_info;   /* program info holder       */

   /*
    * Start with a "clean" environment
    */
   clean_env();

   /*
    * Check existence/validity of the UID of the user
    * running this program.  Error out if invalid.
    */
   uid = getuid();
   if ((pw = getpwuid(uid)) == NULL) {
       log_err("crit: invalid uid: (%ld)\n", uid);
       exit(102);
   }
   /*
    * See if this is a 'how were you compiled' request, and
    * comply if so.
    * Not supported
    */
#if(0)
   if ((argc > 1)
       && (! strcmp(argv[1], "-V"))
       && ((uid == 0)
#ifdef _OSD_POSIX
       /* User name comparisons are case insensitive on BS2000/OSD */
           || (! strcasecmp(AP_GOPHERD_USER, pw->pw_name)))
#else  /* _OSD_POSIX */
           || (! strcmp(AP_GOPHERD_USER, pw->pw_name)))
#endif /* _OSD_POSIX */
       ) {
#ifdef AP_DOC_ROOT
       fprintf(stderr, " -D AP_DOC_ROOT=\"%s\"\n", AP_DOC_ROOT);
#endif
#ifdef AP_GID_MIN
       fprintf(stderr, " -D AP_GID_MIN=%d\n", AP_GID_MIN);
#endif
#ifdef AP_GOPHERD_USER
       fprintf(stderr, " -D AP_GOPHERD_USER=\"%s\"\n", AP_GOPHERD_USER);
#endif
#ifdef AP_LOG_EXEC
       fprintf(stderr, " -D AP_LOG_EXEC=\"%s\"\n", AP_LOG_EXEC);
#endif
#ifdef AP_SAFE_PATH
       fprintf(stderr, " -D AP_SAFE_PATH=\"%s\"\n", AP_SAFE_PATH);
#endif
#ifdef AP_SUEXEC_UMASK
       fprintf(stderr, " -D AP_SUEXEC_UMASK=%03o\n", AP_SUEXEC_UMASK);
#endif
#ifdef AP_UID_MIN
       fprintf(stderr, " -D AP_UID_MIN=%d\n", AP_UID_MIN);
#endif
#ifdef AP_USERDIR_SUFFIX
       fprintf(stderr, " -D AP_USERDIR_SUFFIX=\"%s\"\n", AP_USERDIR_SUFFIX);
#endif
       exit(0);
   }
#endif
   /*
    * If there are a proper number of arguments, set
    * all of them to variables.  Otherwise, error out.
    */
   if (argc < 4) {
       log_err("too few arguments\n");
       exit(101);
   }
   target_uname = argv[1];
   target_gname = argv[2];
   cmd = argv[3];

   /*
    * Check to see if the user running this program
    * is the user allowed to do so as defined in
    * suexec.h.  If not the allowed user, error out.
    */
#ifdef _OSD_POSIX
   /* User name comparisons are case insensitive on BS2000/OSD */
   if (strcasecmp(AP_GOPHERD_USER, pw->pw_name)) {
       log_err("user mismatch (%s instead of %s)\n", pw->pw_name, AP_GOPHERD_USER);
       exit(103);
   }
#else  /*_OSD_POSIX*/
   if (strcmp(AP_GOPHERD_USER, pw->pw_name)) {
       log_err("user mismatch (%s instead of %s)\n", pw->pw_name, AP_GOPHERD_USER);
       exit(103);
   }
#endif /*_OSD_POSIX*/

   /*
    * Check for a leading '/' (absolute path) in the command to be executed,
    * or attempts to back up out of the current directory,
    * to protect against attacks.  If any are
    * found, error out.  Naughty naughty crackers.
    */
   if ((cmd[0] == '/') || (!strncmp(cmd, "../", 3))
       || (strstr(cmd, "/../") != NULL)) {
       log_err("invalid command (%s)\n", cmd);
       exit(104);
   }

   /*
    * Check to see if this is a ~userdir request.  If
    * so, set the flag, and remove the '~' from the
    * target username.
    */
#ifdef AP_UP_RELATIVE_TO_USER_DIR
   if (!strncmp("~", target_uname, 1)) {
       target_uname++;
       userdir = 1;
   }
#endif

   /*
    * Error out if the target username is invalid.
    * getpwnam() on NetBSD doesn't search YP (suck!) so we give it
    * numeric UIDs.
    */
   if (strspn(target_uname, "1234567890") != strlen(target_uname)) {
       if ((pw = getpwnam(target_uname)) == NULL) {
           log_err("invalid target user name: (%s)\n", target_uname);
           exit(105);
       }
   }
   else {
       if ((pw = getpwuid(atoi(target_uname))) == NULL) {
           log_err("invalid target user id: (%s)\n", target_uname);
           exit(121);
       }
   }

   /*
    * Error out if the target group name is invalid.
    */
   if (strspn(target_gname, "1234567890") != strlen(target_gname)) {
       if ((gr = getgrnam(target_gname)) == NULL) {
           log_err("invalid target group name: (%s)\n", target_gname);
           exit(106);
       }
   }
   else {
       if ((gr = getgrgid(atoi(target_gname))) == NULL) {
           log_err("invalid target group id: (%s)\n", target_gname);
           exit(106);
       }
   }
   gid = gr->gr_gid;
   actual_gname = strdup(gr->gr_name);

#ifdef USE_UFORK
   /*
    * Initialize BS2000 user environment
    */
   {
       pid_t pid;
       int status;

       switch (pid = ufork(target_uname)) {
       case -1:    /* Error */
           log_err("failed to setup bs2000 environment for user %s: %s\n",
                   target_uname, strerror(errno));
           exit(150);
       case 0:     /* Child */
           break;
       default:    /* Father */
           while (pid != waitpid(pid, &status, 0))
               ;
           /* @@@ FIXME: should we deal with STOP signals as well? */
           if (WIFSIGNALED(status)) {
               kill (getpid(), WTERMSIG(status));
           }
           exit(WEXITSTATUS(status));
       }
   }
#endif /* USE_UFORK */

   /*
    * Save these for later since initgroups will hose the struct
    */
   uid = pw->pw_uid;
   actual_uname = strdup(pw->pw_name);
   target_homedir = strdup(pw->pw_dir);

   /*
    * Log the transaction here to be sure we have an open log
    * before we setuid().
    */
   log_no_err("uid: (%s/%s) gid: (%s/%s) cmd: %s\n",
              target_uname, actual_uname,
              target_gname, actual_gname,
              cmd);

   /*
    * Error out if attempt is made to execute as root or as
    * a UID less than AP_UID_MIN.  Tsk tsk.
    */
   if ((uid == 0) || (uid < AP_UID_MIN)) {
       log_err("cannot run as forbidden uid (%d/%s)\n", uid, cmd);
       exit(107);
   }

   /*
    * Error out if attempt is made to execute as root group
    * or as a GID less than AP_GID_MIN.  Tsk tsk.
    */
   if ((gid == 0) || (gid < AP_GID_MIN)) {
       log_err("cannot run as forbidden gid (%d/%s)\n", gid, cmd);
       exit(108);
   }

#if(0)
   /*
    * Change UID/GID here so that the following tests work over NFS.
    *
    * Initialize the group access list for the target user,
    * and setgid() to the target group. If unsuccessful, error out.
    */
   if (((setgid(gid)) != 0) || (initgroups(actual_uname, gid) != 0)) {
       log_err("failed to setgid (%ld: %s)\n", gid, cmd);
       exit(109);
   }

   /*
    * setuid() to the target user.  Error out on fail.
    */
   if ((setuid(uid)) != 0) {
       log_err("failed to setuid (%ld: %s)\n", uid, cmd);
       exit(110);
   }
#endif

   /*
    * Get the current working directory, as well as the proper
    * document root (dependant upon whether or not it is a
    * ~userdir request).  Error out if we cannot get either one,
    * or if the current working directory is not in the docroot.
    * Use chdir()s and getcwd()s to avoid problems with symlinked
    * directories.  Yuck.
    */
   if (getcwd(cwd, AP_MAXPATH) == NULL) {
       log_err("cannot get current working directory\n");
       exit(111);
   }

   if (userdir) {
       if (((chdir(target_homedir)) != 0) ||
#ifdef AP_USERDIR_SUFFIX
           ((chdir(AP_USERDIR_SUFFIX)) != 0) ||
#endif
           ((getcwd(dwd, AP_MAXPATH)) == NULL) ||
           ((chdir(cwd)) != 0)) {
           log_err("cannot get docroot information (%s)\n", target_homedir);
           exit(112);
       }
   }
   else {
       if (((chdir(AP_DOC_ROOT)) != 0) ||
           ((getcwd(dwd, AP_MAXPATH)) == NULL) ||
           ((chdir(cwd)) != 0)) {
           log_err("cannot get docroot information (%s)\n", AP_DOC_ROOT);
           exit(113);
       }
   }

   if ((strncmp(cwd, dwd, strlen(dwd))) != 0) {
       log_err("command not in docroot (%s/%s)\n", cwd, cmd);
       exit(114);
   }

   /*
    * Stat the cwd and verify it is a directory, or error out.
    */
   if (((lstat(cwd, &dir_info)) != 0) || !(S_ISDIR(dir_info.st_mode))) {
       log_err("cannot stat directory: (%s)\n", cwd);
       exit(115);
   }

   /*
    * Error out if cwd is writable by others.
    */
   if ((dir_info.st_mode & S_IWOTH) || (dir_info.st_mode & S_IWGRP)) {
       log_err("directory is writable by others: (%s)\n", cwd);
       exit(116);
   }

   /*
    * Error out if we cannot stat the program.
    */
   if (((lstat(cmd, &prg_info)) != 0) || (S_ISLNK(prg_info.st_mode))) {
       log_err("cannot stat program: (%s)\n", cmd);
       exit(117);
   }

   /*
    * Error out if the program is writable by others.
    */
   if ((prg_info.st_mode & S_IWOTH) || (prg_info.st_mode & S_IWGRP)) {
       log_err("file is writable by others: (%s/%s)\n", cwd, cmd);
       exit(118);
   }

   /*
    * Error out if the file is setuid or setgid.
    */
   if ((prg_info.st_mode & S_ISUID) || (prg_info.st_mode & S_ISGID)) {
       log_err("file is either setuid or setgid: (%s/%s)\n", cwd, cmd);
       exit(119);
   }

   /*
    * Error out if the target name/group is different from
    * the name/group of the cwd or the program.
    */
   if ((uid != dir_info.st_uid) ||
       (gid != dir_info.st_gid) ||
       (uid != prg_info.st_uid) ||
       (gid != prg_info.st_gid)) {
       log_err("target uid/gid (%ld/%ld) mismatch "
               "with directory (%ld/%ld) or program (%ld/%ld)\n",
               uid, gid,
               dir_info.st_uid, dir_info.st_gid,
               prg_info.st_uid, prg_info.st_gid);
       exit(120);
   }
   /*
    * Error out if the program is not executable for the user.
    * Otherwise, she won't find any error in the logs except for
    * "[error] Premature end of script headers: ..."
    */
   if (!(prg_info.st_mode & S_IXUSR)) {
       log_err("file has no execute permission: (%s/%s)\n", cwd, cmd);
       exit(121);
   }

#ifdef AP_SUEXEC_UMASK
   /*
    * umask() uses inverse logic; bits are CLEAR for allowed access.
    */
   if ((~AP_SUEXEC_UMASK) & 0022) {
       log_err("notice: AP_SUEXEC_UMASK of %03o allows "
               "write permission to group and/or other\n", AP_SUEXEC_UMASK);
   }
   umask(AP_SUEXEC_UMASK);
#endif /* AP_SUEXEC_UMASK */

   /* Be sure to close the log file so the CGI can't mess with it. */
   if (log != NULL) {
#ifdef HAVE_FCNTL_H
       /*
        * ask fcntl(2) to set the FD_CLOEXEC flag on the log file,
        * so it'll be automagically closed if the exec() call succeeds.
        */
       fflush(log);
       setbuf(log, NULL);
       if ((fcntl(fileno(log), F_SETFD, FD_CLOEXEC) == -1)) {
           log_err("error: can't set close-on-exec flag");
           exit(122);
       }
#else
       /*
        * In this case, exec() errors won't be logged because we have already
        * dropped privileges and won't be able to reopen the log file.
        */
       fclose(log);
       log = NULL;
#endif
   }

   /*
    * Execute the command, replacing our image with its own.
    */
#ifdef NEED_HASHBANG_EMUL
   /* We need the #! emulation when we want to execute scripts */
   {
       extern char **environ;

       ap_execve(cmd, &argv[3], environ);
   }
#else /*NEED_HASHBANG_EMUL*/
   execv(cmd, &argv[3]);
#endif /*NEED_HASHBANG_EMUL*/

   /*
    * (I can't help myself...sorry.)
    *
    * Uh oh.  Still here.  Where's the kaboom?  There was supposed to be an
    * EARTH-shattering kaboom!
    *
    * Oh well, log the failure and error out.
    */
   log_err("(%d)%s: exec failed (%s)\n", errno, strerror(errno), cmd);
   exit(255);
}