/* mod_scgi.c
 *
 * Apache 2 module implementing the SCGI protocol.
 *
 *
 * Derived from mod_scgi for Apache 1.3 by:
 *    Paul Richardson prichardson@hvc.rr.com
 *    Edmund Mitchell edmund_mitchell@hotmail.com
 */

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


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

/*
 * Configuration record.  Used per-directory configuration data.
 */
typedef struct {
    char *addr;
    apr_port_t port;
    int enabled; /* mod_scgi is enabled from this directory */
    int timeout;
} scgi_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 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, "scgi: %s", msg);
}


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 queue_headers(request_rec *r, apr_bucket_brigade *bb, scgi_cfg *cfg)
{
    apr_table_t *t = apr_table_make(r->pool, 40); /* headers to send */
    const apr_array_header_t *hdrs_arr;
    apr_table_entry_t *hdrs;
    unsigned long int n = 0;
    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");
    /* 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", r->unparsed_uri);
    add_header(t, "QUERY_STRING", r->args ? r->args : "");
    add_header(t, "SCRIPT_NAME", r->uri);
    /* skip PATH_INFO, the 'uri' contains the full path */
    /* add_header(t, "PATH_INFO", r->path_info); */
    add_header(t, "HTTP_USER_AGENT", lookup_header(r, "User-Agent"));
    add_header(t, "HTTP_COOKIE", lookup_header(r, "Cookie"));
    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));
    add_header(t, "HTTP_ACCEPT", lookup_header(r, "Accept"));
    add_header(t, "HTTP_ACCEPT_CHARSET", lookup_header(r, "Accept-Charset"));
    add_header(t, "HTTP_ACCEPT_ENCODING", lookup_header(r, "Accept-Encoding"));
    add_header(t, "HTTP_ACCEPT_LANGUAGE", lookup_header(r, "Accept-Language"));
    add_header(t, "HTTP_AUTHORIZATION", lookup_header(r, "Authorization"));
    add_header(t, "HTTP_COOKIE", lookup_header(r, "Cookie"));
    add_header(t, "HTTP_EXPECT", lookup_header(r, "Expect"));
    add_header(t, "HTTP_FROM", lookup_header(r, "From"));
    add_header(t, "HTTP_IF_MATCH", lookup_header(r, "If-Match"));
    add_header(t, "HTTP_IF_MODIFIED_SINCE",
	       lookup_header(r, "If-Modified-Since"));
    add_header(t, "HTTP_IF_NONE_MATCH", lookup_header(r, "If-None-Match"));
    add_header(t, "HTTP_IF_RANGE", lookup_header(r, "If-Range"));
    add_header(t, "HTTP_IF_UNMODIFIED_SINCE",
	       lookup_header(r, "If-Unmodified-Since"));
    add_header(t, "HTTP_RANGE", lookup_header(r, "Range"));
    add_header(t, "HTTP_REFERER", lookup_header(r, "Referer"));
    add_header(t, "HTTP_TE", lookup_header(r, "TE"));
    add_header(t, "HTTP_USER_AGENT", lookup_header(r, "User-Agent"));

    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;
    }

    rv = apr_brigade_printf(bb, NULL, NULL, "%lu:", n);
    if ( !APR_STATUS_IS_SUCCESS(rv) ) return rv;

    for (i = 0; i < hdrs_arr->nelts; ++i) {
	rv = apr_brigade_puts(bb, NULL, NULL, hdrs[i].key);
	if ( !APR_STATUS_IS_SUCCESS(rv) ) return rv;
	rv = apr_brigade_putc(bb, NULL, NULL, '\0');
	if ( !APR_STATUS_IS_SUCCESS(rv) ) return rv;
	rv = apr_brigade_puts(bb, NULL, NULL, hdrs[i].val);
	if ( !APR_STATUS_IS_SUCCESS(rv) ) return rv;
	rv = apr_brigade_putc(bb, NULL, NULL, '\0');
	if ( !APR_STATUS_IS_SUCCESS(rv) ) return rv;
    }

    rv = apr_brigade_putc(bb, NULL, NULL, ',');
    if ( !APR_STATUS_IS_SUCCESS(rv) ) return rv;

    return APR_SUCCESS;
}

