/* ====================================================================
 *    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 <stdlib.h>

#include <apr_pools.h>
#include <apr_poll.h>
#include <apr_version.h>
#include <apr_portable.h>
#include <apr_strings.h>

#include "serf.h"
#include "serf_bucket_util.h"

#include "serf_private.h"

static apr_status_t clean_resp(void *data)
{
    serf_request_t *request = data;
    apr_pool_t *respool = request->respool;

    /* Note that this function must handle rehooking several
       variables to a different request!

       See serf_connection__request_requeue() */

    request->respool = NULL;

    /* The request's RESPOOL is being cleared.  */
    if (respool && request->writing >= SERF_WRITING_STARTED
                && request->writing < SERF_WRITING_FINISHED) {

        /* HOUSTON, WE HAVE A PROBLEM.

           We created buckets inside the pool that is now cleaned and
           stored them in an aggregate that still lives on.

           This happens when the application decides that it doesn't
           need the connection any more and clears the pool of the
           connection, of which the request pool is a subpool.

           Let's ask the connection to take care of things */
        serf__connection_pre_cleanup(request->conn);
    }

#ifdef SERF_DEBUG_BUCKET_USE
    if (respool && request->allocator) {
        serf_debug__closed_conn(request->allocator);
    }
#endif

    /* If the response has allocated some buckets, then destroy them (since
       the bucket may hold resources other than memory in RESPOOL). Also
       make sure to set their fields to NULL so connection closure does
       not attempt to free them again.  */
    if (request->resp_bkt) {
        serf_bucket_destroy(request->resp_bkt);
        request->resp_bkt = NULL;
    }
    if (request->req_bkt) {
        if (request->writing == SERF_WRITING_NONE)
            serf_bucket_destroy(request->req_bkt);
        request->req_bkt = NULL;
    }

#ifdef SERF_DEBUG_BUCKET_USE
    if (respool && request->allocator) {
        serf_debug__bucket_alloc_check(request->allocator);
    }
#endif

    request->allocator = NULL;

    return APR_SUCCESS;
}

void serf__link_requests(serf_request_t **list, serf_request_t **tail,
                         serf_request_t *request)
{
    if (*list == NULL) {
        *list = request;
        *tail = request;
    }
    else {
        (*tail)->next = request;
        *tail = request;
    }
}

apr_status_t serf__destroy_request(serf_request_t *request)
{
    serf_connection_t *conn = request->conn;

    if (request->depends_first && request->depends_on) {
        apr_uint64_t total = 0;
        serf_request_t *r, **pr;
        apr_uint64_t rqd = request->dep_priority;

        /* Calculate total priority of descendants */
        for (r = request->depends_first; r; r = r->depends_next) {
            total += r->dep_priority;
        }

        /* Apply now, as if they depend on the parent */
        for (r = request->depends_first; r; r = r->depends_next) {
            if (total) {

                r->dep_priority = (apr_uint16_t)(rqd * r->dep_priority
                                                 / total);
            }
            else
                r->dep_priority = 0;

            r->depends_on = request->depends_on;
        }

        /* Remove us from parent */
        pr = &request->depends_on->depends_first;
        while (*pr) {
            if (*pr == request) {
                *pr = request->depends_next;
                break;
            }

            pr = &(*pr)->depends_next;
        }

        /* And append all our descendants */
        *pr = request->depends_first;

        request->depends_on = NULL;
        request->depends_first = NULL;
        request->depends_next = NULL;
    }
    else if (request->depends_first) {
        /* Dependencies will lose their parent */
        serf_request_t *r, *next;

        for (r = request->depends_first; r; r = next) {
            next = r->next;

            r->depends_on = NULL;
            r->depends_next = NULL;
        }
        request->depends_first = NULL;
    }
    else if (request->depends_on) {
        serf_request_t **pr;

        /* Remove us from parent */
        pr = &request->depends_on->depends_first;
        while (*pr) {
            if (*pr == request) {
                *pr = request->depends_next;
                break;
            }

            pr = &(*pr)->depends_next;
        }
        request->depends_on = NULL;
    }

    if (request->writing >= SERF_WRITING_STARTED
        && request->writing < SERF_WRITING_FINISHED) {

        /* Schedule for destroy when it is safe again.

           Destroying now will destroy memory of buckets that we
           may still need.
        */
        serf__link_requests(&conn->done_reqs, &conn->done_reqs_tail,
                            request);
    }
    else {

        if (request->respool) {
          apr_pool_t *pool = request->respool;

          apr_pool_cleanup_run(pool, request, clean_resp);
          apr_pool_destroy(pool);
        }

        serf_bucket_mem_free(conn->allocator, request);
    }

    return APR_SUCCESS;
}

