// Windows
#include <Windows.h>

// CRT
#include <stdio.h>
#include <stdlib.h>
#include <process.h>

// FlowSshC
#include "FlowSshC.h"



enum ExitCodes
{
	Success,
	UsageError,
	SessionError, // including connection error 
	FatalError
};


struct Events
{
	HANDLE m_exitEvent;
	HANDLE m_readyEvent;
};


bool IsUnixSocketCandidate(wchar_t const* str)
{
	if (!!str && *str == L'/')
		return true;
	return
		false;
}


bool IsUnixSocket(wchar_t const* str, unsigned int port)
{
	return IsUnixSocketCandidate(str) && port == FLOWSSHC_PORT_UNIXSOCKET;
}


void ErrorHandler(void* handlerData, unsigned int flags, wchar_t const* desc)
{
	wprintf(L"Fatal error: %s\n", desc ? desc : L"<no error description>");
	// For the sake of simplicity, no cleanup is performed here.
	// The lack of cleanup may cause debugger to report memory leaks.
	exit(FatalError);
}


bool HostKeyHandler(void* handlerData, FlowSshC_PublicKey* publicKey)
{
	BSTR md5 = FlowSshC_PublicKey_GetMd5(publicKey);
	BSTR bb = FlowSshC_PublicKey_GetBubbleBabble(publicKey);
	BSTR sha256 = FlowSshC_PublicKey_GetSha256(publicKey);

	wprintf(L"Received the following host key:\n"
			L"  MD5 Fingerprint: %s\n"
			L"  Bubble-Babble: %s\n"
			L"  SHA-256: %s\n",
			md5, bb, sha256);

	SysFreeString(md5);
	SysFreeString(bb);
	SysFreeString(sha256);

	wprintf(L"Type 'yes' to accept the key, any other input to exit: ");

	wchar_t input[10];
	if (!_getws_s(input, 9))
		return false;
	else
		return _wcsicmp(input, L"yes") == 0;
}


bool HostKeySyncHandler(void* handlerData, FlowSshC_PublicKey* publicKey, bool keyVerified)
{
	BSTR md5 = FlowSshC_PublicKey_GetMd5(publicKey);
	BSTR bb = FlowSshC_PublicKey_GetBubbleBabble(publicKey);
	BSTR sha256 = FlowSshC_PublicKey_GetSha256(publicKey);

	wprintf(L"Received the following %s host key for synchronization:\n"
			L"  MD5 Fingerprint: %s\n"
			L"  Bubble-Babble: %s\n"
			L"  SHA-256: %s\n",
			keyVerified ? L"verified" : L"unverified", md5, bb, sha256);

	SysFreeString(md5);
	SysFreeString(bb);
	SysFreeString(sha256);

	return true;
}


void DisconnectHandler(void* handlerData, unsigned int reason, wchar_t const* desc)
{
	wprintf(L"%s", desc);

	SetEvent(((Events*) handlerData)->m_exitEvent);
}

void ConnectProgressHandler(void* handlerData, unsigned int taskState, unsigned int taskSpecificStep, wchar_t const* auxInfo)
{
	if (taskState == FlowSshC_TaskState_Completed)
	{
		SetEvent(((Events*) handlerData)->m_readyEvent);
	}
	else if (taskState == FlowSshC_TaskState_Failed)
	{
		if (taskSpecificStep == FlowSshC_ConnectStep_ConnectToProxy)
			wprintf(L"Connecting to proxy server failed: %s\n",  auxInfo ? auxInfo : L"(no additional info)");
		else if (taskSpecificStep == FlowSshC_ConnectStep_ConnectToSshServer)
			wprintf(L"Connecting to SSH server failed: %s\n",  auxInfo ? auxInfo : L"(no additional info)");
		else if (taskSpecificStep == FlowSshC_ConnectStep_SshVersionString)
			wprintf(L"SSH version string failed: %s\n",  auxInfo ? auxInfo : L"(no additional info)");
		else if (taskSpecificStep == FlowSshC_ConnectStep_SshKeyExchange)
			wprintf(L"SSH key exchange failed: %s\n",  auxInfo ? auxInfo : L"(no additional info)");
		else if (taskSpecificStep == FlowSshC_ConnectStep_SshUserAuth)
			wprintf(L"SSH authentication failed: %s\n",  auxInfo ? auxInfo : L"(no additional info)");
		else
			wprintf(L"Connecting failed at unknown step: %s\n",  auxInfo ? auxInfo : L"(no additional info)");

		SetEvent(((Events*) handlerData)->m_exitEvent);
	}
}

