/** @file

  User agent control by static IP address.

  This enables specifying the set of methods usable by a user agent based on the remove IP address
  for a user agent connection.

  @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.
 */

#include <string_view>

#include "proxy/IPAllow.h"
#include "records/RecCore.h"
#include "swoc/Errata.h"
#include "swoc/TextView.h"
#include "tscore/Filenames.h"
#include "tscore/ink_memory.h"
#include "tsutil/ts_errata.h"

#include "swoc/Vectray.h"
#include "swoc/BufferWriter.h"
#include "swoc/bwf_ex.h"
#include "swoc/bwf_ip.h"

#include "tsutil/YamlCfg.h"

using swoc::TextView;

using swoc::BufferWriter;
using swoc::bwf::Spec;

namespace
{
DbgCtl dbg_ctl_ip_allow("ip_allow");
}

namespace swoc
{
BufferWriter &
bwformat(BufferWriter &w, Spec const & /* spec ATS_UNUSED */, IpAllow const *obj)
{
  return w.print("{}[{}]", obj->MODULE_NAME, obj->get_config_file().c_str());
}

} // namespace swoc

enum AclOp {
  ACL_OP_ALLOW, ///< Allow access.
  ACL_OP_DENY,  ///< Deny access.
};

const IpAllow::Record IpAllow::ALLOW_ALL_RECORD(ALL_METHOD_MASK);
const IpAllow::ACL    IpAllow::DENY_ALL_ACL;

size_t  IpAllow::configid       = 0;
bool    IpAllow::accept_check_p = true; // initializing global flag for fast deny
uint8_t IpAllow::subjects[Subject::MAX_SUBJECTS];

static ConfigUpdateHandler<IpAllow> *ipAllowUpdate;

//
//   Begin API functions
//
swoc::TextView
IpAllow::localize(swoc::TextView src)
{
  auto span = _arena.alloc(src.size() + 1).rebind<char>(); // always make a C-str if copying.
  memcpy(span.data(), src.data(), src.size());
  span[src.size()] = '\0';
  return span.remove_suffix(1); // don't put the extra terminating nul in the view.
}

void
IpAllow::startup()
{
  // Should not have been initialized before
  ink_assert(IpAllow::configid == 0);

  ipAllowUpdate = new ConfigUpdateHandler<IpAllow>();
  ipAllowUpdate->attach("proxy.config.cache.ip_allow.filename");
  ipAllowUpdate->attach("proxy.config.cache.ip_categories.filename");

  reconfigure();

  ConfigInfo *config = configProcessor.get(configid);
  if (config == nullptr) {
    configid = configProcessor.set(
      configid, new self_type("proxy.config.cache.ip_allow.filename", "proxy.config.cache.ip_categories.filename"));
    Warning("%s not loaded; All IP Addresses will be blocked.", ts::filename::IP_ALLOW);
  }
}

void
IpAllow::reconfigure()
{
  self_type *new_table;

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

  new_table = new self_type("proxy.config.cache.ip_allow.filename", "proxy.config.cache.ip_categories.filename");
  // IP rules need categories, so load them first (if they exist).
  if (auto errata = new_table->BuildCategories(); !errata.is_ok()) {
    std::string text;
    swoc::bwprint(text, "{} failed to load\n{}", new_table->ip_categories_config_file, errata);
    Error("%s", text.c_str());
    delete new_table;
    return;
  }
  if (auto errata = new_table->BuildTable(); !errata.is_ok()) {
    std::string text;
    swoc::bwprint(text, "{} failed to load\n{}", ts::filename::IP_ALLOW, errata);
    if (errata.severity() <= ERRATA_ERROR) {
      Error("%s", text.c_str());
    } else {
      Fatal("%s", text.c_str());
    }
    delete new_table;
    return;
  }
  configid = configProcessor.set(configid, new_table);
  Note("%s finished loading", ts::filename::IP_ALLOW);
}

IpAllow *
IpAllow::acquire()
{
  return static_cast<IpAllow *>(configProcessor.get(configid));
}

void
IpAllow::release(IpAllow *config)
{
  configProcessor.release(configid, config);
}

void
IpAllow::release()
{
  configProcessor.release(configid, this);
}

bool
IpAllow::ip_category_contains_addr(std::string const &category, swoc::IPAddr const &addr)
{
  self_type *self = acquire();
  auto const spot = self->ip_category_map.find(category);
  if (spot == self->ip_category_map.end()) {
    return false;
  }
  auto const &space = spot->second;
  bool const  found = space.find(addr) != space.end();
  self->release();
  return found;
}

