/*
 * This file is part of the SSH Library
 *
 * Copyright (c) 2009 by Aris Adamantiadis
 *
 * The SSH Library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or (at your
 * option) any later version.
 *
 * The SSH Library 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 Lesser General Public
 * License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with the SSH Library; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
 * MA 02111-1307, USA.
 */

/* pcap.c */
/** \defgroup ssh_pcap SSH-pcap
 * \brief libssh pcap file generation
 *
 * \addtogroup ssh_pcap
 * @{ */

#include "config.h"
#ifdef WITH_PCAP

#include <stdio.h>
#ifdef _WIN32
#include <ws2tcpip.h>
#else
#include <sys/time.h>
#include <sys/socket.h>
#endif
#include <errno.h>


#include "libssh/libssh.h"
#include "libssh/pcap.h"
#include "libssh/session.h"
#include "libssh/buffer.h"
#include "libssh/socket.h"

/* The header of a pcap file is the following. We are not going to make it
 * very complicated.
 * Just for information.
 */
struct pcap_hdr_s {
	uint32_t magic_number;   /* magic number */
	uint16_t version_major;  /* major version number */
	uint16_t version_minor;  /* minor version number */
	int32_t   thiszone;       /* GMT to local correction */
	uint32_t sigfigs;        /* accuracy of timestamps */
	uint32_t snaplen;        /* max length of captured packets, in octets */
	uint32_t network;        /* data link type */
};

#define PCAP_MAGIC 0xa1b2c3d4
#define PCAP_VERSION_MAJOR 2
#define PCAP_VERSION_MINOR 4

#define DLT_RAW         12      /* raw IP */

/* TCP flags */
#define TH_FIN        0x01
#define TH_SYN        0x02
#define TH_RST        0x04
#define TH_PUSH       0x08
#define TH_ACK        0x10
#define TH_URG        0x20

/* The header of a pcap packet.
 * Just for information.
 */
struct pcaprec_hdr_s {
	uint32_t ts_sec;         /* timestamp seconds */
	uint32_t ts_usec;        /* timestamp microseconds */
	uint32_t incl_len;       /* number of octets of packet saved in file */
	uint32_t orig_len;       /* actual length of packet */
};

/** @private
 * @brief a pcap context expresses the state of a pcap dump
 * in a SSH session only. Multiple pcap contexts may be used into
 * a single pcap file.
 */

struct ssh_pcap_context_struct {
	ssh_session session;
	ssh_pcap_file file;
	int connected;
	/* All of these information are useful to generate
	 * the dummy IP and TCP packets
	 */
	uint32_t ipsource;
	uint32_t ipdest;
	uint16_t portsource;
	uint16_t portdest;
	uint32_t outsequence;
	uint32_t insequence;
};

/** @private
 * @brief a pcap file expresses the state of a pcap file which may
 * contain several streams.
 */
struct ssh_pcap_file_struct {
	FILE *output;
	uint16_t ipsequence;
};

/**
 * @brief create a new ssh_pcap_file object
 */
ssh_pcap_file ssh_pcap_file_new(){
	return malloc(sizeof(struct ssh_pcap_file_struct));
}

/** @internal
 * @brief writes a packet on file
 */
static int ssh_pcap_file_write(ssh_pcap_file pcap, ssh_buffer packet){
	int err;
	uint32_t len;
	if(pcap == NULL || pcap->output==NULL)
		return SSH_ERROR;
	len=buffer_get_len(packet);
	err=fwrite(buffer_get(packet),len,1,pcap->output);
	if(err<0)
		return SSH_ERROR;
	else
		return SSH_OK;
}

/** @internal
 * @brief prepends a packet with the pcap header and writes packet
 * on file
 */
int ssh_pcap_file_write_packet(ssh_pcap_file pcap, ssh_buffer packet, uint32_t original_len){
	ssh_buffer header=buffer_new();
	struct timeval now;
	int err;
	if(header == NULL)
		return SSH_ERROR;
	gettimeofday(&now,NULL);
	buffer_add_u32(header,htonl(now.tv_sec));
	buffer_add_u32(header,htonl(now.tv_usec));
	buffer_add_u32(header,htonl(buffer_get_len(packet)));
	buffer_add_u32(header,htonl(original_len));
	buffer_add_buffer(header,packet);
	err=ssh_pcap_file_write(pcap,header);
	buffer_free(header);
	return err;
}

