/*
  Copyright (C) 1998-2023 Free Software Foundation, Inc.

  This file is part of GNU Inetutils.

  GNU Inetutils 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.

  GNU Inetutils 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, see `http://www.gnu.org/licenses/'. */

/* Written by Marco d'Itri.  */

#include <config.h>
/* System library */
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <errno.h>
#include <signal.h>
#include <progname.h>
#include <argp.h>
#define obstack_chunk_alloc malloc
#define obstack_chunk_free free
#include <obstack.h>
#include <error.h>
#include <attribute.h>
#include <libinetutils.h>

/* Application-specific */
#include <data.h>
#include <whois.h>
#include "xalloc.h"

/* Global variables */
int sockfd, verb = 0;
struct obstack query_stk;

#ifdef ALWAYS_HIDE_DISCL
int hide_discl = 0;
#else
int hide_discl = 2;
#endif
const char *server = NULL;
const char *port = NULL;
int nopar = 0;

static struct argp_option ripe_argp_options[] = {
#define GRP 10
  {NULL, 'a', NULL, 0,
   "search all databases", GRP},
  {NULL, 'F', NULL, 0,
   "fast raw output (implies -r)", GRP},
  {NULL, 'g', "SOURCE:FIRST-LAST", 0,
   "find updates from SOURCE from serial FIRST to LAST", GRP},
  {NULL, 'i', "ATTR[,ATTR]...", 0,
   "do an inverse lookup for specified ATTRibutes", GRP},
  {NULL, 'l', NULL, 0,
   "one level less specific lookup (RPSL only)", GRP},
  {NULL, 'L', NULL, 0,
   "find all Less specific matches", GRP},
  {NULL, 'M', NULL, 0,
   "find all More specific matches", GRP},
  {NULL, 'm', NULL, 0,
   "find first level more specific matches", GRP},
  {NULL, 'r', NULL, 0,
   "turn off recursive lookups", GRP},
  {NULL, 'R', NULL, 0,
   "force to show local copy of the domain object even "
   "if it contains referral", GRP},
  {NULL, 'S', NULL, 0,
   "tell server to leave out syntactic sugar", GRP},
  {NULL, 's', "SOURCE[,SOURCE]...", 0,
   "search the database from SOURCE", GRP},
  {NULL, 'T', "TYPE[,TYPE]...", 0,
   "only look for objects of TYPE", GRP},
  {NULL, 'q', "version|sources", 0,
   "query specified server info (RPSL only)", GRP},
  {NULL, 't', "TYPE", 0,
   "requests template for object of TYPE ('all' for a list)", GRP},
  {NULL, 'x', NULL, 0,
   "exact match only (RPSL only)", GRP},
#undef GRP
  {NULL, 0, NULL, 0, NULL, 0}
};

static error_t
ripe_argp_parser (int key, char *arg, struct argp_state *state MAYBE_UNUSED)
{
  if (key > 0 && (unsigned) key < 128)
    {
      if (key == 't' || key == 'v' || key == 'q')
	nopar = 1;

      obstack_1grow (&query_stk, '-');
      obstack_1grow (&query_stk, key);
      if (arg)
	{
	  obstack_1grow (&query_stk, ' ');
	  obstack_grow (&query_stk, arg, strlen (arg));
	}
      return 0;
    }
  return ARGP_ERR_UNKNOWN;
}

static struct argp ripe_argp =
  { ripe_argp_options, ripe_argp_parser, NULL, NULL, NULL, NULL, NULL };

static struct argp_option gwhois_argp_options[] = {
#define GRP 20
  {NULL, 0, NULL, 0, "General options", GRP},
  {"verbose", 'V', NULL, 0,
   "explain what is being done", GRP},
  {"server", 'h', "HOST", 0,
   "connect to server HOST", GRP},
  {"port", 'p', "PORT", 0,
   "connect to PORT", GRP},
  {NULL, 'H', NULL, 0,
   "hide legal disclaimers", GRP},
#undef GRP
  {NULL, 0, NULL, 0, NULL, 0}
};

