
/*
 * $Id: refresh_check.c,v 1.2.2.1 2008/05/04 23:23:13 hno Exp $
 *
 * DEBUG: section 84    Refresh Check Helper
 * AUTHOR: Henrik Nordstrom
 *
 * SQUID Web Proxy Cache          http://www.squid-cache.org/
 * ----------------------------------------------------------
 *
 *  The contents of this file is Copyright (C) 2007 by Henrik Nordstrom,
 *  Sweden, unless otherwise is indicated in the specific function. The
 *  author gives his full permission to include this file into the Squid
 *  software product under the terms of the GNU General Public License as
 *  published by the Free Software Foundation; either version 2 of the
 *  License, or (at your option) any later version.
 *
 *  Squid is the result of efforts by numerous individuals from
 *  the Internet community; see the CONTRIBUTORS file for full
 *  details.   Many organizations have provided support for Squid's
 *  development; see the SPONSORS file for full details.  Squid is
 *  Copyrighted (C) 2001 by the Regents of the University of
 *  California; see the COPYRIGHT file for full details.  Squid
 *  incorporates software developed and/or copyrighted by other
 *  sources; see the CREDITS file for full details.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA.
 *
 * Partially based on the External ACL implementation.
 */

#include "squid.h"

#ifndef DEFAULT_REFRESH_CHECK_CHILDREN
#define DEFAULT_REFRESH_CHECK_CHILDREN 5
#endif

/******************************************************************
 * external_refresh_check parser
 */
typedef struct _refresh_check_format refresh_check_format;

struct _refresh_check_helper {
    refresh_check_format *format;
    wordlist *cmdline;
    int children;
    int concurrency;
    helper *helper;
    dlink_list queue;
};

struct _refresh_check_format {
    enum {
	REFRESH_CHECK_UNKNOWN,
	REFRESH_CHECK_URI,
	REFRESH_CHECK_RESP_HEADER,
	REFRESH_CHECK_RESP_HEADER_MEMBER,
	REFRESH_CHECK_RESP_HEADER_ID,
	REFRESH_CHECK_RESP_HEADER_ID_MEMBER,
	REFRESH_CHECK_AGE,
	REFRESH_CHECK_END
    } type;
    refresh_check_format *next;
    char *header;
    char *member;
    char separator;
    http_hdr_type header_id;
};

/* FIXME: These are not really cbdata, but it is an easy way
 * to get them pooled, refcounted, accounted and freed properly...
 */
CBDATA_TYPE(refresh_check_helper);
CBDATA_TYPE(refresh_check_format);

static void
free_refresh_check_format(void *data)
{
    refresh_check_format *p = data;
    safe_free(p->header);
}

static void
free_refresh_check_helper(void *data)
{
    refresh_check_helper *p = data;
    while (p->format) {
	refresh_check_format *f = p->format;
	p->format = f->next;
	cbdataFree(f);
    }
    wordlistDestroy(&p->cmdline);
    if (p->helper) {
	helperShutdown(p->helper);
	helperFree(p->helper);
	p->helper = NULL;
    }
}

