
/*
 * Copyright (C) Yichun Zhang (agentzh)
 */


#ifndef DDEBUG
#define DDEBUG 0
#endif
#include "ddebug.h"


#include "ngx_http_srcache_util.h"
#include "ngx_http_srcache_headers.h"


static ngx_int_t ngx_http_srcache_set_content_length_header(
        ngx_http_request_t *r, off_t len);


static ngx_str_t ngx_http_status_lines[] = {

    ngx_string("200 OK"),
    ngx_string("201 Created"),
    ngx_string("202 Accepted"),
    ngx_null_string,  /* "203 Non-Authoritative Information" */
    ngx_string("204 No Content"),
    ngx_null_string,  /* "205 Reset Content" */
    ngx_string("206 Partial Content"),

    /* ngx_null_string, */  /* "207 Multi-Status" */

#define NGX_HTTP_LAST_LEVEL_200  207
#define NGX_HTTP_LEVEL_200       (NGX_HTTP_LAST_LEVEL_200 - 200)

    /* ngx_null_string, */  /* "300 Multiple Choices" */

    ngx_string("301 Moved Permanently"),
    ngx_string("302 Moved Temporarily"),
    ngx_string("303 See Other"),
    ngx_string("304 Not Modified"),
    ngx_null_string,  /* "305 Use Proxy" */
    ngx_null_string,  /* "306 unused" */
    ngx_string("307 Temporary Redirect"),
    ngx_string("308 Permanent Redirect"),

#define NGX_HTTP_LAST_LEVEL_300  309
#define NGX_HTTP_LEVEL_300       (NGX_HTTP_LAST_LEVEL_300 - 301)

    ngx_string("400 Bad Request"),
    ngx_string("401 Unauthorized"),
    ngx_string("402 Payment Required"),
    ngx_string("403 Forbidden"),
    ngx_string("404 Not Found"),
    ngx_string("405 Not Allowed"),
    ngx_string("406 Not Acceptable"),
    ngx_null_string,  /* "407 Proxy Authentication Required" */
    ngx_string("408 Request Time-out"),
    ngx_string("409 Conflict"),
    ngx_string("410 Gone"),
    ngx_string("411 Length Required"),
    ngx_string("412 Precondition Failed"),
    ngx_string("413 Request Entity Too Large"),
    ngx_null_string,  /* "414 Request-URI Too Large", but we never send it
                       * because we treat such requests as the HTTP/0.9
                       * requests and send only a body without a header
                       */
    ngx_string("415 Unsupported Media Type"),
    ngx_string("416 Requested Range Not Satisfiable"),

    /* ngx_null_string, */  /* "417 Expectation Failed" */
    /* ngx_null_string, */  /* "418 unused" */
    /* ngx_null_string, */  /* "419 unused" */
    /* ngx_null_string, */  /* "420 unused" */
    /* ngx_null_string, */  /* "421 unused" */
    /* ngx_null_string, */  /* "422 Unprocessable Entity" */
    /* ngx_null_string, */  /* "423 Locked" */
    /* ngx_null_string, */  /* "424 Failed Dependency" */

#define NGX_HTTP_LAST_LEVEL_400  417
#define NGX_HTTP_LEVEL_400       (NGX_HTTP_LAST_LEVEL_400 - 400)

    ngx_string("500 Internal Server Error"),
    ngx_string("501 Method Not Implemented"),
    ngx_string("502 Bad Gateway"),
    ngx_string("503 Service Temporarily Unavailable"),
    ngx_string("504 Gateway Time-out"),

    ngx_null_string,        /* "505 HTTP Version Not Supported" */
    ngx_null_string,        /* "506 Variant Also Negotiates" */
    ngx_string("507 Insufficient Storage"),
    /* ngx_null_string, */  /* "508 unused" */
    /* ngx_null_string, */  /* "509 unused" */
    /* ngx_null_string, */  /* "510 Not Extended" */

#define NGX_HTTP_LAST_LEVEL_500  508

};


ngx_str_t  ngx_http_srcache_content_length_header_key =
        ngx_string("Content-Length");
ngx_str_t  ngx_http_srcache_get_method =
        ngx_http_srcache_method_name("GET");
ngx_str_t  ngx_http_srcache_put_method =
        ngx_http_srcache_method_name("PUT");
ngx_str_t  ngx_http_srcache_post_method =
        ngx_http_srcache_method_name("POST");
ngx_str_t  ngx_http_srcache_head_method =
        ngx_http_srcache_method_name("HEAD");
ngx_str_t  ngx_http_srcache_copy_method =
        ngx_http_srcache_method_name("COPY");
ngx_str_t  ngx_http_srcache_move_method =
        ngx_http_srcache_method_name("MOVE");
ngx_str_t  ngx_http_srcache_lock_method =
        ngx_http_srcache_method_name("LOCK");
ngx_str_t  ngx_http_srcache_mkcol_method =
        ngx_http_srcache_method_name("MKCOL");
