/*
 * Copyright (C) 2012 Martin Willi
 * Copyright (C) 2012 revosec 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.
 */

#include "pt_tls_server.h"

#include <sasl/sasl_mechanism.h>

#include <utils/debug.h>

typedef struct private_pt_tls_server_t private_pt_tls_server_t;

/**
 * Private data of an pt_tls_server_t object.
 */
struct private_pt_tls_server_t {

	/**
	 * Public pt_tls_server_t interface.
	 */
	pt_tls_server_t public;

	/**
	 * TLS protected socket
	 */
	tls_socket_t *tls;

	/**
	 * Client authentication requirements
	 */
	pt_tls_auth_t auth;

	enum {
		/* expecting version negotiation */
		PT_TLS_SERVER_VERSION,
		/* expecting an SASL exchange */
		PT_TLS_SERVER_AUTH,
		/* expecting TNCCS exchange */
		PT_TLS_SERVER_TNCCS,
		/* terminating state */
		PT_TLS_SERVER_END,
	} state;

	/**
	 * Message Identifier
	 */
	uint32_t identifier;

	/**
	 * TNCCS protocol handler, implemented as tls_t
	 */
	tls_t *tnccs;

};

/**
 * Negotiate PT-TLS version
 */
static bool negotiate_version(private_pt_tls_server_t *this)
{
	bio_reader_t *reader;
	bio_writer_t *writer;
	uint32_t vendor, type, identifier;
	uint8_t reserved, vmin, vmax, vpref;
	bool res;

	reader = pt_tls_read(this->tls, &vendor, &type, &identifier);
	if (!reader)
	{
		return FALSE;
	}
	if (vendor != 0 || type != PT_TLS_VERSION_REQUEST ||
		!reader->read_uint8(reader, &reserved) ||
		!reader->read_uint8(reader, &vmin) ||
		!reader->read_uint8(reader, &vmax) ||
		!reader->read_uint8(reader, &vpref))
	{
		DBG1(DBG_TNC, "PT-TLS version negotiation failed");
		reader->destroy(reader);
		return FALSE;
	}
	reader->destroy(reader);

	if (vmin > PT_TLS_VERSION || vmax < PT_TLS_VERSION)
	{
		/* TODO: send error */
		return FALSE;
	}

	writer = bio_writer_create(4);
	writer->write_uint24(writer, 0);
	writer->write_uint8(writer, PT_TLS_VERSION);
	res = pt_tls_write(this->tls, PT_TLS_VERSION_RESPONSE,
					   this->identifier++, writer->get_buf(writer));
	writer->destroy(writer);
	return res;
}

/**
 * Process SASL data, send result
 */
static status_t process_sasl(private_pt_tls_server_t *this,
							 sasl_mechanism_t *sasl, chunk_t data)
{
	bio_writer_t *writer;
	identification_t *client;
	tnccs_t *tnccs;
	bool res;

	switch (sasl->process(sasl, data))
	{
		case NEED_MORE:
			return NEED_MORE;
		case SUCCESS:
			DBG1(DBG_TNC, "SASL %s authentication successful",
				 sasl->get_name(sasl));
			client = sasl->get_client(sasl);
			if (client)
			{
				DBG1(DBG_TNC, "SASL client identity is '%Y'", client);
				this->tnccs->set_peer_id(this->tnccs, client);
				if (streq(sasl->get_name(sasl), "PLAIN"))
				{
					tnccs = (tnccs_t*)this->tnccs;
					tnccs->set_auth_type(tnccs, TNC_AUTH_PASSWORD);
				}
			}
			writer = bio_writer_create(1);
			writer->write_uint8(writer, PT_TLS_SASL_RESULT_SUCCESS);
			res = pt_tls_write(this->tls, PT_TLS_SASL_RESULT,
							   this->identifier++, writer->get_buf(writer));
			writer->destroy(writer);
			return res ? SUCCESS : FAILED;
		case FAILED:
		default:
			DBG1(DBG_TNC, "SASL %s authentication failed",
				 sasl->get_name(sasl));
			writer = bio_writer_create(1);
			/* sending abort does not allow the client to retry */
			writer->write_uint8(writer, PT_TLS_SASL_RESULT_ABORT);
			pt_tls_write(this->tls, PT_TLS_SASL_RESULT,
						 this->identifier++, writer->get_buf(writer));
			return FAILED;
	}
}

