/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 4 -*- */
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <libgen.h>
#include <libxml/parser.h>
#include <libxml/tree.h>
#include "IMAuth.hh"
#include "IMLog.hh"
#ifdef HAVE_TLS
#include "IMTLS.hh"
#endif
#include "IMSvrXMLConf.hh"

#define	EMPTY_NODE_ERROR(n)		LOG_ERROR("<%s> is an empty node.", n)
#define	DUPLICATED_NODE_ERROR(n, p)	LOG_ERROR("<%s> had the duplicated node `<%s>'", p, n)
#define	UNKNOWN_NODE_ERROR(n, p)	LOG_WARNING("<%s> had an unknown node `<%s>'", p, n)
#define	INVALID_NODE_ERROR(n, v)	LOG_ERROR("<%s> is an invalid node. [%s]", n, v)
#define	MISSING_NODE_ERROR(n, p)	LOG_ERROR("<%s> is missing in <%s>", n, p)


IMAuth::access_type
IMSvrXMLConf::get_access_type(xmlChar *key)
{
    if (key == NULL) {
	return IMAuth::UNKNOWN;
    } else if (xmlStrcasecmp(key, (xmlChar *)"permit") == 0) {
	return IMAuth::PERMIT;
    } else if (xmlStrcasecmp(key, (xmlChar *)"checkuser") == 0) {
	return IMAuth::CHECKUSER;
    } else if (xmlStrcasecmp(key, (xmlChar *)"password") == 0) {
	return IMAuth::PASSWORD;
    } else if (xmlStrcasecmp(key, (xmlChar *)"deny") == 0) {
	return IMAuth::DENY;
    }

    return IMAuth::UNKNOWN;
}

string
IMSvrXMLConf::parse_text_node(xmlNodePtr &node)
{
    string retval;

    while (node != NULL) {
	if (xmlStrcmp(node->name, (xmlChar *)"text") == 0) {
	    retval = (const char *)node->content;
	    node = node->next;
	} else if (xmlStrcmp(node->name, (xmlChar *)"comment") == 0) {
	    /* ignore comment node */
	    node = node->next;
	} else {
	    UNKNOWN_NODE_ERROR(node->name, "text");
	    node = node->next;
	}
    }

    return retval;
}

string
IMSvrXMLConf::parse_hostname_node(xmlNodePtr &node)
{
    string retval;

    if (node->name == NULL || xmlStrlen(node->name) == 0) {
	EMPTY_NODE_ERROR("hostname");
	return retval;
    } else {
	retval = parse_text_node(node);
    }

    return retval;
}

string
IMSvrXMLConf::parse_port_node(xmlNodePtr &node)
{
    string retval;

    if (node->name == NULL || xmlStrlen(node->name) == 0) {
	EMPTY_NODE_ERROR("port");
	return retval;
    } else {
	retval = parse_text_node(node);
    }

#if 0
    if (retval.size() > 0) {
	for (int i = 0; i < retval.size(); i++) {
	    if (!(retval.c_str()[i] >= '0' && retval.c_str()[i] <= '9')) {
		INVALID_NODE_ERROR("port", retval.c_str());
		retval = "";
		break;
	    }
	}
    }
#endif

    return retval;
}

string
IMSvrXMLConf::parse_file_node(xmlNodePtr &node)
{
    string retval;

    if (node->name == NULL || xmlStrlen(node->name) == 0) {
	EMPTY_NODE_ERROR("file");
	return retval;
    } else {
	retval = parse_text_node(node);
    }

    return retval;
}

