/*
 * Copyright 2014 Cloud Linux Zug GmbH
 *
 * 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 <unixd.h>
#include <ap_mpm.h>
#include <apr_thread_proc.h>
#include <apr_strings.h>
#include <apr_queue.h>
#include <apr_global_mutex.h>
#include <apr_version.h>
#if APR_MAJOR_VERSION < 2
#include <apr_support.h>
#endif
#include <http_config.h>


#if MODULE_MAGIC_NUMBER_MAJOR >= 20090209
#include "mod_unixd.h"
#endif

#if MODULE_MAGIC_NUMBER_MAJOR < 20081201
#define ap_unixd_config unixd_config
#define ap_unixd_setup_child unixd_setup_child
#endif

struct lsapi_starter_command {
    lscapi_argvs_descr_t argv_descr;
    lscapi_envs_descr_t env_descr;
    char argv_buffer[MAX_BACKEND_ARGS_BUFFER];
    char env_buffer[MAX_BACKEND_ENVS_BUFFER];
    unsigned force_start: 1;
};
typedef struct lsapi_starter_command lsapi_starter_command;

struct lsapi_starter_notify {
    pid_t pid;
    int err;
};
typedef struct lsapi_starter_notify lsapi_starter_notify;


/* The APR other-child API doesn't tell us how the daemon exited
 * (SIGSEGV vs. exit(1)).  The other-child maintenance function
 * needs to decide whether to restart the daemon after a failure
 * based on whether or not it exited due to a fatal startup error
 * or something that happened at steady-state.  This exit status
 * is unlikely to collide with exit signals.
 */
#define DAEMON_STARTUP_ERROR 254


static int g_wakeup_timeout = 0;
static apr_proc_t *g_starter_proc = NULL;
static apr_file_t *g_st_read_pipe = NULL;
static apr_file_t *g_st_write_pipe = NULL;
static apr_file_t *g_ap_write_pipe = NULL;
static apr_file_t *g_ap_read_pipe = NULL;
static apr_global_mutex_t *g_pipelock = NULL;
static const char *g_pipelock_name;
static const char *g_pipelock_mutex_type = "lsapi-pipe";
static const char *g_prefix = "lsapi-util";

static int volatile g_caughtSigTerm = 0;
static pid_t g_starter_pid = 0;

static void signal_handler(int signo)
{
    /* Sanity check, Make sure I am not the subprocess. A subprocess may
       get signal after fork() and before execve() */
    if (getpid() != g_starter_pid) {
        exit(0);
        return;
    }

    if ((signo == SIGTERM) || (signo == SIGUSR1) || (signo == SIGHUP)) {
        g_caughtSigTerm = 1;
    }
}

static int starter_should_exit(void)
{
    return g_caughtSigTerm;
}

static apr_status_t init_signal(const char *prefix, server_rec * main_server)
{
    struct sigaction sa;

    /* Setup handlers */
    sa.sa_handler = signal_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    if (sigaction(SIGTERM, &sa, NULL) < 0) {
        lscapi_log(APLOG_ERR, errno, main_server,
                     "%s Can't install SIGTERM handler", prefix);
        return APR_EGENERAL;
    }

    /* Httpd restart */
    if (sigaction(SIGHUP, &sa, NULL) < 0) {
        lscapi_log(APLOG_ERR, errno, main_server,
                     "%s Can't install SIGHUP handler", prefix);
        return APR_EGENERAL;
    }

    /* Httpd graceful restart */
    if (sigaction(SIGUSR1, &sa, NULL) < 0) {
        lscapi_log(APLOG_ERR, errno, main_server,
                     "%s Can't install SIGUSR1 handler", prefix);
        return APR_EGENERAL;
    }

    /* Ignore SIGPIPE */
    sa.sa_handler = SIG_IGN;
    if (sigaction(SIGPIPE, &sa, NULL) < 0) {
        lscapi_log(APLOG_ERR, errno, main_server,
                     "%s Can't install SIGPIPE handler", prefix);
        return APR_EGENERAL;
    }

    /* Ignore SIGCHLD */
    if (sigaction(SIGCHLD, &sa, NULL) < 0) {
        lscapi_log(APLOG_ERR, errno, main_server,
                     "%s Can't install SIGCHLD handler", prefix);
        return APR_EGENERAL;
    }

    return APR_SUCCESS;
}

