/** @file

  A brief file description

  @section license License

  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
  distributed with this work for additional information
  regarding copyright ownership.  The ASF licenses this file
  to you under the Apache License, Version 2.0 (the
  "License"); you may not use this file except in compliance
  with the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
 */

/*****************************************************************************
 *
 *  SplitDNS.cc - Implementation of "split" DNS (as the name says)
 *
 *
 ****************************************************************************/

#include "P_SplitDNSProcessor.h"
#include "tscore/Tokenizer.h"
#include "tscore/Filenames.h"

#include <sys/types.h>
#include "P_SplitDNS.h"
#include "tscore/MatcherUtils.h"
#include "tscore/HostLookup.h"

/* --------------------------------------------------------------
   this file is built using "ParentSelection.cc as a template.
   -------------    ------------------------------------------------- */

/* --------------------------------------------------------------
   globals
   -------------------------------------------------------------- */
static const char modulePrefix[] = "[SplitDNS]";

ConfigUpdateHandler<SplitDNSConfig> *SplitDNSConfig::splitDNSUpdate = nullptr;

static ClassAllocator<DNSRequestData, false> DNSReqAllocator("DNSRequestDataAllocator");

/* --------------------------------------------------------------
   used by a lot of protocols. We do not have dest ip in most
   cases.
   -------------------------------------------------------------- */
const matcher_tags sdns_dest_tags = {"dest_host", "dest_domain", nullptr, "url_regex", "url", nullptr, true};

/* --------------------------------------------------------------
   config Callback Prototypes
   -------------------------------------------------------------- */
enum SplitDNSCB_t {
  SDNS_FILE_CB,
  SDNS_ENABLE_CB,
};

namespace
{

DbgCtl dbg_ctl_splitdns{"splitdns"};
DbgCtl dbg_ctl_splitdns_config{"splitdns_config"};

const char *SDNSResultStr[] = {"DNSServer_Undefined", "DNSServer_Specified", "DNSServer_Failed"};

} // end anonymous namespace

int             SplitDNSConfig::m_id               = 0;
int             SplitDNSConfig::gsplit_dns_enabled = 0;
int             splitDNSFile_CB(const char *name, RecDataT data_type, RecData data, void *cookie);
Ptr<ProxyMutex> SplitDNSConfig::dnsHandler_mutex;

/* --------------------------------------------------------------
   SplitDNSResult::SplitDNSResult()
   -------------------------------------------------------------- */
inline SplitDNSResult::SplitDNSResult() {}

/* --------------------------------------------------------------
   SplitDNS::SplitDNS()
   -------------------------------------------------------------- */
SplitDNS::SplitDNS() {}

SplitDNS::~SplitDNS()
{
  if (m_DNSSrvrTable) {
    delete m_DNSSrvrTable;
  }
}

/* --------------------------------------------------------------
   SplitDNSConfig::acquire()
   -------------------------------------------------------------- */
SplitDNS *
SplitDNSConfig::acquire()
{
  return static_cast<SplitDNS *>(configProcessor.get(SplitDNSConfig::m_id));
}

/* --------------------------------------------------------------
   SplitDNSConfig::release()
   -------------------------------------------------------------- */
void
SplitDNSConfig::release(SplitDNS *params)
{
  configProcessor.release(SplitDNSConfig::m_id, params);
}

/* --------------------------------------------------------------
   SplitDNSConfig::startup()
   -------------------------------------------------------------- */
void
SplitDNSConfig::startup()
{
  // startup just check gsplit_dns_enabled
  REC_ReadConfigInt32(gsplit_dns_enabled, "proxy.config.dns.splitDNS.enabled");
  SplitDNSConfig::splitDNSUpdate = new ConfigUpdateHandler<SplitDNSConfig>();
  SplitDNSConfig::splitDNSUpdate->attach("proxy.config.cache.splitdns.filename");
}

/* --------------------------------------------------------------
   SplitDNSConfig::reconfigure()
   -------------------------------------------------------------- */
