// SPDX-FileCopyrightText: 2012 Icinga GmbH <https://icinga.com>
// SPDX-License-Identifier: GPL-2.0-or-later

#include "remote/apilistener.hpp"
#include "remote/apifunction.hpp"
#include "config/configcompiler.hpp"
#include "base/tlsutility.hpp"
#include "base/json.hpp"
#include "base/configtype.hpp"
#include "base/logger.hpp"
#include "base/convert.hpp"
#include "base/application.hpp"
#include "base/exception.hpp"
#include "base/shared.hpp"
#include "base/utility.hpp"
#include <fstream>
#include <iomanip>
#include <thread>

using namespace icinga;

REGISTER_APIFUNCTION(Update, config, &ApiListener::ConfigUpdateHandler);

std::mutex ApiListener::m_ConfigSyncStageLock;

/**
 * Entrypoint for updating all authoritative configs from /etc/zones.d, packages, etc.
 * into var/lib/icinga2/api/zones
 */
void ApiListener::SyncLocalZoneDirs() const
{
	for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
		try {
			SyncLocalZoneDir(zone);
		} catch (const std::exception&) {
			continue;
		}
	}
}

/**
 * Sync a zone directory where we have an authoritative copy (zones.d, packages, etc.)
 *
 * This function collects the registered zone config dirs from
 * the config compiler and reads the file content into the config
 * information structure.
 *
 * Returns early when there are no updates.
 *
 * @param zone Pointer to the zone object being synced.
 */
void ApiListener::SyncLocalZoneDir(const Zone::Ptr& zone) const
{
	if (!zone)
		return;

	ConfigDirInformation newConfigInfo;
	newConfigInfo.UpdateV1 = new Dictionary();
	newConfigInfo.UpdateV2 = new Dictionary();
	newConfigInfo.Checksums = new Dictionary();

	String zoneName = zone->GetName();

	// Load registered zone paths, e.g. '_etc', '_api' and user packages.
	for (const ZoneFragment& zf : ConfigCompiler::GetZoneDirs(zoneName)) {
		ConfigDirInformation newConfigPart = LoadConfigDir(zf.Path);

		// Config files '*.conf'.
		{
			ObjectLock olock(newConfigPart.UpdateV1);
			for (const Dictionary::Pair& kv : newConfigPart.UpdateV1) {
				String path = "/" + zf.Tag + kv.first;

				newConfigInfo.UpdateV1->Set(path, kv.second);
				newConfigInfo.Checksums->Set(path, GetChecksum(kv.second));
			}
		}

		// Meta files.
		{
			ObjectLock olock(newConfigPart.UpdateV2);
			for (const Dictionary::Pair& kv : newConfigPart.UpdateV2) {
				String path = "/" + zf.Tag + kv.first;

				newConfigInfo.UpdateV2->Set(path, kv.second);
				newConfigInfo.Checksums->Set(path, GetChecksum(kv.second));
			}
		}
	}

	size_t sumUpdates = newConfigInfo.UpdateV1->GetLength() + newConfigInfo.UpdateV2->GetLength();

	// Return early if there are no updates.
	if (sumUpdates == 0)
		return;

	String productionZonesDir = GetApiZonesDir() + zoneName;

	Log(LogInformation, "ApiListener")
		<< "Copying " << sumUpdates << " zone configuration files for zone '" << zoneName << "' to '" << productionZonesDir << "'.";

	// Purge files to allow deletion via zones.d.
	if (Utility::PathExists(productionZonesDir))
		Utility::RemoveDirRecursive(productionZonesDir);

	Utility::MkDirP(productionZonesDir, 0700);

	/* Note: We cannot simply copy directories here.
	 *
	 * Zone directories are registered from everywhere and we already
	 * have read their content into memory with LoadConfigDir().
	 */
	Dictionary::Ptr newConfig = MergeConfigUpdate(newConfigInfo);

	{
		ObjectLock olock(newConfig);

		for (const Dictionary::Pair& kv : newConfig) {
			String dst = productionZonesDir + "/" + kv.first;

			Utility::MkDirP(Utility::DirName(dst), 0755);

			Log(LogInformation, "ApiListener")
				<< "Updating configuration file: " << dst;

			String content = kv.second;

			std::ofstream fp(dst.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);

			fp << content;
			fp.close();
		}
	}

	// Additional metadata.
	String tsPath = productionZonesDir + "/.timestamp";

	if (!Utility::PathExists(tsPath)) {
		std::ofstream fp(tsPath.CStr(), std::ofstream::out | std::ostream::trunc);

		fp << std::fixed << Utility::GetTime();
		fp.close();
	}

	String authPath = productionZonesDir + "/.authoritative";

	if (!Utility::PathExists(authPath)) {
		std::ofstream fp(authPath.CStr(), std::ofstream::out | std::ostream::trunc);
		fp.close();
	}

	// Checksums.
	String checksumsPath = productionZonesDir + "/.checksums";

	if (Utility::PathExists(checksumsPath))
		Utility::Remove(checksumsPath);

	std::ofstream fp(checksumsPath.CStr(), std::ofstream::out | std::ostream::trunc);

	fp << std::fixed << JsonEncode(newConfigInfo.Checksums);
	fp.close();

	Log(LogNotice, "ApiListener")
		<< "Updated meta data for cluster config sync. Checksum: '" << checksumsPath
		<< "', timestamp: '" << tsPath << "', auth: '" << authPath << "'.";
}

