/*
 * This file is part of DesktopBSD, see the README file.
 *
 * Author: Peter Hofer <ph@desktopbsd.net>
 *			(C) 2004, 2005
 *
 * Copyright: BSD, see the COPYING file included in this distribution
 *
 */

#include "netinterface.h"

#include <sys/param.h> /* __FreeBSD_version */

#include <net/ethernet.h>
#include <net/if_dl.h>
#include <net/if_types.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <net/if_media.h>
#include <sysctl.h>
#include <ifaddrs.h>
#include <net/route.h>
#include <net/if.h>
#if defined(__FreeBSD_version) && __FreeBSD_version >= 800038
#include <net80211/_ieee80211.h>
#endif
#include <net80211/ieee80211.h>
#include <net80211/ieee80211_crypto.h>
#include <net80211/ieee80211_ioctl.h>

#include <qregexp.h>

#include <lan.h>
#include <ipv4addr.h>
#include <ipv4netmask.h>
#include <executor.h>
#include <rcconf.h>
#include <resolver.h>
#include <defaultrouter.h>
#include <wirelessinterface.h>

NetInterface::NetInterface(const QString& devName):
		Device(devName)
{
}


bool  NetInterface::connectTo(const LAN&  lan) {
	bool success = true;

	if(lan.usesDHCP())
		success = setDHCPEnabled(true);
	else {
		QPtrList<IPv4Addr> ns = lan.getNameservers();

		success =  (success &&
						setIP(lan.getIP())
						&& setNetmask(lan.getNetmask())
						&& (ns.count() == 0 || Resolver::setDNSList(ns))
						&& (!lan.hasGateway() || DefaultRouter::set(lan.getGateway().toString()))
						&& up() >= 0);

		if(success)
			setRCConfEntry(getCurrentConf());
	}
	
	return success;
}


/** Sets a new IPv4 address. Disables DHCP if enabled. */
bool  NetInterface::setIP(const IPv4Addr& ip) {
	int  s = newSocket();
	if(s < 0)
		return  false;

	struct	ifreq	ifr;

	memset(&ifr, 0, sizeof(struct ifreq));
	strncpy(ifr.ifr_name, getName(), IFNAMSIZ);

	struct sockaddr_in *addrp = (struct sockaddr_in *) &ifr.ifr_addr;
	addrp->sin_len = sizeof(struct sockaddr_in);
	addrp->sin_family = AF_INET;
	addrp->sin_addr.s_addr = ip.toInetAddr();

	if(ioctl(s, SIOCSIFADDR, &ifr) < 0) {
		close(s);
		return  false;
	}

	close(s);
	return  true;
}


/** Sets a new IPv4 netmask. Disables DHCP when enabled. */
bool  NetInterface::setNetmask(const IPv4Netmask& netmask) {
	int  s = newSocket();
	if(s < 0)
		return  false;

	struct	ifreq  ifr;

	memset(&ifr, 0, sizeof(struct ifreq));
	strncpy(ifr.ifr_name, getName(), IFNAMSIZ);

	struct sockaddr_in *addrp = (struct sockaddr_in *) &ifr.ifr_addr;
	addrp->sin_len = sizeof(struct sockaddr_in);
	addrp->sin_family = AF_INET;
	addrp->sin_addr.s_addr = netmask.toInetAddr();

	if(ioctl(s, SIOCSIFNETMASK, &ifr) < 0) {
		close(s);
		return  false;
	}

	close(s);
	return  true;
}


/** Enables/Disables DHCP.
  *
  * Stops dhclient if it's already running, but does
  * not release the current lease. Also deletes the
  * current gateway (default route) before dhclient is
  * started.
  * 
  */
bool  NetInterface::setDHCPEnabled(bool enable) {
	bool success;
	
#if defined(__FreeBSD_version) && __FreeBSD_version < 600000
	const QString stop = "/bin/kill `/bin/sh -c '. /etc/rc.subr ; . /etc/defaults/rc.conf ; . /etc/rc.conf ; check_pidfile /var/run/dhclient.pid ${dhclient_program}'`";
	const QString start = "/bin/sh -c '. /etc/defaults/rc.conf ; . /etc/rc.conf ; ${dhclient_program} -nw \"%1\"'";

	Executor::exec(stop);
#else
	/* New DHCP client in FreeBSD 6 */
	
	/* TODO dhclient stays in the foreground when no link
	 * is present, even with -b specified; this blocks the
	 * user interface.
	 */
	const QString start = "/bin/sh -c '. /etc/defaults/rc.conf ; . /etc/rc.conf ; ${dhclient_program} -b \"%1\"'";

	/* Dies when the interface is down */
	down();
#endif

	if(enable) {
		/* Don't check return value since it fails when no route was set before */
		DefaultRouter::unset();
		
		success = (Executor::exec(start.arg(getName())) == Executor::NoError);
		setRCConfEntry("DHCP");
	} else
		success = setRCConfEntry(getCurrentConf());

	return success;
}


