/* mod_scgi.c
 *
 * Apache 2 implementation of the SCGI protocol.
 *
 */

#include "ap_config.h"
#include "apr_lib.h"
#include "apr_strings.h"
#include "httpd.h"
#include "http_config.h"
#include "http_core.h"
#include "http_request.h"
#include "http_log.h"
#include "http_protocol.h"
#include "util_script.h"

#define MOD_SCGI_VERSION "1.9"
#define SCGI_PROTOCOL_VERSION "1"
#define DEFAULT_TIMEOUT  60 /* default socket timeout */

#define UNSET 0
#define ENABLED 1
#define DISABLED 2

typedef struct {
    char *path;
    char *addr;
    apr_port_t port;
} mount_entry;

/*
 * Configuration record.  Used per-directory configuration data.
 */
typedef struct {
    mount_entry mount;
    int enabled; /* mod_scgi is enabled from this directory */
    int timeout;
} scgi_cfg;

/* Server level configuration */
typedef struct {
    apr_array_header_t *mounts;
    int timeout;
} scgi_server_cfg;

/*
 * Declare ourselves so the configuration routines can find and know us.
 * We'll fill it in at the end of the module.
 */
module AP_MODULE_DECLARE_DATA scgi_module;

/*
 * Locate our directory configuration record for the current request.
 */
static scgi_cfg *
our_dconfig(request_rec *r)
{
    return (scgi_cfg *) ap_get_module_config(r->per_dir_config, &scgi_module);
}

static scgi_server_cfg *our_sconfig(server_rec *s)
{
    return (scgi_server_cfg *) ap_get_module_config(s->module_config,
                                                    &scgi_module);
}

static int
mount_entry_matches(const char *url, const char *prefix,
                    const char **path_info)
{
    int i;
    for (i=0; prefix[i] != '\0'; i++) {
        if (url[i] == '\0' || url[i] != prefix[i])
            return 0;
    }
    if (url[i] == '\0' || url[i] == '/') {
        *path_info = url + i;
        return 1;
    }
    return 0;
}

static int scgi_translate(request_rec *r)
{
    scgi_cfg *cfg = our_dconfig(r);

    if (cfg->enabled == DISABLED) {
        return DECLINED;
    }

    if (cfg->mount.addr != UNSET) {
        ap_assert(cfg->mount.port != UNSET);
        r->handler = "scgi-handler";
        r->filename = r->uri;
        ap_set_module_config(r->request_config, &scgi_module, &cfg->mount);
        return OK;
    }
    else {
        int i;
        scgi_server_cfg *scfg = our_sconfig(r->server);
        mount_entry *entries = (mount_entry *) scfg->mounts->elts;
        for (i = 0; i < scfg->mounts->nelts; ++i) {
            const char *path_info;
            mount_entry *mount = &entries[i];
            if (mount_entry_matches(r->uri, mount->path, &path_info)) {
                r->handler = "scgi-handler";
                r->path_info = apr_pstrdup(r->pool, path_info);
                r->filename = r->uri;
                ap_set_module_config(r->request_config, &scgi_module, mount);
                return OK;
            }
        }
    }
    return DECLINED;
}

static int scgi_map_location(request_rec *r)
{
    if (r->handler && strcmp(r->handler, "scgi-handler") == 0) {
        return OK; /* We don't want directory walk. */
    }
    return DECLINED;
}

static void log_err(const char *file, int line, request_rec *r,
                    apr_status_t status, const char *msg)
{
    char buf[256] = "";
    apr_strerror(status, buf, sizeof(buf));
    ap_log_rerror(file, line, APLOG_ERR, status, r, "scgi: %s: %s", buf, msg);
}

static void log_debug(const char *file, int line, request_rec *r, const
                      char *msg)
{
    ap_log_rerror(file, line, APLOG_DEBUG, APR_SUCCESS, r, msg);
}

