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

#include "http_main.h"
#include "http_log.h"
#include "http_protocol.h"
#include "http_request.h"

#include "apreq_private_apache.h"

/* This is basically the cgi_handle struct with a request_rec */

struct apache_handle {
    struct apreq_handle_t        handle;
    request_rec                 *r;
    apr_pool_t                  *pool;
    apr_bucket_alloc_t          *bucket_alloc;

    apr_table_t                 *jar, *args, *body;
    apr_status_t                 jar_status,
                                 args_status,
                                 body_status;

    apreq_parser_t              *parser;
    apreq_hook_t                *hook_queue;

    const char                  *temp_dir;
    apr_size_t                   brigade_limit;
    apr_uint64_t                 read_limit;
    apr_uint64_t                 bytes_read;
    apr_bucket_brigade          *in;

};

static const char *apache_header_in(apreq_handle_t *env, const char *name)
{
    struct apache_handle *req = (struct apache_handle *)env;
    return ap_table_get(req->r->headers_in, name);
}

static apr_status_t apache_header_out(apreq_handle_t *env,
                                       const char *name, char *value)
{
    struct apache_handle *req = (struct apache_handle *)env;
    ap_table_add(req->r->err_headers_out, name, value);
    return APR_SUCCESS;
}

#ifdef APR_POOL_DEBUG
static apr_status_t ba_cleanup(void *data)
{
    apr_bucket_alloc_t *ba = data;
    apr_bucket_alloc_destroy(ba);
    return APR_SUCCESS;
}
#endif

static void init_body(apreq_handle_t *env)
{
    struct apache_handle *req = (struct apache_handle *)env;
    const char *cl_header = apache_header_in(env, "Content-Length");
    apr_bucket_alloc_t *ba = req->bucket_alloc;
    request_rec *r = req->r;

    req->body  = apr_table_make(req->pool, APREQ_DEFAULT_NELTS);
#ifdef APR_POOL_DEBUG
    apr_pool_cleanup_register(req->pool, ba, ba_cleanup, ba_cleanup);
#endif
    if (cl_header != NULL) {
        char *dummy;
        apr_int64_t content_length = apr_strtoi64(cl_header, &dummy, 10);

        if (dummy == NULL || *dummy != 0) {
            req->body_status = APREQ_ERROR_BADHEADER;
            ap_log_rerror(APLOG_MARK, APLOG_ERR, r,
                          "Invalid Content-Length header (%s)", cl_header);
            return;
        }
        else if ((apr_uint64_t)content_length > req->read_limit) {
            req->body_status = APREQ_ERROR_OVERLIMIT;
            ap_log_rerror(APLOG_MARK, APLOG_ERR, r,
                          "Content-Length header (%s) exceeds configured "
                          "max_body limit (%" APR_UINT64_T_FMT ")",
                          cl_header, req->read_limit);
            return;
        }
    }

    if (req->parser == NULL) {
        const char *ct_header = apache_header_in(env, "Content-Type");

        if (ct_header != NULL) {
            apreq_parser_function_t pf = apreq_parser(ct_header);

            if (pf != NULL) {
                req->parser = apreq_parser_make(req->pool,
                                                ba,
                                                ct_header,
                                                pf,
                                                req->brigade_limit,
                                                req->temp_dir,
                                                req->hook_queue,
                                                NULL);
            }
            else {
                req->body_status = APREQ_ERROR_NOPARSER;
                return;
            }
        }
        else {
            req->body_status = APREQ_ERROR_NOHEADER;
            return;
        }
    }
    else {
        if (req->parser->brigade_limit > req->brigade_limit)
            req->parser->brigade_limit = req->brigade_limit;
        if (req->temp_dir != NULL)
            req->parser->temp_dir = req->temp_dir;
        if (req->hook_queue != NULL)
            apreq_parser_add_hook(req->parser, req->hook_queue);
    }

    req->hook_queue = NULL;
    req->in         = apr_brigade_create(req->pool, ba);
    req->body_status = APR_INCOMPLETE;

}

