/* MIT License
 *
 * Copyright (c) 1998, 2011, 2013 Massachusetts Institute of Technology
 * Copyright (c) The c-ares project and its contributors
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice (including the next
 * paragraph) shall be included in all copies or substantial portions of the
 * Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * SPDX-License-Identifier: MIT
 */

#include "ares_private.h"

#ifdef HAVE_NETINET_IN_H
#  include <netinet/in.h>
#endif
#ifdef HAVE_NETDB_H
#  include <netdb.h>
#endif
#ifdef HAVE_ARPA_INET_H
#  include <arpa/inet.h>
#endif

#include "ares_nameser.h"

#ifdef HAVE_STRINGS_H
#  include <strings.h>
#endif

#include "ares_inet_net_pton.h"

static void   sort_addresses(const struct hostent  *host,
                             const struct apattern *sortlist, size_t nsort);
static void   sort6_addresses(const struct hostent  *host,
                              const struct apattern *sortlist, size_t nsort);
static size_t get_address_index(const struct in_addr  *addr,
                                const struct apattern *sortlist, size_t nsort);
static size_t get6_address_index(const struct ares_in6_addr *addr,
                                 const struct apattern *sortlist, size_t nsort);

struct host_query {
  ares_host_callback callback;
  void              *arg;
  ares_channel_t    *channel;
};

static void ares_gethostbyname_callback(void *arg, int status, int timeouts,
                                        struct ares_addrinfo *result)
{
  struct hostent    *hostent  = NULL;
  struct host_query *ghbn_arg = arg;

  if (status == ARES_SUCCESS) {
    status = (int)ares_addrinfo2hostent(result, AF_UNSPEC, &hostent);
  }

  /* addrinfo2hostent will only return ENODATA if there are no addresses _and_
   * no cname/aliases.  However, gethostbyname will return ENODATA even if there
   * is cname/alias data */
  if (status == ARES_SUCCESS && hostent &&
      (!hostent->h_addr_list || !hostent->h_addr_list[0])) {
    status = ARES_ENODATA;
  }

  if (status == ARES_SUCCESS && ghbn_arg->channel->nsort && hostent) {
    if (hostent->h_addrtype == AF_INET6) {
      sort6_addresses(hostent, ghbn_arg->channel->sortlist,
                      ghbn_arg->channel->nsort);
    }
    if (hostent->h_addrtype == AF_INET) {
      sort_addresses(hostent, ghbn_arg->channel->sortlist,
                     ghbn_arg->channel->nsort);
    }
  }

  ghbn_arg->callback(ghbn_arg->arg, status, timeouts, hostent);

  ares_freeaddrinfo(result);
  ares_free(ghbn_arg);
  ares_free_hostent(hostent);
}

void ares_gethostbyname(ares_channel_t *channel, const char *name, int family,
                        ares_host_callback callback, void *arg)
{
  struct ares_addrinfo_hints hints;
  struct host_query         *ghbn_arg;

  if (!callback) {
    return;
  }

  memset(&hints, 0, sizeof(hints));
  hints.ai_flags  = ARES_AI_CANONNAME;
  hints.ai_family = family;

  ghbn_arg = ares_malloc(sizeof(*ghbn_arg));
  if (!ghbn_arg) {
    callback(arg, ARES_ENOMEM, 0, NULL);
    return;
  }

  ghbn_arg->callback = callback;
  ghbn_arg->arg      = arg;
  ghbn_arg->channel  = channel;

  /* NOTE: ares_getaddrinfo() locks the channel, we don't use the channel
   *       outside so no need to lock */
  ares_getaddrinfo(channel, name, NULL, &hints, ares_gethostbyname_callback,
                   ghbn_arg);
}

static void sort_addresses(const struct hostent  *host,
                           const struct apattern *sortlist, size_t nsort)
{
  struct in_addr a1;
  struct in_addr a2;
  int            i1;
  int            i2;
  size_t         ind1;
  size_t         ind2;

  /* This is a simple insertion sort, not optimized at all.  i1 walks
   * through the address list, with the loop invariant that everything
   * to the left of i1 is sorted.  In the loop body, the value at i1 is moved
   * back through the list (via i2) until it is in sorted order.
   */
  for (i1 = 0; host->h_addr_list[i1]; i1++) {
    memcpy(&a1, host->h_addr_list[i1], sizeof(struct in_addr));
    ind1 = get_address_index(&a1, sortlist, nsort);
    for (i2 = i1 - 1; i2 >= 0; i2--) {
      memcpy(&a2, host->h_addr_list[i2], sizeof(struct in_addr));
      ind2 = get_address_index(&a2, sortlist, nsort);
      if (ind2 <= ind1) {
        break;
      }
      memcpy(host->h_addr_list[i2 + 1], &a2, sizeof(struct in_addr));
    }
    memcpy(host->h_addr_list[i2 + 1], &a1, sizeof(struct in_addr));
  }
}

/* Find the first entry in sortlist which matches addr.  Return nsort
 * if none of them match.
 */
static size_t get_address_index(const struct in_addr  *addr,
                                const struct apattern *sortlist, size_t nsort)
{
  size_t           i;
  struct ares_addr aaddr;

  memset(&aaddr, 0, sizeof(aaddr));
  aaddr.family = AF_INET;
  memcpy(&aaddr.addr.addr4, addr, 4);

  for (i = 0; i < nsort; i++) {
    if (sortlist[i].addr.family != AF_INET) {
      continue;
    }

    if (ares_subnet_match(&aaddr, &sortlist[i].addr, sortlist[i].mask)) {
      break;
    }
  }

  return i;
}

