diff --git a/contrib/i2pd.conf b/contrib/i2pd.conf index 7d2b3a20..afdebf60 100644 --- a/contrib/i2pd.conf +++ b/contrib/i2pd.conf @@ -100,7 +100,7 @@ ipv6 = false ## Note: that mode uses much more network connections and CPU! # floodfill = true -## For places with limited connectivity (default: false) +## For places with limited connectivity (default: false) # stan = true [ntcp2] @@ -245,6 +245,8 @@ verify = true # proxy = http://127.0.0.1:8118 ## Minimum number of known routers, below which i2pd triggers reseeding (default: 25) # threshold = 25 +## Follow redirects when reseeding from URLs (default: false) +# followredirects = false [addressbook] ## AddressBook subscription URL for initial setup diff --git a/libi2pd/Config.cpp b/libi2pd/Config.cpp index ddcc1e4b..53671d33 100644 --- a/libi2pd/Config.cpp +++ b/libi2pd/Config.cpp @@ -281,6 +281,7 @@ namespace config { "http://[320:f09a:f09f:7acd::216]/," "http://[316:f9e0:f22e:a74f::216]/" ), "Reseed URLs through the Yggdrasil, separated by comma") + ("reseed.followredirect", value()->default_value(false), "Follow redirects when reseeding") ; options_description addressbook("AddressBook options"); diff --git a/libi2pd/HTTP.cpp b/libi2pd/HTTP.cpp index 056dbbe9..e04e8ca2 100644 --- a/libi2pd/HTTP.cpp +++ b/libi2pd/HTTP.cpp @@ -1,5 +1,5 @@ /* -* Copyright (c) 2013-2025, The PurpleI2P Project +* Copyright (c) 2013-2026, The PurpleI2P Project * * This file is part of Purple i2pd project and licensed under BSD3 * @@ -20,52 +20,52 @@ namespace i2p namespace http { // list of valid HTTP methods - static constexpr std::array HTTP_METHODS = + static constexpr std::array HTTP_METHODS = { "GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "CONNECT", // HTTP basic methods "COPY", "LOCK", "MKCOL", "MOVE", "PROPFIND", "PROPPATCH", "UNLOCK", "SEARCH" // WebDAV methods, for SEARCH see rfc5323 }; // list of valid HTTP versions - static constexpr std::array HTTP_VERSIONS = + static constexpr std::array HTTP_VERSIONS = { "HTTP/1.0", "HTTP/1.1" }; - - static constexpr std::array weekdays = + + static constexpr std::array weekdays = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; - - static constexpr std::array months = + + static constexpr std::array months = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; - static inline bool is_http_version(std::string_view str) + static inline bool is_http_version(std::string_view str) { return std::find(HTTP_VERSIONS.begin(), HTTP_VERSIONS.end(), str) != std::end(HTTP_VERSIONS); } - static inline bool is_http_method(std::string_view str) + static inline bool is_http_method(std::string_view str) { return std::find(HTTP_METHODS.begin(), HTTP_METHODS.end(), str) != std::end(HTTP_METHODS); } - - static void strsplit(std::string_view line, std::vector &tokens, char delim, std::size_t limit = 0) - { + + static void strsplit(std::string_view line, std::vector &tokens, char delim, std::size_t limit = 0) + { size_t count = 1, pos; while ((pos = line.find (delim)) != line.npos) { count++; if (limit > 0 && count >= limit) delim = '\n'; // reset delimiter tokens.push_back (line.substr (0, pos)); - line = line.substr (pos + 1); + line = line.substr (pos + 1); } if (!line.empty ()) tokens.push_back (line); } - + static std::pair parse_header_line(std::string_view line) { std::size_t pos = 0; @@ -94,17 +94,17 @@ namespace http out = buf; } - bool URL::parse(const char *str, std::size_t len) + bool URL::parse(const char *str, std::size_t len) { return parse({str, len ? len : strlen(str)}); } - bool URL::parse(std::string_view url) + bool URL::parse(std::string_view url) { if (url.empty ()) return false; std::size_t pos_p = 0; /* < current parse position */ std::size_t pos_c = 0; /* < work position */ - if(url.at(0) != '/' || pos_p > 0) + if(url.at(0) != '/' || pos_p > 0) { std::size_t pos_s = 0; @@ -209,7 +209,7 @@ namespace http return true; } - bool URL::parse_query(std::map & params) + bool URL::parse_query(std::map & params) { std::vector tokens; strsplit(query, tokens, '&'); @@ -287,12 +287,21 @@ namespace http headers.erase(name); } - int HTTPReq::parse(const char *buf, size_t len) + std::string HTTPMsg::get_header(const std::string& name) const + { + auto it = headers.find(name); + if (it == headers.end()) + return ""; + else + return it->second; + } + + int HTTPReq::parse(const char *buf, size_t len) { return parse({buf, len}); } - int HTTPReq::parse(std::string_view str) + int HTTPReq::parse(std::string_view str) { enum { REQ_LINE, HEADER_LINE } expect = REQ_LINE; std::size_t eoh = str.find(HTTP_EOH); /* request head size */ @@ -302,14 +311,14 @@ namespace http if (eoh == std::string::npos) return 0; /* str not contains complete request */ - while ((eol = str.find(CRLF, pos)) != std::string::npos) + while ((eol = str.find(CRLF, pos)) != std::string::npos) { - if (expect == REQ_LINE) + if (expect == REQ_LINE) { std::string_view line = str.substr(pos, eol - pos); std::vector tokens; strsplit(line, tokens, ' '); - + if (tokens.size() != 3) return -1; if (!is_http_method(tokens[0])) @@ -432,12 +441,12 @@ namespace http return length; } - int HTTPRes::parse(const char *buf, size_t len) + int HTTPRes::parse(const char *buf, size_t len) { return parse({buf,len}); } - int HTTPRes::parse(std::string_view str) + int HTTPRes::parse(std::string_view str) { enum { RES_LINE, HEADER_LINE } expect = RES_LINE; std::size_t eoh = str.find(HTTP_EOH); /* request head size */ @@ -446,9 +455,9 @@ namespace http if (eoh == std::string::npos) return 0; /* str not contains complete request */ - while ((eol = str.find(CRLF, pos)) != std::string::npos) + while ((eol = str.find(CRLF, pos)) != std::string::npos) { - if (expect == RES_LINE) + if (expect == RES_LINE) { std::string_view line = str.substr(pos, eol - pos); std::vector tokens; @@ -466,8 +475,8 @@ namespace http version = tokens[0]; status = tokens[2]; expect = HEADER_LINE; - } - else + } + else { std::string_view line = str.substr(pos, eol - pos); auto p = parse_header_line(line); @@ -505,10 +514,10 @@ namespace http return ss.str(); } - std::string_view HTTPCodeToStatus(int code) + std::string_view HTTPCodeToStatus(int code) { std::string_view ptr; - switch (code) + switch (code) { case 105: ptr = "Name Not Resolved"; break; /* success */ @@ -546,7 +555,7 @@ namespace http auto c = url[i]; if (c == '%') { - decoded.append (url, start, i - start); + decoded.append (url, start, i - start); if (i + 2 <= url.length ()) { unsigned char ch; @@ -557,7 +566,7 @@ namespace http decoded.append (url, i, 3); i += 2; start = i + 1; - } + } else break; } diff --git a/libi2pd/HTTP.h b/libi2pd/HTTP.h index 0055211b..3af31210 100644 --- a/libi2pd/HTTP.h +++ b/libi2pd/HTTP.h @@ -1,5 +1,5 @@ /* -* Copyright (c) 2013-2025, The PurpleI2P Project +* Copyright (c) 2013-2026, The PurpleI2P Project * * This file is part of Purple i2pd project and licensed under BSD3 * @@ -71,6 +71,7 @@ namespace http void add_header(const char *name, const std::string & value, bool replace = false); void add_header(const char *name, const char *value, bool replace = false); void del_header(const char *name); + std::string get_header(const std::string& name) const; /** @brief Returns declared message length or -1 if unknown */ long int content_length() const; diff --git a/libi2pd/Reseed.cpp b/libi2pd/Reseed.cpp index 23dae8ff..623f20ef 100644 --- a/libi2pd/Reseed.cpp +++ b/libi2pd/Reseed.cpp @@ -1,5 +1,5 @@ /* -* Copyright (c) 2013-2025, The PurpleI2P Project +* Copyright (c) 2013-2026, The PurpleI2P Project * * This file is part of Purple i2pd project and licensed under BSD3 * @@ -489,13 +489,13 @@ namespace data BIGNUM * n1 = BN_new (); if (EVP_PKEY_get_bn_param (pubKey, OSSL_PKEY_PARAM_RSA_N, &n1) > 0) n = n1; -#else +#else const RSA * key = EVP_PKEY_get0_RSA (pubKey); const BIGNUM * e, * d; RSA_get0_key(key, &n, &e, &d); -#endif +#endif if (n) - { + { PublicKey value; i2p::crypto::bn2buf (n, value, 512); if (cn) @@ -507,7 +507,7 @@ namespace data LogPrint (eLogError, "Reseed: Can't extract RSA key from ", filename); #if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0 BN_free (n1); -#endif +#endif } SSL_free (ssl); } @@ -636,19 +636,19 @@ namespace data // TODO: support username/password auth etc bool success = false; i2p::transport::Socks5Handshake (sock, std::make_pair(url.host, url.port), - [&success](const boost::system::error_code& ec) - { + [&success](const boost::system::error_code& ec) + { if (!ec) success = true; else LogPrint (eLogError, "Reseed: SOCKS handshake failed: ", ec.message()); - }); + }); service.run (); // execute all async operations if (!success) { sock.close(); return ""; - } + } } } } @@ -669,9 +669,9 @@ namespace data if (ep.address ().is_v4 ()) supported = i2p::context.SupportsV4 (); else if (ep.address ().is_v6 ()) - supported = i2p::util::net::IsYggdrasilAddress (ep.address ()) ? + supported = i2p::util::net::IsYggdrasilAddress (ep.address ()) ? i2p::context.SupportsMesh () : i2p::context.SupportsV6 (); - } + } if (supported) { s.lowest_layer().connect (ep, ecode); @@ -710,12 +710,16 @@ namespace data template std::string Reseeder::ReseedRequest (Stream& s, const std::string& uri) { + bool follow; i2p::config::GetOption("reseed.followredirect", follow); boost::system::error_code ecode; i2p::http::HTTPReq req; + i2p::http::HTTPRes res; + req.uri = uri; req.AddHeader("User-Agent", "Wget/1.11.4"); req.AddHeader("Connection", "close"); s.write_some (boost::asio::buffer (req.to_string())); + // read response std::stringstream rs; char recv_buf[1024]; size_t l = 0; @@ -723,18 +727,32 @@ namespace data l = s.read_some (boost::asio::buffer (recv_buf, sizeof(recv_buf)), ecode); if (l) rs.write (recv_buf, l); } while (!ecode && l); + // process response std::string data = rs.str(); - i2p::http::HTTPRes res; int len = res.parse(data); if (len <= 0) { LogPrint(eLogWarning, "Reseed: Incomplete/broken response from ", uri); return ""; } + + if ((res.code == 301 || res.code == 302 || res.code == 307) && follow) { + LogPrint(eLogDebug, "Reseed: Recieved redirect from ", uri); + + std::string location = res.get_header("Location"); + if (location.length() == 0) { + LogPrint(eLogWarning, "Reseed: Broken redirect from ", uri); + return ""; + } + bool isHttps = (location.length() > 8 && location.substr(0, 8) == "https://"); + return (isHttps ? HttpsRequest (location) : YggdrasilRequest (location)); + } + if (res.code != 200) { LogPrint(eLogError, "Reseed: Failed to reseed from ", uri, ", http code ", res.code); return ""; } + data.erase(0, len); /* drop http headers from response */ LogPrint(eLogDebug, "Reseed: Got ", data.length(), " bytes of data from ", uri); if (res.is_chunked()) {