void
parse_refreshCheckHelper(refresh_check_helper ** ptr)
{
    refresh_check_helper *a;
    char *token;
    refresh_check_format **p;

    if (*ptr)
	self_destruct();

    CBDATA_INIT_TYPE_FREECB(refresh_check_helper, free_refresh_check_helper);
    CBDATA_INIT_TYPE_FREECB(refresh_check_format, free_refresh_check_format);

    a = cbdataAlloc(refresh_check_helper);

    a->children = DEFAULT_REFRESH_CHECK_CHILDREN;

    /* Parse options */
    while ((token = strtok(NULL, w_space)) != NULL) {
	if (strncmp(token, "children=", 9) == 0) {
	    a->children = atoi(token + 9);
	} else if (strncmp(token, "concurrency=", 12) == 0) {
	    a->concurrency = atoi(token + 12);
	} else {
	    break;
	}
    }

    /* Parse format */
    p = &a->format;
    while (token) {
	refresh_check_format *format;

	/* stop on first non-format token found */
	if (*token != '%')
	    break;

	format = cbdataAlloc(refresh_check_format);

	if (strncmp(token, "%RES{", 5) == 0) {
	    /* header format */
	    char *header, *member, *end;
	    header = token + 5;
	    end = strchr(header, '}');
	    /* cut away the terminating } */
	    if (end && strlen(end) == 1)
		*end = '\0';
	    else
		self_destruct();

	    member = strchr(header, ':');
	    if (member) {
		/* Split in header and member */
		*member++ = '\0';
		if (!xisalnum(*member))
		    format->separator = *member++;
		else
		    format->separator = ',';
		format->member = xstrdup(member);
		format->type = REFRESH_CHECK_RESP_HEADER_MEMBER;
	    } else {
		format->type = REFRESH_CHECK_RESP_HEADER;
	    }
	    format->header = xstrdup(header);
	    format->header_id = httpHeaderIdByNameDef(header, strlen(header));
	    if (format->header_id != -1) {
		if (member)
		    format->type = REFRESH_CHECK_RESP_HEADER_ID_MEMBER;
		else
		    format->type = REFRESH_CHECK_RESP_HEADER_ID;
	    }
	} else if (strcmp(token, "%URI") == 0)
	    format->type = REFRESH_CHECK_URI;
	else if (strcmp(token, "%URL") == 0)
	    format->type = REFRESH_CHECK_URI;
	else if (strcmp(token, "%CACHE_URI") == 0)
	    format->type = REFRESH_CHECK_URI;
	else if (strcmp(token, "%AGE") == 0)
	    format->type = REFRESH_CHECK_AGE;
	else {
	    self_destruct();
	}
	*p = format;
	p = &format->next;
	token = strtok(NULL, w_space);
    }
    /* There must be at least one format token */
    if (!a->format)
	self_destruct();

    /* helper */
    if (!token)
	self_destruct();
    wordlistAdd(&a->cmdline, token);

    /* arguments */
    parse_wordlist(&a->cmdline);

    *ptr = a;
}