static error_t
gwhois_argp_parser (int key, char *arg, struct argp_state *state MAYBE_UNUSED)
{
  char *p, *q;

  switch (key)
    {
    case 'h':
      server = q = xmalloc (strlen (arg) + 1);
      for (p = arg; *p != '\0' && *p != ':'; *q++ = tolower (*p++));
      if (*p == ':')
	port = p + 1;
      *q = '\0';
      break;

    case 'H':
      hide_discl = 0;		/* enable disclaimers hiding */
      break;

    case 'p':
      port = arg;
      break;

    case 'V':
      verb = 1;
      break;

    default:
      return ARGP_ERR_UNKNOWN;
    }
  return 0;
}

struct argp_child gwhois_argp_children[] = {
  {&ripe_argp,
   0,
   "RIPE-specific options",
   0},
  {NULL, 0, NULL, 0}
};

static struct argp gwhois_argp = {
  gwhois_argp_options,
  gwhois_argp_parser,
  "OBJECT...",
  "client for the whois directory service",
  gwhois_argp_children,
  NULL,
  NULL
};

const char *program_authors[] = {
  "Marco d'Itri",
  NULL
};

int
main (int argc, char *argv[])
{
  int index;
  char *fstring;
  char *qstring;
  char *p;

  set_program_name (argv[0]);

#ifdef ENABLE_NLS
  setlocale (LC_MESSAGES, "");
  bindtextdomain (NLS_CAT_NAME, LOCALEDIR);
  textdomain (NLS_CAT_NAME);
#endif

  obstack_init (&query_stk);
  iu_argp_init ("whois", program_authors);
  argp_parse (&gwhois_argp, argc, argv, ARGP_IN_ORDER, &index, NULL);
  obstack_1grow (&query_stk, 0);
  fstring = obstack_finish (&query_stk);
  argc -= index;
  argv += index;

  if (argc == 0 && !nopar)	/* there is no parameter */
    error (EXIT_FAILURE, 0, "not enough arguments");

  /* parse other parameters, if any */
  if (!nopar)
    {
      while (argc--)
	{
	  const char *arg = *argv++;
	  obstack_grow (&query_stk, arg, strlen (arg));
	  if (argc)
	    obstack_1grow (&query_stk, ' ');
	}
    }
  obstack_1grow (&query_stk, 0);
  qstring = obstack_finish (&query_stk);

  if (!server && domfind (qstring, gtlds))
    {
      if (verb)
	puts (_("Connecting to whois.internic.net."));
      sockfd = openconn ("whois.internic.net", NULL);
      server = query_crsnic (sockfd, qstring);
      closeconn (sockfd);
      if (!server)
	exit (EXIT_SUCCESS);
      printf (_("\nFound InterNIC referral to %s.\n\n"), server);
    }

  if (!server)
    {
      server = whichwhois (qstring);
      switch (server[0])
	{
	case 0:
	  if (!(server = getenv ("WHOIS_SERVER")))
	    server = DEFAULTSERVER;
	  if (verb)
	    printf (_("Using default server %s.\n"), server);
	  break;
	case 1:
	  puts (_("This TLD has no whois server, but you can access the "
		  "whois database at"));
	  /* FALLTHROUGH */
	case 2:
	  puts (server + 1);
	  exit (EXIT_SUCCESS);
	case 3:
	  puts (_("This TLD has no whois server."));
	  exit (EXIT_SUCCESS);
	default:
	  if (verb)
	    printf (_("Using server %s.\n"), server);
	}
    }

  if (getenv ("WHOIS_HIDE"))
    hide_discl = 0;

  p = queryformat (server, fstring, qstring);
  if (verb)
    printf (_("Query string: \"%s\"\n\n"), p);
  strcat (p, "\r\n");

  signal (SIGTERM, sighandler);
  signal (SIGINT, sighandler);

  sockfd = openconn (server, port);
  do_query (sockfd, p);
  closeconn (sockfd);

  exit (EXIT_SUCCESS);
}

const char *
whichwhois (const char *s)
{
  unsigned long ip;
  unsigned int i;

  /* Step over any single glyph preamble, as it must be some
   * kind of informed instructions for an anticipated server.
   */
  while ((*s != '\0') && (*(s + 1) != '\0') && (*(s + 1) == ' '))
    s += 2;

  /* -v or -t has been used */
  if (*s == '\0')
    return "whois.ripe.net";

  /* email address */
  if (strchr (s, '@'))
    return "";

  /* no dot, no colon, and no hyphen means it's an NSI NIC handle or ASN (?) */
  if (!strpbrk (s, ".-:"))
    {
      const char *p;

      for (p = s; *p != '\0'; p++);	/* go to the end of s */
      if (strncasecmp (s, "as", 2) == 0 &&	/* it's an AS */
	  ((s[2] >= '0' && s[2] <= '9') || s[2] == ' '))
	return whereas (atoi (s + 2), as_assign);
      else if (strncasecmp (p - 2, "jp", 2) == 0)	/* JP NIC handle */
	return "whois.nic.ad.jp";
      if (*p == '!')		/* NSI NIC handle */
	return "whois.networksolutions.com";
      else			/* it's a NSI NIC handle or something we don't know about */
	return "";
    }

  /* IPv6 address */
  if (strchr (s, ':'))
    {
      int a = 0, b = 0;

      /* Only the first two double-octets are ever scanned.
       * At least one is required.
       */
      if (sscanf (s, "%x:%x:", &a, &b) == 0)
	return "";

      /* Mask off any non-significant bits and generate prefix.  */
      ip = (a & 0xffff) << 16 | (b & 0xffff);

      for (i = 0; ip6_assign[i].serv; i++)
	if ((ip & ip6_assign[i].mask) == ip6_assign[i].net)
	  return ip6_assign[i].serv;

      if (verb)
	puts (_("Delegation server of this address is not known.\n"
		"The fallback is ARIN, hoping for success."));

      return "whois.arin.net";
    }

  /* smells like an IP? */
  if ((ip = myinet_aton (s)))
    {
      for (i = 0; ip_assign[i].serv; i++)
	if ((ip & ip_assign[i].mask) == ip_assign[i].net)
	  return ip_assign[i].serv;
      if (verb)
	puts (_("I don't know where this IP has been delegated.\n"
		"I'll try ARIN and hope for the best..."));
      return "whois.arin.net";
    }

  /* check TLD list */
  for (i = 0; tld_serv[i]; i += 2)
    if (domcmp (s, tld_serv[i]))
      return tld_serv[i + 1];

  /* no dot but hyphen */
  if (!strchr (s, '.'))
    {
      /* search for strings at the start of the word */
      for (i = 0; nic_handles[i]; i += 2)
	if (strncasecmp (s, nic_handles[i], strlen (nic_handles[i])) == 0)
	  return nic_handles[i + 1];
      if (verb)
	puts (_("I guess it's a netblock name but I don't know where to"
		" look it up."));
      return "whois.arin.net";
    }

  /* has dot and hypen and it's not in tld_serv[], WTF is it? */
  if (verb)
    puts (_("I guess it's a domain but I don't know where to look it"
	    " up."));
  return "";
}

const char *
whereas (int asn, struct as_del aslist[])
{
  int i;

  if (asn > 398287)
    puts (_("Unknown AS number. Please upgrade this program."));
  for (i = 0; aslist[i].serv; i++)
    if (asn >= aslist[i].first && asn <= aslist[i].last)
      return aslist[i].serv;
  return "whois.arin.net";
}

int
is_ripe_server (const char *const *srvtab, const char *name)
{
  struct in_addr addr;
  int isip = 0;

  isip = inet_aton (name, &addr);

  for (; *srvtab; ++srvtab)
    {
      const char *server = *srvtab;
      struct hostent *hp;

      if (strcmp (server, name) == 0)
	return 1;
      if (isip && (hp = gethostbyname (server)) != NULL)
	{
	  char **pa;
	  for (pa = hp->h_addr_list; *pa; pa++)
	    if (*(unsigned long *) *pa == addr.s_addr)
	      return 1;
	}
    }
  return 0;
}