void DisconnectProgressHandler(void* handlerData, unsigned int taskState, unsigned int taskSpecificStep, wchar_t const* auxInfo)
{
	if (taskState == FlowSshC_TaskState_Completed || taskState == FlowSshC_TaskState_Failed)
		SetEvent((HANDLE) handlerData);
}


enum Operations
{
	Add,
	Cancel,
	Invite,
	EnableProxy,
	DisableProxy,
};

struct ForwardingHandlerData
{
	int m_operation;
	bool m_clientToServer;
	wchar_t const* m_listInterface;
	unsigned int m_listPort;
	wchar_t const* m_destHost;
	unsigned int m_destPort;
	HANDLE m_readyEvent;
};

void ForwardingHandler(void* handlerData, unsigned int listPort, FlowSshC_ForwardingErr const* error)
{
	ForwardingHandlerData* data = (ForwardingHandlerData*) handlerData;

	wchar_t const* direction = data->m_clientToServer ? L"client-to-server" : L"server-to-client";

	if (data->m_operation == Add)
	{
		bool listOnUnixSocket = !data->m_clientToServer && IsUnixSocket(data->m_listInterface, (error ? data->m_listPort : listPort));
		bool destIsUnixSocket =  data->m_clientToServer && IsUnixSocket(data->m_destHost, data->m_destPort);
		bool listIp6 = !listOnUnixSocket && wcschr(data->m_listInterface, L':') != NULL;
		bool destIp6 = !destIsUnixSocket && wcschr(data->m_destHost, L':') != NULL;

		wchar_t sListPort[8]; 
		if (listOnUnixSocket) sListPort[0] = L'\0';
		else				  swprintf(sListPort, 8, L":%u", (error ? data->m_listPort : listPort));

		wchar_t sDestPort[8];
		if (destIsUnixSocket) sDestPort[0] = L'\0';
		else				  swprintf(sDestPort, 8, L":%u", data->m_destPort);

		wprintf(L"%s client-side %s forwarding from %s%s%s%s%s to %s%s%s%s%s.\n",
			(error ? L"Error adding" : L"Added"), direction, 
			(listOnUnixSocket ? L"Unix socket " : L""), (listIp6 ? L"[" : L""), data->m_listInterface, (listIp6 ? L"]" : L""), 
			sListPort,
			(destIsUnixSocket ? L"Unix socket " : L""), (destIp6 ? L"[" : L""), data->m_destHost, (destIp6 ? L"]" : L""),
			sDestPort);
	}
	else if (data->m_operation == Cancel)
	{
		bool listOnUnixSocket = !data->m_clientToServer && IsUnixSocket(data->m_listInterface, data->m_listPort);
		bool listIp6 = !listOnUnixSocket && wcschr(data->m_listInterface, L':') != NULL;
		
		wchar_t sListPort[8];
		if (listOnUnixSocket) sListPort[0] = L'\0';
		else				  swprintf(sListPort, 8, L":%u", data->m_listPort);

		wprintf(L"%s client-side %s forwarding on %s%s%s%s%s.\n",
			(error ? L"Error cancelling" : L"Cancelled"), direction, 
			(listOnUnixSocket ? L"Unix socket " : L""), (listIp6 ? L"[" : L""), data->m_listInterface, (listIp6 ? L"]" : L""),
			sListPort);
	}
	else if (data->m_operation == Invite)
		wprintf(L"%s server-side %s forwardings.\n", (error ? L"Error inviting" : L"Invited"), direction);
	else if (data->m_operation == EnableProxy)
	{
		bool listIp6 = wcschr(data->m_listInterface, L':') != NULL;

		wprintf(L"%s proxy forwarding on %s%s%s:%u.\n", (error ? L"Error enabling" : L"Enabled"), 
			(listIp6 ? L"[" : L""), data->m_listInterface, (listIp6 ? L"]" : L""),
			(error ? data->m_listPort : listPort));
	}
	else if (data->m_operation == DisableProxy)
		wprintf(L"%s %s proxy forwarding.\n", (error ? L"Error disabling" : L"Disabled"), direction);

	if (error)
	{
		if (error->m_errCode == FlowSshC_ForwardingErrCode_GeneralError)
			wprintf(L"General error: %s\n", error->m_auxInfo ? error->m_auxInfo : L"(no additional info)");
		else if (error->m_errCode == FlowSshC_ForwardingErrCode_ClientNotConnected)
			wprintf(L"Client is not connected: %s\n", error->m_auxInfo ? error->m_auxInfo : L"(no additional info)");
		else if (error->m_errCode == FlowSshC_ForwardingErrCode_AddressInUse)
			wprintf(L"Address is already in use: %s\n", error->m_auxInfo ? error->m_auxInfo : L"(no additional info)");
		else if (error->m_errCode == FlowSshC_ForwardingErrCode_RejectedByServer)
			wprintf(L"Rejected by the server: %s\n", error->m_auxInfo ? error->m_auxInfo : L"(no additional info)");
		else if (error->m_errCode == FlowSshC_ForwardingErrCode_RuleNotFound)
			wprintf(L"Forwarding rule not found: %s\n", error->m_auxInfo ? error->m_auxInfo : L"(no additional info)");
		else if (error->m_errCode == FlowSshC_ForwardingErrCode_AlreadyInvited)
			wprintf(L"Forwardings already invited: %s\n", error->m_auxInfo ? error->m_auxInfo : L"(no additional info)");
		else if (error->m_errCode == FlowSshC_ForwardingErrCode_AlreadyDisabled)
			wprintf(L"Proxy forwarding already disabled: %s\n", error->m_auxInfo ? error->m_auxInfo : L"(no additional info)");
		else
			wprintf(L"Error code %u: %s\n", error->m_errCode, error->m_auxInfo ? error->m_auxInfo : L"(no additional info)");	
	}

	SetEvent(data->m_readyEvent);
}