IpAllow::ACL
IpAllow::match(swoc::IPAddr const &addr, match_key_t key)
{
  self_type    *self   = acquire();
  Record const *record = nullptr;
  if (SRC_ADDR == key) {
    if (auto spot = self->_src_map.find(addr); spot != self->_src_map.end()) {
      auto r = std::get<1>(*spot);
      // Special case - if checking in accept is enabled and the record is a deny all,
      // then return a missing record instead to force an immediate deny. Otherwise it's delayed
      // until after remap, to allow remap rules to tweak the result.
      if (!(accept_check_p && r->_method_mask == 0 && r->_nonstandard_methods.empty())) {
        record = r;
      }
    }
  } else if (auto spot = self->_dst_map.find(addr); spot != self->_dst_map.end()) {
    record = std::get<1>(*spot);
  }

  if (record == nullptr) {
    self->release(); // no record, don't keep a reference to the config.
    return {};
  }

  return ACL{record, self}; // Note this keeps the config in memory.
}

//
//   End API functions
//

IpAllow::IpAllow(const char *ip_allow_config_var, const char *ip_categories_config_var)
  : ip_allow_config_file(ats_scoped_str(RecConfigReadConfigPath(ip_allow_config_var)).get())
{
  int matching_policy = 0;
  REC_ReadConfigInteger(matching_policy, "proxy.config.url_remap.acl_behavior_policy");
  if (matching_policy == 0) {
    this->_is_legacy_action_policy = true;
  } else {
    this->_is_legacy_action_policy = false;
  }
  std::string const path = RecConfigReadConfigPath(ip_categories_config_var);
  if (!path.empty()) {
    ip_categories_config_file = ats_scoped_str(path).get();
  }

  RecString subjects_char;
  REC_ReadConfigStringAlloc(subjects_char, "proxy.config.acl.subjects");
  std::string_view            subjects_sv{subjects_char};
  int                         i = 0;
  std::string_view::size_type s, e;
  for (s = 0, e = 0; s < subjects_sv.size() && e != subjects_sv.npos; s = e + 1) {
    e                           = subjects_sv.find(",", s);
    std::string_view subject_sv = subjects_sv.substr(s, e);
    if (i >= MAX_SUBJECTS) {
      Error("Too many ACL subjects were provided");
    }
    if (subject_sv == "PEER") {
      subjects[i] = Subject::PEER;
      ++i;
    } else if (subject_sv == "PROXY") {
      subjects[i] = Subject::PROXY;
      ++i;
    } else {
      Dbg(dbg_ctl_ip_allow, "Unknown subject %.*s was ignored", static_cast<int>(subject_sv.length()), subject_sv.data());
    }
  }
  if (i < Subject::MAX_SUBJECTS) {
    subjects[i] = Subject::MAX_SUBJECTS;
  }
  ats_free(subjects_char);
}

BufferWriter &
bwformat(BufferWriter &w, Spec const & /* spec ATS_UNUSED */, IpAllow::IpMap const &map)
{
  w.print("{} entries", map.count());
  for (auto const &spot : map) {
    auto const *r = std::get<1>(spot);
    w.print("\n  Line {}: {} methods=", r->_src_line, std::get<0>(spot));
    uint32_t mask = IpAllow::ALL_METHOD_MASK & r->_method_mask;
    if (IpAllow::ALL_METHOD_MASK == mask) {
      w.write("ALL");
    } else if (0 == mask) {
      w.write("NONE");
    } else {
      bool     leader    = false; // need leading vbar?
      uint32_t test_mask = 1;     // mask for current method.
      for (int i = 0; i < HTTP_WKSIDX_METHODS_CNT; ++i, test_mask <<= 1) {
        if (mask & test_mask) {
          w.print("{}{}", swoc::bwf::If(leader, "|"), hdrtoken_index_to_wks(i + HTTP_WKSIDX_CONNECT));
          leader = true;
        }
      }
    }

    if (!r->_nonstandard_methods.empty()) {
      w.print(" {}=", r->_deny_nonstandard_methods ? IpAllow::YAML_VALUE_ACTION_ALLOW : IpAllow::YAML_VALUE_ACTION_DENY);
      bool leader = false; // need leading vbar?
      for (auto const &name : r->_nonstandard_methods) {
        w.print("{}{}", swoc::bwf::If(leader, "|"), name);
        leader = true;
      }
    }
  }
  return w;
}

