/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 * Authors: Michael Zucchi <notzed@ximian.com>
 *	    Jeffrey Stedfast <fejj@ximian.com>
 *	    Chris Toshok <toshok@ximian.com>
 *
 * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
 *
 * This library is free software you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation.
 *
 * This library 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 Lesser General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 */

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

#include <errno.h>
#include <stdio.h>

#include <glib/gi18n-lib.h>

#include "camel-msgport.h"
#include "camel-net-utils.h"
#ifdef G_OS_WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#ifdef HAVE_WSPIAPI_H
#include <wspiapi.h>
#endif
#endif
#include "camel-object.h"
#include "camel-operation.h"
#include "camel-service.h"

#define d(x)

/* These are GNU extensions */
#ifndef NI_MAXHOST
#define NI_MAXHOST	1025
#endif
#ifndef NI_MAXSERV
#define NI_MAXSERV	32
#endif

#ifdef G_OS_WIN32

typedef gshort in_port_t;

#undef gai_strerror
#define gai_strerror my_gai_strerror

/* gai_strerror() is implemented as an inline function in Microsoft's
 * SDK, but mingw lacks that. So implement here. The EAI_* errors can
 * be handled with the normal FormatMessage() API,
 * i.e. g_win32_error_message().
 */

static const gchar *
gai_strerror (gint error_code)
{
	gchar *msg = g_win32_error_message (error_code);
	GQuark quark = g_quark_from_string (msg);
	const gchar *retval = g_quark_to_string (quark);

	g_free (msg);

	return retval;
}

#endif

/* gethostbyname emulation code for emulating getaddrinfo code ...
 *
 * This should probably go away */

#ifdef NEED_ADDRINFO

#if !defined (HAVE_GETHOSTBYNAME_R) || !defined (HAVE_GETHOSTBYADDR_R)
G_LOCK_DEFINE_STATIC (gethost_mutex);
#endif

#define ALIGN(x) (((x) + (sizeof (gchar *) - 1)) & ~(sizeof (gchar *) - 1))

#define GETHOST_PROCESS(h, host, buf, buflen, herr) G_STMT_START { \
	gint num_aliases = 0, num_addrs = 0; \
	gint req_length; \
	gchar *p; \
	gint i; \
 \
	/* check to make sure we have enough room in our buffer */ \
	req_length = 0; \
	if (h->h_aliases) { \
		for (i = 0; h->h_aliases[i]; i++) \
			req_length += strlen (h->h_aliases[i]) + 1; \
		num_aliases = i; \
	} \
 \
	if (h->h_addr_list) { \
		for (i = 0; h->h_addr_list[i]; i++) \
			req_length += h->h_length; \
		num_addrs = i; \
	} \
 \
	req_length += sizeof (gchar *) * (num_aliases + 1); \
	req_length += sizeof (gchar *) * (num_addrs + 1); \
	req_length += strlen (h->h_name) + 1; \
 \
	if (buflen < req_length) { \
		*herr = ERANGE; \
		G_UNLOCK (gethost_mutex); \
		return ERANGE; \
	} \
 \
	/* we store the alias/addr pointers in the buffer */ \
        /* their addresses here. */ \
	p = buf; \
	if (num_aliases) { \
		host->h_aliases = (gchar **) p; \
		p += sizeof (gchar *) * (num_aliases + 1); \
	} else \
		host->h_aliases = NULL; \
 \
	if (num_addrs) { \
		host->h_addr_list = (gchar **) p; \
		p += sizeof (gchar *) * (num_addrs + 1); \
	} else \
		host->h_addr_list = NULL; \
 \
	/* copy the host name into the buffer */ \
	host->h_name = p; \
	strcpy (p, h->h_name); \
	p += strlen (h->h_name) + 1; \
	host->h_addrtype = h->h_addrtype; \
	host->h_length = h->h_length; \
 \
	/* copy the aliases/addresses into the buffer */ \
        /* and assign pointers into the hostent */ \
	*p = 0; \
	if (num_aliases) { \
		for (i = 0; i < num_aliases; i++) { \
			strcpy (p, h->h_aliases[i]); \
			host->h_aliases[i] = p; \
			p += strlen (h->h_aliases[i]); \
		} \
		host->h_aliases[num_aliases] = NULL; \
	} \
 \
	if (num_addrs) { \
		for (i = 0; i < num_addrs; i++) { \
			memcpy (p, h->h_addr_list[i], h->h_length); \
			host->h_addr_list[i] = p; \
			p += h->h_length; \
		} \
		host->h_addr_list[num_addrs] = NULL; \
	} \
} G_STMT_END

