//////////////////////////////////////////////////////////////////////////
//
// pgAdmin III - PostgreSQL Tools
//
// Copyright (C) 2002 - 2016, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
// sshTunnel.cpp - Used to create SSH Tunnels
//
//////////////////////////////////////////////////////////////////////////

// App headers
#include "pgAdmin3.h"
#include <wx/dynlib.h>
#if defined(HAVE_OPENSSL_CRYPTO) || defined(HAVE_GCRYPT)

#pragma comment (lib, "Ws2_32.lib")
#undef ssize_t
#define ssize_t long
#include "libssh2.h"
#include "utils/sshTunnel.h"
#include "frm/frmMain.h"

typedef const char *(*inet_ntop_t) (int af, const void *src, char *dst, socklen_t size);

#ifdef WIN32
const char *custom_inet_ntop(int af, const void *src, char *dst, int cnt)
{
	struct sockaddr_in srcaddr;

	memset(&srcaddr, 0, sizeof(struct sockaddr_in));
	memcpy(&(srcaddr.sin_addr), src, sizeof(srcaddr.sin_addr));

	srcaddr.sin_family = af;
	if (WSAAddressToStringA((struct sockaddr *) &srcaddr, sizeof(struct sockaddr_in), 0, dst, (LPDWORD) &cnt) != 0)
	{

		wxLogInfo(wxT("SSH error: WSAAddressToStringA failed with error code %d"), WSAGetLastError());
		return NULL;
	}
	return dst;
}

static inet_ntop_t gs_fnPtr_inet_ntop = &custom_inet_ntop;
#else
static inet_ntop_t gs_fnPtr_inet_ntop = &inet_ntop;
#endif

char CSSHTunnelThread::m_keyboard_interactive_pwd[SSH_MAX_PASSWORD_LEN];

CSSHTunnelThread::CSSHTunnelThread(const wxString tunnelhost, const wxString remote_desthost, const unsigned int remote_destport,
                                   const wxString username, const wxString password, const wxString publickey, const wxString privatekey,
                                   const enAuthenticationMethod &enAuthMethod, const unsigned int tunnelPort)
	: m_tunnelhost(tunnelhost), m_remote_desthost(remote_desthost), m_remote_destport(remote_destport), m_username(username),
	  m_password(password), m_publickey(publickey), m_privatekey(privatekey), m_enAuthMethod(enAuthMethod), m_tunnelPort(tunnelPort)
{
	m_local_listenip = wxEmptyString;
	m_local_listenport = 0;
	m_listensock = -1, m_sock = -1;
	m_session = NULL;

	memset(m_keyboard_interactive_pwd, 0 , strlen(m_keyboard_interactive_pwd));
	strncpy(m_keyboard_interactive_pwd, (const char *)password.mb_str(wxConvUTF8), password.Length());
}

CSSHTunnelThread::~CSSHTunnelThread(void)
{
}

