/*
 * Copyright (c) Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2018 All Rights Reserved
 *
 * Licensed under CLOUD LINUX LICENSE AGREEMENT
 * http://cloudlinux.com/docs/LICENSE.TXT
 *
 * The part the support library for lsapi & proxy_lsapi Apache modules
 * author Alexander Demeshko <ademeshko@cloudlinux.com>
 *
 */
#include <lscapi_util.h>

#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>

#ifdef SULSPHP_MAXPATH
#undef SULSPHP_MAXPATH
#endif // SULSPHP_MAXPATH

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

/*
 * HTTPD_USER -- Define as the username under which Apache normally
 *               runs.  This is the only user allowed to execute
 *               this program.
 */
#ifndef SULSPHP_HTTPD_USER

#ifdef AP_HTTPD_USER
#define SULSPHP_HTTPD_USER AP_HTTPD_USER
#else
#define SULSPHP_HTTPD_USER "nobody"
#endif //AP_HTTPD_USER

#endif //SULSPHP_HTTPD_USER

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

#ifdef AP_UID_MIN
#define SULSPHP_UID_MIN AP_UID_MIN
#else
#define SULSPHP_UID_MIN 100
#endif //AP_UID_MIN

#endif //SULSPHP_UID_MIN

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

#ifdef AP_GID_MIN
#define SULSPHP_GID_MIN AP_GID_MIN
#else
#define SULSPHP_GID_MIN 100
#endif //AP_GID_MIN

#endif //SULSPHP_GID_MIN

/*
 * DOC_ROOT -- Define as the DocumentRoot set for Apache.  This
 *             will be the only hierarchy (aside from UserDirs)
 *             that can be used for su behavior.
 */
#ifndef SULSPHP_DOC_ROOT

#ifdef AP_DOC_ROOT
#define SULSPHP_DOC_ROOT AP_DOC_ROOT
#else
#define SULSPHP_DOC_ROOT "/"
#endif //AP_DOC_ROOT

#endif //SULSPHP_DOC_ROOT

/*
 * SAFE_PATH -- Define a safe PATH environment to pass to executable.
 *
 */
#ifndef SULSPHP_SAFE_PATH

#ifdef AP_SAFE_PATH
#define SULSPHP_SAFE_PATH AP_SAFE_PATH
#else
#define SULSPHP_SAFE_PATH "/usr/local/bin:/usr/bin:/bin"
#endif //AP_SAFE_PATH

#endif //SULSPHP_SAFE_PATH

/*
 * ALLOWED_CMD -- Define the only allowed executable.
 *
 */
#ifndef SULSPHP_ALLOWED_CMD
#define SULSPHP_ALLOWED_CMD "lsphp"
#endif // SULSPHP_ALLOWED_CMD

/*
 * ALLOWED_DIR -- Define a allowable directory.
 *
 */
#ifndef SULSPHP_ALLOWED_DIR
#define SULSPHP_ALLOWED_DIR "/usr/local/bin"
#endif // SULSPHP_ALLOWED_DIR

/*
 * SAFE_DIR -- Define a safe directory.
 *
 */
#ifndef SULSPHP_SAFE_DIR
#define SULSPHP_SAFE_DIR "/opt/alt"
#endif // SULSPHP_SAFE_DIR

/*
 * CPANELSAFE_DIR -- Define a safe directory for cPanel.
 *
 */
#ifndef SULSPHP_CPANEL_SAFE_DIR
#define SULSPHP_CPANEL_SAFE_DIR "/opt/cpanel"
#endif // SULSPHP_SAFE_DIR

#ifndef SULSPHP_CLOUD_SAFE_DIR
#define SULSPHP_CLOUD_SAFE_DIR "/opt/cloudlinux"
#endif

#ifdef SULSPHP_UMASK
#undef SULSPHP_UMASK
#endif // SULSPHP_UMASK
#define SULSPHP_UMASK 022

#define APACHE_DEFAULT_UID 48
#define NOBODY_DEFAULT_UID 99

