/*
 * Copyright (C) Igor Sysoev
 * Copyright (C) 2007 Manlio Perillo (manlio.perillo@gmail.com)
 * Copyright (C) 2008 Phusion
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <ngx_config.h>
#include <ngx_core.h>
#include <nginx.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <signal.h>
#include <string.h>
#include <errno.h>

#include "ngx_http_passenger_module.h"
#include "Configuration.h"
#include "ContentHandler.h"


#define HELPER_SERVER_MAX_SHUTDOWN_TIME 5
#define HELPER_SERVER_PASSWORD_SIZE     64


static int        first_start = 1;
static ngx_str_t  ngx_http_scgi_script_name = ngx_string("scgi_script_name");
static pid_t      helper_server_pid = 0;
static int        helper_server_admin_pipe;
static u_char     helper_server_password_data[HELPER_SERVER_PASSWORD_SIZE];
/** perl_module destroys the original environment variables for some reason,
 * so when we get a SIGHUP (for restarting Nginx) $TMPDIR might not have the
 * same value as it had during Nginx startup. We need the original $TMPDIR
 * value for calculating the Passenger temp dir location, so here we cache
 * the original value instead of getenv()'ing it every time.
 */
const char       *system_temp_dir = NULL;
const char        passenger_temp_dir[NGX_MAX_PATH];
ngx_str_t         passenger_schema_string;
ngx_str_t         passenger_helper_server_password;
const char        passenger_helper_server_socket[NGX_MAX_PATH];
CachedFileStat   *passenger_stat_cache;

static void shutdown_helper_server(ngx_cycle_t *cycle);


/*
    HISTORIC NOTE:
    We used to register passenger_content_handler as a default content handler,
    instead of setting ngx_http_core_loc_conf_t->handler. However, if
    ngx_http_read_client_request_body (and thus passenger_content_handler)
    returns NGX_AGAIN, then Nginx will pass the not-fully-receive file upload
    data to the upstream handler even though it shouldn't. Is this an Nginx
    bug? In any case, setting ngx_http_core_loc_conf_t->handler fixed the
    problem.
    
static ngx_int_t
register_content_handler(ngx_conf_t *cf)
{
    ngx_http_handler_pt        *h;
    ngx_http_core_main_conf_t  *cmcf;

    cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);

    h = ngx_array_push(&cmcf->phases[NGX_HTTP_CONTENT_PHASE].handlers);
    if (h == NULL) {
        return NGX_ERROR;
    }
    *h = passenger_content_handler;
    
    return NGX_OK;
}
*/

static void
ignore_sigpipe()
{
    struct sigaction action;
    
    action.sa_handler = SIG_IGN;
    action.sa_flags   = 0;
    sigemptyset(&action.sa_mask);
    sigaction(SIGPIPE, &action, NULL);
}