char *
queryformat (const char *server, const char *flags, const char *query)
{
  char *buf;
  int isripe = 0;

  /* +10 for CORE; +2 for \r\n; +1 for NULL */
  buf = xmalloc (strlen (flags) + strlen (query) + 10 + 2 + 1);
  *buf = '\0';

  isripe = is_ripe_server (ripe_servers, server)
    || is_ripe_server (ripe_servers_old, server);
  if (isripe)
    strcat (buf, "-V" IDSTRING " ");

  if (*flags != '\0')
    {
      if (!isripe && strcmp (server, "whois.corenic.net") != 0)
	puts (_("Warning: RIPE flags ignored for a traditional server."));
      else
	strcat (buf, flags);
    }
  if (!isripe &&
      (strcmp (server, "whois.arin.net") == 0 ||
       strcmp (server, "whois.nic.mil") == 0) &&
      strncasecmp (query, "AS", 2) == 0 && query[2] >= '0' && query[2] <= '9')
    sprintf (buf, "a %s", query + 2);	/* fix query for ARIN */
  else if (!isripe && strcmp (server, "whois.corenic.net") == 0)
    sprintf (buf, "--machine %s", query);	/* machine readable output */
  else if (!isripe && strcmp (server, "whois.ncst.ernet.in") == 0 &&
	   !strchr (query, ' '))
    sprintf (buf, "domain %s", query);	/* ask for a domain */
  else if (!isripe && strcmp (server, "whois.nic.ad.jp") == 0)
    {
      char *lang = getenv ("LANG");	/* not a perfect check, but... */
      if (!lang || (strncmp (lang, "ja", 2) != 0))
	sprintf (buf, "%s/e", query);	/* ask for english text */
      else
	strcat (buf, query);
    }
  else
    strcat (buf, query);
  return buf;
}

void
do_query (const int sock, const char *query)
{
  char buf[200], *p;
  FILE *fi;
  int i = 0, hide = hide_discl;

  fi = fdopen (sock, "r");
  if (write (sock, query, strlen (query)) < 0)
    err_sys ("write");

  while (fgets (buf, 200, fi))
    {				/* XXX errors? */
      if (hide == 1)
	{
	  if (strncmp (buf, hide_strings[i + 1], strlen (hide_strings[i + 1]))
	      == 0)
	    hide = 0;		/* go into testing mode */
	  continue;		/* hide this line */
	}
      if (hide == 0)
	{
	  for (i = 0; hide_strings[i] != NULL; i += 2)
	    {
	      if (strncmp (buf, hide_strings[i], strlen (hide_strings[i])) ==
		  0)
		{
		  hide = 1;	/* start hiding */
		  break;
		}
	    }
	  if (hide == 1)
	    continue;		/* hide the first line */
	}
#ifdef EXT_6BONE
      /* % referto: whois -h whois.arin.net -p 43 as 1 */
      if (strncmp (buf, "% referto:", 10) == 0)
	{
	  char nh[256], np[16], nq[1024];

	  if (sscanf (buf, REFERTO_FORMAT, nh, np, nq) == 3)
	    {
	      int fd;

	      if (verb)
		printf (_("Detected referral to %s on %s.\n"), nq, nh);
	      strcat (nq, "\r\n");
	      fd = openconn (nh, np);
	      do_query (fd, nq);
	      closeconn (fd);
	      continue;
	    }
	}
#endif
      for (p = buf; *p && *p != '\r' && *p != '\n'; p++);
      *p = '\0';
      fprintf (stdout, "%s\n", buf);
    }
  if (ferror (fi))
    err_sys ("fgets");

  if (hide == 1)
    err_quit (_("Catastrophic error: disclaimer text has been changed.\n"
		"Please upgrade this program.\n"));
}