/**
 * Entrypoint for sending a file based config update to a cluster client.
 * This includes security checks for zone relations.
 * Loads the zone config files where this client belongs to
 * and sends the 'config::Update' JSON-RPC message.
 *
 * @param aclient Connected JSON-RPC client.
 */
void ApiListener::SendConfigUpdate(const JsonRpcConnection::Ptr& aclient)
{
	Endpoint::Ptr endpoint = aclient->GetEndpoint();
	ASSERT(endpoint);

	Zone::Ptr clientZone = endpoint->GetZone();
	Zone::Ptr localZone = Zone::GetLocalZone();

	// Don't send config updates to parent zones
	if (!clientZone->IsChildOf(localZone))
		return;

	Dictionary::Ptr configUpdateV1 = new Dictionary();
	Dictionary::Ptr configUpdateV2 = new Dictionary();
	Dictionary::Ptr configUpdateChecksums = new Dictionary(); // new since 2.11

	String zonesDir = GetApiZonesDir();

	for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
		String zoneName = zone->GetName();
		String zoneDir = zonesDir + zoneName;

		// Only sync child and global zones.
		if (!zone->IsChildOf(clientZone) && !zone->IsGlobal())
			continue;

		// Zone was configured, but there's no configuration directory.
		if (!Utility::PathExists(zoneDir))
			continue;

		Log(LogInformation, "ApiListener")
			<< "Syncing configuration files for " << (zone->IsGlobal() ? "global " : "")
			<< "zone '" << zoneName << "' to endpoint '" << endpoint->GetName() << "'.";

		ConfigDirInformation config = LoadConfigDir(zoneDir);

		configUpdateV1->Set(zoneName, config.UpdateV1);
		configUpdateV2->Set(zoneName, config.UpdateV2);
		configUpdateChecksums->Set(zoneName, config.Checksums); // new since 2.11
	}

	Dictionary::Ptr message = new Dictionary({
		{ "jsonrpc", "2.0" },
		{ "method", "config::Update" },
		{ "params", new Dictionary({
			{ "update", configUpdateV1 },
			{ "update_v2", configUpdateV2 },	// Since 2.4.2.
			{ "checksums", configUpdateChecksums } 	// Since 2.11.0.
		}) }
	});

	aclient->SendMessage(message);
}

