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

#if !defined(WIN32) && !defined(__APPLE__) /* We don't need to build a Windows or Mac server at all. Like this we avoid some build fuss. */

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

#ifdef WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <pthread.h>
#include <sys/socket.h>
#include <pwd.h>
#include <sys/signal.h>
#include <sys/time.h>
#include <sys/resource.h>
#endif
#include <sys/unistd.h>
#include <errno.h>
#include <dirent.h>
#include <stdlib.h>
/* Old systems have select() in sys/time.h */
#ifdef HAVE_SYS_SELECT_H
#include <sys/select.h>
#endif

#include <rfb/SSecurityMulti.h>
#include <rfb/LogWriter.h>
#include <rdr/MultiInStream.h>
#include <rdr/MultiOutStream.h>
#include <rdr/types.h>
#include <rdr/mutex.h>
#include <rfb/Exception.h>
#include <rfb/TightJPEGEncoder.h>
#include <rfb/TightMPEncoder.h>
#include <rdr/MultiStream.h>
#include <rfb/Configuration.h>
#include <rfb/Timer.h>
#include <rfb/ServerCore.h>
#include <rfb/MP.h>

#include <rfb/SAutotransferDirWatcher.h>
#include <rfb/SAutotransferNow.h>
#include <rfb/SAutotransferMime.h>
#include <rfb/SAutotransferOpswat.h>
#include <rfb/SAutotransferVaithex.h>

#include <tgpro_environment.h>

#ifndef WIN32
#define Sleep(x) usleep((x)*1000)
#else
#define pthread_exit(NULL)
#endif

#if !defined(WIN32) && !defined(WIN64)
#include <syslog.h>
#endif

using namespace rfb;
using namespace rdr;

static LogWriter vlog("SSecurityMulti");

#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 logoutMagicApp("LogoutMagicApp", "App to start for logout", "/usr/local/bin/mplogout");
StringParameter urlMagicApp("UrlMagicApp", "App to start on URL", "/usr/local/bin/firefoxauto");
IntParameter keepAliveTimeout("KeepAliveTimeout", "Keep-alive timeout in seconds (0 to disable)", 150, 0);
IntParameter initialNoAudio("InitialNoAudio", "time to block audio at startup in seconds (0 to disable)", 0, 0);
IntParameter frameRateResetTimeout("FrameRateResetTimeout", "timeout in seconds after which frameRate gets reset to FrameRateMaxQuality (0 to disable)", 5, 0, 120);
IntParameter frameRateMaxDelayHigh("FrameRateMaxDelayHigh", "time in milliseconds for a ping roundtrip over which quality gets reduced or frameRate decreased", 50, 1, 1000);
IntParameter frameRateMaxDelayLow("FrameRateMaxDelayLow", "time in milliseconds for a ping roundtrip below which quality gets increased or frameRate increased", 10, 1, 200);
IntParameter frameRateMaxDelayPerLevel("FrameRateMaxDelayPerLevel", "time in milliseconds to add to MaxDelay per congestion level", 10, 0, 100);
IntParameter frameRateMaxDelayPerCheck("FrameRateMaxDelayPerCheck", "time in microseconds to add to MaxDelay per PingInterval, if congestion > 0", 100, 0, 10000);
IntParameter frameRateMaxQuality("FrameRateMaxQuality","Max. JPEG/compression quality to use after timeout", 9, 6, 9);
IntParameter frameRateTargetQuality("FrameRateTargetQuality","Target JPEG/compression quality to use during auto adjust", 8, 6, 9);
IntParameter frameRateMinQuality("FrameRateMinQuality","Min. JPEG/compression quality to use during auto adjust", 6, 1, 9);
IntParameter frameRateMin("FrameRateMin","The minimum number of updates per second sent to each client", 2, 1, 50);
IntParameter sendFileBurst("SendFileBurst","Number of file transfer packages to send before wait and flush", 256, 1, 32768);

static U32 paReadBufferSizeInt;
static U32 keepAliveTimeoutInt;

static SWebcamHandler* webcamHandler = NULL;
static SAutotransferNow* autotransferNow = NULL;
static SAutotransferMime* autotransferMime = NULL;
static SAutotransferOpswat* autotransferOpswat = NULL;
static SAutotransferVaithex* autotransferVaithex = NULL;

#ifndef WIN32
static void* urlMagicLastURL = NULL;
static rdr::U16 urlMagicLastLen = 0;
static struct timeval urlMagicLastTime;
#endif

static int origFrameRate;
static rdr::U8 congestionLevel = 0;
static int qualityLevel = frameRateMaxQuality;
static rdr::S64 pongDiffMin = 1000000;
static rdr::S64 pongDiffMax = 0;
static S64 rateLow = frameRateMaxDelayLow * 1000;
static S64 rateHigh = frameRateMaxDelayHigh * 1000;

#ifndef WIN32
int SSecurityMulti::paInSocket = -1;
#endif
rdr::MultiInStream* multiIS = NULL;
rdr::MultiOutStream* multiOS = NULL;
bool runThreads = true;
bool SSecurityMulti::sendPdfThreadRunning = false;
bool SSecurityMulti::sendPdfThreadRunAgain = false;
THREAD_ID SSecurityMulti::paListenerThreadId = (THREAD_ID) NULL;

static SSecurityMulti * signalTarget = NULL;

static bool clientSupportsSound = true;
static bool clientSupportsPrint = true;

rdr::U32 SSecurityMulti::lastPaHandle = 1;
handleToSocketMap_t SSecurityMulti::handleToSocketMap;
bufferMap_t SSecurityMulti::paBufferMap;
bufferLenMap_t SSecurityMulti::paBufferLenMap;

MUTEX_TYPE SSecurityMulti::mapLock;

#ifndef WIN32
static int writepulsecookie(const void *cookie, const size_t size)
{
	char * path;

	char* homedir = getenv("HOME");
	if (!homedir) {
		const uid_t uid = getuid();
		const struct passwd* passwd = getpwuid(uid);
		if (!passwd) {
			/* Do we want emit error msg here? */
			return -1;
		}
		homedir = passwd->pw_dir;
	}

	const size_t len = strlen(homedir);
	path = new char[len+23];
	if (!path)
		return -1;
	memcpy(path, homedir, len);
	memcpy(path + len, "/.config/pulse/cookie\0", 22);

	int fd = open(path, O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR);
	if (fd < 0) {
		vlog.error("writepulsecookie: opening %s failed: %s", path, strerror(errno));
		return -1;
	}
	size_t res = write(fd, cookie, size);
	close(fd);
	if (res < size) {
		return -1;
	}
	vlog.debug("writepulsecookie: pulseaudio cookie saved to %s", path);

	return 0;
}

