diff --git a/src/net/i2p_address.cpp b/src/net/i2p_address.cpp index 3009ab01c..b3f6d40eb 100644 --- a/src/net/i2p_address.cpp +++ b/src/net/i2p_address.cpp @@ -45,21 +45,16 @@ namespace net { namespace { - // !TODO only b32 addresses right now - constexpr const char tld[] = u8".b32.i2p"; - constexpr const char unknown_host[] = ""; - - constexpr const unsigned b32_length = 52; - constexpr const char base32_alphabet[] = u8"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz234567"; - expect host_check(boost::string_ref host) noexcept + //! Validate a Base32 address (.b32.i2p) + expect host_check_b32(boost::string_ref host) noexcept { - if (!host.ends_with(tld)) + if (!host.ends_with(tld_b32)) return {net::error::expected_tld}; - host.remove_suffix(sizeof(tld) - 1); + host.remove_suffix(sizeof(tld_b32) - 1); if (host.size() != b32_length) return {net::error::invalid_i2p_address}; @@ -69,6 +64,55 @@ namespace net return success(); } + //! Validate a human-readable hostname (.i2p) + expect host_check_human(boost::string_ref host) noexcept + { + if (!host.ends_with(tld_i2p)) + return {net::error::expected_tld}; + + host.remove_suffix(sizeof(tld_i2p) - 1); + + if (host.empty() || host.size() > i2p_name_max_length) + return {net::error::invalid_i2p_address}; + + //! Reject addresses starting or ending with a dot or hyphen + if (host.front() == '.' || host.front() == '-' || + host.back() == '.' || host.back() == '-') + return {net::error::invalid_i2p_address}; + + //! Reject invalid characters, and two dots/hyphens in a row (or a dot next to a hyphen) + char previous = '\0'; + for (const char c : host) + { + if (!(('a' <= c && c <= 'z') || ('0' <= c && c <= '9') || c == '.' || c == '-')) + { + return {net::error::invalid_i2p_address}; + } + if ((c == '.' || c == '-') && (previous == '.' || previous == '-')) + { + return {net::error::invalid_i2p_address}; + } + previous = c; + } + + return success(); + } + + //! Use appropriate function for address validation + expect host_check(boost::string_ref host) noexcept + { + if (host.ends_with(tld_b32)) + { + return host_check_b32(host); + } + else if (host.ends_with(tld_i2p)) + { + return host_check_human(host); + } + + return {net::error::expected_tld}; + } + struct i2p_serialized { std::string host; @@ -110,7 +154,9 @@ namespace net net::canonicalize_host(normalized_host); MONERO_CHECK(host_check(normalized_host)); - static_assert(b32_length + sizeof(tld) == sizeof(i2p_address::host_), "bad internal host size"); + static_assert(sizeof(i2p_address::host_) >= b32_length + sizeof(tld_b32) && + sizeof(i2p_address::host_) >= i2p_name_max_length + sizeof(tld_i2p)); + return i2p_address{normalized_host}; } diff --git a/src/net/i2p_address.h b/src/net/i2p_address.h index 44e268683..ec38a2b1e 100644 --- a/src/net/i2p_address.h +++ b/src/net/i2p_address.h @@ -47,10 +47,27 @@ namespace serialization namespace net { - //! b32 i2p address; internal format not condensed/decoded. + /** + * Naming rules defined here: + * https://i2p.net/en/docs/overview/naming/#naming-rules + */ + constexpr const char tld_b32[] = u8".b32.i2p"; + constexpr const char tld_i2p[] = u8".i2p"; + constexpr const char unknown_host[] = ""; + + //! Length of a Base32 I2P address in characters, not including TLD + constexpr const unsigned b32_length = 52; + + //! Maximum length of a human-readable I2P hostname in characters, not including TLD + constexpr const unsigned i2p_name_max_length = 63; + + /** + * An I2P address; internal format not condensed/decoded. + * Supports human-readable and Base32 types. + */ class i2p_address { - char host_[61]; // null-terminated + char host_[i2p_name_max_length + sizeof(tld_i2p)] = {}; // null-terminated //! Keep in private, `host.size()` has no runtime check i2p_address(boost::string_ref host) noexcept; @@ -59,7 +76,7 @@ namespace net //! \return Size of internal buffer for host. static constexpr std::size_t buffer_size() noexcept { return sizeof(host_); } - //! \return ``. + //! \return ``. static const char* unknown_str() noexcept; //! An object with `port() == 0` and `host_str() == unknown_str()`. @@ -69,7 +86,7 @@ namespace net static i2p_address unknown() noexcept { return i2p_address{}; } /*! - Parse `address` in b32 i2p format (i.e. x.b32.i2p:80) + Parse `address` in the detected format (i.e. x[.b32].i2p:80) with `default_port` being used if port is not specified in `address`. */ @@ -96,10 +113,10 @@ namespace net //! \return True if i2p addresses are identical. bool is_same_host(const i2p_address& rhs) const noexcept; - //! \return `x.b32.i2p` or `x.b32.i2p:z` if `port() != 0`. + //! \return `x[.b32].i2p` or `x[.b32].i2p:z` if `port() != 0`. std::string str() const; - //! \return Null-terminated `x.b32.i2p` value or `unknown_str()`. + //! \return Null-terminated `x[.b32].i2p` value or `unknown_str()`. const char* host_str() const noexcept { return host_; } //! \return `1` to work with I2P socks which considers `0` error. diff --git a/tests/unit_tests/net.cpp b/tests/unit_tests/net.cpp index 64748e787..d55463722 100644 --- a/tests/unit_tests/net.cpp +++ b/tests/unit_tests/net.cpp @@ -1,5 +1,4 @@ // Copyright (c) 2018-2024, The Monero Project - // // All rights reserved. // @@ -491,6 +490,9 @@ namespace "VWW6YBAL4BD7SZMGNCYRUUCPGFKQAHZDDI37KTCEO3AH7NGMCOPN.B32.I2P"; static constexpr const char b32_i2p_2[] = "xmrto2bturnore26xmrto2bturnore26xmrto2bturnore26xmr2.b32.i2p"; + + static constexpr const char standard_i2p[] = "test.i2p"; + static constexpr const char standard_i2p_2[] = "reg.i2p"; } TEST(i2p_address, constants) @@ -511,12 +513,40 @@ TEST(i2p_address, invalid) EXPECT_TRUE(net::i2p_address::make(":").has_error()); EXPECT_TRUE(net::i2p_address::make(".b32.i2p").has_error()); EXPECT_TRUE(net::i2p_address::make(".b32.i2p:").has_error()); + EXPECT_TRUE(net::i2p_address::make(".i2p").has_error()); + EXPECT_TRUE(net::i2p_address::make(".i2p:").has_error()); + + EXPECT_TRUE(net::i2p_address::make("m=nero.i2p").has_error()); + EXPECT_TRUE(net::i2p_address::make("-monero.i2p").has_error()); + EXPECT_TRUE(net::i2p_address::make(std::string(net::i2p_name_max_length, 'a') + ".i2p").has_value()); + EXPECT_TRUE(net::i2p_address::make(std::string(net::i2p_name_max_length + 1, 'a') + ".i2p").has_error()); + EXPECT_TRUE(net::i2p_address::make("monero-.i2p").has_error()); + EXPECT_TRUE(net::i2p_address::make("a.-monero.i2p").has_error()); + EXPECT_TRUE(net::i2p_address::make("m..ero.i2p").has_error()); + EXPECT_TRUE(net::i2p_address::make("m..ero.b32.i2p.i2p").has_error()); + EXPECT_TRUE(net::i2p_address::make("monero.i2p-").has_error()); + EXPECT_TRUE(net::i2p_address::make("monero..i2p").has_error()); + EXPECT_TRUE(net::i2p_address::make("a-.b.i2p").has_error()); + EXPECT_TRUE(net::i2p_address::make("a.-b.i2p").has_error()); + EXPECT_TRUE(net::i2p_address::make("a--b.i2p").has_error()); + EXPECT_TRUE(net::i2p_address::make("a.b.i2p").has_value()); + EXPECT_TRUE(net::i2p_address::make("abc-123.i2p").has_value()); + EXPECT_TRUE(net::i2p_address::make("example.b32.i2p").has_error()); EXPECT_TRUE(net::i2p_address::make(b32_i2p + 1).has_error()); + + EXPECT_TRUE(net::i2p_address::make("test.b32.i2p.i2p").has_value()); + EXPECT_TRUE(net::i2p_address::make(std::string{b32_i2p} + ".i2p").has_value()); + EXPECT_TRUE(net::i2p_address::make(boost::string_ref{b32_i2p, sizeof(b32_i2p) - 2}).has_error()); + EXPECT_TRUE(net::i2p_address::make(boost::string_ref{standard_i2p, sizeof(standard_i2p) - 2}).has_error()); std::string i2p{b32_i2p}; i2p.at(10) = 1; EXPECT_TRUE(net::i2p_address::make(i2p).has_error()); + + std::string i2p_standard{standard_i2p}; + i2p_standard.at(7) = 1; + EXPECT_TRUE(net::i2p_address::make(i2p_standard).has_error()); } TEST(i2p_address, unblockable_types) @@ -547,7 +577,7 @@ TEST(i2p_address, unblockable_types) EXPECT_EQ(net::i2p_address{}, net::i2p_address::unknown()); } -TEST(i2p_address, valid) +TEST(i2p_address, valid_b32) { const auto address1 = net::i2p_address::make(b32_i2p); @@ -624,6 +654,85 @@ TEST(i2p_address, valid) EXPECT_FALSE(address2.less(address3)); } +TEST(i2p_address, valid_standard) +{ + const auto address1 = net::i2p_address::make(standard_i2p); + + ASSERT_TRUE(address1.has_value()); + EXPECT_EQ(1u, address1->port()); + EXPECT_STREQ(standard_i2p, address1->host_str()); + EXPECT_STREQ(standard_i2p, address1->str().c_str()); + EXPECT_TRUE(address1->is_blockable()); + + net::i2p_address address2{*address1}; + + EXPECT_EQ(1u, address2.port()); + EXPECT_STREQ(standard_i2p, address2.host_str()); + EXPECT_STREQ(standard_i2p, address2.str().c_str()); + EXPECT_TRUE(address2.is_blockable()); + EXPECT_TRUE(address2.equal(*address1)); + EXPECT_TRUE(address1->equal(address2)); + EXPECT_TRUE(address2 == *address1); + EXPECT_TRUE(*address1 == address2); + EXPECT_FALSE(address2 != *address1); + EXPECT_FALSE(*address1 != address2); + EXPECT_TRUE(address2.is_same_host(*address1)); + EXPECT_TRUE(address1->is_same_host(address2)); + EXPECT_FALSE(address2.less(*address1)); + EXPECT_FALSE(address1->less(address2)); + + address2 = MONERO_UNWRAP(net::i2p_address::make(std::string{standard_i2p_2} + ":6545")); + + EXPECT_EQ(1u, address2.port()); + EXPECT_STREQ(standard_i2p_2, address2.host_str()); + EXPECT_EQ(std::string{standard_i2p_2}, address2.str().c_str()); + EXPECT_TRUE(address2.is_blockable()); + EXPECT_FALSE(address2.equal(*address1)); + EXPECT_FALSE(address1->equal(address2)); + EXPECT_FALSE(address2 == *address1); + EXPECT_FALSE(*address1 == address2); + EXPECT_TRUE(address2 != *address1); + EXPECT_TRUE(*address1 != address2); + EXPECT_FALSE(address2.is_same_host(*address1)); + EXPECT_FALSE(address1->is_same_host(address2)); + EXPECT_FALSE(address1->less(address2)); + EXPECT_TRUE(address2.less(*address1)); + + net::i2p_address address3 = MONERO_UNWRAP(net::i2p_address::make(std::string{standard_i2p} + ":65535")); + + EXPECT_EQ(1u, address3.port()); + EXPECT_STREQ(standard_i2p, address3.host_str()); + EXPECT_EQ(std::string{standard_i2p}, address3.str().c_str()); + EXPECT_TRUE(address3.is_blockable()); + EXPECT_TRUE(address3.equal(*address1)); + EXPECT_TRUE(address1->equal(address3)); + EXPECT_TRUE(address3 == *address1); + EXPECT_TRUE(*address1 == address3); + EXPECT_FALSE(address3 != *address1); + EXPECT_FALSE(*address1 != address3); + EXPECT_TRUE(address3.is_same_host(*address1)); + EXPECT_TRUE(address1->is_same_host(address3)); + EXPECT_FALSE(address3.less(*address1)); + EXPECT_FALSE(address1->less(address3)); + + EXPECT_FALSE(address3.equal(address2)); + EXPECT_FALSE(address2.equal(address3)); + EXPECT_FALSE(address3 == address2); + EXPECT_FALSE(address2 == address3); + EXPECT_TRUE(address3 != address2); + EXPECT_TRUE(address2 != address3); + EXPECT_FALSE(address3.is_same_host(address2)); + EXPECT_FALSE(address2.is_same_host(address3)); + EXPECT_TRUE(address2.less(address3)); + EXPECT_FALSE(address3.less(address2)); + + { + const auto address = net::i2p_address::make("Example.I2P"); + ASSERT_TRUE(address.has_value()); + EXPECT_STREQ("example.i2p", address->host_str()); + } +} + TEST(i2p_address, generic_network_address) { const epee::net_utils::network_address i2p1{MONERO_UNWRAP(net::i2p_address::make(b32_i2p))}; @@ -645,6 +754,22 @@ TEST(i2p_address, generic_network_address) EXPECT_TRUE(i2p1.is_blockable()); EXPECT_TRUE(i2p2.is_blockable()); EXPECT_TRUE(ip.is_blockable()); + + const epee::net_utils::network_address i2p1_standard{MONERO_UNWRAP(net::i2p_address::make(standard_i2p))}; + const epee::net_utils::network_address i2p2_standard{MONERO_UNWRAP(net::i2p_address::make(standard_i2p))}; + + EXPECT_EQ(i2p1_standard, i2p2_standard); + EXPECT_NE(ip, i2p1_standard); + EXPECT_LT(ip, i2p1_standard); + + EXPECT_STREQ(standard_i2p, i2p1_standard.host_str().c_str()); + EXPECT_STREQ(standard_i2p, i2p1_standard.str().c_str()); + EXPECT_EQ(epee::net_utils::address_type::i2p, i2p1_standard.get_type_id()); + EXPECT_EQ(epee::net_utils::address_type::i2p, i2p2_standard.get_type_id()); + EXPECT_EQ(epee::net_utils::zone::i2p, i2p1_standard.get_zone()); + EXPECT_EQ(epee::net_utils::zone::i2p, i2p2_standard.get_zone()); + EXPECT_TRUE(i2p1_standard.is_blockable()); + EXPECT_TRUE(i2p2_standard.is_blockable()); } namespace @@ -847,9 +972,8 @@ TEST(i2p_address, boost_serialize_unknown) TEST(get_network_address, i2p) { - expect address = - net::get_network_address("i2p", 0); - EXPECT_EQ(net::error::unsupported_address, address); + expect address = net::get_network_address(".i2p", 0); + EXPECT_EQ(net::error::invalid_i2p_address, address); address = net::get_network_address(".b32.i2p", 0); EXPECT_EQ(net::error::invalid_i2p_address, address); @@ -865,6 +989,18 @@ TEST(get_network_address, i2p) EXPECT_EQ(epee::net_utils::address_type::i2p, address->get_type_id()); EXPECT_STREQ(b32_i2p, address->host_str().c_str()); EXPECT_EQ(std::string{b32_i2p}, address->str()); + + address = net::get_network_address(standard_i2p, 1000); + ASSERT_TRUE(bool(address)); + EXPECT_EQ(epee::net_utils::address_type::i2p, address->get_type_id()); + EXPECT_STREQ(standard_i2p, address->host_str().c_str()); + EXPECT_EQ(std::string{standard_i2p}, address->str()); + + address = net::get_network_address(std::string{standard_i2p} + ":2000", 1000); + ASSERT_TRUE(bool(address)); + EXPECT_EQ(epee::net_utils::address_type::i2p, address->get_type_id()); + EXPECT_STREQ(standard_i2p, address->host_str().c_str()); + EXPECT_EQ(std::string{standard_i2p}, address->str()); } TEST(get_network_address, ipv4)