/* MIT License
 *
 * Copyright (c) 1998 Massachusetts Institute of Technology
 * Copyright (c) 2008 Daniel Stenberg
 *
 * 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_ARPA_INET_H
#  include <arpa/inet.h>
#endif

#include "ares_data.h"
#include "ares_inet_net_pton.h"

void ares_destroy_options(struct ares_options *options)
{
  int i;

  ares_free(options->servers);

  for (i = 0; options->domains && i < options->ndomains; i++) {
    ares_free(options->domains[i]);
  }

  ares_free(options->domains);
  ares_free(options->sortlist);
  ares_free(options->lookups);
  ares_free(options->resolvconf_path);
  ares_free(options->hosts_path);
}

static struct in_addr *ares_save_opt_servers(const ares_channel_t *channel,
                                             int                  *nservers)
{
  ares_slist_node_t *snode;
  struct in_addr    *out =
    ares_malloc_zero(ares_slist_len(channel->servers) * sizeof(*out));

  *nservers = 0;

  if (out == NULL) {
    return NULL;
  }

  for (snode = ares_slist_node_first(channel->servers); snode != NULL;
       snode = ares_slist_node_next(snode)) {
    const ares_server_t *server = ares_slist_node_val(snode);

    if (server->addr.family != AF_INET) {
      continue;
    }

    memcpy(&out[*nservers], &server->addr.addr.addr4, sizeof(*out));
    (*nservers)++;
  }

  return out;
}

/* Save options from initialized channel */
int ares_save_options(const ares_channel_t *channel,
                      struct ares_options *options, int *optmask)
{
  size_t i;

  /* NOTE: We can't zero the whole thing out, this is because the size of the
   *       struct ares_options changes over time, so if someone compiled
   *       with an older version, their struct size might be smaller and
   *       we might overwrite their memory! So using the optmask is critical
   *       here, as they could have only set options they knew about.
   *
   *       Unfortunately ares_destroy_options() doesn't take an optmask, so
   *       there are a few pointers we *must* zero out otherwise we won't
   *       know if they were allocated or not
   */
  options->servers         = NULL;
  options->domains         = NULL;
  options->sortlist        = NULL;
  options->lookups         = NULL;
  options->resolvconf_path = NULL;
  options->hosts_path      = NULL;

  if (!ARES_CONFIG_CHECK(channel)) {
    return ARES_ENODATA;
  }

  if (channel->optmask & ARES_OPT_FLAGS) {
    options->flags = (int)channel->flags;
  }

  /* We convert ARES_OPT_TIMEOUT to ARES_OPT_TIMEOUTMS in
   * ares_init_by_options() */
  if (channel->optmask & ARES_OPT_TIMEOUTMS) {
    options->timeout = (int)channel->timeout;
  }

  if (channel->optmask & ARES_OPT_TRIES) {
    options->tries = (int)channel->tries;
  }

  if (channel->optmask & ARES_OPT_NDOTS) {
    options->ndots = (int)channel->ndots;
  }

  if (channel->optmask & ARES_OPT_MAXTIMEOUTMS) {
    options->maxtimeout = (int)channel->maxtimeout;
  }

  if (channel->optmask & ARES_OPT_UDP_PORT) {
    options->udp_port = channel->udp_port;
  }
  if (channel->optmask & ARES_OPT_TCP_PORT) {
    options->tcp_port = channel->tcp_port;
  }

  if (channel->optmask & ARES_OPT_SOCK_STATE_CB) {
    options->sock_state_cb      = channel->sock_state_cb;
    options->sock_state_cb_data = channel->sock_state_cb_data;
  }

  if (channel->optmask & ARES_OPT_SERVERS) {
    options->servers = ares_save_opt_servers(channel, &options->nservers);
    if (options->servers == NULL) {
      return ARES_ENOMEM;
    }
  }

