/*
 * libdhcpcd
 * Copyright 2009-2016 Roy Marples <roy@marples.name>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

// For strverscmp(3)
#define _GNU_SOURCE

#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>

#include <arpa/inet.h>

#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define IN_LIBDHCPCD

#include "config.h"
#include "dhcpcd.h"

#ifdef HAS_GETTEXT
#include <libintl.h>
#define _ gettext
#else
#define _(a) (a)
#endif

#ifdef HAVE_VIS_H
#include <vis.h>
#endif

#ifndef SUN_LEN
#define SUN_LEN(su) \
	(sizeof(*(su)) - sizeof((su)->sun_path) + strlen((su)->sun_path))
#endif

#ifndef iswhite
#define iswhite(c)	(c == ' ' || c == '\t' || c == '\n')
#endif

const char * const dhcpcd_cstates[DHC_MAX] = {
	"unknown",
	"down",
	"opened",
	"initialised",
	"disconnected",
	"connecting",
	"waiting",
	"connected",
};

struct dhcpcd_vs {
	unsigned int val;
	const char *str;
};

static const struct dhcpcd_vs dhcpcd_states[] = {
    { DHS_DUMP,			"DUMP" },
    { DHS_TEST,			"TEST" },
    { DHS_STOPPED,		"STOPPED" },
    { DHS_FAIL,			"FAIL" },
    { DHS_STOP,			"STOP" },
    { DHS_PREINIT,		"PREINIT" },
    { DHS_DEPARTED,		"DEPARTED" },
    { DHS_NOCARRIER,		"NOCARRIER" },
    { DHS_NOCARRIER_ROAMING,	"NOCARRIER_ROAMING " },
    { DHS_NAK,			"NAK" },
    { DHS_EXPIRE,		"EXPIRE" },
    { DHS_RECONFIGURE,		"RECONFIGURE" },
    { DHS_CARRIER,		"CARRIER" },
    { DHS_STATIC,		"STATIC" },
    { DHS_3RDPARTY,		"3RDPARTY" },
    { DHS_IPV4LL,		"IPV4LL" },
    { DHS_INFORM,		"INFORM" },
    { DHS_BOUND,		"BOUND" },
    { DHS_RENEW,		"RENEW" },
    { DHS_REBIND,		"REBIND" },
    { DHS_REBOOT,		"REBOOT" },
    { DHS_ROUTERADVERT,		"ROUTERADVERT" },
    { DHS_BOUND,		"DELEGATED" },
    { DHS_UNKNOWN,		NULL    }
};

static ssize_t
dhcpcd_command_fd(DHCPCD_CONNECTION *con,
    int fd, bool progname, const char *cmd, char **buffer)
{
	size_t pl, cl, len;
	ssize_t bytes;
	char buf[1024], *p;
	char *nbuf;

	assert(con);
	assert(cmd);

	/* Each command is \n terminated.
	 * Each argument is NULL seperated.
	 * We may need to send a space one day, so the API
	 * in this function may need to be improved */
	cl = strlen(cmd);
	if (progname) {
		pl = strlen(con->progname);
		len = pl + 1 + cl + 1;
	} else {
		pl = 0;
		len = cl + 1;
	}
	if (con->terminate_commands)
		len++;
	if (len > sizeof(buf)) {
		errno = ENOBUFS;
		return -1;
	}
	p = buf;
	if (progname) {
		memcpy(buf, con->progname, pl);
		buf[pl] = '\0';
		p = buf + pl + 1;
	}
	memcpy(p, cmd, cl);
	p[cl] = '\0';
	while ((p = strchr(p, ' ')) != NULL)
		*p++ = '\0';
	if (con->terminate_commands) {
		buf[len - 2] = '\n';
		buf[len - 1] = '\0';
	} else
		buf[len - 1] = '\0';

	if (write(fd, buf, len) == -1)
		return -1;

	if (con->read_error) {
		int error;

		bytes = read(fd, &error, sizeof(error));
		if (bytes != sizeof(error)) {
			errno = EINVAL;
			return -1;
		}
		con->error = error;
		if (error != 0) {
			errno = error;
			return -1;
		}
	}

	if (buffer == NULL)
		return 0;

	bytes = read(fd, buf, sizeof(size_t));
	if (bytes == 0 || bytes == -1)
		return bytes;
	memcpy(&len, buf, sizeof(size_t));
	nbuf = realloc(*buffer, len + 1);
	if (nbuf == NULL)
		return -1;
	*buffer = nbuf;
	bytes = read(fd, *buffer, len);
	if (bytes != -1 && (size_t)bytes < len)
		*buffer[bytes] = '\0';
	return bytes;
}

