changeset 123:c7fee63ccf92

Irccd: new configuration mechanism, #397
author David Demelier <markand@malikania.fr>
date Tue, 10 May 2016 21:27:40 +0200
parents 2aecbd638b1c
children 0895acad4072
files irccd/main.cpp lib/irccd/cmd-plugin-list.cpp lib/irccd/cmd-server-list.cpp lib/irccd/cmd-server-reconnect.cpp lib/irccd/config.cpp lib/irccd/config.hpp lib/irccd/fs.cpp lib/irccd/ini.cpp lib/irccd/ini.hpp lib/irccd/irccd.cpp lib/irccd/irccd.hpp lib/irccd/irccdctl.cpp lib/irccd/js-plugin.cpp lib/irccd/js-server.cpp lib/irccd/plugin.cpp lib/irccd/plugin.hpp lib/irccd/server-event.cpp
diffstat 17 files changed, 1163 insertions(+), 922 deletions(-) [+]
line wrap: on
line diff
--- a/irccd/main.cpp	Wed May 04 13:38:02 2016 +0200
+++ b/irccd/main.cpp	Tue May 10 21:27:40 2016 +0200
@@ -16,24 +16,51 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+#include <irccd/sysconfig.hpp>
+
+#if defined(HAVE_GETPID)
+#  include <sys/types.h>
+#  include <unistd.h>
+#  include <cerrno>
+#  include <cstring>
+#  include <fstream>
+#endif
+
+#if defined(HAVE_DAEMON)
+#  include <cstdlib>
+#endif
+
 #include <csignal>
 
-#include <irccd/sysconfig.hpp>
+#include <format.h>
 
 #include <irccd/logger.hpp>
 #include <irccd/options.hpp>
 #include <irccd/path.hpp>
 #include <irccd/system.hpp>
-
 #include <irccd/config.hpp>
 #include <irccd/irccd.hpp>
 
+using namespace fmt::literals;
+
 using namespace irccd;
 
 namespace {
 
 std::unique_ptr<Irccd> instance;
 
+void usage()
+{
+	log::warning() << "usage: " << sys::programName() << " [options...]\n\n";
+	log::warning() << "Available options:\n";
+	log::warning() << "  -c, --config file       specify the configuration file\n";
+	log::warning() << "  -f, --foreground        do not run as a daemon\n";
+	log::warning() << "      --help              show this help\n";
+	log::warning() << "  -p, --plugin name       load a specific plugin\n";
+	log::warning() << "  -v, --verbose           be verbose" << std::endl;
+	std::exit(1);
+}
+
 void stop(int)
 {
 	instance->stop();
@@ -41,16 +68,15 @@
 
 void init(int &argc, char **&argv)
 {
-	// MOVE THIS IN Application
-
-	/* Needed for some components */
+	// Needed for some components.
 	sys::setProgramName("irccd");
 	path::setApplicationPath(argv[0]);
 
-	/* Default logging to console */
+	// Default logging to console.
 	log::setVerbose(false);
 	log::setInterface(std::make_unique<log::Console>());
 
+	// Register some signals.
 	signal(SIGINT, stop);
 	signal(SIGTERM, stop);
 
@@ -58,25 +84,9 @@
 	++ argv;
 }
 
-void usage()
+parser::Result parse(int &argc, char **&argv)
 {
-	log::warning() << "usage: " << sys::programName() << " [options...]\n\n";
-	log::warning() << "Available options:\n";
-	log::warning() << "  -c, --config file       specify the configuration file\n";
-	log::warning() << "  -f, --foreground        do not run as a daemon\n";
-	log::warning() << "  --help                  show this help\n";
-	log::warning() << "  -p, --plugin name       load a specific plugin\n";
-	log::warning() << "  -v, --verbose           be verbose" << std::endl;
-	std::exit(1);
-}
-
-} // !namespace
-
-int main(int argc, char **argv)
-{
-	init(argc, argv);
-
-	/* Parse command line options */
+	// Parse command line options.
 	parser::Result result;
 
 	try {
@@ -99,7 +109,7 @@
 				usage();
 				// NOTREACHED
 			}
-	
+
 			if (pair.first == "-v" || pair.first == "--verbose") {
 				log::setVerbose(true);
 			}
@@ -109,8 +119,153 @@
 		usage();
 	}
 
+	return result;
+}
+
+Config open(const parser::Result &result)
+{
+	auto it = result.find("-c");
+
+	if (it != result.end() || (it = result.find("--config")) != result.end()) {
+		try {
+			return Config(it->second);
+		} catch (const std::exception &ex) {
+			throw std::runtime_error("{}: {}"_format(it->second, ex.what()));
+		}
+	}
+
+	return Config::find();
+}
+
+void loadPid(const std::string &path)
+{
+	if (path.empty()) {
+		return;
+	}
+
+	try {
+#if defined(HAVE_GETPID)
+		std::ofstream out(path, std::ofstream::trunc);
+
+		if (!out) {
+			throw std::runtime_error("irccd: could not open pidfile {}: {}"_format(path, std::strerror(errno)));
+		}
+
+		log::debug() << "irccd: pid written in " << path << std::endl;
+		out << getpid() << std::endl;
+#else
+		throw std::runtime_error("irccd: pidfile option not supported on this platform");
+#endif
+	} catch (const std::exception &ex) {
+		log::warning() << "irccd: " << ex.what() << std::endl;
+	}
+}
+
+void loadGid(const std::string gid)
+{
+	try {
+		if (!gid.empty()) {
+#if defined(HAVE_SETGID)
+			sys::setGid(gid);
+#else
+			throw std::runtime_error("irccd: gid option not supported on this platform");
+#endif
+		}
+	} catch (const std::exception &ex) {
+		log::warning() << "irccd: " << ex.what() << std::endl;
+	}
+}
+
+void loadUid(const std::string &uid)
+{
+	try {
+		if (!uid.empty()) {
+#if defined(HAVE_SETUID)
+			sys::setUid(uid);
+#else
+			throw std::runtime_error("irccd: uid option not supported on this platform");
+#endif
+		}
+	} catch (const std::exception &ex) {
+		log::warning() << "irccd: " << ex.what() << std::endl;
+	}
+}
+
+void loadForeground(bool foreground, const parser::Result &options)
+{
+	try {
+#if defined(HAVE_DAEMON)
+		if (options.count("-f") == 0 && options.count("--foreground") == 0 && !foreground) {
+			daemon(1, 0);
+		}
+#endif
+	} catch (const std::exception &ex) {
+		log::warning() << "irccd: " << ex.what() << std::endl;
+	}
+}
+
+void load(const Config &config, const parser::Result &options)
+{
+	/*
+	 * Order matters, please be careful when changing this.
+	 *
+	 * 1. Open logs as early as possible to use the defined outputs on any loading errors.
+	 */
+
+	// [logs] and [format] sections.
+	config.loadLogs();
+	config.loadFormats();
+
+	// Show message here to use the formats.
+	log::info() << "irccd: using " << config.path() << std::endl;
+
+	// [general] section.
+	loadPid(config.pidfile());
+	loadGid(config.gid());
+	loadUid(config.uid());
+	loadForeground(config.isForeground(), options);
+
+	// [transport]
+	for (const auto &transport : config.loadTransports()) {
+		instance->addTransport(transport);
+	}
+
+	// [server] section.
+	for (const auto &server : config.loadServers()) {
+		instance->addServer(server);
+	}
+
+	// [rule] section.
+	for (const auto &rule : config.loadRules()) {
+		instance->addRule(rule);
+	}
+
+	// [plugin] section.
+	for (const auto &plugin : config.loadPlugins()) {
+		instance->addPlugin(plugin);
+	}
+}
+
+} // !namespace
+
+int main(int argc, char **argv)
+{
+	init(argc, argv);
+
+	parser::Result options = parse(argc, argv);
+
+	// Find configuration file.
 	instance = std::make_unique<Irccd>();
-	instance->load(Config{result});
+
+	try {
+		Config cfg = open(options);
+
+		load(cfg, options);
+	} catch (const std::exception &ex) {
+		log::warning() << "irccd: " << ex.what() << std::endl;
+		return 1;
+	}
+
 	instance->run();
 
 	return 0;
--- a/lib/irccd/cmd-plugin-list.cpp	Wed May 04 13:38:02 2016 +0200
+++ b/lib/irccd/cmd-plugin-list.cpp	Tue May 10 21:27:40 2016 +0200
@@ -43,7 +43,7 @@
 	json::Value list = json::array({});
 
 	for (const auto &plugin : irccd.plugins()) {
-		list.append(plugin.first);
+		list.append(plugin->name());
 	}
 
 	response.insert("list", std::move(list));
@@ -68,4 +68,4 @@
 
 } // !command
 
-}
+} // !irccd
--- a/lib/irccd/cmd-server-list.cpp	Wed May 04 13:38:02 2016 +0200
+++ b/lib/irccd/cmd-server-list.cpp	Tue May 10 21:27:40 2016 +0200
@@ -40,8 +40,8 @@
 	auto json = json::object({});
 	auto list = json::array({});
 