#ifdef ENABLE_IPv6
/* some helpful utils for IPv6 lookups */
#define IPv6_BUFLEN_MIN  (sizeof (gchar *) * 3)

static gint
ai_to_herr (gint error)
{
	switch (error) {
	case EAI_NONAME:
	case EAI_FAIL:
		return HOST_NOT_FOUND;
		break;
	case EAI_SERVICE:
		return NO_DATA;
		break;
	case EAI_ADDRFAMILY:
		return NO_ADDRESS;
		break;
	case EAI_NODATA:
		return NO_DATA;
		break;
	case EAI_MEMORY:
		return ENOMEM;
		break;
	case EAI_AGAIN:
		return TRY_AGAIN;
		break;
	case EAI_SYSTEM:
		return errno;
		break;
	default:
		return NO_RECOVERY;
		break;
	}
}

#endif /* ENABLE_IPv6 */

static gint
camel_gethostbyname_r (const gchar *name,
                       struct hostent *host,
                       gchar *buf,
                       gsize buflen,
                       gint *herr)
{
#ifdef ENABLE_IPv6
	struct addrinfo hints, *res;
	gint retval, len;
	gchar *addr;

	memset (&hints, 0, sizeof (struct addrinfo));
#ifdef HAVE_AI_ADDRCONFIG
	hints.ai_flags = AI_CANONNAME | AI_ADDRCONFIG;
#else
	hints.ai_flags = AI_CANONNAME;
#endif
	hints.ai_family = PF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_protocol = IPPROTO_TCP;

	if ((retval = getaddrinfo (name, NULL, &hints, &res)) != 0) {
		*herr = ai_to_herr (retval);
		return -1;
	}

	len = ALIGN (strlen (res->ai_canonname) + 1);
	if (buflen < IPv6_BUFLEN_MIN + len + res->ai_addrlen + sizeof (gchar *))
		return ERANGE;

	/* h_name */
	g_strlcpy (buf, res->ai_canonname, buflen);
	host->h_name = buf;
	buf += len;

	/* h_aliases */
	((gchar **) buf)[0] = NULL;
	host->h_aliases = (gchar **) buf;
	buf += sizeof (gchar *);

	/* h_addrtype and h_length */
	host->h_length = res->ai_addrlen;
	if (res->ai_family == PF_INET6) {
		host->h_addrtype = AF_INET6;

		addr = (gchar *) &((struct sockaddr_in6 *) res->ai_addr)->sin6_addr;
	} else {
		host->h_addrtype = AF_INET;

		addr = (gchar *) &((struct sockaddr_in *) res->ai_addr)->sin_addr;
	}

	memcpy (buf, addr, host->h_length);
	addr = buf;
	buf += ALIGN (host->h_length);

	/* h_addr_list */
	((gchar **) buf)[0] = addr;
	((gchar **) buf)[1] = NULL;
	host->h_addr_list = (gchar **) buf;

	freeaddrinfo (res);

	return 0;
#else /* No support for IPv6 addresses */
#ifdef HAVE_GETHOSTBYNAME_R
#ifdef GETHOSTBYNAME_R_FIVE_ARGS
	if (gethostbyname_r (name, host, buf, buflen, herr))
		return 0;
	else
		return errno;
#else
	struct hostent *hp;
	gint retval;

	retval = gethostbyname_r (name, host, buf, buflen, &hp, herr);
	if (hp != NULL) {
		*herr = 0;
	} else if (retval == 0) {
		/* glibc 2.3.2 workaround - it seems that
		 * gethostbyname_r will sometimes return 0 on fail and
		 * not set the hostent values (hence the crash in bug
		 * #56337).  Hopefully we can depend on @hp being NULL
		 * in this error case like we do with
		 * gethostbyaddr_r().
		 */
		retval = -1;
	}

	return retval;
#endif
#else /* No support for gethostbyname_r */
	struct hostent *h;

	G_LOCK (gethost_mutex);

	h = gethostbyname (name);

	if (!h) {
		*herr = h_errno;
		G_UNLOCK (gethost_mutex);
		return -1;
	}

	GETHOST_PROCESS (h, host, buf, buflen, herr);

	G_UNLOCK (gethost_mutex);

	return 0;
#endif /* HAVE_GETHOSTBYNAME_R */
#endif /* ENABLE_IPv6 */
}

