/* 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.
 */
 
#include <assert.h>
#include <apr_strings.h>

#include <ap_mpm.h>
#include <ap_mmn.h>

#include <httpd.h>
#include <http_core.h>
#include <http_config.h>
#include <http_log.h>
#include <http_connection.h>
#include <http_protocol.h>
#include <http_request.h>
#include <http_ssl.h>

#include <mpm_common.h>

#include "h2_private.h"
#include "h2.h"
#include "h2_bucket_beam.h"
#include "h2_config.h"
#include "h2_conn_ctx.h"
#include "h2_mplx.h"
#include "h2_session.h"
#include "h2_stream.h"
#include "h2_protocol.h"
#include "h2_workers.h"
#include "h2_c1.h"
#include "h2_version.h"
#include "h2_util.h"

static struct h2_workers *workers;

static int async_mpm;
#ifdef AP_MPMQ_CAN_WAITIO
static int mpm_can_waitio;
#endif

APR_OPTIONAL_FN_TYPE(ap_logio_add_bytes_in) *h2_c_logio_add_bytes_in;
APR_OPTIONAL_FN_TYPE(ap_logio_add_bytes_out) *h2_c_logio_add_bytes_out;

apr_status_t h2_c1_child_init(apr_pool_t *pool, server_rec *s)
{
    int minw, maxw;
    apr_time_t idle_limit;

    if (ap_mpm_query(AP_MPMQ_IS_ASYNC, &async_mpm)) {
        /* some MPMs do not implemnent this */
        async_mpm = 0;
    }
#ifdef AP_MPMQ_CAN_WAITIO
    if (!async_mpm || ap_mpm_query(AP_MPMQ_CAN_WAITIO, &mpm_can_waitio)) {
        mpm_can_waitio = 0;
    }
#endif

    h2_config_init(pool);

    h2_get_workers_config(s, &minw, &maxw, &idle_limit);
    workers = h2_workers_create(s, pool, maxw, minw, idle_limit);
 
    h2_c_logio_add_bytes_in = APR_RETRIEVE_OPTIONAL_FN(ap_logio_add_bytes_in);
    h2_c_logio_add_bytes_out = APR_RETRIEVE_OPTIONAL_FN(ap_logio_add_bytes_out);

    return h2_mplx_c1_child_init(pool, s);
}

void h2_c1_child_stopping(apr_pool_t *pool, int graceful)
{
    if (workers) {
        h2_workers_shutdown(workers, graceful);
    }
}


apr_status_t h2_c1_setup(conn_rec *c, request_rec *r, server_rec *s)
{
    h2_session *session;
    h2_conn_ctx_t *ctx;
    apr_status_t rv;
    
    if (!workers) {
        ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, c, APLOGNO(02911) 
                      "workers not initialized");
        rv = APR_EGENERAL;
        goto cleanup;
    }

    rv = h2_session_create(&session, c, r, s, workers);
    if (APR_SUCCESS != rv) goto cleanup;

    ctx = h2_conn_ctx_get(c);
    ap_assert(ctx);
    h2_conn_ctx_assign_session(ctx, session);
    /* remove the input filter of mod_reqtimeout, now that the connection
     * is established and we have switched to h2. reqtimeout has supervised
     * possibly configured handshake timeouts and needs to get out of the way
     * now since the rest of its state handling assumes http/1.x to take place. */
    ap_remove_input_filter_byhandle(c->input_filters, "reqtimeout");

cleanup:
    return rv;
}