static int writescaling(const U16 scaling)
{
	char * path;

	char* homedir = getenv("HOME");
	if (!homedir) {
		const uid_t uid = getuid();
		const struct passwd* passwd = getpwuid(uid);
		if (!passwd) {
			/* Do we want emit error msg here? */
			vlog.debug("writescaling: failed to find homedir");
			return -1;
		}
		homedir = passwd->pw_dir;
	}

	const size_t len = strlen(homedir);
	path = new char[len+23];
	if (!path)
		return -1;
	memcpy(path, homedir, len);
	memcpy(path + len, "/.config/mpscaling\0", 19);

	int fd = open(path, O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR);
	if (fd < 0) {
		vlog.error("writescaling: opening %s failed: %s", path, strerror(errno));
		return -1;
	}

	char buf[10];
	const size_t buflen = snprintf(buf, 9, "%u\n", scaling);
	buf[9] = 0;
	size_t res = write(fd, buf, buflen);
	if (res < buflen) {
		close(fd);
		if (res < 0)
			vlog.error("writescaling: writing to %s failed: %s", path, strerror(errno));
		return -1;
	}
	close(fd);
	vlog.debug("writescaling: screen scaling saved to %s", path);

	return 0;
}
#endif

SSecurityMulti::SSecurityMulti(SConnection* sc)
	: SSecurity(sc), keepAliveTimer(this), frameRateResetTimer(this)
{
	signalTarget = this;
	MUTEX_INIT(&mapLock);
#ifndef WIN32
	paInSocket = socket(AF_UNIX, SOCK_STREAM, 0);
	if (paInSocket < 0) {
		vlog.error("Could not create socket for Pulseaudio.");
		return;
	}
	paInSockaddr.sun_family = AF_UNIX;
	strcpy(paInSockaddr.sun_path, PULSEAUDIO_UNIX_SOCKET);
#endif
	if (paReadBufferSize > OLDPABUF)
		paReadBufferSizeInt = OLDPABUF;
	else if (paReadBufferSize < MINPABUF)
		paReadBufferSizeInt = MINPABUF;
	else
		paReadBufferSizeInt = paReadBufferSize;
	vlog.debug("initialized PA read buffer size to %u", paReadBufferSizeInt);
	keepAliveTimeoutInt = keepAliveTimeout * 1000;
	origFrameRate = rfb::Server::frameRate;
}


SSecurityMulti::~SSecurityMulti() {
	vlog.info("~SSecurityMulti(): pongDiffMin %llu, pongDiffMax %llu", pongDiffMin, pongDiffMax);
	signalTarget = NULL;
	keepAliveTimer.stop();
	frameRateResetTimer.stop();
	runThreads = false;
	if (paInSocket > -1)
		close(paInSocket);
#ifndef WIN32
	if (paListenerThreadId) {
		THREAD_CANCEL(paListenerThreadId); // pthread_cancel might misbehave if paListenerThreadId was never set (e.g. if postAuth() wasn't called because of a failed authentication)
	}
	delete webcamHandler;
#endif
	vlog.debug("~SSecurityMulti(): paListenerThread cancelled");
	MUTEX_DESTROY(&mapLock);
	if(!access(FLAG_FILE, F_OK)) {
		if (unlink(FLAG_FILE) == -1)
			vlog.error("Deleting file '%s' error: %s", FLAG_FILE, strerror(errno));
		else
			vlog.debug("deleted file '%s'", FLAG_FILE);
	}
	delete multiIS;
	delete multiOS;
#ifndef WIN32
	unlink(paInSockaddr.sun_path);
	unlink(NOAUDIO_FILE);
	unlink(NOPRINT_FILE);
#endif
}

THREAD_FUNC SSecurityMulti::paReadThread(void* _handle) {
	const rdr::U32 handle = *((rdr::U32 *) _handle);
	const int curBufferSize = paReadBufferSizeInt;
	char * readPaBuffer;

	free(_handle);
	MUTEX_LOCK(&mapLock);
	const int sock = handleToSocketMap[handle];
	MUTEX_UNLOCK(&mapLock);
	if (unlikely(!sock)) {
#ifndef WIN32
#if defined(__APPLE__)
		vlog.error("paReadThread %u: no socket for handle %u", gettid(), handle);
#else
		vlog.error("paReadThread %lu: no socket for handle %u", gettid(), handle);
#endif
#endif
		THREAD_EXIT(THREAD_NULL);
	}
#ifndef WIN32
#if defined(__APPLE__)
	vlog.debug("paReadThread %u created for incoming handle %u, socket %u with buffer size %u", gettid(), handle, sock, curBufferSize);
#else
	vlog.debug("paReadThread %lu created for incoming handle %u, socket %u with buffer size %u", gettid(), handle, sock, curBufferSize);
#endif
#endif
	readPaBuffer = (char *) malloc(curBufferSize + 4);
	if (unlikely(!readPaBuffer)) {
#ifndef WIN32
#if defined(__APPLE__)
		vlog.error("paReadThread %u cannot allocate buffer of size %u for handle %u", gettid(), curBufferSize + 4, handle);
#else
		vlog.error("paReadThread %lu cannot allocate buffer of size %u for handle %u", gettid(), curBufferSize + 4, handle);
#endif
#endif
		close(sock);
		MUTEX_LOCK(&mapLock);
		handleToSocketMap.erase(handle);
		MUTEX_UNLOCK(&mapLock);
		THREAD_EXIT(THREAD_NULL);
	}
	while (!multiOS) {
		Sleep(100);
		if (unlikely(!runThreads)) {
			free(readPaBuffer);
#ifndef WIN32
#if defined(__APPLE__)
			vlog.debug("paReadThread %u canceled before multiOS for handle %u", gettid(), handle);
#else
			vlog.debug("paReadThread %lu canceled before multiOS for handle %u", gettid(), handle);
#endif
#endif
			close(sock);
			MUTEX_LOCK(&mapLock);
			handleToSocketMap.erase(handle);
			MUTEX_UNLOCK(&mapLock);
			THREAD_EXIT(THREAD_NULL);
		}
	}

	*((U32 *)readPaBuffer) = handle;
	ssize_t nBytes = 0;
	ssize_t totalNBytes = 0;
	U32 nReads = 0;
	ssize_t maxNBytes = 0;
	ssize_t minNBytes = curBufferSize;
	struct timeval tv;

	fd_set fds;
	int n;

#ifndef WIN32
#if defined(__APPLE__)
	vlog.debug("paReadThread %u: current priority is %i, now set nice(-10)", gettid(), getpriority(PRIO_PROCESS, 0));
#else
	vlog.debug("paReadThread %lu: current priority is %i, now set nice(-10)", gettid(), getpriority(PRIO_PROCESS, 0));
#endif
	errno = 0;
	n = nice(-10);
	if (n == -1 && errno != 0) {
#if defined(__APPLE__)
		vlog.error("paReadThread %u: failed to set nice(-10), error: %s", gettid(), strerror(errno));
#else
		vlog.error("paReadThread %lu: failed to set nice(-10), error: %s", gettid(), strerror(errno));
#endif
	} else {
#if defined(__APPLE__)
		vlog.debug("paReadThread %u: new priority is %i", gettid(), getpriority(PRIO_PROCESS, 0));
#else
		vlog.debug("paReadThread %lu: new priority is %i", gettid(), getpriority(PRIO_PROCESS, 0));
#endif
	}
#endif

	while (runThreads) {
		do {
			FD_ZERO(&fds);
			FD_SET(sock, &fds);
			tv.tv_sec = 60;
			tv.tv_usec = 0;
			n = select(sock+1, &fds, 0, 0, &tv);
		} while (n < 0 && errno == EINTR);
		if (unlikely(n < 0)) {
#ifndef WIN32
#if defined(__APPLE__)
			vlog.debug("paReadThread %u, handle %u, sock %u, select() failed with error %i: %s", gettid(), handle, sock, errno, strerror(errno));
#else
			vlog.debug("paReadThread %lu, handle %u, sock %u, select() failed with error %i: %s", gettid(), handle, sock, errno, strerror(errno));
#endif
#endif
			break;
		}
		if (n > 0) {
			nBytes = read(sock, readPaBuffer + 4, curBufferSize);
			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;
			}
			if (unlikely(nBytes < 0)) {
#ifndef WIN32
#if defined(__APPLE__)
				vlog.debug("paReadThread %u, handle %u, sock %u, read() failed with error %i: %s", gettid(), handle, sock, errno, strerror(errno));
#else
				vlog.debug("paReadThread %lu, handle %u, sock %u, read() failed with error %i: %s", gettid(), handle, sock, errno, strerror(errno));
#endif
#endif
				break;
			}
#ifndef WIN32
			if (unlikely(nBytes == curBufferSize)) {
#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: send %zu bytes to handle %u", nBytes, handle);
			multiOS->writeBuffer((U8*)readPaBuffer, nBytes + 4, PULSEAUDIO_STREAM_ID);
			if (unlikely(nBytes > maxNBytes))
				maxNBytes = nBytes;
			if (unlikely(nBytes < minNBytes))
				minNBytes = nBytes;
			nReads++;
			totalNBytes += nBytes;
#ifndef WIN32
		} else {
#if defined(__APPLE__)
			vlog.verbose("paReadThread %u, handle %u, select timed out", gettid(), handle);
#else
			vlog.verbose("paReadThread %lu, handle %u, select timed out", gettid(), handle);
#endif
#endif
		}
	}

	/* Notify other end that stream is closed */
	multiOS->writeBuffer((U8*)readPaBuffer, 4, PULSEAUDIO_STREAM_ID);
	free(readPaBuffer);
	close(sock);
	MUTEX_LOCK(&mapLock);
	handleToSocketMap.erase(handle);
	MUTEX_UNLOCK(&mapLock);
#ifndef WIN32
#if defined(__APPLE__)
	vlog.debug("paReadThread %u finished handle %u with error %i: %s, min read %lu, max read %lu, total of %lu bytes in %u reads, avg %lu bytes per read", gettid(), handle, nBytes ? errno : 0, nBytes ? strerror(errno) : "none", minNBytes, maxNBytes, totalNBytes, nReads, nReads > 0 ? totalNBytes/nReads : 0);
#else
	vlog.debug("paReadThread %lu finished handle %u with error %i: %s, min read %lu, max read %lu, total of %lu bytes in %u reads, avg %lu bytes per read", gettid(), handle, nBytes ? errno : 0, nBytes ? strerror(errno) : "none", minNBytes, maxNBytes, totalNBytes, nReads, nReads > 0 ? totalNBytes/nReads : 0);
#endif
#endif
	THREAD_EXIT(THREAD_NULL);
}

