/*
 * Copyright (C) 2008 Martin Willi
 * Copyright (C) 2008 Philip Boetschi, Adrian Doerig
 *
 * 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 "peer_controller.h"

#include <library.h>
#include <utils/debug.h>
#include <asn1/asn1.h>
#include <asn1/oid.h>
#include <utils/identification.h>
#include <credentials/keys/public_key.h>

typedef struct private_peer_controller_t private_peer_controller_t;

/**
 * private data of the peer_controller
 */
struct private_peer_controller_t {

	/**
	 * public functions
	 */
	peer_controller_t public;

	/**
	 * active user session
	 */
	user_t *user;

	/**
	 * underlying database
	 */
	database_t *db;
};

/**
 * list the configured peer configs
 */
static void list(private_peer_controller_t *this, fast_request_t *request)
{
	enumerator_t *query;

	query = this->db->query(this->db,
			"SELECT id, alias, keyid FROM peer WHERE user = ? ORDER BY alias",
			DB_UINT, this->user->get_user(this->user),
			DB_UINT, DB_TEXT, DB_BLOB);

	if (query)
	{
		u_int id;
		char *alias;
		chunk_t keyid;
		identification_t *identifier;

		while (query->enumerate(query, &id, &alias, &keyid))
		{
			request->setf(request, "peers.%d.alias=%s", id, alias);
			identifier = identification_create_from_encoding(ID_KEY_ID, keyid);
			request->setf(request, "peers.%d.identifier=%Y", id, identifier);
			identifier->destroy(identifier);
		}
		query->destroy(query);
	}
	request->render(request, "templates/peer/list.cs");
}

/**
 * verify a peer alias
 */
static bool verify_alias(private_peer_controller_t *this, fast_request_t *request,
						 char *alias)
{
	if (!alias || *alias == '\0')
	{
		request->setf(request, "error=Alias is missing.");
		return FALSE;
	}
	while (*alias != '\0')
	{
		switch (*alias)
		{
			case 'a' ... 'z':
			case 'A' ... 'Z':
			case '0' ... '9':
			case '-':
			case '_':
			case '@':
			case '.':
				alias++;
				continue;
			default:
				request->setf(request, "error=Alias invalid, "
							  "valid characters: A-Z a-z 0-9 - _ @ .");
				return FALSE;
		}
	}
	return TRUE;
}

/**
 * parse and verify a public key
 */
static bool parse_public_key(private_peer_controller_t *this,
							 fast_request_t *request, char *public_key,
							 chunk_t *encoding, chunk_t *keyid)
{
	public_key_t *public;
	chunk_t blob, id;

	if (!public_key || *public_key == '\0')
	{
		request->setf(request, "error=Public key is missing.");
		return FALSE;
	}
	blob = chunk_clone(chunk_create(public_key, strlen(public_key)));
	public = lib->creds->create(lib->creds, CRED_PUBLIC_KEY, KEY_ANY,
								BUILD_BLOB_PEM, blob,
								BUILD_END);
	chunk_free(&blob);
	if (!public)
	{
		request->setf(request, "error=Parsing public key failed.");
		return FALSE;
	}
	/* TODO: use get_encoding() with an encoding type */
	if (!public->get_fingerprint(public, KEYID_PUBKEY_SHA1, &id) ||
		!public->get_encoding(public, PUBKEY_SPKI_ASN1_DER, encoding))
	{
		request->setf(request, "error=Encoding public key failed.");
		return FALSE;
	}
	*keyid = chunk_clone(id);
	public->destroy(public);
	return TRUE;
}

/**
 * register a new peer
 */
static void add(private_peer_controller_t *this, fast_request_t *request)
{
	char *alias = "", *public_key = "";

	if (request->get_query_data(request, "back"))
	{
		return request->redirect(request, "peer/list");
	}
	while (request->get_query_data(request, "add"))
	{
		chunk_t encoding, keyid;

		alias = request->get_query_data(request, "alias");
		public_key = request->get_query_data(request, "public_key");

		if (!verify_alias(this, request, alias))
		{
			break;
		}
		if (!parse_public_key(this, request, public_key, &encoding, &keyid))
		{
			break;
		}
		if (this->db->execute(this->db, NULL,
						  "INSERT INTO peer (user, alias, public_key, keyid) "
						  "VALUES (?, ?, ?, ?)",
						  DB_UINT, this->user->get_user(this->user),
						  DB_TEXT, alias, DB_BLOB, encoding,
						  DB_BLOB, keyid) <= 0)
		{
			request->setf(request, "error=Peer already exists.");
			free(keyid.ptr);
			free(encoding.ptr);
			break;
		}
		free(keyid.ptr);
		free(encoding.ptr);
		return request->redirect(request, "peer/list");
	}
	request->set(request, "alias", alias);
	request->set(request, "public_key", public_key);

	return request->render(request, "templates/peer/add.cs");
}

