/*
 * tinytcp.c - Tiny Implementation of the Transmission Control Protocol
 *
 * Written March 28, 1986 by Geoffrey Cooper, IMAGEN Corporation.
 * May 1986 (dab): Modified for C-Gateway.
 *
 * This code is a small implementation of the TCP and IP protocols,
 * suitable for burning into ROM.  The implementation is bare-bones
 * and represents two days' coding efforts.  A timer and an ethernet
 * board are assumed.  The implementation is based on busy-waiting,
 * but the tcp_handler procedure could easily be integrated into an
 * interrupt driven scheme.  [dab: changed to interrupt driven scheme
 * for C-Gateway.]
 *
 * IP routing is accomplished on active opens by broadcasting the tcp
 * SYN packet when ARP mapping fails.  If anyone answers, the ethernet
 * address used is saved for future use.  This also allows IP routing
 * on incoming connections.  [dab: IP routing now handled by the
 * normal C-Gateway IP routing.]
 * 
 * The TCP does not implement urgent pointers (easy to add), and discards
 * segments that are received out of order.  It ignores the received window
 * and always offers a fixed window size on input (i.e., it is not flow
 * controlled).
 *
 * Copyright (C) 1986, IMAGEN Corporation
 *  "This code may be duplicated in whole or in part provided that [1] there
 *   is no commercial gain involved in the duplication, and [2] that this
 *   copyright notice is preserved on all copies.  Any other duplication
 *   requires written notice of the author."
 */

/*
 *------------------------------------------------------------------
 *
 * $Source: /mit/cgw/src/gw/in/RCS/tcp.c,v $
 * $Revision: 1.3 $
 * $Date: 88/06/08 15:10:18 $
 * $State: Exp $
 * $Author: jon $
 * $Locker: jon $
 *
 * $Log:	tcp.c,v $
 * Revision 1.3  88/06/08  15:10:18  jon
 * Handle selection of our address when we don't have a route to the target
 * destination and we don't have a default gateway.
 * 
 * 
 *------------------------------------------------------------------
 */

#ifndef lint
static char *rcsid_tcp_c = "$Header: tcp.c,v 1.3 88/06/08 15:10:18 jon Locked $";
#endif	lint

#include <types.h>
#include <sys.h>
#include <sysext.h>
#include "../src/const.h"
#include "../src/param.h"
#include "../src/defs.h"
#include "../src/macs.h"
#include "../src/net.h"
#include "../src/ext.h"
#include "in.h"
#include "inext.h"
#include "inrte.h"
#include "tcp.h"

/* TCP connections */
tcp_Socket *tcp_allsocs;

/* Timer definitions (in seconds) */
#define tcp_RETRANSMITTIME 1	/* interval at which retransmitter is called */
#define tcp_LONGTIMEOUT 31	/* timeout for opens */
#define tcp_TIMEOUT 10		/* timeout during a connection */

#define tcp_RETRANSPRIO 200	/* priority of retransmit task */


/*
 * Initialize the tcp implementation
 */
tcp_Init()
{
    int tcp_Retransmitter();

    tcp_allsocs = NIL;
    itime(tcp_Retransmitter, 0, tcp_RETRANSMITTIME, tcp_RETRANSPRIO);
    log_register_flag ("tcp", &MSGFLG);
}

/*
 * Actively open a TCP connection to a particular destination.
 */
tcp_Open(s, lport, ina, port, datahandler)
tcp_Socket *s;
inaddr *ina;
unss lport, port;
procref datahandler;
{
    iprte *ipr;
    net *netp;
    int net_index;

    s->state = tcp_StateSYNSENT;
    s->timeout = tcp_LONGTIMEOUT;
    if (lport == 0) lport = systick;
    if ((ipr = ipr_lookup(ina->i_long)) == NULL)
      netp = indefgi;
    else
      netp = ipr->ipr_netp;
    /* if we don't have a default route use net 0, it'll have to do,
       but it doesn't really matter since our packets will have no where
       to go ... */
    if (netp != NULL)
      net_index = netp->n_net;
    else
      net_index = 0;
    s->myaddr.i_long = mkina(iniatlst[net_index]->inia_addr)->i_long;
    s->myport = s2ip(lport);
    s->hisaddr.i_long = ina->i_long;
    s->hisport = s2ip(port);
    s->seqnum = 0;
    s->dataSize = 0;
    s->flags = tcp_FlagSYN;
    s->unhappy = true;
    s->dataHandler = datahandler;
    s->next = tcp_allsocs;
    tcp_allsocs = s;
    tcp_Send(s);
}