void
IMSvrXMLConf::parse_listen_tcp_node(xmlNodePtr &node, bool ssl)
{
    string hostname;
    string port;

#if defined(HAVE_UNIX_SOCKET)
    if (string(get_strval(IMSvrCfg::UDSFILE)).size() != 0) return;
#endif /* HAVE_UNIX_SOCKET */

    while (node != NULL) {
	if (xmlStrcmp(node->name, (xmlChar *)"hostname") == 0) {
	    if (hostname.size() > 0) {
		DUPLICATED_NODE_ERROR("hostname", "listen");
	    } else {
		hostname = parse_hostname_node(node->xmlChildrenNode);
	    }
	    node = node->next;
	} else if (xmlStrcmp(node->name, (xmlChar *)"port") == 0) {
	    if (port.size() > 0) {
		DUPLICATED_NODE_ERROR("port", "listen");
	    } else {
		port = parse_port_node(node->xmlChildrenNode);
	    }
	    node = node->next;
	} else if (xmlStrcmp(node->name, (xmlChar *)"text") == 0
		   || xmlStrcmp(node->name, (xmlChar *)"comment") == 0) {
	    /* ignore text and comment node */
	    node = node->next;
	} else {
	    /* ignore the unknown nodes */
	    UNKNOWN_NODE_ERROR(node->name, "listen");
	    node = node->next;
	}
    }

    /* validate the given directives */
    if (hostname.size() == 0)
	MISSING_NODE_ERROR("hostname", "listen");
    if (port.size() == 0)
	MISSING_NODE_ERROR("port", "listen");
    if (hostname.size() > 0 && port.size() > 0) {
	if (!get_boolval(USER)) {
	    if (ssl) {
#ifdef HAVE_TLS
		listenaddrvec.push_back(IMSocketAddress(IMSocketAddress::INET, IMSocketAddress::TLS,
							hostname, port));
		LOG_DEBUG("<ssl><listen type=\"tcp\"><hostname>%s</hostname><port>%s</port></listen></ssl>",
			  hostname.c_str(), port.c_str());
#else
		LOG_WARNING("it was compiled without SSL support.");
#endif /* HAVE_TLS */
	    } else {
		listenaddrvec.push_back(IMSocketAddress(IMSocketAddress::INET, hostname, port));
		LOG_DEBUG("<listen type=\"tcp\"><hostname>%s</hostname><port>%s</port></listen>",
			  hostname.c_str(), port.c_str());
	    }
	}
    }
}

void
IMSvrXMLConf::parse_listen_unix_node(xmlNodePtr &node, bool ssl)
{
#ifdef HAVE_UNIX_SOCKET
    string file;
    string prefix = SOCKETDIR "/.iiimp-unix";
    char *p;

    if (string(get_strval(IMSvrCfg::UDSFILE)).size() != 0) return;

    while (node != NULL) {
	if (xmlStrcmp(node->name, (xmlChar *)"file") == 0) {
	    if (file.size() > 0) {
		DUPLICATED_NODE_ERROR("file", "listen");
	    } else {
		file = parse_file_node(node->xmlChildrenNode);
	    }
	    node = node->next;
	} else if (xmlStrcmp(node->name, (xmlChar *)"text") == 0
		   || xmlStrcmp(node->name, (xmlChar *)"comment") == 0) {
	    /* ignore text and comment node */
	    node = node->next;
	} else {
	    /* ignore the unknown nodes */
	    UNKNOWN_NODE_ERROR(node->name, "listen");
	    node = node->next;
	}
    }

    /* drop path in order to not allow ../path/to say */
    p = new char[file.size() + 1];
    strncpy(p, file.c_str(), file.size());
    p[file.size()] = 0;
    file = basename(p);
    delete[] p;

    /* validate the given directives */
    if (file.size() == 0) {
	MISSING_NODE_ERROR("file", "listen");
    } else {
	if (get_boolval(IMSvrCfg::USER)) {
	    string user_name;

	    if (get_process_user(user_name)) {
		if (ssl) {
#ifdef HAVE_TLS
		    listenaddrvec.push_back(IMSocketAddress(IMSocketAddress::UNIX_DOMAIN_PER_USER,
							    IMSocketAddress::TLS,
							    prefix + string("-") + user_name,
							    file));
#else
		    LOG_WARNING("it was compiled without SSL support.");
#endif /* HAVE_TLS */
		} else {
		    listenaddrvec.push_back(IMSocketAddress(IMSocketAddress::UNIX_DOMAIN_PER_USER,
							    prefix + string("-") + user_name,
							    file));
		}
	    }
	} else {
	    if (ssl) {
#ifdef HAVE_TLS
		listenaddrvec.push_back(IMSocketAddress(IMSocketAddress::UNIX_DOMAIN,
							IMSocketAddress::TLS,
							prefix, file));
#else
		LOG_WARNING("it was compiled without SSL support.");
#endif /* HAVE_TLS */
	    } else {
		listenaddrvec.push_back(IMSocketAddress(IMSocketAddress::UNIX_DOMAIN,
							prefix, file));
	    }
	}
	if (ssl) {
#ifdef HAVE_TLS
	    LOG_DEBUG("<ssl><listen type=\"unix\"><file>%s</file></listen></ssl>", file.c_str());
#else
	    /* not supported */
#endif /* HAVE_TLS */
	} else {
	    LOG_DEBUG("<listen type=\"unix\"><file>%s</file></listen>", file.c_str());
	}
    }
#else
    LOG_WARNING("it was compiled without Unix domain socket support.")
#endif /* HAVE_UNIX_SOCKET */
}

