/*
 * Perform a wireless interface scan
 *
 * Copyright (C) 2007--2010  Enrico Zini <enrico@debian.org>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include "scanner/iwscan.h"
#include "util/output.h"
#include "options.h"

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

//extern "C" {
#include <iwlib.h>
//}

using namespace std;
using namespace wibble::sys;

namespace scanner {

WirelessScan::WirelessScan(const std::string& name)
	: Scan(name), _hasOpen(false)
{
	bzero(&_mac, sizeof(struct ether_addr));
}

bool WirelessScan::hasMAC() const
{
	struct ether_addr zeromac;
	bzero(&zeromac, sizeof(struct ether_addr));
	return !MAC_MATCHES(&_mac, &zeromac);
}

string WirelessScan::signature() const
{
	string res = "wireless";
	if (hasESSID())
	{
		res += " essid ";
		res += essid();
	}
	if (hasMAC())
	{
		res += " mac ";
		res += fmt(mac());
	}
	if (hasOpen())
	{
		res += " ";
		res += (open() ? "open" : "closed");
	}
	return res;
}

void WirelessScan::finaliseInit()
{
	// Register with the NetWatcher
	IWScan::get().addCandidate(this);
}

bool WirelessScan::matches(const wireless_scan& data)
{
	debug("Testing %s\n", signature().c_str());

	if (hasESSID())
	{
		if (!data.b.has_essid)
		{
			debug("Testing %s/essid: fail as scan did not report essid\n", signature().c_str());
			return false;
		}
		if (_essid != data.b.essid)
		{
			debug("Testing %s/essid: fail as essid \"%s\" is not \"%s\"\n",
				signature().c_str(), data.b.essid, _essid.c_str());
			return false;
		}
		debug("Testing %s: essid passed\n", signature().c_str());
	}
	
	if (hasMAC())
	{
		if (!data.has_ap_addr)
		{
			debug("Testing %s/mac: fail as scan did not report ap mac\n", signature().c_str());
			return false;
		}

		if (data.ap_addr.sa_family != ARPHRD_ETHER)
		{
			debug("Testing %s/mac: fail reported ap mac seems to be an unknown address format (%d instead of %d, see AF_* macros in sys/socket.h)\n", signature().c_str(), data.ap_addr.sa_family, ARPHRD_ETHER);
			return false;
		}

		if (!MAC_MATCHES(&_mac, data.ap_addr.sa_data))
		{
			debug("Testing %s/mac: fail as the two addresses are different (%s != %s)\n",
				signature().c_str(), fmt(_mac).c_str(), fmt(*(struct ether_addr*)data.ap_addr.sa_data).c_str());
			return false;
		}

		debug("Testing %s: mac passed\n", signature().c_str());
	}

	if (hasOpen())
	{
		bool isOpen = data.b.key_flags & IW_ENCODE_DISABLED;
		debug("Testing %s: network %s isOpen is %d, flags %x\n", signature().c_str(), data.b.essid, isOpen, data.b.key_flags);

		if (open() && !isOpen)
		{
			debug("Testing %s/open: failed because network is closed\n", signature().c_str());
			return false;
		}
		if (!open() && isOpen)
		{
			debug("Testing %s/closed: failed because network is open\n", signature().c_str());
			return false;
		}
		debug("Testing %s: %s network passed\n", signature().c_str(), open() ? "open" : "closed");
	}

	debug("Testing %s: match successful\n", signature().c_str());
	return true;
}


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

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

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


IWScan::IWScan(const std::string& iface) : iface(iface)
{
}

IWScan::~IWScan()
{
}

static void free_scan(wireless_scan* scan)
{
	if (scan)
	{
		free_scan(scan->next);
		free(scan);
	}
}

void* IWScan::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 {
		// Apre il socket per parlare con il supporto di rete del kernel
		int skfd = iw_sockets_open();
		if (skfd < 0)
			throw wibble::exception::System("opening a socket");

		// Questo serve a iw_scan e iw_scan lo vuole passato perché non vuole
		// ricalcolarselo ogni volta
		int we_version = iw_get_kernel_we_version();

		debug("Starting wireless scan\n");

		// Fa lo scan, bloccante
		//(iw_process_scan è la non blocking)
		wireless_scan_head scan_context;
		int triesleft = options.iwscan_tries;
		int timeout = options.timeout;
		int ret;
		while ((ret = iw_scan(skfd, (char*)iface.c_str(), we_version, &scan_context)) < 0 && --triesleft > 0) {
			debug("scan failed, retrying\n");
			sleep(timeout);
		}
		if (ret < 0)
			throw wibble::exception::System("running the scan");

		if (util::Output::get().debug())
			for (wireless_scan* i = scan_context.result; i != 0; i = i->next)
				debug("Found network %s\n", i->b.essid);

		vector<WirelessScan*> matched;
		for (list<WirelessScan*>::iterator j = candidates.begin();
				j != candidates.end(); ++j)
			for (wireless_scan* i = scan_context.result; i != 0; i = i->next)
				if ((*j)->matches(*i))
				{
					// Signal success on the first of the matching stanzas
					debug("%s matched network %s\n",
						(*j)->signature().c_str(), i->b.essid);
					(*j)->success();
					goto outer_break;
				}

outer_break:
		// Deallocate the result list
		free_scan(scan_context.result);

		debug("End of wireless scan\n");
	} catch (std::exception& e) {
		error("%s.  Quitting IWScan thread.\n", e.what());
	}

	return 0;
}

void IWScan::addCandidate(WirelessScan* scan)
{
	candidates.push_back(scan);
}

}

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