void ForwardingLogHandler(void* handlerData, FlowSshC_ForwardingLog const* log)
{
	wprintf(L"%s\n", log->m_desc);
}

BSTR DecodeForwardingRule(wchar_t const* str, bool const c2s, FlowSshC_ForwardingRule* rule)
{
	BSTR bstr = SysAllocString(str);	
	if (bstr)
	{
		wchar_t* str[4];
		wchar_t* ptr = bstr;
		for (int i = 0; i < 4; ++i)
		{
			if (!ptr)
				str[i] = L"";
			else
			{
				str[i] = ptr;
				ptr = wcschr(ptr, L',');
				if (ptr)
				{
					*ptr = L'\0';
					++ptr;
				}
			}
		}

		rule->m_clientToServer = c2s;
		rule->m_listInterface = str[0];
		{
			int const res = swscanf_s(str[1], L"%u", &rule->m_listPort);
			if (!res || res == EOF)
			{
				if (!c2s && IsUnixSocketCandidate(rule->m_listInterface))
					rule->m_listPort = FLOWSSHC_PORT_UNIXSOCKET;
				else
					rule->m_listPort = 0;
			}
		}
		rule->m_destHost = str[2];
		{
			int const res = swscanf_s(str[3], L"%u", &rule->m_destPort);
			if (!res || res == EOF)
			{
				if (c2s && IsUnixSocketCandidate(rule->m_destHost))
					rule->m_destPort = FLOWSSHC_PORT_UNIXSOCKET;
				else if (!res)
					rule->m_destPort = 0;
			}
		}
	}

	return bstr;
}

