
/*
 * $Id: forward.c,v 1.141 2010/03/07 18:13:33 serassio Exp $
 *
 * DEBUG: section 17    Request Forwarding
 * AUTHOR: Duane Wessels
 *
 * SQUID Web Proxy Cache          http://www.squid-cache.org/
 * ----------------------------------------------------------
 *
 *  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.
 *
 */


#include "squid.h"

#if LINUX_NETFILTER
#include <linux/types.h>
#include <linux/netfilter_ipv4.h>
#endif
#if LINUX_TPROXY
#include <linux/netfilter_ipv4/ip_tproxy.h>
#endif

static PSC fwdStartComplete;
static void fwdDispatch(FwdState *);
static void fwdConnectStart(void *);	/* should be same as EVH */
static void fwdStateFree(FwdState * fwdState);
static PF fwdConnectTimeout;
static PF fwdServerClosed;
static PF fwdPeerClosed;
static CNCB fwdConnectDone;
static int fwdCheckRetry(FwdState * fwdState);
static int fwdReforward(FwdState *);
static void fwdRestart(void *);
static void fwdStartFoo(int answer, void *);
static void fwdStartFail(FwdState *);
static void fwdLogReplyStatus(int tries, http_status status);
static OBJH fwdStats;
static STABH fwdAbort;

#define MAX_FWD_STATS_IDX 9
static int FwdReplyCodes[MAX_FWD_STATS_IDX + 1][HTTP_INVALID_HEADER + 1];

#if WIP_FWD_LOG
static void fwdLog(FwdState * fwdState);
static Logfile *logfile = NULL;
#endif

static void
fwdServerFree(FwdServer * fs)
{
    if (fs->peer)
	cbdataUnlock(fs->peer);
    memFree(fs, MEM_FWD_SERVER);
}

static void
fwdStateFree(FwdState * fwdState)
{
    StoreEntry *e = fwdState->entry;
    int sfd;
    debug(17, 3) ("fwdStateFree: %p\n", fwdState);
    assert(e->mem_obj);
#if URL_CHECKSUM_DEBUG
    assert(e->mem_obj->chksum == url_checksum(e->mem_obj->url));
#endif
#if WIP_FWD_LOG
    fwdLog(fwdState);
#endif
    if (e->store_status == STORE_PENDING) {
	storeRequestFailed(e, fwdState->err);
	fwdState->err = NULL;
    }
    if (EBIT_TEST(e->flags, ENTRY_DEFER_READ))
	storeResetDefer(e);
    if (storePendingNClients(e) > 0)
	assert(!EBIT_TEST(e->flags, ENTRY_FWD_HDR_WAIT));
    fwdServersFree(&fwdState->servers);
    requestUnlink(fwdState->request);
    fwdState->request = NULL;
    if (fwdState->err)
	errorStateFree(fwdState->err);
    storeUnregisterAbort(e);
    storeUnlockObject(e);
    fwdState->entry = NULL;
    sfd = fwdState->server_fd;
    if (sfd > -1) {
	comm_remove_close_handler(sfd, fwdServerClosed, fwdState);
	fwdState->server_fd = -1;
	debug(17, 3) ("fwdStateFree: closing FD %d\n", sfd);
	comm_close(sfd);
    }
    cbdataFree(fwdState);
}

static int
fwdCheckRetry(FwdState * fwdState)
{
    if (shutting_down)
	return 0;
    if (fwdState->entry->store_status != STORE_PENDING)
	return 0;
    if (fwdState->entry->mem_obj->inmem_hi > 0)
	return 0;
    if (fwdState->n_tries > 10)
	return 0;
    if (fwdState->origin_tries > 2)
	return 0;
    if (squid_curtime - fwdState->start >= Config.Timeout.forward)
	return 0;
    if (fwdState->flags.dont_retry)
	return 0;
    if (fwdState->request->flags.body_sent)
	return 0;
    return 1;
}

static int
fwdCheckRetriable(FwdState * fwdState)
{
    /* If there is a request body then Squid can only try once
     * even if the method is indempotent
     */
    if (fwdState->request->body_reader)
	return 0;

    /* RFC2616 9.1 Safe and Idempotent Methods */
    switch (fwdState->request->method->code) {
	/* 9.1.1 Safe Methods */
    case METHOD_GET:
    case METHOD_HEAD:
	/* 9.1.2 Indepontent Methods */
    case METHOD_PUT:
    case METHOD_DELETE:
    case METHOD_OPTIONS:
    case METHOD_TRACE:
	break;
    default:
	return 0;
    }

    return 1;
}

static void
fwdServerClosed(int fd, void *data)
{
    FwdState *fwdState = data;
    debug(17, 2) ("fwdServerClosed: FD %d %s\n", fd, storeUrl(fwdState->entry));
    assert(fwdState->server_fd == fd);
    fwdState->server_fd = -1;
    if (EBIT_TEST(fwdState->entry->flags, ENTRY_DEFER_READ))
	storeResetDefer(fwdState->entry);
    if (fwdCheckRetry(fwdState)) {
	int originserver = (fwdState->servers->peer == NULL);
	debug(17, 3) ("fwdServerClosed: re-forwarding (%d tries, %d secs)\n",
	    fwdState->n_tries,
	    (int) (squid_curtime - fwdState->start));
	if (fwdState->servers->next) {
	    /* use next, or cycle if origin server isn't last */
	    FwdServer *fs = fwdState->servers;
	    FwdServer **T, *T2 = NULL;
	    fwdState->servers = fs->next;
	    for (T = &fwdState->servers; *T; T2 = *T, T = &(*T)->next);
	    if (T2 && T2->peer) {
		/* cycle */
		*T = fs;
		fs->next = NULL;
	    } else {
		/* Use next. The last "direct" entry is retried multiple times */
		fwdState->servers = fs->next;
		fwdServerFree(fs);
		originserver = 0;
	    }
	}
	/* use eventAdd to break potential call sequence loops and to slow things down a little */
	eventAdd("fwdRestart", fwdRestart, fwdState, originserver ? 0.05 : 0.005, 0);
	return;
    }
    if (!fwdState->err && shutting_down)
	fwdState->err = errorCon(ERR_SHUTTING_DOWN, HTTP_GATEWAY_TIMEOUT, fwdState->request);
    fwdStateFree(fwdState);
}