static gint
camel_gethostbyaddr_r (const gchar *addr,
                       gint addrlen,
                       gint type,
                       struct hostent *host,
                       gchar *buf,
                       gsize buflen,
                       gint *herr)
{
#ifdef ENABLE_IPv6
	gint retval, len;

	if ((retval = getnameinfo (addr, addrlen, buf, buflen, NULL, 0, NI_NAMEREQD)) != 0) {
		*herr = ai_to_herr (retval);
		return -1;
	}

	len = ALIGN (strlen (buf) + 1);
	if (buflen < IPv6_BUFLEN_MIN + len + addrlen + sizeof (gchar *))
		return ERANGE;

	/* h_name */
	host->h_name = buf;
	buf += len;

	/* h_aliases */
	((gchar **) buf)[0] = NULL;
	host->h_aliases = (gchar **) buf;
	buf += sizeof (gchar *);

	/* h_addrtype and h_length */
	host->h_length = addrlen;
	host->h_addrtype = type;

	memcpy (buf, addr, host->h_length);
	addr = buf;
	buf += ALIGN (host->h_length);

	/* h_addr_list */
	((gchar **) buf)[0] = addr;
	((gchar **) buf)[1] = NULL;
	host->h_addr_list = (gchar **) buf;

	return 0;
#else /* No support for IPv6 addresses */
#ifdef HAVE_GETHOSTBYADDR_R
#ifdef GETHOSTBYADDR_R_SEVEN_ARGS
	if (gethostbyaddr_r (addr, addrlen, type, host, buf, buflen, herr))
		return 0;
	else
		return errno;
#else
	struct hostent *hp;
	gint retval;

	retval = gethostbyaddr_r (addr, addrlen, type, host, buf, buflen, &hp, herr);
	if (hp != NULL) {
		*herr = 0;
		retval = 0;
	} else if (retval == 0) {
		/* glibc 2.3.2 workaround - it seems that
		 * gethostbyaddr_r will sometimes return 0 on fail and
		 * fill @host with garbage strings from /etc/hosts
		 * (failure to parse the file? who knows). Luckily, it
		 * seems that we can rely on @hp being NULL on
		 * fail.
		 */
		retval = -1;
	}

	return retval;
#endif
#else /* No support for gethostbyaddr_r */
	struct hostent *h;

	G_LOCK (gethost_mutex);

	h = gethostbyaddr (addr, addrlen, type);

	if (!h) {
		*herr = h_errno;
		G_UNLOCK (gethost_mutex);
		return -1;
	}

	GETHOST_PROCESS (h, host, buf, buflen, herr);

	G_UNLOCK (gethost_mutex);

	return 0;
#endif /* HAVE_GETHOSTBYADDR_R */
#endif /* ENABLE_IPv6 */
}
#endif /* NEED_ADDRINFO */

/* ********************************************************************** */
struct _addrinfo_msg {
	CamelMsg msg;
	guint cancelled : 1;

	/* for host lookup */
	const gchar *name;
	const gchar *service;
	gint result;
	const struct addrinfo *hints;
	struct addrinfo **res;

	/* for host lookup emulation */
#ifdef NEED_ADDRINFO
	struct hostent hostbuf;
	gint hostbuflen;
	gchar *hostbufmem;
#endif

	/* for name lookup */
	const struct sockaddr *addr;
	socklen_t addrlen;
	gchar *host;
	gint hostlen;
	gchar *serv;
	gint servlen;
	gint flags;
};

static void
cs_freeinfo (struct _addrinfo_msg *msg)
{
	g_free (msg->host);
	g_free (msg->serv);
#ifdef NEED_ADDRINFO
	g_free (msg->hostbufmem);
#endif
	g_free (msg);
}

