#define MOD_SCGI_VERSION "1.9"
#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

typedef struct {
	char *path;
	unsigned long addr;	/* address in network byte order */
	unsigned short port;	/* port in network byte order */
} mount_entry;

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

/* Server level configuration */
typedef struct {
	array_header *mounts;
} 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 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 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(char *url, char *prefix, 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_trans(request_rec *r)
{
	scgi_cfg *cfg = our_dconfig(r);
	scgi_server_cfg *scfg = our_sconfig(r->server);
	if (cfg->enabled == DISABLED) {
		return DECLINED;
	}
	if (cfg->mount.addr != UNSET) {
		r->handler = "scgi-handler";
		ap_set_module_config(r->request_config, &scgi_module,
				     &cfg->mount);
		return OK;
	}
	else {
    		int i;
    		mount_entry *entries = (mount_entry *) scfg->mounts->elts;
		for (i = 0; i < scfg->mounts->nelts; ++i) {
			char *path_info;
			mount_entry *p = &entries[i];
			if (mount_entry_matches(r->uri, p->path, &path_info)) {
				r->handler = "scgi-handler";
				r->path_info = path_info;
				ap_set_module_config(r->request_config,
						     &scgi_module,
						     p);
				return OK;
			}
		}
	}
	return DECLINED;
}

int open_socket(request_rec *r)
{
	int retries, sleeptime, rv;
	struct sockaddr_in addr;
	int sock;
	mount_entry *p = (mount_entry *) ap_get_module_config(r->request_config,
							      &scgi_module);

	if (p->addr == UNSET)
		addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	else
		addr.sin_addr.s_addr = p->addr;
	if (p->port == UNSET)
		addr.sin_port = htons(4000);
	else
		addr.sin_port = p->port;
	addr.sin_family = AF_INET;

	/* try to connect */
	retries = 4;
	sleeptime = 1;

restart:
	/* create the socket */
	sock = ap_psocket(r->pool, AF_INET, SOCK_STREAM, 0);
	if (sock == -1) 
		return -1;
  
	rv = connect(sock, (struct sockaddr *)&addr, sizeof addr);
	if (rv != 0) {
		ap_pclosesocket(r->pool, sock);
		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 *http2env(pool *p, const char *name)
{
	char *env_name = ap_pstrcat(p, "HTTP_", name, NULL);
	char *cp;

	for (cp = env_name + 5; *cp != 0; cp++) {
		if (*cp == '-') {
			*cp = '_';
		}
		else {
			*cp = ap_toupper(*cp);
		}
	}

	return env_name;
}


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 find_path_info(const char *uri, const char *path_info)
{
	int n;
	n = strlen(uri) - strlen(path_info);
	ap_assert(n >= 0);
	return n;
}

static int send_headers(request_rec *r, BUFF *f)
{
	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 : "");
	if (r->path_info) {
		int path_info_start = find_path_info(r->uri, r->path_info);
		add_header(t, "SCRIPT_NAME", ap_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 = ap_table_elts(r->headers_in);
	hdrs = (table_entry *) hdrs_arr->elts;
	for (i = 0; i < hdrs_arr->nelts; ++i) {
		if (!hdrs[i].key)
			continue;
		add_header(t, http2env(r->pool, hdrs[i].key), hdrs[i].val);
	}

        /* 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;

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

	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);
	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)) {
		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 *
create_dir_config(pool *p, char *dirspec)
{
	scgi_cfg *cfg = (scgi_cfg *) ap_pcalloc(p, sizeof(scgi_cfg));

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

	return (void *) cfg;
}

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

static void *
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->mount.addr = MERGE(base, new, mount.addr);
	cfg->mount.port = MERGE(base, new, mount.port);

	return (void *) cfg;
}

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

	c->mounts = ap_make_array(p, 20, sizeof(mount_entry));
	return c;
}

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

	c->mounts = ap_append_arrays(p, overrides->mounts, base->mounts);
	return c;
}


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->mount.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->mount.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 char *
cmd_mount(cmd_parms *cmd, void *dummy, char *path, char *addr)
{
	int n;
	char *colon;
	char *addr2;
	char *tmp;

	scgi_server_cfg *scfg = our_sconfig(cmd->server);
	mount_entry *new = ap_push_array(scfg->mounts);
	n = strlen(path);
	while (n > 0 && path[n-1] == '/') {
		n--; /* strip trailing slashes */
	}
	new->path = ap_pstrndup(cmd->pool, path, n);
	if ((colon = strchr(addr, ':')) == NULL)
		return "Invalid syntax for server address";
	addr2 = ap_pstrndup(cmd->pool, addr, colon - addr);
	if ((new->addr = inet_addr(addr2)) == INADDR_NONE)
		return "Invalid syntax for server address";
	n = strtol(colon + 1, &tmp, 0);
	if (tmp[0] != 0 || n < 0 || n > 65535)
		return "Invalid server port";
	new->port = htons((unsigned short) n);
	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"},
    { "SCGIMount", cmd_mount, NULL, RSRC_CONF, TAKE2,
      "path prefix and address of SCGI server"},
    {NULL}
};

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

module scgi_module =
{
	STANDARD_MODULE_STUFF,
	scgi_init,			/* module initializer */
	create_dir_config,		/* per-directory config creator */
	merge_dir_config,		/* dir config merger */
	create_server_config,		/* server config creator */
	merge_server_config,		/* 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
};
