/* netxml-ups.c	Driver routines for network XML UPS units

   Copyright (C)
	2008-2009	Arjen de Korte <adkorte-guest@alioth.debian.org>
	2013		Vaclav Krpec <VaclavKrpec@Eaton.com>

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/

#include "main.h"
#include "netxml-ups.h"
#include "mge-xml.h"
#include "dstate.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <ne_request.h>
#include <ne_basic.h>
#include <ne_props.h>
#include <ne_uri.h>
#include <ne_xml.h>
#include <ne_xmlreq.h>
#include <ne_ssl.h>
#include <ne_auth.h>
#include <ne_socket.h>

#include "nut_stdint.h"

#define DRIVER_NAME	"network XML UPS"
#define DRIVER_VERSION	"0.49"

/** *_OBJECT query multi-part body boundary */
#define FORM_POST_BOUNDARY "NUT-NETXML-UPS-OBJECTS"

#ifdef WIN32 /* FIXME ?? skip alarm handling */
#define HAVE_NE_SET_CONNECT_TIMEOUT  1
#define HAVE_NE_SOCK_CONNECT_TIMEOUT 1
#endif	/* WIN32 */

/* driver description structure */
upsdrv_info_t	upsdrv_info = {
	DRIVER_NAME,
	DRIVER_VERSION,
	"Arjen de Korte <adkorte-guest@alioth.debian.org>" \
	"Vaclav Krpec <VaclavKrpec@Eaton.com>",
	DRV_EXPERIMENTAL,
	{ NULL }
};


/** *_OBJECT query status */
typedef enum {
	OBJECT_OK = 0,       /**< OK            */
	OBJECT_PARSE_ERROR,  /**< Parse error   */
	OBJECT_ERROR,        /**< Generic error */
} object_query_status_t;  /* end of typedef enum */


/** *_OBJECT entry type */
typedef enum {
	SET_OBJECT_REQUEST,   /**< SET_OBJECT request  */
	SET_OBJECT_RESPONSE,  /**< SET_OBJECT response */
} object_query_type_t;  /* end of typedef enum */


/** *_OBJECT POST request mode */
typedef enum {
	RAW_POST,   /**< RAW POST mode  */
	FORM_POST,  /**< FORM POST mode */
} object_post_mode_t;  /* end of typedef enum */


typedef struct set_object_req   set_object_req_t;    /**< SET_OBJECT request  carrier */
typedef struct set_object_resp  set_object_resp_t;   /**< SET_OBJECT response carrier */
typedef struct object_entry     object_entry_t;      /**< *_OBJECT entry      carrier */
typedef struct object_query     object_query_t;      /**< *_OBJECT query      handle  */


/** SET_OBJECT request carrier */
struct set_object_req {
	char *name;   /**< OBJECT name  */
	char *value;  /**< OBJECT value */
};  /* end of struct set_object_req */


/** SET_OBJECT response carrier */
struct set_object_resp {
	char *name;    /**< OBJECT name   */
	char *unit;    /**< OBJECT unit   */
	char *access;  /**< OBJECT access */
	char *value;   /**< OBJECT value  */
};  /* end of struct set_object_resp */


/** *_OBJECT query entry */
struct object_entry {
	/** Payload */
	union {
		set_object_req_t  req;   /**< Request  entry */
		set_object_resp_t resp;  /**< Response entry */
	} payld;

	/* Metadata */
	object_entry_t *next;  /**< Next     entry */
	object_entry_t *prev;  /**< Previous entry */
};  /* end of struct object_entry */


/** *_OBJECT query handle */
struct object_query {
	object_query_status_t  status;  /**< Query status      */
	object_query_type_t    type;    /**< List entries type */
	object_post_mode_t     mode;    /**< POST request mode */
	size_t                 cnt;     /**< Count of entries  */
	object_entry_t        *head;    /**< List head         */
	object_entry_t        *tail;    /**< List tail         */
};  /* end of struct object_query */


/**
 *  \brief  *_OBJECT query constructor
 *
 *  \param  type  Query type
 *  \param  mode  Query mode
 *
 *  \return *_OBJECT query handle or \c NULL in case of memory error
 */
static object_query_t *object_query_create(
	object_query_type_t type,
	object_post_mode_t  mode);


/**
 *  \brief  Number of *_OBJECT query entries
 *
 *  \param  handle  Query handle
 *
 *  \return NUmber of entries
 */
static size_t object_query_size(object_query_t *handle);


/**
 *  \brief  *_OBJECT query destructor
 *
 *  \param  handle  Query handle
 */
static void object_query_destroy(object_query_t *handle);


/**
 *  \brief  SET_OBJECT: add request query entry
 *
 *  \param  handle  Request query handle
 *  \param  name    OBJECT name
 *  \param  value   OBJECT value
 *
 *  \return Query entry or \c NULL in case of memory error
 */
static object_entry_t *set_object_add(
	object_query_t *handle,
	const char     *name,
	const char     *value);


/**
 *  \brief  SET_OBJECT: RAW POST mode implementation
 *
 *  \param  req  SET_OBJECT request
 *
 *  \return Response to the request
 */
static object_query_t *set_object_raw(object_query_t *req);


/**
 *  \brief  SET_OBJECT: FORM POST mode implementation
 *
 *  \param  req  SET_OBJECT request
 *
 *  \return \c NULL (FORM POST mode resp. is ignored by specification)
 */
static object_query_t *set_object_form(object_query_t *req);


/**
 *  \brief  SET_OBJECT: implementation
 *
 *  \param  req  SET_OBJECT request
 *
 *  \return Response to the request
 */
static object_query_t *set_object(object_query_t *req);


/**
 *  \brief  SET_OBJECT: RAW POST mode request serialisation
 *
 *  \param  handle  Request query handle
 *
 *  \return POST request body
 */
static ne_buffer *set_object_serialise_raw(object_query_t *handle);


/**
 *  \brief  SET_OBJECT: FORM POST mode request serialisation
 *
 *  \param  handle  Request query handle
 *
 *  \return POST request body
 */
static ne_buffer *set_object_serialise_form(object_query_t *handle);


/* FIXME:
 * "built with neon library %s" LIBNEON_VERSION
 * subdrivers (limited to MGE only ATM) */

/* Global vars */
uint32_t		ups_status = 0;
static int		timeout = 5;
int		shutdown_duration = 120;
static int		shutdown_timer = 0;
static time_t		lastheard = 0;
static subdriver_t	*subdriver = &mge_xml_subdriver;
static ne_session	*session = NULL;
static ne_socket	*sock = NULL;
static ne_uri		uri;
static char	*product_page = NULL;

/* Support functions */
static void netxml_alarm_set(void);
static void netxml_status_set(void);
static int netxml_authenticate(void *userdata, const char *realm, int try_num, char *username, char *password);
static int netxml_dispatch_request(ne_request *request, ne_xml_parser *parser);
static int netxml_get_page(const char *page);