#define APACHE_DEFAULT_GID 48
#define NOBODY_DEFAULT_GID 99

static const char prefix[] = "sulsphp:";

void starter_log_error(server_rec *s, int errnum, const char *fmt, ...)
                        __attribute__((format(printf,3,4)));

static void make_custom_env(server_rec *s, apr_table_t *envTable,
                            spawn_info_t *spawn_info,
                            char **origEnv,
                            char *buf, size_t bufLen,
                            int *envLenPtr, int *envNumPtr)
{
    const apr_array_header_t *arr = apr_table_elts(envTable);
    const apr_table_entry_t *elt = (apr_table_entry_t *)arr->elts;
    int i, j;
    int found_PWD = (spawn_info->pwd_disabled) ? 1 : 0; /* even do not try to find it if it's disabled */
    size_t usedLen = 0;
    size_t keyLen;
    size_t valLen;
    const char *keyPtr;
    const char *valPtr;

    for (i = 0; i < arr->nelts; ++i) {
        keyLen = strlen(elt[i].key);
        valLen = strlen(elt[i].val);
        keyPtr = elt[i].key;
        valPtr = elt[i].val;

        if (valLen > 3 && valPtr[0] == '$' && valPtr[1] == '{' && valPtr[valLen-1] == '}') {
            // special case, substitute val with value from origEnv
            const char *findKey = valPtr + 2;
            int findKeyLen = valLen - 3;
            int foundIt = 0;
            for (j = 0; origEnv[j] != NULL; j++) {
                int curLen = strlen(origEnv[j]);
                if (curLen > findKeyLen
                    && memcmp(origEnv[j], findKey, findKeyLen) == 0
                    && origEnv[j][findKeyLen] == '=')
                {
                    valPtr = origEnv[j] + findKeyLen + 1;
                    valLen = curLen - findKeyLen - 1;
                    foundIt = 1;
                    break;
                }
            }
            if (!foundIt) {
                valPtr = "";
                valLen = 0;
            }
        }

        // no space even for current env in format KEY=VAL<NUL> - ignore all
        if(bufLen - usedLen < keyLen + valLen + 2)
            return;
        //lscapi_log(APLOG_NOTICE, 0, s, "make_custom_env: %d: key(%s); val(%s); len %lu/%lu",
        //           i, elt[i].key, elt[i].val, usedLen, bufLen);
        if(!found_PWD && keyLen == 3 && memcmp(keyPtr, "PWD", 3) == 0)
        {
            found_PWD = 1;
            // If PWD is present in the custom env from lsapi.conf, it still will be copied into backend env
        }

        memcpy(buf+usedLen, keyPtr, keyLen); usedLen += keyLen;
        buf[usedLen++] = '=';
        memcpy(buf+usedLen, valPtr, valLen+1); usedLen += valLen+1;
    }

    *envNumPtr = arr->nelts;

    // PWD won't be created automatically, when pwd_disabled is set
    if(!found_PWD && !spawn_info->pwd_disabled)
    {
        keyLen = 3;
        valLen = strlen(SULSPHP_DOC_ROOT);
        keyPtr = "PWD";
        valPtr = SULSPHP_DOC_ROOT;
        if(bufLen - usedLen >= keyLen + valLen + 2)
        {
            memcpy(buf+usedLen, keyPtr, keyLen); usedLen += keyLen;
            buf[usedLen++] = '=';
            memcpy(buf+usedLen, valPtr, valLen+1); usedLen += valLen+1;
            (*envNumPtr)++;
        }
    }

    *envLenPtr = usedLen;
}

static const char *safe_dirs[] = { SULSPHP_SAFE_DIR, SULSPHP_CPANEL_SAFE_DIR, SULSPHP_CLOUD_SAFE_DIR };
static const int  safe_dir_num = 3;

static void lscapi_set_spawn_context(spawn_context_t *context, int restore, int log_file)
{
    context->log_sock = log_file;
    context->restore  = restore;
}