ngx_str_t  ngx_http_srcache_trace_method =
        ngx_http_srcache_method_name("TRACE");
ngx_str_t  ngx_http_srcache_delete_method =
        ngx_http_srcache_method_name("DELETE");
ngx_str_t  ngx_http_srcache_unlock_method =
        ngx_http_srcache_method_name("UNLOCK");
ngx_str_t  ngx_http_srcache_options_method =
        ngx_http_srcache_method_name("OPTIONS");
ngx_str_t  ngx_http_srcache_propfind_method =
        ngx_http_srcache_method_name("PROPFIND");
ngx_str_t  ngx_http_srcache_proppatch_method =
        ngx_http_srcache_method_name("PROPPATCH");


void
ngx_http_srcache_discard_bufs(ngx_pool_t *pool, ngx_chain_t *in)
{
    ngx_chain_t         *cl;

    for (cl = in; cl; cl = cl->next) {
        cl->buf->pos = cl->buf->last;
    }
}


ngx_int_t
ngx_http_srcache_parse_method_name(ngx_str_t **method_name_ptr)
{
    const ngx_str_t         *method_name = *method_name_ptr;

    switch (method_name->len) {

    case 3:
        if (ngx_http_srcache_strcmp_const(method_name->data, "GET") == 0) {
            *method_name_ptr = &ngx_http_srcache_get_method;
            return NGX_HTTP_GET;
        }

        if (ngx_http_srcache_strcmp_const(method_name->data, "PUT") == 0) {
            *method_name_ptr = &ngx_http_srcache_put_method;
            return NGX_HTTP_PUT;
        }

        return NGX_HTTP_UNKNOWN;

    case 4:
        if (ngx_http_srcache_strcmp_const(method_name->data, "POST") == 0) {
            *method_name_ptr = &ngx_http_srcache_post_method;
            return NGX_HTTP_POST;
        }

        if (ngx_http_srcache_strcmp_const(method_name->data, "HEAD") == 0) {
            *method_name_ptr = &ngx_http_srcache_head_method;
            return NGX_HTTP_HEAD;
        }

        if (ngx_http_srcache_strcmp_const(method_name->data, "COPY") == 0) {
            *method_name_ptr = &ngx_http_srcache_copy_method;
            return NGX_HTTP_COPY;
        }

        if (ngx_http_srcache_strcmp_const(method_name->data, "MOVE") == 0) {
            *method_name_ptr = &ngx_http_srcache_move_method;
            return NGX_HTTP_MOVE;
        }

        if (ngx_http_srcache_strcmp_const(method_name->data, "LOCK") == 0) {
            *method_name_ptr = &ngx_http_srcache_lock_method;
            return NGX_HTTP_LOCK;
        }

        return NGX_HTTP_UNKNOWN;

    case 5:
        if (ngx_http_srcache_strcmp_const(method_name->data, "MKCOL") == 0) {
            *method_name_ptr = &ngx_http_srcache_mkcol_method;
            return NGX_HTTP_MKCOL;
        }

        if (ngx_http_srcache_strcmp_const(method_name->data, "TRACE") == 0) {
            *method_name_ptr = &ngx_http_srcache_trace_method;
            return NGX_HTTP_TRACE;
        }

        return NGX_HTTP_UNKNOWN;

    case 6:
        if (ngx_http_srcache_strcmp_const(method_name->data, "DELETE") == 0) {
            *method_name_ptr = &ngx_http_srcache_delete_method;
            return NGX_HTTP_DELETE;
        }

        if (ngx_http_srcache_strcmp_const(method_name->data, "UNLOCK") == 0) {
            *method_name_ptr = &ngx_http_srcache_unlock_method;
            return NGX_HTTP_UNLOCK;
        }

        return NGX_HTTP_UNKNOWN;

    case 7:
        if (ngx_http_srcache_strcmp_const(method_name->data, "OPTIONS") == 0) {
            *method_name_ptr = &ngx_http_srcache_options_method;
            return NGX_HTTP_OPTIONS;
        }

        return NGX_HTTP_UNKNOWN;

    case 8:
        if (ngx_http_srcache_strcmp_const(method_name->data, "PROPFIND") == 0) {
            *method_name_ptr = &ngx_http_srcache_propfind_method;
            return NGX_HTTP_PROPFIND;
        }

        return NGX_HTTP_UNKNOWN;

    case 9:
        if (ngx_http_srcache_strcmp_const(method_name->data, "PROPPATCH")
            == 0)
        {
            *method_name_ptr = &ngx_http_srcache_proppatch_method;
            return NGX_HTTP_PROPPATCH;
        }

        return NGX_HTTP_UNKNOWN;

    default:
        return NGX_HTTP_UNKNOWN;
    }

    return NGX_HTTP_UNKNOWN;
}