static char *http2env(apr_pool_t *p, const char *name)
{
    char *env_name = apr_pstrcat(p, "HTTP_", name, NULL);
    char *cp;
    
    for (cp = env_name + 5; *cp != 0; cp++) {
        if (*cp == '-') {
            *cp = '_';
        }
        else {
            *cp = apr_toupper(*cp);
        }
    }

    return env_name;
}

static char *lookup_name(apr_table_t *t, const char *name)
{
    const apr_array_header_t *hdrs_arr = apr_table_elts(t);
    apr_table_entry_t *hdrs = (apr_table_entry_t *) hdrs_arr->elts;
    int i;

    for (i = 0; i < hdrs_arr->nelts; ++i) {
        if (hdrs[i].key == NULL)
            continue;

        if (strcasecmp(hdrs[i].key, name) == 0)
            return hdrs[i].val;
    }
    return NULL;
}


static char *lookup_header(request_rec *r, const char *name)
{
    return lookup_name(r->headers_in, name);
}


static char *lookup_env(request_rec *r, const char *name)
{
    return lookup_name(r->subprocess_env, name);
}


static void add_header(apr_table_t *t, const char *name, const char *value)
{
    if (name != NULL && value != NULL)
        apr_table_addn(t, name, value);
}

static int find_path_info(const char *uri, const char *path_info)
{
    int n;
    n = strlen(uri) - strlen(path_info);
    ap_assert(n >= 0);
    return n;
}

/* This code is a duplicate of what's in util_script.c.  We can't use
 * r->unparsed_uri because it gets changed if there was a redirect. */
static char *original_uri(request_rec *r)
{
    char *first, *last;

    if (r->the_request == NULL) {
	return (char *) apr_pcalloc(r->pool, 1);
    }

    first = r->the_request;	/* use the request-line */

    while (*first && !apr_isspace(*first)) {
	++first;		/* skip over the method */
    }
    while (apr_isspace(*first)) {
	++first;		/*   and the space(s)   */
    }

    last = first;
    while (*last && !apr_isspace(*last)) {
	++last;			/* end at next whitespace */
    }

    return apr_pstrmemdup(r->pool, first, last - first);
}

/* buffered socket implementation (buckets are overkill) */

#define BUFFER_SIZE 8000

struct sockbuff {
    apr_socket_t *sock;
    char buf[BUFFER_SIZE];
    int used;
};

static void binit(struct sockbuff *s, apr_socket_t *sock)
{
    s->sock = sock;
    s->used = 0;
}

static apr_status_t sendall(apr_socket_t *sock, char *buf, apr_size_t len)
{
    apr_status_t rv;
    apr_size_t n;
    while (len > 0) {
        n = len;
        if ((rv = apr_send(sock, buf, &n))) return rv;
        buf += n;
        len -= n;
    }
    return APR_SUCCESS;
}

static apr_status_t bflush(struct sockbuff *s)
{
    apr_status_t rv;
    ap_assert(s->used >= 0 && s->used <= BUFFER_SIZE);
    if (s->used) {
            if ((rv = sendall(s->sock, s->buf, s->used))) return rv;
            s->used = 0;
    }
    return APR_SUCCESS;
}

static apr_status_t bwrite(struct sockbuff *s, char *buf, apr_size_t len)
{
    apr_status_t rv;
    if (len >= BUFFER_SIZE - s->used) {
        if ((rv = bflush(s))) return rv;
        while (len >= BUFFER_SIZE) {
            if ((rv = sendall(s->sock, buf, BUFFER_SIZE))) return rv;
            buf += BUFFER_SIZE;
            len -= BUFFER_SIZE;
        }
    }
    if (len > 0) {
        ap_assert(len < BUFFER_SIZE - s->used);
        memcpy(s->buf + s->used, buf, len);
        s->used += len;
    }
    return APR_SUCCESS;
}

static apr_status_t bputs(struct sockbuff *s, char *buf)
{
    return bwrite(s, buf, strlen(buf));
}