void
IpAllow::DebugMap(const IpMap &map) const
{
  std::string out;
  out.resize(8192);
  swoc::bwprint(out, "{}", map);
  Dbg(dbg_ctl_ip_allow, "%s", out.c_str());
}

void
IpAllow::Print() const
{
  Dbg(dbg_ctl_ip_allow, "Printing src map");
  DebugMap(_src_map);
  Dbg(dbg_ctl_ip_allow, "Printing dest map");
  DebugMap(_dst_map);
}

swoc::Errata
IpAllow::BuildTable()
{
  // Table should be empty
  ink_assert(_src_map.count() == 0 && _dst_map.count() == 0);

  std::error_code ec;
  std::string     content{swoc::file::load(ip_allow_config_file, ec)};
  swoc::Errata    errata;
  if (ec.value() == 0) {
    try {
      errata = this->YAMLBuildTable(content);
    } catch (std::exception &ex) {
      return swoc::Errata(ec, ERRATA_ERROR, "{} - Invalid config: {}", this, ex.what());
    }
    if (!errata.is_ok()) {
      errata.note("While parsing config file");
      return errata;
    }

    if (_src_map.count() == 0 && _dst_map.count() == 0) {
      return swoc::Errata(ERRATA_ERROR, "{} - No entries found. All IP Addresses will be blocked", this);
    }

    if (dbg_ctl_ip_allow.on()) {
      Print();
    }
  } else {
    return swoc::Errata(ERRATA_ERROR, "{} Failed to load {}. All IP Addresses will be blocked", this, ec);
  }
  return {};
}

swoc::Errata
IpAllow::YAMLLoadMethod(const YAML::Node &node, Record &rec)
{
  swoc::TextView                   value{node.Scalar()};
  swoc::Vectray<swoc::TextView, 8> names;
  // Process a single token. Required to deal with the variable number of tokens.
  auto parse_method = [&](swoc::TextView value) -> void {
    if (0 == strcasecmp(value, YAML_VALUE_METHODS_ALL)) {
      rec._method_mask = ALL_METHOD_MASK;
    } else {
      int method_idx = hdrtoken_tokenize(value.data(), value.size());
      if (HTTP_WKSIDX_CONNECT <= method_idx && method_idx < HTTP_WKSIDX_CONNECT + HTTP_WKSIDX_METHODS_CNT) {
        rec._method_mask |= ACL::MethodIdxToMask(method_idx);
      } else {
        names.push_back(value);
        Dbg(dbg_ctl_ip_allow, "Found nonstandard method '%.*s' at line %d", int(value.size()), value.data(), node.Mark().line);
      }
    }
  };

  if (node.IsScalar()) {
    parse_method(swoc::TextView(node.Scalar()));
  } else if (node.IsSequence()) {
    for (auto const &elt : node) {
      if (elt.IsScalar()) {
        parse_method(swoc::TextView(elt.Scalar()));
        if (rec._method_mask == ALL_METHOD_MASK) {
          break; // we're done here, nothing else matters.
        }
      } else {
        return swoc::Errata(ERRATA_ERROR, "{} {} - item ignored, all values for '{}' must be strings.", this, elt.Mark(),
                            YAML_TAG_METHODS);
      }
    }
  } else {
    return swoc::Errata(ERRATA_ERROR, "{} {} - item ignored, value for '{}' must be a single string or a list of strings.", this,
                        node.Mark(), YAML_TAG_METHODS);
  }

  // copy over to local memory if it's not all methods and there are non-standard ones.
  if (rec._method_mask != ALL_METHOD_MASK && !names.empty()) {
    rec._nonstandard_methods = _arena.alloc_span<swoc::TextView>(names.size());
    for (unsigned idx = 0; idx < names.size(); ++idx) {
      rec._nonstandard_methods[idx] = this->localize(names[idx]);
    }
  }
  return {};
}

swoc::Errata
IpAllow::YAMLLoadIPAddrRange(const YAML::Node &node, IpMap *map, IpAllow::Record const *record)
{
  if (!node.IsScalar()) {
    return swoc::Errata(ERRATA_ERROR, "{} Expected IP address range at {}, found non-literal.", this, node.Mark());
  }

  swoc::TextView ip_range(node.Scalar());
  if (swoc::IPRange r; r.load(ip_range)) {
    map->fill(r, record);
  } else {
    return swoc::Errata(ERRATA_ERROR, "{} {} - '{}' is not a valid range.", this, node.Mark(), node.Scalar());
  }
  return {};
}