ngx_int_t
ngx_http_srcache_adjust_subrequest(ngx_http_request_t *sr,
    ngx_http_srcache_parsed_request_t *parsed_sr)
{
    ngx_http_core_main_conf_t  *cmcf;
    ngx_http_request_t         *r;
    ngx_http_request_body_t    *body;
    ngx_int_t                   rc;

    sr->method      = parsed_sr->method;
    sr->method_name = parsed_sr->method_name;

    r = sr->parent;

    dd("subrequest method: %d %.*s", (int) sr->method,
       (int) sr->method_name.len, sr->method_name.data);

    sr->header_in = r->header_in;

#if 1
    /* XXX work-around a bug in ngx_http_subrequest */
    if (r->headers_in.headers.last == &r->headers_in.headers.part) {
        sr->headers_in.headers.last = &sr->headers_in.headers.part;
    }
#endif

    /* we do not inherit the parent request's variables */
    cmcf = ngx_http_get_module_main_conf(sr, ngx_http_core_module);
    sr->variables = ngx_pcalloc(sr->pool, cmcf->variables.nelts
                                * sizeof(ngx_http_variable_value_t));

    if (sr->variables == NULL) {
        return NGX_ERROR;
    }

    body = parsed_sr->request_body;
    if (body) {
        sr->request_body = body;

        rc = ngx_http_srcache_set_content_length_header(sr,
                                                 parsed_sr->content_length_n);

        if (rc != NGX_OK) {
            return NGX_ERROR;
        }
    }

    return NGX_OK;
}


ngx_int_t
ngx_http_srcache_add_copy_chain(ngx_pool_t *pool, ngx_chain_t **chain,
    ngx_chain_t *in, unsigned *plast)
{
    ngx_chain_t     *cl, **ll;
    size_t           len;

    ll = chain;

    for (cl = *chain; cl; cl = cl->next) {
        ll = &cl->next;
    }

    *plast = 0;

    while (in) {
        cl = ngx_alloc_chain_link(pool);
        if (cl == NULL) {
            return NGX_ERROR;
        }

        if (in->buf->last_buf || in->buf->last_in_chain) {
            *plast = 1;
        }

        if (ngx_buf_special(in->buf)) {
            cl->buf = in->buf;

        } else {
            if (ngx_buf_in_memory(in->buf)) {
                len = ngx_buf_size(in->buf);
                cl->buf = ngx_create_temp_buf(pool, len);
                if (cl->buf == NULL) {
                    return NGX_ERROR;
                }

                dd("buf: %.*s", (int) len, in->buf->pos);

                cl->buf->last = ngx_copy(cl->buf->pos, in->buf->pos, len);

            } else {
                return NGX_ERROR;
            }
        }

        *ll = cl;
        ll = &cl->next;
        in = in->next;
    }

    *ll = NULL;

    return NGX_OK;
}


ngx_int_t
ngx_http_srcache_post_request_at_head(ngx_http_request_t *r,
    ngx_http_posted_request_t *pr)
{
    dd_enter();

    if (pr == NULL) {
        pr = ngx_palloc(r->pool, sizeof(ngx_http_posted_request_t));
        if (pr == NULL) {
            return NGX_ERROR;
        }
    }

    pr->request = r;
    pr->next = r->main->posted_requests;
    r->main->posted_requests = pr;

    return NGX_OK;
}


static ngx_int_t
ngx_http_srcache_set_content_length_header(ngx_http_request_t *r, off_t len)
{
    ngx_table_elt_t                 *h, *header;
    u_char                          *p;
    ngx_list_part_t                 *part;
    ngx_http_request_t              *pr;
    ngx_uint_t                       i;

    r->headers_in.content_length_n = len;

    if (ngx_list_init(&r->headers_in.headers, r->pool, 20,
                      sizeof(ngx_table_elt_t))
        != NGX_OK)
    {
        return NGX_ERROR;
    }

    h = ngx_list_push(&r->headers_in.headers);
    if (h == NULL) {
        return NGX_ERROR;
    }

    h->key = ngx_http_srcache_content_length_header_key;
    h->lowcase_key = ngx_pnalloc(r->pool, h->key.len);
    if (h->lowcase_key == NULL) {
        return NGX_ERROR;
    }

    ngx_strlow(h->lowcase_key, h->key.data, h->key.len);

    r->headers_in.content_length = h;

    p = ngx_palloc(r->pool, NGX_OFF_T_LEN);
    if (p == NULL) {
        return NGX_ERROR;
    }

    h->value.data = p;

    h->value.len = ngx_sprintf(h->value.data, "%O", len) - h->value.data;

    h->hash = ngx_hash(ngx_hash(ngx_hash(ngx_hash(ngx_hash(ngx_hash(ngx_hash(
            ngx_hash(ngx_hash(ngx_hash(ngx_hash(ngx_hash(
            ngx_hash('c', 'o'), 'n'), 't'), 'e'), 'n'), 't'), '-'), 'l'), 'e'),
            'n'), 'g'), 't'), 'h');

    dd("r content length: %.*s",
       (int) r->headers_in.content_length->value.len,
       r->headers_in.content_length->value.data);

    pr = r->parent;

    if (pr == NULL) {
        return NGX_OK;
    }

    /* forward the parent request's all other request headers */

    part = &pr->headers_in.headers.part;
    header = part->elts;

    for (i = 0; /* void */; i++) {

        if (i >= part->nelts) {
            if (part->next == NULL) {
                break;
            }

            part = part->next;
            header = part->elts;
            i = 0;
        }

        if (header[i].key.len == sizeof("Content-Length") - 1
            && ngx_strncasecmp(header[i].key.data,
                               (u_char *) "Content-Length",
                               sizeof("Content-Length") - 1) == 0)
        {
            continue;
        }

        h = ngx_list_push(&r->headers_in.headers);
        if (h == NULL) {
            return NGX_ERROR;
        }

        *h = header[i];
    }

    /* XXX maybe we should set those built-in header slot in
     * ngx_http_headers_in_t too? */

    return NGX_OK;
}