/*
 * Passive open: listen for a connection on a particular port
 */
tcp_Listen(s, port, datahandler, timeout)
tcp_Socket *s;
unss port;
procref datahandler;
int timeout;
{
    s->state = tcp_StateLISTEN;
    if (timeout == 0) s->timeout = 0x7ffffff; /* forever... */
    else s->timeout = timeout;
    s->myport = s2ip(port);
    s->hisport = 0;
    s->seqnum = 0;
    s->dataSize = 0;
    s->flags = 0;
    s->unhappy = 0;
    s->dataHandler = datahandler;
    s->next = tcp_allsocs;
    tcp_allsocs = s;
}

/*
 * Send a FIN on a particular port -- only works if it is open
 */
tcp_Close(s)
tcp_Socket *s;
{
    if (s->state == tcp_StateESTAB || s->state == tcp_StateSYNREC) {
        s->flags = tcp_FlagACK | tcp_FlagFIN;
        s->state = tcp_StateFINWT1;
        s->unhappy = true;
    }
}

/*
 * Abort a tcp connection
 */
tcp_Abort(s)
tcp_Socket *s;
{
    if (s->state != tcp_StateLISTEN && s->state != tcp_StateCLOSED) {
        s->flags = tcp_FlagRST | tcp_FlagACK;
        tcp_Send(s);
    }
    s->unhappy = 0;
    s->dataSize = 0;
    s->state = tcp_StateCLOSED;
    tcp_Unthread(s);
    s->dataHandler(s, 0, -1);
}

/*
 * Retransmitter - called periodically to perform tcp retransmissions
 */
tcp_Retransmitter()
{
    tcp_Socket *s;
    bits x;

    for (s = tcp_allsocs; s; s = s->next) {
        x = false;
        if (s->dataSize > 0 || s->unhappy) {
            tcp_Send(s);
            x = true;
        }
        if (x || s->state != tcp_StateESTAB)
            s->timeout -= tcp_RETRANSMITTIME;
        if (s->timeout <= 0) {
            if (s->state == tcp_StateTIMEWT) {
		niflog(L_TRCO)
		  dolog("TCP Closed.    \n");
                s->state = tcp_StateCLOSED;
                tcp_Unthread(s);
                s->dataHandler(s, 0, 0);
            } else {
		niflog(L_TRCO)
		  dolog("TCP Timeout, aborting\n");
                tcp_Abort(s);
            }
        }
    }
}

/*
 * Unthread a socket from the socket list, if it's there 
 */
tcp_Unthread(ds)
tcp_Socket *ds;
{
    tcp_Socket *s, **sp;

    sp = &tcp_allsocs;
    for (;;) {
        s = *sp;
        if (s == ds) {
            *sp = s->next;
            break;
        }
        if (s == NIL) break;
        sp = &s->next;
    }
}

/*
 * Write data to a connection.
 * Returns number of bytes written, == 0 when connection is not in
 * established state.
 */
tcp_Write(s, dp, len)
tcp_Socket *s;
byte *dp;
int len;
{
    int x;

    if (s->state != tcp_StateESTAB) len = 0;
    if (len > (x = tcp_MaxData - s->dataSize)) len = x;
    if (len > 0) {
        Move(dp, &s->data[s->dataSize], len);
        s->dataSize += len;
        tcp_Flush(s);
    }

    return (len);
}

/*
 * Send pending data
 */
tcp_Flush(s)
tcp_Socket *s;
{
    if (s->dataSize > 0) {
        s->flags |= tcp_FlagPUSH;
        tcp_Send(s);
    }
}

/*
 * Handler for incoming packets.
 */