static int instcmd(const char *cmdname, const char *extra);
static int setvar(const char *varname, const char *val);

static int netxml_alarm_subscribe(const char *page);

#if HAVE_NE_SET_CONNECT_TIMEOUT && HAVE_NE_SOCK_CONNECT_TIMEOUT
	/* we don't need to use alarm() */
#else
static void netxml_alarm_handler(int sig)
{
	/* don't do anything here, just return */
}
#endif

void upsdrv_initinfo(void)
{
	char	*page, *last = NULL;
	char	buf[SMALLBUF];

	snprintf(buf, sizeof(buf), "%s", subdriver->initinfo);

	for (page = strtok_r(buf, " ", &last); page != NULL; page = strtok_r(NULL, " ", &last)) {

		if (netxml_get_page(page) != NE_OK) {
			continue;
		}
		/* store product page, for later use */
		product_page = xstrdup(page);

		dstate_setinfo("driver.version.data", "%s", subdriver->version);

		if (testvar("subscribe") && (netxml_alarm_subscribe(subdriver->subscribe) == NE_OK)) {
#ifndef WIN32
			extrafd = ne_sock_fd(sock);
#else	/* WIN32 */
			NUT_WIN32_INCOMPLETE_DETAILED("TODO: port extrafd to Windows");
#endif	/* WIN32 */
			time(&lastheard);
		}

		/* Register r/w variables */
		vname_register_rw();

		/* Set UPS driver handler callbacks */
		upsh.setvar  = &setvar;
		upsh.instcmd = &instcmd;

		return;
	}

	fatalx(EXIT_FAILURE, "%s: communication failure [%s]", __func__, ne_get_error(session));
}

void upsdrv_updateinfo(void)
{
	ssize_t	ret;
	int	errors = 0;

	/* We really should be dealing with alarms through a separate callback, so that we can keep the
	 * processing of alarms and polling for data separated. Currently, this isn't supported by the
	 * driver main body, so we'll have to revert to polling each time we're called, unless the
	 * socket indicates we're no longer connected.
	 */
	if (testvar("subscribe")) {
		char	buf[LARGEBUF];

		ret = ne_sock_read(sock, buf, sizeof(buf));

		if (ret > 0) {
			/* alarm message received */

			ne_xml_parser	*parser = ne_xml_create();
			upsdebugx(2, "%s: ne_sock_read(%" PRIiSIZE " bytes) => %s", __func__, ret, buf);
			ne_xml_push_handler(parser, subdriver->startelm_cb, subdriver->cdata_cb, subdriver->endelm_cb, NULL);
			ne_xml_parse(parser, buf, strlen(buf));
			ne_xml_destroy(parser);
			time(&lastheard);

		} else if ((ret == NE_SOCK_TIMEOUT) && (difftime(time(NULL), lastheard) < 180)) {
			/* timed out */

			upsdebugx(2, "%s: ne_sock_read(timeout)", __func__);

		} else {
			/* connection closed or unknown error */

			upslogx(LOG_ERR, "NSM connection with '%s' lost", uri.host);

			upsdebugx(2, "%s: ne_sock_read(%" PRIiSIZE ") => %s", __func__, ret, ne_sock_error(sock));
			ne_sock_close(sock);

			if (netxml_alarm_subscribe(subdriver->subscribe) == NE_OK) {
#ifndef WIN32
				extrafd = ne_sock_fd(sock);
#else	/* WIN32 */
				NUT_WIN32_INCOMPLETE_DETAILED("TODO: port extrafd to Windows");
#endif	/* WIN32 */
				time(&lastheard);
				return;
			}

			dstate_datastale();

#ifndef WIN32
			extrafd = ERROR_FD;
#else	/* WIN32 */
			NUT_WIN32_INCOMPLETE_DETAILED("TODO: port extrafd to Windows");
#endif	/* WIN32 */

			return;
		}
	}

	/* get additional data */
	ret = netxml_get_page(subdriver->getobject);
	if (ret != NE_OK) {
		errors++;
	}

	ret = netxml_get_page(subdriver->summary);
	if (ret != NE_OK) {
		errors++;
	}

	/* also refresh the product information, at least for firmware information */
	ret = netxml_get_page(product_page);
	if (ret != NE_OK) {
		errors++;
	}

	if (errors > 1) {
		dstate_datastale();
		return;
	}

	status_init();

	alarm_init();
	netxml_alarm_set();
	alarm_commit();

	netxml_status_set();
	status_commit();

	dstate_dataok();
}

void upsdrv_shutdown(void) {
	/* Only implement "shutdown.default"; do not invoke
	 * general handling of other `sdcommands` here */

	/*
	 * WARNING:
	 * This driver will probably never support this properly:
	 * In order to be of any use, the driver should be called
	 * near the end of the system halt script (or a service
	 * management framework's equivalent, if any). By that
	 * time we, in all likelyhood, won't have basic network
	 * capabilities anymore, so we could never send this
	 * command to the UPS. This is not an error, but rather
	 * a limitation (on some platforms) of the interface/media
	 * used for these devices.
	 */

	/* FIXME: Make a name for default original shutdown
	 * in particular to make it one of the options and
	 * call protocol cleanup below, if needed.
	 */

	/* tell the UPS to shut down, then return - DO NOT SLEEP HERE */

	/* maybe try to detect the UPS here, but try a shutdown even if
	 * it doesn't respond at first if possible */

	/* replace with a proper shutdown function */
	/* fatalx(EXIT_FAILURE, "shutdown not supported"); */

	/* you may have to check the line status since the commands
	 * for toggling power are frequently different for OL vs. OB */

	/* OL: this must power cycle the load if possible */

	/* OB: the load must remain off until the power returns */

	int status = STAT_SET_FAILED;  /* pessimistic assumption */

	object_query_t *resp = NULL;
	object_query_t *req  = NULL;

	/* Pragmatic do { ... } while (0) loop allowing break to cleanup */
	do {
		/* Create SET_OBJECT request */
		req = object_query_create(SET_OBJECT_REQUEST, FORM_POST);

		if (NULL == req)
			break;

		if (NULL == set_object_add(req, "battery.runtime.low", "999999999"))
			break;

		/* Send SET_OBJECT request */
		resp = set_object(req);

#if (0)  /* FORM_POST method response is ignored, we can only hope it worked... */
		if (NULL == resp)
			break;

		/* Check if setting was done */
		if (1 > object_query_size(resp)) {
			status = STAT_SET_UNKNOWN;

			break;
		}
#endif  /* end of code removal */

		status = STAT_SET_HANDLED;  /* success */

	} while (0);  /* end of pragmatic loop, break target */

	/* Cleanup */
	if (NULL != req)
		object_query_destroy(req);

	if (NULL != resp)
		object_query_destroy(resp);

	if (STAT_SET_HANDLED != status) {
		upslogx(LOG_ERR, "Shutdown failed: %d", status);
		if (handling_upsdrv_shutdown > 0)
			set_exit_flag(EF_EXIT_FAILURE);
	}
}

