/* gnome-vfs-resolve.c - Resolver API

Copyright (C) 2004 Christian Kellner <gicmo@gnome.org>

The Gnome Library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public License as
published by the Free Software Foundation; either version 2 of the
License, or (at your option) any later version.

The Gnome 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
Library General Public License for more details.

You should have received a copy of the GNU Library General Public
License along with the Gnome Library; see the file COPYING.LIB.  If not,
write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
Boston, MA 02111-1307, USA.

*/

#include <config.h>

#include <errno.h>
#include <string.h>
#include <glib.h>
#ifndef G_OS_WIN32
/* Keep <sys/types.h> above the network includes for FreeBSD. */
#include <sys/types.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <fcntl.h>
#include <arpa/inet.h>
#else
#include <winsock2.h>
#include <ws2tcpip.h>
#endif

#include <unistd.h>
#include <sys/time.h>

#ifdef HAVE_RES_NINIT
#include <arpa/nameser.h>
#include <resolv.h>
/* RELOAD_TIMEVAL specifies the minimum of seconds between resolver reloads */
#define RELOAD_TIMEVAL 2
#endif

#include <glib-object.h>

#include <libgnomevfs/gnome-vfs-resolve.h>

#define INIT_BUFSIZE 8192 /* Unix Network Programming Chapter 11 Page 304 :) */

#if !HAVE_GETADDRINFO && !HAVE_GETHOSTBYNAME_R_GLIBC && !HAVE_GETHOSTBYNAME_R_SOLARIS && !HAVE_GETHOSTBYNAME_R_HPUX
G_LOCK_DEFINE (dns_lock);
# ifndef G_OS_WIN32
#  ifndef h_errno
extern int h_errno;
#  endif /* h_errno */
# endif /* G_OS_WIN32 */
#endif

struct GnomeVFSResolveHandle_ {
#ifdef HAVE_GETADDRINFO
	   struct addrinfo *result;
	   struct addrinfo *current;
#else
	   GList *result;
	   GList *current;
#endif	
};

#ifndef HAVE_GETADDRINFO
static GnomeVFSResult
resolvehandle_from_hostent (struct hostent *he, GnomeVFSResolveHandle **handle)
{
	   GnomeVFSAddress *addr;
	   GList *result;
	   char **iter;
	   void *aptr;
	   size_t addrlen;
	   struct sockaddr *sa;
	   struct sockaddr_in sin;
#ifdef ENABLE_IPV6	   
	   struct sockaddr_in6 sin6;
#endif

	   switch (he->h_addrtype) {
			 
	   case AF_INET:
			 memset (&sin, 0, sizeof (sin));
			 sin.sin_family = AF_INET;
			 aptr = &(sin.sin_addr);
			 sa = (struct sockaddr *) &sin;
			 addrlen = sizeof (struct sockaddr_in);
			 break;
			 
#ifdef ENABLE_IPV6
	   case AF_INET6:
			 memset (&sin6, 0, sizeof (sin6));
			 sin6.sin6_family = AF_INET6;
			 aptr = &(sin6.sin6_addr);
			 sa = (struct sockaddr *) &sin6;
			 addrlen = sizeof (struct sockaddr_in6);
			 break;
#endif
	   default:
			 return GNOME_VFS_ERROR_INTERNAL;
	   }

	   result = NULL;
	   *handle = NULL;
	   
	   for (iter = he->h_addr_list; *iter != NULL; iter++) {
			 g_memmove (aptr, *iter, he->h_length);
			 addr = gnome_vfs_address_new_from_sockaddr (sa, addrlen);

			 if (addr != NULL)
				    result = g_list_append (result, addr);
	   }

	   if (result == NULL)
			 return GNOME_VFS_ERROR_INTERNAL;

	   *handle = g_new0 (GnomeVFSResolveHandle, 1);
	   (*handle)->result = result;
	   (*handle)->current = result;
	   
	   return GNOME_VFS_OK;
}

#endif

#ifdef HAVE_RES_NINIT
static gboolean
restart_resolve (void)
{
	   static GTimeVal last_reload = { 0, 0 };
	   static GStaticMutex mutex = G_STATIC_MUTEX_INIT;
	   GTimeVal now;
	   gboolean ret;

	   g_static_mutex_lock (&mutex);
	   g_get_current_time (&now);
	   
	   if ((now.tv_sec - last_reload.tv_sec) > RELOAD_TIMEVAL) {

			 last_reload.tv_sec = now.tv_sec;
			 ret = (res_ninit (&_res) == 0);
	   } else {

			 ret = FALSE;
	   }
	
	   g_static_mutex_unlock (&mutex);
	   return ret;
}
#else
#define restart_resolve() FALSE
#endif


