//
// VMime library (http://www.vmime.org)
// Copyright (C) 2002-2013 Vincent Richard <vincent@vmime.org>
//
// This program 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 3 of
// the License, or (at your option) any later version.
//
// This program 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 program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// Linking this library statically or dynamically with other modules is making
// a combined work based on this library.  Thus, the terms and conditions of
// the GNU General Public License cover the whole combination.
//

#include "vmime/config.hpp"


#if VMIME_PLATFORM_IS_POSIX && VMIME_HAVE_MESSAGING_FEATURES


#include "vmime/platforms/posix/posixSocket.hpp"
#include "vmime/platforms/posix/posixHandler.hpp"

#ifndef _GNU_SOURCE
#define _GNU_SOURCE  // for getaddrinfo_a() in <netdb.h>
#endif

#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <netdb.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <poll.h>

#include "vmime/utility/stringUtils.hpp"

#include "vmime/exception.hpp"


#if defined(EWOULDBLOCK)
#   define IS_EAGAIN(x)  ((x) == EAGAIN || (x) == EWOULDBLOCK || (x) == EINTR || (x) == EINPROGRESS)
#else
#   define IS_EAGAIN(x)  ((x) == EAGAIN || (x) == EINTR || (x) == EINPROGRESS)
#endif


// Workaround for detection of strerror_r variants
#if VMIME_HAVE_STRERROR_R

namespace
{

char* vmime_strerror_r_result(int /* res */, char* buf)
{
	// XSI-compliant prototype:
	// int strerror_r(int errnum, char *buf, size_t buflen);
	return buf;
}

char* vmime_strerror_r_result(char* res, char* /* buf */)
{
	// GNU-specific prototype:
	// char *strerror_r(int errnum, char *buf, size_t buflen);
	return res;
}

}

#endif // VMIME_HAVE_STRERROR_R