static int instcmd(const char *cmdname, const char *extra)
{
	/* May be used in logging below, but not as a command argument */
	NUT_UNUSED_VARIABLE(extra);
	upsdebug_INSTCMD_STARTING(cmdname, extra);

	/* FIXME: shutdown per above? */

/*
	if (!strcasecmp(cmdname, "test.battery.stop")) {
		upslog_INSTCMD_POWERSTATE_MAYBE(cmdname, extra);
		ser_send_buf(upsfd, ...);
		return STAT_INSTCMD_HANDLED;
	}
*/

	upslog_INSTCMD_UNKNOWN(cmdname, extra);
	return STAT_INSTCMD_UNKNOWN;
}

static int setvar(const char *varname, const char *val) {
	int status = STAT_SET_FAILED;  /* pessimistic assumption */

	object_query_t *resp = NULL;
	object_query_t *req  = NULL;

	upsdebug_SET_STARTING(varname, val);

	/* Pragmatic do { ... } while (0) loop allowing break to cleanup */
	do {
		/* Create SET_OBJECT request */
		req = object_query_create(SET_OBJECT_REQUEST, FORM_POST);

		if (NULL == req)
			break;

		if (NULL == set_object_add(req, varname, val))
			break;

		/* Send SET_OBJECT request */
		resp = set_object(req);

		if (NULL == resp)
			break;

		/* Check if setting was done */
		if (1 > object_query_size(resp)) {
			upslog_SET_UNKNOWN(varname, val);
			status = STAT_SET_UNKNOWN;

			break;
		}

		status = STAT_SET_HANDLED;  /* success */

	} while (0);  /* end of pragmatic loop, break target */

	/* Cleanup */
	if (NULL != req)
		object_query_destroy(req);

	if (NULL != resp)
		object_query_destroy(resp);

	if (status == STAT_SET_FAILED)
		upslog_SET_FAILED(varname, val);
	return status;
}

void upsdrv_help(void)
{
}

/* optionally tweak prognames[] entries */
void upsdrv_tweak_prognames(void)
{
}

/* list flags and values that you want to receive via -x */
void upsdrv_makevartable(void)
{
	char	buf[SMALLBUF];

	snprintf(buf, sizeof(buf), "network timeout (default: %d seconds)", timeout);
	addvar(VAR_VALUE, "timeout", buf);

	addvar(VAR_FLAG, "subscribe", "authenticated subscription on NMC");

	addvar(VAR_VALUE | VAR_SENSITIVE, "login", "login value for authenticated mode");
	addvar(VAR_VALUE | VAR_SENSITIVE, "password", "password value for authenticated mode");

	snprintf(buf, sizeof(buf), "shutdown duration in second (default: %d seconds)", shutdown_duration);
	addvar(VAR_VALUE, "shutdown_duration", buf);

	if( shutdown_timer > 0 ) {
		snprintf(buf, sizeof(buf), "shutdown timer in second (default: %d seconds)", shutdown_timer);
	}
	else {
		snprintf(buf, sizeof(buf), "shutdown timer in second (default: none)");
	}
	addvar(VAR_VALUE, "shutdown_timer", buf);

	/* Legacy MGE-XML conversion from 2000's, not needed in modern firmwares */
	addvar(VAR_FLAG, "do_convert_deci", "enable legacy convert_deci() for certain measurements 10x too large");
}

void upsdrv_initups(void)
{
	int	ret;
	char	*val;
	FILE	*fp;

#if HAVE_NE_SET_CONNECT_TIMEOUT && HAVE_NE_SOCK_CONNECT_TIMEOUT
	/* we don't need to use alarm() */
#else
	struct sigaction	sa;

	sigemptyset(&sa.sa_mask);
	sa.sa_flags = 0;

	sa.sa_handler = netxml_alarm_handler;
	sigaction(SIGALRM, &sa, NULL);
#endif
	/* allow override of default network timeout value */
	val = getval("timeout");
	if (val) {
		timeout = atoi(val);

		if (timeout < 1) {
			fatalx(EXIT_FAILURE, "timeout must be greater than 0");
		}
	}

	val = getval("shutdown_duration");
	if (val) {
		shutdown_duration = atoi(val);

		if (shutdown_duration < 0) {
			fatalx(EXIT_FAILURE, "shutdown duration must be greater than or equal to 0");
		}
	}

	val = getval("shutdown_timer");
	if (val) {
		shutdown_timer = atoi(val);

		if (shutdown_timer < 0) {
			fatalx(EXIT_FAILURE, "shutdown timer must be greater than or equal to 0");
		}
	}

	if (nut_debug_level > 5) {
		ne_debug_init(stderr, NE_DBG_HTTP | NE_DBG_HTTPBODY);
	}

	if (ne_sock_init()) {
		fatalx(EXIT_FAILURE, "%s: failed to initialize socket libraries", progname);
	}

	if (ne_uri_parse(device_path, &uri) || uri.host == NULL) {
		fatalx(EXIT_FAILURE, "%s: invalid hostname '%s'", progname, device_path);
	}
/*
	if (uri.scheme == NULL) {
		uri.scheme = strdup("http");
	}

	if (uri.host == NULL) {
		uri.host = strdup(device_path);
	}
 */
	if (uri.port == 0) {
		uri.port = ne_uri_defaultport(uri.scheme);
	}

	upsdebugx(1, "using %s://%s port %u", uri.scheme, uri.host, uri.port);

	session = ne_session_create(uri.scheme, uri.host, uri.port);

	/* timeout if we can't (re)connect to the UPS */
#ifdef HAVE_NE_SET_CONNECT_TIMEOUT
	ne_set_connect_timeout(session, timeout);
#endif

	/* just wait for a couple of seconds */
	ne_set_read_timeout(session, timeout);

	ne_set_useragent(session, subdriver->version);

	if (strcasecmp(uri.scheme, "https") == 0) {
		ne_ssl_trust_default_ca(session);
	}

	ne_set_server_auth(session, netxml_authenticate, NULL);

	/* if debug level is set, direct output to stderr */
	if (!nut_debug_level) {
#ifndef WIN32
		fp = fopen("/dev/null", "w");
#else	/* WIN32 */
		fp = fopen("nul", "w");
#endif	/* WIN32 */
	} else {
		fp = stderr;
	}

	if (!fp) {
		fatal_with_errno(EXIT_FAILURE, "Connectivity test failed");
	}

	/* see if we have a connection */
	ret = ne_get(session, subdriver->initups, fileno(fp));

	if (!nut_debug_level) {
		fclose(fp);
	} else {
		fprintf(fp, "\n");
	}

	if (ret != NE_OK) {
		fatalx(EXIT_FAILURE, "Connectivity test: %s", ne_get_error(session));
	}

	upslogx(LOG_INFO, "Connectivity test: %s", ne_get_error(session));
}