ssize_t
dhcpcd_command(DHCPCD_CONNECTION *con, const char *cmd, char **buffer)
{

	assert(con);
	if (!con->privileged) {
		errno = EACCES;
		return -1;
	}
	return dhcpcd_command_fd(con, con->command_fd, true, cmd, buffer);
}

static ssize_t
dhcpcd_ctrl_command(DHCPCD_CONNECTION *con, const char *cmd, char **buffer)
{

	return dhcpcd_command_fd(con, con->command_fd, false, cmd, buffer);
}

bool
dhcpcd_realloc(DHCPCD_CONNECTION *con, size_t len)
{

	assert(con);
	if (con->buflen < len) {
		char *nbuf;

		nbuf = realloc(con->buf, len);
		if (nbuf == NULL)
			return false;
		con->buf = nbuf;
		con->buflen = len;
	}
	return true;
}

ssize_t
dhcpcd_command_arg(DHCPCD_CONNECTION *con, const char *cmd, const char *arg,
    char **buffer)
{
	size_t cmdlen, len;

	assert(con);
	assert(cmd);

	cmdlen = strlen(cmd);
	if (arg)
		len = cmdlen + strlen(arg) + 2;
	else
		len = cmdlen + 1;
	if (!dhcpcd_realloc(con, len))
		return -1;
	strlcpy(con->buf, cmd, con->buflen);
	if (arg) {
		con->buf[cmdlen] = ' ';
		strlcpy(con->buf + cmdlen + 1, arg, con->buflen - 1 - cmdlen);
	}

	return dhcpcd_command_fd(con, con->command_fd, true, con->buf, buffer);
}


static int
dhcpcd_connect(const char *path)
{
	int fd;
	socklen_t len;
	struct sockaddr_un sun;

	fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
	if (fd == -1)
		return -1;

	memset(&sun, 0, sizeof(sun));
	sun.sun_family = AF_UNIX;
	strlcpy(sun.sun_path, path, sizeof(sun.sun_path));
	len = (socklen_t)SUN_LEN(&sun);
	if (connect(fd, (struct sockaddr *)&sun, len) == 0)
		return fd;
	close(fd);
	return -1;
}

static const char *
get_value(const char *data, size_t len, const char *var)
{
	const char *end, *p;
	size_t vlen;

	assert(var);
	end = data + len;
	vlen = strlen(var);
	p = NULL;
	while (data + vlen + 1 < end) {
		/* Skip past NUL padding */
		if (*data == '\0') {
			data++;
			continue;
		}
		if (strncmp(data, var, vlen) == 0 && data[vlen] == '=') {
			p = data + vlen + 1;
			break;
		}
		data += strlen(data) + 1;
	}
	if (p != NULL && *p != '\0')
		return p;
	return NULL;
}

const char *
dhcpcd_get_value(const DHCPCD_IF *i, const char *var)
{

	assert(i);
	assert(var);
	return get_value(i->data, i->data_len, var);
}

ssize_t
dhcpcd_encode_string_escape(char *dst, size_t len, const char *src, size_t slen)
{
	const char *end;
	size_t bytes;
	unsigned char c;

	end = src + slen;
	bytes = 0;
	while (src < end) {
		c = (unsigned char)*src++;
		if ((c == '\\' || !isascii(c) || !isprint(c))) {
			if (c == '\\') {
				if (dst) {
					if (len  == 0 || len == 1) {
						errno = ENOSPC;
						return -1;
					}
					*dst++ = '\\'; *dst++ = '\\';
					len -= 2;
				}
				bytes += 2;
				continue;
			}
			if (dst) {
				if (len < 5) {
					errno = ENOSPC;
					return -1;
				}
				*dst++ = '\\';
		                *dst++ = (char)(((c >> 6) & 03) + '0');
		                *dst++ = (char)(((c >> 3) & 07) + '0');
		                *dst++ = (char)(( c       & 07) + '0');
				len -= 4;
			}
			bytes += 4;
		} else {
			if (dst) {
				if (len == 0) {
					errno = ENOSPC;
					return -1;
				}
				*dst++ = (char)c;
				len--;
			}
			bytes++;
		}
	}

	if (dst) {
		if (len == 0) {
			errno = ENOSPC;
			return -1;
		}
		*dst = '\0';
	}

	return (ssize_t)bytes;
}