#if USE_SSL
static void
fwdNegotiateSSL(int fd, void *data)
{
    FwdState *fwdState = data;
    FwdServer *fs = fwdState->servers;
    SSL *ssl = fd_table[fd].ssl;
    int ret;
    ErrorState *err;
    request_t *request = fwdState->request;

    errno = 0;
    ERR_clear_error();
    if ((ret = SSL_connect(ssl)) <= 0) {
	int ssl_error = SSL_get_error(ssl, ret);
	switch (ssl_error) {
	case SSL_ERROR_WANT_READ:
	    commSetSelect(fd, COMM_SELECT_READ, fwdNegotiateSSL, fwdState, 0);
	    return;
	case SSL_ERROR_WANT_WRITE:
	    commSetSelect(fd, COMM_SELECT_WRITE, fwdNegotiateSSL, fwdState, 0);
	    return;
	default:
	    debug(81, 1) ("fwdNegotiateSSL: Error negotiating SSL connection on FD %d: %s (%d/%d/%d)\n", fd, ERR_error_string(ERR_get_error(), NULL), ssl_error, ret, errno);
	    err = errorCon(ERR_CONNECT_FAIL, HTTP_BAD_GATEWAY, request);
#ifdef EPROTO
	    err->xerrno = EPROTO;
#else
	    err->xerrno = EACCES;
#endif
	    fwdFail(fwdState, err);
	    if (fs->peer) {
		peerConnectFailed(fs->peer);
		fs->peer->stats.conn_open--;
	    }
	    comm_close(fd);
	    return;
	}
    }
    if (fs->peer && !SSL_session_reused(ssl)) {
	if (fs->peer->sslSession)
	    SSL_SESSION_free(fs->peer->sslSession);
	fs->peer->sslSession = SSL_get1_session(ssl);
    }
#if NOT_YET
    if (verify_domain) {
	char *host;
	STACK_OF(GENERAL_NAME) * altnames;
	if (fs->peer) {
	    if (fs->peer->ssldomain)
		host = fs->peer->ssldomain;
	    else
		host = fs->peer->host;
	} else {
	    host = fs->request->host;
	}
	if (!ssl_verify_domain(host, ssl)) {
	    debug(17, 1) ("Warning: SSL certificate does not match host name '%s'\n", host);
	}
    }
#endif
    fwdDispatch(fwdState);
}

static void
fwdInitiateSSL(FwdState * fwdState)
{
    FwdServer *fs = fwdState->servers;
    int fd = fwdState->server_fd;
    SSL *ssl;
    SSL_CTX *sslContext = NULL;
    peer *peer = fs->peer;
    if (peer) {
	assert(peer->use_ssl);
	sslContext = peer->sslContext;
    } else {
	sslContext = Config.ssl_client.sslContext;
    }
    assert(sslContext);
    if ((ssl = SSL_new(sslContext)) == NULL) {
	ErrorState *err;
	debug(83, 1) ("fwdInitiateSSL: Error allocating handle: %s\n",
	    ERR_error_string(ERR_get_error(), NULL));
	err = errorCon(ERR_SOCKET_FAILURE, HTTP_INTERNAL_SERVER_ERROR, fwdState->request);
	err->xerrno = errno;
	fwdFail(fwdState, err);
	fwdStateFree(fwdState);
	return;
    }
    SSL_set_fd(ssl, fd);
    if (peer) {
	if (peer->sslSession)
	    SSL_set_session(ssl, peer->sslSession);
    }
    fd_table[fd].ssl = ssl;
    fd_table[fd].read_method = &ssl_read_method;
    fd_table[fd].write_method = &ssl_write_method;
    fd_note(fd, "Negotiating SSL");
    fwdNegotiateSSL(fd, fwdState);
}
#endif

