/* ====================================================================
 *    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 "serf.h"
#include "serf_bucket_util.h"
#include "serf_private.h"

#include "protocols/fcgi_buckets.h"

#define FCGI_RECORD_SIZE 8

typedef struct fcgi_unframe_ctx_t
{
    serf_bucket_t *stream;

    serf_bucket_end_of_frame_t end_of_frame;
    void *end_of_frame_baton;

    apr_size_t record_remaining;
    apr_size_t payload_remaining;
    apr_size_t pad_remaining;

    apr_uint16_t frame_type;
    apr_uint16_t streamid;

    unsigned char buffer[FCGI_RECORD_SIZE];
} fcgi_unframe_ctx_t;

serf_bucket_t * serf__bucket_fcgi_unframe_create(serf_bucket_t *stream,
                                                 serf_bucket_alloc_t *allocator)
{
    fcgi_unframe_ctx_t *ctx;

    ctx = serf_bucket_mem_calloc(allocator, sizeof(*ctx));
    ctx->stream = stream;
    ctx->record_remaining = FCGI_RECORD_SIZE;

    return serf_bucket_create(&serf_bucket_type__fcgi_unframe, allocator, ctx);
}

void serf__bucket_fcgi_unframe_set_eof(serf_bucket_t *bucket,
                                       serf_bucket_end_of_frame_t end_of_frame,
                                       void *end_of_frame_baton)
{
    fcgi_unframe_ctx_t *ctx = bucket->data;

    ctx->end_of_frame = end_of_frame;
    ctx->end_of_frame_baton = end_of_frame_baton;
}

apr_status_t serf__bucket_fcgi_unframe_read_info(serf_bucket_t *bucket,
                                                 apr_uint16_t *stream_id,
                                                 apr_uint16_t *frame_type)
{
    fcgi_unframe_ctx_t *ctx = bucket->data;
    const unsigned char *header;
    apr_status_t status;

    if (ctx->record_remaining == 0)
    {
        if (stream_id)
            *stream_id = ctx->streamid;
        if (frame_type)
            *frame_type = ctx->frame_type;

        return APR_SUCCESS;
    }

    do
    {
        const char *data;
        apr_size_t len;

        status = serf_bucket_read(ctx->stream, ctx->record_remaining, &data, &len);

        if (SERF_BUCKET_READ_ERROR(status))
            return status;
        else if (!len && !status)
            return SERF_ERROR_EMPTY_READ;

        if (len < FCGI_RECORD_SIZE) {
            memcpy(ctx->buffer + FCGI_RECORD_SIZE - ctx->record_remaining,
                   data, len);

            ctx->record_remaining -= len;
            header = ctx->buffer;
        }
        else {
            header = (const void *)data;
            ctx->record_remaining = 0;
        }
    } while (!status && ctx->record_remaining > 0);

    if (ctx->record_remaining == 0)
    {
        /* We combine version and frametype in a single value */
        ctx->frame_type = (header[0] << 8) | header[1];
        ctx->streamid = (header[2] << 8) | header[3];
        ctx->payload_remaining = (header[4] << 8) | header[5];
        /* header[6] is reserved */
        ctx->pad_remaining = header[7];

        /* Fill output arguments if necessary */
        if (stream_id)
            *stream_id = ctx->streamid;
        if (frame_type)
            *frame_type = ctx->frame_type;

        if (ctx->payload_remaining == 0)
            status = APR_EOF;
        else if (APR_STATUS_IS_EOF(status))
            status = SERF_ERROR_TRUNCATED_STREAM;

        /* If we hava a zero-length frame we have to call the eof callback
           now, as the read operations will just shortcut to APR_EOF */
        if (ctx->payload_remaining == 0 && ctx->end_of_frame)
        {
            apr_status_t cb_status;

            cb_status = (*ctx->end_of_frame)(ctx->end_of_frame_baton,
                                                bucket);

            ctx->end_of_frame = NULL;

            if (SERF_BUCKET_READ_ERROR(cb_status))
                status = cb_status;
        }
    }
    else if (APR_STATUS_IS_EOF(status))
    {
        /* Reading frame failed because we couldn't read the header. Report
            a read failure instead of semi-success */
        if (ctx->record_remaining == FCGI_RECORD_SIZE)
            status = SERF_ERROR_EMPTY_STREAM;
        else
            status = SERF_ERROR_TRUNCATED_STREAM;
    }

    return status;
}

