/* upsclient - network communications functions for UPS clients

   Copyright (C)
	2002	Russell Kroll <rkroll@exploits.org>
	2008	Arjen de Korte <adkorte-guest@alioth.debian.org>

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

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

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

#include "config.h"	/* safe because it doesn't contain prototypes */

#include <errno.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>

#include "upsclient.h"
#include "timehead.h"

#define UPSCLIENT_MAGIC 0x19980308

#define SMALLBUF	512

#ifdef SHUT_RDWR
#define shutdown_how SHUT_RDWR
#else
#define shutdown_how 2
#endif

struct {
	int	flags;
	const	char	*str;
} upscli_errlist[] =
{
	{ 0, "Unknown error"			},	/*  0: UPSCLI_ERR_UNKNOWN */
	{ 0, "Variable not supported by UPS"	},	/*  1: UPSCLI_ERR_VARNOTSUPP */
	{ 0, "No such host"			},	/*  2: UPSCLI_ERR_NOSUCHHOST */
	{ 0, "Invalid response from server"	},	/*  3: UPSCLI_ERR_INVRESP */
	{ 0, "Unknown UPS"			},	/*  4: UPSCLI_ERR_UNKNOWNUPS */
	{ 0, "Invalid list type"		},	/*  5: UPSCLI_ERR_INVLISTTYPE */
	{ 0, "Access denied"			},	/*  6: UPSCLI_ERR_ACCESSDENIED */
	{ 0, "Password required"		},	/*  7: UPSCLI_ERR_PWDREQUIRED */
	{ 0, "Password incorrect"		},	/*  8: UPSCLI_ERR_PWDINCORRECT */
	{ 0, "Missing argument"			},	/*  9: UPSCLI_ERR_MISSINGARG */
	{ 0, "Data stale"			},	/* 10: UPSCLI_ERR_DATASTALE */
	{ 0, "Variable unknown"			},	/* 11: UPSCLI_ERR_VARUNKNOWN */
	{ 0, "Already logged in"		},	/* 12: UPSCLI_ERR_LOGINTWICE */
	{ 0, "Already set password"		},	/* 13: UPSCLI_ERR_PWDSETTWICE */
	{ 0, "Unknown variable type"		},	/* 14: UPSCLI_ERR_UNKNOWNTYPE */
	{ 0, "Unknown variable"			},	/* 15: UPSCLI_ERR_UNKNOWNVAR */
	{ 0, "Read-only variable"		},	/* 16: UPSCLI_ERR_VARREADONLY */
	{ 0, "New value is too long"		},	/* 17: UPSCLI_ERR_TOOLONG */
	{ 0, "Invalid value for variable"	},	/* 18: UPSCLI_ERR_INVALIDVALUE */
	{ 0, "Set command failed"		},	/* 19: UPSCLI_ERR_SETFAILED */
	{ 0, "Unknown instant command"		},	/* 20: UPSCLI_ERR_UNKINSTCMD */
	{ 0, "Instant command failed"		},	/* 21: UPSCLI_ERR_CMDFAILED */
	{ 0, "Instant command not supported"	},	/* 22: UPSCLI_ERR_CMDNOTSUPP */
	{ 0, "Invalid username"			},	/* 23: UPSCLI_ERR_INVUSERNAME */
	{ 0, "Already set username"		},	/* 24: UPSCLI_ERR_USERSETTWICE */
	{ 0, "Unknown command"			},	/* 25: UPSCLI_ERR_UNKCOMMAND */
	{ 0, "Invalid argument"			},	/* 26: UPSCLI_ERR_INVALIDARG */
	{ 1, "Send failure: %s"			},	/* 27: UPSCLI_ERR_SENDFAILURE */
	{ 1, "Receive failure: %s"		},	/* 28: UPSCLI_ERR_RECVFAILURE */
	{ 1, "socket failure: %s"		},	/* 29: UPSCLI_ERR_SOCKFAILURE */
	{ 1, "bind failure: %s"			},	/* 30: UPSCLI_ERR_BINDFAILURE */
	{ 1, "Connection failure: %s"		},	/* 31: UPSCLI_ERR_CONNFAILURE */
	{ 1, "Write error: %s"			},	/* 32: UPSCLI_ERR_WRITE */
	{ 1, "Read error: %s"			},	/* 33: UPSCLI_ERR_READ */
	{ 0, "Invalid password"			},	/* 34: UPSCLI_ERR_INVPASSWORD */
	{ 0, "Username required"		},	/* 35: UPSCLI_ERR_USERREQUIRED */
	{ 0, "SSL is not available",		},	/* 36: UPSCLI_ERR_SSLFAIL */
	{ 2, "SSL error: %s",			},	/* 37: UPSCLI_ERR_SSLERR */
	{ 0, "Server disconnected",		},	/* 38: UPSCLI_ERR_SRVDISC */
	{ 0, "Driver not connected",		},	/* 39: UPSCLI_ERR_DRVNOTCONN */
	{ 0, "Memory allocation failure",	},	/* 40: UPSCLI_ERR_NOMEM */
	{ 3, "Parse error: %s",			},	/* 41: UPSCLI_ERR_PARSE */
	{ 0, "Protocol error",			},	/* 42: UPSCLI_ERR_PROTOCOL */
};