apr_status_t serf__cancel_request(serf_request_t *request,
                                  serf_request_t **list,
                                  int notify_request)
{
    /* If we haven't run setup, then we won't have a handler to call. */
    if (request->handler && notify_request) {
        /* We actually don't care what the handler returns.
         * We have bigger matters at hand.
         */
        (void)request->handler(request, NULL, request->handler_baton,
                               request->respool);

        request->handler = NULL;
    }

    if (request->conn && request->conn->perform_cancel_request) {
        request->conn->perform_cancel_request(request,
                                              SERF_ERROR_HTTP2_CANCEL);
    }

    if (*list == request) {
        *list = request->next;
    }
    else {
        serf_request_t *scan = *list;

        while (scan->next && scan->next != request)
            scan = scan->next;

        if (scan->next) {
            scan->next = scan->next->next;
        }
    }

    return serf__destroy_request(request);
}

/* Calculate the length of a linked list of requests. */
unsigned int serf__req_list_length(serf_request_t *req)
{
    unsigned int length = 0;

    while (req) {
        length++;
        req = req->next;
    }

    return length;
}

apr_status_t serf__setup_request(serf_request_t *request)
{
    serf_connection_t *conn = request->conn;
    apr_status_t status;

    /* Now that we are about to serve the request, allocate a pool. */
    apr_pool_create(&request->respool, conn->pool);
    request->allocator = serf_bucket_allocator_create(request->respool,
                                                      NULL, NULL);
    apr_pool_cleanup_register(request->respool, request,
                              clean_resp, apr_pool_cleanup_null);

    /* Fill in the rest of the values for the request. */
    status = request->setup(request, request->setup_baton,
                            &request->req_bkt,
                            &request->acceptor,
                            &request->acceptor_baton,
                            &request->handler,
                            &request->handler_baton,
                            request->respool);
    return status;
}

/* A response message was received from the server, so call
   the handler as specified on the original request. */
apr_status_t serf__handle_response(serf_request_t *request,
                                   apr_pool_t *pool)
{
    bool consumed_response = false;

    /* Only enable the new authentication framework if the program has
     * registered an authentication credential callback.
     *
     * This permits older Serf apps to still handle authentication
     * themselves by not registering credential callbacks.
     */
    if (!request->auth_done && request->conn->ctx->cred_cb) {
        apr_status_t status;

        if (!SERF_BUCKET_IS_RESPONSE(request->resp_bkt)) {
            request->auth_done = true;
            status = APR_SUCCESS;
        }
        else
            status = serf__handle_auth_response(&consumed_response,
                                                request,
                                                request->resp_bkt,
                                                pool);

        if (SERF_BUCKET_READ_ERROR(status)) {

            /* There was an error while checking the authentication headers of
               the response. We need to inform the application - which
               hasn't seen this response yet - of the error.

               These are the possible causes of the error:

               1. A communication error while reading the response status line,
                  headers or while discarding the response body: pass the
                  response unchanged to the application, it will see the same
                  error as serf did.

               2. A 401/407 response status for a supported authn scheme that
                  resulted in authn failure:
                  Pass the response as received to the application, the response
                  body can contain an error description. Terminate the response
                  body with the AUTHN error instead of APR_EOF.

               3. A 401/407 response status for a supported authn scheme that
                  resulted in an unknown error returned by the application in
                  the credentials callback (Basic/Digest):
                  Handle the same as 2.

               4. A 2xx response status for a supported authn scheme that
                  resulted in authn failure:
                  Pass the response headers to the application. The response
                  body is untrusted, so we should drop it and return the AUTHN
                  error instead of APR_EOF.

                  serf__handle_auth_response will already discard the response
                  body, so we can handle this case the same as 2.

               In summary, all these cases can be handled in the same way: call
               the application's response handler with the response bucket, but
               make sure that the application sees error code STATUS instead of
               APR_EOF after reading the response body.
            */

            serf__bucket_response_set_error_on_eof(request->resp_bkt, status);

            /* Ignore the application's status code here, use the error status
               from serf__handle_auth_response. */
            (void)(*request->handler)(request,
                                      request->resp_bkt,
                                      request->handler_baton,
                                      pool);
        }

        if (status)
            return status;
    }

    if (!consumed_response) {
        return (*request->handler)(request,
                                   request->resp_bkt,
                                   request->handler_baton,
                                   pool);
    }

    return APR_SUCCESS;
}