static apr_status_t serf_fcgi_unframe_read(serf_bucket_t *bucket,
                                           apr_size_t requested,
                                           const char **data,
                                           apr_size_t *len)
{
    fcgi_unframe_ctx_t *ctx = bucket->data;
    apr_status_t status;

    status = serf__bucket_fcgi_unframe_read_info(bucket, NULL, NULL);
    if (status)
    {
        *len = 0;
        return (status == SERF_ERROR_EMPTY_READ) ? APR_SUCCESS : status;
    }

    if (requested > ctx->payload_remaining)
        requested = ctx->payload_remaining;

    if (requested == ctx->payload_remaining && ctx->pad_remaining)
        requested += ctx->pad_remaining;

    status = serf_bucket_read(ctx->stream, requested, data, len);
    if (!SERF_BUCKET_READ_ERROR(status)) {
        if (*len >= ctx->payload_remaining) {
            ctx->pad_remaining -= (*len - ctx->payload_remaining);
            *len = ctx->payload_remaining;
            ctx->payload_remaining = 0;
        }
        else {
            ctx->payload_remaining -= *len;
        }

        if (!ctx->payload_remaining && !ctx->pad_remaining) {
            if (ctx->end_of_frame) {
                status = (*ctx->end_of_frame)(ctx->end_of_frame_baton,
                                              bucket);
                ctx->end_of_frame = NULL;
            }

            if (!SERF_BUCKET_READ_ERROR(status))
                status = APR_EOF;
        }
        else if (APR_STATUS_IS_EOF(status))
            status = SERF_ERROR_TRUNCATED_STREAM;
    }

    return status;
}

static apr_status_t serf_fcgi_unframe_peek(serf_bucket_t *bucket,
                                           const char **data,
                                           apr_size_t *len)
{
    fcgi_unframe_ctx_t *ctx = bucket->data;
    apr_status_t status;

    status = serf__bucket_fcgi_unframe_read_info(bucket, NULL, NULL);

    if (status)
    {
        *len = 0;
        return (status == SERF_ERROR_EMPTY_READ) ? APR_SUCCESS : status;
    }

    status = serf_bucket_peek(ctx->stream, data, len);
    if (!SERF_BUCKET_READ_ERROR(status))
    {
        if (*len > ctx->payload_remaining)
            *len = ctx->payload_remaining;
    }

    return status;
}

static apr_uint64_t serf_fcgi_unframe_get_remaining(serf_bucket_t *bucket)
{
    fcgi_unframe_ctx_t *ctx = bucket->data;

    if (ctx->record_remaining)
        return SERF_LENGTH_UNKNOWN;
    else
        return ctx->payload_remaining;
}

static apr_status_t serf_fcgi_unframe_set_config(serf_bucket_t *bucket,
                                                 serf_config_t *config)
{
    fcgi_unframe_ctx_t *ctx = bucket->data;

    return serf_bucket_set_config(ctx->stream, config);
}

const serf_bucket_type_t serf_bucket_type__fcgi_unframe =
{
    "FCGI-UNFRAME",
    serf_fcgi_unframe_read,
    serf_default_readline,
    serf_default_read_iovec,
    serf_default_read_for_sendfile,
    serf_buckets_are_v2,
    serf_fcgi_unframe_peek,
    serf_default_destroy_and_data,
    serf_default_read_bucket,
    serf_fcgi_unframe_get_remaining,
    serf_fcgi_unframe_set_config
};
/* ==================================================================== */