static int set_group_privs(void)
{
    if (!geteuid()) {
        const char *name;

        /* Get username if passed as a uid */
        if (ap_unixd_config.user_name[0] == '#') {
            struct passwd *ent;

            uid_t uid = atoi(&ap_unixd_config.user_name[1]);

            if ((ent = getpwuid(uid)) == NULL) {
                lscapi_log(APLOG_ALERT, errno, NULL,
                             "getpwuid: couldn't determine user name from uid %"APR_PID_T_FMT", "
                             "you probably need to modify the User directive",
                             uid);
                return -1;
            }
            name = ent->pw_name;
        }

        else
            name = ap_unixd_config.user_name;

#if !defined(OS2) && !defined(TPF)
        /* OS/2 and TPF don't support groups. */

        /*
         * Set the GID before initgroups(), since on some platforms
         * setgid() is known to zap the group list.
         */
        if (setgid(ap_unixd_config.group_id) == -1) {
            lscapi_log(APLOG_ALERT, errno, NULL,
                         "setgid: unable to set group id to Group %"APR_PID_T_FMT,
                         ap_unixd_config.group_id);
            return -1;
        }

        /* Reset `groups' attributes. */
        if (initgroups(name, ap_unixd_config.group_id) == -1) {
            lscapi_log(APLOG_ALERT, errno, NULL,
                         "initgroups: unable to set groups for User %s "
                         "and Group %"APR_PID_T_FMT, name,
                         ap_unixd_config.group_id);
            return -1;
        }
#endif                          /* !defined(OS2) && !defined(TPF) */
    }
    return 0;
}


/* Base on ap_unixd_setup_child() */
static int suexec_setup_child(void)
{
    if (set_group_privs()) {
        exit(-1);
    }

    /* Only try to switch if we're running as root */
    if (!geteuid() && (setuid(ap_unixd_config.user_id) == -1)) {
        lscapi_log(APLOG_ALERT, errno, NULL,
                     "setuid: unable to change to uid %ld",
                     (long) ap_unixd_config.user_id);
        exit(-1);
    }
    return 0;
}

static void starter_maint(int reason, void *data, apr_wait_t status)
{
    apr_proc_t *proc = data;
    int mpm_state;

    switch (reason) {
    case APR_OC_REASON_DEATH:
        apr_proc_other_child_unregister(data);
        if (ap_mpm_query(AP_MPMQ_MPM_STATE, &mpm_state) == APR_SUCCESS
            && mpm_state != AP_MPMQ_STOPPING) {
            if (status == DAEMON_STARTUP_ERROR) {
                lscapi_log(APLOG_CRIT, 0, NULL,
                             "%s: lsapi starter failed to initialize; stopping httpd", g_prefix);
                /* 
                 * try to terminate httpd
                 */
                kill(getpid(), SIGTERM);

            }
            else {
                lscapi_log(APLOG_ERR, 0, NULL,
                             "%s: lsapi starter died, restarting the server", g_prefix);

                /* HACK: I can't just call create_process_manager() to
                   restart a process manager, because it will use the dirty
                   share memory, I have to kill myself a SIGHUP, to make
                   a clean restart */
                /* FIXME: This is the httpd parent; it is doing a hard
                 * restart of the server!
                 */
                if (kill(getpid(), SIGHUP) < 0) {
                    lscapi_log(APLOG_EMERG,
                                 apr_get_os_error(), NULL,
                                 "%s: can't send SIGHUP to self", g_prefix);
                    exit(0);
                }
            }
        }
        break;
    case APR_OC_REASON_RESTART:
        apr_proc_other_child_unregister(data);
        break;
    case APR_OC_REASON_LOST:
        apr_proc_other_child_unregister(data);
        /* It hack here too, a note above */
        /* FIXME: This is the httpd parent; mod_lsapi is doing a hard
         * restart of the server!
         */
        if (kill(getpid(), SIGHUP) < 0) {
            lscapi_log(APLOG_EMERG,
                         apr_get_os_error(), NULL,
                         "%s: can't send SIGHUP to self", g_prefix);
            exit(0);
        }
        break;
    case APR_OC_REASON_UNREGISTER:
        /* I don't think it's going to happen */
        kill(proc->pid, SIGHUP);
        break;
    }
}