static apr_status_t apache_read(apreq_handle_t *env,
                                apr_off_t bytes)
{
    struct apache_handle *req = (struct apache_handle *)env;
    request_rec *r = req->r;
    apr_bucket *e;
    long got = 0;
    char buf[HUGE_STRING_LEN];
    char tc[] = "[apreq] apache_read";
    int want = sizeof buf;

    if (req->body_status == APR_EINIT)
        init_body(env);

    if (req->body_status != APR_INCOMPLETE)
        return req->body_status;

    /* XXX want a loop here, instead of reducing bytes */

    if (bytes > HUGE_STRING_LEN)
        want = HUGE_STRING_LEN;
    else
        want = bytes;


    ap_hard_timeout(tc, r);
    got = ap_get_client_block(r, buf, want);
    ap_kill_timeout(r);

    if (got > 0) {
        e = apr_bucket_transient_create(buf, got, req->bucket_alloc);
        req->bytes_read += got;
    }
    else
        e = apr_bucket_eos_create(req->bucket_alloc);

    APR_BRIGADE_INSERT_TAIL(req->in, e);


    if (req->bytes_read <= req->read_limit) {
        req->body_status = apreq_parser_run(req->parser, req->body, req->in);
        apr_brigade_cleanup(req->in);
    }
    else {
        req->body_status = APREQ_ERROR_OVERLIMIT;
        ap_log_rerror(APLOG_MARK, APLOG_ERR, req->r,
                      "Bytes read (%" APR_UINT64_T_FMT
                      ") exceeds configured limit (%" APR_UINT64_T_FMT ")",
                      req->bytes_read, req->read_limit);
    }

    return req->body_status;
}






static apr_status_t apache_jar(apreq_handle_t *env, const apr_table_t **t)
{
    struct apache_handle *req = (struct apache_handle *)env;
    request_rec *r = req->r;

    if (req->jar_status == APR_EINIT) {
        const char *cookies = ap_table_get(r->headers_in, "Cookie");
        if (cookies != NULL) {
            req->jar = apr_table_make(req->pool, APREQ_DEFAULT_NELTS);
            req->jar_status =
                apreq_parse_cookie_header(req->pool, req->jar, cookies);
        }
        else
            req->jar_status = APREQ_ERROR_NODATA;
    }

    *t = req->jar;
    return req->jar_status;
}

static apr_status_t apache_args(apreq_handle_t *env, const apr_table_t **t)
{
    struct apache_handle *req = (struct apache_handle *)env;
    request_rec *r = req->r;

    if (req->args_status == APR_EINIT) {
        if (r->args != NULL) {
            req->args = apr_table_make(req->pool, APREQ_DEFAULT_NELTS);
            req->args_status =
                apreq_parse_query_string(req->pool, req->args, r->args);
        }
        else
            req->args_status = APREQ_ERROR_NODATA;
    }

    *t = req->args;
    return req->args_status;
}




static apreq_cookie_t *apache_jar_get(apreq_handle_t *env, const char *name)
{
    struct apache_handle *req = (struct apache_handle *)env;
    const apr_table_t *t;
    const char *val;

    if (req->jar_status == APR_EINIT)
        apache_jar(env, &t);
    else
        t = req->jar;

    if (t == NULL)
        return NULL;

    val = apr_table_get(t, name);
    if (val == NULL)
        return NULL;

    return apreq_value_to_cookie(val);
}

static apreq_param_t *apache_args_get(apreq_handle_t *env, const char *name)
{
    struct apache_handle *req = (struct apache_handle *)env;
    const apr_table_t *t;
    const char *val;

    if (req->args_status == APR_EINIT)
        apache_args(env, &t);
    else
        t = req->args;

    if (t == NULL)
        return NULL;

    val = apr_table_get(t, name);
    if (val == NULL)
        return NULL;

    return apreq_value_to_param(val);
}


static apr_status_t apache_body(apreq_handle_t *env, const apr_table_t **t)
{
    struct apache_handle *req = (struct apache_handle *)env;

    switch (req->body_status) {

    case APR_EINIT:
        init_body(env);
        if (req->body_status != APR_INCOMPLETE)
            break;

    case APR_INCOMPLETE:
        while (apache_read(env, APREQ_DEFAULT_READ_BLOCK_SIZE) == APR_INCOMPLETE)
            ;   /*loop*/
    }

    *t = req->body;
    return req->body_status;
}

static apreq_param_t *apache_body_get(apreq_handle_t *env, const char *name)
{
    struct apache_handle *req = (struct apache_handle *)env;
    const char *val;

    switch (req->body_status) {

    case APR_EINIT:

        init_body(env);
        if (req->body_status != APR_INCOMPLETE)
            return NULL;
        apache_read(env, APREQ_DEFAULT_READ_BLOCK_SIZE);

    case APR_INCOMPLETE:

        val = apr_table_get(req->body, name);
        if (val != NULL)
            return apreq_value_to_param(val);

        do {
            /* riff on Duff's device */
            apache_read(env, APREQ_DEFAULT_READ_BLOCK_SIZE);

    default:

            val = apr_table_get(req->body, name);
            if (val != NULL)
                return apreq_value_to_param(val);

        } while (req->body_status == APR_INCOMPLETE);

    }

    return NULL;
}

static
apr_status_t apache_parser_get(apreq_handle_t *env,
                                  const apreq_parser_t **parser)
{
    struct apache_handle *req = (struct apache_handle *)env;
    *parser = req->parser;
    return APR_SUCCESS;
}