  if (channel->optmask & ARES_OPT_DOMAINS) {
    options->domains = NULL;
    if (channel->ndomains) {
      options->domains = ares_malloc(channel->ndomains * sizeof(char *));
      if (!options->domains) {
        return ARES_ENOMEM;
      }

      for (i = 0; i < channel->ndomains; i++) {
        options->domains[i] = ares_strdup(channel->domains[i]);
        if (!options->domains[i]) {
          options->ndomains = (int)i;
          return ARES_ENOMEM;
        }
      }
    }
    options->ndomains = (int)channel->ndomains;
  }

  if (channel->optmask & ARES_OPT_LOOKUPS) {
    options->lookups = ares_strdup(channel->lookups);
    if (!options->lookups && channel->lookups) {
      return ARES_ENOMEM;
    }
  }

  if (channel->optmask & ARES_OPT_SORTLIST) {
    options->sortlist = NULL;
    if (channel->nsort) {
      options->sortlist = ares_malloc(channel->nsort * sizeof(struct apattern));
      if (!options->sortlist) {
        return ARES_ENOMEM;
      }
      for (i = 0; i < channel->nsort; i++) {
        options->sortlist[i] = channel->sortlist[i];
      }
    }
    options->nsort = (int)channel->nsort;
  }

  if (channel->optmask & ARES_OPT_RESOLVCONF) {
    options->resolvconf_path = ares_strdup(channel->resolvconf_path);
    if (!options->resolvconf_path) {
      return ARES_ENOMEM;
    }
  }

  if (channel->optmask & ARES_OPT_HOSTS_FILE) {
    options->hosts_path = ares_strdup(channel->hosts_path);
    if (!options->hosts_path) {
      return ARES_ENOMEM;
    }
  }

  if (channel->optmask & ARES_OPT_SOCK_SNDBUF &&
      channel->socket_send_buffer_size > 0) {
    options->socket_send_buffer_size = channel->socket_send_buffer_size;
  }

  if (channel->optmask & ARES_OPT_SOCK_RCVBUF &&
      channel->socket_receive_buffer_size > 0) {
    options->socket_receive_buffer_size = channel->socket_receive_buffer_size;
  }

  if (channel->optmask & ARES_OPT_EDNSPSZ) {
    options->ednspsz = (int)channel->ednspsz;
  }

  if (channel->optmask & ARES_OPT_UDP_MAX_QUERIES) {
    options->udp_max_queries = (int)channel->udp_max_queries;
  }

  if (channel->optmask & ARES_OPT_QUERY_CACHE) {
    options->qcache_max_ttl = channel->qcache_max_ttl;
  }

  if (channel->optmask & ARES_OPT_EVENT_THREAD) {
    options->evsys = channel->evsys;
  }

  /* Set options for server failover behavior */
  if (channel->optmask & ARES_OPT_SERVER_FAILOVER) {
    options->server_failover_opts.retry_chance = channel->server_retry_chance;
    options->server_failover_opts.retry_delay  = channel->server_retry_delay;
  }

  *optmask = (int)channel->optmask;

  return ARES_SUCCESS;
}

static ares_status_t ares_init_options_servers(ares_channel_t       *channel,
                                               const struct in_addr *servers,
                                               size_t                nservers)
{
  ares_llist_t *slist = NULL;
  ares_status_t status;

  status = ares_in_addr_to_sconfig_llist(servers, nservers, &slist);
  if (status != ARES_SUCCESS) {
    return status; /* LCOV_EXCL_LINE: OutOfMemory */
  }

  status = ares_servers_update(channel, slist, ARES_TRUE);

  ares_llist_destroy(slist);

  return status;
}

