/*
 * Modsecurity wrapper for haproxy
 *
 * This file contains the wrapper which sends data in ModSecurity
 * and returns the verdict.
 *
 * Copyright 2016 OZON, Thierry Fournier <thierry.fournier@ozon.io>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version
 * 2 of the License, or (at your option) any later version.
 *
 */
#include <limits.h>
#include <stdio.h>
#include <stdarg.h>

#include <haproxy/arg.h>
#include <haproxy/errors.h>
#include <haproxy/global.h>
#include <haproxy/hlua.h>
#include <haproxy/spoe.h>
#include <haproxy/stream-t.h>
#include <haproxy/time.h>

#include <api.h>

#include "modsec_wrapper.h"
#include "spoa.h"

static char host_name[60];

/* Note: The document and the code of "apr_table_make" considers
 * that this function doesn't fails. The Apache APR code says
 * other thing. If the system doesn't have any more memory, a
 * a segfault occurs :(. Be carrefull with this module.
 */

struct directory_config *modsec_config = NULL;
static server_rec *modsec_server = NULL;

struct apr_bucket_haproxy {
	apr_bucket_refcount refcount;
	char *buffer;
	size_t length;
};

static void haproxy_bucket_destroy(void *data)
{
	struct apr_bucket_haproxy *bucket = data;

	if (apr_bucket_shared_destroy(bucket))
		apr_bucket_free(bucket);
}

static apr_status_t haproxy_bucket_read(apr_bucket *bucket, const char **str,
                                        apr_size_t *len, apr_read_type_e block)
{
	struct apr_bucket_haproxy *data = bucket->data;

	if (bucket->start) {
		*str = NULL;
		*len = 0;
		return APR_SUCCESS;
	}

	*str = data->buffer;
	*len = data->length;
	bucket->start = 1; /* Just a flag to say that the read is started */

	return APR_SUCCESS;
}

static const apr_bucket_type_t apr_bucket_type_haproxy = {
	"HAProxy", 7, APR_BUCKET_DATA,
	haproxy_bucket_destroy,
	haproxy_bucket_read,
	apr_bucket_setaside_noop,
	apr_bucket_shared_split,
	apr_bucket_shared_copy
};

static char *chunk_strdup(struct request_rec *req, const char *str, size_t len)
{
	char *out;

	out = apr_pcalloc(req->pool, len + 1);
	if (!out)
		return NULL;
	memcpy(out, str, len);
	out[len] = '\0';
	return out;
}

static char *printf_dup(struct request_rec *req, char *fmt, ...)
{
	char *out;
	va_list ap;
	int len;

	va_start(ap, fmt);
	len = vsnprintf(NULL, 0, fmt, ap);
	va_end(ap);

	if (len == -1)
		return NULL;

	out = apr_pcalloc(req->pool, len + 1);
	if (!out)
		return NULL;

	va_start(ap, fmt);
	len = vsnprintf(out, len + 1, fmt, ap);
	va_end(ap);

	if (len == -1)
		return NULL;

	return out;
}

/* This function send logs. For now, it do nothing. */
static void modsec_log(void *obj, int level, char *str)
{
	LOG(&null_worker, "%s", str);
}

/* This function load the ModSecurity file. It returns -1 if the
 * initialisation fails.
 */
int modsecurity_load(const char *file)
{
	const char *msg;
	char cwd[128];

	/* Initialises modsecurity. */

	modsec_server = modsecInit();
	if (modsec_server == NULL) {
		LOG(&null_worker, "ModSecurity initialisation failed.\n");
		return -1;
	}

	modsecSetLogHook(NULL, modsec_log);

	gethostname(host_name, 60);
	modsec_server->server_hostname = host_name;

	modsecStartConfig();

	modsec_config = modsecGetDefaultConfig();
	if (modsec_config == NULL) {
		LOG(&null_worker, "ModSecurity default configuration initialisation failed.\n");
		return -1;
	}

	msg = modsecProcessConfig(modsec_config, file, getcwd(cwd, 128));
	if (msg != NULL) {
		LOG(&null_worker, "ModSecurity load configuration failed.\n");
		return -1;
	}

	modsecFinalizeConfig();

	modsecInitProcess();

	return 1;
}

struct modsec_hdr {
	const char *name;
	uint64_t name_len;
	const char *value;
	uint64_t value_len;
};

