/*
 * /etc/network/interfaces parser
 *
 * Copyright (C) 2003--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
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#define APPNAME PACKAGE
#else
#warning No config.h found: using fallback values
#define APPNAME __FILE__
#define VERSION "unknown"
#endif

#include "IfaceParser.h"
#include <wibble/regexp.h>
#include "util/output.h"
#include "scanner/scanbag.h"
#include "scanner/peer.h"
#include "scanner/iwscan.h"
#include "scanner/script.h"
#include "scanner/dhcp.h"
#include "scanner/linkbeat.h"
#include "options.h"

#include <map>
#include <memory>

#include <wordexp.h>

using namespace std;
using namespace scanner;

class Tokenizer
{
protected:
	std::string str;
	std::string::size_type s;
public:
	Tokenizer(const std::string& str) throw ()
		: str(str), s(0) {}

	std::string next()
	{
		// Skip leading spaces
		while (s < str.size() && isspace(str[s]))
			s++;

		if (s == str.size()) return string();
		
		string::size_type start = s;

		while (s < str.size() && !isspace(str[s]))
			s++;

		return str.substr(start, s - start);
	}
};

struct IfupdownFilter : public IfaceFilter
{
	set<string> ifupdownProfiles;
	bool ifupdownProfilesMatchInverted;

	IfupdownFilter() : ifupdownProfilesMatchInverted(false)
	{
	}
	virtual ~IfupdownFilter() {}

	virtual bool operator()(const std::string& name) const
	{
		bool hasmatch = false;
		if (options.autofilter) 
			hasmatch = (name.substr(0, options.iface.size()+1) == options.iface+"-") ;
		else 
			hasmatch = ifupdownProfilesMatchInverted || (ifupdownProfiles.size() == 0);

		if (ifupdownProfilesMatchInverted && ifupdownProfiles.find(name) != ifupdownProfiles.end())
		{
			hasmatch = false;
		}
		else if (!ifupdownProfilesMatchInverted && ifupdownProfiles.find(name) != ifupdownProfiles.end())
		{
			hasmatch = true;
		} 
		return hasmatch;
	}
};

void IfaceParser::parseMapping(FILE* in)
{
	auto_ptr<IfupdownFilter> filter(new IfupdownFilter);

	::debug("program name is guessnet-ifupdown: enabling ifupdown mode\n");

	// Read stuff from stdin
	wibble::ERegexp null_line("^[[:blank:]]*(#.*)?$");
	wibble::ERegexp parm_line("^[[:blank:]]*([A-Za-z_-]+):[[:blank:]]*(.+)$", 3);

	string line;
	int linenum = 1;
	int c;
	while ((c = fgetc(in)) != EOF)
	{
		if (c != '\n')
			line += c;
		else
		{
			if (null_line.match(line))
			{
				//fprintf(stderr, "EMPTY\n");
			}
			else if (parm_line.match(line))
			{
				string name = parm_line[1];
				Tokenizer t(parm_line[2]);
				string val = t.next();

				if (name == "default")
				{
					if (!val.empty())
						options.defprof = val;
				}
				else if (name == "verbose")
				{
					if (!val.empty())
						util::Output::get().verbose(val == "true");
				}
				else if (name == "debug")
				{
					if (!val.empty())
						util::Output::get().debug(val == "true");
				}
				else if (name == "syslog")
				{
					if (!val.empty())
						util::Output::get().syslog(val == "true");
				}
				else if (name == "timeout")
				{
					int v = atoi(val.c_str());
					if (v > 0)
						options.timeout = v;
				}
				else if (name == "init-time")
				{
					int v = atoi(val.c_str());
					if (v > 0)
						options.init_timeout = v;
				}
				else if (name == "autofilter")
				{
					options.autofilter = val == "true";
				}
				else if (name == "init-delay")
				{
					int v = atoi(val.c_str());
					if (v > 0)
						options.initdelay = v;
				}
				else if (name == "iwscan-tries")
				{
					int v = atoi(val.c_str());
					if (v > 0)
						options.iwscan_tries = v;
				}
			}
			else
			{
				Tokenizer t(line);
				bool first = true;
				for (string w = t.next(); !w.empty(); w = t.next())
				{
					if (first)
					{
						filter->ifupdownProfilesMatchInverted = w[0] == '!';
						first = false;
					}
					if (w[0] == '!')
					{
						if (!filter->ifupdownProfilesMatchInverted)
							throw wibble::exception::Consistency(
									"parsing list of interfaces to use",
									"found negated interface "+w+" after a normal interface");
						filter->ifupdownProfiles.insert(w.substr(1));
						std::string dm = "Added " + w + "\n";
						::debug(dm.c_str());
					} else {
						if (filter->ifupdownProfilesMatchInverted)
							throw wibble::exception::Consistency(
									"parsing list of interfaces to use",
									"found normal interface "+w+" after negated interface");
						filter->ifupdownProfiles.insert(w);
						std::string dm = "Added " + w + "\n";
						::debug(dm.c_str());
					}
				}
			}
			line = string();
			linenum++;
		}
	}

	options.iface_filter = filter.release();
}

/* Parse the input from `input'
 * To make it simple, use regexps on input lines instead of implementing a real
 * parser.
 */
