/* transprt.c       Copyright (c) 2000-2003 Nagy Daniel
 *
 * $Date: 2003/11/12 10:57:10 $
 * $Revision: 1.3 $
 *
 * This module is the SSH1 transport layer.
 *
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
 */

#include <stdio.h>
#include <stdarg.h>
#include <conio.h>
#include <string.h>

#include "zlib.h"

#include "tcp.h"
#include "config.h"
#include "cipher.h"
#include "sshcrc.h"
#include "macros.h"
#include "ssh.h"
#include "transprt.h"
#include "sshbn.h"
#include "xmalloc.h"
#include "common.h"
#include "keyio.h"

/* external functions */
extern SessKeyInitPtr SessKeyInit;

/* external data */
extern Config GlobalConfig;		/* global configuration structure */
extern unsigned short Configuration;	/* Configuration bits */

/* global data */
Packet pktin;	/* incoming SSH packet */
Packet pktout;	/* outgoing SSH packet */
char protocolerror[] = "Protocol error";

/* local static data */

static z_stream comp;	        	/* compression stream */
static z_stream decomp; 	        /* decompression stream */

CryptPtr EncryptPacket;
CryptPtr DecryptPacket;

/*
 * Save packets in hexadecimal format for debugging purposes
 */
static void fwritehex(unsigned char *dbgbuf, unsigned short length)
{
unsigned short i;

   if(!length)
      return;

   for(i = 1; i <= length; i++){ /* print hexa dump first */
	fprintf(GlobalConfig.debugfile, "%02X ", *dbgbuf++);
	if(i%16 == 0) /* 16 bytes per row */
	   fputs("\n", GlobalConfig.debugfile);
   }

   fputs("\n", GlobalConfig.debugfile);
   dbgbuf-=length;

   for(i = 1; i <= length; i++){ /* now print raw data */
	if(*dbgbuf >= ' ' && *dbgbuf < 126)
	   fprintf(GlobalConfig.debugfile, "%c", *dbgbuf);
	else                    /* put '.' instead of non-readable bytes */
	   fputc('.', GlobalConfig.debugfile);
	if(i%16==0)		/* line break after 16 bytes */
	    fputs("\n", GlobalConfig.debugfile);
	dbgbuf++;
   }
}


/*
 * Compress a packet
 */
static void SSH_Compress(const char *source, uLong sourceLen,
		char **dest, uLong *destLen)
{
int m;

   *dest = (char *)xmalloc(*destLen);
   comp.next_in = (Bytef*)source;	/* source buffer */
   comp.avail_in = (uInt)sourceLen;	/* source length */
   comp.next_out = *dest;		/* destination buffer */
   comp.avail_out = *destLen;		/* max destination length */

   if((m = deflate(&comp, Z_SYNC_FLUSH)) != Z_OK)
        SSH_Disconnect("Compression error %d", m);

   if(comp.avail_out == 0) /* FIXME: compression buffer is too small */
        SSH_Disconnect("Compression buffer is too small");

   *destLen = *destLen - comp.avail_out;
}

/*
 * Uncompress a packet. We enlarge the decompression buffer as
 * needed.
 */
static void SSH_Uncompress(const char *source, uLong sourceLen,
		  char **dest, uLongf *destLen)
{
int n, m;

   *destLen = 512;
   *dest = (char *)xmalloc(*destLen);
   decomp.next_in = (Bytef*)source;	  /* source buffer */
   decomp.avail_in = (uInt)sourceLen;	  /* source length */
   decomp.avail_out = 512;

   for(n = 0; ; n++, decomp.avail_out = 512){
      decomp.next_out = *dest + n * 512;  /* destination buffer */
      if((m = inflate(&decomp, Z_SYNC_FLUSH)) != Z_OK)
         SSH_Disconnect("Decompression error %d", m);
      *destLen -= decomp.avail_out;
      if(decomp.avail_out)		  /* end if no more input data */
         break;
      *destLen += 512;
      *dest = (char *)xrealloc(*dest, *destLen);
   }
}

/*
 * Free compression structures
 */
void Disable_Compression(void)
{
   deflateEnd(&comp);
   inflateEnd(&decomp);
}


/*
 * Request compression
 */
void Request_Compression(int level)
{
   if(Configuration & VERBOSE_MODE)
	puts("Requesting compression");

   SSH_pkt_init(SSH_CMSG_REQUEST_COMPRESSION);
   SSH_putuint32(level);
   SSH_pkt_send();

   SSH_pkt_read_sucfail();

   if(pktin.type == SSH_SMSG_SUCCESS){
	   memset(&comp, 0, sizeof(comp));
	   memset(&decomp, 0, sizeof(decomp));
	   if(deflateInit(&comp, level) != Z_OK)
	        SSH_Disconnect("Cannot initialize compression");
	   if(inflateInit(&decomp) != Z_OK)
	        SSH_Disconnect("Cannot initialize decompression");
           Configuration |= COMPRESSION_ENABLED;
   } else
	   puts("Server refused compression");
}