THREAD_FUNC SSecurityMulti::paListenerThread(void* _myself) {
	SSecurityMulti* myself = (SSecurityMulti*) _myself;
#ifndef WIN32
#if defined(__APPLE__)
	vlog.debug("paListenerThread %u created", gettid());
#else
	vlog.debug("paListenerThread %lu created", gettid());
#endif
#endif

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

	if (initialNoAudio > 0) {
#ifndef WIN32
#if defined(__APPLE__)
		vlog.debug("paListenerThread %u: blocking audio for %u seconds", gettid(), int(initialNoAudio));
#else
		vlog.debug("paListenerThread %lu: blocking audio for %u seconds", gettid(), int(initialNoAudio));
#endif
#endif
		Sleep(1000 * initialNoAudio);
	}
#ifndef WIN32
#if defined(__APPLE__)
	vlog.info("paListenerThread %u: start listening on socket %u", gettid(), myself->paInSocket);
#else
	vlog.info("paListenerThread %lu: start listening on socket %u", gettid(), myself->paInSocket);
#endif
#endif
	while (runThreads && listen(myself->paInSocket, 5) < 0) {
#ifndef WIN32
#if defined(__APPLE__)
		vlog.info("paListenerThread %u: listening on socket for Pulseaudio failed, retry in 1s", gettid());
#else
		vlog.info("paListenerThread %lu: listening on socket for Pulseaudio failed, retry in 1s", gettid());
#endif
#endif
		Sleep(1000);
	}

	while (runThreads) {
		FD_ZERO(&fds);
		FD_SET(myself->paInSocket, &fds);
		tv.tv_sec = 20;
		tv.tv_usec = 0;
		n = select(myself->paInSocket+1, &fds, NULL, NULL, &tv);
		if (n > 0) {
#ifndef WIN32
#if defined(__APPLE__)
			vlog.debug("paListenerThread %u: select signals new incoming connection", gettid());
#else
			vlog.debug("paListenerThread %lu: select signals new incoming connection", gettid());
#endif
#endif
			newConnection = accept(myself->paInSocket, NULL, NULL);
			if (newConnection < 0) {
#ifndef WIN32
#if defined(__APPLE__)
				vlog.error("paListenerThread %u: accept failed with error %s", gettid(), strerror(errno));
#else
				vlog.error("paListenerThread %lu: accept failed with error %s", gettid(), strerror(errno));
#endif
#endif
			} else {
				if (clientSupportsSound) {
					rdr::U32 * newHandle = (rdr::U32 *) malloc(4);

					MUTEX_LOCK(&mapLock);
					*newHandle = lastPaHandle++;
					handleToSocketMap[*newHandle] = newConnection;
					MUTEX_UNLOCK(&mapLock);
#ifndef WIN32
#if defined(__APPLE__)
					vlog.debug("paListenerThread %u accepted new incoming connection on socket %lu, new handle is %u", gettid(), newConnection, *newHandle);
#else
					vlog.debug("paListenerThread %lu accepted new incoming connection on socket %lu, new handle is %u", gettid(), newConnection, *newHandle);
#endif
#endif
					THREAD_CREATE(paReadThread, threadId, (void *) newHandle);
					THREAD_SET_NAME(threadId, "tg-paRead");
				} else {
#ifndef WIN32
#if defined(__APPLE__)
					vlog.debug("paListenerThread %u: client disabled sound, closing connection %lu", gettid(), newConnection);
#else
					vlog.debug("paListenerThread %lu: client disabled sound, closing connection %lu", gettid(), newConnection);
#endif
#endif
					close(newConnection);
				}
			}
		} else if (unlikely(n < 0)) {
			if (errno != EINTR) {
#ifndef WIN32
#if defined(__APPLE__)
				vlog.error("paListenerThread %u: select failed with error %s", gettid(), strerror(errno));
#else
				vlog.error("paListenerThread %lu: select failed with error %s", gettid(), strerror(errno));
#endif
#endif
				break;
			}
//			vlog.debug("paListenerThread %lu: select interrupted, next loop", gettid());
//		} else {
//			vlog.debug("paListenerThread %lu: select timeout, next loop", gettid());
		}
	}
	close(myself->paInSocket);
	myself->paInSocket = -1;
	THREAD_EXIT(THREAD_NULL);
}

