/* Copyright 2017-2025 m-privacy GmbH.
 *
 * This 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.
 *
 * This software 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this software; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
 * USA.
 */

#if !defined(WIN32) && !defined(WIN64) && !defined(__APPLE__)

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <algorithm>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <string.h>
#include <strings.h>

#include <syslog.h>

#include <sys/inotify.h>

#include <rdr/types.h>
#include <rfb/LogWriter.h>
#include <rdr/MultiStream.h>

#include <mp_utils.h>
#include <tgpro_environment.h>
#include <transfer_log_helpers.h>

#include "SAutotransferMime.h"

#define MAGIC_CONF_DIR "/etc/ssh/sftp_magic"
#define MAGIC_ALLOWED 0
#define MAGIC_NOT_ALLOWED 1
#define MAGIC_FAILED -1
#define MAXPATHLEN 4096

static rfb::LogWriter vlog("SAutotransferMime");

#define mplog_error(...) vlog.error(__VA_ARGS__);
#define mplog_info(...) vlog.info(__VA_ARGS__);
#define mplog_debug(...) vlog.debug(__VA_ARGS__);
#define mplog_verbose(...) vlog.verbose(__VA_ARGS__);

#include <mime.h>

#define MAXUNPACKLEVEL 5
#define MAXUNPACKSIZE (100 * 1024 * 1024)

rfb::BoolParameter logAutotransfer
("logAutotransfer",
 "Log (allowed) auto-transfered files to transfer.log. Not allowed transfers are always logged",
 false);
rfb::BoolParameter calcAutotransferChecksum
("calcAutotransferChecksum",
 "Calculate a 256 SHA checksum of auto-transfered and log to transfer.log (only if denied transfer or logAutotransfer is enabled).",
 false);

SAutotransferMime::SAutotransferMime(): SAutotransferDirWatcher()
{
	autotransferNowDir = (char*) malloc(4096);
	snprintf(autotransferNowDir, 4095, "/home/user/.autotransfer-now/%s", getpwuid(geteuid())->pw_name);
	autotransferNowDir[4095] = '\0';

	lastAllowedMimeTypesRead = 0;
	initMagicFile();
}

std::string SAutotransferMime::getDirToWatch() const
{
	return "/home/user/.transfer/" + getEffectiveUser() + "/autotransfer";
}

/*
 * After checking the MIME type (and some small stuff),
 * this method justs moves the file to the user's autotransfer-now
 * dir (/home/user/.autotransfer-now/$USER) and just forgets about
 * it. The SAutotransferNow will take care of it from this point
 * onwards.
 */
void SAutotransferMime::processFile(char* filename)
{
	vlog.debug("%s: Processing file '%s'", __func__, filename);
	if (checkFile(filename)) {
		char destination[4096];
		snprintf(destination, 4095, "%s/%s", autotransferNowDir, basename(filename));
		destination[4095] = 0;
		vlog.debug("%s: Trying to move file from '%s' to '%s' (from where it should be auto-downloaded by the client)", __func__, filename, destination);
		mp_move(filename, destination, 1); // TODO: overwriting? Is this okay?
	} else {
		vlog.debug("%s: Not transfering file to the client (leaving file in %s)", __func__, filename);
	}
}