const char *upscli_strerror(UPSCONN_t *ups)
{
#ifdef HAVE_SSL
	unsigned long	err;
	char	sslbuf[UPSCLI_ERRBUF_LEN];
#endif

	if (!ups) {
		return upscli_errlist[UPSCLI_ERR_INVALIDARG].str;
	}

	if (ups->upsclient_magic != UPSCLIENT_MAGIC) {
		return upscli_errlist[UPSCLI_ERR_INVALIDARG].str;
	}

	if (ups->upserror > UPSCLI_ERR_MAX) {
		return "Invalid error number";
	}

	switch (upscli_errlist[ups->upserror].flags) {

	case 0:		/* simple error */
		return upscli_errlist[ups->upserror].str;

	case 1:		/* add message from system's strerror */
		snprintf(ups->errbuf, UPSCLI_ERRBUF_LEN,
			upscli_errlist[ups->upserror].str,
			strerror(ups->syserrno));
		return ups->errbuf;

	case 2:		/* SSL error */
#ifdef HAVE_SSL
		err = ERR_get_error();
		if (err) {
			ERR_error_string(err, sslbuf);
			snprintf(ups->errbuf, UPSCLI_ERRBUF_LEN,
				upscli_errlist[ups->upserror].str,
				sslbuf);
		} else {
			snprintf(ups->errbuf, UPSCLI_ERRBUF_LEN,
				upscli_errlist[ups->upserror].str,
				"peer disconnected");
		}
#else
		snprintf(ups->errbuf, UPSCLI_ERRBUF_LEN, 
			"SSL error, but SSL wasn't enabled at compile-time");
#endif	/* HAVE_SSL */
		return ups->errbuf;

	case 3:		/* parsing (parseconf) error */
		snprintf(ups->errbuf, UPSCLI_ERRBUF_LEN,
			upscli_errlist[ups->upserror].str,
			ups->pc_ctx.errmsg);
		return ups->errbuf;
	}

	/* fallthrough */

	snprintf(ups->errbuf, UPSCLI_ERRBUF_LEN, "Unknown error flag %d",
		upscli_errlist[ups->upserror].flags);

	return ups->errbuf;
}

/* Read up to buflen bytes from fd and return the number of bytes
   read. If no data is available within d_sec + d_usec, return 0.
   On error, a value < 0 is returned (errno indicates error). */
static int select_read(const int fd, void *buf, const size_t buflen, const long d_sec, const long d_usec)
{
	int		ret;
	fd_set		fds;
	struct timeval	tv;

	FD_ZERO(&fds);
	FD_SET(fd, &fds);

	tv.tv_sec = d_sec;
	tv.tv_usec = d_usec;

	ret = select(fd + 1, &fds, NULL, NULL, &tv);

	if (ret < 1) {
		return ret;
	}

	return read(fd, buf, buflen);
}

