/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

/* $Rev$ $Date$ */

/**
 * HTTPD module used to tunnel traffic over an HTTPS connection.
 */

#include <sys/stat.h>

#define WANT_HTTPD_LOG 1
#include "string.hpp"
#include "stream.hpp"
#include "list.hpp"
#include "tree.hpp"
#include "value.hpp"
#include "monad.hpp"
#include "httpd.hpp"
#include "http.hpp"

// Ignore cast align warnings in APR macros
#ifdef WANT_MAINTAINER_WARNINGS
#pragma GCC diagnostic ignored "-Wcast-align"
#endif

extern "C" {
extern module AP_MODULE_DECLARE_DATA mod_tuscany_ssltunnel;
}

namespace tuscany {
namespace httpd {
namespace modssltunnel {

/**
 * Server configuration.
 */
class ServerConf {
public:
    ServerConf(apr_pool_t* const p, server_rec* const s) : p(p), server(s) {
    }

    const gc_pool p;
    server_rec* const server;
    gc_mutable_ref<string> pass;
    gc_mutable_ref<string> host;
    gc_mutable_ref<string> path;
    gc_mutable_ref<string> ca;
    gc_mutable_ref<string> cert;
    gc_mutable_ref<string> key;
};

extern "C" {
extern module AP_DECLARE_DATA core_module;
}

/**
 * Process the module configuration.
 */
int M_SSLTUNNEL;
int postConfigMerge(const ServerConf& mainsc, apr_pool_t* const p, server_rec* const s) {
    if (s == NULL)
        return OK;
    ServerConf& sc = httpd::serverConf<ServerConf>(s, &mod_tuscany_ssltunnel);
    debug(httpd::serverName(s), "modssltunnel::postConfigMerge::serverName");

    // Merge configuration from main server
    if (length(sc.ca) == 0 && length(mainsc.ca) !=0)
        sc.ca = mainsc.ca;
    if (length(sc.cert) == 0 && length(mainsc.cert) !=0)
        sc.cert = mainsc.cert;
    if (length(sc.key) == 0 && length(mainsc.key) !=0)
        sc.key = mainsc.key;

    // Parse the configured TunnelPass URI
    if (length(sc.pass) != 0) {
        apr_uri_t uri;
        apr_status_t prc = apr_uri_parse(p, c_str(sc.pass), &uri);
        if (prc != APR_SUCCESS) {
            mkfailure<int>("Couldn't parse TunnelPass: " + sc.pass + ", " + http::apreason(prc));
            return prc;
        }
        sc.host = uri.hostname;
        sc.path = uri.path;
    }
    return postConfigMerge(mainsc, p, s->next);
}

int postConfig(apr_pool_t* const p, unused apr_pool_t* const plog, unused apr_pool_t* const ptemp, server_rec* const s) {
    const gc_scoped_pool sp(p);

    const ServerConf& sc = httpd::serverConf<ServerConf>(s, &mod_tuscany_ssltunnel);
    debug(httpd::serverName(s), "modssltunnel::postConfig::serverName");

    // Register the SSLTUNNEL method
    M_SSLTUNNEL = ap_method_register(p, "SSLTUNNEL");

    // Merge and process server configurations
    return postConfigMerge(sc, p, s);
}

/**
 * Close a connection.
 */
const int close(conn_rec* const conn, apr_socket_t* const csock) {
    debug("modssltunnel::close");
    apr_socket_close(csock);
    conn->aborted = 1;
    return OK;
}

/**
 * Abort a connection.
 */
const int abort(conn_rec* const conn, apr_socket_t* const csock, const string& reason) {
    debug("modssltunnel::abort");
    apr_socket_close(csock);
    conn->aborted = 1;
    return httpd::reportStatus(mkfailure<int>(reason));
}

/**
 * Tunnel traffic from a client connection to a target URL.
 */
int tunnel(conn_rec* const conn, const string& ca, const string& cert, const string& key, const string& url, const string& preamble, const gc_pool& p, unused ap_filter_t* const ifilter, ap_filter_t* const ofilter) {

    // Create input/output bucket brigades
    apr_bucket_brigade* const ib = apr_brigade_create(pool(p), conn->bucket_alloc);
    apr_bucket_brigade* const ob = apr_brigade_create(pool(p), conn->bucket_alloc);

    // Get client connection socket
    apr_socket_t* const csock = (apr_socket_t*)ap_get_module_config(conn->conn_config, &core_module);

    // Open connection to target
    const http::CURLSession cs(ca, cert, key, emptyString, 0);
    const failable<bool> crc = http::connect(url, cs);
    if (!hasContent(crc))
        return abort(conn, csock, reason(crc));
    apr_socket_t* const tsock = http::sock(cs);

    // Send preamble
    if (length(preamble) != 0) {
        debug(preamble, "modssltunnel::tunnel::sendPreambleToTarget");
        const failable<bool> src = http::send(c_str(preamble), length(preamble), cs);
        if (!hasContent(src))
            return abort(conn, csock, string("Couldn't send to target: ") + reason(src));
    }

    // Create a pollset for the client and target sockets
    apr_pollset_t* pollset;
    const apr_status_t cprc = apr_pollset_create(&pollset, 2, pool(p), 0);
    if (cprc != APR_SUCCESS)
        return abort(conn, csock, http::apreason(cprc));
    const apr_pollfd_t* cpollfd = http::pollfd(csock, APR_POLLIN, p);
    apr_pollset_add(pollset, cpollfd);
    const apr_pollfd_t* tpollfd = http::pollfd(tsock, APR_POLLIN, p);
    apr_pollset_add(pollset, tpollfd);

    // Relay traffic in both directions until end of stream
    const apr_pollfd_t* pollfds = cpollfd;
    apr_int32_t pollcount = 1;
    for(;;) {
        for (; pollcount > 0; pollcount--, pollfds++) {
            if (pollfds->rtnevents & APR_POLLIN) {
                if (pollfds->desc.s == csock) {

                    // Receive buckets from client
                    const apr_status_t getrc = ap_get_brigade(conn->input_filters, ib, AP_MODE_READBYTES, APR_BLOCK_READ, HUGE_STRING_LEN);
                    if (getrc != APR_SUCCESS)
                        return abort(conn, csock, string("Couldn't receive from client"));

                    for (apr_bucket* bucket = APR_BRIGADE_FIRST(ib); bucket != APR_BRIGADE_SENTINEL(ib); bucket = APR_BUCKET_NEXT(bucket)) {
                        if (APR_BUCKET_IS_FLUSH(bucket))
                            continue;

                        // Client connection closed
                        if (APR_BUCKET_IS_EOS(bucket))
                            return close(conn, csock);

                        const char *data;
                        apr_size_t rl;
                        apr_bucket_read(bucket, &data, &rl, APR_BLOCK_READ);
                        if (rl > 0) {
                            debug(string(data, rl), "modssltunnel::tunnel::sendToTarget");

                            // Send to target
                            const failable<bool> src = http::send(data, rl, cs);
                            if (!hasContent(src))
                                return abort(conn, csock, string("Couldn't send to target: ") + reason(src));
                        }
                    }
                    apr_brigade_cleanup(ib);
                } else {

                    // Receive from target
                    char data[8192];
                    const failable<size_t> frl = http::recv(data, sizeof(data), cs);
                    if (!hasContent(frl))
                        return abort(conn, csock, string("Couldn't receive from target") + reason(frl));
                    const size_t rl = content(frl);

                    // Target connection closed
                    if (rl == 0)
                        return close(conn, csock);


                    // Send bucket to client
                    debug(string(data, rl), "modssltunnel::tunnel::sendToClient");
                    APR_BRIGADE_INSERT_TAIL(ob, apr_bucket_transient_create(data, rl, conn->bucket_alloc));
                    APR_BRIGADE_INSERT_TAIL(ob, apr_bucket_flush_create(conn->bucket_alloc));
                    if (ap_pass_brigade(ofilter, ob) != APR_SUCCESS)
                        return abort(conn, csock, "Couldn't send data bucket to client");
                    apr_brigade_cleanup(ob);
                }
            }

            // Error
            if (pollfds->rtnevents & (APR_POLLERR | APR_POLLHUP | APR_POLLNVAL)) {
                if (pollfds->desc.s == csock) 
                    return abort(conn, csock, "Couldn't receive from client");
                else
                    return abort(conn, csock, "Couldn't receive from target");
            }
        }

        // Poll the client and target sockets
        debug("modssltunnel::tunnel::poll");
        const apr_status_t pollrc = apr_pollset_poll(pollset, -1, &pollcount, &pollfds);
        if (pollrc != APR_SUCCESS)
            return abort(conn, csock, "Couldn't poll sockets");
        debug(pollcount, "modssltunnel::tunnel::pollfds");
    }

    // Close client connection
    return close(conn, csock);
}

/**
 * Return the first connection filter in a list of filters.
 */
ap_filter_t* const connectionFilter(ap_filter_t* const f) {
    if (f == NULL)
        return f;
    if (f->frec->ftype < AP_FTYPE_CONNECTION)
        return connectionFilter(f->next);
    return f;
}

/**
 * Process a client connection and relay it to a tunnel.
 */
int processConnection(conn_rec* conn) {
    // Only allow configured virtual hosts
    if (!conn->base_server->is_virtual)
        return DECLINED;
    if (ap_get_module_config(conn->base_server->module_config, &mod_tuscany_ssltunnel) == NULL)
        return DECLINED;

    const gc_scoped_pool sp(conn->pool);

    // Get the server configuration
    const ServerConf& sc = httpd::serverConf<ServerConf>(conn->base_server, &mod_tuscany_ssltunnel);
    if (length(sc.pass) == 0)
        return DECLINED;
    debug(sc.pass, "modssltunnel::processConnection::pass");

    // Run the tunnel
    const string preamble = string("SSLTUNNEL ") + sc.path + string(" HTTP/1.1\r\nHost: ") + sc.host + string("\r\n\r\n");
    debug(preamble, "modssltunnel::processConnection::preamble");
    return tunnel(conn, sc.ca, sc.cert, sc.key, sc.pass, preamble, gc_pool(conn->pool), connectionFilter(conn->input_filters), connectionFilter(conn->output_filters));
}

/**
 * Tunnel a SSLTUNNEL request to a target host/port.
 */
int handler(request_rec* r) {
    if (r->method_number != M_SSLTUNNEL)
        return DECLINED;

    // Only allow HTTPS
    if (strcmp(r->server->server_scheme, "https"))
        return DECLINED;

    const gc_scoped_pool sp(r->pool);

    // Build the target URL
    debug(r->uri, "modssltunnel::handler::uri");
    const list<value> path(pathValues(r->uri));
    const string url = string(cadr(path)) + ":" + (string)caddr(path);
    debug(url, "modssltunnel::handler::target");

    // Run the tunnel
    return tunnel(r->connection, emptyString, emptyString, emptyString, url, emptyString, gc_pool(r->pool), connectionFilter(r->proto_input_filters), connectionFilter(r->proto_output_filters));
}

/**
 * Configuration commands.
 */
char* confTunnelPass(cmd_parms *cmd, unused void *c, const char *arg) {
    const gc_scoped_pool sp(cmd->pool);
    ServerConf& sc = httpd::serverConf<ServerConf>(cmd, &mod_tuscany_ssltunnel);
    sc.pass = arg;
    return NULL;
}
char* confCAFile(cmd_parms *cmd, unused void *c, const char *arg) {
    const gc_scoped_pool sp(cmd->pool);
    ServerConf& sc = httpd::serverConf<ServerConf>(cmd, &mod_tuscany_ssltunnel);
    sc.ca = arg;
    return NULL;
}
char* confCertFile(cmd_parms *cmd, unused void *c, const char *arg) {
    const gc_scoped_pool sp(cmd->pool);
    ServerConf& sc = httpd::serverConf<ServerConf>(cmd, &mod_tuscany_ssltunnel);
    sc.cert = arg;
    return NULL;
}
char* confCertKeyFile(cmd_parms *cmd, unused void *c, const char *arg) {
    const gc_scoped_pool sp(cmd->pool);
    ServerConf& sc = httpd::serverConf<ServerConf>(cmd, &mod_tuscany_ssltunnel);
    sc.key = arg;
    return NULL;
}

/**
 * HTTP server module declaration.
 */
const command_rec commands[] = {
    AP_INIT_TAKE1("TunnelPass", (const char*(*)())confTunnelPass, NULL, RSRC_CONF, "Tunnel server name"),
    AP_INIT_TAKE1("TunnelSSLCACertificateFile", (const char*(*)())confCAFile, NULL, RSRC_CONF, "Tunnel SSL CA certificate file"),
    AP_INIT_TAKE1("TunnelSSLCertificateFile", (const char*(*)())confCertFile, NULL, RSRC_CONF, "Tunnel SSL certificate file"),
    AP_INIT_TAKE1("TunnelSSLCertificateKeyFile", (const char*(*)())confCertKeyFile, NULL, RSRC_CONF, "Tunnel SSL certificate key file"),
    {NULL, NULL, NULL, 0, NO_ARGS, NULL}
};

void registerHooks(unused apr_pool_t *p) {
    ap_hook_post_config(postConfig, NULL, NULL, APR_HOOK_MIDDLE);
    ap_hook_handler(handler, NULL, NULL, APR_HOOK_MIDDLE);
    ap_hook_process_connection(processConnection, NULL, NULL, APR_HOOK_MIDDLE);
}

}
}
}

extern "C" {

module AP_MODULE_DECLARE_DATA mod_tuscany_ssltunnel = {
    STANDARD20_MODULE_STUFF,
    // dir config and merger
    NULL, NULL,
    // server config and merger
    tuscany::httpd::makeServerConf<tuscany::httpd::modssltunnel::ServerConf>, NULL,
    // commands and hooks
    tuscany::httpd::modssltunnel::commands, tuscany::httpd::modssltunnel::registerHooks
};

}

// Reenable cast align warnings
#ifdef WANT_MAINTAINER_WARNINGS
#pragma GCC diagnostic warning "-Wcast-align"
#endif

