/*
 * Copyright (C) 2024 Tobias Brunner
 * Copyright (C) 2008 Martin Willi
 *
 * Copyright (C) secunet Security Networks AG
 *
 * 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.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * 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.
 */

#define _GNU_SOURCE
#include <string.h>
#include <arpa/inet.h>

#include "ha_message.h"

#include <daemon.h>

#define ALLOCATION_BLOCK 64

typedef struct private_ha_message_t private_ha_message_t;

/**
 * Private data of an ha_message_t object.
 */
struct private_ha_message_t {

	/**
	 * Public ha_message_t interface.
	 */
	ha_message_t public;

	/**
	 * Allocated size of buf
	 */
	size_t allocated;

	/**
	 * Buffer containing encoded data
	 */
	chunk_t buf;
};

ENUM(ha_message_type_names, HA_IKE_ADD, HA_IKE_IV,
	"IKE_ADD",
	"IKE_UPDATE",
	"IKE_MID_INITIATOR",
	"IKE_MID_RESPONDER",
	"IKE_DELETE",
	"CHILD_ADD",
	"CHILD_DELETE",
	"SEGMENT_DROP",
	"SEGMENT_TAKE",
	"STATUS",
	"RESYNC",
	"IKE_IV",
);

typedef struct ike_sa_id_encoding_t ike_sa_id_encoding_t;

/**
 * Encoding if an ike_sa_id_t
 */
struct ike_sa_id_encoding_t {
	uint8_t ike_version;
	uint64_t initiator_spi;
	uint64_t responder_spi;
	uint8_t initiator;
} __attribute__((packed));

typedef struct identification_encoding_t identification_encoding_t;

/**
 * Encoding of a identification_t
 */
struct identification_encoding_t {
	uint8_t type;
	uint8_t len;
	char encoding[];
} __attribute__((packed));

typedef struct host_encoding_t host_encoding_t;

/**
 * encoding of a host_t
 */
struct host_encoding_t {
	uint16_t port;
	uint8_t family;
	char encoding[];
} __attribute__((packed));

typedef struct ts_encoding_t ts_encoding_t;

/**
 * encoding of a traffic_selector_t
 */
struct ts_encoding_t {
	uint8_t type;
	uint8_t protocol;
	uint16_t from_port;
	uint16_t to_port;
	uint8_t dynamic;
	char encoding[];
} __attribute__((packed));

METHOD(ha_message_t, get_type, ha_message_type_t,
	private_ha_message_t *this)
{
	return this->buf.ptr[1];
}

/**
 * check for space in buffer, increase if necessary
 */
static void check_buf(private_ha_message_t *this, size_t len)
{
	int increased = 0;

	while (this->buf.len + len > this->allocated)
	{	/* double size */
		this->allocated += ALLOCATION_BLOCK;
		increased++;
	}
	if (increased)
	{
		this->buf.ptr = realloc(this->buf.ptr, this->allocated);
	}
}