void
IMSvrXMLConf::parse_acl_node(xmlNodePtr &node)
{
    xmlChar *type = NULL;
    IMAuth::access_type at;
    string hostname;

    type = xmlGetProp(node, (xmlChar *)"type");
    at = get_access_type(type);
    node = node->xmlChildrenNode;

    while (node != NULL) {
	if (xmlStrcmp(node->name, (xmlChar *)"hostname") == 0) {
	    if (hostname.size() > 0) {
		DUPLICATED_NODE_ERROR("hostname", "acl");
	    } else {
		hostname = parse_hostname_node(node->xmlChildrenNode);
	    }
	    node = node->next;
	} else if (xmlStrcmp(node->name, (xmlChar *)"text") == 0
		   || xmlStrcmp(node->name, (xmlChar *)"comment") == 0) {
	    /* ignore text and comment node */
	    node = node->next;
	} else {
	    /* ignore the unknown nodes */
	    UNKNOWN_NODE_ERROR(node->name, "acl");
	    node = node->next;
	}
    }

    /* validate the given directives */
    if (hostname.size() == 0) {
	MISSING_NODE_ERROR("hostname", "acl");
    } else if (at != IMAuth::UNKNOWN) {
	get_usermgr(ptarget)->set_entry(hostname.c_str(), at);
	LOG_DEBUG("<acl type=\"%s\"><hostname>%s</hostname></acl>", type, hostname.c_str());
    } else {
	LOG_ERROR("<acl> needs `type' attribute.");
    }

    if (type != NULL)
	xmlFree(type);
}

void
IMSvrXMLConf::parse_acls_node(xmlNodePtr &node)
{
    xmlNodePtr topnode = NULL;
    xmlChar *type = NULL;
    IMAuth::access_type at;

    type = xmlGetProp(node, (xmlChar *)"default");
    at = get_access_type(type);
    if (at != IMAuth::UNKNOWN) {
	get_usermgr(ptarget)->set_default_entry(at);
	LOG_DEBUG("<acls default=\"%s\">", type);
    } else {
	LOG_WARNING("no default permission.");
    }
    if (type != NULL)
	xmlFree(type);

    node = node->xmlChildrenNode;

    while (node != NULL) {
	if (xmlStrcmp(node->name, (xmlChar *)"acl") == 0) {
	    topnode = node;
	    parse_acl_node(node);
	    node = topnode->next;
	    topnode = NULL;
	} else if (xmlStrcmp(node->name, (xmlChar *)"text") == 0
		   || xmlStrcmp(node->name, (xmlChar *)"comment") == 0) {
	    /* ignore text and comment node */
	    node = node->next;
	} else {
	    /* ignore the unknown nodes */
	    UNKNOWN_NODE_ERROR(node->name, "acls");
	    node = node->next;
	}
    }
}

void
IMSvrXMLConf::parse_system_node(xmlNodePtr &node)
{
    xmlChar *type = NULL;
    IMAuth::access_type at;

    type = xmlGetProp(node, (xmlChar *)"type");
    at = get_access_type(type);
    if (at != IMAuth::UNKNOWN) {
	get_usermgr(ptarget)->set_systemuser_permission(at);
	LOG_DEBUG("<system type=\"%s\"/>", type);
    } else {
	LOG_ERROR("no permission or unknown permission `%s' on <system>.", type);
    }
    if (type != NULL)
	xmlFree(type);
}