/** @return TRUE when DHCP is used to configure
  * this interface, otherwise FALSE.
  *
  * @attention The ifconfig_* variables in /etc/rc.conf are
  *				used for this, which can lead to wrong results
  *				because they only define the behaviour on start-up.
  */
bool  NetInterface::isDHCPEnabled() const {
	return  (RCConf::getVar("ifconfig_" + getName()) == "DHCP");
}


/** @returns The link status of the network device.
  * @sa NetInterface::Status
  */
NetInterface::Status NetInterface::getStatus() const {
	Status  status = Invalid;
	struct ifmediareq  *ifmr = getIfMediaReq();
	if(ifmr != NULL) {
		if(ifmr->ifm_status & IFM_AVALID) {
			if(ifmr->ifm_status & IFM_ACTIVE) {
				if(!isWireless())
					status = Active;
				else
					status = Associated;
				} else
					status = NoCarrier;
		}

		delete  ifmr;
	}

	return  status;
}


IPv4Addr NetInterface::getIP() const {
	int  s = newSocket();
	if(s < 0)
		return  IPv4Addr();

	struct	ifreq  ifr;

	bzero(&ifr,  sizeof(struct ifreq));
	strncpy(ifr.ifr_name, getName(), IFNAMSIZ);

	if(ioctl(s, SIOCGIFADDR, &ifr) < 0) {
		close(s);
		return  IPv4Addr();
	}

	close(s);
	return  IPv4Addr(((sockaddr_in *) &ifr.ifr_addr)->sin_addr);
}


IPv4Netmask NetInterface::getNetmask() const {
	int  s = newSocket();
	if(s < 0)
		return  IPv4Netmask();

	struct	ifreq  ifr;

	bzero(&ifr,  sizeof(struct ifreq));
	strncpy(ifr.ifr_name, getName(), IFNAMSIZ);

	if(ioctl(s, SIOCGIFNETMASK, &ifr) < 0) {
		close(s);
		return  IPv4Netmask();
	}

	close(s);
	return  IPv4Netmask(((sockaddr_in *) &ifr.ifr_addr)->sin_addr);
}


/** @return 1 if the interface is up,
  *			0 if the interface is down,
  *			< 0 if an error occured.
  */
int  NetInterface::isUp()  const {
	int  ret = -1;
	struct	ifreq	*ifr = getIfrFlags();
	if(ifr != NULL) {
		ret = (ifr->ifr_flags & IFF_UP) ? 1 : 0;
		delete  ifr;
	}

	return  ret;
}


/** Activates the network interface. */
int NetInterface::up() {
	int  s = newSocket();
	if(s < 0)
		return -1;

	struct	ifreq  ifr;

	memset(&ifr, 0, sizeof(struct ifreq));
	strncpy(ifr.ifr_name, getName(), IFNAMSIZ);

	if(ioctl(s, SIOCGIFFLAGS, &ifr) < 0) {
		close(s);
		return -2;
	}

	if(!(ifr.ifr_flags & IFF_UP)) {
		ifr.ifr_flags |= IFF_UP;
		if(ioctl(s, SIOCSIFFLAGS, &ifr) < 0) {
			close(s);
			return -3;
		}
	}

	close(s);
	return 0;
}


/** Deactivates the network interface. */
int NetInterface::down() {
	int  s = newSocket();
	if(s < 0)
		return -1;

	struct	ifreq  ifr;

	memset(&ifr, 0, sizeof(struct ifreq));
	strncpy(ifr.ifr_name, getName(), IFNAMSIZ);

	if(ioctl(s, SIOCGIFFLAGS, &ifr) < 0) {
		close(s);
		return -2;
	}

	if(ifr.ifr_flags & IFF_UP) {
		ifr.ifr_flags &= ~IFF_UP;
		if(ioctl(s, SIOCSIFFLAGS, &ifr) < 0) {
			close(s);
			return -3;
		}
	}

	close(s);
	return 0;
}


/** Remove the network address specified. */
bool  NetInterface::remove() {
	const QString command = "/sbin/ifconfig '%1' -alias";
	
	return (Executor::exec(command.arg(getName())) == Executor::NoError);
}