int modsecurity_process(struct worker *worker, struct modsecurity_parameters *params)
{
	struct conn_rec *cr;
	struct request_rec *req;
	struct apr_bucket_brigade *brigade;
	struct apr_bucket *link_bucket;
	struct apr_bucket_haproxy *data_bucket;
	struct apr_bucket *last_bucket;
	int i;
	long clength;
	char *err;
	int fail;
	const char *lang;
	char *name, *value;
	// int body_partial;
	struct timeval now;
	int ret;
	char *buf;
	char *end;
	const char *uniqueid;
	uint64_t uniqueid_len;
	const char *meth;
	uint64_t meth_len;
	const char *path;
	uint64_t path_len;
	const char *qs;
	uint64_t qs_len;
	const char *vers;
	uint64_t vers_len;
	const char *body;
	uint64_t body_len;
	uint64_t body_exposed_len;
	uint64_t hdr_nb;
	struct modsec_hdr hdrs[255];
	struct modsec_hdr hdr;
	int status;
	int return_code = -1;

	/* Decode uniqueid. */
	uniqueid = params->uniqueid.data.u.str.area;
	uniqueid_len = params->uniqueid.data.u.str.data;

	/* Decode method. */
	meth = params->method.data.u.str.area;
	meth_len = params->method.data.u.str.data;

	/* Decode path. */
	path = params->path.data.u.str.area;
	path_len = params->path.data.u.str.data;

	/* Decode query string. */
	qs = params->query.data.u.str.area;
	qs_len = params->query.data.u.str.data;

	/* Decode version. */
	vers = params->vers.data.u.str.area;
	vers_len = params->vers.data.u.str.data;

	/* Decode header binary block. */
	buf = params->hdrs_bin.data.u.str.area;
	end = buf + params->hdrs_bin.data.u.str.data;

	/* Decode each header. */
	hdr_nb = 0;
	while (1) {

		/* Initialise the storage struct. It is useless
		 * because the process fail if the struct is not
		 * fully filled. This init is just does in order
		 * to prevent bug after some improvements.
		 */
		memset(&hdr, 0, sizeof(hdr));

		/* Decode header name. */
		ret = decode_varint(&buf, end, &hdr.name_len);
		if (ret == -1)
			return -1;
		hdr.name = buf;
		buf += hdr.name_len;
		if (buf > end)
			return -1;

		/* Decode header value. */
		ret = decode_varint(&buf, end, &hdr.value_len);
		if (ret == -1)
			return -1;
		hdr.value = buf;
		buf += hdr.value_len;
		if (buf > end)
			return -1;

		/* Detect the end of the headers. */
		if (hdr.name_len == 0 && hdr.value_len == 0)
			break;

		/* Store the header. */
		if (hdr_nb < 255) {
			memcpy(&hdrs[hdr_nb], &hdr, sizeof(hdr));
			hdr_nb++;
		}
	}

	/* Decode body length. Note that the following control
	 * is just set for avoifing a gcc warning.
	 */
	body_exposed_len = (uint64_t)params->body_length.data.u.sint;
	if (body_exposed_len < 0)
		return -1;

	/* Decode body. */
	body = params->body.data.u.str.area;
	body_len = params->body.data.u.str.data;

	fail = 1;

	/* Init processing */

	cr = modsecNewConnection();
	req = modsecNewRequest(cr, modsec_config);

	/* Load request. */

	req->proxyreq = PROXYREQ_NONE;
	req->header_only = 0; /* May modified later */

	/* Copy header list. */

	for (i = 0; i < hdr_nb; i++) {
		name = chunk_strdup(req, hdrs[i].name, hdrs[i].name_len);
		if (!name) {
			errno = ENOMEM;
			goto fail;
		}
		value = chunk_strdup(req, hdrs[i].value, hdrs[i].value_len);
		if (!value) {
			errno = ENOMEM;
			goto fail;
		}
		apr_table_setn(req->headers_in, name, value);
	}

	/* Process special headers. */
	req->range = apr_table_get(req->headers_in, "Range");
	req->content_type = apr_table_get(req->headers_in, "Content-Type");
	req->content_encoding = apr_table_get(req->headers_in, "Content-Encoding");
	req->hostname = apr_table_get(req->headers_in, "Host");
	if (req->hostname != NULL) {
		req->parsed_uri.hostname = chunk_strdup(req, req->hostname, strlen(req->hostname));
	} else {
		req->parsed_uri.hostname = NULL;
	}

	lang = apr_table_get(req->headers_in, "Content-Languages");
	if (lang != NULL) {
		req->content_languages = apr_array_make(req->pool, 1, sizeof(const char *));
		*(const char **)apr_array_push(req->content_languages) = lang;
	}

	lang = apr_table_get(req->headers_in, "Content-Length");
	if (lang) {
		errno = 0;
		clength = strtol(lang, &err, 10);
		if (*err != '\0' || errno != 0 || clength < 0 || clength > INT_MAX) {
			errno = ERANGE;
			goto fail;
		}
		req->clength = clength;
	}

	/* Copy the first line of the request. */
	req->the_request = printf_dup(req, "%.*s %.*s%s%.*s %.*s",
	                              meth_len, meth,
	                              path_len, path,
	                              qs_len > 0 ? "?" : "",
	                              qs_len, qs,
	                              vers_len, vers);
	if (!req->the_request) {
		errno = ENOMEM;
		goto fail;
	}

	/* Copy the method. */
	req->method = chunk_strdup(req, meth, meth_len);
	if (!req->method) {
		errno = ENOMEM;
		goto fail;
	}

	/* Set the method number. */
	if (meth_len < 3) {
		errno = EINVAL;
		goto fail;
	}

	/* Detect the method */
	switch (meth_len) {
	case 3:
		if (strncmp(req->method, "GET", 3) == 0)
			req->method_number = M_GET;
		else if (strncmp(req->method, "PUT", 3) == 0)
			req->method_number = M_PUT;
		else {
			errno = EINVAL;
			goto fail;
		}
		break;
	case 4:
		if (strncmp(req->method, "POST", 4) == 0)
			req->method_number = M_POST;
		else if (strncmp(req->method, "HEAD", 4) == 0) {
			req->method_number = M_GET;
			req->header_only = 1;
		}
		else if (strncmp(req->method, "COPY", 4) == 0)
			req->method_number = M_COPY;
		else if (strncmp(req->method, "MOVE", 4) == 0)
			req->method_number = M_MOVE;
		else if (strncmp(req->method, "LOCK", 4) == 0)
			req->method_number = M_LOCK;
		else {
			errno = EINVAL;
			goto fail;
		}
		break;
	case 5:
		if (strncmp(req->method, "TRACE", 5) == 0)
			req->method_number = M_TRACE;
		else if (strncmp(req->method, "PATCH", 5) == 0)
			req->method_number = M_PATCH;
		else if (strncmp(req->method, "MKCOL", 5) == 0)
			req->method_number = M_MKCOL;
		else if (strncmp(req->method, "MERGE", 5) == 0)
			req->method_number = M_MERGE;
		else if (strncmp(req->method, "LABEL", 5) == 0)
			req->method_number = M_LABEL;
		else {
			errno = EINVAL;
			goto fail;
		}
		break;
	case 6:
		if (strncmp(req->method, "DELETE", 6) == 0)
			req->method_number = M_DELETE;
		else if (strncmp(req->method, "REPORT", 6) == 0)
			req->method_number = M_REPORT;
		else if (strncmp(req->method, "UPDATE", 6) == 0)
			req->method_number = M_UPDATE;
		else if (strncmp(req->method, "UNLOCK", 6) == 0)
			req->method_number = M_UNLOCK;
		else {
			errno = EINVAL;
			goto fail;
		}
		break;
	case 7:
		if (strncmp(req->method, "CHECKIN", 7) == 0)
			req->method_number = M_CHECKIN;
		else if (strncmp(req->method, "INVALID", 7) == 0)
			req->method_number = M_INVALID;
		else if (strncmp(req->method, "CONNECT", 7) == 0)
			req->method_number = M_CONNECT;
		else if (strncmp(req->method, "OPTIONS", 7) == 0)
			req->method_number = M_OPTIONS;
		else {
			errno = EINVAL;
			goto fail;
		}
		break;
	case 8:
		if (strncmp(req->method, "PROPFIND", 8) == 0)
			req->method_number = M_PROPFIND;
		else if (strncmp(req->method, "CHECKOUT", 8) == 0)
			req->method_number = M_CHECKOUT;
		else {
			errno = EINVAL;
			goto fail;
		}
		break;
	case 9:
		if (strncmp(req->method, "PROPPATCH", 9) == 0)
			req->method_number = M_PROPPATCH;
		else {
			errno = EINVAL;
			goto fail;
		}
		break;
	case 10:
		if (strncmp(req->method, "MKACTIVITY", 10) == 0)
			req->method_number = M_MKACTIVITY;
		else if (strncmp(req->method, "UNCHECKOUT", 10) == 0)
			req->method_number = M_UNCHECKOUT;
		else {
			errno = EINVAL;
			goto fail;
		}
		break;
	case 11:
		if (strncmp(req->method, "MKWORKSPACE", 11) == 0)
			req->method_number = M_MKWORKSPACE;
		else {
			errno = EINVAL;
			goto fail;
		}
		break;
	case 15:
		if (strncmp(req->method, "VERSION_CONTROL", 15) == 0)
			req->method_number = M_VERSION_CONTROL;
		else {
			errno = EINVAL;
			goto fail;
		}
		break;
	case 16:
		if (strncmp(req->method, "BASELINE_CONTROL", 16) == 0)
			req->method_number = M_BASELINE_CONTROL;
		else {
			errno = EINVAL;
			goto fail;
		}
		break;
	default:
		errno = EINVAL;
		goto fail;
	}

	/* Copy the protocol. */
	req->protocol = chunk_strdup(req, vers, vers_len);
	if (!req->protocol) {
		errno = ENOMEM;
		goto fail;
	}

	/* Compute the protocol number. */
	if (vers_len >= 8)
		req->proto_num = 1000 + !!(vers[7] == '1');

	/* The request time. */
	gettimeofday(&now, NULL);
	req->request_time = apr_time_make(now.tv_sec, now.tv_usec / 1000);

	/* No status line. */
	req->status_line = NULL;
	req->status = 0;

	/* Copy path. */
	req->parsed_uri.path = chunk_strdup(req, path, path_len);
	if (!req->parsed_uri.path) {
		errno = ENOMEM;
		goto fail;
	}

	/* Copy args (query string). */
	req->args = chunk_strdup(req, qs, qs_len);
	if (!req->args) {
		errno = ENOMEM;
		goto fail;
	}

	/* Set parsed_uri */

	req->parsed_uri.scheme = "http";

	if (req->hostname && req->parsed_uri.scheme && req->parsed_uri.path) {
		i = snprintf(NULL, 0, "%s://%s%s",
		             req->parsed_uri.scheme, req->hostname, req->parsed_uri.path);
		req->uri = apr_pcalloc(req->pool, i + 1);
		if (!req->uri) {
			errno = ENOMEM;
			goto fail;
		}
		i = snprintf(req->uri, i + 1, "%s://%s%s",
		             req->parsed_uri.scheme, req->hostname, req->parsed_uri.path);
	}

	req->filename = req->parsed_uri.path;

	/* Set unique id */

	apr_table_setn(req->subprocess_env, "UNIQUE_ID", chunk_strdup(req, uniqueid, uniqueid_len));

	/*
	 *
	 * Load body.
	 *
	 */

	/* Create an empty bucket brigade */
	brigade = apr_brigade_create(req->pool, req->connection->bucket_alloc);
	if (!brigade) {
		errno = ENOMEM;
		goto fail;
	}

	/* Stores HTTP body available data in a bucket */
	data_bucket = apr_bucket_alloc(sizeof(*data_bucket), req->connection->bucket_alloc);
	if (!data_bucket) {
		errno = ENOMEM;
		goto fail;
	}
	data_bucket->buffer = (char *)body;
	data_bucket->length = body_len;

	/* Create linked bucket */
	link_bucket = apr_bucket_alloc(sizeof(*link_bucket), req->connection->bucket_alloc);
	if (!link_bucket) {
		errno = ENOMEM;
		goto fail;
	}
	APR_BUCKET_INIT(link_bucket); /* link */
	link_bucket->free = apr_bucket_free;
	link_bucket->list = req->connection->bucket_alloc;
	link_bucket = apr_bucket_shared_make(link_bucket, data_bucket, 0, body_len);
	link_bucket->type = &apr_bucket_type_haproxy;

	/* Insert the bucket at the end of the brigade. */
	APR_BRIGADE_INSERT_TAIL(brigade, link_bucket);

	/* Insert the last bucket. */
	last_bucket = apr_bucket_eos_create(req->connection->bucket_alloc);
	APR_BRIGADE_INSERT_TAIL(brigade, last_bucket);

	/* Declares the bucket brigade in modsecurity */
	modsecSetBodyBrigade(req, brigade);

	/*
	 *
	 * Process analysis.
	 *
	 */

	/* Process request headers analysis. */
	status = modsecProcessRequestHeaders(req);
	if (status != DECLINED && status != DONE)
		return_code = status;

	/* Process request body analysis. */
	status = modsecProcessRequestBody(req);
	if (status != DECLINED && status != DONE)
		return_code = status;

	/* End processing. */

	fail = 0;
	if (return_code == -1)
		return_code = 0;

fail:

	modsecFinishRequest(req);
	modsecFinishConnection(cr);

	if (fail) {

		/* errno == ERANGE / ENOMEM / EINVAL */
		switch (errno) {
		case ERANGE: LOG(worker, "Invalid range");
		case ENOMEM: LOG(worker, "Out of memory error");
		case EINVAL: LOG(worker, "Invalid value");
		default:     LOG(worker, "Unknown error");
		}
	}

	return return_code;
}
