/*
 * Encapsulate access to a network interface
 *
 * Copyright (C) 2003  Enrico Zini <enrico@debian.org>
 *
 * Interface configuration routines are adapted from
 * Laptop-net, Copyright 2002 Massachusetts Institute of Technology
 *
 * 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
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#else
#warning No config.h found: using fallback values
#define IFCONFIG "/sbin/ifconfig"
#endif

#include "IFace.h"

#include <string.h>		// strncpy
#include <sys/types.h>	// socket
#include <sys/socket.h>	// socket
#include <sys/ioctl.h>	// ioctl
//#include <net/if.h>
#include <unistd.h>		// close
#include <sys/wait.h>	/* wait */
#include <errno.h>
#include <unistd.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <linux/sockios.h>
#include <linux/if_ether.h>
#include <linux/if.h>
#include <syslog.h>
#include <string.h>
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <assert.h>

#include <sstream>

#include "util/output.h"

#ifndef SIOCGMIIPHY
#define SIOCGMIIPHY (SIOCDEVPRIVATE)			/* Get the PHY in use. */
#define SIOCGMIIREG (SIOCDEVPRIVATE+1)			/* Read a PHY register. */
#endif

#define IFP_VALID_ADDR 0x1
#define IFP_VALID_DSTADDR 0x2
#define IFP_VALID_BROADADDR 0x4
#define IFP_VALID_NETMASK 0x8
#define IFP_VALID_ALL 0xF

#define IFP_ALL_ADDRESSES_VALID(ifp)									\
  ((((ifp) -> addr_flags) & IFP_VALID_ALL) == IFP_VALID_ALL)

using namespace std;



#include "ethtool-local.h"
#if 0
#include "wireless.h"
#endif





typedef enum { IFSTATUS_UP, IFSTATUS_DOWN, IFSTATUS_ERR } interface_status_t;

static interface_status_t interface_detect_beat_mii(int fd, const char *iface)
{
    struct ifreq ifr;
    
    memset(&ifr, 0, sizeof(ifr));
    strncpy(ifr.ifr_name, iface, sizeof(ifr.ifr_name)-1);

    if (ioctl(fd, SIOCGMIIPHY, &ifr) == -1)
		throw wibble::exception::MII("SIOCGMIIPHY failed");

    ((unsigned short*) &ifr.ifr_data)[1] = 1;

    if (ioctl(fd, SIOCGMIIREG, &ifr) == -1)
		throw wibble::exception::MII("SIOCGMIIREG failed");

    return (((unsigned short*) &ifr.ifr_data)[3] & 0x0016) == 0x0004 ? IFSTATUS_UP : IFSTATUS_DOWN;
}

static interface_status_t interface_detect_beat_priv(int fd, const char *iface)
{
    struct ifreq ifr;
    
    memset(&ifr, 0, sizeof(ifr));
    strncpy(ifr.ifr_name, iface, sizeof(ifr.ifr_name)-1);

    if (ioctl(fd, SIOCDEVPRIVATE, &ifr) == -1)
		throw wibble::exception::MII("SIOCDEVPRIVATE failed");

    ((unsigned short*) &ifr.ifr_data)[1] = 1;

    if (ioctl(fd, SIOCDEVPRIVATE+1, &ifr) == -1)
		throw wibble::exception::MII("SIOCDEVPRIVATE+1 failed");

    return (((unsigned short*) &ifr.ifr_data)[3] & 0x0016) == 0x0004 ? IFSTATUS_UP : IFSTATUS_DOWN;
}

static interface_status_t interface_detect_beat_ethtool(int fd, const char *iface)
{
    struct ifreq ifr;
    struct ethtool_value edata;

    memset(&ifr, 0, sizeof(ifr));
    strncpy(ifr.ifr_name, iface, sizeof(ifr.ifr_name)-1);

    edata.cmd = ETHTOOL_GLINK;
    ifr.ifr_data = (caddr_t) &edata;

    if (ioctl(fd, SIOCETHTOOL, &ifr) == -1)
		throw wibble::exception::MII("ETHTOOL_GLINK failed");

    return edata.data ? IFSTATUS_UP : IFSTATUS_DOWN;
}

