/* Copyright (c) 1995-1999 NEC USA, Inc.  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: proxy.c,v 1.94.2.2.2.9 1999/02/26 00:04:59 wlu Exp $
 */

#include "socks5p.h"
#include "daemon.h"
#include "validate.h"
#include "protocol.h"
#include "msgids.h"
#include "null.h"
#include "info.h"
#include "log.h"
#include "msg.h"

#include "upwd.h"
#include "gss.h"
#include "tcp.h"
#include "udp.h"
#include "tracer.h"
#include "packet.h"

#ifndef ESTABLISH_TIMEOUT
#define ESTABLISH_TIMEOUT 10
#endif
    
#ifndef START_TIMEOUT
#define START_TIMEOUT 60
#endif


#define PROXY_IOFLAGS S5_IOFLAGS_TIMED|S5_IOFLAGS_NBYTES|S5_IOFLAGS_RESTART

static int HandleS4Connection(S5LinkInfo *pri, S5IOInfo *iio, list *auths, double *timerm) {
#ifdef FORK_SOCKD
    dup2(pri->in, 0);
    dup2(pri->in, 1);
    dup2(pri->in, 2);
    close(pri->in);
    
    execlp("sockd", "sockd", NULL);
    return EXIT_ERR;
#else
    char buf[256+256+8], *tmp, resp[] = { SOCKS4_VERSION, SOCKS_FAIL, (char)0xff, (char)0xff, (char)0xff, (char)0xff, (char)0xff, (char)0xff }; 

    S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "Proxy: Acting as a Socks4 server");

    if (auths) {
	for ( ;auths; auths = auths->next) {
	    if (auths->dataint == AUTH_NONE || auths->dataint == AUTH_PASSWD) break;
	}

	if (!auths) {
	    S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, MSGID_SERVER_AUTH_NONE, "Socks%d: No acceptable authentication method found", (int)pri->peerVersion);
	    if (S5IOSend(iio->fd, NULL, resp, sizeof(resp), 0, PROXY_IOFLAGS, timerm) != sizeof(resp)) return EXIT_ERR; 
	    return EXIT_AUTH;
	}
    }
    
    if (S5IORecv(iio->fd, NULL, buf, 8, 0, PROXY_IOFLAGS, timerm) != 8) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(0), 0, "Socks4: Read failed: %m");
	return EXIT_ERR;
    }

    pri->peerAuth     = AUTH_NONE;
    pri->peerVersion  = buf[SP_VERSION];
    pri->peerCommand  = buf[SP_COMMAND];
    if (lsGetProtoAddr(SOCKS4_VERSION, buf, &pri->dstAddr) < 0) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(0), 0, "Socks4: Invalid address passed from client");
	return EXIT_ERR;
    }

    for (tmp = buf, *tmp = '\0'; tmp < buf+sizeof(buf)-1; *++tmp = '\0') { 
	if (S5IORecv(iio->fd, iio, tmp, 1, 0, PROXY_IOFLAGS, timerm) != 1) {
	    S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(0), 0, "Socks4: Read failed: %m");
	    return EXIT_ERR;
	}

	if (*tmp == '\0') break; 
    }

    S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "Socks4: Read user: %s", buf);

    strcpy(pri->srcUser, buf);
    if (lsNullSrvAuth(iio->fd, &iio->auth, pri->srcUser) != AUTH_OK) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, MSGID_SERVER_AUTH_FAILED, "Socks%d: Authentication method %d failed", (int)pri->peerVersion, 0);
	resp[2] = SOCKS_BAD_ID;
	if (S5IOSend(iio->fd, NULL, resp, sizeof(resp), 0, PROXY_IOFLAGS, timerm) != sizeof(resp)) return EXIT_ERR; 
	return EXIT_AUTH;
    }

    /* If we haven't gotten authentication specific read and write functions */
    /* we should set the client functions to be "normal", rather than timed  */
    /* Since in all likelyhood, it will take a long time to read in a whole  */
    /* buffers worth of data (in fact it may never happen)...                */
    if (pri->peerCommand != SOCKS_CONNECT && pri->peerCommand != SOCKS_BIND) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(0), MSGID_SERVER_BAD_COMMAND, "Socks%d: Invalid command: %d", (int)pri->peerVersion, (int)pri->peerCommand);
	S5IOSend(iio->fd, NULL, resp, sizeof(resp), 0, PROXY_IOFLAGS, timerm); 
	return EXIT_ERR;
    }

    S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "Socks4: Done initialization");
    return 0;