bool SAutotransferMime::checkFile(char* filename) {
	// Do not notify for .part files (firefox downloads)
	if (ends_with(filename, ".part")) {
		vlog.debug("%s: Not processing file '%s' because of its file ending (.part)", __func__, filename);
		return false;
	}
	// Do not notify for .crdownload files (Chrome downloads)
	if (ends_with(filename, ".crdownload")) {
		vlog.debug("%s: Not processing file '%s' because of its file ending (.crdownload)", __func__, filename);
		return false;
	}
	// Do not notifiy LibreOffice lock files
	if (strstr(filename, ".~lock.")) {
		vlog.debug("%s: Not processing file '%s' because its a LibreOffice lock file (.~lock.)", __func__, filename);
		return false;
	}

	// Check file size
	vlog.verbose("%s: Sleeping 200 ms before processing file '%s' to avoid 0 bytes false alarms", __func__, filename);
	usleep(200000); // Sleep 200 ms to try to avoid getting a timing problem that causes some false 0-byte-file positives.
	struct stat buffer;
	int status = stat(filename, &buffer);
	if (status == 0) {
		vlog.debug("%s: file size: %zu", __func__, buffer.st_size);
		if (buffer.st_size == 0) {
			vlog.debug("%s: Not processing file '%s' because of file size (0 bytes)", __func__, filename);
			return false;
		}
	} else {
		vlog.error("%s: error: %s. Couldn't execute stat on '%s'. Skipping...", __func__, strerror(errno), filename);
		return false;
	}

	int checkMagicResult = checkMagic(filename);
	if (checkMagicResult == MAGIC_NOT_ALLOWED) {
		vlog.debug("%s: Not processing file '%s' because the MIME type doesn't allow it", __func__, filename);
		return false;
	} else if (checkMagicResult == MAGIC_FAILED) {
		vlog.error("%s: Failed to run checkMagic on '%s'. Re-adding it to queue...", __func__, filename);
		addToProcessQueue(filename);
		return false;
	}

	return true;
}

void SAutotransferMime::checkGlibPacker(char * filename, const char * mimeTypeLibmagic)
{
//	char glibtype[BUFFER_SIZE];

//	if (get_mime_type_glib(filename, (char (*)[4096]) glibtype) && strcmp(glibtype, mimeTypeLibmagic)) {
//		vlog.info("%s (child (pid=%d)): warning: mime type: %s, file: \"%s\", glib shows different type %s!", __func__, getpid(), mimeTypeLibmagic, filename, glibtype);
//		syslog(LOG_DEBUG, "warning: mime type: %s, file: \"%s\", glib shows different type %s!", mimeTypeLibmagic, filename, glibtype);
//	}
	for (const char* packerCheckType : packerCheckTypes) {
		if (!strncasecmp(mimeTypeLibmagic, packerCheckType, strlen(packerCheckType))) {
			char packercheckcmd[4096];
			int result;

			snprintf(packercheckcmd, 4095, "/usr/bin/timeout -k 2 60 /usr/bin/7z l -bb0 -bd -y -aoa -p '%s' &>/dev/null", filename);
			packercheckcmd[4095] = 0;
			result = system(packercheckcmd);
			if (WIFEXITED(result) && ( WEXITSTATUS(result) == 0 || WEXITSTATUS(result) == 1)) {
				vlog.info("%s (child (pid=%d)): warning: mime type: %s, file: \"%s\", can be unpacked with 7z as archive!", __func__, getpid(), mimeTypeLibmagic, filename);
				syslog(LOG_DEBUG, "warning: mime type: %s, file: \"%s\", can be unpacked with 7z as archive!", mimeTypeLibmagic, filename);
			}
			snprintf(packercheckcmd, 4095, "/usr/bin/timeout -k 2 60 /usr/bin/unzip -l -o -B -P '' -qq '%s' &>/dev/null", filename);
			packercheckcmd[4095] = 0;
			result = system(packercheckcmd);
			if (WIFEXITED(result) && ( WEXITSTATUS(result) == 0 || WEXITSTATUS(result) == 1)) {
				vlog.info("%s (child (pid=%d)): warning: mime type: %s, file: \"%s\", can be unpacked with unzip as archive!", __func__, getpid(), mimeTypeLibmagic, filename);
				syslog(LOG_DEBUG, "warning: mime type: %s, file: \"%s\", can be unpacked with unzip as archive!", mimeTypeLibmagic, filename);
			}
			break;
		}
	}
}