/**
 * Read a SASL message and process it
 */
static status_t read_sasl(private_pt_tls_server_t *this,
						  sasl_mechanism_t *sasl)
{
	uint32_t vendor, type, identifier;
	bio_reader_t *reader;
	status_t status;
	chunk_t data;

	reader = pt_tls_read(this->tls, &vendor, &type, &identifier);
	if (!reader)
	{
		return FAILED;
	}
	if (vendor != 0 || type != PT_TLS_SASL_AUTH_DATA ||
		!reader->read_data(reader, reader->remaining(reader), &data))
	{
		reader->destroy(reader);
		return FAILED;
	}
	status = process_sasl(this, sasl, data);
	reader->destroy(reader);
	return status;
}

/**
 * Build and write SASL message, or result message
 */
static status_t write_sasl(private_pt_tls_server_t *this,
						   sasl_mechanism_t *sasl)
{
	bio_writer_t *writer;
	chunk_t chunk;
	bool res;

	switch (sasl->build(sasl, &chunk))
	{
		case NEED_MORE:
			res = pt_tls_write(this->tls, PT_TLS_SASL_AUTH_DATA,
							   this->identifier++, chunk);
			free(chunk.ptr);
			return res ? NEED_MORE : FAILED;
		case SUCCESS:
			DBG1(DBG_TNC, "SASL %s authentication successful",
				 sasl->get_name(sasl));
			writer = bio_writer_create(1 + chunk.len);
			writer->write_uint8(writer, PT_TLS_SASL_RESULT_SUCCESS);
			writer->write_data(writer, chunk);
			free(chunk.ptr);
			res = pt_tls_write(this->tls, PT_TLS_SASL_RESULT,
							   this->identifier++, writer->get_buf(writer));
			writer->destroy(writer);
			return res ? SUCCESS : FAILED;
		case FAILED:
		default:
			DBG1(DBG_TNC, "SASL %s authentication failed",
				 sasl->get_name(sasl));
			/* sending abort does not allow the client to retry */
			chunk = chunk_from_chars(PT_TLS_SASL_RESULT_ABORT);
			pt_tls_write(this->tls, PT_TLS_SASL_RESULT,
						 this->identifier++, chunk);
			return FAILED;
	}
}

/**
 * Send the list of supported SASL mechanisms
 */
static bool send_sasl_mechs(private_pt_tls_server_t *this)
{
	enumerator_t *enumerator;
	bio_writer_t *writer = NULL;
	char *name;
	bool res;

	enumerator = sasl_mechanism_create_enumerator(TRUE);
	while (enumerator->enumerate(enumerator, &name))
	{
		if (!writer)
		{
			writer = bio_writer_create(32);
		}
		DBG1(DBG_TNC, "offering SASL %s", name);
		writer->write_data8(writer, chunk_from_str(name));
	}
	enumerator->destroy(enumerator);

	if (!writer)
	{	/* no mechanisms available? */
		return FALSE;
	}
	res = pt_tls_write(this->tls, PT_TLS_SASL_MECHS,
					   this->identifier++, writer->get_buf(writer));
	writer->destroy(writer);
	return res;
}

/**
 * Read the selected SASL mechanism, and process piggybacked data
 */