ngx_int_t
ngx_http_srcache_request_no_cache(ngx_http_request_t *r, unsigned *no_store)
{
    ngx_table_elt_t                 *h;
    ngx_list_part_t                 *part;
    u_char                          *p;
    u_char                          *last;
    ngx_uint_t                       i;
    unsigned                         no_cache;

    part = &r->headers_in.headers.part;
    h = part->elts;

    *no_store = 0;
    no_cache = 0;

    for (i = 0; /* void */; i++) {

        if (i >= part->nelts) {
            if (part->next == NULL) {
                break;
            }

            part = part->next;
            h = part->elts;
            i = 0;
        }

        if (h[i].key.len == sizeof("Cache-Control") - 1
            && ngx_strncasecmp(h[i].key.data, (u_char *) "Cache-Control",
                               sizeof("Cache-Control") - 1) == 0)
        {
            p = h[i].value.data;
            last = p + h[i].value.len;

            if (!*no_store
                && ngx_strlcasestrn(p, last, (u_char *) "no-store", 8 - 1)
                   != NULL)
            {
                *no_store = 1;
            }

            if (ngx_strlcasestrn(p, last, (u_char *) "no-cache", 8 - 1) != NULL)
            {
                no_cache = 1;
            }

            continue;
        }

        if (h[i].key.len == sizeof("Pragma") - 1
            && ngx_strncasecmp(h[i].key.data, (u_char *) "Pragma",
                               sizeof("Pragma") - 1) == 0)
        {
            p = h[i].value.data;
            last = p + h[i].value.len;

            if (ngx_strlcasestrn(p, last, (u_char *) "no-cache", 8 - 1) != NULL)
            {
                no_cache = 1;
            }
        }
    }

    return no_cache ? NGX_OK : NGX_DECLINED;
}


ngx_int_t
ngx_http_srcache_response_no_cache(ngx_http_request_t *r,
    ngx_http_srcache_loc_conf_t *conf, ngx_http_srcache_ctx_t *ctx)
{
    ngx_table_elt_t    *h;
#if defined(nginx_version) && nginx_version >= 1023000
    ngx_table_elt_t    *cc;
#else
    ngx_table_elt_t   **ccp;
    ngx_uint_t          i;
#endif
    u_char             *p, *last;
    ngx_int_t           n;
    time_t              expires;

    dd("checking response cache control settings");

#if defined(nginx_version) && nginx_version >= 1023000
    cc = r->headers_out.cache_control;

    if (cc == NULL) {
        goto check_expires;
    }

    for (; cc; cc = cc->next) {
        if (!cc->hash) {
            continue;
        }

        p = cc->value.data;
        last = p + cc->value.len;
#else
    ccp = r->headers_out.cache_control.elts;

    if (ccp == NULL) {
        goto check_expires;
    }

    for (i = 0; i < r->headers_out.cache_control.nelts; i++) {
        if (!ccp[i]->hash) {
            continue;
        }

        p = ccp[i]->value.data;
        last = p + ccp[i]->value.len;
#endif

        if (!conf->store_private
            && ngx_strlcasestrn(p, last, (u_char *) "private", 7 - 1) != NULL)
        {
            return NGX_OK;
        }

        if (!conf->store_no_store
            && ngx_strlcasestrn(p, last, (u_char *) "no-store", 8 - 1) != NULL)
        {
            return NGX_OK;
        }

        if (!conf->store_no_cache
            && ngx_strlcasestrn(p, last, (u_char *) "no-cache", 8 - 1) != NULL)
        {
            return NGX_OK;
        }

        if (ctx->valid_sec != 0) {
            continue;
        }

        p = ngx_strlcasestrn(p, last, (u_char *) "max-age=", 8 - 1);

        if (p == NULL) {
            continue;
        }

        n = 0;

        for (p += 8; p < last; p++) {
            if (*p == ',' || *p == ';' || *p == ' ') {
                break;
            }

            if (*p >= '0' && *p <= '9') {
                n = n * 10 + *p - '0';
                continue;
            }

            return NGX_OK;
        }

        if (n == 0) {
            return NGX_OK;
        }

        ctx->valid_sec = ngx_time() + n;
    }

check_expires:

    dd("valid_sec after processing cache-control: %d", (int) ctx->valid_sec);

    if (ctx->valid_sec == 0) {
        h = r->headers_out.expires;

        dd("expires header: %p", h);

        if (h != NULL && h->hash != 0) {
            expires = ngx_http_parse_time(h->value.data, h->value.len);

            if (expires == NGX_ERROR || expires <= ngx_time()) {
                return NGX_OK;
            }

            ctx->valid_sec = expires;
        }
    }

    return NGX_DECLINED;
}


ngx_int_t
ngx_http_srcache_process_status_line(ngx_http_request_t *r, ngx_buf_t *b)
{
    ngx_int_t                    rc;
    ngx_http_srcache_ctx_t      *ctx;
    ngx_http_request_t          *pr;
    ngx_http_srcache_loc_conf_t *conf;

    ctx = ngx_http_get_module_ctx(r, ngx_http_srcache_filter_module);

    rc = ngx_http_parse_status_line(r, b, &ctx->status);

    if (rc == NGX_AGAIN) {
        return NGX_AGAIN;
    }

    if (rc == NGX_ERROR) {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "srcache_fetch: cache sent invalid status line");
        return NGX_ERROR;
    }

    /* rc == NGX_OK */

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                   "srcache_fetch status line done");

    pr = r->parent;
    pr->headers_out.status = ctx->status.code;

    ctx->process_header = ngx_http_srcache_process_header;

    conf = ngx_http_get_module_loc_conf(pr, ngx_http_srcache_filter_module);

    dd("header buffer size: %d", (int) conf->header_buf_size);

    ctx->header_buf = ngx_create_temp_buf(r->pool, conf->header_buf_size);
    if (ctx->header_buf == NULL) {
        return NGX_ERROR;
    }

    if (b->pos == b->last) {
        return NGX_AGAIN;
    }

    return ngx_http_srcache_process_header(r, b);
}


