/*
 * Thread to capture packets from a network
 *
 * Copyright (C) 2003--2007  Enrico Zini <enrico@debian.org>
 *
 * This 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.
 *
 * This 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 this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
 */

#include "util/netwatcher.h"
#include "util/output.h"

#include <wibble/sys/thread.h>
#include <wibble/sys/mutex.h>

extern "C" {
#include <pcap.h>
#include <libnet.h>
}

#include <list>
#include <queue>

#include <netinet/in.h>	// ntohs, htons, ...

using namespace std;
using namespace wibble::sys;

namespace util {

static NetWatcher* instance = 0;
static bool requested = false;

void NetWatcher::configure(const std::string& iface)
{
	if (instance)
	{
		delete instance;
		instance = 0;
	}
	instance = new NetWatcher(iface);
}

NetWatcher& NetWatcher::get()
{
	if (!requested)
	{
		// Don't start unless something needs it
		util::Starter::get().add(instance, 100);
		requested = true;
	}
	return *instance;
}


const int NetWatcher::captureSize = LIBNET_ETH_H + \
						   (LIBNET_ARP_H > LIBNET_IPV4_H ? LIBNET_ARP_H : LIBNET_IPV4_H) + \
						   (LIBNET_UDP_H > ICMP_ECHO ? LIBNET_UDP_H : ICMP_ECHO) + 300;


NetWatcher::NetWatcher(const string& iface)
	: iface(iface), pcap_interface(0), _canceled(false)
{
}

NetWatcher::~NetWatcher()
{
	shutdown();
	if (pcap_interface)
		pcap_close(pcap_interface);
}

void NetWatcher::startableStart()
{
	char errbuf[PCAP_ERRBUF_SIZE];

	// TODO: Try to use 1 from "promisc": maybe there won't be a need to setup an
	// address on the interface
	if (!(pcap_interface = pcap_open_live (
					(char*)iface.c_str(), captureSize, 1, 1000, errbuf)))
		throw wibble::exception::Pcap (errbuf, "initializing pcap packet capture library");

	return start();
}

void NetWatcher::shutdown()
{
	if (!_canceled)
		try {
			cancel();
			join();
			_canceled = true;
		} catch (wibble::exception::System& e) {
			warning("%s when shutting down NetWatcher\n", e.what());
		}
}

void NetWatcher::addARPListener(PacketListener* pl)
{
	MutexLock lock(listenersMutex);
	listeners_arp.push_back(pl);
}

void NetWatcher::addEthernetListener(PacketListener* pl)
{
	MutexLock lock(listenersMutex);
	listeners_ethernet.push_back(pl);
}

void NetWatcher::NetWatcher::addDHCPListener(PacketListener* pl)
{
	MutexLock lock(listenersMutex);
	listeners_dhcp.push_back(pl);
}

void NetWatcher::addICMPListener(PacketListener* pl)
{
	MutexLock lock(listenersMutex);
	listeners_icmp.push_back(pl);
}

/*
static void memdump(const string& prefix, unsigned char* mem, int size) throw ()
{
	warning("%.*s", PFSTR(prefix));
	for (int i = 0; i < size; i++)
	{
		warning(" %02x", (int)mem[i]);
	}
	warning("\n");
}
*/

void* NetWatcher::main()
{
	// Let the signals be caught by some other process
	sigset_t sigs, oldsigs;
	sigfillset(&sigs);
	sigdelset(&sigs, SIGFPE);
	sigdelset(&sigs, SIGILL);
	sigdelset(&sigs, SIGSEGV);
	sigdelset(&sigs, SIGBUS);
	sigdelset(&sigs, SIGABRT);
	sigdelset(&sigs, SIGIOT);
	sigdelset(&sigs, SIGTRAP);
	sigdelset(&sigs, SIGSYS);
	pthread_sigmask(SIG_SETMASK, &sigs, &oldsigs);

	try {
		while (true)
		{
			struct pcap_pkthdr* pcap_header;
			unsigned char* packet;
			int err = pcap_next_ex(pcap_interface, &pcap_header, (const u_char**)&packet);
			switch (err)
			{
				case 1: break;    // ok
				case 0: continue; // timeout expired, try again
				default:
					throw wibble::exception::Pcap(
							pcap_geterr(pcap_interface),
							"getting a new packet from the network");
			}

			NetBuffer pkt(packet, captureSize, false);

			MutexLock lock(listenersMutex);
			if (!listeners_ethernet.empty())
				for (list<PacketListener*>::iterator i = listeners_ethernet.begin();
						i != listeners_ethernet.end(); i++)
					(*i)->handleEthernet(pkt);

			const libnet_ethernet_hdr* packet_header = pkt.cast<struct libnet_ethernet_hdr>();
			if (ntohs (packet_header->ether_type) == ETHERTYPE_ARP)
			{
				if (!listeners_arp.empty())
				{
					// ARP packet
					NetBuffer arp = pkt.after(LIBNET_ETH_H);
					for (list<PacketListener*>::iterator i = listeners_arp.begin();
							i != listeners_arp.end(); i++)
						(*i)->handleARP(arp);
				}
			}
			else if (ntohs (packet_header->ether_type) == ETHERTYPE_IP)
			{
				if (!(listeners_dhcp.empty() || listeners_icmp.empty()))
				{
					// IPv4 packet
					NetBuffer ipv4 = pkt.after(LIBNET_ETH_H);

					const libnet_ipv4_hdr* ipv4_header = ipv4.cast<struct libnet_ipv4_hdr>();
/* If needed again, add a memdump method to NetBuffer
debug("--IP proto %d src %x dst %x len %d\n",
			(int)ipv4_header->ip_p,
			*(int*)&(ipv4_header->ip_src),
			*(int*)&(ipv4_header->ip_dst),
			(int)(ipv4_header->ip_hl*4));
memdump("IP: ", (unsigned char*)ipv4_header, (int)(ipv4_header->ip_hl*4) + LIBNET_UDP_H + 200);
*/

					if (ipv4_header->ip_p == IPPROTO_UDP && *(uint32_t*)&(ipv4_header->ip_dst) == 0xffffffff)
					{
						// UDP packet
						NetBuffer udp = ipv4.after(ipv4_header->ip_hl*4);
						const libnet_udp_hdr* udp_header = udp.cast<struct libnet_udp_hdr>();
						int sport = ntohs(udp_header->uh_sport);
						int dport = ntohs(udp_header->uh_dport);
//memdump("UDP: ", (unsigned char*)udp_header, LIBNET_UDP_H + 20);
//debug("---UDP %d->%d len %d\n", sport, dport, ntohs(udp_header->uh_ulen));

						if (sport == 67 && dport == 68)
						{
//debug("----DHCP\n");
							// DHCP packet
							NetBuffer dhcp = udp.after(LIBNET_UDP_H);
							for (list<PacketListener*>::iterator i = listeners_dhcp.begin();
									i != listeners_dhcp.end(); i++)
								(*i)->handleDHCP(dhcp);
						}
					} else if (ipv4_header->ip_p == IPPROTO_ICMP) {
						// ICMP packet
						NetBuffer icmp = ipv4.after(ipv4_header->ip_hl*4);
						for (list<PacketListener*>::iterator i = listeners_icmp.begin();
								i != listeners_icmp.end(); i++)
							(*i)->handleICMP(icmp);
					}
				}
			}
		}
	} catch (std::exception& e) {
		error("%s.  Quitting NetWatcher thread.\n", e.what());
	}
	return 0;
}

}

// vim:set ts=4 sw=4:
