/*
 * Copyright (C) 2004 Red Hat Inc.
 * Copyright (C) 2005 Martin Koegler
 * Copyright (C) 2010 TigerVNC Team
 * Copyright (C) 2010-2026 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.
 */

#ifdef WIN32
#include <winsock2.h>
#endif

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

#include <stdlib.h>
#ifndef WIN32
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <vncviewer/MagicUrlHelper.h>
#include <sys/time.h>
#include <sys/resource.h>
#endif
#include <errno.h>
#ifdef WIN32
#include <rfb/WinErrors.h>
#endif
#if defined(__APPLE__)
#include </usr/SDK/MacOSX10.15.sdk/usr/include/c++/v1/math.h>
#include </usr/SDK/MacOSX10.15.sdk/usr/include/c++/v1/cmath>
#include </usr/SDK/MacOSX10.15.sdk/usr/include/c++/v1/queue>
#include </usr/SDK/MacOSX10.15.sdk/usr/include/c++/v1/unordered_map>
#else
#include <math.h>
#include <cmath>
#include <queue>
#include <unordered_map>
#endif
/* Old systems have select() in sys/time.h */
#ifdef HAVE_SYS_SELECT_H
#include <sys/select.h>
#endif

#include <rfb/CSecurityMulti.h>
#include <rfb/SSecurityVeNCrypt.h>
#include <rfb/CConnection.h>
#include <rfb/LogWriter.h>
#include <rfb/Exception.h>
#include <rfb/Configuration.h>
#include <rfb/MP.h>
#include <rdr/MultiInStream.h>
#include <rdr/MultiOutStream.h>
#include <rdr/MultiStream.h>
#include <rdr/types.h>
#include <rdr/mutex.h>
#include <os/os.h>
#include <vncviewer/printing.h>
#include <vncviewer/parameters.h>
#include <vncviewer/CAutotransferHandler.h>
#include <rfb/CMsgWriter.h>
#include <rfb/CWebcamHandler.h>
#include "tgpro_environment.h"
#include "mp_utils.h"

#ifdef WIN32
static const char PATH_DIVIDER = '\\';
#define closesocket close
#else
static const char PATH_DIVIDER = '/';
#define Sleep(x) usleep((x)*1000)
#endif

static const char* PRINT_TMP_SUBDIR = "print";
static const char* AUTOTRANSFER_TMP_SUBDIR = "autotransfer";

using namespace rfb;
using namespace rdr;

#define MINPABUF 15 * 1024
#define OLDPABUF 16 * 1024 - 256
#define DEFPABUF 256 * 1024
#define MAXPABUF 10 * 1024 * 1024

IntParameter paReadBufferSize("PaReadBufferSize", "Buffer size for reads from Pulseaudio socket (min. 15KB, sanity limit at 10MB)", DEFPABUF, MINPABUF, MAXPABUF);
StringParameter paUnixSocket("PaUnixSocket", "Path to native Unix socker", "", rfb::ConfViewer);
IntParameter keepAliveTimeout("KeepAliveTimeout", "Keep-alive timeout in seconds (0 to disable)", 150, 0);
IntParameter multiInBufferSize("multiInBufferSize", "Multi input buffer size", 1024 * 1024, 128 * 1024, 16 * 1024 * 1024);
IntParameter audioCheckIdleInterval("AudioCheckIdleInterval", "time in seconds between audio idle checks (0 to disable)", 2, 0, 600);
IntParameter audioMaxIdle("AudioMaxIdle", "time in seconds audio channels may stay idle (0 to disable)", 600, 0, 86400);
static U32 paReadBufferSizeInt;
static U32 keepAliveTimeoutInt;
static U32 audioCheckIdleIntervalInt;
#if !defined(WIN32) && !defined(WIN64) && !defined(__APPLE__)
static U32 magicurlBufferSizeInt = 4096;
#endif

static LogWriter vlog("CSecurityMulti");

#if !defined(WIN32) && !defined(WIN64) && !defined(__APPLE__)
int CSecurityMulti::magicurlInSocket = -1;
THREAD_ID CSecurityMulti::magicurlListenerThreadId;
#endif

bool CSecurityMulti::runThreads = true;
THREAD_ID CSecurityMulti::fileWriterThreadId;
THREAD_ID paReadThreadId;
MUTEX_TYPE CSecurityMulti::fileQueueMutex;
std::queue<fileQueueData> CSecurityMulti::fileQueue;
handleToSocketMap_t CSecurityMulti::handleToSocketMap;
handleTimeMap_t CSecurityMulti::handleTimeMap;
bufferMap_t CSecurityMulti::paBufferMap;
bufferLenMap_t CSecurityMulti::paBufferLenMap;

rdr::MultiInStream* CSecurityMulti::multiIS = NULL;
rdr::MultiOutStream* CSecurityMulti::multiOS = NULL;
char* tgproTmpPath = NULL;
rdr::U8 CSecurityMulti::congestionLevel = 0;

MUTEX_TYPE paConnectMutex;

TGVNC_CONDITION_TYPE CSecurityMulti::packetsWaitingCondition;
MUTEX_TYPE CSecurityMulti::packetsWaitingConditionLock;
MUTEX_TYPE CSecurityMulti::mapLock;

static CWebcamHandler* webcamHandler = NULL;

static CSecurityMulti * signalTarget = NULL;

#if !defined(WIN32) && !defined(WIN64) && !defined(__APPLE__)
THREAD_FUNC CSecurityMulti::magicurlReadThread(void* _sock) {
	int sock = (U64) _sock;
	char * readMagicurlBuffer;
	U32 curBufferSize = magicurlBufferSizeInt;

	vlog.debug("CSecurityMulti::magicurlReadThread %lu created for incoming connection %u with buffer size %u", gettid(), sock, curBufferSize);
	readMagicurlBuffer = (char *) malloc(curBufferSize + 1);
	if (unlikely(!readMagicurlBuffer)) {
		vlog.error("CSecurityMulti::magicurlReadThread %lu cannot allocate buffer of size %u for connection %u", gettid(), curBufferSize + 1, sock);
		close(sock);
		THREAD_EXIT(THREAD_NULL);
	}

	if (read(sock, readMagicurlBuffer, curBufferSize) > 0) {
		signalTarget->cc->writer()->sendURL(readMagicurlBuffer);
	}
	free(readMagicurlBuffer);
	close(sock);
	THREAD_EXIT(THREAD_NULL);
}

THREAD_FUNC CSecurityMulti::magicurlListenerThread(void* _myself) {
	CSecurityMulti* myself = (CSecurityMulti*) _myself;
	vlog.debug("magicurlListenerThread %lu created", gettid());

	long newConnection;
	fd_set fds;
	struct timeval tv;
	THREAD_ID threadId;
	int n;

	while (runThreads) {
		FD_ZERO(&fds);
		FD_SET(myself->magicurlInSocket, &fds);
		tv.tv_sec = 20;
		tv.tv_usec = 0;
		n = select(myself->magicurlInSocket+1, &fds, NULL, NULL, &tv);
		if (n > 0) {
			newConnection = accept(myself->magicurlInSocket, NULL, NULL);
			if (newConnection < 0) {
				vlog.error("magicurlListenerThread %lu: accept failed with error %s", gettid(), strerror(errno));
			} else {
				vlog.debug("magicurlListenerThread %lu accepted new incoming connection %lu", gettid(), newConnection);
				THREAD_CREATE(magicurlReadThread, threadId, (void *) newConnection);
				THREAD_SET_NAME(threadId, "tg-magicurlRead");
			}
		} else if (n < 0) {
			if (errno != EINTR) {
				vlog.error("magicurlListenerThread %lu: select failed with error %s", gettid(), strerror(errno));
				THREAD_EXIT(THREAD_NULL);
			}
		}
	}
	THREAD_EXIT(THREAD_NULL);
}

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