ngx_int_t
ngx_http_srcache_process_header(ngx_http_request_t *r, ngx_buf_t *b)
{
    ngx_int_t                       rc;
    ngx_table_elt_t                 header;
    ngx_http_srcache_ctx_t         *ctx;
    off_t                           len, rest;
    unsigned                        truncate;
    u_char                         *p;
    ngx_http_srcache_header_t      *hh;
    ngx_http_srcache_main_conf_t   *smcf;

    smcf = ngx_http_get_module_main_conf(r, ngx_http_srcache_filter_module);

    ctx = ngx_http_get_module_ctx(r, ngx_http_srcache_filter_module);

    for ( ;; ) {

        len = b->last - b->pos;
        rest = ctx->header_buf->end - ctx->header_buf->last;

        dd("len: %d, rest: %d", (int) len, (int) rest);

        if (len > rest) {
            len = rest;
            truncate = 1;

        } else {
            truncate = 0;
        }

        ctx->header_buf->last = ngx_copy(ctx->header_buf->last, b->pos,
                                         (size_t) len);

        p = ctx->header_buf->pos;

        rc = ngx_http_parse_header_line(r, ctx->header_buf, 1);

        b->pos += ctx->header_buf->pos - p;

        if (rc == NGX_OK) {

            /* a header line has been parsed successfully */

            ngx_memzero(&header, sizeof(ngx_table_elt_t));

            header.hash = r->header_hash;

            header.key.len = r->header_name_end - r->header_name_start;
            header.value.len = r->header_end - r->header_start;

            header.key.data = ngx_pnalloc(r->pool,
                                          header.key.len + 1
                                          + header.value.len + 1
                                          + header.key.len);

            if (header.key.data == NULL) {
                return NGX_ERROR;
            }

            header.value.data = header.key.data + header.key.len + 1;
            header.lowcase_key = header.key.data + header.key.len + 1
                                 + header.value.len + 1;

            ngx_cpystrn(header.key.data, r->header_name_start,
                        header.key.len + 1);

            ngx_cpystrn(header.value.data, r->header_start,
                        header.value.len + 1);

            if (header.key.len == r->lowcase_index) {
                ngx_memcpy(header.lowcase_key, r->lowcase_header,
                           header.key.len);

            } else {
                ngx_strlow(header.lowcase_key, header.key.data, header.key.len);
            }

            hh = ngx_hash_find(&smcf->headers_in_hash, header.hash,
                               header.lowcase_key, header.key.len);

            if (hh) {
                if (hh->handler(r->parent, &header, hh->offset) != NGX_OK) {
                    return NGX_ERROR;
                }

            } else {

                if (ngx_http_srcache_process_header_line(r->parent, &header, 0)
                    != NGX_OK)
                {
                    return NGX_ERROR;
                }
            }

            ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                           "srcache_fetch header: \"%V: %V\"",
                           &header.key, &header.value);

            ctx->header_buf->pos = ctx->header_buf->start;
            ctx->header_buf->last = ctx->header_buf->start;

            continue;
        }

        if (rc == NGX_HTTP_PARSE_HEADER_DONE) {

            /* a whole header has been parsed successfully */

            ctx->header_buf->pos = ctx->header_buf->start;
            ctx->header_buf->last = ctx->header_buf->start;
            ngx_pfree(r->pool, ctx->header_buf->start);

            ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                           "srcache_fetch header done");

            return NGX_OK;
        }

        if (rc == NGX_AGAIN) {
            if (truncate) {
                ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                              "srcache_fetch: header buffer overflown "
                              "(maybe you should consider increasing "
                              "srcache_header_buffer_size?)");

                ctx->header_buf->pos = ctx->header_buf->start;
                ctx->header_buf->last = ctx->header_buf->start;
                ngx_pfree(r->pool, ctx->header_buf->start);

                return NGX_ERROR;
            }

            return NGX_AGAIN;
        }

        /* there was error while a header line parsing */

        ctx->header_buf->pos = ctx->header_buf->start;
        ctx->header_buf->last = ctx->header_buf->start;
        ngx_pfree(r->pool, ctx->header_buf->start);

        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "srcache_fetch: cache sent invalid header");

        return NGX_ERROR;
    }
}