static int containsMacros(const char* file)
{
	if (!file) {
		vlog.error("%s: Invalid file pointer", __func__);
		return 0;
	}

	size_t unzipCommandSize = snprintf(NULL, 0, "unzip -l \"%s\" > /dev/null 2>&1", file) + 1;
	char* unzipCommand = (char*) malloc(unzipCommandSize);
	if (!unzipCommand) {
		vlog.error("%s: Memory allocation failed", __func__);
		return 0;
	}
	snprintf(unzipCommand, unzipCommandSize, "unzip -l \"%s\" > /dev/null 2>&1", file);
	unzipCommand[unzipCommandSize - 1] = '\0';

	int status = system(unzipCommand);
	free(unzipCommand);
	if (status != 0) {
		// If 'unzip -l' fails (non-zero exit code), assume it has macros
		return 1;
	}

	size_t unzipAndEgrepCommandSize = snprintf(NULL, 0, "unzip -l \"%s\" | grep -v \"^Archive:\" | egrep -iq \"basic|scripts|macros|vb\"", file) + 1;
	char* unzipAndEgrepCommand = (char*) malloc(unzipAndEgrepCommandSize);
	if (!unzipAndEgrepCommand) {
		vlog.error("%s: Memory allocation failed", __func__);
		return 0;
	}
	snprintf(unzipAndEgrepCommand, unzipAndEgrepCommandSize, "unzip -l \"%s\" | grep -v \"^Archive:\" | egrep -iq \"basic|scripts|macros|vb\"", file);
	unzipAndEgrepCommand[unzipAndEgrepCommandSize - 1] = '\0';

	status = system(unzipAndEgrepCommand);
	free(unzipAndEgrepCommand);

	// If 'egrep' finds a match, it returns 0 (success), otherwise returns 1
	return (WIFEXITED(status) && WEXITSTATUS(status) == 0) ? 1 : 0;
}

static bool listContains(const std::list<const char*>& allowedMimeTypes, const char* element)
{
	for (const char* allowedMimeType : allowedMimeTypes) {
		if(!strcmp(element, allowedMimeType)) {
			return true;
		}
    }
	return false;
}

bool SAutotransferMime::checkSingleFile(char *filename, char **mimeTypeLibmagicP, char **reasonP)
{
	const char *magicName;
	char *mmagic = NULL;
	const char *mimeTypeLibmagic;

	magicName = magic_file(magicSet, filename);
	if(!magicName) {
		vlog.info("%s (child (pid=%d)): failed to lookup mime type: file: \"%s\", error: %s", __func__, getpid(), filename, magic_error(magicSet));
		syslog(LOG_DEBUG, "Failed to lookup mime type: file: \"%s\", error: %s", filename, magic_error(magicSet));
		return false;
	}
	/* Remove extra mime informations if present */
	mmagic = strdup(magicName);
	if(strstr(mmagic, ";"))
		mimeTypeLibmagic = strsep(&mmagic, ";");
	else
		mimeTypeLibmagic = mmagic;

	// application/msword and application/vnc.ms- are actually older types which are not unzippable
	// but since we now fail for unzippable files this is okay (and actually part of the requirements).
	if (starts_with(mimeTypeLibmagic, "application/vnd.oasis.opendocument.") ||
		starts_with(mimeTypeLibmagic, "application/vnd.openxmlformats-officedocument.")) {
		if (allowedMimeTypes.empty() || listContains(allowedMimeTypes, "no-macros")) {
			if (containsMacros(filename)) {
				if (mimeTypeLibmagicP)
					*mimeTypeLibmagicP = strdup(mimeTypeLibmagic);
				if (reasonP)
					*reasonP = strdup("no-macros");
				vlog.info("%s (child (pid=%d)): rejected (no-macros): mime type: %s, file: \"%s\"", __func__, getpid(), mimeTypeLibmagic, filename);
//				syslog(LOG_DEBUG, "rejected (no-macros): mime type: %s, file: \"%s\"", mimeTypeLibmagic, filename);
				return false;
			}
		}
	}

	if (!strcmp(mimeTypeLibmagic, "application/octet-stream")) {
		/* check for pkcs12 */
		char pkcs12cmd[4096];

		snprintf(pkcs12cmd, 4095, "openssl pkcs12 -noout -password pass:none < '%s' 2>&1 | grep -q 'invalid password'", filename);
		pkcs12cmd[4095] = 0;
		if (!system(pkcs12cmd)) {
			mimeTypeLibmagic = "application/pkcs12";
		}
	}

	if (mimeTypeLibmagicP)
		*mimeTypeLibmagicP = strdup(mimeTypeLibmagic);
	if (allowedMimeTypes.empty()) {
		if (mimeTypeLibmagicP)
			vlog.debug("%s (child (pid=%d)): File \"%s\" detected MIME type '%s' is allowed", __func__, getpid(), filename, mimeTypeLibmagic);
		if(access("/etc/cu/sftpskiparchives", F_OK))
			checkGlibPacker(filename, mimeTypeLibmagic);
		return true;
	}
	for (const char* allowedMimeType : allowedMimeTypes) {
		if (!strncasecmp(mimeTypeLibmagic, allowedMimeType, strlen(allowedMimeType))) {
			// It is an allowed type
			if (mimeTypeLibmagicP)
				vlog.debug("%s (child (pid=%d)): File \"%s\" detected MIME type '%s' is allowed", __func__, getpid(), filename, mimeTypeLibmagic);
			else
				vlog.verbose("%s (child (pid=%d)): File \"%s\" detected MIME type '%s' is allowed", __func__, getpid(), filename, mimeTypeLibmagic);
			if(access("/etc/cu/sftpskiparchives", F_OK))
				checkGlibPacker(filename, mimeTypeLibmagic);
			return true;
		}
	}
	if (!mimeTypeLibmagicP) {
		vlog.info("%s (child (pid=%d)): rejected unpacked: mime type: %s, file: \"%s\"", __func__, getpid(), mimeTypeLibmagic, filename);
		syslog(LOG_DEBUG, "rejected unpacked: mime type: %s, file: \"%s\"", mimeTypeLibmagic, filename);
	}
	return false;
}

