
/*
 * Copyright (C) Igor Sysoev
 */


#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
#include <ngx_http_proxy_handler.h>


static int ngx_http_proxy_process_cached_response(ngx_http_proxy_ctx_t *p,
                                                  int rc);
static int ngx_http_proxy_process_cached_header(ngx_http_proxy_ctx_t *p);
static void ngx_http_proxy_cache_look_complete_request(ngx_http_proxy_ctx_t *p);


int ngx_http_proxy_get_cached_response(ngx_http_proxy_ctx_t *p)
{
    char                            *last;
    ngx_http_request_t              *r;
    ngx_http_proxy_cache_t          *c;
    ngx_http_proxy_upstream_conf_t  *u;

    r = p->request;

    if (!(c = ngx_pcalloc(r->pool, sizeof(ngx_http_proxy_cache_t)))) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    p->cache = c;

    c->ctx.file.fd = NGX_INVALID_FILE;
    c->ctx.file.log = r->connection->log;
    c->ctx.path = p->lcf->cache_path;

    u = p->lcf->upstream;

    c->ctx.key.len = u->url.len + r->uri.len - u->location->len + r->args.len;
    if (!(c->ctx.key.data = ngx_palloc(r->pool, c->ctx.key.len + 1))) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    last = ngx_cpymem(c->ctx.key.data, u->url.data, u->url.len);

    last = ngx_cpymem(last, r->uri.data + u->location->len,
                      r->uri.len - u->location->len);

    if (r->args.len > 0) {
        *(last++) = '?';
        last = ngx_cpymem(last, r->args.data, r->args.len);
    }
    *last = '\0';

    p->header_in = ngx_create_temp_hunk(r->pool, p->lcf->header_buffer_size);
    if (p->header_in == NULL) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }
    p->header_in->tag = (ngx_hunk_tag_t) &ngx_http_proxy_module;

    c->ctx.buf = p->header_in; 
    c->ctx.log = r->connection->log;

    return ngx_http_proxy_process_cached_response(p,
                                          ngx_http_cache_get_file(r, &c->ctx));
}


static int ngx_http_proxy_process_cached_response(ngx_http_proxy_ctx_t *p,
                                                  int rc)
{
    if (rc == NGX_OK) {
        p->state->cache_state = NGX_HTTP_PROXY_CACHE_HIT;
        p->header_in->pos = p->header_in->start + p->cache->ctx.header_size;

        if (ngx_http_proxy_process_cached_header(p) == NGX_ERROR) {
            return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }

        p->valid_header_in = 1;

        return ngx_http_proxy_send_cached_response(p);
    }

    if (rc == NGX_HTTP_CACHE_STALE) {
        p->state->cache_state = NGX_HTTP_PROXY_CACHE_EXPR;

    } else if (rc == NGX_HTTP_CACHE_AGED) {
        p->state->cache_state = NGX_HTTP_PROXY_CACHE_AGED;
    }

    if (rc == NGX_HTTP_CACHE_STALE || rc == NGX_HTTP_CACHE_AGED) {
        p->state->expired = ngx_time() - p->cache->ctx.expires;
        p->header_in->pos = p->header_in->start + p->cache->ctx.header_size;

        if (ngx_http_proxy_process_cached_header(p) == NGX_ERROR) {
            return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }

        p->header_in->pos = p->header_in->start + p->cache->ctx.header_size;
        p->header_in->last = p->header_in->pos;

        p->stale = 1;
        p->valid_header_in = 1;

    } else if (rc == NGX_DECLINED) {
        p->state->cache_state = NGX_HTTP_PROXY_CACHE_MISS;
        p->header_in->pos = p->header_in->start + p->cache->ctx.header_size;
        p->header_in->last = p->header_in->pos;
    }

    if (p->lcf->busy_lock) {
        p->try_busy_lock = 1;

        p->header_in->pos = p->header_in->start;
        p->header_in->last = p->header_in->start;

        p->busy_lock.time = 0;
        p->busy_lock.event = p->request->connection->read;
        p->busy_lock.event_handler = ngx_http_proxy_busy_lock_handler;
        p->busy_lock.md5 = p->cache->ctx.md5;

        ngx_http_proxy_cache_busy_lock(p);
        return NGX_DONE;
    }

    return ngx_http_proxy_request_upstream(p);
}