bool SSecurityMulti::incomingWebcamFfmpegCallback(const rdr::U8* buf, int bufLen) {
	return webcamHandler->handlePacket(buf, bufLen);
}

bool SSecurityMulti::incomingOldWebcamFfmpegCallback(const rdr::U8* buf, int bufLen) {
	return webcamHandler->handleOldPacket(buf, bufLen);
}

bool SSecurityMulti::incomingPaCallback(const rdr::U8* buf, int bufLen) {
	if (unlikely(paInSocket < 0)) {
		vlog.error("incomingPaCallback(): There is Pulseaudio data coming via VNC, but I have no PA client connected. I don't know what to do with the sound data, so return failure.");
		return false;
	}
	if (unlikely(bufLen < 4)) {
		vlog.error("incomingPaCallback(): buffer needs to have at least 4 bytes.");
		return false;
	}

	U32 handle = *((U32*)buf);
	MUTEX_LOCK(&mapLock);
	int sock = handleToSocketMap[handle];
	MUTEX_UNLOCK(&mapLock);
	if (unlikely(!sock)) {
		if (bufLen > 4) {
			vlog.debug("incomingPaCallback(): unknown handle %u, notifying client to close and ignoring packet", handle);
			multiOS->writeBuffer(buf, 4, PULSEAUDIO_STREAM_ID);
		}
		return true;
	}
	int tmperr;
	bufLen -= 4;

	rdr::U8 * tmpBuffer;
	MUTEX_LOCK(&mapLock);
	tmpBuffer = paBufferMap[handle];
	MUTEX_UNLOCK(&mapLock);

	/* end of stream? */
	if (unlikely(bufLen == 0)) {
		vlog.debug("incomingPaCallback(): client signals closed socket, closing local handle %u, socket %u", handle, sock);
		close(sock);
		MUTEX_LOCK(&mapLock);
		handleToSocketMap.erase(handle);
		if (tmpBuffer) {
			free(tmpBuffer);
			paBufferMap.erase(handle);
			paBufferLenMap.erase(handle);
		}
		MUTEX_UNLOCK(&mapLock);
		return true;
	}

	char* paData = (char*)(buf + 4);
// 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(tmpBuffer)) {
		int tmpBufferLen;

		MUTEX_LOCK(&mapLock);
		tmpBufferLen = paBufferLenMap[handle];
		MUTEX_UNLOCK(&mapLock);
		vlog.debug("incomingPaCallback(): Found old buffer for handle %u, socket %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 client to close for handle %u.", MAXPABUFFERSIZE, 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;
					}
					newBuffer = (rdr::U8 *) realloc(tmpBuffer, tmpBufferLen + bufLen);
					if (unlikely(!newBuffer)) {
						vlog.error("incomingPaCallback(): Failed to realloc buffer for handle %u, return failure", 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[handle] = newBuffer;
					paBufferLenMap[handle] = tmpBufferLen + bufLen;
					MUTEX_UNLOCK(&mapLock);
					memcpy(newBuffer + tmpBufferLen, paData, bufLen);
					return true;
				default:
					vlog.error("incomingPaCallback(): Sending data to Pulseaudio failed: %s. Notifying client to close for handle %u.", strerror(errno), handle);
					close(sock);
					free(tmpBuffer);
					MUTEX_LOCK(&mapLock);
					handleToSocketMap.erase(handle);
					paBufferMap.erase(handle);
					paBufferLenMap.erase(handle);
					MUTEX_UNLOCK(&mapLock);
					multiOS->writeBuffer(buf, 4, PULSEAUDIO_STREAM_ID);
					return true;
			}
		}
		free(tmpBuffer);
		MUTEX_LOCK(&mapLock);
		paBufferMap.erase(handle);
		paBufferLenMap.erase(handle);
		MUTEX_UNLOCK(&mapLock);
	}

	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 (!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 client to close for handle %u.", strerror(errno), handle);
				close(sock);
				MUTEX_LOCK(&mapLock);
				handleToSocketMap.erase(handle);
				MUTEX_UNLOCK(&mapLock);
				multiOS->writeBuffer(buf, 4, PULSEAUDIO_STREAM_ID);
				return true;
		}
	}

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

bool SSecurityMulti::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) {
			const U8 value = *((U8 *) buf);

			vlog.debug("incomingSignalCallback: PULSEZSTD: set Pulseaudio ZSTD level to %u", value);
			((MultiOutStream *)multiOS)->setRemoteSupportsPulseZstd(true);
			((MultiOutStream *)multiOS)->pulseZstdLevel.setParam(value);
		}