void upsdrv_cleanup(void)
{
	free(subdriver->configure);
	free(subdriver->subscribe);
	free(subdriver->summary);
	free(subdriver->getobject);
	free(subdriver->setobject);
	free(product_page);

	if (sock) {
		ne_sock_close(sock);
	}

	if (session) {
		ne_session_destroy(session);
	}

	ne_uri_free(&uri);
}

/**********************************************************************
 * Support functions
 *********************************************************************/

static int netxml_get_page(const char *page)
{
	int		ret = NE_ERROR;
	ne_request	*request;
	ne_xml_parser	*parser;

	upsdebugx(2, "%s: %s", __func__, (page != NULL)?page:"(null)");

	if (page != NULL) {
		request = ne_request_create(session, "GET", page);

		parser = ne_xml_create();

		ne_xml_push_handler(parser, subdriver->startelm_cb, subdriver->cdata_cb, subdriver->endelm_cb, NULL);

		ret = netxml_dispatch_request(request, parser);

		if (ret) {
			upsdebugx(2, "%s: %s", __func__, ne_get_error(session));
		}

		ne_xml_destroy(parser);
		ne_request_destroy(request);
	}
	return ret;
}

static int netxml_alarm_subscribe(const char *page)
{
	ssize_t	ret;
	int	secret = -1;
	unsigned int	port = 0;
	char	buf[LARGEBUF], *s;
	ne_request	*request;
	ne_sock_addr	*addr;
	const ne_inet_addr	*ai;
	char	resp_buf[LARGEBUF];

	/* Clear response buffer */
	memset(resp_buf, 0, sizeof(resp_buf));

	upsdebugx(2, "%s: %s", __func__, page);

	sock = ne_sock_create();

	if (gethostname(buf, sizeof(buf)) == 0) {
		dstate_setinfo("driver.hostname", "%s", buf);
	} else {
		dstate_setinfo("driver.hostname", "<unknown>");
	}

#ifdef HAVE_NE_SOCK_CONNECT_TIMEOUT
	ne_sock_connect_timeout(sock, timeout);
#endif
	ne_sock_read_timeout(sock, 1);

	netxml_get_page(subdriver->configure);

	snprintf(buf, sizeof(buf),	"<?xml version=\"1.0\"?>\n");
	snprintfcat(buf, sizeof(buf),	"<Subscribe>\n");
	snprintfcat(buf, sizeof(buf),		"<Class>%s v%s</Class>\n", progname, DRIVER_VERSION);
	snprintfcat(buf, sizeof(buf),		"<Type>connected socket</Type>\n");
	snprintfcat(buf, sizeof(buf),		"<HostName>%s</HostName>\n", dstate_getinfo("driver.hostname"));
	snprintfcat(buf, sizeof(buf),		"<XMLClientParameters>\n");
	snprintfcat(buf, sizeof(buf),		"<ShutdownDuration>%d</ShutdownDuration>\n", shutdown_duration);
	if( shutdown_timer > 0 ) {
		snprintfcat(buf, sizeof(buf),	"<ShutdownTimer>%d</ShutdownTimer>\r\n", shutdown_timer);
	}
	else {
		snprintfcat(buf, sizeof(buf),	"<ShutdownTimer>NONE</ShutdownTimer>\n");
	}
	snprintfcat(buf, sizeof(buf),			"<AutoConfig>LOCAL</AutoConfig>\n");
	snprintfcat(buf, sizeof(buf),			"<OutletGroup>1</OutletGroup>\n");
	snprintfcat(buf, sizeof(buf),		"</XMLClientParameters>\n");
	snprintfcat(buf, sizeof(buf),		"<Warning></Warning>\n");
	snprintfcat(buf, sizeof(buf),	"</Subscribe>\n");

	/* now send subscription message setting all the proper flags */
	request = ne_request_create(session, "POST", page);
	ne_set_request_body_buffer(request, buf, strlen(buf));

	/* as the NMC reply is not xml standard compliant let's parse it this way */
	do {
#ifndef HAVE_NE_SOCK_CONNECT_TIMEOUT
		alarm(timeout+1);
#endif
		ret = ne_begin_request(request);

#ifndef HAVE_NE_SOCK_CONNECT_TIMEOUT
		alarm(0);
#endif
		if (ret != NE_OK) {
			break;
		}

		ret = ne_read_response_block(request, resp_buf, sizeof(resp_buf));

		if (ret == NE_OK) {
			ret = ne_end_request(request);
		}

	} while (ret == NE_RETRY);

	ne_request_destroy(request);

	/* due to different formats used by the various NMCs, we need to\
	 * break up the reply in lines and parse each one separately */
	for (s = strtok(resp_buf, "\r\n"); s != NULL; s = strtok(NULL, "\r\n")) {
		long long int	tmp_port = -1, tmp_secret = -1;
		upsdebugx(2, "%s: parsing %s", __func__, s);

		if (!strncasecmp(s, "<Port>", 6) && (sscanf(s+6, "%lli", &tmp_port) != 1)) {
			return NE_RETRY;
		}

		/* FIXME? Does a port==0 make sense here? Or should the test below be for port<1?
		 * Legacy code until a fix here used sscanf() above to get a '%u' value...
		 */
		if (tmp_port < 0 || tmp_port > UINT_MAX) {
			upsdebugx(2, "%s: parsing initial subcription failed, bad port value", __func__);
			return NE_RETRY;
		}

		if (!strncasecmp(s, "<Secret>", 8) && (sscanf(s+8, "%lli", &tmp_secret) != 1)) {
			return NE_RETRY;
		}

		if (tmp_secret < 0 || tmp_secret > UINT_MAX) {
			upsdebugx(2, "%s: parsing initial subcription failed, bad secret value", __func__);
			return NE_RETRY;
		}

		/* Range of valid values constrained above */
		port = (unsigned int)tmp_port;
		secret = (int)tmp_secret;

	}

	if ((port < 1) || (secret == -1)) {
		upsdebugx(2, "%s: parsing initial subcription failed", __func__);
		return NE_RETRY;
	}

	/* Resolve the given hostname.  'flags' must be zero.  Hex
	* string IPv6 addresses (e.g. `::1') may be enclosed in brackets
	* (e.g. `[::1]'). */
	addr = ne_addr_resolve(uri.host, 0);

	/* Returns zero if name resolution was successful, non-zero on
	* error. */
	if (ne_addr_result(addr) != 0) {
		upsdebugx(2, "%s: name resolution failure on %s: %s", __func__, uri.host, ne_addr_error(addr, buf, sizeof(buf)));
		ne_addr_destroy(addr);
		return NE_RETRY;
	}

	for (ai = ne_addr_first(addr); ai != NULL; ai = ne_addr_next(addr)) {

		upsdebugx(2, "%s: connecting to host %s port %u", __func__, ne_iaddr_print(ai, buf, sizeof(buf)), port);

#ifndef HAVE_NE_SOCK_CONNECT_TIMEOUT
		alarm(timeout+1);
#endif
		ret = ne_sock_connect(sock, ai, port);

#ifndef HAVE_NE_SOCK_CONNECT_TIMEOUT
		alarm(0);
#endif
		if (ret == NE_OK) {
			upsdebugx(2, "%s: connection to %s open on fd %d", __func__, uri.host, ne_sock_fd(sock));
			break;
		}
	}

	ne_addr_destroy(addr);

	if (ai == NULL) {
		upsdebugx(2, "%s: failed to create listening socket", __func__);
		return NE_RETRY;
	}

	snprintf(buf, sizeof(buf), "<Subscription Identification=\"%u\"></Subscription>", (unsigned int)secret);
	ret = ne_sock_fullwrite(sock, buf, strlen(buf) + 1);

	if (ret != NE_OK) {
		upsdebugx(2, "%s: send failed: %s", __func__, ne_sock_error(sock));
		return NE_RETRY;
	}

	ret = ne_sock_read(sock, buf, sizeof(buf));

	if (ret < 1) {
		upsdebugx(2, "%s: read failed: %s", __func__, ne_sock_error(sock));
		return NE_RETRY;
	}

	if (strcasecmp(buf, "<Subscription Answer=\"ok\"></Subscription>")) {
		upsdebugx(2, "%s: subscription rejected", __func__);
		return NE_RETRY;
	}

	upslogx(LOG_INFO, "NSM connection to '%s' established", uri.host);
	return NE_OK;
}