METHOD(ha_message_t, add_attribute, void,
	private_ha_message_t *this, ha_message_attribute_t attribute, ...)
{
	size_t len;
	va_list args;

	check_buf(this, sizeof(uint8_t));
	this->buf.ptr[this->buf.len] = attribute;
	this->buf.len += sizeof(uint8_t);

	va_start(args, attribute);
	switch (attribute)
	{
		/* ike_sa_id_t* */
		case HA_IKE_ID:
		case HA_IKE_REKEY_ID:
		{
			ike_sa_id_encoding_t *enc;
			ike_sa_id_t *id;

			id = va_arg(args, ike_sa_id_t*);
			check_buf(this, sizeof(ike_sa_id_encoding_t));
			enc = (ike_sa_id_encoding_t*)(this->buf.ptr + this->buf.len);
			this->buf.len += sizeof(ike_sa_id_encoding_t);
			enc->initiator = id->is_initiator(id);
			enc->ike_version = id->get_ike_version(id);
			enc->initiator_spi = id->get_initiator_spi(id);
			enc->responder_spi = id->get_responder_spi(id);
			break;
		}
		/* identification_t* */
		case HA_LOCAL_ID:
		case HA_REMOTE_ID:
		case HA_REMOTE_EAP_ID:
		{
			identification_encoding_t *enc;
			identification_t *id;
			chunk_t data;

			id = va_arg(args, identification_t*);
			data = id->get_encoding(id);
			check_buf(this, sizeof(identification_encoding_t) + data.len);
			enc = (identification_encoding_t*)(this->buf.ptr + this->buf.len);
			this->buf.len += sizeof(identification_encoding_t) + data.len;
			enc->type = id->get_type(id);
			enc->len = data.len;
			memcpy(enc->encoding, data.ptr, data.len);
			break;
		}
		/* host_t* */
		case HA_LOCAL_ADDR:
		case HA_REMOTE_ADDR:
		case HA_LOCAL_VIP:
		case HA_REMOTE_VIP:
		case HA_PEER_ADDR:
		{
			host_encoding_t *enc;
			host_t *host;
			chunk_t data;

			host = va_arg(args, host_t*);
			data = host->get_address(host);
			check_buf(this, sizeof(host_encoding_t) + data.len);
			enc = (host_encoding_t*)(this->buf.ptr + this->buf.len);
			this->buf.len += sizeof(host_encoding_t) + data.len;
			enc->family = host->get_family(host);
			enc->port = htons(host->get_port(host));
			memcpy(enc->encoding, data.ptr, data.len);
			break;
		}
		/* char* */
		case HA_CONFIG_NAME:
		{
			char *str;

			str = va_arg(args, char*);
			len = strlen(str) + 1;
			check_buf(this, len);
			memcpy(this->buf.ptr + this->buf.len, str, len);
			this->buf.len += len;
			break;
		}
		/* uint8_t */
		case HA_IKE_VERSION:
		case HA_INITIATOR:
		case HA_IPSEC_MODE:
		case HA_IPCOMP:
		{
			uint8_t val;

			val = va_arg(args, u_int);
			check_buf(this, sizeof(val));
			this->buf.ptr[this->buf.len] = val;
			this->buf.len += sizeof(val);
			break;
		}
		/* uint16_t */
		case HA_ALG_KE:
		case HA_ALG_PRF:
		case HA_ALG_OLD_PRF:
		case HA_ALG_ENCR:
		case HA_ALG_ENCR_LEN:
		case HA_ALG_INTEG:
		case HA_INBOUND_CPI:
		case HA_OUTBOUND_CPI:
		case HA_SEGMENT:
		case HA_ESN:
		case HA_AUTH_METHOD:
		{
			uint16_t val;

			val = va_arg(args, u_int);
			check_buf(this, sizeof(val));
			*(uint16_t*)(this->buf.ptr + this->buf.len) = htons(val);
			this->buf.len += sizeof(val);
			break;
		}
		/** uint32_t */
		case HA_CONDITIONS:
		case HA_EXTENSIONS:
		case HA_INBOUND_SPI:
		case HA_OUTBOUND_SPI:
		case HA_MID:
		{
			uint32_t val;

			val = va_arg(args, u_int);
			check_buf(this, sizeof(val));
			*(uint32_t*)(this->buf.ptr + this->buf.len) = htonl(val);
			this->buf.len += sizeof(val);
			break;
		}
		/** chunk_t */
		case HA_NONCE_I:
		case HA_NONCE_R:
		case HA_SECRET:
		case HA_ADD_SECRET:
		case HA_LOCAL_DH:
		case HA_REMOTE_DH:
		case HA_PSK:
		case HA_IV:
		case HA_OLD_SKD:
		case HA_ALG_ADD_KES:
		{
			chunk_t chunk;

			chunk = va_arg(args, chunk_t);
			check_buf(this, chunk.len + sizeof(uint16_t));
			*(uint16_t*)(this->buf.ptr + this->buf.len) = htons(chunk.len);
			memcpy(this->buf.ptr + this->buf.len + sizeof(uint16_t),
				   chunk.ptr, chunk.len);
			this->buf.len += chunk.len + sizeof(uint16_t);
			break;
		}
		/** traffic_selector_t */
		case HA_LOCAL_TS:
		case HA_REMOTE_TS:
		{
			ts_encoding_t *enc;
			traffic_selector_t *ts;
			chunk_t data;

			ts = va_arg(args, traffic_selector_t*);
			data = chunk_cata("cc", ts->get_from_address(ts),
							  ts->get_to_address(ts));
			check_buf(this, sizeof(ts_encoding_t) + data.len);
			enc = (ts_encoding_t*)(this->buf.ptr + this->buf.len);
			this->buf.len += sizeof(ts_encoding_t) + data.len;
			enc->type = ts->get_type(ts);
			enc->protocol = ts->get_protocol(ts);
			enc->from_port = htons(ts->get_from_port(ts));
			enc->to_port = htons(ts->get_to_port(ts));
			enc->dynamic = ts->is_dynamic(ts);
			memcpy(enc->encoding, data.ptr, data.len);
			break;
		}
		default:
		{
			DBG1(DBG_CFG, "unable to encode, attribute %d unknown", attribute);
			this->buf.len -= sizeof(uint8_t);
			break;
		}
	}
	va_end(args);
}