#ifndef WIN32
	} else if (signal == LOGOUT_SIGNAL_ID) {
		vlog.debug("incomingSignalCallback: starting logout program: %s", logoutMagicApp.getData());
		if(vlog.getLevel() >= vlog.LEVEL_DEBUG)
			multiOS->printBufferUsage();
		int err = fork();
		if (!err) {
			/* the child */
			char * execargs[2];

			/* close potentially open FDs */
			for (int fd = 0; fd < 20; fd++)
				close(fd);
			execargs[0] = logoutMagicApp.getData();
			execargs[1] = NULL;
			execv(logoutMagicApp.getData(), execargs);
			vlog.error("Could not execute logout program: %s", logoutMagicApp.getData());
			exit(1);
		} else if (err < 0) {
			vlog.error("incomingSignalCallback: failed to fork for logout application %s, error %i", logoutMagicApp.getData(), err);
		}
	} else if (signal == OLDURL_SIGNAL_ID) {
		if (length == 0) {
			vlog.error("incomingSignalCallback: (OLDURL) signal without data!");
			return true;
		}
		vlog.debug("incomingSignalCallback: (OLDURL) saving URL to %s", INITIALURL_FILE);
		FILE *f1;
		f1 = fopen(INITIALURL_FILE, "w");
		if (!f1)
			vlog.error("Could not save signalled URL");
		else {
			fprintf(f1, "%s\n", buf);
			fclose(f1);
			vlog.debug("incomingSignalCallback: (OLDURL) saved URL %s to %s", buf, INITIALURL_FILE);
		}
		struct timeval now;
		gettimeofday(&now, 0);
		if (length == urlMagicLastLen && !strncmp((char *) urlMagicLastURL, (char *) buf, length) && now.tv_sec < urlMagicLastTime.tv_sec + 2) {
			vlog.debug("incomingSignalCallback: (OLDURL) same URL repeated within 2s, ignoring: %s", buf);
			return true;
		}
		if(!urlMagicLastURL || urlMagicLastLen < length) {
			void * tmp;

			tmp = realloc(urlMagicLastURL, length);
			if (!tmp) {
				vlog.error("incomingSignalCallback: (OLDURL) cannot allocate memory!");
				return false;
			}
			urlMagicLastURL = tmp;
		}
		urlMagicLastTime = now;
		urlMagicLastLen = length;
		memcpy(urlMagicLastURL, buf, length);
		vlog.debug("incomingSignalCallback: (OLDURL) starting URL program: %s %s", urlMagicApp.getData(), buf);
		int err = fork();
		if (!err) {
			/* the child */
			char * execargs[3];

			/* close potentially open FDs */
			for (int fd = 0; fd < 20; fd++)
				close(fd);
			execargs[0] = urlMagicApp.getData();
			execargs[1] = (char *) buf;
			execargs[2] = NULL;
			execv(urlMagicApp.getData(), execargs);
			vlog.error("incomingSignalCallback: (OLDURL) could not execute url program: %s", urlMagicApp.getData());
			exit(1);
		} else if (err < 0) {
			vlog.error("incomingSignalCallback: (OLDURL) failed to fork for URL application %s, error %i", urlMagicApp.getData(), err);
		}
	} else if (signal == INITIALURL_SIGNAL_ID) {
		if (length == 0) {
			vlog.error("incomingSignalCallback: URL signal without data!");
			return true;
		}
		vlog.debug("incomingSignalCallback: saving URL to %s", INITIALURL_FILE);
		FILE *f1;
		f1 = fopen(INITIALURL_FILE, "w");
		if (!f1)
			vlog.error("incomingSignalCallback: could not save signalled URL");
		else {
			fprintf(f1, "%s\n", buf);
			fclose(f1);
			vlog.debug("incomingSignalCallback: saved URL %s to %s", buf, INITIALURL_FILE);
		}
	} else if (signal == URL_SIGNAL_ID) {
		if (length == 0) {
			vlog.error("incomingSignalCallback: URL signal without data!");
			return true;
		}
		struct timeval now;
		gettimeofday(&now, 0);
		if (length == urlMagicLastLen && !strncmp((char *) urlMagicLastURL, (char *) buf, length) && now.tv_sec < urlMagicLastTime.tv_sec + 2) {
			vlog.debug("incomingSignalCallback: same URL repeated within 2s, ignoring: %s", buf);
			return true;
		}
		if(!urlMagicLastURL || urlMagicLastLen < length) {
			void * tmp;

			tmp = realloc(urlMagicLastURL, length);
			if (!tmp) {
				vlog.error("incomingSignalCallback: cannot allocate memory!");
				return false;
			}
			urlMagicLastURL = tmp;
		}
		urlMagicLastTime = now;
		urlMagicLastLen = length;
		memcpy(urlMagicLastURL, buf, length);
		vlog.debug("incomingSignalCallback: starting URL program: %s %s", urlMagicApp.getData(), buf);
		int err = fork();
		if (!err) {
			/* the child */
			char * execargs[3];

			/* close potentially open FDs */
			for (int fd = 0; fd < 20; fd++)
				close(fd);
			execargs[0] = urlMagicApp.getData();
			execargs[1] = (char *) buf;
			execargs[2] = NULL;
			execv(urlMagicApp.getData(), execargs);
			vlog.error("incomingSignalCallback: could not execute url program: %s", urlMagicApp.getData());
			exit(1);
		} else if (err < 0) {
			vlog.error("incomingSignalCallback: failed to fork for URL application %s, error %i", urlMagicApp.getData(), err);
		}
	} else if (signal == PACOOKIE_SIGNAL_ID) {
		if (length == 0) {
			vlog.error("incomingSignalCallback: PACOOKIE signal without data!");
			return true;
		}
		vlog.debug("incomingSignalCallback: saving pulseaudio cookie of length %u", length);
		if (writepulsecookie(buf, length))
			vlog.error("incomingSignalCallback: saving pulseaudio cookie failed!");
	} else if (signal == MPSCALING_SIGNAL_ID) {
		if (length != 2) {
			vlog.error("MPSCALING signal with data len not 2!");
			return true;
		}
		const U16 value = *((U16 *) buf);
		if (value != 0) {
			vlog.debug("incomingSignalCallback: MPSCALING: set MP screen scaling to %u", value);
			if (writescaling(value))
				vlog.error("incomingSignalCallback: saving MP screen scaling failed!");
		} else {
			vlog.debug("incomingSignalCallback: MPSCALING: ignore MP screen scaling value 0");
		}
#endif
	} 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 %u, sending pong", *((U32 *) buf));
			((MultiOutStream *)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 %u, sending highpong", *((U32 *) buf));
			((MultiOutStream *)multiOS)->sendSignal(HIGHPONG_SIGNAL_ID, buf, length, 0, QPRIOHIGH);
		}
	} else if (signal == PONG_SIGNAL_ID) {
		U64 pongTime;
		struct timeval now;
		S64 diff;

		if (length != 8) {
			vlog.error("incomingSignalCallback: PONG signal with wrong data length %u!", length);
			return true;
		}

		pongTime = *((U64 *) buf);
		if (frameRateResetTimeout > 0 && !gettimeofday(&now, NULL)) {
			diff = now.tv_sec * 1000000 + now.tv_usec - pongTime;
			if (diff < pongDiffMin && diff > 0)
				pongDiffMin = diff;
			else if (diff > pongDiffMax)
				pongDiffMax = diff;
			if (diff <= rateLow && diff > 0) {
				if (rfb::Server::frameRate < origFrameRate) {
					int newFrameRate = rfb::Server::frameRate * 15 / 10;
					if (newFrameRate > origFrameRate)
						newFrameRate = origFrameRate;
					if (congestionLevel > 0) {
						congestionLevel--;
						rateLow -= frameRateMaxDelayPerLevel * 1000;
						rateHigh -= frameRateMaxDelayPerLevel * 1000;
					}
					vlog.debug("incomingSignalCallback: fast PONG signal, diff %lli us below lower boundary %llu, increase frameRate to %u and congestionLevel to %u, qualityLevel kept at %u, audio %lus ago!", diff, rateLow, newFrameRate, congestionLevel, qualityLevel, now.tv_sec - ((MultiOutStream *)multiOS)->lastAudioTime);
					rfb::Server::frameRate.setParam(newFrameRate);
					signalTarget->frameRateResetTimer.start(frameRateResetTimeout * 1000);
					((MultiOutStream *)multiOS)->sendSignal(CONGESTION_SIGNAL_ID, &congestionLevel, 1);
				} else if (qualityLevel < frameRateTargetQuality) {
					if (congestionLevel > 0) {
						congestionLevel--;
						rateLow -= frameRateMaxDelayPerLevel * 1000;
						rateHigh -= frameRateMaxDelayPerLevel * 1000;
					}
					qualityLevel++;
					TightJPEGEncoder::acceptSetQuality = false;
					TightJPEGEncoder::qualityLevel = qualityLevel;
					TightMPEncoder::acceptSetLevel = false;
					TightMPEncoder::mpLevel = qualityLevel;
					vlog.debug("incomingSignalCallback: fast PONG signal, diff %lli us below lower boundary %llu, set qualityLevel to %u and congestionLevel to %u, audio %lus ago!", diff, rateLow, qualityLevel, congestionLevel, now.tv_sec - ((MultiOutStream *)multiOS)->lastAudioTime);
					signalTarget->frameRateResetTimer.start(frameRateResetTimeout * 1000);
					((MultiOutStream *)multiOS)->sendSignal(CONGESTION_SIGNAL_ID, &congestionLevel, 1);
				} else {
					congestionLevel = 0;
					rateLow = frameRateMaxDelayLow * 1000;
					rateHigh = frameRateMaxDelayHigh * 1000;
					vlog.debug("incomingSignalCallback: fast PONG signal, diff %lli us below lower boundary %llu, keep frameRate at original %u and qualityLevel at %u, audio %lus ago!", diff, rateLow, origFrameRate, qualityLevel, now.tv_sec - ((MultiOutStream *)multiOS)->lastAudioTime);
					if (qualityLevel < frameRateMaxQuality) {
						signalTarget->frameRateResetTimer.start(frameRateResetTimeout * 1000);
					} else {
						signalTarget->frameRateResetTimer.stop();
					}
					TightJPEGEncoder::acceptSetQuality = true;
					TightMPEncoder::acceptSetLevel = true;
				}
			} else if (diff > rateLow && diff <= rateHigh) {
				vlog.debug("incomingSignalCallback: PONG signal diff %lli us in range %llu-%llu, keep frameRate %u, qualityLevel %u and congestionLevel %u, audio %lus ago!", diff, rateLow, rateHigh, int(rfb::Server::frameRate), qualityLevel, congestionLevel, now.tv_sec - ((MultiOutStream *)multiOS)->lastAudioTime);
				if (qualityLevel < frameRateMaxQuality) {
					signalTarget->frameRateResetTimer.start(frameRateResetTimeout * 1000);
				} else {
					signalTarget->frameRateResetTimer.stop();
				}
			} else if (diff > rateHigh && diff < 100000000) {
				if (qualityLevel > frameRateMinQuality) {
					qualityLevel--;
					TightJPEGEncoder::acceptSetQuality = false;
					TightJPEGEncoder::qualityLevel = qualityLevel;
					TightMPEncoder::acceptSetLevel = false;
					TightMPEncoder::mpLevel = qualityLevel;
					congestionLevel++;
					vlog.debug("incomingSignalCallback: slow PONG signal, diff %lli us over upper boundary %llu, set qualityLevel to %u and congestionLevel to %u, audio %lus ago!", diff, rateHigh, qualityLevel, congestionLevel, now.tv_sec - ((MultiOutStream *)multiOS)->lastAudioTime);
					rateLow += frameRateMaxDelayPerLevel * 1000;
					rateHigh += frameRateMaxDelayPerLevel * 1000;
					((MultiOutStream *)multiOS)->sendSignal(CONGESTION_SIGNAL_ID, &congestionLevel, 1);
				} else if(rfb::Server::frameRate > frameRateMin) {
					int newFrameRate = rfb::Server::frameRate * 10 / 15;
					congestionLevel++;
					vlog.debug("incomingSignalCallback: slow PONG signal, diff %lli us, decrease frameRate to %u and congestionLevel to %u, qualityLevel kept at %u, audio %lus ago!", diff, newFrameRate, congestionLevel, qualityLevel, now.tv_sec - ((MultiOutStream *)multiOS)->lastAudioTime);
					rateLow += frameRateMaxDelayPerLevel * 1000;
					rateHigh += frameRateMaxDelayPerLevel * 1000;
					rfb::Server::frameRate.setParam(newFrameRate);
					((MultiOutStream *)multiOS)->sendSignal(CONGESTION_SIGNAL_ID, &congestionLevel, 1);
				}
				signalTarget->frameRateResetTimer.start(frameRateResetTimeout * 1000);
			} else {
				vlog.debug("incomingSignalCallback: PONG signal diff %lli us is out of range, pong time is %llu, now is %lu ignored, audio %lus ago!", diff, pongTime, now.tv_sec * 1000000 + now.tv_usec, now.tv_sec - ((MultiOutStream *)multiOS)->lastAudioTime);
			}
			if (congestionLevel > 0) {
				rateLow += frameRateMaxDelayPerCheck;
				rateHigh += frameRateMaxDelayPerCheck;
			}
		}
	} 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 == AUTOTRANSFER_ENABLED_SIGNAL_ID) {
		if (length != 1) {
			vlog.error("AUTOTRANSFER_ENABLED signal without data!");
			return true;
		}
		vlog.debug("incomingSignalCallback: AUTOTRANSFER_ENABLED: value %u", *buf);
		if (*buf) {
			SAutotransferDirWatcher::clientSupportsAutotransfer = true;
		} else {
			SAutotransferDirWatcher::clientSupportsAutotransfer = false;
		}
	} else if (signal == WEBCAM_ENABLED_SIGNAL_ID) {
		if (length != 1) {
			vlog.error("WEBCAM_ENABLED signal without data!");
			return true;
		}
		vlog.debug("incomingSignalCallback: WEBCAM_ENABLED: value %u", *buf);
		if (webcamHandler) {
			if (*buf)
				webcamHandler->setClientSupportsWebcam(true);
			else
				webcamHandler->setClientSupportsWebcam(false);
		}
	} 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: client 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);
		if (localtime > remotetime + 10 || remotetime > localtime + 10) {
			vlog.error("incomingSignalCallback: LOCALTIME: client time %lu (%s), local time %lu (%s), times differ more than 10s!", remotetime, ctime_r(&remotetime, remotebuf), localtime, ctime_r(&localtime, localbuf));
#if !defined(WIN32) && !defined(WIN64)
			openlog("Xtightgatevnc", LOG_PID, LOG_AUTH);
			syslog(LOG_DEBUG, "client time %lu (%s), local time %lu (%s), times differ more than 10s!", remotetime, ctime_r(&remotetime, remotebuf), localtime, ctime_r(&localtime, localbuf));
#endif
		} else {
			vlog.info("incomingSignalCallback: LOCALTIME: client time %lu (%s), local time %lu (%s)", remotetime, ctime_r(&remotetime, remotebuf), localtime, ctime_r(&localtime, localbuf));
		}
	} else
		vlog.info("incomingSignalCallback: received unknown signal %u", signal);
	return true;
}