ssize_t
dhcpcd_decode_string_escape(char *dst, size_t dlen, const char *src)
{
	char c, esc;
	int oct;
	ssize_t bytes;

	bytes = 0;
	for (;;) {
		c = *src++;
		if (c == '\0')
			break;
		if (dst && --dlen == 0) {
			errno = ENOSPC;
			return -1;
		}
		switch (c) {
		case '\\':
			if (*src == '\0') {
				errno = EINVAL;
				return -1;
			}
			esc = *src++;
			switch (esc) {
			case '\\':
				if (dst)
					*dst++ = esc;
				break;
			case '0':
			case '1':
			case '2':
			case '3':
			case '4':
			case '5':
			case '6':
			case '7':
				oct = esc - '0';
				if (*src >= '0' && *src <='7')
					oct = oct * 8 + (*src++ - '0');
				else {
					errno = EINVAL;
					return -1;
				}
				if (*src >= '0' && *src <='7')
					oct = oct * 8 + (*src++ - '0');
				else {
					errno = EINVAL;
					return -1;
				}
				if (dst)
					*dst++ = (char)oct;
				break;
			default:
				errno = EINVAL;
				return -1;
			}
			break;
		default:
			if (dst)
				*dst++ = c;
		}
		bytes++;
	}

	if (dst) {
		if (--dlen == 0) {
			errno = ENOSPC;
			return -1;
		}
		*dst = '\0';
	}
	return bytes;
}

ssize_t
dhcpcd_decode_hex(char *dst, size_t dlen, const char *src)
{
	size_t bytes, i;
	char c;
	int val, n;

	bytes = 0;
	while (*src) {
		if (dlen == 0 || dlen == 1) {
			errno = ENOSPC;
			return -1;
		}
		val = 0;
		for (i = 0; i < 2; i++) {
			c = *src++;
			if (c >= '0' && c <= '9')
				n = c - '0';
			else if (c >= 'a' && c <= 'f')
				n = 10 + c - 'a';
			else if (c >= 'A' && c <= 'F')
				n = 10 + c - 'A';
			else {
				errno = EINVAL;
				return -1;
			}
			val = val * 16 + n;
		}
		*dst++ = (char)val;
		bytes += 2;
		dlen -= 2;
		if (*src == ':')
			src++;
	}
	return (ssize_t)bytes;
}

const char *
dhcpcd_get_prefix_value(const DHCPCD_IF *i, const char *prefix, const char *var)
{
	char pvar[128], *p;
	size_t plen, l;

	assert(i);
	assert(prefix);
	assert(var);

	p = pvar;
	plen = sizeof(pvar);
	l = strlcpy(p, prefix, plen);
	if (l >= sizeof(pvar)) {
		errno = ENOBUFS;
		return NULL;
	}
	p += l;
	plen -= l;
	if (strlcpy(p, var, plen) >= plen) {
		errno = ENOBUFS;
		return NULL;
	}
	return dhcpcd_get_value(i, pvar);
}

static bool
strtobool(const char *var)
{

	if (var == NULL)
		return false;

	 return (*var == '0' || *var == '\0' ||
	    strcmp(var, "false") == 0 ||
	    strcmp(var, "no") == 0) ? false : true;
}

static unsigned int
get_status(DHCPCD_CONNECTION *con)
{
	DHCPCD_IF *i;
	unsigned int status;

	assert(con);
	if (con->command_fd == -1)
		return DHC_DOWN;

	if (con->listen_fd == -1 || con->error != 0)
		return DHC_OPENED;

	if (con->interfaces == NULL)
		return DHC_INITIALISED;

	status = DHC_DISCONNECTED;
	for (i = con->interfaces; i; i = i->next) {
		if (i->up) {
			if (i->type == DHT_LINK) {
				if (status == DHC_DISCONNECTED)
					status = DHC_CONNECTING;
			} else {
				if (con->af_waiting)
					status = DHC_AF_WAITING;
				else {
					status = DHC_CONNECTED;
					break;
				}
			}
		}
	}
	return status;
}

static void
update_status(DHCPCD_CONNECTION *con, unsigned int nstatus)
{

	assert(con);
	if (nstatus == DHC_UNKNOWN)
		nstatus = get_status(con);
	if (con->status != nstatus) {
		con->status = nstatus;
		if (con->status_cb)
			con->status_cb(con, con->status,
			    dhcpcd_cstates[con->status], con->status_context);
	}
}

DHCPCD_IF *
dhcpcd_interfaces(DHCPCD_CONNECTION *con)
{

	assert(con);
	return con->interfaces;
}