/* internal: abstract the SSL calls for the other functions */
static int net_read(UPSCONN_t *ups, char *buf, size_t buflen)
{
	int	ret;

#ifdef HAVE_SSL
	if (ups->ssl) {
		ret = SSL_read(ups->ssl, buf, buflen);

		if (ret < 1) {
			ups->upserror = UPSCLI_ERR_SSLERR;
		}

		return ret;
	}
#endif

	ret = select_read(ups->fd, buf, buflen, 5, 0);

	/* error reading data, server disconnected? */
	if (ret < 0) {
		ups->upserror = UPSCLI_ERR_READ;
		ups->syserrno = errno;
	}

	/* no data available, server disconnected? */
	if (ret == 0) {
		ups->upserror = UPSCLI_ERR_SRVDISC;
	}

	return ret;
}

/* Write up to buflen bytes to fd and return the number of bytes
   written. If no data is available within d_sec + d_usec, return 0.
   On error, a value < 0 is returned (errno indicates error). */
static int select_write(const int fd, const void *buf, const size_t buflen, const long d_sec, const long d_usec)
{
	int		ret;
	fd_set		fds;
	struct timeval	tv;

	FD_ZERO(&fds);
	FD_SET(fd, &fds);

	tv.tv_sec = d_sec;
	tv.tv_usec = d_usec;

	ret = select(fd + 1, NULL, &fds, NULL, &tv);

	if (ret < 1) {
		return ret;
	}

	return write(fd, buf, buflen);
}

/* internal: abstract the SSL calls for the other functions */
static int net_write(UPSCONN_t *ups, const char *buf, size_t buflen)
{
	int	ret;

#ifdef HAVE_SSL
	if (ups->ssl) {
		ret = SSL_write(ups->ssl, buf, buflen);

		if (ret < 1) {
			ups->upserror = UPSCLI_ERR_SSLERR;
		}

		return ret;
	}
#endif

	ret = select_write(ups->fd, buf, buflen, 0, 0);

	/* error writing data, server disconnected? */
	if (ret < 0) {
		ups->upserror = UPSCLI_ERR_WRITE;
		ups->syserrno = errno;
	}

	/* not ready for writing, server disconnected? */
	if (ret == 0) {
		ups->upserror = UPSCLI_ERR_SRVDISC;
	}

	return ret;
}

/* stub first */
#ifndef HAVE_SSL
static int upscli_sslinit(UPSCONN_t *ups)
{
	return 0;	/* not supported */
}

int upscli_sslcert(UPSCONN_t *ups, const char *dir, const char *file, int verify)
{
	if (!ups) {
		return -1;
	}

	/* if forcing the verification, this fails since we have no SSL */
	if (verify == 1) {
		ups->upserror = UPSCLI_ERR_SSLFAIL;
		return -1;
	}
		
	return 0;		/* not supported */
}

#else

static int upscli_sslinit(UPSCONN_t *ups)
{
#if OPENSSL_VERSION_NUMBER >= 0x10000000L
	const SSL_METHOD	*ssl_method;
#else
	SSL_METHOD	*ssl_method;
#endif
	char	buf[UPSCLI_NETBUF_LEN];

	/* see if upsd even talks SSL/TLS */
	snprintf(buf, sizeof(buf), "STARTTLS\n");

	if (upscli_sendline(ups, buf, strlen(buf)) != 0) {
		return -1;
	}

	if (upscli_readline(ups, buf, sizeof(buf)) != 0) {
		return -1;
	}

	if (strncmp(buf, "OK STARTTLS", 11) != 0) {
		return 0;		/* not supported */
	}

	/* upsd is happy, so let's crank up the client */

	SSL_load_error_strings();
	SSL_library_init();

	ssl_method = TLSv1_client_method();

	if (!ssl_method) {
		return 0;
	}

	ups->ssl_ctx = SSL_CTX_new(ssl_method);

	if (!ups->ssl_ctx) {
		return 0;
	}

	ups->ssl = SSL_new(ups->ssl_ctx);

	if (!ups->ssl) {
		return 0;
	}

	if (SSL_set_fd(ups->ssl, ups->fd) != 1) {
		return -1;
	}

	SSL_set_connect_state(ups->ssl);

	return 1;	/* OK */
}