void SSecurityMulti::postAuth() {
#ifndef WIN32
	deleteAllPdfs();
	unlink(paInSockaddr.sun_path);
	const size_t paInSockAddrLen = strlen(paInSockaddr.sun_path) + sizeof(paInSockaddr.sun_family);
	mode_t saved_umask = umask(0177);
	if (bind(paInSocket, (struct sockaddr *)&paInSockaddr, paInSockAddrLen) < 0) {
		umask(saved_umask);
		vlog.error("Bind unix socket for Pulseaudio failed: %s.", strerror(errno));
		close(paInSocket);
		paInSocket = -1;
		return;
	}
	umask(saved_umask);
	webcamHandler = new SWebcamHandler(this);

	THREAD_CREATE(paListenerThread, paListenerThreadId, this);
	THREAD_SET_NAME(paListenerThreadId, "tg-paListener");

	struct passwd* pw = getpwuid(geteuid());
	if (!pw || !pw->pw_name) {
		vlog.error("%s: getpwuid(geteuid()) failed", __func__);
	} else {
		std::string effectiveUser = pw->pw_name;
		std::string autotransferNowDir = "/home/user/.autotransfer-now/" + effectiveUser;
		if (!file_exists(autotransferNowDir.c_str())) {
			vlog.debug("%s: %s doesn't exist. Not starting any type of autotransfer thread (mime, vaithex, opswat...)", __func__, autotransferNowDir.c_str());
		} else {
			autotransferNow = new SAutotransferNow(this); // This takes care itself of creating a Thread!
			autotransferNow->start();

			std::string autotransferDir = "/home/user/.transfer/" + effectiveUser + "/autotransfer";
			if (file_exists(autotransferDir.c_str())) {
				autotransferMime = new SAutotransferMime(); // This takes care itself of creating a Thread!
				autotransferMime->start();
			}

			std::string opswatDir = "/home/user/.transfer/" + effectiveUser + "/opswat";
			if (file_exists(opswatDir.c_str())) {
				autotransferOpswat = new SAutotransferOpswat(); // This takes care itself of creating a Thread!
				autotransferOpswat->start();
			}

			std::string vaithexDir = "/home/user/.transfer/" + effectiveUser + "/vaithex";
			if (file_exists(vaithexDir.c_str())) {
				autotransferVaithex = new SAutotransferVaithex(); // This takes care itself of creating a Thread!
				autotransferVaithex->start();
			}
		}
	}

	FILE* fp = fopen(FLAG_FILE, "ab+");
	if (fp)
		fclose(fp);
	else
		vlog.error("Could not create flag file '%s'.", FLAG_FILE);
	if (clientSupportsPrint) {
		vlog.debug("client supports print, set usr2SignalHandler");
		signal(SIGUSR2, usr2SignalHandler);
	} else {
		vlog.debug("client does not support print, disable usr2SignalHandler");
		signal(SIGUSR2, SIG_IGN);
	}
#endif
}