static int ngx_http_proxy_process_cached_header(ngx_http_proxy_ctx_t *p)
{
    int                      rc, i;
    ngx_table_elt_t         *h;
    ngx_http_request_t      *r;
    ngx_http_proxy_cache_t  *c;

    rc = ngx_http_proxy_parse_status_line(p);

    c = p->cache;
    r = p->request;

    if (rc == NGX_AGAIN) {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "\"proxy_header_buffer_size\" "
                      "is too small to read header from \"%s\"",
                      c->ctx.file.name.data);
        return NGX_ERROR;
    }

    if (rc == NGX_HTTP_PROXY_PARSE_NO_HEADER) {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "no valid HTTP/1.0 header in \"%s\"",
                      c->ctx.file.name.data);
        return NGX_ERROR;
    }

    /* rc == NGX_OK */

    c->status = p->status;
    c->status_line.len = p->status_end - p->status_start;
    c->status_line.data = ngx_palloc(r->pool, c->status_line.len + 1);
    if (c->status_line.data == NULL) {
        return NGX_ERROR;
    }

    /* reset for the possible parsing the upstream header */

    p->status = 0;
    p->status_count = 0;

    ngx_cpystrn(c->status_line.data, p->status_start, c->status_line.len + 1);

    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                   "http cache status %ui \"%V\"", 
                   c->status, &c->status_line);

    /* TODO: ngx_init_table */
    c->headers_in.headers = ngx_create_table(r->pool, 20);

    for ( ;; ) {
        rc = ngx_http_parse_header_line(r, p->header_in);

        if (rc == NGX_OK) {

            /* a header line has been parsed successfully */

            h = ngx_http_add_header(&c->headers_in, ngx_http_proxy_headers_in);
            if (h == NULL) {
                return NGX_ERROR;
            }

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

            h->key.data = ngx_palloc(r->pool,
                                     h->key.len + 1 + h->value.len + 1);
            if (h->key.data == NULL) {
                return NGX_ERROR;
            }

            h->value.data = h->key.data + h->key.len + 1;
            ngx_cpystrn(h->key.data, r->header_name_start, h->key.len + 1);
            ngx_cpystrn(h->value.data, r->header_start, h->value.len + 1);

            for (i = 0; ngx_http_proxy_headers_in[i].name.len != 0; i++) {
                if (ngx_http_proxy_headers_in[i].name.len != h->key.len) {
                    continue;
                }

                if (ngx_strcasecmp(ngx_http_proxy_headers_in[i].name.data,
                                                             h->key.data) == 0)
                {
                    *((ngx_table_elt_t **) ((char *) &c->headers_in
                                   + ngx_http_proxy_headers_in[i].offset)) = h;
                    break;
                }
            }

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

            continue;

        } else if (rc == NGX_HTTP_PARSE_HEADER_DONE) {

            /* a whole header has been parsed successfully */

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

            c->ctx.file_start = p->header_in->pos - p->header_in->start;

            return NGX_OK;

        } else if (rc == NGX_HTTP_PARSE_INVALID_HEADER) {

            ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                          "invalid header in \"%s\"",
                          c->ctx.file.name.data);
            return NGX_ERROR;
        }

        /* rc == NGX_AGAIN || rc == NGX_HTTP_PARSE_TOO_LONG_HEADER */

        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "\"proxy_header_buffer_size\" "
                      "is too small to read header from \"%s\"",
                      c->ctx.file.name.data);
        return NGX_ERROR;
    }
}


void ngx_http_proxy_cache_busy_lock(ngx_http_proxy_ctx_t *p)
{
    int  rc, ft_type;

    rc = ngx_http_busy_lock_cachable(p->lcf->busy_lock, &p->busy_lock,
                                     p->try_busy_lock);

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, p->request->connection->log, 0,
                   "http cache busy lock cachable: %d", rc);

    if (rc == NGX_OK) {
        if (p->try_busy_lock) {
            p->busy_locked = 1;
            p->header_in->pos = p->header_in->start + p->cache->ctx.header_size;
            p->header_in->last = p->header_in->pos;

            ngx_http_proxy_request_upstream(p);
            return;
        }

        ngx_http_proxy_cache_look_complete_request(p);
        return;
    }

    p->try_busy_lock = 0;

    if (p->cache->ctx.file.fd != NGX_INVALID_FILE
        && !p->cache->ctx.file.info_valid)
    {
        if (ngx_fd_info(p->cache->ctx.file.fd, &p->cache->ctx.file.info)
                                                             == NGX_FILE_ERROR)
        {
            ngx_log_error(NGX_LOG_CRIT, p->request->connection->log, ngx_errno,
                          ngx_fd_info_n " \"%s\" failed",
                          p->cache->ctx.file.name.data);
            ngx_http_proxy_finalize_request(p, NGX_HTTP_INTERNAL_SERVER_ERROR);
            return;
        }

        p->cache->ctx.file.info_valid = 1;
    }

    if (rc == NGX_AGAIN) {

        if ((ngx_event_flags & (NGX_USE_CLEAR_EVENT|NGX_USE_KQUEUE_EVENT))
            && !p->request->connection->write->active)
        {
            /*
             * kqueue allows to detect when client closes prematurely
             * connection
             */

            p->request->connection->write->event_handler =
                                        ngx_http_proxy_check_broken_connection;

            if (ngx_add_event(p->request->connection->write, NGX_WRITE_EVENT,
                                                NGX_CLEAR_EVENT) == NGX_ERROR)
            {
                ngx_http_proxy_finalize_request(p,
                                                NGX_HTTP_INTERNAL_SERVER_ERROR);
                return;
            }
        }

        return;
    }

    ngx_http_busy_unlock(p->lcf->busy_lock, &p->busy_lock);

    if (rc == NGX_DONE) {
        ft_type = NGX_HTTP_PROXY_FT_BUSY_LOCK;

    } else {
        /* rc == NGX_ERROR */
        ft_type = NGX_HTTP_PROXY_FT_MAX_WAITING;
    }
    
    if (p->stale && (p->lcf->use_stale & ft_type)) {
        ngx_http_proxy_finalize_request(p,
                                        ngx_http_proxy_send_cached_response(p));
        return;
    }
    
    p->state->status = NGX_HTTP_SERVICE_UNAVAILABLE;
    ngx_http_proxy_finalize_request(p, NGX_HTTP_SERVICE_UNAVAILABLE);
}