bool CSSHTunnelThread::Initialize()
{
	int rc, auth = AUTH_NONE;
	const char *fingerprint;
	char *userauthlist;

#ifdef WIN32
	char sockopt;
	WSADATA wsadata;
	int err;

	err = WSAStartup(MAKEWORD(2, 0), &wsadata);
	if(err != 0)
	{
		wxLogInfo(wxT("WSAStartup failed with error: %d"), err);
		return false;
	}
#else
	int sockopt;
#endif

	wxArrayString arrTunnelHostIP;

	if (resolveDNS(m_tunnelhost.mb_str(), arrTunnelHostIP))
	{
		rc = libssh2_init (0);

		if (rc != 0)
		{
			LogSSHTunnelErrors(wxString::Format(_("libssh2 initialization failed with error code %d"), rc), GetId());
			return false;
		}

		/* Connect to SSH server */
		m_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
		m_sin.sin_family = AF_INET;
		if (INADDR_NONE == (m_sin.sin_addr.s_addr = inet_addr(arrTunnelHostIP.Item(0).mb_str())))
		{
			LogSSHTunnelErrors(wxString::Format(_("SSH error: Error in inet address with error code %d"), wxSysErrorCode()), GetId());
			return false;
		}
		m_sin.sin_port = htons(m_tunnelPort);
		if (connect(m_sock, (struct sockaddr *)(&m_sin),
		            sizeof(struct sockaddr_in)) != 0)
		{
			LogSSHTunnelErrors(wxString::Format(_("SSH error: Could not connect to socket with error code %d"), wxSysErrorCode()), GetId());
			return false;
		}

		/* Create a session instance */
		m_session = libssh2_session_init();

		if (!m_session)
		{
			LogSSHTunnelErrors(_("SSH error: Could not initialize SSH session!"), GetId());
			return false;
		}

		/* ... start it up. This will trade welcome banners, exchange keys,
		* and setup crypto, compression, and MAC layers
		*/
		rc = libssh2_session_handshake(m_session, m_sock);
		if (rc)
		{
			LogSSHTunnelErrors(wxString::Format(_("SSH error: Error when starting up SSH session with error code %d"), rc), GetId(), m_session);
			return false;
		}

		/* At this point we havn't yet authenticated.  The first thing to do
		* is check the hostkey's fingerprint against our known hosts Your app
		* may have it hard coded, may go to a file, may present it to the
		* user, that's your call
		*/
		fingerprint = libssh2_hostkey_hash(m_session, LIBSSH2_HOSTKEY_HASH_SHA1);
		wxString newHostKey = wxEmptyString;
		for(int i = 0; i < 20; i++)
		{
			newHostKey += wxString::Format(wxT("%02X "), (unsigned char)fingerprint[i]);
		}

		// Check if the SSH Host Key is verified
		if(!IsHostKeyVerified(newHostKey))
		{
			Cleanup();
			return false;
		}


		/* check what authentication methods are available */
		userauthlist = libssh2_userauth_list(m_session, m_username.mb_str(), strlen(m_username.mb_str()));

		if (strstr(userauthlist, "password"))
			auth |= AUTH_PASSWORD;
		if(strstr(userauthlist, "keyboard-interactive"))
			auth |= AUTH_KEYBOARD_INTERACTIVE;
		if (strstr(userauthlist, "publickey"))
			auth |= AUTH_PUBLICKEY;

		if ((auth & AUTH_PASSWORD) && (m_enAuthMethod == AUTH_PASSWORD))
			auth = AUTH_PASSWORD;
		else if ((auth & AUTH_KEYBOARD_INTERACTIVE) && (m_enAuthMethod == AUTH_PASSWORD))
			auth = AUTH_KEYBOARD_INTERACTIVE;
		if ((auth & AUTH_PUBLICKEY) && (m_enAuthMethod == AUTH_PUBLICKEY))
			auth = AUTH_PUBLICKEY;

		if (auth & AUTH_PASSWORD)
		{
			rc = libssh2_userauth_password(m_session, m_username.mb_str(), m_password.mb_str());
			if (rc)
			{
				LogSSHTunnelErrors(wxString::Format(_("SSH error: Authentication by password failed with error code %d"), rc), GetId(), m_session);
				Cleanup();
				return false;
			}
		}
		else if (auth & AUTH_KEYBOARD_INTERACTIVE)
		{
			rc = libssh2_userauth_keyboard_interactive(m_session, m_username.mb_str(), &CSSHTunnelThread::keyboard_interactive);
			if (rc)
			{
				LogSSHTunnelErrors(wxString::Format(_("SSH error: Authentication by password failed with error code %d"), rc), GetId(), m_session);
				Cleanup();
				return false;
			}
		}
		else if (auth & AUTH_PUBLICKEY)
		{
#ifdef HAVE_GCRYPT
			rc = libssh2_userauth_publickey_fromfile(m_session, m_username.mb_str(), m_publickey.mb_str(), m_privatekey.mb_str(), m_password.mb_str());
#else
			rc = libssh2_userauth_publickey_fromfile(m_session, m_username.mb_str(), NULL, m_privatekey.mb_str(), m_password.mb_str());
#endif
			if (rc)
			{
				LogSSHTunnelErrors(wxString::Format(_("SSH error: Authentication by identity file failed with error code %d"), rc), GetId(), m_session);
				Cleanup();
				return false;
			}
		}
		else
		{
			LogSSHTunnelErrors(_("SSH error: No supported authentication methods found!"), GetId());
			Cleanup();
			return false;
		}

		// Get the IP Address of local machine
		wxArrayString arrLocalIP;
		if(resolveDNS("localhost", arrLocalIP))
		{
			m_listensock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
			memset(&m_sin, 0 , sizeof(m_sin));
			m_sin.sin_family = AF_INET;

			// Give port no to 0 so that bind will automatically select the available port.
			m_sin.sin_port = htons(0);
			if (INADDR_NONE == (m_sin.sin_addr.s_addr = inet_addr(arrLocalIP.Item(0).mb_str())))
			{
				Cleanup();
				return false;
			}

			sockopt = 1;
			setsockopt(m_listensock, SOL_SOCKET, SO_REUSEADDR, &sockopt, sizeof(sockopt));
			m_sinlen = sizeof(m_sin);
			if (-1 == bind(m_listensock, (struct sockaddr *)&m_sin, m_sinlen))
			{
				LogSSHTunnelErrors(wxString::Format(_("SSH error: bind failed with error code %d"), wxSysErrorCode()), GetId());
				Cleanup();
				return false;
			}

			if (getsockname(m_listensock, (struct sockaddr *)&m_sin, &m_sinlen) == -1)
			{
				LogSSHTunnelErrors(wxString::Format(_("SSH error: getsockname() failed with error code %d"), wxSysErrorCode()), GetId());
				Cleanup();
				return false;
			}

			if (-1 == listen(m_listensock, 2))
			{
				LogSSHTunnelErrors(wxString::Format(_("SSH error: listen failed with error code %d"), wxSysErrorCode()), GetId());
				Cleanup();
				return false;
			}

			m_local_listenip = wxString(inet_ntoa(m_sin.sin_addr), wxConvLibc);
			m_local_listenport = ntohs(m_sin.sin_port);

			wxLogInfo(wxT("Waiting for TCP connection on %s:%d..."), m_local_listenip.c_str(), m_local_listenport);
			return true;
		}
		else
		{
			LogSSHTunnelErrors(_("SSH error: Unable to resolve localhost"), GetId());
		}
	}
	else
	{
		LogSSHTunnelErrors(wxString::Format(_("SSH error: Unable to resolve host: %s"), m_tunnelhost.c_str()), GetId());
	}

	return false;
}