typedef struct fcgi_params_decode_ctx_t
{
    serf_bucket_t *stream;

    const char *last_data;
    apr_size_t last_len;

    apr_size_t key_sz;
    apr_size_t val_sz;

    enum fcgi_param_decode_status_t
    {
        DS_SIZES = 0,
        DS_KEY,
        DS_VALUE,
    } state;

    char size_buffer[8];
    apr_size_t tmp_size;

    char *key;
    char *val;

    const char *method;
    const char *path;

    serf_bucket_t *headers;

} fcgi_params_decode_ctx_t;

serf_bucket_t *
serf__bucket_fcgi_params_decode_create(serf_bucket_t *stream,
                                       serf_bucket_alloc_t *alloc)
{
    fcgi_params_decode_ctx_t *ctx;

    ctx = serf_bucket_mem_calloc(alloc, sizeof(*ctx));
    ctx->stream = stream;

    return serf_bucket_create(&serf_bucket_type__fcgi_params_decode, alloc,
                              ctx);
}

static apr_size_t size_data_requested(fcgi_params_decode_ctx_t *ctx)
{
    apr_size_t requested;

    if (ctx->tmp_size < 1)
        requested = 2;
    else if (ctx->size_buffer[0] & 0x80) {
        requested = 5;

        if (ctx->tmp_size > 4
            && ctx->size_buffer[4] & 0x80) {
            requested = 8;
        }
    }
    else if (ctx->tmp_size >= 2
             && ctx->size_buffer[1] & 0x80) {
        requested = 5;
    }
    else
        requested = 2;

    return requested;
}

static void fcgi_handle_keypair(serf_bucket_t *bucket)
{
    fcgi_params_decode_ctx_t *ctx = bucket->data;
    char *key = ctx->key;
    char *val = ctx->val;

    ctx->key = NULL;
    ctx->val = NULL;

    if (!ctx->headers)
        ctx->headers = serf_bucket_headers_create(bucket->allocator);

    if (strncasecmp(key, "HTTP_", 5) == 0
        && strncasecmp(key + 5, "_FCGI_", 6) != 0)
    {
        apr_size_t i;
        memmove(key, key + 5, ctx->key_sz - 5 + 1);
        for (i = 0; i < ctx->key_sz; i++) {
            if (key[i] == '_')
                key[i] = '-';
        }
        ctx->key_sz -= 5;
    }
    else if (ctx->key_sz == 6 && !strcasecmp(key, "METHOD"))
    {
        ctx->method = val;
        serf_bucket_mem_free(bucket->allocator, key);
        return;
    }
    else if (ctx->key_sz == 11 && !strcasecmp(key, "REQUEST_URI"))
    {
        ctx->path = val;
        serf_bucket_mem_free(bucket->allocator, key);
        return;
    }
    else
    {
        memmove(key + 6, key, ctx->key_sz + 1);
        memcpy(key, "_FCGI_", 6);
        ctx->key_sz += 6;
    }

    serf_bucket_headers_setx(ctx->headers,
                             key, ctx->key_sz, TRUE,
                             val, ctx->val_sz, TRUE);
    serf_bucket_mem_free(bucket->allocator, key);
    serf_bucket_mem_free(bucket->allocator, val);
}