#endif
}

static int AuthOK(u_char auth) {
    switch (auth) {	
#ifdef HAVE_LIBGSSAPI_KRB5
	case AUTH_GSSAPI: 
#endif
	case AUTH_PASSWD:   
	case AUTH_NONE:   return 1;
	default:          return 0;
    }
}

static int HandleS5Connection(S5LinkInfo *pri, S5IOInfo *iio, list *auths, double *timerm) {
    char fail[] = { 0x05, (char)0xff }, buf[256+2];
    int ret, i;
    list *tl;

  
    if (S5IORecv(iio->fd, iio, buf, 2, 0, PROXY_IOFLAGS, timerm) != 2) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(0), 0, "Socks5: Read failed: %m");
	return EXIT_ERR;
    }

    if (S5IORecv(iio->fd, iio, buf+2, (u_char)buf[1], 0, PROXY_IOFLAGS, timerm) != (u_char)buf[1]) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(0), 0, "Socks5: Read failed: %m");
	return EXIT_ERR;
    }

    pri->peerAuth = 0xff;
    
    if (!auths) {
	/* Anything is ok, pick the first one the client wanted to do...    */
	for (i = 0; i < ((int)(u_char)buf[1]); i++) if (AuthOK((u_char)buf[i+2])) break;
	if (i != ((int)(u_char)buf[1])) pri->peerAuth = (u_char)(buf[i+2]);
    } else for (tl = auths; tl; tl = tl->next) {
	/* For each method (in order) in the server's config file, See if    */
	/* the client wanted to do this method...And, of course, make sure   */
	/* we can do it...                                                   */
	for (i = 0; i < ((int)(u_char)buf[1]); i++) if (tl->dataint == (int)(u_char)buf[i+2]) break;
	if (i == (int)(u_char)buf[1] || !AuthOK((u_char)buf[i+2])) continue;

	pri->peerAuth = (u_char)buf[i+2];
	break;
    }

    buf[1] = (u_char)pri->peerAuth;

    if (S5IOSend(iio->fd, iio, buf, 2, 0, PROXY_IOFLAGS, timerm) != 2) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(0), 0, "Socks5: Write failed: %m");
	return EXIT_ERR;
    }

    if (pri->peerAuth == 0xff) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, MSGID_SERVER_AUTH_NONE, "Socks%d: No acceptable authentication method found", (int)pri->peerVersion);
	return EXIT_AUTH;
    }
    
    S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "Socks5: Told client to do authentication method #%d", (int)pri->peerAuth);

    switch (pri->peerAuth) {
	case AUTH_PASSWD: ret = lsPasswdSrvAuth(iio->fd, &iio->auth, pri->srcUser); break;
	case AUTH_GSSAPI: ret = lsGssapiSrvAuth(iio->fd, &iio->auth, pri->srcUser); break;
	case AUTH_NONE:   ret = lsNullSrvAuth  (iio->fd, &iio->auth, pri->srcUser); break;
	default:
	    S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, MSGID_SERVER_AUTH_BAD, "Socks%d: Bad Authentication method number: %d", (int)pri->peerVersion, (int)pri->peerAuth);
	    S5IOSend(iio->fd, iio, fail, sizeof(fail), 0, PROXY_IOFLAGS, timerm);
	    ret = AUTH_FAIL;
    }

    if (ret == AUTH_FAIL) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, MSGID_SERVER_AUTH_FAILED, "Socks%d: Authentication method %d failed", (int)pri->peerVersion, (int)pri->peerAuth);
	return EXIT_AUTH;
    } 

    if (lsReadRequest(iio->fd, iio, &pri->dstAddr, &pri->peerVersion, &pri->peerCommand, &pri->peerReserved) < 0) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(0), 0, "Socks5: Read request failed: %m");
	return EXIT_ERR;
    }

    return EXIT_OK;
}