#ifdef HAVE_GETADDRINFO
static GnomeVFSResult
_gnome_vfs_result_from_gai_error (int error)
{

	   switch (error) {
			 
	   case EAI_NONAME: return GNOME_VFS_ERROR_HOST_NOT_FOUND;
#ifdef EAI_ADDRFAMILY
	   case EAI_ADDRFAMILY: return GNOME_VFS_ERROR_HOST_HAS_NO_ADDRESS;
#endif
#ifdef EAI_NODATA
	   case EAI_NODATA: return GNOME_VFS_ERROR_HOST_HAS_NO_ADDRESS;
#endif
#ifdef EAI_SYSTEM
	   case EAI_SYSTEM: return gnome_vfs_result_from_errno ();
#endif
	   case EAI_FAIL:
	   case EAI_AGAIN: return GNOME_VFS_ERROR_NAMESERVER;
			 
	   case EAI_MEMORY: return GNOME_VFS_ERROR_NO_MEMORY;
			 
			 /* We should not get these errors there just here to have
			    complete list of getaddrinfo errors*/
	   case EAI_FAMILY:
	   case EAI_SOCKTYPE:
	   case EAI_SERVICE:
	   case EAI_BADFLAGS:
	   default:
			 return GNOME_VFS_ERROR_INTERNAL;
	   }
}

#endif

/**
 * gnome_vfs_resolve:
 * @hostname: hostname you want to resolve.
 * @handle: pointer to a pointer to a #GnomeVFSResolveHandle.
 *
 * Tries to resolve @hostname. If the operation was successful you can
 * get the resolved addresses in form of #GnomeVFSAddress by calling
 * gnome_vfs_resolve_next_address().
 * 
 * Return value: A #GnomeVFSResult indicating the success of the operation.
 *
 * Since: 2.8
 */
GnomeVFSResult
gnome_vfs_resolve (const char              *hostname,
			    GnomeVFSResolveHandle  **handle)
{
#ifdef HAVE_GETADDRINFO
	   struct addrinfo hints, *result;
	   int res;
	   gboolean retry = TRUE;

restart:	   
	   memset (&hints, 0, sizeof (hints));
	   hints.ai_socktype = SOCK_STREAM;

#ifdef ENABLE_IPV6
# ifdef HAVE_AI_ADDRCONFIG /* RFC3493 */
	   hints.ai_flags = AI_ADDRCONFIG;
	   hints.ai_family = AF_UNSPEC;
# else	
	   hints.ai_family = AF_UNSPEC;
# endif	
#else
	   hints.ai_family = AF_INET;
#endif /* ENABLE_IPV6 */
	   res = getaddrinfo (hostname, NULL, &hints, &result);
	   
	   if (res != 0) {
			 if (retry && restart_resolve ()) {
				    retry = FALSE;
				    goto restart;
			 } else {
				    return _gnome_vfs_result_from_gai_error (res);
			 }
	   }

	   *handle = g_new0 (GnomeVFSResolveHandle, 1);
	   
	   (*handle)->result  = result;
	   (*handle)->current = result;
	   
	   return GNOME_VFS_OK;
#else /* HAVE_GETADDRINFO */
	   struct hostent resbuf, *result = &resbuf;
	   GnomeVFSResult ret;
	   int res;
	   gboolean retry = TRUE;
#ifdef HAVE_GETHOSTBYNAME_R_GLIBC
	   size_t buflen;
	   char *buf;
	   int h_errnop;

restart:	   
	   buf = NULL;
	   buflen = INIT_BUFSIZE;
	   h_errnop = 0;
	   
	   do {
			 buf = g_renew (char, buf, buflen);
			 
			 res = gethostbyname_r (hostname,
							    &resbuf,
							    buf,
							    buflen,
							    &result,
							    &h_errnop);
			 buflen *= 2;

	   } while (res == ERANGE);

	   if (res != 0 || result == NULL || result->h_addr_list[0] == NULL) {
			 g_free (buf);
			 if (retry && restart_resolve ()) {
				    retry = FALSE;
				    goto restart;
			 } else {
				    return gnome_vfs_result_from_h_errno_val (h_errnop);
			 }
	   }

	   ret = resolvehandle_from_hostent (result, handle);
	   g_free (buf);
#elif HAVE_GETHOSTBYNAME_R_SOLARIS
	   size_t buflen;
	   char *buf;
	   int h_errnop;	   
restart:
	   
	   buf = NULL;
	   buflen = INIT_BUFSIZE;
	   h_errnop = 0;
	   
	   do {
			 buf = g_renew (char, buf, buflen);
			 
			 result = gethostbyname_r (hostname,
								  &resbuf,
								  buf,
								  buflen,
								  &h_errnop);
			 buflen *= 2;

	   } while (h_errnop == ERANGE);

	   if (result == NULL) {
			 g_free (buf);
		   
			 if (retry && restart_resolve ()) {
				    retry = FALSE;
				    goto restart;
			 } else {
				    return gnome_vfs_result_from_h_errno_val (h_errnop);
			 }
	   }

	   ret = resolvehandle_from_hostent (result, handle);	
	   g_free (buf);
#elif HAVE_GETHOSTBYNAME_R_HPUX
	   struct hostent_data buf;

restart:
	   res = gethostbyname_r (hostname, result, &buf);

	   if (res != 0) {
			 if (retry && restart_resolve ()) {
				    retry = FALSE;
				    goto restart;
			 } else {
				    return gnome_vfs_result_from_h_errno_val (h_errnop);
			 }
	   }

	   ret = resolvehandle_from_hostent (result, handle);
#else /* !HAVE_GETHOSTBYNAME_R_GLIBC && !HAVE_GETHOSTBYNAME_R_SOLARIS && !HAVE_GETHOSTBYNAME_R_HPUX */
	   res = 0;/* only set to avoid unused variable error */
	   
	   G_LOCK (dns_lock);
restart:
	   result = gethostbyname (hostname);

	   if (result == NULL) {
			 if (retry && restart_resolve ()) {
				    retry = FALSE;
				    goto restart;
			 } else {
#ifndef G_OS_WIN32
				    ret = gnome_vfs_result_from_h_errno ();
#else
				    switch (WSAGetLastError ()) {
				    case WSAHOST_NOT_FOUND:
					    ret = GNOME_VFS_ERROR_HOST_NOT_FOUND;
					    break;
				    case WSANO_DATA:
					    ret = GNOME_VFS_ERROR_HOST_HAS_NO_ADDRESS;
					    break;
				    case WSATRY_AGAIN:
				    case WSANO_RECOVERY:
					    ret = GNOME_VFS_ERROR_NAMESERVER;
				    default:
					    ret = GNOME_VFS_ERROR_GENERIC;
				    }
#endif
			 }
	   } else {
			 ret = resolvehandle_from_hostent (result, handle);
	   }

	   G_UNLOCK (dns_lock);
#endif /* !HAVE_GETHOSTBYNAME_R_GLIBC && !HAVE_GETHOSTBYNAME_R_SOLARIS && !HAVE_GETHOSTBYNAME_R_HPUX */
	   return ret;
#endif /* HAVE_GETADDRINFO */
}