void SSecurityMulti::setClientSupportsSound(bool value)
{
	clientSupportsSound = value;
}

void SSecurityMulti::setClientSupportsPrint(bool value)
{
	if (value) {
		if (!clientSupportsPrint)
			vlog.debug("setClientSupportsPrint: client now supports printing, set usr2SignalHandler");
#ifndef WIN32
		signal(SIGUSR2, usr2SignalHandler);
#endif
	} else {
		if (clientSupportsPrint)
			vlog.debug("setClientSupportsPrint: client no longer supports printing, disable usr2SignalHandler");
#ifndef WIN32
		signal(SIGUSR2, SIG_IGN);
#endif
	}
	clientSupportsPrint = value;
}

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

	vlog.debug("Processing security message");
	if (!multiIS) {
		vlog.debug("Creating MultiInStream");
		multiIS = new rdr::MultiInStream(sc->getInStream());
	}
	if (!multiOS) {
		vlog.debug("Creating MultiOutStream");
		multiOS = new rdr::MultiOutStream(sc->getOutStream());
	}
	multiIS->setCallback(PULSEAUDIO_STREAM_ID, incomingPaCallback);
	multiIS->setCallback(SIGNAL_STREAM_ID, incomingSignalCallback);
	multiIS->setCallback(WEBCAM_VIDEO_STREAM_ID, incomingWebcamFfmpegCallback);
	multiIS->setCallback(OLD_WEBCAM_VIDEO_STREAM_ID, incomingOldWebcamFfmpegCallback);
	vlog.debug("Calling setStreams()");
	sc->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);

	if (paReadBufferSize > MAXPABUF)
		paReadBufferSizeInt = MAXPABUF;
	else if (paReadBufferSize < MINPABUF)
		paReadBufferSizeInt = MINPABUF;
	else
		paReadBufferSizeInt = paReadBufferSize;
	vlog.debug("Processing security message done");
	return true;
}

bool SSecurityMulti::handleTimeout(rfb::Timer * t)
{
	if (t == &keepAliveTimer) {
		vlog.error("keep-alive timed out after %us, client assumed dead, exiting!",
			keepAliveTimeoutInt / 1000);
#if !defined(WIN32) && !defined(WIN64)
		openlog("Xtightgatevnc", LOG_PID, LOG_AUTH);
		syslog(LOG_DEBUG, "Keep-alive timed out after %us, uid %u, exiting!",
			keepAliveTimeoutInt / 1000, getuid());
#endif
		runThreads = false;
		delete multiIS;
		delete multiOS;
#ifndef WIN32
		unlink(paInSockaddr.sun_path);
		unlink(NOAUDIO_FILE);
		unlink(NOPRINT_FILE);
#endif
		exit(10);
	} else if (t == &frameRateResetTimer) {
		rateLow = frameRateMaxDelayLow * 1000;
		rateHigh = frameRateMaxDelayHigh * 1000;
		vlog.debug("handleTimeout: reset frameRate to %u, qualityLevel to %u, congestionLevel to 0, rateLow to %llu, rateHigh to %llu", origFrameRate, int(frameRateMaxQuality), rateLow, rateHigh);
		if (rfb::Server::frameRate != origFrameRate)
			rfb::Server::frameRate.setParam(origFrameRate);
		qualityLevel = frameRateMaxQuality;
		if (TightJPEGEncoder::qualityLevel != -1)
			TightJPEGEncoder::qualityLevel = frameRateMaxQuality;
		TightJPEGEncoder::acceptSetQuality = true;
		if (TightMPEncoder::mpLevel != -1)
			TightMPEncoder::mpLevel = frameRateMaxQuality;
		TightMPEncoder::acceptSetLevel = true;
		congestionLevel = 0;
		((MultiOutStream *)multiOS)->sendSignal(CONGESTION_SIGNAL_ID, &congestionLevel, 1);
		return false;
	} else
		return false;
}