namespace vmime {
namespace platforms {
namespace posix {


//
// posixSocket
//

posixSocket::posixSocket(shared_ptr <vmime::net::timeoutHandler> th)
	: m_timeoutHandler(th), m_desc(-1), m_status(0)
{
}


posixSocket::~posixSocket()
{
	if (m_desc != -1)
		::close(m_desc);
}


void posixSocket::connect(const vmime::string& address, const vmime::port_t port)
{
	// Close current connection, if any
	if (m_desc != -1)
	{
		::close(m_desc);
		m_desc = -1;
	}

	if (m_tracer)
	{
		std::ostringstream trace;
		trace << "Connecting to " << address << ", port " << port;

		m_tracer->traceSend(trace.str());
	}

#if VMIME_HAVE_GETADDRINFO  // use thread-safe and IPv6-aware getaddrinfo() if available

	// Resolve address, if needed
	m_serverAddress = address;

	struct ::addrinfo* addrInfo = NULL;  // resolved addresses
	resolve(&addrInfo, address, port);

	// Connect to host
	int sock = -1;
	int connectErrno = 0;

	if (m_timeoutHandler != NULL)
		m_timeoutHandler->resetTimeOut();

	for (struct ::addrinfo* curAddrInfo = addrInfo ;
	     sock == -1 && curAddrInfo != NULL ;
	     curAddrInfo = curAddrInfo->ai_next, connectErrno = ETIMEDOUT)
	{
		if (curAddrInfo->ai_family != AF_INET && curAddrInfo->ai_family != AF_INET6)
			continue;

		sock = ::socket(curAddrInfo->ai_family, curAddrInfo->ai_socktype, curAddrInfo->ai_protocol);

		if (sock < 0)
		{
			connectErrno = errno;
			continue;  // try next
		}

#if VMIME_HAVE_SO_KEEPALIVE

		// Enable TCP Keepalive
		int keepAlive_optval = 1;
		socklen_t keepAlive_optlen = sizeof(keepAlive_optval);

		::setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &keepAlive_optval, keepAlive_optlen);

#endif // VMIME_HAVE_SO_KEEPALIVE

#if VMIME_HAVE_SO_NOSIGPIPE

		// Return EPIPE instead of generating SIGPIPE
		int nosigpipe_optval = 1;
		socklen_t nosigpipe_optlen = sizeof(nosigpipe_optval);

		::setsockopt(sock, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe_optval, nosigpipe_optlen);

#endif // VMIME_HAVE_SO_NOSIGPIPE


		if (m_timeoutHandler != NULL)
		{
			::fcntl(sock, F_SETFL, ::fcntl(sock, F_GETFL) | O_NONBLOCK);

			if (::connect(sock, curAddrInfo->ai_addr, curAddrInfo->ai_addrlen) < 0)
			{
				switch (errno)
				{
				case 0:
				case EINPROGRESS:
				case EINTR:
#if defined(EAGAIN)
				case EAGAIN:
#endif // EAGAIN
#if defined(EWOULDBLOCK) && (!defined(EAGAIN) || (EWOULDBLOCK != EAGAIN))
				case EWOULDBLOCK:
#endif // EWOULDBLOCK

					// Connection in progress
					break;

				default:

					connectErrno = errno;
					::close(sock);
					sock = -1;
					continue;  // try next
				}

				// Wait for socket to be connected.
				bool connected = false;

				const int pollTimeout = 1000;   // poll() timeout (ms)
				const int tryNextTimeout = 5000;  // maximum time before trying next (ms)

				timeval startTime = { 0, 0 };
				gettimeofday(&startTime, /* timezone */ NULL);

				do
				{
					pollfd fds[1];
					fds[0].fd = sock;
					fds[0].events = POLLIN | POLLOUT;

					const int ret = ::poll(fds, sizeof(fds) / sizeof(fds[0]), pollTimeout);

					// Success
					if (ret > 0)
					{
						if (fds[0].revents & (POLLIN | POLLOUT))
						{
							int error = 0;
							socklen_t len = sizeof(error);

							if (getsockopt(sock, SOL_SOCKET, SO_ERROR, &error, &len) < 0)
							{
								connectErrno = errno;
							}
							else
							{
								if (error != 0)
									connectErrno = error;
								else
									connected = true;
							}
						}

						break;
					}
					// Error
					else if (ret < -1)
					{
						if (errno != EAGAIN && errno != EINTR)
						{
							// Cancel connection
							connectErrno = errno;
							break;
						}
					}

					// Check for timeout
					if (m_timeoutHandler->isTimeOut())
					{
						if (!m_timeoutHandler->handleTimeOut())
						{
							// Cancel connection
							connectErrno = ETIMEDOUT;
							break;
						}
						else
						{
							// Reset timeout and keep waiting for connection
							m_timeoutHandler->resetTimeOut();
						}
					}
					else
					{
						// Keep waiting for connection
					}

					timeval curTime = { 0, 0 };
					gettimeofday(&curTime, /* timezone */ NULL);

					if (curAddrInfo->ai_next != NULL &&
						curTime.tv_usec - startTime.tv_usec >= tryNextTimeout * 1000)
					{
						connectErrno = ETIMEDOUT;
						break;
					}

				} while (true);

				if (!connected)
				{
					::close(sock);
					sock = -1;
					continue;  // try next
				}

				break;
			}
			else
			{
				// Connection successful
				break;
			}
		}
		else
		{
			if (::connect(sock, curAddrInfo->ai_addr, curAddrInfo->ai_addrlen) < 0)
			{
				connectErrno = errno;
				::close(sock);
				sock = -1;
				continue;  // try next
			}
		}
	}

	::freeaddrinfo(addrInfo);

	if (sock == -1)
	{
		try
		{
			throwSocketError(connectErrno);
		}
		catch (exceptions::socket_exception& e)
		{
			throw vmime::exceptions::connection_error
				("Error while connecting socket.", e);
		}
	}

	m_desc = sock;

#else // !VMIME_HAVE_GETADDRINFO

	// Resolve address
	::sockaddr_in addr;

	memset(&addr, 0, sizeof(addr));