/**
 * @brief opens a new pcap file and create header
 */
int ssh_pcap_file_open(ssh_pcap_file pcap, const char *filename){
	ssh_buffer header;
	int err;
	if(pcap == NULL)
		return SSH_ERROR;
	if(pcap->output){
		fclose(pcap->output);
		pcap->output=NULL;
	}
	pcap->output=fopen(filename,"wb");
	if(pcap->output==NULL)
		return SSH_ERROR;
	header=buffer_new();
	if(header==NULL)
		return SSH_ERROR;
	buffer_add_u32(header,htonl(PCAP_MAGIC));
	buffer_add_u16(header,htons(PCAP_VERSION_MAJOR));
	buffer_add_u16(header,htons(PCAP_VERSION_MINOR));
	/* currently hardcode GMT to 0 */
	buffer_add_u32(header,htonl(0));
	/* accuracy */
	buffer_add_u32(header,htonl(0));
	/* size of the biggest packet */
	buffer_add_u32(header,htonl(MAX_PACKET_LEN));
	/* we will write sort-of IP */
	buffer_add_u32(header,htonl(DLT_RAW));
	err=ssh_pcap_file_write(pcap,header);
	buffer_free(header);
	return err;
}

int ssh_pcap_file_close(ssh_pcap_file pcap){
	int err;
	if(pcap ==NULL || pcap->output==NULL)
		return SSH_ERROR;
	err=fclose(pcap->output);
	pcap->output=NULL;
	if(err != 0)
		return SSH_ERROR;
	else
		return SSH_OK;
}

void ssh_pcap_file_free(ssh_pcap_file pcap){
	ssh_pcap_file_close(pcap);
	SAFE_FREE(pcap);
}


/** @internal
 * @brief allocates a new ssh_pcap_context object
 */

ssh_pcap_context ssh_pcap_context_new(ssh_session session){
	ssh_pcap_context ctx=malloc(sizeof(struct ssh_pcap_context_struct));
	if(ctx==NULL){
		ssh_set_error_oom(session);
		return NULL;
	}
	ZERO_STRUCTP(ctx);
	ctx->session=session;
	return ctx;
}

void ssh_pcap_context_free(ssh_pcap_context ctx){
	SAFE_FREE(ctx);
}

void ssh_pcap_context_set_file(ssh_pcap_context ctx, ssh_pcap_file pcap){
	ctx->file=pcap;
}

/** @internal
 * @brief sets the IP and port parameters in the connection
 */
static int ssh_pcap_context_connect(ssh_pcap_context ctx){
	ssh_session session=ctx->session;
	struct sockaddr_in local, remote;
	socket_t fd;
	socklen_t len;
	if(session==NULL)
		return SSH_ERROR;
	if(session->socket==NULL)
		return SSH_ERROR;
	fd=ssh_socket_get_fd(session->socket);
	/* TODO: adapt for windows */
	if(fd<0)
		return SSH_ERROR;
	len=sizeof(local);
	if(getsockname(fd,(struct sockaddr *)&local,&len)<0){
		ssh_set_error(session,SSH_REQUEST_DENIED,"Getting local IP address: %s",strerror(errno));
		return SSH_ERROR;
	}
	len=sizeof(remote);
	if(getpeername(fd,(struct sockaddr *)&remote,&len)<0){
		ssh_set_error(session,SSH_REQUEST_DENIED,"Getting remote IP address: %s",strerror(errno));
		return SSH_ERROR;
	}
	if(local.sin_family != AF_INET){
		ssh_set_error(session,SSH_REQUEST_DENIED,"Only IPv4 supported for pcap logging");
		return SSH_ERROR;
	}
	memcpy(&ctx->ipsource,&local.sin_addr,sizeof(ctx->ipsource));
	memcpy(&ctx->ipdest,&remote.sin_addr,sizeof(ctx->ipdest));
	memcpy(&ctx->portsource,&local.sin_port,sizeof(ctx->portsource));
	memcpy(&ctx->portdest,&remote.sin_port,sizeof(ctx->portdest));

	ctx->connected=1;
	return SSH_OK;
}