static ngx_int_t
start_helper_server(ngx_cycle_t *cycle)
{
    passenger_main_conf_t *main_conf = &passenger_main_conf;
    ngx_core_conf_t       *ccf;
    u_char                 helper_server_filename[NGX_MAX_PATH];
    u_char                 log_level_string[10];
    u_char                 max_pool_size_string[10];
    u_char                 max_instances_per_app_string[10];
    u_char                 pool_idle_time_string[10];
    u_char                 user_switching_string[2];
    u_char                 worker_uid_string[15];
    u_char                 worker_gid_string[15];
    u_char                 filename[NGX_MAX_PATH];
    u_char                *last;
    int                    admin_pipe[2], feedback_pipe[2], e;
    pid_t                  pid;
    long                   i;
    ssize_t                ret;
    char                   buf;
    ngx_str_t             *log_filename;
    FILE                  *f, *log_file;
    
    shutdown_helper_server(cycle);
    
    if (main_conf->root_dir.len == 0) {
        ngx_log_error(NGX_LOG_ALERT, cycle->log, 0,
                      "Phusion Passenger is disabled because the "
                      "'passenger_root' option is not set. Please set "
                      "this option if you want to enable Phusion Passenger.");
        return NGX_OK;
    }
    
    ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);
    
    if (first_start) {
        first_start = 0;
        
        /* Ignore SIGPIPE now so that, if the helper server fails to start,
         * nginx doesn't get killed by the default SIGPIPE handler upon
         * writing the password to the helper server.
         */
        ignore_sigpipe();
    }
    
    /* Build strings that we need later. */
    
    ngx_memzero(helper_server_filename, sizeof(helper_server_filename));
    ngx_snprintf(helper_server_filename, sizeof(helper_server_filename),
                     "%s/ext/nginx/HelperServer",
                     main_conf->root_dir.data);
    
    ngx_memzero(log_level_string, sizeof(log_level_string));
    ngx_snprintf(log_level_string, sizeof(log_level_string), "%d",
                 (int) main_conf->log_level);
    
    ngx_memzero(max_pool_size_string, sizeof(max_pool_size_string));
    ngx_snprintf(max_pool_size_string, sizeof(max_pool_size_string), "%d",
                 (int) main_conf->max_pool_size);
    
    ngx_memzero(max_instances_per_app_string, sizeof(max_instances_per_app_string));
    ngx_snprintf(max_instances_per_app_string, sizeof(max_instances_per_app_string), "%d",
                 (int) main_conf->max_instances_per_app);
    
    ngx_memzero(pool_idle_time_string, sizeof(pool_idle_time_string));
    ngx_snprintf(pool_idle_time_string, sizeof(pool_idle_time_string), "%d",
                 (int) main_conf->pool_idle_time);
    
    ngx_memzero(user_switching_string, sizeof(user_switching_string));
    ngx_snprintf(user_switching_string, 1, "%d",
                 (int) main_conf->user_switching);
    
    ngx_memzero(worker_uid_string, sizeof(worker_uid_string));
    ngx_snprintf(worker_uid_string, sizeof(worker_uid_string), "%L",
                 (long long) ccf->user);
    
    ngx_memzero(worker_gid_string, sizeof(worker_gid_string));
    ngx_snprintf(worker_gid_string, sizeof(worker_gid_string), "%L",
                 (long long) ccf->group);
    
    /* Generate random password for the helper server. */
    
    f = fopen("/dev/urandom", "r");
    if (f == NULL) {
        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                      "could not generate a random password for the "
                      "Passenger helper server: cannot open /dev/urandom");
        return NGX_ERROR;
    }
    ngx_memzero(helper_server_password_data, HELPER_SERVER_PASSWORD_SIZE);
    if (fread(helper_server_password_data, 1, HELPER_SERVER_PASSWORD_SIZE, f)
              != HELPER_SERVER_PASSWORD_SIZE) {
        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                      "could not generate a random password for the "
                      "Passenger helper server: cannot read sufficient "
                      "data from /dev/urandom");
        return NGX_ERROR;
    }
    fclose(f);
    passenger_helper_server_password.data = helper_server_password_data;
    passenger_helper_server_password.len  = HELPER_SERVER_PASSWORD_SIZE;
    
    
    /* Now spawn the helper server. */
    if (pipe(admin_pipe) == -1) {
        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                      "could not start the Passenger helper server: pipe() failed");
        return NGX_ERROR;
    }
    if (pipe(feedback_pipe) == -1) {
        close(admin_pipe[0]);
        close(admin_pipe[1]);
        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                      "could not start the Passenger helper server: pipe() failed");
        return NGX_ERROR;
    }
    
    pid = fork();
    switch (pid) {
    case 0:
        /* Child process. */
        close(admin_pipe[1]);
        close(feedback_pipe[0]);
        
        /* At this point, stdout and stderr may still point to the console.
         * Make sure that they're both redirected to the log file.
         */
        log_file = NULL;
        #if NGINX_VERSION_NUM < 7000
            log_filename = &cycle->new_log->file->name;
        #else
            log_filename = &cycle->new_log.file->name;
        #endif
        if (log_filename->len > 0) {
            log_file = fopen((const char *) log_filename->data, "a");
            if (log_file == NULL) {
                ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                              "could not open the error log file for writing");
            }
        }
        if (log_file == NULL) {
            /* If the log file cannot be opened then we redirect stdout
             * and stderr to /dev/null, because if the user disconnects
             * from the console on which Nginx is started, then on Linux
             * any writes to stdout or stderr will result in an EIO error.
             */
            log_file = fopen("/dev/null", "w");
        }
        if (log_file != NULL) {
            dup2(fileno(log_file), 1);
            dup2(fileno(log_file), 2);
            fclose(log_file);
        }
        
        /* Close all file descriptors except stdin, stdout, stderr and
         * the reader part of the pipe we just created. 
         */
        if (dup2(admin_pipe[0], 3) == -1) {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                          "could not start the Passenger helper server: "
                          "dup2() failed");
        }
        if (dup2(feedback_pipe[1], 4) == -1) {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                          "could not start the Passenger helper server: "
                          "dup2() failed");
        }
        for (i = sysconf(_SC_OPEN_MAX) - 1; i > 4; i--) {
            close(i);
        }
        
        setenv("SERVER_SOFTWARE", NGINX_VER, 1);
        
        execlp((const char *) helper_server_filename,
               "PassengerNginxHelperServer",
               main_conf->root_dir.data,
               main_conf->ruby.data,
               "3",  /* Admin pipe file descriptor number. */
               "4",  /* Feedback pipe file descriptor number. */
               log_level_string,
               max_pool_size_string,
               max_instances_per_app_string,
               pool_idle_time_string,
               user_switching_string,
               main_conf->default_user.data,
               worker_uid_string,
               worker_gid_string,
               passenger_temp_dir,
               (char *) 0);
        e = errno;
        fprintf(stderr, "*** Could not start the Passenger helper server (%s): "
                "exec() failed: %s (%d)\n",
                (const char *) helper_server_filename, strerror(e), e);
        _exit(1);
        
    case -1:
        /* Error */
        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                      "could not start the Passenger helper server: "
                      "fork() failed");
        close(admin_pipe[0]);
        close(admin_pipe[1]);
        close(feedback_pipe[0]);
        close(feedback_pipe[1]);
        return NGX_ERROR;
        
    default:
        /* Parent process. */
        close(admin_pipe[0]);
        close(feedback_pipe[1]);
        
        /* Pass our auto-generated password to the helper server. */
        i = 0;
        do {
            ret = write(admin_pipe[1], helper_server_password_data,
                        HELPER_SERVER_PASSWORD_SIZE - i);
            if (ret == -1) {
                ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                              "could not send password to the Passenger helper server: "
                              "write() failed");
                close(admin_pipe[1]);
                kill(pid, SIGTERM);
                waitpid(pid, NULL, 0);
                return NGX_ERROR;
            } else {
                i += ret;
            }
        } while (i < HELPER_SERVER_PASSWORD_SIZE);
        
        /* Wait until the HelperServer has done initializing. */
        do {
            /* We must do something with read()'s return value
             * because otherwise on some platform it might raise a
             * compile warning.
             */
            ret = read(feedback_pipe[0], &buf, 1);
        } while (ret == -1 && errno == EINTR);
        close(feedback_pipe[0]);
        
        /* Create the file passenger_temp_dir + "/control_process.pid"
         * and make it writable by the worker processes. This is because
         * save_master_process_pid is run after Nginx has lowered privileges.
         */
        last = ngx_snprintf(filename, sizeof(filename) - 1,
                            "%s/control_process.pid", passenger_temp_dir);
        *last = (u_char) '\0';
        f = fopen((const char *) filename, "w");
        if (f != NULL) {
            /* We must do something with these return values because
             * otherwise on some platforms it will cause a compiler
             * warning.
             */
            do {
                ret = fchmod(fileno(f), S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
            } while (ret == -1 && errno == EINTR);
            do {
                ret = fchown(fileno(f), ccf->user, ccf->group);
            } while (ret == -1 && errno == EINTR);
            fclose(f);
        } else {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                          "could not create %s", filename);
        }
        
        helper_server_pid        = pid;
        helper_server_admin_pipe = admin_pipe[1];
        break;
    }
    
    return NGX_OK;
}