void
IMSvrXMLConf::parse_user_node(xmlNodePtr &node)
{
    xmlChar *type = NULL, *name = NULL;
    IMAuth::access_type at;
    string password;

    type = xmlGetProp(node, (xmlChar *)"type");
    name = xmlGetProp(node, (xmlChar *)"name");

    at = get_access_type(type);
    if (at != IMAuth::UNKNOWN) {
	if (at == IMAuth::PASSWORD
	    && node->xmlChildrenNode != NULL
	    && node->xmlChildrenNode->name != NULL) {
	    password += parse_text_node(node->xmlChildrenNode);
	    get_usermgr(ptarget)->add_user((const char *)name, password.c_str(), at);
	} else {
	    get_usermgr(ptarget)->add_user((const char *)name, NULL, at);
	}
	LOG_DEBUG("<user type=\"%s\" name=\"%s\">%s</user>", type, name, password.c_str());
    } else {
	LOG_ERROR("no permission or unknown permission `%s' on <user>.", type);
    }

    if (type != NULL)
	xmlFree(type);
    if (name != NULL)
	xmlFree(name);
}

void
IMSvrXMLConf::parse_auth_node(xmlNodePtr &node)
{
    xmlNodePtr topnode = NULL;

    while (node != NULL) {
	if (xmlStrcmp(node->name, (xmlChar *)"system") == 0) {
	    topnode = node;
	    parse_system_node(node);
	    node = topnode->next;
	    topnode = NULL;
	} else if (xmlStrcmp(node->name, (xmlChar *)"user") == 0) {
	    topnode = node;
	    parse_user_node(node);
	    node = topnode->next;
	    topnode = NULL;
	} else if (xmlStrcmp(node->name, (xmlChar *)"text") == 0
		   || xmlStrcmp(node->name, (xmlChar *)"comment") == 0) {
	    /* ignore text and comment node */
	    node = node->next;
	} else {
	    /* ignore the unknown nodes */
	    UNKNOWN_NODE_ERROR(node->name, "auth");
	    node = node->next;
	}
    }
}

void
IMSvrXMLConf::parse_ssl_node(xmlNodePtr &node)
{
#ifdef HAVE_TLS
    xmlNodePtr topnode = NULL;
    xmlChar *verify, *depth;
    string v;

    verify = xmlGetProp(node, (xmlChar *)"verify");
    depth = xmlGetProp(node, (xmlChar *)"depth");
    if (verify != NULL) {
	v = (const char *)verify;
	IMTLS::get_instance()->set_verify_client(v);
    }
    if (depth != NULL) {
	v = (const char *)depth;
	IMTLS::get_instance()->set_verify_depth(v);
    }
    LOG_DEBUG("<ssl verify=\"%s\" depth=\"%s\">", verify, depth);

    node = node->xmlChildrenNode;

    while (node != NULL) {
	if (xmlStrcmp(node->name, (xmlChar *)"listen") == 0) {
	    xmlChar *type = NULL;

	    type = xmlGetProp(node, (xmlChar *)"type");
	    if (xmlStrcmp(type, (xmlChar *)"tcp") == 0) {
		topnode = node;
		node = node->xmlChildrenNode;
		parse_listen_tcp_node(node, true);
		node = topnode->next;
		topnode = NULL;
	    } else if (xmlStrcmp(type, (xmlChar *)"unix") == 0) {
		topnode = node;
		node = node->xmlChildrenNode;
		parse_listen_unix_node(node, true);
		node = topnode->next;
		topnode = NULL;
	    } else {
		/* ignore the unknown nodes */
		LOG_WARNING("unknown listen type `%s'. ignoring...", type);
		node = node->next;
	    }
	    if (type != NULL)
		xmlFree(type);
	} else if (xmlStrcmp(node->name, (xmlChar *)"CACertificate") == 0) {
	    xmlChar *path, *file;
	    string v;

	    path = xmlGetProp(node, (xmlChar *)"path");
	    file = xmlGetProp(node, (xmlChar *)"file");

	    if (path != NULL) {
		v = (const char *)path;
		IMTLS::get_instance()->set_cacertificate_path(v);
	    }
	    if (file != NULL) {
		v = (const char *)file;
		IMTLS::get_instance()->set_cacertificate_file(v);
	    }
	    LOG_DEBUG("<CACertificate path=\"%s\" file=\"%s\"/>", path, file);
	    if (path != NULL)
		xmlFree(path);
	    if (file != NULL)
		xmlFree(file);
	    node = node->next;
	} else if (xmlStrcmp(node->name, (xmlChar *)"Certificate") == 0) {
	    xmlChar *file, *key;
	    string v;

	    file = xmlGetProp(node, (xmlChar *)"file");
	    key = xmlGetProp(node, (xmlChar *)"key");

	    if (file != NULL) {
		v = (const char *)file;
		IMTLS::get_instance()->set_certificate_file(v);
	    }
	    if (key != NULL) {
		v = (const char *)key;
		IMTLS::get_instance()->set_certificate_key_file(v);
	    }
	    LOG_DEBUG("<Certificate key=\"%s\" file=\"%s\"/>", key, file);
	    if (file != NULL)
		xmlFree(file);
	    if (key != NULL)
		xmlFree(key);
	    node = node->next;
	} else if (xmlStrcmp(node->name, (xmlChar *)"text") == 0
		   || xmlStrcmp(node->name, (xmlChar *)"comment") == 0) {
	    /* ignore text and comment node */
	    node = node->next;
	} else {
	    /* ignore the unknown nodes */
	    UNKNOWN_NODE_ERROR(node->name, "ssl");
	    node = node->next;
	}
    }
    LOG_DEBUG("</ssl>");

    if (verify != NULL)
	xmlFree(verify);
    if (depth != NULL)
	xmlFree(depth);
#else
    LOG_WARNING("it was compiled without SSL support.");
#endif
}