CSecurityMulti::CSecurityMulti(CConnection* cc) : CSecurity(cc), keepAliveTimer(this), runtimeAutotransferSupport(autotransferSupport)
	, checkIdleTimer(this)
{
	signalTarget = this;
	MUTEX_INIT(&packetsWaitingConditionLock);
	TGVNC_CONDITION_INIT(&packetsWaitingCondition);
	MUTEX_INIT(&mapLock);
	MUTEX_INIT(&paConnectMutex);
	MUTEX_INIT(&fileQueueMutex);
	THREAD_CREATE(fileWriterThread, fileWriterThreadId, this);
	THREAD_SET_NAME(fileWriterThreadId, "tg-fileWriter");

	if (paReadBufferSize > OLDPABUF)
		paReadBufferSizeInt = OLDPABUF;
	else if (paReadBufferSize < MINPABUF)
		paReadBufferSizeInt = MINPABUF;
	else
		paReadBufferSizeInt = paReadBufferSize;

	keepAliveTimeoutInt = keepAliveTimeout * 1000;
	audioCheckIdleIntervalInt = audioCheckIdleInterval * 1000;
#if !defined(WIN32) && !defined(WIN64) && !defined(__APPLE__)
	magicurlInSocket = socket(AF_UNIX, SOCK_STREAM, 0);
	if (magicurlInSocket < 0) {
		vlog.error("Could not create socket for MagicURL.");
		return;
	}
	magicurlInSockaddr.sun_family = AF_UNIX;
	char *magicurl_socket_path = MagicUrlHelper::get_magicurl_socket_path();
	if (!magicurl_socket_path) {
		vlog.error("Creating MagicURL socket path failed.");
		close(magicurlInSocket);
		magicurlInSocket = -1;
		return;
	}
	strncpy(magicurlInSockaddr.sun_path, magicurl_socket_path, 107);
	free(magicurl_socket_path);
	unlink(magicurlInSockaddr.sun_path);
	const size_t magicurlInSockAddrLen = strlen(magicurlInSockaddr.sun_path) + sizeof(magicurlInSockaddr.sun_family);
	mode_t saved_umask;
	saved_umask = umask(0177);
	if (bind(magicurlInSocket, (struct sockaddr *)&magicurlInSockaddr, magicurlInSockAddrLen) < 0) {
		umask(saved_umask);
		vlog.error("Bind unix socket for MagicURL %s failed: %s.", magicurlInSockaddr.sun_path, strerror(errno));
		close(magicurlInSocket);
		magicurlInSocket = -1;
		return;
	}
	umask(saved_umask);

	if (listen(magicurlInSocket, 5) < 0) {
		vlog.error("Listen on socket for MagicURL %s failed: %s", magicurlInSockaddr.sun_path, strerror(errno));
		close(magicurlInSocket);
		magicurlInSocket = -1;
		return;
	}
	vlog.debug("Unix socket for MagicURL created: %s.", magicurlInSockaddr.sun_path);

	THREAD_CREATE(magicurlListenerThread, magicurlListenerThreadId, this);
	THREAD_SET_NAME(magicurlListenerThreadId, "tg-magicurlListener");
#endif
}

/*As far as I can see this destructor is never called.*/
CSecurityMulti::~CSecurityMulti() {
	signalTarget = NULL;
	keepAliveTimer.stop();
	checkIdleTimer.stop();
	runThreads = false;
	THREAD_JOIN(fileWriterThreadId);
	TGVNC_CONDITION_DESTROY(&packetsWaitingCondition);
	MUTEX_DESTROY(&packetsWaitingConditionLock);
	MUTEX_DESTROY(&mapLock);
//	printing::stop_mptimer();
	stopSound();
	MUTEX_DESTROY(&fileQueueMutex);
	delete multiIS;
	delete multiOS;
#ifdef WIN32
	delete webcamHandler;
	WSACleanup();
#endif
#if !defined(WIN32) && !defined(WIN64) && !defined(__APPLE__)
	if (magicurlInSocket > -1) {
		vlog.debug("Magicurl removing socket.");
		close(magicurlInSocket);
	}
	unlink(magicurlInSockaddr.sun_path);
	THREAD_JOIN(magicurlListenerThreadId);
#endif
}


void CSecurityMulti::stopSound() {
	runThreads = false;
	THREAD_JOIN(paReadThreadId);
}

#if defined(WIN32) || defined(WIN64)
// Convert an UTF8 string to a wide Unicode String

#endif


THREAD_FUNC CSecurityMulti::fileWriterThread(void* _me) {
	CSecurityMulti* myself = (CSecurityMulti*)_me;
	bool runWriter = true;
#if defined(WIN32) || defined(WIN64)
	vlog.debug("fileWriterThread %lu created", GetCurrentThreadId());
#elif defined(__APPLE__)
	vlog.debug("fileWriterThread %u created", gettid());
#else
	vlog.debug("fileWriterThread %lu created", gettid());
#endif

	char* lastFilename = strdup("");
	FILE* file = NULL;
	fileQueueData writeData;
	memset(&writeData, 0, sizeof(writeData));

	while(runWriter) {
		bool newWriteData;

		MUTEX_LOCK(&myself->fileQueueMutex);
		if (!myself->fileQueue.empty()) {
			writeData = myself->fileQueue.front();
			myself->fileQueue.pop();
			newWriteData = true;
		} else
			newWriteData = false;
		MUTEX_UNLOCK(&myself->fileQueueMutex);

		if (newWriteData) {
			if (strcmp(lastFilename, writeData.filename)) {
				if (file) {
					vlog.debug("%s: pausing writing of '%s' because a packet from another file '%s' just arrived", __func__, lastFilename, writeData.filename);
					// We explicitely set file to NULL while closing it. This means we didn't finish writing to lastFilename so we have to close it.
					fclose(file);
					file = NULL;
				}
				free(lastFilename);
				lastFilename = strdup(writeData.filename);
			}
			if (!file) {
#if defined(WIN32) || defined(WIN64)
				wchar_t* filename = utf8_char_to_wchar_t(writeData.filename);
				file = _wfopen(filename, writeData.packetNumber ? L"ab" : L"wb");
				free(filename);
#else
				file = fopen(writeData.filename, writeData.packetNumber ? "ab" : "wb");
#endif
				if (!file) {
					vlog.error("I could not open file to write to: %s", writeData.filename);
					vlog.error("Error message from OS: %s", strerror(errno));
					continue;
				}
			}

			const size_t writtenBytes = fwrite(writeData.data, 1, writeData.len, file);

			if (writeData.len != writtenBytes) {
				vlog.error("Error writing to file %s. I should have written %zu bytes, but I did write %zu bytes.", writeData.filename, writeData.len, writtenBytes);
				vlog.error("Error message from OS: %s", strerror(errno));
				continue;
			}

			if (writeData.finalPacket) {
				struct stat buffer;

				fclose(file);
				file = NULL;
				free(lastFilename);
				lastFilename = strdup("");

				if (stat(writeData.filename, &buffer) == 0) {
					vlog.debug("%s: received final packet for '%s', size %zu", __func__, writeData.filename, buffer.st_size);
				} else {
					vlog.debug("%s: received final packet for '%s', size unknown.", __func__, writeData.filename);
				}

				switch (writeData.fileTransferProtocol) {
					case PRINT_PROTOCOL_V1:
						vlog.debug("PRINT_PROTOCOL_V1: printing '%s'", writeData.filename);
						printing::print_pdf_thread(writeData.filename);
						break;
					case AUTOTRANSFER_PROTOCOL_V1:
						vlog.debug("AUTOTRANSFER_PROTOCOL_V1: moving '%s' to autotransfer user folder", writeData.filename);
						CAutotransferHandler::addToProcessingQueue(strdup(writeData.filename));
						break;
					default:
						vlog.error("Unknown file transfer protocol. Deleting file '%s'", writeData.filename);
						if (unlink(writeData.filename)) {
							vlog.error("Could not delete '%s'", writeData.filename);
						} else {
							vlog.debug("Deleted '%s'", writeData.filename);
						}
				}
			}

			delete[] writeData.filename;
			delete[] writeData.data;
		} else {
			if (runThreads) {
				bool packetsWaiting = false;
				MUTEX_LOCK(&myself->fileQueueMutex);
				packetsWaiting = !myself->fileQueue.empty();
				MUTEX_UNLOCK(&myself->fileQueueMutex);
				if (!packetsWaiting) {
					// If there are no packets waiting, wait to be woken
					MUTEX_LOCK(&packetsWaitingConditionLock);
					TGVNC_CONDITION_WAIT(&packetsWaitingCondition, &packetsWaitingConditionLock);
					MUTEX_UNLOCK(&packetsWaitingConditionLock);
				}
			}
			runWriter = runThreads;
		}
	}
	THREAD_EXIT(THREAD_NULL);
}