void IfaceParser::parse(FILE* input, const IfaceFilter& filter)
{
#define ATLINESTART "^[[:blank:]]*(guessnet[0-9]*[[:blank:]]+)?test[0-9]*(-|[[:blank:]]+)"
#define MACPATTERN "[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}"
	wibble::ERegexp null_line("^[[:blank:]]*(#.*)?$");
	wibble::ERegexp iface_line(
		"^[[:blank:]]*iface[[:blank:]]+"
		"([^[:blank:]]+)[[:blank:]]+"
		"([^[:blank:]]+)[[:blank:]]+"
		"([^[:blank:]]+)[[:blank:]]*$", 4);
	wibble::ERegexp source_line(
		"^[[:blank:]]*source[[:blank:]]+(.+)$", 2);
	wibble::ERegexp peer_line(
		ATLINESTART "peer[[:blank:]]+(.+)$", 4);
	wibble::ERegexp cable_line(
		ATLINESTART "missing-cable[[:blank:]]*([[:blank:]]+.+)?$");
	wibble::ERegexp script_line(
		ATLINESTART "(script|command)[[:blank:]]+(.+)$", 5);
	wibble::ERegexp dhcp_line(
		ATLINESTART "dhcp[[:blank:]]*([[:blank:]]+.+)?$");
	wibble::ERegexp pppoe_line(
		ATLINESTART "pppoe[[:blank:]]*([[:blank:]]+.+)?$");
	wibble::ERegexp wireless_line(
		ATLINESTART "wireless[[:blank:]]+(.+)$", 4);
#if 0
	wibble::ERegexp wireless_mac_essid_line(
		ATLINESTART "wireless[[:blank:]]+mac[[:blank:]]+([^[:blank:]]+)[[:blank:]]+essid[[:blank:]](.+)$", 5);
	wibble::ERegexp wireless_mac_line(
		ATLINESTART "wireless[[:blank:]]+mac[[:blank:]]+([^[:blank:]]+)[[:blank:]]*$", 4);
	wibble::ERegexp wireless_essid_line(
		ATLINESTART "wireless[[:blank:]]+essid[[:blank:]](.+)$", 4);
#endif
	wibble::ERegexp old_default_line(
		"^[[:blank:]]*guessnet[[:blank:]]+"
		"default[[:blank:]]*$");
	wibble::ERegexp generic_guessnet_line(ATLINESTART);
	wibble::ERegexp parm_line(
		"^[[:blank:]]*([^[:blank:]]+)[[:blank:]]+(.+)$", 2);
	string profileName;

	string line;
	int linenum = 1;
	int found = 0;
	int c;
	while ((c = fgetc(input)) != EOF)
	{
		if (c != '\n')
			line += c;
		else
		{
			if (null_line.match(line))
			{
				//fprintf(stderr, "EMPTY\n");
			}
			else if (iface_line.match(line))
			{
				//string name(line, parts[1].rm_so, parts[1].rm_eo - parts[1].rm_so);
				//string net(line, parts[2].rm_so, parts[2].rm_eo - parts[2].rm_so);
				//string type(line, parts[3].rm_so, parts[3].rm_eo - parts[3].rm_so);

				//fprintf(stderr, "IFACE: %.*s/%.*s/%.*s\n", PFSTR(name), PFSTR(net), PFSTR(type));
				if (filter(iface_line[1]))
					profileName = iface_line[1];
				else
					profileName.clear();
			}
			else if (source_line.match(line))
			{
				/* process ifupdown's source directive, expand globs */
				wordexp_t p;
				char ** w;
				size_t i;
				const char * rest = source_line[1].c_str();
				int fail = wordexp(rest, &p, WRDE_NOCMD);
				if (!fail)
				{
					w = p.we_wordv;
					for (i = 0; i < p.we_wordc; i++)
					{
						FILE * f = fopen(w[i], "r");
						if (f) {
							IfaceParser::parse(f, filter);
							fclose(f);
						}
					}
					wordfree(&p);
				}

			}
			else if (peer_line.match(line))
			{
				//string ip(line, parts[1].rm_so, parts[1].rm_eo - parts[1].rm_so);
				//string mac(line, parts[2].rm_so, parts[2].rm_eo - parts[2].rm_so);
				//fprintf(stderr, "PEER: %.*s/%.*s\n", PFSTR(ip), PFSTR(mac));
				if (profileName.size())
				{
					string argstr = peer_line[3];

					// split in a map of key->val
					map<string, string> args;
					string key;
					string val;
					enum { SKEY, KEY, SVAL, VAL } state = KEY;
					for (string::const_iterator s = argstr.begin();
							s != argstr.end(); s++)
					{
						//debug("Read `%c', state: %d\n", *s, (int)state);
						if (isspace(*s))
							switch (state)
							{
								case SKEY: break;
								case KEY: state = SVAL; break;
								case SVAL: break;
								case VAL:
									state = SKEY;
									//debug("Found args: %.*s: %.*s\n", PFSTR(key), PFSTR(val));
									args.insert(make_pair(key, val));
									key = string();
									val = string();
									break;
							}
						else
							switch (state)
							{
								case SKEY: key += *s; state = KEY; break;
								case KEY: key += *s; break;
								case SVAL: val += *s; state = VAL; break;
								case VAL: val += *s; break;
							}
					}
					if (key.size() > 0 && val.size() > 0)
						args.insert(make_pair(key, val));

					map<string, string>::const_iterator ip = args.find("address");
					map<string, string>::const_iterator mac = args.find("mac");
					map<string, string>::const_iterator src = args.find("source");
					
					auto_ptr<scanner::PeerScan> s(new scanner::PeerScan(profileName));

					if (ip == args.end() && mac == args.end())
					{
						warning("Missing IP or MAC address at line %d: skipping line\n", linenum);
					} else {
						if (ip != args.end())
							s->setIP(IPAddress(ip->second));

						if (mac != args.end())
						{
							struct ether_addr macAddr;
							parse_mac(&macAddr, mac->second);
							s->setMAC(macAddr);
						} else
							warning("No mac provided at line %d: scans will be less accurate\n", linenum);

						if (src != args.end())
							s->setSource(IPAddress(src->second));

						if (ip == args.end() && mac != args.end() && src == args.end())
							warning("Mac-only peer test is likely to fail unless you provide a source IP address");
						
						s->finaliseInit();
						ScanBag::get().add(s.release());
						found++;
					}
				}
			}
			else if (cable_line.match(line))
			{
				if (profileName.size())
				{
					ScanBag::get().add(scanner::LinkBeat::createScan(profileName));
					found++;
				}
			}
			else if (script_line.match(line))
			{
				if (profileName.size())
				{
					ScanBag::get().add(scanner::Script::createScan(profileName, script_line[4]));
					found++;
				}
			}
			else if (dhcp_line.match(line))
			{
				if (profileName.size())
				{
					ScanBag::get().add(scanner::DHCP::createScan(profileName));
					found++;
				}
			}
			else if (pppoe_line.match(line))
			{
				if (profileName.size())
				{
					ScanBag::get().add(scanner::Script::createScan(profileName, string("pppoe -I ") + options.iface + " -A >/dev/null 2>&1"));
					found++;
				}
			}
			else if (wireless_line.match(line))
			{
				if (profileName.size())
				{
					string str = wireless_line[3];
					wibble::Tokenizer tokens(str, "[^\" \t][^ \t]+|\"[^\"]+\"", REG_EXTENDED);

					// split in a map of key->val
					map<string, string> args;
					string key;
					for (wibble::Tokenizer::const_iterator i = tokens.begin();
							i != tokens.end(); ++i)
					{
						// Remove quotes from around values, if present
						string token = *i;
						if (token.size() > 2 && token[0] == '"')
							token = token.substr(1, token.size() - 2);

						if (key.empty())
						{
							if (token == "open")
								args["open"] = "true";
							else if (token == "closed")
								args["open"] = "false";
							else
								key = token;
						}
						else
						{
							args.insert(make_pair(key, token));
							key.clear();
						}
					}
					if (!key.empty())
						warning("Ignoring extra argument \"%s\" at the end of line %d\n",
							key.c_str(), linenum);

					map<string, string>::const_iterator essid = args.find("essid");
					map<string, string>::const_iterator mac = args.find("mac");
					map<string, string>::const_iterator open = args.find("open");
					
					auto_ptr<scanner::WirelessScan> s(new scanner::WirelessScan(profileName));

					if (essid == args.end() && mac == args.end() && open == args.end())
					{
						warning("Missing ESSID or MAC address or open/closed at line %d: skipping line\n", linenum);
					} else {
						if (essid != args.end())
							s->setESSID(essid->second);

						if (mac != args.end())
						{
							struct ether_addr macAddr;
							parse_mac(&macAddr, mac->second);
							s->setMAC(macAddr);
						}

						if (open != args.end())
						{
							if (open->second == "true")
								s->setOpen(true);
							else if (open->second == "false")
								s->setOpen(false);
							else
								warning("Internal logic error: args[\"open\"] has been set to \"%s\" for line %d\n", open->second.c_str(), linenum);
						}

						s->finaliseInit();
						ScanBag::get().add(s.release());
						found++;
					}
				}
			}
#if 0
			else if (wireless_mac_essid_line.match(line))
			{
				if (profileName.size())
				{
					ScanBag::get().add(scanner::Script::createScan(profileName, string(SCRIPTDIR "/test-wireless ") + Environment::get().iface() + " mac " + wireless_mac_essid_line[3] + " essid \"" + wireless_mac_essid_line[4] + "\"" ));
					found++;
				}
			}
			else if (wireless_mac_line.match(line))
			{
				if (profileName.size())
				{
					ScanBag::get().add(scanner::Script::createScan(profileName, string(SCRIPTDIR "/test-wireless ") + Environment::get().iface() + " mac " + wireless_mac_line[3] ));
					found++;
				}
			}
			else if (wireless_essid_line.match(line))
			{
				if (profileName.size())
				{
					ScanBag::get().add(scanner::Script::createScan(profileName, string(SCRIPTDIR "/test-wireless ") + Environment::get().iface() + " essid \"" + wireless_essid_line[3] + "\"" ));
					found++;
				}
			}
#endif
			else if (old_default_line.match(line))
			{
				warning("line %d: Use of \"guessnet default\" lines is obsolete and will be discontinued in the future.  Use \"map default: profile\" in the \"mapping\" section instead.\n", linenum);
				//fprintf(stderr, "DEFAULT\n");
				if (profileName.size())
				{
					//debug("Will use tag %.*s as default\n", PFSTR(profileName));

					options.defprof = profileName;
				}
			}
			else if (generic_guessnet_line.match(line))
			{
				warning("Parse error at line %d: line ignored\n", linenum);
			}
			else if (parm_line.match(line))
			{
				//string name(line, parts[1].rm_so, parts[1].rm_eo - parts[1].rm_so);
				//string parms(line, parts[2].rm_so, parts[2].rm_eo - parts[2].rm_so);
				//fprintf(stderr, "PARM: %.*s/%.*s\n", PFSTR(name), PFSTR(parms));
			}
			else
			{
				warning("Parse error at line %d: line ignored\n", linenum);
			}
			line = string();
			linenum++;
		}
	}
	debug("%d candidates found in input\n", found);
}

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