void
IMSvrXMLConf::parse_server_node(xmlNodePtr &node)
{
    xmlNodePtr topnode = NULL;

    while (node != NULL) {
	if (xmlStrcmp(node->name, (xmlChar *)"listen") == 0) {
	    xmlChar *type = NULL;

	    type = xmlGetProp(node, (xmlChar *)"type");
	    if (xmlStrcmp(type, (xmlChar *)"tcp") == 0) {
		topnode = node;
		node = node->xmlChildrenNode;
		parse_listen_tcp_node(node, false);
		node = topnode->next;
		topnode = NULL;
	    } else if (xmlStrcmp(type, (xmlChar *)"unix") == 0) {
		topnode = node;
		node = node->xmlChildrenNode;
		parse_listen_unix_node(node, false);
		node = topnode->next;
		topnode = NULL;
	    } else {
		/* ignore the unknown nodes */
		LOG_WARNING("unknown listen type `%s'. ignoring...", type);
		node = node->next;
	    }
	    if (type != NULL)
		xmlFree(type);
	} else if (xmlStrcmp(node->name, (xmlChar *)"acls") == 0) {
	    topnode = node;
	    parse_acls_node(node);
	    node = topnode->next;
	    topnode = NULL;
	} else if (xmlStrcmp(node->name, (xmlChar *)"auth") == 0) {
	    topnode = node;
	    node = node->xmlChildrenNode;
	    parse_auth_node(node);
	    node = topnode->next;
	    topnode = NULL;
	} else if (xmlStrcmp(node->name, (xmlChar *)"ssl") == 0) {
	    topnode = node;
	    parse_ssl_node(node);
	    node = topnode->next;
	    topnode = NULL;
	} else if (xmlStrcmp(node->name, (xmlChar *)"text") == 0
		   || xmlStrcmp(node->name, (xmlChar *)"comment") == 0) {
	    /* ignore text and comment node */
	    node = node->next;
	} else {
	    /* ignore the unknown nodes */
	    UNKNOWN_NODE_ERROR(node->name, "server");
	    node = node->next;
	}
    }
}