static void
fwdConnectDone(int server_fd, int status, void *data)
{
    FwdState *fwdState = data;
    FwdServer *fs = fwdState->servers;
    ErrorState *err;
    request_t *request = fwdState->request;
    assert(fwdState->server_fd == server_fd);
    if (Config.onoff.log_ip_on_direct && status != COMM_ERR_DNS && fs->code == HIER_DIRECT)
	hierarchyNote(&fwdState->request->hier, fs->code, fd_table[server_fd].ipaddr);
    if (status == COMM_ERR_DNS) {
	/*
	 * Only set the dont_retry flag if the DNS lookup fails on
	 * a direct connection.  If DNS lookup fails when trying
	 * a neighbor cache, we may want to retry another option.
	 *
	 * If this is a transparent connection, we will retry using the client's
	 * DNS lookup
	 */
	if ((NULL == fs->peer) && !fwdState->request->flags.transparent)
	    fwdState->flags.dont_retry = 1;
	debug(17, 4) ("fwdConnectDone: Unknown host: %s\n",
	    request->host);
	err = errorCon(ERR_DNS_FAIL, HTTP_GATEWAY_TIMEOUT, fwdState->request);
	err->dnsserver_msg = xstrdup(dns_error_message);
	fwdFail(fwdState, err);
	comm_close(server_fd);
    } else if (status != COMM_OK) {
	assert(fs);
	err = errorCon(ERR_CONNECT_FAIL, HTTP_GATEWAY_TIMEOUT, fwdState->request);
	err->xerrno = errno;
	fwdFail(fwdState, err);
	if (fs->peer)
	    peerConnectFailed(fs->peer);
	comm_close(server_fd);
    } else {
	debug(17, 3) ("fwdConnectDone: FD %d: '%s'\n", server_fd, storeUrl(fwdState->entry));
#if USE_SSL
	if ((fs->peer && fs->peer->use_ssl) ||
	    (!fs->peer && request->protocol == PROTO_HTTPS)) {
	    fwdInitiateSSL(fwdState);
	    return;
	}
#endif
	fwdDispatch(fwdState);
    }
}

static void
fwdConnectTimeout(int fd, void *data)
{
    FwdState *fwdState = data;
    StoreEntry *entry = fwdState->entry;
    FwdServer *fs = fwdState->servers;
    ErrorState *err;
    debug(17, 2) ("fwdConnectTimeout: FD %d: '%s'\n", fd, storeUrl(entry));
    assert(fd == fwdState->server_fd);
    if (Config.onoff.log_ip_on_direct && fs->code == HIER_DIRECT && fd_table[fd].ipaddr[0])
	hierarchyNote(&fwdState->request->hier, fs->code, fd_table[fd].ipaddr);
    if (entry->mem_obj->inmem_hi == 0) {
	err = errorCon(ERR_CONNECT_FAIL, HTTP_GATEWAY_TIMEOUT, fwdState->request);
	err->xerrno = ETIMEDOUT;
	fwdFail(fwdState, err);
	/*
	 * This marks the peer DOWN ... 
	 */
	if (fwdState->servers)
	    if (fwdState->servers->peer)
		peerConnectFailed(fwdState->servers->peer);
    }
    comm_close(fd);
}

typedef struct {
    peer *peer;
    char *domain;
} idleconn;

static void
free_idleconn(void *data)
{
    idleconn *idle = data;
    safe_free(idle->domain)
}

CBDATA_TYPE(idleconn);

static void
fwdConnectIdleDone(int fd, int status, void *data)
{
    idleconn *idle = data;
    idle->peer->stats.idle_opening--;
    if (fd >= 0)
	pconnPush(fd, idle->peer->name, idle->peer->http_port, idle->domain, NULL, 0);
    cbdataFree(idle);
}

static void
fwdConnectIdleTimeout(int fd, void *data)
{
    idleconn *idle = data;
    debug(17, 2) ("fwdConnectIdleTimeout: FD %d: '%s'\n", fd, idle->peer->name);
    idle->peer->stats.idle_opening--;
    comm_close(fd);
    cbdataFree(idle);
}

static void
openIdleConn(peer * peer, const char *domain, struct in_addr outgoing, unsigned short tos, int ctimeout)
{
    int fd = comm_openex(SOCK_STREAM,
	IPPROTO_TCP,
	outgoing,
	0,
	COMM_NONBLOCKING,
	tos,
	peer->name);
    idleconn *idle;
    if (fd < 0) {
	debug(50, 4) ("openIdleConn: %s\n", xstrerror());
	return;;
    }
    CBDATA_INIT_TYPE_FREECB(idleconn, free_idleconn);
    idle = cbdataAlloc(idleconn);
    idle->peer = peer;
    cbdataLock(idle->peer);
    idle->domain = xstrdup(domain);
    /*
     * stats.conn_open is used to account for the number of
     * connections that we have open to the peer, so we can limit
     * based on the max-conn option.  We need to increment here,
     * even if the connection may fail.
     */
    peer->stats.conn_open++;
    peer->stats.idle_opening++;
    comm_add_close_handler(fd, fwdPeerClosed, peer);
    commSetTimeout(fd,
	ctimeout,
	fwdConnectIdleTimeout,
	idle);
    commConnectStart(fd, peer->host, peer->http_port, fwdConnectIdleDone, idle, NULL);
}

static struct in_addr
aclMapAddr(acl_address * head, aclCheck_t * ch)
{
    acl_address *l;
    struct in_addr addr;
    aclChecklistCacheInit(ch);
    for (l = head; l; l = l->next) {
	if (aclMatchAclList(l->acl_list, ch))
	    return l->addr;
    }
    addr.s_addr = INADDR_ANY;
    return addr;
}

static int
aclMapTOS(acl_tos * head, aclCheck_t * ch)
{
    acl_tos *l;
    aclChecklistCacheInit(ch);
    for (l = head; l; l = l->next) {
	if (aclMatchAclList(l->acl_list, ch))
	    return l->tos;
    }
    return 0;
}

struct in_addr
getOutgoingAddr(request_t * request)
{
    aclCheck_t ch;
    memset(&ch, '\0', sizeof(aclCheck_t));
    if (request) {
	ch.request = request;
    }
    return aclMapAddr(Config.accessList.outgoing_address, &ch);
}