static apr_status_t fcgi_params_decode(serf_bucket_t *bucket)
{
    fcgi_params_decode_ctx_t *ctx = bucket->data;
    apr_status_t status = APR_SUCCESS;

    while (status == APR_SUCCESS) {
        apr_size_t requested;
        const char *data;
        const unsigned char *udata;
        apr_size_t len;

        switch (ctx->state) {
            case DS_SIZES:
                requested = size_data_requested(ctx);
                status = serf_bucket_read(ctx->stream,
                                          requested - ctx->tmp_size,
                                          &data, &len);
                if (SERF_BUCKET_READ_ERROR(status))
                    return status;

                if (len < requested) {
                    memcpy(ctx->size_buffer + ctx->tmp_size, data, len);
                    ctx->tmp_size += len;

                    len = ctx->tmp_size;
                    data = ctx->size_buffer;
                }

                if (size_data_requested(ctx) < len) {
                    /* Read again. More bytes needed for
                       determining lengths */
                    if (data != ctx->size_buffer) {
                        memcpy(ctx->size_buffer, data, len);
                        ctx->tmp_size = len;
                    }
                    break;
                }

                udata = (const unsigned char*)data;

                if (udata[0] & 0x80) {
                    ctx->key_sz = (udata[0] & 0x7F) << 24 | (udata[1] << 16)
                                        | (udata[2] << 8) | (udata[3]);
                    udata += 4;
                }
                else {
                    ctx->key_sz = udata[0] & 0x7F;
                    udata += 1;
                }

                if (udata[0] & 0x80) {
                    ctx->val_sz = (udata[0] & 0x7F) << 24 | (udata[1] << 16)
                                        | (udata[2] << 8) | (udata[3]);
                    udata += 4;
                }
                else {
                    ctx->val_sz = udata[0] & 0x7F;
                    udata += 1;
                }

                ctx->tmp_size = 0;
                ctx->state++;
                break;
            case DS_KEY:
                status = serf_bucket_read(ctx->stream, ctx->key_sz,
                                          &data, &len);
                if (SERF_BUCKET_READ_ERROR(status))
                    break;

                if (!ctx->key) {
                    ctx->key = serf_bucket_mem_alloc(bucket->allocator,
                                                     ctx->key_sz + 1 + 6);
                    ctx->key[ctx->key_sz] = 0;
                }

                memcpy(ctx->key + ctx->tmp_size, data, len);
                ctx->tmp_size += len;

                if (ctx->tmp_size == ctx->key_sz) {
                    ctx->state++;
                    ctx->tmp_size = 0;
                }
                break;
            case DS_VALUE:
                status = serf_bucket_read(ctx->stream, ctx->val_sz,
                                          &data, &len);
                if (SERF_BUCKET_READ_ERROR(status))
                    break;
                if (!ctx->val) {
                    ctx->val = serf_bucket_mem_alloc(bucket->allocator,
                                                     ctx->val_sz + 1);
                    ctx->val[ctx->val_sz] = 0;
                }

                if (len == ctx->val_sz)
                    ctx->state++;

                memcpy(ctx->val + ctx->tmp_size, data, len);
                ctx->tmp_size += len;

                if (ctx->tmp_size == ctx->val_sz) {

                    fcgi_handle_keypair(bucket);
                    ctx->state = DS_SIZES;
                    ctx->tmp_size = 0;
                }
                break;
        }
    }

    if (APR_STATUS_IS_EOF(status)) {
        if ((ctx->state == DS_SIZES && !ctx->tmp_size)
            || (ctx->state == DS_KEY && !ctx->key_sz && !ctx->val_sz))
        {
            return APR_SUCCESS;
        }

        return SERF_ERROR_TRUNCATED_STREAM;
    }

    return status;
}

