LCOV - code coverage report
Current view: top level - src/evo - netinfo.cpp (source / functions) Hit Total Coverage
Test: total_coverage.info Lines: 339 376 90.2 %
Date: 2026-06-25 07:23:43 Functions: 66 81 81.5 %

          Line data    Source code
       1             : // Copyright (c) 2025 The Dash Core developers
       2             : // Distributed under the MIT software license, see the accompanying
       3             : // file COPYING or http://www.opensource.org/licenses/mit-license.php.
       4             : 
       5             : #include <evo/netinfo.h>
       6             : 
       7             : #include <chainparams.h>
       8             : #include <evo/providertx.h>
       9             : #include <netbase.h>
      10             : #include <span.h>
      11             : #include <util/check.h>
      12             : #include <util/string.h>
      13             : #include <util/system.h>
      14             : 
      15             : #include <univalue.h>
      16             : 
      17             : namespace {
      18             : static std::unique_ptr<const CChainParams> g_main_params{nullptr};
      19             : static std::once_flag g_main_params_flag;
      20             : 
      21             : /** Maximum length of a label in a domain per RFC 1035 */
      22             : static constexpr uint8_t DOMAIN_LABEL_MAX_LEN{63};
      23             : /** Maximum possible length of a ASCII FQDN */
      24             : static constexpr uint8_t DOMAIN_MAX_LEN{253};
      25             : /** Minimum length of a FQDN */
      26             : static constexpr uint8_t DOMAIN_MIN_LEN{3};
      27             : 
      28             : static constexpr std::string_view SAFE_CHARS_ALPHA{"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"};
      29             : static constexpr std::string_view SAFE_CHARS_IPV4{"1234567890."};
      30             : static constexpr std::string_view SAFE_CHARS_IPV4_6{"abcdefABCDEF1234567890.:[]"};
      31             : static constexpr std::string_view SAFE_CHARS_RFC1035{"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-"};
      32             : static constexpr std::array<std::string_view, 13> TLDS_BAD{
      33             :     // ICANN resolution 2018.02.04.12
      34             :     ".mail",
      35             :     // Infrastructure TLD
      36             :     ".arpa",
      37             :     // RFC 6761
      38             :     ".example", ".invalid", ".localhost", ".test",
      39             :     // RFC 6762
      40             :     ".local",
      41             :     // RFC 6762, Appendix G
      42             :     ".corp", ".home", ".internal", ".intranet", ".lan", ".private",
      43             : };
      44             : static constexpr std::array<std::string_view, 2> TLDS_PRIVACY{".i2p", ".onion"};
      45             : 
      46        1826 : bool MatchCharsFilter(std::string_view input, std::string_view filter)
      47             : {
      48       24300 :     return std::all_of(input.begin(), input.end(), [&filter](char c) { return filter.find(c) != std::string_view::npos; });
      49             : }
      50             : 
      51         194 : bool MatchSuffix(const std::string& str, Span<const std::string_view> list)
      52             : {
      53         194 :     if (str.empty()) return false;
      54        1216 :     for (const auto& suffix : list) {
      55        1066 :         if (suffix.size() > str.size()) continue;
      56        1066 :         if (std::string_view{str}.ends_with(suffix)) return true;
      57             :     }
      58         150 :     return false;
      59         194 : }
      60             : 
      61           6 : bool IsAllowedPlatformHTTPPort(uint16_t port)
      62             : {
      63           6 :     switch (port) {
      64             :     case 443:
      65           5 :         return true;
      66             :     }
      67           1 :     return false;
      68           6 : }
      69             : } // anonymous namespace
      70             : 
      71        8889 : bool IsNodeOnMainnet() { return Params().NetworkIDString() == CBaseChainParams::MAIN; }
      72             : 
      73        4764 : bool IsServiceDeprecatedRPCEnabled()
      74             : {
      75        4764 :     const auto args = gArgs.GetArgs("-deprecatedrpc");
      76        4764 :     return std::find(args.begin(), args.end(), "service") != args.end();
      77        4764 : }
      78             : 
      79        9385 : const CChainParams& MainParams()
      80             : {
      81       10005 :     std::call_once(g_main_params_flag, [&]() { g_main_params = CreateChainParams(::gArgs, CBaseChainParams::MAIN); });
      82        9385 :     return *Assert(g_main_params);
      83             : }
      84             : 
      85       16078 : UniValue ArrFromService(const CService& addr)
      86             : {
      87       16078 :     UniValue obj(UniValue::VARR);
      88       16078 :     obj.push_back(addr.ToStringAddrPort());
      89       16078 :     return obj;
      90       16078 : }
      91             : 
      92         463 : DomainPort::Status DomainPort::ValidateDomain(const std::string& addr)
      93             : {
      94         463 :     if (addr.length() > DOMAIN_MAX_LEN || addr.length() < DOMAIN_MIN_LEN) {
      95           2 :         return DomainPort::Status::BadLen;
      96             :     }
      97         461 :     if (!MatchCharsFilter(addr, SAFE_CHARS_RFC1035)) {
      98           2 :         return DomainPort::Status::BadChar;
      99             :     }
     100         459 :     if (addr.front() == '.' || addr.back() == '.') {
     101           2 :         return DomainPort::Status::BadCharPos;
     102             :     }
     103         457 :     std::vector<std::string> labels{SplitString(addr, '.')};
     104         457 :     if (labels.size() < 2) {
     105           2 :         return DomainPort::Status::BadDotless;
     106             :     }
     107        1788 :     for (const auto& label : labels) {
     108        1336 :         if (label.empty() || label.length() > DOMAIN_LABEL_MAX_LEN) {
     109           2 :             return DomainPort::Status::BadLabelLen;
     110             :         }
     111        1334 :         if (label.front() == '-' || label.back() == '-') {
     112           1 :             return DomainPort::Status::BadLabelCharPos;
     113             :         }
     114             :     }
     115         452 :     return DomainPort::Status::Success;
     116         463 : }
     117             : 
     118          39 : DomainPort::Status DomainPort::Set(const std::string& addr, const uint16_t port)
     119             : {
     120          39 :     if (port == 0) {
     121           1 :         return DomainPort::Status::BadPort;
     122             :     }
     123          38 :     const auto ret{ValidateDomain(addr)};
     124          38 :     if (ret == DomainPort::Status::Success) {
     125             :         // Convert to lowercase to avoid duplication by changing case (domains are case-insensitive)
     126          27 :         m_addr = ToLower(addr);
     127          27 :         m_port = port;
     128          27 :     }
     129          38 :     return ret;
     130          39 : }
     131             : 
     132         436 : DomainPort::Status DomainPort::Validate() const
     133             : {
     134         436 :     if (m_addr.empty() || m_addr != ToLower(m_addr)) {
     135          11 :         return DomainPort::Status::Malformed;
     136             :     }
     137         425 :     if (m_port == 0) {
     138           0 :         return DomainPort::Status::BadPort;
     139             :     }
     140         425 :     return ValidateDomain(m_addr);
     141         436 : }
     142             : 
     143      191428 : bool NetInfoEntry::operator==(const NetInfoEntry& rhs) const
     144             : {
     145      191428 :     if (m_type != rhs.m_type) return false;
     146       27932 :     return std::visit(
     147       27932 :         [](auto&& lhs, auto&& rhs) -> bool {
     148             :             if constexpr (std::is_same_v<decltype(lhs), decltype(rhs)>) {
     149       27932 :                 return lhs == rhs;
     150             :             }
     151           0 :             return false;
     152             :         },
     153       27932 :         m_data, rhs.m_data);
     154      191428 : }
     155             : 
     156        4689 : bool NetInfoEntry::operator<(const NetInfoEntry& rhs) const
     157             : {
     158        4689 :     if (m_type != rhs.m_type) return m_type < rhs.m_type;
     159        4412 :     return std::visit(
     160        4412 :         [](auto&& lhs, auto&& rhs) -> bool {
     161             :             using T1 = std::decay_t<decltype(lhs)>;
     162             :             using T2 = std::decay_t<decltype(rhs)>;
     163             :             if constexpr (std::is_same_v<T1, T2>) {
     164             :                 // Both the same type, compare as usual
     165        4412 :                 return lhs < rhs;
     166             :             } else if constexpr ((std::is_same_v<T1, CService> || std::is_same_v<T1, DomainPort>) &&
     167             :                                  (std::is_same_v<T2, CService> || std::is_same_v<T2, DomainPort>)) {
     168             :                 // Differing types but both implement ToStringAddrPort(), lexicographical compare strings
     169           0 :                 return lhs.ToStringAddrPort() < rhs.ToStringAddrPort();
     170             :             }
     171             :             // If lhs is monostate, it less than rhs; otherwise rhs is greater
     172           0 :             return std::is_same_v<T1, std::monostate>;
     173           0 :         },
     174        4412 :         m_data, rhs.m_data);
     175        4689 : }
     176             : 
     177      356194 : std::optional<CService> NetInfoEntry::GetAddrPort() const
     178             : {
     179      356194 :     if (const auto* data_ptr{std::get_if<CService>(&m_data)}; m_type == NetInfoType::Service && data_ptr) {
     180             :         ASSERT_IF_DEBUG(data_ptr->IsValid());
     181      350953 :         return *data_ptr;
     182             :     }
     183        5241 :     return std::nullopt;
     184      356194 : }
     185             : 
     186         385 : std::optional<DomainPort> NetInfoEntry::GetDomainPort() const
     187             : {
     188         385 :     if (const auto* data_ptr{std::get_if<DomainPort>(&m_data)}; m_type == NetInfoType::Domain && data_ptr) {
     189             :         ASSERT_IF_DEBUG(data_ptr->IsValid());
     190         136 :         return *data_ptr;
     191             :     }
     192         249 :     return std::nullopt;
     193         385 : }
     194             : 
     195          34 : uint16_t NetInfoEntry::GetPort() const
     196             : {
     197          34 :     return std::visit(
     198          34 :         [](auto&& input) -> uint16_t {
     199             :             using T1 = std::decay_t<decltype(input)>;
     200             :             if constexpr (std::is_same_v<T1, CService> || std::is_same_v<T1, DomainPort>) {
     201          33 :                 return input.GetPort();
     202             :             }
     203           1 :             return 0;
     204             :         },
     205          34 :         m_data);
     206             : }
     207             : 
     208             : // NetInfoEntry is a dumb object that doesn't enforce validation rules, that is the responsibility of
     209             : // types that utilize NetInfoEntry (MnNetInfo and others). IsTriviallyValid() is there to check if a
     210             : // NetInfoEntry object is properly constructed.
     211       16452 : bool NetInfoEntry::IsTriviallyValid() const
     212             : {
     213       16452 :     if (m_type == NetInfoType::Invalid) return false;
     214       16433 :     return std::visit(
     215       32866 :         [this](auto&& input) -> bool {
     216             :             using T1 = std::decay_t<decltype(input)>;
     217             :             static_assert(std::is_same_v<T1, std::monostate> || std::is_same_v<T1, CService> || std::is_same_v<T1, DomainPort>,
     218             :                           "Unexpected type");
     219             :             if constexpr (std::is_same_v<T1, std::monostate>) {
     220             :                 // Empty underlying data isn't a valid entry
     221           0 :                 return false;
     222             :             } else if constexpr (std::is_same_v<T1, CService>) {
     223             :                 // Type code should be truthful as it decides what underlying type is used when (de)serializing
     224       16329 :                 if (m_type != NetInfoType::Service) return false;
     225             :                 // Underlying data must meet surface-level validity checks for its type
     226       16329 :                 if (!input.IsValid()) return false;
     227             :             } else if constexpr (std::is_same_v<T1, DomainPort>) {
     228             :                 // Type code should be truthful as it decides what underlying type is used when (de)serializing
     229         104 :                 if (m_type != NetInfoType::Domain) return false;
     230             :                 // Underlying data should at least meet surface-level validity checks
     231         104 :                 if (!input.IsValid()) return false;
     232             :             }
     233       16433 :             return true;
     234       16433 :         },
     235       16433 :         m_data);
     236       16452 : }
     237             : 
     238        6736 : std::string NetInfoEntry::ToString() const
     239             : {
     240        6736 :     return std::visit(
     241        6736 :         [](auto&& input) -> std::string {
     242             :             using T1 = std::decay_t<decltype(input)>;
     243             :             if constexpr (std::is_same_v<T1, CService>) {
     244        6715 :                 return strprintf("CService(addr=%s, port=%u)", input.ToStringAddr(), input.GetPort());
     245             :             } else if constexpr (std::is_same_v<T1, DomainPort>) {
     246          20 :                 return strprintf("DomainPort(addr=%s, port=%u)", input.ToStringAddr(), input.GetPort());
     247             :             }
     248           1 :             return "[invalid entry]";
     249           0 :         },
     250        6736 :         m_data);
     251             : }
     252             : 
     253         774 : std::string NetInfoEntry::ToStringAddr() const
     254             : {
     255         774 :     return std::visit(
     256         774 :         [](auto&& input) -> std::string {
     257             :             using T1 = std::decay_t<decltype(input)>;
     258             :             if constexpr (std::is_same_v<T1, CService> || std::is_same_v<T1, DomainPort>) {
     259         773 :                 return input.ToStringAddr();
     260             :             }
     261           1 :             return "[invalid entry]";
     262             :         },
     263         774 :         m_data);
     264             : }
     265             : 
     266        7959 : std::string NetInfoEntry::ToStringAddrPort() const
     267             : {
     268        7959 :     return std::visit(
     269        7959 :         [](auto&& input) -> std::string {
     270             :             using T1 = std::decay_t<decltype(input)>;
     271             :             if constexpr (std::is_same_v<T1, CService> || std::is_same_v<T1, DomainPort>) {
     272        7958 :                 return input.ToStringAddrPort();
     273             :             }
     274           1 :             return "[invalid entry]";
     275             :         },
     276        7959 :         m_data);
     277             : }
     278             : 
     279        1108 : std::shared_ptr<NetInfoInterface> NetInfoInterface::MakeNetInfo(const uint16_t nVersion)
     280             : {
     281        1108 :     assert(nVersion > 0);
     282        1108 :     if (nVersion >= ProTxVersion::ExtAddr) {
     283          90 :         return std::make_shared<ExtNetInfo>();
     284             :     }
     285        1018 :     return std::make_shared<MnNetInfo>();
     286        1108 : }
     287             : 
     288        8395 : NetInfoStatus MnNetInfo::ValidateService(const CService& service)
     289             : {
     290        8395 :     if (!service.IsValid()) {
     291           1 :         return NetInfoStatus::BadAddress;
     292             :     }
     293        8394 :     if (!service.IsIPv4()) {
     294           0 :         return NetInfoStatus::BadType;
     295             :     }
     296        8394 :     if (Params().RequireRoutableExternalIP() && !service.IsRoutable()) {
     297           1 :         return NetInfoStatus::NotRoutable;
     298             :     }
     299             : 
     300        8393 :     if (IsNodeOnMainnet() != (service.GetPort() == MainParams().GetDefaultPort())) {
     301             :         // Must use mainnet port on mainnet.
     302             :         // Must NOT use mainnet port on other networks.
     303           4 :         return NetInfoStatus::BadPort;
     304             :     }
     305             : 
     306        8389 :     return NetInfoStatus::Success;
     307        8395 : }
     308             : 
     309         945 : NetInfoStatus MnNetInfo::AddEntry(const NetInfoPurpose purpose, const std::string& input)
     310             : {
     311         945 :     if (purpose != NetInfoPurpose::CORE_P2P || !IsEmpty()) {
     312          11 :         return NetInfoStatus::MaxLimit;
     313             :     }
     314             : 
     315         934 :     std::string addr;
     316         934 :     uint16_t port{Params().GetDefaultPort()};
     317         934 :     SplitHostPort(input, port, addr);
     318             :     // Contains invalid characters, unlikely to pass Lookup(), fast-fail
     319         934 :     if (!MatchCharsFilter(addr, SAFE_CHARS_IPV4)) {
     320           6 :         return NetInfoStatus::BadInput;
     321             :     }
     322             : 
     323        1854 :     if (auto service_opt{Lookup(addr, /*portDefault=*/port, /*fAllowLookup=*/false)}) {
     324         926 :         const auto ret{ValidateService(*service_opt)};
     325         926 :         if (ret == NetInfoStatus::Success) {
     326         920 :             m_addr = NetInfoEntry{*service_opt};
     327             :             ASSERT_IF_DEBUG(m_addr.GetAddrPort().has_value());
     328         920 :         }
     329         926 :         return ret;
     330             :     }
     331           2 :     return NetInfoStatus::BadInput;
     332         945 : }
     333             : 
     334      119971 : NetInfoList MnNetInfo::GetEntries(std::optional<NetInfoPurpose> purpose_opt) const
     335             : {
     336      119971 :     if (!IsEmpty() && (!purpose_opt || *purpose_opt == NetInfoPurpose::CORE_P2P)) {
     337             :         ASSERT_IF_DEBUG(m_addr.GetAddrPort().has_value());
     338      119496 :         return {m_addr};
     339             :     }
     340             :     // If MnNetInfo is empty or we are given an unexpected purpose code, we don't
     341             :     // expect any entries to show up, so we return a blank set instead.
     342         475 :     return NetInfoList{};
     343      119971 : }
     344             : 
     345      200009 : CService MnNetInfo::GetPrimary() const
     346             : {
     347      395771 :     if (const auto service_opt{m_addr.GetAddrPort()}) {
     348      195762 :         return *service_opt;
     349             :     }
     350        4247 :     return CService{};
     351      200009 : }
     352             : 
     353        7485 : NetInfoStatus MnNetInfo::Validate() const
     354             : {
     355        7485 :     if (!m_addr.IsTriviallyValid()) {
     356          16 :         return NetInfoStatus::Malformed;
     357             :     }
     358        7469 :     return ValidateService(GetPrimary());
     359        7485 : }
     360             : 
     361       14793 : UniValue MnNetInfo::ToJson(std::optional<NetInfoPurpose> purpose_opt) const
     362             : {
     363       14793 :     if (purpose_opt) {
     364           0 :         UniValue arr(UniValue::VARR);
     365           0 :         for (const auto& entry : GetEntries(purpose_opt)) {
     366           0 :             arr.push_back(entry.ToStringAddrPort());
     367             :         }
     368           0 :         return arr;
     369           0 :     }
     370             : 
     371       14793 :     UniValue ret{UniValue::VOBJ};
     372       14793 :     if (!IsEmpty()) {
     373       14750 :         ret.pushKV(PurposeToString(NetInfoPurpose::CORE_P2P).data(), ArrFromService(GetPrimary()));
     374       14750 :     }
     375       14793 :     return ret;
     376       29586 : }
     377             : 
     378        6514 : std::string MnNetInfo::ToString() const
     379             : {
     380       13018 :     return IsEmpty() ? "MnNetInfo()"
     381       13008 :                      : strprintf("MnNetInfo(NetInfo(purpose=%s, [%s]))", PurposeToString(NetInfoPurpose::CORE_P2P),
     382        6504 :                                  m_addr.ToString());
     383           0 : }
     384             : 
     385         124 : bool ExtNetInfo::HasAddrPortDuplicates() const
     386             : {
     387         124 :     std::set<NetInfoEntry> known{};
     388         715 :     for (const auto& entry : m_all_entries) {
     389         591 :         if (auto [_, inserted] = known.insert(entry); !inserted) {
     390           0 :             return true;
     391             :         }
     392             :     }
     393             :     ASSERT_IF_DEBUG(known.size() == m_all_entries.size());
     394         124 :     return false;
     395         124 : }
     396             : 
     397         275 : bool ExtNetInfo::IsAddrPortDuplicate(const NetInfoEntry& candidate) const
     398             : {
     399         550 :     return std::any_of(m_all_entries.begin(), m_all_entries.end(),
     400         765 :                        [&candidate](const auto& entry) { return candidate == entry; });
     401             : }
     402             : 
     403         350 : bool ExtNetInfo::HasAddrDuplicates(const NetInfoList& entries) const
     404             : {
     405         350 :     std::unordered_set<std::string> known{};
     406         941 :     for (const auto& entry : entries) {
     407         591 :         if (auto [_, inserted] = known.insert(entry.ToStringAddr()); !inserted) {
     408           0 :             return true;
     409             :         }
     410             :     }
     411             :     ASSERT_IF_DEBUG(known.size() == entries.size());
     412         350 :     return false;
     413         350 : }
     414             : 
     415          84 : bool ExtNetInfo::IsAddrDuplicate(const NetInfoEntry& candidate, const NetInfoList& entries) const
     416             : {
     417          84 :     const std::string& candidate_str{candidate.ToStringAddr()};
     418         168 :     return std::any_of(entries.begin(), entries.end(),
     419         181 :                        [&candidate_str](const auto& entry) { return candidate_str == entry.ToStringAddr(); });
     420          84 : }
     421             : 
     422         275 : NetInfoStatus ExtNetInfo::ProcessCandidate(const NetInfoPurpose purpose, const NetInfoEntry& candidate)
     423             : {
     424         275 :     assert(candidate.IsTriviallyValid());
     425             : 
     426         275 :     if (IsAddrPortDuplicate(candidate)) {
     427           6 :         return NetInfoStatus::Duplicate;
     428             :     }
     429         269 :     if (candidate.GetDomainPort().has_value() && purpose != NetInfoPurpose::PLATFORM_HTTPS) {
     430             :         // Domains only allowed for Platform HTTPS API
     431           2 :         return NetInfoStatus::BadInput;
     432             :     }
     433         267 :     if (auto it{m_data.find(purpose)}; it != m_data.end()) {
     434             :         // Existing entries list found, check limit
     435          85 :         auto& [_, entries] = *it;
     436          85 :         if (entries.size() >= MAX_ENTRIES_EXTNETINFO) {
     437           1 :             return NetInfoStatus::MaxLimit;
     438             :         }
     439         168 :         if (IsAddrDuplicate(candidate, entries)) {
     440           1 :             return NetInfoStatus::Duplicate;
     441             :         }
     442          83 :         entries.push_back(candidate);
     443          83 :     } else {
     444             :         // First entry for purpose code, create new entries list
     445         182 :         auto [_, status] = m_data.try_emplace(purpose, std::vector<NetInfoEntry>({candidate}));
     446         182 :         assert(status); // We did just check to see if our value already existed, try_emplace shouldn't fail
     447             :     }
     448             : 
     449             :     // Candidate successfully added, update cache
     450         265 :     m_all_entries.push_back(candidate);
     451         265 :     return NetInfoStatus::Success;
     452         275 : }
     453             : 
     454         813 : NetInfoStatus ExtNetInfo::ValidateService(const CService& service)
     455             : {
     456         813 :     if (!service.IsValid()) {
     457           1 :         return NetInfoStatus::BadAddress;
     458             :     }
     459         812 :     if (!service.IsCJDNS() && !service.IsI2P() && !service.IsIPv4() && !service.IsIPv6() && !service.IsTor()) {
     460           0 :         return NetInfoStatus::BadType;
     461             :     }
     462         812 :     if (Params().RequireRoutableExternalIP() && !service.IsRoutable()) {
     463           1 :         return NetInfoStatus::NotRoutable;
     464             :     }
     465         811 :     const uint16_t service_port{service.GetPort()};
     466         811 :     if (service.IsI2P()) {
     467          68 :         if (service_port != I2P_SAM31_PORT) {
     468             :             // I2P SAM 3.1 and earlier don't support arbitrary ports
     469           1 :             return NetInfoStatus::BadPort;
     470             :         }
     471          67 :     } else {
     472         743 :         if (service_port == 0 || IsBadPort(service_port)) {
     473           6 :             return NetInfoStatus::BadPort;
     474             :         }
     475             :     }
     476             : 
     477         804 :     return NetInfoStatus::Success;
     478         813 : }
     479             : 
     480          65 : NetInfoStatus ExtNetInfo::ValidateDomainPort(const DomainPort& domain)
     481             : {
     482          65 :     if (!domain.IsValid()) {
     483           0 :         return NetInfoStatus::BadInput;
     484             :     }
     485          65 :     const uint16_t domain_port{domain.GetPort()};
     486          65 :     if (domain_port == 0 || (IsBadPort(domain_port) && !IsAllowedPlatformHTTPPort(domain_port))) {
     487           1 :         return NetInfoStatus::BadPort;
     488             :     }
     489          64 :     const std::string& addr{domain.ToStringAddr()};
     490          64 :     if (MatchSuffix(addr, TLDS_BAD) || MatchSuffix(addr, TLDS_PRIVACY)) {
     491           1 :         return NetInfoStatus::BadInput;
     492             :     }
     493          64 :     if (const auto labels{SplitString(addr, '.')}; !MatchCharsFilter(labels.at(labels.size() - 1), SAFE_CHARS_ALPHA)) {
     494           1 :         return NetInfoStatus::BadInput;
     495             :     }
     496             : 
     497          62 :     return NetInfoStatus::Success;
     498          65 : }
     499             : 
     500         302 : NetInfoStatus ExtNetInfo::AddEntry(const NetInfoPurpose purpose, const std::string& input)
     501             : {
     502         302 :     if (!IsValidPurpose(purpose)) {
     503           1 :         return NetInfoStatus::MaxLimit;
     504             :     }
     505             : 
     506             :     // We don't allow assuming ports, so we set the default value to 0 so that if no port is specified
     507             :     // it uses a fallback value of 0, which will return a NetInfoStatus::BadPort
     508         301 :     std::string addr;
     509         301 :     uint16_t port{0};
     510         301 :     SplitHostPort(input, port, addr);
     511             : 
     512         301 :     if (!MatchCharsFilter(addr, SAFE_CHARS_IPV4_6)) {
     513          67 :         if (!MatchCharsFilter(addr, SAFE_CHARS_RFC1035)) {
     514             :             // Neither IP:port safe nor domain-safe, we can safely assume it's bad input
     515           0 :             return NetInfoStatus::BadInput;
     516             :         }
     517             : 
     518             :         // Not IP:port safe but domain safe
     519          67 :         if (MatchSuffix(addr, TLDS_PRIVACY)) {
     520             :             // Special domain, try storing it as CService
     521          43 :             CNetAddr netaddr;
     522          43 :             if (netaddr.SetSpecial(addr)) {
     523          43 :                 const CService service{netaddr, port};
     524          43 :                 const auto ret{ValidateService(service)};
     525          43 :                 if (ret == NetInfoStatus::Success) {
     526          41 :                     return ProcessCandidate(purpose, NetInfoEntry{service});
     527             :                 }
     528           2 :                 return ret; /* ValidateService() failed */
     529          43 :             }
     530          90 :         } else if (DomainPort domain; domain.Set(addr, port) == DomainPort::Status::Success) {
     531             :             // Regular domain
     532          23 :             const auto ret{ValidateDomainPort(domain)};
     533          23 :             if (ret == NetInfoStatus::Success) {
     534          20 :                 return ProcessCandidate(purpose, NetInfoEntry{domain});
     535             :             }
     536           3 :             return ret; /* ValidateDomainPort() failed */
     537             :         }
     538           1 :         return NetInfoStatus::BadInput; /* CNetAddr::SetSpecial() or DomainPort::Set() failed */
     539             :     }
     540             : 
     541             :     // IP:port safe, try to parse it as IP:port
     542         455 :     if (auto service_opt{Lookup(addr, /*portDefault=*/port, /*fAllowLookup=*/false)}) {
     543         221 :         const auto service{MaybeFlipIPv6toCJDNS(*service_opt)};
     544         221 :         const auto ret{ValidateService(service)};
     545         221 :         if (ret == NetInfoStatus::Success) {
     546         214 :             return ProcessCandidate(purpose, NetInfoEntry{service});
     547             :         }
     548           7 :         return ret; /* ValidateService() failed */
     549         221 :     }
     550          13 :     return NetInfoStatus::BadInput; /* Lookup() failed */
     551         302 : }
     552             : 
     553         833 : NetInfoList ExtNetInfo::GetEntries(std::optional<NetInfoPurpose> purpose_opt) const
     554             : {
     555         833 :     if (!purpose_opt) {
     556             :         // Without a purpose code specified, we're being requested for a flattened list of all
     557             :         // entries. ExtNetInfo is an append-only structure, so we can avoid re-calculating a
     558             :         // list of all entries by maintaining a small cache.
     559         801 :         return m_all_entries;
     560             :     }
     561          32 :     if (!IsValidPurpose(*purpose_opt)) return NetInfoList{};
     562          32 :     const auto& it{m_data.find(*purpose_opt)};
     563          32 :     return it != m_data.end() ? it->second : NetInfoList{};
     564         833 : }
     565             : 
     566          64 : CService ExtNetInfo::GetPrimary() const
     567             : {
     568          64 :     if (const auto& it{m_data.find(NetInfoPurpose::CORE_P2P)}; it != m_data.end()) {
     569          58 :         const auto& [_, entries] = *it;
     570             :         // If a purpose code is in the map, there should be at least one entry
     571             :         ASSERT_IF_DEBUG(!entries.empty());
     572          58 :         if (entries.size() >= 1) {
     573         116 :             if (const auto& service_opt{entries[0].GetAddrPort()}) {
     574          58 :                 return *service_opt;
     575             :             }
     576           0 :         }
     577           0 :     }
     578           6 :     return CService{};
     579          64 : }
     580             : 
     581         445 : bool ExtNetInfo::HasEntries(NetInfoPurpose purpose) const
     582             : {
     583         445 :     if (!IsValidPurpose(purpose)) return false;
     584         444 :     const auto& it{m_data.find(purpose)};
     585         444 :     return it != m_data.end() && !it->second.empty();
     586         445 : }
     587             : 
     588         139 : NetInfoStatus ExtNetInfo::Validate() const
     589             : {
     590         139 :     if (m_version == 0 || m_version > CURRENT_VERSION || m_data.empty()) {
     591          15 :         return NetInfoStatus::Malformed;
     592             :     }
     593         124 :     if (HasAddrPortDuplicates()) {
     594           0 :         return NetInfoStatus::Duplicate;
     595             :     }
     596         516 :     for (const auto& [purpose, entries] : m_data) {
     597         350 :         if (!IsValidPurpose(purpose)) {
     598           0 :             return NetInfoStatus::Malformed;
     599             :         }
     600         350 :         if (entries.empty()) {
     601             :             // Purpose if present in map must have at least one entry
     602           0 :             return NetInfoStatus::Malformed;
     603             :         }
     604         350 :         if (HasAddrDuplicates(entries)) {
     605           0 :             return NetInfoStatus::Duplicate;
     606             :         }
     607         941 :         for (const auto& entry : entries) {
     608         591 :             if (!entry.IsTriviallyValid()) {
     609             :                 // Trivially invalid NetInfoEntry, no point checking against consensus rules
     610           0 :                 return NetInfoStatus::Malformed;
     611             :             }
     612        1182 :             if (const auto& service_opt{entry.GetAddrPort()}) {
     613         549 :                 if (auto ret{ValidateService(*service_opt)}; ret != NetInfoStatus::Success) {
     614             :                     // Stores CService underneath but doesn't pass validation rules
     615           0 :                     return ret;
     616             :                 }
     617         633 :             } else if (const auto domain_opt{entry.GetDomainPort()}) {
     618          42 :                 if (purpose != NetInfoPurpose::PLATFORM_HTTPS) {
     619             :                     // Domains only allowed for Platform HTTPS API
     620           0 :                     return NetInfoStatus::BadInput;
     621             :                 }
     622          42 :                 if (auto ret{ValidateDomainPort(*domain_opt)}; ret != NetInfoStatus::Success) {
     623             :                     // Stores DomainPort underneath but doesn't pass validation rules
     624           0 :                     return ret;
     625             :                 }
     626          42 :             } else {
     627             :                 // Doesn't store valid type underneath
     628           0 :                 return NetInfoStatus::Malformed;
     629             :             }
     630             :         }
     631             :     }
     632         124 :     return NetInfoStatus::Success;
     633         139 : }
     634             : 
     635          54 : UniValue ExtNetInfo::ToJson(std::optional<NetInfoPurpose> purpose_opt) const
     636             : {
     637             :     // Report only a subset of all addresses if supplied a purpose code
     638          54 :     if (purpose_opt) {
     639           0 :         UniValue arr(UniValue::VARR);
     640           0 :         for (const auto& entry : GetEntries(purpose_opt)) {
     641           0 :             arr.push_back(entry.ToStringAddrPort());
     642             :         }
     643           0 :         return arr;
     644           0 :     }
     645             : 
     646          54 :     UniValue ret(UniValue::VOBJ);
     647         222 :     for (const auto& [purpose, entries] : m_data) {
     648          84 :         UniValue arr(UniValue::VARR);
     649         242 :         for (const auto& entry : entries) {
     650         158 :             arr.push_back(entry.ToStringAddrPort());
     651             :         }
     652          84 :         ret.pushKV(PurposeToString(purpose).data(), arr);
     653          84 :     }
     654          54 :     return ret;
     655         108 : }
     656             : 
     657          70 : std::string ExtNetInfo::ToString() const
     658             : {
     659         110 :     return IsEmpty() ? "ExtNetInfo()" : strprintf("ExtNetInfo(%s)", [&]() -> std::string {
     660          40 :         std::string ret{};
     661          40 :         bool first{true};
     662         280 :         for (const auto& [purpose, entries] : m_data) {
     663         120 :             if (!first) { ret += ", "; } else { first = false; }
     664         240 :             ret += strprintf("NetInfo(purpose=%s, [%s])", PurposeToString(purpose), [&]() -> std::string {
     665         120 :                 if (entries.empty()) {
     666           0 :                     return "invalid list";
     667             :                 }
     668         350 :                 return Join(entries, ", ", [](const auto& entry) { return entry.ToString(); });
     669         120 :             }());
     670             :         }
     671          40 :         return ret;
     672          40 :     }());
     673           0 : }

Generated by: LCOV version 1.16