#ifdef WIN32
#include <winsock2.h>
#pragma comment(lib,"wininet.lib")
#pragma comment(lib,"Ws2_32.lib")
#else
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#endif
#include <string>
#include <set>
#include <sstream>
using namespace std;
#include "dhcpserv.h"

#ifdef WIN32
typedef int socklen_t;
#endif

extern "C" {
int startDHCPServer(void * server){
	return ((DHCPserv*)server)->start();
}
int stopDHCPServer(void * server){
	return ((DHCPserv*)server)->stop();
}
// Creates DHCP server
void* createDHCPServer(){
	return (void*) new DHCPserv();
}
// Deletes the server
void destroyDHCPServer(void * server){
	delete (DHCPserv*)server;
}
// Sets an option in the server
void setDHCPOption(void * server, char* name, unsigned int namelen, char* opt, unsigned int optlen){
	string namestr(name,namelen);
	string optstr(opt,optlen);
	((DHCPserv*)server)->setOption(namestr,optstr);
}
// Gets the DHCP log
unsigned char * getDHCPLog(void * server, unsigned long * size){
	string * log = ((DHCPserv*)server)->getLog();
	*size = (unsigned long)log->size();
	unsigned char* res = (unsigned char*)malloc(*size);
	memcpy(res, log->data(), *size);
	log->clear();
	return res;
}
}

//Gets IP of default interface, or at least default interface to 8.8.8.8
unsigned int getLocalIp(){
	//get socket
	SOCKET smellySock = socket(AF_INET, SOCK_DGRAM, 0);
	if (smellySock == INVALID_SOCKET)
	{
		return 0;
	}
	
	//Se up server socket address
	struct sockaddr_in server;
	server.sin_family = AF_INET;
	server.sin_addr.s_addr = 0x08080808; //8.8.8.8
	connect(smellySock, (const sockaddr*) &server, sizeof(server));

	struct sockaddr_in myaddr;
	socklen_t size = sizeof(myaddr);
	getsockname(smellySock, (struct sockaddr*)&myaddr, &size);
#ifdef WIN32
	closesocket(smellySock);
#else
	close(smellySock);
#endif
	return ntohl(*((unsigned int*) & myaddr.sin_addr));
}

// Creates a DHCP option of format type length value
string dhcpoption(unsigned char type, string val){
	string ret(1,type);
	ret.append(1, (char) val.length()).append(val);
	return ret;
}

// Creates a DHCP option with no value
string dhcpoption(unsigned char type){
	string ret(1,type);
	ret.append(1, '\x00');
	return ret;
}

//convert long to IP binary string
string iton(unsigned int ip){
	unsigned int source = htonl(ip);
	string res((char *)&source,4);
	return res;
}

//constructor
DHCPserv::DHCPserv(): options(), log(), thread(NULL), smellySock(0), shuttingDown(false){
}

// Sets an option
void DHCPserv::setOption(string option, string value){
	options[option] = value;
}
// Asks server to stop
int DHCPserv::stop(){
	shuttingDown = true;
	DWORD res = 0xffffffff;
	if(thread != NULL)
		res = WaitForSingleObject(thread, 5000);
	thread = NULL;
	return res;
}

// Method to pass to CreateThread
DWORD WINAPI runDHCPServer(void* server)
{
	return ((DHCPserv*)server)->run();
}

// Starts server
int DHCPserv::start()
{
	//reset log
	log.clear();

	//get socket
	smellySock = socket(AF_INET, SOCK_DGRAM, 0);
	if (smellySock == INVALID_SOCKET)
	{
		return invalidSocket;
	}

	//Se up server socket address
	struct sockaddr_in server;
	server.sin_family = AF_INET;
	server.sin_port = htons(dhcpServPort);
	server.sin_addr.s_addr = 0; //a.k.a. INADDR_ANY

	// Get local IP
	string hoststr("SRVHOST");
	if (options.count(hoststr) > 0)
	{
		myIp = ntohl(inet_addr(options[string(hoststr)].c_str()));
		server.sin_addr.s_addr = htonl(myIp);
	}
	else
	{
		myIp = getLocalIp();
	}

	if (myIp == 0 || myIp == 0xffffffff)
	{
		return ipError;
	}

	// Bind address to socket
	if (bind(smellySock, (struct sockaddr *)&server, sizeof(server)) != 0)
	{
		return bindError;
	}

	// Now run it!
	thread = CreateThread(NULL, 0, &runDHCPServer, this, 0, NULL);
	if (thread != NULL)
	{
		return ERROR_SUCCESS;
	}

	return GetLastError();
}