bool SAutotransferMime::checkTree(char *dirname)
{
	DIR * dirstream;
	struct dirent * entry;
	int result;
	char subpath[4096];

	vlog.verbose("%s (child (pid=%d)): checkTree(): checking %s for mime type", __func__, getpid(), dirname);
	dirstream = opendir(dirname);
	if (!dirstream) {
		vlog.info("%s (child (pid=%d)): failed to open dir \"%s\" for mime type, error: %s", __func__, getpid(), dirname, strerror(errno));
		syslog(LOG_DEBUG, "failed to open dir \"%s\" for mime type, error: %s", dirname, strerror(errno));
		return false;
	}
	subpath[4095] = 0;
	while ((entry = readdir(dirstream))) {
		if (entry->d_name[0] == '.' && (entry->d_name[1] == 0 || (entry->d_name[1] == '.' && entry->d_name[2] == 0)))
			continue;
		snprintf(subpath, 4095, "%s/%s", dirname, entry->d_name);
		if (entry->d_type == DT_DIR) {
			result = checkTree(subpath);
		} else if (entry->d_type == DT_REG) {
			result = checkSingleFile(subpath, NULL, NULL);
		} else if (entry->d_type == DT_UNKNOWN) {
			struct stat statbuf;

			if(lstat(subpath, &statbuf) < 0) {
				vlog.info("%s (child (pid=%d)): failed to stat dir entry \"%s\" for mime type, not unpacking: %s", __func__, getpid(), entry->d_name, strerror(errno));
				syslog(LOG_DEBUG, "failed to stat dir entry \"%s\" for mime type, not unpacking: %s", entry->d_name, strerror(errno));
				continue;
			}
			if (S_ISDIR(statbuf.st_mode)) {
				result = checkTree(subpath);
			} else if (S_ISREG(statbuf.st_mode)) {
				result = checkSingleFile(subpath, NULL, NULL);
			} else {
				vlog.verbose("%s (child (pid=%d)): neither dir nor file for mime type: \"%s\"", __func__, getpid(), subpath);
				continue;
			}
		} else {
			continue;
		}
		if (!result) {
			closedir(dirstream);
			return false;
		}
	}
	closedir(dirstream);
	return true;
}