tcp_Handler(iob)
iorb *iob;
{
    inpkt *ip;
    tcp_Header *tp;
    tcp_PseudoHeader ph;
    int len;
    byte *dp;
    int x, diff;
    tcp_Socket *s;
    unss flags;

    ip = mkin(iob->i_addr);
    tp = (tcp_Header *)(iob->i_addr + sizeof(inpkt));
    len = ip2s(ip->i_len) - sizeof(inpkt);

    /* demux to active sockets */
    for (s = tcp_allsocs; s; s = s->next)
        if (s->hisport != 0 &&
	    tp->dstPort == s->myport &&
	    tp->srcPort == s->hisport &&
	    ip->i_src.i_long == s->hisaddr.i_long) break;
    if (s == NIL) {
        /* demux to passive sockets */
        for (s = tcp_allsocs; s; s = s->next)
            if (s->hisport == 0 && tp->dstPort == s->myport) break;
    }
    if (s == NIL) {
	niflog(L_DUMP)
	  tcp_DumpHeader(ip, tp, "Discarding");
        return;
    }

    niflog(L_DUMP)
      tcp_DumpHeader(ip, tp, "Received");

    ph.src.i_long = ip->i_src.i_long;
    ph.dst.i_long = ip->i_dst.i_long;
    ph.mbz = 0;
    ph.protocol = 6;
    ph.length = s2ip(len);
    ph.checksum = checksum(tp, len);
    /* Fucking Berkeley C compiler!!! sizeof() is being rounded up to
     * a multiple of 4, i.e. long word aligned. */
    if (checksum(&ph, 14 /* sizeof(tcp_PseudoHeader) */) != 0xffff) {
	niflog(L_ERRU)
	  dolog("TCP bad cksum, received anyway\n");
    }

    flags = tp->flags;
    if (flags & tcp_FlagRST) {
	niflog(L_INFC)
	  dolog("TCP connection reset\n");
        s->state = tcp_StateCLOSED;
        tcp_Unthread(s);
        s->dataHandler(s, 0, -1);
        return;
    }

    switch (s->state) {

    case tcp_StateLISTEN:
        if (flags & tcp_FlagSYN) {
            s->acknum = ip2l(tp->seqnum) + 1;
            s->hisport = tp->srcPort;
            s->hisaddr.i_long = ip->i_src.i_long;
	    s->myaddr.i_long = ip->i_dst.i_long;
            s->flags = tcp_FlagSYN | tcp_FlagACK;
            tcp_Send(s);
            s->state = tcp_StateSYNREC;
            s->unhappy = true;
            s->timeout = tcp_TIMEOUT;
	    niflog(L_INFC)
	      dolog("TCP Syn from 0x%x#%d (seq 0x%x)\n",
		    s->hisaddr.i_long, ip2s(s->hisport), ip2l(tp->seqnum));
        }
        break;

    case tcp_StateSYNSENT:
        if (flags & tcp_FlagSYN) {
            s->acknum++;
            s->flags = tcp_FlagACK;
            s->timeout = tcp_TIMEOUT;
            if ((flags & tcp_FlagACK) && ip2l(tp->acknum) == (s->seqnum + 1)) {
		iflog(L_TRCO)
		  dolog("TCP Open\n");
                s->state = tcp_StateESTAB;
                s->seqnum++;
                s->acknum = ip2l(tp->seqnum) + 1;
                s->unhappy = false;
            } else {
                s->state = tcp_StateSYNREC;
            }
        }
        break;

    case tcp_StateSYNREC:
        if (flags & tcp_FlagSYN) {
            s->flags = tcp_FlagSYN | tcp_FlagACK;
            tcp_Send(s);
            s->timeout = tcp_TIMEOUT;
	    niflog(L_TRCO)
	      dolog("TCP  retransmit of original syn\n");
        }
        if ((flags & tcp_FlagACK) && ip2l(tp->acknum) == (s->seqnum + 1)) {
            s->flags = tcp_FlagACK;
            tcp_Send(s);
            s->seqnum++;
            s->unhappy = false;
            s->state = tcp_StateESTAB;
            s->timeout = tcp_TIMEOUT;
	    niflog(L_TRCO)
	      dolog("TCP Synack received - connection established\n");
        }
        break;

    case tcp_StateESTAB:
        if ((flags & tcp_FlagACK) == 0) return;
        /* process ack value in packet */
        diff = ip2l(tp->acknum) - s->seqnum;
        if (diff > 0) {
            Move(&s->data[diff], &s->data[0], s->dataSize - diff);
            s->dataSize -= diff;
            s->seqnum += diff;
        }
        s->flags = tcp_FlagACK;
        tcp_ProcessData(s, tp, len);
        break;

    case tcp_StateFINWT1:
        if ((flags & tcp_FlagACK) == 0) return;
        diff = ip2l(tp->acknum) - s->seqnum - 1;
        s->flags = tcp_FlagACK | tcp_FlagFIN;
        if (diff == 0) {
            s->state = tcp_StateFINWT2;
            s->flags = tcp_FlagACK;
            niflog(L_TRCO)
	      dolog("TCP finack received.\n");
        }
        tcp_ProcessData(s, tp, len);
        break;

    case tcp_StateFINWT2:
        s->flags = tcp_FlagACK;
        tcp_ProcessData(s, tp, len);
        break;

    case tcp_StateCLOSING:
        if (ip2l(tp->acknum) == (s->seqnum + 1)) {
            s->state = tcp_StateTIMEWT;
            s->timeout = tcp_TIMEOUT;
        }
        break;

    case tcp_StateLASTACK:
        if (ip2l(tp->acknum) == (s->seqnum + 1)) {
            s->state = tcp_StateCLOSED;
            s->unhappy = false;
            s->dataSize = 0;
            tcp_Unthread(s);
            s->dataHandler(s, 0, 0);
	    niflog(L_TRCO)
	      dolog("TCP Closed.    \n");
        } else {
            s->flags = tcp_FlagACK | tcp_FlagFIN;
            tcp_Send(s);
            s->timeout = tcp_TIMEOUT;
	    niflog(L_TRCO)
	      dolog("TCP retransmitting FIN\n");
        }
        break;

    case tcp_StateTIMEWT:
        s->flags = tcp_FlagACK;
        tcp_Send(s);
    }
}