void
SplitDNSConfig::reconfigure()
{
  if (0 == gsplit_dns_enabled) {
    return;
  }

  Note("%s loading ...", ts::filename::SPLITDNS);

  SplitDNS *params = new SplitDNS;

  params->m_SplitDNSlEnable = gsplit_dns_enabled;
  params->m_DNSSrvrTable    = new DNS_table("proxy.config.dns.splitdns.filename", modulePrefix, &sdns_dest_tags);

  if (nullptr == params->m_DNSSrvrTable || (0 == params->m_DNSSrvrTable->getEntryCount())) {
    Warning("Failed to load %s - No NAMEDs provided! Disabling SplitDNS", ts::filename::SPLITDNS);
    gsplit_dns_enabled = 0;
    delete params;
    return;
  }
  params->m_numEle = params->m_DNSSrvrTable->getEntryCount();

  if (nullptr != params->m_DNSSrvrTable->getHostMatcher() && nullptr == params->m_DNSSrvrTable->getReMatcher() &&
      nullptr == params->m_DNSSrvrTable->getIPMatcher() && 4 >= params->m_numEle) {
    HostLookup *pxHL          = params->m_DNSSrvrTable->getHostMatcher()->getHLookup();
    params->m_pxLeafArray     = pxHL->get_leaf_array();
    params->m_bEnableFastPath = true;
  }

  m_id = configProcessor.set(m_id, params);

  if (dbg_ctl_splitdns_config.on()) {
    SplitDNSConfig::print();
  }

  Note("%s finished loading", ts::filename::SPLITDNS);
}

/* --------------------------------------------------------------
   SplitDNSConfig::print()
   -------------------------------------------------------------- */
void
SplitDNSConfig::print()
{
  SplitDNS *params = SplitDNSConfig::acquire();

  Dbg(dbg_ctl_splitdns_config, "DNS Server Selection Config");
  Dbg(dbg_ctl_splitdns_config, "\tEnabled=%d", params->m_SplitDNSlEnable);

  params->m_DNSSrvrTable->Print();
  SplitDNSConfig::release(params);
}

/* --------------------------------------------------------------
   SplitDNS::getDNSRecord()
   -------------------------------------------------------------- */
void *
SplitDNS::getDNSRecord(swoc::TextView hostname)
{
  Dbg(dbg_ctl_splitdns, "Called SplitDNS::getDNSRecord(%.*s)", int(hostname.size()), hostname.data());

  DNSRequestData *pRD = DNSReqAllocator.alloc();
  pRD->m_pHost        = hostname;

  SplitDNSResult res;
  findServer(pRD, &res);

  DNSReqAllocator.free(pRD);

  if (DNS_SRVR_SPECIFIED == res.r) {
    return &(res.m_rec->m_servers);
  }

  Dbg(dbg_ctl_splitdns, "Fail to match a valid splitdns rule, fallback to default dns resolver");
  return nullptr;
}

/* --------------------------------------------------------------
   SplitDNS::findServer()
   -------------------------------------------------------------- */
void
SplitDNS::findServer(RequestData *rdata, SplitDNSResult *result)
{
  DNS_table      *tablePtr = m_DNSSrvrTable;
  SplitDNSRecord *rec;

  ink_assert(result->r == DNS_SRVR_UNDEFINED);

  if (m_SplitDNSlEnable == 0) {
    result->r = DNS_SRVR_UNDEFINED;
    return;
  }

  result->m_rec         = nullptr;
  result->m_line_number = 0xffffffff;

  /* ---------------------------
     the 'alleged' fast path ...
     --------------------------- */
  if (m_bEnableFastPath && m_pxLeafArray) {
    SplitDNSRecord *data_ptr = nullptr;
    char           *pHost    = const_cast<char *>(rdata->get_host());
    if (nullptr == pHost) {
      Warning("SplitDNS: No host to match !");
      return;
    }

    int len = strlen(pHost);
    int n   = std::min(static_cast<size_t>(m_numEle), m_pxLeafArray->size());
    for (int i = 0; i < n; i++) {
      const HostLeaf &pxHL = m_pxLeafArray->at(i);

      if (false == pxHL.isNot && static_cast<int>(pxHL.match.size()) > len) {
        continue;
      }

      int         idx    = len - pxHL.match.size();
      char       *pH     = &pHost[idx];
      const char *pMatch = pxHL.match.data();
      char        cNot   = *pMatch;

      if ('!' == cNot) {
        pMatch++;
      }

      int res = memcmp(pH, pMatch, pxHL.match.size());

      if ((0 != res && '!' == cNot) || (0 == res && '!' != cNot)) {
        data_ptr = static_cast<SplitDNSRecord *>(pxHL.opaque_data);
        data_ptr->UpdateMatch(result, rdata);
        break;
      }
    }
  } else {
    tablePtr->Match(rdata, result);
  }

  rec = result->m_rec;
  if (rec == nullptr) {
    result->r = DNS_SRVR_UNDEFINED;
    return;
  } else {
    result->r = DNS_SRVR_SPECIFIED;
  }

  if (dbg_ctl_splitdns_config.on()) {
    const char *host = rdata->get_host();

    switch (result->r) {
    case DNS_SRVR_FAIL:
      Dbg(dbg_ctl_splitdns_config, "Result for %s was %s", host, SDNSResultStr[result->r]);
      break;
    case DNS_SRVR_SPECIFIED:
      Dbg(dbg_ctl_splitdns_config, "Result for %s was dns servers", host);
      result->m_rec->Print();
      break;
    default:
      // DNS_SRVR_UNDEFINED
      break;
    }
  }
}