static bool CompareTimestampsConfigChange(const Dictionary::Ptr& productionConfig, const Dictionary::Ptr& receivedConfig,
	const String& stageConfigZoneDir)
{
	double productionTimestamp;
	double receivedTimestamp;

	// Missing production timestamp means that something really broke. Always trigger a config change then.
	if (!productionConfig->Contains("/.timestamp"))
		productionTimestamp = 0;
	else
		productionTimestamp = productionConfig->Get("/.timestamp");

	// Missing received config timestamp means that something really broke. Always trigger a config change then.
	if (!receivedConfig->Contains("/.timestamp"))
		receivedTimestamp = Utility::GetTime() + 10;
	else
		receivedTimestamp = receivedConfig->Get("/.timestamp");

	bool configChange;

	// Skip update if our configuration files are more recent.
	if (productionTimestamp >= receivedTimestamp) {

		Log(LogInformation, "ApiListener")
			<< "Our production configuration is more recent than the received configuration update."
			<< " Ignoring configuration file update for path '" << stageConfigZoneDir << "'. Current timestamp '"
			<< Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", productionTimestamp) << "' ("
			<< std::fixed << std::setprecision(6) << productionTimestamp
			<< ") >= received timestamp '"
			<< Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", receivedTimestamp) << "' ("
			<< receivedTimestamp << ").";

		configChange = false;

	} else {
		configChange = true;
	}

	// Update the .timestamp file inside the staging directory.
	String tsPath = stageConfigZoneDir + "/.timestamp";

	if (!Utility::PathExists(tsPath)) {
		std::ofstream fp(tsPath.CStr(), std::ofstream::out | std::ostream::trunc);
		fp << std::fixed << receivedTimestamp;
		fp.close();
	}

	return configChange;
}

/**
 * Registered handler when a new config::Update message is received.
 *
 * Checks destination and permissions first, locks the transaction and analyses the update.
 * The newly received configuration is not copied to production immediately,
 * but into the staging directory first.
 * Last, the async validation and restart is triggered.
 *
 * @param origin Where this message came from.
 * @param params Message parameters including the config updates.
 * @returns Empty, required by the interface.
 */
Value ApiListener::ConfigUpdateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
{
	// Verify permissions and trust relationship.
	if (!origin->FromClient->GetEndpoint() || (origin->FromZone && !Zone::GetLocalZone()->IsChildOf(origin->FromZone)))
		return Empty;

	ApiListener::Ptr listener = ApiListener::GetInstance();

	if (!listener) {
		Log(LogCritical, "ApiListener", "No instance available.");
		return Empty;
	}

	if (!listener->GetAcceptConfig()) {
		Log(LogWarning, "ApiListener")
			<< "Ignoring config update. '" << listener->GetName() << "' does not accept config.";
		return Empty;
	}

	std::thread([origin, params, listener]() {
		try {
			listener->HandleConfigUpdate(origin, params);
		} catch (const std::exception& ex) {
			auto msg ("Exception during config sync: " + DiagnosticInformation(ex));

			Log(LogCritical, "ApiListener") << msg;
			listener->UpdateLastFailedZonesStageValidation(msg);
		}
	}).detach();
	return Empty;
}