/**
 * Save the Nginx master process's PID into a file under the Phusion Passenger
 * temp directory.
 *
 * A bug/limitation in Nginx doesn't allow us to initialize the temp dir *after*
 * Nginx has daemonized, so the temp dir's filename contains Nginx's PID before
 * daemonization. Normally PhusionPassenger::AdminTools::ControlProcess (used
 * by e.g. passenger-status) will think that the temp dir is stale because the
 * PID in the filename doesn't exist. This PID file tells AdminTools::ControlProcess
 * what the actual PID is.
 */
static ngx_int_t
save_master_process_pid(ngx_cycle_t *cycle) {
    u_char filename[NGX_MAX_PATH];
    u_char *last;
    FILE *f;
    
    if (passenger_main_conf.root_dir.len == 0) {
        return NGX_OK;
    }
    
    last = ngx_snprintf(filename, sizeof(filename) - 1,
        "%s/control_process.pid", passenger_temp_dir);
    *last = (u_char) '\0';
    
    f = fopen((const char *) filename, "w");
    if (f != NULL) {
        fprintf(f, "%ld", (long) getppid());
        fclose(f);
    } else {
        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                      "could not create %s", filename);
    }
    
    return NGX_OK;
}

static void
shutdown_helper_server(ngx_cycle_t *cycle)
{
    time_t begin_time;
    int    helper_server_exited, ret;
    u_char command[NGX_MAX_PATH + 10];
    
    if (helper_server_pid == 0) {
        return;
    }
    
    /* We write one byte to the admin pipe, doesn't matter what the byte is.
     * The helper server will detect this as an exit command.
     */
    do {
        ret = write(helper_server_admin_pipe, "x", 1);
    } while ((ret == -1 && errno == EINTR) || ret == 0);
    close(helper_server_admin_pipe);
    
    /* Wait at most HELPER_SERVER_MAX_SHUTDOWN_TIME seconds for the helper
     * server to exit.
     */
    begin_time = time(NULL);
    helper_server_exited = 0;
    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, cycle->log, 0,
                   "Waiting for Passenger helper server (PID %l) to exit...",
                   (long) helper_server_pid);
    while (!helper_server_exited && time(NULL) - begin_time < HELPER_SERVER_MAX_SHUTDOWN_TIME) {
        pid_t ret;
        
        ret = waitpid(helper_server_pid, NULL, WNOHANG);
        if (ret > 0 || (ret == -1 && errno == ECHILD)) {
            helper_server_exited = 1;
        } else {
            usleep(100000);
        }
    }
    
    /* Kill helper server if it did not exit in time. */
    if (!helper_server_exited) {
        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, cycle->log, 0,
                       "Passenger helper server did not exit in time. "
                       "Killing it...");
        kill(helper_server_pid, SIGKILL);
        waitpid(helper_server_pid, NULL, 0);
    }
    
    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, cycle->log, 0,
                   "Passenger helper server exited.");

    
    /* Remove Passenger temp dir. */
    ngx_memzero(command, sizeof(command));
    if (ngx_snprintf(command, sizeof(command), "chmod -R u=rwx \"%s\"",
                     passenger_temp_dir) != NULL) {
        do {
            ret = system((const char *) command);
        } while (ret == -1 && errno == EINTR);
    }
    
    ngx_memzero(command, sizeof(command));
    if (ngx_snprintf(command, sizeof(command), "rm -rf \"%s\"",
                     passenger_temp_dir) != NULL) {
        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, cycle->log, 0,
                       "Removing Passenger temp folder with command: %s",
                       command);
        errno = 0;
        if (system((const char *) command) != 0) {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                          "Could not remove Passenger temp folder '%s'",
                          passenger_temp_dir);
        }
    }
    helper_server_pid = 0;
}