/** @return > 0 if this device is a 802.11 wireless
  *			 network device, otherwise 0.
  *			< 0 if the function fails to create a socket.
  *
  * @todo	Use a cleaner solution than just checking if
  *			 the ioctl() call to obtain the SSID fails
  *			 (this can have other reasons, too...)
  */
int  NetInterface::isWireless()  const {
	int  retval,  s = newSocket();
	if(s < 0)
		return  -1;

	uint8_t  data[32];
	struct	ieee80211req	ireq;
	bzero(&ireq,  sizeof ireq);
	strlcpy(ireq.i_name,  getName(),  sizeof ireq.i_name);
	ireq.i_data = &data;
	ireq.i_type = IEEE80211_IOC_SSID;
	ireq.i_val = -1;

	retval =  !ioctl(s,  SIOCG80211,  &ireq);

	close(s);
	return  retval;
}

/** @return > 0 if this device is a loopback
  *			device, otherwise 0 or a value < 0
  *			if an error occurs.
  */
int  NetInterface::isLoopback()  const {
	struct	ifreq	*ifr = getIfrFlags();
	if(ifr != NULL) {
		int  retVal = (ifr->ifr_flags & IFF_LOOPBACK) ? 1 : 0;
		delete  ifr;
		return  retVal;
	}

	return  -1;
}

/** Returns a pointer to an if_mediareq structure
  * that must be free'd with delete or NULL if
  * an error occured.
  */
struct ifmediareq  *NetInterface::getIfMediaReq()  const {
	int  s = newSocket();
	if(s < 0)
		return  NULL;

	struct ifmediareq  *ifmr = new struct ifmediareq;
	memset(ifmr, 0, sizeof(struct ifmediareq));
	strncpy(ifmr->ifm_name, getName(), sizeof ifmr->ifm_name);

	if(ioctl(s, SIOCGIFMEDIA, ifmr) < 0) {
		close(s);
		delete  ifmr;
		return  NULL;
	}

	close(s);
	return  ifmr;
}

/** Returns all network interfaces.
  *
  * @param withLoopback	  List loopback devices.
  * @param withWireless	  List wireless devices
  *
  * Wireless device objects created by this method are
  * guaranteed to be of type WirelessInterface.
  */
QPtrList<NetInterface>  NetInterface::getIfs(bool  withWireless,  bool  withLoopback) {
	QPtrList<NetInterface>  lst;

	struct if_nameindex  *ifidx = if_nameindex();

	struct if_nameindex  *ifptr = ifidx;
	while(ifptr != NULL  &&  ifptr->if_name != NULL) {
		NetInterface  *netIf = new WirelessInterface(ifptr->if_name);

		if((withLoopback  ||  !netIf->isLoopback())
			&& (withWireless  ||  !netIf->isWireless()))
			lst.append(netIf);
		else
			delete  netIf;

		++ifptr;
	}

	if_freenameindex(ifidx);
	return  lst;
}

/** Creates a new datagram socket. */
int  NetInterface::newSocket()  const {
	return  socket(AF_INET, SOCK_DGRAM, 0);
}

/** Returns a pointer to an ifreq structure
  * that must be free'd with delete or NULL if
  * an error occured.
  */
struct ifreq  *NetInterface::getIfrFlags()  const {
	int  s = newSocket();
	if(s < 0)
		return  NULL;

	struct	ifreq  *ifr = new struct ifreq;
	memset(ifr, 0, sizeof(struct ifreq));
	strncpy(ifr->ifr_name, getName(), IFNAMSIZ);

	if(ioctl(s, SIOCGIFFLAGS, ifr) < 0) {
		close(s);
		delete  ifr;
		return  NULL;
	}

	close(s);
	return  ifr;
}


/**
  * Returns the current interface configuration as a
  * string suitable for rc.conf entries.
  * 
  * Does NOT return "DHCP" if the device is configured
  * by dhclient, only it's current IP address and
  * subnet mask.
  * 
  * Can be implemented in subclasses to include other
  * parameters like ssid for wireless devices.
  *
  */
QString NetInterface::getCurrentConf() {
	return QString("inet %1 netmask %1").arg(getIP().toString())
													.arg(getNetmask().toString());
}


/**
  * Sets this device's configuration entry in rc.conf.
  *
  */
bool NetInterface::setRCConfEntry(const QString & s) {
	return RCConf::setVar("ifconfig_" + getName(), s);
}