static int netxml_dispatch_request(ne_request *request, ne_xml_parser *parser)
{
	int ret;

	/*
	 * Starting with neon-0.27.0 the ne_xml_dispatch_request() function will check
	 * for a valid XML content-type (following RFC 3023 rules) in the header.
	 * Unfortunately, (at least) the Transverse NMC doesn't follow this RFC, so
	 * we can't use this anymore and we'll have to roll our own here.
	 */
	do {
#ifndef HAVE_NE_SET_CONNECT_TIMEOUT
		alarm(timeout+1);
#endif
		ret = ne_begin_request(request);

#ifndef HAVE_NE_SET_CONNECT_TIMEOUT
		alarm(0);
#endif
		if (ret != NE_OK) {
			break;
		}

		ret = ne_xml_parse_response(request, parser);

		if (ret == NE_OK) {
			ret = ne_end_request(request);
		}

	} while (ret == NE_RETRY);

	return ret;
}

/* Supply the 'login' and 'password' when authentication is required */
static int netxml_authenticate(void *userdata, const char *realm, int try_num, char *username, char *password)
{
	char	*val;
	NUT_UNUSED_VARIABLE(userdata);

	upsdebugx(2, "%s: realm = [%s], attempt = %d", __func__, realm, try_num);

	val = getval("login");
	snprintf(username, NE_ABUFSIZ, "%s", val ? val : "");

	val = getval("password");
	snprintf(password, NE_ABUFSIZ, "%s", val ? val : "");

	return try_num;
}

/* Convert the local status information to NUT format and set NUT
   alarms. */
static void netxml_alarm_set(void)
{
	if (STATUS_BIT(REPLACEBATT)) {
		alarm_set("Replace battery!");
	}
	if (STATUS_BIT(SHUTDOWNIMM)) {
		alarm_set("Shutdown imminent!");
	}
	if (STATUS_BIT(FANFAIL)) {
		alarm_set("Fan failure!");
	}
	if (STATUS_BIT(NOBATTERY)) {
		alarm_set("No battery installed!");
	}
	if (STATUS_BIT(BATTVOLTLO)) {
		alarm_set("Battery voltage too low!");
	}
	if (STATUS_BIT(BATTVOLTHI)) {
		alarm_set("Battery voltage too high!");
	}
	if (STATUS_BIT(CHARGERFAIL)) {
		alarm_set("Battery charger fail!");
	}
	if (STATUS_BIT(OVERHEAT)) {
		alarm_set("Temperature too high!");
	}
	if (STATUS_BIT(COMMFAULT)) {
		alarm_set("Communication fault!");
	}
	if (STATUS_BIT(INTERNALFAULT)) {
		alarm_set("Internal UPS fault!");
	}
	if (STATUS_BIT(FUSEFAULT)) {
		alarm_set("Fuse fault!");
	}
	if (STATUS_BIT(BYPASSAUTO)) {
		alarm_set("Automatic bypass mode!");
	}
	if (STATUS_BIT(BYPASSMAN)) {
		alarm_set("Manual bypass mode!");
	}
}

/* Convert the local status information to NUT format and set NUT
   status. */
static void netxml_status_set(void)
{
	if (STATUS_BIT(VRANGE)) {
		dstate_setinfo("input.transfer.reason", "input voltage out of range");
	} else if (STATUS_BIT(FRANGE)) {
		dstate_setinfo("input.transfer.reason", "input frequency out of range");
	} else {
		dstate_delinfo("input.transfer.reason");
	}

	if (STATUS_BIT(ONLINE)) {
		status_set("OL");		/* on line */
	} else {
		status_set("OB");		/* on battery */
	}
	if (STATUS_BIT(DISCHRG)) {
		status_set("DISCHRG");		/* discharging */
	}
	if (STATUS_BIT(CHRG)) {
		status_set("CHRG");		/* charging */
	}
	if (STATUS_BIT(LOWBATT)) {
		status_set("LB");		/* low battery */
	}
	if (STATUS_BIT(OVERLOAD)) {
		status_set("OVER");		/* overload */
	}
	if (STATUS_BIT(REPLACEBATT) || STATUS_BIT(NOBATTERY)) {
		status_set("RB");		/* replace batt */
	}
	if (STATUS_BIT(TRIM)) {
		status_set("TRIM");		/* SmartTrim */
	}
	if (STATUS_BIT(BOOST)) {
		status_set("BOOST");		/* SmartBoost */
	}
	if (STATUS_BIT(BYPASSAUTO) || STATUS_BIT(BYPASSMAN)) {
		status_set("BYPASS");		/* on bypass */
	}
	if (STATUS_BIT(OFF)) {
		status_set("OFF");		/* ups is off */
	}

	if (STATUS_BIT(SHUTDOWNIMM)) {
		status_set("FSD");		/* shutdown imminent */
	}
	if (STATUS_BIT(CALIB)) {
		status_set("CAL");		/* calibrating */
	}
}


/*
 * *_OBJECT interface implementation
 */

static object_query_t *object_query_create(
	object_query_type_t type,
	object_post_mode_t  mode)
{
	object_query_t *handle = (object_query_t *)calloc(1,
		sizeof(object_query_t));

	if (NULL == handle)
		return NULL;

	handle->type = type;
	handle->mode = mode;

	return handle;
}


static size_t object_query_size(object_query_t *handle) {
	assert(NULL != handle);

	return handle->cnt;
}