#if 0
static int get_wlan_qual_old(const char *iface)
{
    FILE *f;
    char buf[256];
    char *bp;
    int l, q = -1;
    
    l = strlen(iface);
    
    if (!(f = fopen("/proc/net/wireless", "r")))
		throw wibble::exception::MII("Failed to open /proc/net/wireless");
    
    while (fgets(buf, sizeof(buf)-1, f)) {
        bp = buf;

        while (*bp && isspace(*bp))
            bp++;
        
        if(!strncmp(bp, iface, l) && bp[l]==':') {

            /* skip device name */
            if (!(bp = strchr(bp,' ')))
                break;

            bp++;
            
            /* skip status */
            if (!(bp = strchr(bp,' ')))
                break;

            q = atoi(bp);
            break;
        };
    }

    fclose(f);

    if (q < 0)
		throw wibble::exception::MII("Failed to find interface in /proc/net/wireless");

    return q;
}

static int get_wlan_qual_new(int fd, const char *iface)
{
    struct iwreq req;
    struct iw_statistics q;
    static struct iw_range range;

    memset(&req, 0, sizeof(req));
    strncpy(req.ifr_ifrn.ifrn_name, iface, IFNAMSIZ);

    req.u.data.pointer = (caddr_t) &q;
    req.u.data.length = sizeof(q);
    req.u.data.flags = 1;

    if (ioctl(fd, SIOCGIWSTATS, &req) < 0)
		throw wibble::exception::MII("Failed to get interface quality");

    memset(&req, 0, sizeof(req));
    strncpy(req.ifr_ifrn.ifrn_name, iface, IFNAMSIZ);

    memset(&range, 0, sizeof(struct iw_range));
    req.u.data.pointer = (caddr_t) &range;
    req.u.data.length = sizeof(struct iw_range);
    req.u.data.flags = 0;
     
    if (ioctl(fd, SIOCGIWRANGE, &req) < 0)
		throw wibble::exception::MII("SIOCGIWRANGE failed");
    
    /* Test if both qual and level are on their lowest level */
    if (q.qual.qual <= 0 &&
        (q.qual.level > range.max_qual.level ? q.qual.level <= 156 : q.qual.level <= 0))
        return 0;

    return 1;
}


static int is_assoc_ap(uint8_t mac[ETH_ALEN]) throw ()
{
    int b, j;
    b = 1;
    
    for (j = 1; j < ETH_ALEN; j++)
        if (mac[j] != mac[0]) {
            b = 0;
            break;
        }

    return !b || (mac[0] != 0xFF && mac[0] != 0x44 && mac[0] != 0x00);
}

static interface_status_t interface_detect_beat_wlan(int fd, const char *iface)
{
    uint8_t mac[6];
    int q;
    struct iwreq req;

    memset(&req, 0, sizeof(req));
    strncpy(req.ifr_ifrn.ifrn_name, iface, IFNAMSIZ);
     
    if (ioctl(fd, SIOCGIWAP, &req) < 0)
		throw wibble::exception::MII("Failed to get AP address");

    memcpy(mac, &(req.u.ap_addr.sa_data), ETH_ALEN);
           
    if (!is_assoc_ap(mac))
        return IFSTATUS_DOWN;

    if ((q = get_wlan_qual_new(fd, iface)) < 0)
        if ((q = get_wlan_qual_old(iface)) < 0)
			throw wibble::exception::MII("Failed to get wireless link quality");
    
    return q > 0 ? IFSTATUS_UP : IFSTATUS_DOWN;
}
#endif

static interface_status_t (*cached_detect_beat_func)(int, const char*) = NULL;