#define MAX_IOVEC_TO_WRITE 16 /* copied from Apache2 server/core.c */

static apr_status_t
flush_brigade_to_scgi_server(request_rec *r, apr_bucket_brigade *bb,
			     apr_socket_t *s)
{
    apr_status_t rv = 0;
    struct iovec iov[MAX_IOVEC_TO_WRITE];
    int niov = sizeof(iov)/sizeof(iov[0]);
    int i = 0;
    apr_size_t bytes_to_send = 0;
    apr_size_t bytes_sent = 0;

    while (bb && !APR_BRIGADE_EMPTY(bb)) {
	rv = apr_brigade_to_iovec(bb, iov, &niov);
	if ( !APR_STATUS_IS_SUCCESS(rv) ) {
	    log_err(APLOG_MARK, r, rv, "ap_brigade_to_iovec()");
	    return rv;
	}

	for ( ; i < niov ; i++ ) {
            bytes_to_send += iov[i].iov_len;
        }

	while (bytes_to_send) {
	    rv = apr_socket_sendv(s, iov, niov, &bytes_sent);
	    if ( !APR_STATUS_IS_SUCCESS(rv) ) {
		log_err(APLOG_MARK, r, rv,
			"ap_get_brigade() error loading request body");
		return rv;
	    }
	    bytes_to_send -= bytes_sent;
	}

	/* I don't see it doc'd anywhere, but in examining the
	 * apr_brigade_to_iovec() implementation I see 1 iovec per
	 * bucket. Doesn't seem likely that would/could change.
	 */
        while (niov--) {
            apr_bucket* b = APR_BRIGADE_FIRST(bb);
            apr_bucket_delete(b);
        }
    }

    rv = apr_socket_shutdown(s, APR_SHUTDOWN_WRITE);
    if ( !APR_STATUS_IS_SUCCESS(rv) ) {
        log_err(APLOG_MARK, r, rv, "ap_socket_shutdown() error");
        return rv;
    }
    
    return APR_SUCCESS;
}


apr_status_t open_socket(apr_socket_t **sock, request_rec *r, scgi_cfg *cfg)
{
    char *laddr		= cfg->addr ? cfg->addr : "localhost";
    apr_port_t lport	= cfg->port ? cfg->port : 4000;
    int ltimeout	= cfg->timeout ? cfg->timeout : DEFAULT_TIMEOUT;
    apr_interval_time_t timeout = apr_time_from_sec(ltimeout);
    int retries = 4;
    int sleeptime = 1;
    apr_status_t rv;
    apr_sockaddr_t *addr;

    rv = apr_sockaddr_info_get(&addr, laddr, AF_INET, lport, 0, r->pool);
    if (!APR_STATUS_IS_SUCCESS(rv) ) {
	log_err(APLOG_MARK, r, rv, "apr_sockaddr_info_get() error");
	return rv;
    }

    *sock = NULL;
    rv = apr_socket_create(sock, AF_INET, SOCK_STREAM, r->pool);
    if ( !APR_STATUS_IS_SUCCESS(rv) ) {
	log_err(APLOG_MARK, r, rv, "apr_socket_create() error");
	return rv;
    }

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

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

    ap_sock_disable_nagle(*sock);
    return APR_SUCCESS;
}



/* This was kind of swiped from ap_get_client_block(). ap_get_client_block()
 * announces in an internal comment that it shouldn't be used because it
 * loses the error code from ap_get_brigade().
 */