static
apr_status_t apache_parser_set(apreq_handle_t *env,
                                apreq_parser_t *parser)
{
    struct apache_handle *req = (struct apache_handle *)env;

    if (req->parser == NULL) {

        if (req->hook_queue != NULL) {
            apr_status_t s = apreq_parser_add_hook(parser, req->hook_queue);
            if (s != APR_SUCCESS)
                return s;
        }
        if (req->temp_dir != NULL) {
            req->temp_dir = req->temp_dir;
        }
        if (req->brigade_limit < req->brigade_limit) {
            req->brigade_limit = req->brigade_limit;
        }

        req->hook_queue = NULL;
        req->parser = parser;
        return APR_SUCCESS;
    }
    else
        return APREQ_ERROR_MISMATCH;
}

static
apr_status_t apache_hook_add(apreq_handle_t *env,
                              apreq_hook_t *hook)
{
    struct apache_handle *req = (struct apache_handle *)env;

    if (req->parser != NULL) {
        return apreq_parser_add_hook(req->parser, hook);
    }
    else if (req->hook_queue != NULL) {
        apreq_hook_t *h = req->hook_queue;
        while (h->next != NULL)
            h = h->next;
        h->next = hook;
    }
    else {
        req->hook_queue = hook;
    }
    return APR_SUCCESS;

}

static
apr_status_t apache_brigade_limit_set(apreq_handle_t *env,
                                       apr_size_t bytes)
{
    struct apache_handle *req = (struct apache_handle *)env;
    apr_size_t *limit = (req->parser == NULL)
                      ? &req->brigade_limit
                      : &req->parser->brigade_limit;

    if (*limit > bytes) {
        *limit = bytes;
        return APR_SUCCESS;
    }

    return APREQ_ERROR_MISMATCH;
}

static
apr_status_t apache_brigade_limit_get(apreq_handle_t *env,
                                       apr_size_t *bytes)
{
    struct apache_handle *req = (struct apache_handle *)env;
    *bytes = (req->parser == NULL)
           ?  req->brigade_limit
           :  req->parser->brigade_limit;

    return APR_SUCCESS;
}

static
apr_status_t apache_read_limit_set(apreq_handle_t *env,
                                    apr_uint64_t bytes)
{
    struct apache_handle *req = (struct apache_handle *)env;
    if (req->read_limit > bytes && req->bytes_read < bytes) {
        req->read_limit = bytes;
        return APR_SUCCESS;
    }

    return APREQ_ERROR_MISMATCH;
}

static
apr_status_t apache_read_limit_get(apreq_handle_t *env,
                                    apr_uint64_t *bytes)
{
    struct apache_handle *req = (struct apache_handle *)env;
    *bytes = req->read_limit;
    return APR_SUCCESS;
}

static
apr_status_t apache_temp_dir_set(apreq_handle_t *env,
                                  const char *path)
{
    struct apache_handle *req = (struct apache_handle *)env;

    const char **curpath = (req->parser == NULL)
                         ? &req->temp_dir
                         : &req->parser->temp_dir;

    if (*curpath == NULL) {
        *curpath = apr_pstrdup(req->pool, path);
        return APR_SUCCESS;
    }

    return APREQ_ERROR_NOTEMPTY;
}

static
apr_status_t apache_temp_dir_get(apreq_handle_t *env,
                                  const char **path)
{
    struct apache_handle *req = (struct apache_handle *)env;

    *path = req->parser ? req->parser->temp_dir : req->temp_dir;
    return APR_SUCCESS;
}

static APREQ_MODULE(apache, APREQ_APACHE_MMN);

static void apreq_cleanup(void *data)
{
    struct apache_handle *req = data;
    apr_pool_destroy(req->pool);
}


APREQ_DECLARE(apreq_handle_t *) apreq_handle_apache(request_rec *r)
{
    struct apache_handle *req =
        ap_get_module_config(r->request_config, &apreq_module);
    struct dir_config *d =
        ap_get_module_config(r->per_dir_config, &apreq_module);

    if (req != NULL)
        return &req->handle;

    apr_pool_create(&req->pool, NULL);
    req = apr_pcalloc(req->pool, sizeof *req);
    req->bucket_alloc = apr_bucket_alloc_create(req->pool);

    req->handle.module = &apache_module;
    req->r = r;

    req->args_status = req->jar_status = req->body_status = APR_EINIT;
    ap_register_cleanup(r->pool, req, apreq_cleanup, apreq_cleanup);

    if (d == NULL) {
        req->read_limit    = (apr_uint64_t)-1;
        req->brigade_limit = APREQ_DEFAULT_BRIGADE_LIMIT;
    }
    else {
        req->temp_dir      = d->temp_dir;
        req->read_limit    = d->read_limit;
        req->brigade_limit = d->brigade_limit;
    }

    ap_set_module_config(r->request_config, &apreq_module, req);

    return &req->handle;
}