ares_status_t ares_init_by_options(ares_channel_t            *channel,
                                   const struct ares_options *options,
                                   int                        optmask)
{
  size_t i;

  if (channel == NULL) {
    return ARES_ENODATA; /* LCOV_EXCL_LINE: DefensiveCoding */
  }

  if (options == NULL) {
    if (optmask != 0) {
      return ARES_ENODATA; /* LCOV_EXCL_LINE: DefensiveCoding */
    }
    return ARES_SUCCESS;
  }

  /* Easy stuff. */

  /* Event Thread requires threading support and is incompatible with socket
   * state callbacks */
  if (optmask & ARES_OPT_EVENT_THREAD) {
    if (!ares_threadsafety()) {
      return ARES_ENOTIMP;
    }
    if (optmask & ARES_OPT_SOCK_STATE_CB) {
      return ARES_EFORMERR;
    }
    channel->evsys = options->evsys;
  }

  if (optmask & ARES_OPT_FLAGS) {
    channel->flags = (unsigned int)options->flags;
  }

  if (optmask & ARES_OPT_TIMEOUTMS) {
    /* Apparently some integrations were passing -1 to tell c-ares to use
     * the default instead of just omitting the optmask */
    if (options->timeout <= 0) {
      optmask &= ~(ARES_OPT_TIMEOUTMS);
    } else {
      channel->timeout = (unsigned int)options->timeout;
    }
  } else if (optmask & ARES_OPT_TIMEOUT) {
    optmask &= ~(ARES_OPT_TIMEOUT);
    /* Apparently some integrations were passing -1 to tell c-ares to use
     * the default instead of just omitting the optmask */
    if (options->timeout > 0) {
      /* Convert to milliseconds */
      optmask          |= ARES_OPT_TIMEOUTMS;
      channel->timeout  = (unsigned int)options->timeout * 1000;
    }
  }

  if (optmask & ARES_OPT_TRIES) {
    if (options->tries <= 0) {
      optmask &= ~(ARES_OPT_TRIES);
    } else {
      channel->tries = (size_t)options->tries;
    }
  }

  if (optmask & ARES_OPT_NDOTS) {
    if (options->ndots < 0) {
      optmask &= ~(ARES_OPT_NDOTS);
    } else {
      channel->ndots = (size_t)options->ndots;
    }
  }

  if (optmask & ARES_OPT_MAXTIMEOUTMS) {
    if (options->maxtimeout <= 0) {
      optmask &= ~(ARES_OPT_MAXTIMEOUTMS);
    } else {
      channel->maxtimeout = (size_t)options->maxtimeout;
    }
  }

  if (optmask & ARES_OPT_ROTATE) {
    channel->rotate = ARES_TRUE;
  }

  if (optmask & ARES_OPT_NOROTATE) {
    channel->rotate = ARES_FALSE;
  }

  if (optmask & ARES_OPT_UDP_PORT) {
    channel->udp_port = options->udp_port;
  }

  if (optmask & ARES_OPT_TCP_PORT) {
    channel->tcp_port = options->tcp_port;
  }

  if (optmask & ARES_OPT_SOCK_STATE_CB) {
    channel->sock_state_cb      = options->sock_state_cb;
    channel->sock_state_cb_data = options->sock_state_cb_data;
  }

  if (optmask & ARES_OPT_SOCK_SNDBUF) {
    if (options->socket_send_buffer_size <= 0) {
      optmask &= ~(ARES_OPT_SOCK_SNDBUF);
    } else {
      channel->socket_send_buffer_size = options->socket_send_buffer_size;
    }
  }

  if (optmask & ARES_OPT_SOCK_RCVBUF) {
    if (options->socket_receive_buffer_size <= 0) {
      optmask &= ~(ARES_OPT_SOCK_RCVBUF);
    } else {
      channel->socket_receive_buffer_size = options->socket_receive_buffer_size;
    }
  }

  if (optmask & ARES_OPT_EDNSPSZ) {
    if (options->ednspsz <= 0) {
      optmask &= ~(ARES_OPT_EDNSPSZ);
    } else {
      channel->ednspsz = (size_t)options->ednspsz;
    }
  }

  /* Copy the domains, if given.  Keep channel->ndomains consistent so
   * we can clean up in case of error.
   */
  if (optmask & ARES_OPT_DOMAINS && options->ndomains > 0) {
    channel->domains =
      ares_malloc_zero((size_t)options->ndomains * sizeof(char *));
    if (!channel->domains) {
      return ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */
    }
    channel->ndomains = (size_t)options->ndomains;
    for (i = 0; i < (size_t)options->ndomains; i++) {
      channel->domains[i] = ares_strdup(options->domains[i]);
      if (!channel->domains[i]) {
        return ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */
      }
    }
  }

  /* Set lookups, if given. */
  if (optmask & ARES_OPT_LOOKUPS) {
    if (options->lookups == NULL) {
      optmask &= ~(ARES_OPT_LOOKUPS);
    } else {
      channel->lookups = ares_strdup(options->lookups);
      if (!channel->lookups) {
        return ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */
      }
    }
  }

  /* copy sortlist */
  if (optmask & ARES_OPT_SORTLIST && options->nsort > 0) {
    channel->nsort = (size_t)options->nsort;
    channel->sortlist =
      ares_malloc((size_t)options->nsort * sizeof(struct apattern));
    if (!channel->sortlist) {
      return ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */
    }
    for (i = 0; i < (size_t)options->nsort; i++) {
      channel->sortlist[i] = options->sortlist[i];
    }
  }

  /* Set path for resolv.conf file, if given. */
  if (optmask & ARES_OPT_RESOLVCONF) {
    if (options->resolvconf_path == NULL) {
      optmask &= ~(ARES_OPT_RESOLVCONF);
    } else {
      channel->resolvconf_path = ares_strdup(options->resolvconf_path);
      if (channel->resolvconf_path == NULL) {
        return ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */
      }
    }
  }

  /* Set path for hosts file, if given. */
  if (optmask & ARES_OPT_HOSTS_FILE) {
    if (options->hosts_path == NULL) {
      optmask &= ~(ARES_OPT_HOSTS_FILE);
    } else {
      channel->hosts_path = ares_strdup(options->hosts_path);
      if (channel->hosts_path == NULL) {
        return ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */
      }
    }
  }

  if (optmask & ARES_OPT_UDP_MAX_QUERIES) {
    if (options->udp_max_queries <= 0) {
      optmask &= ~(ARES_OPT_UDP_MAX_QUERIES);
    } else {
      channel->udp_max_queries = (size_t)options->udp_max_queries;
    }
  }

  /* As of c-ares 1.31.0, the Query Cache is on by default.  The only way to
   * disable it is to set options->qcache_max_ttl = 0 while specifying the
   * ARES_OPT_QUERY_CACHE which will actually disable it completely. */
  if (optmask & ARES_OPT_QUERY_CACHE) {
    /* qcache_max_ttl is unsigned unlike the others */
    channel->qcache_max_ttl = options->qcache_max_ttl;
  } else {
    optmask                 |= ARES_OPT_QUERY_CACHE;
    channel->qcache_max_ttl  = 3600;
  }

  /* Initialize the ipv4 servers if provided */
  if (optmask & ARES_OPT_SERVERS) {
    if (options->nservers <= 0) {
      optmask &= ~(ARES_OPT_SERVERS);
    } else {
      ares_status_t status;
      status = ares_init_options_servers(channel, options->servers,
                                         (size_t)options->nservers);
      if (status != ARES_SUCCESS) {
        return status; /* LCOV_EXCL_LINE: OutOfMemory */
      }
    }
  }

  /* Set fields for server failover behavior */
  if (optmask & ARES_OPT_SERVER_FAILOVER) {
    channel->server_retry_chance = options->server_failover_opts.retry_chance;
    channel->server_retry_delay  = options->server_failover_opts.retry_delay;
  }

  channel->optmask = (unsigned int)optmask;

  return ARES_SUCCESS;
}