char **
dhcpcd_interface_names(DHCPCD_CONNECTION *con, size_t *nnames)
{
	char **names;
	size_t n;
	DHCPCD_IF *i;

	assert(con);
	if (con->interfaces == NULL)
		return NULL;

	n = 0;
	for (i = con->interfaces; i; i = i->next) {
		 if (i->type == DHT_LINK)
			n++;
	}
	names = malloc(sizeof(char *) * (n + 1));
	if (names == NULL)
		return NULL;
	n = 0;
	for (i = con->interfaces; i; i = i->next) {
		if (i->type == DHT_LINK) {
			names[n] = strdup(i->ifname);
			if (names[n] == NULL) {
				dhcpcd_freev(names);
				return NULL;
			}
			n++;
		}
	}
	names[n] = NULL;
	if (nnames)
		*nnames = n;

	return names;
}

void
dhcpcd_freev(char **argv)
{
	char **v;

	if (argv) {
		for (v = argv; *v; v++)
			free(*v);
		free(argv);
	}
}

static int
dhcpcd_cmpstring(const void *p1, const void *p2)
{
	const char *s1, *s2;
	int cmp;

	s1 = *(char * const *)p1;
	s2 = *(char * const *)p2;
	if ((cmp = strcasecmp(s1, s2)) == 0)
		cmp = strcmp(s1, s2);
	return cmp;
}

char **
dhcpcd_interface_names_sorted(DHCPCD_CONNECTION *con)
{
	char **names;
	size_t nnames;

	names = dhcpcd_interface_names(con, &nnames);
	if (names)
		qsort(names, nnames, sizeof(char *), dhcpcd_cmpstring);
	return names;
}

DHCPCD_IF *
dhcpcd_get_if(DHCPCD_CONNECTION *con, const char *ifname, unsigned int type)
{
	DHCPCD_IF *i;

	assert(con);
	assert(ifname);
	assert(type);

	for (i = con->interfaces; i; i = i->next)
		if (i->type == type && strcmp(i->ifname, ifname) == 0)
			return i;
	return NULL;
}

static unsigned int
dhcpcd_reason_to_state1(const char *reason, int *isdhcp6)
{
	unsigned int i;
	char rbuf[32], *p;

	assert(reason);

	/* Take a copy of reason and trim 6 from the end
	 * so our DHCPv6 state can match DHCP states */
	if (strlcpy(rbuf, reason, sizeof(rbuf)) >= sizeof(rbuf))
		return DHS_UNKNOWN;
	p = rbuf + (strlen(rbuf) - 1);
	if (*p == '6') {
		if (isdhcp6)
			*isdhcp6 = 1;
		*p = '\0';
	} else if (isdhcp6)
		*isdhcp6 = 0;

	for (i = 0; dhcpcd_states[i].val != DHS_UNKNOWN; i++) {
		if (strcmp(rbuf, dhcpcd_states[i].str) == 0)
			return dhcpcd_states[i].val;
	}
	return DHS_UNKNOWN;
}

static void
dhcpcd_reason_to_statetype(const char *reason,
    unsigned int *state, unsigned int *type)
{
	int isdhcp6;

	assert(state);
	assert(type);
	*state = dhcpcd_reason_to_state1(reason, &isdhcp6);
	switch (*state) {
	case DHS_UNKNOWN:
	case DHS_PREINIT:
	case DHS_CARRIER:
	case DHS_NOCARRIER:
	case DHS_NOCARRIER_ROAMING:
	case DHS_DEPARTED:
	case DHS_STOPPED:
		*type = DHT_LINK;
		return;
	case DHS_ROUTERADVERT:
		*type =  DHT_RA;
		return;
	case DHS_IPV4LL:
		*type = DHT_IPV4LL;
		return;
	case DHS_STATIC:
		if (isdhcp6) {
			*type = DHT_IPV6;
			return;
		}
	}

	if (isdhcp6)
		*type = DHT_DHCP6;
	else
		*type = DHT_IPV4;
}