static apr_status_t starter_main(const char *prefix, server_rec * main_server, apr_pool_t * config_pool);

apr_status_t lscapi_starter_child_init(server_rec* main_server, apr_pool_t* config_pool, const char *prefix)
{
    apr_status_t rc;

    if ((rc = apr_global_mutex_child_init(&g_pipelock,
                                          g_pipelock_name,
                                          main_server->process->pconf)) != APR_SUCCESS) {
        lscapi_log(APLOG_EMERG, rc, main_server,
                     "%s apr_global_mutex_child_init error for pipe mutex", prefix);
        exit(1);
    }

    return APR_SUCCESS;
}

apr_status_t lscapi_starter_pre_config(apr_pool_t *p, apr_pool_t *plog, apr_pool_t *ptemp)
{
    return lscapi_mutex_register(g_pipelock_mutex_type, p);
}


apr_status_t lscapi_starter_init(const char *prefix, server_rec *main_server, apr_pool_t *config_pool, lsapi_svr_conf_t *cfg) 
{
    apr_status_t rc;
    apr_finfo_t finfo;
    
    g_wakeup_timeout = 1; // TODO: some config?
    g_prefix = prefix;

    rc = apr_stat(&finfo, cfg->selfstarter_socket_path, APR_FINFO_USER, config_pool);
    if (rc != APR_SUCCESS) {
            
        /* Make dir for unix domain socket */
        if ((rc = apr_dir_make_recursive(cfg->selfstarter_socket_path,
                                         APR_UREAD | APR_UWRITE | APR_UEXECUTE,
                                         config_pool)) != APR_SUCCESS) {
            lscapi_log(APLOG_ERR, rc, main_server,
                         "%s Can't create unix socket dir %s",
                         prefix, cfg->selfstarter_socket_path);
            exit(1);
        }

        if (!geteuid()) {
            if (chown(cfg->selfstarter_socket_path,
                      ap_unixd_config.user_id, -1) < 0) {
                lscapi_log(APLOG_ERR, errno, main_server,
                             "%s Can't set ownership of unix socket dir %s",
                             prefix, cfg->selfstarter_socket_path);
                exit(1);
            }
        } //if (!geteuid())

    } //if (rc != APR_SUCCESS)

    /* Create mutex for pipe reading and writing */
    rc = lscapi_mutex_create(&g_pipelock, &g_pipelock_name,
                            g_pipelock_mutex_type,
                            main_server->process->pconf, main_server);
    if (rc != APR_SUCCESS) {
        exit(1);
    }
    
    /* Create pipes to communicate between starter and apache */
    if ((rc = apr_file_pipe_create(&g_st_read_pipe, &g_ap_write_pipe,
                                   config_pool)) != APR_SUCCESS
        || (rc = apr_file_pipe_create(&g_ap_read_pipe, &g_st_write_pipe,
                                      config_pool))) {
        lscapi_log(APLOG_ERR, rc, main_server,
                     "%s Can't create pipe between starter and module", prefix);
        return rc;
    }

    /* Spawn the starter */
    g_starter_proc =
        (apr_proc_t *) apr_pcalloc(config_pool, sizeof(*g_starter_proc));
    rc = apr_proc_fork(g_starter_proc, config_pool);
    if (rc == APR_INCHILD) {
        /* I am the child */
        g_starter_pid = getpid();
        lscapi_log(APLOG_INFO, 0, main_server,
                     "%s Selfstarter %" APR_PID_T_FMT  " started", prefix, getpid());

        if ((rc = init_signal(prefix, main_server)) != APR_SUCCESS) {
            lscapi_log(APLOG_EMERG, rc, main_server,
                         "%s can't install signal handler, exiting now", prefix);
            exit(DAEMON_STARTUP_ERROR);
        }

        /* If running as root, switch to configured user.
         *
         * When running children via suexec, only the effective uid is
         * switched.
         *
         * When running children as the configured user, the real uid
         * is switched.
         */
        if (ap_unixd_config.suexec_enabled) {
            if (getuid() != 0) {
                lscapi_log(APLOG_EMERG, 0, main_server,
                             "%s current user is not root while suexec is enabled, exiting now", prefix);
                exit(DAEMON_STARTUP_ERROR);
            }
            suexec_setup_child();
        } else {
            ap_unixd_setup_child();
        }
        
        apr_file_pipe_timeout_set(g_st_read_pipe, apr_time_from_sec(g_wakeup_timeout));
        apr_file_close(g_ap_write_pipe);
        apr_file_close(g_ap_read_pipe);

        starter_main(prefix, main_server, config_pool);

        lscapi_log(APLOG_INFO, 0, main_server,
                    "%s Selfstarter %" APR_PID_T_FMT " stopped", prefix, getpid());
        exit(0);

    } else if (rc != APR_INPARENT) {
        lscapi_log(APLOG_EMERG, rc, main_server,
                    "%s Create selfstarter error", prefix );
        exit(1);
    }

    /* I am the parent
        I will send the stop signal in procmgr_stop_procmgr() */
    apr_pool_note_subprocess(config_pool, g_starter_proc,
                                APR_KILL_ONLY_ONCE);
    apr_proc_other_child_register(g_starter_proc, starter_maint,
                                  g_starter_proc, NULL, config_pool);

    return APR_SUCCESS;
}