void* SSecurityMulti::sendFile(const char* path, rdr::U8 streamId, const unsigned int protocol = PRINT_PROTOCOL_V1, bool sendOnlyFilename = false) {
#if !defined(WIN32) && !defined(WIN64)
	struct stat buffer;

	if (unlikely(stat(path, &buffer) != 0)) {
		vlog.error("I could not stat the file %s.", path);
		vlog.error("OS error message: %s", strerror(errno));
		// pthread_exit(NULL);
		return NULL; // Not exiting the thread might be a problem with PDF's, but I don't think so.
	}

	FILE* file = fopen(path, "rb");
	if (unlikely(!file)) {
		vlog.error("I could not open the file %s.", path);
		vlog.error("OS error message: %s", strerror(errno));
		// pthread_exit(NULL);
		return NULL; // Not exiting the thread might be a problem with PDF's, but I don't think so.
	}

	vlog.debug("Deleting file to send %s", path);
	int err = unlink(path);
	if (err == -1) {
		vlog.info("I could not delete file to send %s, error %s, retrying after 1s", path, strerror(errno));
		Sleep(1000);
		err = unlink(path);
		if (err == -1) {
			vlog.error("I could not delete file to send %s at second try, error %s, giving up!", path, strerror(errno));
		}
	}
	if (sendOnlyFilename) {
		if (const char* lastSlash = strrchr(path, '/')) {
			path = lastSlash + 1;
		}
	}
	const size_t pathLen = strlen(path);
	if (pathLen > 4 * 1024 - 256) {
		vlog.error("%s: Filename too long: %lu characters.", __func__, pathLen);
		fclose(file);
		// pthread_exit(NULL);
		return NULL; // Not exiting the thread might be a problem with PDF's, but I don't think so.
	}

	// optimize for multi chunk size
	// see PRINTMAXCHUNK in common/rdr/MultiOutStream.cxx
	// for compatibility with old clients, printing must use less than 1 chunk
	size_t sendBufferLen = 4*1024 - 4;
	ssize_t sentLen = 0;

	vlog.debug("%s: Sending file with multi '%s', size %zu", __func__, path, buffer.st_size);
	rdr::U32 packageNumber = 0;
	rdr::U8* sendBuffer = new rdr::U8[sendBufferLen];
	bool doFlush;
	while (!feof(file)) {
		rdr::U8* bufPtr = sendBuffer;
		*bufPtr++ = protocol; // The first byte is the protocol (see SSecurityMulti.h for more info)
		strcpy((char*) bufPtr, path);
		bufPtr += pathLen + 1;
		*(rdr::U32*)bufPtr = packageNumber++;
		bufPtr += 4;
		rdr::U8* finalPackage = bufPtr++;
		const size_t bytesToRead = sendBufferLen - (bufPtr - sendBuffer);
		const size_t readBytes = fread(bufPtr, 1, bytesToRead, file);
		if (unlikely(readBytes != bytesToRead && ferror(file))) {
			vlog.error("Error reading from file '%s'", path);

			//			pthread_exit(NULL);
			break;
		}
		bufPtr += readBytes;
		*finalPackage = feof(file) ? 1 : 0;
		sentLen += readBytes;

		if (unlikely((bufPtr - sendBuffer) > (int) sendBufferLen))
			throw rfb::Exception("Buffer overrun in sendFile()");

		if (unlikely(!packageNumber)) {
			*finalPackage = 1;
			vlog.error("%s: sending file '%s' overflows 32 Bit package counter, cutting transfer after %zu bytes", __func__, path, sentLen);
		}
		if (*finalPackage == 1 || (packageNumber % sendFileBurst) == 0) {
			doFlush = true;
			/* sleep 100 ms to let Multi get some breath */
			Sleep(100);
			vlog.verbose("%s: requesting flush() for file '%s' at packet %u", __func__, path, packageNumber);
		} else {
			doFlush = false;
		}
		multiOS->writeBuffer(sendBuffer, bufPtr - sendBuffer, streamId, doFlush);
		if (!packageNumber)
			break;
	}

	delete[] sendBuffer;

	fclose(file);

	vlog.debug("%s: sending file '%s' completed after %zu bytes in %u packages", __func__, path, sentLen, packageNumber);
	if (unlikely(buffer.st_size != sentLen))
		vlog.error("%s: sending file '%s' size mismatch: size is %zu, but sent %zu bytes", __func__, path, buffer.st_size, sentLen);

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

	return NULL;
}

void* SSecurityMulti::sendPdf(void* _pdfFileName) {
#ifndef WIN32
	char* pdfFileName = (char*) _pdfFileName;
	char pdfFilePath[4096];

	snprintf(pdfFilePath, 4095, "/home/user/.spool/%s/%s", getpwuid(geteuid())->pw_name, pdfFileName);
	pdfFilePath[4095] = 0;
	sendFile(pdfFilePath, FILE_TRANSFER_STREAM_ID, PRINT_PROTOCOL_V1, true);
#endif
	return NULL;
}


void SSecurityMulti::usr2SignalHandler(int sig) {
#ifndef WIN32
	pthread_t thread_id;
	pthread_attr_t thread_attr;
	pthread_attr_init(&thread_attr);
	pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED);
	if (unlikely(pthread_create(&thread_id, &thread_attr, sendPdfThread, NULL) < 0))
		vlog.error("Could not create sendPdf thread.");
	pthread_attr_destroy(&thread_attr);
	THREAD_SET_NAME(thread_id, "tg-sendPdf");
#endif
}

bool SSecurityMulti::sendSignal(rdr::U16 sigId, const void* data, rdr::U16 dataLength)
{
	if (likely(multiOS)) {
		return multiOS->sendSignal(sigId, data, dataLength);
	} else {
		return false;
	}
}

inline int check_pdf(const char* file_name) {
	const size_t fn_length = strlen(file_name);
	if (unlikely(fn_length < 4))
		return -1;
	return strcmp(file_name + fn_length - 4, ".pdf");
}


void SSecurityMulti::deleteAllPdfs() {
#ifndef WIN32
	char spoolPath[4096];
	snprintf(spoolPath, 4095, "/home/user/.spool/%s/", getpwuid(geteuid())->pw_name);
	DIR* spoolDir = opendir(spoolPath);
	if (!spoolDir) {
		vlog.error("deleteAllPdfs(): Could not open the user's spool dir: %s", spoolPath);
		return;
	}
	while (struct dirent* tdirent = readdir(spoolDir))
		if (!check_pdf(tdirent->d_name)) {
			char deletePath[4096];
			snprintf(deletePath, 4095, "/home/user/.spool/%s/%s", getpwuid(geteuid())->pw_name, tdirent->d_name);
			unlink(deletePath);
		}
	closedir(spoolDir);
#endif
}


void* SSecurityMulti::sendPdfThread(void* unused) {
#ifndef WIN32
	if (sendPdfThreadRunning) {
		sendPdfThreadRunAgain = true;
		return NULL;
	}
	sendPdfThreadRunning = true;
	char spoolPath[4096];
	snprintf(spoolPath, 4095, "/home/user/.spool/%s/", getpwuid(geteuid())->pw_name);
	do {
		sendPdfThreadRunAgain = false;
		DIR* spoolDir = opendir(spoolPath);
		if (unlikely(!spoolDir)) {
			vlog.error("Could not open the user's spool dir: %s", spoolPath);
			return NULL;
		}
		while (struct dirent* tdirent = readdir(spoolDir)) {
			if (unlikely(!runThreads))
				break;
			if(strstr(tdirent->d_name, "/") || strstr(tdirent->d_name, "\\")) {
				char pdfFilePath[4096];

				vlog.info("Rejecting to send PDF file with / or \\ in its name: %s", tdirent->d_name);
				snprintf(pdfFilePath, 4095, "/home/user/.spool/%s/%s", getpwuid(geteuid())->pw_name, tdirent->d_name);
				pdfFilePath[4095] = 0;
				unlink(pdfFilePath);
				continue;
			}
			if (!check_pdf(tdirent->d_name))
				sendPdf(tdirent->d_name);
		}
		closedir(spoolDir);
	} while (runThreads && sendPdfThreadRunAgain);
#endif
	sendPdfThreadRunning = false;
	return NULL;
}

#endif /* #if !defined(WIN32) && !defined(__APPLE__) We don't need to build a Windows or Mac server at all. Like this we avoid some build fuss. */