int CSecurityMulti::connectPa(rdr::U32 handle) {
#ifdef WIN32
	WORD requiredVersion = MAKEWORD(2,0);
	WSADATA initResult;

	if (WSAStartup(requiredVersion, &initResult))
		throw network::SocketException("Ich kann Winsock2 nicht initialisieren", WSAGetLastError());

	SOCKET newSocket = socket(AF_INET, SOCK_STREAM, 0);
	if (newSocket == INVALID_SOCKET) {
		vlog.error("connectPa(): Could not create socket for Pulseaudio.");
		return -1;
	}

	struct sockaddr_in msockaddr;
	msockaddr.sin_family = AF_INET;
	char ip[] = "127.0.0.1";
	msockaddr.sin_addr.S_un.S_addr = inet_addr(ip);
	char * paPort = getenv("PAPORT");
	if (paPort) {
		msockaddr.sin_port = htons(strtoul(paPort, NULL, 10));
		vlog.debug("connectPa(): took Pulseaudio TCP port %u from environment variable PAPORT.", ntohs(msockaddr.sin_port));
	} else {
		msockaddr.sin_port = htons(4713);
		vlog.debug("connectPa(): using default Pulseaudio TCP port 4713, no environment variable PAPORT found.");
	}
	if (connect(newSocket, (const sockaddr*)&msockaddr, sizeof(msockaddr)) < 0) {
		vlog.error("connectPa(): TCP connect for handle %u to Pulseaudio at 127.0.0.1:%u failed: %s.", handle, ntohs(msockaddr.sin_port), strerror(errno));
		close(newSocket);
		return -1;
	} else {
		vlog.debug("connectPa(): connected handle %u with local TCP socket %u to Pulseaudio at 127.0.0.1:%u", handle, (unsigned int) newSocket, ntohs(msockaddr.sin_port));
	}
	unsigned long mode = 1;
	if (ioctlsocket(newSocket, FIONBIO, &mode) != NO_ERROR)
		vlog.debug("connectPa(): Failed to set new socket %u to non-blocking for handle %u", (unsigned int) newSocket, handle);

#elif defined(__APPLE__)

	struct sockaddr_in msockaddr;
	int newSocket;

	newSocket = socket(AF_INET, SOCK_STREAM, 0);
	if (newSocket < 0) {
		vlog.error("connectPa(): Could not create socket for Pulseaudio for handle %u.", handle);
		return -1;
	}

	msockaddr.sin_family = AF_INET;
	msockaddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
	char * paPort = getenv("PAPORT");
	if (paPort) {
		msockaddr.sin_port = htons(strtoul(paPort, NULL, 10));
		vlog.debug("connectPa(): took Pulseaudio TCP port %u from environment variable PAPORT for handle %u", ntohs(msockaddr.sin_port), handle);
	} else {
		msockaddr.sin_port = htons(4713);
		vlog.debug("connectPa(): using default Pulseaudio TCP port 4713 for handle %u, no environment variable PAPORT found.", handle);
	}

	if (connect(newSocket, (const sockaddr*)&msockaddr, sizeof(msockaddr)) < 0) {
		vlog.error("connectPa(): TCP connect to %s:%u for Pulseaudio for handle %u failed: %s.", inet_ntoa(msockaddr.sin_addr), ntohs(msockaddr.sin_port), handle, strerror(errno));
		close(newSocket);
		return -1;
	} else {
		vlog.debug("connectPa(): connected handle %u with local TCP socket %u to Pulseaudio at %s:%u", handle, newSocket, inet_ntoa(msockaddr.sin_addr), ntohs(msockaddr.sin_port));
	}
	fcntl(newSocket, F_SETFL, fcntl(newSocket, F_GETFL, 0) | O_NONBLOCK);

#else /* neither WIN32 nor __APPLE__ */

	struct sockaddr_un usockaddr;
	int newSocket = socket(AF_UNIX, SOCK_STREAM, 0);
	if (unlikely(newSocket < 0)) {
		vlog.error("connectPa(): Could not create socket for Pulseaudio for handle %u.", handle);
		return -1;
	}

	usockaddr.sun_family = AF_UNIX;
	if (strlen(paUnixSocket) > 0) {
		snprintf(usockaddr.sun_path, 108, "%s", paUnixSocket.getValueStr());
	} else {
		char* unixpath = getenv("PULSE_SERVER");
		if (unixpath && !strncmp(unixpath, "unix:", 5))
			snprintf(usockaddr.sun_path, 108, "%s", unixpath + 5);
		else
			snprintf(usockaddr.sun_path, 108, "/run/user/%u/pulse/native", getuid());
	}
	usockaddr.sun_path[107] = 0;

	if (unlikely(connect(newSocket, (const sockaddr*)&usockaddr, sizeof(usockaddr)) < 0)) {
		vlog.debug("connectPa(): Connect to Unix socket %s for Pulseaudio for handle %u failed, so retry with TCP: %s.", usockaddr.sun_path, handle, strerror(errno));
		close(newSocket);
		newSocket = socket(AF_INET, SOCK_STREAM, 0);
		if (newSocket < 0) {
			vlog.error("connectPa(): Could not create AF_INET socket for Pulseaudio for handle %u.", handle);
			return -1;
		}
		struct sockaddr_in msockaddr;
		msockaddr.sin_family = AF_INET;
		msockaddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
		char * paPort = getenv("PAPORT");
		if (paPort) {
			msockaddr.sin_port = htons(strtoul(paPort, NULL, 10));
			vlog.debug("connectPa(): took Pulseaudio TCP port %u from environment variable PAPORT for handle %u.", ntohs(msockaddr.sin_port), handle);
		} else {
			msockaddr.sin_port = htons(4713);
			vlog.debug("connectPa(): using default Pulseaudio TCP port 4713 for handle %u, no environment variable PAPORT found.", handle);
		}

		int two = 2;
		if (setsockopt(newSocket, IPPROTO_TCP, TCP_SYNCNT, (void *)&two, sizeof(two)) < 0) {
			vlog.info("unable to set TCP_SYNCNT on connection to Pulseaudio for handle %u", handle);
		}
		if (connect(newSocket, (const sockaddr*)&msockaddr, sizeof(msockaddr)) < 0) {
			vlog.error("connectPa(): TCP connect to %s:%u for Pulseaudio for handle %u failed: %s.", inet_ntoa(msockaddr.sin_addr), ntohs(msockaddr.sin_port), handle, strerror(errno));
			close(newSocket);
			return -1;
		}
		vlog.debug("connectPa(): connected handle %u with local TCP socket %u to Pulseaudio at %s:%u", handle, newSocket, inet_ntoa(msockaddr.sin_addr), ntohs(msockaddr.sin_port));
	} else
		vlog.debug("connectPa(): connected handle %u with local UNIX socket %u to Pulseaudio at %s", handle, newSocket, usockaddr.sun_path);
	fcntl(newSocket, F_SETFL, fcntl(newSocket, F_GETFL, 0) | O_NONBLOCK);

#endif /* end of system type */

	U32 * handle_p = (U32 *) malloc(4);
	*handle_p = handle;
	MUTEX_LOCK(&mapLock);
	handleToSocketMap[handle] = newSocket;
	MUTEX_UNLOCK(&mapLock);
	THREAD_CREATE(paReadThread, paReadThreadId, (void *) handle_p);
	char threadName[16];
	snprintf(threadName, 15, "tg-paLst-%u", handle);
	threadName[15] = 0;
	THREAD_SET_NAME(paReadThreadId, threadName);
	return newSocket;
}

THREAD_FUNC CSecurityMulti::paReadThread(void* _handle) {
	const U32 handle = *((U32 *) _handle);
	const int curBufferSize = paReadBufferSizeInt;
	fd_set fds;
	int n = 0;
	ssize_t nBytes = 0;
	ssize_t totalNBytes = 0;
	U32 nReads = 0;
	ssize_t maxNBytes = 0;
	ssize_t minNBytes = curBufferSize;
	struct timeval tv;

	free(_handle);

	if (!soundSupport) {
		vlog.debug("paReadThread(): no sound support with handle %u, exiting!", handle);
		THREAD_EXIT(THREAD_NULL);
	}
	MUTEX_LOCK(&mapLock);
	const int sock = handleToSocketMap[handle];
	MUTEX_UNLOCK(&mapLock);
	if (unlikely(!sock)) {
		vlog.error("paReadThread(): invalid handle %u, exiting!", handle);
		THREAD_EXIT(THREAD_NULL);
	}
	vlog.debug("paReadThread(): starting for handle %u with buffer size %u", handle, curBufferSize);

	while (!multiOS) {
		vlog.error("paReadThread(): There is no outstream. I wait.");
		Sleep(1000);
		if (!runThreads)
			return 0;
	}

	char * readPaBuffer = (char *) malloc(curBufferSize + 4);
	if (unlikely(!readPaBuffer)) {
		vlog.error("paReadThread(): could not allocate buffer for handle %u", handle);
		THREAD_EXIT(THREAD_NULL);
	}
	*((U32 *) readPaBuffer) = handle;

#ifdef WIN32
	vlog.debug("paReadThread %lu: trying to raise thread priority", GetCurrentThreadId());
	SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL);

#else

#if defined(__APPLE__)
	vlog.debug("paReadThread %u: trying to raise thread priority", gettid());
#else
	vlog.debug("paReadThread %lu: trying to raise thread priority", gettid());
#endif
	errno = 0;
	n = nice(-10);
	if (n == -1 && errno != 0) {
#if defined(__APPLE__)
		vlog.debug("paReadThread %u: failed to set nice(-10), error: %s", gettid(), strerror(errno));
#else
		vlog.debug("paReadThread %lu: failed to set nice(-10), error: %s", gettid(), strerror(errno));
#endif
	}
#endif

	while (runThreads) {
		do {
			FD_ZERO(&fds);
			FD_SET(sock, &fds);
			tv.tv_sec = 20;
			tv.tv_usec = 0;
			n = select(sock+1, &fds, 0, 0, &tv);
#ifdef WIN32
		} while (n == SOCKET_ERROR && errno == WSAEINTR);
#else
		} while (n < 0 && errno == EINTR);
#endif

		if (unlikely(n < 0)) {
			vlog.debug("paReadThread(): select for handle %u on local Pulseaudio socket %u failed: %s", handle, sock, strerror(errno));
			/* Notify other end that stream is closed */
			multiOS->writeBuffer((U8*)readPaBuffer, 4, PULSEAUDIO_STREAM_ID);
			break;
		}
		if (n > 0) {
			nBytes = recv(sock, readPaBuffer + 4, curBufferSize, 0);
#ifdef WIN32
			if (nBytes == SOCKET_ERROR) {
				n = WSAGetLastError();
				if (n == WSAEWOULDBLOCK) {
					vlog.verbose("paReadThread(): would block for handle %u on local socket %u", handle, sock);
					continue;
				} else {
					vlog.debug("paReadThread(): Pulseaudio data read error for handle %u from local socket %u: %u (%s)", handle, sock, n, WinErrorName(n));
					/* Notify other end that stream is closed */
					multiOS->writeBuffer((U8*)readPaBuffer, 4, PULSEAUDIO_STREAM_ID);
					break;
				}
#else
			if (unlikely(nBytes < 0)) {
				if (   errno == EAGAIN
#if EAGAIN != EWOULDBLOCK
				    || errno == EWOULDBLOCK
#endif
				) {
					vlog.debug("paReadThread(): would block for handle %u on local socket %u", handle, sock);
					continue;
				} else {
					vlog.debug("paReadThread(): Pulseaudio data read error for handle %u from local socket %u: %s", handle, sock, strerror(errno));
					/* Notify other end that stream is closed */
					multiOS->writeBuffer((U8*)readPaBuffer, 4, PULSEAUDIO_STREAM_ID);
					break;
				}
#endif
			} else if (nBytes == 0) {
#ifndef WIN32
#if defined(__APPLE__)
				vlog.debug("paReadThread %u, handle %u, sock %u, read() returned 0, end of file", gettid(), handle, sock);
#else
				vlog.debug("paReadThread %lu, handle %u, sock %u, read() returned 0, end of file", gettid(), handle, sock);
#endif
#endif
				break;
			} else {
				if (nBytes == curBufferSize) {
#ifdef WIN32
					vlog.info("paReadThread %lu with handle %u used full buffer size %u, increase PaReadBufferSize!", GetCurrentThreadId(), handle, curBufferSize);
#else
#if defined(__APPLE__)
					vlog.info("paReadThread %u with handle %u used full buffer size %u, increase PaReadBufferSize!", gettid(), handle, curBufferSize);
#else
					vlog.info("paReadThread %lu with handle %u used full buffer size %u, increase PaReadBufferSize!", gettid(), handle, curBufferSize);
#endif
#endif
				}
//				vlog.debug("paReadThread(): sending handle %u, local socket %i, bytes %zu to outstream", handle, sock, nBytes);
				MUTEX_LOCK(&mapLock);
				if (!handleToSocketMap[handle]) {
					MUTEX_UNLOCK(&mapLock);
					vlog.error("paReadThread(): not sending handle %u, local socket %i, bytes %zu to outstream, because handle has become invalid", handle, sock, nBytes);
					break;
				}
				MUTEX_UNLOCK(&mapLock);
				multiOS->writeBuffer((U8*)readPaBuffer, nBytes + 4, PULSEAUDIO_STREAM_ID);
				if (unlikely(nBytes > maxNBytes))
					maxNBytes = nBytes;
				if (unlikely(nBytes < minNBytes))
					minNBytes = nBytes;
				nReads++;
				totalNBytes += nBytes;
				continue;
			}
		} else {
			vlog.debug("paReadThread(): select for handle %u on local Pulseaudio socket %u timed out", handle, sock);
		}
	}

	close(sock);
	MUTEX_LOCK(&mapLock);
	handleToSocketMap.erase(handle);
	handleTimeMap.erase(handle);
	paBufferMap.erase(handle);
	paBufferLenMap.erase(handle);
	MUTEX_UNLOCK(&mapLock);
	free(readPaBuffer);
#ifdef WIN32
	vlog.debug("paReadThread %lu finished handle %u, sock %u: min read %lu, max read %lu, total of %lu bytes in %u reads, avg %lu bytes per read", GetCurrentThreadId(), handle, sock, minNBytes, maxNBytes, totalNBytes, nReads, nReads > 0 ? totalNBytes/nReads : 0);
#else
#if defined(__APPLE__)
	vlog.debug("paReadThread %u finished handle %u, sock %u: min read %lu, max read %lu, total of %lu bytes in %u reads, avg %lu bytes per read", gettid(), handle, sock, minNBytes, maxNBytes, totalNBytes, nReads, nReads > 0 ? totalNBytes/nReads : 0);
#else
	vlog.debug("paReadThread %lu finished handle %u, sock %u: min read %lu, max read %lu, total of %lu bytes in %u reads, avg %lu bytes per read", gettid(), handle, sock, minNBytes, maxNBytes, totalNBytes, nReads, nReads > 0 ? totalNBytes/nReads : 0);
#endif
#endif
	THREAD_EXIT(THREAD_NULL);
}


bool CSecurityMulti::incomingPaCallback(const rdr::U8* buf, int bufLen) {
	if (unlikely(bufLen < 4)) {
		vlog.error("incomingPaCallback(%i bytes): buffer must have at least 4 bytes", bufLen);
		return true;
	}

	U32 handle = *((U32 *) buf);

	if (!soundSupport) {
		vlog.info("incomingPaCallback(%i bytes): I do not play incoming Pulseaudio sound. SoundSupport is off. Notifying server to close handle %u.", bufLen, handle);
		multiOS->writeBuffer(buf, 4, PULSEAUDIO_STREAM_ID);
		return true;
	}
//	vlog.debug("incomingPaCallback(): there comes Pulseaudio data with handle %u", handle);
	MUTEX_LOCK(&paConnectMutex);
	MUTEX_LOCK(&mapLock);
	int sock = handleToSocketMap[handle];
	MUTEX_UNLOCK(&mapLock);

	if (likely(bufLen > 4)) {
		int tmperr;
		struct timeval now;
		rdr::U8 * tmpBuffer;
// Only Windows has the complete socket set to non-blocking, because it does
// not support MSG_DONTWAIT
#ifdef WIN32
		const int flags = 0;
#else
		const int flags = MSG_DONTWAIT;
#endif

		if (unlikely(!sock)) {
			vlog.debug("incomingPaCallback(): no local socket for handle %u, connecting to Pulseaudio", handle);
			sock = connectPa(handle);
			if (sock < 0) {
				MUTEX_UNLOCK(&paConnectMutex);
				vlog.error("incomingPaCallback(): Connection to Pulseaudio failed: %s. Notifying server to close handle %u.", strerror(errno), handle);
				multiOS->writeBuffer(buf, 4, PULSEAUDIO_STREAM_ID);
				return true;
			}
		}
		if (likely(!gettimeofday(&now, NULL))) {
			MUTEX_LOCK(&mapLock);
			handleTimeMap[handle] = now.tv_sec;
			MUTEX_UNLOCK(&mapLock);
		}
		if (audioCheckIdleIntervalInt > 0 && !signalTarget->checkIdleTimer.isStarted()) {
			vlog.debug("starting checkIdleTimer with %u interval, audioMaxIdle %u", int(audioCheckIdleInterval), int(audioMaxIdle));
			signalTarget->checkIdleTimer.start(audioCheckIdleIntervalInt);
		}
		MUTEX_UNLOCK(&paConnectMutex);

		bufLen -= 4;
		char* paData = (char*)(buf + 4);

		MUTEX_LOCK(&mapLock);
		tmpBuffer = paBufferMap[handle];
		MUTEX_UNLOCK(&mapLock);
		if (tmpBuffer) {
			int tmpBufferLen;

			MUTEX_LOCK(&mapLock);
			tmpBufferLen = paBufferLenMap[handle];
			MUTEX_UNLOCK(&mapLock);
			vlog.debug("incomingPaCallback(): Found old buffer for handle %u and local sock %u of length %u, try sending now.", handle, sock, tmpBufferLen);
			tmperr = send(sock, (const char*) tmpBuffer, tmpBufferLen, flags);
			if (tmperr < 0) {
				rdr::U8 * newBuffer;

				switch(errno) {
					case EAGAIN:
#if EAGAIN != EWOULDBLOCK
					case EWOULDBLOCK:
#endif
						vlog.debug("incomingPaCallback(): Sending old buffer of length %u for handle %u to local socket %u would block, so append new buffer of length %u and return.", tmpBufferLen, handle, sock, bufLen);
						if (unlikely(tmpBufferLen + bufLen > MAXPABUFFERSIZE)) {
							vlog.error("incomingPaCallback(): buffer size would exceed sanity limit of %u, giving up. Notifying server to close handle %u.", MAXPABUFFERSIZE, handle);
							close(sock);
							free(tmpBuffer);
							MUTEX_LOCK(&mapLock);
							handleToSocketMap.erase(handle);
							paBufferMap.erase(sock);
							paBufferLenMap.erase(sock);
							MUTEX_UNLOCK(&mapLock);
							multiOS->writeBuffer(buf, 4, PULSEAUDIO_STREAM_ID);
							return true;
						}
						newBuffer = (rdr::U8 *) realloc(tmpBuffer, tmpBufferLen + bufLen);
						if (unlikely(!newBuffer)) {
							vlog.error("incomingPaCallback(): Failed to realloc buffer for handle %u, return failure", handle);
							return false;
						}
						memcpy(newBuffer + tmpBufferLen, paData, bufLen);
						MUTEX_LOCK(&mapLock);
						paBufferMap[handle] = newBuffer;
						paBufferLenMap[handle] = tmpBufferLen + bufLen;
						MUTEX_UNLOCK(&mapLock);
						return true;
					default:
						vlog.error("incomingPaCallback(): Sending data to Pulseaudio failed: %s. Notifying server to close handle %u.", strerror(errno), handle);
						close(sock);
						MUTEX_LOCK(&mapLock);
						handleToSocketMap.erase(handle);
						paBufferMap.erase(handle);
						paBufferLenMap.erase(handle);
						MUTEX_UNLOCK(&mapLock);
						free(tmpBuffer);
						multiOS->writeBuffer(buf, 4, PULSEAUDIO_STREAM_ID);
						return true;
				}
			}
			MUTEX_LOCK(&mapLock);
			paBufferMap.erase(handle);
			paBufferLenMap.erase(handle);
			MUTEX_UNLOCK(&mapLock);
			free(tmpBuffer);
		}

		tmperr = send(sock, paData, bufLen, flags);
		if (unlikely(tmperr < 0)) {
			rdr::U8 * newBuffer;

			switch(errno) {
				case EAGAIN:
#if EAGAIN != EWOULDBLOCK
				case EWOULDBLOCK:
#endif
					vlog.debug("incomingPaCallback(): Sending buffer for handle %u to local socket %u of length %u would block, so store buffer and return.", handle, sock, bufLen);
					newBuffer = (rdr::U8 *) malloc(bufLen);
					if (unlikely(!newBuffer)) {
						vlog.error("incomingPaCallback(): Failed to malloc buffer for handle %u, return failure", handle);
						return false;
					}
					memcpy(newBuffer, paData, bufLen);
					MUTEX_LOCK(&mapLock);
					paBufferMap[handle] = newBuffer;
					paBufferLenMap[handle] = bufLen;
					MUTEX_UNLOCK(&mapLock);
					return true;
				default:
					vlog.error("incomingPaCallback(): Sending data to Pulseaudio failed: %s. Notifying server to close handle %u.", strerror(errno), handle);
					MUTEX_LOCK(&mapLock);
					handleToSocketMap.erase(handle);
					MUTEX_UNLOCK(&mapLock);
					close(sock);
					multiOS->writeBuffer(buf, 4, PULSEAUDIO_STREAM_ID);
					return true;
			}
		}
	} else {
		MUTEX_UNLOCK(&paConnectMutex);
		if (sock) {
			struct timeval now;
			time_t idle = 0;

			close(sock);
			if (likely(!gettimeofday(&now, NULL))) {
				MUTEX_LOCK(&mapLock);
				idle = now.tv_sec - handleTimeMap[handle];
				MUTEX_UNLOCK(&mapLock);
			}
			vlog.debug("incomingPaCallback(): server signaled closed handle %u, closing local socket %u, idle %lus.", handle, sock, idle);
			MUTEX_LOCK(&mapLock);
			handleToSocketMap.erase(handle);
			handleTimeMap.erase(handle);
			rdr::U8 * tmpBuffer = paBufferMap[handle];
			if (tmpBuffer)
				free(tmpBuffer);
			paBufferMap.erase(handle);
			paBufferLenMap.erase(handle);
			MUTEX_UNLOCK(&mapLock);
		}
	}

//	vlog.debug("incomingPaCallback(): Sending data for handle %u to local socket %i successfull.", handle, sock);

	if (signalTarget && keepAliveTimeoutInt > 0 && signalTarget->keepAliveTimer.isStarted() )
		signalTarget->keepAliveTimer.start(keepAliveTimeoutInt);
	return true;
}