apr_status_t
serf__provide_credentials(serf_context_t *ctx,
                          char **username,
                          char **password,
                          serf_request_t *request,
                          int code, const char *authn_type,
                          const char *realm,
                          apr_pool_t *pool)
{
    serf_connection_t *conn = request->conn;
    serf_request_t *authn_req = request;
    apr_status_t status;

    if (request->ssltunnel == 1 &&
        conn->state == SERF_CONN_SETUP_SSLTUNNEL) {
        /* This is a CONNECT request to set up an SSL tunnel over a proxy.
           This request is created by serf, so if the proxy requires
           authentication, we can't ask the application for credentials with
           this request.

           Solution: setup the first request created by the application on
           this connection, and use that request and its handler_baton to
           call back to the application. */

        /* request->next will be NULL if this was the last request written */
        authn_req = request->next;
        if (!authn_req)
            authn_req = conn->unwritten_reqs;

        /* assert: app_request != NULL */
        if (!authn_req)
            return APR_EGENERAL;

        if (!authn_req->req_bkt) {
            status = serf__setup_request(authn_req);
            /* If we can't setup a request, don't bother setting up the
               ssl tunnel. */
            if (status)
                return status;
        }
    }

    /* Ask the application. */
    status = (*ctx->cred_cb)(username, password,
                             authn_req, authn_req->handler_baton,
                             code, authn_type, realm, pool);
    if (status)
        return status;

    return APR_SUCCESS;
}

static serf_request_t *
create_request(serf_connection_t *conn,
               serf_request_setup_t setup,
               void *setup_baton,
               bool priority,
               bool ssltunnel)
{
    serf_request_t *request;

    request = serf_bucket_mem_alloc(conn->allocator, sizeof(*request));
    request->conn = conn;
    request->setup = setup;
    request->setup_baton = setup_baton;
    request->handler = NULL;
    request->acceptor = NULL;
    request->respool = NULL;
    request->req_bkt = NULL;
    request->resp_bkt = NULL;
    request->priority = priority;
    request->writing = SERF_WRITING_NONE;
    request->ssltunnel = ssltunnel;
    request->auth_done = FALSE;
    request->next = NULL;
    request->auth_baton = NULL;
    request->protocol_baton = NULL;
    request->depends_on = NULL;
    request->depends_next = NULL;
    request->depends_first = NULL;
    request->dep_priority = SERF_REQUEST_PRIORITY_DEFAULT;

    return request;
}

serf_request_t *serf_connection_request_create(
    serf_connection_t *conn,
    serf_request_setup_t setup,
    void *setup_baton)
{
    serf_request_t *request;

    request = create_request(conn, setup, setup_baton,
                             false /* priority */,
                             false /* ssl tunnel */);

    /* Link the request to the end of the request chain. */
    serf__link_requests(&conn->unwritten_reqs, &conn->unwritten_reqs_tail, request);
    conn->nr_of_unwritten_reqs++;

    /* Ensure our pollset becomes writable in context run */
    serf_io__set_pollset_dirty(&conn->io);

    return request;
}

static void
insert_priority_request(serf_request_t *request)
{
    serf_request_t *iter, *prev;
    serf_connection_t *conn = request->conn;

    /* Link the new request after the last written request. */
    iter = conn->unwritten_reqs;
    prev = NULL;

    /* TODO: what if a request is partially written? */
    /* Find a request that has data which needs to be delivered. */
    while (iter != NULL && iter->req_bkt == NULL
           && (iter->writing >= SERF_WRITING_STARTED)) {
        prev = iter;
        iter = iter->next;
    }

    /* A CONNECT request to setup an ssltunnel has absolute priority over all
       other requests on the connection, so:
       a. add it first to the queue
       b. ensure that other priority requests are added after the CONNECT
          request */
    if (!request->ssltunnel) {
        /* Advance to next non priority request */
        while (iter != NULL && iter->priority) {
            prev = iter;
            iter = iter->next;
        }
    }

    if (prev) {
        request->next = iter;
        prev->next = request;
    } else {
        request->next = iter;
        conn->unwritten_reqs = request;
    }
    conn->nr_of_unwritten_reqs++;

    /* Ensure our pollset becomes writable in context run */
    serf_io__set_pollset_dirty(&conn->io);
}

static serf_request_t *
priority_request_create(serf_connection_t *conn,
                        int ssltunnelreq,
                        serf_request_setup_t setup,
                        void *setup_baton)
{
    serf_request_t *request;

    request = create_request(conn, setup, setup_baton,
                             true /* priority */,
                             ssltunnelreq);

    insert_priority_request(request);

    return request;
}