static apr_status_t starter_fetch_cmd(const char* prefix, 
                                      lsapi_starter_command* command,
                                      server_rec* main_server)
{
    apr_status_t rc;

    /* Sanity check */
    if (!g_st_read_pipe)
        return APR_EPIPE;

    /* Wait for next command */
#if APR_MAJOR_VERSION < 2
    rc = apr_wait_for_io_or_timeout(g_st_read_pipe, NULL, 1);
#else
    rc = apr_file_pipe_wait(g_pm_read_pipe, APR_WAIT_READ);
#endif

    /* Log any unexpect result */
    if (rc != APR_SUCCESS && !APR_STATUS_IS_TIMEUP(rc)) {
        lscapi_log(APLOG_WARNING, rc, main_server,
                     "%s: error while waiting for message from pipe", prefix);
        return rc;
    }

    /* Timeout */
    if (rc != APR_SUCCESS)
        return rc;

    return apr_file_read_full(g_st_read_pipe, command, sizeof(*command), NULL);
}

/*
static int starter_is_spawn_allowed(server_rec * main_server, fcgid_command * command)
{
    return 1;
}
*/

static void starter_spawn(const char *prefix, 
                          lsapi_starter_command *command, 
                          lsapi_starter_notify *notify,
                          server_rec *main_server, 
                          apr_pool_t *config_pool) {

    const char *argvs[command->argv_descr.argv_num+1];
    const char *envs[command->env_descr.env_num+1];
    
    for(int i = 0; i < command->argv_descr.argv_num; i++) {
        argvs[i] = command->argv_buffer + command->argv_descr.argv_offsets[i];
    }
    argvs[command->argv_descr.argv_num] = NULL;

    for(int i = 0; i < command->env_descr.env_num; i++) {
        envs[i] = command->env_buffer + command->env_descr.env_offsets[i];
    }
    envs[command->env_descr.env_num] = NULL;

    pid_t pid;

    if ((pid = fork()) < 0) {
        notify->pid = -1;
        notify->err = errno;
        return;
    }
    else if (pid == 0) {
        /* child process */

        /* daemonize new process */
        setsid();
        setpgid(0, 0);
        
        execve(argvs[0], (char * const *)argvs, (char * const *)envs);
        lscapi_log(APLOG_ERR, errno, main_server, "%s: execve(%s) failed", prefix, argvs[0]);
        _exit(-1);
    }

    notify->pid = pid;
    notify->err = 0;
}

