/* 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: msg.c,v 1.30.2.1.2.5 1999/04/14 14:54:29 wlu Exp $
 */

/* This file contains functions support reliable message transport under any */
/* (UDP or TCP) protocol.                                                    */
#include "socks5p.h"
#include "buffer.h"
#include "block.h"
#include "msg.h"
#include "log.h"

int S5IORecv(S5IOHandle fd, S5IOInfo *info, char *buf, int size, int ioflags, int libflags, double *timerm) {
    int nr, sval, rlen = size;
    struct timeval sv, *svpt;
    fd_set fds, b;
    
#ifdef STRICT_TIMEOUT
    struct timeval ts, te;
#endif

    if (libflags & S5_IOFLAGS_TIMED && !timerm) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, 0, "S5IORecv: NULL timeout pointer passed");
	SETSOCKETERROR(ETIMEDOUT);
	return -1;
    }
    
    if ((libflags & S5_IOFLAGS_NBYTES) && !(libflags & S5_IOFLAGS_RESTART)) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, 0, "S5IORecv: Warning: Cannot reliably read n bytes and not handle restarts");
    }
    
    /* We may have read in some stuff and buffered it.  Rather than replace  */
    /* select, lets just read the stuff *out* of the buffer first...         */
    while (1) {
	if ((sval = S5BufCheckData(fd, info)) < 0) return -1;
	if (sval == 0) break;

	if ((nr = S5BufReadPacket(fd, info, buf, rlen, ioflags)) < 0) return -1;
	if (nr == 0) return (size - rlen);

	rlen -= nr; buf += nr;
	if (!(libflags & S5_IOFLAGS_NBYTES)) return nr;
	if (rlen == 0) return size;
    }

    /* Read again and again until we have an error, we run out of time, or   */
    /* we just read all that we wanted to.  If STRICT_TIMEOUT is defined,    */
    /* subtract the amount of time we've spent at each point form the time   */
    /* we are allowing *total*...This is kind of expensive...An alternative, */
    /* which has nearly the same consequences is is to spend that amount of  */
    /* time *each* read.  If the data is too interspersed, it will die...    */
    FD_ZERO(&b);
    FD_SET(fd, &b);
    
    for (fds = b; rlen > 0; fds = b) {
	if (libflags & S5_IOFLAGS_TIMED) {
	    sv.tv_sec  = (int)*timerm;
	    sv.tv_usec = (int)((*timerm - (double)sv.tv_sec) * 1000000.0);
	
#ifdef STRICT_TIMEOUT
	    if (gettimeofday(&ts, NULL) < 0) {
		S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, 0, "S5IORecv: gettimeofday failed: %m");
		SETSOCKETERROR(ETIMEDOUT);
		goto interrupted;
	    }
#endif
	} else {
	    sv.tv_sec = 0;
	    sv.tv_usec = 0;
	}
	
	if (libflags & S5_IOFLAGS_TIMED) svpt = &sv;
	else if (ISNBLOCK(fd) && !(libflags & S5_IOFLAGS_NBYTES)) svpt = &sv;
	else svpt = NULL;

	if ((sval = REAL(select)(fd + 1, &fds, NULL, NULL, svpt)) == 0) {
	    S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(0), 0, "S5IORecv: select failed: Timed out");
	    if (libflags & S5_IOFLAGS_TIMED) SETSOCKETERROR(ETIMEDOUT);
#if defined(sun) && !defined(__svr4__)
            else SETSOCKETERROR(EWOULDBLOCK);
#else
            else SETSOCKETERROR(EAGAIN);
#endif
	    sval = -1;
	    goto interrupted;
	}
	
#ifdef STRICT_TIMEOUT
	if (libflags & S5_IOFLAGS_TIMED) {
	    if (gettimeofday(&te, NULL) < 0) {
		S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, 0, "S5IORecv: gettimeofday failed: %m");
		SETSOCKETERROR(ETIMEDOUT);
	        sval = -1;
		goto interrupted;
	    }
	
	    *timerm -= (te.tv_sec  - ts.tv_sec);
	    *timerm -= (te.tv_usec - ts.tv_usec)/1000000.0;
	    if (*timerm < 0.0) *timerm = 0.0;
	}
#endif
	
	if (sval < 0) {
	    if (ISSOCKETERROR(EINTR) && libflags & S5_IOFLAGS_RESTART) continue;
	    S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, 0, "S5IORecv: Select failed: %m");
	    goto interrupted;
	}
	
	while (rlen > 0) {
	    switch ((sval = S5BufCheckPacket(fd, info))) {
		case 0:
		case -1: goto interrupted;
		default: break;
	    }

            /* packet is not available...                                    */
	    if (sval < 0) break;

 	    if ((nr = S5BufReadPacket(fd, info, buf, rlen, ioflags)) <= 0) {
		sval = nr;
	 	goto interrupted;
	    }

	    if (!(libflags & S5_IOFLAGS_NBYTES)) return nr;

    	    rlen -= nr;
	    buf  += nr;
        }

        if (sval < 0 && !(libflags & S5_IOFLAGS_NBYTES)) {
	    sval = -1;
	    goto interrupted;
	}
    }
    
    return size;