// Determines whether ip option should be updated based on option name
void DHCPserv::ipOptionCheck(unsigned int * defaultOption, char * option)
{
	string s(option);

	if (options.count(s) > 0)
	{
		(*defaultOption) = ntohl(inet_addr(options[s].c_str()));
	}
}
// Determines whether string option should be updated based on option name
void DHCPserv::stringOptionCheck(string * defaultOption, char * option)
{
	string s(option);
	if (options.count(s) > 0)
	{
		(*defaultOption) = options[s];
	}
}
//Internal run method that does all the hard work
int DHCPserv::run()
{
	//All IP-related options are in host byte order
	//get SRVHOST
	if (myIp == 0)
	{
		return localIpError; //No default route?
	}

	string ipString = iton(myIp);

	//get DHCPIPSTART
	unsigned int startIp = myIp + 1; //default start is just above our IP
	ipOptionCheck(&startIp, "DHCPIPSTART");
	unsigned int currentIp = startIp;

	//get DHCPIPEND
	unsigned int endIp = (myIp | 0xFFFFFF00) + 0xFE; //last octet is .254
	ipOptionCheck(&endIp, "DHCPIPEND");

	//get NETMASK
	unsigned int netmask = 0xFFFFFF00; //default class C
	ipOptionCheck(&netmask, "NETMASK");

	//get ROUTER
	unsigned int router = myIp; //we are default ROUTER
	ipOptionCheck(&router,"ROUTER");

	//get DNSSERVER
	unsigned int dnsServer = myIp; //we are default DNSSERVER
	ipOptionCheck(&dnsServer, "DNSSERVER");

	//get BROADCAST
	unsigned int broadcast = INADDR_BROADCAST; //Mandatory for some clients
	ipOptionCheck(&broadcast, "BROADCAST");

	//get SERVEONCE
	bool serveOnce = true;
	string soncestr("SERVEONCE");
	if (options.count(soncestr) > 0)
	{
		serveOnce = atoi(options[soncestr].c_str()) != 0 || (options[soncestr].at(0) | 0x20) == 't';
	}

	//get PXE
	bool servePXE = true;
	string pxestring("PXE");

	if (options.count(pxestring) > 0)
	{
		servePXE = atoi(options[pxestring].c_str()) != 0 || (options[pxestring].at(0) | 0x20) == 't';
	}

	//get HOSTNAME
	string hostname; //hostname to give out
	stringOptionCheck(&hostname, "HOSTNAME");

	//get HOSTSTART
	unsigned int servedOver = 0;
	string hoststr("HOSTSTART");
	if(options.count(hoststr) > 0)
		servedOver = atoi(options[hoststr].c_str());

	//get DHCP filename
	string fileName("update1");
	stringOptionCheck(&fileName, "FILENAME");
	fileName.append(128 - fileName.length(), '\x00');

	//get pxelinux conf filename
	string pxeConfigFile("update2");
	stringOptionCheck(&pxeConfigFile, "PXECONF");

	string pxeAltConfigFile("update0");
	stringOptionCheck(&pxeAltConfigFile, "PXEALTCONF");

	string pxePathPrefix("");
	//get DHCP parameters
	unsigned int leaseTime = 600;
	unsigned int relayIp = 0; // relay ip - not currently suported
	unsigned int pxeRebootTime = 2000;

	//Se up broadcast socket address
	struct sockaddr_in broadcastAddr;
	broadcastAddr.sin_family = AF_INET;
	broadcastAddr.sin_port = htons(68);
	broadcastAddr.sin_addr.s_addr = htonl(broadcast); //a.k.a. inet_addr("255.255.255.255")
	int value = 1;

	if (setsockopt(smellySock, SOL_SOCKET, SO_BROADCAST, (char*)&value, sizeof(value)) != 0)
	{
		return setsockoptError;
	}

	// Setup timeout
	fd_set sockSet;
	int n;
	struct timeval tv;
	tv.tv_sec = 0;
	tv.tv_usec = 100;

	// Setup tracker for served yet
	set<string> served;

	//Main packet-handling loop
	while (true)
	{
		//Wait for request
		FD_ZERO(&sockSet);
		FD_SET(smellySock, &sockSet);
		n = select(1, &sockSet, NULL, NULL, &tv);

		if (n == 0)  //Timeout
		{
			if (!shuttingDown)
			{
				continue;
			}
			else //we're done!
			{
				break;
			}
		}
		else if (n == -1)
		{
			break; //Error
		}

		//Get request
		char receiveBuf[bufferSize];
		struct sockaddr client;
		socklen_t clientLength = sizeof(client);
		int receiveSize = recvfrom(smellySock, receiveBuf, bufferSize, 0, &client, &clientLength);
		if (receiveSize <= 240) break; //Error would be -1, DHCP packet must be at least 240
		//String-ize it!
		string receivedPacket(receiveBuf,receiveSize);
		char type = receivedPacket.at(0);
		//char hwtype = receivedPacket.at(1);
		char hwlen = receivedPacket.at(2);
		//char hops = receivedPacket.at(3); 
		string txid = receivedPacket.substr(4,4); //like buf[4..7]
		string elapsed = receivedPacket.substr(8,2);
		string flags = receivedPacket.substr(10,2);
		string clientip = receivedPacket.substr(12,4);
		string givenip = receivedPacket.substr(16,4);
		string nextip = receivedPacket.substr(20,4);
		string receivedRelayip = receivedPacket.substr(24,4);
		string clienthwaddr = receivedPacket.substr(28,hwlen);
		string servhostname = receivedPacket.substr(44,64);
		string filename = receivedPacket.substr(108,128);
		string magic = receivedPacket.substr(236,4);
		
		if (type != Request || magic.compare(DHCPMagic) != 0)
		{
			continue; //Verify DHCP request
		}

		unsigned char messageType = 0;
		bool pxeclient = false;

		// options parsing loop
		unsigned int spot = 240;
		while (spot < receivedPacket.length() - 3)
		{
			unsigned char optionType = receivedPacket.at(spot);
			if (optionType == 0xff)
			{
				break;
			}

			unsigned char optionLen = receivedPacket.at(spot + 1);
			string optionValue = receivedPacket.substr(spot + 2, optionLen);
			spot = spot + optionLen + 2;

			if (optionType == 53)
			{
				messageType = optionValue.at(0);
			}
			else if (optionType == 150)
			{
				pxeclient = true;
			}
		}
		
		if (pxeclient == false && servePXE == true)
		{
			continue;//No tftp server request; ignoring (probably not PXE client)
		}

		// prepare response
		ostringstream pkt;
		pkt << Response;
		pkt << receivedPacket.substr(1,7); //hwtype, hwlen, hops, txid
		string elaspedFlags("\x00\x00\x00\x00",4); //elapsed, flags
		pkt << elaspedFlags; 
		pkt << clientip;
		if (messageType == DHCPDiscover)
		{
			// give next ip address (not super reliable high volume but it should work for a basic server)
			currentIp += 1;
			if (currentIp > endIp)
			{
				currentIp = startIp;
			}
		}
		
		pkt << iton(currentIp);
		pkt << ipString; //next server ip
		pkt << iton(relayIp);
		pkt << receivedPacket.substr(28,16); //client hw address
		pkt << servhostname;
		pkt << fileName;
		pkt << DHCPMagic;
		pkt << "\x35\x01"; //Option

		if (messageType == DHCPDiscover)
		{  //DHCP Discover - send DHCP Offer
			pkt << DHCPOffer;
		}
		else if (messageType == DHCPRequest)
		{ //DHCP Request - send DHCP ACK
			pkt << DHCPAck;

			if (servedOver != 0) // NOTE: this is sufficient for low-traffic net
			{
				servedOver += 1;
			}
		}
		else
		{
			continue; //ignore unknown DHCP request
		}

		// Options!
		pkt << dhcpoption(OpDHCPServer, ipString);
		pkt << dhcpoption(OpLeaseTime, iton(leaseTime));
		pkt << dhcpoption(OpSubnetMask, iton(netmask));
		pkt << dhcpoption(OpRouter, iton(router));
		pkt << dhcpoption(OpDns, iton(dnsServer));
		string pxemagic(PXEMagic,4);
		pkt << dhcpoption(OpPXEMagic, pxemagic);

		// check if already served based on hw addr (MAC address)
		if (serveOnce == true && served.count(clienthwaddr) > 0)
		{
			pkt << dhcpoption(OpPXEConfigFile, pxeAltConfigFile); //Already served; allowing normal boot
		}
		else
		{
			pkt << dhcpoption(OpPXEConfigFile, pxeConfigFile);
			if (messageType == DHCPRequest)
			{
				log.append(clienthwaddr);
				log.append(iton(currentIp));
			}
		}

		pkt << dhcpoption(OpPXEPathPrefix, pxePathPrefix);
		pkt << dhcpoption(OpPXERebootTime, iton(pxeRebootTime));

		if ( hostname.length() > 0 )
		{
			ostringstream sendHostname;
			sendHostname << hostname;
			if (servedOver != 0)
			{
				sendHostname << servedOver;
			}
			pkt << dhcpoption(OpHostname, sendHostname.str());
		}

		pkt << dhcpoption(OpEnd);
		string sendPacket = pkt.str();

		// now mark as served. We will then ignore their discovers 
		//(but we'll respond to requests in case a packet was lost)
		if (messageType == DHCPRequest)
		{
			served.insert(clienthwaddr);
		}

		//Send response
		int sent = sendto(smellySock, sendPacket.c_str(), (int)sendPacket.length(), 0, (struct sockaddr*)&broadcastAddr, (int)sizeof(broadcastAddr));

		if (sent != (int)sendPacket.length())
		{
			break; //Error
		}
	}
#ifdef WIN32
	closesocket(smellySock);
#else
	close(smellySock);
#endif
	return 0;
}