/* Copyright (c) 1995,1996,1997 NEC Corporation.  All rights reserved.       */
/*                                                                           */
/* The redistribution, use and modification in source or binary forms of     */
/* this software is subject to the conditions set forth in the copyright     */
/* document ("Copyright") included with this distribution.                   */

/*
 * $Id: tcp.c,v 1.74.2.1.2.3 1998/07/19 22:52:00 wlu Exp $
 */

/* This file has all the function to do tcp proxying itself.  The only one   */
/* that is visible to the outside world should be HandleTcpConnection.       */
#include "socks5p.h"
#include "threads.h"
#include "daemon.h"
#include "validate.h"
#include "protocol.h"
#include "msgids.h"
#include "sident.h"
#include "flow.h"
#include "info.h"
#include "null.h"
#include "log.h"
#include "tcp.h"
#include "msg.h"
#include "s2s.h"

#ifdef DONT_SUPPORT_RCMD
#define doRcmd  0		 /* I will support rcmd's...                 */
#else
#define doRcmd  1		 /* I won't support rcmd's....               */
#endif

#ifndef ACCEPT_TIMEOUT
#define ACCEPT_TIMEOUT 60
#endif

struct tcpinfo {
    S5IOInfo iio, oio;
    char idtentry[IDTENTRY_SIZE];
    int ndests, exitval;
    char *packetbuf;
};

typedef struct tcpinfo TcpInfo;

#define ResvPort(x) ((int)ntohs(lsAddr2Port((x))) < IPPORT_RESERVED && (int)ntohs(lsAddr2Port((x))) >= (IPPORT_RESERVED/2))
#define PortDecr(x) (lsAddrSetPort((x), htons(ntohs(lsAddr2Port((x)))-1)))

/* Send a status message back to the client with socks_err if the client was */
/* a socks server or socks5_err if it was a socks5 server...res is the       */
/* result of the current state of connection...connect host for connect,     */
/* bound host for bind, accepted host for accept...Don't send more than the  */
/* client expects though (1 for connect, 2 for bind/accept...)               */
static int SendDest(S5LinkInfo *pri, TcpInfo *tcpinfo, const S5NetAddr *res, u_char s5error, u_char s4error) {
    S5NetAddr tmp;

    if (tcpinfo->ndests++ >= ((pri->peerCommand==SOCKS_CONNECT)?1:2)) return 0;
    
    if (pri->peerVersion == SOCKS4_VERSION) {
	if (res->sa.sa_family != AF_INET) {
	    memset(&tmp, 0, sizeof(S5NetAddr));
	    tmp.sin.sin_addr.s_addr = INADDR_ANY;
	    tmp.sin.sin_port        = 0;
	    tmp.sin.sin_family      = AF_INET;
	    res = &tmp;
	} 
    }

    if (!lsSendResponse(tcpinfo->iio.fd, &tcpinfo->iio, res, pri->peerVersion, (pri->peerVersion == SOCKS5_VERSION)?s5error:s4error, pri->nextReserved, NULL)) return 0;
    S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, 0, "TCP Sending Response failed");
    return -1;
}