/* --------------------------------------------------------------
   SplitDNSRecord::ProcessDNSHosts()
   -------------------------------------------------------------- */
const char *
SplitDNSRecord::ProcessDNSHosts(char *val)
{
  Tokenizer pTok(",; \t\r");
  int       numTok;
  int       port  = 0;
  int       totsz = 0, sz = 0;

  numTok = pTok.Initialize(val, SHARE_TOKS);
  if (MAXNS < numTok) {
    numTok = MAXNS;
    Warning("Only first %d DNS servers are tracked", numTok);
  }
  if (numTok == 0) {
    return "No servers specified";
  }

  /* ------------------------------------------------
     Allocate the servers array and Loop through the
     set of servers specified
     ------------------------------------------------ */
  for (int i = 0; i < numTok; i++) {
    const char *current = pTok[i];
    char       *tmp     = const_cast<char *>(strchr(current, ':'));
    // coverity[secure_coding]
    if (tmp != nullptr && sscanf(tmp + 1, "%d", &port) != 1) {
      return "Malformed DNS port";
    }

    /* ----------------------------------------
       Make sure that is no garbage beyond the
       server port
       ---------------------------------------- */
    if (tmp) {
      char *scan = tmp + 1;
      for (; *scan != '\0' && ParseRules::is_digit(*scan); scan++) {
        ;
      }
      for (; *scan != '\0' && ParseRules::is_wslfcr(*scan); scan++) {
        ;
      }

      if (*scan != '\0') {
        return "Garbage trailing entry or invalid separator";
      }

      if (tmp - current > (MAXDNAME - 1)) {
        return "DNS server name (ip) is too long";
      } else if (tmp - current == 0) {
        return "server string is empty";
      }
      *tmp = 0;
    }

    if (0 != ats_ip_pton(current, &m_servers.x_server_ip[i].sa)) {
      return "invalid IP address given for a DNS server";
    }

    ats_ip_port_cast(&m_servers.x_server_ip[i].sa) = htons(port ? port : NAMESERVER_PORT);

    if ((MAXDNAME * 2 - 1) > totsz) {
      sz = strlen(current);
      memcpy((m_servers.x_dns_ip_line + totsz), current, sz);
      totsz += sz;
    }
  }

  m_dnsSrvr_cnt = numTok;
  return nullptr;
}

/* --------------------------------------------------------------
   SplitDNSRecord::ProcessDefDomain()
   -------------------------------------------------------------- */
const char *
SplitDNSRecord::ProcessDefDomain(char *val)
{
  Tokenizer pTok(",; \t\r");
  int       numTok;

  numTok = pTok.Initialize(val, SHARE_TOKS);

  if (numTok > 1) {
    return "more than one default domain name specified";
  }

  if (numTok == 0) {
    return "no default domain name specified";
  }

  int len = 0;
  if (pTok[0] && 0 != (len = strlen(pTok[0]))) {
    memcpy(&m_servers.x_def_domain[0], pTok[0], len);
    m_servers.x_def_domain[len] = '\0';
  }

  return nullptr;
}

/* --------------------------------------------------------------
   SplitDNSRecord::ProcessDomainSrchList()
   -------------------------------------------------------------- */
const char *
SplitDNSRecord::ProcessDomainSrchList(char *val)
{
  Tokenizer pTok(",; \t\r");
  int       numTok;
  int       sz  = 0;
  char     *pSp = nullptr;

  numTok = pTok.Initialize(val, SHARE_TOKS);

  if (numTok == 0) {
    return "No servers specified";
  }

  pSp = &m_servers.x_domain_srch_list[0];

  for (int i = 0; i < numTok; i++) {
    const char *current = pTok[i];
    int         cnt = sz += strlen(current);

    if (MAXDNAME - 1 < sz) {
      break;
    }

    memcpy(pSp, current, cnt);
    pSp += (cnt + 1);
  }

  m_domain_srch_list = numTok;
  return nullptr;
}