static status_t read_sasl_mech_selection(private_pt_tls_server_t *this,
										 sasl_mechanism_t **out)
{
	uint32_t vendor, type, identifier;
	sasl_mechanism_t *sasl;
	bio_reader_t *reader;
	chunk_t chunk;
	uint8_t len;
	char buf[21];

	reader = pt_tls_read(this->tls, &vendor, &type, &identifier);
	if (!reader)
	{
		return FAILED;
	}
	if (vendor != 0 || type != PT_TLS_SASL_MECH_SELECTION ||
		!reader->read_uint8(reader, &len) ||
		!reader->read_data(reader, len & 0x1F, &chunk))
	{
		reader->destroy(reader);
		return FAILED;
	}
	snprintf(buf, sizeof(buf), "%.*s", (int)chunk.len, chunk.ptr);

	DBG1(DBG_TNC, "client starts SASL %s authentication", buf);

	sasl = sasl_mechanism_create(buf, NULL);
	if (!sasl)
	{
		reader->destroy(reader);
		return FAILED;
	}
	/* initial SASL data piggybacked? */
	if (reader->remaining(reader))
	{
		switch (process_sasl(this, sasl, reader->peek(reader)))
		{
			case NEED_MORE:
				break;
			case SUCCESS:
				reader->destroy(reader);
				*out = sasl;
				return SUCCESS;
			case FAILED:
			default:
				reader->destroy(reader);
				sasl->destroy(sasl);
				return FAILED;
		}
	}
	reader->destroy(reader);
	*out = sasl;
	return NEED_MORE;
}

/**
 * Do a single SASL exchange
 */
static bool do_sasl(private_pt_tls_server_t *this)
{
	sasl_mechanism_t *sasl;
	identification_t *client_id;
	tnccs_t *tnccs;
	status_t status;

	client_id = this->tls->get_peer_id(this->tls);
	tnccs = (tnccs_t*)this->tnccs;

	switch (this->auth)
	{
		case PT_TLS_AUTH_NONE:
			return TRUE;
		case PT_TLS_AUTH_TLS:
			if (client_id)
			{
				this->tnccs->set_peer_id(this->tnccs, client_id);
				tnccs->set_auth_type(tnccs, TNC_AUTH_X509_CERT);
				return TRUE;
			}
			DBG1(DBG_TNC, "requiring TLS certificate-based "
						  "client authentication");
			return FALSE;
		case PT_TLS_AUTH_SASL:
			break;
		case PT_TLS_AUTH_TLS_OR_SASL:
			if (client_id)
			{
				this->tnccs->set_peer_id(this->tnccs, client_id);
				tnccs->set_auth_type(tnccs, TNC_AUTH_X509_CERT);
				DBG1(DBG_TNC, "skipping SASL, client already authenticated by "
							  "TLS certificate");
				return TRUE;
			}
			break;
		case PT_TLS_AUTH_TLS_AND_SASL:
		default:
			if (!client_id)
			{
				DBG1(DBG_TNC, "requiring TLS certificate-based "
							  "client authentication");
				return FALSE;
			}
			break;
	}

	if (!send_sasl_mechs(this))
	{
		return FALSE;
	}
	status = read_sasl_mech_selection(this, &sasl);
	if (status == FAILED)
	{
		return FALSE;
	}
	while (status == NEED_MORE)
	{
		status = write_sasl(this, sasl);
		if (status == NEED_MORE)
		{
			status = read_sasl(this, sasl);
		}
	}
	sasl->destroy(sasl);
	return status == SUCCESS;
}

/**
 * Authenticated PT-TLS session with a single SASL method
 */
static bool authenticate(private_pt_tls_server_t *this)
{
	if (do_sasl(this))
	{
		/* complete SASL with empty mechanism list */
		return pt_tls_write(this->tls, PT_TLS_SASL_MECHS, this->identifier++,
							chunk_empty);
	}
	return FALSE;
}

/**
 * Perform assessment
 */