/**
 *  \brief  SET_OBJECT request list entry destructor
 *
 *  \param  req  SET_OBJECT request list entry
 */
static void set_object_req_destroy(set_object_req_t *req) {
	assert(NULL != req);

	if (NULL != req->name)
		free(req->name);

	if (NULL != req->value)
		free(req->value);
}


/**
 *  \brief  SET_OBJECT response list entry destructor
 *
 *  \param  resp  SET_OBJECT response list entry
 */
static void set_object_resp_destroy(set_object_resp_t *resp) {
	assert(NULL != resp);

	if (NULL != resp->name)
		free(resp->name);

	if (NULL != resp->unit)
		free(resp->unit);

	if (NULL != resp->access)
		free(resp->access);

	if (NULL != resp->value)
		free(resp->value);
}


/**
 *  \brief  *_OBJECT query entry destructor
 *
 *  \param  handle  SET_OBJECT query handle
 *  \param  entry   SET_OBJECT query entry
 */
static void object_entry_destroy(object_query_t *handle, object_entry_t *entry) {
	assert(NULL != handle);
	assert(NULL != entry);

	/* Sanity checks */
	assert(0 < handle->cnt);

	/* Relink list */
	if (entry == handle->head) {
		handle->head = entry->next;
	}
	else {
		assert(NULL != entry->prev);

		entry->prev->next = entry->next;
	}

	if (entry == handle->tail) {
		handle->tail = entry->prev;
	}
	else {
		assert(NULL != entry->next);

		entry->next->prev = entry->prev;
	}

	--handle->cnt;

	/* Destroy payload */
	switch (handle->type) {
		case SET_OBJECT_REQUEST:
			set_object_req_destroy(&entry->payld.req);
			break;

		case SET_OBJECT_RESPONSE:
			set_object_resp_destroy(&entry->payld.resp);
			break;

#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE) )
# pragma GCC diagnostic push
#endif
#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT
# pragma GCC diagnostic ignored "-Wcovered-switch-default"
#endif
#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE
# pragma GCC diagnostic ignored "-Wunreachable-code"
#endif
/* Older CLANG (e.g. clang-3.4) seems to not support the GCC pragmas above */
#ifdef __clang__
# pragma clang diagnostic push
# pragma clang diagnostic ignored "-Wunreachable-code"
# pragma clang diagnostic ignored "-Wcovered-switch-default"
#endif
		default:
			/* Must not occur. */
			break;
#ifdef __clang__
# pragma clang diagnostic pop
#endif
#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE) )
# pragma GCC diagnostic pop
#endif
	}

	/* Destroy entry */
	free(entry);
}


static void object_query_destroy(object_query_t *handle) {
	assert(NULL != handle);

	/* Destroy entries */
	while (handle->cnt)
		object_entry_destroy(handle, handle->head);

	/* Destroy handle */
	free(handle);
}


/**
 *  \brief  Add *_OBJECT list entry (at list end)
 *
 *  \param  handle  Entry list handle
 *  \param  entry   Entry
 */
static void object_add_entry(object_query_t *handle, object_entry_t *entry) {
	assert(NULL != handle);
	assert(NULL != entry);

	/* Sanity checks */
	assert(SET_OBJECT_REQUEST == handle->type);

	/* Add entry at end of bi-directional list */
	if (handle->cnt) {
		assert(NULL != handle->tail);
		assert(NULL == handle->tail->next);

		handle->tail->next = entry;
		entry->prev = handle->tail;
	}

	/* Add the very first entry */
	else {
		handle->head = entry;
		entry->prev  = NULL;
	}

	handle->tail = entry;
	entry->next  = NULL;

	++handle->cnt;
}


static object_entry_t *set_object_add(
	object_query_t *handle,
	const char     *name,
	const char     *value)
{
	char	*name_cpy;
	char	*value_cpy;
	object_entry_t	*entry;

	assert(NULL != name);
	assert(NULL != value);

	entry = (object_entry_t *)calloc(1,
		sizeof(object_entry_t));

	if (NULL == entry)
		return NULL;

	/* Copy payload data */
	name_cpy  = strdup(name);
	value_cpy = strdup(value);

	/* Cleanup in case of memory error */
	if (NULL == name_cpy || NULL == value_cpy) {
		if (NULL != name_cpy)
			free(name_cpy);

		if (NULL != value_cpy)
			free(value_cpy);

		free(entry);

		return NULL;
	}

	/* Set payload */
	entry->payld.req.name  = name_cpy;
	entry->payld.req.value = value_cpy;

	/* Enlist */
	object_add_entry(handle, entry);

	return entry;
}


/**
 *  \brief  Common SET_OBJECT entries serialiser
 *
 *  \param  buff   Buffer
 *  \param  entry  SET_OBJECT request entry
 *
 *  \return OBJECT_OK    on success
 *  \return OBJECT_ERROR otherwise
 */