/* Perform the connect command.  Basically connect to the client, send any   */
/* protocol necessary (via RRconnect), get our peer's name, and then send    */
/* that info back to the client...(dumb?)...                                 */
static int TcpConnect(S5LinkInfo *pri, TcpInfo *tcpinfo, u_char *s5ep, u_char *s4ep) {
    int len = sizeof(ssi);
    
    if (pri->nextVersion) {
	if (S5SExchangeProtocol(&tcpinfo->iio, &tcpinfo->oio, pri, tcpinfo->idtentry, &pri->dstAddr, &pri->intAddr) < 0) {
	    S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, 0, "TCP Connect to %s:%d failed: %m", ADDRANDPORT(&pri->dstAddr));
	    *s5ep = (ISSOCKETERROR(ETIMEDOUT))?SOCKS5_TTLEXP:SOCKS5_CONNREF;
	    return EXIT_ERR;
	}
    } else {
	GetRoute(&pri->dstAddr, pri->dstName, "tcp", &pri->intAddr);

    	/* If the client had a reserved port and we support that ability,        */
    	/* reserve a port...                                                     */
        if (doRcmd && ResvPort(&pri->srcAddr)) {
	    S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "TCP Setup: trying to reserve port");
	    lsAddrSetPort(&pri->intAddr, htons(IPPORT_RESERVED-1));

	    for ( ; ResvPort(&pri->intAddr); PortDecr(&pri->intAddr)) {
	    	if (!bind(tcpinfo->oio.fd, &pri->intAddr.sa, lsAddrSize(&pri->intAddr))) break;
	    	if (ISSOCKETERROR(EADDRINUSE)) continue;
	    
	    	S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, 0, "TCP Setup: Failed to get reserved port: %m");
	    	return EXIT_ERR;
	    }
    
	    if (lsAddr2Port(&pri->intAddr) == htons(IPPORT_RESERVED/2-1)) {
	    	S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, 0, "TCP Setup: Failed to get reserved port: %m");
	    	return EXIT_ERR;
	    }
        } else if (bind(tcpinfo->oio.fd, &pri->intAddr.sa, lsAddrSize(&pri->intAddr)) < 0) {
		S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, 0, "TCP Setup: Failed to bind to out interface: %m");
		return EXIT_ERR;
        }

	if (connect(tcpinfo->oio.fd, &pri->dstAddr.sa, lsAddrSize(&pri->dstAddr)) < 0) {
	    S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, 0, "TCP Connect to %s:%d failed: %m", ADDRANDPORT(&pri->dstAddr));
	    return EXIT_ERR;
	}

	MakeIdentEntry(tcpinfo->iio.fd, tcpinfo->oio.fd, pri, tcpinfo->idtentry);

    	if (getsockname(tcpinfo->oio.fd, &pri->intAddr.sa, &len) < 0) {
	    S5LogUpdate(S5LogDefaultHandle, S5_LOG_ERROR, 0, "TCP Getsockname failed: %m");
	    return EXIT_ERR;
        }
    }

    if (SendDest(pri, tcpinfo, &pri->intAddr, SOCKS5_RESULT, SOCKS_RESULT) < 0) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, 0, "TCP SendDest failed: %m");
	return EXIT_NETERR;
    }
    
    S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "TCP out interface %s:%d", ADDRANDPORT(&pri->intAddr));
    return EXIT_OK;
}