/*
 * Process the data in an incoming packet.
 * Called from all states where incoming data can be received: established,
 * fin-wait-1, fin-wait-2
 */
tcp_ProcessData(s, tp, len)
tcp_Socket *s;
tcp_Header *tp;
int len;
{
    int diff, x;
    unss flags;
    byte *dp;

    flags = tp->flags;
    diff = s->acknum - ip2l(tp->seqnum);
    if (flags & tcp_FlagSYN) diff--;
    x = tcp_GetDataOffset(tp) << 2;
    dp = (byte *)tp + x;
    len -= x;
    if (diff >= 0) {
        dp += diff;
        len -= diff;
        s->acknum += len;
        s->dataHandler(s, dp, len);
        if (flags & tcp_FlagFIN) {
            s->acknum++;
	    niflog(L_TRCO)
	      dolog("TCP consumed fin.\n");

            switch(s->state) {
              case tcp_StateESTAB:
                /* note: skip state CLOSEWT by automatically closing conn */
                x = tcp_StateLASTACK;
                s->flags |= tcp_FlagFIN;
                s->unhappy = true;
		niflog(L_TRCO)
		  dolog("TCP sending fin.\n");
                break;
              case tcp_StateFINWT1:
                x = tcp_StateCLOSING;
                break;
              case tcp_StateFINWT2:
                x = tcp_StateTIMEWT;
                break;
            }
            s->state = x;
        }
    }
    s->timeout = tcp_TIMEOUT;
    tcp_Send(s);
}

/*
 * Format and send an outgoing segment
 */