bool CSecurityMulti::incomingSignalCallback(const rdr::U8* buf, int bufLen) {
	if (unlikely(bufLen < 4)) {
		vlog.error("incomingSignalCallback: signal buffer length %i is below minimum of 4", bufLen);
		return true;
	}
	int b0 = *buf++;
	int b1 = *buf++;
	rdr::U16 signal = b0 << 8 | b1;
	b0 = *buf++;
	b1 = *buf++;
	rdr::U16 length = b0 << 8 | b1;
	if (unlikely(bufLen < length + 4)) {
		vlog.error("incomingSignalCallback: signal buffer length %i is below specified length %u + 4", bufLen, length);
		return true;
	}
//	vlog.debug("incomingSignalCallback: got signal %u, length %u", signal, length);
	if (signal == BIGMULTI_SIGNAL_ID) {
		if (length != 0) {
			vlog.error("BIGMULTI signal with data!");
			return true;
		}
		if (multiOS) {
			((MultiOutStream *)multiOS)->setBigMulti();
			if (paReadBufferSize > MAXPABUF)
				paReadBufferSizeInt = MAXPABUF;
			else if (paReadBufferSize < MINPABUF)
				paReadBufferSizeInt = MINPABUF;
			else
				paReadBufferSizeInt = paReadBufferSize;
			if (!multiOS->getMultiPartSupport() && paReadBufferSizeInt > (U32) multiOS->getMaxBufferSize() - 4)
				paReadBufferSizeInt = multiOS->getMaxBufferSize() - 4;
			vlog.debug("incomingSignalCallback: BIGMULTI: changed PA read buffer size to %u", paReadBufferSizeInt);
		}
	} else if (signal == MULTIPART_SIGNAL_ID) {
		if (length != 0) {
			vlog.error("MULTIPART signal with data!");
			return true;
		}
		if (multiOS) {
			vlog.debug("incomingSignalCallback: MULTIPART: enable MultiPart support");
			((MultiOutStream *)multiOS)->setMultiPart();
			if (paReadBufferSize > MAXPABUF)
				paReadBufferSizeInt = MAXPABUF;
			else if (paReadBufferSize < MINPABUF)
				paReadBufferSizeInt = MINPABUF;
			else
				paReadBufferSizeInt = paReadBufferSize;
			vlog.debug("incomingSignalCallback: MULTIPART: changed PA read buffer size to %u", paReadBufferSizeInt);
		}
	} else if (signal == PULSEZSTD_SIGNAL_ID) {
		if (length != 1) {
			vlog.error("PULSEZSTD signal with data len not 1!");
			return true;
		}
		if (multiOS) {
			vlog.debug("incomingSignalCallback: PULSEZSTD: set remote Pulseaudio ZSTD support");
			((MultiOutStream *)multiOS)->setRemoteSupportsPulseZstd(true);
		}
	} else if (signal == SHUTDOWN_SIGNAL_ID) {
		if (length != 0) {
			vlog.error("SHUTDOWN signal with data!");
			return true;
		}
		vlog.debug("incomingSignalCallback: SHUTDOWN");
		if (multiOS)
			((MultiInStream *)multiIS)->setShutdown();
	} else if (signal == KEEPALIVE_SIGNAL_ID) {
		if (signalTarget && keepAliveTimeoutInt > 0) {
			if (!signalTarget->keepAliveTimer.isStarted())
				vlog.debug("incomingSignalCallback: starting keep-alive with timeout %us", keepAliveTimeoutInt / 1000);
			else
				vlog.verbose("incomingSignalCallback: received keep-alive signal %us before timeout", signalTarget->keepAliveTimer.getRemainingMs() / 1000);
			signalTarget->keepAliveTimer.start(keepAliveTimeoutInt);
		}
	} else if (signal == PING_SIGNAL_ID) {
		if (length != 8) {
			vlog.error("incomingSignalCallback: PING signal with wrong data length %u!", length);
			return true;
		}
		if (multiOS) {
//			vlog.debug("incomingSignalCallback: got ping %llu, sending pong", *((U64 *) buf));
			multiOS->sendSignal(PONG_SIGNAL_ID, buf, length);
		}
	} else if (signal == HIGHPING_SIGNAL_ID) {
		if (length != 8) {
			vlog.error("incomingSignalCallback: HIGHPING signal with wrong data length %u!", length);
			return true;
		}
		if (multiOS) {
//			vlog.debug("incomingSignalCallback: got highping %llu, sending highpong", *((U64 *) buf));
			multiOS->sendSignal(HIGHPONG_SIGNAL_ID, buf, length, 0, QPRIOHIGH);
		}
	} else if (signal == CONGESTION_SIGNAL_ID) {
		if (length != 1) {
			vlog.error("incomingSignalCallback: CONGESTION signal with wrong data length %u!", length);
			return true;
		}
		congestionLevel = *((U8*) buf);
		vlog.debug("incomingSignalCallback: got congestion level %u", congestionLevel);
	} else if (signal == PONG_SIGNAL_ID) {
		struct timeval now;

		if (length != 8) {
			vlog.error("incomingSignalCallback: PONG signal with wrong data length %u!", length);
			return true;
		}
		if (!gettimeofday(&now, NULL)) {
			U64 pongTime;
			S64 diff;

			pongTime = *((U64 *) buf);
			diff = now.tv_sec * 1000000 + now.tv_usec - pongTime;
			vlog.debug("incomingSignalCallback: PONG signal diff %lli us", diff);
		}
	} else if (signal == HIGHPONG_SIGNAL_ID) {
		struct timeval now;

		if (length != 8) {
			vlog.error("incomingSignalCallback: HIGHPONG signal with wrong data length %u!", length);
			return true;
		}
		if (!gettimeofday(&now, NULL)) {
			U64 pongTime;
			S64 diff;

			pongTime = *((U64 *) buf);
			diff = now.tv_sec * 1000000 + now.tv_usec - pongTime;
			vlog.debug("incomingSignalCallback: HIGHPONG signal diff %lli us", diff);
		}
	} else if (signal == AUTOTRANSFERPATH_SIGNAL_ID) {
		if (length == 0) {
			vlog.error("incomingSignalCallback: AUTOTRANSFERPATH_SIGNAL_ID signal without data!");
			return true;
		}
#if defined(WIN32) || defined(WIN64)
		char * tmpbuf = strdup((const char*) buf);
		char * tmp = tmpbuf;

		vlog.debug("incomingSignalCallback: AUTOTRANSFERPATH_SIGNAL_ID: change autotransferDir from %s to %s, overriding local setting. Not actually changing the setting (autotransferFolder), just overriding during runtime because the server says so.", autotransferFolder.getData(), (const char*) buf);
		//		autotransferFolder.setParam((const char*) buf);
		while(*tmp) {
			*tmp = tolower(*tmp);
			tmp++;
		}
		if (strstr((const char*) tmpbuf, "start menu")) {
			vlog.error("incomingSignalCallback: AUTOTRANSFERPATH_SIGNAL_ID: rejecting invalid autotransferDir %s!", (const char*) buf);
		} else {
			CAutotransferHandler::autotransferDirFromServer = strdup((const char*) buf);
		}
		free(tmpbuf);
#else
		vlog.debug("incomingSignalCallback: AUTOTRANSFERPATH_SIGNAL_ID: the server wants us to set autotransferFolder to %s. This is not implemented (not even planned, for that matter) for unix clients. We therefore kindly rebel against it and keep the local setting: %s", (const char*) buf, autotransferFolder.getData());
#endif /* defined(WIN32) || defined(WIN64) */
	} else if (signal == AUTOTRANSFER_ENABLED_SIGNAL_ID) {
		if (length != 1) {
			vlog.error("AUTOTRANSFER_ENABLED signal without data!");
			return true;
		}
		vlog.debug("incomingSignalCallback: AUTOTRANSFER_ENABLED: value %u", *buf);
	} else if (signal == WEBCAM_ENABLED_SIGNAL_ID) {
		vlog.debug("incomingSignalCallback: server signals that video device has been prepared");
	} else if (signal == VERSION_SIGNAL_ID) {
		char * out;

		if (length < 1) {
			vlog.error("VERSION signal without data!");
			return true;
		}
		out = (char *) malloc(length + 1);
		if (out) {
			memcpy(out, buf, length);
			out[length] = 0;
			vlog.info("incomingSignalCallback: VERSION: server version %s, my version %s %s", out, PACKAGE_VERSION, BUILD_TIMESTAMP);
			free(out);
		}
	} else if (signal == LOCALTIME_SIGNAL_ID) {
		struct timeval now;
		time_t localtime = 0;
		time_t remotetime;
		char localbuf[26];
		char remotebuf[26];

		if (length != 8) {
			vlog.error("LOCALTIME signal with wrong length!");
			return true;
		}
		if (!gettimeofday(&now, NULL))
			localtime = now.tv_sec;
		remotetime = *((rdr::U64 *) buf);
#ifdef WIN32
		if (ctime_s(localbuf, sizeof(localbuf), &localtime)) {
			vlog.error("%s: Failed to convert local time to a human-readable format");
			localbuf[0] = '\0';
		}
		if (ctime_s(remotebuf, sizeof(remotebuf), &remotetime)) {
			vlog.error("%s: Failed to convert remote time to a human-readable format");
			remotebuf[0] = '\0';
		}
#else
		ctime_r(&localtime, localbuf);
		ctime_r(&remotetime, remotebuf);
#endif
		if (localtime > remotetime + 10 || remotetime > localtime + 10)
			vlog.error("incomingSignalCallback: LOCALTIME: server time %lu (%s), local time %lu (%s), times differ more than 10s!", remotetime, remotebuf, localtime, localbuf);
		else
			vlog.info("incomingSignalCallback: LOCALTIME: server time %lu (%s), local time %lu (%s)", remotetime, remotebuf, localtime, localbuf);
	} else {
		vlog.info("Received unknown signal %u", signal);
	}
	return true;
}