static apr_status_t bputc(struct sockbuff *s, char c)
{
    char buf[1];
    buf[0] = c;
    return bwrite(s, buf, 1);
}


static apr_status_t
send_headers(request_rec *r, struct sockbuff *s)
{
    /* headers to send */
    apr_table_t *t;
    const apr_array_header_t *hdrs_arr, *env_arr;
    apr_table_entry_t *hdrs, *env;
    unsigned long int n = 0;
    char *buf;
    int i;
    apr_status_t rv = 0;
    apr_port_t  port = 0;
    apr_sockaddr_port_get(&port, r->connection->remote_addr);

    log_debug(APLOG_MARK,r, "sending headers");
    t = apr_table_make(r->pool, 40);
    if (!t)
	    return APR_ENOMEM;
    /* CONTENT_LENGTH must come first and always be present */
    add_header(t, "CONTENT_LENGTH",
               apr_psprintf(r->pool, "%ld", r->remaining));
    add_header(t, "SCGI", SCGI_PROTOCOL_VERSION);
    add_header(t, "SERVER_SOFTWARE", ap_get_server_version());
    add_header(t, "SERVER_PROTOCOL", r->protocol);
    add_header(t, "SERVER_NAME", ap_get_server_name(r));
    add_header(t, "SERVER_ADMIN", r->server->server_admin);
    add_header(t, "SERVER_ADDR", r->connection->local_ip);
    add_header(t, "SERVER_PORT", apr_psprintf(r->pool, "%u",
                                              ap_get_server_port(r)));
    add_header(t, "REMOTE_ADDR", r->connection->remote_ip);
    add_header(t, "REMOTE_PORT", apr_psprintf(r->pool, "%d", port));
    add_header(t, "REMOTE_USER", r->user);
    add_header(t, "REQUEST_METHOD", r->method);
    add_header(t, "REQUEST_URI", original_uri(r));
    add_header(t, "QUERY_STRING", r->args ? r->args : "");
    add_header(t, "SCRIPT_NAME", r->uri);
    if (r->path_info) {
        int path_info_start = find_path_info(r->uri, r->path_info);
        add_header(t, "SCRIPT_NAME", apr_pstrndup(r->pool, r->uri,
                                                  path_info_start));
        add_header(t, "PATH_INFO", r->path_info);
    }
    else {
        /* skip PATH_INFO, don't know it */
        add_header(t, "SCRIPT_NAME", r->uri);
    }
    add_header(t, "HTTPS", lookup_env(r, "HTTPS"));
    add_header(t, "CONTENT_TYPE", lookup_header(r, "Content-type"));
    add_header(t, "DOCUMENT_ROOT", ap_document_root(r));

    /* HTTP headers */
    hdrs_arr = apr_table_elts(r->headers_in);
    hdrs = (apr_table_entry_t *) hdrs_arr->elts;
    for (i = 0; i < hdrs_arr->nelts; ++i) {
        if (hdrs[i].key) {
            add_header(t, http2env(r->pool, hdrs[i].key), hdrs[i].val);
        }
    }

    /* environment variables */
    env_arr = apr_table_elts(r->subprocess_env);
    env = (apr_table_entry_t*) env_arr->elts;
    for (i = 0; i < env_arr->nelts; ++i) {
        add_header(t, env[i].key, env[i].val);
    }

    hdrs_arr = apr_table_elts(t);
    hdrs = (apr_table_entry_t*) hdrs_arr->elts;

    /* calculate length of header data (including nulls) */
    for (i = 0; i < hdrs_arr->nelts; ++i) {
        n += strlen(hdrs[i].key) + 1;
        n += strlen(hdrs[i].val) + 1;
    }

    buf = apr_psprintf(r->pool, "%lu:", n);
    if (!buf)
        return APR_ENOMEM;
    rv = bputs(s, buf);
    if (rv)
        return rv;

    for (i = 0; i < hdrs_arr->nelts; ++i) {
        rv = bputs(s, hdrs[i].key);
        if (rv) return rv;
        rv = bputc(s, '\0');
        if (rv) return rv;
        rv = bputs(s, hdrs[i].val);
        if (rv) return rv;
        rv = bputc(s, '\0');
        if (rv) return rv;
    }

    rv = bputc(s, ',');
    if (rv)
        return rv;

    return APR_SUCCESS;
}