/* set the paths for the certs to verify the server */
int upscli_sslcert(UPSCONN_t *ups, const char *file, const char *path, int verify)
{
	int	ret, ssl_mode = SSL_VERIFY_NONE;

	if (!ups) {
		return -1;
	}

	if (!ups->ssl_ctx) {
		ups->upserror = UPSCLI_ERR_INVALIDARG;
		return -1;
	}

	switch(verify) {

	case 0:
		ssl_mode = SSL_VERIFY_NONE;
		break;
	case 1:
		ssl_mode = SSL_VERIFY_PEER;
		break;
	default:
		ups->upserror = UPSCLI_ERR_INVALIDARG;
		return -1;
	}

	ret = SSL_CTX_load_verify_locations(ups->ssl_ctx, file, path);

	if (ret != 1) {
		ups->upserror = UPSCLI_ERR_SSLERR;
		return -1;
	}

	SSL_set_verify(ups->ssl, ssl_mode, NULL);

	return 1;
}

#endif	/* HAVE_SSL */

int upscli_tryconnect(UPSCONN_t *ups, const char *host, int port, int flags,struct timeval * timeout)
{
	int	sock_fd;
	struct addrinfo	hints, *res, *ai;
	char			sport[NI_MAXSERV];
	int			v;
	fd_set 			wfds;
	int			error;
	socklen_t		error_size;
	long			fd_flags;

	if (!ups) {
		return -1;
	}

	/* clear out any lingering junk */
	memset(ups, 0, sizeof(*ups));
	ups->upsclient_magic = UPSCLIENT_MAGIC;
	ups->fd = -1;

	if (!host) {
		ups->upserror = UPSCLI_ERR_NOSUCHHOST;
		return -1;
	}

	snprintf(sport, sizeof(sport), "%hu", (unsigned short int)port);

	memset(&hints, 0, sizeof(hints));

	if (flags & UPSCLI_CONN_INET6) {
		hints.ai_family = AF_INET6;
	} else if (flags & UPSCLI_CONN_INET) {
		hints.ai_family = AF_INET;
	} else {
		hints.ai_family = AF_UNSPEC;
	}

	hints.ai_socktype = SOCK_STREAM;
	hints.ai_protocol = IPPROTO_TCP;

	while ((v = getaddrinfo(host, sport, &hints, &res)) != 0) {
		switch (v)
		{
		case EAI_AGAIN:
			continue;
		case EAI_NONAME:
			ups->upserror = UPSCLI_ERR_NOSUCHHOST;
			return -1;
		case EAI_MEMORY:
			ups->upserror = UPSCLI_ERR_NOMEM;
			return -1;
		case EAI_SYSTEM:
			ups->syserrno = errno;
			break;
		}

		ups->upserror = UPSCLI_ERR_UNKNOWN;
		return -1;
	}

	for (ai = res; ai != NULL; ai = ai->ai_next) {

		sock_fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);

		if (sock_fd < 0) {
			switch (errno)
			{
			case EAFNOSUPPORT:
			case EINVAL:
                                break;
			default:
				ups->upserror = UPSCLI_ERR_SOCKFAILURE;
				ups->syserrno = errno;
			}
			continue;
		}

		/* non blocking connect */
		if(timeout != NULL) {
			fd_flags = fcntl(sock_fd, F_GETFL);
			fd_flags |= O_NONBLOCK;
			fcntl(sock_fd, F_SETFL, fd_flags);
		}

		while ((v = connect(sock_fd, ai->ai_addr, ai->ai_addrlen)) < 0) {
			if(errno == EINPROGRESS) {
				FD_ZERO(&wfds);
				FD_SET(sock_fd, &wfds);
				select(sock_fd+1,NULL,&wfds,NULL,
						timeout);
				if (FD_ISSET(sock_fd, &wfds)) {
					error_size = sizeof(error);
					getsockopt(sock_fd,SOL_SOCKET,SO_ERROR,
							&error,&error_size);
					if( error == 0) {
						/* connect successful */
						v = 0;
						break;
					}
					errno = error;
				}
				else {
					/* Timeout */
					v = -1;
					break;
				}
			}

			switch (errno)
			{
			case EAFNOSUPPORT:
				break;
			case EINTR:
			case EAGAIN:
				continue;
			default:
				ups->upserror = UPSCLI_ERR_CONNFAILURE;
				ups->syserrno = errno;
			}
			break;
		}

		if (v < 0) {
			close(sock_fd);
			continue;
		}

		/* switch back to blocking operation */
		if(timeout != NULL) {
			fd_flags = fcntl(sock_fd, F_GETFL);
			fd_flags &= ~O_NONBLOCK;
			fcntl(sock_fd, F_SETFL, fd_flags);
		}

		ups->fd = sock_fd;
		ups->upserror = 0;
		ups->syserrno = 0;
		break;
	}

	freeaddrinfo(res);

	if (ups->fd < 0) {
		return -1;
	}

	pconf_init(&ups->pc_ctx, NULL);

	ups->host = strdup(host);

	if (!ups->host) {
		ups->upserror = UPSCLI_ERR_NOMEM;
		upscli_disconnect(ups);
		return -1;
	}

	ups->port = port;

	if (flags & UPSCLI_CONN_TRYSSL) {
		upscli_sslinit(ups);

		/* see if something made us die inside sslinit */
		if (ups->upserror != 0) {
			upscli_disconnect(ups);
			return -1;
		}
	}

	if ((flags & UPSCLI_CONN_REQSSL) && (upscli_sslinit(ups) != 1)) {
		ups->upserror = UPSCLI_ERR_SSLFAIL;
		upscli_disconnect(ups);
		return -1;
	}
		
	return 0;
}