interrupted:
    /* We read some data in, but we were interrupted and we aren't supposed  */
    /* to restart... So we have to store what we've read so far in info...   */
    if (S5BufUnreadPacket(info, buf-(size-rlen), size-rlen)) {
        /* Nowhere to store what we've read... Bummer...                     */
    }

    return sval;
}

int S5IOSend(S5IOHandle fd, S5IOInfo *info, char *buf, int size, int flags, int libflags, double *timerm) {
    struct timeval sv, *svpt;
    int nw, wlen, sval;
    fd_set fds, b;
    
#ifdef STRICT_TIMEOUT
    struct timeval ts, te;
#endif

    if (libflags & S5_IOFLAGS_TIMED && !timerm) {
	SETSOCKETERROR(ETIMEDOUT);
	return -1;
    }
    
    if ((libflags & S5_IOFLAGS_NBYTES) && !(libflags & S5_IOFLAGS_RESTART)) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, 0, "S5IOSend: Warning: Cannot reliably write n bytes and not handle restarts");
    }
    
    FD_ZERO(&b);
    FD_SET(fd, &b);
    
    for (fds = b, wlen = size; wlen > 0; fds = b) {
	if (libflags & S5_IOFLAGS_TIMED) {
	    sv.tv_sec  = (int)*timerm;
	    sv.tv_usec = (int)((*timerm - (double)sv.tv_sec) * 1000000.0);
	    
#ifdef STRICT_TIMEOUT
	    if (gettimeofday(&ts, NULL) < 0) {
		S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, 0, "S5IOSend: gettimeofday failed: %m");
		SETSOCKETERROR(ETIMEDOUT);
		return -1;
	    }
#endif
	} else {
	    sv.tv_sec = 0;
	    sv.tv_usec = 0;
	}
	
	if (libflags & S5_IOFLAGS_TIMED) svpt = &sv;
	else if (ISNBLOCK(fd) && !(libflags & S5_IOFLAGS_NBYTES)) svpt = &sv;
	else svpt = NULL;

	if ((sval = REAL(select)(fd + 1, NULL, &fds, NULL, svpt)) == 0) {
	    S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(5), 0, "S5IOSend: select failed: Timed out");
	    if (libflags & S5_IOFLAGS_TIMED) SETSOCKETERROR(ETIMEDOUT);
#if defined(sun) && !defined(__svr4__)
            else SETSOCKETERROR(EWOULDBLOCK);
#else
            else SETSOCKETERROR(EAGAIN);
#endif
	    return -1;
	}
	
#ifdef STRICT_TIMEOUT
	if (libflags & S5_IOFLAGS_TIMED) {
	    if (gettimeofday(&te, NULL) < 0) {
		S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, 0, "S5IOSend: gettimeofday failed: %m");
		SETSOCKETERROR(ETIMEDOUT);
		return -1;
	    }
	    
	    *timerm -= (te.tv_sec  - ts.tv_sec);
	    *timerm -= (te.tv_usec - ts.tv_usec)/1000000.0;
	    if (*timerm < 0.0) *timerm = 0.0;
	}
#endif
	
	if (sval < 0) {
	    if (ISSOCKETERROR(EINTR) && libflags & S5_IOFLAGS_RESTART) continue;
	    S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, 0, "S5IOSend: Select failed: %m");
	    return -1;
	}
	
	if ((nw = S5BufWritePacket(fd, info, buf, wlen, flags)) < 0) {
            if (ISSOCKETERROR(EINTR) && (libflags & S5_IOFLAGS_RESTART)) continue;
            if ((ISSOCKETERROR(EWOULDBLOCK) || ISSOCKETERROR(EAGAIN)) && (libflags & S5_IOFLAGS_NBYTES)) continue;
	    S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING,  0, "S5IOSend: failed: %m");
	    return nw;
	}

	if (nw == 0) {
	    S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING,  0, "S5IOSend: peer closed");
	    SETSOCKETERROR(EPIPE);
	    return -1;
	}

	if (!(libflags & S5_IOFLAGS_NBYTES)) return nw;

	wlen -= nw;
	buf  += nw;
    }
    
    return size;
}

/* See what's going on with a socket -- is the connection still valid?       */
int S5IOCheck(S5IOHandle fd) {
    struct timeval tv = { 0, 0 };
    fd_set rfds, b;
    char dummy;
    int sv, n;
		
    FD_ZERO(&b);
    FD_SET(fd, &b);

    S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "S5IOCheck: Checking socket status");

    /* Poll the file descriptor real quick, and see what comes back...       */
    while (1) {
	rfds = b; 
        switch ((sv = REAL(select)(fd+1, &rfds, NULL, NULL, &tv))) {
	    case 1:
	        /* Something happened -- either an error or data, ok to recv */
	        n = RECVSOCKET(fd, &dummy, 1, MSG_PEEK);
	        if (n < 0 && ISSOCKETERROR(EINTR)) continue;
		else if (n <= 0) {
		    S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(1), 0, "S5IOCheck: recv failed: %m");
		    return -1;
	        }
	        /* fallthrough -- there is real data there...                */
	    case 0:
	        /* Nothing's happening -- no error, all's ok.                */
	        S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "S5IOCheck: ok");
	        return sv;
	    default:
	        /* Not a valid socket (?).                                   */
                if (ISSOCKETERROR(EINTR)) continue;
	        S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, 0, "S5IOCheck: select failed: %m");
	        return -1;
        }
    }
}

