From add429b913c90360c771959dbffad5618a808fab Mon Sep 17 00:00:00 2001 From: anonimal Date: Mon, 5 Sep 2016 13:08:25 +0000 Subject: [PATCH] HTTP/AddressBook: major refactoring + download impl Major: - Implementation improvements * HTTP: download impl * HTTP: URI handling impl * HTTP: response header handling for clearnet * AddressBook: unhook in-net HTTP impl (now in class HTTP) * Add documentation Medium: - If default subscription file does not exist, create/save after downloading from publisher Minor: - Adjust related reseed and tunnel code for class HTTP - Update variables names to accurately reflect purpose - Update log messages for accuracy - Update address book flow narrative - Update/improve HTTP unit-test References: - #305 (parent ticket) - #168 and #155 (both in terms of HTTP/S) - #154 and #48 (we now have a multi-purpose HTTP/S download impl) Notes: - My apologies for not keeping this commit atomic. Bigger design work can be harder to keep atomic (especially if one wants new features to work before committing) - This commit has been successfully tested and all unit-tests pass --- src/client/address_book.cc | 399 +++++++++---------------- src/client/address_book.h | 33 +- src/client/address_book_storage.cc | 12 +- src/client/address_book_storage.h | 11 +- src/client/i2p_tunnel/http_proxy.cc | 4 +- src/client/i2p_tunnel/i2p_tunnel.h | 2 +- src/core/reseed.cc | 8 +- src/core/util/http.cc | 361 ++++++++++++++++------ src/core/util/http.h | 254 ++++++++++++---- src/tests/unit_tests/core/util/http.cc | 19 +- 10 files changed, 647 insertions(+), 456 deletions(-) diff --git a/src/client/address_book.cc b/src/client/address_book.cc index 5ab124ac..92cf4141 100644 --- a/src/client/address_book.cc +++ b/src/client/address_book.cc @@ -76,10 +76,9 @@ namespace client { * 4. Kovri hooks its subscriber to into an asio timer that regularly * updates a subscription (only downloads new subscription if Etag is set) * 5. If available, kovri loads default packaged subscription before downloading - * 6. Kovri's subscriber checks if publisher is in-net or clearnet then downloads - * subscription/updated subscription + * 6. Kovri's subscriber checks if downloads subscription/updated subscription * 7. Kovri saves subscription to storage - * 8. Kovri repeats download ad infinitum on a timer using specified constants + * 8. Kovri repeats download ad infinitum with a timer based on specified constants */ void AddressBook::Start( @@ -108,53 +107,56 @@ void AddressBook::Start( } void AddressBook::LoadPublishers() { - if (!m_Subscribers.size()) { - auto filename = GetDefaultPublishersFilename(); + // TODO(unassigned): this is a one-shot: we won't be able to + // edit publisher's file manually with any effect after router start + if (m_Subscribers.empty()) { + auto publishers = GetPublishersFilename(); + LogPrint(eLogInfo, "AddressBook: loading publisher file ", publishers); std::ifstream file( - i2p::util::filesystem::GetFullPath(filename), - std::ofstream::in); // in text mode + i2p::util::filesystem::GetFullPath(publishers), + std::ofstream::in); if (file.is_open()) { - std::string address; + std::string publisher; while (!file.eof()) { - getline(file, address); - if (!address.length()) + getline(file, publisher); + if (!publisher.length()) continue; // skip empty line // Perform sanity test for valid URI - i2p::util::http::URI uri; - if (!uri.Parse(address)) { + i2p::util::http::HTTP http(publisher); + if (!http.GetURI().is_valid()) { LogPrint(eLogWarn, "AddressBook: invalid/malformed publisher address, skipping"); continue; } m_Subscribers.push_back( - std::make_unique(*this, address)); + std::make_unique(*this, publisher)); } LogPrint(eLogInfo, "AddressBook: ", m_Subscribers.size(), " publishers loaded"); } else { LogPrint(eLogWarn, - "AddressBook: ", filename, " not found; using default publisher"); - m_Subscribers.push_back( - std::make_unique(*this, GetDefaultPublisherURI())); + "AddressBook: ", publishers, " not found; we'll use default publisher"); + // TODO(anonimal): create default publisher file if file is missing } } else { - LogPrint(eLogError, "AddressBook: publishers already loaded"); + LogPrint(eLogError, "AddressBook: publisher(s) already loaded"); } } void AddressBook::SubscriberUpdateTimer( const boost::system::error_code& ecode) { if (ecode != boost::asio::error::operation_aborted) { - // If hosts are saved + we're not currently downloading + are fully online - if (m_HostsAreLoaded && !m_SubscriberIsDownloading && - m_SharedLocalDestination->IsReady()) { + if (m_SubscriptionIsLoaded + && !m_SubscriberIsDownloading + && m_SharedLocalDestination->IsReady()) { // Pick a random subscription if user has multiple publishers + // TODO(anonimal): subscribers size and bounds checking auto publisher = i2p::crypto::RandInRange(0, m_Subscribers.size() - 1); m_SubscriberIsDownloading = true; m_Subscribers[publisher]->DownloadSubscription(); } else { - if (!m_HostsAreLoaded) { + if (!m_SubscriptionIsLoaded) { // If subscription not available, will attempt download with subscriber LoadSubscriptionFromPublisher(); } @@ -174,203 +176,62 @@ void AddressBook::SubscriberUpdateTimer( } } -void AddressBookSubscriber::DownloadSubscription() { - std::thread download(&AddressBookSubscriber::DownloadSubscriptionImpl, this); - download.detach(); // TODO(anonimal): use join -} - -// TODO(anonimal): see HTTP for notes on cpp-netlib refactor -// tl;dr, all HTTP-related code + stream handling should be refactored -void AddressBookSubscriber::DownloadSubscriptionImpl() { - LogPrint(eLogInfo, - "AddressBookSubscriber: downloading hosts from ", m_URI, - " ETag: ", m_Etag, - " Last-Modified: ", m_LastModified); - bool success = false; - i2p::util::http::URI uri; - // TODO(anonimal): implement check to see if URI is in-net or clearnet, - // and then implement download appropriately - // If in-net, translate address to ident hash, find lease-set, and download - // TODO(unassigned): we need to abstract this download process. See #168. - i2p::data::IdentHash ident; - if (m_Book.CheckAddressIdentHashFound(uri.m_Host, ident) && - m_Book.GetSharedLocalDestination()) { - std::condition_variable new_data_received; - std::mutex new_data_received_mutex; - auto lease_set = m_Book.GetSharedLocalDestination()->FindLeaseSet(ident); - if (!lease_set) { - std::unique_lock lock(new_data_received_mutex); - m_Book.GetSharedLocalDestination()->RequestDestination( - ident, - [&new_data_received, &lease_set]( - std::shared_ptr ls) { - lease_set = ls; - new_data_received.notify_all(); - }); - if (new_data_received.wait_for( - lock, - std::chrono::seconds( - static_cast(SubscriberTimeout::Request))) - == std::cv_status::timeout) - LogPrint(eLogError, - "AddressBookSubscriber: ", - "subscription lease set request timeout expired"); - } - // Lease-set found, download in-net subscription from publisher - if (lease_set) { - std::stringstream request, response; - // Standard header - i2p::util::http::HTTP http; - request << http.Header(uri.m_Path, uri.m_Host, "1.1"); - if (m_Etag.length () > 0) // etag - request << http.IF_NONE_MATCH - << ": \"" << m_Etag << "\"\r\n"; - if (m_LastModified.length () > 0) // if modified since - request << http.IF_MODIFIED_SINCE - << ": " << m_LastModified << "\r\n"; - request << "\r\n"; // end of header - auto stream = - m_Book.GetSharedLocalDestination()->CreateStream( - lease_set, - std::stoi(uri.m_Port)); - stream->Send( - reinterpret_cast(request.str().c_str()), - request.str().length()); - std::array buf; - bool end_of_data = false; - while (!end_of_data) { - stream->AsyncReceive( - boost::asio::buffer( - buf.data(), - buf.size()), - [&](const boost::system::error_code& ecode, - std::size_t bytes_transferred) { - if (bytes_transferred) - response.write( - reinterpret_cast(buf.data()), - bytes_transferred); - if (ecode == boost::asio::error::timed_out || !stream->IsOpen()) - end_of_data = true; - new_data_received.notify_all(); - }, - static_cast(SubscriberTimeout::Receive)); - std::unique_lock lock(new_data_received_mutex); - if (new_data_received.wait_for( - lock, - std::chrono::seconds( - static_cast(SubscriberTimeout::Request))) - == std::cv_status::timeout) - LogPrint(eLogError, - "AddressBookSubscriber: subscription timeout expired"); - } - // process remaining buffer - while (std::size_t len = stream->ReadSome(buf.data(), buf.size())) - response.write(reinterpret_cast(buf.data()), len); - // parse response - std::string version; - response >> version; // HTTP version - int status = 0; - response >> status; // status - if (status == 200) { // OK - bool is_chunked = false; - std::string header, status_message; - std::getline(response, status_message); - // read until new line meaning end of header - while (!response.eof() && header != "\r") { - std::getline(response, header); - auto colon = header.find(':'); - if (colon != std::string::npos) { - std::string field = header.substr(0, colon); - header.resize(header.length() - 1); // delete \r - if (field == http.ETAG) - m_Etag = header.substr(colon + 1); - else if (field == http.LAST_MODIFIED) - m_LastModified = header.substr(colon + 1); - else if (field == http.TRANSFER_ENCODING) - is_chunked = - !header.compare(colon + 1, std::string::npos, "chunked"); - } - } - LogPrint(eLogInfo, - "AddressBookSubscriber: ", m_URI, - " ETag: ", m_Etag, - " Last-Modified: ", m_LastModified); - if (!response.eof()) { - success = true; - if (!is_chunked) { - m_Book.ValidateSubscriptionThenSaveToStorage(response); - } else { - // merge chunks - std::stringstream merged; - http.MergeChunkedResponse(response, merged); - m_Book.ValidateSubscriptionThenSaveToStorage(merged); - } - } - } else if (status == 304) { - success = true; - LogPrint(eLogInfo, - "AddressBookSubscriber: no updates from ", m_URI); - } else { - LogPrint(eLogWarn, - "AddressBookSubscriber: HTTP response ", status); +void AddressBook::LoadSubscriptionFromPublisher() { + // First, ensure we have a storage instance ready + if (!m_Storage) + m_Storage = GetNewStorageInstance(); + // If so, see if we have subscription available + if (m_Storage->Load(m_Addresses)) { + // If so, we don't need to download from a publisher + m_SubscriptionIsLoaded = true; + return; + } + // If available, load default subscription from file + auto subscription = GetSubscriptionFilename(); + std::ifstream file( + i2p::util::filesystem::GetFullPath(subscription), + std::ofstream::in); + LogPrint(eLogInfo, "AddressBook: loading subscription ", subscription); + if (file.is_open()) { // Open subscription, validate, and save to storage + if (!ValidateSubscriptionThenSaveToStorage(file)) + LogPrint(eLogWarn, + "AddressBook: invalid host in ", subscription, ", not loading"); + } else { // Use default publisher and download + LogPrint(eLogWarn, "AddressBook: ", subscription, " not found"); + if (!m_SubscriberIsDownloading) { + if (m_Subscribers.empty()) { + auto publisher = GetDefaultPublisherURI(); + LogPrint(eLogWarn, "AddressBook: using default publisher ", publisher); + m_Subscribers.push_back( + std::make_unique(*this, publisher)); } + m_SubscriberIsDownloading = true; + m_Subscribers.front()->DownloadSubscription(); } else { - LogPrint(eLogError, - "AddressBookSubscriber: address ", uri.m_Host, " not found"); + LogPrint(eLogWarn, "AddressBook: subscriber is downloading"); } - } else { - LogPrint(eLogError, - "AddressBookSubscriber: can't resolve ", uri.m_Host); } - LogPrint(eLogInfo, - "AddressBookSubscriber: download complete ", - success ? "Success" : "Failed"); - m_Book.HostsDownloadComplete(success); } -// For in-net download only -bool AddressBook::CheckAddressIdentHashFound( - const std::string& address, - i2p::data::IdentHash& ident) { - auto pos = address.find(".b32.i2p"); - if (pos != std::string::npos) { - if (!i2p::util::Base32ToByteStream(address.c_str(), pos, ident, 32)) { - LogPrint(eLogError, "AddressBook: invalid base32 address"); - return false; - } - return true; - } else { - pos = address.find(".i2p"); - if (pos != std::string::npos) { - auto ident_hash = GetLoadedAddressIdentHash(address); - if (ident_hash) { - ident = *ident_hash; - return true; - } else { - return false; - } - } - } - // If not .b32, test for full base64 address - i2p::data::IdentityEx dest; - if (!dest.FromBase64(address)) - return false; // Invalid base64 address - ident = dest.GetIdentHash(); - return true; +void AddressBookSubscriber::DownloadSubscription() { + std::thread download(&AddressBookSubscriber::DownloadSubscriptionImpl, this); + download.detach(); // TODO(anonimal): use join } -// For in-net download only -std::unique_ptr AddressBook::GetLoadedAddressIdentHash( - const std::string& address) { - if (!m_HostsAreLoaded) - LoadSubscriptionFromPublisher(); - if (m_HostsAreLoaded) { - auto it = m_Addresses.find(address); - if (it != m_Addresses.end()) { - return std::make_unique(it->second); +void AddressBookSubscriber::DownloadSubscriptionImpl() { + LogPrint(eLogInfo, + "AddressBookSubscriber: downloading subscription ", m_HTTP.GetURI().string(), + " ETag: ", m_HTTP.GetPreviousETag(), + " Last-Modified: ", m_HTTP.GetPreviousLastModified()); + bool download_result = m_HTTP.Download(); + if (download_result) { + std::stringstream stream(m_HTTP.GetDownloadedContents()); + if (!m_Book.ValidateSubscriptionThenSaveToStorage(stream)) { + // Error during validation or storage, download again later + download_result = false; } } - return nullptr; + m_Book.HostsDownloadComplete(download_result); } void AddressBook::HostsDownloadComplete( @@ -390,53 +251,32 @@ void AddressBook::HostsDownloadComplete( } } -void AddressBook::LoadSubscriptionFromPublisher() { - // First, ensure we have a storage instance ready - if (!m_Storage) - m_Storage = GetNewStorageInstance(); - // If so, see if we have hosts available - if (m_Storage->Load(m_Addresses) > 0) { - // If so, we don't need to download from a publisher - m_HostsAreLoaded = true; - return; - } - // If available, load default subscription from file - auto filename = GetDefaultSubscriptionFilename(); - std::ifstream file( - i2p::util::filesystem::GetFullPath(filename), - std::ofstream::in); - if (file.is_open()) { - if (!ValidateSubscriptionThenSaveToStorage(file)) { - LogPrint(eLogWarn, "AddressBook: invalid host in subscription"); - m_HostsAreLoaded = false; - } - m_HostsAreLoaded = true; - } else { - // If file not found, download from default address - LogPrint(eLogInfo, - "AddressBook: ", filename, " not found, ", - "attempting to download a subscription from default publisher"); - if (!m_SubscriberIsDownloading) { - m_SubscriberIsDownloading = true; - if (!m_DefaultSubscriber) - m_DefaultSubscriber = - std::make_unique( - *this, - GetDefaultPublisherURI()); - m_DefaultSubscriber->DownloadSubscription(); - } - } -} - bool AddressBook::ValidateSubscriptionThenSaveToStorage( std::istream& stream) { std::unique_lock lock(m_AddressBookMutex); - std::size_t num_addresses = 0; std::string host; + std::size_t num_addresses = 0; + // Save to subscription file if default file does not exist + // TODO(unassigned): extend this to append new hosts (when other subscriptions are used) + bool file_exists = false; + auto filename = GetSubscriptionFilename(); + std::ofstream file; + if (!boost::filesystem::exists(i2p::util::filesystem::GetFullPath(filename))) { // TODO(anonimal): throw gets into address book storage + file.open( + i2p::util::filesystem::GetFullPath(filename), + std::ofstream::out); + if (file) + file_exists = true; + } + // Read through stream, add to address book while (!stream.eof()) { getline(stream, host); if (!host.length()) continue; // skip empty line + // If default subscription is not written to disk, write to disk + if (file_exists) + file << host << std::endl; + // Parse host from address, save to address book std::size_t pos = host.find('='); if (pos != std::string::npos) { std::string name = host.substr(0, pos++); @@ -449,19 +289,64 @@ bool AddressBook::ValidateSubscriptionThenSaveToStorage( } else { LogPrint(eLogError, "AddressBook: malformed address ", addr, " for ", name); - return false; + m_SubscriptionIsLoaded = false; } } } - LogPrint(eLogInfo, - "AddressBook: ", num_addresses, " addresses processed"); - if (num_addresses > 0) { + LogPrint(eLogInfo, "AddressBook: ", num_addresses, " addresses processed"); + if (num_addresses) { + // Save list of hosts within subscription to a catalog file m_Storage->Save(m_Addresses); - m_HostsAreLoaded = true; + m_SubscriptionIsLoaded = true; + } + return m_SubscriptionIsLoaded; +} + +// For in-net download only +bool AddressBook::CheckAddressIdentHashFound( + const std::string& address, + i2p::data::IdentHash& ident) { + auto pos = address.find(".b32.i2p"); + if (pos != std::string::npos) { + if (!i2p::util::Base32ToByteStream(address.c_str(), pos, ident, 32)) { + LogPrint(eLogError, "AddressBook: invalid base32 address"); + return false; + } + return true; + } else { + pos = address.find(".i2p"); + if (pos != std::string::npos) { + auto ident_hash = GetLoadedAddressIdentHash(address); + if (ident_hash) { + ident = *ident_hash; + return true; + } else { + return false; + } + } } + // If not .b32, test for full base64 address + i2p::data::IdentityEx dest; + if (!dest.FromBase64(address)) + return false; // Invalid base64 address + ident = dest.GetIdentHash(); return true; } +// For in-net download only +std::unique_ptr AddressBook::GetLoadedAddressIdentHash( + const std::string& address) { + if (!m_SubscriptionIsLoaded) + LoadSubscriptionFromPublisher(); + if (m_SubscriptionIsLoaded) { + auto it = m_Addresses.find(address); + if (it != m_Addresses.end()) { + return std::make_unique(it->second); + } + } + return nullptr; +} + // Used only by HTTP Proxy void AddressBook::InsertAddressIntoStorage( const std::string& address, @@ -471,7 +356,7 @@ void AddressBook::InsertAddressIntoStorage( if (!m_Storage) m_Storage = GetNewStorageInstance(); m_Storage->AddAddress(ident); - m_Addresses[address] = ident.GetIdentHash(); + m_Addresses[address] = ident.GetIdentHash(); // TODO(anonimal): bounds checking LogPrint(eLogInfo, "AddressBook: ", address, "->", GetB32AddressFromIdentHash(ident.GetIdentHash()), " added"); @@ -488,7 +373,7 @@ void AddressBook::Stop() { LogPrint(eLogInfo, "AddressBook: subscription is downloading, waiting for termination"); for (std::size_t seconds = 0; - seconds < static_cast(SubscriberTimeout::Receive); + seconds < static_cast(i2p::util::http::Timeout::Receive); seconds++) { if (!m_SubscriberIsDownloading) { LogPrint(eLogInfo, "AddressBook: subscription download complete"); @@ -499,15 +384,11 @@ void AddressBook::Stop() { LogPrint(eLogError, "AddressBook: subscription download hangs"); m_SubscriberIsDownloading = false; } - // Save addressses to storage + // Save addresses to storage if (m_Storage) { m_Storage->Save(m_Addresses); m_Storage.reset(nullptr); } - // Kill subscriber - if (m_DefaultSubscriber) { - m_DefaultSubscriber.reset(nullptr); - } m_Subscribers.clear(); } diff --git a/src/client/address_book.h b/src/client/address_book.h index f6fc9e83..c36ea220 100644 --- a/src/client/address_book.h +++ b/src/client/address_book.h @@ -50,12 +50,13 @@ #include "destination.h" #include "router_context.h" #include "util/base64.h" +#include "util/http.h" #include "util/log.h" namespace i2p { namespace client { -/// @enum SubscriptionTimeout +/// @enum SubscriberTimeout /// @brief Constants used for timeout intervals when fetching subscriptions /// @notes Scoped to prevent namespace pollution (otherwise, purely stylistic) enum struct SubscriberTimeout : std::uint16_t { @@ -64,9 +65,6 @@ enum struct SubscriberTimeout : std::uint16_t { InitialRetry = 1, ContinuousUpdate = 720, // 12 hours ContinuousRetry = 5, - // Seconds - Request = 60, - Receive = 30, }; class AddressBookSubscriber; @@ -78,9 +76,8 @@ class AddressBook { AddressBook() : m_SharedLocalDestination(nullptr), m_Storage(nullptr), - m_DefaultSubscriber(nullptr), m_SubscriberUpdateTimer(nullptr), - m_HostsAreLoaded(false), + m_SubscriptionIsLoaded(false), m_SubscriberIsDownloading(false) {} /// @brief Stops address book implementation @@ -195,8 +192,8 @@ class AddressBook { /// @return Const reference to publishers filename /// @notes A publishers file holds a list of publisher addresses /// of whom publish 'subscriptions' that contain a list of hosts to .b32.i2p - const std::string& GetDefaultPublishersFilename() { - return AddressBookDefaults.at("PublisherFilename"); + const std::string& GetPublishersFilename() { + return AddressBookDefaults.at("PublishersFilename"); } /// @brief Gets default publishers URI @@ -209,7 +206,7 @@ class AddressBook { /// @brief Gets default subscription filename /// @return Const reference to subscription filename /// @notes Filename used by publishers when they publish a 'subscription' - const std::string& GetDefaultSubscriptionFilename() { + const std::string& GetSubscriptionFilename() { return AddressBookDefaults.at("SubscriptionFilename"); } @@ -246,19 +243,14 @@ class AddressBook { /// @brief Vector of unique pointers to respective subscriber implementation std::vector> m_Subscribers; - /// @var m_DefaultSubscriber - /// @brief Unique pointer to address book subscriber implementation - /// @notes Used if we don't have any publisher addresses on file - std::unique_ptr m_DefaultSubscriber; - /// @var m_SubscriberUpdateTimer /// @brief Unique pointer to Boost.Asio deadline_timer /// @details Handles all timer-related needs for subscription fetching std::unique_ptr m_SubscriberUpdateTimer; - /// @var m_HostsAreLoaded + /// @var m_SubscriptionIsLoaded /// @brief Are hosts loaded into memory? - std::atomic m_HostsAreLoaded; + std::atomic m_SubscriptionIsLoaded; /// @var m_SubscriberIsDownloading /// @brief Are subscriptions in the process of being downloaded? @@ -274,7 +266,7 @@ class AddressBookSubscriber { AddressBook& book, const std::string& uri) : m_Book(book), - m_URI(uri) {} + m_HTTP(uri) {} /// @brief Instantiates thread that fetches in-net subscriptions void DownloadSubscription(); @@ -284,12 +276,13 @@ class AddressBookSubscriber { /// @warning Must be run in separate thread void DownloadSubscriptionImpl(); - private: /// @var m_Book /// @brief Reference to address book implementation AddressBook& m_Book; - // Used for HTTP request // TODO(anonimal): remove when refactored with cpp-netlib - std::string m_URI, m_Etag, m_LastModified; + + /// @var m_HTTP + /// @brief HTTP instance for subscribing to publisher + i2p::util::http::HTTP m_HTTP; }; } // namespace client diff --git a/src/client/address_book_storage.cc b/src/client/address_book_storage.cc index 51c748b4..e4bbf802 100644 --- a/src/client/address_book_storage.cc +++ b/src/client/address_book_storage.cc @@ -105,7 +105,7 @@ void AddressBookFilesystemStorage::RemoveAddress( std::size_t AddressBookFilesystemStorage::Load( std::map& addresses) { std::size_t num = 0; - auto filename = GetAddressesFilename(); + auto filename = GetAddressesFilenamePath(); std::ifstream file(filename, std::ofstream::in); if (file.is_open()) { addresses.clear(); @@ -120,7 +120,7 @@ std::size_t AddressBookFilesystemStorage::Load( std::string addr = host.substr(pos); i2p::data::IdentHash ident; ident.FromBase32(addr); - addresses[name] = ident; + addresses[name] = ident; // TODO(anonimal): bounds checking num++; } } @@ -136,16 +136,18 @@ std::size_t AddressBookFilesystemStorage::Load( std::size_t AddressBookFilesystemStorage::Save( const std::map& addresses) { std::size_t num = 0; - auto filename = GetAddressesFilename(); + auto filename = GetAddressesFilenamePath(); std::ofstream file(filename, std::ofstream::out); if (file.is_open()) { for (auto it : addresses) { file << it.first << "," << it.second.ToBase32() << std::endl; num++; } - LogPrint(eLogInfo, "AddressBookFilesystemStorage: ", num, " addresses saved"); + LogPrint(eLogInfo, + "AddressBookFilesystemStorage: ", num, " addresses saved"); } else { - LogPrint(eLogError, "AddressBookFilesystemStorage: can't open file ", filename); + LogPrint(eLogError, + "AddressBookFilesystemStorage: can't open file ", filename); } return num; } diff --git a/src/client/address_book_storage.h b/src/client/address_book_storage.h index d924fed4..8461a38b 100644 --- a/src/client/address_book_storage.h +++ b/src/client/address_book_storage.h @@ -52,6 +52,7 @@ namespace i2p { namespace client { +// TODO(anonimal): remove this map in favor of a struct for defaults /// @var AddressBookDefaults /// @brief Default string constants used throughout address book const std::unordered_map AddressBookDefaults = { @@ -60,9 +61,11 @@ const std::unordered_map AddressBookDefaults = { // TODO(anonimal): "addresses" is confusing and so is its real purpose // (currently only used to verify that addresses have indeed been saved) { "AddressesFilename", "addresses.csv" }, - { "PublisherFilename", "publishers.txt" }, + { "PublishersFilename", "publishers.txt" }, // TODO(unassigned): replace with Monero's b32 publisher service { "PublisherURI", "https://downloads.getmonero.org/kovri/hosts.txt" }, + // Below is only used for testing in-net download (this is *not* our default subscription) + //{ "PublisherURI", "http://udhdrtrcetjm5sxzskjyr5ztpeszydbh4dpl3pl4utgqqw2v4jna.b32.i2p/hosts.txt" }, }; /// @class AddressBookStorage @@ -133,9 +136,11 @@ class AddressBookFilesystemStorage : public AddressBookStorage { const std::map& addresses); private: - /// @brief Gets addresses filename (file list of saved addresses) + // TODO(anonimal): move Get* defaults here, refactor filename versus full path + + /// @brief Gets addresses file (file list of saved addresses) /// @return Const reference to addresses filename - const std::string& GetAddressesFilename() { + const std::string GetAddressesFilenamePath() { return (GetAddressBookPath() / AddressBookDefaults.at("AddressesFilename")).string(); } diff --git a/src/client/i2p_tunnel/http_proxy.cc b/src/client/i2p_tunnel/http_proxy.cc index ffc809f2..e69c86b3 100644 --- a/src/client/i2p_tunnel/http_proxy.cc +++ b/src/client/i2p_tunnel/http_proxy.cc @@ -309,8 +309,8 @@ void HTTPProxyHandler::HandleJumpService() { } auto base64 = m_Path.substr(pos + m_JumpService.at(0).size()); // We must decode - i2p::util::http::URI uri; - base64 = uri.Decode(base64); + i2p::util::http::HTTP uri; + base64 = uri.HTTPProxyDecode(base64); // Insert into address book LogPrint(eLogDebug, "HTTPProxyHandler: jump service for ", m_Address, diff --git a/src/client/i2p_tunnel/i2p_tunnel.h b/src/client/i2p_tunnel/i2p_tunnel.h index 81c555ea..ad2462a7 100644 --- a/src/client/i2p_tunnel/i2p_tunnel.h +++ b/src/client/i2p_tunnel/i2p_tunnel.h @@ -42,7 +42,7 @@ #include #include "identity.h" -#include "streaming.h" +#include "client/streaming.h" #include "client/destination.h" #include "client/i2p_service.h" diff --git a/src/core/reseed.cc b/src/core/reseed.cc index 4885be22..3072d64e 100644 --- a/src/core/reseed.cc +++ b/src/core/reseed.cc @@ -187,11 +187,11 @@ bool Reseed::FetchStream( const std::string& url) { LogPrint(eLogInfo, "Reseed: fetching stream from ", url); // TODO(unassigned): abstract our downloading mechanism (see #168) - i2p::util::http::HTTP http; - if (!http.Download(url)) - return false; + i2p::util::http::HTTP http(url); + if (!http.Download()) + return false; // Download failed // Replace our stream with downloaded stream - m_Stream = http.m_Stream; + m_Stream.assign(http.GetDownloadedContents()); // TODO(unassigned): replace with constants if this isn't rewritten by #155/#168 return ((m_Stream.size() > 0) && (m_Stream.size() <= 128 * 1024)); // Arbitrary size in bytes } diff --git a/src/core/util/http.cc b/src/core/util/http.cc index a719933b..98952f47 100644 --- a/src/core/util/http.cc +++ b/src/core/util/http.cc @@ -32,10 +32,8 @@ #include "http.h" -// cpp-netlib -#define BOOST_NETWORK_ENABLE_HTTPS -#include -#include +#include +#include #include #include @@ -44,63 +42,70 @@ #include #include "router_context.h" +#include "client/client_context.h" +#include "client/address_book.h" #include "util/filesystem.h" namespace i2p { namespace util { -namespace http { +namespace http { // TODO(anonimal): consider removing this namespace (its not needed) -bool URI::Parse( +// TODO(unassigned): currently unused but will be useful +// without needing to create a new object for each given URI +bool HTTP::Download( const std::string& uri) { - boost::network::uri::uri URI(uri); - if (URI.is_valid()) { - m_Protocol.assign(URI.scheme()); - m_Host.assign(URI.host()); - m_Port.assign(URI.port()); - m_Path.assign(URI.path()); - m_Query.assign(URI.query()); - // Set defaults for AddressBook - // TODO(anonimal): this should disappear once we finish other refactoring - if (!m_Port.empty()) - return true; - if (m_Protocol == "https") { - m_Port.assign("443"); - } else { - m_Port.assign("80"); - } - return true; - } else { + SetURI(uri); + return Download(); +} + +bool HTTP::Download() { + if (!GetURI().is_valid()) { LogPrint(eLogError, "URI: invalid URI"); return false; } + // TODO(anonimal): ideally, we simply swapout the request/response handler + // with cpp-netlib so we don't need two separate functions + if (!HostIsI2P()) + return DownloadViaClearnet(); + return DownloadViaI2P(); } -// TODO(anonimal): research if cpp-netlib can do this better -// Used by HTTPProxy -std::string URI::Decode( - const std::string& data) { - std::string res(data); - for (size_t pos = res.find('%'); - pos != std::string::npos; - pos = res.find('%', pos + 1)) { - const char c = strtol(res.substr(pos + 1, 2).c_str(), NULL, 16); - res.replace(pos, 3, 1, c); +bool HTTP::HostIsI2P() { + auto uri = GetURI(); + if (uri.host().substr(uri.host().size() - 4) == ".i2p") { + if (!uri.port().empty()) + return true; + // We must assign a port if none was assigned (for internal reasons) + std::string port; + if (uri.scheme() == "https") + port.assign("443"); + else + port.assign("80"); + // If user supplied user:password, we must append @ + std::string user_info; + if (!uri.user_info().empty()) { + user_info.assign(uri.user_info() + "@"); + } + // TODO(anonimal): easier way with cpp-netlib? + std::string new_uri( + uri.scheme() + "://" + user_info + + uri.host() + ":" + port + + uri.path() + uri.query() + uri.fragment()); + SetURI(new_uri); + return true; + } else { + return false; } - return res; } -bool HTTP::Download( - const std::string& address) { - // Validate URI - URI uri; - if (!uri.Parse(address)) - return false; - namespace http = boost::network::http; - namespace network = boost::network; - http::client::options options; +bool HTTP::DownloadViaClearnet() { + auto uri = GetURI(); + // Create and set options + Options options; + options.timeout(45); // Java I2P defined // Ensure that we only download from certified reseed servers if (!i2p::context.ReseedSkipSSLCheck()) { - const std::string cert = uri.m_Host + ".crt"; + const std::string cert = uri.host() + ".crt"; const boost::filesystem::path cert_path = i2p::util::filesystem::GetSSLCertsPath() / cert; if (!boost::filesystem::exists(cert_path)) { LogPrint(eLogError, "HTTP: certificate unavailable: ", cert_path); @@ -109,87 +114,255 @@ bool HTTP::Download( // Set SSL options options .openssl_certificate(cert_path.string()) - .openssl_sni_hostname(uri.m_Host); + .openssl_sni_hostname(uri.host()); } + // Create client with options + Client client(options); try { - // Set extra options - options.timeout(45); // Java I2P defined - http::client client(options); - // Prepare and initiate session - http::client::request request(address); - request << network::header("User-Agent", "Wget/1.11.4"); // Java I2P defined - http::client::response response = client.get(request); - // Assign stream our downloaded contents - m_Stream.assign(http::body(response)); - } catch (const std::exception& e) { - LogPrint(eLogError, "HTTP: unable to complete download: ", e.what()); + // Create request + Request request(uri.string()); // A fully-qualified, completed URI + // Add required Java I2P defined user-agent + request << boost::network::header("User-Agent", "Wget/1.11.4"); + // Are we requesting the same file? + if (uri.path() == GetPreviousPath()) { + // Add ETag and Last-Modified headers if previously set + if (!GetPreviousETag().empty()) + request << boost::network::header("If-None-Match", GetPreviousETag()); + if (!GetPreviousLastModified().empty()) + request << boost::network::header("If-Modified-Since", GetPreviousLastModified()); + } else { + // Set path to test against for future download (if this is a single instance) + SetPath(uri.path()); + } + // Create response object, send request and receive response + Response response = client.get(request); + // Test HTTP response status code + switch (response.status()) { + case 200: // OK (also, cached version does not match, so we download) + // Parse response headers for ETag and Last-Modified + for (auto const& header : response.headers()) { + if (header.first == "ETag") { + if (header.second != GetPreviousETag()) + SetETag(header.second); // Set new ETag + } + if (header.first == "Last-Modified") { + if (header.second != GetPreviousLastModified()) + SetLastModified(header.second); // Set new Last-Modified + } + } + // Save downloaded content + SetDownloadedContents(boost::network::http::body(response)); + break; + case 304: // Not Modified + LogPrint(eLogInfo, "HTTP: no new updates available from ", uri.host()); + break; + default: + LogPrint(eLogWarn, "HTTP: response code: ", response.status()); + return false; + } + } catch (const std::exception& ex) { + LogPrint(eLogError, "HTTP: unable to complete download: ", ex.what()); return false; } return true; } -// TODO(anonimal): remove once AddressBookSubscription has been refactored -const std::string HTTP::Header( - const std::string& path, - const std::string& host, - const std::string& version) { +// TODO(anonimal): cpp-netlib refactor: request/response handler +bool HTTP::DownloadViaI2P() { + // Clear buffers (for when we're only using a single instance) + m_Request.clear(); + m_Response.clear(); + // Get URI + auto uri = GetURI(); + // Reference the only instantiated address book instance in the singleton client context + auto& address_book = i2p::client::context.GetAddressBook(); + // For identity hash of URI host + i2p::data::IdentHash ident; + // Get URI host's ident hash then find its lease-set + if (address_book.CheckAddressIdentHashFound(uri.host(), ident) + && address_book.GetSharedLocalDestination()) { + std::condition_variable new_data_received; + std::mutex new_data_received_mutex; + auto lease_set = address_book.GetSharedLocalDestination()->FindLeaseSet(ident); + // Lease-set not available, request + if (!lease_set) { + std::unique_lock lock(new_data_received_mutex); + address_book.GetSharedLocalDestination()->RequestDestination( + ident, + [&new_data_received, &lease_set]( + std::shared_ptr ls) { + lease_set = ls; + new_data_received.notify_all(); + }); + if (new_data_received.wait_for( + lock, + std::chrono::seconds( + static_cast(Timeout::Request))) + == std::cv_status::timeout) + LogPrint(eLogError, "HTTP: lease-set request timeout expired"); + } + // Test against requested lease-set + if (!lease_set) { + LogPrint(eLogError, + "HTTP: lease-set for address ", uri.host(), " not found"); + } else { + PrepareI2PRequest(); // TODO(anonimal): remove after refactor + // Send request + auto stream = + i2p::client::context.GetAddressBook().GetSharedLocalDestination()->CreateStream( + lease_set, + std::stoi(uri.port())); + stream->Send( + reinterpret_cast(m_Request.str().c_str()), + m_Request.str().length()); + // Receive response + std::array buf; // Arbitrary buffer size + bool end_of_data = false; + while (!end_of_data) { + stream->AsyncReceive( + boost::asio::buffer( + buf.data(), + buf.size()), + [&](const boost::system::error_code& ecode, + std::size_t bytes_transferred) { + if (bytes_transferred) + m_Response.write( + reinterpret_cast(buf.data()), + bytes_transferred); + if (ecode == boost::asio::error::timed_out || !stream->IsOpen()) + end_of_data = true; + new_data_received.notify_all(); + }, + static_cast(Timeout::Receive)); + std::unique_lock lock(new_data_received_mutex); + // Check if we timeout + if (new_data_received.wait_for( + lock, + std::chrono::seconds( + static_cast(Timeout::Request))) + == std::cv_status::timeout) + LogPrint(eLogError,"HTTP: in-net timeout expired"); + } + // Process remaining buffer + while (std::size_t len = stream->ReadSome(buf.data(), buf.size())) + m_Response.write(reinterpret_cast(buf.data()), len); + } + } else { + LogPrint(eLogError, "HTTP: can't resolve I2P address: ", uri.host()); + return false; + } + return ProcessI2PResponse(); // TODO(anonimal): remove after refactor +} + +// TODO(anonimal): remove after refactor +void HTTP::PrepareI2PRequest() { + // Create header + auto uri = GetURI(); std::string header = - "GET " + path + " HTTP/" + version + "\r\n" + - "Host: " + host + "\r\n" + + "GET " + uri.path() + " HTTP/1.1\r\n" + + "Host: " + uri.host() + "\r\n" + "Accept: */*\r\n" + "User-Agent: Wget/1.11.4\r\n" + - "Connection: close\r\n"; - return header; + "Connection: Close\r\n"; + // Add header to request + m_Request << header; + // Check fields + if (!GetPreviousETag().empty()) // Send previously set ETag if available + m_Request << "If-None-Match" << ": \"" << GetPreviousETag() << "\"\r\n"; + if (!GetPreviousLastModified().empty()) // Send previously set Last-Modified if available + m_Request << "If-Modified-Since" << ": " << GetPreviousLastModified() << "\r\n"; + m_Request << "\r\n"; // End of header } -// TODO(anonimal): remove once AddressBookSubscription has been refactored -const std::string HTTP::GetContent( - std::istream& response) { - std::string version, statusMessage; - response >> version; // HTTP version - response >> m_Status; // Response code status - std::getline(response, statusMessage); - if (m_Status == 200) { // OK - bool isChunked = false; - std::string header; - while (!response.eof() && header != "\r") { - std::getline(response, header); +// TODO(anonimal): remove after refactor +bool HTTP::ProcessI2PResponse() { + std::string http_version; + std::uint16_t response_code = 0; + m_Response >> http_version; + m_Response >> response_code; + if (response_code == 200) { + bool is_chunked = false; + std::string header, status_message; + std::getline(m_Response, status_message); + // Read response until end of header (new line) + while (!m_Response.eof() && header != "\r") { + std::getline(m_Response, header); auto colon = header.find(':'); if (colon != std::string::npos) { std::string field = header.substr(0, colon); - if (field == TRANSFER_ENCODING) - isChunked = (header.find("chunked", colon + 1) != std::string::npos); + header.resize(header.length() - 1); // delete \r + // We currently don't differentiate between strong or weak ETags + // We currently only care if an ETag is present + if (field == "ETag") + SetETag(header.substr(colon + 1)); + else if (field == "Last-Modified") + SetLastModified(header.substr(colon + 1)); + else if (field == "Transfer-Encoding") + is_chunked = !header.compare(colon + 1, std::string::npos, "chunked"); } } - std::stringstream ss; - if (isChunked) - MergeChunkedResponse(response, ss); - else - ss << response.rdbuf(); - return ss.str(); + // Get content after header + std::stringstream content; + while (!m_Response.eof()) { + // TODO(anonimal): this can be improved but since we + // won't need this after the refactor, it 'works' for now + std::getline(m_Response, header); + auto colon = header.find(':'); + if (colon != std::string::npos) + continue; + else + content << header << std::endl; + } + // Test if response is chunked / save downloaded contents + if (!content.eof()) { + if (is_chunked) { + std::stringstream merged; + MergeI2PChunkedResponse(content, merged); + SetDownloadedContents(merged.str()); + } else { + SetDownloadedContents(content.str()); + } + } + } else if (response_code == 304) { + LogPrint(eLogInfo, "HTTP: no new updates available from ", GetURI().host()); } else { - LogPrint("HTTP response ", m_Status); - return ""; + LogPrint(eLogWarn, "HTTP: response code: ", response_code); + return false; } + return true; } -// TODO(anonimal): remove once AddressBookSubscription has been refactored -void HTTP::MergeChunkedResponse( +// TODO(anonimal): remove after refactor +// Note: Transfer-Encoding is handled automatically by cpp-netlib +void HTTP::MergeI2PChunkedResponse( std::istream& response, std::ostream& merged) { while (!response.eof()) { - std::string hexLen; + std::string hex_len; int len; - std::getline(response, hexLen); - std::istringstream iss(hexLen); + std::getline(response, hex_len); + std::istringstream iss(hex_len); iss >> std::hex >> len; if (!len) break; auto buf = std::make_unique(len); response.read(buf.get(), len); merged.write(buf.get(), len); - std::getline(response, hexLen); // read \r\n after chunk + std::getline(response, hex_len); // read \r\n after chunk + } +} + +// TODO(anonimal): research if cpp-netlib can do this better +std::string HTTP::HTTPProxyDecode( + const std::string& data) { + std::string res(data); + for (size_t pos = res.find('%'); + pos != std::string::npos; + pos = res.find('%', pos + 1)) { + const char c = strtol(res.substr(pos + 1, 2).c_str(), NULL, 16); + res.replace(pos, 3, 1, c); } + return res; } } // namespace http diff --git a/src/core/util/http.h b/src/core/util/http.h index 011725eb..501a7ff8 100644 --- a/src/core/util/http.h +++ b/src/core/util/http.h @@ -33,6 +33,8 @@ #ifndef SRC_CORE_UTIL_HTTP_H_ #define SRC_CORE_UTIL_HTTP_H_ +// TODO(anonimal): cleanup headers + #include #include #include @@ -40,10 +42,12 @@ #include #include -#include -#include -#include +// cpp-netlib +#define BOOST_NETWORK_ENABLE_HTTPS +#include +#include +#include #include #include #include @@ -54,78 +58,208 @@ #include "log.h" #include "reseed.h" +// TODO(anonimal): fix certificates for i2*.manas.ca and downloads.getmonero.org + namespace i2p { namespace util { -namespace http { +namespace http { // TODO(anonimal): consider removing this namespace (its not needed) + +/// @enum Timeout +/// @brief Constants used for HTTP timeout lengths when downloading +/// @notes Scoped to prevent namespace pollution (otherwise, purely stylistic) +enum struct Timeout : std::uint8_t { + // Seconds + Request = 45, // Java I2P defined + Receive = 30, +}; + +/// @class HTTPStorage +/// @brief Storage for class HTTP +class HTTPStorage { + public: + /// @brief Set URI path to test against future downloads + /// @param path URI path + /// @notes Needed in conjunction with ETag + void SetPath( + const std::string& path) { + m_Path.assign(path); + } + + /// @brief Get previously set URI path + /// @notes Needed in conjunction with ETag + const std::string GetPreviousPath() { + return m_Path; + } + + /// @brief Set ETag member from response header + void SetETag( + const std::string& etag) { + m_ETag.assign(etag); + } + + /// @brief Get previously set ETag member from response header + /// @return ETag value + const std::string& GetPreviousETag() { + return m_ETag; + } + + /// @brief Set Last-Modified member from response header + void SetLastModified( + const std::string& last_modified) { + m_LastModified.assign(last_modified); + } -// TODO(anonimal): refactor everything in this namespace with cpp-netlib -// Will require review/refactor of AddressBook + /// @brief Get previously set Last-Modified member from response header + /// @return Last-Modified value + const std::string& GetPreviousLastModified() { + return m_LastModified; + } -/// @class URI -/// @brief Provides functionality for preparing a URI -class URI { + /// @brief Sets downloaded contents to stream + /// @notes Called after completed download + void SetDownloadedContents( + const std::string& stream) { + m_Stream.assign(stream); + } + + /// @brief Gets downloaded contents after successful download + /// @return String of downloaded contents + /// @notes Called after completed download + const std::string& GetDownloadedContents() { + return m_Stream; + } + + private: + /// @var m_Path + /// @brief Path value from a 1st request that can be tested against later + /// @notes If path is same as previous request, apply required header values + std::string m_Path; + + /// @var m_ETag + /// @brief ETag value from response header + /// @notes Used primarily for subscriptions. Can be extended to auto-update + std::string m_ETag; + + /// @var m_LastModified + /// @brief Last-Modified value from response header + /// @notes Used primarily for subscriptions. Can be extended to auto-update + std::string m_LastModified; + + /// @var m_Stream + /// @brief Downloaded contents + std::string m_Stream; // TODO(anonimal): consider refactoring into an actual stream +}; + +/// @class HTTP +/// @brief Provides functionality for implementing HTTP/S +/// @details URI is typically passed to ctor. Otherwise, see below. +/// @notes Vocabulary: +/// Clearnet: Connections made outside of the I2P network +/// In-net: Connections made within the I2P network +class HTTP : public HTTPStorage { public: - /// @param uri The URI string to be parsed - /// @note The default port is 80, for HTTPS it is 443 - /// @return False if URI is invalid, true if valid - bool Parse( + HTTP() {} // for HTTPProxy and tests + ~HTTP() {} + + HTTP(const std::string& uri) + : m_URI(uri) {} + + /// @brief Set cpp-netlib URI object if not set with ctor + /// @param uri String URI (complete) + void SetURI( + const std::string& uri) { + // Remove existing URI if set + if (!m_URI.string().empty()) { + boost::network::uri::uri new_uri; + m_URI.swap(new_uri); + } + // Set new URI + m_URI.append(uri); + } + + /// @brief Get initialized URI + /// @return cpp-netlib URI object + boost::network::uri::uri GetURI() { + return m_URI; + } + + /// @brief Tests if TLD is I2P and set default ports for in-net downloading + /// @return True if TLD is .i2p + /// @notes The default port is 80, for HTTPS it is 443 + /// @notes Removing this will require refactoring stream implementation + bool HostIsI2P(); + + /// @brief Downloads parameter URI + /// @param uri String URI + /// @details Sets member URI with param uri, calls Download() + /// @return Bool result of Download() + /// @notes Only used if URI not initialized with ctor + bool Download( const std::string& uri); + /// @brief Download wrapper function for clearnet and in-net download + /// @return False on failure + bool Download(); + + /// @brief Decode URI in HTTP proxy request /// @return The decoded URI as string - std::string Decode( + std::string HTTPProxyDecode( const std::string& data); - // TODO(anonimal): consider Get/Set functions if we keep class URI - std::string m_Protocol, m_Host, m_Port, m_Path, m_Query; -}; + private: + /// @brief Downloads over clearnet + /// @return False on failure + bool DownloadViaClearnet(); + + /// @brief Downloads within I2P + /// @return False on failure + /// @notes Used for address book and for future in-net autoupdates + bool DownloadViaI2P(); + + private: + /// @var m_URI + /// @brief cpp-netlib URI instance + boost::network::uri::uri m_URI; + + // TODO(anonimal): consider removing typedefs after refactor + // TODO(anonimal): remove the following notes after refactor + + /// @brief HTTP client object + /// @notes Currently only applies to clearnet download + typedef boost::network::http::client Client; + + /// @brief HTTP client options object (timeout, SNI, etc.) + /// @notes Currently only applies to clearnet download + typedef boost::network::http::client::options Options; + + /// @brief HTTP client request object (header, etc.) + /// @notes Currently only applies to clearnet download + typedef boost::network::http::client::request Request; + + /// @brief HTTP client response object (body, status, etc.) + /// @notes Currently only applies to clearnet download + typedef boost::network::http::client::response Response; -/** - * Provides functionality for implementing HTTP - */ -class HTTP { public: - /** - * @return the result of the download, or an empty string if it fails - */ - bool Download( - const std::string& address); - - /** - * Header for HTTP requests. - * @return a string of the complete header - * @warning this function does NOT append an additional \r\n - */ - const std::string Header( - const std::string& path, - const std::string& host, - const std::string& version); - - /** - * @return the content of the given HTTP stream without headers - */ - const std::string GetContent( - std::istream& response); - - /** - * Merge chunks of an HTTP response. - */ - void MergeChunkedResponse( + // TODO(anonimal): remove after refactor + /// @brief Prepares header for in-net request + void PrepareI2PRequest(); + + // TODO(anonimal): remove after refactor + /// @brief Process in-net HTTP response + /// @return True if processing was successful + bool ProcessI2PResponse(); + + // TODO(anonimal): remove after refactor + /// @brief Merge chunks of an in-net HTTP response + void MergeI2PChunkedResponse( std::istream& response, std::ostream& merged); - public: - /** - * Used almost exclusively by Addressbook - */ - const std::string ETAG = "ETag"; - const std::string IF_NONE_MATCH = "If-None-Match"; - const std::string IF_MODIFIED_SINCE = "If-Modified-Since"; - const std::string LAST_MODIFIED = "Last-Modified"; - const std::string TRANSFER_ENCODING = "Transfer-Encoding"; - - public: - std::string m_Stream; // Downloaded stream - std::uint16_t m_Status; // HTTP Response Code + private: + // TODO(anonimal): remove after refactor + /// @brief In-net HTTP request and response + std::stringstream m_Request, m_Response; }; } // namespace http diff --git a/src/tests/unit_tests/core/util/http.cc b/src/tests/unit_tests/core/util/http.cc index c11518c5..51d1ae25 100644 --- a/src/tests/unit_tests/core/util/http.cc +++ b/src/tests/unit_tests/core/util/http.cc @@ -34,26 +34,29 @@ #include "util/http.h" -#include -#include - BOOST_AUTO_TEST_SUITE(HTTPUtilityTests) -i2p::util::http::URI uri; +i2p::util::http::HTTP http; BOOST_AUTO_TEST_CASE(UriParse) { // Note: cpp-netlib has better tests. // We simply test our implementation here. - BOOST_CHECK(uri.Parse("https://domain.org:8443/path/file.type")); - BOOST_CHECK(!uri.Parse("3;axc807uasdfh123m,nafsdklfj;;klj0a9u01q3")); + http.SetURI("https://domain.org:8443/path/file.type"); + BOOST_CHECK(http.GetURI().is_valid()); + + http.SetURI("3;axc807uasdfh123m,nafsdklfj;;klj0a9u01q3"); + BOOST_CHECK(!http.GetURI().is_valid()); + + http.SetURI("http://username:password@udhdrtrcetjm5sxzskjyr5ztpeszydbh4dpl3pl4utgqqw2v4jna.b32.i2p/hosts.txt"); + BOOST_CHECK(http.GetURI().is_valid() && http.HostIsI2P()); } BOOST_AUTO_TEST_CASE(DecodeEmptyUri) { - BOOST_CHECK_EQUAL(uri.Decode(""), ""); + BOOST_CHECK_EQUAL(http.HTTPProxyDecode(""), ""); } BOOST_AUTO_TEST_CASE(DecodeUri) { - BOOST_CHECK_EQUAL(uri.Decode("%20"), " "); + BOOST_CHECK_EQUAL(http.HTTPProxyDecode("%20"), " "); } BOOST_AUTO_TEST_SUITE_END()