serf_request_t *serf_connection_priority_request_create(
    serf_connection_t *conn,
    serf_request_setup_t setup,
    void *setup_baton)
{
    return priority_request_create(conn,
                                   0, /* not a ssltunnel CONNECT request */
                                   setup, setup_baton);
}

serf_request_t *serf__ssltunnel_request_create(serf_connection_t *conn,
                                               serf_request_setup_t setup,
                                               void *setup_baton)
{
    return priority_request_create(conn,
                                   1, /* This is a ssltunnel CONNECT request */
                                   setup, setup_baton);
}

/* Serf request_handler_t to discard a request */
static apr_status_t discard_response_handler(serf_request_t *request,
                                             serf_bucket_t *response,
                                             void *handler_baton,
                                             apr_pool_t *pool)
{
    apr_status_t status;
    apr_size_t len;

    do
    {
        const char *data;

        status = serf_bucket_read(response, SERF_READ_ALL_AVAIL, &data, &len);
    } while (!status && len);

    return status;
}

apr_status_t serf_connection__request_requeue(serf_request_t *request)
{
    serf_request_t *discard;
    serf_request_t **pr;

    /* For some reason, somebody wants to restart this request, that was
       at least partially written and partially read. Most likely to
       slightly change it. (E.g. to add auth headers).

       To handle this we have to do two things: we have to handle remaining
       incoming data on the existing request. And we have to make the request
       ready to write it again.. preferably as the first new request.
     */

    /* Setup a new request to handle discarding the remaining body */
    discard = create_request(request->conn,
                             NULL, NULL,
                             request->priority,
                             request->ssltunnel);

    discard->respool = request->respool;
    discard->allocator = request->allocator;
    discard->req_bkt = request->req_bkt;
    discard->resp_bkt = request->resp_bkt;

    discard->setup = NULL;  /* Already setup */
    discard->setup_baton = NULL;

    discard->acceptor = NULL;
    discard->acceptor_baton = NULL; /* Already accepted */

    discard->handler = discard_response_handler;
    discard->handler_baton = NULL;

    discard->protocol_baton = request->protocol_baton;
    discard->auth_done = request->auth_done;
    discard->auth_baton = request->auth_baton;

    discard->writing = request->writing;

    if (discard->respool) {
        apr_pool_cleanup_kill(discard->respool, request, clean_resp);
        apr_pool_cleanup_register(discard->respool, discard,
                                  clean_resp, apr_pool_cleanup_null);
    }

    if (request->depends_on) {
        /* Update request dependencies */
        discard->depends_on = request->depends_on;

        for (pr = &discard->depends_on->depends_first;
             *pr;
             pr = &(*pr)->depends_next)
        {
            if (*pr == request) {
                *pr = discard;
                discard->depends_next = request->depends_next;
                request->depends_next = NULL;
                request->depends_on = NULL;
                break;
            }
        }
    }
    if (request->depends_first) {
        serf_request_t *r;

        for (r = request->depends_first; r; r = r->depends_next) {
            r->depends_on = discard;
        }
        discard->depends_first = request->depends_first;
        request->depends_first = NULL;
    }

    /* And now make the discard request take the place of the old request */
    for (pr = &request->conn->written_reqs; *pr; pr = &(*pr)->next) {
        if (*pr == request) {
            *pr = discard;
            discard->next = request->next;
            request->next = NULL;

            if (!discard->next)
                request->conn->written_reqs_tail = discard;
            break;
        }
    }

    /* Tell the protocol handler (if any) that we are no longer looking at the
       request */

    if (request->conn->perform_cancel_request)
        request->conn->perform_cancel_request(discard, SERF_ERROR_HTTP2_CANCEL);

    /* And now unhook the existing request */
    request->allocator = NULL;
    request->respool = NULL;
    request->req_bkt = NULL;
    request->resp_bkt = NULL;
    request->writing = SERF_WRITING_NONE;
    request->auth_baton = NULL;
    request->protocol_baton = NULL;

    request->acceptor = NULL;
    request->acceptor_baton = NULL;
    request->handler = NULL;
    request->handler_baton = NULL;
    request->auth_done = false;

    if (discard->depends_first || discard->depends_on) {
        serf_connection_request_prioritize(request, discard, 0xFFFF,
                                           TRUE);
    }

    insert_priority_request(request);

    return APR_SUCCESS;
}