static interface_status_t detect_beat_auto(int fd, const char *iface)
{
	if (cached_detect_beat_func)
		try {
			return cached_detect_beat_func(fd, iface);
		} catch (wibble::exception::MII& e) {
			verbose("Link beat detection (cached) failed: %s\n", e.desc().c_str());
		}

	try {
		interface_status_t status = interface_detect_beat_mii(fd, iface);
		cached_detect_beat_func = interface_detect_beat_mii;
		return status;
	} catch (wibble::exception::MII& e) {
		verbose("Link beat detection (mii) failed: %s\n", e.desc().c_str());
	}

	try {
		interface_status_t status = interface_detect_beat_ethtool(fd, iface);
		cached_detect_beat_func = interface_detect_beat_ethtool;
		return status;
	} catch (wibble::exception::MII& e) {
		verbose("Link beat detection (ethtool) failed: %s\n", e.desc().c_str());
	}

#if 0
	try {
		interface_status_t status = interface_detect_beat_wlan(fd, iface);
		cached_detect_beat_func = interface_detect_beat_wlan;
		return status;
	} catch (wibble::exception::MII& e) {
		verbose("Link beat detection (wlan) failed: %s\n", e.desc().c_str());
	}
#endif

	try {
		interface_status_t status = interface_detect_beat_priv(fd, iface);
		cached_detect_beat_func = interface_detect_beat_priv;
		return status;
	} catch (wibble::exception::MII& e) {
		verbose("Link beat detection (priv) failed: %s\n", e.desc().c_str());
	}

	verbose("No working link beat detection function available for interface %s\n", iface);
	return IFSTATUS_ERR;
}