static DHCPCD_IF *
dhcpcd_new_if(DHCPCD_CONNECTION *con, char *data, size_t len)
{
	const char *ifname, *ifclass, *reason, *order, *flags;
	unsigned int state, type;
	char *orderdup, *o, *p;
	DHCPCD_IF *e, *i, *l, *n, *nl;
	bool addedi;

#if 0
	char *dp = data, *de = data + len;
	while (dp < de) {
		printf ("XX: %s\n", dp);
		dp += strlen(dp) + 1;
	}
#endif

	ifname = get_value(data, len, "interface");
	if (ifname == NULL || *ifname == '\0') {
		errno = ESRCH;
		return NULL;
	}

	reason = get_value(data, len, "reason");
	if (reason == NULL || *reason == '\0') {
		errno = ESRCH;
		return NULL;
	}
	dhcpcd_reason_to_statetype(reason, &state, &type);

	ifclass = get_value(data, len, "ifclass");
	/* Skip pseudo interfaces */
	if (ifclass && *ifclass != '\0') {
		errno = ENOTSUP;
		return NULL;
	}
	if (state == DHS_RECONFIGURE || state == DHS_INFORM) {
		errno = ENOTSUP;
		return NULL;
	}

	order = get_value(data, len, "interface_order");
	if (order == NULL || *order == '\0') {
		errno = ESRCH;
		return NULL;
	}

	i = NULL;
       /* Remove all instances on carrier drop */
        if (state == DHS_NOCARRIER ||
	    state == DHS_DEPARTED || state == DHS_STOPPED)
        {
                l = NULL;
                for (e = con->interfaces; e; e = n) {
                        n = e->next;
                        if (strcmp(e->ifname, ifname) == 0) {
                                if (e->type == type)
                                        l = i = e;
                                else {
                                        if (l)
                                                l->next = e->next;
                                        else
                                                con->interfaces = e->next;
                                        free(e);
                                }
                        } else
                                l = e;
                }
        } else if (type != DHT_LINK) {
		/* If link is down, ignore it */
		e = dhcpcd_get_if(con, ifname, DHT_LINK);
		if (e && !e->up)
			return NULL;
	}

	orderdup = strdup(order);
	if (orderdup == NULL)
		return NULL;

	/* Find our pointer */
        if (i == NULL) {
                l = NULL;
                for (e = con->interfaces; e; e = e->next) {
                        if (e->type == type && strcmp(e->ifname, ifname) == 0) {
                                i = e;
                                break;
                        }
                        l = e;
                }
        }
	if (i == NULL) {
		i = malloc(sizeof(*i));
		if (i == NULL) {
			free(orderdup);
			return NULL;
		}
		if (l)
			l->next = i;
		else
			con->interfaces = i;
		i->next = NULL;
		i->last_message = NULL;
	} else
		free(i->data);

	/* Now fill out our interface structure */
	i->con = con;
	i->data = data;
	i->data_len = len;
	i->ifname = ifname;
	i->type = type;
	i->state = state;
	i->reason = reason;
	flags = dhcpcd_get_value(i, "ifflags");
	if (flags)
		i->ifflags = (unsigned int)strtoul(flags, NULL, 0);
	else
		i->ifflags = 0;
	if (state == DHS_CARRIER || state == DHS_DELEGATED)
		i->up = true;
	else
		i->up = strtobool(dhcpcd_get_value(i, "if_up"));
	i->wireless = strtobool(dhcpcd_get_value(i, "ifwireless"));
	i->ssid = dhcpcd_get_value(i, "ifssid");
	i->freq = 0; /* wpa_supplicant will set this when opened */
	if (i->ssid == NULL && i->wireless)
		i->ssid = dhcpcd_get_value(i, i->up ? "new_ssid" : "old_ssid");

	/* Work out if we're waiting for any other addresses */
	if (dhcpcd_get_value(i, "af_waiting") == NULL)
		con->af_waiting = false;
	else
		con->af_waiting = true;

       /* Sort! */
	n = nl = NULL;
	p = orderdup;
	addedi = false;
        while ((o = strsep(&p, " ")) != NULL) {
                for (type = 0; type < DHT_MAX; type++) {
                        l = NULL;
                        for (e = con->interfaces; e; e = e->next) {
                                if (e->type == type && strcmp(e->ifname, o) == 0)
                                        break;
                                l = e;
                        }
                        if (e == NULL)
                                continue;
			if (i == e)
				addedi = true;
                        if (l)
                                l->next = e->next;
                        else
                                con->interfaces = e->next;
                        e->next = NULL;
                        if (nl == NULL)
                                n = nl = e;
                        else {
                                nl->next = e;
                                nl = e;
                        }
                }
        }
	free(orderdup);
        /* Free any stragglers */
        while (con->interfaces) {
                e = con->interfaces->next;
		free(con->interfaces->data);
		free(con->interfaces->last_message);
                free(con->interfaces);
                con->interfaces = e;
        }
        con->interfaces = n;

	return addedi ? i : NULL;
}