static apr_status_t starter_main(const char *prefix, 
                                 server_rec * main_server, 
                                 apr_pool_t * config_pool)
{
    lsapi_starter_command command;
    lsapi_starter_notify notify;
    apr_size_t notify_size;
    apr_status_t rc;

    apr_hash_t *starttime_by_socket = apr_hash_make(config_pool);
    
    while(1) {
        if(starter_should_exit())
            break;

        /* Wait for command */
        if(starter_fetch_cmd(prefix, &command, main_server) == APR_SUCCESS) {
/*
            if(!starter_is_spawn_allowed(main_server, &command)) continue;
*/ 
            // HACK. Starter knows that socket name is in argv[4]
            const char *key = command.argv_descr.argv_num >= 5 
                            ? command.argv_buffer + command.argv_descr.argv_offsets[4]
                            : "null";

            apr_time_t *startTimePtr = apr_hash_get(starttime_by_socket, key, APR_HASH_KEY_STRING);
            
            if(!startTimePtr) {
                //lscapi_log(APLOG_DEBUG, 0, main_server, "Could not find starting time for (%s) - try to invoke", key);
                
                // the first invoking with this socket path
                starter_spawn(prefix, &command, &notify, main_server, config_pool);
                
                if(notify.pid > 0) {
                    startTimePtr = apr_palloc(config_pool, sizeof(apr_time_t) );

                    // store invoking time associating with socket path
                    *startTimePtr = apr_time_now();
                    //lscapi_log(APLOG_DEBUG, 0, main_server, "Setting starting time for (%s) to %ld", key, *startTimePtr);
                    apr_hash_set(starttime_by_socket, key, APR_HASH_KEY_STRING, startTimePtr);
                //} else {
                //        lscapi_log(APLOG_DEBUG, 0, main_server, "Invoke (%s) successful so setting start time to %ld and storing it", key, *startTimePtr);
                }


            } else {

                //lscapi_log(APLOG_DEBUG, 0, main_server, "Starting time for (%s) is %ld", key, *startTimePtr);
                
#define STARTER_TIMEOUT  (2 * (APR_USEC_PER_SEC))
                
                if(command.force_start || apr_time_now() - *startTimePtr > STARTER_TIMEOUT) {

                    //lscapi_log(APLOG_DEBUG, 0, main_server, "Try to invoke for(%s) due to force_start(%d) or to timediff(%ld) more than timeout(%ld)", 
                    //            key, command.force_start, apr_time_now() - *startTimePtr, STARTER_TIMEOUT);
                    starter_spawn(prefix, &command, &notify, main_server, config_pool);

                    if(notify.pid > 0) {
                        // store invoking time associating with socket path
                        *startTimePtr = apr_time_now();
                        //lscapi_log(APLOG_DEBUG, 0, main_server, "Invoke (%s) successful so setting start time to %ld", 
                        //        key, *startTimePtr);
                    //} else {
                        //lscapi_log(APLOG_DEBUG, 0, main_server, "Invoke (%s) failed so leaving start time prev %ld", 
                        //        key, *startTimePtr);
                    }
                    
                } else {
                    notify.pid = 0;  // special case of delayed invoke
                    notify.err = 0;
                }
                
            }

            notify_size = sizeof notify;
            if ((rc = apr_file_write(g_st_write_pipe, &notify, &notify_size)) != APR_SUCCESS) {
                    lscapi_log(APLOG_WARNING, rc, main_server,
                                    "%s: can't send notify from selfstarter", prefix);
            }

        } //if(starter_fetch_cmd(prefix, &command, main_server) == APR_SUCCESS)
        
    } //while (1)

    return APR_SUCCESS;
}
 