pid_t lscapi_spawn_backend_lsphp(server_rec *s,
                                spawn_info_t *spawn_info,
                                int restore,
                                int log_file)
{
    spawn_context_t spawn_context = {0};

    if (restore) {
        lscapi_set_spawn_context(&spawn_context, 1, log_file);
        return lscapi_spawn_backend(spawn_info, &spawn_context);
    }

    int envLen = 0, envNum = 0;
    char errbuf[512];
    char custom_env[2048];
    const char *path = NULL;
    char **lsphpEnv = NULL;
    int rc = 0;
    uid_t real_uid;              /* real user information     */
    struct passwd real_pw;
    char real_pw_storage[16384];
    struct passwd *indicator_pw;
    char cwd[SULSPHP_MAXPATH];   /* current working directory */
    char dwd[SULSPHP_MAXPATH];   /* docroot working directory */

    lsapi_svr_conf_t *cfg = lsapi_get_svr_config(s);

    make_custom_env(s, cfg->envTable, spawn_info, ENVIRONMENT, custom_env, sizeof custom_env, &envLen, &envNum);

    path = cfg->backend_env_path ? cfg->backend_env_path : SULSPHP_SAFE_PATH;

    lsphpEnv = lscapi_prepare_env(spawn_info, custom_env, envLen, envNum, path, errbuf, sizeof errbuf);
    if(lsphpEnv == NULL) {
       lscapi_log(APLOG_ERR, errno, s, "%s prepare_env failed: %s", "sulsphp :", errbuf);
       return -1;
    }

    char **oldEnv = ENVIRONMENT;
    ENVIRONMENT = lsphpEnv;

#ifdef SULSPHP_UMASK
    /*
     * umask() uses inverse logic; bits are CLEAR for allowed access.
     */
    umask(SULSPHP_UMASK);
#endif /* SULSPHP_UMASK */

    real_uid = getuid();
    if (real_uid <= 0) {
        starter_log_error(s, rc, "%s wrong real uid:(%d)", prefix, (int)real_uid);
        return -1;
    }

    /*
     * Check existence/validity of the UID of the user
     * running this program.  Error out if invalid.
     */
    rc = getpwuid_r(real_uid, &real_pw, real_pw_storage, sizeof real_pw_storage, &indicator_pw);
    if (rc != 0) {
        starter_log_error(s, rc, "%s could not get user info uid:(%u)", prefix, real_uid);
        return -1;
    }

    if (indicator_pw == NULL) {
        starter_log_error(s, -1, "%s invalid uid:(%u)", prefix, real_uid);
        return -1;
    }

    uid_t apache_uid;
#if MODULE_MAGIC_NUMBER_MAJOR > 20081212
    apache_uid = ap_unixd_config.user_id;
#else
    apache_uid = unixd_config.user_id;
#endif
    // no check for apache_uid>0 - comparison with real_uid just failed in this case

    /*
     * Check to see if the user running this program
     * is the user allowed to do so -
     * whether it is the same as Apache config user (from unixd)
     * or the same as defined in SULSPHP_HTTPD_USER
     */
    if (real_uid != apache_uid
        && strcmp(SULSPHP_HTTPD_USER, real_pw.pw_name)) {
        starter_log_error(s, -1, "%s uid mismatch (%d vs %d) and user mismatch (%s vs %s)",
                          prefix, (int) real_uid, (int) apache_uid,
                          real_pw.pw_name, SULSPHP_HTTPD_USER);
        return -1;
    }

    /*
     * Check for an allowable command.
     */
    size_t allowed_cmd_len = strlen(SULSPHP_ALLOWED_CMD);
    size_t cmd_len = strlen(spawn_info->cmd);
    if (  (cmd_len < (allowed_cmd_len+1))
       || (spawn_info->cmd[cmd_len - allowed_cmd_len - 1] != '/')
       || (strncmp(spawn_info->cmd + cmd_len - allowed_cmd_len, SULSPHP_ALLOWED_CMD, allowed_cmd_len)) )
    {
        starter_log_error(s, -1, "%s uid:%u; gid:%u; invalid command (%s)",
                          prefix, spawn_info->real_uid, spawn_info->real_gid, spawn_info->cmd);
        return -1;
    }

    /*
     * Check for an allowable (or safe) directory.
     */
    size_t allowed_dir_len = strlen(SULSPHP_ALLOWED_DIR);
    if ( (cmd_len != (allowed_dir_len + allowed_cmd_len + 1))
         || strncmp(spawn_info->cmd, SULSPHP_ALLOWED_DIR, allowed_dir_len)
         || (spawn_info->cmd[allowed_dir_len] != '/') )
    {
        int i;
        for (i = 0; i < safe_dir_num; i++) {
            size_t safe_dir_len = strlen(safe_dirs[i]);
            if  (!((cmd_len < safe_dir_len + allowed_cmd_len + 1)
                  || strncmp(spawn_info->cmd, safe_dirs[i], safe_dir_len)
                  || (spawn_info->cmd[safe_dir_len] != '/')))
            {
                break;
            }
        }
        if (i >= safe_dir_num) {
            starter_log_error(s, -1, "%s uid:%u; gid:%u; invalid command (%s)",
                              prefix, spawn_info->real_uid, spawn_info->real_gid, spawn_info->cmd);
            return -1;
        }
    }

    /*
     * Error out if attempt is made to execute as root or as
     * a UID less than SULSPHP_UID_MIN.
     * Though default good known apache and nobody uids are allowed.
     */
    if ( (spawn_info->real_uid == 0) ||
         ( (spawn_info->real_uid != APACHE_DEFAULT_UID) &&
           (spawn_info->real_uid != NOBODY_DEFAULT_UID) &&
           (spawn_info->real_uid <  SULSPHP_UID_MIN) ) )
    {
        starter_log_error(s, -1, "%s cannot run as forbidden uid (%u/%s)",
                          prefix, spawn_info->real_uid, spawn_info->cmd);
        return -1;
    }

    /*
     * Error out if attempt is made to execute as root group
     * or as a GID less than SULSPHP_GID_MIN.
     * Though default good known apache and nobody gids are allowed.
     */
    if ( (spawn_info->real_gid == 0) ||
         ( (spawn_info->real_gid != APACHE_DEFAULT_GID) &&
           (spawn_info->real_gid != NOBODY_DEFAULT_GID) &&
           (spawn_info->real_gid <  SULSPHP_GID_MIN) ) )
    {
        starter_log_error(s, -1, "%s cannot run as forbidden gid (%u/%s)",
                          prefix, spawn_info->real_gid, spawn_info->cmd);
        return -1;
    }

    /*
     * Get the current working directory, as well as the proper
     * document root. Error out if we cannot get either one,
     * or if the current working directory is not in the docroot.
     */
    if (getcwd(cwd, SULSPHP_MAXPATH) == NULL) {
        starter_log_error(s, errno, "%s uid:%u; gid:%u; getcwd failed",
                          prefix, spawn_info->real_uid, spawn_info->real_gid);
        return -1;
    }

    if ( ((chdir(SULSPHP_DOC_ROOT)) != 0) ||
         ((getcwd(dwd, SULSPHP_MAXPATH)) == NULL) ||
         ((chdir(cwd)) != 0) )
    {
        starter_log_error(s, -1, "%s uid:%u; gid:%u; cannot get docroot information (%s)",
                          prefix, spawn_info->real_uid, spawn_info->real_gid, SULSPHP_DOC_ROOT);
        return -1;
    }

    lscapi_set_spawn_context(&spawn_context, 0, log_file);

    if (lscapi_spawn_backend(spawn_info, &spawn_context) < 0) {
        ENVIRONMENT = oldEnv;
        starter_log_error(s, -1, "%s uid:%u; gid:%u; Error spawn_backend_lsphp",
                          prefix, spawn_info->real_uid, spawn_info->real_gid);
        return -1;
    }
    return 0;
}