ngx_int_t
ngx_http_srcache_store_response_header(ngx_http_request_t *r,
    ngx_http_srcache_ctx_t *ctx)
{
    ngx_chain_t             *cl;
    size_t                   len;
    ngx_buf_t               *b;
    ngx_uint_t               status;
    ngx_uint_t               i;
    ngx_str_t               *status_line;
    ngx_list_part_t         *part;
    ngx_table_elt_t         *header;

    u_char                   buf[sizeof("Mon, 28 Sep 1970 06:00:00 GMT") - 1];

    ngx_http_srcache_loc_conf_t    *conf;

    conf = ngx_http_get_module_loc_conf(r, ngx_http_srcache_filter_module);

    dd("request: %p, uri: %.*s", r, (int) r->uri.len, r->uri.data);

    len = sizeof("HTTP/1.x ") - 1 + sizeof(CRLF) - 1
          /* the end of the header */
          + sizeof(CRLF) - 1;

    if (r->headers_out.status_line.len) {
        dd("status line defined");
        len += r->headers_out.status_line.len;
        status_line = &r->headers_out.status_line;
        status = 0;

    } else {
        dd("status line not defined");

        status = r->headers_out.status;

        if (status >= NGX_HTTP_OK
            && status < NGX_HTTP_LAST_LEVEL_200)
        {
            /* 2XX */

            status -= NGX_HTTP_OK;
            dd("status: %d", (int) status);
            status_line = &ngx_http_status_lines[status];
            len += ngx_http_status_lines[status].len;

        } else if (status >= NGX_HTTP_MOVED_PERMANENTLY
                   && status < NGX_HTTP_LAST_LEVEL_300)
        {
            /* 3XX */

            if (status == NGX_HTTP_NOT_MODIFIED) {
                r->header_only = 1;
            }

            status = status - NGX_HTTP_MOVED_PERMANENTLY + NGX_HTTP_LEVEL_200;
            status_line = &ngx_http_status_lines[status];
            len += ngx_http_status_lines[status].len;

        } else if (status >= NGX_HTTP_BAD_REQUEST
                   && status < NGX_HTTP_LAST_LEVEL_400)
        {
            /* 4XX */
            status = status - NGX_HTTP_BAD_REQUEST
                            + NGX_HTTP_LEVEL_200
                            + NGX_HTTP_LEVEL_300;

            status_line = &ngx_http_status_lines[status];
            len += ngx_http_status_lines[status].len;

        } else if (status >= NGX_HTTP_INTERNAL_SERVER_ERROR
                   && status < NGX_HTTP_LAST_LEVEL_500)
        {
            /* 5XX */
            status = status - NGX_HTTP_INTERNAL_SERVER_ERROR
                            + NGX_HTTP_LEVEL_200
                            + NGX_HTTP_LEVEL_300
                            + NGX_HTTP_LEVEL_400;

            status_line = &ngx_http_status_lines[status];
            len += ngx_http_status_lines[status].len;

        } else {
            len += NGX_INT_T_LEN;
            status_line = NULL;
        }
    }

    if (!conf->hide_content_type && r->headers_out.content_type.len) {
        len += sizeof("Content-Type: ") - 1
               + r->headers_out.content_type.len + 2;

        if (r->headers_out.content_type_len == r->headers_out.content_type.len
            && r->headers_out.charset.len)
        {
            len += sizeof("; charset=") - 1 + r->headers_out.charset.len;
        }
    }

    if (!conf->hide_last_modified) {
        if (r->headers_out.last_modified_time != -1) {
            if (r->headers_out.status != NGX_HTTP_OK
                && r->headers_out.status != NGX_HTTP_PARTIAL_CONTENT
                && r->headers_out.status != NGX_HTTP_NOT_MODIFIED
                && r->headers_out.status != NGX_HTTP_NO_CONTENT)
            {
                r->headers_out.last_modified_time = -1;
                r->headers_out.last_modified = NULL;
            }
        }

        dd("last modified time: %d", (int) r->headers_out.last_modified_time);

        if (r->headers_out.last_modified == NULL
            && r->headers_out.last_modified_time != -1)
        {
            (void) ngx_http_time(buf, r->headers_out.last_modified_time);

            len += sizeof("Last-Modified: ") - 1 + sizeof(buf) + 2;
        }
    }

    if (r->allow_ranges) {
        len += sizeof("X-SRCache-Allow-Ranges: 1") - 1 + 2;
    }

    part = &r->headers_out.headers.part;
    header = part->elts;

    for (i = 0; /* void */; i++) {

        if (i >= part->nelts) {
            if (part->next == NULL) {
                break;
            }

            part = part->next;
            header = part->elts;
            i = 0;
        }

        if (header[i].hash == 0) {
            continue;
        }

        if (ngx_hash_find(&conf->hide_headers_hash, header[i].hash,
                          header[i].lowcase_key, header[i].key.len))
        {
            continue;
        }

        len += header[i].key.len + sizeof(": ") - 1 + header[i].value.len
               + sizeof(CRLF) - 1;
    }

    b = ngx_create_temp_buf(r->pool, len);
    if (b == NULL) {
        return NGX_ERROR;
    }

    /* "HTTP/1.x " */
    b->last = ngx_cpymem(b->last, "HTTP/1.1 ", sizeof("HTTP/1.x ") - 1);

    /* status line */
    if (status_line) {
        b->last = ngx_copy(b->last, status_line->data, status_line->len);

    } else {
        b->last = ngx_sprintf(b->last, "%ui", status);
    }
    *b->last++ = CR; *b->last++ = LF;

    if (!conf->hide_content_type && r->headers_out.content_type.len) {
        b->last = ngx_cpymem(b->last, "Content-Type: ",
                             sizeof("Content-Type: ") - 1);
        b->last = ngx_copy(b->last, r->headers_out.content_type.data,
                           r->headers_out.content_type.len);

        if (r->headers_out.content_type_len == r->headers_out.content_type.len
            && r->headers_out.charset.len)
        {
            b->last = ngx_cpymem(b->last, "; charset=",
                                 sizeof("; charset=") - 1);
            b->last = ngx_copy(b->last, r->headers_out.charset.data,
                               r->headers_out.charset.len);
        }

        *b->last++ = CR; *b->last++ = LF;
    }

    if (!conf->hide_last_modified
        && r->headers_out.last_modified == NULL
        && r->headers_out.last_modified_time != -1)
    {
        b->last = ngx_cpymem(b->last, "Last-Modified: ",
                             sizeof("Last-Modified: ") - 1);

        b->last = ngx_cpymem(b->last, buf, sizeof(buf));

        *b->last++ = CR; *b->last++ = LF;
    }

    if (r->allow_ranges) {
        b->last = ngx_cpymem(b->last, "X-SRCache-Allow-Ranges: 1\r\n",
                             sizeof("X-SRCache-Allow-Ranges: 1\r\n") - 1);
    }

    part = &r->headers_out.headers.part;
    header = part->elts;

    for (i = 0; /* void */; i++) {

        if (i >= part->nelts) {
            if (part->next == NULL) {
                break;
            }

            part = part->next;
            header = part->elts;
            i = 0;
        }

        if (header[i].hash == 0) {
            continue;
        }

        dd("header hash: %lu, hash lc: %lu", (unsigned long) header[i].hash,
           (unsigned long) ngx_hash_key_lc(header[i].key.data,
                                           header[i].key.len));

        if (ngx_hash_find(&conf->hide_headers_hash, header[i].hash,
                          header[i].lowcase_key, header[i].key.len))
        {
            dd("skipped header key: %.*s", (int) header[i].key.len,
               header[i].key.data);
            continue;
        }

        dd("header not skipped");

        b->last = ngx_copy(b->last, header[i].key.data, header[i].key.len);
        *b->last++ = ':'; *b->last++ = ' ';

        b->last = ngx_copy(b->last, header[i].value.data, header[i].value.len);
        *b->last++ = CR; *b->last++ = LF;
    }

    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                   "srcache store header %*s", (size_t) (b->last - b->pos),
                   b->pos);

    /* the end of HTTP header */
    *b->last++ = CR; *b->last++ = LF;

    if (b->last != b->end) {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "srcache_fetch: buffer error when serializing the "
                      "response header: %O left", (off_t) (b->last - b->end));

        return NGX_ERROR;
    }

    cl = ngx_alloc_chain_link(r->pool);
    if (cl == NULL) {
        return NGX_ERROR;
    }

    cl->buf = b;
    cl->next = NULL;

    ctx->body_to_cache = cl;

    ctx->response_length += len;

    return NGX_OK;
}


