/*
 * Embedded Linux library
 * Copyright (C) 2020  Intel Corporation
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#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 <errno.h>

#include "strv.h"
#include "private.h"
#include "useful.h"
#include "dhcp6-private.h"
#include "dhcp6.h"
#include "net-private.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_strfreev(lease->domain_list);

	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 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 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 DHCP6_OPTION_STATUS_CODE:
			if (l < 2)
				return -EBADMSG;

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

			break;
		case DHCP6_OPTION_IA_ADDR:
			if (tag != DHCP6_OPTION_IA_NA)
				return -EBADMSG;

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

			have_info = true;
			break;
		case DHCP6_OPTION_IA_PREFIX:
			if (tag != 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 DHCP6_OPTION_SERVER_ID:
			lease->server_id = l_memdup(v, l);
			lease->server_id_len = l;
			break;
		case DHCP6_OPTION_PREFERENCE:
			if (l != 1)
				goto error;

			lease->preference = l_get_u8(v);
			break;
		case 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 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;
		case DHCP6_OPTION_RAPID_COMMIT:
			if (l != 0)
				goto error;

			lease->rapid_commit = true;
			break;
		case L_DHCP6_OPTION_DOMAIN_LIST:
			lease->domain_list = net_domain_list_parse(v, l, false);
			if (!lease->domain_list)
				goto error;

			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);
}

LIB_EXPORT 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);
}

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

	return l_strv_copy(lease->domain_list);
}

LIB_EXPORT uint8_t l_dhcp6_lease_get_prefix_length(
					const struct l_dhcp6_lease *lease)
{
	if (unlikely(!lease))
		return 0;

	if (lease->have_na)
		return 128;

	if (lease->have_pd)
		return lease->ia_pd.info.prefix_len;

	return 0;
}

#define PICK_IA() \
	const struct dhcp6_ia *ia;	\
					\
	if (lease->have_na)		\
		ia = &lease->ia_na;	\
	else if (lease->have_pd)	\
		ia = &lease->ia_pd;	\
	else				\
		return 0		\

uint32_t _dhcp6_lease_get_t1(const struct l_dhcp6_lease *lease)
{
	PICK_IA();

	if (ia->t1)
		return ia->t1;

	if (ia->info.valid_lifetime == 0xffffffffu)
		return ia->info.valid_lifetime;

	return ia->info.valid_lifetime / 2;
}

uint32_t _dhcp6_lease_get_t2(const struct l_dhcp6_lease *lease)
{
	PICK_IA();

	if (ia->t2)
		return ia->t2;

	if (ia->info.valid_lifetime == 0xffffffffu)
		return ia->info.valid_lifetime;

	return ia->info.valid_lifetime /  10 * 8;
}

LIB_EXPORT uint32_t l_dhcp6_lease_get_valid_lifetime(
					const struct l_dhcp6_lease *lease)
{
	if (unlikely(!lease))
		return 0;

	{
		PICK_IA();
		return ia->info.valid_lifetime;
	}
}

LIB_EXPORT uint32_t l_dhcp6_lease_get_preferred_lifetime(
					const struct l_dhcp6_lease *lease)
{
	if (unlikely(!lease))
		return 0;

	{
		PICK_IA();
		return ia->info.preferred_lifetime;
	}
}

/* Get the reception timestamp, i.e. when lifetimes are counted from */
LIB_EXPORT uint64_t l_dhcp6_lease_get_start_time(
					const struct l_dhcp6_lease *lease)
{
	if (unlikely(!lease))
		return 0;

	return lease->start_time;
}