swoc::Errata
IpAllow::YAMLLoadIPCategory(const YAML::Node &node, IpMap *map, IpAllow::Record const *record)
{
  if (!node.IsScalar()) {
    return swoc::Errata(ERRATA_ERROR, "{} Expected IP address category at {}, found non-literal.", this, node.Mark());
  }
  std::string const &category(node.Scalar());
  if (auto spot = ip_category_map.find(category); spot != ip_category_map.end()) {
    for (auto const &range : spot->second) {
      map->fill(range.range_view(), record);
    }
  } else {
    return swoc::Errata(ERRATA_ERROR, "{} {} - '{}' is not category with a defined range.", this, node.Mark(), category);
  }
  return {};
}

swoc::Errata
IpAllow::YAMLLoadEntry(const YAML::Node &entry)
{
  AclOp      op = ACL_OP_DENY; // "shut up", I explained to the compiler.
  YAML::Node node;
  auto       record = _arena.make<Record>();
  IpMap     *map    = nullptr; // src or dst map.

  if (!entry.IsMap()) {
    return swoc::Errata(ERRATA_ERROR, "{} {} - ACL items must be maps.", this, entry.Mark());
  }

  if (YAML::Node apply_node{entry[YAML_TAG_APPLY]}; apply_node) {
    if (apply_node.IsScalar()) {
      swoc::TextView value{apply_node.Scalar()};
      if (0 == strcasecmp(value, YAML_VALUE_APPLY_IN)) {
        map = &_src_map;
      } else if (0 == strcasecmp(value, YAML_VALUE_APPLY_OUT)) {
        map = &_dst_map;
      } else {
        return swoc::Errata(ERRATA_ERROR, R"("{}" value at {} must be "{}" or "{}")", YAML_TAG_APPLY, entry.Mark(),
                            YAML_VALUE_APPLY_IN, YAML_VALUE_APPLY_OUT);
      }
    } else {
      return swoc::Errata(ERRATA_ERROR, R"("{}" value at {} must be a scalar, "{}" or "{}")", YAML_TAG_APPLY, entry.Mark(),
                          YAML_VALUE_APPLY_IN, YAML_VALUE_APPLY_OUT);
    }
  } else {
    return swoc::Errata(ERRATA_ERROR, R"(Object at {} must have a "{}" key.)", entry.Mark(), YAML_TAG_APPLY);
  }

  if (node = entry[YAML_TAG_ACTION]; node) {
    if (node.IsScalar()) {
      swoc::TextView value(node.Scalar());
      if (!this->_is_legacy_action_policy &&
          (value == YAML_VALUE_ACTION_ALLOW_OLD_NAME || value == YAML_VALUE_ACTION_DENY_OLD_NAME)) {
        return swoc::Errata(
          ERRATA_FATAL, R"(Legacy action name of "{}" detected at {}. Use "set_allow" or "set_deny" instead of "allow" or "deny".)",
          value, entry.Mark());
      }
      if (value == YAML_VALUE_ACTION_ALLOW || value == YAML_VALUE_ACTION_ALLOW_OLD_NAME) {
        op = ACL_OP_ALLOW;
      } else if (value == YAML_VALUE_ACTION_DENY || value == YAML_VALUE_ACTION_DENY_OLD_NAME) {
        op = ACL_OP_DENY;
      } else {
        return swoc::Errata(ERRATA_ERROR, "{} {} - item ignored, value for tag '{}' must be '{}' or '{}'", this, node.Mark(),
                            YAML_TAG_ACTION, YAML_VALUE_ACTION_ALLOW, YAML_VALUE_ACTION_DENY);
      }
    } else {
      return swoc::Errata(ERRATA_ERROR, "{} {} - item ignored, value for tag '{}' must be a string", this, node.Mark(),
                          YAML_TAG_ACTION);
    }
  } else {
    return swoc::Errata(ERRATA_ERROR, "{} {} - item ignored, required '{}' key not found.", this, entry.Mark(), YAML_TAG_ACTION);
  }

  if (entry[YAML_TAG_IP_ADDRS] && entry[YAML_TAG_IP_CATEGORIES]) {
    return swoc::Errata(ERRATA_ERROR, "{} {} - '{}' and '{}' cannot both be used in the same rule.", this, entry.Mark(),
                        YAML_TAG_IP_ADDRS, YAML_TAG_IP_CATEGORIES);
  }
  if (YAML::Node addr_node = entry[YAML_TAG_IP_ADDRS]; addr_node) {
    bool marked_p = false;
    if (addr_node.IsSequence()) {
      for (auto const &n : addr_node) {
        if (auto errata = this->YAMLLoadIPAddrRange(n, map, record); errata.is_ok()) {
          marked_p = true;
        } else {
          errata.note(R"(In record at {})", entry.Mark());
          return errata;
        }
      }
    } else {
      if (auto errata = this->YAMLLoadIPAddrRange(addr_node, map, record); errata.is_ok()) {
        marked_p = true;
      } else {
        errata.note(R"(In record at {})", entry.Mark());
        return errata;
      }
    }
    if (!marked_p) {
      return swoc::Errata(ERRATA_ERROR, "No valid addresses for rule at {}", node.Mark());
    }
  } else if (YAML::Node category_node = entry[YAML_TAG_IP_CATEGORIES]; category_node) {
    bool marked_p = false;
    if (category_node.IsSequence()) {
      for (auto const &n : category_node) {
        if (auto errata = this->YAMLLoadIPCategory(n, map, record); errata.is_ok()) {
          marked_p = true;
        } else {
          errata.note(R"(In record at {})", entry.Mark());
          return errata;
        }
      }
    } else {
      if (auto errata = this->YAMLLoadIPCategory(category_node, map, record); errata.is_ok()) {
        marked_p = true;
      } else {
        errata.note(R"(In record at {})", entry.Mark());
        return errata;
      }
    }
    if (!marked_p) {
      return swoc::Errata(ERRATA_ERROR, "No valid IP category for rule at {}", node.Mark());
    }
  } else {
    return swoc::Errata(ERRATA_ERROR, "{} {} - item ignored, required '{}' or '{}' key not found.", this, entry.Mark(),
                        YAML_TAG_IP_ADDRS, YAML_TAG_IP_CATEGORIES);
  }

  if (auto methodNode = entry[YAML_TAG_METHODS]) {
    // methods are specified.
    if (auto errata = this->YAMLLoadMethod(methodNode, *record); !errata.is_ok()) {
      return errata;
    }
  } else {
    record->_method_mask = ALL_METHOD_MASK;
  }

  if (op == ACL_OP_DENY) {
    record->_method_mask              = ALL_METHOD_MASK & ~record->_method_mask;
    record->_deny_nonstandard_methods = true;
  }

  record->_src_line = entry.Mark().line;
  return {};
}