void *CSSHTunnelThread::Entry()
{
	while (1)
	{
		int forwardsock = accept(m_listensock, (struct sockaddr *) & m_sin, &m_sinlen);
		if (-1 == forwardsock)
		{
#ifdef WIN32
			if(wxSysErrorCode() != WSAEINTR && wxSysErrorCode() != WSAEBADF)
#else
			if(wxSysErrorCode() != EINTR && wxSysErrorCode() != EBADF && wxSysErrorCode() != ECONNABORTED)
#endif
			{
				LogSSHTunnelErrors(wxString::Format(_("SSH error: accept failed with error code %d"), wxSysErrorCode()), GetId());
				Cleanup();
			}
			break;
		}

		// Create thread for read/write.
		CSubThread *subThread = new CSubThread(m_sin, m_remote_desthost, m_remote_destport, m_session, forwardsock);
		if ( subThread->Create() != wxTHREAD_NO_ERROR )
		{
			delete subThread;
			subThread = NULL;
		}
		else
		{
			if (subThread->Run() != wxTHREAD_NO_ERROR )
			{
				delete subThread;
				subThread = NULL;
			}
			else
			{
				g_SSHThreadMutex.Lock();
				g_setSocketDescriptor.insert(forwardsock);
				g_SSHThreadMutex.Unlock();
			}
		}
	}

	return NULL;
}

void CSSHTunnelThread::Cleanup()
{
	// Close all the sockets
	g_SSHThreadMutex.Lock();
	subThreadSDSet::iterator it;
	for( it = g_setSocketDescriptor.begin(); it != g_setSocketDescriptor.end(); ++it )
	{
		int socketDescriptor = *it;
#ifdef WIN32
		closesocket(socketDescriptor);
#else
		close(socketDescriptor);
#endif
	}
	g_SSHThreadMutex.Unlock();

	Sleep(1000);

	if(m_session)
	{
		libssh2_session_disconnect(m_session, "Client disconnecting normally");
		libssh2_session_free(m_session);
		m_session = NULL;
	}

	if (m_listensock)
	{
#ifdef WIN32
		closesocket(m_listensock);
#else
		close(m_listensock);
#endif
		m_listensock = 0;
	}

	if (m_sock)
	{
#ifdef WIN32
		closesocket(m_sock);
#else
		close(m_sock);
#endif
		m_sock = 0;
	}

	libssh2_exit();
}