int h2_c1_run(conn_rec *c)
{
    apr_status_t status;
    int mpm_state = 0, keepalive = 0;
    h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(c);
    
    ap_assert(conn_ctx);
    ap_assert(conn_ctx->session);
    c->clogging_input_filters = 0;
    do {
        if (c->cs) {
            c->cs->state = CONN_STATE_HANDLER;
        }
    
        status = h2_session_process(conn_ctx->session, async_mpm, &keepalive);
        if (status != APR_SUCCESS) {
            ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, c, 
                          H2_SSSN_LOG(APLOGNO(03045), conn_ctx->session,
                          "process, closing conn"));
            c->keepalive = AP_CONN_CLOSE;
        }
        else {
            c->keepalive = AP_CONN_KEEPALIVE;
        }
        
        if (ap_mpm_query(AP_MPMQ_MPM_STATE, &mpm_state)) {
            break;
        }
    } while (!async_mpm
             && c->keepalive == AP_CONN_KEEPALIVE 
             && mpm_state != AP_MPMQ_STOPPING);

    if (c->cs) {
        switch (conn_ctx->session->state) {
            case H2_SESSION_ST_INIT:
            case H2_SESSION_ST_IDLE:
            case H2_SESSION_ST_BUSY:
            case H2_SESSION_ST_WAIT:
                if (keepalive) {
                    /* Flush then keep-alive */
                    c->cs->sense = CONN_SENSE_DEFAULT;
                    c->cs->state = CONN_STATE_WRITE_COMPLETION;
                }
                else {
                    /* Let the MPM know that we are not done and want to wait
                     * for read using Timeout instead of KeepAliveTimeout.
                     * See PR 63534. 
                     */
                    c->cs->sense = CONN_SENSE_WANT_READ;
#ifdef AP_MPMQ_CAN_WAITIO
                    if (mpm_can_waitio) {
                        /* This tells the MPM to wait for the connection to be
                         * readable (CONN_SENSE_WANT_READ) within the configured
                         * Timeout and then come back to the process_connection()
                         * hooks again when ready.
                         */
                        c->cs->state = CONN_STATE_ASYNC_WAITIO;
                    }
                    else
#endif
                    {
                        /* This is a compat workaround to do the same using the
                         * CONN_STATE_WRITE_COMPLETION state but with both
                         * CONN_SENSE_WANT_READ to wait for readability rather
                         * than writing and c->clogging_input_filters to force
                         * reentering the process_connection() hooks from any
                         * state when ready. This somehow will use Timeout too.
                         */
                        c->cs->state = CONN_STATE_WRITE_COMPLETION;
                        c->clogging_input_filters = 1;
                    }
                }
                break;

            case H2_SESSION_ST_CLEANUP:
            case H2_SESSION_ST_DONE:
            default:
                c->cs->state = CONN_STATE_LINGER;
                break;
        }
    }

    return OK;
}

apr_status_t h2_c1_pre_close(struct h2_conn_ctx_t *ctx, conn_rec *c)
{
    h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(c);

    if (conn_ctx && conn_ctx->session) {
        apr_status_t status = h2_session_pre_close(conn_ctx->session, async_mpm);
        return (status == APR_SUCCESS)? DONE : status;
    }
    return DONE;
}

int h2_c1_allows_direct(conn_rec *c)
{
    if (!c->master) {
        int is_tls = ap_ssl_conn_is_ssl(c);
        const char *needed_protocol = is_tls? "h2" : "h2c";
        int h2_direct = h2_config_cgeti(c, H2_CONF_DIRECT);

        if (h2_direct < 0) {
            h2_direct = is_tls? 0 : 1;
        }
        return (h2_direct && ap_is_allowed_protocol(c, NULL, NULL, needed_protocol));
    }
    return 0;
}

int h2_c1_can_upgrade(request_rec *r)
{
    if (!r->connection->master) {
        int h2_upgrade = h2_config_rgeti(r, H2_CONF_UPGRADE);
        return h2_upgrade > 0 || (h2_upgrade < 0 && !ap_ssl_conn_is_ssl(r->connection));
    }
    return 0;
}