swoc::Errata
IpAllow::YAMLBuildTable(std::string const &content)
{
  YAML::Node root{YAML::Load(content)};
  if (!root.IsMap()) {
    return swoc::Errata("{} - top level object was not a map. All IP Addresses will be blocked", this);
  }

  // IP categories are optional. Load them if specified. Note that the rules,
  // if they use categories, depend upon the categories being defined. So the
  // categories have to be processed first before the rules are.
  YAML::Node categories{root[YAML_TAG_CATEGORY_ROOT.data()]};
  if (auto errata = this->YAMLLoadCategoryRoot(categories); !errata.is_ok()) {
    return errata;
  }

  // Now load the IPAllow rules.
  YAML::Node rules{root[YAML_TAG_ROOT.data()]};
  if (!rules) {
    return swoc::Errata("{} - root tag '{}' not found. All IP Addresses will be blocked", this, YAML_TAG_ROOT);
  } else if (rules.IsSequence()) {
    for (auto const &entry : rules) {
      if (auto errata = this->YAMLLoadEntry(entry); !errata.is_ok()) {
        return errata;
      }
    }
  } else if (rules.IsMap()) {
    return this->YAMLLoadEntry(rules); // singleton, just load it.
  } else {
    return swoc::Errata("{} - root tag '{}' is not an map or sequence. All IP Addresses will be blocked", this, YAML_TAG_ROOT);
  }
  return {};
}