static int TcpBind(S5LinkInfo *pri, TcpInfo *tcpinfo, u_char *s5ep, u_char *s4ep) {
    int len = sizeof(struct sockaddr_in), rval;
    u_char errbyte = 0;
    S5NetAddr wtdaddr;
    S5IOHandle fd;
    fd_set fds, b;
    
    if (pri->nextVersion) {
	rval = S5SExchangeProtocol(&tcpinfo->iio, &tcpinfo->oio, pri, tcpinfo->idtentry, &pri->dstAddr, &pri->intAddr);
    } else {
	GetRoute(&pri->dstAddr, pri->dstName, "tcp", &pri->intAddr);
	rval = bind(tcpinfo->oio.fd, &pri->intAddr.sa, lsAddrSize(&pri->intAddr));
    }
    
    if (rval < 0) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_ERROR, 0, "TCP Binding to address %s:%d failed: %m", ADDRANDPORT(&pri->intAddr));
	return EXIT_ERR;
    }

    if (!pri->nextVersion && getsockname(tcpinfo->oio.fd, &pri->intAddr.sa, &len) < 0) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_ERROR, 0, "TCP Getsockname failed: %m");
	return EXIT_ERR;
    }

    if (!pri->nextVersion && listen(tcpinfo->oio.fd, 1) < 0) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_ERROR, 0, "TCP Listen failed: %m");
	return EXIT_ERR;
    }

    S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "TCP out interface %s:%d", ADDRANDPORT(&pri->intAddr));

    if (SendDest(pri, tcpinfo, &pri->intAddr, SOCKS5_RESULT, SOCKS_RESULT) < 0) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, 0, "TCP SendDest failed: %m");
	return EXIT_NETERR;
    }

    FD_ZERO(&b);
    FD_SET(tcpinfo->iio.fd, &b);
    FD_SET(tcpinfo->oio.fd, &b);
    lsAddrCopy(&wtdaddr, &pri->dstAddr, lsAddrSize(&pri->dstAddr));

    /* Do a select here, so we can have a timeout on how long we wait...     */
    /* XXX We should also check to make sure the client doesn't become read  */
    /* ready, that would mean it exitted.                                    */
    for (fds = b; ; fds = b) {
	struct timeval to = { ACCEPT_TIMEOUT, 0 };
	
	switch (select(MAX(tcpinfo->iio.fd, tcpinfo->oio.fd)+1, &fds, NULL, NULL, &to)) {
	    case -1:
		if (ISSOCKETERROR(EINTR)) continue;
		S5LogUpdate(S5LogDefaultHandle, S5_LOG_ERROR, 0, "TCP Accept Select failed: %m");
		return EXIT_ERR;
	    case  0:
		S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, 0, "TCP Accept Timeout expired");
		*s5ep = SOCKS5_TTLEXP;
		return EXIT_ERR;
	}

	break;
    }

    if (FD_ISSET(tcpinfo->iio.fd, &fds) && S5IOCheck(tcpinfo->iio.fd) < 0) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(0), 0, "TCP Client closed connection");
	return EXIT_NETERR;
    }
    
    S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "TCP accepting connection");

    switch (pri->nextVersion) {
	case 0:
	    /* Accept will not restart on a signal interrupt, so we might    */
	    /* have to restart it...                                         */
	    while ((fd = accept(tcpinfo->oio.fd, &pri->dstAddr.sa, &len)) == S5InvalidIOHandle) {
		if (ISSOCKETERROR(EINTR)) continue;
		
		S5LogUpdate(S5LogDefaultHandle, S5_LOG_ERROR, 0, "TCP Accept failed: %m");
		return EXIT_ERR;
	    }

	    CLOSESOCKET(tcpinfo->oio.fd);
	    tcpinfo->oio.fd = fd;
	    break;
	case SOCKS4_VERSION:
	case SOCKS5_VERSION:
	    if (lsReadResponse(tcpinfo->oio.fd, &tcpinfo->oio, &pri->dstAddr, pri->nextVersion, &errbyte, &pri->nextReserved) < 0) {
	        S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "TCP Recieved bad reply from proxy at %s:%d", ADDRANDPORT(&pri->sckAddr));
    		lsAddrCopy(&pri->dstAddr, &wtdaddr, lsAddrSize(&wtdaddr));
	        return EXIT_ERR;
	    }

 	    break;
	default:
	    S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "TCP Unknown next version number: %d", pri->nextVersion);
	    return EXIT_ERR;
    }
    
    GetName(pri->dstName, &pri->dstAddr);
    GetServ(pri->dstServ, pri->dstAddr.sin.sin_port, "tcp");

    /* We need to check first that the accepted address (pri->dstAddr)   */
    /* matches the requested address (wtdaddr).                          */
    if (wtdaddr.sa.sa_family == pri->dstAddr.sa.sa_family && lsAddrAddrComp(&pri->dstAddr, &wtdaddr) != 0) {
        S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(0), MSGID_SERVER_TCP_ACCEPT_AUTH, "TCP Accepted authorization failed for host: %s:%d", pri->dstName, (int)ntohs(lsAddr2Port(&pri->dstAddr)));
        *s5ep = SOCKS5_AUTHORIZE;
        return EXIT_AUTH;
    }

    /* Note that the requested address can be type of S5NAME while the   */
    /* accepted address can be type of INET. If this is the case and     */
    /* we can't resolve the name, we have accept as is...                */
    if (pri->dstAddr.sa.sa_family == AF_INET &&
	    (pri->retAddr.sin.sin_addr.s_addr == INVALIDADDR && pri->retName[0] != '\0')) {
	lsAddrCopy(&wtdaddr, &pri->dstAddr, lsAddrSize(&pri->dstAddr));
	memset((char *)&pri->dstAddr, 0, sizeof(S5NetAddr));

	pri->dstAddr.sn.sn_family = AF_S5NAME;
	strcpy(pri->dstAddr.sn.sn_name, pri->retName);
	lsAddrSetPort(&pri->dstAddr, lsAddr2Port(&wtdaddr));
    }

    if (Authorize(pri, 0) != AUTH_OK) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(0), MSGID_SERVER_TCP_ACCEPT_AUTH, "TCP Accepted authorization failed for host: %s:%d", pri->dstName, (int)ntohs(lsAddr2Port(&pri->dstAddr)));
	*s5ep = SOCKS5_AUTHORIZE;
	return EXIT_AUTH;
    }
    
    if (SendDest(pri, tcpinfo, &pri->dstAddr, SOCKS5_RESULT, SOCKS_RESULT) < 0) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(0), 0, "TCP SendDest failed: %m");
	return EXIT_NETERR;
    }

    /* If this isn't through another socks server, we didn't have the right  */
    /* info before, but we do now.  If it is through another socks server,   */
    /* it is our peer, and hasn't changed since the entry was added before.  */
    if (!pri->nextVersion) {
	MakeIdentEntry(tcpinfo->iio.fd, tcpinfo->oio.fd, pri, tcpinfo->idtentry);
    }

    return EXIT_OK;
}