/*
 * Read and convert raw packet to readable structure.
 * Uncrypt and uncompress if necessary
 */
static void ssh_gotdata(void)
{
unsigned long len;
unsigned long PktLength; /* full packet length with padding */
unsigned short pad;      /* number of padding bytes */
char PktInLength[4];     /* first four bytes of a packet (length) */
char *inbuf;             /* buffer for incoming packet */
char *decompblk;         /* buffer for decompressed data */

/*
 * 1. Get four bytes from packet, which is the packet size without padding.
 * 2. Calculate the padding length and full packet length.
 * 3. Allocate memory for the whole packet and read it.
 * 4. Uncrypt packet.
 * 5. Fill incoming packet structure
 * 6. Verify CRC (padding + type + body)
 * 7. Uncompress packet (type + body)
 * 8. Get packet type
 */

   if(sock_read(&GlobalConfig.s, PktInLength, 4) != 4)
      fatal("Socket read error");
   len = GET_32BIT_MSB_FIRST(PktInLength);       /* get length */
   pad = 8 - (len%8);
   PktLength = len + pad;
   if(PktLength > MAX_PACKET_SIZE)
      fatal("Received packet too large");

   inbuf = (char *)xmalloc(PktLength);
   if(sock_read(&GlobalConfig.s, inbuf, PktLength) != PktLength) /* Read rest */
      fatal("Socket read error");

   if(Configuration & CIPHER_ENABLED) /* uncrypt */
      DecryptPacket(inbuf, PktLength);

   pktin.length = PktLength - 4 - pad;  /* minus CRC, padding (type included) */

   if(Configuration & COMPRESSION_ENABLED){
        SSH_Uncompress(inbuf + pad, pktin.length, &decompblk, &len);
        inbuf = xrealloc(inbuf, len + pad + 1);
        memcpy(inbuf + pad, decompblk, len);  /* copy uncompressed */
        xfree(decompblk);
        pktin.length = len;              /* fix length after uncompression */
   } /* Compression */

   pktin.body = (pktin.body == NULL ? xmalloc(pktin.length) :
                                      xrealloc(pktin.body, pktin.length));

   pktin.length--;                      /* remove type from body */
   pktin.type = inbuf[pad];             /* fill type */
   if(pktin.length)
      memcpy(pktin.body, inbuf + pad + 1, pktin.length);
   xfree(inbuf); /* it's now in pktin.body, so free it */

   if(GlobalConfig.debugfile){
      fputs("\nRECEIVED packet:\n", GlobalConfig.debugfile);
      fprintf(GlobalConfig.debugfile, "Type: %x\n", pktin.type);
      fwritehex(pktin.body, pktin.length);
      fputc('\n', GlobalConfig.debugfile);
   } /* debug */

}

/*
 * Calculate full packet length from pktout.length (data length)
 * and reallocate memory for it
 */
static void SSH_pkt_size(void)
{
   /* add length field, padding and CRC for total length */
   pktout.whole = xrealloc(pktout.whole, pktout.length + 4 + 8 + 4);
   pktout.body = pktout.whole + 4 + 8;
}

/*
 * Enlarge outgoing packet if needed
 */
static void SSH_pkt_grow(unsigned long len)
{
   pktout.length += len;
   if(pktout.length > pktout.maxlen){
	pktout.maxlen = pktout.length + 256;
	pktout.whole = xrealloc(pktout.whole, pktout.maxlen);
	pktout.body = pktout.whole + 4 + 8;
	pktout.ptr = pktout.body + pktout.length - len;
   }
}

/*
 * Initialize an outgoing SSH1 packet.
 */
void SSH_pkt_init(unsigned char type)
{
   pktout.length = 1;		/* We only have the type now */
   pktout.maxlen = 1024;	/* Seems good size to start a packet */
   pktout.whole = xmalloc(pktout.maxlen);
   pktout.body = pktout.whole + 4 + 8; /* leave space for length and padding */
   pktout.ptr = pktout.body + 1;
   *pktout.body = type;
}

/*
 * SSH1 packet assembly functions. These can put multiple
 * types of data into an SSH1 packet
 */
void SSH_putdata(char *data, unsigned short len)
{
   SSH_pkt_grow(len);
   memcpy(pktout.ptr, data, len);
   pktout.ptr += len;
}

void SSH_putbool(char value)
{
   SSH_putdata(&value, 1);
}

void SSH_putuint32(unsigned long value)
{
char x[4];

   PUT_32BIT_MSB_FIRST(x, value);
   SSH_putdata(x, 4);
}

void SSH_putstring(char *str)
{
   SSH_putuint32(strlen(str));
   SSH_putdata(str, strlen(str));
}

