/*
 * adns.c: Asynchronous DNS queries
 *
 * Copyright(c) 2000  - All Rights Reserved
 *
 * See the COPYRIGHT file.
 */

#ifndef lint
static char rcsid[] = "@(#)$Id: adns.c,v 1.6 2000/08/01 19:39:51 kalt Exp $";
#endif

#include "os.h"

#include "struct.h"
#include "server.h"
#include "window.h"
#include "utils.h"

extern int h_errno;

extern  struct server_ *server;

struct query_ {
    struct server_ *srv;
    char *qdata;
#if defined(HAVE_GETADDRINFO)
    struct addrinfo *rdata;
    char *rname;
#else
    char *rdata;
#endif
    char *error;
    struct query_ *nextq;
};

struct query_ *qlist = NULL;
struct query_ *rlist = NULL;

static pthread_t	main_t;
static pthread_t	dns_t;
static pthread_mutex_t	dns_qlist;
static pthread_cond_t	dns_qrdy;
static pthread_mutex_t	dns_rlist;

/* dns_loop: start point, and self contained loop for the resolver thread */
static void *
dns_loop(arg)
void *arg;
{
    sigset_t ss;
    struct query_ *currentq;

    /* block all signals: let the main thread handle them */
    sigfillset(&ss);
    if (pthread_sigmask(SIG_BLOCK, &ss, NULL))
	abort();

    while (1)
      {
	pthread_mutex_lock(&dns_qlist);
	if (qlist == NULL)
	  {
	    /* No queries in the list, release the mutex, and wait.. */
	    pthread_cond_wait(&dns_qrdy, &dns_qlist);
	  }

	/* pop the query off the list */
	assert(qlist);
	currentq = qlist;
	qlist = qlist->nextq;
	pthread_mutex_unlock(&dns_qlist);

	/* work on the query */
#if defined(HAVE_GETADDRINFO)
	/* use it if it's there, and automagically get IPv6 support :-) */
	{
	  struct addrinfo *address, hints;
	  int rc;
	  char port[10];

	  if (currentq->srv)
	      sprintf(port, "%d", currentq->srv->port);

	  /* first, is it an IP or a host? */
	  bzero((char *)&hints, sizeof(hints));
	  hints.ai_flags = AI_NUMERICHOST;
	  hints.ai_family = PF_UNSPEC;
	  hints.ai_socktype = SOCK_STREAM;
	  /* the following call is DNS-less */
	  rc = getaddrinfo(currentq->qdata, currentq->srv ? port : NULL,
			   &hints, &address);

	  if (rc != 0)
	    {
	      /* look up what appears to be a hostname */
	      bzero((char *)&hints, sizeof(hints));
	      hints.ai_flags = AI_CANONNAME;
	      hints.ai_family = PF_UNSPEC;
	      hints.ai_socktype = SOCK_STREAM;
	      rc = getaddrinfo(currentq->qdata, currentq->srv ? port : NULL,
			       &hints, &address);
	      if (rc == 0)
		currentq->rdata = address;
	      else
		{
		  if (address)
		    freeaddrinfo(address);
		  currentq->error = malloc(strlen(currentq->qdata) +
					   strlen(gai_strerror(rc)) + 10);
		  sprintf(currentq->error, "\"%s\" %s.", currentq->qdata,
			  gai_strerror(rc));
		}
	    }
	  else
	    {
	      /* it seems we have an IP address */
	      char host[NI_MAXHOST];

	      assert(currentq->srv == NULL);

	      rc = getnameinfo(address->ai_addr, address->ai_addrlen,
			       host, NI_MAXHOST, NULL, 0, 0);
	      freeaddrinfo(address);
	      if (rc == 0)
		  currentq->rname = strdup(host);
	      else
		{
		  currentq->error = malloc(strlen(currentq->qdata) +
					   strlen("Host not found") + 10);
		  sprintf(currentq->error, "\"%s\" Host not found.",
			  currentq->qdata);
		}
	    }
	}
#else
	{
	  struct hostent *h;
	  unsigned long ad;
	  
	  ad = inet_addr(currentq->qdata);
	  if (ad != INADDR_NONE)
	    {
	      /* doesn't seem to be an IP address */
	      if (h = gethostbyaddr((char *) &ad, sizeof(ad), AF_INET))
		  currentq->rdata = strdup(h->h_name);
	      else
		{
		  currentq->error = malloc(strlen(currentq->qdata) + 40);
		  sprintf(currentq->error, "\"%s\" %s. [%d]", currentq->qdata,
			  (h_errno == HOST_NOT_FOUND) ? "not found" :
			  (h_errno == TRY_AGAIN) ? "not found, try again" :
			  (h_errno == NO_DATA) ? "has no IP address" :
			  "unknown error", h_errno);
		}
	    } else {
	      if (h = gethostbyname(currentq->qdata))
		  currentq->rdata = strdup((char *) inet_ntoa(*((struct in_addr *)h->h_addr_list[0])));
	      else
		{
		  currentq->error = malloc(strlen(currentq->qdata) + 40);
		  sprintf(currentq->error, "\"%s\" %s. [%d]", currentq->qdata,
			  (h_errno == HOST_NOT_FOUND) ? "not found" :
			  (h_errno == TRY_AGAIN) ? "not found, try again" :
			  (h_errno == NO_DATA) ? "has no IP address" :
			  "unknown error", h_errno);
		}
	    }
	}
#endif

	/* put currentq in the list of replies */
	pthread_mutex_lock(&dns_rlist);
	currentq->nextq = rlist;
	rlist = currentq;
	pthread_mutex_unlock(&dns_rlist);
	pthread_kill(main_t, SIGUSR1);
      }
}