unsigned long
getOutgoingTOS(request_t * request)
{
    aclCheck_t ch;
    memset(&ch, '\0', sizeof(aclCheck_t));
    if (request) {
	ch.request = request;
    }
    return aclMapTOS(Config.accessList.outgoing_tos, &ch);
}

static void
fwdConnectStart(void *data)
{
    FwdState *fwdState = data;
    const char *url = storeUrl(fwdState->entry);
    int fd = -1;
    ErrorState *err;
    FwdServer *fs = fwdState->servers;
    const char *host;
    const char *name;
    unsigned short port;
    const char *domain = NULL;
    int ctimeout;
    int ftimeout = Config.Timeout.forward - (squid_curtime - fwdState->start);
    struct in_addr outgoing;
    unsigned short tos;
#if LINUX_TPROXY
    struct in_tproxy itp;
#endif
    int idle = -1;

    assert(fs);
    assert(fwdState->server_fd == -1);
    debug(17, 3) ("fwdConnectStart: %s\n", url);
    if (fs->peer) {
	host = fs->peer->host;
	name = fs->peer->name;
	port = fs->peer->http_port;
	if (fs->peer->options.originserver)
	    domain = fwdState->request->host;
	else
	    domain = "*";
	ctimeout = fs->peer->connect_timeout > 0 ? fs->peer->connect_timeout
	    : Config.Timeout.peer_connect;
    } else {
	host = name = fwdState->request->host;
	port = fwdState->request->port;
	ctimeout = Config.Timeout.connect;
    }
    if (ftimeout < 0)
	ftimeout = 5;
    if (ftimeout < ctimeout)
	ctimeout = ftimeout;
    fwdState->request->flags.pinned = 0;
    if (fs->code == PINNED) {
	int auth;
	fd = clientGetPinnedConnection(fwdState->request->pinned_connection, fwdState->request, fs->peer, &auth);
	if (fd >= 0) {
#if 0
	    if (!fs->peer)
		fs->code = HIER_DIRECT;
#endif
	    fwdState->server_fd = fd;
	    fwdState->n_tries++;
	    fwdState->request->flags.pinned = 1;
	    if (auth)
		fwdState->request->flags.auth = 1;
	    comm_add_close_handler(fd, fwdServerClosed, fwdState);
	    fwdConnectDone(fd, COMM_OK, fwdState);
	    return;
	}
	/* Failure. Fall back on next path */
	cbdataUnlock(fwdState->request->pinned_connection);
	fwdState->request->pinned_connection = NULL;
	fwdState->servers = fs->next;
	fwdServerFree(fs);
	fwdRestart(fwdState);
	return;
    }
#if LINUX_TPROXY
    if (fd == -1 && fwdState->request->flags.tproxy)
	fd = pconnPop(name, port, domain, &fwdState->request->client_addr, 0, NULL);
#endif
    if (fd == -1) {
	fd = pconnPop(name, port, domain, NULL, 0, &idle);
    }
    if (fd != -1) {
	/* Don't cache if the returned fd does not have valid DNS */
	if (fd_table[fd].flags.dnsfailed)
	    storeRelease(fwdState->entry);
	if (fwdCheckRetriable(fwdState)) {
	    debug(17, 3) ("fwdConnectStart: reusing pconn FD %d\n", fd);
	    fwdState->server_fd = fd;
	    fwdState->n_tries++;
	    if (!fs->peer)
		fwdState->origin_tries++;
	    comm_add_close_handler(fd, fwdServerClosed, fwdState);
	    if (fs->peer)
		hierarchyNote(&fwdState->request->hier, fs->code, fs->peer->name);
	    else if (Config.onoff.log_ip_on_direct && fs->code == HIER_DIRECT)
		hierarchyNote(&fwdState->request->hier, fs->code, fd_table[fd].ipaddr);
	    else
		hierarchyNote(&fwdState->request->hier, fs->code, name);
	    if (fs->peer && idle >= 0 && idle < fs->peer->idle) {
		debug(17, 3) ("fwdConnectStart: Opening idle connetions for '%s'\n",
		    fs->peer->name);
		outgoing = getOutgoingAddr(fwdState->request);
		tos = getOutgoingTOS(fwdState->request);
		debug(17, 3) ("fwdConnectStart: got addr %s, tos %d\n",
		    inet_ntoa(outgoing), tos);
		idle += fs->peer->stats.idle_opening;
		while (idle < fs->peer->idle) {
		    openIdleConn(fs->peer, domain, outgoing, tos, ctimeout);
		    idle++;
		}
	    }
	    fwdDispatch(fwdState);
	    return;
	} else {
	    /* Discard the persistent connection to not cause
	     * a imbalance in number of conenctions open if there
	     * is a lot of POST requests
	     */
	    comm_close(fd);
	}
    }
#if URL_CHECKSUM_DEBUG
    assert(fwdState->entry->mem_obj->chksum == url_checksum(url));
#endif
    outgoing = getOutgoingAddr(fwdState->request);
    tos = getOutgoingTOS(fwdState->request);

    fwdState->request->out_ip = outgoing;

    debug(17, 3) ("fwdConnectStart: got addr %s, tos %d\n",
	inet_ntoa(outgoing), tos);
    fd = comm_openex(SOCK_STREAM,
	IPPROTO_TCP,
	outgoing,
	0,
	COMM_NONBLOCKING,
	tos,
	url);
    if (fd < 0) {
	debug(50, 4) ("fwdConnectStart: %s\n", xstrerror());
	err = errorCon(ERR_SOCKET_FAILURE, HTTP_INTERNAL_SERVER_ERROR, fwdState->request);
	err->xerrno = errno;
	fwdFail(fwdState, err);
	fwdStateFree(fwdState);
	return;
    }
    fwdState->server_fd = fd;
    fwdState->n_tries++;
    if (!fs->peer)
	fwdState->origin_tries++;
    /*
     * stats.conn_open is used to account for the number of
     * connections that we have open to the peer, so we can limit
     * based on the max-conn option.  We need to increment here,
     * even if the connection may fail.
     */
    if (fs->peer) {
	fs->peer->stats.conn_open++;
	comm_add_close_handler(fd, fwdPeerClosed, fs->peer);
    }
    comm_add_close_handler(fd, fwdServerClosed, fwdState);
    commSetTimeout(fd,
	ctimeout,
	fwdConnectTimeout,
	fwdState);
    if (fs->peer) {
	hierarchyNote(&fwdState->request->hier, fs->code, fs->peer->name);
    } else {
#if LINUX_TPROXY
	if (fwdState->request->flags.tproxy) {

	    itp.v.addr.faddr.s_addr = fwdState->src.sin_addr.s_addr;
	    itp.v.addr.fport = 0;

	    /* If these syscalls fail then we just fallback to connecting
	     * normally by simply ignoring the errors...
	     */
	    itp.op = TPROXY_ASSIGN;
	    if (setsockopt(fd, SOL_IP, IP_TPROXY, &itp, sizeof(itp)) == -1) {
		debug(20, 1) ("tproxy ip=%s,0x%x,port=%d ERROR ASSIGN\n",
		    inet_ntoa(itp.v.addr.faddr),
		    itp.v.addr.faddr.s_addr,
		    itp.v.addr.fport);
	    } else {
		itp.op = TPROXY_FLAGS;
		itp.v.flags = ITP_CONNECT;
		if (setsockopt(fd, SOL_IP, IP_TPROXY, &itp, sizeof(itp)) == -1) {
		    debug(20, 1) ("tproxy ip=%x,port=%d ERROR CONNECT\n",
			itp.v.addr.faddr.s_addr,
			itp.v.addr.fport);
		}
	    }
	}
#endif
	hierarchyNote(&fwdState->request->hier, fs->code, fwdState->request->host);
    }

    /*
     * If we are retrying a transparent connection that is not being sent to a
     * peer, then don't cache, and use the IP that the client's DNS lookup
     * returned
     */
    if (fwdState->request->flags.transparent && (fwdState->n_tries > 1) && (NULL == fs->peer)) {
	storeRelease(fwdState->entry);
	commConnectStart(fd, host, port, fwdConnectDone, fwdState, &fwdState->request->my_addr);
    } else {
	commConnectStart(fd, host, port, fwdConnectDone, fwdState, NULL);
    }
}