/**
 * gnome_vfs_resolve_reset_to_beginning:
 * @handle: a #GnomeVFSResolveHandle.
 *
 * Reset @handle so that a following call to gnome_vfs_resolve_next_address()
 * will return the first resolved address.
 *
 * Since: 2.8
 */
void
gnome_vfs_resolve_reset_to_beginning (GnomeVFSResolveHandle *handle)
{
	   g_return_if_fail (handle != NULL);

	   handle->current = handle->result;
}

/**
 * gnome_vfs_resolve_next_address:
 * @handle: a #GnomeVFSResolveHandle.
 * @address: a pointer to a pointer to a #GnomeVFSAddress.
 * 
 * Stores the next #GnomeVFSAddress available in @handle of the
 * former lookup in @address.
 * 
 * Return value: %TRUE if the next address was stored in @address or
 * %FALSE if no other address is available.
 *
 * Since: 2.8
 */
gboolean
gnome_vfs_resolve_next_address (GnomeVFSResolveHandle  *handle,
				GnomeVFSAddress       **address)
{
	g_return_val_if_fail (address != NULL, FALSE);
	g_return_val_if_fail (handle != NULL, FALSE);
	
	*address = NULL;
	
#ifdef HAVE_GETADDRINFO   
	while (*address == NULL && handle->current != NULL) {
#ifdef _AIX
		/* getaddrinfo() on AIX 4.3.2 and probably others don't set sa_family in
		   ai_addr so we have to copy it from ai_family */
		handle->current->ai_addr->sa_family = handle->current->ai_family;
#endif
		*address = gnome_vfs_address_new_from_sockaddr (handle->current->ai_addr,
								handle->current->ai_addrlen);
		handle->current = handle->current->ai_next;
	}
#else
	if (handle->current) {
		*address = gnome_vfs_address_dup (handle->current->data);
		handle->current = handle->current->next;
	}
#endif	
	return *address != NULL;	
}


/**
 * gnome_vfs_resolve_free:
 * @handle: a #GnomeVFSResolveHandle.
 *
 * Use this function to free a #GnomeVFSResolveHandle returned by
 * gnome_vfs_resolve().
 * 
 * Since: 2.8
 */
void
gnome_vfs_resolve_free (GnomeVFSResolveHandle  *handle)
{
#ifdef HAVE_GETADDRINFO
	   if (handle->result != NULL)
			 freeaddrinfo (handle->result);

#else
	   for (handle->current = handle->result; handle->current != NULL;
		   handle->current = handle->current->next) {
			 gnome_vfs_address_free (handle->current->data);
	   }
	   g_list_free (handle->result);
	   
#endif
	   g_free (handle);
}