void
dump_refreshCheckHelper(StoreEntry * sentry, const char *name, const refresh_check_helper * list)
{
    const refresh_check_helper *node = list;
    const refresh_check_format *format;
    const wordlist *word;
    if (node) {
	storeAppendPrintf(sentry, "%s", name);
	if (node->children != DEFAULT_REFRESH_CHECK_CHILDREN)
	    storeAppendPrintf(sentry, " children=%d", node->children);
	if (node->concurrency)
	    storeAppendPrintf(sentry, " concurrency=%d", node->concurrency);
	for (format = node->format; format; format = format->next) {
	    switch (format->type) {
	    case REFRESH_CHECK_RESP_HEADER:
	    case REFRESH_CHECK_RESP_HEADER_ID:
		storeAppendPrintf(sentry, " %%{%s}", format->header);
		break;
	    case REFRESH_CHECK_RESP_HEADER_MEMBER:
	    case REFRESH_CHECK_RESP_HEADER_ID_MEMBER:
		storeAppendPrintf(sentry, " %%{%s:%s}", format->header, format->member);
		break;
#define DUMP_REFRESH_CHECK_TYPE(a) \
	    case REFRESH_CHECK_##a: \
		storeAppendPrintf(sentry, " %%%s", #a); \
		break
		DUMP_REFRESH_CHECK_TYPE(URI);
		DUMP_REFRESH_CHECK_TYPE(AGE);
	    case REFRESH_CHECK_UNKNOWN:
	    case REFRESH_CHECK_END:
		fatal("unknown refresh_check format error");
		break;
	    }
	}
	for (word = node->cmdline; word; word = word->next)
	    storeAppendPrintf(sentry, " %s", word->key);
	storeAppendPrintf(sentry, "\n");
    }
}

void
free_refreshCheckHelper(refresh_check_helper ** list)
{
    while (*list) {
	refresh_check_helper *node = *list;
	*list = NULL;
	cbdataFree(node);
    }
}

/******************************************************************
 * refresh_check runtime
 */

static inline int
refreshCheckOverload(refresh_check_helper * def)
{
    return def->helper->stats.queue_size > def->helper->n_running;
}

static char *
makeRefreshCheckRequest(StoreEntry * entry, refresh_check_format * format)
{
    static MemBuf mb = MemBufNULL;
    int first = 1;
    HttpReply *reply;
    String sb = StringNull;

    if (!entry->mem_obj)
	return NULL;

    reply = entry->mem_obj->reply;
    memBufReset(&mb);
    for (; format; format = format->next) {
	char buf[256];
	const char *str = NULL;
	const char *quoted;
	switch (format->type) {
	case REFRESH_CHECK_URI:
	    str = entry->mem_obj->url;
	    break;
	case REFRESH_CHECK_AGE:
	    snprintf(buf, sizeof(buf), "%ld", (long int) (squid_curtime - entry->timestamp));
	    str = buf;
	    break;
	case REFRESH_CHECK_RESP_HEADER:
	    sb = httpHeaderGetByName(&reply->header, format->header);
	    str = strBuf(sb);
	    break;
	case REFRESH_CHECK_RESP_HEADER_ID:
	    sb = httpHeaderGetStrOrList(&reply->header, format->header_id);
	    str = strBuf(sb);
	    break;
	case REFRESH_CHECK_RESP_HEADER_MEMBER:
	    sb = httpHeaderGetByNameListMember(&reply->header, format->header, format->member, format->separator);
	    str = strBuf(sb);
	    break;
	case REFRESH_CHECK_RESP_HEADER_ID_MEMBER:
	    sb = httpHeaderGetListMember(&reply->header, format->header_id, format->member, format->separator);
	    str = strBuf(sb);
	    break;

	case REFRESH_CHECK_UNKNOWN:
	case REFRESH_CHECK_END:
	    fatal("unknown refresh_check_program format error");
	    break;
	}
	if (!str || !*str)
	    str = "-";
	if (!first)
	    memBufAppend(&mb, " ", 1);
	quoted = rfc1738_escape(str);
	memBufAppend(&mb, quoted, strlen(quoted));
	stringClean(&sb);
	first = 0;
    }
    return mb.buf;
}

/******************************************************************
 * refresh_check requests
 */

typedef struct _refreshCheckState refreshCheckState;
struct _refreshCheckState {
    REFRESHCHECK *callback;
    void *callback_data;
    StoreEntry *entry;
    refresh_check_helper *def;
    dlink_node list;
    refreshCheckState *queue;
};

CBDATA_TYPE(refreshCheckState);
static void
free_refreshCheckState(void *data)
{
    refreshCheckState *state = data;
    storeUnlockObject(state->entry);
    cbdataUnlock(state->callback_data);
    cbdataUnlock(state->def);
}

/*
 * The helper program receives queries on stdin, one
 * per line, and must return the result on on stdout
 */
static void
refreshCheckHandleReply(void *data, char *reply)
{
    refreshCheckState *state = data;
    refreshCheckState *next;
    int freshness = -1;
    char *log = NULL;
    MemBuf hdrs = MemBufNULL;


    debug(84, 2) ("refreshCheckHandleReply: reply=\"%s\"\n", reply);

    if (reply) {
	char *t = NULL;
	char *token = strwordtok(reply, &t);
	if (token && strcmp(token, "FRESH") == 0)
	    freshness = 0;
	else if (token && strcmp(token, "OK") == 0)
	    freshness = 0;

	while ((token = strwordtok(NULL, &t))) {
	    char *value = strchr(token, '=');
	    if (value) {
		*value++ = '\0';	/* terminate the token, and move up to the value */
		rfc1738_unescape(value);
		if (strcmp(token, "freshness") == 0)
		    freshness = atoi(value);
		else if (strcmp(token, "log") == 0)
		    log = value;
		else if (strncmp(token, "res{", 4) == 0) {
		    char *header, *t;
		    header = token + 4;
		    t = strrchr(header, '}');
		    if (!t)
			continue;
		    *t = '\0';
		    if (!hdrs.buf)
			memBufDefInit(&hdrs);
		    memBufPrintf(&hdrs, "%s: %s\r\n", header, value);
		}
	    }
	}
    }
    if (freshness >= 0) {
	if (hdrs.size) {
	    HttpReply *rep = httpReplyCreate();
	    httpHeaderParse(&rep->header, hdrs.buf, hdrs.buf + hdrs.size);
	    httpReplyUpdateOnNotModified(state->entry->mem_obj->reply, rep);
	    storeTimestampsSet(state->entry);
	    if (!httpHeaderHas(&rep->header, HDR_DATE)) {
		state->entry->timestamp = squid_curtime;
		state->entry->expires = squid_curtime + freshness;
	    } else if (freshness) {
		state->entry->expires = squid_curtime + freshness;
	    }
	    httpReplyDestroy(rep);
	    storeUpdate(state->entry, NULL);
	} else {
	    state->entry->timestamp = squid_curtime;
	    state->entry->expires = squid_curtime + freshness;
	}
    }
    if (hdrs.buf)
	memBufClean(&hdrs);
    dlinkDelete(&state->list, &state->def->queue);
    do {
	cbdataUnlock(state->def);
	state->def = NULL;

	if (state->callback && cbdataValid(state->callback_data))
	    state->callback(state->callback_data, freshness >= 0, log);
	cbdataUnlock(state->callback_data);
	state->callback_data = NULL;

	next = state->queue;
	cbdataFree(state);
	state = next;
    } while (state);
}

void
refreshCheckSubmit(StoreEntry * entry, REFRESHCHECK * callback, void *callback_data)
{
    MemBuf buf;
    const char *key;
    refresh_check_helper *def = Config.Program.refresh_check;
    refreshCheckState *state;
    dlink_node *node;
    refreshCheckState *oldstate = NULL;

    if (!def) {
	callback(callback_data, 0, NULL);
	return;
    }
    key = makeRefreshCheckRequest(entry, def->format);
    if (!key) {
	callback(callback_data, 0, NULL);
	return;
    }
    debug(84, 2) ("refreshCheckSubmit: for '%s'\n", key);

    /* Check for a pending lookup to hook into */
    for (node = def->queue.head; node; node = node->next) {
	refreshCheckState *oldstatetmp = node->data;
	if (entry == oldstatetmp->entry) {
	    oldstate = oldstatetmp;
	    break;
	}
    }

    state = cbdataAlloc(refreshCheckState);
    state->def = def;
    cbdataLock(state->def);
    state->entry = entry;
    storeLockObject(entry);
    state->callback = callback;
    state->callback_data = callback_data;
    cbdataLock(state->callback_data);
    if (oldstate) {
	/* Hook into pending lookup */
	state->queue = oldstate->queue;
	oldstate->queue = state;
    } else {
	/* No pending lookup found. Sumbit to helper */
	/* Check for queue overload */
	if (refreshCheckOverload(def)) {
	    debug(84, 1) ("refreshCheckSubmit: queue overload\n");
	    cbdataFree(state);
	    callback(callback_data, 0, "Overload");
	    return;
	}
	/* Send it off to the helper */
	memBufDefInit(&buf);
	memBufPrintf(&buf, "%s\n", key);
	helperSubmit(def->helper, buf.buf, refreshCheckHandleReply, state);
	dlinkAdd(state, &state->list, &def->queue);
	memBufClean(&buf);
    }
}

static void
refreshCheckStats(StoreEntry * sentry)
{
    refresh_check_helper *p = Config.Program.refresh_check;

    if (p) {
	storeAppendPrintf(sentry, "Refresh Check helper statistics:\n");
	helperStats(sentry, p->helper);
	storeAppendPrintf(sentry, "\n");
    }
}
void
refreshCheckConfigure(void)
{
    refresh_check_helper *p = Config.Program.refresh_check;
    if (p) {
	requirePathnameExists("external_refresh_check", p->cmdline->key);
    }
}
void
refreshCheckInit(void)
{
    static int firstTimeInit = 1;
    refresh_check_helper *p = Config.Program.refresh_check;

    if (p) {
	if (!p->helper)
	    p->helper = helperCreate("external_refresh_check");
	p->helper->cmdline = p->cmdline;
	p->helper->n_to_start = p->children;
	p->helper->concurrency = p->concurrency;
	p->helper->ipc_type = IPC_STREAM;
	helperOpenServers(p->helper);
	if (firstTimeInit) {
	    firstTimeInit = 0;
	    cachemgrRegister("refresh_check",
		"External ACL stats",
		refreshCheckStats, 0, 1);
	}
	CBDATA_INIT_TYPE_FREECB(refreshCheckState, free_refreshCheckState);
    }
}

void
refreshCheckShutdown(void)
{
    refresh_check_helper *p = Config.Program.refresh_check;
    if (p) {
	helperShutdown(p->helper);
    }
}