static void
fwdRestart(void *data)
{
    FwdState *fwdState = data;
    if (fwdState->servers)
	fwdConnectStart(fwdState);
    else
	fwdStartFail(fwdState);
}

static void
fwdStartComplete(FwdServer * servers, void *data)
{
    FwdState *fwdState = data;
    debug(17, 3) ("fwdStartComplete: %s\n", storeUrl(fwdState->entry));
    if (servers != NULL) {
	fwdState->servers = servers;
	fwdConnectStart(fwdState);
    } else {
	fwdStartFail(fwdState);
    }
}

static void
fwdStartFail(FwdState * fwdState)
{
    ErrorState *err;
    debug(17, 3) ("fwdStartFail: %s\n", storeUrl(fwdState->entry));
    err = errorCon(ERR_CANNOT_FORWARD, HTTP_GATEWAY_TIMEOUT, fwdState->request);
    err->xerrno = errno;
    fwdFail(fwdState, err);
    fwdStateFree(fwdState);
}

static void
fwdDispatch(FwdState * fwdState)
{
    peer *p = NULL;
    request_t *request = fwdState->request;
    StoreEntry *entry = fwdState->entry;
    ErrorState *err;
    int server_fd = fwdState->server_fd;
    FwdServer *fs = fwdState->servers;
    debug(17, 3) ("fwdDispatch: FD %d: Fetching '%s %s'\n",
	fwdState->client_fd, request->method->string, storeUrl(entry));
    /*
     * Assert that server_fd is set.  This is to guarantee that fwdState
     * is attached to something and will be deallocated when server_fd
     * is closed.
     */
    assert(server_fd > -1);
    /*assert(!EBIT_TEST(entry->flags, ENTRY_DISPATCHED)); */
    assert(entry->ping_status != PING_WAITING);
    assert(entry->lock_count);
    EBIT_SET(entry->flags, ENTRY_DISPATCHED);
    fd_note(server_fd, storeUrl(fwdState->entry));
    fd_table[server_fd].uses++;
    if (fd_table[server_fd].uses == 1 && fs->peer)
	peerConnectSucceded(fs->peer);
    fwdState->request->out_ip = fd_table[server_fd].local_addr;
    netdbPingSite(request->host);
    entry->mem_obj->refresh_timestamp = squid_curtime;
    if (fwdState->servers && (p = fwdState->servers->peer)) {
	p->stats.fetches++;
	fwdState->request->peer_login = p->login;
	fwdState->request->peer_domain = p->domain;
	httpStart(fwdState);
    } else {
	fwdState->request->peer_login = NULL;
	fwdState->request->peer_domain = NULL;
	switch (request->protocol) {
#if USE_SSL
	case PROTO_HTTPS:
	    httpStart(fwdState);
	    break;
#endif
	case PROTO_HTTP:
	    httpStart(fwdState);
	    break;
	case PROTO_GOPHER:
	    gopherStart(fwdState);
	    break;
	case PROTO_FTP:
	    ftpStart(fwdState);
	    break;
	case PROTO_CACHEOBJ:
	case PROTO_INTERNAL:
	case PROTO_URN:
	    fatal_dump("Should never get here");
	    break;
	case PROTO_WHOIS:
	    whoisStart(fwdState);
	    break;
	case PROTO_WAIS:	/* not implemented */
	default:
	    debug(17, 1) ("fwdDispatch: Cannot retrieve '%s'\n",
		storeUrl(entry));
	    err = errorCon(ERR_UNSUP_REQ, HTTP_BAD_REQUEST, fwdState->request);
	    fwdFail(fwdState, err);
	    /*
	     * Force a persistent connection to be closed because
	     * some Netscape browsers have a bug that sends CONNECT
	     * requests as GET's over persistent connections.
	     */
	    request->flags.proxy_keepalive = 0;
	    /*
	     * Set the dont_retry flag becuase this is not a
	     * transient (network) error; its a bug.
	     */
	    fwdState->flags.dont_retry = 1;
	    comm_close(fwdState->server_fd);
	    break;
	}
    }
}