apr_status_t serf_request_cancel(serf_request_t *request)
{
    serf_connection_t *conn = request->conn;
    serf_request_t *tmp = conn->unwritten_reqs;

    /* Find out which queue holds the request */
    while (tmp != NULL && tmp != request)
        tmp = tmp->next;

    if (tmp)
        return serf__cancel_request(request, &conn->unwritten_reqs, 0);
    else
        return serf__cancel_request(request, &conn->written_reqs, 0);

}

void serf_connection_request_prioritize(serf_request_t *request,
                                        serf_request_t *depends_on,
                                        apr_uint16_t priority,
                                        int exclusive)
{
    if (request->depends_on != depends_on) {
        serf_request_t *r;

        if (depends_on->conn != request->conn || depends_on == request)
            abort();

        /* If we are indirectly made dependent on ourself, we first
           reprioritize the descendant on our current parent. See
           https://tools.ietf.org/html/rfc7540#section-5.3.3

           If a stream is made dependent on one of its own dependencies, the
           formerly dependent stream is first moved to be dependent on the
           reprioritized stream's previous parent.  The moved dependency
           retains its weight. */

        r = depends_on;

        while (r && r != request && r->depends_on)
            r = r->depends_on;

        if (r == request)
        {
            serf_connection_request_prioritize(depends_on,
                                               request->depends_on,
                                               depends_on->dep_priority,
                                               false /* exclusive */);
        }

        if (request->depends_on) {
        /* Ok, we can now update our dependency */

            serf_request_t **pr = &request->depends_on->depends_first;

            while (*pr) {
                if (*pr == request) {
                    *pr = request->depends_next;
                    break;
                }
                pr = &(*pr)->depends_next;
            }
        }

        request->depends_on = depends_on;

        if (depends_on) {
            if (exclusive) {
                r = depends_on->depends_first;

                while (r) {
                    r->depends_on = request;

                    if (r->depends_next)
                        r = r->depends_next;
                    else
                        break;
                }

                if (r) {
                    r->depends_next = request->depends_first;
                    r->depends_first = depends_on->depends_first;
                }
                request->depends_next = NULL;
            }
            else {
                request->depends_next = depends_on->depends_first;
            }
            depends_on->depends_first = request;
        }
        else
            request->depends_next = NULL;
    }

    if (priority)
        request->dep_priority = priority;

    /* And now tell the protocol about this */
    if (request->conn->perform_prioritize_request)
        request->conn->perform_prioritize_request(request, exclusive != 0);
}


apr_status_t serf_request_is_written(serf_request_t *request)
{
    if (request->writing >= SERF_WRITING_FINISHED)
        return APR_SUCCESS;

    return APR_EBUSY;
}

apr_pool_t *serf_request_get_pool(const serf_request_t *request)
{
    return request->respool;
}


serf_bucket_alloc_t *serf_request_get_alloc(
    const serf_request_t *request)
{
    return request->allocator;
}


serf_connection_t *serf_request_get_conn(
    const serf_request_t *request)
{
    return request->conn;
}


void serf_request_set_handler(
    serf_request_t *request,
    const serf_response_handler_t handler,
    const void **handler_baton)
{
    request->handler = handler;
    request->handler_baton = handler_baton;
}


serf_bucket_t *serf_request_bucket_request_create(
    serf_request_t *request,
    const char *method,
    const char *uri,
    serf_bucket_t *body,
    serf_bucket_alloc_t *allocator)
{
    serf_bucket_t *req_bkt;
    serf_bucket_t *hdrs_bkt;
    serf_connection_t *conn = request->conn;
    serf_context_t *ctx = conn->ctx;
    int tunneled;

    tunneled = ctx->proxy_address
               && (strcmp(conn->host_info.scheme, "https") == 0);

    req_bkt = serf_bucket_request_create(method, uri, body, allocator);
    hdrs_bkt = serf_bucket_request_get_headers(req_bkt);

    /* Use absolute uri's in requests to a proxy. USe relative uri's in
       requests directly to a server or sent through an SSL tunnel. */
    if (ctx->proxy_address && conn->host_url && !tunneled)
    {
        serf_bucket_request_set_root(req_bkt, conn->host_url);
    }

    if (conn->host_info.hostinfo)
    {
        serf_bucket_headers_setn(hdrs_bkt, "Host",  conn->host_info.hostinfo);
    }

    /* Setup server authentication headers.  */
    serf__auth_setup_request(HOST, request, method, uri, hdrs_bkt);

    /* Setup proxy authentication headers, unless we're tunneling.  */
    if (!tunneled)
        serf__auth_setup_request(PROXY, request, method, uri, hdrs_bkt);

    return req_bkt;
}
