/*
 * Copyright (C) 2013 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.
 */

#include "osx_attr_handler.h"

#include <networking/host.h>
#include <utils/debug.h>

#include <SystemConfiguration/SCDynamicStore.h>

typedef struct private_osx_attr_handler_t private_osx_attr_handler_t;

/**
 * Private data of an osx_attr_handler_t object.
 */
struct private_osx_attr_handler_t {

	/**
	 * Public interface
	 */
	osx_attr_handler_t public;

	/**
	 * Backup of original DNS servers, before we mess with it
	 */
	CFMutableArrayRef original;

	/**
	 * Append DNS servers to existing entries, instead of replacing
	 */
	bool append;
};

/**
 * Create a path to the DNS configuration of the Primary IPv4 Service
 */
static CFStringRef create_dns_path(SCDynamicStoreRef store)
{
	CFStringRef service, path = NULL;
	CFDictionaryRef dict;

	/* get primary service */
	dict = SCDynamicStoreCopyValue(store, CFSTR("State:/Network/Global/IPv4"));
	if (dict)
	{
		service = CFDictionaryGetValue(dict, CFSTR("PrimaryService"));
		if (service)
		{
			path = CFStringCreateWithFormat(NULL, NULL,
								CFSTR("State:/Network/Service/%@/DNS"), service);
		}
		else
		{
			DBG1(DBG_CFG, "SystemConfiguration PrimaryService not known");
		}
		CFRelease(dict);
	}
	else
	{
		DBG1(DBG_CFG, "getting global IPv4 SystemConfiguration failed");
	}
	return path;
}

/**
 * Create a mutable dictionary from path, a new one if not found
 */
static CFMutableDictionaryRef get_dictionary(SCDynamicStoreRef store,
											 CFStringRef path)
{
	CFDictionaryRef dict;
	CFMutableDictionaryRef mut = NULL;

	dict = SCDynamicStoreCopyValue(store, path);
	if (dict)
	{
		if (CFGetTypeID(dict) == CFDictionaryGetTypeID())
		{
			mut = CFDictionaryCreateMutableCopy(NULL, 0, dict);
		}
		CFRelease(dict);
	}
	if (!mut)
	{
		mut = CFDictionaryCreateMutable(NULL, 0,
										&kCFTypeDictionaryKeyCallBacks,
										&kCFTypeDictionaryValueCallBacks);
	}
	return mut;
}

/**
 * Create a mutable array from dictionary path, a new one if not found
 */
static CFMutableArrayRef get_array_from_dict(CFDictionaryRef dict,
											 CFStringRef name)
{
	CFArrayRef arr;

	arr = CFDictionaryGetValue(dict, name);
	if (arr && CFGetTypeID(arr) == CFArrayGetTypeID())
	{
		return CFArrayCreateMutableCopy(NULL, 0, arr);
	}
	return CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
}

/**
 * Add/Remove a DNS server to the configuration
 */
static bool manage_dns(private_osx_attr_handler_t *this,
					   int family, chunk_t data, bool add)
{
	SCDynamicStoreRef store;
	CFStringRef path, dns;
	CFMutableArrayRef arr;
	CFMutableDictionaryRef dict;
	CFIndex i;
	host_t *server;
	char buf[64];
	bool success = FALSE;

	server = host_create_from_chunk(family, data, 0);
	if (!server)
	{
		return FALSE;
	}
	snprintf(buf, sizeof(buf), "%H", server);
	server->destroy(server);

	store = SCDynamicStoreCreate(NULL, CFSTR("osx-attr"), NULL, NULL);
	path = create_dns_path(store);
	if (path)
	{
		dict = get_dictionary(store, path);
		arr = get_array_from_dict(dict, CFSTR("ServerAddresses"));
		dns = CFStringCreateWithCString(NULL, buf, kCFStringEncodingUTF8);
		if (add)
		{
			if (!this->append && !this->original)
			{	/* backup original config, start with empty set */
				this->original = arr;
				arr = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
			}
			DBG1(DBG_CFG, "installing %s as DNS server", buf);
			CFArrayInsertValueAtIndex(arr, 0, dns);
		}
		else
		{
			i = CFArrayGetFirstIndexOfValue(arr,
									CFRangeMake(0, CFArrayGetCount(arr)), dns);
			if (i >= 0)
			{
				DBG1(DBG_CFG, "removing %s from DNS servers (%d)", buf, i);
				CFArrayRemoveValueAtIndex(arr, i);
			}
			if (!this->append && this->original && CFArrayGetCount(arr) == 0)
			{	/* restore original config */
				CFRelease(arr);
				arr = this->original;
				this->original = NULL;
			}
		}
		CFRelease(dns);
		CFDictionarySetValue(dict, CFSTR("ServerAddresses"), arr);
		CFRelease(arr);

		success = SCDynamicStoreSetValue(store, path, dict);
		CFRelease(dict);
		CFRelease(path);
	}
	CFRelease(store);

	if (!success)
	{
		DBG1(DBG_CFG, "adding DNS server to SystemConfiguration failed");
	}
	return success;
}

METHOD(attribute_handler_t, handle, bool,
	private_osx_attr_handler_t *this, ike_sa_t *ike_sa,
	configuration_attribute_type_t type, chunk_t data)
{
	switch (type)
	{
		case INTERNAL_IP4_DNS:
			return manage_dns(this, AF_INET, data, TRUE);
		default:
			return FALSE;
	}
}

METHOD(attribute_handler_t, release, void,
	private_osx_attr_handler_t *this, ike_sa_t *ike_sa,
	configuration_attribute_type_t type, chunk_t data)
{
	switch (type)
	{
		case INTERNAL_IP4_DNS:
			manage_dns(this, AF_INET, data, FALSE);
			break;
		default:
			break;
	}
}

METHOD(enumerator_t, enumerate_dns, bool,
	enumerator_t *this, va_list args)
{
	configuration_attribute_type_t *type;
	chunk_t *data;

	VA_ARGS_VGET(args, type, data);
	*type = INTERNAL_IP4_DNS;
	*data = chunk_empty;
	this->venumerate = (void*)return_false;
	return TRUE;
}

METHOD(attribute_handler_t, create_attribute_enumerator, enumerator_t *,
	private_osx_attr_handler_t *this, ike_sa_t *ike_sa,
	linked_list_t *vips)
{
	enumerator_t *enumerator;

	INIT(enumerator,
		.enumerate = enumerator_enumerate_default,
		.venumerate = _enumerate_dns,
		.destroy = (void*)free,
	);
	return enumerator;
}

METHOD(osx_attr_handler_t, destroy, void,
	private_osx_attr_handler_t *this)
{
	free(this);
}

/**
 * See header
 */
osx_attr_handler_t *osx_attr_handler_create()
{
	private_osx_attr_handler_t *this;

	INIT(this,
		.public = {
			.handler = {
				.handle = _handle,
				.release = _release,
				.create_attribute_enumerator = _create_attribute_enumerator,
			},
			.destroy = _destroy,
		},
		.append = lib->settings->get_bool(lib->settings,
								"%s.plugins.osx-attr.append", TRUE, lib->ns),
	);

	return &this->public;
}