static int
fwdReforward(FwdState * fwdState)
{
    StoreEntry *e = fwdState->entry;
    FwdServer *fs = fwdState->servers;
    http_status s;
    assert(e->store_status == STORE_PENDING);
    assert(e->mem_obj);
#if URL_CHECKSUM_DEBUG
    assert(e->mem_obj->chksum == url_checksum(e->mem_obj->url));
#endif
    debug(17, 3) ("fwdReforward: %s?\n", storeUrl(e));
    if (!EBIT_TEST(e->flags, ENTRY_FWD_HDR_WAIT)) {
	debug(17, 3) ("fwdReforward: No, ENTRY_FWD_HDR_WAIT isn't set\n");
	return 0;
    }
    if (fwdState->request->flags.pinned) {
	debug(17, 3) ("fwdReforward: No, Connection oriented authentication is used\n");
	return 0;
    }
    if (fwdState->n_tries >= Config.forward_max_tries)
	return 0;
    if (fwdState->origin_tries > 1)
	return 0;
    if (fwdState->request->flags.body_sent)
	return 0;
    assert(fs);
    fwdState->servers = fs->next;
    fwdServerFree(fs);
    if (fwdState->servers == NULL) {
	debug(17, 3) ("fwdReforward: No forward-servers left\n");
	return 0;
    }
    s = e->mem_obj->reply->sline.status;
    debug(17, 3) ("fwdReforward: status %d\n", (int) s);
    return fwdReforwardableStatus(s);
}

/* PUBLIC FUNCTIONS */

void
fwdServersFree(FwdServer ** FS)
{
    FwdServer *fs;
    while ((fs = *FS)) {
	*FS = fs->next;
	fwdServerFree(fs);
    }
}

void
fwdStartPeer(peer * p, StoreEntry * e, request_t * r)
{
    FwdState *fwdState;
    FwdServer *peer = NULL;
    debug(17, 3) ("fwdStartPeer: '%s'\n", storeUrl(e));
    e->mem_obj->request = requestLink(r);
#if URL_CHECKSUM_DEBUG
    assert(e->mem_obj->chksum == url_checksum(e->mem_obj->url));
#endif
    fwdState = cbdataAlloc(FwdState);
    fwdState->entry = e;
    fwdState->client_fd = -1;
    fwdState->server_fd = -1;
    fwdState->request = requestLink(r);
    fwdState->start = squid_curtime;
    fwdState->orig_entry_flags = e->flags;
    storeLockObject(e);
    if (!fwdState->request->flags.pinned)
	EBIT_SET(e->flags, ENTRY_FWD_HDR_WAIT);
    storeRegisterAbort(e, fwdAbort, fwdState);
    peerAddFwdServer(&peer, p, HIER_DIRECT);
    fwdStartComplete(peer, fwdState);
}

void
fwdStart(int fd, StoreEntry * e, request_t * r)
{
    aclCheck_t *ch;
    FwdState *fwdState;
    fwdState = cbdataAlloc(FwdState);
    fwdState->client_fd = fd;
    fwdState->entry = e;
    fwdState->request = r;	/* XXX: requestLink? */
    /*
     * client_addr == no_addr indicates this is an "internal" request
     * from peer_digest.c, asn.c, netdb.c, etc and should always
     * be allowed.  yuck, I know.
     */
    if (r->client_addr.s_addr != no_addr.s_addr && r->protocol != PROTO_INTERNAL && r->protocol != PROTO_CACHEOBJ) {
	/*
	 * Check if this host is allowed to fetch MISSES from us (miss_access)
	 */
	ch = aclChecklistCreate(Config.accessList.miss, r, NULL);
	aclNBCheck(ch, fwdStartFoo, fwdState);
    } else {
	fwdStartFoo(1, fwdState);
    }
}