/* returns -1 if we didn't wait for reply from thread */
static gint
cs_waitinfo (gpointer (worker)(gpointer),
             struct _addrinfo_msg *msg,
             const gchar *errmsg,
             GCancellable *cancellable,
             GError **error)
{
	CamelMsgPort *reply_port;
	GThread *thread;
	gint cancel_fd, cancel = 0, fd;

	cancel_fd = g_cancellable_get_fd (cancellable);
	if (cancel_fd == -1) {
		worker (msg);
		return 0;
	}

	reply_port = msg->msg.reply_port = camel_msgport_new ();
	fd = camel_msgport_fd (msg->msg.reply_port);
	if ((thread = g_thread_new (NULL, worker, msg)) != NULL) {
		gint status;
#ifndef G_OS_WIN32
		GPollFD polls[2];

		polls[0].fd = fd;
		polls[0].events = G_IO_IN;
		polls[1].fd = cancel_fd;
		polls[1].events = G_IO_IN;

		d (printf ("waiting for name return/cancellation in main process\n"));
		do {
			polls[0].revents = 0;
			polls[1].revents = 0;
			status = g_poll (polls, 2, -1);
		} while (status == -1 && errno == EINTR);
#else
		fd_set read_set;

		FD_ZERO (&read_set);
		FD_SET (fd, &read_set);
		FD_SET (cancel_fd, &read_set);

		status = select (MAX (fd, cancel_fd) + 1, &read_set, NULL, NULL, NULL);
#endif

		if (status == -1 ||
#ifndef G_OS_WIN32
		    (polls[1].revents & G_IO_IN)
#else
		    FD_ISSET (cancel_fd, &read_set)
#endif
						   ) {
			if (status == -1)
				g_set_error (
					error, G_IO_ERROR,
					g_io_error_from_errno (errno),
					"%s: %s", errmsg,
#ifndef G_OS_WIN32
					g_strerror (errno)
#else
					g_win32_error_message (WSAGetLastError ())
#endif
					);
			else
				g_set_error (
					error, G_IO_ERROR,
					G_IO_ERROR_CANCELLED,
					_("Cancelled"));

			/* We cancel so if the thread impl is decent it causes immediate exit.
			 * We check the reply port incase we had a reply in the mean time, which we free later */
			d (printf ("Canceling lookup thread and leaving it\n"));
			msg->cancelled = 1;
			g_thread_join (thread);
			cancel = 1;
		} else {
			struct _addrinfo_msg *reply;

			d (printf ("waiting for child to exit\n"));
			g_thread_join (thread);
			d (printf ("child done\n"));

			reply = (struct _addrinfo_msg *) camel_msgport_try_pop (reply_port);
			if (reply != msg)
				g_warning ("%s: Received msg reply %p doesn't match msg %p", G_STRFUNC, reply, msg);
		}
	}
	camel_msgport_destroy (reply_port);

	g_cancellable_release_fd (cancellable);

	return cancel;
}

#ifdef NEED_ADDRINFO
static gpointer
cs_getaddrinfo (gpointer data)
{
	struct _addrinfo_msg *msg = data;
	gint herr;
	struct hostent h;
	struct addrinfo *res, *last = NULL;
	struct sockaddr_in *sin;
	in_port_t port = 0;
	gint i;

	/* This is a pretty simplistic emulation of getaddrinfo */

	while ((msg->result = camel_gethostbyname_r (msg->name, &h, msg->hostbufmem, msg->hostbuflen, &herr)) == ERANGE) {
		if (msg->cancelled)
			break;
		msg->hostbuflen *= 2;
		msg->hostbufmem = g_realloc (msg->hostbufmem, msg->hostbuflen);
	}

	/* If we got cancelled, dont reply, just free it */
	if (msg->cancelled)
		goto cancel;

	/* FIXME: map error numbers across */
	if (msg->result != 0)
		goto reply;

	/* check hints matched */
	if (msg->hints && msg->hints->ai_family && msg->hints->ai_family != h.h_addrtype) {
		msg->result = EAI_FAMILY;
		goto reply;
	}

	/* we only support ipv4 for this interface, even if it could supply ipv6 */
	if (h.h_addrtype != AF_INET) {
		msg->result = EAI_FAMILY;
		goto reply;
	}

	/* check service mapping */
	if (msg->service) {
		const gchar *p = msg->service;

		while (*p) {
			if (*p < '0' || *p > '9')
				break;
			p++;
		}

		if (*p) {
			const gchar *socktype = NULL;
			struct servent *serv;

			if (msg->hints && msg->hints->ai_socktype) {
				if (msg->hints->ai_socktype == SOCK_STREAM)
					socktype = "tcp";
				else if (msg->hints->ai_socktype == SOCK_DGRAM)
					socktype = "udp";
			}

			serv = getservbyname (msg->service, socktype);
			if (serv == NULL) {
				msg->result = EAI_NONAME;
				goto reply;
			}
			port = serv->s_port;
		} else {
			port = htons (strtoul (msg->service, NULL, 10));
		}
	}

	for (i = 0; h.h_addr_list[i] && !msg->cancelled; i++) {
		res = g_malloc0 (sizeof (*res));
		if (msg->hints) {
			res->ai_flags = msg->hints->ai_flags;
			if (msg->hints->ai_flags & AI_CANONNAME)
				res->ai_canonname = g_strdup (h.h_name);
			res->ai_socktype = msg->hints->ai_socktype;
			res->ai_protocol = msg->hints->ai_protocol;
		} else {
			res->ai_flags = 0;
			res->ai_socktype = SOCK_STREAM;	/* fudge */
			res->ai_protocol = 0;	/* fudge */
		}
		res->ai_family = AF_INET;
		res->ai_addrlen = sizeof (*sin);
		res->ai_addr = g_malloc (sizeof (*sin));
		sin = (struct sockaddr_in *) res->ai_addr;
		sin->sin_family = AF_INET;
		sin->sin_port = port;
		memcpy (&sin->sin_addr, h.h_addr_list[i], sizeof (sin->sin_addr));

		if (last == NULL) {
			*msg->res = last = res;
		} else {
			last->ai_next = res;
			last = res;
		}
	}
reply:
	camel_msgport_reply ((CamelMsg *) msg);
cancel:
	return NULL;
}
#else
static gpointer
cs_getaddrinfo (gpointer data)
{
	struct _addrinfo_msg *info = data;

	info->result = getaddrinfo (info->name, info->service, info->hints, info->res);

	/* On Solaris, the service name 'http' or 'https' is not defined.
	 * Use the port as the service name directly. */
	if (info->result && info->service) {
		if (strcmp (info->service, "http") == 0)
			info->result = getaddrinfo (info->name, "80", info->hints, info->res);
		else if (strcmp (info->service, "https") == 0)
			info->result = getaddrinfo (info->name, "443", info->hints, info->res);
	}

	if (!info->cancelled)
		camel_msgport_reply ((CamelMsg *) info);

	return NULL;
}
#endif /* NEED_ADDRINFO */