int upscli_connect(UPSCONN_t *ups, const char *host, int port, int flags)
{
	return upscli_tryconnect(ups,host,port,flags,NULL);
}

/* map upsd error strings back to upsclient internal numbers */
static struct {
	int	errnum;
	const	char	*text;
}	upsd_errlist[] =
{
	{ UPSCLI_ERR_VARNOTSUPP,	"VAR-NOT-SUPPORTED"	},	
	{ UPSCLI_ERR_UNKNOWNUPS,	"UNKNOWN-UPS"		},
	{ UPSCLI_ERR_ACCESSDENIED, 	"ACCESS-DENIED"		},
	{ UPSCLI_ERR_PWDREQUIRED,	"PASSWORD-REQUIRED"	},
	{ UPSCLI_ERR_PWDINCORRECT,	"PASSWORD-INCORRECT"	},
	{ UPSCLI_ERR_MISSINGARG,	"MISSING-ARGUMENT"	},
	{ UPSCLI_ERR_DATASTALE,		"DATA-STALE"		},
	{ UPSCLI_ERR_VARUNKNOWN,	"VAR-UNKNOWN"		},
	{ UPSCLI_ERR_LOGINTWICE,	"ALREADY-LOGGED-IN"	},
	{ UPSCLI_ERR_PWDSETTWICE,	"ALREADY-SET-PASSWORD"	},
	{ UPSCLI_ERR_UNKNOWNTYPE,	"UNKNOWN-TYPE"		},
	{ UPSCLI_ERR_UNKNOWNVAR,	"UNKNOWN-VAR"		},
	{ UPSCLI_ERR_VARREADONLY,	"READONLY"		},
	{ UPSCLI_ERR_TOOLONG,		"TOO-LONG"		},
	{ UPSCLI_ERR_INVALIDVALUE,	"INVALID-VALUE"		},
	{ UPSCLI_ERR_SETFAILED,		"SET-FAILED"		},
	{ UPSCLI_ERR_UNKINSTCMD,	"UNKNOWN-INSTCMD"	},
	{ UPSCLI_ERR_CMDFAILED,		"INSTCMD-FAILED"	},
	{ UPSCLI_ERR_CMDNOTSUPP,	"CMD-NOT-SUPPORTED"	},
	{ UPSCLI_ERR_INVUSERNAME,	"INVALID-USERNAME"	},
	{ UPSCLI_ERR_USERSETTWICE,	"ALREADY-SET-USERNAME"	},
	{ UPSCLI_ERR_UNKCOMMAND,	"UNKNOWN-COMMAND"	},
	{ UPSCLI_ERR_INVPASSWORD,	"INVALID-PASSWORD"	},
	{ UPSCLI_ERR_USERREQUIRED,	"USERNAME-REQUIRED"	},
	{ UPSCLI_ERR_DRVNOTCONN,	"DRIVER-NOT-CONNECTED"	},
	
	{ 0,			NULL,		}
};