/* HandlePxyConnection will be the routine for Socks5, which handles the     */
/* initial part of the connection.  Specifically, we want to do several      */
/* things here.  We want to check the version, if its not version 5, we can  */
/* either exec the old sockd, or we can call a separate routine to handle    */
/* the connection establishment.  (That routine should read the name, and    */
/* possible make an ident query).  If the version is version 5, we have to   */
/* decide what kind of authentication we are going to do.  This would mean   */
/* reading the number of methods, and then the method numbers themselves,    */
/* after that we pick the one we'd like to do, and send back a reply with    */
/* my version (5), and the method number we picked, or 0xff if we couldn't   */
/* find one that was acceptable given the information we already have (the   */
/* source host.)  We probably want to drop the connection immediately if we  */
/* know there is no acceptable auth from a host, so we don't waste fd's.     */
/* after all this is done, we read the command, then finally do it.  Well,   */
/* if we haven't already quit, that is.                                      */
int HandlePxyConnection(S5IOHandle fd) {
    char buf[] = { SOCKS5_VERSION, SOCKS5_FAIL, 0, SOCKS5_IPV4ADDR, 0, 0, 0, 0, 0, 0 };
    int action, dir, rval = EXIT_ERR, len = sizeof(S5NetAddr), cleaned = 0;
    double timerm = (double)START_TIMEOUT;
    list *auths;

    S5Packet inPacket, outPacket;
    S5CommandInfo ci;
    S5FilterInfo fi;
    S5NetAddr route;
    S5LinkInfo li;
    S5IOInfo iio;

    memset(&li, 0, sizeof(S5LinkInfo));
    memset(&fi, 0, sizeof(S5FilterInfo));
    memset(&ci, 0, sizeof(S5CommandInfo));
    S5BufSetupContext(&iio);
    iio.fd = fd;

    if (getpeername(iio.fd, (ss *)&li.srcAddr, &len) < 0) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(0), 0, "Proxy: getpeername failed: %m");
	goto cleanup;
    }

    len = sizeof(S5NetAddr);

    if (getsockname(iio.fd, (ss *)&li.bndAddr, &len) < 0) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(0), 0, "Proxy: getsockname failed: %m");
	goto cleanup;
    }

    GetName(li.srcName, &li.srcAddr);

    if (!GetRoute(&li.srcAddr, li.srcName, "tcp", &route)) {
    	/* If somehow we got a connection from somewhere on an interface which   */
    	/* we wouldn't use to get to that same place, this is bad (IP SPOOF?),   */
    	/* so we'll quit.                                                        */
    	if (route.sin.sin_addr.s_addr != INADDR_ANY &&
	    route.sin.sin_addr.s_addr != li.bndAddr.sin.sin_addr.s_addr) {
	    S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, MSGID_SERVER_WRONG_ROUTE, "Proxy: Received connection via wrong route");
	    S5LogUpdate(S5LogDefaultHandle, S5_LOG_INFO, MSGID_SERVER_AUTH_FAILED, "Auth Failed: (%s:%d)", li.srcName, (int)ntohs(lsAddr2Port(&li.srcAddr)));
	    goto cleanup;
    	}
    }

    /* If this host was "banned", quit...                                    */
    if ((GetAuths(&li, &auths)) < 0) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, MSGID_SERVER_BANNED_HOST, "Proxy: Received connection from banned host");
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_INFO, MSGID_SERVER_AUTH_FAILED, "Auth Failed: (%s:%d)", li.srcName, (int)ntohs(lsAddr2Port(&li.srcAddr)));
	goto cleanup;
    }
    
    if ((S5IORecv(iio.fd, NULL, (char *)&li.peerVersion, 1, MSG_PEEK, PROXY_IOFLAGS, &timerm)) != 1) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, MSGID_SERVER_RECV_VERSION, "Proxy: Unable to determine version number: %m");
	goto cleanup;
    }

    timerm = (double)ESTABLISH_TIMEOUT;

    switch (li.peerVersion) {
	case SOCKS5_VERSION:
	    rval = HandleS5Connection(&li, &iio, auths, &timerm);
	    break;
        case SOCKS4_VERSION:
	    if (getenv("SOCKS5_V4SUPPORT")) {
		rval = HandleS4Connection(&li, &iio, auths, &timerm);
		break;
	    }
	    /* fall through... */
	default:
	    S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, MSGID_SERVER_BAD_VERSION, "Proxy: Received request with incompatible version number: %d", (int)li.peerVersion);
	    S5IOSend(iio.fd, NULL, buf, sizeof(buf), 0, PROXY_IOFLAGS, &timerm);
	    rval = EXIT_ERR;
    }


    if (rval < 0) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_INFO, MSGID_SERVER_AUTH_FAILED, "Auth Failed: (%s:%d)", li.srcName, (int)ntohs(lsAddr2Port(&li.srcAddr)));
	goto cleanup;
    }

    S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "Proxy: vers:%d cmnd:%d addr:%s port:%d user:%s",
	    (int)li.peerVersion, (int)li.peerCommand, ADDRANDPORT(&li.dstAddr), li.srcUser);

    /* If we haven't gotten authentication specific read and write functions */
    /* we should set the client functions to be "normal", rather than timed  */
    /* Since in all likelyhood, it will take a long time to read in a whole  */
    /* buffers worth of data (in fact it may never happen)...                */
    switch (li.peerCommand) {
 	case SOCKS_PING:    
 	case SOCKS_TRACER:  rval = PTSetup (&iio, &li, &ci);  break;
	case SOCKS_CONNECT: 
	case SOCKS_BIND:    rval = TcpSetup(&iio, &li, &ci); break;
	case SOCKS_UDP:     rval = UdpSetup(&iio, &li, &ci); break;
	default:
	    S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, MSGID_SERVER_BAD_COMMAND, "Proxy: Bad command number %d", (int)li.peerCommand);

 	    lsSendResponse(iio.fd, &iio, &li.dstAddr, li.peerVersion, (li.peerVersion == SOCKS5_VERSION)?SOCKS5_BADCMND:SOCKS_BAD_ID, 0, NULL);
    	    S5BufCleanContext(&iio);
	    rval = -1;
    }

    if (rval < 0) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "Proxy: Command setup failed");
	cleaned = 1;
	goto cleanup;
    }

    PacketPrintSetup(&li, &fi);

    inPacket.data  = NULL;
    outPacket.data = NULL;
    inPacket.oob  = 0;
    outPacket.oob = 0;

    for (action = S5_ACTION_READ, dir = S5_DIRECTION_ANY;; ) {
	switch (action) {
	    case S5_ACTION_READ:
		inPacket.off = 0;
	    case S5_ACTION_MORE_READ:
		/* Need more info...                                         */
		if ((rval = ci.recvpkt(&inPacket, &li, ci.option, &dir)) <= 0) {
		    /* but we had an error...                                */
		    action = S5_ACTION_CLOSE;
		    continue;
		} else {
		    /* Update the byte count on the read not the write?      */
		    if (dir == S5_DIRECTION_IN) {
			li.inbc += rval;
		    } else {
			li.outbc += rval;
		    }

		    break;
		}
	    case S5_ACTION_WRITE:
		/* Just write...                                             */
		if (ci.sendpkt(&outPacket, &li, ci.option, &dir) <= 0) {
		    action = S5_ACTION_CLOSE;
		    continue;
		} else {
		    /* Go back to reading...                                 */
		    dir    = S5_DIRECTION_ANY;
		    action = S5_ACTION_READ;
		    continue;
		}
	    case S5_ACTION_MORE_WRITE:
		if (ci.sendpkt(&outPacket, &li, ci.option, &dir) <= 0) {
		    action = S5_ACTION_CLOSE;
		    continue;
		} else {
		    break;
		}
	    case S5_ACTION_CLOSE:
		/* Any error messages that need to get printed must be done  */
		/* within ci.clean                                           */
		goto cleanup;
	}

	/* Only valid way of getting here is via S5_ACTION_READ              */
	if (fi.filter && !fi.filter(&inPacket, &outPacket, &li, fi.option, &dir, &action)) continue;

	action    = S5_ACTION_WRITE;
	outPacket = inPacket;
    }

  cleanup:

    if (ci.option && ci.clean) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "Proxy: cleaning command context");
	ci.clean(&li, ci.option);
	cleaned = 1;
    }

    if (!cleaned) S5BufCleanContext(&iio);

    if (fi.clean) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "Proxy: cleaning filter context");
	fi.clean(fi.option);
    }

    S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "Proxy: done cleaning up");
    return rval;
}