const char *
query_crsnic (const int sock, const char *query)
{
  char *temp, buf[100], *ret = NULL;
  FILE *fi;

  temp = xmalloc (strlen (query) + 1 + 2 + 1);
  *temp = '=';
  strcpy (temp + 1, query);
  strcat (temp, "\r\n");

  fi = fdopen (sock, "r");
  if (write (sock, temp, strlen (temp)) < 0)
    err_sys ("write");

  while (fgets (buf, 100, fi))
    {
      /* If there are multiple matches only the server of the first record
         is queried */
      if (strncmp (buf, "   Whois Server:", 16) == 0 && !ret)
	{
	  char *p, *q;

	  for (p = buf; *p != ':'; p++);	/* skip until colon */
	  for (p++; *p == ' '; p++);	/* skip colon and spaces */
	  ret = xmalloc (strlen (p) + 1);
	  for (q = ret; *p != '\n' && *p != '\r'; *q++ = *p++);	/*copy data */
	  *q = '\0';
	}
      fputs (buf, stdout);
    }
  if (ferror (fi))
    err_sys ("fgets");

  free (temp);
  return ret;
}

int
openconn (const char *server, const char *port)
{
  int fd;
  int i;
  struct addrinfo hints, *res, *ressave;

  memset (&hints, 0, sizeof (struct addrinfo));
  hints.ai_family = AF_UNSPEC;
  hints.ai_socktype = SOCK_STREAM;

  /* The well-known service name is no longer universally acknowledged,
   * because IANA has tampered with aliasing, making `Who' and `Is'
   * into legal and functional aliases.
   */
  if ((i = getaddrinfo (server, port ? port : "whois", &hints, &res)) != 0)
    if ((i =
	 getaddrinfo (server, port ? port : "nicname", &hints, &res)) != 0)
      err_quit ("getaddrinfo: %s", gai_strerror (i));

  for (ressave = res; res; res = res->ai_next)
    {
      if ((fd =
	   socket (res->ai_family, res->ai_socktype, res->ai_protocol)) < 0)
	continue;		/* ignore */
      if (connect (fd, res->ai_addr, res->ai_addrlen) == 0)
	break;			/* success */
      close (fd);
    }

  if (!res)
    err_sys ("connect");
  freeaddrinfo (ressave);
  return (fd);
}

void
closeconn (const int fd)
{
  close (fd);
}

void
sighandler (int signum)
{
  closeconn (sockfd);
  err_quit (_("Interrupted by signal %d..."), signum);
}

/* check if dom ends with tld */
int
domcmp (const char *dom, const char *tld)
{
  const char *p, *q;

  for (p = dom; *p != '\0'; p++);
  p--;				/* move to the last char */
  for (q = tld; *q != '\0'; q++);
  q--;
  while (p >= dom && q >= tld && tolower (*p) == *q)
    {				/* compare backwards */
      if (q == tld)		/* start of the second word? */
	return 1;
      p--;
      q--;
    }
  return 0;
}

/* check if dom ends with an element of tldlist[] */
int
domfind (const char *dom, const char *tldlist[])
{
  int i;

  for (i = 0; tldlist[i]; i++)
    if (domcmp (dom, tldlist[i]))
      return 1;
  return 0;
}

unsigned long
myinet_aton (const char *s)
{
  int a, b, c, d;

  if (!s)
    return 0;
  if (sscanf (s, "%d.%d.%d.%d", &a, &b, &c, &d) != 4)
    return 0;
  return (a << 24) + (b << 16) + (c << 8) + d;
}


/* Error routines */
void
err_sys (const char *fmt, ...)
{
  va_list ap;

  va_start (ap, fmt);
  vfprintf (stderr, fmt, ap);
  fprintf (stderr, ": %s\n", strerror (errno));
  va_end (ap);
  exit (EXIT_FAILURE);
}

void
err_quit (const char *fmt, ...)
{
  va_list ap;

  va_start (ap, fmt);
  vfprintf (stderr, fmt, ap);
  fputs ("\n", stderr);
  va_end (ap);
  exit (EXIT_FAILURE);
}
