/* 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: tracer.c,v 1.40.4.5 1999/02/03 22:35:50 steve Exp $
 */

#include "socks5p.h"
#include "daemon.h"
#include "protocol.h"
#include "validate.h"
#include "sident.h"
#include "msgids.h"
#include "flow.h"
#include "info.h"
#include "log.h"
#include "msg.h"
#include "s2s.h"

struct ptinfo {
    S5IOInfo iio, oio, pio;
    int stopchild, childpid, exitval, msgsent;
    char tmpmsg[GENERICBUFSIZE], idtentry[IDTENTRY_SIZE];
    char *packetbuf;
};

typedef struct ptinfo PTInfo;

#define CommandName(x) (((x)->peerCommand == SOCKS_PING)?"PING":"Traceroute")

#ifndef REPLYTIMEOUT
#define REPLYTIMEOUT 60
#endif

static int Popen(S5LinkInfo *pri, PTInfo *ptinfo) {
    S5IOHandle pds[2];

#ifndef TROUTEPROG
    if (pri->peerCommand == SOCKS_TRACER) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "%s: No real program to exec", CommandName(pri));
	return -1;
    }
#endif

#ifndef PINGPROG
    if (pri->peerCommand != SOCKS_TRACER) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "%s: No real program to exec", CommandName(pri));
	return -1;
    }
#endif

    if (pipe(pds) < 0) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "%s pipe failed: %m", CommandName(pri));
	return -1;
    }

    if ((ptinfo->childpid = fork()) < 0) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "%s fork failed: %m", CommandName(pri));
	close(pds[0]);
	close(pds[1]);
	return -1;
    }

    S5BufSetupContext(&ptinfo->pio);
    ptinfo->pio.fd        = pds[0];

    if (ptinfo->childpid == 0) {
	close(pds[0]);
	if (dup2(pds[1], STDOUT_FILENO) < 0) _exit(-1);
	if (dup2(pds[1], STDERR_FILENO) < 0) _exit(-1);
	close(pds[1]);

#ifdef TROUTEPROG
	if ((pri->peerCommand == SOCKS_TRACER)) {
	    int verbose = pri->peerReserved & SOCKS5_FLAG_VERBOSE;
	    int noname  = pri->peerReserved & SOCKS5_FLAG_NONAME;
	    
	    if (pri->nextVersion) {
	    	if (noname) execlp(TROUTEPROG, "traceroute", "-n", (verbose?"-v":pri->sckName), verbose?pri->sckName:NULL, NULL);
	    	else        execlp(TROUTEPROG, "traceroute",       (verbose?"-v":pri->sckName), verbose?pri->sckName:NULL, NULL);
	    } else {
	    	if (noname) execlp(TROUTEPROG, "traceroute", "-n", (verbose?"-v":pri->dstName), verbose?pri->dstName:NULL, NULL);
	    	else        execlp(TROUTEPROG, "traceroute",       (verbose?"-v":pri->dstName), verbose?pri->dstName:NULL, NULL);
	    }
	} 
#endif

#ifdef PINGPROG
	if (!(pri->peerCommand == SOCKS_TRACER)) {
	    execlp(PINGPROG, "ping", pri->dstName, NULL);
	}	    
#endif

	_exit(0);
    }

    ptinfo->stopchild  = 1;
    close(pds[1]);

    return 0;
}

static int InitProxy(S5LinkInfo *pri, PTInfo *ptinfo) {
    S5NetAddr junk;

    S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "%s Using proxy (version %d) %s:%d", CommandName(pri), (int)pri->nextVersion, ADDRANDPORT(&pri->sckAddr));

    if ((ptinfo->oio.fd = socket(AF_INET, SOCK_STREAM, 0)) == S5InvalidIOHandle) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_ERROR, 0, "%s socket failed: %m", CommandName(pri));
	return -1;
    }

    if (S5SExchangeProtocol(&ptinfo->iio, &ptinfo->oio, pri, ptinfo->idtentry, &pri->dstAddr, &junk) == 0) {
	return 0;
    }

    CLOSESOCKET(ptinfo->oio.fd);
    ptinfo->oio.fd = S5InvalidIOHandle;
    return -1;
}

