/*
 *
 *  Embedded Linux library
 *
 *  Copyright (C) 2020  Intel Corporation. All rights reserved.
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library 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
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <linux/types.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>

#include "ell/private.h"
#include "ell/dhcp6-private.h"
#include "ell/dhcp6.h"

static inline char *get_ip(const uint8_t a[static 16])
{
	struct in6_addr addr;
	char buf[INET6_ADDRSTRLEN];

	memcpy(addr.s6_addr, a, 16);
	return l_strdup(inet_ntop(AF_INET6, &addr, buf, sizeof(buf)));
}

struct l_dhcp6_lease *_dhcp6_lease_new(void)
{
	struct l_dhcp6_lease *ret = l_new(struct l_dhcp6_lease, 1);

	return ret;
}

void _dhcp6_lease_free(struct l_dhcp6_lease *lease)
{
	if (!lease)
		return;

	l_free(lease->server_id);
	l_free(lease->dns);

	l_free(lease);
}

static char **convert_ipv6_addresses(const void *a, uint16_t l)
{
	uint16_t i = 0;
	char **r;

	l >>= 4;
	r = l_new(char *, l + 1);

	while (i < l) {
		r[i++] = get_ip(a);
		a += 16;
	}

	return r;
}

static int parse_ia_address(const void *ia_addr, uint16_t ia_addr_len,
				struct dhcp6_address_info *out)
{
	struct dhcp6_option_iter iter;
	uint16_t t;
	uint16_t l;
	const void *v;
	uint32_t preferred_lifetime;
	uint32_t valid_lifetime;

	if (ia_addr_len < 24)
		return -EBADMSG;

	preferred_lifetime = l_get_be32(ia_addr + 16);
	valid_lifetime = l_get_be32(ia_addr + 20);

	/*
	 * TODO: The RFC allows valid_lifetime to be 0 in the case that the
	 * DHCP server might want to invalidate or take back an address.
	 * However, since the address space is so large, the use case for doing
	 * so is a bit unclear.  For now we do not treat this as a possibility
	 * that can happen and simply reject the option
	 */
	if (preferred_lifetime > valid_lifetime || !valid_lifetime)
		return -EINVAL;

	__dhcp6_option_iter_init(&iter, ia_addr + 24, ia_addr_len - 24);

	while (_dhcp6_option_iter_next(&iter, &t, &l, &v)) {
		uint16_t status;

		switch (t) {
		case L_DHCP6_OPTION_STATUS_CODE:
			if (l < 2)
				return -EBADMSG;

			status = l_get_be16(v);
			if (status != 0)
				return -EINVAL;

			break;
		}
	}

	memset(out, 0, sizeof(*out));
	memcpy(out->addr, ia_addr, sizeof(out->addr));
	out->preferred_lifetime = preferred_lifetime;
	out->valid_lifetime = valid_lifetime;

	return 0;
}

static int parse_ia_prefix(const void *ia_prefix, uint16_t ia_prefix_len,
				struct dhcp6_address_info *out)
{
	struct dhcp6_option_iter iter;
	uint16_t t;
	uint16_t l;
	const void *v;
	uint32_t preferred_lifetime;
	uint32_t valid_lifetime;

	if (ia_prefix_len < 25)
		return -EBADMSG;

	preferred_lifetime = l_get_be32(ia_prefix);
	valid_lifetime = l_get_be32(ia_prefix + 4);

	if (preferred_lifetime > valid_lifetime || !valid_lifetime)
		return -EINVAL;

	__dhcp6_option_iter_init(&iter, ia_prefix + 25, ia_prefix_len - 25);

	while (_dhcp6_option_iter_next(&iter, &t, &l, &v)) {
		uint16_t status;

		switch (t) {
		case L_DHCP6_OPTION_STATUS_CODE:
			if (l < 2)
				return -EBADMSG;

			status = l_get_be16(v);
			if (status != 0)
				return -EINVAL;

			break;
		}
	}

	memset(out, 0, sizeof(*out));
	out->prefix_len = l_get_u8(ia_prefix + 8);
	memcpy(out->addr, ia_prefix + 9, sizeof(out->addr));
	out->preferred_lifetime = preferred_lifetime;
	out->valid_lifetime = valid_lifetime;

	return 0;
}