/* dns_init: start the resolver thread. */
void
dns_init()
{
    main_t = pthread_self();
    if (pthread_create(&dns_t, NULL, dns_loop, NULL) < 0)
      {
	perror("Couldn't create resolver thread");
	exit(1);
      }
    if (pthread_mutex_init(&dns_qlist, NULL) < 0)
      {
	perror("Couldn't initialize DNS mutex");
	exit(1);
      }
    if (pthread_cond_init(&dns_qrdy, NULL) < 0)
      {
	perror("Couldn't initialize DNS condition variable");
	exit(1);
      }
    if (pthread_mutex_init(&dns_rlist, NULL) < 0)
      {
	perror("Couldn't initialize DNS mutex");
	exit(1);
      }
}

/* dns_lookup: add lookup to the list of pending DNS lookups */
void
dns_lookup(what, srv)
  char *what;
  struct server_ *srv;
{
    struct query_ *new, **tmp;

    vsic_slog(LOG_DEBUG, "DNS: Adding \"%s\" to the list of queries [%d].",
	      what, srv ? 1 : 0);

    new = (struct query_ *) malloc(sizeof(struct query_));
    new->srv = srv;
    new->qdata = strdup(what);
    new->rdata = NULL;
#if defined(HAVE_GETADDRINFO)
    new->rname = NULL;
#endif
    new->error = NULL;
    new->nextq = NULL;

    /* lock the mutex before manipulating qlist */
    pthread_mutex_lock(&dns_qlist);
    /* add the new query to the end of the list */
    tmp = &qlist;
    while (*tmp)
	tmp = &((*tmp)->nextq);
    *tmp = new;

    /* signal the resolver thread that there's work to be done */
    pthread_cond_signal(&dns_qrdy);
    /* release the mutex */
    pthread_mutex_unlock(&dns_qlist);
}