static void sort6_addresses(const struct hostent  *host,
                            const struct apattern *sortlist, size_t nsort)
{
  struct ares_in6_addr a1;
  struct ares_in6_addr a2;
  int                  i1;
  int                  i2;
  size_t               ind1;
  size_t               ind2;

  /* This is a simple insertion sort, not optimized at all.  i1 walks
   * through the address list, with the loop invariant that everything
   * to the left of i1 is sorted.  In the loop body, the value at i1 is moved
   * back through the list (via i2) until it is in sorted order.
   */
  for (i1 = 0; host->h_addr_list[i1]; i1++) {
    memcpy(&a1, host->h_addr_list[i1], sizeof(struct ares_in6_addr));
    ind1 = get6_address_index(&a1, sortlist, nsort);
    for (i2 = i1 - 1; i2 >= 0; i2--) {
      memcpy(&a2, host->h_addr_list[i2], sizeof(struct ares_in6_addr));
      ind2 = get6_address_index(&a2, sortlist, nsort);
      if (ind2 <= ind1) {
        break;
      }
      memcpy(host->h_addr_list[i2 + 1], &a2, sizeof(struct ares_in6_addr));
    }
    memcpy(host->h_addr_list[i2 + 1], &a1, sizeof(struct ares_in6_addr));
  }
}

/* Find the first entry in sortlist which matches addr.  Return nsort
 * if none of them match.
 */
static size_t get6_address_index(const struct ares_in6_addr *addr,
                                 const struct apattern *sortlist, size_t nsort)
{
  size_t           i;
  struct ares_addr aaddr;

  memset(&aaddr, 0, sizeof(aaddr));
  aaddr.family = AF_INET6;
  memcpy(&aaddr.addr.addr6, addr, 16);

  for (i = 0; i < nsort; i++) {
    if (sortlist[i].addr.family != AF_INET6) {
      continue;
    }

    if (ares_subnet_match(&aaddr, &sortlist[i].addr, sortlist[i].mask)) {
      break;
    }
  }
  return i;
}

static ares_status_t ares_hostent_localhost(const char *name, int family,
                                            struct hostent **host_out)
{
  ares_status_t              status;
  struct ares_addrinfo      *ai = NULL;
  struct ares_addrinfo_hints hints;

  memset(&hints, 0, sizeof(hints));
  hints.ai_family = family;

  ai = ares_malloc_zero(sizeof(*ai));
  if (ai == NULL) {
    status = ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */
    goto done;            /* LCOV_EXCL_LINE: OutOfMemory */
  }

  status = ares_addrinfo_localhost(name, 0, &hints, ai);
  if (status != ARES_SUCCESS) {
    goto done; /* LCOV_EXCL_LINE: OutOfMemory */
  }

  status = ares_addrinfo2hostent(ai, family, host_out);
  if (status != ARES_SUCCESS) {
    goto done; /* LCOV_EXCL_LINE: OutOfMemory */
  }

done:
  ares_freeaddrinfo(ai);
  return status;
}

/* I really have no idea why this is exposed as a public function, but since
 * it is, we can't kill this legacy function. */
static ares_status_t ares_gethostbyname_file_int(ares_channel_t *channel,
                                                 const char *name, int family,
                                                 struct hostent **host)
{
  const ares_hosts_entry_t *entry;
  ares_status_t             status;

  /* We only take the channel to ensure that ares_init() been called. */
  if (channel == NULL || name == NULL || host == NULL) {
    /* Anything will do, really.  This seems fine, and is consistent with
       other error cases. */
    if (host != NULL) {
      *host = NULL;
    }
    return ARES_ENOTFOUND;
  }

  /* Per RFC 7686, reject queries for ".onion" domain names with NXDOMAIN. */
  if (ares_is_onion_domain(name)) {
    return ARES_ENOTFOUND;
  }

  status = ares_hosts_search_host(channel, ARES_FALSE, name, &entry);
  if (status != ARES_SUCCESS) {
    goto done;
  }

  status = ares_hosts_entry_to_hostent(entry, family, host);
  if (status != ARES_SUCCESS) {
    goto done; /* LCOV_EXCL_LINE: OutOfMemory */
  }

done:
  /* RFC6761 section 6.3 #3 states that "Name resolution APIs and libraries
   * SHOULD recognize localhost names as special and SHOULD always return the
   * IP loopback address for address queries".
   * We will also ignore ALL errors when trying to resolve localhost, such
   * as permissions errors reading /etc/hosts or a malformed /etc/hosts */
  if (status != ARES_SUCCESS && status != ARES_ENOMEM &&
      ares_is_localhost(name)) {
    return ares_hostent_localhost(name, family, host);
  }

  return status;
}

int ares_gethostbyname_file(ares_channel_t *channel, const char *name,
                            int family, struct hostent **host)
{
  ares_status_t status;
  if (channel == NULL) {
    return ARES_ENOTFOUND;
  }

  ares_channel_lock(channel);
  status = ares_gethostbyname_file_int(channel, name, family, host);
  ares_channel_unlock(channel);
  return (int)status;
}