static status_t assess(private_pt_tls_server_t *this, tls_t *tnccs)
{
	size_t msglen;
	size_t buflen = PT_TLS_MAX_MESSAGE_LEN;
	char buf[buflen];
	bio_reader_t *reader;
	uint32_t vendor, type, identifier;
	chunk_t data;
	status_t status;

	reader = pt_tls_read(this->tls, &vendor, &type, &identifier);
	if (!reader)
	{
		return FAILED;
	}
	if (vendor == 0)
	{
		if (type == PT_TLS_ERROR)
		{
			DBG1(DBG_TNC, "received PT-TLS error");
			reader->destroy(reader);
			return FAILED;
		}
		if (type != PT_TLS_PB_TNC_BATCH)
		{
			DBG1(DBG_TNC, "unexpected PT-TLS message: %d", type);
			reader->destroy(reader);
			return FAILED;
		}
		data = reader->peek(reader);
		switch (tnccs->process(tnccs, data.ptr, data.len))
		{
			case SUCCESS:
				reader->destroy(reader);
				return tnccs->is_complete(tnccs) ? SUCCESS : FAILED;
			case FAILED:
			default:
				reader->destroy(reader);
				return FALSE;
			case NEED_MORE:
				break;
		}
	}
	else
	{
		DBG1(DBG_TNC, "ignoring vendor specific PT-TLS message");
	}
	reader->destroy(reader);

	status = tnccs->build(tnccs, buf, &buflen, &msglen);
	if (status == ALREADY_DONE)
	{
		data = chunk_create(buf, buflen);
		if (!pt_tls_write(this->tls, PT_TLS_PB_TNC_BATCH,
						  this->identifier++, data))
		{
			return FAILED;
		}
	}
	return status;
}

METHOD(pt_tls_server_t, handle, status_t,
	private_pt_tls_server_t *this)
{
	switch (this->state)
	{
		case PT_TLS_SERVER_VERSION:
			DBG1(DBG_TNC, "entering PT-TLS negotiation phase");
			if (!negotiate_version(this))
			{
				return FAILED;
			}
			DBG1(DBG_TNC, "negotiated PT-TLS version %d", PT_TLS_VERSION);
			this->state = PT_TLS_SERVER_AUTH;
			/* fall through to next state */
		case PT_TLS_SERVER_AUTH:
			DBG1(DBG_TNC, "doing SASL client authentication");
			if (!authenticate(this))
			{
				return FAILED;
			}
			this->state = PT_TLS_SERVER_TNCCS;
			DBG1(DBG_TNC, "entering PT-TLS data transport phase");
			break;
		case PT_TLS_SERVER_TNCCS:
			switch (assess(this, (tls_t*)this->tnccs))
			{
				case SUCCESS:
					this->state = PT_TLS_SERVER_END;
					return SUCCESS;
				case FAILED:
					return FAILED;
				default:
					break;
			}
			break;
		default:
			return FAILED;
	}
	return NEED_MORE;
}

METHOD(pt_tls_server_t, get_fd, int,
	private_pt_tls_server_t *this)
{
	return this->tls->get_fd(this->tls);
}

METHOD(pt_tls_server_t, destroy, void,
	private_pt_tls_server_t *this)
{
	this->tnccs->destroy(this->tnccs);
	this->tls->destroy(this->tls);
	free(this);
}

/**
 * See header
 */
pt_tls_server_t *pt_tls_server_create(identification_t *server, int fd,
									  pt_tls_auth_t auth, tnccs_t *tnccs)
{
	private_pt_tls_server_t *this;
	identification_t *client = NULL;
	tls_flag_t flags = 0;

	switch (auth)
	{
		case PT_TLS_AUTH_TLS_OR_SASL:
			flags |= TLS_FLAG_CLIENT_AUTH_OPTIONAL;
			/* fall-through */
		case PT_TLS_AUTH_TLS:
		case PT_TLS_AUTH_TLS_AND_SASL:
			client = identification_create_from_encoding(ID_ANY, chunk_empty);
			break;
		default:
			break;
	}

	INIT(this,
		.public = {
			.handle = _handle,
			.get_fd = _get_fd,
			.destroy = _destroy,
		},
		.state = PT_TLS_SERVER_VERSION,
		.tls = tls_socket_create(TRUE, server, client, fd, NULL, TLS_UNSPEC,
								 TLS_UNSPEC, flags),
		.tnccs = (tls_t*)tnccs,
		.auth = auth,
	);

	DESTROY_IF(client);

	if (!this->tls)
	{
		this->tnccs->destroy(this->tnccs);
		free(this);
		return NULL;
	}

	return &this->public;
}