static ngx_int_t
ngx_http_scgi_script_name_variable(ngx_http_request_t *r,
    ngx_http_variable_value_t *v, uintptr_t data)
{
    u_char                *p;
    passenger_loc_conf_t  *slcf;

    if (r->uri.len) {
        v->valid = 1;
        v->no_cacheable = 0;
        v->not_found = 0;

        slcf = ngx_http_get_module_loc_conf(r, ngx_http_passenger_module);

        if (r->uri.data[r->uri.len - 1] != '/') {
            v->len = r->uri.len;
            v->data = r->uri.data;
            return NGX_OK;
        }

        v->len = r->uri.len + slcf->index.len;

        v->data = ngx_palloc(r->pool, v->len);
        if (v->data == NULL) {
            return NGX_ERROR;
        }

        p = ngx_copy(v->data, r->uri.data, r->uri.len);
        ngx_memcpy(p, slcf->index.data, slcf->index.len);

    } else {
        v->len = 0;
        v->valid = 1;
        v->no_cacheable = 0;
        v->not_found = 0;
        v->data = NULL;

        return NGX_OK;
    }

    return NGX_OK;
}

static ngx_int_t
add_variables(ngx_conf_t *cf)
{
    ngx_http_variable_t  *var;

    var = ngx_http_add_variable(cf, &ngx_http_scgi_script_name,
                                NGX_HTTP_VAR_NOHASH|NGX_HTTP_VAR_NOCACHEABLE);
    if (var == NULL) {
        return NGX_ERROR;
    }

    var->get_handler = ngx_http_scgi_script_name_variable;

    return NGX_OK;
}