/* dns_process: process results from queries */
void
dns_process()
{
    struct query_ *reply, *tmp;

#if defined(GNU_PTH)
    /*
    ** This might not be necessary since we use pth_select() rather than
    ** select() itself, but might as well be cautious here.
    */
    if (qlist) pth_yield(NULL);
#endif
    if (rlist == NULL) return;
    pthread_mutex_lock(&dns_rlist);
    tmp = rlist;
    while (reply = tmp) {
      tmp = tmp->nextq;
#if defined(HAVE_GETADDRINFO)
      vsic_slog(LOG_DEBUG, "DNS: query \"%s\", reply %X, error \"%s\"",
		reply->qdata, reply->rdata, reply->error ? reply->error : "");
#else
      vsic_slog(LOG_DEBUG, "DNS: query \"%s\", reply \"%s\", error \"%s\"",
		reply->qdata, reply->rdata ? reply->rdata : "",
		reply->error ? reply->error : "");
#endif
      if (reply->error)
	  /* the lookup failed */
	{
	  assert(reply->rdata == NULL);
	  if (reply->srv)
	    {
	      server = reply->srv;
	      if (option(reply->srv->sopt, S_CONNECTING))
		  select_active(NULL, 0);
	    }
	  vsic_slog(LOG_CLIENT, "--- %s: %s",
		    reply->srv ?
                    option(reply->srv->sopt, S_CONNECTING) ? "Connect failed":
                    "Server lookup failed" : "DNS",
		    reply->error);
	  if (reply->srv)
	    {
              unset_option(reply->srv->sopt, S_DNS);
              if (option(reply->srv->sopt, S_CONNECTING))
                  unset_option(reply->srv->sopt, S_CONNECTING);
	    }
	  free(reply->error);
	}
      else
	{
	  /* the lookup was successful */
	  assert(reply->error == NULL);
	  if (reply->srv)
	    {
#if defined(HAVE_GETADDRINFO)
	      struct addrinfo *atmp;
	      
	      assert(reply->srv->ip == NULL);

	      server = reply->srv;
	      if (option(reply->srv->sopt, S_CONNECTING))
		  select_active(NULL, 0);

	      atmp = reply->srv->address = reply->rdata;
	      while (atmp && atmp->ai_family != PF_INET &&
		     atmp->ai_family != PF_INET6)
		  atmp = atmp->ai_next;
	      if (atmp)
		  reply->srv->ip = aitoip(reply->rdata);
	      else
		{
		  vsic_slog(LOG_CLIENT, "--- %s: No address found",
			    option(reply->srv->sopt, S_CONNECTING) ?
			    "Connect failed" : "Server lookup failed");
		  unset_option(reply->srv->sopt, S_DNS);
		  if (option(reply->srv->sopt, S_CONNECTING))
		      unset_option(reply->srv->sopt, S_CONNECTING);
		  reply->srv->address = NULL;
		  freeaddrinfo(reply->rdata);
		  free(reply->qdata);
		  free(reply);
		  continue;
		}
#else
	      reply->srv->ip = strdup(reply->rdata);
#endif
	      unset_option(reply->srv->sopt, S_DNS);
	      if (option(reply->srv->sopt, S_CONNECTING))
		{
		  vsic_slog(LOG_CLIENT, "--- Connecting to %s %d [%s]",
			    reply->srv->sname, reply->srv->port,
			    reply->srv->ip);
		  sic_connect(reply->srv);
		}
	    }
	  else
	    {
#if defined(HAVE_GETADDRINFO)
	      server = NULL;
	      select_active(NULL, 2);
	      if (reply->rdata)
		{
		  struct addrinfo *atmp;
		  
		  if (reply->rdata->ai_canonname &&
		      reply->rdata->ai_canonname[0] &&
		      strcasecmp(reply->qdata, reply->rdata->ai_canonname))
		      vsic_slog(LOG_CLIENT, "DNS: %s is nickname for %s",
				reply->qdata, reply->rdata->ai_canonname);
		  atmp = reply->rdata;
		  while (atmp)
		    {
		      if (atmp->ai_family != PF_INET &&
			  atmp->ai_family != PF_INET6)
			{
			  vsic_slog(LOG_DEBUG, "DNS: unexpected family: %d",
				    atmp->ai_family);
			  continue;
			}
		      vsic_slog(LOG_CLIENT, "--- DNS: %s is %s",
				reply->qdata, reply->rdata->ai_canonname);
		      atmp = atmp->ai_next;
		    }
		  freeaddrinfo(reply->rdata);
		}
	      else
		{
		  vsic_slog(LOG_CLIENT, "--- DNS: %s is %s",
			    reply->qdata, reply->rname);
		  free(reply->rname);
		}
#else
	      vsic_slog(LOG_CLIENT, "--- DNS: %s is %s",
			reply->qdata, reply->rdata);
#endif
	    }
#if !defined(HAVE_GETADDRINFO)
	  free(reply->rdata);
#endif
	}
      free(reply->qdata);
      free(reply);
    }
    rlist = NULL;
    pthread_mutex_unlock(&dns_rlist);
}