METHOD(ha_message_t, add_key_exchange_methods, void,
	private_ha_message_t *this, proposal_t *proposal)
{
	uint16_t algs[MAX_ADDITIONAL_KEY_EXCHANGES] = {};
	int i, count = 0;

	if (proposal->get_algorithm(proposal, KEY_EXCHANGE_METHOD, &algs[0], NULL))
	{
		add_attribute(this, HA_ALG_KE, algs[0]);
		algs[0] = 0;
	}
	for (i = 0; i < countof(algs); i++)
	{
		if (proposal->get_algorithm(proposal, i + ADDITIONAL_KEY_EXCHANGE_1,
									&algs[i], NULL))
		{
			count = i+1;
		}
	}
	if (count)
	{
		for (i = 0; i < count; i++)
		{
			algs[i] = htons(algs[i]);
		}
		add_attribute(this, HA_ALG_ADD_KES,
					  chunk_create((u_char*)algs, count * sizeof(uint16_t)));
	}
}

/**
 * Attribute enumerator implementation
 */
typedef struct {
	/** implements enumerator_t */
	enumerator_t public;
	/** position in message */
	chunk_t buf;
	/** cleanup handler of current element, if any */
	void (*cleanup)(void* data);
	/** data to pass to cleanup handler */
	void *cleanup_data;
} attribute_enumerator_t;