// Unpack and check all files in dir
bool SAutotransferMime::unpackAndCheck(char *name)
{
	char unpackdirname[1024];
	char unpackcmd[4096];
	int err;
	bool result;
	struct stat statbuf;
	pid_t pid;

	if(lstat(name, &statbuf) < 0) {
		vlog.info("%s (child (pid=%d)): failed to stat file \"%s\" for mime type, not unpacking: \"%s\"", __func__, getpid(), name, strerror(errno));
		syslog(LOG_DEBUG, "failed to stat file \"%s\" for mime type, not unpacking: \"%s\"", name, strerror(errno));
		return false;
	}
	if (statbuf.st_size > MAXUNPACKSIZE) {
		vlog.verbose("%s (child (pid=%d)): size of file \"%s\" is bigger than %u for mime type, not unpacking", __func__, getpid(), name, MAXUNPACKSIZE);
		return false;
	}
	snprintf(unpackdirname, 1023, "/tmp/.auto-transfer.XXXXXX");
	unpackdirname[1023] = 0;
	if (mkdtemp(unpackdirname) == NULL) {
		vlog.info("%s (child (pid=%d)): failed to create temp dir \"%s\" for mime type: %s", __func__, getpid(), unpackdirname, strerror(errno));
		syslog(LOG_DEBUG, "failed to create temp dir \"%s\" for mime type: %s", unpackdirname, strerror(errno));
		return false;
	}

	snprintf(unpackcmd, 4095, "/usr/bin/timeout -k 5 500 /usr/local/bin/checker-unpack %u \"%s\" \"%s\" 60 &>/dev/null", MAXUNPACKLEVEL, name, unpackdirname);
	unpackcmd[4095] = 0;
	err = system(unpackcmd);
	if (err) {
		vlog.info("%s (child (pid=%d)): failed to unpack \"%s\" for mime type", __func__, getpid(), name);
		syslog(LOG_DEBUG, "failed to unpack \"%s\" for mime type", name);
		result = false;
	} else {
		result = checkTree(unpackdirname);
	}
	pid = fork();
	if (pid <= 0) {
		if (pid == 0)
			daemon(0, 0);
		snprintf(unpackcmd, 4095, "/usr/bin/rm -rf \"%s\"", unpackdirname);
		system(unpackcmd);
		if (pid == 0)
			exit(0);
	}
	waitpid(-1, NULL, WNOHANG);
	return result;
}