/**
 * camel_getaddrinfo:
 *
 * Since: 2.22
 **/
struct addrinfo *
camel_getaddrinfo (const gchar *name,
                   const gchar *service,
                   const struct addrinfo *hints,
                   GCancellable *cancellable,
                   GError **error)
{
	struct _addrinfo_msg *msg;
	struct addrinfo *res = NULL;
#ifndef ENABLE_IPv6
	struct addrinfo myhints;
#endif
	g_return_val_if_fail (name != NULL, NULL);

	if (g_cancellable_set_error_if_cancelled (cancellable, error))
		return NULL;

	camel_operation_push_message (
		cancellable, _("Resolving: %s"), name);

	/* force ipv4 addresses only */
#ifndef ENABLE_IPv6
	if (hints == NULL)
		memset (&myhints, 0, sizeof (myhints));
	else
		memcpy (&myhints, hints, sizeof (myhints));

	myhints.ai_family = AF_INET;
	hints = &myhints;
#endif

	msg = g_malloc0 (sizeof (*msg));
	msg->name = name;
	msg->service = service;
	msg->hints = hints;
	msg->res = &res;
#ifdef NEED_ADDRINFO
	msg->hostbuflen = 1024;
	msg->hostbufmem = g_malloc (msg->hostbuflen);
#endif
	if (cs_waitinfo (
		cs_getaddrinfo, msg, _("Host lookup failed"),
		cancellable, error) == 0) {

		if (msg->result == EAI_NONAME || msg->result == EAI_FAIL) {
			g_set_error (
				error, CAMEL_SERVICE_ERROR, CAMEL_SERVICE_ERROR_URL_INVALID,
				_("Host lookup '%s' failed. Check your host name for spelling errors."), name);
		} else if (msg->result != 0) {
			g_set_error (
				error, CAMEL_SERVICE_ERROR, CAMEL_SERVICE_ERROR_URL_INVALID,
				_("Host lookup '%s' failed: %s"),
				name, gai_strerror (msg->result));
		}
	} else
		res = NULL;

	cs_freeinfo (msg);

	camel_operation_pop_message (cancellable);

	return res;
}

/**
 * camel_freeaddrinfo:
 *
 * Since: 2.22
 **/
void
camel_freeaddrinfo (struct addrinfo *host)
{
#ifdef NEED_ADDRINFO
	while (host) {
		struct addrinfo *next = host->ai_next;

		g_free (host->ai_canonname);
		g_free (host->ai_addr);
		g_free (host);
		host = next;
	}
#else
	freeaddrinfo (host);
#endif
}