static DHCPCD_IF *
dhcpcd_read_if(DHCPCD_CONNECTION *con, int fd)
{
	char *rbuf, *rbufp;
	size_t len;
	ssize_t bytes;
	DHCPCD_IF *i;

	bytes = read(fd, &len, sizeof(len));
	if (bytes == 0 || bytes == -1) {
		dhcpcd_close(con);
		return NULL;
	}
	if (bytes != sizeof(len)) {
		errno = EINVAL;
		return NULL;
	}
	if (len >= SSIZE_MAX) {
		/* Even this is probably too big! */
		errno = ENOBUFS;
		return NULL;
	}
	rbuf = malloc(len + 1);
	if (rbuf == NULL)
		return NULL;
	rbufp = rbuf;
again:
	bytes = read(fd, rbufp, len);
	if (bytes == 0 || bytes == -1) {
		free(rbuf);
		dhcpcd_close(con);
		return NULL;
	}
	if ((size_t)bytes < len) {
		rbufp += bytes;
		len -= (size_t)bytes;
		goto again;
	}
	if ((size_t)bytes != len) {
		free(rbuf);
		errno = EINVAL;
		return NULL;
	}
	rbufp[bytes] = '\0';

	i = dhcpcd_new_if(con, rbuf, (size_t)((rbufp - rbuf) + bytes));
	if (i == NULL)
		free(rbuf);
	return i;
}

static void
dhcpcd_dispatchif(DHCPCD_IF *i)
{

	assert(i);
	if (i->con->if_cb)
		i->con->if_cb(i, i->con->if_context);
	dhcpcd_wpa_if_event(i);
}

void
dhcpcd_dispatch(DHCPCD_CONNECTION *con)
{
	DHCPCD_IF *i;

	assert(con);
	i = dhcpcd_read_if(con, con->listen_fd);

	if (i)
		dhcpcd_dispatchif(i);

	/* Have to call update_status last as it could
	 * cause the interface to be destroyed. */
	update_status(con, DHC_UNKNOWN);
}

DHCPCD_CONNECTION *
dhcpcd_new(void)
{
	DHCPCD_CONNECTION *con;

	con = calloc(1, sizeof(*con));
	con->command_fd = con->listen_fd = -1;
	con->open = false;
	con->progname = "libdhcpcd";
	con->af_waiting = false;
	return con;
}

void
dhcpcd_set_progname(DHCPCD_CONNECTION *con, const char *progname)
{

	assert(con);
	con->progname = progname;
}

const char *
dhcpcd_get_progname(const DHCPCD_CONNECTION *con)
{

	assert(con);
	return con->progname;
}

#ifndef HAVE_STRVERSCMP
/* Good enough for our needs */
static int
strverscmp(const char *s1, const char *s2)
{
	int s1maj, s1min, s1part;
	int s2maj, s2min, s2part;
	int r;

	s1min = s1part = 0;
	if (sscanf(s1, "%d.%d.%d", &s1maj, &s1min, &s1part) < 1)
		return -1;
	s2min = s2part = 0;
	if (sscanf(s2, "%d.%d.%d", &s2maj, &s2min, &s2part) < 1)
		return -1;
	r = s1maj - s2maj;
	if (r != 0)
		return r;
	r = s1min - s2min;
	if (r != 0)
		return r;
	return s1part - s2part;
}
#endif

int
dhcpcd_open(DHCPCD_CONNECTION *con)
{
	bool privileged = false, read_error = false;
	const char *path;
	ssize_t bytes;
	size_t nifs, n;
	int fd, flags;

	assert(con);
	if (con->open) {
		if (con->listen_fd != -1)
			return con->listen_fd;
		errno = EISCONN;
		return -1;
	}

	/* We need to block the command fd */
	path = DHCPCD_SOCKET;
	fd = dhcpcd_connect(path);
	if (fd == -1) {
		if (errno == EACCES || errno == EPERM) {
			path = DHCPCD_UNPRIV_SOCKET;
			fd = dhcpcd_connect(path);
		} else if (errno == ENOENT) {
			path = DHCPCD_OSOCKET;
			fd = dhcpcd_connect(path);
			if (fd == -1) {
				if (errno == EACCES || EPERM) {
					path = DHCPCD_UNPRIV_OSOCKET;
					fd = dhcpcd_connect(path);
				}
			} else
				privileged = true;
		}
	} else
		privileged = true;
	if (fd == -1)
		goto err_exit;
	con->command_fd = fd;
	con->error = 0;

	con->terminate_commands = false;
	if (dhcpcd_ctrl_command(con, "--version", &con->version) <= 0)
		goto err_exit;
	con->terminate_commands =
	    strverscmp(con->version, "6.4.1") >= 0 ? true : false;
	read_error =
	    strverscmp(con->version, "10.5.0") >= 0 ? true : false;

	if (dhcpcd_ctrl_command(con, "--getconfigfile", &con->cffile) <= 0)
		goto err_exit;

	con->open = true;
	con->privileged = privileged;

	/* Newer dhcpcd versions only have the one socket, so
	 * work out if we are a privileged user. */
	if (privileged && read_error) {
		char *priv = NULL;

		if (dhcpcd_ctrl_command(con, "--isprivileged", &priv) <= 0)
			goto err_exit;
		con->privileged = strcmp(priv, "true") == 0 ? true : false;
		free(priv);
	}

	update_status(con, DHC_UNKNOWN);

	con->listen_fd = dhcpcd_connect(path);
	if (con->listen_fd == -1)
		goto err_exit;

	/* We are now reading errors */
	con->read_error = read_error;
	bytes = dhcpcd_command_fd(con, con->listen_fd, false, "--listen", NULL);
	if (bytes == -1)
		goto out;
	flags = fcntl(con->listen_fd, F_GETFD, 0);
	if (flags == -1)
		goto err_exit;
	flags |= O_NONBLOCK;
	if (fcntl(con->listen_fd, F_SETFD, flags) == -1)
		goto err_exit;

	bytes = dhcpcd_command_fd(con, con->command_fd, false, "--getinterfaces", NULL);
	if (bytes == -1)
		goto out;

	bytes = read(con->command_fd, &nifs, sizeof(nifs));
	if (bytes != sizeof(nifs))
		goto err_exit;

	/* We don't dispatch each interface here as that
	 * causes too much notification spam when the GUI starts */
	for (n = 0; n < nifs; n++) {
		/* Some interface states we do not create an interface for
		 * such as DHS_INFORM. */
		dhcpcd_read_if(con, con->command_fd);
	}

out:
	update_status(con, con->error == 0 ? DHC_UNKNOWN: DHC_OPENED);

	return con->listen_fd;

err_exit:
	dhcpcd_close(con);
	return -1;
}