ngx_int_t
ngx_http_srcache_hide_headers_hash(ngx_conf_t *cf,
    ngx_http_srcache_loc_conf_t *conf, ngx_http_srcache_loc_conf_t *prev,
    ngx_str_t *default_hide_headers, ngx_hash_init_t *hash)
{
    ngx_str_t       *h;
    ngx_uint_t       i, j;
    ngx_array_t      hide_headers;
    ngx_hash_key_t  *hk;

    if (conf->hide_headers == NGX_CONF_UNSET_PTR
        && conf->pass_headers == NGX_CONF_UNSET_PTR)
    {
        conf->hide_headers_hash = prev->hide_headers_hash;

        if (conf->hide_headers_hash.buckets) {
            return NGX_OK;
        }

        conf->hide_headers = prev->hide_headers;
        conf->pass_headers = prev->pass_headers;
        conf->hide_content_type = prev->hide_content_type;
        conf->hide_last_modified = prev->hide_last_modified;

    } else {
        if (conf->hide_headers == NGX_CONF_UNSET_PTR) {
            conf->hide_headers = prev->hide_headers;
        }

        if (conf->pass_headers == NGX_CONF_UNSET_PTR) {
            conf->pass_headers = prev->pass_headers;
        }
    }

    dd("init hide headers");

    if (ngx_array_init(&hide_headers, cf->temp_pool, 4, sizeof(ngx_hash_key_t))
        != NGX_OK)
    {
        return NGX_ERROR;
    }

    for (h = default_hide_headers; h->len; h++) {
        hk = ngx_array_push(&hide_headers);
        if (hk == NULL) {
            return NGX_ERROR;
        }

        hk->key = *h;
        hk->key_hash = ngx_hash_key_lc(h->data, h->len);
        hk->value = (void *) 1;
    }

    if (conf->hide_headers != NGX_CONF_UNSET_PTR) {
        dd("hide headers not empty");

        h = conf->hide_headers->elts;

        for (i = 0; i < conf->hide_headers->nelts; i++) {

            hk = hide_headers.elts;

            for (j = 0; j < hide_headers.nelts; j++) {
                if (ngx_strcasecmp(h[i].data, hk[j].key.data) == 0) {
                    goto exist;
                }
            }

            hk = ngx_array_push(&hide_headers);
            if (hk == NULL) {
                return NGX_ERROR;
            }

            hk->key = h[i];
            hk->key_hash = ngx_hash_key_lc(h[i].data, h[i].len);
            hk->value = (void *) 1;

            if (h[i].len == sizeof("Last-Modified") - 1
                && ngx_strncasecmp(h[i].data, (u_char *) "Last-Modified",
                                   sizeof("Last-Modified") - 1)
                == 0)
            {
                conf->hide_last_modified = 1;
            }

            if (h[i].len == sizeof("Content-Type") - 1
                && ngx_strncasecmp(h[i].data, (u_char *) "Content-Type",
                                   sizeof("Content-Type") - 1) == 0)
            {
                conf->hide_content_type = 1;
            }

            dd("adding header to hide headers: %.*s", (int) h[i].len,
               h[i].data);

        exist:

            continue;
        }
    }

    if (conf->pass_headers != NGX_CONF_UNSET_PTR) {

        h = conf->pass_headers->elts;
        hk = hide_headers.elts;

        for (i = 0; i < conf->pass_headers->nelts; i++) {
            for (j = 0; j < hide_headers.nelts; j++) {

                if (hk[j].key.data == NULL) {
                    continue;
                }

                if (h[i].len == sizeof("Content-Type") - 1
                    && ngx_strncasecmp(h[i].data, (u_char *) "Content-Type",
                                       sizeof("Content-Type") - 1) == 0)
                {
                    conf->hide_content_type = 0;
                }

                if (h[i].len == sizeof("Last-Modified") - 1
                    && ngx_strncasecmp(h[i].data, (u_char *) "Last-Modified",
                                       sizeof("Last-Modified") - 1) == 0)
                {
                    conf->hide_last_modified = 0;
                }

                if (ngx_strcasecmp(h[i].data, hk[j].key.data) == 0) {
                    hk[j].key.data = NULL;
                    break;
                }
            }
        }
    }

    hash->hash = &conf->hide_headers_hash;
    hash->key = ngx_hash_key_lc;
    hash->pool = cf->pool;
    hash->temp_pool = NULL;

    return ngx_hash_init(hash, hide_headers.elts, hide_headers.nelts);
}


ngx_int_t
ngx_http_srcache_cmp_int(const void *one, const void *two)
{
    const ngx_int_t           *a = one;
    const ngx_int_t           *b = two;

    return (*a < *b);
}

/* vi:set ft=c ts=4 sw=4 et fdm=marker: */