swoc::Errata
IpAllow::BuildCategories()
{
  std::error_code ec;
  if (ip_categories_config_file.empty()) {
    return {};
  }

  Note("%s loading categores file %s ...", ts::filename::IP_ALLOW, ip_categories_config_file.c_str());
  std::string  content{swoc::file::load(ip_categories_config_file, ec)};
  swoc::Errata errata;
  if (ec.value() == 0) {
    try {
      errata = this->YAMLBuildCategories(content);
    } catch (std::exception &ex) {
      return swoc::Errata(ec, ERRATA_ERROR, "{} - Invalid IP Categories {} content: {}", this, ip_categories_config_file,
                          ex.what());
    }
    if (!errata.is_ok()) {
      errata.note("While parsing ip categories file: {}", ip_categories_config_file);
      return errata;
    }
  } else {
    return swoc::Errata(ERRATA_ERROR, "{} Failed to load {}", this, ec);
  }
  Note("%s done loading categores file %s ...", ts::filename::IP_ALLOW, ip_categories_config_file.c_str());
  return {};
}

swoc::Errata
IpAllow::YAMLBuildCategories(std::string const &content)
{
  YAML::Node root{YAML::Load(content)};
  YAML::Node categories{root[YAML_TAG_CATEGORY_ROOT.data()]};
  if (auto errata = this->YAMLLoadCategoryRoot(categories); !errata.is_ok()) {
    return errata;
  }
  return {};
}

swoc::Errata
IpAllow::YAMLLoadCategoryRoot(const YAML::Node &categories)
{
  if (categories) {
    if (!categories.IsSequence()) {
      return swoc::Errata("{} - '{}' tag must be a sequence of maps. All IP Addresses will be blocked", this,
                          YAML_TAG_CATEGORY_ROOT);
    }
    for (auto const &category : categories) {
      if (!category.IsMap()) {
        return swoc::Errata("{} - '{}' tag must be a sequence of maps. All IP Addresses will be blocked", this,
                            YAML_TAG_CATEGORY_ROOT);
      }
      if (auto errata = this->YAMLLoadCategoryDefinition(category); !errata.is_ok()) {
        return errata;
      }
    }
  }
  return {};
}

swoc::Errata
IpAllow::YAMLLoadCategoryDefinition(const YAML::Node &entry)
{
  /* Parse this into ip_category_map:
   *
   *   - name: <category name>
   *     ip_addrs:
   *       - <ip range>
   *       - <ip range>
   *       - <ip range>
   */
  if (!entry.IsMap()) {
    return swoc::Errata(ERRATA_ERROR, "{} {} - Category definition must be a map.", this, entry.Mark());
  }

  if (auto name_node = entry[YAML_TAG_CATEGORY_NAME]; name_node) {
    if (!name_node.IsScalar()) {
      return swoc::Errata(ERRATA_ERROR, "{} {} - Category name must be a string.", this, name_node.Mark());
    }
    std::string const &name(name_node.Scalar());
    if (auto ip_addrs_node = entry[YAML_TAG_CATEGORY_IP_ADDRS]; ip_addrs_node) {
      if (ip_addrs_node.IsSequence()) {
        for (auto const &ip_addr_node : ip_addrs_node) {
          if (auto errata = this->YAMLLoadCategoryIpRange(ip_addr_node, ip_category_map[name]); !errata.is_ok()) {
            errata.note(R"(In category definition at {})", entry.Mark());
            return errata;
          }
        }
      } else {
        if (auto errata = this->YAMLLoadCategoryIpRange(ip_addrs_node, ip_category_map[name]); !errata.is_ok()) {
          errata.note(R"(In category definition at {})", entry.Mark());
          return errata;
        }
      }
    } else { // No ip_addrs.
      return swoc::Errata(ERRATA_ERROR, "{} {} - IP Addresses must be specified.", this, entry.Mark());
    }
  } else { // No name
    return swoc::Errata(ERRATA_ERROR, "{} {} - Category name must be specified.", this, entry.Mark());
  }
  return {};
}

swoc::Errata
IpAllow::YAMLLoadCategoryIpRange(const YAML::Node &node, swoc::IPSpace<bool> &space)
{
  if (!node.IsScalar()) {
    return swoc::Errata(ERRATA_ERROR, "{} Expected IP address range for category at {}, found non-literal.", this, node.Mark());
  }

  swoc::TextView ip_range(node.Scalar());
  if (swoc::IPRange r; r.load(ip_range)) {
    space.fill(r, true);
  } else {
    return swoc::Errata(ERRATA_ERROR, "{} {} - '{}' is not a valid range.", this, node.Mark(), node.Scalar());
  }

  return {};
}