int
dhcpcd_get_fd(DHCPCD_CONNECTION *con)
{

	assert(con);
	return con->listen_fd;
}

bool
dhcpcd_privileged(DHCPCD_CONNECTION *con)
{

	assert(con);
	return con->privileged;
}

unsigned int
dhcpcd_status(DHCPCD_CONNECTION *con, const char **status)
{

	assert(con);
	if (status)
		*status = dhcpcd_cstates[con->status];
	return con->status;
}

bool
dhcpcd_af_waiting(const DHCPCD_CONNECTION *con)
{

	assert(con);
	return con->af_waiting;
}

const char *
dhcpcd_version(DHCPCD_CONNECTION *con)
{

	assert(con);
	return con->version;
}

const char *
dhcpcd_cffile(DHCPCD_CONNECTION *con)
{

	assert(con);
	return con->cffile;
}

int
dhcpcd_error(DHCPCD_CONNECTION *con)
{

	assert(con);
	return con->error;
}

void
dhcpcd_set_if_callback(DHCPCD_CONNECTION *con,
    void (*cb)(DHCPCD_IF *, void *), void *ctx)
{

	assert(con);
	con->if_cb = cb;
	con->if_context = ctx;
}

void
dhcpcd_set_status_callback(DHCPCD_CONNECTION *con,
    void (*cb)(DHCPCD_CONNECTION *, unsigned int, const char *, void *),
    void *ctx)
{

	assert(con);
	con->status_cb = cb;
	con->status_context = ctx;
}

void
dhcpcd_close(DHCPCD_CONNECTION *con)
{
	DHCPCD_IF *nif;
	DHCPCD_WPA *nwpa;
	DHCPCD_WI_HIST *nh;

	assert(con);

	if (con->open) {
		if (con->command_fd != -1)
			shutdown(con->command_fd, SHUT_RDWR);
		if (con->listen_fd != -1)
			shutdown(con->listen_fd, SHUT_RDWR);
		con->open = false;
	}

	/* Shut down WPA listeners as they aren't much good without dhcpcd.
	 * They'll be restarted anyway when dhcpcd comes back up. */
	while (con->wpa) {
		nwpa = con->wpa->next;
		dhcpcd_wpa_close(con->wpa);
		free(con->wpa);
		con->wpa = nwpa;
	}
	while (con->wi_history) {
		nh = con->wi_history->next;
		free(con->wi_history);
		con->wi_history = nh;
	}
	while (con->interfaces) {
		nif = con->interfaces->next;
		free(con->interfaces->data);
		free(con->interfaces->last_message);
		free(con->interfaces);
		con->interfaces = nif;
	}

	update_status(con, DHC_DOWN);

	if (con->command_fd != -1) {
		close(con->command_fd);
		con->command_fd = -1;
	}
	if (con->listen_fd != -1) {
		close(con->listen_fd);
		con->listen_fd = -1;
	}

	if (con->cffile) {
		free(con->cffile);
		con->cffile = NULL;
	}
	if (con->version) {
		free(con->version);
		con->version = NULL;
	}
	if (con->buf) {
		free(con->buf);
		con->buf = NULL;
		con->buflen = 0;
	}
}