static void ngx_http_proxy_cache_look_complete_request(ngx_http_proxy_ctx_t *p)
{
    int                    rc;
    ngx_http_cache_ctx_t  *ctx;

    if (!(ctx = ngx_pcalloc(p->request->pool, sizeof(ngx_http_cache_ctx_t)))) {
        ngx_http_proxy_finalize_request(p, NGX_HTTP_INTERNAL_SERVER_ERROR);
        return;
    }

    *ctx = p->cache->ctx;

    rc = ngx_http_cache_open_file(ctx, ngx_file_uniq(&p->cache->ctx.file.info));

    if (rc == NGX_DECLINED || rc == NGX_HTTP_CACHE_THE_SAME) {
        p->try_busy_lock = 1;
        p->busy_lock.time = 0;
        ngx_http_proxy_cache_busy_lock(p);
        return;
    }

    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, p->request->connection->log, 0,
                   "http cache old fd:%d, new fd:%d",
                   p->cache->ctx.file.fd, ctx->file.fd);

    if (p->cache->ctx.file.fd != NGX_INVALID_FILE) {
        if (ngx_close_file(p->cache->ctx.file.fd) == NGX_FILE_ERROR) {
            ngx_log_error(NGX_LOG_ALERT, p->request->connection->log, ngx_errno,
                          ngx_close_file_n " \"%s\" failed",
                          p->cache->ctx.file.name.data);
        }
    }

    p->cache->ctx = *ctx;

    p->status = 0;
    p->status_count = 0;

    ngx_http_proxy_finalize_request(p,
                                ngx_http_proxy_process_cached_response(p, rc));
}


int ngx_http_proxy_send_cached_response(ngx_http_proxy_ctx_t *p)
{
    int                  rc, len, i;
    off_t                rest;
    ngx_hunk_t          *h0, *h1;
    ngx_chain_t          out[2];
    ngx_http_request_t  *r;

    r = p->request;

    r->headers_out.status = p->cache->status;

#if 0
    r->headers_out.content_length_n = -1;
    r->headers_out.content_length = NULL;
#endif

    /* copy an cached header to r->headers_out */
    
    if (ngx_http_proxy_copy_header(p, &p->cache->headers_in) == NGX_ERROR) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    /* we need to allocate all before the header would be sent */

    len = p->header_in->end - (p->header_in->start + p->cache->ctx.file_start);

    h0 = NULL;
    h1 = NULL;

    if (len) {
        if (!((h0 = ngx_calloc_hunk(r->pool)))) {
            return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }

        if (!((h0->file = ngx_pcalloc(r->pool, sizeof(ngx_file_t))))) {
            return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }
    }

    if (len < p->cache->ctx.length) {
        if (!((h1 = ngx_calloc_hunk(r->pool)))) {
            return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }

        if (!((h1->file = ngx_pcalloc(r->pool, sizeof(ngx_file_t))))) {
            return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }
    }

    rc = ngx_http_send_header(r);

    /* NEEDED ??? */ p->header_sent = 1;

    if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) {
        return rc;
    }

    rest = p->cache->ctx.length;

    if (len) {
        if (p->valid_header_in) {
            h0->pos = p->header_in->start + p->cache->ctx.file_start;

            if (len > p->cache->ctx.length) {
                h0->last = h0->pos + p->cache->ctx.length;

            } else {
                h0->last = p->header_in->end;
            }

            h0->type = NGX_HUNK_IN_MEMORY|NGX_HUNK_TEMP;
        }

        h0->type |= NGX_HUNK_FILE;
        h0->file_pos = p->cache->ctx.file_start;

        h0->file->fd = p->cache->ctx.file.fd;
        h0->file->log = r->connection->log;

        if (len > p->cache->ctx.length) {
            h0->file_last = h0->file_pos + p->cache->ctx.length;
            rest = 0;

        } else {
            h0->file_last = h0->file_pos + len;
            rest -= len;
        }

        out[0].hunk = h0;
        out[0].next = &out[1];
        i = 0;

    } else {
        i = -1;
    }

    if (rest) {
        h1->file_pos = p->cache->ctx.file_start + len;
        h1->file_last = h1->file_pos + rest;
        h1->type = NGX_HUNK_FILE;

        h1->file->fd = p->cache->ctx.file.fd;
        h1->file->log = r->connection->log;

        out[++i].hunk = h1;
    }

    out[i].next = NULL;
    if (!r->main) {
        out[i].hunk->type |= NGX_HUNK_LAST;
    }

    r->file.fd = p->cache->ctx.file.fd;

    return ngx_http_output_filter(r, out);
}