METHOD(enumerator_t, attribute_enumerate, bool,
	attribute_enumerator_t *this, va_list args)
{
	ha_message_attribute_t attr, *attr_out;
	ha_message_value_t *value;

	VA_ARGS_VGET(args, attr_out, value);

	if (this->cleanup)
	{
		this->cleanup(this->cleanup_data);
		this->cleanup = NULL;
	}
	if (this->buf.len < 1)
	{
		return FALSE;
	}
	attr = this->buf.ptr[0];
	this->buf = chunk_skip(this->buf, 1);
	switch (attr)
	{
		/* ike_sa_id_t* */
		case HA_IKE_ID:
		case HA_IKE_REKEY_ID:
		{
			ike_sa_id_encoding_t *enc;

			if (this->buf.len < sizeof(ike_sa_id_encoding_t))
			{
				return FALSE;
			}
			enc = (ike_sa_id_encoding_t*)(this->buf.ptr);
			value->ike_sa_id = ike_sa_id_create(enc->ike_version,
										enc->initiator_spi, enc->responder_spi,
										enc->initiator);
			*attr_out = attr;
			this->cleanup = (void*)value->ike_sa_id->destroy;
			this->cleanup_data = value->ike_sa_id;
			this->buf = chunk_skip(this->buf, sizeof(ike_sa_id_encoding_t));
			return TRUE;
		}
		/* identification_t* */
		case HA_LOCAL_ID:
		case HA_REMOTE_ID:
		case HA_REMOTE_EAP_ID:
		{
			identification_encoding_t *enc;

			enc = (identification_encoding_t*)(this->buf.ptr);
			if (this->buf.len < sizeof(identification_encoding_t) ||
				this->buf.len < sizeof(identification_encoding_t) + enc->len)
			{
				return FALSE;
			}
			value->id = identification_create_from_encoding(enc->type,
										chunk_create(enc->encoding, enc->len));
			*attr_out = attr;
			this->cleanup = (void*)value->id->destroy;
			this->cleanup_data = value->id;
			this->buf = chunk_skip(this->buf,
								sizeof(identification_encoding_t) + enc->len);
			return TRUE;
		}
		/* host_t* */
		case HA_LOCAL_ADDR:
		case HA_REMOTE_ADDR:
		case HA_LOCAL_VIP:
		case HA_REMOTE_VIP:
		case HA_PEER_ADDR:
		{
			host_encoding_t *enc;

			enc = (host_encoding_t*)(this->buf.ptr);
			if (this->buf.len < sizeof(host_encoding_t))
			{
				return FALSE;
			}
			value->host = host_create_from_chunk(enc->family,
									chunk_create(enc->encoding,
										this->buf.len - sizeof(host_encoding_t)),
									ntohs(enc->port));
			if (!value->host)
			{
				return FALSE;
			}
			*attr_out = attr;
			this->cleanup = (void*)value->host->destroy;
			this->cleanup_data = value->host;
			this->buf = chunk_skip(this->buf, sizeof(host_encoding_t) +
								   value->host->get_address(value->host).len);
			return TRUE;
		}
		/* char* */
		case HA_CONFIG_NAME:
		{
			size_t len;

			len = strnlen(this->buf.ptr, this->buf.len);
			if (len >= this->buf.len)
			{
				return FALSE;
			}
			value->str = this->buf.ptr;
			*attr_out = attr;
			this->buf = chunk_skip(this->buf, len + 1);
			return TRUE;
		}
		/* uint8_t */
		case HA_IKE_VERSION:
		case HA_INITIATOR:
		case HA_IPSEC_MODE:
		case HA_IPCOMP:
		{
			if (this->buf.len < sizeof(uint8_t))
			{
				return FALSE;
			}
			value->u8 = *(uint8_t*)this->buf.ptr;
			*attr_out = attr;
			this->buf = chunk_skip(this->buf, sizeof(uint8_t));
			return TRUE;
		}
		/** uint16_t */
		case HA_ALG_KE:
		case HA_ALG_PRF:
		case HA_ALG_OLD_PRF:
		case HA_ALG_ENCR:
		case HA_ALG_ENCR_LEN:
		case HA_ALG_INTEG:
		case HA_INBOUND_CPI:
		case HA_OUTBOUND_CPI:
		case HA_SEGMENT:
		case HA_ESN:
		case HA_AUTH_METHOD:
		{
			if (this->buf.len < sizeof(uint16_t))
			{
				return FALSE;
			}
			value->u16 = ntohs(*(uint16_t*)this->buf.ptr);
			*attr_out = attr;
			this->buf = chunk_skip(this->buf, sizeof(uint16_t));
			return TRUE;
		}
		/** uint32_t */
		case HA_CONDITIONS:
		case HA_EXTENSIONS:
		case HA_INBOUND_SPI:
		case HA_OUTBOUND_SPI:
		case HA_MID:
		{
			if (this->buf.len < sizeof(uint32_t))
			{
				return FALSE;
			}
			value->u32 = ntohl(*(uint32_t*)this->buf.ptr);
			*attr_out = attr;
			this->buf = chunk_skip(this->buf, sizeof(uint32_t));
			return TRUE;
		}
		/** chunk_t */
		case HA_NONCE_I:
		case HA_NONCE_R:
		case HA_SECRET:
		case HA_ADD_SECRET:
		case HA_LOCAL_DH:
		case HA_REMOTE_DH:
		case HA_PSK:
		case HA_IV:
		case HA_OLD_SKD:
		case HA_ALG_ADD_KES:
		{
			size_t len;

			if (this->buf.len < sizeof(uint16_t))
			{
				return FALSE;
			}
			len = ntohs(*(uint16_t*)this->buf.ptr);
			this->buf = chunk_skip(this->buf, sizeof(uint16_t));
			if (this->buf.len < len)
			{
				return FALSE;
			}
			value->chunk.len = len;
			value->chunk.ptr = this->buf.ptr;
			*attr_out = attr;
			this->buf = chunk_skip(this->buf, len);
			return TRUE;
		}
		case HA_LOCAL_TS:
		case HA_REMOTE_TS:
		{
			ts_encoding_t *enc;
			host_t *host;
			int addr_len;

			enc = (ts_encoding_t*)(this->buf.ptr);
			if (this->buf.len < sizeof(ts_encoding_t))
			{
				return FALSE;
			}
			switch (enc->type)
			{
				case TS_IPV4_ADDR_RANGE:
					addr_len = 4;
					if (this->buf.len < sizeof(ts_encoding_t) + 2 * addr_len)
					{
						return FALSE;
					}
					break;
				case TS_IPV6_ADDR_RANGE:
					addr_len = 16;
					if (this->buf.len < sizeof(ts_encoding_t) + 2 * addr_len)
					{
						return FALSE;
					}
					break;
				default:
					return FALSE;
			}
			if (enc->dynamic)
			{
				host = host_create_from_chunk(0,
									chunk_create(enc->encoding, addr_len), 0);
				if (!host)
				{
					return FALSE;
				}
				value->ts = traffic_selector_create_dynamic(enc->protocol,
									ntohs(enc->from_port), ntohs(enc->to_port));
				value->ts->set_address(value->ts, host);
				host->destroy(host);
			}
			else
			{
				value->ts = traffic_selector_create_from_bytes(enc->protocol,
								enc->type, chunk_create(enc->encoding, addr_len),
								ntohs(enc->from_port),
								chunk_create(enc->encoding + addr_len, addr_len),
								ntohs(enc->to_port));
				if (!value->ts)
				{
					return FALSE;
				}
			}
			*attr_out = attr;
			this->cleanup = (void*)value->ts->destroy;
			this->cleanup_data = value->ts;
			this->buf = chunk_skip(this->buf, sizeof(ts_encoding_t)
										+ addr_len * 2);
			return TRUE;
		}
		default:
		{
			return FALSE;
		}
	}
}