bool CSecurityMulti::handleTimeout(rfb::Timer * t)
{
	if (t == &keepAliveTimer) {
		vlog.error("keep-alive timed out after %us, server assumed dead, exiting!",
			keepAliveTimeoutInt / 1000);
		runThreads = false;
		delete multiIS;
		delete multiOS;
#ifdef WIN32
		WSACleanup();
#endif
		exit(10);
	} else
	if (t == &checkIdleTimer) {
		struct timeval now;
		time_t diff;
		bool found = true;
		U32 handle;

		vlog.verbose("checkIdleTimer: checking audio idle times");
		if (gettimeofday(&now, NULL))
			return false;

		while(found && !handleTimeMap.empty()) {
			found = false;
			MUTEX_LOCK(&mapLock);
			for(auto iter = handleTimeMap.begin(); iter != handleTimeMap.end(); ++iter) {
				diff = now.tv_sec - iter->second;
//				vlog.verbose("checkIdleTimer: client socket %u, handle %u, idle %lu", handleToSocketMap[iter->first], iter->first, diff);
				if (diff > audioMaxIdle) {
					handle = iter->first;
					vlog.info("checkIdleTimer: client socket %u, server socket %u, idle %lu > audioMaxIdle %u, closing down", handleToSocketMap[handle], handle, diff, int(audioMaxIdle));
					found = true;
					break;
				}
			}
			MUTEX_UNLOCK(&mapLock);
			if (found) {
				/* Notify other end that stream is closed */
				signalTarget->multiOS->writeBuffer((U8*)&handle, 4, PULSEAUDIO_STREAM_ID);
				MUTEX_LOCK(&mapLock);
				close(handleToSocketMap[handle]);
				paBufferMap.erase(handle);
				paBufferLenMap.erase(handle);
				handleToSocketMap.erase(handle);
				handleTimeMap.erase(handle);
				MUTEX_UNLOCK(&mapLock);
			}
		}
		if (!handleTimeMap.empty())
			checkIdleTimer.start(audioCheckIdleIntervalInt);
		return false;
	} else
		return false;
}


#ifndef WIN32
void GetTempPath(const size_t maxBytes, char* tmpStr) {
	char* tmpDir = getenv("TMPDIR");
	if (!tmpDir)
		tmpDir = getenv("TMP");
	if (!tmpDir)
		tmpDir = getenv("TEMP");
	if (!tmpDir)
		tmpDir = getenv("TEMPDIR");
	if (!tmpDir)
		tmpDir = (char*)"/tmp";
	strncpy(tmpStr, tmpDir, maxBytes);
	tmpStr[maxBytes - 1] = '\0';
}
#endif