	addr.sin_family = AF_INET;
	addr.sin_port = htons(static_cast <unsigned short>(port));
	addr.sin_addr.s_addr = ::inet_addr(address.c_str());

	if (addr.sin_addr.s_addr == static_cast <in_addr_t>(-1))
	{
		::hostent* hostInfo = ::gethostbyname(address.c_str());

		if (hostInfo == NULL)
		{
			// Error: cannot resolve address
			throw vmime::exceptions::connection_error("Cannot resolve address.");
		}

		::memcpy(reinterpret_cast <char*>(&addr.sin_addr), hostInfo->h_addr, hostInfo->h_length);
	}

	m_serverAddress = address;

	// Get a new socket
	m_desc = ::socket(AF_INET, SOCK_STREAM, 0);

	if (m_desc == -1)
	{
		try
		{
			throwSocketError(errno);
		}
		catch (exceptions::socket_exception& e)
		{
			throw vmime::exceptions::connection_error
				("Error while creating socket.", e);
		}
	}

	// Start connection
	if (::connect(m_desc, reinterpret_cast <sockaddr*>(&addr), sizeof(addr)) == -1)
	{
		try
		{
			throwSocketError(errno);
		}
		catch (exceptions::socket_exception& e)
		{
			::close(m_desc);
			m_desc = -1;

			// Error
			throw vmime::exceptions::connection_error
				("Error while connecting socket.", e);
		}
	}

#endif // VMIME_HAVE_GETADDRINFO