static void WaitChild(PTInfo *coption) {
    int wstatus;

    if (!coption->stopchild) return;
#ifdef HAVE_WAITPID
    waitpid(coption->childpid, &wstatus, 0);
#else
    wait4(coption->childpid, &wstatus, 0, NULL);
#endif

    close(coption->pio.fd);
    coption->pio.fd    = S5InvalidIOHandle;
    coption->stopchild = 0;
    coption->childpid  = 0;
}    

static int PTRecvPkt(S5Packet *packet, S5LinkInfo *pri, PTInfo *coption, int *dir) {
    int rv = EXIT_ERR, savedir;

    if (!coption) {
	return EXIT_ERR;
    } else if (coption->pio.fd != S5InvalidIOHandle && !coption->msgsent) {
	packet->data = (char *)malloc(GENERICBUFSIZE * sizeof(char));
	packet->len  = GENERICBUFSIZE;
	packet->off  = 0;
	
	if (packet->data) {
	    strcpy(packet->data, coption->tmpmsg);
	    packet->off = rv = strlen(coption->tmpmsg);
	    *dir = S5_DIRECTION_IN;
	    coption->msgsent = 1;
	} else return EXIT_ERR;
    } else {
	if (coption->pio.fd != S5InvalidIOHandle) {
	    for (;;) {
	        savedir = *dir;
	        rv = S5TcpFlowRecv(&coption->iio, &coption->pio, packet, dir);

	        if (rv == 0 && *dir == S5_DIRECTION_IN) {
	    	    /* The pipe is closed (command done) as just the first part   */
	    	    /* of a 2 part process by closing the pipe and go to the      */
	    	    /* second part to get dome "real" data...                     */
	    	    WaitChild(coption);
	    	    *dir = savedir;
	 	    if (pri->nextVersion) rv = S5TcpFlowRecv(&coption->iio, &coption->oio, packet, dir);
	        } else if (rv > 0 && *dir == S5_DIRECTION_OUT) {
	    	    /* If the client sends anything, it is a message to stop.     */
	    	    if (coption->stopchild) {
			kill(coption->childpid, SIGINT);
	    	    	coption->stopchild = 0;
		    }
	    	    *dir = savedir;
		    packet->off = 0;
		    continue;
	        }
	        break;
	    }
	} else rv = S5TcpFlowRecv(&coption->iio, &coption->oio, packet, dir);
    }

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

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

static int PTSendPkt(S5Packet *packet, S5LinkInfo *pri, PTInfo *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;
}

static int PTClose(S5LinkInfo *linkinfo, PTInfo *coption) {
    if (!coption) return -1;
    
    if (coption->childpid > 0 && coption->stopchild) {
        kill(coption->childpid, SIGINT);
	WaitChild(coption);
    }

    S5LogUpdate(S5LogDefaultHandle, S5_LOG_INFO, MSGID_SERVER_PT_END, "%s Proxy Terminated: %s (%s to %s) for user %s: %d bytes out, %d bytes in",
	CommandName(linkinfo), (coption->exitval == EXIT_ERR)?"Abnormal":"Normal",
	linkinfo->srcName, linkinfo->dstName, linkinfo->srcUser,
	linkinfo->outbc,    linkinfo->inbc);
    
    S5BufCleanContext(&coption->oio);
    S5BufCleanContext(&coption->iio);
    S5BufCleanContext(&coption->pio);

    if (coption->packetbuf) free(coption->packetbuf);
    RemoveIdentEntry(coption->idtentry);
    free(coption);
    return 0;
}

int PTSetup(S5IOInfo *ioinfo, S5LinkInfo *linkinfo, S5CommandInfo *cinfo) {
    int turnon = 1, rval = EXIT_ERR;
    PTInfo *ptinfo = NULL;
    
    if (ResolveNames(linkinfo) < 0) {
	goto cleanup;
    }

    if (!(cinfo->option = (void *)(ptinfo = (PTInfo *)malloc(sizeof(PTInfo))))) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_ERROR, 0, "%s malloc failed", CommandName(linkinfo));
	goto cleanup;
    }

    ptinfo->exitval       = EXIT_OK;
    ptinfo->msgsent       = 0;
    ptinfo->stopchild     = 0;
    ptinfo->childpid      = 0;
    ptinfo->iio           = *ioinfo;
    ptinfo->packetbuf     = NULL;
    InitIdentEntry(ptinfo->idtentry);

    S5BufSetupContext(&ptinfo->oio);
    S5BufSetupContext(&ptinfo->pio);

    S5LogUpdate(S5LogDefaultHandle, S5_LOG_INFO, MSGID_SERVER_PT_START, "%s Proxy Request: (%s to %s) for user %s",
	    CommandName(linkinfo), linkinfo->srcName, linkinfo->dstName, linkinfo->srcUser);

    if (Authorize(linkinfo, 0) != AUTH_OK) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, MSGID_SERVER_PT_AUTH, "%s Authorization failed", CommandName(linkinfo));
	rval = EXIT_AUTH;
	goto cleanup;
    }
    
    if (linkinfo->nextVersion && InitProxy(linkinfo, ptinfo) < 0) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(0), 0, "%s Init Proxy Failed", CommandName(linkinfo));
	goto cleanup;
    }

    if (linkinfo->peerCommand == SOCKS_TRACER) {
	sprintf(ptinfo->tmpmsg, "\nTraceroute to %s...\n", linkinfo->nextVersion?linkinfo->sckName:linkinfo->dstName);
    } else {
	ptinfo->msgsent = 1;
    }

    if (!linkinfo->nextVersion || linkinfo->peerCommand == SOCKS_TRACER) {
	if (Popen(linkinfo, ptinfo) < 0) {
	    if (!linkinfo->nextVersion) {
		S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "%s Failed to open pipe for real command", CommandName(linkinfo));
		goto cleanup;
	    }

	    if (linkinfo->peerCommand == SOCKS_TRACER) {
		strcat(ptinfo->tmpmsg, "Traceroute not available on this host\n");
	    }
	}
    }

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

    S5LogUpdate(S5LogDefaultHandle, S5_LOG_INFO, MSGID_SERVER_PT_ESTAB, "%s Proxy Established: (%s to %s) for user %s",
	    CommandName(linkinfo), linkinfo->srcName, linkinfo->dstName, linkinfo->srcUser);
    if (lsSendResponse(ptinfo->iio.fd, &ptinfo->iio, &linkinfo->dstAddr, linkinfo->peerVersion, 0, 0, NULL) < 0) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "Client closed connection");
	rval = EXIT_NETERR;
	goto cleanup;
    }
    recv_sigio(ioinfo->fd);

    cinfo->recvpkt = (int (*)(S5Packet *, S5LinkInfo *, void *, int *))PTRecvPkt;
    cinfo->sendpkt = (int (*)(S5Packet *, S5LinkInfo *, void *, int *))PTSendPkt;
    cinfo->clean   = (int (*)(S5LinkInfo *, void *))PTClose;
    return EXIT_OK;

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

    if (ptinfo != NULL) {
	ptinfo->exitval = EXIT_ERR;
    	PTClose(linkinfo, ptinfo);
    } else {
    	S5LogUpdate(S5LogDefaultHandle, S5_LOG_INFO, MSGID_SERVER_PT_END, "%s Proxy Terminated: %s (%s to %s) for user %s: %d bytes out, %d bytes in",
        	CommandName(linkinfo), "Abnormal",
        	linkinfo->srcName, linkinfo->dstName, linkinfo->srcUser,
        	linkinfo->outbc,    linkinfo->inbc);

    	S5BufCleanContext(ioinfo);
    }

    cinfo->option = NULL;
    if (rval == EXIT_OK) rval = EXIT_ERR;
    return rval;
}