int TcpFlowRecvMsg(S5Packet *packet, S5LinkInfo *pri, TcpInfo *coption, int *dir) {
    int rv;
    
    if (!coption) return -1;

    rv = S5TcpFlowRecv(&coption->iio, &coption->oio, packet, dir);

    if (rv < 0) coption->exitval = EXIT_ERR;
    else        coption->exitval = EXIT_OK;

    if (packet->data != NULL) coption->packetbuf = packet->data;
    return rv;
}

int TcpFlowSendMsg(S5Packet *packet, S5LinkInfo *pri, TcpInfo *coption, int *dir) {
    int rv;
    
    if (!coption) return -1;

    rv = S5TcpFlowSend(&coption->iio, &coption->oio, packet, dir);

    if (rv < 0) coption->exitval = EXIT_ERR;
    else        coption->exitval = EXIT_OK;

    return rv;
}

int TcpCloseConnection(S5LinkInfo *linkinfo, TcpInfo *coption) {
    if (!coption) return -1;

    S5LogUpdate(S5LogDefaultHandle, S5_LOG_INFO, 0, "TCP Connection Terminated: %s (%s:%d to %s:%s) for user %s: %d bytes out, %d bytes in",
	(coption->exitval == EXIT_ERR)?"Abnormal":"Normal",
	linkinfo->srcName, ntohs(lsAddr2Port(&linkinfo->srcAddr)),
	linkinfo->dstName, linkinfo->dstServ, linkinfo->srcUser,
	linkinfo->outbc,   linkinfo->inbc);

    RemoveIdentEntry(coption->idtentry);

    S5BufCleanContext(&coption->oio);
    S5BufCleanContext(&coption->iio);

    if (coption->packetbuf) free(coption->packetbuf);
    free(coption);

    S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(15), 0, "TCP Close: Cleanup done");
    return 0;
}

int TcpSetup(S5IOInfo *ioinfo, S5LinkInfo *linkinfo, S5CommandInfo *cmdinfo) {
    int turnon = 1, rval = EXIT_ERR;
    u_char s5error = SOCKS5_FAIL, s4error = SOCKS_FAIL;
    TcpInfo *tcpinfo = NULL;

    if (ResolveNames(linkinfo) < 0) {
	s5error = SOCKS5_BADADDR;
	goto cleanup;
    }
    
    /* Allocate space for the tcp specific options we'll be using...         */
    if (!(cmdinfo->option = (void *)(tcpinfo = (TcpInfo *)malloc(sizeof(TcpInfo))))) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, 0, "TCP Setup: Malloc() failed: %m");
	goto cleanup;
    }

    tcpinfo->packetbuf     = NULL;
    tcpinfo->ndests        = 0;
    tcpinfo->iio           = *ioinfo;

    InitIdentEntry(tcpinfo->idtentry);
    S5BufSetupContext(&tcpinfo->oio);

    S5LogUpdate(S5LogDefaultHandle, S5_LOG_INFO, MSGID_SERVER_TCP_START, "TCP Connection Request: %s (%s:%d to %s:%s) for user %s",
	(linkinfo->peerCommand == SOCKS_CONNECT)?"Connect":"Bind",
	linkinfo->srcName, ntohs(lsAddr2Port(&linkinfo->srcAddr)),
	linkinfo->dstName, linkinfo->dstServ, linkinfo->srcUser);

    /* Make sure we're allowed to do this before we start any work on it...   */
    if (Authorize(linkinfo, (linkinfo->peerCommand == SOCKS_BIND)?1:0) != AUTH_OK) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, MSGID_SERVER_TCP_AUTH, "TCP Setup: Authorization failed");
	s5error = SOCKS5_AUTHORIZE;
	rval    = EXIT_AUTH;
	goto cleanup;
    }

    /* Make the socket we'll use for the server side...                      */
    if ((tcpinfo->oio.fd = socket(AF_INET, SOCK_STREAM,0)) == S5InvalidIOHandle) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_ERROR, 0, "TCP Setup: Socket() failed: %m");
	goto cleanup;
    }

    switch (linkinfo->peerCommand) {
	case SOCKS_BIND:
	    if ((rval = TcpBind(linkinfo, tcpinfo, &s5error, &s4error)) != 0) goto cleanup;
	    rval = EXIT_ERR;
	    break; 
	case SOCKS_CONNECT:
	    if ((rval = TcpConnect(linkinfo, tcpinfo, &s5error, &s4error)) != 0) goto cleanup;
	    rval = EXIT_ERR;
	    break; 
	default:            
	    S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, 0, "TCP Setup: Invalid command: %d", (int)linkinfo->peerCommand);
	    s5error = SOCKS5_BADCMND;
	    goto cleanup;
    }

    /* Set out of band data inline, since we won't be dealing with it....    */
    if (setsockopt(tcpinfo->iio.fd, SOL_SOCKET, SO_OOBINLINE, (char *)&turnon, sizeof(int)) < 0) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(15), 0, "TCP Setup: Failed to inline out-of-band data: %m");
	rval = EXIT_NETERR;
	goto cleanup;
    }
    
    if (setsockopt(tcpinfo->oio.fd, SOL_SOCKET, SO_OOBINLINE, (char *)&turnon, sizeof(int)) < 0) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(15), 0, "TCP Setup: Failed to inline out-of-band data: %m");
	goto cleanup;
    }