BSTR DecodeProxyForwarding(wchar_t const* str, FlowSshC_ProxyForwarding* settings)
{
	BSTR bstr = SysAllocString(str);	
	if (bstr)
	{
		wchar_t* str[4];
		wchar_t* ptr = bstr;
		for (int i = 0; i < 4; ++i)
		{
			if (!ptr)
				str[i] = L"";
			else
			{
				str[i] = ptr;
				ptr = wcschr(ptr, L',');
				if (ptr)
				{
					*ptr = L'\0';
					++ptr;
				}
			}
		}

		settings->m_listInterface = str[0];
		if (!swscanf_s(str[1], L"%u", &settings->m_listPort))
			settings->m_listPort = 0;

		settings->m_bindPublicAddressIP4 = *str[2] ? str[2] : NULL;
		settings->m_bindPublicAddressIP6 = *str[3] ? str[3] : NULL;
		settings->m_bindListInterfaceIP4 = NULL;
		settings->m_bindListInterfaceIP6 = NULL;
	}

	return bstr;
}


unsigned __stdcall InputThread(void* param)
{
	BSTR buffer = (BSTR) param;

	wprintf(L"> ");
	if (!_getws_s(buffer, SysStringLen(buffer)))
		wcscpy_s(buffer, SysStringLen(buffer), L"exit");
	return 0;
};