bool
IMSvrXMLConf::load(void)
{
#ifdef HAVE_XMLCTXTREAD
    xmlParserCtxtPtr parser = NULL;
#endif
    xmlDocPtr doc = NULL;
    xmlNodePtr root = NULL, node, topnode = NULL;
    bool retval = false;

    if (filename.size() == 0)
	return false;

#ifdef HAVE_XMLCTXTREAD
    parser = xmlNewParserCtxt();
    if ((doc = xmlCtxtReadFile(parser, filename.c_str(), "UTF-8", 0)) == NULL) {
#else
    if ((doc = xmlParseFile(filename.c_str())) == NULL) {
#endif
	goto ensure;
#ifdef HAVE_XMLCTXTREAD
    }
#else
    }
#endif
    if ((root = xmlDocGetRootElement(doc)) == NULL)
	goto ensure;
    if (xmlStrcmp(root->name, (xmlChar *)"iiimf") != 0) {
	LOG_ERROR("invalid configuration file `%s'", filename.c_str());
	goto ensure;
    }

    node = root->xmlChildrenNode;
    while (node != NULL) {
	if (xmlStrcmp(node->name, (xmlChar *)"server") == 0) {
	    topnode = node;
	    node = node->xmlChildrenNode;
	    parse_server_node(node);
	    node = topnode->next;
	    topnode = NULL;
	} else if (xmlStrcmp(node->name, (xmlChar *)"text") == 0
		   || xmlStrcmp(node->name, (xmlChar *)"comment") == 0) {
	    /* ignore text and comment node */
	    node = node->next;
	} else {
	    /* ignore the unknown nodes */
	    UNKNOWN_NODE_ERROR(node->name, "iiimf");
	    node = node->next;
	}
    }
    retval = true;

  ensure:
    if (doc != NULL)
	xmlFreeDoc(doc);
#ifdef HAVE_XMLCTXTREAD
    if (parser != NULL)
	xmlFreeParserCtxt(parser);
#endif

    return retval;
}

bool
IMSvrXMLConf::configure(IMSvr *pimsvr)
{
    string prefix = SOCKETDIR "/.iiimp-unix";

    ptarget = pimsvr;
    if (!load())
	return false;

#ifdef HAVE_TLS
    IMTLS::get_instance()->setup();
#endif
    /* config listen address if it has not been configured yet. */
    if (listenaddrvec.empty()) {
        if (get_boolval(USER)) {
            // when iiimd run as a per-user daemon.
            // listens to /tmp/.iiimp-unix-${USER}/${PORT}
            string user_name;
            if (get_process_user(user_name)) {
		string sf;
		IM_unix_domain_socket_file_dir(user_name, sf);
		if (0 < sf.size()) {
		    listenaddrvec.push_back(IMSocketAddress(IMSocketAddress::UNIX_DOMAIN_PER_USER,
							    sf,
							    get_strval(IMSvrCfg::PORT)));
		}
#if 0
                listenaddrvec.push_back(IMSocketAddress(IMSocketAddress::UNIX_DOMAIN_PER_USER,
							string("/tmp/.iiimp-unix-") + user_name,
							get_strval(IMSvrCfg::PORT)));
#endif
	    }
	} else if (string(get_strval(IMSvrCfg::UDSFILE)).size() != 0) {
	    listenaddrvec.push_back(IMSocketAddress(IMSocketAddress::UNIX_DOMAIN,
						    get_strval(IMSvrCfg::UDSFILE), ""));
        } else {
            // by default, iiimd listens to
            // localhost:9010, /var/run/iiim/.iiimp-unix/${PORT}
	    /* disable tcp listening by default */
#if 0
	    listenaddrvec.push_back(IMSocketAddress(IMSocketAddress::INET,
                                              get_strval(IMSvrCfg::HOSTNAME),
					      get_strval(IMSvrCfg::PORT)));
#endif
            listenaddrvec.push_back(IMSocketAddress(IMSocketAddress::UNIX_DOMAIN,
					      prefix,
                                              get_strval(IMSvrCfg::PORT)));
        }
    }
    config_listenaddress(pimsvr, listenaddrvec);

    return true;
}

IMSvrXMLConf::IMSvrXMLConf(IMSvrCfg *pbase,
			   const char *conffile) : IMSvrCfg(*pbase), filename(conffile)
{
}

IMSvrXMLConf::~IMSvrXMLConf()
{
}