static apr_status_t send_request_body(request_rec *r, struct sockbuff *s)
{
    if (ap_should_client_block(r)) {
        char buf[BUFFER_SIZE];
        apr_status_t rv;
        apr_off_t len;

        while ((len = ap_get_client_block(r, buf, sizeof buf)) > 0) {
            if ((rv = bwrite(s, buf, len))) return rv;
        }
        if (len == -1)
            return HTTP_INTERNAL_SERVER_ERROR; /* what to return? */
    }
    return APR_SUCCESS;
}

#define CONFIG_VALUE(value, fallback) ((value) != UNSET ? (value) : (fallback))

static apr_status_t
open_socket(apr_socket_t **sock, request_rec *r)
{
    int timeout;
    int retries = 4;
    int sleeptime = 1;
    apr_status_t rv;
    apr_sockaddr_t *sockaddr;
    scgi_server_cfg *scfg = our_sconfig(r->server);
    scgi_cfg *cfg = our_dconfig(r);
    mount_entry *m = (mount_entry *) ap_get_module_config(r->request_config,
                                                          &scgi_module);
    timeout = CONFIG_VALUE(cfg->timeout, CONFIG_VALUE(scfg->timeout,
                                                      DEFAULT_TIMEOUT));
    rv = apr_sockaddr_info_get(&sockaddr,
                               CONFIG_VALUE(m->addr, "localhost"),
                               APR_UNSPEC,
                               CONFIG_VALUE(m->port, 4000),
                               0,
                               r->pool);
    if (rv) {
        log_err(APLOG_MARK, r, rv, "apr_sockaddr_info_get() error");
        return rv;
    }

 restart:
    *sock = NULL;
    rv = apr_socket_create(sock, sockaddr->family, SOCK_STREAM, r->pool);
    if (rv) {
        log_err(APLOG_MARK, r, rv, "apr_socket_create() error");
        return rv;
    }

    rv = apr_socket_timeout_set(*sock, apr_time_from_sec(timeout));
    if (rv) {
        log_err(APLOG_MARK, r, rv, "apr_socket_timeout_set() error");
        return rv;
    }

    rv = apr_socket_connect(*sock, sockaddr);
    if (rv) {
	apr_socket_close(*sock);
        if ((APR_STATUS_IS_ECONNREFUSED(rv) |
	     APR_STATUS_IS_EINPROGRESS(rv)) && retries > 0) {
            /* server may be temporarily down, retry */
            ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, rv, r,
                          "scgi: connection failed, retrying");
            apr_sleep(apr_time_from_sec(sleeptime));
            --retries;
            sleeptime *= 2;
            goto restart;
        }
        log_err(APLOG_MARK, r, rv, "scgi: can't connect to server");
        return rv;
    }

#ifdef APR_TCP_NODELAY
    /* disable Nagle, we don't send small packets */
    apr_socket_opt_set(*sock, APR_TCP_NODELAY, 1);
#endif

    return APR_SUCCESS;
}

