#define MOD_SCGI_VERSION "1.2"
#define SCGI_PROTOCOL_VERSION "1"
/* #define VERBOSE_DEBUG */


#include "httpd.h"
#include "http_config.h"
#include "http_core.h"
#include "http_log.h"
#include "http_main.h"
#include "http_protocol.h"
#include "http_request.h"
#include "util_script.h"

#include <netinet/tcp.h> /* for TCP_NODELAY */

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

/*
 * Configuration record.  Used per-directory configuration data.
 */
typedef struct {
	unsigned long addr;	/* address in network byte order */
	unsigned short port; 	/* port in network byte order */
	int enabled;		/* mod_scgi enabled from this directory */
} 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 MODULE_VAR_EXPORT 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 int scgi_trans(request_rec *r)
{
	scgi_cfg *cfg = our_dconfig(r);

	if (cfg->enabled == ENABLED) {
		r->handler = "scgi-handler";
		return OK;
	}
	return DECLINED;
}


int open_socket(request_rec *r, scgi_cfg *cfg)
{
	int retries, sleeptime, rv;
	struct sockaddr_in addr;
	int sock;

	if (cfg->addr == UNSET)
		addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	else
		addr.sin_addr.s_addr = cfg->addr;
	if (cfg->port == UNSET)
		addr.sin_port = htons(4000);
	else
		addr.sin_port = cfg->port;
	addr.sin_family = AF_INET;
  
	/* create the socket */
	sock = ap_psocket(r->pool, AF_INET, SOCK_STREAM, 0);
	if (sock == -1) 
		return -1;
  
	/* try to connect */
	retries = 4;
	sleeptime = 1;
restart:
	rv = connect(sock, (struct sockaddr *)&addr, sizeof addr);
	if (rv != 0) {
		if (errno == EINTR)
			goto restart; /* signals suck */
		if (errno == ECONNREFUSED && retries > 0) {
			/* server may be temporarily down, retry */
			ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r,
				      "scgi: connection refused, retrying");
			sleep(sleeptime);
			--retries;
			sleeptime *= 2;
			goto restart;
		}
		ap_log_rerror(APLOG_MARK, APLOG_ERR, r,
			      "scgi: connecting to server");
		return -1;
	}

#ifdef TCP_NODELAY
	if (addr.sin_family == AF_INET) {
		/* disable Nagle */
		int set = 1;
		setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char *)&set, sizeof(set));
	}
#endif

	return sock;
}

static char *lookup_name(table *t, const char *name)
{
	array_header *hdrs_arr;
	table_entry *hdrs;
	int i;

	hdrs_arr = ap_table_elts(t);
	hdrs = (table_entry *)hdrs_arr->elts;
	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 char *original_uri(request_rec *r)
{
	char *first, *last;

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

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

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

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

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

static void log_err(request_rec *r, const char *msg)
{
	ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r,
			"scgi: %s", msg);
}

static void log_debug(request_rec *r, const char *msg)
{
#ifdef VERBOSE_DEBUG
	ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, r,
			"scgi: %s", msg);
#endif
}

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

static int send_headers(request_rec *r, BUFF *f, scgi_cfg *cfg)
{
	table *t;
	array_header *hdrs_arr, *env_arr;
	table_entry *hdrs, *env;
	int i;
	unsigned long n;

	log_debug(r, "sending headers");
	t = ap_make_table(r->pool, 40); /* headers to send */
	if (!t)
		return 0;
	/* CONTENT_LENGTH must come first and always be present */
	add_header(t, "CONTENT_LENGTH",
		   	ap_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",
			ap_psprintf(r->pool, "%u", ap_get_server_port(r)));
	add_header(t, "REMOTE_ADDR", r->connection->remote_ip);
	add_header(t, "REMOTE_PORT",
			ap_psprintf(r->pool, "%d",
				ntohs(r->connection->remote_addr.sin_port)));
	add_header(t, "REMOTE_USER", r->connection->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);
	/* skip PATH_INFO, it's always empty */
	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_HOST", lookup_header(r, "Host"));
	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"));

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

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

	/* send header data as netstring */
	if (ap_bprintf(f, "%lu:", n) < 0) {
		return 0;
	}
	for (i = 0; i < hdrs_arr->nelts; ++i) {
		if (ap_bputs(hdrs[i].key, f) < 0) return 0;
		if (ap_bputc('\0', f) < 0) return 0;
		if (ap_bputs(hdrs[i].val, f) < 0) return 0;
		if (ap_bputc('\0', f) < 0) return 0;
	}
	if (ap_bputc(',', f) < 0) return 0;

	return 1;
}

static int send_request_body (request_rec *r, BUFF *f)
{
	if (ap_should_client_block(r)) {
		int n;
		char buffer[HUGE_STRING_LEN];
		while ((n = ap_get_client_block(r, buffer,
						sizeof buffer)) > 0) {
			if (ap_bwrite(f, buffer, n) != n) return 0;
			ap_reset_timeout(r);
		}
	}
	if (ap_bflush(f) < 0) return 0;
	return 1;
}