pid_t lscapi_starter_send_spawn_cmd(const lscapi_argvs_descr_t *argv_descr, const char *argv_buf, 
                                    const lscapi_envs_descr_t *env_descr, const char *env_buf, 
                                    request_rec *r, int force_start) {
    apr_status_t rc;
    lsapi_starter_command command;
    lsapi_starter_notify notify;
    apr_size_t msgsize;

    if(argv_descr->argv_buf_size > MAX_BACKEND_ARGS_BUFFER) {
        
        lscapi_rlog(APLOG_ERR, 0, r,
                      "%s: argv buffer too large: %u, max %u allowed ", 
                      g_prefix, argv_descr->argv_buf_size, MAX_BACKEND_ARGS_BUFFER);
        return -1;
    }

    if(env_descr->env_buf_size > MAX_BACKEND_ENVS_BUFFER) {
        
        lscapi_rlog(APLOG_ERR, 0, r,
                      "%s: env buffer too large: %u, max %u allowed ", 
                      g_prefix, env_descr->env_buf_size, MAX_BACKEND_ENVS_BUFFER);
        return -1;
    }
    
    if(argv_descr->argv_num > MAX_BACKEND_ARGS) {
        
        lscapi_rlog(APLOG_ERR, 0, r,
                      "%s: argv number too large: %u, max %u allowed ", 
                      g_prefix, argv_descr->argv_num, MAX_BACKEND_ARGS);
        return -1;
    }

    if(env_descr->env_num > MAX_BACKEND_ENVS) {
        
        lscapi_rlog(APLOG_ERR, 0, r,
                      "%s: env number too large: %u, max %u allowed ", 
                      g_prefix, env_descr->env_num, MAX_BACKEND_ENVS);
        return -1;
    }
    
    command.argv_descr = *argv_descr;
    command.env_descr = *env_descr;
    memcpy(command.argv_buffer, argv_buf, argv_descr->argv_buf_size);
    memcpy(command.env_buffer, env_buf, env_descr->env_buf_size);
    command.force_start = force_start;

    /* Get the global mutex before posting the request */
    if ((rc = apr_global_mutex_lock(g_pipelock)) != APR_SUCCESS) {
        lscapi_rlog(APLOG_ERR, rc, r,
                      "%s: can't get pipe mutex", g_prefix);
        return -1;
    }
    
    if ((rc =
         apr_file_write_full(g_ap_write_pipe, &command, sizeof command, NULL)) != APR_SUCCESS) {
        /* Just print some error log and fall through */
        lscapi_rlog(APLOG_WARNING, rc, r,
                      "%s: can't write spawn command", g_prefix);
    } else {
        /* Wait the finish notify while send the request successfully */
        msgsize = sizeof notify;
        if ((rc = apr_file_read(g_ap_read_pipe, &notify, &msgsize)) != APR_SUCCESS) {
            lscapi_rlog(APLOG_WARNING, rc, r,
                          "%s: can't get notify from selfstarter", g_prefix);
            notify.pid = -1;
            notify.err = rc;
        }
    }

    /* Release the lock */
    if ((rc = apr_global_mutex_unlock(g_pipelock)) != APR_SUCCESS) {
        lscapi_rlog(APLOG_ERR, rc, r,
                      "%s: can't release pipe mutex", g_prefix);
        return -1;
    }
    
    if(notify.pid < 0) {
        lscapi_rlog(APLOG_ERR, notify.err, r,
                      "%s: Backend spawn failed", g_prefix);
        return -1;
    }
    
    /*
    if(notify.pid == 0) {
        lscapi_rlog(APLOG_DEBUG, 0, r, "%s: Spawn delayed", g_prefix);
    }
    */
    
    return notify.pid;
}