bool CSSHTunnelThread::resolveDNS(const char *host, wxArrayString &arrIPAddress)
{
	struct addrinfo hints, *res, *p;
	int status;
	char ipstr[INET6_ADDRSTRLEN];
#ifndef AI_ADDRCONFIG
#define AI_ADDRCONFIG 0
#endif
	memset(&hints, 0, sizeof hints);
	memset(&ipstr, 0, sizeof ipstr);
	hints.ai_family = AF_UNSPEC; // AF_INET or AF_INET6 to force version
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_flags = AI_ADDRCONFIG | AI_PASSIVE;

	if ((status = getaddrinfo(host, NULL, &hints, &res)) != 0)
	{
		wxLogInfo(wxT("getaddrinfo failed with error code: %d"), status);
		return false;
	}

	for(p = res; p != NULL; p = p->ai_next)
	{
		void *addr;
		if (p->ai_family == AF_INET)
		{
			struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr;
			addr = &(ipv4->sin_addr);

			/* convert the IP to a string*/
			if(NULL != gs_fnPtr_inet_ntop(p->ai_family, addr, ipstr, sizeof ipstr))
				arrIPAddress.Add(wxString(ipstr, wxConvLocal));
		}
	}

	if(res)
	{
		freeaddrinfo(res); // free the linked list
	}

	if(arrIPAddress.Count() > 0)
	{
		return true;
	}

	return false;
}

void CSSHTunnelThread::keyboard_interactive(const char *name, int name_len, const char *instr, int instr_len,
        int num_prompts,  const struct _LIBSSH2_USERAUTH_KBDINT_PROMPT *prompts, struct _LIBSSH2_USERAUTH_KBDINT_RESPONSE *res,
        void **abstract)
{
	if (num_prompts == 1)
	{
		res[0].text = strdup(m_keyboard_interactive_pwd);
		res[0].length = strlen(m_keyboard_interactive_pwd);
	}
}

bool CSSHTunnelThread::IsHostKeyVerified(const wxString &newHostKey)
{
	bool bIsVerified = false;
	wxString cachedHostKey = settings->Read(wxT("HostKeys/") + m_tunnelhost, wxEmptyString);

	// If cached host key is empty then ask user to accept or reject
	if (cachedHostKey == wxEmptyString)
	{
		// Prompt User to accept or reject
		wxString msg = wxString::Format(wxT("Host key received for the SSH server \"%s\" is \n\n%s\n\nWould you like to accept it and continue with the connection?"), m_tunnelhost.c_str(), newHostKey.c_str());
		int answer = wxMessageBox(msg, wxT("Host key verification"), wxYES_NO | wxNO_DEFAULT);
		if (answer == wxYES)
		{
			// Write the host key with respect to tunnel host
			settings->Write(wxT("HostKeys/") + m_tunnelhost, newHostKey);
			bIsVerified = true;
		}
	}
	else
	{
		// Compare the cached host key with the new key
		if (cachedHostKey.compare(newHostKey) == 0)
			bIsVerified = true;
		else
		{
			// Prompt user to accept or reject the new host key received for the tunnel host
			wxString msg = wxString::Format(wxT("The host key received from the server \"%s\" is\n\n%s\n\nbut the stored key is\n\n%s\n\nThis may indicate that this is not the same server that was previously used.\n\nDo you wish to accept and store the new key, and continue with the connection?"), m_tunnelhost.c_str(), newHostKey.c_str(), cachedHostKey.c_str());
			int answer = wxMessageBox(msg, wxT("Host key verification - WARNING"), wxYES_NO | wxNO_DEFAULT);
			if (answer == wxYES)
			{
				// Write the host key with respect to tunnel host
				settings->Write(wxT("HostKeys/") + m_tunnelhost, newHostKey);
				bIsVerified = true;
			}
		}
	}

	return bIsVerified;
}

CSubThread::CSubThread(const struct sockaddr_in sin, const wxString remote_desthost, const unsigned int remote_destport,
                       LIBSSH2_SESSION *session, int forwardsock)
	: m_sin(sin), m_remote_desthost(remote_desthost), m_remote_destport(remote_destport),
	  m_subThreadSession(session), m_forwardsock(forwardsock)
{
}

CSubThread::~CSubThread(void)
{
	g_SSHThreadMutex.Lock();
	g_setSocketDescriptor.erase(m_forwardsock);
	g_SSHThreadMutex.Unlock();
}