/**
 * pem encode a public key into an allocated string
 */
static char* pem_encode(chunk_t der)
{
	static const char *begin = "-----BEGIN PUBLIC KEY-----\n";
	static const char *end = "-----END PUBLIC KEY-----";
	size_t len;
	char *pem;
	chunk_t base64;
	int i = 0;

	base64 = chunk_to_base64(der, NULL);
	len = strlen(begin) + base64.len + base64.len/64 + strlen(end) + 2;
	pem = malloc(len + 1);

	strcpy(pem, begin);
	do
	{
		strncat(pem, base64.ptr + i, 64);
		strcat(pem, "\n");
		i += 64;
	}
	while (i < base64.len - 2);
	strcat(pem, end);

	free(base64.ptr);
	return pem;
}

/**
 * edit a peer
 */
static void edit(private_peer_controller_t *this, fast_request_t *request, int id)
{
	char *alias = "", *public_key = "", *pem;
	chunk_t encoding, keyid;

	if (request->get_query_data(request, "back"))
	{
		return request->redirect(request, "peer/list");
	}
	if (request->get_query_data(request, "delete"))
	{
		this->db->execute(this->db, NULL,
						  "DELETE FROM peer WHERE id = ? AND user = ?",
						  DB_INT, id, DB_UINT, this->user->get_user(this->user));
		return request->redirect(request, "peer/list");
	}
	if (request->get_query_data(request, "save"))
	{
		while (TRUE)
		{
			alias = request->get_query_data(request, "alias");
			public_key = request->get_query_data(request, "public_key");

			if (!verify_alias(this, request, alias))
			{
				break;
			}
			if (!parse_public_key(this, request, public_key, &encoding, &keyid))
			{
				break;
			}
			if (this->db->execute(this->db, NULL,
				  "UPDATE peer SET alias = ?, public_key = ?, keyid = ? "
				  "WHERE id = ? AND user = ?",
				  DB_TEXT, alias, DB_BLOB, encoding, DB_BLOB, keyid,
				  DB_INT, id, DB_UINT, this->user->get_user(this->user)) < 0)
			{
				request->setf(request, "error=Peer already exists.");
				free(keyid.ptr);
				free(encoding.ptr);
				break;
			}
			free(keyid.ptr);
			free(encoding.ptr);
			return request->redirect(request, "peer/list");
		}
	}
	else
	{
		enumerator_t *query = this->db->query(this->db,
				"SELECT alias, public_key FROM peer WHERE id = ? AND user = ?",
				DB_INT, id, DB_UINT, this->user->get_user(this->user),
				DB_TEXT, DB_BLOB);
		if (query && query->enumerate(query, &alias, &encoding))
		{
			alias = strdupa(alias);
			pem = pem_encode(encoding);
			public_key = strdupa(pem);
			free(pem);
		}
		else
		{
			return request->redirect(request, "peer/list");
		}
		DESTROY_IF(query);
	}
	request->set(request, "alias", alias);
	request->set(request, "public_key", public_key);
	return request->render(request, "templates/peer/edit.cs");
}

/**
 * delete a peer from the database
 */
static void delete(private_peer_controller_t *this, fast_request_t *request, int id)
{
	this->db->execute(this->db, NULL,
					  "DELETE FROM peer WHERE id = ? AND user = ?",
					  DB_INT, id, DB_UINT, this->user->get_user(this->user));
}

METHOD(fast_controller_t, get_name, char*,
	private_peer_controller_t *this)
{
	return "peer";
}

METHOD(fast_controller_t, handle, void,
	private_peer_controller_t *this, fast_request_t *request, char *action,
	char *idstr, char *p3, char *p4, char *p5)
{
	if (action)
	{
		int id = 0;
		if (idstr)
		{
			id = atoi(idstr);
		}

		if (streq(action, "list"))
		{
			return list(this, request);
		}
		else if (streq(action, "add"))
		{
			return add(this, request);
		}
		else if (streq(action, "edit") && id)
		{
			return edit(this, request, id);
		}
		else if (streq(action, "delete") && id)
		{
			delete(this, request, id);
		}
	}
	request->redirect(request, "peer/list");
}

METHOD(fast_controller_t, destroy, void,
	private_peer_controller_t *this)
{
	free(this);
}

/*
 * see header file
 */
fast_controller_t *peer_controller_create(user_t *user, database_t *db)
{
	private_peer_controller_t *this;

	INIT(this,
		.public = {
			.controller = {
				.get_name = _get_name,
				.handle = _handle,
				.destroy = _destroy,
			},
		},
		.user = user,
		.db = db,
	);

	return &this->public.controller;
}