int SAutotransferMime::checkMagic(char* filename) {
	struct timeval tv;

	gettimeofday(&tv, NULL);
	if (lastAllowedMimeTypesRead + 5 < tv.tv_sec) {
		vlog.debug("%s: reload allowed MIME type list for user %s, last read: %lu, now: %lu", __func__, getenv("USER"), lastAllowedMimeTypesRead, tv.tv_sec);
		loadAllowedMimeTypes();
	}

	vlog.debug("%s (child (pid=%d)): Running check magic as a separate process (in case it dies) for file '%s'", __func__, getpid(), filename);

	int pid = fork();
	if (pid == 0){
		bool magicOk;
		char* mimeType;
		char* reason = NULL;

		// I am the child
		vlog.verbose("%s (child (pid=%d)): Running in a separate process (fork)", __func__, getpid());

		if (!magicSet) {
			_exit(2);
		}

		magicOk = checkSingleFile(filename, &mimeType, &reason);

		/* try to unpack ? */
		if (!magicOk && mimeType) {
			char unpackMime[100];
			bool doUnpack = false;

			snprintf(unpackMime, 99, "unpack/%s", mimeType);
			unpackMime[99] = 0;
			for (const char* allowedMimeType : allowedMimeTypes) {
				vlog.verbose("%s (child (pid=%d)): Comparing allowed MIME type '%s' with unpack MIME type '%s'", __func__, getpid(), allowedMimeType, unpackMime);
				if (!strncasecmp(unpackMime, allowedMimeType, strlen(allowedMimeType))) {
					// It is a type to be unpacked
					vlog.debug("%s (child (pid=%d)): File's detected MIME type '%s' is allowed", __func__, getpid(), unpackMime);
					doUnpack = true;
					break;
				}
			}
			if (doUnpack) {
				vlog.debug("%s (child (pid=%d)): unpack: mime type: %s, file: \"%s\"",  __func__, getpid(), mimeType, filename);
				syslog(LOG_DEBUG, "unpack: mime type: %s, file: \"%s\"", mimeType, filename);
				magicOk = unpackAndCheck(filename);
			}
		}

		logTransferIfNecessary(filename, mimeType ? mimeType : "unknown", reason, magicOk);
		if (mimeType)
			free(mimeType);
		if (reason)
			free(reason);
		if (magicOk) {
			vlog.verbose("%s (child (pid=%d)): '%s' allowed", __func__, getpid(), filename);
			_exit(0);
		} else {
			vlog.verbose("%s (child (pid=%d)): '%s' NOT allowed!", __func__, getpid(), filename);
			_exit(1);
		}
	} else if (pid > 0) {
		// I am the parent
		vlog.verbose("%s (parent): waiting for checkMagic to run in a separate process (fork) with pid %d for file '%s'", __func__, pid, filename);

		unsigned int waited = 0;
		unsigned const int waitPeriod = 300; // in ms
		unsigned const int timeout = 600000; // in ms
		int status;

		while (waited < timeout) {
			usleep(waitPeriod * 1000);
			waited += waitPeriod;
			pid_t waitpidResult = waitpid(pid, &status, WNOHANG);
			if (waitpidResult == -1) {
				vlog.error("%s (parent): child (pid=%d). Error while executing waitpid.", __func__, pid);
				break;
			}

			if (waitpidResult == 0) {
				// man: if WNOHANG was specified and one or more child(ren) specified by pid exist, but have not yet changed state, then 0 is returned.
				// So we have to check WIFEXITED *and* waitpid's returned value
				vlog.error("%s (parent): child (pid=%d). We were probably too fast running waitpid. Will try again in %u ms.", __func__, pid, waitPeriod);
			} else if (waitpidResult > 0) { // Status of child changed
				vlog.verbose("%s (parent): child (pid=%d). status: %d. waitpidResult: %d", __func__, pid, status, waitpidResult);
				if (WIFEXITED(status)) {
					vlog.verbose("%s (parent): Child (pid=%d) exited, status=%d (slept %u ms)\n", __func__, pid, WEXITSTATUS(status), waited);
					if (WEXITSTATUS(status) == 0) {
						return MAGIC_ALLOWED;
					} else {
						return MAGIC_NOT_ALLOWED;
					}
				} else {
					// The child's status changed but didn'e exit. Assuming it is dead.
					break;
				}
			}
		}
		// We only reach this code if the timeout is reached and it means we weren't able to read a status from the child.
		vlog.error("%s (parent): Child (pid=%d) not responding after timeout. Waited %u ms. Killing it", __func__, pid, waited);
		kill(pid, SIGKILL);
		vlog.error("%s (parent): Child (pid=%d) checkMagic failed for file '%s'", __func__, pid, filename);
		syslog(LOG_DEBUG, "checkMagic timed out for file '%s'", filename);
	}

	return MAGIC_FAILED;
}