int ngx_http_proxy_is_cachable(ngx_http_proxy_ctx_t *p)
{
    time_t                        date, last_modified, expires, t;
    ngx_http_proxy_headers_in_t  *h;

    switch (p->upstream->status) {
    case NGX_HTTP_OK:
    case NGX_HTTP_MOVED_PERMANENTLY:
    case NGX_HTTP_MOVED_TEMPORARILY:
        break;

#if 0
    case NGX_HTTP_NOT_MODIFIED:
        return 1;
#endif

    default:
        return 0;
    }

    h = &p->upstream->headers_in;

    date = NGX_ERROR;
    if (h->date) {
        date = ngx_http_parse_time(h->date->value.data, h->date->value.len);
    }
    if (date == NGX_ERROR) {
        date = ngx_time();
    }
    p->cache->ctx.date = date;

    last_modified = NGX_ERROR;
    if (h->last_modified) {
        last_modified = ngx_http_parse_time(h->last_modified->value.data,
                                            h->last_modified->value.len);
        p->cache->ctx.last_modified = last_modified;
    }

    if (h->x_accel_expires) {
        expires = ngx_atoi(h->x_accel_expires->value.data,
                           h->x_accel_expires->value.len);
        if (expires != NGX_ERROR) {
            p->state->reason = NGX_HTTP_PROXY_CACHE_XAE;
            p->state->expires = expires;
            p->cache->ctx.expires = date + expires;
            return (expires > 0);
        }
    }

    if (!p->lcf->ignore_expires) {

        /* TODO: Cache-Control: no-cache, max-age= */

        if (h->expires) {
            expires = ngx_http_parse_time(h->expires->value.data,
                                          h->expires->value.len);
            if (expires != NGX_ERROR) {
                p->state->reason = NGX_HTTP_PROXY_CACHE_EXP;
                p->state->expires = expires - date;
                p->cache->ctx.expires = expires;
                return (date < expires);
            }
        }
    }

    if (p->upstream->status == NGX_HTTP_MOVED_PERMANENTLY) {
        p->state->reason = NGX_HTTP_PROXY_CACHE_MVD;
        p->state->expires = /* STUB: 1 hour */ 60 * 60;
        p->cache->ctx.expires = /* STUB: 1 hour */ 60 * 60;
        return 1;
    }

    if (p->upstream->status == NGX_HTTP_MOVED_TEMPORARILY) {
        return 1;
    }

    if (last_modified != NGX_ERROR && p->lcf->lm_factor > 0) {

        /* FIXME: time_t == int_64_t, we can use fpu */ 

        p->state->reason = NGX_HTTP_PROXY_CACHE_LMF;
        t = (time_t)
              ((((int64_t) (date - last_modified)) * p->lcf->lm_factor) / 100);
        p->state->expires = t;
        p->cache->ctx.expires = ngx_time() + t;
        return 1;
    }

    if (p->lcf->default_expires > 0) {
        p->state->reason = NGX_HTTP_PROXY_CACHE_PDE;
        p->state->expires = p->lcf->default_expires;
        p->cache->ctx.expires = ngx_time() + p->lcf->default_expires;
        return 1;
    }

    return 0;
}


int ngx_http_proxy_update_cache(ngx_http_proxy_ctx_t *p)
{
    ngx_event_pipe_t  *ep;

    if (p->cache == NULL) {
        return NGX_OK;
    }

    ep = p->upstream->event_pipe;

    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, p->request->connection->log, 0,
                   "http cache update len: %O:%O",
                   p->cache->ctx.length, ep->read_length);

    if (p->cache->ctx.length == -1) {
        /* TODO: test rc */
        ngx_write_file(&ep->temp_file->file,
                       (char *) &ep->read_length, sizeof(off_t),
                       offsetof(ngx_http_cache_header_t, length));
    }

    return ngx_http_cache_update_file(p->request, &p->cache->ctx,
                                      &ep->temp_file->file.name);
}