void
fwdStartFoo(int answer, void *data)
{
    FwdState *fwdState = data;
    int fd = fwdState->client_fd;
    request_t *r = fwdState->request;
    StoreEntry *e = fwdState->entry;
    ErrorState *err;
    if (answer == 0) {
	err_type page_id;
	cbdataFree(fwdState);
	page_id = aclGetDenyInfoPage(&Config.denyInfoList, AclMatchedName, 1);
	if (page_id == ERR_NONE)
	    page_id = ERR_FORWARDING_DENIED;
	err = errorCon(page_id, HTTP_FORBIDDEN, r);
	errorAppendEntry(e, err);
	return;
    }
    debug(17, 3) ("fwdStart: '%s'\n", storeUrl(e));
    if (!e->mem_obj->request)
	e->mem_obj->request = requestLink(r);
#if URL_CHECKSUM_DEBUG
    assert(e->mem_obj->chksum == url_checksum(e->mem_obj->url));
#endif
    if (shutting_down) {
	/* more yuck */
	err = errorCon(ERR_SHUTTING_DOWN, HTTP_GATEWAY_TIMEOUT, r);
	errorAppendEntry(e, err);
	cbdataFree(fwdState);
	return;
    }
    switch (r->protocol) {
	/*
	 * Note, don't create fwdState for these requests
	 */
    case PROTO_INTERNAL:
	cbdataFree(fwdState);
	internalStart(r, e);
	return;
    case PROTO_CACHEOBJ:
	cbdataFree(fwdState);
	cachemgrStart(fd, r, e);
	return;
    case PROTO_URN:
	cbdataFree(fwdState);
	urnStart(r, e);
	return;
    default:
	break;
    }
    fwdState->server_fd = -1;
    fwdState->request = requestLink(r);
    fwdState->start = squid_curtime;
    fwdState->orig_entry_flags = e->flags;

#if LINUX_TPROXY
    /* If we need to transparently proxy the request
     * then we need the client source address and port */
    fwdState->src.sin_family = AF_INET;
    fwdState->src.sin_addr = r->client_addr;
    fwdState->src.sin_port = r->client_port;
#endif

    storeLockObject(e);
    if (!fwdState->request->flags.pinned)
	EBIT_SET(e->flags, ENTRY_FWD_HDR_WAIT);
    storeRegisterAbort(e, fwdAbort, fwdState);
    peerSelect(r, e, fwdStartComplete, fwdState);
}

int
fwdCheckDeferRead(int fd, void *data)
{
    StoreEntry *e = data;
    MemObject *mem = e->mem_obj;
    int rc = 0;
    if (mem == NULL)
	return 0;
#if URL_CHECKSUM_DEBUG
    assert(e->mem_obj->chksum == url_checksum(e->mem_obj->url));
#endif
#if DELAY_POOLS
    if (fd < 0)
	(void) 0;
    else if (delayIsNoDelay(fd))
	(void) 0;
    else {
	int i = delayMostBytesWanted(mem, INT_MAX);
	if (0 == i) {
	    /* No storeDeferRead here as it's not store dependent.
	     * Will get changed the day delay pools interact nicely
	     * with the store..
	     */
	    commDeferFD(fd);
	    return 1;
	}
	/* was: rc = -(rc != INT_MAX); */
	else if (INT_MAX == i)
	    rc = 0;
	else
	    rc = -1;
    }
#endif
    if (EBIT_TEST(e->flags, ENTRY_DEFER_READ)) {
	commDeferFD(mem->serverfd);
	return 1;
    }
    if (EBIT_TEST(e->flags, ENTRY_FWD_HDR_WAIT))
	return rc;
    if (EBIT_TEST(e->flags, RELEASE_REQUEST)) {
	/* Just a small safety cap to defer storing more data into the object
	 * if there already is way too much. This handles the case when there
	 * is disk clients pending on a too large object being fetched and a
	 * few other corner cases.
	 */
	if (fd >= 0 && mem->inmem_hi - mem->inmem_lo > SM_PAGE_SIZE + Config.Store.maxInMemObjSize + Config.readAheadGap) {
	    storeDeferRead(e, fd);
	    return 1;
	}
    }
    if (fd >= 0 && mem->inmem_hi - storeLowestMemReaderOffset(e) > Config.readAheadGap) {
	storeDeferRead(e, fd);
	return 1;
    }
    return rc;
}

void
fwdFail(FwdState * fwdState, ErrorState * errorState)
{
    debug(17, 3) ("fwdFail: %s \"%s\"\n\t%s\n",
	err_type_str[errorState->type],
	httpStatusString(errorState->http_status),
	storeUrl(fwdState->entry));
    if (fwdState->err)
	errorStateFree(fwdState->err);
    fwdState->err = errorState;
    if (!errorState->request)
	errorState->request = requestLink(fwdState->request);
}

/*
 * Called when someone else calls StoreAbort() on this entry
 */
static void
fwdAbort(void *data)
{
    FwdState *fwdState = data;
    debug(17, 2) ("fwdAbort: %s\n", storeUrl(fwdState->entry));
    fwdStateFree(fwdState);
}

/*
 * Accounts for closed persistent connections
 */
static void
fwdPeerClosed(int fd, void *data)
{
    peer *p = data;
    p->stats.conn_open--;
}

/*
 * Frees fwdState without closing FD or generating an abort
 */
void
fwdUnregister(int fd, FwdState * fwdState)
{
    debug(17, 3) ("fwdUnregister: %s\n", storeUrl(fwdState->entry));
    assert(fd == fwdState->server_fd);
    assert(fd > -1);
    commSetDefer(fd, NULL, NULL);
    if (EBIT_TEST(fwdState->entry->flags, ENTRY_DEFER_READ))
	storeResumeRead(fwdState->entry);
    comm_remove_close_handler(fd, fwdServerClosed, fwdState);
    fwdState->server_fd = -1;
}