#define IPHDR_LEN 20
#define TCPHDR_LEN 20
#define TCPIPHDR_LEN (IPHDR_LEN + TCPHDR_LEN)
/** @internal
 * @brief write a SSH packet as a TCP over IP in a pcap file
 * @param ctx open pcap context
 * @param direction SSH_PCAP_DIRECTION_IN if the packet has been received
 * @param direction SSH_PCAP_DIRECTION_OUT if the packet has been emitted
 * @param data pointer to the data to write
 * @param len data to write in the pcap file. May be smaller than origlen.
 * @param origlen number of bytes of complete data.
 * @returns SSH_OK write is successful
 * @returns SSH_ERROR an error happened.
 */
int ssh_pcap_context_write(ssh_pcap_context ctx,enum ssh_pcap_direction direction
		, void *data, uint32_t len, uint32_t origlen){
	ssh_buffer ip;
	int err;
	if(ctx==NULL || ctx->file ==NULL)
		return SSH_ERROR;
	if(ctx->connected==0)
		if(ssh_pcap_context_connect(ctx)==SSH_ERROR)
			return SSH_ERROR;
	ip=buffer_new();
	if(ip==NULL){
		ssh_set_error_oom(ctx->session);
		return SSH_ERROR;
	}
	/* build an IP packet */
	/* V4, 20 bytes */
	buffer_add_u8(ip,4 << 4 | 5);
	/* tos */
	buffer_add_u8(ip,0);
	/* total len */
	buffer_add_u16(ip,htons(origlen + TCPIPHDR_LEN));
	/* IP id number */
	buffer_add_u16(ip,htons(ctx->file->ipsequence));
	ctx->file->ipsequence++;
	/* fragment offset */
	buffer_add_u16(ip,htons(0));
	/* TTL */
	buffer_add_u8(ip,64);
	/* protocol TCP=6 */
	buffer_add_u8(ip,6);
	/* checksum */
	buffer_add_u16(ip,0);
	if(direction==SSH_PCAP_DIR_OUT){
		buffer_add_u32(ip,ctx->ipsource);
		buffer_add_u32(ip,ctx->ipdest);
	} else {
		buffer_add_u32(ip,ctx->ipdest);
		buffer_add_u32(ip,ctx->ipsource);
	}
	/* TCP */
	if(direction==SSH_PCAP_DIR_OUT){
		buffer_add_u16(ip,ctx->portsource);
		buffer_add_u16(ip,ctx->portdest);
	} else {
		buffer_add_u16(ip,ctx->portdest);
		buffer_add_u16(ip,ctx->portsource);
	}
	/* sequence number */
	if(direction==SSH_PCAP_DIR_OUT){
		buffer_add_u32(ip,ntohl(ctx->outsequence));
		ctx->outsequence+=origlen;
	} else {
		buffer_add_u32(ip,ntohl(ctx->insequence));
		ctx->insequence+=origlen;
	}
	/* ack number */
	if(direction==SSH_PCAP_DIR_OUT){
		buffer_add_u32(ip,ntohl(ctx->insequence));
	} else {
		buffer_add_u32(ip,ntohl(ctx->outsequence));
	}
	/* header len = 20 = 5 * 32 bits, at offset 4*/
	buffer_add_u8(ip,5 << 4);
	/* flags */
	buffer_add_u8(ip,TH_PUSH | TH_ACK);
	/* window */
	buffer_add_u16(ip,htons(65535));
	/* checksum */
	buffer_add_u16(ip,htons(0));
	/* urgent data ptr */
	buffer_add_u16(ip,0);
	/* actual data */
	buffer_add_data(ip,data,len);
	err=ssh_pcap_file_write_packet(ctx->file,ip,origlen + TCPIPHDR_LEN);
	buffer_free(ip);
	return err;
}

/** @brief sets the pcap file used to trace the session
 * @param current session
 * @param pcap an handler to a pcap file. A pcap file may be used in several
 * sessions.
 * @returns SSH_ERROR in case of error, SSH_OK otherwise.
 */
int ssh_set_pcap_file(ssh_session session, ssh_pcap_file pcap){
	ssh_pcap_context ctx=ssh_pcap_context_new(session);
	if(ctx==NULL){
		ssh_set_error_oom(session);
		return SSH_ERROR;
	}
	ctx->file=pcap;
	if(session->pcap_ctx)
		ssh_pcap_context_free(session->pcap_ctx);
	session->pcap_ctx=ctx;
	return SSH_OK;
}


#endif /* WITH_PCAP */
/** @} */
/* vim: set ts=2 sw=2 et cindent: */