void SSH_putbn(Bignum bn)
{
unsigned short len = ssh1_bignum_length(bn);
short i;
unsigned short bitc = bignum_bitcount(bn);

    SSH_putbool((bitc >> 8) & 0xFF);
    SSH_putbool((bitc) & 0xFF);
    for (i = len - 2; i--;)
	SSH_putbool(bignum_byte(bn, i));
}

/*
 * Assemble and send a raw SSH1 packet
 */
void SSH_pkt_send(void)
{
int pad, i;
unsigned long crc;
char *compblk;
unsigned long complen;

/*
 * 1. Compress (type + data)
 * 2. Calculate padding length (8 - ((type + body + CRC) modulo 8))
 * 3. Calculate and fill length field (type + body + CRC)
 * 4. Add padding
 * 5. Calculate CRC (padding + type + body)
 * 6. Encrypt (padding + type + data + CRC)
 * 7. Send
 */

   if(GlobalConfig.debugfile){
	fputs("\nSENT packet:\n", GlobalConfig.debugfile);
	fwritehex(pktout.body, pktout.length);
	fputc('\n', GlobalConfig.debugfile);
   }

   if(Configuration & COMPRESSION_ENABLED){
	complen = (pktout.length + 13) * 1.1;
        SSH_Compress(pktout.body, pktout.length, &compblk, &complen);
	pktout.length = complen;
	SSH_pkt_size();	/* adjust packet size for sending */
	memcpy(pktout.body, compblk, complen);
	xfree(compblk);
   } /* Compression */
   else
	SSH_pkt_size();	/* adjust packet size for sending */

   pad = 8 - ((pktout.length + 4) % 8);	/* calculate padding length */
   pktout.ptr = pktout.body - pad - 4;	/* point to start of raw packet */
   PUT_32BIT_MSB_FIRST(pktout.ptr, pktout.length + 4); /* fill length field */

   for (i = 0; i < pad; i++)		/* add padding */
	pktout.ptr[i + 4] = rand() % 256;

   crc = sshcrc32(pktout.ptr + 4, pad + pktout.length);	/* add CRC */
   PUT_32BIT_MSB_FIRST(pktout.ptr + 4 + pad + pktout.length, crc);

   if(Configuration & CIPHER_ENABLED)
	EncryptPacket(pktout.ptr + 4, pad + pktout.length + 4);

   sock_flushnext(&GlobalConfig.s);
   if(sock_write(&GlobalConfig.s, pktout.ptr, 4 + pad + pktout.length + 4)
					   != 4 + pad + pktout.length + 4)
	fatal("Socket write error");

   xfree(pktout.whole);
}

/*
 * Get a packet with blocking. Handle debug, ignore and disconnect packets.
 * If type != NULL, also checks type to avoid protocol confusion.
 * Check for user input too.
 */
void SSH_pkt_read(unsigned char type)
{

restart:
   while(!sock_dataready(&GlobalConfig.s)){	/* do we got some data? */
      if(!tcp_tick(&GlobalConfig.s))		/* TCP wait and 	*/
         fatal("Remote host closed connection");
         while(ConChk())				/* examine STDIN if not */
            DoKey();
   }

   ssh_gotdata();

   switch(pktin.type){
        case SSH_MSG_DISCONNECT:
	   pktin.body[pktin.length] = 0;
	   fatal("Remote host disconnected: %s\n", pktin.body + 4);

        case SSH_MSG_IGNORE:
	   goto restart;

        case SSH_MSG_DEBUG:
           pktin.body[pktin.length] = 0;
           printf("DEBUG: %s\n", pktin.body + 4);
           goto restart;

        default:
          break;
   } /* switch */

   if(type)
      if(pktin.type != type)
         SSH_Disconnect(protocolerror);
}

/*
 * Get a packet from the authentication layer, but let
 * only SUCCESS and FAILURE response packets through.
 */
void SSH_pkt_read_sucfail(void)
{
   SSH_pkt_read(0); /* Get packet from transport layer */

   switch(pktin.type){
        case SSH_SMSG_SUCCESS:	/* let these through */
        case SSH_SMSG_FAILURE:
	   break;

        default:
	   SSH_Disconnect(protocolerror);
   } /* switch */
}

/*
 * Send n data bytes as an SSH packet
 */
void SendSSHPacket(char *buff, unsigned short len)
{
   SSH_pkt_init(SSH_CMSG_STDIN_DATA);
   SSH_putuint32(len);
   SSH_putdata(buff, len);
   SSH_pkt_send();
}

/*
 * Disconnect and print error if needed
 */
void SSH_Disconnect(const char *fmt, ...)
{
va_list ap;
char buf[256];

   va_start(ap, fmt);
   vsprintf(buf, fmt, ap);
   va_end(ap);

   SSH_pkt_init(SSH_MSG_DISCONNECT);
   SSH_putstring(buf);
   SSH_putuint32(0);
   SSH_pkt_send();

   fatal(buf);
}