const char* CSecurityMulti::getTgproTmpPath() {
	if (tgproTmpPath)
		return tgproTmpPath;
	const char* tmpPath = get_tgpro_tmp_path();
	if (!tmpPath) {
		vlog.error("%s: Failed to get_tgpro_tmp_path()", __func__);
		return NULL;
	}
#ifdef WIN32
	if (mkdir(tmpPath) && errno != EEXIST)
#else
	if (mkdir(tmpPath, 0700) && errno != EEXIST)
#endif
		vlog.error("Failed to create a tgpro temporary directory.");

	/* Create separate subdirectories for printing and autotransfer */

	char* multiPrintTmpPath = new char[strlen(tmpPath) + strlen(PRINT_TMP_SUBDIR) + 2];
	sprintf(multiPrintTmpPath, "%s%c%s", tmpPath, PATH_DIVIDER, PRINT_TMP_SUBDIR);
	char* multiAutotransferTmpPath = new char[strlen(tmpPath) + strlen(AUTOTRANSFER_TMP_SUBDIR) + 2];
	sprintf(multiAutotransferTmpPath, "%s%c%s", tmpPath, PATH_DIVIDER, AUTOTRANSFER_TMP_SUBDIR);

#ifdef WIN32
	if (mkdir(multiPrintTmpPath) && errno != EEXIST)
#else
	if (mkdir(multiPrintTmpPath, 0700) && errno != EEXIST)
#endif
		vlog.error("Failed to create a tgpro temporary directory for printing.");

#ifdef WIN32
	if (mkdir(multiAutotransferTmpPath) && errno != EEXIST)
#else
	if (mkdir(multiAutotransferTmpPath, 0700) && errno != EEXIST)
#endif
		vlog.error("Failed to create a tgpro temporary directory for autotransfer.");

	return tmpPath;
}


void CSecurityMulti::addFilePacketToDataQueue(rdr::U8* buf, size_t len, char* fileName, rdr::U32 packetNumber, rdr::U8 finalPacket, const rdr::U8 fileTransferProtocol) {
	fileQueueData newData;
	newData.data = buf;
	newData.len = len;
	newData.filename = fileName;
	newData.packetNumber = packetNumber;
	newData.finalPacket = finalPacket;
	newData.fileTransferProtocol = fileTransferProtocol;
	MUTEX_LOCK(&fileQueueMutex);
	fileQueue.push(newData);
	MUTEX_UNLOCK(&fileQueueMutex);
	TGVNC_CONDITION_SEND_SIG(&packetsWaitingCondition);
}


bool CSecurityMulti::incomingFileTransferPacketCallback(const rdr::U8* buf, int bufLen) {
	if (bufLen < 10) {
		vlog.error("File buffer length is too small. It is %i.", bufLen);
		return false;
	}
	const rdr::U8* fileProtocol = buf++;
	if (*fileProtocol != PRINT_PROTOCOL_V1 &&
		*fileProtocol != AUTOTRANSFER_PROTOCOL_V1) {
		vlog.error("Unknown file transfer protocol.");
		return true;
	}
	if (*fileProtocol == PRINT_PROTOCOL_V1 && !printSupport) {
		vlog.error("SupportPrint is set to 0. Yet the server is ignoring our wish and sending something to print. Ignoring packet...");
		return true;
	}
	if (*fileProtocol == AUTOTRANSFER_PROTOCOL_V1 && !autotransferSupport) {
		vlog.error("AutotransferSupport is set to 0. Yet the server is ignoring our wish and sending something for us to download. Ignoring packet...");
		return true;
	}
	const char* fileName = (const char*)buf;
	const size_t fileNameLen = strlen(fileName);
	if (fileNameLen < 1 || fileNameLen > 512 || (signed)fileNameLen > (bufLen - 3)) {
		vlog.error("Wrong file name length of %zu in a file transfer packet of size %i.", fileNameLen, bufLen);
		return true;
	}
	buf += fileNameLen + 1 ;

	const rdr::U32* packetNumber = (rdr::U32*)buf;
	if (*packetNumber == 0) {
		vlog.debug("New file %s coming in.", fileName);
	}
	buf += 4;
	const rdr::U8* finalPacket = buf++;
	if (*finalPacket) {
		vlog.debug("File %s complete.", fileName);
	}

	const char* tmpPath = getTgproTmpPath();
	const char* subdir;
	if (*fileProtocol == PRINT_PROTOCOL_V1) {
		subdir = PRINT_TMP_SUBDIR;
	} else if (*fileProtocol == AUTOTRANSFER_PROTOCOL_V1) {
		subdir = AUTOTRANSFER_TMP_SUBDIR;
	} else {
		subdir = "";
	}

	char* filenameSanitized = replace_invalid_windows_path_chars(fileName);
	char* filePath;
	if (filenameSanitized) {
		filePath = new char[strlen(tmpPath) + strlen(subdir) + strlen(filenameSanitized) + 3];
		sprintf(filePath, "%s%c%s%c%s", tmpPath, PATH_DIVIDER, subdir, PATH_DIVIDER, filenameSanitized);
		free(filenameSanitized);
	} else {
		filePath = new char[strlen(tmpPath) + strlen(subdir) + fileNameLen + 3];
		sprintf(filePath, "%s%c%s%c%s", tmpPath, PATH_DIVIDER, subdir, PATH_DIVIDER, fileName);
	}
	if (strlen(filePath) > 250) { // 260 is Windows max path length. We want to allow some wiggle room (to rename with (n) if necessary)
		filePath[250] = '\0';
		vlog.error("%s: filename was too long, truncated: %s", __func__, filePath);
	}

	const size_t fileDataLen = bufLen - (buf - fileProtocol);
	rdr::U8* fileDataBuf = new rdr::U8[fileDataLen];
	memcpy(fileDataBuf, buf, fileDataLen);
	addFilePacketToDataQueue(fileDataBuf, fileDataLen, filePath, *packetNumber, *finalPacket, *fileProtocol);

	return true;
}


bool CSecurityMulti::processMsg() {
	char version[256];
	struct timeval now;
	rdr::U64 localtime = 0;

	multiIS = new rdr::MultiInStream(cc->getInStream(), multiInBufferSize);
	multiOS = new rdr::MultiOutStream(cc->getOutStream());
	multiIS->setCallback(PULSEAUDIO_STREAM_ID, incomingPaCallback);
	multiIS->setCallback(FILE_TRANSFER_STREAM_ID, incomingFileTransferPacketCallback);
	multiIS->setCallback(SIGNAL_STREAM_ID, incomingSignalCallback);
	vlog.debug("Calling setStreams()");
	cc->setStreams(multiIS, multiOS);

	snprintf(version, 255, "%s %s", PACKAGE_VERSION, BUILD_TIMESTAMP);
	version[255] = 0;
	if (!gettimeofday(&now, NULL))
		localtime = now.tv_sec;
	multiOS->sendSignal(VERSION_SIGNAL_ID, version, strlen(version));
	multiOS->sendSignal(LOCALTIME_SIGNAL_ID, &localtime, 8);

	vlog.debug("Streams are ready, creating new CWebcamHandler()");
	webcamHandler = new CWebcamHandler(multiOS);

	setAutotransferSupport(autotransferSupport);
	return true;
}

void CSecurityMulti::setAutotransferSupport(bool value)
{
	rdr::U8 runtimeAutotransferSupport = value ? 1 : 0;
	vlog.debug("Setting autotransferSupport to %u", value);
	multiOS->sendSignal(AUTOTRANSFER_ENABLED_SIGNAL_ID, &runtimeAutotransferSupport, 1);
}