static ngx_int_t
pre_config_init(ngx_conf_t *cf)
{
    ngx_int_t   ret;
    u_char      command[NGX_MAX_PATH + 30];
    u_char     *last;
    
    ngx_memzero(&passenger_main_conf, sizeof(passenger_main_conf_t));
    
    passenger_schema_string.data = (u_char *) "passenger://";
    passenger_schema_string.len  = sizeof("passenger://") - 1;
    
    passenger_stat_cache = cached_file_stat_new(1024);
    
    ret = add_variables(cf);
    if (ret != NGX_OK) {
        return ret;
    }
    
    /* Setup Passenger temp folder. */
    
    if (system_temp_dir == NULL) {
        const char *tmp = getenv("TMPDIR");
        if (tmp == NULL || *tmp == '\0') {
            system_temp_dir = "/tmp";
        } else {
            system_temp_dir = strdup(tmp);
        }
    }
    
    ngx_memzero(&passenger_temp_dir, sizeof(passenger_temp_dir));
    if (ngx_snprintf((u_char *) passenger_temp_dir, sizeof(passenger_temp_dir),
                     "%s/passenger.%d", system_temp_dir, getpid()) == NULL) {
        ngx_log_error(NGX_LOG_ALERT, cf->log, ngx_errno,
                      "could not create Passenger temp dir string");
        return NGX_ERROR;
    }
    
    /* Temporarily create this temp directory. It must exist before the configuration is loaded,
     * because during Configuration loading Nginx's upstream module will attempt to create
     * the directory passenger_temp_dir + "/webserver_private".
     *
     * passenger_temp_dir will be removed and recreated by the HelperServer, with the right
     * permissions. So right now we don't have to pay any attention to the permissions.
     */
    ngx_memzero(command, sizeof(command));
    ngx_snprintf(command, sizeof(command), "mkdir -p \"%s\"", passenger_temp_dir);
    do {
        ret = system((const char *) command);
    } while (ret == -1 && errno == EINTR);
    
    /* Build helper server socket filename string. */
    
    last = ngx_snprintf((u_char *) passenger_helper_server_socket, NGX_MAX_PATH,
                        "unix:%s/master/helper_server.sock",
                        passenger_temp_dir);
    if (last == NULL) {
        ngx_log_error(NGX_LOG_ALERT, cf->log, ngx_errno,
                      "could not create Passenger helper server "
                      "socket filename string");
        return NGX_ERROR;
    } else {
        *last = (u_char) '\0';
    }
    
    return NGX_OK;
}


static ngx_http_module_t passenger_module_ctx = {
    pre_config_init,                     /* preconfiguration */
    /* register_content_handler */ NULL, /* postconfiguration */

    passenger_create_main_conf,          /* create main configuration */
    passenger_init_main_conf,            /* init main configuration */

    NULL,                                /* create server configuration */
    NULL,                                /* merge server configuration */

    passenger_create_loc_conf,           /* create location configuration */
    passenger_merge_loc_conf             /* merge location configuration */
};


ngx_module_t ngx_http_passenger_module = {
    NGX_MODULE_V1,
    &passenger_module_ctx,                  /* module context */
    (ngx_command_t *) passenger_commands,   /* module directives */
    NGX_HTTP_MODULE,                        /* module type */
    NULL,                                   /* init master */
    start_helper_server,                    /* init module */
    save_master_process_pid,                /* init process */
    NULL,                                   /* init thread */
    NULL,                                   /* exit thread */
    NULL,                                   /* exit process */
    shutdown_helper_server,                 /* exit master */
    NGX_MODULE_V1_PADDING
};