tcp_Send(s)
tcp_Socket *s;
{
    tcp_PseudoHeader ph;
    iorb *iob;
    tcp_Header *tp;
    byte *dp;

    if ((iob = getbuf()) == NULL) {
	niflog(L_ERRU)
	  dolog("TCP couldn't alloc pkt\n");
	return;
    }
    mkinpkt(iob, &s->myaddr, &s->hisaddr, INTCP,
	    s->dataSize + sizeof(tcp_Header) +
	    ((s->flags & tcp_FlagSYN) ? 4 : 0));

    /* tcp header */
    tp = (tcp_Header *)(iob->i_addr + sizeof(inpkt));
    dp = (byte *)(tp + 1);
    tp->srcPort = s->myport;
    tp->dstPort = s->hisport;
    tp->seqnum = l2ip(s->seqnum);
    tp->acknum = l2ip(s->acknum);
    tp->window = s2ip(1024);
    tp->checksum = 0;
    tp->urgentPointer = 0;
    if (s->flags & tcp_FlagSYN) {
#ifdef BIG_ENDIAN
        tp->flags = s->flags | 0x6000;
#else
	tp->flags = s->flags | 0x0060;
#endif
        *(unsl *)dp = 0x02040578; /* 1400 bytes */
        dp += 4;
    }
    else {
#ifdef BIG_ENDIAN
	tp->flags = s->flags | 0x5000;
#else
	tp->flags = s->flags | 0x0050;
#endif
    }

    Move(s->data, dp, s->dataSize);

    /* compute tcp checksum */
    ph.src.i_long = s->myaddr.i_long;
    ph.dst.i_long = s->hisaddr.i_long;
    ph.mbz = 0;
    ph.protocol = INTCP;
    ph.length = s2ip(iob->i_breq - sizeof(inpkt));
    ph.checksum = checksum(tp, iob->i_breq - sizeof(inpkt));
    tp->checksum = ~checksum(&ph, sizeof(ph));

    niflog(L_DUMP)
      tcp_DumpHeader(iob->i_addr, tp, "Sending");
    inxaddq(iob);
}

/*
 * Do a one's complement checksum
 */
checksum(dp, length)
unss *dp;
int length;
{
    int len;
    unsl sum;

    len = length >> 1;
    sum = 0;
    while (len-- > 0)
      sum += *dp++;
    if (length & 1)
#ifdef BIG_ENDIAN
      sum += (*dp & 0xFF00);
#else
      sum += (*dp & 0x00FF);
#endif
    sum = (sum & 0xFFFF) + ((sum >> 16) & 0xFFFF);
    sum = (sum & 0xFFFF) + ((sum >> 16) & 0xFFFF);

    return (sum);
}

/*
 * Dump the tcp protocol header of a packet
 */
tcp_DumpHeader(ip, tp, mesg)
inpkt *ip;
char *mesg;
{
    reg tcp_Header *tp = (tcp_Header *)((byte *)ip + sizeof(inpkt));
    static char *flags[] = { "FIN", "SYN", "RST", "PUSH", "ACK", "URG" };
    int len;
    unss f;

    len = ip2s(ip->i_len) - ((tcp_GetDataOffset(tp) << 2) + sizeof(inpkt));
    dolog("TCP: %s packet:\nS: %x; D: %x; SN=%x ACK=%x W=%d DLen=%d\n",
	  mesg, ip2s(tp->srcPort), ip2s(tp->dstPort), ip2l(tp->seqnum),
	  ip2l(tp->acknum), ip2s(tp->window), len);
    dolog("DO=%d, C=%x U=%d",
	  tcp_GetDataOffset(tp), tp->checksum, ip2s(tp->urgentPointer));
    /* output flags */
    f = ip2s(tp->flags);
    for (len = 0; len < 6; len++)
      if (f & (1 << len))
	dolog(" %s", flags[len]);
    dolog("\n");
}

/*
 * Move bytes from hither to yon.  Is careful not to step on its toes if
 * shifting data in a buffer.
 */
Move(src, dest, numbytes)
reg byte *src, *dest;
reg int numbytes;
{
    if (numbytes <= 0) return;
    if (src < dest) {
        src += numbytes;
        dest += numbytes;
        do {
            *--dest = *--src;
        } while ( --numbytes > 0 );
    } else
        do {
             *dest++ = *src++;
        } while ( --numbytes > 0 );
}