static void fcgi_serialize(serf_bucket_t *bucket)
{
    fcgi_params_decode_ctx_t *ctx = bucket->data;

    serf_bucket_aggregate_become(bucket);

    if (ctx->method || ctx->path) {
        serf_bucket_t *tmp;
        if (ctx->method) {
            tmp = serf_bucket_simple_own_create(ctx->method, strlen(ctx->method),
                                                bucket->allocator);
        }
        else
            tmp = SERF_BUCKET_SIMPLE_STRING("GET", bucket->allocator);
        serf_bucket_aggregate_append(bucket, tmp);

        tmp = SERF_BUCKET_SIMPLE_STRING(" ", bucket->allocator);
        serf_bucket_aggregate_append(bucket, tmp);

        if (ctx->path) {
            tmp = serf_bucket_simple_own_create(ctx->path, strlen(ctx->path),
                                                bucket->allocator);
        }
        else
            tmp = SERF_BUCKET_SIMPLE_STRING("/", bucket->allocator);
        serf_bucket_aggregate_append(bucket, tmp);

        tmp = SERF_BUCKET_SIMPLE_STRING(" HTTP/2.0\r\n", bucket->allocator);
        serf_bucket_aggregate_append(bucket, tmp);
    }

    if (ctx->headers)
        serf_bucket_aggregate_append(bucket, ctx->headers);

    if (ctx->key)
        serf_bucket_mem_free(bucket->allocator, ctx->key);
    if (ctx->val)
        serf_bucket_mem_free(bucket->allocator, ctx->val);

    serf_bucket_mem_free(bucket->allocator, ctx);
}

static apr_status_t fcgi_params_decode_read(serf_bucket_t *bucket,
                                            apr_size_t requested,
                                            const char **data,
                                            apr_size_t *len)
{
    apr_status_t status;

    status = fcgi_params_decode(bucket);

    if (status) {
        *len = 0;
        return status;
    }

    fcgi_serialize(bucket);
    return bucket->type->read(bucket, requested, data, len);
}

static apr_status_t fcgi_params_decode_peek(serf_bucket_t *bucket,
                                            const char **data,
                                            apr_size_t *len)
{
    apr_status_t status;

    status = fcgi_params_decode(bucket);

    if (status) {
        *len = 0;
        return status;
    }

    fcgi_serialize(bucket);
    return bucket->type->peek(bucket, data, len);
}

static void fcgi_params_decode_destroy(serf_bucket_t *bucket)
{
    fcgi_serialize(bucket);

    bucket->type->destroy(bucket);
}


const serf_bucket_type_t serf_bucket_type__fcgi_params_decode =
{
    "FCGI-PARAMS_DECODE",
    fcgi_params_decode_read,
    serf_default_readline,
    serf_default_read_iovec,
    serf_default_read_for_sendfile,
    serf_buckets_are_v2,
    fcgi_params_decode_peek,
    fcgi_params_decode_destroy,
    serf_default_read_bucket,
    serf_default_get_remaining,
    serf_default_ignore_config
};
/* ==================================================================== */
typedef struct fcgi_frame_ctx_t
{
    serf_bucket_t *stream;
    serf_bucket_t *agg;
    apr_uint16_t stream_id;
    apr_uint16_t frame_type;
    bool send_stream;
    bool send_eof;
    bool at_eof;

    serf_config_t *config;

    char record_data[FCGI_RECORD_SIZE];
} fcgi_frame_ctx_t;


serf_bucket_t *
serf__bucket_fcgi_frame_create(serf_bucket_t *stream,
                               apr_uint16_t stream_id,
                               apr_uint16_t frame_type,
                               bool send_as_stream,
                               bool send_eof,
                               serf_bucket_alloc_t *alloc)
{
    fcgi_frame_ctx_t *ctx;

    ctx = serf_bucket_mem_alloc(alloc, sizeof(*ctx));
    ctx->stream = stream;
    ctx->stream_id = stream_id;
    ctx->frame_type = frame_type;
    ctx->send_stream = send_as_stream;
    ctx->send_eof = send_eof;
    ctx->at_eof = false;
    ctx->agg = NULL;
    ctx->config = NULL;

    return serf_bucket_create(&serf_bucket_type__fcgi_frame, alloc, ctx);
}

static apr_status_t destroy_bucket(void *baton,
                                   apr_uint64_t bytes_read)
{
    serf_bucket_t *head = baton;
    serf_bucket_destroy(head);
    return APR_SUCCESS;
}