static int upscli_errcheck(UPSCONN_t *ups, char *buf)
{
	int	i;

	if (!ups) {
		return -1;
	}

	if (!buf) {
		ups->upserror = UPSCLI_ERR_INVALIDARG;
		return -1;
	}

	/* see if it's even an error now */
	if (strncmp(buf, "ERR", 3) != 0) {
		return 0;
	}

	/* look it up in the table */
	for (i = 0; upsd_errlist[i].text != NULL; i++) {
		if (!strncmp(&buf[4], upsd_errlist[i].text,
			strlen(upsd_errlist[i].text))) {
			ups->upserror = upsd_errlist[i].errnum;
			return -1;
		}
	}

	/* hmm - don't know what upsd is telling us */
	ups->upserror = UPSCLI_ERR_UNKNOWN;
	return -1;
}

static void build_cmd(char *buf, size_t bufsize, const char *cmdname,
	int numarg, const char **arg)
{
	int	i;
	size_t	len;
	char	enc[UPSCLI_NETBUF_LEN];
	const char	*format;

	memset(buf, '\0', bufsize);
	snprintf(buf, bufsize, "%s", cmdname);

	/* encode all arguments so they arrive intact */
	for (i = 0; i < numarg; i++) {

		if (strchr(arg[i], ' ')) {
			format = " \"%s\"";	/* wrap in "" */
		} else {
			format = " %s";
		}

		/* snprintfcat would tie us to common */

		len = strlen(buf);
		snprintf(buf + len, bufsize - len, format, 
			pconf_encode(arg[i], enc, sizeof(enc)));
	}

	len = strlen(buf);
	snprintf(buf + len, bufsize - len, "\n");
}

/* make sure upsd is giving us what we asked for */
static int verify_resp(int num, const char **q, char **a)
{
	int	i;

	for (i = 0; i < num; i++) {
		if (strcasecmp(q[i], a[i]) != 0) {

			/* FUTURE: handle -/+ options here */
			return 0;	/* mismatch */
		}
	}

	return 1;	/* OK */
}

int upscli_get(UPSCONN_t *ups, unsigned int numq, const char **query, 
		unsigned int *numa, char ***answer)
{
	char	cmd[UPSCLI_NETBUF_LEN], tmp[UPSCLI_NETBUF_LEN];
	
	if (!ups) {
		return -1;
	}

	if (numq < 1) {
		ups->upserror = UPSCLI_ERR_INVALIDARG;
		return -1;
	}

	/* create the string to send to upsd */
	build_cmd(cmd, sizeof(cmd), "GET", numq, query);

	if (upscli_sendline(ups, cmd, strlen(cmd)) != 0) {
		return -1;
	}

	if (upscli_readline(ups, tmp, sizeof(tmp)) != 0) {
		return -1;
	}

	if (upscli_errcheck(ups, tmp) != 0) {
		return -1;
	}

	if (!pconf_line(&ups->pc_ctx, tmp)) {
		ups->upserror = UPSCLI_ERR_PARSE;
		return -1;
	}

	/* q: [GET] VAR <ups> <var>   *
	 * a: VAR <ups> <var> <val> */

	if (ups->pc_ctx.numargs < numq) {
		ups->upserror = UPSCLI_ERR_PROTOCOL;
		return -1;
	}

	if (!verify_resp(numq, query, ups->pc_ctx.arglist)) {
		ups->upserror = UPSCLI_ERR_PROTOCOL;
		return -1;
	}

	*numa = ups->pc_ctx.numargs;
	*answer = ups->pc_ctx.arglist;

	return 0;
}

int upscli_list_start(UPSCONN_t *ups, unsigned int numq, const char **query)
{
	char	cmd[UPSCLI_NETBUF_LEN], tmp[UPSCLI_NETBUF_LEN];

	if (!ups) {
		return -1;
	}

	if (numq < 1) {
		ups->upserror = UPSCLI_ERR_INVALIDARG;
		return -1;
	}

	/* create the string to send to upsd */
	build_cmd(cmd, sizeof(cmd), "LIST", numq, query);

	if (upscli_sendline(ups, cmd, strlen(cmd)) != 0) {
		return -1;
	}

	if (upscli_readline(ups, tmp, sizeof(tmp)) != 0) {
		return -1;
	}

	if (upscli_errcheck(ups, tmp) != 0) {
		return -1;
	}

	if (!pconf_line(&ups->pc_ctx, tmp)) {
		ups->upserror = UPSCLI_ERR_PARSE;
		return -1;
	}

	if (ups->pc_ctx.numargs < 2) {
		ups->upserror = UPSCLI_ERR_PROTOCOL;
		return -1;
	}

	/* the response must start with BEGIN LIST */
	if ((strcasecmp(ups->pc_ctx.arglist[0], "BEGIN") != 0) ||
		(strcasecmp(ups->pc_ctx.arglist[1], "LIST") != 0)) {
		ups->upserror = UPSCLI_ERR_PROTOCOL;
		return -1;
	}

	/* q: [LIST] VAR <ups>       *
	 * a: [BEGIN LIST] VAR <ups> */

	/* compare q[0]... to a[2]... */

	if (!verify_resp(numq, query, &ups->pc_ctx.arglist[2])) {
		ups->upserror = UPSCLI_ERR_PROTOCOL;
		return -1;
	}

	return 0;
}