void *
CSubThread::Entry()
{
	fd_set fds;
	struct timeval tv;
	ssize_t len, wr;
	char buf[20480];
	int rc, i = 0;

	const char *shost = inet_ntoa(m_sin.sin_addr);
	unsigned int sport = ntohs(m_sin.sin_port);

	wxLogInfo(wxT("Forwarding connection from %s:%d to %s:%d"), wxString(inet_ntoa(m_sin.sin_addr), wxConvLibc).c_str(),
	          sport, m_remote_desthost.c_str(), m_remote_destport);

	/* Must use blocking here to avoid connect errors */
	//libssh2_session_set_blocking(m_subThreadSession, 1);

	while((m_channel = libssh2_channel_direct_tcpip_ex(m_subThreadSession, m_remote_desthost.mb_str(),
	                   m_remote_destport, shost, sport)) == NULL)
	{
		rc = libssh2_session_last_error(m_subThreadSession, NULL, NULL, 0);
		if (rc == LIBSSH2_ERROR_EAGAIN)
			Sleep(10);
		else
			break;
	}

	/* Must use non-blocking IO hereafter due to the current libssh2 API */
	libssh2_session_set_blocking(m_subThreadSession, 0);

	if (!m_channel)
	{
		wxLogInfo(_("SSH error: Could not open a direct-tcpip channel!"));
		goto shutdown;
	}

	while (1)
	{
		FD_ZERO(&fds);
		FD_SET(m_forwardsock, &fds);
		tv.tv_sec = 0;
		tv.tv_usec = 100000;
		rc = select(m_forwardsock + 1, &fds, NULL, NULL, &tv);
		memset(buf, 0, sizeof(buf));
		if (-1 == rc)
		{
			wxLogInfo(_("SSH error: select failed with error code %d"), wxSysErrorCode());
			goto shutdown;
		}
		if (rc && FD_ISSET(m_forwardsock, &fds))
		{
			len = recv(m_forwardsock, buf, sizeof(buf), 0);
			if (len < 0)
			{
				wxLogInfo(_("SSH error: read failed with error code %d"), wxSysErrorCode());
				goto shutdown;
			}
			else if (0 == len)
			{
				wxLogInfo(_("The client at %s:%d disconnected!"), wxString(inet_ntoa(m_sin.sin_addr), wxConvLibc).c_str(), sport);
				goto shutdown;
			}
			wr = 0;
			do
			{
				i = libssh2_channel_write(m_channel, buf, len);

				if (i < 0)
				{
					wxLogInfo(_("SSH error: libssh2_channel_write with error code %d"), i);
					goto shutdown;
				}
				wr += i;
			}
			while (i > 0 && wr < len);
		}
		while (1)
		{
			len = libssh2_channel_read(m_channel, buf, sizeof(buf));

			if (LIBSSH2_ERROR_EAGAIN == len)
				break;
			else if (len < 0)
			{
				wxLogInfo(_("SSH error: libssh2_channel_read with error code %d"), (int)len);
				goto shutdown;
			}
			wr = 0;
			while (wr < len)
			{
				i = send(m_forwardsock, buf + wr, len - wr, 0);
				if (i <= 0)
				{
					wxLogInfo(_("SSH error: write failed with error code %d"), wxSysErrorCode());
					goto shutdown;
				}
				wr += i;
			}
			if (libssh2_channel_eof(m_channel))
			{
				wxLogInfo(_("Connection at %s:%d disconnected by server"),
				          wxString(inet_ntoa(m_sin.sin_addr), wxConvLibc).c_str(), sport);
				goto shutdown;
			}
		}
	}
shutdown:
#ifdef WIN32
	closesocket(m_forwardsock);
#else
	close(m_forwardsock);
#endif

	if (m_channel)
	{
		libssh2_channel_close(m_channel);
		libssh2_channel_free(m_channel);
		m_channel = NULL;
	}

	return NULL;
}

void LogSSHTunnelErrors(const wxString &msg, const int &id, struct _LIBSSH2_SESSION *session)
{
	g_SSHThreadMutex.TryLock();

	wxString errorMsg = msg;
	// If session is not NULL then fetch the last error on that session
	if (session)
	{
		char *errmsg;
		int errmsg_len;
		libssh2_session_last_error(session, &errmsg, &errmsg_len, 0);
		if (errmsg_len > 0)
		{
			wxString errmsg_s(errmsg, wxConvLibc);
			errorMsg += wxString::Format(_(" [%s]"), errmsg_s.c_str());
		}
	}

	wxCommandEvent event(SSH_TUNNEL_ERROR_EVENT, id);
	// Give it some contents
	event.SetString(errorMsg);

	// Do send it
	wxPostEvent(winMain, event);

	g_SSHThreadMutex.Unlock();
}
#endif