void SAutotransferMime::logTransferIfNecessary(const char* filename, const char* mimeType, const char* reason, const bool magicOk) {
	if (!magicOk || logAutotransfer) {
		const unsigned int TRANSLOGLEN = 4096;
		const char* direction = "download"; // This is just here because I copy/pasted it from the sftp-server and prefer to keep the snprintf
		const char* allowed = magicOk ? "allowed" : "rejected";

		char sha256_text[75];
		*sha256_text = 0;
		if (calcAutotransferChecksum) {
			char checksum[65];
			*checksum = 0;
			rfb::sha256HashFile(filename, checksum);
			checksum[64] = 0;
			if (*checksum) {
				vlog.debug("%s: calculated checksum for file: %s", __func__, checksum);
				snprintf(sha256_text, 75, " (sha256 %s)", checksum);
			} else {
				vlog.error("%s: failed to calculate checksum for file: %s", __func__, filename);
			}
		}
		char* new_username = resolve_username_for_logs();
		if (!new_username) {
			vlog.error("%s: Failed to resolve username for logs. Can't log to transfer.log", __func__);
			return;
		}
		char* new_filename = replace_username_dir_for_logs(filename, new_username);
		if (!new_filename) {
			vlog.error("%s: Failed to replace username in dir for logs (for %s). Can't log to transfer.log", __func__, new_username);
			free(new_username);
			return;
		}

		char log[TRANSLOGLEN];
		if (reason)
			snprintf(log, TRANSLOGLEN, "\"%s\"%s mime type \"%s\", %s %s for user %s, reason: %s", new_filename, sha256_text, mimeType, allowed, direction, new_username, reason);
		else
			snprintf(log, TRANSLOGLEN, "\"%s\"%s mime type \"%s\", %s %s for user %s", new_filename, sha256_text, mimeType, allowed, direction, new_username);

		free(new_username);
		free(new_filename);

		log[TRANSLOGLEN - 1] = 0;
		syslog(LOG_DEBUG, "%s", log);
	}
}

void SAutotransferMime::loadAllowedMimeTypes() {
	char magicpath[MAXPATHLEN];
	struct timeval tv;

	allowedMimeTypes.clear();
	gettimeofday(&tv, NULL);
	lastAllowedMimeTypesRead = tv.tv_sec;

	/* Try to open MAGIC_CONF_DIR/name_magic, then
	 * MAGIC_CONF_DIR/uid_magic, else
	 * MAGIC_CONF_DIR/default_magic.conf
	 * If none exists, proceed without restrictions!
	 */
	struct passwd* pw = getpwuid(geteuid());
	snprintf(magicpath, sizeof(magicpath), "%s/%s_magic", MAGIC_CONF_DIR, pw->pw_name);
	magicpath[sizeof(magicpath) - 1] = 0;
	if (!file_exists(magicpath)) {
		snprintf(magicpath, sizeof(magicpath), "%s/%d_magic", MAGIC_CONF_DIR, pw->pw_uid);
		vlog.debug("file %s does not exist, setting allowed MIME types to NONE.", magicpath);
		allowedMimeTypes.push_front(strdup("NONE"));
	} else {
		FILE* mfile;
		char line[256];

		vlog.debug("Reading magic list from: %s", magicpath);
		mfile = fopen(magicpath, "r");
		while (fgets(line, sizeof(line), mfile) != NULL) {
			if(line[0] == '#' || line[0] == '\n') {
				vlog.verbose("%s: Skipping line (starts with # or is empty)", __func__);
				continue;
			}
			line[strlen(line) - 1] = '\0'; // Remove newline char (last char)
			vlog.verbose("%s: Adding MIME type to list of allowed types: %s", __func__, line);
			allowedMimeTypes.push_front(strdup(line));
		}
		fclose(mfile);
	}
}

void SAutotransferMime::initMagicFile() {
	const int magicFlags = MAGIC_MIME|MAGIC_SYMLINK;
	magicSet = magic_open(magicFlags);
	if(!magicSet) {
		vlog.error("Failed to open magic. You won't be able to transfer any files using multi!");
		return;
	}
	if (magic_load(magicSet, NULL) == -1) {
		vlog.error("Failed to initialize magic. You won't be able to transfer any files using multi!");
		magic_close(magicSet);
		return;
	}
	loadAllowedMimeTypes();
	packerCheckTypes.push_back("application/pdf");
	packerCheckTypes.push_back("audio/");
	packerCheckTypes.push_back("chemical/");
	packerCheckTypes.push_back("font/");
	packerCheckTypes.push_back("image/");
	packerCheckTypes.push_back("message/");
	packerCheckTypes.push_back("model/");
	packerCheckTypes.push_back("rinex/");
	packerCheckTypes.push_back("text/");
	packerCheckTypes.push_back("video/");
	packerCheckTypes.push_back("x-epoc/");
}

#endif /* #if !defined(WIN32) && !defined(WIN64) && !defined(__APPLE__) */