#define READ_ADDR(symbol, field, addr_flag)								\
{																		\
  if ((ioctl (_socket, symbol, _ifr)) < 0)									\
	{																	\
	  if (errno != EADDRNOTAVAIL)										\
		  throw wibble::exception::IFace(string("getting " #field " for ") + name()); \
	  (ifp -> addr_flags) &=~ (addr_flag);								\
	}																	\
  else																	\
	{																	\
	  (ifp -> field)													\
		= (* ((struct sockaddr_in *) (& (_ifr -> ifr_##field))));			\
	  (ifp -> addr_flags) |= (addr_flag);								\
	}																	\
}

void IFace::read_interface_configuration(struct if_params *ifp)
	throw (wibble::exception::IFace)
{
	if ((ioctl (_socket, SIOCGIFFLAGS, _ifr)) < 0)
		throw wibble::exception::IFace(string("getting interface flags for ") + name());
	ifp->flags = _ifr->ifr_flags;

	(ifp -> addr_flags) = 0;
	READ_ADDR (SIOCGIFADDR, addr, IFP_VALID_ADDR);
	READ_ADDR (SIOCGIFDSTADDR, dstaddr, IFP_VALID_DSTADDR);
	READ_ADDR (SIOCGIFBRDADDR, broadaddr, IFP_VALID_BROADADDR);
	READ_ADDR (SIOCGIFNETMASK, netmask, IFP_VALID_NETMASK);

  if ((ioctl (_socket, SIOCGIFHWADDR, _ifr)) < 0)
	{
	  if (errno != EADDRNOTAVAIL)
		  throw wibble::exception::IFace(string("getting hwaddr for ") + name());
	  bzero(&(ifp->hwaddr), sizeof(struct sockaddr));
	}
  else
	{
	  ifp->hwaddr = _ifr->ifr_hwaddr;
	}
}

#define WRITE_ADDR(symbol, field, addr_flag)							\
{																		\
  if (((ifp . addr_flags) & (addr_flag)) != 0)							\
	{																	\
      memcpy(&(_ifr->ifr_##field), &(ifp.field), sizeof(struct sockaddr));				\
	  if ((ioctl (_socket, symbol, _ifr)) < 0)					\
		  throw wibble::exception::IFace("setting interface configuration for " + name()); \
	}																	\
}

void IFace::write_interface_configuration(const struct if_params& ifp)
	throw (wibble::exception::IFace)
{
  //writeField(fd, ifr, SIOCSIFADDR);
  WRITE_ADDR (SIOCSIFADDR, addr, IFP_VALID_ADDR);
  WRITE_ADDR (SIOCSIFDSTADDR, dstaddr, IFP_VALID_DSTADDR);
  WRITE_ADDR (SIOCSIFBRDADDR, broadaddr, IFP_VALID_BROADADDR);
  WRITE_ADDR (SIOCSIFNETMASK, netmask, IFP_VALID_NETMASK);

  (_ifr -> ifr_flags) = (ifp . flags);
  if ((ioctl (_socket, SIOCSIFFLAGS, _ifr)) < 0)
	  throw wibble::exception::IFace("setting interface configuration for " + name());
}

IFace::IFace(const string& name) throw (wibble::exception::System, wibble::exception::IFace, wibble::exception::MII)
	: _socket(-1), _ifr(0), _up(false), _run(false), _conn(false), _has_iface(false), _has_mii(true)
{
	_socket = socket(AF_INET, SOCK_DGRAM, 0);
	if (_socket == -1)
		throw wibble::exception::System("opening generic socket");

	_ifr = new struct ifreq;
	int sz = name.size();
	if (sz > IFNAMSIZ - 1) sz = IFNAMSIZ - 1;
	memcpy(_ifr->ifr_name, name.data(), sz);
	_ifr->ifr_name[sz] = 0;

	update();
}

IFace::~IFace() throw ()
{
	if (_ifr)
		delete _ifr;
	if (_socket != -1)
		close(_socket);
}

string IFace::name() const throw ()
{
	return _ifr->ifr_name;
}

void IFace::update() throw (wibble::exception::IFace, wibble::exception::MII)
{
	try {
		if (ioctl(_socket, SIOCGIFFLAGS, _ifr) == -1)
			throw wibble::exception::IFace("getting interface flags for " + name());
			
		_up = (_ifr->ifr_flags & IFF_UP) != 0;
		_run = (_ifr->ifr_flags & IFF_RUNNING) != 0;

		if (!_has_iface)
		{
			//log_info("Interface queries for " + name() + " started working");
			_has_iface = true;
			_up = false;
			_run = false;
			_conn = false;
		}

		if (_has_mii)
		{
			debug("Trying MII detection\n");
			interface_status_t status = detect_beat_auto(_socket, name().c_str());
	
			if (status == IFSTATUS_ERR)
			{
				_has_mii = false;
				// If we have no link beat, consider as we're always connected
				_conn = true;
			} else {
				_conn = (status == IFSTATUS_UP);
			}

			/*
			
			if (ioctl(_socket, SIOCGMIIPHY, _ifr) == -1)
				throw wibble::exception::MII("getting MII informations for " + name());

			unsigned short* ifdata = (unsigned short *)&(_ifr->ifr_data);
			ifdata[1] = 1;
			if (ioctl(_socket, SIOCGMIIREG, _ifr) == -1)
				throw wibble::exception::MII("reading MII register for " + name());

			_conn = (ifdata[3] & 0x0016) == 0x0004;
			*/
		}
	} catch (wibble::exception::MII& e) {
		//log_info("Link beat detection failed for interface " + name() +
		//		": disabling it.  Error was: " +
		//		e.type() + ": " + e.desc());
		verbose("Exception during link beat detection: %s: %s\n", e.type(), e.desc().c_str());
		_has_mii = false;
	} catch (wibble::exception::IFace& e) {
		if (_has_iface)
		{
			//log_info("Interface query failed for interface " + name() +
			//	".	Error was: " + e.type() + ": " + e.desc());
			_has_iface = false;
			_conn = false;
		}
	}

}

static const void storeIP(struct sockaddr_in& field, const char* addr)
{
  memset(&field, 0, sizeof(field));
  field.sin_family = AF_INET;
  if (!inet_aton(addr, &(field.sin_addr)))
	  fatal_error("Invalid address: %s", addr);
}

#define STORE_IP(string, field, addr_flag)						\
  ((memset ((& (ifp . field)), 0, (sizeof (ifp . field)))),		\
   ((ifp . field . sin_family) = AF_INET),						\
   ((inet_aton ((string), (& (ifp . field . sin_addr))))		\
    ? (((ifp . addr_flags) |= (addr_flag)), 1)					\
    : (((ifp . addr_flags) &=~ (addr_flag)), 0)))

/* Initialize the interface (with "ifconfig <iface> up" if it is found down
 * Return true if the interface was down
 */
if_params IFace::initBroadcast(int timeout)
	throw (wibble::exception::IFace)
{
	if_params res;
	read_interface_configuration(&res);

	if_params ifp = res;

	/*
	if (up())
	{
		verbose("Interface %.*s is up\n", PFSTR(name()));
		return;
	}

	if ((((ifp . flags) & IFF_UP) != 0)
			&& (((ifp . flags) & IFF_RUNNING) != 0)
			&& (IFP_ALL_ADDRESSES_VALID (&ifp)))
		return (0);
	*/

	if (/*libnet_workaround*/ false)
	{
		ifp.flags |= IFF_UP | IFF_RUNNING;
		storeIP(ifp.addr, "192.168.254.254");
		storeIP(ifp.dstaddr, "192.168.254.0");
		storeIP(ifp.broadaddr, "192.168.254.255");
		storeIP(ifp.broadaddr, "255.255.255.0");
		ifp.addr_flags |= (IFP_VALID_ADDR | IFP_VALID_DSTADDR | IFP_VALID_BROADADDR | IFP_VALID_NETMASK);
		write_interface_configuration(ifp);
	} else {
		memset(&(_ifr->ifr_addr), 0, sizeof(sockaddr));
		((struct sockaddr_in*)&_ifr->ifr_addr)->sin_family = AF_INET;
		((struct sockaddr_in*)&_ifr->ifr_addr)->sin_addr.s_addr = INADDR_ANY;

		if ((ioctl (_socket, SIOCSIFADDR, _ifr)) < 0)
			throw wibble::exception::IFace("setting interface configuration for " + name());
  
		if ((ioctl (_socket, SIOCGIFFLAGS, _ifr)) < 0)
			throw wibble::exception::IFace(string("getting interface flags for ") + name());
		_ifr->ifr_flags |= IFF_UP | IFF_RUNNING;
		if ((ioctl (_socket, SIOCSIFFLAGS, _ifr)) < 0)
			throw wibble::exception::IFace("setting interface configuration for " + name());

		/*
		storeEmptyIP(ifp.addr);
		storeEmptyIP(ifp.dstaddr);
		storeEmptyIP(ifp.broadaddr);
		storeEmptyIP(ifp.broadaddr);
		ifp.addr_flags &= !(IFP_VALID_ADDR | IFP_VALID_DSTADDR | IFP_VALID_BROADADDR | IFP_VALID_NETMASK);
		//ifp.addr_flags |= (IFP_VALID_ADDR | IFP_VALID_NETMASK);
		ifp.addr_flags |= (IFP_VALID_ADDR);
		*/
	}

	/* Wait a little for the interface to initialize */
	time_t pre_wait = time(0);
	while (time(0) < pre_wait + timeout)
	{
		update();
		if (up())
			return res;
		else
			usleep(300000);
	}
	warning("Interface did not come up in %d seconds\n", timeout);
	return res;
}

if_params IFace::getConfiguration()
	throw (wibble::exception::IFace)
{
	if_params res;
	read_interface_configuration(&res);
	return res;
}

if_params IFace::setConfiguration(const if_params& config)
	throw (wibble::exception::IFace)
{
	if_params res;
	read_interface_configuration(&res);
	write_interface_configuration(config);
	return res;
}

void if_params::print()
{
	string s_addr = addr_flags & IFP_VALID_ADDR    ? inet_ntoa(addr.sin_addr) : "invalid";
	string s_dest = addr_flags & IFP_VALID_DSTADDR ? inet_ntoa(dstaddr.sin_addr) : "invalid";
	string s_bcst = addr_flags & IFP_VALID_BROADADDR ? inet_ntoa(broadaddr.sin_addr) : "invalid";
	string s_mask = addr_flags & IFP_VALID_NETMASK ? inet_ntoa(netmask.sin_addr) : "invalid";

	stringstream str;
	str << s_addr << " dst " << s_dest << " bcast " << s_bcst << " nm "
		 << s_mask << " flags " << flags << " aflags " << addr_flags
		 << " hwaddrtype " << hwaddr.sa_family << endl;
	output("%s", str.str().c_str());
}

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