/* --------------------------------------------------------------
   SplitDNSRecord::Init()

   matcher_line* line_info - contains parsed label/value pairs
   of the current split.config line
   -------------------------------------------------------------- */
Result
SplitDNSRecord::Init(matcher_line *line_info)
{
  const char *errPtr = nullptr;

  this->line_num = line_info->line_num;
  for (int i = 0; i < MATCHER_MAX_TOKENS; i++) {
    char *label = line_info->line[0][i];
    char *val   = line_info->line[1][i];

    if (label == nullptr) {
      continue;
    }

    if (strcasecmp(label, "def_domain") == 0) {
      if (nullptr != (errPtr = ProcessDefDomain(val))) {
        return Result::failure("%s %s at line %d", modulePrefix, errPtr, line_num);
      }
      line_info->line[0][i] = nullptr;
      line_info->num_el--;
      continue;
    }

    if (strcasecmp(label, "search_list") == 0) {
      if (nullptr != (errPtr = ProcessDomainSrchList(val))) {
        return Result::failure("%s %s at line %d", modulePrefix, errPtr, line_num);
      }
      line_info->line[0][i] = nullptr;
      line_info->num_el--;
      continue;
    }

    if (strcasecmp(label, "named") == 0) {
      if (nullptr != (errPtr = ProcessDNSHosts(val))) {
        return Result::failure("%s %s at line %d", modulePrefix, errPtr, line_num);
      }
      line_info->line[0][i] = nullptr;
      line_info->num_el--;
      continue;
    }
  }

  if (!ats_is_ip(&m_servers.x_server_ip[0].sa)) {
    return Result::failure("%s No server specified in %s at line %d", modulePrefix, ts::filename::SPLITDNS, line_num);
  }

  DNSHandler   *dnsH = new DNSHandler;
  ink_res_state res  = new ts_imp_res_state;

  memset(res, 0, sizeof(ts_imp_res_state));
  if ((-1 == ink_res_init(res, m_servers.x_server_ip, m_dnsSrvr_cnt, dns_search, m_servers.x_def_domain,
                          m_servers.x_domain_srch_list, nullptr))) {
    char ab[INET6_ADDRPORTSTRLEN];
    return Result::failure("Failed to build res record for the servers %s ...",
                           ats_ip_ntop(&m_servers.x_server_ip[0].sa, ab, sizeof ab));
  }

  dnsH->m_res = res;
  dnsH->mutex = SplitDNSConfig::dnsHandler_mutex;
  ats_ip_invalidate(&dnsH->ip.sa); // Mark to use default DNS.

  m_servers.x_dnsH = dnsH;

  SET_CONTINUATION_HANDLER(dnsH, &DNSHandler::startEvent_sdns);
  eventProcessor.thread_group[ET_DNS]._thread[0]->schedule_imm(dnsH);

  /* -----------------------------------------------------
     Process any modifiers to the directive, if they exist
     ----------------------------------------------------- */
  if (line_info->num_el > 0) {
    const char *tmp = ProcessModifiers(line_info);
    if (tmp != nullptr) {
      return Result::failure("%s %s at line %d in %s", modulePrefix, tmp, line_num, ts::filename::SPLITDNS);
    }
  }

  return Result::ok();
}

/* --------------------------------------------------------------
    SplitDNSRecord::UpdateMatch()
   -------------------------------------------------------------- */
void
SplitDNSRecord::UpdateMatch(SplitDNSResult *result, RequestData * /* rdata ATS_UNUSED */)
{
  int last_number = result->m_line_number;

  if ((last_number < 0) || (last_number > this->line_num)) {
    result->m_rec         = this;
    result->m_line_number = this->line_num;

    Dbg(dbg_ctl_splitdns_config, "Matched with %p dns node from line %d", this, this->line_num);
  }
}

/* --------------------------------------------------------------
    SplitDNSRecord::Print()
   -------------------------------------------------------------- */
void
SplitDNSRecord::Print() const
{
  for (int i = 0; i < m_dnsSrvr_cnt; i++) {
    char ab[INET6_ADDRPORTSTRLEN];
    Dbg(dbg_ctl_splitdns_config, " %s", ats_ip_ntop(&m_servers.x_server_ip[i].sa, ab, sizeof ab));
  }
}

void
ink_split_dns_init(ts::ModuleVersion v)
{
  static int init_called = 0;

  ink_release_assert(v.check(SPLITDNS_MODULE_INTERNAL_VERSION));
  if (init_called) {
    return;
  }

  init_called = 1;
}