static apr_status_t serf_fcgi_frame_refill(serf_bucket_t *bucket)
{
    fcgi_frame_ctx_t *ctx = bucket->data;
    apr_status_t status;
    serf_bucket_t *head = NULL;
    apr_size_t payload;

    if (ctx->at_eof)
        return APR_EOF;

    if (!ctx->agg)
        ctx->agg = serf_bucket_aggregate_create(bucket->allocator);

    if (!ctx->stream)
    {
        payload = 0;
        ctx->at_eof = true;

        if (ctx->send_stream && !ctx->send_eof)
            return APR_EOF;
    }
    else if (ctx->send_stream)
    {
        apr_uint64_t remaining;

        serf_bucket_split_create(&head, &ctx->stream, ctx->stream,
                                 1, 0xFFFF);

        remaining = serf_bucket_get_remaining(head);
        if (remaining == 0)
        {
            payload = 0;
            serf_bucket_destroy(head);
            ctx->at_eof = true;
        }
        else if (remaining != SERF_LENGTH_UNKNOWN) {
            serf_bucket_aggregate_append(ctx->agg, head);
            payload = (apr_size_t)remaining;
        }
        else
        {
            struct iovec vecs[SERF__STD_IOV_COUNT];
            int vecs_used;

            status = serf_bucket_read_iovec(head, 0xFFF0, COUNT_OF(vecs),
                                            vecs, &vecs_used);

            if (SERF_BUCKET_READ_ERROR(status))
                return status;
            else if (!APR_STATUS_IS_EOF(status)) {
                /* No get_remaining()... then the split screen should stop
                   directly when done reading this amount of vecs */
                return SERF_ERROR_TRUNCATED_STREAM;
            }

            if (vecs_used) {
                serf_bucket_t *tmp;

                tmp = serf_bucket_iovec_create(vecs, vecs_used, bucket->allocator);
                payload = (apr_size_t)serf_bucket_get_remaining(ctx->agg);

                tmp = serf__bucket_event_create(tmp, head, NULL, NULL,
                                                destroy_bucket, bucket->allocator);

                serf_bucket_aggregate_append(ctx->agg, tmp);
            }
            else
                payload = 0;
        }

        if (!payload && !ctx->send_eof)
            return APR_SUCCESS;
    }
    else
    {
        apr_uint64_t remaining = serf_bucket_get_remaining(ctx->stream);

        if (remaining >= 0xFFFF)
            abort(); /* Impossible (or unimplemented yet) */

        payload = (apr_size_t)remaining;
        ctx->at_eof = true;

        serf_bucket_aggregate_append(ctx->agg, ctx->stream);
        ctx->stream = NULL;
    }

    serf__log(LOGLVL_DEBUG, LOGCOMP_CONN, __FILE__, ctx->config,
              "Generating 0x%x frame on stream 0x%x of size %"
              APR_SIZE_T_FMT "\n",
              ctx->frame_type, ctx->stream_id, payload);

    /* Create FCGI record */
    ctx->record_data[0] = (ctx->frame_type >> 8);
    ctx->record_data[1] = (ctx->frame_type & 0xFF);
    ctx->record_data[2] = (ctx->stream_id >> 8);
    ctx->record_data[3] = (ctx->stream_id & 0xFF);
    ctx->record_data[4] = (payload >> 8) & 0xFF;
    ctx->record_data[5] = (payload & 0xFF);
    ctx->record_data[6] = 0; /* padding */
    ctx->record_data[7] = 0; /* reserved */

    serf_bucket_aggregate_prepend(ctx->agg,
                                  serf_bucket_simple_create(ctx->record_data,
                                                            FCGI_RECORD_SIZE,
                                                            NULL, NULL,
                                                            bucket->allocator));
    return APR_SUCCESS;
}