	::fcntl(m_desc, F_SETFL, ::fcntl(m_desc, F_GETFL) | O_NONBLOCK);
}


void posixSocket::resolve(struct ::addrinfo** addrInfo, const vmime::string& address, const vmime::port_t port)
{
	char portStr[16];
	snprintf(portStr, sizeof(portStr), "%u", static_cast <unsigned int>(port));


	struct ::addrinfo hints;
	memset(&hints, 0, sizeof(hints));

	hints.ai_flags = AI_CANONNAME | AI_NUMERICSERV;
	hints.ai_family = PF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;

#if VMIME_HAVE_GETADDRINFO_A

	// If getaddrinfo_a() is available, use asynchronous resolving to allow
	// the timeout handler to cancel the operation

	struct ::gaicb gaiRequest;
	memset(&gaiRequest, 0, sizeof(gaiRequest));

	gaiRequest.ar_name = address.c_str();
	gaiRequest.ar_service = portStr;
	gaiRequest.ar_request = &hints;

	struct ::gaicb* gaiRequests = &gaiRequest;
	int gaiError;

	if ((gaiError = getaddrinfo_a(GAI_NOWAIT, &gaiRequests, 1, NULL)) != 0)
	{
		throw vmime::exceptions::connection_error
			("getaddrinfo_a() failed: " + std::string(gai_strerror(gaiError)));
	}

	if (m_timeoutHandler != NULL)
		m_timeoutHandler->resetTimeOut();

	while (true)
	{
		struct timespec gaiTimeout;
		gaiTimeout.tv_sec = 1;  // query timeout handler every second
		gaiTimeout.tv_nsec = 0;

		gaiError = gai_suspend(&gaiRequests, 1, &gaiTimeout);

		if (gaiError == 0 || gaiError == EAI_ALLDONE)
		{
			const int ret = gai_error(&gaiRequest);

			if (ret != 0)
			{
				throw vmime::exceptions::connection_error
					("getaddrinfo_a() request failed: " + std::string(gai_strerror(ret)));
			}
			else
			{
				*addrInfo = gaiRequest.ar_result;
				break;
			}
		}
		else if (gaiError != EAI_AGAIN)
		{
			if (gaiError == EAI_SYSTEM)
			{
				const int ret = gai_error(&gaiRequest);

				if (ret != EAI_INPROGRESS && errno != 0)
				{
					try
					{
						throwSocketError(errno);
					}
					catch (exceptions::socket_exception& e)
					{
						throw vmime::exceptions::connection_error
							("Error while connecting socket.", e);
					}
				}
			}
			else
			{
				throw vmime::exceptions::connection_error
					("gai_suspend() failed: " + std::string(gai_strerror(gaiError)));
			}
		}

		// Check for timeout
		if (m_timeoutHandler && m_timeoutHandler->isTimeOut())
		{
			if (!m_timeoutHandler->handleTimeOut())
			{
				throw exceptions::operation_timed_out();
			}
			else
			{
				// Reset timeout and keep waiting for connection
				m_timeoutHandler->resetTimeOut();
			}
		}
	}

#else  // !VMIME_HAVE_GETADDRINFO_A

	if (::getaddrinfo(address.c_str(), portStr, &hints, addrInfo) != 0)
	{
		// Error: cannot resolve address
		throw vmime::exceptions::connection_error("Cannot resolve address.");
	}

#endif  // VMIME_HAVE_GETADDRINFO_A

}


bool posixSocket::isConnected() const
{
	if (m_desc == -1)
		return false;

	char buff;

	return ::recv(m_desc, &buff, 1, MSG_PEEK) != 0;
}


void posixSocket::disconnect()
{
	if (m_desc != -1)
	{
		if (m_tracer)
			m_tracer->traceSend("Disconnecting");

		::shutdown(m_desc, SHUT_RDWR);
		::close(m_desc);

		m_desc = -1;
	}
}


static bool isNumericAddress(const char* address)
{

#if VMIME_HAVE_GETADDRINFO

	struct addrinfo hint, *info = NULL;
	memset(&hint, 0, sizeof(hint));

	hint.ai_family = AF_UNSPEC;
	hint.ai_flags = AI_NUMERICHOST;

	if (getaddrinfo(address, 0, &hint, &info) == 0)
	{
		freeaddrinfo(info);
		return true;
	}
	else
	{
		return false;
	}

#else

	return inet_addr(address) != INADDR_NONE;

#endif

}


const string posixSocket::getPeerAddress() const
{
	// Get address of connected peer
	sockaddr peer;
	socklen_t peerLen = sizeof(peer);

	if (getpeername(m_desc, &peer, &peerLen) != 0)
	{
		throwSocketError(errno);
	}

	// Convert to numerical presentation format
	char buf[INET6_ADDRSTRLEN];

	if (!inet_ntop(peer.sa_family, &(reinterpret_cast <struct sockaddr_in *>(&peer))->sin_addr, buf, sizeof(buf)))
	{
		throwSocketError(errno);
	}

	return string(buf);
}


const string posixSocket::getPeerName() const
{
	// Get address of connected peer
	sockaddr peer;
	socklen_t peerLen = sizeof(peer);

	if (getpeername(m_desc, &peer, &peerLen) != 0)
	{
		throwSocketError(errno);
	}

	// If server address as specified when connecting is a numeric
	// address, try to get a host name for it
	if (isNumericAddress(m_serverAddress.c_str()))
	{

#if VMIME_HAVE_GETNAMEINFO

		char host[NI_MAXHOST + 1];
		char service[NI_MAXSERV + 1];

		if (getnameinfo(reinterpret_cast <sockaddr *>(&peer), peerLen,
				host, sizeof(host), service, sizeof(service),
				/* flags */ NI_NAMEREQD) == 0)
		{
			return string(host);
		}

#else

		struct hostent *hp;

		if ((hp = gethostbyaddr(reinterpret_cast <const void *>(&peer),
				sizeof(peer), peer.sa_family)) != NULL)
		{
			return string(hp->h_name);
		}

#endif

	}

	return m_serverAddress;
}


size_t posixSocket::getBlockSize() const
{
	return 16384;  // 16 KB
}


bool posixSocket::waitForData(const bool read, const bool write, const int msecs)
{
	for (int i = 0 ; i <= msecs / 10 ; ++i)
	{
		// Check whether data is available
		pollfd fds[1];
		fds[0].fd = m_desc;
		fds[0].events = 0;

		if (read)
			fds[0].events |= POLLIN;

		if (write)
			fds[0].events |= POLLOUT;

		const int ret = ::poll(fds, sizeof(fds) / sizeof(fds[0]), 10 /* ms */);

		if (ret < 0)
		{
			if (errno != EAGAIN && errno != EINTR)
				throwSocketError(errno);
		}
		else if (ret > 0)
		{
			if (fds[0].revents & (POLLIN | POLLOUT))
				return true;
		}

		// No data available at this time
		// Check if we are timed out
		if (m_timeoutHandler &&
		    m_timeoutHandler->isTimeOut())
		{
			if (!m_timeoutHandler->handleTimeOut())
			{
				// Server did not react within timeout delay
				throw exceptions::operation_timed_out();
			}
			else
			{
				// Reset timeout
				m_timeoutHandler->resetTimeOut();
			}
		}
	}

	return false;  // time out
}


bool posixSocket::waitForRead(const int msecs)
{
	return waitForData(/* read */ true, /* write */ false, msecs);
}


bool posixSocket::waitForWrite(const int msecs)
{
	return waitForData(/* read */ false, /* write */ true, msecs);
}


void posixSocket::receive(vmime::string& buffer)
{
	const size_t size = receiveRaw(m_buffer, sizeof(m_buffer));
	buffer = utility::stringUtils::makeStringFromBytes(m_buffer, size);
}


size_t posixSocket::receiveRaw(byte_t* buffer, const size_t count)
{
	m_status &= ~STATUS_WOULDBLOCK;

	// Check whether data is available
	if (!waitForRead(50 /* msecs */))
	{
		m_status |= STATUS_WOULDBLOCK;

		// Continue waiting for data
		return 0;
	}

	// Read available data
	ssize_t ret = ::recv(m_desc, buffer, count, 0);

	if (ret < 0)
	{
		if (!IS_EAGAIN(errno))
			throwSocketError(errno);

		// Check if we are timed out
		if (m_timeoutHandler &&
		    m_timeoutHandler->isTimeOut())
		{
			if (!m_timeoutHandler->handleTimeOut())
			{
				// Server did not react within timeout delay
				throwSocketError(errno);
			}
			else
			{
				// Reset timeout
				m_timeoutHandler->resetTimeOut();
			}
		}

		m_status |= STATUS_WOULDBLOCK;

		// No data available at this time
		return 0;
	}
	else if (ret == 0)
	{
		// Host shutdown
		throwSocketError(ENOTCONN);
	}
	else
	{
		// Data received, reset timeout
		if (m_timeoutHandler)
			m_timeoutHandler->resetTimeOut();
	}

	return ret;
}


void posixSocket::send(const vmime::string& buffer)
{
	sendRaw(reinterpret_cast <const byte_t*>(buffer.data()), buffer.length());
}


void posixSocket::send(const char* str)
{
	sendRaw(reinterpret_cast <const byte_t*>(str), ::strlen(str));
}


void posixSocket::sendRaw(const byte_t* buffer, const size_t count)
{
	m_status &= ~STATUS_WOULDBLOCK;

	size_t size = count;

	while (size > 0)
	{

#if VMIME_HAVE_MSG_NOSIGNAL
		const ssize_t ret = ::send(m_desc, buffer, size, MSG_NOSIGNAL);
#else
		const ssize_t ret = ::send(m_desc, buffer, size, 0);
#endif

		if (ret <= 0)
		{
			if (ret < 0 && !IS_EAGAIN(errno))
				throwSocketError(errno);

			waitForWrite(50 /* msecs */);
		}
		else
		{
			buffer += ret;
			size -= ret;
		}
	}

	// Reset timeout
	if (m_timeoutHandler)
		m_timeoutHandler->resetTimeOut();
}


size_t posixSocket::sendRawNonBlocking(const byte_t* buffer, const size_t count)
{
	m_status &= ~STATUS_WOULDBLOCK;

#if VMIME_HAVE_MSG_NOSIGNAL
	const ssize_t ret = ::send(m_desc, buffer, count, MSG_NOSIGNAL);
#else
	const ssize_t ret = ::send(m_desc, buffer, count, 0);
#endif

	if (ret <= 0)
	{
		if (ret < 0 && !IS_EAGAIN(errno))
			throwSocketError(errno);

		// Check if we are timed out
		if (m_timeoutHandler &&
		    m_timeoutHandler->isTimeOut())
		{
			if (!m_timeoutHandler->handleTimeOut())
			{
				// Could not send data within timeout delay
				throw exceptions::operation_timed_out();
			}
			else
			{
				// Reset timeout
				m_timeoutHandler->resetTimeOut();
			}
		}

		m_status |= STATUS_WOULDBLOCK;

		// No data can be written at this time
		return 0;
	}

	// Reset timeout
	if (m_timeoutHandler)
		m_timeoutHandler->resetTimeOut();

	return ret;
}


void posixSocket::throwSocketError(const int err)
{
	const char* msg = NULL;

	switch (err)
	{
	case EACCES:          msg = "EACCES: permission denied"; break;
	case EAFNOSUPPORT:    msg = "EAFNOSUPPORT: address family not supported"; break;
	case EMFILE:          msg = "EMFILE: process file table overflow"; break;
	case ENFILE:          msg = "ENFILE: system limit reached"; break;
	case EPROTONOSUPPORT: msg = "EPROTONOSUPPORT: protocol not supported"; break;
	case EAGAIN:          msg = "EGAIN: blocking operation"; break;
	case EBADF:           msg = "EBADF: invalid descriptor"; break;
	case ECONNRESET:      msg = "ECONNRESET: connection reset by peer"; break;
	case EFAULT:          msg = "EFAULT: bad user space address"; break;
	case EINTR:           msg = "EINTR: signal occurred before transmission"; break;
	case EINVAL:          msg = "EINVAL: invalid argument"; break;
	case EMSGSIZE:        msg = "EMSGSIZE: message cannot be sent atomically"; break;
	case ENOBUFS:         msg = "ENOBUFS: output queue is full"; break;
	case ENOMEM:          msg = "ENOMEM: out of memory"; break;
	case EPIPE:           msg = "EPIPE: broken pipe"; break;
	case ENOTCONN:        msg = "ENOTCONN: not connected"; break;
	case ECONNREFUSED:    msg = "ECONNREFUSED: connection refused"; break;
	}

	if (msg)
	{
		throw exceptions::socket_exception(msg);
	}
	else
	{

		// Use strerror() to get string describing error number

#if VMIME_HAVE_STRERROR_R

		char errbuf[512];
		throw exceptions::socket_exception(vmime_strerror_r_result(strerror_r(err, errbuf, sizeof(errbuf)), errbuf));

#else  // !VMIME_HAVE_STRERROR_R

		const std::string strmsg(strerror(err));
		throw exceptions::socket_exception(strmsg);

#endif  // VMIME_HAVE_STRERROR_R

	}
}


unsigned int posixSocket::getStatus() const
{
	return m_status;
}


shared_ptr <net::timeoutHandler> posixSocket::getTimeoutHandler()
{
	return m_timeoutHandler;
}


void posixSocket::setTracer(shared_ptr <net::tracer> tracer)
{
	m_tracer = tracer;
}


shared_ptr <net::tracer> posixSocket::getTracer()
{
	return m_tracer;
}



//
// posixSocketFactory
//

shared_ptr <vmime::net::socket> posixSocketFactory::create()
{
	shared_ptr <vmime::net::timeoutHandler> th;
	return make_shared <posixSocket>(th);
}


shared_ptr <vmime::net::socket> posixSocketFactory::create(shared_ptr <vmime::net::timeoutHandler> th)
{
	return make_shared <posixSocket>(th);
}


} // posix
} // platforms
} // vmime


#endif // VMIME_PLATFORM_IS_POSIX && VMIME_HAVE_MESSAGING_FEATURES