static int h2_c1_hook_process_connection(conn_rec* c)
{
    apr_status_t status;
    h2_conn_ctx_t *ctx;

    if (c->master) goto declined;
    ctx = h2_conn_ctx_get(c);

    ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, "h2_h2, process_conn");
    if (!ctx && c->keepalives == 0) {
        const char *proto = ap_get_protocol(c);

        if (APLOGctrace1(c)) {
            ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, "h2_h2, process_conn, "
                          "new connection using protocol '%s', direct=%d, "
                          "tls acceptable=%d", proto, h2_c1_allows_direct(c),
                          h2_protocol_is_acceptable_c1(c, NULL, 1));
        }

        if (!strcmp(AP_PROTOCOL_HTTP1, proto)
            && h2_c1_allows_direct(c)
            && h2_protocol_is_acceptable_c1(c, NULL, 1)) {
            /* Fresh connection still is on http/1.1 and H2Direct is enabled.
             * Otherwise connection is in a fully acceptable state.
             * -> peek at the first 24 incoming bytes
             */
            apr_bucket_brigade *temp;
            char *peek = NULL;
            apr_size_t peeklen;

            temp = apr_brigade_create(c->pool, c->bucket_alloc);
            status = ap_get_brigade(c->input_filters, temp,
                                    AP_MODE_SPECULATIVE, APR_BLOCK_READ, 24);

            if (status != APR_SUCCESS) {
                ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, c, APLOGNO(03054)
                              "h2_h2, error reading 24 bytes speculative");
                apr_brigade_destroy(temp);
                return DECLINED;
            }

            apr_brigade_pflatten(temp, &peek, &peeklen, c->pool);
            if ((peeklen >= 24) && !memcmp(H2_MAGIC_TOKEN, peek, 24)) {
                ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c,
                              "h2_h2, direct mode detected");
                ctx = h2_conn_ctx_create_for_c1(c, c->base_server,
                                                ap_ssl_conn_is_ssl(c)? "h2" : "h2c");
            }
            else if (APLOGctrace2(c)) {
                ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c,
                              "h2_h2, not detected in %d bytes(base64): %s",
                              (int)peeklen, h2_util_base64url_encode(peek, peeklen, c->pool));
            }
            apr_brigade_destroy(temp);
        }
    }

    if (!ctx) goto declined;

    ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, "process_conn");
    if (!ctx->session) {
        status = h2_c1_setup(c, NULL, ctx->server? ctx->server : c->base_server);
        ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, c, "conn_setup");
        if (status != APR_SUCCESS) {
            h2_conn_ctx_detach(c);
            return !OK;
        }
    }
    return h2_c1_run(c);

declined:
    ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, "h2_h2, declined");
    return DECLINED;
}

static int h2_c1_hook_pre_close(conn_rec *c)
{
    h2_conn_ctx_t *ctx;

    /* secondary connection? */
    if (c->master) {
        return DECLINED;
    }

    ctx = h2_conn_ctx_get(c);
    if (ctx) {
        /* If the session has been closed correctly already, we will not
         * find a h2_conn_ctx_there. The presence indicates that the session
         * is still ongoing. */
        return h2_c1_pre_close(ctx, c);
    }
    return DECLINED;
}

static const char* const mod_ssl[]        = { "mod_ssl.c", NULL};
static const char* const mod_reqtimeout[] = { "mod_ssl.c", "mod_reqtimeout.c", NULL};

void h2_c1_register_hooks(void)
{
    /* Our main processing needs to run quite late. Definitely after mod_ssl,
     * as we need its connection filters, but also before reqtimeout as its
     * method of timeouts is specific to HTTP/1.1 (as of now).
     * The core HTTP/1 processing run as REALLY_LAST, so we will have
     * a chance to take over before it.
     */
    ap_hook_process_connection(h2_c1_hook_process_connection,
                               mod_reqtimeout, NULL, APR_HOOK_LAST);

    /* One last chance to properly say goodbye if we have not done so
     * already. */
    ap_hook_pre_close_connection(h2_c1_hook_pre_close, NULL, mod_ssl, APR_HOOK_LAST);
}