METHOD(enumerator_t, enum_destroy, void,
	attribute_enumerator_t *this)
{
	if (this->cleanup)
	{
		this->cleanup(this->cleanup_data);
	}
	free(this);
}

METHOD(ha_message_t, create_attribute_enumerator, enumerator_t*,
	private_ha_message_t *this)
{
	attribute_enumerator_t *e;

	INIT(e,
		.public = {
			.enumerate = enumerator_enumerate_default,
			.venumerate = _attribute_enumerate,
			.destroy = _enum_destroy,
		},
		.buf = chunk_skip(this->buf, 2),
	);

	return &e->public;
}

METHOD(ha_message_t, get_encoding, chunk_t,
	private_ha_message_t *this)
{
	return this->buf;
}

METHOD(ha_message_t, destroy, void,
	private_ha_message_t *this)
{
	chunk_clear(&this->buf);
	free(this);
}


static private_ha_message_t *ha_message_create_generic()
{
	private_ha_message_t *this;

	INIT(this,
		.public = {
			.get_type = _get_type,
			.add_attribute = _add_attribute,
			.add_key_exchange_methods = _add_key_exchange_methods,
			.create_attribute_enumerator = _create_attribute_enumerator,
			.get_encoding = _get_encoding,
			.destroy = _destroy,
		},
	);
	return this;
}

/**
 * See header
 */
ha_message_t *ha_message_create(ha_message_type_t type)
{
	private_ha_message_t *this = ha_message_create_generic();

	this->allocated = ALLOCATION_BLOCK;
	this->buf.ptr = malloc(this->allocated);
	this->buf.len = 2;
	this->buf.ptr[0] = HA_MESSAGE_VERSION;
	this->buf.ptr[1] = type;

	return &this->public;
}

/**
 * See header
 */
ha_message_t *ha_message_parse(chunk_t data)
{
	private_ha_message_t *this;

	if (data.len < 2)
	{
		DBG1(DBG_CFG, "HA message too short");
		return NULL;
	}
	if (data.ptr[0] != HA_MESSAGE_VERSION)
	{
		DBG1(DBG_CFG, "HA message has version %d, expected %d",
			 data.ptr[0], HA_MESSAGE_VERSION);
		return NULL;
	}

	this = ha_message_create_generic();
	this->buf = chunk_clone(data);
	this->allocated = this->buf.len;

	return &this->public;
}