void
dhcpcd_free(DHCPCD_CONNECTION *con)
{

	assert(con);
	free(con);
}

DHCPCD_CONNECTION *
dhcpcd_if_connection(DHCPCD_IF *i)
{

	assert(i);
	return i->con;
}

char *
dhcpcd_if_message(DHCPCD_IF *i, bool *new_msg)
{
	const char *ip, *iplen, *pfx;
	char *msg, *p;
	const char *reason = NULL;
	size_t len;
	bool showssid;

	assert(i);
	/* Don't report non SLAAC configurations */
	if (i->type == DHT_RA && i->up &&
	    dhcpcd_get_value(i, "nd1_addr1") == NULL &&
	    dhcpcd_get_value(i, "ra1_prefix") == NULL)
		return NULL;

	showssid = false;
	switch (i->state) {
	case DHS_EXPIRE:
		reason = _("Expired");
		break;
	case DHS_CARRIER:
		if (i->wireless) {
			showssid = true;
			reason = _("Associated with");
		} else {
			/* Don't report able in if we have addresses */
			const DHCPCD_IF *ci;

			for (ci = i->con->interfaces; ci; ci = ci->next) {
				if (ci != i &&
				    strcmp(i->ifname, ci->ifname) == 0 &&
				    ci->up)
					break;
			}
			if (ci)
				return NULL;
			reason = _("Link is up, configuring");
		}
		break;
	case DHS_NOCARRIER:
		if (i->wireless) {
			if (i->ssid) {
				reason = _("Disassociated from");
				showssid = true;
			} else
				reason = _("Not associated");
		} else
			reason = _("Link is down");
		break;
	case DHS_NOCARRIER_ROAMING:
		reason = _("Link is down, roaming");
		break;
	case DHS_DEPARTED:
		reason = _("Departed");
		break;
	case DHS_UNKNOWN:
		reason = _("Unknown link state");
		break;
	case DHS_FAIL:
		reason = _("Automatic configuration not possible");
		break;
	case DHS_3RDPARTY:
		reason = _("Waiting for 3rd Party configuration");
		break;
	}

	if (reason == NULL) {
		if (i->up) {
			if (i->state == DHS_DELEGATED)
				reason = _("Delegated");
			else
				reason = _("Configured");
		} else if (i->type == DHT_RA)
			reason = "Expired RA";
		else if (i->type == DHT_IPV4LL)
			reason = "Expired IPv4LL";
		else
			reason = i->reason;
	}

	pfx = i->up ? "new_" : "old_";
	if ((ip = dhcpcd_get_prefix_value(i, pfx, "ip_address")))
		iplen = dhcpcd_get_prefix_value(i, pfx, "subnet_cidr");
	else if ((ip = dhcpcd_get_value(i, "nd1_addr1")))
		iplen = NULL;
	else if ((ip = dhcpcd_get_value(i, "ra1_addr")))
		iplen = NULL;
	else if ((ip = dhcpcd_get_value(i, "ra1_prefix")))
		iplen = NULL;
	else if ((ip = dhcpcd_get_prefix_value(i, pfx,
	    "dhcp6_ia_na1_ia_addr1")))
		iplen = "128";
	else if ((ip = dhcpcd_get_prefix_value(i, pfx,
	    "delegated_dhcp6_prefix")))
		iplen = NULL;
	else if ((ip = dhcpcd_get_prefix_value(i, pfx, "ip6_address")))
		iplen = NULL;
	else {
		ip = NULL;
		iplen = NULL;
	}

	len = strlen(i->ifname) + strlen(reason) + 3;
	if (showssid && i->ssid)
		len += strlen(i->ssid) + 1;
	if (ip)
		len += strlen(ip) + 1;
	if (iplen)
		len += strlen(iplen) + 1;
	msg = p = malloc(len);
	if (msg == NULL)
		return NULL;
	p += snprintf(msg, len, "%s: %s", i->ifname, reason);
	if (showssid)
		p += snprintf(p, len - (size_t)(p - msg), " %s", i->ssid);
	if (iplen)
		snprintf(p, len - (size_t)(p - msg), " %s/%s", ip, iplen);
	else if (ip)
		snprintf(p, len - (size_t)(p - msg), " %s", ip);

	if (new_msg) {
		if (i->last_message == NULL || strcmp(i->last_message, msg))
			*new_msg = true;
		else
			*new_msg = false;
	}
	free(i->last_message);
	i->last_message = strdup(msg);

	return msg;
}