static apr_status_t serf_fcgi_frame_read(serf_bucket_t *bucket,
                                         apr_size_t requested,
                                         const char **data,
                                         apr_size_t *len)
{
    fcgi_frame_ctx_t *ctx = bucket->data;
    apr_status_t status;

    if (ctx->agg) {
        status = serf_bucket_read(ctx->agg, requested, data, len);

        if (!APR_STATUS_IS_EOF(status))
            return status;
        else if (*len)
            return APR_SUCCESS;
    }

    status = serf_fcgi_frame_refill(bucket);
    if (status) {
        *len = 0;
        return status;
    }

    status = serf_bucket_read(ctx->agg, requested, data, len);

    if (APR_STATUS_IS_EOF(status) && !ctx->at_eof)
        status = APR_SUCCESS;

    return status;
}

static apr_status_t serf_fcgi_frame_read_iovec(serf_bucket_t *bucket,
                                               apr_size_t requested,
                                               int vecs_size,
                                               struct iovec *vecs,
                                               int *vecs_used)
{
    fcgi_frame_ctx_t *ctx = bucket->data;
    apr_status_t status;

    if (ctx->agg) {
        status = serf_bucket_read_iovec(ctx->agg, requested, vecs_size,
                                        vecs, vecs_used);

        if (!APR_STATUS_IS_EOF(status))
            return status;
        else if (*vecs_used)
            return APR_SUCCESS;
    }

    status = serf_fcgi_frame_refill(bucket);
    if (status) {
        *vecs_used = 0;
        return status;
    }

    status = serf_bucket_read_iovec(ctx->agg, requested, vecs_size,
                                        vecs, vecs_used);

    if (APR_STATUS_IS_EOF(status) && !ctx->at_eof)
        status = APR_SUCCESS;

    return status;
}

static apr_status_t serf_fcgi_frame_peek(serf_bucket_t *bucket,
                                         const char **data,
                                         apr_size_t *len)
{
    fcgi_frame_ctx_t *ctx = bucket->data;
    apr_status_t status;

    if (ctx->agg) {
        status = serf_bucket_peek(ctx->agg, data, len);

        if (!APR_STATUS_IS_EOF(status))
            return status;
        else if (*len)
            return APR_SUCCESS;
    }

    status = serf_fcgi_frame_refill(bucket);
    if (status) {
        *len = 0;
        return status;
    }

    status = serf_bucket_peek(ctx->agg, data, len);

    if (APR_STATUS_IS_EOF(status) && !ctx->at_eof)
        status = APR_SUCCESS;

    return status;
}

static void serf_fcgi_frame_destroy(serf_bucket_t *bucket)
{
    fcgi_frame_ctx_t *ctx = bucket->data;

    if (ctx->agg)
        serf_bucket_destroy(ctx->agg);
    if (ctx->stream)
        serf_bucket_destroy(ctx->stream);

    serf_default_destroy_and_data(bucket);
}

static apr_uint64_t serf_fcgi_frame_get_remaining(serf_bucket_t *bucket)
{
    return SERF_LENGTH_UNKNOWN;
}

static apr_status_t serf_fcgi_frame_set_config(serf_bucket_t *bucket,
                                               serf_config_t *config)
{
    fcgi_frame_ctx_t *ctx = bucket->data;

    ctx->config = config;

    if (!ctx->agg)
        ctx->agg = serf_bucket_aggregate_create(bucket->allocator);
    serf_bucket_set_config(ctx->agg, config);
    if (ctx->stream)
        serf_bucket_set_config(ctx->stream, config);

    return APR_SUCCESS;
}

const serf_bucket_type_t serf_bucket_type__fcgi_frame =
{
    "FCGI-FRAME",
    serf_fcgi_frame_read,
    serf_default_readline,
    serf_fcgi_frame_read_iovec,
    serf_default_read_for_sendfile,
    serf_buckets_are_v2,
    serf_fcgi_frame_peek,
    serf_fcgi_frame_destroy,
    serf_default_read_bucket,
    serf_fcgi_frame_get_remaining,
    serf_fcgi_frame_set_config
};