static int parse_ia(const void *ia, uint16_t ia_len, uint16_t tag,
			const uint8_t expected_iaid[static 4],
			struct dhcp6_ia *out)
{
	struct dhcp6_option_iter iter;
	uint16_t t;
	uint16_t l;
	const void *v;
	uint32_t t1;
	uint32_t t2;
	struct dhcp6_address_info info;
	bool have_info = false;

	if (ia_len < 12)
		return -EBADMSG;

	if (memcmp(ia, expected_iaid, 4))
		return -EINVAL;

	t1 = l_get_be32(ia + 4);
	t2 = l_get_be32(ia + 8);

	/*
	 * RFC 8415, Section 21.4:
	 * "If a client receives an IA_NA with T1 greater than T2 and both T1
	 * and T2 are greater than 0, the client discards the IA_NA option and
	 * processes the remainder of the message as though the server had not
	 * included the invalid IA_NA option."
	 */
	if (t1 > t2 && t2)
		return -EINVAL;

	__dhcp6_option_iter_init(&iter, ia + 12, ia_len - 12);

	while (_dhcp6_option_iter_next(&iter, &t, &l, &v)) {
		uint16_t status;

		switch (t) {
		case L_DHCP6_OPTION_STATUS_CODE:
			if (l < 2)
				return -EBADMSG;

			status = l_get_be16(v);
			if (status != 0)
				return -EINVAL;

			break;
		case L_DHCP6_OPTION_IA_ADDR:
			if (tag != L_DHCP6_OPTION_IA_NA)
				return -EBADMSG;

			if (have_info || parse_ia_address(v, l, &info) < 0)
				continue;

			have_info = true;
			break;
		case L_DHCP6_OPTION_IA_PREFIX:
			if (tag != L_DHCP6_OPTION_IA_PD)
				return -EBADMSG;

			if (have_info || parse_ia_prefix(v, l, &info) < 0)
				continue;

			have_info = true;
			break;
		default:
			break;
		}
	}

	if (!have_info)
		return -EINVAL;

	memcpy(out->iaid, expected_iaid, 4);
	out->t1 = t1;
	out->t2 = t2;
	memcpy(&out->info, &info, sizeof(info));

	return 0;
}

struct l_dhcp6_lease *_dhcp6_lease_parse_options(
					struct dhcp6_option_iter *iter,
					const uint8_t expected_iaid[static 4])
{
	struct l_dhcp6_lease *lease = _dhcp6_lease_new();
	uint16_t t;
	uint16_t l;
	const void *v;

	while (_dhcp6_option_iter_next(iter, &t, &l, &v)) {
		switch (t) {
		case L_DHCP6_OPTION_SERVER_ID:
			lease->server_id = l_memdup(v, l);
			lease->server_id_len = l;
			break;
		case L_DHCP6_OPTION_PREFERENCE:
			if (l != 1)
				goto error;

			lease->preference = l_get_u8(v);
			break;
		case L_DHCP6_OPTION_IA_NA:
			if (lease->have_na ||
					parse_ia(v, l, t, expected_iaid,
							&lease->ia_na) < 0)
				continue;

			lease->have_na = true;
			break;
		case L_DHCP6_OPTION_IA_PD:
			if (lease->have_pd ||
					parse_ia(v, l, t, expected_iaid,
							&lease->ia_pd) < 0)
				continue;

			lease->have_pd = true;
			break;
		case L_DHCP6_OPTION_DNS_SERVERS:
			if (!l || l % sizeof(struct in6_addr))
				goto error;

			lease->dns = l_memdup(v, l);
			lease->dns_len = l;
			break;
		}
	}

	return lease;

error:
	_dhcp6_lease_free(lease);
	return NULL;
}

LIB_EXPORT char *l_dhcp6_lease_get_address(const struct l_dhcp6_lease *lease)
{
	if (unlikely(!lease))
		return NULL;

	if (!lease->have_na)
		return NULL;

	return get_ip(lease->ia_na.info.addr);
}

char **l_dhcp6_lease_get_dns(const struct l_dhcp6_lease *lease)
{
	if (unlikely(!lease))
		return NULL;

	if (!lease->dns)
		return NULL;

	return convert_ipv6_addresses(lease->dns, lease->dns_len);
}