static int scgi_handler(request_rec *r)
{
    apr_status_t rv = 0;
    int http_status = 0;
    struct sockbuff s;
    apr_socket_t *sock;
    apr_bucket_brigade *bb = NULL;
    apr_bucket *b          = NULL;
    const char *location;

    if (strcmp(r->handler, "scgi-handler"))
        return DECLINED;

    http_status = ap_setup_client_block(r, REQUEST_CHUNKED_ERROR);
    if (http_status != OK)
        return http_status;

    log_debug(APLOG_MARK, r, "connecting to server");

    rv = open_socket(&sock, r);
    if (rv) {
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    binit(&s, sock);

    rv = send_headers(r, &s);
    if (rv) {
        log_err(APLOG_MARK, r, rv, "error sending request headers");
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    rv = send_request_body(r, &s);
    if (rv) {
        log_err(APLOG_MARK, r, rv, "error sending request body");
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    rv = bflush(&s);
    if (rv) {
        log_err(APLOG_MARK, r, rv, "error sending request");
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    log_debug(APLOG_MARK, r, "reading response headers");
    bb = apr_brigade_create(r->connection->pool, r->connection->bucket_alloc);
    b = apr_bucket_socket_create(sock, r->connection->bucket_alloc);
    APR_BRIGADE_INSERT_TAIL(bb, b);
    b = apr_bucket_eos_create(r->connection->bucket_alloc);
    APR_BRIGADE_INSERT_TAIL(bb, b);

    rv = ap_scan_script_header_err_brigade(r, bb, NULL);
    if (rv) {
        if (rv == HTTP_INTERNAL_SERVER_ERROR) {
            log_err(APLOG_MARK, r, rv, "error reading response headers");
        }
        else {
            /* Work around an Apache bug whereby the returned status is
             * ignored and status_line is used instead.  This bug is
             * present at least in 2.0.54.
             */
            r->status_line = NULL;
        }
        apr_brigade_destroy(bb);
        return rv;
    }

    location = apr_table_get(r->headers_out, "Location");

    if (location && location[0] == '/' &&
        ((r->status == HTTP_OK) || ap_is_HTTP_REDIRECT(r->status))) {

        apr_brigade_destroy(bb);

        /* Internal redirect -- fake-up a pseudo-request */
        r->status = HTTP_OK;

        /* This redirect needs to be a GET no matter what the original
         * method was.
         */
        r->method = apr_pstrdup(r->pool, "GET");
        r->method_number = M_GET;

        ap_internal_redirect_handler(location, r);
        return OK;
    }

    rv = ap_pass_brigade(r->output_filters, bb);
    if (rv) {
        log_err(APLOG_MARK, r, rv, "ap_pass_brigade()");
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    return OK;
}


static int scgi_init(apr_pool_t *p, apr_pool_t *plog, apr_pool_t *ptemp,
                       server_rec *base_server)
{
    ap_add_version_component(p, "mod_scgi/" MOD_SCGI_VERSION);
    return OK;
}


static void *
create_dir_config(apr_pool_t *p, char *dirspec)
{
    scgi_cfg *cfg = apr_pcalloc(p, sizeof(scgi_cfg));

    cfg->enabled = UNSET;
    cfg->mount.addr = UNSET;
    cfg->mount.port = UNSET;
    cfg->timeout = UNSET;

    return cfg;
}

#define MERGE(b, n, a) (n->a == UNSET ? b->a : n->a)

static void *
merge_dir_config(apr_pool_t *p, void *basev, void *newv)
{
    scgi_cfg* cfg = apr_pcalloc(p, sizeof(scgi_cfg));
    scgi_cfg* base = basev;
    scgi_cfg* new = newv;

    cfg->enabled = MERGE(base, new, enabled);
    cfg->mount.addr = MERGE(base, new, mount.addr);
    cfg->mount.port = MERGE(base, new, mount.port);
    cfg->timeout = MERGE(base, new, timeout);

    return cfg;
}

static void *
create_server_config(apr_pool_t *p, server_rec *s)
{
	scgi_server_cfg *c =
		(scgi_server_cfg *) apr_pcalloc(p, sizeof(scgi_server_cfg));

	c->mounts = apr_array_make(p, 20, sizeof(mount_entry));
        c->timeout = UNSET;
	return c;
}

static void *
merge_server_config(apr_pool_t *p, void *basev, void *overridesv)
{
	scgi_server_cfg *c = (scgi_server_cfg *)
		apr_pcalloc(p, sizeof(scgi_server_cfg));
	scgi_server_cfg *base = (scgi_server_cfg *) basev;
	scgi_server_cfg *overrides = (scgi_server_cfg *) overridesv;

	c->mounts = apr_array_append(p, overrides->mounts, base->mounts);
        c->timeout = MERGE(base, overrides, timeout);
	return c;
}

static const char *
cmd_mount(cmd_parms *cmd, void *dummy, const char *path, const char *addr)
{
    int n;
    apr_status_t rv;
    char *scope_id = NULL; /* A ip6 parameter - not used here. */
    scgi_server_cfg *scfg = our_sconfig(cmd->server);
    mount_entry *new = apr_array_push(scfg->mounts);
    n = strlen(path);
    while (n > 0 && path[n-1] == '/') {
        n--; /* strip trailing slashes */
    }
    new->path = apr_pstrndup(cmd->pool, path, n);
    rv = apr_parse_addr_port(&new->addr, &scope_id, &new->port, addr,
                             cmd->pool);
    if (rv)
        return "error parsing address:port string";
    return NULL;
}

static const char *
cmd_server(cmd_parms *cmd, void *pcfg, const char *addr_and_port)
{
    apr_status_t rv;
    scgi_cfg *cfg = pcfg;
    char *scope_id = NULL; /* A ip6 parameter - not used here. */

    if (cmd->path == NULL)
        return "not a server command";

    rv = apr_parse_addr_port(&cfg->mount.addr, &scope_id, &cfg->mount.port,
                             addr_and_port, cmd->pool);
    if (rv)
        return "error parsing address:port string";

    return NULL;
}


static const char *
cmd_handler(cmd_parms* cmd, void* pcfg, int flag)
{
    scgi_cfg *cfg = pcfg;

    if (cmd->path == NULL) /* server command */
        return "not a server command";

    if (flag)
        cfg->enabled = ENABLED;
    else
        cfg->enabled = DISABLED;

    return NULL;
}


static const char *
cmd_timeout(cmd_parms *cmd, void* pcfg, const char *strtimeout)
{
    scgi_cfg *dcfg = pcfg;
    int timeout = atoi(strtimeout);

    if (cmd->path == NULL) {
        scgi_server_cfg *scfg = our_sconfig(cmd->server);
        scfg->timeout = timeout;
    }
    else {
        dcfg->timeout = timeout;
    }

    return NULL;
}

static const command_rec scgi_cmds[] =
{
    AP_INIT_TAKE2("SCGIMount", cmd_mount, NULL, RSRC_CONF,
                  "path prefix and address of SCGI server"),
    AP_INIT_TAKE1("SCGIServer", cmd_server, NULL, ACCESS_CONF,
                  "Address and port of an SCGI server (e.g. localhost:4000)"),
    AP_INIT_FLAG( "SCGIHandler", cmd_handler, NULL, ACCESS_CONF,
                  "On or Off to enable or disable the SCGI handler"),
    AP_INIT_TAKE1("SCGIServerTimeout", cmd_timeout, NULL, ACCESS_CONF|RSRC_CONF,
                  "Timeout (in seconds) for communication with the SCGI server."),
    {NULL}
};


static void scgi_register_hooks(apr_pool_t *p)
{
    ap_hook_post_config(scgi_init, NULL, NULL, APR_HOOK_MIDDLE);
    ap_hook_handler(scgi_handler, NULL, NULL, APR_HOOK_MIDDLE);
    ap_hook_translate_name(scgi_translate, NULL, NULL, APR_HOOK_LAST);
    ap_hook_map_to_storage(scgi_map_location, NULL, NULL, APR_HOOK_FIRST);
}


/* Dispatch list for API hooks */
module AP_MODULE_DECLARE_DATA scgi_module = {
    STANDARD20_MODULE_STUFF,
    create_dir_config,             /* create per-dir config structs */
    merge_dir_config,              /* merge per-dir config structs */
    create_server_config,          /* create per-server config structs */
    merge_server_config,           /* merge per-server config structs */
    scgi_cmds,                     /* table of config file commands */
    scgi_register_hooks,           /* register hooks */
};
