MINOR: Add ModSecurity wrapper as contrib
This patch contains a base for a modsecurity wrapper in HAProxy using SPOE.
diff --git a/contrib/modsecurity/modsec_wrapper.c b/contrib/modsecurity/modsec_wrapper.c
new file mode 100644
index 0000000..8927b5b
--- /dev/null
+++ b/contrib/modsecurity/modsec_wrapper.c
@@ -0,0 +1,639 @@
+/*
+ * 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 <common/time.h>
+
+#include <types/global.h>
+#include <types/stream.h>
+
+#include <proto/arg.h>
+#include <proto/hdr_idx.h>
+#include <proto/hlua.h>
+#include <proto/log.h>
+#include <proto/proto_http.h>
+#include <proto/spoe.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);
+ if (len == -1)
+ return NULL;
+ va_end(ap);
+
+ out = apr_pcalloc(req->pool, len + 1);
+ if (!out)
+ return NULL;
+
+ va_start(ap, fmt);
+ len = vsnprintf(out, len + 1, fmt, ap);
+ if (len == -1)
+ return NULL;
+ va_end(ap);
+
+ 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 fucntion 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 initilisation 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 initilisation 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.str;
+ uniqueid_len = params->uniqueid.data.u.str.len;
+
+ /* Decode method. */
+ meth = params->method.data.u.str.str;
+ meth_len = params->method.data.u.str.len;
+
+ /* Decode path. */
+ path = params->path.data.u.str.str;
+ path_len = params->path.data.u.str.len;
+
+ /* Decode query string. */
+ qs = params->query.data.u.str.str;
+ qs_len = params->query.data.u.str.len;
+
+ /* Decode version. */
+ vers = params->vers.data.u.str.str;
+ vers_len = params->vers.data.u.str.len;
+
+ /* Decode header binary block. */
+ buf = params->hdrs_bin.data.u.str.str;
+ end = buf + params->hdrs_bin.data.u.str.len;
+
+ /* 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.str;
+ body_len = params->body.data.u.str.len;
+
+ 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");
+ req->parsed_uri.hostname = chunk_strdup(req, req->hostname, strlen(req->hostname));
+
+ 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 avalaible 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;
+}