static object_query_status_t set_object_serialise_entries(ne_buffer *buff, object_entry_t *entry) {
	object_query_status_t status = OBJECT_OK;

	assert(NULL != buff);

	ne_buffer_zappend(buff, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n");
	ne_buffer_zappend(buff, "<SET_OBJECT>\r\n");

	for (; NULL != entry; entry = entry->next) {
		const char *vname = vname_nut2mge_xml(entry->payld.req.name);

		/* Serialise one object */
		if (NULL != vname) {
			ne_buffer_zappend(buff, "  <OBJECT name=\"");
			ne_buffer_zappend(buff, vname);
			ne_buffer_zappend(buff, "\">");
			ne_buffer_zappend(buff, entry->payld.req.value);
			ne_buffer_zappend(buff, "</OBJECT>\r\n");
		}

		/* Var. name resolution error */
		else
			status = OBJECT_ERROR;
	}

	ne_buffer_zappend(buff, "</SET_OBJECT>\r\n");

	return status;
}


static ne_buffer *set_object_serialise_raw(object_query_t *handle) {
	ne_buffer	*buff;

	assert(NULL != handle);

	/* Sanity checks */
	assert(SET_OBJECT_REQUEST == handle->type);

	/* Create buffer */
	buff = ne_buffer_create();

	/* neon API ref. states that the function always succeeds */
	assert(NULL != buff);

	/* Serialise all entries */
	set_object_serialise_entries(buff, handle->head);

	return buff;
}


static ne_buffer *set_object_serialise_form(object_query_t *handle) {
	const char	*vname = NULL;
	ne_buffer	*buff;

	assert(NULL != handle);

	/* Sanity checks */
	assert(SET_OBJECT_REQUEST == handle->type);

	/* Create buffer */
	buff = ne_buffer_create();

	/* neon API ref. states that the function always succeeds */
	assert(NULL != buff);

	/* Simple request */
	if (1 == object_query_size(handle)) {
		assert(NULL != handle->head);

		/* TODO: Simple req. doesn't seem to work
		vname = vname_nut2mge_xml(handle->head->payld.req.name);
		*/
	}
	if (NULL != vname) {
		assert(NULL != handle->head);

		ne_buffer_zappend(buff, "objectName=");
		ne_buffer_zappend(buff, vname);
		ne_buffer_zappend(buff, "&objectValue=");
		ne_buffer_zappend(buff, handle->head->payld.req.value);
	}

	/* Multi set request (or empty request) */
	else {
		/* Add request prologue */
		ne_buffer_zappend(buff, "--" FORM_POST_BOUNDARY "\r\n");
		ne_buffer_zappend(buff, "Content-Disposition: form-data; name=\"file\"; "
					"filename=\"Configuration.xml\"\r\n");
		ne_buffer_zappend(buff, "Content-Type: application/octet-stream\r\n");
		ne_buffer_zappend(buff, "\r\n");

		/* Serialise all entries */
		set_object_serialise_entries(buff, handle->head);

		/* Add request epilogue */
		ne_buffer_zappend(buff, "--" FORM_POST_BOUNDARY "--\r\n");
	}

	return buff;
}


/**
 *  \brief  neon callback for SET_OBJECT RAW POST mode response element start
 *
 *  \param  userdata  Obfuscated SET_OBJECT_RESPONSE query handle
 *  \param  parent    Element parent
 *  \param  nspace    Element namespace (empty)
 *  \param  name      Element name
 *  \param  attrs     Element attributes
 *
 *  \return \c NE_XML_STATEROOT + distance of the element from root
 */
static int set_object_raw_resp_start_element(
	void        *userdata,
	int          parent,
	const char  *nspace,
	const char  *name,
	const char **attrs)
{
	object_query_t *handle = (object_query_t *)userdata;

	assert(NULL != handle);

	/* Sanity checks */
	assert(SET_OBJECT_RESPONSE == handle->type);

	/* Check that namespace is empty */
	if (NULL != nspace && '\0' != *nspace) {
		handle->status = OBJECT_PARSE_ERROR;

		return NE_XML_STATEROOT;
	}

	/* OBJECT (as a SET_OBJECT child) */
	if (NE_XML_STATEROOT + 1 == parent && 0 == strcasecmp(name, "OBJECT")) {
		size_t i;

		object_entry_t *entry = (object_entry_t *)calloc(1,
			sizeof(object_entry_t));

		/* Memory error */
		if (NULL == entry) {
			handle->status = OBJECT_ERROR;

			return NE_XML_STATEROOT;
		}

		/* Set attributes */
		for (i = 0; NULL != attrs[i] && NULL != attrs[i + 1]; i += 2) {
			char       **attr = NULL;
			const char  *aval = NULL;

			/* Skip unset attribute name and/or value (useless) */
			if (NULL == attrs[i] || NULL == attrs[i + 1])
				continue;

			/* Obviously, the following holds, now */
			assert(NULL != attrs[i]);
			assert(NULL != attrs[i + 1]);

			/* name */
			if (0 == strcasecmp(attrs[i], "name")) {
				attr = &entry->payld.resp.name;
				aval = vname_mge_xml2nut(attrs[i + 1]);
			}

			/* unit */
			else if (0 == strcasecmp(attrs[i], "unit")) {
				attr = &entry->payld.resp.unit;
				aval = attrs[i + 1];
			}

			/* access */
			else if (0 == strcasecmp(attrs[i], "access")) {
				attr = &entry->payld.resp.access;
				aval = attrs[i + 1];
			}

			/* Set known attribute */
			if (NULL != attr) {
				/* Copy value */
				if (NULL != aval) {
					*attr = strdup(aval);

					if (NULL == *attr)
						handle->status = OBJECT_ERROR;
				}

				/* Value resolution error */
				else
					handle->status = OBJECT_ERROR;
			}
		}

		object_add_entry(handle, entry);

		return NE_XML_STATEROOT + 2;  /* signal to cdata callback */
	}

	/* SET_OBJECT (as the root child) */
	if (NE_XML_STATEROOT == parent && 0 == strcasecmp(name, "SET_OBJECT"))
		return NE_XML_STATEROOT + 1;

	/* Unknown element (as a SET_OBJECT child) */
	if (NE_XML_STATEROOT + 1 == parent)
		return NE_XML_STATEROOT + 1;

	/* Ignore any other root children */
	return NE_XML_STATEROOT;
}


/**
 *  \brief  neon callback for SET_OBJECT RAW POST mode response data start
 *
 *  The callback is used to set OBJECT element value.
 *  This is done for state \c NE_XML_STATEROOT + 2
 *  (see \ref set_object_raw_resp_start_element).
 *
 *  \param  userdata  Obfuscated SET_OBJECT_RESPONSE query handle
 *  \param  state     Element distance from root
 *  \param  cdata     Character data
 *  \param  len       Character data length
 *
 *  \return state
 */
static int set_object_raw_resp_cdata(
	void       *userdata,
	int         state,
	const char *cdata,
	size_t      len)
{
	object_query_t *handle = (object_query_t *)userdata;

	assert(NULL != handle);

	/* Sanity checks */
	assert(SET_OBJECT_RESPONSE == handle->type);

	/* Ignore any element except OBJECT */
	if (NE_XML_STATEROOT + 2 != state)
		return state;

	if (OBJECT_OK == handle->status) {
		char *value;

		/* Set last object value */
		assert(NULL != handle->tail);
		assert(NULL != handle->tail->payld.resp.name);

		value = vvalue_mge_xml2nut(handle->tail->payld.resp.name, cdata, len);

		handle->tail->payld.resp.value = value;

		if (NULL == handle->tail->payld.resp.value)
			handle->status = OBJECT_ERROR;
	}

	return state;
}


/**
 *  \brief  neon callback for SET_OBJECT RAW POST mode response element start
 *
 *  \param  userdata  Obfuscated SET_OBJECT_RESPONSE query handle
 *  \param  state     Element distance from root
 *  \param  nspace    Element namespace (empty)
 *  \param  name      Element name
 *
 *  \return \c NE_XML_STATEROOT + distance of the element from root
 */
static int set_object_raw_resp_end_element(
	void       *userdata,
	int         state,
	const char *nspace,
	const char *name)
{
	NUT_UNUSED_VARIABLE(userdata);
	NUT_UNUSED_VARIABLE(nspace);

	/* OBJECT (as a SET_OBJECT child) */
	if (NE_XML_STATEROOT + 2 == state) {
		assert(0 == strcasecmp(name, "OBJECT"));

		return NE_XML_STATEROOT + 1;
	}

	/*
	 * Otherwise, state is either NE_XML_STATEROOT or NE_XML_STATEROOT + 1
	 * In any case, we return NE_XML_STATEROOT
	 */
	return NE_XML_STATEROOT;
}


static object_query_t *set_object_deserialise_raw(ne_buffer *buff) {
	int	ne_status;
	object_query_t	*handle;
	ne_xml_parser	*parser;

	assert(NULL != buff);

	/* Create SET_OBJECT query response */
	handle = object_query_create(SET_OBJECT_RESPONSE, RAW_POST);

	if (NULL == handle)
		return NULL;

	/* Create XML parser */
	parser = ne_xml_create();

	/* neon API ref. states that the function always succeeds */
	assert(NULL != parser);

	/* Set element & data handlers */
	ne_xml_push_handler(
		parser,
		set_object_raw_resp_start_element,
		set_object_raw_resp_cdata,
		set_object_raw_resp_end_element,
		handle);

	/* Parse the response */
	ne_status = ne_xml_parse(parser, buff->data, buff->used);

	if (NE_OK != ne_status)
		handle->status = OBJECT_PARSE_ERROR;

	/* Destroy parser */
	ne_xml_destroy(parser);

	return handle;
}


/**
 *  \brief  Send HTTP request over a session
 *
 *  The function creates HTTP request, sends it and reads-out the response.
 *
 *  \param[in]   argsession HTTP session
 *  \param[in]   method     Request method
 *  \param[in]   arguri     Request URI
 *  \param[in]   ct         Request content type (optional, \c NULL accepted)
 *  \param[in]   req_body   Request body (optional, \c NULL is accepted)
 *  \param[out]  resp_body  Response body (optional, \c NULL is accepted)
 *
 *  \return HTTP status code if response was sent, 0 on send error
 */
static int send_http_request(
	ne_session *argsession,
	const char *method,
	const char *arguri,
	const char *ct,
	ne_buffer  *req_body,
	ne_buffer  *resp_body)
{
	int resp_code = 0;

	ne_request *req = NULL;

	/* Create request */
	req = ne_request_create(argsession, method, arguri);

	/* Neon claims that request creation is always successful */
	assert(NULL != req);

	do {  /* Pragmatic do ... while (0) loop allowing breaks on error */
		const ne_status	*req_st;
		int	status;

		/* Set Content-Type */
		if (NULL != ct)
			ne_add_request_header(req, "Content-Type", ct);

		/* Set request body */
		if (NULL != req_body)
			/* BEWARE: The terminating '\0' byte is "used", too */
			ne_set_request_body_buffer(req,
				req_body->data, req_body->used - 1);

		/* Send request */
		status = ne_begin_request(req);

		if (NE_OK != status) {
			break;
		}

		/* Read response */
		assert(NE_OK == status);

		for (;;) {
			char buff[512];

			ssize_t read;

			read = ne_read_response_block(req, buff, sizeof(buff));

			/* Read failure */
			if (0 > read) {
				status = NE_ERROR;

				break;
			}

			if (0 == read)
				break;

			if (NULL != resp_body)
				ne_buffer_append(resp_body, buff, (size_t)read);
		}

		if (NE_OK != status) {
			break;
		}

		/* Request served */
		ne_end_request(req);

		/* Get response code */
		req_st = ne_get_status(req);

		assert(NULL != req_st);

		resp_code = req_st->code;

	} while (0);  /* end of do ... while (0) pragmatic loop */

	if (NULL != req)
		ne_request_destroy(req);

	return resp_code;
}


static object_query_t *set_object_raw(object_query_t *req) {
	int             resp_code;
	object_query_t *resp      = NULL;
	ne_buffer      *req_body  = NULL;
	ne_buffer      *resp_body = NULL;

	assert(NULL != req);

	/* Sanity check */
	assert(SET_OBJECT_REQUEST == req->type);
	assert(RAW_POST           == req->mode);

	/* Serialise request POST data */
	req_body = set_object_serialise_raw(req);

	/* Send request */
	resp_body = ne_buffer_create();

	assert(NULL != resp_body);

	resp_code = send_http_request(session,
		"POST", "/set_obj.htm", NULL, req_body, resp_body);

	/*
	 * Repeat in case of 401 - Unauthorised
	 *
	 * Note that this is a WA of NMC sending Connection: close
	 * header in the 401 response, in which case neon closes
	 * connection (quite rightfully).
	 */
	if (401 == resp_code)
		resp_code = send_http_request(session,
			"POST", "/set_obj.htm", NULL, req_body, resp_body);

	/* Deserialise response */
	if (200 == resp_code)
		resp = set_object_deserialise_raw(resp_body);

	/* Cleanup */
	if (NULL != req_body)
		ne_buffer_destroy(req_body);

	if (NULL != resp_body)
		ne_buffer_destroy(resp_body);

	return resp;
}


static object_query_t *set_object_form(object_query_t *req) {
	int       resp_code;
	ne_buffer *req_body = NULL;

	const char *ct = "multipart/form-data; boundary=" FORM_POST_BOUNDARY;

	/* TODO: Single request doesn't seem to work
	if (1 == object_query_size(req))
		ct = "application/x-form-urlencoded";
	*/

	assert(NULL != req);

	/* Sanity check */
	assert(SET_OBJECT_REQUEST == req->type);
	assert(FORM_POST          == req->mode);

	/* Serialise request POST data */
	req_body = set_object_serialise_form(req);

	/* Send request (response is ignored by the proto. spec v3) */
	resp_code = send_http_request(session,
		"POST", "/Forms/set_obj_2", ct, req_body, NULL);

	/*
	 * Repeat in case of 401 - Unauthorised
	 *
	 * Note that this is a WA of NMC sending Connection: close
	 * header in the 401 response, in which case neon closes
	 * connection (quite rightfully).
	 */
	if (401 == resp_code) {
		resp_code = send_http_request(session,
			"POST", "/Forms/set_obj_2", ct, req_body, NULL);
	}

	/* Cleanup */
	if (NULL != req_body)
		ne_buffer_destroy(req_body);

	return NULL;
}


static object_query_t *set_object(object_query_t *req) {
	object_query_t *resp = NULL;

	assert(NULL != req);

	/* Sanity checks */
	assert(SET_OBJECT_REQUEST == req->type);

	/* Select implementation by POST request mode */
	switch (req->mode) {
		case RAW_POST:
			resp = set_object_raw(req);
			break;

		case FORM_POST:
			resp = set_object_form(req);
			break;

#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE) )
# pragma GCC diagnostic push
#endif
#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT
# pragma GCC diagnostic ignored "-Wcovered-switch-default"
#endif
#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE
# pragma GCC diagnostic ignored "-Wunreachable-code"
#endif
/* Older CLANG (e.g. clang-3.4) seems to not support the GCC pragmas above */
#ifdef __clang__
# pragma clang diagnostic push
# pragma clang diagnostic ignored "-Wunreachable-code"
# pragma clang diagnostic ignored "-Wcovered-switch-default"
#endif
		default:
			/* Must not occur. */
			break;
#ifdef __clang__
# pragma clang diagnostic pop
#endif
#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE) )
# pragma GCC diagnostic pop
#endif
	}

	return resp;
}