int upscli_list_next(UPSCONN_t *ups, unsigned int numq, const char **query, 
		unsigned int *numa, char ***answer)
{
	char	tmp[UPSCLI_NETBUF_LEN];

	if (!ups) {
		return -1;
	}

	if (upscli_readline(ups, tmp, sizeof(tmp)) != 0) {
		return -1;
	}

	if (upscli_errcheck(ups, tmp) != 0) {
		return -1;
	}

	if (!pconf_line(&ups->pc_ctx, tmp)) {
		ups->upserror = UPSCLI_ERR_PARSE;
		return -1;
	}

	if (ups->pc_ctx.numargs < 1) {
		ups->upserror = UPSCLI_ERR_PROTOCOL;
		return -1;
	}

	*numa = ups->pc_ctx.numargs;
	*answer = ups->pc_ctx.arglist;

	/* see if this is the end */
	if (ups->pc_ctx.numargs >= 2) {
		if ((!strcmp(ups->pc_ctx.arglist[0], "END")) &&
			(!strcmp(ups->pc_ctx.arglist[1], "LIST")))
			return 0;
	}

	/* q: VAR <ups> */
	/* a: VAR <ups> <val> */

	if (!verify_resp(numq, query, ups->pc_ctx.arglist)) {
		ups->upserror = UPSCLI_ERR_PROTOCOL;
		return -1;
	}

	/* just another part of the list */
	return 1;
}

int upscli_sendline(UPSCONN_t *ups, const char *buf, size_t buflen)
{
	int	ret;

	if (!ups) {
		return -1;
	}

	if (ups->fd < 0) {
		ups->upserror = UPSCLI_ERR_DRVNOTCONN;
		return -1;
	}

	if ((!buf) || (buflen < 1)) {
		ups->upserror = UPSCLI_ERR_INVALIDARG;
		return -1;
	}

	if (ups->upsclient_magic != UPSCLIENT_MAGIC) {
		ups->upserror = UPSCLI_ERR_INVALIDARG;
		return -1;
	}

	ret = net_write(ups, buf, buflen);

	if (ret < 1) {
		upscli_disconnect(ups);
		return -1;
	}

	return 0;
}

int upscli_readline(UPSCONN_t *ups, char *buf, size_t buflen)
{
	int	ret;
	size_t	recv;

	if (!ups) {
		return -1;
	}

	if (ups->fd < 0) {
		ups->upserror = UPSCLI_ERR_DRVNOTCONN;
		return -1;
	}

	if ((!buf) || (buflen < 1)) {
		ups->upserror = UPSCLI_ERR_INVALIDARG;
		return -1;
	}

	if (ups->upsclient_magic != UPSCLIENT_MAGIC) {
		ups->upserror = UPSCLI_ERR_INVALIDARG;
		return -1;
	}

	for (recv = 0; recv < (buflen-1); recv++) {

		if (ups->readidx == ups->readlen) {

			ret = net_read(ups, ups->readbuf, sizeof(ups->readbuf));

			if (ret < 1) {
				upscli_disconnect(ups);
				return -1;
			}

			ups->readlen = ret;
			ups->readidx = 0;
		}

		buf[recv] = ups->readbuf[ups->readidx++];

		if (buf[recv] == '\n') {
			break;
		}
	}

	buf[recv] = '\0';
	return 0;
}