#ifdef USE_LINGERING
    /* ATM: use SO_LINGER so it won't hang up on client                      */
    /* submitted by Andy McFadden fadden@uts.amdahl.com                      */
    /* snarfed from Socks V4 cstc version, not clear when its necessary.     */
    if (setsockopt(tcpinfo->iio.fd, SOL_SOCKET, SO_LINGER, (char *)&ling, sizeof(struct linger)) < 0) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(15), 0, "TCP Setup: Failed to turn off lingering: %m");
	rval = EXIT_NETERR;
	goto cleanup;
    }
    
    if (setsockopt(tcpinfo->oio.fd, SOL_SOCKET, SO_LINGER, (char *)&ling, sizeof(struct linger)) < 0) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(15), 0, "TCP Setup: Failed to turn off lingering: %m");
	goto cleanup;
    }
#endif
    
    S5LogUpdate(S5LogDefaultHandle, S5_LOG_INFO, MSGID_SERVER_TCP_ESTAB, "TCP Connection Established: %s (%s:%d to %s:%s) for user %s",
	(linkinfo->peerCommand == SOCKS_CONNECT)?"Connect":"Bind",
	linkinfo->srcName, ntohs(lsAddr2Port(&linkinfo->srcAddr)),
	linkinfo->dstName, linkinfo->dstServ, linkinfo->srcUser);

    cmdinfo->recvmsg  = (int (*)(S5Packet *, S5LinkInfo *, void *, int *))TcpFlowRecvMsg;
    cmdinfo->sendmsg  = (int (*)(S5Packet *, S5LinkInfo *, void *, int *))TcpFlowSendMsg;
    cmdinfo->clean    = (int (*)(S5LinkInfo *, void *))TcpCloseConnection;
    return EXIT_OK;

  cleanup:
    if (rval != EXIT_NETERR) lsSendResponse(ioinfo->fd, ioinfo, &linkinfo->dstAddr, linkinfo->peerVersion, (linkinfo->peerVersion == SOCKS5_VERSION)?s5error:s4error, 0, NULL);

    if (tcpinfo != NULL) {
    	tcpinfo->exitval = EXIT_ERR;
    	TcpCloseConnection(linkinfo, tcpinfo);
    } else {
    	S5LogUpdate(S5LogDefaultHandle, S5_LOG_INFO, 0, "TCP Connection Terminated: %s (%s:%d to %s:%s) for user %s: %d bytes out, %d bytes in",
	"Abnormal", linkinfo->srcName, ntohs(lsAddr2Port(&linkinfo->srcAddr)),
	linkinfo->dstName, linkinfo->dstServ, linkinfo->srcUser,
	linkinfo->outbc,   linkinfo->inbc);

    	S5BufCleanContext(ioinfo);
    }

    S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(15), 0, "TCP Setup failed");

    /* Prevent any further problems from being seg faults.                   */
    cmdinfo->option = NULL;
    if (rval == EXIT_OK) rval = EXIT_ERR;
    return rval;
}