int wmain(int argc, wchar_t const* argv[])
{
	FlowSshC_Initialize(ErrorHandler, 0);

	// For use in deployed applications where Bitvise SSH Client might not be installed, 
	// provide an activation code using SetActCode to ensure that FlowSsh does not  
	// display an evaluation dialog. On computers with Bitvise SSH Client, use of 
	// FlowSsh is permitted under the same terms as the Client; in this case, there 
	// will be no evaluation dialog, and SetActCode does not have to be called.
	//
	//FlowSshC_SetActCode(L"Enter Your Activation Code Here");

	FlowSshC_Client* client = FlowSshC_Client_Create();
	// CHANGE APPLICATION NAME IN PRODUCTION CODE!
	FlowSshC_Client_SetAppName(client, L"FlowSshC_Tnl 1.1");

	// Process command-line parameters

	bool usageError = false;

	bool disableProxy = false;
	bool c2sInvite = false;
	bool s2cInvite = false;
	unsigned int c2sAddCount = 0;
	unsigned int s2cAddCount = 0;
	unsigned int const maxFwds = 100;
	wchar_t const* c2sAdds[maxFwds];
	wchar_t const* s2cAdds[maxFwds];
	wchar_t const* c2sCancel = NULL;
	wchar_t const* s2cCancel = NULL;
	wchar_t const* enableProxy = NULL;
	bool interactive = false;
	bool log = false;
	
	for (int i = 1; i < argc && !usageError; ++i)
	{
		if (wcsncmp(argv[i], L"-user=", 6) == 0)
			FlowSshC_Client_SetUserName(client, argv[i] + 6);
		else if (wcsncmp(argv[i], L"-pw=", 4) == 0)
			FlowSshC_Client_SetPassword(client, argv[i] + 4);
		else if (wcsncmp(argv[i], L"-host=", 6) == 0)
			FlowSshC_Client_SetHost(client, argv[i] + 6);
		else if (wcsncmp(argv[i], L"-port=", 6) == 0)
		{
			unsigned int port;
			if (swscanf_s(argv[i] + 6, L"%u", &port))
				FlowSshC_Client_SetPort(client, port);
		}
		else if (wcscmp(argv[i], L"-c2sInvite") == 0)
			c2sInvite = true;
		else if (wcscmp(argv[i], L"-s2cInvite") == 0)
			s2cInvite = true;
		else if (wcsncmp(argv[i], L"-c2sAdd=", 8) == 0)
		{
			if (c2sAddCount >= maxFwds)
				usageError = true;
			else
				c2sAdds[c2sAddCount++] = argv[i] + 8;
		}
		else if (wcsncmp(argv[i], L"-s2cAdd=", 8) == 0)
		{
			if (s2cAddCount >= maxFwds)
				usageError = true;
			else
				s2cAdds[s2cAddCount++] = argv[i] + 8;
		}
		else if (wcsncmp(argv[i], L"-proxy=", 7) == 0)
		{
			enableProxy = argv[i] + 7;
		}
		else if (wcscmp(argv[i], L"-interactive") == 0)
			interactive = true;
		else if (wcscmp(argv[i], L"-log") == 0)
			log = true;
		else
			usageError = true;
	}

	int exitCode = Success;

	if (usageError)
	{
		wprintf(L"FlowSshC sample tunneling client.\n"
				L"\n"
				L"Parameters:\n"
				L" -host=... (default localhost)\n"
				L" -port=... (default 22)\n"
				L" -user=...\n"
				L" -pw=...\n"
				L" -c2sInvite (optional)\n"
				L" -s2cInvite (optional)\n"
				L" -c2sAdd=... (optional, multiple instances allowed)\n"
				L" -s2cAdd=... (optional, multiple instances allowed)\n"
				L" -proxy=...  (optional)\n"
				L" -interactive (optional)\n"
				L" -log (optional)\n"
				L"\n"
				L" Notes:\n"
				L"  -s2cAdd and -c2sAdd values take the following forms:\n"
				L"    <ListenInterface>,<Port>,<DestinationHost>,<Port>\n"
				L"    <ListenInterface>,<Port>,</UnixSocket> (c2sAdd)\n"
				L"    </UnixSocket>,,<DestinationHost>,<Port> (s2cAdd)\n"
				L"  -proxy takes the following form:\n"
				L"    <ListenInterface>,<Port>[,<IP4PublicAddress>]\n"
				L"                            [,<IP6PublicAddress>]\n"
				L"\n");
		
		if (exitCode == Success)
			exitCode = UsageError;
	}
	else
	{
		// Create events

		Events events;
		events.m_exitEvent = CreateEvent(NULL, true, false, NULL);
		events.m_readyEvent = CreateEvent(NULL, false, false, NULL);

		if (!events.m_exitEvent || !events.m_readyEvent)
		{
			wprintf(L"CreateEvent() failed with Windows error %u.\n", GetLastError());
			exitCode = FatalError;
		}
		else
		{
			// Connect the client

			FlowSshC_Client_SetHostKeyHandler(client, HostKeyHandler, NULL);
			FlowSshC_Client_SetHostKeySyncHandler(client, HostKeySyncHandler, NULL);
			FlowSshC_Client_SetDisconnectHandler(client, DisconnectHandler, &events);
			FlowSshC_Client_Connect(client, ConnectProgressHandler, &events);
			if (log)
				FlowSshC_Client_SetForwardingLogHandler(client, ForwardingLogHandler, NULL);

			HANDLE handles[2] = { events.m_exitEvent, events.m_readyEvent };
			DWORD dw = WaitForMultipleObjects(2, handles, false, INFINITE);
			if (dw == WAIT_FAILED)
			{
				wprintf(L"WaitForMultipleObjects() failed with Windows error %u.\n", GetLastError());
				exitCode = FatalError;
			}
			else if (dw == WAIT_OBJECT_0)
				exitCode = SessionError;
			else
			{
				ForwardingHandlerData data;
				data.m_readyEvent = events.m_readyEvent;

				BSTR inputBuffer = SysAllocStringLen(0, 512);
				if (!inputBuffer)
				{
					wprintf(L"SysAllocString() failed.\n");
					exitCode = FatalError;
				}
			
				while (true)
				{
					if (exitCode != Success)
						break;

					for (unsigned int i = 0; c2sCancel || (i < c2sAddCount); )
					{
						FlowSshC_ForwardingRule rule;
						BSTR bstr = DecodeForwardingRule(c2sCancel ? c2sCancel : c2sAdds[i], true, &rule);
						if (!bstr)
						{
							wprintf(L"SysAllocString() failed.\n");
							exitCode = FatalError;
							break;
						}
						else
						{
							data.m_operation = c2sCancel ? Cancel : Add;
							data.m_clientToServer = rule.m_clientToServer;
							data.m_listInterface = rule.m_listInterface;
							data.m_listPort = rule.m_listPort;
							data.m_destHost = rule.m_destHost;
							data.m_destPort = rule.m_destPort;

							if (c2sCancel)
								FlowSshC_Client_CancelForwarding(client, &rule, ForwardingHandler, &data);
							else
								FlowSshC_Client_AddForwarding(client, &rule, ForwardingHandler, &data);

							DWORD dw = WaitForSingleObject(data.m_readyEvent, INFINITE);
							SysFreeString(bstr);
							if (dw == WAIT_FAILED)
							{
								wprintf(L"WaitForSingleObject() failed with Windows error %u.\n", GetLastError());
								exitCode = FatalError;
								break;
							}

							if (c2sCancel) 
								c2sCancel = NULL;
							else
								++i;
						}
					}

					c2sCancel = NULL;
					c2sAddCount = 0;
					if (exitCode != Success)
						break;

					for (unsigned int i = 0; s2cCancel || (i < s2cAddCount); )
					{
						FlowSshC_ForwardingRule rule;
						BSTR bstr = DecodeForwardingRule(s2cCancel ? s2cCancel : s2cAdds[i], false, &rule);
						if (!bstr)
						{
							wprintf(L"SysAllocString() failed.\n");
							exitCode = FatalError;
							break;
						}
						else
						{
							data.m_operation = s2cCancel ? Cancel : Add;
							data.m_clientToServer = rule.m_clientToServer;
							data.m_listInterface = rule.m_listInterface;
							data.m_listPort = rule.m_listPort;
							data.m_destHost = rule.m_destHost;
							data.m_destPort = rule.m_destPort;

							if (s2cCancel)
								FlowSshC_Client_CancelForwarding(client, &rule, ForwardingHandler, &data);
							else
								FlowSshC_Client_AddForwarding(client, &rule, ForwardingHandler, &data);

							DWORD dw = WaitForSingleObject(data.m_readyEvent, INFINITE);
							SysFreeString(bstr);
							if (dw == WAIT_FAILED)
							{
								wprintf(L"WaitForSingleObject() failed with Windows error %u.\n", GetLastError());
								exitCode = FatalError;
								break;
							}

							if (s2cCancel) 
								s2cCancel = NULL;
							else
								++i;
						}
					}

					s2cCancel = NULL;
					s2cAddCount = 0;
					if (exitCode != Success)
						break;

					if (disableProxy)
					{
						data.m_operation = DisableProxy;
						FlowSshC_Client_DisableProxyForwarding(client, ForwardingHandler, &data);
							
						DWORD dw = WaitForSingleObject(data.m_readyEvent, INFINITE);
						if (dw == WAIT_FAILED)
						{
							wprintf(L"WaitForSingleObject() failed with Windows error %u.\n", GetLastError());
							exitCode = FatalError;
							break;
						}

						disableProxy = false;
					}
					if (enableProxy)
					{
						FlowSshC_ProxyForwarding settings;
						BSTR bstr = DecodeProxyForwarding(enableProxy, &settings);
						if (!bstr)
						{
							wprintf(L"SysAllocString() failed.\n");
							exitCode = FatalError;
							break;
						}
						else
						{
							data.m_operation = EnableProxy;
							data.m_listInterface = settings.m_listInterface;
							data.m_listPort = settings.m_listPort;
							
							FlowSshC_Client_EnableProxyForwarding(client, &settings, ForwardingHandler, &data);
							
							DWORD dw = WaitForSingleObject(data.m_readyEvent, INFINITE);
							SysFreeString(bstr);
							if (dw == WAIT_FAILED)
							{
								wprintf(L"WaitForSingleObject() failed with Windows error %u.\n", GetLastError());
								exitCode = FatalError;
								break;
							}

							enableProxy = NULL;
						}
					}
						
					if (c2sInvite)
					{
						data.m_operation = Invite;
						data.m_clientToServer = true;
						FlowSshC_Client_InviteForwardings(client, true, ForwardingHandler, &data);
						
						if (WaitForSingleObject(data.m_readyEvent, INFINITE) == WAIT_FAILED)
						{
							wprintf(L"WaitForSingleObject() failed with Windows error %u.\n", GetLastError());
							exitCode = FatalError;
							break;
						}
					}

					if (s2cInvite)
					{
						data.m_operation = Invite;
						data.m_clientToServer = false;
						FlowSshC_Client_InviteForwardings(client, false, ForwardingHandler, &data);
						
						if (WaitForSingleObject(data.m_readyEvent, INFINITE) == WAIT_FAILED)
						{
							wprintf(L"WaitForSingleObject() failed with Windows error %u.\n", GetLastError());
							exitCode = FatalError;
							break;
						}
					}

					c2sInvite = false;
					s2cInvite = false;

					if (!interactive)
					{
						if (WaitForSingleObject(events.m_exitEvent, INFINITE) == WAIT_FAILED)
						{
							wprintf(L"WaitForSingleObject() failed with Windows error %u.\n", GetLastError());
							exitCode = FatalError;
						}

						break;
					}
				
					HANDLE threadHandle = (HANDLE) _beginthreadex(0, 0, InputThread, inputBuffer, 0, 0);
					if (!threadHandle)
					{
						wprintf(L"_beginthreadex() failed with error code %u.\n", errno);
						exitCode = FatalError;
						break;
					}
			
					HANDLE handles[2] = { events.m_exitEvent, threadHandle };
					DWORD dw = WaitForMultipleObjects(2, handles, false, INFINITE);
					if (dw != WAIT_OBJECT_0 + 1)
					{
						if (dw == WAIT_FAILED)
						{
							wprintf(L"WaitForMultipleObjects() failed with Windows error %u.\n", GetLastError());
							exitCode = FatalError;
						}

						TerminateThread(threadHandle, 0);
						CloseHandle(threadHandle);
						break;
					}

					CloseHandle(threadHandle);

					if (wcscmp(inputBuffer, L"exit") == 0 || wcscmp(inputBuffer, L"quit") == 0)
						break;
					else if (wcsncmp(inputBuffer, L"c2sAdd ", 7) == 0)
					{
						c2sAddCount = 1;
						c2sAdds[0] = ((wchar_t const*) inputBuffer) + 7;
					}
					else if (wcsncmp(inputBuffer, L"s2cAdd ", 7) == 0)
					{
						s2cAddCount = 1;
						s2cAdds[0] = ((wchar_t const*) inputBuffer) + 7;
					}
					else if (wcsncmp(inputBuffer, L"c2sCancel ", 10) == 0)
						c2sCancel = ((wchar_t const*) inputBuffer) + 10;
					else if (wcsncmp(inputBuffer, L"s2cCancel ", 10) == 0)
						s2cCancel = ((wchar_t const*) inputBuffer) + 10;
					else if (wcscmp(inputBuffer, L"c2sInvite") == 0)
						c2sInvite = true;
					else if (wcscmp(inputBuffer, L"s2cInvite") == 0)
						s2cInvite = true;
					else if (wcsncmp(inputBuffer, L"enableProxy ", 12) == 0)
					{
						disableProxy = false;
						enableProxy = ((wchar_t const*) inputBuffer) + 12;
					}
					else if (wcscmp(inputBuffer, L"disableProxy") == 0)
					{
						disableProxy = true;
						enableProxy = NULL;
					}
					else
					{
						wprintf(L"Available commands:\n"
								L"c2sAdd <ListenInterface>,<Port>,<DestinationHost>,<Port>\n"
								L"c2sAdd <ListenInterface>,<Port>,</UnixSocket>\n"
								L"s2cAdd <ListenInterface>,<Port>,<DestinationHost>,<Port>\n"
								L"s2cAdd </UnixSocket>,,<DestinationHost>,<Port>\n"
								L"c2sCancel <ListenInterface>,<Port>\n"
								L"s2cCancel <ListenInterface>,<Port>\n"
								L"s2cCancel </UnixSocket>\n"
								L"c2sInvite\n"
								L"s2cInvite\n"
								L"enableProxy <ListenInterface>,<Port>[,<IP4PublicAddress>][,<IP6PublicAddress>]\n"
								L"disableProxy\n"
								L"exit\n"
								L"\n");
					}
				}

				if (inputBuffer)
					SysFreeString(inputBuffer);
			}
		}

		FlowSshC_Shutdown();

		if (events.m_exitEvent)
			CloseHandle(events.m_exitEvent);
		if (events.m_readyEvent)
			CloseHandle(events.m_readyEvent);
	}

	FlowSshC_Client_Release(client);
	return exitCode;
}