diff --git a/cmake/setup.cmake b/cmake/setup.cmake index e1e5c9cd..c1a57575 100644 --- a/cmake/setup.cmake +++ b/cmake/setup.cmake @@ -102,6 +102,7 @@ function(with_vocab target) target_link_libraries(${target} PUBLIC OpenSSL::Crypto + OpenSSL::SSL ) endif() find_package(nlohmann_json) diff --git a/component/block_http_request.h b/component/block_http_request.h index a34d88b7..2fbfc44f 100644 --- a/component/block_http_request.h +++ b/component/block_http_request.h @@ -3,7 +3,7 @@ #include -#include +#include #include namespace network { @@ -23,7 +23,7 @@ class BlockHttpRequest : public std::enable_shared_from_this { std::unique_ptr loader_; public: - using HttpCompleteCallback = ipfs::ContextApi::HttpCompleteCallback; + using HttpCompleteCallback = ctx::HttpApi::HttpCompleteCallback; BlockHttpRequest(ipfs::HttpRequestDescription, HttpCompleteCallback); ~BlockHttpRequest() noexcept; @@ -33,7 +33,7 @@ class BlockHttpRequest : public std::enable_shared_from_this { ipfs::HttpRequestDescription const inf_; HttpCompleteCallback callback_; std::string status_line_; - ContextApi::HeaderAccess header_accessor_ = [](auto) { + ctx::HttpApi::HeaderAccess header_accessor_ = [](auto) { return std::string{}; }; diff --git a/component/chromium_http.cc b/component/chromium_http.cc new file mode 100644 index 00000000..4e93da9a --- /dev/null +++ b/component/chromium_http.cc @@ -0,0 +1,14 @@ +#include "chromium_http.h" + +#include "block_http_request.h" + +using Self = ipfs::ChromiumHttp; + +Self::ChromiumHttp(network::mojom::URLLoaderFactory& lf) + : loader_factory_{&lf} {} + +void Self::SendHttpRequest(ipfs::ctx::HttpApi::HttpRequestDescription desc, + ipfs::ctx::HttpApi::HttpCompleteCallback cb) const { + auto ptr = std::make_shared(desc, cb); + ptr->send(loader_factory_); +} \ No newline at end of file diff --git a/component/chromium_http.h b/component/chromium_http.h new file mode 100644 index 00000000..cd4ec271 --- /dev/null +++ b/component/chromium_http.h @@ -0,0 +1,23 @@ +#ifndef IPFS_CHROMIUM_CHROMIUM_HTTP_H +#define IPFS_CHROMIUM_CHROMIUM_HTTP_H + +#include + +#include + +namespace network::mojom { +class URLLoaderFactory; +} // namespace network::mojom + +namespace ipfs { +class ChromiumHttp : public ctx::HttpApi { + raw_ptr loader_factory_ = nullptr; + + public: + void SendHttpRequest(HttpRequestDescription, + HttpCompleteCallback cb) const override; + ChromiumHttp(network::mojom::URLLoaderFactory&); +}; +} // namespace ipfs + +#endif // IPFS_CHROMIUM_CHROMIUM_HTTP_H diff --git a/component/chromium_ipfs_context.cc b/component/chromium_ipfs_context.cc index 6b68cf65..3e91cdaf 100644 --- a/component/chromium_ipfs_context.cc +++ b/component/chromium_ipfs_context.cc @@ -2,9 +2,9 @@ #include "block_http_request.h" #include "chromium_cbor_adapter.h" +#include "chromium_http.h" #include "chromium_json_adapter.h" #include "inter_request_state.h" -#include "ipfs_client/crypto_api.h" #include "preferences.h" #include @@ -27,8 +27,8 @@ using Self = ipfs::ChromiumIpfsContext; -void Self::SetLoaderFactory(network::mojom::URLLoaderFactory& lf) { - loader_factory_ = &lf; +void Self::SetupHttp(network::mojom::URLLoaderFactory& lf) { + http_api_ = std::make_unique(lf); } std::string Self::MimeType(std::string extension, @@ -72,12 +72,6 @@ void Self::SendDnsTextRequest(std::string host, auto* nc = state_->network_context(); dns_reqs_[host] = std::make_unique(host, res, don_wrap, nc); } -void Self::SendHttpRequest(HttpRequestDescription req_inf, - HttpCompleteCallback cb) const { - DCHECK(loader_factory_); - auto ptr = std::make_shared(req_inf, cb); - ptr->send(loader_factory_); -} auto Self::ParseCbor(ipfs::ContextApi::ByteView bytes) const -> std::unique_ptr { cbor::Reader::Config cfg; diff --git a/component/chromium_ipfs_context.h b/component/chromium_ipfs_context.h index 216907d8..85dae85b 100644 --- a/component/chromium_ipfs_context.h +++ b/component/chromium_ipfs_context.h @@ -29,7 +29,6 @@ class IpfsRequest; class NetworkRequestor; class ChromiumIpfsContext final : public ContextApi { - raw_ptr loader_factory_ = nullptr; raw_ref state_; std::map> dns_reqs_; GatewayRates rates_; @@ -41,8 +40,6 @@ class ChromiumIpfsContext final : public ContextApi { void SendDnsTextRequest(std::string, DnsTextResultsCallback, DnsTextCompleteCallback) override; - void SendHttpRequest(HttpRequestDescription req_inf, - HttpCompleteCallback cb) const override; std::unique_ptr ParseCbor(ByteView) const override; std::unique_ptr ParseJson(std::string_view) const override; @@ -55,7 +52,7 @@ class ChromiumIpfsContext final : public ContextApi { public: ChromiumIpfsContext(InterRequestState&, PrefService* prefs); ~ChromiumIpfsContext() noexcept override; - void SetLoaderFactory(network::mojom::URLLoaderFactory&); + void SetupHttp(network::mojom::URLLoaderFactory&); }; } // namespace ipfs diff --git a/component/ipfs_url_loader.cc b/component/ipfs_url_loader.cc index cf60f0b2..eb85fd2a 100644 --- a/component/ipfs_url_loader.cc +++ b/component/ipfs_url_loader.cc @@ -74,7 +74,7 @@ void ipfs::IpfsUrlLoader::StartRequest( auto path = resource_request.url.path(); auto abs_path = "/" + ns + "/" + cid_str + path; me->root_ = cid_str; - me->api_->SetLoaderFactory(*(me->lower_loader_factory_)); + me->api_->SetupHttp(*(me->lower_loader_factory_)); auto whendone = [me](IpfsRequest const& req, ipfs::Response const& res) { VLOG(2) << "whendone(" << req.path().to_string() << ',' << res.status_ << ',' << res.body_.size() << "B mime=" << res.mime_ << ')'; diff --git a/component/patches/122.0.6226.2.patch b/component/patches/122.0.6226.2.patch deleted file mode 100644 index d09aad3b..00000000 --- a/component/patches/122.0.6226.2.patch +++ /dev/null @@ -1,880 +0,0 @@ -diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn -index 516f8d3bc275f..3188eaaf003a1 100644 ---- a/chrome/browser/BUILD.gn -+++ b/chrome/browser/BUILD.gn -@@ -39,6 +39,7 @@ import("//rlz/buildflags/buildflags.gni") - import("//sandbox/features.gni") - import("//testing/libfuzzer/fuzzer_test.gni") - import("//third_party/blink/public/public_features.gni") -+import("//third_party/ipfs_client/args.gni") - import("//third_party/protobuf/proto_library.gni") - import("//third_party/webrtc/webrtc.gni") - import("//third_party/widevine/cdm/widevine.gni") -@@ -2618,6 +2619,14 @@ static_library("browser") { - ] - } - -+ if (enable_ipfs) { -+ sources += [ -+ "ipfs_extra_parts.cc", -+ "ipfs_extra_parts.h", -+ ] -+ deps += [ "//components/ipfs" ] -+ } -+ - if (is_chromeos_ash) { - deps += [ "//chrome/browser/screen_ai:screen_ai_dlc_installer" ] - } -diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc -index cba0c0e2bc1c2..91e14f34800de 100644 ---- a/chrome/browser/about_flags.cc -+++ b/chrome/browser/about_flags.cc -@@ -214,6 +214,7 @@ - #include "third_party/blink/public/common/features_generated.h" - #include "third_party/blink/public/common/forcedark/forcedark_switches.h" - #include "third_party/blink/public/common/switches.h" -+#include "third_party/ipfs_client/ipfs_buildflags.h" - #include "ui/accessibility/accessibility_features.h" - #include "ui/accessibility/accessibility_switches.h" - #include "ui/base/ozone_buildflags.h" -@@ -310,6 +311,10 @@ - #include "extensions/common/switches.h" - #endif // BUILDFLAG(ENABLE_EXTENSIONS) - -+#if BUILDFLAG(ENABLE_IPFS) -+#include "components/ipfs/ipfs_features.h" -+#endif -+ - #if BUILDFLAG(ENABLE_PDF) - #include "pdf/pdf_features.h" - #endif -@@ -9379,6 +9384,14 @@ const FeatureEntry kFeatureEntries[] = { - flag_descriptions::kOmitCorsClientCertDescription, kOsAll, - FEATURE_VALUE_TYPE(network::features::kOmitCorsClientCert)}, - -+#if BUILDFLAG(ENABLE_IPFS) -+ {"enable-ipfs", -+ flag_descriptions::kEnableIpfsName, -+ flag_descriptions::kEnableIpfsDescription, -+ kOsMac | kOsWin | kOsLinux,//TODO: These are the only variants currently getting built, but that is not likely to remain the case -+ FEATURE_VALUE_TYPE(ipfs::kEnableIpfs)}, -+#endif -+ - {"use-idna2008-non-transitional", - flag_descriptions::kUseIDNA2008NonTransitionalName, - flag_descriptions::kUseIDNA2008NonTransitionalDescription, kOsAll, -diff --git a/chrome/browser/autocomplete/chrome_autocomplete_scheme_classifier.cc b/chrome/browser/autocomplete/chrome_autocomplete_scheme_classifier.cc -index 4c88614c68c25..f8bb12a3b0c2e 100644 ---- a/chrome/browser/autocomplete/chrome_autocomplete_scheme_classifier.cc -+++ b/chrome/browser/autocomplete/chrome_autocomplete_scheme_classifier.cc -@@ -10,6 +10,8 @@ - #include "chrome/browser/custom_handlers/protocol_handler_registry_factory.h" - #include "chrome/browser/external_protocol/external_protocol_handler.h" - #include "chrome/browser/profiles/profile.h" -+#include "third_party/ipfs_client/ipfs_buildflags.h" -+ - #if BUILDFLAG(IS_ANDROID) - #include "chrome/browser/profiles/profile_android.h" - #endif -@@ -18,6 +20,9 @@ - #include "chrome/browser/ui/android/omnibox/jni_headers/ChromeAutocompleteSchemeClassifier_jni.h" - #endif - #include "components/custom_handlers/protocol_handler_registry.h" -+#if BUILDFLAG(ENABLE_IPFS) -+#include "components/ipfs/ipfs_features.h" -+#endif - #include "content/public/common/url_constants.h" - #include "url/url_util.h" - -@@ -55,12 +60,20 @@ ChromeAutocompleteSchemeClassifier::GetInputTypeForScheme( - if (scheme.empty()) { - return metrics::OmniboxInputType::EMPTY; - } -- if (base::IsStringASCII(scheme) && -- (ProfileIOData::IsHandledProtocol(scheme) || -- base::EqualsCaseInsensitiveASCII(scheme, content::kViewSourceScheme) || -- base::EqualsCaseInsensitiveASCII(scheme, url::kJavaScriptScheme) || -- base::EqualsCaseInsensitiveASCII(scheme, url::kDataScheme))) { -- return metrics::OmniboxInputType::URL; -+ if (base::IsStringASCII(scheme)) { -+ if (ProfileIOData::IsHandledProtocol(scheme) || -+ base::EqualsCaseInsensitiveASCII(scheme, content::kViewSourceScheme) || -+ base::EqualsCaseInsensitiveASCII(scheme, url::kJavaScriptScheme) || -+ base::EqualsCaseInsensitiveASCII(scheme, url::kDataScheme)) { -+ return metrics::OmniboxInputType::URL; -+ } -+#if BUILDFLAG(ENABLE_IPFS) -+ if (base::FeatureList::IsEnabled(ipfs::kEnableIpfs) && -+ (base::EqualsCaseInsensitiveASCII(scheme, "ipfs") || base::EqualsCaseInsensitiveASCII(scheme, "ipns")) -+ ) { -+ return metrics::OmniboxInputType::URL; -+ } -+#endif - } - - // Also check for schemes registered via registerProtocolHandler(), which -diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc -index 23ee785c34a30..8550b43c61f2f 100644 ---- a/chrome/browser/chrome_content_browser_client.cc -+++ b/chrome/browser/chrome_content_browser_client.cc -@@ -376,6 +376,7 @@ - #include "third_party/blink/public/common/switches.h" - #include "third_party/blink/public/mojom/browsing_topics/browsing_topics.mojom.h" - #include "third_party/blink/public/public_buildflags.h" -+#include "third_party/ipfs_client/ipfs_buildflags.h" - #include "third_party/widevine/cdm/buildflags.h" - #include "ui/base/clipboard/clipboard_format_type.h" - #include "ui/base/l10n/l10n_util.h" -@@ -499,6 +500,13 @@ - #include "chrome/browser/fuchsia/chrome_browser_main_parts_fuchsia.h" - #endif - -+#if BUILDFLAG(ENABLE_IPFS) -+#include "chrome/browser/ipfs_extra_parts.h" -+#include "components/ipfs/interceptor.h" -+#include "components/ipfs/ipfs_features.h" -+#include "components/ipfs/url_loader_factory.h" -+#endif -+ - #if BUILDFLAG(IS_CHROMEOS) - #include "base/debug/leak_annotations.h" - #include "chrome/browser/apps/app_service/app_install/app_install_navigation_throttle.h" -@@ -1711,6 +1719,11 @@ ChromeContentBrowserClient::CreateBrowserMainParts(bool is_integration_test) { - main_parts->AddParts( - std::make_unique()); - -+#if BUILDFLAG(ENABLE_IPFS) -+ if (base::FeatureList::IsEnabled(ipfs::kEnableIpfs)) { -+ main_parts->AddParts(std::make_unique()); -+ } -+#endif - return main_parts; - } - -@@ -6057,12 +6070,25 @@ void ChromeContentBrowserClient:: - const absl::optional& request_initiator_origin, - NonNetworkURLLoaderFactoryMap* factories) { - #if BUILDFLAG(IS_CHROMEOS_ASH) || BUILDFLAG(ENABLE_EXTENSIONS) || \ -- !BUILDFLAG(IS_ANDROID) -+ !BUILDFLAG(IS_ANDROID) || BUILDFLAG(ENABLE_IPFS) - content::RenderFrameHost* frame_host = - RenderFrameHost::FromID(render_process_id, render_frame_id); - WebContents* web_contents = WebContents::FromRenderFrameHost(frame_host); - #endif // BUILDFLAG(IS_CHROMEOS_ASH) || BUILDFLAG(ENABLE_EXTENSIONS) || \ - // !BUILDFLAG(IS_ANDROID) -+#if BUILDFLAG(ENABLE_IPFS) -+ if (base::FeatureList::IsEnabled(ipfs::kEnableIpfs)) { -+ network::mojom::URLLoaderFactory* default_factory = g_browser_process->system_network_context_manager()->GetURLLoaderFactory(); -+ auto* context = web_contents->GetBrowserContext(); -+ ipfs::IpfsURLLoaderFactory::Create( -+ factories, -+ context, -+ default_factory, -+ GetSystemNetworkContext(), -+ Profile::FromBrowserContext(context)->GetPrefs() -+ ); -+ } -+#endif // BUILDFLAG(ENABLE_IPFS) - - #if BUILDFLAG(IS_CHROMEOS_ASH) - if (web_contents) { -@@ -6204,6 +6230,11 @@ ChromeContentBrowserClient::WillCreateURLLoaderRequestInterceptors( - scoped_refptr navigation_response_task_runner) { - std::vector> - interceptors; -+#if BUILDFLAG(ENABLE_IPFS) -+ if (base::FeatureList::IsEnabled(ipfs::kEnableIpfs)) { -+ interceptors.push_back(std::make_unique(g_browser_process->system_network_context_manager()->GetURLLoaderFactory(), GetSystemNetworkContext())); -+ } -+#endif - #if BUILDFLAG(ENABLE_OFFLINE_PAGES) - interceptors.push_back( - std::make_unique( -diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json -index ae98bbb22d81a..c85d48a4a77c1 100644 ---- a/chrome/browser/flag-metadata.json -+++ b/chrome/browser/flag-metadata.json -@@ -2946,6 +2946,11 @@ - "owners": [ "hanxi@chromium.org", "wychen@chromium.org" ], - "expiry_milestone": 130 - }, -+ { -+ "name": "enable-ipfs", -+ "owners": [ "//components/ipfs/OWNERS" ], -+ "expiry_milestone": 150 -+ }, - { - "name": "enable-isolated-sandboxed-iframes", - "owners": [ "wjmaclean@chromium.org", "alexmos@chromium.org", "creis@chromium.org" ], -diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc -index ec5799f66a720..7be72282040c1 100644 ---- a/chrome/browser/flag_descriptions.cc -+++ b/chrome/browser/flag_descriptions.cc -@@ -284,6 +284,11 @@ const char kEnableBenchmarkingDescription[] = - "after 3 restarts. On the third restart, the flag will appear to be off " - "but the effect is still active."; - -+#if BUILDFLAG(ENABLE_IPFS) -+extern const char kEnableIpfsName[] = "Enable IPFS"; -+extern const char kEnableIpfsDescription[] = "Enable ipfs:// and ipns:// URLs"; -+#endif -+ - const char kPreloadingOnPerformancePageName[] = - "Preloading Settings on Performance Page"; - const char kPreloadingOnPerformancePageDescription[] = -diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h -index 3224ecf17e6c8..14976704c1345 100644 ---- a/chrome/browser/flag_descriptions.h -+++ b/chrome/browser/flag_descriptions.h -@@ -23,6 +23,7 @@ - #include "pdf/buildflags.h" - #include "printing/buildflags/buildflags.h" - #include "third_party/blink/public/common/buildflags.h" -+#include "third_party/ipfs_client/ipfs_buildflags.h" - - // This file declares strings used in chrome://flags. These messages are not - // translated, because instead of end-users they target Chromium developers and -@@ -176,6 +177,11 @@ extern const char kDownloadWarningImprovementsDescription[]; - extern const char kEnableBenchmarkingName[]; - extern const char kEnableBenchmarkingDescription[]; - -+#if BUILDFLAG(ENABLE_IPFS) -+extern const char kEnableIpfsName[]; -+extern const char kEnableIpfsDescription[]; -+#endif -+ - #if BUILDFLAG(USE_FONTATIONS_BACKEND) - extern const char kFontationsFontBackendName[]; - extern const char kFontationsFontBackendDescription[]; -diff --git a/chrome/browser/prefs/browser_prefs.cc b/chrome/browser/prefs/browser_prefs.cc -index e4e82350ade4a..7023d6c5e07c3 100644 ---- a/chrome/browser/prefs/browser_prefs.cc -+++ b/chrome/browser/prefs/browser_prefs.cc -@@ -189,6 +189,7 @@ - #include "printing/buildflags/buildflags.h" - #include "rlz/buildflags/buildflags.h" - #include "third_party/abseil-cpp/absl/types/optional.h" -+#include "third_party/ipfs_client/ipfs_buildflags.h" - - #if BUILDFLAG(ENABLE_BACKGROUND_MODE) - #include "chrome/browser/background/background_mode_manager.h" -@@ -233,6 +234,11 @@ - #include "chrome/browser/pdf/pdf_pref_names.h" - #endif // BUILDFLAG(ENABLE_PDF) - -+#if BUILDFLAG(ENABLE_IPFS) -+#include "components/ipfs/ipfs_features.h" -+#include "components/ipfs/preferences.h" -+#endif -+ - #if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE) - #include "chrome/browser/screen_ai/pref_names.h" - #endif -@@ -1685,6 +1691,11 @@ void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry, - IncognitoModePrefs::RegisterProfilePrefs(registry); - invalidation::PerUserTopicSubscriptionManager::RegisterProfilePrefs(registry); - invalidation::InvalidatorRegistrarWithMemory::RegisterProfilePrefs(registry); -+#if BUILDFLAG(ENABLE_IPFS) -+ if (base::FeatureList::IsEnabled(ipfs::kEnableIpfs)) { -+ ipfs::RegisterPreferences(registry); -+ } -+#endif - language::LanguagePrefs::RegisterProfilePrefs(registry); - login_detection::prefs::RegisterProfilePrefs(registry); - lookalikes::RegisterProfilePrefs(registry); -diff --git a/chrome/common/chrome_content_client.cc b/chrome/common/chrome_content_client.cc -index 246ec9c5c911f..5d66d133a7907 100644 ---- a/chrome/common/chrome_content_client.cc -+++ b/chrome/common/chrome_content_client.cc -@@ -296,6 +296,12 @@ void ChromeContentClient::AddAdditionalSchemes(Schemes* schemes) { - #if BUILDFLAG(IS_ANDROID) - schemes->local_schemes.push_back(url::kContentScheme); - #endif -+ for ( const char* ip_s : {"ipfs", "ipns"} ) { -+ schemes->standard_schemes.push_back(ip_s); -+ schemes->cors_enabled_schemes.push_back(ip_s); -+ schemes->secure_schemes.push_back(ip_s); -+ schemes->csp_bypassing_schemes.push_back(ip_s); -+ } - } - - std::u16string ChromeContentClient::GetLocalizedString(int message_id) { -diff --git a/components/cbor/reader.cc b/components/cbor/reader.cc -index 306ba52fa4944..6b13b3a679a65 100644 ---- a/components/cbor/reader.cc -+++ b/components/cbor/reader.cc -@@ -22,7 +22,7 @@ - namespace cbor { - - namespace constants { --const char kUnsupportedMajorType[] = "Unsupported major type."; -+const char kUnsupportedMajorType[] = "Unsupported major type operation."; - } - - namespace { -@@ -156,7 +156,11 @@ absl::optional Reader::DecodeCompleteDataItem(const Config& config, - case Value::Type::FLOAT_VALUE: - // Floating point values also go here since they are also type 7. - return DecodeToSimpleValueOrFloat(*header, config); -- case Value::Type::TAG: // We explicitly don't support TAG. -+ case Value::Type::TAG: -+ if (config.parse_tags) { -+ return ReadTagContent(*header, config, max_nesting_level); -+ } -+ break; - case Value::Type::NONE: - case Value::Type::INVALID_UTF8: - break; -@@ -347,6 +351,17 @@ absl::optional Reader::ReadByteStringContent( - return Value(std::move(cbor_byte_string)); - } - -+absl::optional Reader::ReadTagContent( -+ const Reader::DataItemHeader& header, -+ const Config& config, -+ int max_nesting_level) { -+ auto tagged_content = DecodeCompleteDataItem(config, max_nesting_level); -+ if (tagged_content.has_value()) { -+ tagged_content.value().SetTag(header.value); -+ } -+ return tagged_content; -+} -+ - absl::optional Reader::ReadArrayContent( - const Reader::DataItemHeader& header, - const Config& config, -diff --git a/components/cbor/reader.h b/components/cbor/reader.h -index f0b43a5517528..a57e277a1bc66 100644 ---- a/components/cbor/reader.h -+++ b/components/cbor/reader.h -@@ -130,6 +130,11 @@ class CBOR_EXPORT Reader { - // during decoding will set raise the `UNSUPPORTED_FLOATING_POINT_VALUE` - // error. - bool allow_floating_point = false; -+ -+ // If the parser encounters a TAG element, should it be parsed out and -+ // the tag value saved (true), or should the entire node and its content -+ // be discarded (false) -+ bool parse_tags = false; - }; - - Reader(const Reader&) = delete; -@@ -204,6 +209,9 @@ class CBOR_EXPORT Reader { - absl::optional ReadMapContent(const DataItemHeader& header, - const Config& config, - int max_nesting_level); -+ absl::optional ReadTagContent(const DataItemHeader& header, -+ const Config& config, -+ int max_nesting_level); - absl::optional ReadByte(); - absl::optional> ReadBytes(uint64_t num_bytes); - bool IsKeyInOrder(const Value& new_key, -diff --git a/components/cbor/reader_unittest.cc b/components/cbor/reader_unittest.cc -index 83d44a48d6dfa..a6ec5299b3241 100644 ---- a/components/cbor/reader_unittest.cc -+++ b/components/cbor/reader_unittest.cc -@@ -1451,5 +1451,42 @@ TEST(CBORReaderTest, AllowInvalidUTF8) { - EXPECT_FALSE(cbor); - EXPECT_EQ(Reader::DecoderError::INVALID_UTF8, error); - } -+TEST(CBORReaderTest, RejectsTagUnderDefaultConfig) { -+ static const uint8_t kTaggedCbor[] = { -+ 0xd8, 0x2a, 0x58, 0x25, 0x00, 0x01, 0x71, 0x12, 0x20, 0x69, 0xea, 0x07, -+ 0x40, 0xf9, 0x80, 0x7a, 0x28, 0xf4, 0xd9, 0x32, 0xc6, 0x2e, 0x7c, 0x1c, -+ 0x83, 0xbe, 0x05, 0x5e, 0x55, 0x07, 0x2c, 0x90, 0x26, 0x6a, 0xb3, 0xe7, -+ 0x9d, 0xf6, 0x3a, 0x36, 0x5b -+ }; -+ Reader::Config config; -+ absl::optional cbor = Reader::Read(kTaggedCbor, config); -+ EXPECT_FALSE(cbor.has_value()); -+} -+TEST(CBORReaderTest, ReadsTagWhenConfiguredToDoSo) { -+ static const uint8_t kTaggedCbor[] = { -+ 0xd8, 0x2a, 0x58, 0x25, 0x00, 0x01, 0x71, 0x12, 0x20, 0x69, 0xea, 0x07, -+ 0x40, 0xf9, 0x80, 0x7a, 0x28, 0xf4, 0xd9, 0x32, 0xc6, 0x2e, 0x7c, 0x1c, -+ 0x83, 0xbe, 0x05, 0x5e, 0x55, 0x07, 0x2c, 0x90, 0x26, 0x6a, 0xb3, 0xe7, -+ 0x9d, 0xf6, 0x3a, 0x36, 0x5b -+ }; -+ Reader::Config config; -+ config.parse_tags = true; -+ absl::optional cbor = Reader::Read(kTaggedCbor, config); -+ EXPECT_TRUE(cbor.has_value()); -+ auto& v = cbor.value(); -+ EXPECT_TRUE(v.has_tag()); -+ EXPECT_EQ(v.GetTag(),42UL); -+ EXPECT_TRUE(v.is_bytestring()); -+ EXPECT_EQ(v.type(), Value::Type::BYTE_STRING); -+ auto& bytes = v.GetBytestring(); -+ EXPECT_EQ(bytes.size(), 37UL); -+ EXPECT_EQ(bytes.at(0), 0x00);//identity multibase (e.g. not base-encoded, bytes are themselves) -+ EXPECT_EQ(bytes.at(1), 0x01);//CID version 1 -+ EXPECT_EQ(bytes.at(2), 0x71);//codec = dag-cbor -+ EXPECT_EQ(bytes.at(3), 0x12);//multihash = 18 = sha2-256 -+ EXPECT_EQ(bytes.at(4), 0x20);//hash length = 32 bytes -+ EXPECT_EQ(bytes.at(5), 0x69);//first byte of hash digest -+ EXPECT_EQ(bytes.at(36),0x5b);//last byte of hash digest -+} - - } // namespace cbor -diff --git a/components/cbor/values.cc b/components/cbor/values.cc -index 02498209c820e..34055aef24cfe 100644 ---- a/components/cbor/values.cc -+++ b/components/cbor/values.cc -@@ -66,32 +66,34 @@ Value::Value(Type type) : type_(type) { - NOTREACHED(); - } - --Value::Value(SimpleValue in_simple) -- : type_(Type::SIMPLE_VALUE), simple_value_(in_simple) { -+Value::Value(SimpleValue in_simple, uint64_t tag) -+ : type_(Type::SIMPLE_VALUE), simple_value_(in_simple), tag_(tag) { - CHECK(static_cast(in_simple) >= 20 && static_cast(in_simple) <= 23); - } - --Value::Value(bool boolean_value) : type_(Type::SIMPLE_VALUE) { -+Value::Value(bool boolean_value, uint64_t tag) : type_(Type::SIMPLE_VALUE), tag_(tag) { - simple_value_ = boolean_value ? Value::SimpleValue::TRUE_VALUE - : Value::SimpleValue::FALSE_VALUE; - } - --Value::Value(double float_value) -- : type_(Type::FLOAT_VALUE), float_value_(float_value) {} -+Value::Value(double float_value, uint64_t tag) -+ : type_(Type::FLOAT_VALUE), float_value_(float_value), tag_(tag) {} - --Value::Value(int integer_value) -- : Value(base::checked_cast(integer_value)) {} -+Value::Value(int integer_value, uint64_t tag) -+ : Value(base::checked_cast(integer_value), tag) {} - --Value::Value(int64_t integer_value) : integer_value_(integer_value) { -+Value::Value(int64_t integer_value, uint64_t tag) : integer_value_(integer_value), tag_(tag) { - type_ = integer_value >= 0 ? Type::UNSIGNED : Type::NEGATIVE; - } - --Value::Value(base::span in_bytes) -+Value::Value(base::span in_bytes, uint64_t tag) - : type_(Type::BYTE_STRING), -- bytestring_value_(in_bytes.begin(), in_bytes.end()) {} -+ bytestring_value_(in_bytes.begin(), in_bytes.end()), -+ tag_(tag) -+ {} - --Value::Value(base::span in_bytes, Type type) -- : type_(type), bytestring_value_(in_bytes.begin(), in_bytes.end()) { -+Value::Value(base::span in_bytes, Type type, uint64_t tag) -+ : type_(type), bytestring_value_(in_bytes.begin(), in_bytes.end()), tag_(tag) { - DCHECK(type_ == Type::BYTE_STRING || type_ == Type::INVALID_UTF8); - } - -@@ -117,7 +119,8 @@ Value::Value(std::string&& in_string, Type type) noexcept : type_(type) { - } - } - --Value::Value(base::StringPiece in_string, Type type) : type_(type) { -+Value::Value(base::StringPiece in_string, Type type, uint64_t tag) -+: type_(type), tag_(tag) { - switch (type_) { - case Type::STRING: - new (&string_value_) std::string(); -@@ -133,16 +136,18 @@ Value::Value(base::StringPiece in_string, Type type) : type_(type) { - } - } - --Value::Value(const ArrayValue& in_array) : type_(Type::ARRAY), array_value_() { -+Value::Value(const ArrayValue& in_array, uint64_t tag) -+: type_(Type::ARRAY), array_value_(), tag_(tag) { - array_value_.reserve(in_array.size()); - for (const auto& val : in_array) - array_value_.emplace_back(val.Clone()); - } - --Value::Value(ArrayValue&& in_array) noexcept -- : type_(Type::ARRAY), array_value_(std::move(in_array)) {} -+Value::Value(ArrayValue&& in_array, uint64_t tag) noexcept -+ : type_(Type::ARRAY), array_value_(std::move(in_array)), tag_(tag) {} - --Value::Value(const MapValue& in_map) : type_(Type::MAP), map_value_() { -+Value::Value(const MapValue& in_map, uint64_t tag) -+: type_(Type::MAP), map_value_(), tag_(tag) { - map_value_.reserve(in_map.size()); - for (const auto& it : in_map) - map_value_.emplace_hint(map_value_.end(), it.first.Clone(), -@@ -168,31 +173,36 @@ Value Value::Clone() const { - case Type::NONE: - return Value(); - case Type::INVALID_UTF8: -- return Value(bytestring_value_, Type::INVALID_UTF8); -+ return Value(bytestring_value_, Type::INVALID_UTF8, tag_); - case Type::UNSIGNED: - case Type::NEGATIVE: -- return Value(integer_value_); -+ return Value(integer_value_, tag_); - case Type::BYTE_STRING: -- return Value(bytestring_value_); -+ return Value(bytestring_value_, tag_); - case Type::STRING: -- return Value(string_value_); -+ return Value(string_value_, Type::STRING, tag_); - case Type::ARRAY: -- return Value(array_value_); -+ return Value(array_value_, tag_); - case Type::MAP: -- return Value(map_value_); -+ return Value(map_value_, tag_); - case Type::TAG: - NOTREACHED() << constants::kUnsupportedMajorType; - return Value(); - case Type::SIMPLE_VALUE: -- return Value(simple_value_); -+ return Value(simple_value_, tag_); - case Type::FLOAT_VALUE: -- return Value(float_value_); -+ return Value(float_value_, tag_); - } - - NOTREACHED(); - return Value(); - } - -+Value& Value::SetTag(uint64_t tag) noexcept { -+ tag_ = tag; -+ return *this; -+} -+ - Value::SimpleValue Value::GetSimpleValue() const { - CHECK(is_simple()); - return simple_value_; -@@ -258,9 +268,14 @@ const Value::BinaryValue& Value::GetInvalidUTF8() const { - return bytestring_value_; - } - -+uint64_t Value::GetTag() const { -+ CHECK(has_tag()); -+ return tag_; -+} -+ - void Value::InternalMoveConstructFrom(Value&& that) { - type_ = that.type_; -- -+ tag_ = that.tag_; - switch (type_) { - case Type::UNSIGNED: - case Type::NEGATIVE: -diff --git a/components/cbor/values.h b/components/cbor/values.h -index d81ef5607c55a..10216a8dcdc57 100644 ---- a/components/cbor/values.h -+++ b/components/cbor/values.h -@@ -127,28 +127,29 @@ class CBOR_EXPORT Value { - - explicit Value(Type type); - -- explicit Value(SimpleValue in_simple); -- explicit Value(bool boolean_value); -- explicit Value(double in_float); -+ explicit Value(SimpleValue in_simple, uint64_t tag = NO_TAG); -+ explicit Value(bool boolean_value, uint64_t tag = NO_TAG); -+ explicit Value(double in_float, uint64_t tag = NO_TAG); - -- explicit Value(int integer_value); -- explicit Value(int64_t integer_value); -+ explicit Value(int integer_value, uint64_t tag = NO_TAG); -+ explicit Value(int64_t integer_value, uint64_t tag = NO_TAG); - explicit Value(uint64_t integer_value) = delete; - -- explicit Value(base::span in_bytes); -+ explicit Value(base::span in_bytes, uint64_t tag = NO_TAG); - explicit Value(BinaryValue&& in_bytes) noexcept; - - explicit Value(const char* in_string, Type type = Type::STRING); - explicit Value(std::string&& in_string, Type type = Type::STRING) noexcept; -- explicit Value(base::StringPiece in_string, Type type = Type::STRING); -+ explicit Value(base::StringPiece in_string, Type type = Type::STRING, uint64_t tag = NO_TAG); - -- explicit Value(const ArrayValue& in_array); -- explicit Value(ArrayValue&& in_array) noexcept; -+ explicit Value(const ArrayValue& in_array, uint64_t tag = NO_TAG); -+ explicit Value(ArrayValue&& in_array, uint64_t tag = NO_TAG) noexcept; - -- explicit Value(const MapValue& in_map); -+ explicit Value(const MapValue& in_map, uint64_t tag = NO_TAG); - explicit Value(MapValue&& in_map) noexcept; - - Value& operator=(Value&& that) noexcept; -+ Value& SetTag(uint64_t) noexcept; - - Value(const Value&) = delete; - Value& operator=(const Value&) = delete; -@@ -179,6 +180,7 @@ class CBOR_EXPORT Value { - bool is_string() const { return type() == Type::STRING; } - bool is_array() const { return type() == Type::ARRAY; } - bool is_map() const { return type() == Type::MAP; } -+ bool has_tag() const { return tag_ != NO_TAG; } - - // These will all fatally assert if the type doesn't match. - SimpleValue GetSimpleValue() const; -@@ -194,12 +196,13 @@ class CBOR_EXPORT Value { - const ArrayValue& GetArray() const; - const MapValue& GetMap() const; - const BinaryValue& GetInvalidUTF8() const; -+ uint64_t GetTag() const; - - private: - friend class Reader; - // This constructor allows INVALID_UTF8 values to be created, which only - // |Reader| and InvalidUTF8StringValueForTesting() may do. -- Value(base::span in_bytes, Type type); -+ Value(base::span in_bytes, Type type, uint64_t tag = NO_TAG); - - Type type_; - -@@ -213,6 +216,11 @@ class CBOR_EXPORT Value { - MapValue map_value_; - }; - -+ //This value specified as Invalid, -+ // used here to represent absence of TAG -+ constexpr static uint64_t NO_TAG = 0xFFFF; -+ uint64_t tag_ = NO_TAG; -+ - void InternalMoveConstructFrom(Value&& that); - void InternalCleanup(); - }; -diff --git a/components/cbor/writer.cc b/components/cbor/writer.cc -index bb22754d36a07..aae4027836377 100644 ---- a/components/cbor/writer.cc -+++ b/components/cbor/writer.cc -@@ -47,6 +47,9 @@ bool Writer::EncodeCBOR(const Value& node, - if (max_nesting_level < 0) - return false; - -+ if (node.has_tag()) { -+ StartItem(Value::Type::TAG, node.GetTag()); -+ } - switch (node.type()) { - case Value::Type::NONE: { - StartItem(Value::Type::BYTE_STRING, 0); -diff --git a/components/cbor/writer_unittest.cc b/components/cbor/writer_unittest.cc -index e3bffe20734bc..0ed569ae164a0 100644 ---- a/components/cbor/writer_unittest.cc -+++ b/components/cbor/writer_unittest.cc -@@ -522,4 +522,31 @@ TEST(CBORWriterTest, OverlyNestedCBOR) { - EXPECT_FALSE(Writer::Write(Value(map), 4).has_value()); - } - -+TEST(CBORWriterTest, CanWriteTag) { -+ std::array content{ -+ 0x00, 0x01, 0x71, 0x12, 0x20, -+ 0x69, 0xea, 0x07, 0x40, 0xf9, -+ 0x80, 0x7a, 0x28, 0xf4, 0xd9, -+ 0x32, 0xc6, 0x2e, 0x7c, 0x1c, -+ 0x83, 0xbe, 0x05, 0x5e, 0x55, -+ 0x07, 0x2c, 0x90, 0x26, 0x6a, -+ 0xb3, 0xe7, 0x9d, 0xf6, 0x3a, -+ 0x36, 0x5b -+ }; -+ Value to_write(content); -+ to_write.SetTag(42); -+ auto result = Writer::Write(to_write); -+ EXPECT_TRUE(result.has_value()); -+ auto& bytes = result.value(); -+ EXPECT_EQ(bytes.size(), 41UL); -+ EXPECT_EQ(bytes.at(0), 0xd8); -+ EXPECT_EQ(bytes.at(1), 0x2a); -+ EXPECT_EQ(bytes.at(2), 0x58); -+ EXPECT_EQ(bytes.at(3), 0x25); -+ for (auto i = 0UL; i < content.size(); ++i) { -+ ASSERT_LT(i + 4UL, bytes.size()); -+ ASSERT_EQ(content.at(i), bytes.at(i+4UL)); -+ } -+} -+ - } // namespace cbor -diff --git a/components/open_from_clipboard/clipboard_recent_content_generic.cc b/components/open_from_clipboard/clipboard_recent_content_generic.cc -index 4dcafecbc66c6..d205209c08162 100644 ---- a/components/open_from_clipboard/clipboard_recent_content_generic.cc -+++ b/components/open_from_clipboard/clipboard_recent_content_generic.cc -@@ -20,7 +20,7 @@ - namespace { - // Schemes appropriate for suggestion by ClipboardRecentContent. - const char* kAuthorizedSchemes[] = { -- url::kAboutScheme, url::kDataScheme, url::kHttpScheme, url::kHttpsScheme, -+ url::kAboutScheme, url::kDataScheme, url::kHttpScheme, url::kHttpsScheme, "ipfs", "ipns" - // TODO(mpearson): add support for chrome:// URLs. Right now the scheme - // for that lives in content and is accessible via - // GetEmbedderRepresentationOfAboutScheme() or content::kChromeUIScheme -diff --git a/net/dns/dns_config_service_linux.cc b/net/dns/dns_config_service_linux.cc -index 5273da5190277..12b28b86a4c00 100644 ---- a/net/dns/dns_config_service_linux.cc -+++ b/net/dns/dns_config_service_linux.cc -@@ -272,11 +272,11 @@ bool IsNsswitchConfigCompatible( - // Ignore any entries after `kDns` because Chrome will fallback to the - // system resolver if a result was not found in DNS. - return true; -- -+ case NsswitchReader::Service::kResolve: -+ break; - case NsswitchReader::Service::kMdns: - case NsswitchReader::Service::kMdns4: - case NsswitchReader::Service::kMdns6: -- case NsswitchReader::Service::kResolve: - case NsswitchReader::Service::kNis: - RecordIncompatibleNsswitchReason( - IncompatibleNsswitchReason::kIncompatibleService, -diff --git a/third_party/blink/renderer/platform/weborigin/scheme_registry.cc b/third_party/blink/renderer/platform/weborigin/scheme_registry.cc -index 4eadf46ea0c24..d62fc7fb14e01 100644 ---- a/third_party/blink/renderer/platform/weborigin/scheme_registry.cc -+++ b/third_party/blink/renderer/platform/weborigin/scheme_registry.cc -@@ -67,7 +67,7 @@ class URLSchemesRegistry final { - // is considered secure. Additional checks are performed to ensure that - // other http pages are filtered out. - service_worker_schemes({"http", "https"}), -- fetch_api_schemes({"http", "https"}), -+ fetch_api_schemes({"http", "https", "ipfs", "ipns"}), - allowed_in_referrer_schemes({"http", "https"}) { - for (auto& scheme : url::GetCorsEnabledSchemes()) - cors_enabled_schemes.insert(scheme.c_str()); -diff --git a/url/BUILD.gn b/url/BUILD.gn -index b5edb89f7698f..d299856674d7d 100644 ---- a/url/BUILD.gn -+++ b/url/BUILD.gn -@@ -5,6 +5,7 @@ - import("//build/buildflag_header.gni") - import("//testing/libfuzzer/fuzzer_test.gni") - import("//testing/test.gni") -+import("//third_party/ipfs_client/args.gni") - import("features.gni") - - import("//build/config/cronet/config.gni") -@@ -68,6 +69,7 @@ component("url") { - public_deps = [ - "//base", - "//build:robolectric_buildflags", -+ "//third_party/ipfs_client:ipfs_buildflags", - ] - - configs += [ "//build/config/compiler:wexit_time_destructors" ] -@@ -90,6 +92,11 @@ component("url") { - public_configs = [ "//third_party/jdk" ] - } - -+ if (enable_ipfs) { -+ sources += [ "url_canon_ipfs.cc" ] -+ deps += [ "//third_party/ipfs_client:ipfs_client" ] -+ } -+ - if (is_win) { - # Don't conflict with Windows' "url.dll". - output_name = "url_lib" -diff --git a/url/url_canon.h b/url/url_canon.h -index 8c48f9825d8cf..b9ad961e1b123 100644 ---- a/url/url_canon.h -+++ b/url/url_canon.h -@@ -804,6 +804,23 @@ bool CanonicalizeMailtoURL(const char16_t* spec, - CanonOutput* output, - Parsed* new_parsed); - -+COMPONENT_EXPORT(URL) -+bool CanonicalizeIpfsURL(const char* spec, -+ int spec_len, -+ const Parsed& parsed, -+ SchemeType scheme_type, -+ CharsetConverter* query_converter, -+ CanonOutput* output, -+ Parsed* new_parsed); -+COMPONENT_EXPORT(URL) -+bool CanonicalizeIpfsURL(const char16_t* spec, -+ int spec_len, -+ const Parsed& parsed, -+ SchemeType scheme_type, -+ CharsetConverter* query_converter, -+ CanonOutput* output, -+ Parsed* new_parsed); -+ - // Part replacer -------------------------------------------------------------- - - // Internal structure used for storing separate strings for each component. -diff --git a/url/url_canon_ipfs.cc b/url/url_canon_ipfs.cc -new file mode 100644 -index 0000000000000..d7c9fdc78eb91 ---- /dev/null -+++ b/url/url_canon_ipfs.cc -@@ -0,0 +1,55 @@ -+#include "url_canon_internal.h" -+ -+#include -+#include -+ -+#include -+ -+bool url::CanonicalizeIpfsURL(const char* spec, -+ int spec_len, -+ const Parsed& parsed, -+ SchemeType scheme_type, -+ CharsetConverter* charset_converter, -+ CanonOutput* output, -+ Parsed* output_parsed) { -+ if ( spec_len < 1 || !spec ) { -+ return false; -+ } -+ if ( parsed.host.len < 1 ) { -+ return false; -+ } -+ std::string_view cid_str{ spec + parsed.host.begin, static_cast(parsed.host.len) }; -+ auto cid = ipfs::Cid(cid_str); -+ if ( !cid.valid() ) { -+ cid = ipfs::id_cid::forText( std::string{cid_str} + " is not a valid CID." ); -+ } -+ auto as_str = cid.to_string(); -+ if ( as_str.empty() ) { -+ return false; -+ } -+ std::string stdurl{ spec, static_cast(parsed.host.begin) }; -+ stdurl.append( as_str ); -+ stdurl.append( spec + parsed.host.end(), spec_len - parsed.host.end() ); -+ spec = stdurl.data(); -+ spec_len = static_cast(stdurl.size()); -+ Parsed parsed_input; -+ ParseStandardURL(spec, spec_len, &parsed_input); -+ return CanonicalizeStandardURL( -+ spec, -+ parsed_input, -+ scheme_type, -+ charset_converter, -+ output, output_parsed -+ ); -+} -+bool url::CanonicalizeIpfsURL(const char16_t* spec, -+ int spec_len, -+ const Parsed& parsed, -+ SchemeType scheme_type, -+ CharsetConverter* query_converter, -+ CanonOutput* output, -+ Parsed* new_parsed) { -+ RawCanonOutput<2048> as8; -+ ConvertUTF16ToUTF8(spec, spec_len, &as8); -+ return CanonicalizeIpfsURL(as8.data(), as8.length(), parsed, scheme_type, query_converter, output, new_parsed); -+} -diff --git a/url/url_util.cc b/url/url_util.cc -index 6f83f33c01c6b..a248e11c49445 100644 ---- a/url/url_util.cc -+++ b/url/url_util.cc -@@ -273,8 +273,15 @@ bool DoCanonicalize(const CHAR* spec, - } else if (DoCompareSchemeComponent(spec, scheme, url::kFileSystemScheme)) { - // Filesystem URLs are special. - ParseFileSystemURL(spec, spec_len, &parsed_input); -- success = CanonicalizeFileSystemURL(spec, parsed_input, charset_converter, -- output, output_parsed); -+ success = CanonicalizeFileSystemURL(spec, parsed_input, -+ charset_converter, output, -+ output_parsed); -+ -+ } else if (DoCompareSchemeComponent(spec, scheme, "ipfs")) { -+ // Switch multibase away from case-sensitive ones before continuing canonicalization. -+ ParseStandardURL(spec, spec_len, &parsed_input); -+ success = CanonicalizeIpfsURL(spec, spec_len, parsed_input, scheme_type, -+ charset_converter, output, output_parsed); - - } else if (DoIsStandard(spec, scheme, &scheme_type)) { - // All "normal" URLs. - diff --git a/library/include/ipfs_client/context_api.h b/library/include/ipfs_client/context_api.h index 227609c7..c89e502d 100644 --- a/library/include/ipfs_client/context_api.h +++ b/library/include/ipfs_client/context_api.h @@ -19,6 +19,10 @@ #include #include +namespace boost::asio { +class io_context; +} + namespace ipfs { class IpfsRequest; class DagJsonValue; @@ -31,7 +35,7 @@ class DagJsonValue; */ class ContextApi : public std::enable_shared_from_this { public: - ContextApi(); + ContextApi(boost::asio::io_context* boost_context = nullptr); virtual ~ContextApi() noexcept {} ctx::HttpApi& http(); diff --git a/library/include/ipfs_client/ctx/boost_beast_http.h b/library/include/ipfs_client/ctx/boost_beast_http.h new file mode 100644 index 00000000..13c249da --- /dev/null +++ b/library/include/ipfs_client/ctx/boost_beast_http.h @@ -0,0 +1,26 @@ +#ifndef BOOST_BEAST_HTTP_H_INCLUDED +#define BOOST_BEAST_HTTP_H_INCLUDED 1 + +#include "http_api.h" + +#if __has_include() +#define HAS_BOOST_BEAST 1 + +#include +#include + +namespace ipfs::ctx { +class BoostBeastHttp : public HttpApi { + boost::asio::io_context& io_; + boost::asio::ssl::context mutable ssl_ctx_ = + boost::asio::ssl::context{boost::asio::ssl::context::tls_client}; + + public: + BoostBeastHttp(boost::asio::io_context&); + ~BoostBeastHttp() noexcept override {} + void SendHttpRequest(HttpRequestDescription, HttpCompleteCallback cb) const; +}; +} // namespace ipfs::ctx + +#endif // tcp_stream.hpp +#endif // BOOST_BEAST_HTTP_H_INCLUDED diff --git a/library/include/ipfs_client/ctx/http_api.h b/library/include/ipfs_client/ctx/http_api.h index 3a1c4942..24c8843d 100644 --- a/library/include/ipfs_client/ctx/http_api.h +++ b/library/include/ipfs_client/ctx/http_api.h @@ -13,6 +13,8 @@ class HttpApi { std::function; virtual void SendHttpRequest(HttpRequestDescription, HttpCompleteCallback cb) const = 0; + + virtual ~HttpApi() noexcept {} }; } // namespace ipfs::ctx diff --git a/library/src/ipfs_client/ctx/null_http_provider.h b/library/include/ipfs_client/ctx/null_http_provider.h similarity index 88% rename from library/src/ipfs_client/ctx/null_http_provider.h rename to library/include/ipfs_client/ctx/null_http_provider.h index cfaa8f07..a4982622 100644 --- a/library/src/ipfs_client/ctx/null_http_provider.h +++ b/library/include/ipfs_client/ctx/null_http_provider.h @@ -1,7 +1,7 @@ #ifndef IPFS_CHROMIUM_NULLHTTPPROVIDER_H #define IPFS_CHROMIUM_NULLHTTPPROVIDER_H -#include +#include "http_api.h" namespace ipfs::ctx { class NullHttpProvider : public HttpApi { diff --git a/library/include/ipfs_client/test_context.h b/library/include/ipfs_client/test_context.h index fe5267b1..476b809e 100644 --- a/library/include/ipfs_client/test_context.h +++ b/library/include/ipfs_client/test_context.h @@ -30,13 +30,10 @@ #define HAS_ALL_INCLUSIVE 1 #include -#include -#include -#include -#include -#include -#include -#include + +namespace boost::asio { +class io_context; +} namespace google::protobuf { constexpr LogLevel LOGLEVEL_DEBUG = static_cast(-1); @@ -112,12 +109,8 @@ class TestContext final : public ContextApi { << " bytes of JSON string and got " << oss.str(); return std::make_unique(data); } - - std::vector gateways_; boost::asio::io_context& io_; - boost::asio::ssl::context mutable ssl_ctx_ = - boost::asio::ssl::context{boost::asio::ssl::context::tls_client}; - // boost::asio::ssl::context{boost::asio::ssl::context::tlsv13_client}; + std::vector gateways_; ares_channel_t* ares_channel_ = nullptr; void CAresProcess(); diff --git a/library/src/ipfs_client/context_api.cc b/library/src/ipfs_client/context_api.cc index f618bd8c..c853c7d9 100644 --- a/library/src/ipfs_client/context_api.cc +++ b/library/src/ipfs_client/context_api.cc @@ -2,13 +2,14 @@ #include "crypto/openssl_sha2_256.h" #include "crypto/openssl_signature_verifier.h" -#include "ctx/null_http_provider.h" +#include "ipfs_client/ctx/boost_beast_http.h" +#include "ipfs_client/ctx/null_http_provider.h" #include "log_macros.h" using Self = ipfs::ContextApi; -Self::ContextApi() { +Self::ContextApi(boost::asio::io_context* boost_context) { #if HAS_OPENSSL_SHA hashers_.emplace(HashType::SHA2_256, std::make_unique()); @@ -19,6 +20,11 @@ Self::ContextApi() { verifiers_[SigningKeyType::Ed25519] = std::make_unique(EVP_PKEY_ED25519); #endif +#if HAS_BOOST_BEAST + if (boost_context) { + http_api_ = std::make_unique(*boost_context); + } +#endif } auto Self::Hash(HashType ht, ByteView data) diff --git a/library/src/ipfs_client/ctx/boost_beast_http.cc b/library/src/ipfs_client/ctx/boost_beast_http.cc new file mode 100644 index 00000000..7c1d46ad --- /dev/null +++ b/library/src/ipfs_client/ctx/boost_beast_http.cc @@ -0,0 +1,299 @@ +#include + +#include + +#include "log_macros.h" + +#if HAS_BOOST_BEAST + +#include +#include +#include +#include +#include +#include +#include + +#include + +using Self = ipfs::ctx::BoostBeastHttp; + +Self::BoostBeastHttp(boost::asio::io_context& c) : io_{c} {} + +namespace http = boost::beast::http; +class HttpSession : public std::enable_shared_from_this { + using tcp = boost::asio::ip::tcp; + boost::asio::io_context& ioc_; + boost::asio::strand strand_; + tcp::resolver resolver_; + boost::asio::ssl::context& ssl_ctx_; + boost::beast::ssl_stream stream_; + boost::beast::flat_buffer buffer_; // (Must persist between reads) + http::request req_; + using api = ipfs::ctx::HttpApi; + api::HttpCompleteCallback cb_; + int expiry_seconds_ = 91; + std::string host_, port_, target_; + ipfs::HttpRequestDescription desc_; + static std::map resolutions_; + std::string parsed_host_; + http::response_parser response_parser_; + std::optional> res_; + std::shared_ptr prev; + + void fail(boost::beast::error_code ec, char const* what) { + LOG(INFO) << what << ": " << ec.value() << ' ' << ec.message() + << " URL:" << desc_.url << " HOST:" << host_ << " PORT:" << port_ + << " TARGET:" << target_; + auto status = ec.value() == 1 ? 408 : 500; + cb_(status, "", [](auto) { return std::string{}; }); + } + std::string parse_url() { + ipfs::SlashDelimited ss{desc_.url}; + auto scheme = ss.pop(); + if (port_.empty()) { + port_.assign(scheme); + if (port_.back() == ':') { + port_.resize(port_.size() - 1UL); + } + } + std::string host{ss.pop()}; + auto colon = host.find(':'); + if (colon < host.size()) { + port_ = host.substr(colon + 1UL); + host.resize(colon); + } else if (port_ == "https") { + port_ = "443"; + } else if (port_ == "http") { + port_ = "80"; + } + target_.assign("/").append(ss.to_string()); + return host; + } + + public: + explicit HttpSession(boost::asio::io_context& ioc, + boost::asio::ssl::context& ssc, + ipfs::HttpRequestDescription& desc, + api::HttpCompleteCallback cb) + : ioc_{ioc}, + strand_{boost::asio::make_strand(ioc)}, + resolver_(strand_), + ssl_ctx_(ssc), + stream_(strand_, ssc), + cb_{cb}, + desc_{desc} { + if (auto sz = desc_.max_response_size) { + response_parser_.body_limit(*sz * 2); + } else { + response_parser_.body_limit(boost::none); + } + } + tcp::resolver::results_type& resolution() { + return resolutions_[host_ + port_]; + } + // Start the asynchronous operation + void run() { + auto parsed_host_ = parse_url(); + if (host_.empty()) { + host_ = parsed_host_; + } + // Set SNI Hostname (many hosts need this to handshake successfully) + if (!SSL_set_tlsext_host_name(stream_.native_handle(), host_.c_str())) { + boost::beast::error_code ec{static_cast(::ERR_get_error()), + boost::asio::error::get_ssl_category()}; + LOG(ERROR) << "SSL early fail: " << ec.message(); + return; + } + + req_.version(11); + req_.method(http::verb::get); + req_.target(target_); + req_.set(http::field::host, parsed_host_); + if (desc_.accept.size()) { + // std::clog << "Setting Accept: " << desc_.accept << '\n'; + req_.set("Accept", desc_.accept); + } + extend_time(); + auto me = shared_from_this(); + if (resolution().empty()) { + VLOG(1) << "Starting " << desc_.url << " with a host resolution of " + << host_ << ':' << port_; + resolver_.async_resolve( + host_, port_, + boost::beast::bind_front_handler(&HttpSession::on_resolve, me)); + } else { + auto do_connect = [me]() { + boost::beast::get_lowest_layer(me->stream_) + .async_connect(me->resolution(), boost::beast::bind_front_handler( + &HttpSession::on_connect, me)); + }; + boost::asio::defer(strand_, do_connect); + } + } + void on_resolve(boost::beast::error_code ec, + tcp::resolver::results_type results) { + if (ec) + return fail(ec, "resolve"); + resolution() = results; + for (auto& ep : results) { + VLOG(1) << desc_.url << " Resolved " << host_ << ", now connecting to " + << req_[http::field::host] << " aka " << ep.host_name() << ':' + << ep.service_name() << " for " << target_; + } + extend_time(); + boost::beast::get_lowest_layer(stream_).async_connect( + results, boost::beast::bind_front_handler(&HttpSession::on_connect, + shared_from_this())); + } + + void on_connect(boost::beast::error_code ec, + tcp::resolver::results_type::endpoint_type) { + if (ec) + return fail(ec, "connect"); + extend_time(); + LOG(INFO) << desc_.url << " connected."; + if (use_ssl()) { + VLOG(1) << "Perform the SSL handshake because port=" << port_; + stream_.async_handshake( + boost::asio::ssl::stream_base::client, + boost::beast::bind_front_handler(&HttpSession::on_handshake, + shared_from_this())); + } else { + VLOG(1) << "Skipping the SSL handshake because port=" << port_; + http::async_write(boost::beast::get_lowest_layer(stream_), req_, + boost::beast::bind_front_handler(&HttpSession::on_write, + shared_from_this())); + } + } + bool use_ssl() const { return port_ == "443" || port_ == "https"; } + void extend_time() { + expiry_seconds_ += desc_.timeout_seconds + 1; + VLOG(2) << "expiry_seconds_ = " << expiry_seconds_ << '\n'; + boost::beast::get_lowest_layer(stream_).expires_after( + std::chrono::seconds(expiry_seconds_)); + } + void on_handshake(boost::beast::error_code ec) { + if (ec) + return fail(ec, "handshake"); + extend_time(); + http::async_write(stream_, req_, + boost::beast::bind_front_handler(&HttpSession::on_write, + shared_from_this())); + } + + void on_write(boost::beast::error_code ec, std::size_t) { + if (ec) + return fail(ec, "write"); + VLOG(2) << desc_.url << " request written."; + extend_time(); + if (use_ssl()) { + http::async_read(stream_, buffer_, response_parser_, + boost::beast::bind_front_handler(&HttpSession::on_read, + shared_from_this())); + } else { + http::async_read(boost::beast::get_lowest_layer(stream_), buffer_, + response_parser_, + boost::beast::bind_front_handler(&HttpSession::on_read, + shared_from_this())); + } + } + + void on_read(boost::beast::error_code ec, std::size_t bytes_transferred) { + if (ec) + return fail(ec, "read"); + res_ = response_parser_.release(); + api::HeaderAccess get_hdr = [this](std::string_view k) -> std::string { + std::string rv{(*res_)[k]}; + return rv; + }; + LOG(INFO) << "HTTP read (" << desc_.url << ";host=" << host_ << '(' + << host_.size() << ");port=" << port_ << ";target=" << target_ + << ": status=" << res_->result_int() << ", body is " + << bytes_transferred << "B, headers... "; + if (res_->result_int() == 400) { + LOG(WARNING) << "Got that annoying 400 status: " << res_->body(); + } + for (auto& h : *res_) { + auto& n = h.name_string(); + if (n.substr(0, 6) != "Access") { + VLOG(1) << "\t Header=" << h.name_string() << ": " << h.value(); + } + } + if (res_->result_int() / 100 == 3) { + auto loc = (*res_)[http::field::location]; + if (loc.empty()) { + LOG(ERROR) << "No Location header given for a redirect response."; + } else if (redirect_count(loc) >= 0xFF) { + LOG(ERROR) << "Too many redirects!! Giving up on " << loc << '\n'; + } else { + VLOG(1) << "Redirecting to " << loc << " aka " << desc_.url; + auto desc = desc_; + desc.url = loc; + auto next = std::make_shared(ioc_, ssl_ctx_, desc, cb_); + next->prev = shared_from_this(); + next->run(); + } + close(); + return; + } + auto content_type = (*res_)[http::field::content_type]; + auto me = shared_from_this(); + auto respond = [me, get_hdr]() { + auto& r = *(me->res_); + me->cb_(r.result_int(), r.body(), get_hdr); + }; + if (content_type.empty() || + boost::algorithm::icontains(content_type, desc_.accept)) { + VLOG(2) << "Got " << content_type; + } else { + LOG(INFO) << desc_.url + << " response incorrect content type: " << content_type + << " != " << desc_.accept; + res_->result(501); + } + boost::asio::defer(strand_, respond); + close(); + } + int redirect_count(std::string_view comp) { + if (comp == desc_.url) { + LOG(ERROR) << "Redirect loop on " << comp; + return 0xFF; + } else if (!prev) { + return 1; + } else { + return 1 + prev->redirect_count(comp); + } + } + void close() { + if (use_ssl()) { + stream_.async_shutdown(boost::beast::bind_front_handler( + &HttpSession::on_shutdown, shared_from_this())); + } else { + boost::beast::get_lowest_layer(stream_).close(); + } + } + void on_shutdown(boost::beast::error_code ec) { + namespace E = boost::asio::error; + switch (ec.value()) { + case 0: + case 1: + case 2: + case ENOTCONN: + return; + default: + fail(ec, "shutdown"); + } + } +}; + +std::map + HttpSession::resolutions_; + +void Self::SendHttpRequest(HttpRequestDescription desc, + HttpCompleteCallback cb) const { + auto sess = std::make_shared(io_, ssl_ctx_, desc, cb); + sess->run(); +} + +#endif diff --git a/library/src/ipfs_client/ctx/null_http_provider.cc b/library/src/ipfs_client/ctx/null_http_provider.cc index 818fb9d3..08438a87 100644 --- a/library/src/ipfs_client/ctx/null_http_provider.cc +++ b/library/src/ipfs_client/ctx/null_http_provider.cc @@ -1,4 +1,4 @@ -#include "null_http_provider.h" +#include "ipfs_client/ctx/null_http_provider.h" using Self = ipfs::ctx::NullHttpProvider; diff --git a/library/src/ipfs_client/test_context.cc b/library/src/ipfs_client/test_context.cc index 83495694..56fb18f1 100644 --- a/library/src/ipfs_client/test_context.cc +++ b/library/src/ipfs_client/test_context.cc @@ -6,6 +6,8 @@ #include #endif +#include + #include #include "log_macros.h" @@ -39,7 +41,7 @@ static void c_ares_c_callback(void* vp, delete cbcb; } } - +/* Self::TestContext(boost::asio::io_context& io) : gateways_{Gateways::DefaultGateways()}, io_{io} { std::sort(gateways_.begin(), gateways_.end(), @@ -51,6 +53,7 @@ Self::TestContext(boost::asio::io_context& io) throw std::runtime_error("Failed to initialize c-ares channel."); } } + */ Self::~TestContext() { pending_dns_.clear(); ares_destroy(ares_channel_); @@ -72,6 +75,7 @@ void Self::SendDnsTextRequest(std::string host, ares_query(ares_channel_, it->first.c_str(), ns_c_in, ns_t_txt, &c_ares_c_callback, cbcb); io_.post([this]() { CAresProcess(); }); + // CAresProcess(); } } void Self::DnsResults(std::string& host, ares_txt_reply& result) { @@ -110,285 +114,6 @@ void Self::CAresProcess() { io_.post([this]() { CAresProcess(); }); } } -namespace http = boost::beast::http; -class HttpSession : public std::enable_shared_from_this { - using tcp = boost::asio::ip::tcp; - boost::asio::io_context& ioc_; - boost::asio::strand strand_; - tcp::resolver resolver_; - boost::asio::ssl::context& ssl_ctx_; - boost::beast::ssl_stream stream_; - boost::beast::flat_buffer buffer_; // (Must persist between reads) - http::request req_; - using api = ipfs::ctx::HttpApi; - api::HttpCompleteCallback cb_; - int expiry_seconds_ = 91; - std::string host_, port_, target_; - ipfs::HttpRequestDescription desc_; - static std::map resolutions_; - std::string parsed_host_; - http::response_parser response_parser_; - std::optional> res_; - std::shared_ptr prev; - - void fail(boost::beast::error_code ec, char const* what) { - GOOGLE_LOG(INFO) << what << ": " << ec.value() << ' ' << ec.message() - << " URL:" << desc_.url << " HOST:" << host_ - << " PORT:" << port_ << " TARGET:" << target_; - auto status = ec.value() == 1 ? 408 : 500; - cb_(status, "", [](auto) { return std::string{}; }); - } - std::string parse_url() { - ipfs::SlashDelimited ss{desc_.url}; - auto scheme = ss.pop(); - if (port_.empty()) { - port_.assign(scheme); - if (port_.back() == ':') { - port_.resize(port_.size() - 1UL); - } - } - std::string host{ss.pop()}; - auto colon = host.find(':'); - if (colon < host.size()) { - port_ = host.substr(colon + 1UL); - host.resize(colon); - } else if (port_ == "https") { - port_ = "443"; - } else if (port_ == "http") { - port_ = "80"; - } - target_.assign("/").append(ss.to_string()); - return host; - } - - public: - explicit HttpSession(boost::asio::io_context& ioc, - boost::asio::ssl::context& ssc, - ipfs::HttpRequestDescription& desc, - api::HttpCompleteCallback cb) - : ioc_{ioc}, - strand_{boost::asio::make_strand(ioc)}, - resolver_(strand_), - ssl_ctx_(ssc), - stream_(strand_, ssc), - cb_{cb}, - desc_{desc} { - if (auto sz = desc_.max_response_size) { - response_parser_.body_limit(*sz * 2); - } else { - response_parser_.body_limit(boost::none); - } - } - tcp::resolver::results_type& resolution() { - return resolutions_[host_ + port_]; - } - // Start the asynchronous operation - void run() { - auto parsed_host_ = parse_url(); - if (host_.empty()) { - host_ = parsed_host_; - } - // Set SNI Hostname (many hosts need this to handshake successfully) - if (!SSL_set_tlsext_host_name(stream_.native_handle(), host_.c_str())) { - boost::beast::error_code ec{static_cast(::ERR_get_error()), - boost::asio::error::get_ssl_category()}; - GOOGLE_LOG(ERROR) << "SSL early fail: " << ec.message(); - return; - } - - req_.version(11); - req_.method(http::verb::get); - req_.target(target_); - req_.set(http::field::host, parsed_host_); - if (desc_.accept.size()) { - // std::clog << "Setting Accept: " << desc_.accept << '\n'; - req_.set("Accept", desc_.accept); - } - extend_time(); - auto me = shared_from_this(); - if (resolution().empty()) { - GOOGLE_LOG(DEBUG) << "Starting " << desc_.url - << " with a host resolution of " << host_ << ':' - << port_; - resolver_.async_resolve( - host_, port_, - boost::beast::bind_front_handler(&HttpSession::on_resolve, me)); - } else { - auto do_connect = [me]() { - boost::beast::get_lowest_layer(me->stream_) - .async_connect(me->resolution(), boost::beast::bind_front_handler( - &HttpSession::on_connect, me)); - }; - boost::asio::defer(strand_, do_connect); - } - } - void on_resolve(boost::beast::error_code ec, - tcp::resolver::results_type results) { - if (ec) - return fail(ec, "resolve"); - resolution() = results; - for (auto& ep : results) { - GOOGLE_LOG(DEBUG) << desc_.url << " Resolved " << host_ - << ", now connecting to " << req_[http::field::host] - << " aka " << ep.host_name() << ':' << ep.service_name() - << " for " << target_; - } - extend_time(); - boost::beast::get_lowest_layer(stream_).async_connect( - results, boost::beast::bind_front_handler(&HttpSession::on_connect, - shared_from_this())); - } - - void on_connect(boost::beast::error_code ec, - tcp::resolver::results_type::endpoint_type) { - if (ec) - return fail(ec, "connect"); - extend_time(); - GOOGLE_LOG(INFO) << desc_.url << " connected."; - if (use_ssl()) { - GOOGLE_LOG(DEBUG) << "Perform the SSL handshake because port=" << port_; - stream_.async_handshake( - boost::asio::ssl::stream_base::client, - boost::beast::bind_front_handler(&HttpSession::on_handshake, - shared_from_this())); - } else { - GOOGLE_LOG(DEBUG) << "Skipping the SSL handshake because port=" << port_; - http::async_write(boost::beast::get_lowest_layer(stream_), req_, - boost::beast::bind_front_handler(&HttpSession::on_write, - shared_from_this())); - } - } - bool use_ssl() const { return port_ == "443" || port_ == "https"; } - void extend_time() { - expiry_seconds_ += desc_.timeout_seconds + 1; - GOOGLE_LOG(TRACE) << "expiry_seconds_ = " << expiry_seconds_ << '\n'; - boost::beast::get_lowest_layer(stream_).expires_after( - std::chrono::seconds(expiry_seconds_)); - } - void on_handshake(boost::beast::error_code ec) { - if (ec) - return fail(ec, "handshake"); - extend_time(); - http::async_write(stream_, req_, - boost::beast::bind_front_handler(&HttpSession::on_write, - shared_from_this())); - } - - void on_write(boost::beast::error_code ec, std::size_t) { - if (ec) - return fail(ec, "write"); - GOOGLE_LOG(TRACE) << desc_.url << " request written."; - extend_time(); - if (use_ssl()) { - http::async_read(stream_, buffer_, response_parser_, - boost::beast::bind_front_handler(&HttpSession::on_read, - shared_from_this())); - } else { - http::async_read(boost::beast::get_lowest_layer(stream_), buffer_, - response_parser_, - boost::beast::bind_front_handler(&HttpSession::on_read, - shared_from_this())); - } - } - - void on_read(boost::beast::error_code ec, std::size_t bytes_transferred) { - if (ec) - return fail(ec, "read"); - res_ = response_parser_.release(); - api::HeaderAccess get_hdr = [this](std::string_view k) -> std::string { - std::string rv{(*res_)[k]}; - return rv; - }; - GOOGLE_LOG(INFO) << "HTTP read (" << desc_.url << ";host=" << host_ << '(' - << host_.size() << ");port=" << port_ - << ";target=" << target_ - << ": status=" << res_->result_int() << ", body is " - << bytes_transferred << "B, headers... "; - if (res_->result_int() == 400) { - GOOGLE_LOG(WARNING) << "Got that annoying 400 status: " << res_->body(); - } - for (auto& h : *res_) { - auto& n = h.name_string(); - if (n.substr(0, 6) != "Access") { - GOOGLE_LOG(DEBUG) << "\t Header=" << h.name_string() << ": " - << h.value(); - } - } - if (res_->result_int() / 100 == 3) { - auto loc = (*res_)[http::field::location]; - if (loc.empty()) { - LOG(ERROR) << "No Location header given for a redirect response."; - } else if (redirect_count(loc) >= 0xFF) { - LOG(ERROR) << "Too many redirects!! Giving up on " << loc << '\n'; - } else { - VLOG(1) << "Redirecting to " << loc << " aka " << desc_.url; - auto desc = desc_; - desc.url = loc; - auto next = std::make_shared(ioc_, ssl_ctx_, desc, cb_); - next->prev = shared_from_this(); - next->run(); - } - close(); - return; - } - auto content_type = (*res_)[http::field::content_type]; - auto me = shared_from_this(); - auto respond = [me, get_hdr]() { - auto& r = *(me->res_); - me->cb_(r.result_int(), r.body(), get_hdr); - }; - if (content_type.empty() || - boost::algorithm::icontains(content_type, desc_.accept)) { - LOG(TRACE) << "Got " << content_type; - } else { - LOG(INFO) << desc_.url - << " response incorrect content type: " << content_type - << " != " << desc_.accept; - res_->result(501); - } - boost::asio::defer(strand_, respond); - close(); - } - int redirect_count(std::string_view comp) { - if (comp == desc_.url) { - LOG(ERROR) << "Redirect loop on " << comp; - return 0xFF; - } else if (!prev) { - return 1; - } else { - return 1 + prev->redirect_count(comp); - } - } - void close() { - if (use_ssl()) { - stream_.async_shutdown(boost::beast::bind_front_handler( - &HttpSession::on_shutdown, shared_from_this())); - } else { - boost::beast::get_lowest_layer(stream_).close(); - } - } - void on_shutdown(boost::beast::error_code ec) { - namespace E = boost::asio::error; - switch (ec.value()) { - case 0: - case 1: - case 2: - case ENOTCONN: - return; - default: - fail(ec, "shutdown"); - } - } -}; - -std::map - HttpSession::resolutions_; - -// void Self::SendHttpRequest(HttpRequestDescription desc, HttpCompleteCallback -// cb) const { -// auto sess = std::make_shared(io_, ssl_ctx_, desc, cb); -// sess->run(); -// } std::optional Self::GetGateway(std::size_t index) const { if (index < gateways_.size()) { return gateways_.at(index);