void ApiListener::HandleConfigUpdate(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
{
	/* Only one transaction is allowed, concurrent message handlers need to wait.
	 * This affects two parent endpoints sending the config in the same moment.
	 */
	std::lock_guard<std::mutex> lock(m_ConfigSyncStageLock);

	String apiZonesStageDir = GetApiZonesStageDir();
	String fromEndpointName = origin->FromClient->GetEndpoint()->GetName();
	String fromZoneName = GetFromZoneName(origin->FromZone);

	Log(LogInformation, "ApiListener")
		<< "Applying config update from endpoint '" << fromEndpointName
		<< "' of zone '" << fromZoneName << "'.";

	// Config files.
	Dictionary::Ptr updateV1 = params->Get("update");
	// Meta data files: .timestamp, etc.
	Dictionary::Ptr updateV2 = params->Get("update_v2");

	// New since 2.11.0.
	Dictionary::Ptr checksums;

	if (params->Contains("checksums"))
		checksums = params->Get("checksums");

	bool configChange = false;

	// Keep track of the relative config paths for later validation and copying. TODO: Find a better algorithm.
	std::vector<String> relativePaths;

	/*
	 * We can and must safely purge the staging directory, as the difference is taken between
	 * runtime production config and newly received configuration.
	 * This is needed to not mix deleted/changed content between received and stage
	 * config.
	 */
	if (Utility::PathExists(apiZonesStageDir))
		Utility::RemoveDirRecursive(apiZonesStageDir);

	Utility::MkDirP(apiZonesStageDir, 0700);

	// Analyse and process the update.
	size_t count = 0;

	ObjectLock olock(updateV1);

	for (const Dictionary::Pair& kv : updateV1) {

		// Check for the configured zones.
		String zoneName = kv.first;
		Zone::Ptr zone = Zone::GetByName(zoneName);

		if (!zone) {
			Log(LogWarning, "ApiListener")
				<< "Ignoring config update from endpoint '" << fromEndpointName
				<< "' for unknown zone '" << zoneName << "'.";

			continue;
		}

		// Ignore updates where we have an authoritive copy in etc/zones.d, packages, etc.
		if (ConfigCompiler::HasZoneConfigAuthority(zoneName)) {
			Log(LogInformation, "ApiListener")
				<< "Ignoring config update from endpoint '" << fromEndpointName
				<< "' for zone '" << zoneName << "' because we have an authoritative version of the zone's config.";

			continue;
		}

		// Put the received configuration into our stage directory.
		String productionConfigZoneDir = GetApiZonesDir() + zoneName;
		String stageConfigZoneDir = GetApiZonesStageDir() + zoneName;

		Utility::MkDirP(productionConfigZoneDir, 0700);
		Utility::MkDirP(stageConfigZoneDir, 0700);

		// Merge the config information.
		ConfigDirInformation newConfigInfo;
		newConfigInfo.UpdateV1 = kv.second;

		// Load metadata.
		if (updateV2)
			newConfigInfo.UpdateV2 = updateV2->Get(kv.first);

		// Load checksums. New since 2.11.
		if (checksums)
			newConfigInfo.Checksums = checksums->Get(kv.first);

		// Load the current production config details.
		ConfigDirInformation productionConfigInfo = LoadConfigDir(productionConfigZoneDir);

		// Merge updateV1 and updateV2
		Dictionary::Ptr productionConfig = MergeConfigUpdate(productionConfigInfo);
		Dictionary::Ptr newConfig = MergeConfigUpdate(newConfigInfo);

		bool timestampChanged = false;

		if (CompareTimestampsConfigChange(productionConfig, newConfig, stageConfigZoneDir)) {
			timestampChanged = true;
		}

		/* If we have received 'checksums' via cluster message, go for it.
		 * Otherwise do the old timestamp dance for versions < 2.11.
		 */
		if (checksums) {
			Log(LogInformation, "ApiListener")
				<< "Received configuration for zone '" << zoneName << "' from endpoint '"
				<< fromEndpointName << "'. Comparing the timestamp and checksums.";

			if (timestampChanged) {

				if (CheckConfigChange(productionConfigInfo, newConfigInfo))
					configChange = true;
			}

		} else {
			/* Fallback to timestamp handling when the parent endpoint didn't send checks.
			 * This can happen when the satellite is 2.11 and the master is 2.10.
			 *
			 * TODO: Deprecate and remove this behaviour in 2.13+.
			 */

			Log(LogWarning, "ApiListener")
				<< "Received configuration update without checksums from parent endpoint "
				<< fromEndpointName << ". This behaviour is deprecated. Please upgrade the parent endpoint to 2.11+";

			if (timestampChanged) {
				configChange = true;
			}

			// Keep another hack when there's a timestamp file missing.
			{
				ObjectLock olock(newConfig);

				for (const Dictionary::Pair &kv : newConfig) {

					// This is super expensive with a string content comparison.
					if (productionConfig->Get(kv.first) != kv.second) {
						if (!Utility::Match("*/.timestamp", kv.first))
							configChange = true;
					}
				}
			}
		}

		// Dump the received configuration for this zone into the stage directory.
		size_t numBytes = 0;

		{
			ObjectLock olock(newConfig);

			for (const Dictionary::Pair& kv : newConfig) {

				/* Store the relative config file path for later validation and activation.
				 * IMPORTANT: Store this prior to any filters.
				 * */
				relativePaths.push_back(zoneName + "/" + kv.first);

				String path = stageConfigZoneDir + "/" + kv.first;

				if (Utility::Match("*.conf", path)) {
					Log(LogInformation, "ApiListener")
						<< "Stage: Updating received configuration file '" << path << "' for zone '" << zoneName << "'.";
				}

				// Parent nodes < 2.11 always send this, avoid this bug and deny its receival prior to writing it on disk.
				if (Utility::BaseName(path) == ".authoritative")
					continue;

				// Sync string content only.
				String content = kv.second;

				// Generate a directory tree (zones/1/2/3 might not exist yet).
				Utility::MkDirP(Utility::DirName(path), 0755);

				// Write the content to file.
				std::ofstream fp(path.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
				fp << content;
				fp.close();

				numBytes += content.GetLength();
			}
		}

		Log(LogInformation, "ApiListener")
			<< "Applying configuration file update for path '" << stageConfigZoneDir << "' ("
			<< numBytes << " Bytes).";

		if (timestampChanged) {
			// If the update removes a path, delete it on disk and signal a config change.
			ObjectLock xlock(productionConfig);

			for (const Dictionary::Pair& kv : productionConfig) {
				if (!newConfig->Contains(kv.first)) {
					configChange = true;

					String path = stageConfigZoneDir + "/" + kv.first;
					Utility::Remove(path);
				}
			}
		}

		count++;
	}

	/*
	 * We have processed all configuration files and stored them in the staging directory.
	 *
	 * We need to store them locally for later analysis. A config change means
	 * that we will validate the configuration in a separate process sandbox,
	 * and only copy the configuration to production when everything is ok.
	 *
	 * A successful validation also triggers the final restart.
	 */
	if (configChange) {
		Log(LogInformation, "ApiListener")
			<< "Received configuration updates (" << count << ") from endpoint '" << fromEndpointName
			<< "' are different to production, triggering validation and reload.";
		TryActivateZonesStage(relativePaths);
	} else {
		Log(LogInformation, "ApiListener")
			<< "Received configuration updates (" << count << ") from endpoint '" << fromEndpointName
			<< "' are equal to production, skipping validation and reload.";
		ClearLastFailedZonesStageValidation();
	}
}

/**
 * Spawns a new validation process with 'Internal.ZonesStageVarDir' set to override the config validation zone dirs with
 * our current stage. Then waits for the validation result and if it was successful, the configuration is copied from
 * stage to production and a restart is triggered. On validation failure, there is no restart and this is logged.
 *
 * The caller of this function must hold m_ConfigSyncStageLock.
 *
 * @param relativePaths Collected paths including the zone name, which are copied from stage to current directories.
 */
void ApiListener::TryActivateZonesStage(const std::vector<String>& relativePaths)
{
	VERIFY(Application::GetArgC() >= 1);

	/* Inherit parent process args. */
	Array::Ptr args = new Array({
		Application::GetExePath(Application::GetArgV()[0]),
	});

	for (int i = 1; i < Application::GetArgC(); i++) {
		String argV = Application::GetArgV()[i];

		if (argV == "-d" || argV == "--daemonize")
			continue;

		args->Add(argV);
	}

	args->Add("--validate");

	// Set the ZonesStageDir. This creates our own local chroot without any additional automated zone includes.
	args->Add("--define");
	args->Add("Internal.ZonesStageVarDir=" + GetApiZonesStageDir());

	Process::Ptr process = new Process(Process::PrepareCommand(args));
	process->SetTimeout(Application::GetReloadTimeout());

	process->Run();
	const ProcessResult& pr = process->WaitForResult();

	String apiDir = GetApiDir();
	String apiZonesDir = GetApiZonesDir();
	String apiZonesStageDir = GetApiZonesStageDir();

	String logFile = apiDir + "/zones-stage-startup.log";
	std::ofstream fpLog(logFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
	fpLog << pr.Output;
	fpLog.close();

	String statusFile = apiDir + "/zones-stage-status";
	std::ofstream fpStatus(statusFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
	fpStatus << pr.ExitStatus;
	fpStatus.close();

	// Validation went fine, copy stage and reload.
	if (pr.ExitStatus == 0) {
		Log(LogInformation, "ApiListener")
			<< "Config validation for stage '" << apiZonesStageDir << "' was OK, replacing into '" << apiZonesDir << "' and triggering reload.";

		// Purge production before copying stage.
		if (Utility::PathExists(apiZonesDir))
			Utility::RemoveDirRecursive(apiZonesDir);

		Utility::MkDirP(apiZonesDir, 0700);

		// Copy all synced configuration files from stage to production.
		for (const String& path : relativePaths) {
			if (!Utility::PathExists(apiZonesStageDir + path))
				continue;

			Log(LogInformation, "ApiListener")
				<< "Copying file '" << path << "' from config sync staging to production zones directory.";

			String stagePath = apiZonesStageDir + path;
			String currentPath = apiZonesDir + path;

			Utility::MkDirP(Utility::DirName(currentPath), 0700);

			Utility::CopyFile(stagePath, currentPath);
		}

		// Clear any failed deployment before
		ApiListener::Ptr listener = ApiListener::GetInstance();

		if (listener)
			listener->ClearLastFailedZonesStageValidation();

		Application::RequestRestart();

		// All good, return early.
		return;
	}

	String failedLogFile = apiDir + "/zones-stage-startup-last-failed.log";
	std::ofstream fpFailedLog(failedLogFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
	fpFailedLog << pr.Output;
	fpFailedLog.close();

	// Error case.
	Log(LogCritical, "ApiListener")
		<< "Config validation failed for staged cluster config sync in '" << apiZonesStageDir
		<< "'. Aborting. Logs: '" << failedLogFile << "'";

	ApiListener::Ptr listener = ApiListener::GetInstance();

	if (listener)
		listener->UpdateLastFailedZonesStageValidation(pr.Output);
}

/**
 * Update the structure from the last failed validation output.
 * Uses the current timestamp.
 *
 * @param log The process output from the config validation.
 */
void ApiListener::UpdateLastFailedZonesStageValidation(const String& log)
{
	Dictionary::Ptr lastFailedZonesStageValidation = new Dictionary({
		{ "log", log },
		{ "ts", Utility::GetTime() }
	});

	SetLastFailedZonesStageValidation(lastFailedZonesStageValidation);
}

/**
 * Clear the structure for the last failed reload.
 *
 */
void ApiListener::ClearLastFailedZonesStageValidation()
{
	SetLastFailedZonesStageValidation(Dictionary::Ptr());
}

/**
 * Generate a config checksum.
 *
 * @param content String content used for generating the checksum.
 * @returns The checksum as string.
 */
String ApiListener::GetChecksum(const String& content)
{
	return SHA256(content);
}

bool ApiListener::CheckConfigChange(const ConfigDirInformation& oldConfig, const ConfigDirInformation& newConfig)
{
	Dictionary::Ptr oldChecksums = oldConfig.Checksums;
	Dictionary::Ptr newChecksums = newConfig.Checksums;

	// TODO: Figure out whether normal users need this for debugging.
	Log(LogDebug, "ApiListener")
		<< "Checking for config change between stage and production. Old (" << oldChecksums->GetLength() << "): '"
		<< JsonEncode(oldChecksums)
		<< "' vs. new (" << newChecksums->GetLength() << "): '"
		<< JsonEncode(newChecksums) << "'.";

	/* Since internal files are synced here too, we can not depend on length.
	 * So we need to go through both checksum sets to cover the cases"everything is new" and "everything was deleted".
	 */
	{
		ObjectLock olock(oldChecksums);
		for (const Dictionary::Pair& kv : oldChecksums) {
			String path = kv.first;
			String oldChecksum = kv.second;

			/* Ignore internal files, especially .timestamp and .checksums.
			 *
			 * If we don't, this results in "always change" restart loops.
			 */
			if (Utility::Match("/.*", path)) {
				Log(LogDebug, "ApiListener")
					<< "Ignoring old internal file '" << path << "'.";

				continue;
			}

			Log(LogDebug, "ApiListener")
				<< "Checking " << path << " for old checksum: " << oldChecksum << ".";

			// Check if key exists first for more verbose logging.
			// Note: Don't do this later on.
			if (!newChecksums->Contains(path)) {
				Log(LogDebug, "ApiListener")
					<< "File '" << path << "' was deleted by remote.";

				return true;
			}

			String newChecksum = newChecksums->Get(path);

			if (newChecksum != kv.second) {
				Log(LogDebug, "ApiListener")
					<< "Path '" << path << "' doesn't match old checksum '"
					<< oldChecksum << "' with new checksum '" << newChecksum << "'.";

				return true;
			}
		}
	}

	{
		ObjectLock olock(newChecksums);
		for (const Dictionary::Pair& kv : newChecksums) {
			String path = kv.first;
			String newChecksum = kv.second;

			/* Ignore internal files, especially .timestamp and .checksums.
			 *
			 * If we don't, this results in "always change" restart loops.
			 */
			if (Utility::Match("/.*", path)) {
				Log(LogDebug, "ApiListener")
					<< "Ignoring new internal file '" << path << "'.";

				continue;
			}

			Log(LogDebug, "ApiListener")
				<< "Checking " << path << " for new checksum: " << newChecksum << ".";

			// Check if the checksum exists, checksums in both sets have already been compared
			if (!oldChecksums->Contains(path)) {
				Log(LogDebug, "ApiListener")
					<< "File '" << path << "' was added by remote.";

				return true;
			}
		}
	}

	return false;
}

/**
 * Load the given config dir and read their file content into the config structure.
 *
 * @param dir Path to the config directory.
 * @returns ConfigDirInformation structure.
 */
ConfigDirInformation ApiListener::LoadConfigDir(const String& dir)
{
	ConfigDirInformation config;
	config.UpdateV1 = new Dictionary();
	config.UpdateV2 = new Dictionary();
	config.Checksums = new Dictionary();

	Utility::GlobRecursive(dir, "*", [&config, dir](const String& file) { ConfigGlobHandler(config, dir, file); }, GlobFile);
	return config;
}

/**
 * Read the given file and store it in the config information structure.
 * Callback function for Glob().
 *
 * @param config Reference to the config information object.
 * @param path File path.
 * @param file Full file name.
 */
void ApiListener::ConfigGlobHandler(ConfigDirInformation& config, const String& path, const String& file)
{
	// Avoid loading the authoritative marker for syncs at all cost.
	if (Utility::BaseName(file) == ".authoritative")
		return;

	CONTEXT("Creating config update for file '" << file << "'");

	Log(LogNotice, "ApiListener")
		<< "Creating config update for file '" << file << "'.";

	std::ifstream fp(file.CStr(), std::ifstream::binary);
	if (!fp)
		return;

	String content((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());

	Dictionary::Ptr update;
	String relativePath = file.SubStr(path.GetLength());

	/*
	 * 'update' messages contain conf files. 'update_v2' syncs everything else (.timestamp).
	 *
	 * **Keep this intact to stay compatible with older clients.**
	 */
	String sanitizedContent = Utility::ValidateUTF8(content);

	if (Utility::Match("*.conf", file)) {
		update = config.UpdateV1;

		// Configuration files should be automatically sanitized with UTF8.
		update->Set(relativePath, sanitizedContent);
	} else {
		update = config.UpdateV2;

		/*
		 * Ensure that only valid UTF8 content is being read for the cluster config sync.
		 * Binary files are not supported when wrapped into JSON encoded messages.
		 * Rationale: https://github.com/Icinga/icinga2/issues/7382
		 */
		if (content != sanitizedContent) {
			Log(LogCritical, "ApiListener")
				<< "Ignoring file '" << file << "' for cluster config sync: Does not contain valid UTF8. Binary files are not supported.";
			return;
		}

		update->Set(relativePath, content);
	}

	/* Calculate a checksum for each file (and a global one later).
	 *
	 * IMPORTANT: Ignore the .authoritative file above, this must not be synced.
	 * */
	config.Checksums->Set(relativePath, GetChecksum(content));
}

/**
 * Compatibility helper for merging config update v1 and v2 into a global result.
 *
 * @param config Config information structure.
 * @returns Dictionary which holds the merged information.
 */
Dictionary::Ptr ApiListener::MergeConfigUpdate(const ConfigDirInformation& config)
{
	Dictionary::Ptr result = new Dictionary();

	if (config.UpdateV1)
		config.UpdateV1->CopyTo(result);

	if (config.UpdateV2)
		config.UpdateV2->CopyTo(result);

	return result;
}