/* split upsname[@hostname[:port]] into separate components */
int upscli_splitname(const char *buf, char **upsname, char **hostname, int *port)
{
	char	*s, tmp[SMALLBUF], *last = NULL;

	/* paranoia */
	if ((!buf) || (!upsname) || (!hostname) || (!port)) {
		return -1;
	}

	if (snprintf(tmp, sizeof(tmp), "%s", buf) < 1) {
		fprintf(stderr, "upscli_splitname: can't parse empty string\n");
		return -1;
	}

	s = strchr(tmp, '@');

	if ((*upsname = strdup(strtok_r(tmp, "@", &last))) == NULL) {
		fprintf(stderr, "upscli_splitname: strdup failed\n");
		return -1;
	}

	/* only a upsname is specified, fill in defaults */
	if (s == NULL) {
		if ((*hostname = strdup("localhost")) == NULL) {
			fprintf(stderr, "upscli_splitname: strdup failed\n");
			return -1;
		}

		*port = PORT;
		return 0;
	}

	return upscli_splitaddr(s+1, hostname, port);
}

/* split hostname[:port] into separate components */
int upscli_splitaddr(const char *buf, char **hostname, int *port)
{
	char	*s, tmp[SMALLBUF], *last = NULL;

	/* paranoia */
	if ((!buf) || (!hostname) || (!port)) {
		return -1;
	}

	if (snprintf(tmp, sizeof(tmp), "%s", buf) < 1) {
		fprintf(stderr, "upscli_splitaddr: can't parse empty string\n");
		return -1;
	}

	if (*tmp == '[') {
		if (strchr(tmp, ']') == NULL) {
			fprintf(stderr, "upscli_splitaddr: missing closing bracket in [domain literal]\n");
			return -1;
		}

		if ((*hostname = strdup(strtok_r(tmp+1, "]", &last))) == NULL) {
			fprintf(stderr, "upscli_splitaddr: strdup failed\n");
			return -1;
		}

		/* no port specified, use default */
		if (((s = strtok_r(NULL, "\0", &last)) == NULL) || (*s != ':')) {
			*port = PORT;
			return 0;
		}
	} else {
		s = strchr(tmp, ':');

		if ((*hostname = strdup(strtok_r(tmp, ":", &last))) == NULL) {
			fprintf(stderr, "upscli_splitaddr: strdup failed\n");
			return -1;
		}

		/* no port specified, use default */
		if (s == NULL) {
			*port = PORT;
			return 0;
		}
	}

	if ((*(++s) == '\0') || ((*port = strtol(s, NULL, 10)) < 1 )) {
		fprintf(stderr, "upscli_splitaddr: no port specified after ':' separator\n");
		return -1;
	}

	return 0;
}

int upscli_disconnect(UPSCONN_t *ups)
{
	if (!ups) {
		return -1;
	}

	if (ups->upsclient_magic != UPSCLIENT_MAGIC) {
		return -1;
	}

	pconf_finish(&ups->pc_ctx);

	free(ups->host);
	ups->host = NULL;

	if (ups->fd < 0) {
		return 0;
	}

	net_write(ups, "LOGOUT\n", 7);

#ifdef HAVE_SSL
	if (ups->ssl) {
		SSL_shutdown(ups->ssl);
		SSL_free(ups->ssl);
		ups->ssl = NULL;
	}

	if (ups->ssl_ctx) {
		SSL_CTX_free(ups->ssl_ctx);
		ups->ssl_ctx = NULL;
	}
#endif

	shutdown(ups->fd, shutdown_how);

	close(ups->fd);
	ups->fd = -1;

	return 0;
}

int upscli_fd(UPSCONN_t *ups)
{
	if (!ups) {
		return -1;
	}

	if (ups->upsclient_magic != UPSCLIENT_MAGIC) {
		return -1;
	}

	return ups->fd;
}

int upscli_upserror(UPSCONN_t *ups)
{
	if (!ups) {
		return -1;
	}

	if (ups->upsclient_magic != UPSCLIENT_MAGIC) {
		return -1;
	}

	return ups->upserror;
}

int upscli_ssl(UPSCONN_t *ups)
{
	if (!ups) {
		return -1;
	}

	if (ups->upsclient_magic != UPSCLIENT_MAGIC) {
		return -1;
	}

	if (ups->ssl) {
		return 1;
	}

	return 0;
}