-	for (const auto &pair : irccd.servers()) {
-		list.append(pair.first);
+	for (const auto &server : irccd.servers()) {
+		list.append(server->info().name);
 	}
 
 	json.insert("list", std::move(list));
--- a/lib/irccd/cmd-server-reconnect.cpp	Wed May 04 13:38:02 2016 +0200
+++ b/lib/irccd/cmd-server-reconnect.cpp	Tue May 10 21:27:40 2016 +0200
@@ -50,8 +50,8 @@
 	if (server != request.end() && server->isString()) {
 		irccd.requireServer(server->toString())->reconnect();
 	} else {
-		for (auto &pair : irccd.servers()) {
-			pair.second->reconnect();
+		for (auto &server : irccd.servers()) {
+			server->reconnect();
 		}
 	}
 
--- a/lib/irccd/config.cpp	Wed May 04 13:38:02 2016 +0200
+++ b/lib/irccd/config.cpp	Tue May 10 21:27:40 2016 +0200
@@ -16,31 +16,20 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#include "sysconfig.hpp"
+#include <cassert>
 
-#if defined(HAVE_GETPID)
-#  include <sys/types.h>
-#  include <unistd.h>
-#  include <cerrno>
-#  include <cstring>
-#  include <fstream>
-#endif
-
-#if defined(HAVE_DAEMON)
-#  include <cstdlib>
-#endif
+#include <format.h>
 
 #include "config.hpp"
-#include "ini.hpp"
-#include "logger.hpp"
+#include "fs.hpp"
+#include "irccd.hpp"
 #include "path.hpp"
-#include "sockets.hpp"
-#include "system.hpp"
+#include "rule.hpp"
+#include "server.hpp"
+#include "sysconfig.hpp"
 #include "util.hpp"
-#include "irccd.hpp"
 
-using namespace std;
-using namespace std::string_literals;
+using namespace fmt::literals;
 
 namespace irccd {
 
@@ -83,101 +72,74 @@
 	}
 };
 
-} // !namespace
-
-void Config::loadGeneral(const ini::Document &config) const
+std::string get(const ini::Document &doc, const std::string &section, const std::string &key)
 {
-	ini::Document::const_iterator sc = config.find("general");
-	ini::Section::const_iterator it;
-
-#if defined(HAVE_GETPID)
-	if (sc != config.end()) {
-		it = sc->find("pidfile");
-
-		if (it != sc->end() && !it->value().empty()) {
-			std::string path = it->value();
-			std::ofstream out(path, std::ofstream::trunc);
+	auto its = doc.find(section);
 
-			if (!out) {
-				log::warning() << "irccd: could not open pidfile " << path << ": " << std::strerror(errno) << std::endl;
-			} else {
-				log::debug() << "irccd: pid written in " << path << std::endl;
-				out << getpid();
-			}
-		}
+	if (its == doc.end()) {
+		return "";
 	}
-#endif
 
-#if defined(HAVE_DAEMON)
-	/* CLI priority is higher */
-	bool daemonize = m_options.count("-f") == 0 && m_options.count("--foreground") == 0;
+	auto ito = its->find(key);
 
-	if (daemonize && sc != config.end()) {
-		it = sc->find("foreground");
-
-		if (it != sc->end()) {
-			daemonize = !util::isBoolean(it->value());
-		}
+	if (ito == its->end()) {
+		return "";
 	}
 
-	if (daemonize) {
-		daemon(1, 0);
-	}
-#endif
-
-	if (sc != config.end()) {
-		try {
-#if defined(HAVE_SETGID)
-			if ((it = sc->find("gid")) != sc->end()) {
-				sys::setGid(it->value());
-			}
-#endif
-#if defined(HAVE_SETUID)
-			if ((it = sc->find("uid")) != sc->end()) {
-				sys::setUid(it->value());
-			}
-#endif
-		} catch (const std::exception &ex) {
-			log::warning() << "irccd: could not set " << it->key() << ": " << ex.what() << std::endl;
-		}
-	}
+	return ito->value();
 }
 
-void Config::loadFormats(const ini::Document &config) const
+ServerIdentity loadIdentity(const ini::Section &sc)
 {
-	ini::Document::const_iterator sc = config.find("format");
+	assert(sc.key() == "identity");
+	assert(sc.contains("name") && util::isIdentifierValid(sc["name"].value()));
+
+	ServerIdentity identity;
+
+	// Mandatory stuff.
+	identity.name = sc["name"].value();
+
+	// Optional stuff.
+	ini::Section::const_iterator it;
 
-	if (sc == config.end()) {
-		return;
+	if ((it = sc.find("username")) != sc.end()) {
+		identity.username = it->value();
+	}
+	if ((it = sc.find("realname")) != sc.end()) {
+		identity.realname = it->value();
+	}
+	if ((it = sc.find("nickname")) != sc.end()) {
+		identity.nickname = it->value();
+	}
+	if ((it = sc.find("ctcp-version")) != sc.end()) {
+		identity.ctcpversion = it->value();
 	}
 
-	ini::Section::const_iterator it;
-	std::unique_ptr<IrccdLogFilter> filter = std::make_unique<IrccdLogFilter>();
+	return identity;
+}
 
-	if ((it = sc->find("debug")) != sc->cend()) {
-		filter->m_debug = it->value();
-	}
-	if ((it = sc->find("info")) != sc->cend()) {
-		filter->m_info = it->value();
-	}
-	if ((it = sc->find("warning")) != sc->cend()) {
-		filter->m_warning = it->value();
+PluginConfig loadPluginConfig(const ini::Section &sc)
+{
+	PluginConfig config;
+
+	for (const auto &option : sc) {
+		config.emplace(option.key(), option.value());
 	}
 
-	log::setFilter(std::move(filter));
+	return config;
 }
 
-void Config::loadLogFile(const ini::Section &sc) const
+std::unique_ptr<log::Interface> loadLogFile(const ini::Section &sc)
 {
 	/*
 	 * TODO: improve that with CMake options.
 	 */
 #if defined(IRCCD_SYSTEM_WINDOWS)
-	string normal = "log.txt";
-	string errors = "errors.txt";
+	std::string normal = "log.txt";
+	std::string errors = "errors.txt";
 #else
-	string normal = "/var/log/irccd/log.txt";
-	string errors = "/var/log/irccd/errors.txt";
+	std::string normal = "/var/log/irccd/log.txt";
+	std::string errors = "/var/log/irccd/errors.txt";
 #endif
 
 	ini::Section::const_iterator it;
@@ -189,177 +151,228 @@
 		errors = it->value();
 	}
 
-	log::setInterface(make_unique<log::File>(move(normal), move(errors)));
+	return std::make_unique<log::File>(std::move(normal), std::move(errors));
 }
 
-void Config::loadLogSyslog() const
+std::unique_ptr<log::Interface> loadLogSyslog()
 {
 #if defined(HAVE_SYSLOG)
-	log::setInterface(make_unique<log::Syslog>());
+	return std::make_unique<log::Syslog>();
 #else
-	log::warning() << "irccd: syslog is not available on this platform" << endl;
+	throw std::runtime_error("logs: syslog is not available on this platform");
 #endif // !HAVE_SYSLOG
 }
 
-void Config::loadLogs(const ini::Document &config) const
+std::shared_ptr<TransportServer> loadTransportIp(const ini::Section &sc)
 {
-	ini::Document::const_iterator sc = config.find("logs");
+	assert(sc.key() == "transport");
 
-	if (sc == config.end()) {
-		return;
-	}
-
+	std::shared_ptr<TransportServer> transport;
 	ini::Section::const_iterator it;
 
-	if ((it = sc->find("verbose")) != sc->end() && m_options.count("-v") == 0 && m_options.count("--verbose")) {
-		log::setVerbose(util::isBoolean(it->value()));
+	// Port
+	int port;
+
+	if ((it = sc.find("port")) == sc.cend()) {
+		throw std::invalid_argument("transport: missing 'port' parameter");
+	}
+
+	try {
+		port = util::toNumber<std::uint16_t>(it->value());
+	} catch (const std::exception &) {
+		throw std::invalid_argument("transport: invalid port number: {}"_format(it->value()));
 	}
-	if ((it = sc->find("type")) != sc->end()) {
-		/* Console is the default, no test case */
-		if (it->value() == "file") {
-			loadLogFile(*sc);
-		} else if (it->value() == "syslog") {
-			loadLogSyslog();
-		} else {
-			log::warning() << "irccd: unknown log type: " << it->value() << std::endl;
+
+	// Address
+	std::string address = "*";
+
+	if ((it = sc.find("address")) != sc.end()) {
+		address = it->value();
+	}
+
+	// Domain
+	bool ipv6 = true;
+	bool ipv4 = true;
+
+	if ((it = sc.find("domain")) != sc.end()) {
+		ipv6 = false;
+		ipv4 = false;
+
+		for (const auto &v : *it) {
+			if (v == "ipv4") {
+				ipv4 = true;
+			}
+			if (v == "ipv6") {
+				ipv6 = true;
+			}
 		}
 	}
+
+	if (ipv6) {
+		transport = std::make_shared<TransportServerIp>(AF_INET6, move(address), port, !ipv4);
+	} else if (ipv4) {
+		transport = std::make_shared<TransportServerIp>(AF_INET, move(address), port);
+	} else {
+		throw std::invalid_argument("transport: domain must at least have ipv4 or ipv6");
+	}
+
+	return transport;
 }
 
-void Config::loadPlugins(Irccd &irccd, const ini::Section &sc) const
+std::shared_ptr<TransportServer> loadTransportUnix(const ini::Section &sc)
 {
-#if defined(WITH_JS)
-	for (const ini::Option &option : sc) {
-		try {
-			if (option.value().empty()) {
-				irccd.loadPlugin(option.key(), option.key(), true);
-			} else {
-				irccd.loadPlugin(option.key(), option.value(), false);
-			}
-		} catch (const std::exception &ex) {
-			log::warning() << "plugin " << option.key() << ": " << ex.what() << std::endl;
-		}
+	assert(sc.key() == "transport");
+
+#if !defined(IRCCD_SYSTEM_WINDOWS)
+	ini::Section::const_iterator it = sc.find("path");
+
+	if (it == sc.end()) {
+		throw std::invalid_argument("transport: missing 'path' parameter");
 	}
+
+	return std::make_shared<TransportServerUnix>(it->value());
 #else
-	(void)irccd;
 	(void)sc;
+
+	throw std::invalid_argument("transport: unix transport not supported on on this platform");
 #endif
 }
 
-void Config::loadPluginConfig(Irccd &irccd, const ini::Section &sc, string name) const
+std::shared_ptr<TransportServer> loadTransport(const ini::Section &sc)
 {
-#if defined(WITH_JS)
-	PluginConfig config;
+	assert(sc.key() == "transport");
 
-	for (const ini::Option &option : sc) {
-		config.emplace(option.key(), option.value());
+	std::shared_ptr<TransportServer> transport;
+	ini::Section::const_iterator it = sc.find("type");
+
+	if (it == sc.end()) {
+		throw std::invalid_argument("transport: missing 'type' parameter");
 	}
 
-	irccd.addPluginConfig(std::move(name), std::move(config));
-#else
-	(void)irccd;
-	(void)sc;
-	(void)name;
-#endif
+	if (it->value() == "ip") {
+		transport = loadTransportIp(sc);
+	} else if (it->value() == "unix") {
+		transport = loadTransportUnix(sc);
+	} else {
+		throw std::invalid_argument("transport: invalid type given: {}"_format(it->value()));
+	}
+
+	return transport;
 }
 
-void Config::loadPlugins(Irccd &irccd, const ini::Document &config) const
+Rule loadRule(const ini::Section &sc)
 {
-#if defined(WITH_JS)
-	std::regex regex("^plugin\\.([A-Za-z0-9-_]+)$");
-	std::smatch match;
+	assert(sc.key() == "rule");
+
+	// Simple converter from std::vector to std::unordered_set
+	auto toSet = [] (const std::vector<std::string> &v) -> std::unordered_set<std::string> {
+		return std::unordered_set<std::string>(v.begin(), v.end());
+	};
+
+	RuleSet servers, channels, origins, plugins, events;
+	RuleAction action = RuleAction::Accept;
+
+	// Get the sets
+	ini::Section::const_iterator it;
 
-	/*
-	 * Load plugin configurations before we load plugins since we use them
-	 * when we load the plugin itself.
-	 */
-	for (const ini::Section &section : config) {
-		if (regex_match(section.key(), match, regex)) {
-			loadPluginConfig(irccd, section, match[1]);
-		}
+	if ((it = sc.find("servers")) != sc.end()) {
+		servers = toSet(*it);
+	}
+	if ((it = sc.find("channels")) != sc.end()) {
+		channels = toSet(*it);
+	}
+	if ((it = sc.find("origins")) != sc.end()) {
+		origins = toSet(*it);
+	}
+	if ((it = sc.find("plugins")) != sc.end()) {
+		plugins = toSet(*it);
+	}
+	if ((it = sc.find("channels")) != sc.end()) {
+		channels = toSet(*it);
 	}
 
-	ini::Document::const_iterator it = config.find("plugins");
-
-	if (it != config.end()) {
-		loadPlugins(irccd, *it);
+	// Get the action
+	if ((it = sc.find("action")) == sc.end()) {
+		throw std::invalid_argument("rule: missing 'action'' parameter");
 	}
-#else
-	(void)irccd;
-	(void)config;
 
-	log::warning() << "irccd: JavaScript disabled, ignoring plugins" << std::endl;
-#endif
+	if (it->value() == "drop") {
+		action = RuleAction::Drop;
+	} else if (it->value() == "accept") {
+		action = RuleAction::Accept;
+	} else {
+		throw std::invalid_argument("rule: invalid action given: {}"_format(it->value()));
+	}
+
+	return Rule(std::move(servers),
+		    std::move(channels),
+		    std::move(origins),
+		    std::move(plugins),
+		    std::move(events),
+		    action);
 }
 
-void Config::loadServer(Irccd &irccd, const ini::Section &sc) const
+std::shared_ptr<Server> loadServer(const ini::Section &sc, const Config &config)
 {
+	assert(sc.key() == "server");
+
 	ServerInfo info;
 	ServerIdentity identity;
 	ServerSettings settings;
 
-	/* Name */
+	// Name
 	ini::Section::const_iterator it;
 
 	if ((it = sc.find("name")) == sc.end()) {
-		throw std::invalid_argument("server: missing name");
+		throw std::invalid_argument("server: missing 'name' parameter");
 	} else if (!util::isIdentifierValid(it->value())) {
-		throw std::invalid_argument("server " + it->value() + ": name is not valid");
-	} else if (irccd.hasServer(it->value())) {
-		throw std::invalid_argument("server " + it->value() + ": already exists");
+		throw std::invalid_argument("server: invalid identifier: {}"_format(it->value()));
 	}
 
 	info.name = it->value();
 
-	/* Host */
+	// Host
 	if ((it = sc.find("host")) == sc.end()) {
-		throw std::invalid_argument("server " + info.name + ": missing host");
+		throw std::invalid_argument("server {}: missing host"_format(it->value()));
 	}
 
 	info.host = it->value();
 
-	/* Optional identity */
+	// Optional identity
 	if ((it = sc.find("identity")) != sc.end()) {
-		identity = irccd.findIdentity(it->value());
+		identity = config.findIdentity(it->value());
 	}
 
-	/* Optional port */
+	// Optional port
 	if ((it = sc.find("port")) != sc.end()) {
 		try {
-			info.port = std::stoi(it->value());
+			info.port = util::toNumber<std::uint16_t>(it->value());
 		} catch (const std::exception &) {
-			throw std::invalid_argument("server " + info.name + ": invalid port number: " + it->value());
+			throw std::invalid_argument("server {}: invalid number for {}: {}"_format(info.name, it->key(), it->value()));
 		}
 	}
 
-	/* Optional password */
+	// Optional password
 	if ((it = sc.find("password")) != sc.end()) {
 		info.password = it->value();
 	}
 
-	/* Optional flags */
+	// Optional flags
 	if ((it = sc.find("ipv6")) != sc.end() && util::isBoolean(it->value())) {
 		info.flags |= ServerInfo::Ipv6;
 	}
 	if ((it = sc.find("ssl")) != sc.end()) {
 		if (util::isBoolean(it->value())) {
-#if defined(WITH_SSL)
 			info.flags |= ServerInfo::Ssl;
-#else
-			throw std::invalid_argument("server " + info.name + ": ssl is disabled");
-#endif
 		}
 	}
 	if ((it = sc.find("ssl-verify")) != sc.end()) {
 		if (util::isBoolean(it->value())) {
-#if defined(WITH_SSL)
 			info.flags |= ServerInfo::SslVerify;
-#else
-			throw std::invalid_argument("server " + info.name + ": ssl is disabled");
-#endif
 		}
 	}
-	/* Options */
+
+	// Options
 	if ((it = sc.find("auto-rejoin")) != sc.end() && util::isBoolean(it->value())) {
 		settings.flags |= ServerSettings::AutoRejoin;
 	}
@@ -367,7 +380,7 @@
 		settings.flags |= ServerSettings::JoinInvite;
 	}
 
-	/* Channels */
+	// Channels
 	if ((it = sc.find("channels")) != sc.end()) {
 		for (const std::string &s : *it) {
 			ServerChannel channel;
@@ -386,297 +399,246 @@
 		settings.command = it->value();
 	}
 
-	/* Reconnect */
+	// Reconnect and ping timeout
 	try {
 		if ((it = sc.find("reconnect-tries")) != sc.end()) {
-			settings.reconnectTries = std::stoi(it->value());
+			settings.reconnectTries = util::toNumber<std::int8_t>(it->value());
 		}
 		if ((it = sc.find("reconnect-timeout")) != sc.end()) {
-			settings.reconnectDelay = std::stoi(it->value());
+			settings.reconnectDelay = util::toNumber<std::uint16_t>(it->value());
 		}
 		if ((it = sc.find("ping-timeout")) != sc.end()) {
-			settings.pingTimeout = std::stoi(it->value());
+			settings.pingTimeout = util::toNumber<std::uint16_t>(it->value());
 		}
 	} catch (const std::exception &) {
-		throw std::invalid_argument("server " + info.name + ": invalid number for " + it->key() + ": " + it->value());
+		log::warning("server {}: invalid number for {}: {}"_format(info.name, it->key(), it->value()));
 	}
 
-	irccd.addServer(std::make_shared<Server>(std::move(info), std::move(identity), std::move(settings)));
-}
-
-void Config::loadServers(Irccd &irccd, const ini::Document &config) const
-{
-	for (const ini::Section &section : config) {
-		if (section.key() == "server") {
-			try {
-				loadServer(irccd, section);
-			} catch (const exception &ex) {
-				log::warning() << ex.what() << endl;
-			}
-		}
-	}
+	return std::make_shared<Server>(std::move(info), std::move(identity), std::move(settings));
 }
 
-void Config::loadIdentity(Irccd &irccd, const ini::Section &sc) const
+} // !namespace
+
+Config Config::find()
 {
-	ServerIdentity identity;
-	ini::Section::const_iterator it;
+	for (const auto &path : path::list(path::PathConfig)) {
+		std::string fullpath = path + "irccd.conf";
 
-	if ((it = sc.find("name")) == sc.end()) {
-		throw invalid_argument("missing name");
-	} else if (!util::isIdentifierValid(it->value())) {
-		throw invalid_argument("identity name not valid");
+		if (!fs::isReadable(fullpath)) {
+			continue;
+		}
+
+		try {
+			return Config(fullpath);
+		} catch (const std::exception &ex) {
+			throw std::runtime_error("{}: {}"_format(fullpath, ex.what()));
+		}
 	}
 
-	identity.name = it->value();
-
-	/* Optional stuff */
-	if ((it = sc.find("username")) != sc.end()) {
-		identity.username = it->value();
-	}
-	if ((it = sc.find("realname")) != sc.end()) {
-		identity.realname = it->value();
-	}
-	if ((it = sc.find("nickname")) != sc.end()) {
-		identity.nickname = it->value();
-	}
-	if ((it = sc.find("ctcp-version")) != sc.end()) {
-		identity.ctcpversion = it->value();
-	}
-
-	log::debug() << "identity " << identity.name << ": ";
-	log::debug() << "nickname=" << identity.nickname << ", username=" << identity.username << ", ";
-	log::debug() << "realname=" << identity.realname << ", ctcp-version=" << identity.ctcpversion << endl;
-
-	irccd.addIdentity(move(identity));
+	throw std::runtime_error("no configuration file found");
 }
 
-void Config::loadIdentities(Irccd &irccd, const ini::Document &config) const
+ServerIdentity Config::findIdentity(const std::string &name) const
 {
-	for (const ini::Section &section : config) {
-		if (section.key() == "identity") {
-			try {
-				loadIdentity(irccd, section);
-			} catch (const exception &ex) {
-				log::warning() << "identity: " << ex.what() << endl;
-			}
+	assert(util::isIdentifierValid(name));
+
+	for (const auto &section : m_document) {
+		if (section.key() != "identity") {
+			continue;
+		}
+
+		auto it = section.find("name");
+
+		if (it == section.end()) {
+			log::warning("identity: missing 'name' property");
+			continue;
 		}
+		if (!util::isIdentifierValid(it->value())) {
+			log::warning("identity: invalid identifier: {}"_format(it->value()));
+			continue;
+		}
+		if (it->value() != name) {
+			continue;
+		}
+
+		return loadIdentity(section);
 	}
+
+	return ServerIdentity();
 }
 
-void Config::loadRule(Irccd &irccd, const ini::Section &sc) const
+PluginConfig Config::findPluginConfig(const std::string &name) const
+{
+	assert(util::isIdentifierValid(name));
+
+	std::string fullname = std::string("plugin.") + name;
+
+	for (const auto &section : m_document) {
+		if (section.key() != fullname) {
+			continue;
+		}
+
+		return loadPluginConfig(section);
+	}
+
+	return PluginConfig();
+}
+
+bool Config::isVerbose() const noexcept
+{
+	return util::isBoolean(get(m_document, "logs", "verbose"));
+}
+
+bool Config::isForeground() const noexcept
 {
-	/* Simple converter from std::vector to std::unordered_set */
-	auto toSet = [] (const std::vector<std::string> &v) -> std::unordered_set<std::string> {
-		return std::unordered_set<std::string>(v.begin(), v.end());
-	};
+	return util::isBoolean(get(m_document, "general", "foreground"));
+}
+
+std::string Config::pidfile() const
+{
+	return get(m_document, "general", "pidfile");
+}
+
+std::string Config::uid() const
+{
+	return get(m_document, "general", "uid");
+}
 
-	RuleSet servers, channels, origins, plugins, events;
-	RuleAction action = RuleAction::Accept;
+std::string Config::gid() const
+{
+	return get(m_document, "general", "gid");
+}
 
-	/* Get the sets */
+void Config::loadLogs() const
+{
+	ini::Document::const_iterator sc = m_document.find("logs");
+
+	if (sc == m_document.end()) {
+		return;
+	}
+
 	ini::Section::const_iterator it;
 
-	if ((it = sc.find("servers")) != sc.end()) {
-		servers = toSet(*it);
-	}
-	if ((it = sc.find("channels")) != sc.end()) {
-		channels = toSet(*it);
-	}
-	if ((it = sc.find("origins")) != sc.end()) {
-		origins = toSet(*it);
-	}
-	if ((it = sc.find("plugins")) != sc.end()) {
-		plugins = toSet(*it);
-	}
-	if ((it = sc.find("channels")) != sc.end()) {
-		channels = toSet(*it);
-	}
+	if ((it = sc->find("type")) != sc->end()) {
+		std::unique_ptr<log::Interface> iface;
 
-	/* Get the action */
-	if ((it = sc.find("action")) == sc.end()) {
-		throw std::invalid_argument("missing action parameter");
-	}
-	if (it->value() == "drop") {
-		action = RuleAction::Drop;
-	} else if (it->value() == "accept") {
-		action = RuleAction::Accept;
-	} else {
-		throw std::invalid_argument("invalid action given: " + it->value());
-	}
+		/* Console is the default, no test case */
+		if (it->value() == "file") {
+			iface = loadLogFile(*sc);
+		} else if (it->value() == "syslog") {
+			iface = loadLogSyslog();
+		} else {
+			throw std::runtime_error("logs: unknown log type: {}"_format(it->value()));
+		}
 
-	irccd.addRule(Rule(move(servers), move(channels), move(origins), move(plugins), move(events), action));
-}
-
-void Config::loadRules(Irccd &irccd, const ini::Document &config) const
-{
-	for (const ini::Section &sc : config) {
-		if (sc.key() == "rule") {
-			try {
-				loadRule(irccd, sc);
-			} catch (const std::exception &ex) {
-				log::warning() << "rule: " << ex.what() << std::endl;
-			}
+		if (iface) {
+			log::setInterface(std::move(iface));
 		}
 	}
 }
 
-void Config::loadTransportIp(Irccd &irccd, const ini::Section &sc) const
+void Config::loadFormats() const
 {
-	bool ipv6 = true;
-	bool ipv4 = true;
+	ini::Document::const_iterator sc = m_document.find("format");
 
+	if (sc == m_document.end()) {
+		return;
+	}
+
+	std::unique_ptr<IrccdLogFilter> filter = std::make_unique<IrccdLogFilter>();
 	ini::Section::const_iterator it;
 
-	/* Port */
-	int port;
-
-	if ((it = sc.find("port")) == sc.end()) {
-		throw invalid_argument("missing port");
+	if ((it = sc->find("debug")) != sc->cend()) {
+		filter->m_debug = it->value();
 	}
-
-	try {
-		port = stoi(it->value());
-	} catch (const std::exception &) {
-		throw std::invalid_argument("invalid port number: " + it->value());
+	if ((it = sc->find("info")) != sc->cend()) {
+		filter->m_info = it->value();
+	}
+	if ((it = sc->find("warning")) != sc->cend()) {
+		filter->m_warning = it->value();
 	}
 
-	/* Address*/
-	std::string address = "*";
+	log::setFilter(std::move(filter));
+}
 
-	if ((it = sc.find("address")) != sc.end()) {
-		address = it->value();
+std::vector<std::shared_ptr<TransportServer>> Config::loadTransports() const
+{
+	std::vector<std::shared_ptr<TransportServer>> transports;
+
+	for (const auto &section : m_document) {
+		if (section.key() == "transport") {
+			transports.push_back(loadTransport(section));
+		}
 	}
 
-	/* Domain */
-	if ((it = sc.find("domain")) != sc.end()) {
-		ipv6 = false;
-		ipv4 = false;
+	return transports;
+}
 
-		for (const string &v : *it) {
-			if (v == "ipv4") {
-				ipv4 = true;
-			}
-			if (v == "ipv6") {
-				ipv6 = true;
-			}
+std::vector<Rule> Config::loadRules() const
+{
+	std::vector<Rule> rules;
+
+	for (const auto &section : m_document) {
+		if (section.key() == "rule") {
+			rules.push_back(loadRule(section));
 		}
 	}
 
-	if (ipv6) {
-		irccd.addTransport(std::make_shared<TransportServerIp>(AF_INET6, move(address), port, !ipv4));
-	} else if (ipv4) {
-		irccd.addTransport(std::make_shared<TransportServerIp>(AF_INET, move(address), port));
-	} else {
-		throw std::invalid_argument("domain must at least have ipv4 or ipv6");
-	}
-}
-
-void Config::loadTransportUnix(Irccd &irccd, const ini::Section &sc) const
-{
-#if !defined(IRCCD_SYSTEM_WINDOWS)
-	/* Path */
-	ini::Section::const_iterator it = sc.find("path");
-
-	if (it == sc.end()) {
-		throw std::invalid_argument("missing path parameter");
-	} else {
-		string path = sc["path"].value();
-
-		irccd.addTransport(std::make_shared<TransportServerUnix>(move(path)));
-	}
-#else
-	(void)irccd;
-	(void)sc;
-
-	throw std::invalid_argument("local transport not supported on on this platform");
-#endif
+	return rules;
 }
 
-void Config::loadTransports(Irccd &irccd, const ini::Document &config) const
+std::vector<std::shared_ptr<Server>> Config::loadServers() const
 {
-	for (const ini::Section &sc : config) {
-		if (sc.key() == "transport") {
-			try {
-				ini::Section::const_iterator it = sc.find("type");
-
-				if (it == sc.end()) {
-					log::warning() << "transport: missing type parameter" << std::endl;
-				} else if (it->value() == "ip") {
-					loadTransportIp(irccd, sc);
-				} else if (it->value() == "unix") {
-					loadTransportUnix(irccd, sc);
-				} else {
-					log::warning() << "transport: invalid type given: " << std::endl;
-				}
-			} catch (const net::Error &error) {
-				log::warning() << "transport: " << error.function() << ": " << error.what() << std::endl;
-			} catch (const exception &ex) {
-				log::warning() << "transport: error: " << ex.what() << endl;
-			}
-		}
-	}
-}
-
-bool Config::openConfig(Irccd &irccd, const string &path) const
-{
-	try {
-		/*
-		 * Order matters, take care when you change this.
-		 */
-		ini::Document config(ini::File{path});
+	std::vector<std::shared_ptr<Server>> servers;
 
-		loadGeneral(config);
-		loadLogs(config);
-		loadFormats(config);
-		loadIdentities(irccd, config);
-		loadServers(irccd, config);
-		loadRules(irccd, config);
-		loadPlugins(irccd, config);
-		loadTransports(irccd, config);
-	} catch (const ini::Error &ex) {
-		log::warning() << sys::programName() << ": " << path << ":" << ex.line() << ":" << ex.column() << ": " << ex.what() << std::endl;
-		return false;
-	} catch (const std::exception &ex) {
-		log::warning() << sys::programName() << ": " << path << ": " << ex.what() << std::endl;
-		return false;
-	}
-
-	return true;
-}
+	for (const auto &section : m_document) {
+		if (section.key() != "server") {
+			continue;
+		}
 
-Config::Config(parser::Result options) noexcept
-	: m_options(move(options))
-{
-}
-
-void Config::load(Irccd &irccd)
-{
-	auto it = m_options.find("-c");
-	auto found = false;
-
-	if (it != m_options.end()) {
-		found = openConfig(irccd, it->second);
-	} else if ((it = m_options.find("--config")) != m_options.end()) {
-		found = openConfig(irccd, it->second);
-	} else {
-		/* Search for a configuration file */
-		for (const string &path : path::list(path::PathConfig)) {
-			string fullpath = path + "irccd.conf";
-
-			log::info() << "irccd: trying " << fullpath << endl;
-
-			if (openConfig(irccd, fullpath)) {
-				found = true;
-				break;
-			}
+		try {
+			servers.push_back(loadServer(section, *this));
+		} catch (const std::exception &ex) {
+			log::warning(ex.what());
 		}
 	}
 
-	if (!found) {
-		log::warning() << "irccd: no configuration file could be found, exiting" << endl;
-		exit(1);
+	return servers;
+}
+
+std::vector<std::shared_ptr<Plugin>> Config::loadPlugins() const
+{
+	std::vector<std::shared_ptr<Plugin>> plugins;
+
+	// Plugins are defined in only one section.
+	auto it = m_document.find("plugins");
+
+	if (it == m_document.end()) {
+		return plugins;
 	}
+
+	for (const auto &option : *it) {
+		std::string name = option.key();
+		std::string path = option.value();
+
+		try {
+			log::info("plugin {}: loading"_format(name));
+
+			if (path.empty()) {
+				plugins.push_back(Plugin::find(name, findPluginConfig(name)));
+			} else {
+				log::info("plugin {}: trying {}"_format(name, path));
+				plugins.push_back(std::make_shared<Plugin>(name, path, findPluginConfig(name)));
+			}
+		} catch (const duk::ErrorInfo &ex) {
+			log::warning("plugin {}: {}"_format(option.key(), ex.what()));
+			log::warning("plugin {}: {}"_format(option.key(), ex.stack));
+		} catch (const std::exception &ex) {
+			log::warning("plugin {}: {}"_format(option.key(), ex.what()));
+		}
+	}
+
+	return plugins;
 }
 
 } // !irccd
--- a/lib/irccd/config.hpp	Wed May 04 13:38:02 2016 +0200
+++ b/lib/irccd/config.hpp	Tue May 10 21:27:40 2016 +0200
@@ -24,18 +24,21 @@
  * \brief Read .ini configuration file for irccd
  */
 
-#include "options.hpp"
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "ini.hpp"
+#include "plugin.hpp"
 
 namespace irccd {
 
-namespace ini {
+class Rule;
 
-class Document;
-class Section;
+class Server;
+class ServerIdentity;
 
-} // !ini
-
-class Irccd;
+class TransportServer;
 
 /**
  * \class Config
@@ -43,42 +46,127 @@
  */
 class Config {
 private:
-	parser::Result m_options;
-
-	void loadGeneral(const ini::Document &config) const;
-	void loadFormats(const ini::Document &config) const;
-	void loadLogFile(const ini::Section &sc) const;
-	void loadLogSyslog() const;
-	void loadLogs(const ini::Document &config) const;
-	void loadPlugins(Irccd &irccd, const ini::Section &sc) const;
-	void loadPluginConfig(Irccd &irccd, const ini::Section &sc, std::string name) const;
-	void loadPlugins(Irccd &irccd, const ini::Document &config) const;
-	void loadServer(Irccd &irccd, const ini::Section &sc) const;
-	void loadServers(Irccd &irccd, const ini::Document &config) const;
-	void loadIdentity(Irccd &irccd, const ini::Section &sc) const;
-	void loadIdentities(Irccd &irccd, const ini::Document &config) const;
-	void loadRule(Irccd &irccd, const ini::Section &sc) const;
-	void loadRules(Irccd &irccd, const ini::Document &config) const;
-	void loadTransportIp(Irccd &irccd, const ini::Section &sc) const;
-	void loadTransportUnix(Irccd &irccd, const ini::Section &sc) const;
-	void loadTransports(Irccd &irccd, const ini::Document &config) const;
-	bool openConfig(Irccd &irccd, const std::string &path) const;
+	std::string m_path;
+	ini::Document m_document;
 
 public:
 	/**
-	 * Construct the configuration file loader. If path is empty, then the configuration file is searched through
-	 * the standard directories.
+	 * Search the configuration file into the standard defined paths.
+	 *
+	 * \return the config
+	 * \throw std::exception on errors or if no config could be found
+	 */
+	static Config find();
+
+	/**
+	 * Load the configuration from the specified path.
+	 *
+	 * \param path the path
+	 */
+	inline Config(std::string path)
+		: m_path(std::move(path))
+		, m_document(ini::readFile(m_path))
+	{
+	}
+
+	/**
+	 * Get the path to the configuration file.
 	 *
-	 * \param options the option parsed at command line
+	 * \return the path
+	 */
+	inline const std::string &path() const noexcept
+	{
+		return m_path;
+	}
+
+	/**
+	 * Find an entity if defined in the configuration file.
+	 *
+	 * \pre util::isValidIdentifier(name)
+	 * \return default identity if cannot be found
 	 */
-	Config(parser::Result options) noexcept;
+	ServerIdentity findIdentity(const std::string &name) const;
+
+	/**
+	 * Find a plugin configuration if defined in the configuration file.
+	 *
+	 * \pre util::isValidIdentifier(name)
+	 * \return the configuration or empty if not found
+	 */
+	PluginConfig findPluginConfig(const std::string &name) const;
+
+	/**
+	 * Get the path to the pidfile.
+	 *
+	 * \return the path or empty if not defined
+	 */
+	std::string pidfile() const;
 
 	/**
-	 * Load the config into irccd.
+	 * Get the uid.
+	 *
+	 * \return the uid or empty one if no one is set
+	 */
+	std::string uid() const;
+
+	/**
+	 * Get the gid.
+	 *
+	 * \return the gid or empty one if no one is set
+	 */
+	std::string gid() const;
+
+	/**
+	 * Check if verbosity is enabled.
+	 *
+	 * \return true if verbosity was requested
+	 */
+	bool isVerbose() const noexcept;
+
+	/**
+	 * Check if foreground is specified (= no daemonize).
 	 *
-	 * \param irccd the irccd instance
+	 * \return true if foreground was requested
+	 */
+	bool isForeground() const noexcept;
+
+	/**
+	 * Load logging interface.
+	 */
+	void loadLogs() const;
+
+	/**
+	 * Load formats for logging.
+	 */
+	void loadFormats() const;
+
+	/**
+	 * Load transports.
+	 *
+	 * \return the set of transports
 	 */
-	void load(Irccd &irccd);
+	std::vector<std::shared_ptr<TransportServer>> loadTransports() const;
+
+	/**
+	 * Load rules.
+	 *
+	 * \return the rules
+	 */
+	std::vector<Rule> loadRules() const;
+
+	/**
+	 * Get the list of servers defined.
+	 *
+	 * \return the list of servers
+	 */
+	std::vector<std::shared_ptr<Server>> loadServers() const;
+
+	/**
+	 * Get the list of defined plugins.
+	 *
+	 * \return the list of plugins
+	 */
+	std::vector<std::shared_ptr<Plugin>> loadPlugins() const;
 };
 
 } // !irccd
--- a/lib/irccd/fs.cpp	Wed May 04 13:38:02 2016 +0200
+++ b/lib/irccd/fs.cpp	Tue May 10 21:27:40 2016 +0200
@@ -67,11 +67,14 @@
 bool can(const std::string &path, const std::string &mode)
 {
 	auto fp = std::fopen(path.c_str(), mode.c_str());
-	auto result = fp != nullptr;
+
+	if (fp == nullptr) {
+		return false;
+	}
 
 	std::fclose(fp);
 
-	return result;
+	return true;
 }
 
 #if defined(_WIN32)
--- a/lib/irccd/ini.cpp	Wed May 04 13:38:02 2016 +0200
+++ b/lib/irccd/ini.cpp	Tue May 10 21:27:40 2016 +0200
@@ -1,5 +1,5 @@
 /*
- * ini.cpp -- .ini file parsing
+ * ini.cpp -- extended .ini file parser
  *
  * Copyright (c) 2013-2016 David Demelier <markand@malikania.fr>
  *
@@ -16,7 +16,6 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#include <cassert>
 #include <cctype>
 #include <cstring>
 #include <iostream>
@@ -25,16 +24,18 @@
 #include <sstream>
 #include <stdexcept>
 
+/* for PathIsRelative */
 #if defined(_WIN32)
-#  include <Shlwapi.h>	// for PathIsRelative
+#  include <Shlwapi.h>
 #endif
 
 #include "ini.hpp"
 
+namespace irccd {
+
 namespace {
 
-using namespace irccd;
-using namespace irccd::ini;
+using namespace ini;
 
 using StreamIterator = std::istreambuf_iterator<char>;
 using TokenIterator = std::vector<Token>::const_iterator;
@@ -69,7 +70,7 @@
 	return isList(c) || isQuote(c) || c == '[' || c == ']' || c == '@' || c == '#' || c == '=';
 }
 
-void analyzeLine(int &line, int &column, StreamIterator &it) noexcept
+void analyseLine(int &line, int &column, StreamIterator &it) noexcept
 {
 	assert(*it == '\n');
 
@@ -78,7 +79,7 @@
 	column = 0;
 }
 
-void analyzeComment(int &column, StreamIterator &it, StreamIterator end) noexcept
+void analyseComment(int &column, StreamIterator &it, StreamIterator end) noexcept
 {
 	assert(*it == '#');
 
@@ -88,7 +89,7 @@
 	}
 }
 
-void analyzeSpaces(int &column, StreamIterator &it, StreamIterator end) noexcept
+void analyseSpaces(int &column, StreamIterator &it, StreamIterator end) noexcept
 {
 	assert(isSpace(*it));
 
@@ -98,7 +99,7 @@
 	}
 }
 
-void analyzeList(Tokens &list, int line, int &column, StreamIterator &it) noexcept
+void analyseList(Tokens &list, int line, int &column, StreamIterator &it) noexcept
 {
 	assert(isList(*it));
 
@@ -117,7 +118,7 @@
 	}
 }
 
-void analyzeSection(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end)
+void analyseSection(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end)
 {
 	assert(*it == '[');
 
@@ -127,16 +128,23 @@
 	/* Read section name */
 	++ it;
 	while (it != end && *it != ']') {
-		if (*it == '\n')
+		if (*it == '\n') {
 			throw Error(line, column, "section not terminated, missing ']'");
-		if (isReserved(*it))
+		}
+		if (isReserved(*it)) {
 			throw Error(line, column, "section name expected after '[', got '" + std::string(1, *it) + "'");
+		}
+
 		++ column;
 		value += *it++;
 	}
 
-	if (it == end)
+	if (it == end) {
 		throw Error(line, column, "section name expected after '[', got <EOF>");
+	}
+	if (value.empty()) {
+		throw Error(line, column, "empty section name");
+	}
 
 	/* Remove ']' */
 	++ it;
@@ -144,7 +152,7 @@
 	list.emplace_back(Token::Section, line, save, std::move(value));
 }
 
-void analyzeAssign(Tokens &list, int &line, int &column, StreamIterator &it)
+void analyseAssign(Tokens &list, int &line, int &column, StreamIterator &it)
 {
 	assert(*it == '=');
 
@@ -152,7 +160,7 @@
 	++ it;
 }
 
-void analyzeQuotedWord(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end)
+void analyseQuotedWord(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end)
 {
 	std::string value;
 	int save = column;
@@ -164,8 +172,9 @@
 		value += *it++;
 	}
 
-	if (it == end)
+	if (it == end) {
 		throw Error(line, column, "undisclosed '" + std::string(1, quote) + "', got <EOF>");
+	}
 
 	/* Remove quote */
 	++ it;
@@ -173,7 +182,7 @@
 	list.push_back({ Token::QuotedWord, line, save, std::move(value) });
 }
 
-void analyzeWord(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end)
+void analyseWord(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end)
 {
 	assert(!isReserved(*it));
 
@@ -188,7 +197,7 @@
 	list.push_back({ Token::Word, line, save, std::move(value) });
 }
 
-void analyzeInclude(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end)
+void analyseInclude(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end)
 {
 	assert(*it == '@');
 
@@ -202,42 +211,13 @@
 		include += *it++;
 	}
 
-	if (include != "include")
+	if (include != "include") {
 		throw Error(line, column, "expected include after '@' token");
+	}
 
 	list.push_back({ Token::Include, line, save });
 }
 
-Tokens analyze(StreamIterator &it, StreamIterator end)
-{
-	Tokens list;
-	int line = 1;
-	int column = 0;
-
-	while (it != end) {
-		if (*it == '\n')
-			analyzeLine(line, column, it);
-		else if (*it == '#')
-			analyzeComment(column, it, end);
-		else if (*it == '[')
-			analyzeSection(list, line, column, it, end);
-		else if (*it == '=')
-			analyzeAssign(list, line, column, it);
-		else if (isSpace(*it))
-			analyzeSpaces(column, it, end);
-		else if (*it == '@')
-			analyzeInclude(list, line, column, it, end);
-		else if (isQuote(*it))
-			analyzeQuotedWord(list, line, column, it, end);
-		else if (isList(*it))
-			analyzeList(list, line, column, it);
-		else
-			analyzeWord(list, line, column, it, end);
-	}
-
-	return list;
-}
-
 void parseOptionValueSimple(Option &option, TokenIterator &it)
 {
 	assert(it->type() == Token::Word || it->type() == Token::QuotedWord);
@@ -270,8 +250,9 @@
 		}
 	}
 
-	if (it == end)
+	if (it == end) {
 		throw Error(save->line(), save->column(), "unterminated list construct");
+	}
 
 	/* Remove ) */
 	++ it;
@@ -284,50 +265,53 @@
 	TokenIterator save = it;
 
 	/* No '=' or something else? */
-	if (++it == end)
+	if (++it == end) {
 		throw Error(save->line(), save->column(), "expected '=' assignment, got <EOF>");
-	if (it->type() != Token::Assign)
+	}
+	if (it->type() != Token::Assign) {
 		throw Error(it->line(), it->column(), "expected '=' assignment, got " + it->value());
+	}
 
 	/* Empty options are allowed so just test for words */
 	if (++it != end) {
-		if (it->type() == Token::Word || it->type() == Token::QuotedWord)
+		if (it->type() == Token::Word || it->type() == Token::QuotedWord) {
 			parseOptionValueSimple(option, it);
-		else if (it->type() == Token::ListBegin)
+		} else if (it->type() == Token::ListBegin) {
 			parseOptionValueList(option, it, end);
+		}
 	}
 
 	sc.push_back(std::move(option));
 }
 
-void parseInclude(Document &doc, TokenIterator &it, TokenIterator end)
+void parseInclude(Document &doc, const std::string &path, TokenIterator &it, TokenIterator end)
 {
 	TokenIterator save = it;
 
-	if (++it == end)
+	if (++it == end) {
 		throw Error(save->line(), save->column(), "expected file name after '@include' statement, got <EOF>");
-	if (it->type() != Token::Word && it->type() != Token::QuotedWord)
+	}
+
+	if (it->type() != Token::Word && it->type() != Token::QuotedWord) {
 		throw Error(it->line(), it->column(), "expected file name after '@include' statement, got " + it->value());
-	if (doc.path().empty())
-		throw Error(it->line(), it->column(), "'@include' statement invalid with buffer documents");
+	}
 
 	std::string value = (it++)->value();
 	std::string file;
 
 	if (!isAbsolute(value)) {
 #if defined(_WIN32)
-		file = doc.path() + "\\" + value;
+		file = path + "\\" + value;
 #else
-		file = doc.path() + "/" + value;
+		file = path + "/" + value;
 #endif
 	} else {
 		file = value;
 	}
 
-	Document child(File{file});
-
-	for (const auto &sc : child)
+	for (const auto &sc : readFile(file)) {
 		doc.push_back(sc);
+	}
 }
 
 void parseSection(Document &doc, TokenIterator &it, TokenIterator end)
@@ -339,8 +323,9 @@
 
 	/* Read until next section */
 	while (it != end && it->type() != Token::Section) {
-		if (it->type() != Token::Word)
+		if (it->type() != Token::Word) {
 			throw Error(it->line(), it->column(), "unexpected token '" + it->value() + "' in section definition");
+		}
 
 		parseOption(sc, it, end);
 	}
@@ -348,16 +333,56 @@
 	doc.push_back(std::move(sc));
 }
 
-void parse(Document &doc, const Tokens &tokens)
+} // !namespace
+
+namespace ini {
+
+Tokens analyse(std::istreambuf_iterator<char> it, std::istreambuf_iterator<char> end)
 {
+	Tokens list;
+	int line = 1;
+	int column = 0;
+
+	while (it != end) {
+		if (*it == '\n') {
+			analyseLine(line, column, it);
+		} else if (*it == '#') {
+			analyseComment(column, it, end);
+		} else if (*it == '[') {
+			analyseSection(list, line, column, it, end);
+		} else if (*it == '=') {
+			analyseAssign(list, line, column, it);
+		} else if (isSpace(*it)) {
+			analyseSpaces(column, it, end);
+		} else if (*it == '@') {
+			analyseInclude(list, line, column, it, end);
+		} else if (isQuote(*it)) {
+			analyseQuotedWord(list, line, column, it, end);
+		} else if (isList(*it)) {
+			analyseList(list, line, column, it);
+		} else {
+			analyseWord(list, line, column, it, end);
+		}
+	}
+
+	return list;
+}
+
+Tokens analyse(std::istream &stream)
+{
+	return analyse(std::istreambuf_iterator<char>(stream), {});
+}
+
+Document parse(const Tokens &tokens, const std::string &path)
+{
+	Document doc;
 	TokenIterator it = tokens.cbegin();
 	TokenIterator end = tokens.cend();
 
 	while (it != end) {
-		/* Just ignore this */
 		switch (it->type()) {
 		case Token::Include:
-			parseInclude(doc, it, end);
+			parseInclude(doc, path, it, end);
 			break;
 		case Token::Section:
 			parseSection(doc, it, end);
@@ -366,60 +391,44 @@
 			throw Error(it->line(), it->column(), "unexpected '" + it->value() + "' on root document");
 		}
 	}
-}
 
-} // !namespace
-
-namespace irccd {
-
-namespace ini {
-
-Tokens Document::analyze(const File &file)
-{
-	std::fstream stream(file.path);
-
-	if (!stream)
-		throw std::runtime_error(std::strerror(errno));
-
-	std::istreambuf_iterator<char> it(stream);
-	std::istreambuf_iterator<char> end;
-
-	return ::analyze(it, end);
+	return doc;
 }
 
-Tokens Document::analyze(const Buffer &buffer)
+Document readFile(const std::string &filename)
 {
-	std::istringstream stream(buffer.text);
-	std::istreambuf_iterator<char> it(stream);
-	std::istreambuf_iterator<char> end;
+	/* Get parent path */
+	auto parent = filename;
+	auto pos = parent.find_last_of("/\\");
 
-	return ::analyze(it, end);
+	if (pos != std::string::npos) {
+		parent.erase(pos);
+	} else {
+		parent = ".";
+	}
+
+	std::ifstream input(filename);
+
+	if (!input) {
+		throw Error(0, 0, std::strerror(errno));
+	}
+
+	return parse(analyse(input), parent);
 }
 
-Document::Document(const File &file)
-	: m_path(file.path)
+Document readString(const std::string &buffer)
 {
-	/* Update path */
-	auto pos = m_path.find_last_of("/\\");
+	std::istringstream iss(buffer);
 
-	if (pos != std::string::npos)
-		m_path.erase(pos);
-	else
-		m_path = ".";
-
-	parse(*this, analyze(file));
+	return parse(analyse(iss));
 }
 
-Document::Document(const Buffer &buffer)
+void dump(const Tokens &tokens)
 {
-	parse(*this, analyze(buffer));
-}
-
-void Document::dump(const Tokens &tokens)
-{
-	for (const Token &token: tokens)
+	for (const Token &token: tokens) {
 		// TODO: add better description
 		std::cout << token.line() << ":" << token.column() << ": " << token.value() << std::endl;
+	}
 }
 
 } // !ini
--- a/lib/irccd/ini.hpp	Wed May 04 13:38:02 2016 +0200
+++ b/lib/irccd/ini.hpp	Tue May 10 21:27:40 2016 +0200
@@ -1,5 +1,5 @@
 /*
- * ini.hpp -- .ini file parsing
+ * ini.hpp -- extended .ini file parser
  *
  * Copyright (c) 2013-2016 David Demelier <markand@malikania.fr>
  *
@@ -21,10 +21,90 @@
 
 /**
  * \file ini.hpp
- * \brief Configuration file parser.
+ * \brief Extended .ini file parser.
+ * \author David Demelier <markand@malikania.fr>
+ */
+
+/**
+ * \page Ini Ini
+ * \brief Extended .ini file parser.
+ *
+ *   - \subpage ini-syntax
+ */
+
+/**
+ * \page ini-syntax Syntax
+ * \brief File syntax.
+ *
+ * The syntax is similar to most of `.ini` implementations as:
+ *
+ *   - a section is delimited by `[name]` can be redefined multiple times,
+ *   - an option **must** always be defined in a section,
+ *   - empty options must be surrounded by quotes,
+ *   - lists can not includes trailing commas,
+ *   - include statement must always be at the beginning of files (in no sections),
+ *   - comments starts with # until the end of line,
+ *   - options with spaces **must** use quotes.
+ *
+ * # Basic file
+ *
+ * ````ini
+ * # This is a comment.
+ * [section]
+ * option1 = value1
+ * option2 = "value 2 with spaces"    # comment is also allowed here
+ * ````
+ *
+ * # Redefinition
+ *
+ * Sections can be redefined multiple times and are kept the order they are seen.
+ *
+ * ````ini
+ * [section]
+ * value = "1"
+ * 
+ * [section]
+ * value = "2"
+ * ````
+ *
+ * The ini::Document object will contains two ini::Section.
+ *
+ * # Lists
+ *
+ * Lists are defined using `()` and commas, like values, they may have quotes.
+ *
+ * ````ini
+ * [section]
+ * names = ( "x1", "x2" )
+ *
+ * # This is also allowed
+ * biglist = (
+ *   "abc",
+ *   "def"
+ * )
+ * ````
+ *
+ * # Include statement
+ *
+ * You can split a file into several pieces, if the include statement contains a relative path, the path will be relative
+ * to the current file being parsed.
+ *
+ * You **must** use the include statement before any section.
+ *
+ * If the file contains spaces, use quotes.
+ *
+ * ````ini
+ * # main.conf
+ * @include "foo.conf"
+ *
+ * # foo.conf
+ * [section]
+ * option1 = value1
+ * ````
  */
 
 #include <algorithm>
+#include <cassert>
 #include <exception>
 #include <stdexcept>
 #include <string>
@@ -41,7 +121,7 @@
 
 /**
  * \class Error
- * \brief Error in a file
+ * \brief Error in a file.
  */
 class Error : public std::exception {
 private:
@@ -53,14 +133,14 @@
 	/**
 	 * Constructor.
 	 *
-	 * \param l the line
-	 * \param c the column
-	 * \param m the message
+	 * \param line the line
+	 * \param column the column
+	 * \param msg the message
 	 */
-	inline Error(int l, int c, std::string m) noexcept
-		: m_line(l)
-		, m_column(c)
-		, m_message(std::move(m))
+	inline Error(int line, int column, std::string msg) noexcept
+		: m_line(line)
+		, m_column(column)
+		, m_message(std::move(msg))
 	{
 	}
 
@@ -97,16 +177,16 @@
 
 /**
  * \class Token
- * \brief Describe a token read in the .ini source
+ * \brief Describe a token read in the .ini source.
  *
  * This class can be used when you want to parse a .ini file yourself.
  *
- * \see Document::analyze
+ * \see analyze
  */
 class Token {
 public:
 	/**
-	 * \brief Token type
+	 * \brief Token type.
 	 */
 	enum Type {
 		Include,	//!< include statement
@@ -224,30 +304,35 @@
 	/**
 	 * Construct an empty option.
 	 *
+	 * \pre key must not be empty
 	 * \param key the key
-	 * \param value the value
 	 */
 	inline Option(std::string key) noexcept
 		: std::vector<std::string>()
 		, m_key(std::move(key))
 	{
+		assert(!m_key.empty());
 	}
 
 	/**
 	 * Construct a single option.
 	 *
+	 * \pre key must not be empty
 	 * \param key the key
 	 * \param value the value
 	 */
 	inline Option(std::string key, std::string value) noexcept
 		: m_key(std::move(key))
 	{
+		assert(!m_key.empty());
+
 		push_back(std::move(value));
 	}
 
 	/**
 	 * Construct a list option.
 	 *
+	 * \pre key must not be empty
 	 * \param key the key
 	 * \param values the values
 	 */
@@ -255,6 +340,7 @@
 		: std::vector<std::string>(std::move(values))
 		, m_key(std::move(key))
 	{
+		assert(!m_key.empty());
 	}
 
 	/**
@@ -292,11 +378,13 @@
 	/**
 	 * Construct a section with its name.
 	 *
+	 * \pre key must not be empty
 	 * \param key the key
 	 */
 	inline Section(std::string key) noexcept
 		: m_key(std::move(key))
 	{
+		assert(!m_key.empty());
 	}
 
 	/**
@@ -321,30 +409,6 @@
 	}
 
 	/**
-	 * Access an option at the specified key.
-	 *
-	 * \param key the key
-	 * \return the option
-	 * \throw std::out_of_range if the key does not exist
-	 */
-	inline Option &operator[](const std::string &key)
-	{
-		return *find(key);
-	}
-
-	/**
-	 * Access an option at the specified key.
-	 *
-	 * \param key the key
-	 * \return the option
-	 * \throw std::out_of_range if the key does not exist
-	 */
-	inline const Option &operator[](const std::string &key) const
-	{
-		return *find(key);
-	}
-
-	/**
 	 * Find an option by key and return an iterator.
 	 *
 	 * \param key the key
@@ -371,104 +435,52 @@
 	}
 
 	/**
+	 * Access an option at the specified key.
+	 *
+	 * \param key the key
+	 * \return the option
+	 * \pre contains(key) must return true
+	 */
+	inline Option &operator[](const std::string &key)
+	{
+		assert(contains(key));
+
+		return *find(key);
+	}
+
+	/**
+	 * Overloaded function.
+	 *
+	 * \param key the key
+	 * \return the option
+	 * \pre contains(key) must return true
+	 */
+	inline const Option &operator[](const std::string &key) const
+	{
+		assert(contains(key));
+
+		return *find(key);
+	}
+
+	/**
 	 * Inherited operators.
 	 */
 	using std::vector<Option>::operator[];
 };
 
 /**
- * \class File
- * \brief Source for reading .ini files.
- */
-class File {
-public:
-	/**
-	 * Path to the file.
-	 */
-	std::string path;
-};
-
-/**
- * \class Buffer
- * \brief Source for reading ini from text.
- * \note the include statement is not supported with buffers.
- */
-class Buffer {
-public:
-	/**
-	 * The ini content.
-	 */
-	std::string text;
-};
-
-/**
  * \class Document
- * \brief Ini config file loader
+ * \brief Ini document description.
+ * \see readFile
+ * \see readString
  */
 class Document : public std::vector<Section> {
-private:
-	std::string m_path;
-
 public:
 	/**
-	 * Analyze a file and extract tokens. If the function succeeds, that does not mean the content is valid,
-	 * it just means that there are no syntax error.
-	 *
-	 * For example, this class does not allow adding options under no sections and this function will not
-	 * detect that issue.
-	 *
-	 * \param file the file to read
-	 * \return the list of tokens
-	 * \throws Error on errors
-	 */
-	static Tokens analyze(const File &file);
-
-	/**
-	 * Overloaded function for buffers.
-	 *
-	 * \param buffer the buffer to read
-	 * \return the list of tokens
-	 * \throws Error on errors
-	 */
-	static Tokens analyze(const Buffer &buffer);
-
-	/**
-	 * Show all tokens and their description.
-	 *
-	 * \param tokens the tokens
-	 */
-	static void dump(const Tokens &tokens);
-
-	/**
-	 * Construct a document from a file.
-	 *
-	 * \param file the file to read
-	 * \throws Error on errors
-	 */
-	Document(const File &file);
-
-	/**
-	 * Overloaded constructor for buffers.
-	 *
-	 * \param buffer the buffer to read
-	 * \throws Error on errors
-	 */
-	Document(const Buffer &buffer);
-
-	/**
-	 * Get the current document path, only useful when constructed from File source.
-	 *
-	 * \return the path
-	 */
-	inline const std::string &path() const noexcept
-	{
-		return m_path;
-	}
-
-	/**
 	 * Check if a document has a specific section.
 	 *
 	 * \param key the key
+	 * \return true if the document contains the section
 	 */
 	inline bool contains(const std::string &key) const noexcept
 	{
@@ -476,30 +488,6 @@
 	}
 
 	/**
-	 * Access a section at the specified key.
-	 *
-	 * \param key the key
-	 * \return the section
-	 * \throw std::out_of_range if the key does not exist
-	 */
-	inline Section &operator[](const std::string &key)
-	{
-		return *find(key);
-	}
-
-	/**
-	 * Access a section at the specified key.
-	 *
-	 * \param key the key
-	 * \return the section
-	 * \throw std::out_of_range if the key does not exist
-	 */
-	inline const Section &operator[](const std::string &key) const
-	{
-		return *find(key);
-	}
-
-	/**
 	 * Find a section by key and return an iterator.
 	 *
 	 * \param key the key
@@ -526,11 +514,99 @@
 	}
 
 	/**
+	 * Access a section at the specified key.
+	 *
+	 * \param key the key
+	 * \return the section
+	 * \pre contains(key) must return true
+	 */
+	inline Section &operator[](const std::string &key)
+	{
+		assert(contains(key));
+
+		return *find(key);
+	}
+
+	/**
+	 * Overloaded function.
+	 *
+	 * \param key the key
+	 * \return the section
+	 * \pre contains(key) must return true
+	 */
+	inline const Section &operator[](const std::string &key) const
+	{
+		assert(contains(key));
+
+		return *find(key);
+	}
+
+	/**
 	 * Inherited operators.
 	 */
 	using std::vector<Section>::operator[];
 };
 
+/**
+ * Analyse a stream and detect potential syntax errors. This does not parse the file like including other
+ * files in include statement.
+ *
+ * It does only analysis, for example if an option is defined under no section, this does not trigger an
+ * error while it's invalid.
+ *
+ * \param it the iterator
+ * \param end where to stop
+ * \return the list of tokens
+ * \throws Error on errors
+ */
+Tokens analyse(std::istreambuf_iterator<char> it, std::istreambuf_iterator<char> end);
+
+/**
+ * Overloaded function for stream.
+ *
+ * \param stream the stream
+ * \return the list of tokens
+ * \throws Error on errors
+ */
+Tokens analyse(std::istream &stream);
+
+/**
+ * Parse the produced tokens.
+ *
+ * \param tokens the tokens
+ * \param path the parent path
+ * \return the document
+ * \throw Error on errors
+ */
+Document parse(const Tokens &tokens, const std::string &path = ".");
+
+/**
+ * Parse a file.
+ *
+ * \param filename the file name
+ * \return the document
+ * \throw Error on errors
+ */
+Document readFile(const std::string &filename);
+
+/**
+ * Parse a string.
+ *
+ * If the string contains include statements, they are relative to the current working directory.
+ *
+ * \param buffer the buffer
+ * \return the document
+ * \throw Error on errors
+ */
+Document readString(const std::string &buffer);
+
+/**
+ * Show all tokens and their description.
+ *
+ * \param tokens the tokens
+ */
+void dump(const Tokens &tokens);
+
 } // !ini
 
 } // !irccd
--- a/lib/irccd/irccd.cpp	Wed May 04 13:38:02 2016 +0200
+++ b/lib/irccd/irccd.cpp	Tue May 10 21:27:40 2016 +0200
@@ -625,7 +625,7 @@
 		auto tc = ptr.lock();
 
 		if (tc) {
-			m_lookupTransportClients.erase(tc->handle());
+			m_transportClients.erase(std::find(m_transportClients.begin(), m_transportClients.end(), tc));
 		}
 	});
 }
@@ -643,21 +643,21 @@
 
 void Irccd::processTransportClients(fd_set &input, fd_set &output)
 {
-	for (auto &pair : m_lookupTransportClients) {
-		pair.second->sync(input, output);
+	for (auto &client : m_transportClients) {
+		client->sync(input, output);
 	}
 }
 
 void Irccd::processTransportServers(fd_set &input)
 {
-	for (auto &pair : m_lookupTransportServers) {
-		if (!FD_ISSET(pair.second->handle(), &input)) {
+	for (auto &transport : m_transportServers) {
+		if (!FD_ISSET(transport->handle(), &input)) {
 			continue;
 		}
 
 		log::debug("transport: new client connected");
 
-		std::shared_ptr<TransportClient> client = pair.second->accept();
+		std::shared_ptr<TransportClient> client = transport->accept();
 		std::weak_ptr<TransportClient> ptr(client);
 
 		/* Send some information */
@@ -682,14 +682,14 @@
 		client->onDie.connect(std::bind(&Irccd::handleTransportDie, this, ptr));
 
 		/* Register it */
-		m_lookupTransportClients.emplace(client->handle(), move(client));
+		m_transportClients.push_back(std::move(client));
 	}
 }
 
 void Irccd::processServers(fd_set &input, fd_set &output)
 {
-	for (auto &pair : m_servers) {
-		pair.second->sync(input, output);
+	for (auto &server : m_servers) {
+		server->sync(input, output);
 	}
 }
 
@@ -736,7 +736,9 @@
 
 void Irccd::addServer(shared_ptr<Server> server) noexcept
 {
+#if 0
 	assert(m_servers.count(server->info().name) == 0);
+#endif
 
 	std::weak_ptr<Server> ptr(server);
 
@@ -762,50 +764,54 @@
 
 			if (server) {
 				log::info(fmt::format("server {}: removed", server->info().name));
-				m_servers.erase(server->info().name);
+				m_servers.erase(std::find(m_servers.begin(), m_servers.end(), server));
 			}
 		});
 	});
 
-	m_servers.emplace(server->info().name, move(server));
+	m_servers.push_back(std::move(server));
 }
 
 std::shared_ptr<Server> Irccd::getServer(const std::string &name) const noexcept
 {
-	auto it = m_servers.find(name);
+	auto it = std::find_if(m_servers.begin(), m_servers.end(), [&] (const auto &server) {
+		return server->info().name == name;
+	});
 
 	if (it == m_servers.end()) {
 		return nullptr;
 	}
 
-	return it->second;
+	return *it;
 }
 
 std::shared_ptr<Server> Irccd::requireServer(const std::string &name) const
 {
-	auto it = m_servers.find(name);
+	auto server = getServer(name);
 
-	if (it == m_servers.end()) {
-		throw std::invalid_argument("server " + name + " not found");
+	if (!server) {
+		throw std::invalid_argument("server {} not found"_format(name));
 	}
 
-	return it->second;
+	return server;
 }
 
 void Irccd::removeServer(const std::string &name)
 {
-	auto it = m_servers.find(name);
+	auto it = std::find_if(m_servers.begin(), m_servers.end(), [&] (const auto &server) {
+		return server->info().name == name;
+	});
 
 	if (it != m_servers.end()) {
-		it->second->disconnect();
+		(*it)->disconnect();
 		m_servers.erase(it);
 	}
 }
 
 void Irccd::clearServers() noexcept
 {
-	for (auto &pair : m_servers) {
-		pair.second->disconnect();
+	for (auto &server : m_servers) {
+		server->disconnect();
 	}
 
 	m_servers.clear();
@@ -813,14 +819,14 @@
 
 void Irccd::addTransport(std::shared_ptr<TransportServer> ts)
 {
-	m_lookupTransportServers.emplace(ts->handle(), ts);
+	m_transportServers.push_back(std::move(ts));
 }
 
 void Irccd::broadcast(std::string data)
 {
-	/* Asynchronous send */
-	for (auto &pair : m_lookupTransportClients) {
-		pair.second->send(data);
+	// Asynchronous send.
+	for (auto &client : m_transportClients) {
+		client->send(data);
 	}
 }
 
@@ -828,24 +834,26 @@
 
 std::shared_ptr<Plugin> Irccd::getPlugin(const std::string &name) const noexcept
 {
-	auto it = m_plugins.find(name);
+	auto it = std::find_if(m_plugins.begin(), m_plugins.end(), [&] (const auto &plugin) {
+		return plugin->name() == name;
+	});
 
 	if (it == m_plugins.end()) {
 		return nullptr;
 	}
 
-	return it->second;
+	return *it;
 }
 
 std::shared_ptr<Plugin> Irccd::requirePlugin(const std::string &name) const
 {
-	auto it = m_plugins.find(name);
+	auto plugin = getPlugin(name);
 
-	if (it == m_plugins.end()) {
-		throw std::out_of_range(std::string("plugin ") + name + " not found");
+	if (!plugin) {
+		throw std::invalid_argument("plugin {} not found"_format(name));
 	}
 
-	return it->second;
+	return plugin;
 }
 
 void Irccd::addPlugin(std::shared_ptr<Plugin> plugin)
@@ -861,15 +869,20 @@
 	/* Initial load now */
 	try {
 		plugin->onLoad();
-		m_plugins.insert({plugin->name(), plugin});
+		m_plugins.push_back(std::move(plugin));
 	} catch (const std::exception &ex) {
-		log::warning(fmt::format("plugin {}: {}", plugin->name(), ex.what()));
+		log::warning("plugin {}: {}"_format(plugin->name(), ex.what()));
 	}
 }
 
 void Irccd::loadPlugin(std::string name, const std::string &source, bool find)
 {
-	if (m_plugins.count(name) > 0) {
+	// TODO: change with Plugin::find
+	auto it = std::find_if(m_plugins.begin(), m_plugins.end(), [&] (const auto &plugin) {
+		return plugin->name() == name;
+	});
+
+	if (it != m_plugins.end()) {
 		throw std::invalid_argument("plugin already loaded");
 	}
 
@@ -891,7 +904,7 @@
 		log::info() << "  from " << path << std::endl;
 
 		try {
-			plugin = std::make_shared<Plugin>(name, path, m_pluginConf[name]);
+			plugin = std::make_shared<Plugin>(name, path /*, m_pluginConf[name] */);
 			break;
 		} catch (const std::exception &ex) {
 			log::info(fmt::format("    error: {}", ex.what()));
@@ -916,11 +929,13 @@
 
 void Irccd::unloadPlugin(const std::string &name)
 {
-	auto plugin = getPlugin(name);
+	auto it = std::find_if(m_plugins.begin(), m_plugins.end(), [&] (const auto &plugin) {
+		return plugin->name() == name;
+	});
 
-	if (plugin) {
-		plugin->onUnload();
-		m_plugins.erase(name);
+	if (it != m_plugins.end()) {
+		(*it)->onUnload();
+		m_plugins.erase(it);
 	}
 }
 
@@ -999,22 +1014,23 @@
 	FD_SET(m_socketServer.handle(), &setinput);
 
 	/* 2. Add servers */
-	for (auto &pair : m_servers) {
-		pair.second->update();
-		pair.second->prepare(setinput, setoutput, max);
+	for (auto &server : m_servers) {
+		server->update();
+		server->prepare(setinput, setoutput, max);
 	}
 
 	/* 3. Add transports clients */
-	for (auto &pair : m_lookupTransportClients) {
-		set(setinput, pair.first);
+	for (auto &client : m_transportClients) {
+		set(setinput, client->handle());
 
-		if (pair.second->hasOutput())
-			set(setoutput, pair.first);
+		if (client->hasOutput()) {
+			set(setoutput, client->handle());
+		}
 	}
 
 	/* 4. Add transport servers */
-	for (auto &pair : m_lookupTransportServers) {
-		set(setinput, pair.first);
+	for (auto &transport : m_transportServers) {
+		set(setinput, transport->handle());
 	}
 
 	/* 5. Do the selection */
@@ -1045,7 +1061,7 @@
 	 * Make a copy because the events can add other events while we are iterating it. Also lock because the timers
 	 * may alter these events too.
 	 */
-	std::vector<Event> copy;
+	std::vector<std::function<void (Irccd &)>> copy;
 
 	{
 		std::lock_guard<mutex> lock(m_mutex);
--- a/lib/irccd/irccd.hpp	Wed May 04 13:38:02 2016 +0200
+++ b/lib/irccd/irccd.hpp	Tue May 10 21:27:40 2016 +0200
@@ -24,6 +24,7 @@
  * \brief Base class for irccd front end.
  */
 
+#include <algorithm>
 #include <atomic>
 #include <cassert>
 #include <condition_variable>
@@ -52,50 +53,6 @@
 class TransportCommand;
 
 /**
- * Event to execute after the poll.
- */
-using Event = std::function<void (Irccd &)>;
-
-/**
- * List of events.
- */
-using Events = std::vector<Event>;
-
-/**
- * Map of identities.
- */
-using Identities = std::unordered_map<std::string, ServerIdentity>;
-
-/**
- * List of rules.
- */
-using Rules = std::vector<Rule>;
-
-/**
- * Map of servers.
- */
-using Servers = std::unordered_map<std::string, std::shared_ptr<Server>>;
-
-/**
- * Map of transport command handlers.
- */
-using TransportCommands = std::unordered_map<std::string, std::unique_ptr<TransportCommand>>;
-
-#if defined(WITH_JS)
-
-/**
- * Map of plugins.
- */
-using Plugins = std::unordered_map<std::string, std::shared_ptr<Plugin>>;
-
-/**
- * Map of plugin configurations.
- */
-using PluginConfigs = std::unordered_map<std::string, PluginConfig>;
-
-#endif
-
-/**
  * \class Irccd
  * \brief Irccd main instance
  *
@@ -111,40 +68,29 @@
  */
 class Irccd : public Application {
 private:
-	template <typename T>
-	using LookupTable = std::unordered_map<net::Handle, std::shared_ptr<T>>;
-
-	/* Main loop */
+	// Main loop stuff.
 	std::atomic<bool> m_running{true};
+	std::mutex m_mutex;
+	std::vector<std::function<void (Irccd &)>> m_events;
 
-	/* Mutex for post() */
-	std::mutex m_mutex;
-
-	/* IPC */
+	// IPC.
 	net::SocketTcp<net::address::Ip> m_socketServer;
 	net::SocketTcp<net::address::Ip> m_socketClient;
 
-	/* Event loop */
-	Events m_events;
+	// Servers.
+	std::vector<std::shared_ptr<Server>> m_servers;
 
-	/* Servers */
-	Servers m_servers;
-
-	/* Optional JavaScript plugins */
+	// Optional plugins.
 #if defined(WITH_JS)
-	Plugins m_plugins;
-	PluginConfigs m_pluginConf;
+	std::vector<std::shared_ptr<Plugin>> m_plugins;
 #endif
 
-	/* Identities */
-	Identities m_identities;
+	// Rules.
+	std::vector<Rule> m_rules;
 
-	/* Rules */
-	Rules m_rules;
-
-	/* Lookup tables */
-	LookupTable<TransportClient> m_lookupTransportClients;
-	LookupTable<TransportServer> m_lookupTransportServers;
+	// Transports.
+	std::vector<std::shared_ptr<TransportClient>> m_transportClients;
+	std::vector<std::shared_ptr<TransportServer>> m_transportServers;
 
 	/*
 	 * Server slots
@@ -207,17 +153,6 @@
 	Irccd();
 
 	/**
-	 * Load a configuration into irccd. Added as convenience to allow expressions like `irccd.load(Config{"foo"})`.
-	 *
-	 * \param config the configuration loader
-	 */
-	template <typename T>
-	inline void load(T &&config)
-	{
-		config.load(*this);
-	}
-
-	/**
 	 * Add an event to the queue. This will immediately signals the event loop to interrupt itself to dispatch
 	 * the pending events.
 	 *
@@ -227,37 +162,6 @@
 	void post(std::function<void (Irccd &)> ev) noexcept;
 
 	/*
-	 * Identity management
-	 * ----------------------------------------------------------
-	 *
-	 * Functions to get or add new identities.
-	 */
-
-	/**
-	 * Add an identity.
-	 *
-	 * \param identity the identity
-	 * \note If the identity already exists, it is overriden
-	 */
-	inline void addIdentity(ServerIdentity identity) noexcept
-	{
-		m_identities.emplace(identity.name, std::move(identity));
-	}
-
-	/**
-	 * Get an identity, if not found, the default one is used.
-	 *
-	 * \param name the identity name
-	 * \return the identity or default one
-	 */
-	inline ServerIdentity findIdentity(const std::string &name) const noexcept
-	{
-		auto it = m_identities.find(name);
-
-		return it == m_identities.end() ? ServerIdentity() : it->second;
-	}
-
-	/*
 	 * Server management
 	 * ----------------------------------------------------------
 	 *
@@ -274,7 +178,9 @@
 	 */
 	inline bool hasServer(const std::string &name) const noexcept
 	{
-		return m_servers.count(name) > 0;
+		return std::count_if(m_servers.cbegin(), m_servers.end(), [&] (const auto &sv) {
+			return sv->info().name == name;
+		}) > 0;
 	}
 
 	/**
@@ -303,11 +209,11 @@
 	std::shared_ptr<Server> requireServer(const std::string &name) const;
 
 	/**
-	 * Get the map of loaded servers.
+	 * Get the list of servers
 	 *
 	 * \return the servers
 	 */
-	inline const Servers &servers() const noexcept
+	inline const std::vector<std::shared_ptr<Server>> &servers() const noexcept
 	{
 		return m_servers;
 	}
@@ -365,7 +271,9 @@
 	 */
 	inline bool hasPlugin(const std::string &name) const noexcept
 	{
-		return m_plugins.count(name) > 0;
+		return std::count_if(m_plugins.cbegin(), m_plugins.cend(), [&] (const auto &plugin) {
+			return plugin->name() == name;
+		}) > 0;
 	}
 
 	/**
@@ -386,17 +294,6 @@
 	std::shared_ptr<Plugin> requirePlugin(const std::string &name) const;
 
 	/**
-	 * Add plugin configuration for the specified plugin.
-	 *
-	 * \param name
-	 * \param config
-	 */
-	inline void addPluginConfig(std::string name, PluginConfig config)
-	{
-		m_pluginConf.emplace(std::move(name), std::move(config));
-	}
-
-	/**
 	 * Add a loaded plugin.
 	 *
 	 * Plugins signals will be connected to the irccd main loop. The onLoad function will also be called and the
@@ -437,7 +334,7 @@
 	 *
 	 * \return the map of plugins
 	 */
-	inline const Plugins &plugins() const noexcept
+	inline const std::vector<std::shared_ptr<Plugin>> &plugins() const noexcept
 	{
 		return m_plugins;
 	}
--- a/lib/irccd/irccdctl.cpp	Wed May 04 13:38:02 2016 +0200
+++ b/lib/irccd/irccdctl.cpp	Tue May 10 21:27:40 2016 +0200
@@ -250,7 +250,7 @@
 
 void Irccdctl::read(const std::string &path, const parser::Result &options)
 {
-	ini::Document doc(ini::File{path});
+	ini::Document doc = ini::readFile(path);
 	ini::Document::const_iterator it = doc.find("connect");
 
 	/* Do not try to read [connect] if specified at command line */
--- a/lib/irccd/js-plugin.cpp	Wed May 04 13:38:02 2016 +0200
+++ b/lib/irccd/js-plugin.cpp	Tue May 10 21:27:40 2016 +0200
@@ -103,8 +103,8 @@
 	duk::push(ctx, duk::Array{});
 
 	int i = 0;
-	for (const auto &pair : duk::getGlobal<duk::RawPointer<Irccd>>(ctx, "\xff""\xff""irccd")->plugins()) {
-		duk::putProperty(ctx, -1, i++, pair.first);
+	for (const auto &plugin : duk::getGlobal<duk::RawPointer<Irccd>>(ctx, "\xff""\xff""irccd")->plugins()) {
+		duk::putProperty(ctx, -1, i++, plugin->name());
 	}
 
 	return 1;
--- a/lib/irccd/js-server.cpp	Wed May 04 13:38:02 2016 +0200
+++ b/lib/irccd/js-server.cpp	Tue May 10 21:27:40 2016 +0200
@@ -468,8 +468,8 @@
 {
 	duk::push(ctx, duk::Object{});
 
-	for (const auto &pair : duk::getGlobal<duk::RawPointer<Irccd>>(ctx, "\xff""\xff""irccd")->servers()) {
-		duk::putProperty(ctx, -1, pair.first, duk::Shared<Server>{pair.second});
+	for (const auto &server : duk::getGlobal<duk::RawPointer<Irccd>>(ctx, "\xff""\xff""irccd")->servers()) {
+		duk::putProperty(ctx, -1, server->info().name, duk::Shared<Server>{server});
 	}
 
 	return 1;
--- a/lib/irccd/plugin.cpp	Wed May 04 13:38:02 2016 +0200
+++ b/lib/irccd/plugin.cpp	Tue May 10 21:27:40 2016 +0200
@@ -18,6 +18,8 @@
 
 #include <stdexcept>
 
+#include <format.h>
+
 #include "sysconfig.hpp"
 
 #if defined(HAVE_STAT)
@@ -46,9 +48,11 @@
 
 using namespace std;
 
+using namespace fmt::literals;
+
 namespace irccd {
 
-void Plugin::call(const string &name, unsigned nargs)
+void Plugin::call(const std::string &name, unsigned nargs)
 {
 	duk::getGlobal<void>(m_context, name);
 
@@ -148,6 +152,29 @@
 	duk::pop(m_context, 2);
 }
 
+std::shared_ptr<Plugin> Plugin::find(const std::string &name, const PluginConfig &config)
+{
+	log::info("plugin {}: searching {}.js"_format(name, name));
+
+	for (const auto &path : path::list(path::PathPlugins)) {
+		std::string fullpath = path + name + ".js";
+
+		if (!fs::isReadable(fullpath)) {
+			continue;
+		}
+
+		log::info("plugin {}: trying {}"_format(name, fullpath));
+
+		try {
+			return std::make_shared<Plugin>(name, fullpath, config);
+		} catch (const std::exception &ex) {
+			throw std::runtime_error("{}: {}"_format(fullpath, ex.what()));
+		}
+	}
+
+	throw std::runtime_error("no suitable plugin found");
+}
+
 Plugin::Plugin(std::string name, std::string path, const PluginConfig &config)
 	: m_name(std::move(name))
 	, m_path(std::move(path))
--- a/lib/irccd/plugin.hpp	Wed May 04 13:38:02 2016 +0200
+++ b/lib/irccd/plugin.hpp	Tue May 10 21:27:40 2016 +0200
@@ -107,6 +107,14 @@
 
 public:
 	/**
+	 * Find plugin in standard paths.
+	 *
+	 * \param name the plugin name
+	 * \param config the optional configuration
+	 */
+	static std::shared_ptr<Plugin> find(const std::string &name, const PluginConfig &config = PluginConfig());
+
+	/**
 	 * Constructor.
 	 *
 	 * \param name the plugin id
--- a/lib/irccd/server-event.cpp	Wed May 04 13:38:02 2016 +0200
+++ b/lib/irccd/server-event.cpp	Tue May 10 21:27:40 2016 +0200
@@ -37,9 +37,9 @@
 
 void ServerEvent::operator()(Irccd &irccd) const
 {
-	for (auto &pair : irccd.plugins()) {
-		auto name = m_plugin_function_name(*pair.second);
-		auto allowed = Rule::solve(irccd.rules(), m_server, m_target, m_origin, pair.first, name);
+	for (auto &plugin : irccd.plugins()) {
+		auto eventname = m_plugin_function_name(*plugin);
+		auto allowed = Rule::solve(irccd.rules(), m_server, m_target, m_origin, plugin->name(), eventname);
 
 		if (!allowed) {
 			log::debug() << "rule: event skipped on match" << std::endl;
@@ -49,9 +49,9 @@
 		}
 
 		try {
-			m_plugin_exec(*pair.second);
+			m_plugin_exec(*plugin);
 		} catch (const duk::ErrorInfo &info) {
-			log::warning() << "plugin " << pair.second->name() << ": error: " << info.what() << std::endl;
+			log::warning() << "plugin " << plugin->name() << ": error: " << info.what() << std::endl;
 
 			if (!info.fileName.empty()) {
 				log::warning() << "    " << info.fileName << ":" << info.lineNumber << std::endl;