static apr_status_t queue_request_body (request_rec *r,
                                        apr_bucket_brigade* bbout)
{
    apr_status_t rv = 0;
    apr_bucket_brigade *bb = apr_brigade_create(r->connection->pool,
						r->connection->bucket_alloc);
    apr_off_t len = 0;

    while (ap_should_client_block(r)) {

	log_debug(APLOG_MARK, r, "queue_request_body(): reading filter input");

	rv = ap_get_brigade(r->connection->input_filters, bb,
			    AP_MODE_READBYTES, APR_BLOCK_READ, r->remaining);

	if ( !APR_STATUS_IS_SUCCESS(rv) ) {
	    r->connection->keepalive = AP_CONN_CLOSE;
	    apr_brigade_destroy(bb);
	    log_err(APLOG_MARK, r, rv,
		    "ap_get_brigade() error loading request body");
	    return rv;
	}

	/* per ap_get_client_block() comment this is improper handling by
	 * in a previous filter.  Not much we can do about that here.
	 */
	if (APR_BRIGADE_EMPTY(bb))
	    break;

	apr_brigade_length(bb, 1 /* read all */, &len);
	r->read_length += len;

	APR_BRIGADE_CONCAT(bbout, bb);
    }

    apr_brigade_destroy(bb);
    return APR_SUCCESS;
}


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

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

    cfg = our_dconfig(r);
    if (!cfg->enabled)
	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");

    if ( (rv = open_socket(&sock, r, cfg)) != APR_SUCCESS ) {
	log_err(APLOG_MARK, r, rv, "error connecting to scgi server");
	return HTTP_INTERNAL_SERVER_ERROR;
    }

    bb = apr_brigade_create(r->connection->pool, r->connection->bucket_alloc);

    rv = queue_headers(r, bb, cfg);
    if ( !APR_STATUS_IS_SUCCESS(rv) ) {
	log_err(APLOG_MARK, r, rv, "error sending response headers");
	return HTTP_INTERNAL_SERVER_ERROR;
    }

    rv = queue_request_body(r, bb);
    if ( !APR_STATUS_IS_SUCCESS(rv) ) {
	log_err(APLOG_MARK, r, rv, "error sending response body");
	return HTTP_INTERNAL_SERVER_ERROR;
    }

    rv = flush_brigade_to_scgi_server(r, bb, sock);
    if ( !APR_STATUS_IS_SUCCESS(rv) ) {
	log_err(APLOG_MARK, r, rv, "error flushing brigade");
	return HTTP_INTERNAL_SERVER_ERROR;
    }

    apr_brigade_cleanup(bb);
    log_debug(APLOG_MARK, r, "reading response headers");

    b = apr_bucket_socket_create(sock, r->connection->bucket_alloc);
    APR_BRIGADE_INSERT_TAIL(bb, b);
    b = apr_bucket_flush_create(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);

    buf = apr_pcalloc(r->connection->pool, MAX_STRING_LEN);
    rv = ap_scan_script_header_err_brigade(r, bb, buf);
    if ( !APR_STATUS_IS_SUCCESS(rv) ) {
	log_err(APLOG_MARK, r, rv, "error reading response headers");
	return HTTP_INTERNAL_SERVER_ERROR;
    }

    location = apr_table_get(r->headers_out, "Location");
    if (location && location[0] == '/' &&
	((r->status == HTTP_OK) || ap_is_HTTP_REDIRECT(r->status))) {
	/* 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 ( !APR_STATUS_IS_SUCCESS(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 *scgi_create_dir_config(apr_pool_t *p, char *dirspec)
{
    scgi_cfg *cfg = apr_pcalloc(p, sizeof(scgi_cfg));

    cfg->enabled = 0;
    cfg->addr = NULL;
    cfg->port = 0;
    cfg->timeout = 0;

    return cfg;
}


static void *
scgi_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 = new->enabled ? new->enabled : base->enabled;
    cfg->addr    = new->addr ? new->addr : base->addr;
    cfg->port    = new->port ? new->port : base->port;
    cfg->timeout = new->timeout ? new->timeout : base->timeout;

    return cfg;
}


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->addr, &scope_id, &cfg->port,
                             addr_and_port, cmd->pool);
    if ( !APR_STATUS_IS_SUCCESS(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 = 1;
    else
	cfg->enabled = 0;

    return NULL;
}


static const char *
cmd_timeout(cmd_parms *cmd, void* pcfg, const char *strtimeout)
{
    scgi_cfg *cfg = pcfg;

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

    cfg->timeout = atoi(strtimeout);

    return NULL;
}


static const command_rec scgi_cmds[] =
{
    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,
		  "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);
}


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