/*
 * server-side modules call fwdComplete() when they are done
 * downloading an object.  Then, we either 1) re-forward the
 * request somewhere else if needed, or 2) call storeComplete()
 * to finish it off
 */
void
fwdComplete(FwdState * fwdState)
{
    StoreEntry *e = fwdState->entry;
    assert(e->store_status == STORE_PENDING);
    debug(17, 3) ("fwdComplete: %s\n\tstatus %d\n", storeUrl(e),
	e->mem_obj->reply->sline.status);
#if URL_CHECKSUM_DEBUG
    assert(e->mem_obj->chksum == url_checksum(e->mem_obj->url));
#endif
    fwdLogReplyStatus(fwdState->n_tries, e->mem_obj->reply->sline.status);
    if (fwdReforward(fwdState)) {
	debug(17, 3) ("fwdComplete: re-forwarding %d %s\n",
	    e->mem_obj->reply->sline.status,
	    storeUrl(e));
	if (fwdState->server_fd > -1)
	    fwdUnregister(fwdState->server_fd, fwdState);
	storeEntryReset(e);
	/* HACK WARNING: This fiddling with the flags really
	 * should be done in the store layer, but current
	 * design does not allow it to be done proper in a
	 * sane manner.
	 * A sign that we have pushed the design of everything
	 * going via a single StoreEntry per request a bit too far.
	 */
	if (EBIT_TEST(e->flags, ENTRY_NEGCACHED)) {
	    storeSetPrivateKey(e);
	    EBIT_CLR(e->flags, ENTRY_NEGCACHED);
	    EBIT_CLR(e->flags, KEY_EARLY_PUBLIC);
	}
	if (EBIT_TEST(e->flags, RELEASE_REQUEST)) {
	    EBIT_CLR(e->flags, RELEASE_REQUEST);
	    EBIT_CLR(e->flags, KEY_EARLY_PUBLIC);
	}
	if (EBIT_TEST(fwdState->orig_entry_flags, ENTRY_CACHABLE))
	    EBIT_SET(e->flags, ENTRY_CACHABLE);
	fwdStartComplete(fwdState->servers, fwdState);
    } else {
	debug(17, 3) ("fwdComplete: not re-forwarding status %d\n",
	    e->mem_obj->reply->sline.status);
	EBIT_CLR(e->flags, ENTRY_FWD_HDR_WAIT);
	storeComplete(e);
	/*
	 * If fwdState isn't associated with a server FD, it
	 * won't get freed unless we do it here.
	 */
	if (fwdState->server_fd < 0)
	    fwdStateFree(fwdState);
    }
}

void
fwdInit(void)
{
    cachemgrRegister("forward",
	"Request Forwarding Statistics",
	fwdStats, 0, 1);
#if WIP_FWD_LOG
    if (logfile)
	(void) 0;
    else if (NULL == Config.Log.forward)
	(void) 0;
    else
	logfile = logfileOpen(Config.Log.forward, 0, 1);
#endif
}

static void
fwdLogReplyStatus(int tries, http_status status)
{
    if (status > HTTP_INVALID_HEADER)
	return;
    assert(tries);
    tries--;
    if (tries > MAX_FWD_STATS_IDX)
	tries = MAX_FWD_STATS_IDX;
    FwdReplyCodes[tries][status]++;
}

static void
fwdStats(StoreEntry * s)
{
    int i;
    int j;
    storeAppendPrintf(s, "Status");
    for (j = 0; j <= MAX_FWD_STATS_IDX; j++) {
	storeAppendPrintf(s, "\ttry#%d", j + 1);
    }
    storeAppendPrintf(s, "\n");
    for (i = 0; i <= (int) HTTP_INVALID_HEADER; i++) {
	if (FwdReplyCodes[0][i] == 0)
	    continue;
	storeAppendPrintf(s, "%3d", i);
	for (j = 0; j <= MAX_FWD_STATS_IDX; j++) {
	    storeAppendPrintf(s, "\t%d", FwdReplyCodes[j][i]);
	}
	storeAppendPrintf(s, "\n");
    }
}

int
fwdReforwardableStatus(http_status s)
{
    switch (s) {
    case HTTP_BAD_GATEWAY:
    case HTTP_GATEWAY_TIMEOUT:
	return 1;
    case HTTP_FORBIDDEN:
    case HTTP_INTERNAL_SERVER_ERROR:
    case HTTP_NOT_IMPLEMENTED:
    case HTTP_SERVICE_UNAVAILABLE:
	return Config.retry.onerror;
    default:
	return 0;
    }
    /* NOTREACHED */
}

#if WIP_FWD_LOG
void
fwdUninit(void)
{
    if (NULL == logfile)
	return;
    logfileClose(logfile);
    logfile = NULL;
}

void
fwdLogRotate(void)
{
    if (logfile)
	logfileRotate(logfile);
}

static void
fwdLog(FwdState * fwdState)
{
    if (NULL == logfile)
	return;
    logfilePrintf(logfile, "%9d.%03d %03d %s %s\n",
	(int) current_time.tv_sec,
	(int) current_time.tv_usec / 1000,
	fwdState->last_status, fwdState->request->method->string,
	fwdState->request->canonical);
}

void
fwdStatus(FwdState * fwdState, http_status s)
{
    fwdState->last_status = s;
}

#endif