static int scgi_handler(request_rec *r)
{
	int ret, sock;
	BUFF *f;
	char *request_body = NULL;
	const char *location;
	scgi_cfg *cfg = our_dconfig(r);

	if ((ret = ap_setup_client_block(r, REQUEST_CHUNKED_ERROR))) {
		return ret;
	}
	
	/* connect to scgi server */
	ap_hard_timeout("scgi connect", r);
	log_debug(r, "connecting to server");
	sock = open_socket(r, cfg);
	if (sock == -1) {
		if (request_body)
			free(request_body);
		return SERVER_ERROR;
	}
	ap_kill_timeout(r);

	f = ap_bcreate(r->pool, B_RDWR | B_SOCKET);
	ap_bpushfd(f, sock, sock);

	ap_hard_timeout("scgi sending request", r);

	/* send headers */
        if (!send_headers(r, f, cfg)) {
		log_err(r, "error sending response headers");
		return SERVER_ERROR;
	}

	/* send request data */
	if (!send_request_body(r, f)) {
		log_err(r, "error sending response body");
		return SERVER_ERROR;
	}

	ap_kill_timeout(r);

	log_debug(r, "reading response headers");
	ret = ap_scan_script_header_err_buff(r, f, NULL);
	if (ret) {
		if (ret == SERVER_ERROR) {
			log_err(r, "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 Apache 1.3.33.
			 */
			r->status_line = NULL;
		}
		ap_bclose(f);
		return ret;
	}

	location = ap_table_get(r->headers_out, "Location");
	if (location && location[0] == '/' &&
		((r->status == HTTP_OK) || ap_is_HTTP_REDIRECT(r->status))) {

		ap_bclose(f);

		/* 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 = ap_pstrdup(r->pool, "GET");
		r->method_number = M_GET;

		ap_internal_redirect_handler(location, r);
		return OK;
	}

	/* write headers to client */
	ap_send_http_header(r);

	/* write body to client */
	if (!r->header_only) {
		ap_send_fb(f, r);
	}
	ap_bclose(f);

	return OK;
}

static void scgi_init(server_rec *s, pool *p)
{
    ap_add_version_component("mod_scgi/" MOD_SCGI_VERSION);
}

static void *scgi_create_dir_config(pool *p, char *dirspec)
{
	scgi_cfg *cfg = (scgi_cfg *) ap_pcalloc(p, sizeof(scgi_cfg));

	cfg->enabled = UNSET;
	cfg->addr = UNSET;
	cfg->port = UNSET;

	return (void *) cfg;
}

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

static void *
scgi_merge_dir_config(pool *p, void *basev, void *newv)
{
	scgi_cfg *base, *new;
	scgi_cfg *cfg = (scgi_cfg *) ap_pcalloc(p, sizeof(scgi_cfg));
	base = (scgi_cfg *) basev;
	new = (scgi_cfg *) newv;
	
	cfg->enabled = MERGE(base, new, enabled);
	cfg->addr = MERGE(base, new, addr);
	cfg->port = MERGE(base, new, port);

	return (void *) cfg;
}

static const char *
cmd_server(cmd_parms *cmd, scgi_cfg *dcfg, char *addr, char *port)
{
	int n;
	char *tmp;

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

	if ((dcfg->addr = inet_addr(addr)) == INADDR_NONE)
		return "Invalid syntax for server address";

	n = strtol(port, &tmp, 0);
	if (tmp[0] != 0 || n < 0 || n > 65535)
		return "Invalid server port";
	dcfg->port = htons((unsigned short) n);

	return NULL;
}


static const char *
cmd_handler(cmd_parms *cmd, scgi_cfg *dcfg, int flag)
{
	if (cmd->path == NULL) { /* server command */
		return "not a server command";
	} 

	if (flag) {
		dcfg->enabled = ENABLED;
	}
	else {
		dcfg->enabled = DISABLED;
	}

	return NULL;
}

static const command_rec scgi_cmds[] =
{
    { "SCGIServer", cmd_server, NULL, ACCESS_CONF, TAKE2,
      "address and port of an SCGI server (e.g. 127.0.0.1 4000)"},
    { "SCGIHandler", cmd_handler, NULL, ACCESS_CONF, FLAG,
      "On or Off to enable or disable the SCGI handler"},
    {NULL}
};

static const handler_rec scgi_handlers[] =
{
    {"scgi-handler", scgi_handler},
    {NULL}
};

module scgi_module =
{
	STANDARD_MODULE_STUFF,
	scgi_init,			/* module initializer */
	scgi_create_dir_config,		/* per-directory config creator */
	scgi_merge_dir_config,		/* dir config merger */
	NULL,				/* server config creator */
	NULL,				/* server config merger */
	scgi_cmds,			/* command table */
	scgi_handlers,			/* [7] list of handlers */
	scgi_trans,			/* [2] filename-to-URI translation */
	NULL,				/* [5] check/validate user_id */
	NULL,				/* [6] check user_id is valid *here* */
	NULL,				/* [4] check access by host address */
	NULL,				/* [7] MIME type checker/setter */
	NULL,				/* [8] fixups */
	NULL,				/* [10] logger */
#if MODULE_MAGIC_NUMBER >= 19970103
	NULL,				/* [3] header parser */
#endif
#if MODULE_MAGIC_NUMBER >= 19970719
	NULL,				/* process initializer */
#endif
#if MODULE_MAGIC_NUMBER >= 19970728
	NULL,				/* process exit/cleanup */
#endif
#if MODULE_MAGIC_NUMBER >= 19970902
	NULL				/* [1] post read_request handling */
#endif
};
