diff --git a/mobile/docs/root/api/starting_envoy.rst b/mobile/docs/root/api/starting_envoy.rst index b1624d7fefb0..1d23570163b9 100644 --- a/mobile/docs/root/api/starting_envoy.rst +++ b/mobile/docs/root/api/starting_envoy.rst @@ -514,6 +514,18 @@ to use IPv6. Note this is an experimental option and should be enabled with caut builder.forceIPv6(true) +~~~~~~~~~~~~~~~~~~~~~~~~~~ +``enableProxying`` +~~~~~~~~~~~~~~~~~~~~~~~~~~ +Specify whether to respect system Proxy settings when establishing connections. +Available on Andorid only. + +**Example**:: + + // Kotlin + builder.enableProxying(true) + + ---------------------- Advanced configuration ---------------------- diff --git a/mobile/docs/root/intro/version_history.rst b/mobile/docs/root/intro/version_history.rst index 8a0ca6c20732..a23bd47c40f5 100644 --- a/mobile/docs/root/intro/version_history.rst +++ b/mobile/docs/root/intro/version_history.rst @@ -14,7 +14,7 @@ Bugfixes: Features: -- +- kotlin: add a way to tell Envoy Mobile to respect system proxy settings by calling an ``enableProxying(true)`` method on the engine builder. (:issue:`#2416 <2416>`) 0.5.0 (September 2, 2022) diff --git a/mobile/envoy_build_config/BUILD b/mobile/envoy_build_config/BUILD index 2f020118a820..b59a7dd67c62 100644 --- a/mobile/envoy_build_config/BUILD +++ b/mobile/envoy_build_config/BUILD @@ -28,6 +28,7 @@ envoy_cc_library( "@envoy//source/extensions/http/header_formatters/preserve_case:config", "@envoy//source/extensions/stat_sinks/metrics_service:config", "@envoy//source/extensions/stat_sinks/statsd:config", + "@envoy//source/extensions/transport_sockets/http_11_proxy:upstream_config", "@envoy//source/extensions/transport_sockets/raw_buffer:config", "@envoy//source/extensions/transport_sockets/tls:config", "@envoy//source/extensions/transport_sockets/tls/cert_validator:cert_validator_lib", diff --git a/mobile/envoy_build_config/extension_registry.cc b/mobile/envoy_build_config/extension_registry.cc index 59183fc4eecb..6e0717e51351 100644 --- a/mobile/envoy_build_config/extension_registry.cc +++ b/mobile/envoy_build_config/extension_registry.cc @@ -16,6 +16,7 @@ #include "source/extensions/http/header_formatters/preserve_case/config.h" #include "source/extensions/http/original_ip_detection/xff/config.h" #include "source/extensions/stat_sinks/metrics_service/config.h" +#include "source/extensions/transport_sockets/http_11_proxy/config.h" #include "source/extensions/transport_sockets/raw_buffer/config.h" #include "source/extensions/transport_sockets/tls/cert_validator/default_validator.h" #include "source/extensions/transport_sockets/tls/config.h" @@ -59,6 +60,8 @@ void ExtensionRegistry::registerFactories() { Envoy::Extensions::StatSinks::MetricsService::forceRegisterMetricsServiceSinkFactory(); Envoy::Extensions::TransportSockets::RawBuffer::forceRegisterUpstreamRawBufferSocketFactory(); Envoy::Extensions::TransportSockets::Tls::forceRegisterUpstreamSslSocketFactory(); + Envoy::Extensions::TransportSockets::Http11Connect:: + forceRegisterUpstreamHttp11ConnectSocketConfigFactory(); Envoy::Extensions::TransportSockets::Tls::forceRegisterDefaultCertValidatorFactory(); Envoy::Extensions::Upstreams::Http::Generic::forceRegisterGenericGenericConnPoolFactory(); Envoy::Upstream::forceRegisterLogicalDnsClusterFactory(); diff --git a/mobile/envoy_build_config/extensions_build_config.bzl b/mobile/envoy_build_config/extensions_build_config.bzl index 248158ab490c..d8b18b0db01a 100644 --- a/mobile/envoy_build_config/extensions_build_config.bzl +++ b/mobile/envoy_build_config/extensions_build_config.bzl @@ -21,6 +21,7 @@ EXTENSIONS = { "envoy.network.dns_resolver.getaddrinfo": "//source/extensions/network/dns_resolver/getaddrinfo:config", "envoy.retry.options.network_configuration": "@envoy_mobile//library/common/extensions/retry/options/network_configuration:config", "envoy.stat_sinks.metrics_service": "//source/extensions/stat_sinks/metrics_service:config", + "envoy.transport_sockets.http_11_proxy": "//source/extensions/transport_sockets/http_11_proxy:upstream_config", "envoy.transport_sockets.raw_buffer": "//source/extensions/transport_sockets/raw_buffer:config", "envoy.transport_sockets.tls": "//source/extensions/transport_sockets/tls:config", "envoy.http.stateful_header_formatters.preserve_case": "//source/extensions/http/header_formatters/preserve_case:config", diff --git a/mobile/examples/kotlin/hello_world/MainActivity.kt b/mobile/examples/kotlin/hello_world/MainActivity.kt index 88921a499495..98234fc1d7a5 100644 --- a/mobile/examples/kotlin/hello_world/MainActivity.kt +++ b/mobile/examples/kotlin/hello_world/MainActivity.kt @@ -50,6 +50,7 @@ class MainActivity : Activity() { engine = AndroidEngineBuilder(application) .addLogLevel(LogLevel.DEBUG) + .enableProxying(true) .addPlatformFilter(::DemoFilter) .addPlatformFilter(::BufferDemoFilter) .addPlatformFilter(::AsyncDemoFilter) diff --git a/mobile/library/common/config/config.cc b/mobile/library/common/config/config.cc index 5d987e9fc0d4..07e7aa59098c 100644 --- a/mobile/library/common/config/config.cc +++ b/mobile/library/common/config/config.cc @@ -153,15 +153,19 @@ R"( !ignore tls_socket_defs: - &base_tls_socket - name: envoy.transport_sockets.tls + name: envoy.transport_sockets.http_11_proxy typed_config: - "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext - common_tls_context: - tls_params: - tls_maximum_protocol_version: TLSv1_3 - validation_context: - trusted_ca: - inline_string: *tls_root_certs + "@type": type.googleapis.com/envoy.extensions.transport_sockets.http_11_proxy.v3.Http11ProxyUpstreamTransport + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + common_tls_context: + tls_params: + tls_maximum_protocol_version: TLSv1_3 + validation_context: + trusted_ca: + inline_string: *tls_root_certs )"; const char* config_template = R"( @@ -406,9 +410,13 @@ const char* config_template = R"( lb_policy: CLUSTER_PROVIDED cluster_type: *base_cluster_type transport_socket: - name: envoy.transport_sockets.raw_buffer + name: envoy.transport_sockets.http_11_proxy typed_config: - "@type": type.googleapis.com/envoy.extensions.transport_sockets.raw_buffer.v3.RawBuffer + "@type": type.googleapis.com/envoy.extensions.transport_sockets.http_11_proxy.v3.Http11ProxyUpstreamTransport + transport_socket: + name: envoy.transport_sockets.raw_buffer + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.raw_buffer.v3.RawBuffer upstream_connection_options: *upstream_opts circuit_breakers: *circuit_breakers_settings typed_extension_protocol_options: *h1_protocol_options @@ -422,17 +430,21 @@ const char* config_template = R"( lb_policy: CLUSTER_PROVIDED cluster_type: *base_cluster_type transport_socket: - name: envoy.transport_sockets.tls + name: envoy.transport_sockets.http_11_proxy typed_config: - "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext - common_tls_context: - alpn_protocols: [h2] - tls_params: - tls_maximum_protocol_version: TLSv1_3 - validation_context: - trusted_ca: - inline_string: *tls_root_certs - trust_chain_verification: *trust_chain_verification + "@type": type.googleapis.com/envoy.extensions.transport_sockets.http_11_proxy.v3.Http11ProxyUpstreamTransport + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + common_tls_context: + alpn_protocols: [h2] + tls_params: + tls_maximum_protocol_version: TLSv1_3 + validation_context: + trusted_ca: + inline_string: *tls_root_certs + trust_chain_verification: *trust_chain_verification upstream_connection_options: *upstream_opts circuit_breakers: *circuit_breakers_settings typed_extension_protocol_options: *h2_protocol_options diff --git a/mobile/library/common/extensions/filters/http/network_configuration/BUILD b/mobile/library/common/extensions/filters/http/network_configuration/BUILD index 0e0bc8b28e75..6e3788d4e16f 100644 --- a/mobile/library/common/extensions/filters/http/network_configuration/BUILD +++ b/mobile/library/common/extensions/filters/http/network_configuration/BUILD @@ -33,6 +33,7 @@ envoy_cc_extension( "@envoy//source/common/http:header_map_lib", "@envoy//source/common/http:headers_lib", "@envoy//source/common/http:utility_lib", + "@envoy//source/common/network:filter_state_proxy_info_lib", "@envoy//source/extensions/filters/http/common:pass_through_filter_lib", ], ) diff --git a/mobile/library/common/extensions/filters/http/network_configuration/filter.cc b/mobile/library/common/extensions/filters/http/network_configuration/filter.cc index da7ed50300b8..b8a049db515d 100644 --- a/mobile/library/common/extensions/filters/http/network_configuration/filter.cc +++ b/mobile/library/common/extensions/filters/http/network_configuration/filter.cc @@ -2,11 +2,15 @@ #include "envoy/server/filter_config.h" +#include "source/common/network/filter_state_proxy_info.h" + namespace Envoy { namespace Extensions { namespace HttpFilters { namespace NetworkConfiguration { +const Http::LowerCaseString AuthorityHeaderName{":authority"}; + void NetworkConfigurationFilter::setDecoderFilterCallbacks( Http::StreamDecoderFilterCallbacks& callbacks) { ENVOY_LOG(debug, "NetworkConfigurationFilter::setDecoderFilterCallbacks"); @@ -26,6 +30,36 @@ void NetworkConfigurationFilter::setDecoderFilterCallbacks( decoder_callbacks_->addUpstreamSocketOptions(options); } +Http::FilterHeadersStatus +NetworkConfigurationFilter::decodeHeaders(Http::RequestHeaderMap& request_headers, bool) { + const auto proxy_settings = connectivity_manager_->getProxySettings(); + + ENVOY_LOG(trace, "NetworkConfigurationFilter::decodeHeaders", request_headers); + if (proxy_settings == nullptr) { + return Http::FilterHeadersStatus::Continue; + } + + const auto proxy_address = proxy_settings->address(); + + if (proxy_address != nullptr) { + const auto authorityHeader = request_headers.get(AuthorityHeaderName); + if (authorityHeader.empty()) { + return Http::FilterHeadersStatus::Continue; + } + + const auto authority = authorityHeader[0]->value().getStringView(); + + ENVOY_LOG(trace, "netconf_filter_set_proxy_for_request", proxy_settings->asString()); + decoder_callbacks_->streamInfo().filterState()->setData( + Network::Http11ProxyInfoFilterState::key(), + std::make_unique(authority, proxy_address), + StreamInfo::FilterState::StateType::ReadOnly, + StreamInfo::FilterState::LifeSpan::FilterChain); + } + + return Http::FilterHeadersStatus::Continue; +} + Http::FilterHeadersStatus NetworkConfigurationFilter::encodeHeaders(Http::ResponseHeaderMap&, bool) { ENVOY_LOG(debug, "NetworkConfigurationFilter::encodeHeaders"); diff --git a/mobile/library/common/extensions/filters/http/network_configuration/filter.h b/mobile/library/common/extensions/filters/http/network_configuration/filter.h index 20d28bbc8325..e46deb94ec9b 100644 --- a/mobile/library/common/extensions/filters/http/network_configuration/filter.h +++ b/mobile/library/common/extensions/filters/http/network_configuration/filter.h @@ -30,6 +30,7 @@ class NetworkConfigurationFilter final : public Http::PassThroughFilter, // Http::StreamDecoderFilter void setDecoderFilterCallbacks(Http::StreamDecoderFilterCallbacks& callbacks) override; + Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap& request_headers, bool) override; // Http::StreamEncoderFilter Http::FilterHeadersStatus encodeHeaders(Http::ResponseHeaderMap&, bool) override; // Http::StreamFilterBase diff --git a/mobile/library/common/jni/jni_interface.cc b/mobile/library/common/jni/jni_interface.cc index 0d64e14d7010..1b1995cc820d 100644 --- a/mobile/library/common/jni/jni_interface.cc +++ b/mobile/library/common/jni/jni_interface.cc @@ -1132,6 +1132,22 @@ Java_io_envoyproxy_envoymobile_engine_JniLibrary_setPreferredNetwork(JNIEnv* env static_cast(network)); } +extern "C" JNIEXPORT jint JNICALL Java_io_envoyproxy_envoymobile_engine_JniLibrary_setProxySettings( + JNIEnv* env, + jclass, // class + jlong engine, jstring host, jint port) { + jni_log("[Envoy]", "setProxySettings"); + + const char* native_host = env->GetStringUTFChars(host, nullptr); + const uint16_t native_port = static_cast(port); + + envoy_status_t result = + set_proxy_settings(static_cast(engine), native_host, native_port); + + env->ReleaseStringUTFChars(host, native_host); + return result; +} + bool jvm_cert_is_issued_by_known_root(JNIEnv* env, jobject result) { jclass jcls_AndroidCertVerifyResult = env->FindClass("org/chromium/net/AndroidCertVerifyResult"); jmethodID jmid_isIssuedByKnownRoot = diff --git a/mobile/library/common/main_interface.cc b/mobile/library/common/main_interface.cc index d67cf8c273db..4f62f1692391 100644 --- a/mobile/library/common/main_interface.cc +++ b/mobile/library/common/main_interface.cc @@ -75,6 +75,13 @@ envoy_status_t set_preferred_network(envoy_engine_t engine, envoy_network_t netw return ENVOY_SUCCESS; } +envoy_status_t set_proxy_settings(envoy_engine_t e, const char* host, const uint16_t port) { + return Envoy::EngineHandle::runOnEngineDispatcher( + e, + [proxy_settings = Envoy::Network::ProxySettings::parseHostAndPort(host, port)](auto& engine) + -> void { engine.networkConnectivityManager().setProxySettings(proxy_settings); }); +} + envoy_status_t record_counter_inc(envoy_engine_t e, const char* elements, envoy_stats_tags tags, uint64_t count) { return Envoy::EngineHandle::runOnEngineDispatcher( diff --git a/mobile/library/common/main_interface.h b/mobile/library/common/main_interface.h index 6d77a5127c2b..bdbf48a93c88 100644 --- a/mobile/library/common/main_interface.h +++ b/mobile/library/common/main_interface.h @@ -98,6 +98,16 @@ envoy_status_t reset_stream(envoy_engine_t engine, envoy_stream_t stream); */ envoy_status_t set_preferred_network(envoy_engine_t engine, envoy_network_t network); +/** + * @brief Update the currently active proxy settings. + * + * @param engine, the engine whose proxy settings should be updated. + * @param host, the proxy host. + * @param port, the proxy port. + * @return envoy_status_t, the resulting status of the operation. + */ +envoy_status_t set_proxy_settings(envoy_engine_t engine, const char* host, const uint16_t port); + /** * Increment a counter with the given elements and by the given count. * @param engine, the engine that owns the counter. diff --git a/mobile/library/common/network/BUILD b/mobile/library/common/network/BUILD index f8e738c9a4ca..c60ffff3083f 100644 --- a/mobile/library/common/network/BUILD +++ b/mobile/library/common/network/BUILD @@ -20,6 +20,7 @@ envoy_cc_library( hdrs = [ "android.h", "connectivity_manager.h", + "proxy_settings.h", ], copts = select({ "//bazel:include_ifaddrs": ["-DINCLUDE_IFADDRS"], diff --git a/mobile/library/common/network/connectivity_manager.cc b/mobile/library/common/network/connectivity_manager.cc index 61880757379f..b3705802de5d 100644 --- a/mobile/library/common/network/connectivity_manager.cc +++ b/mobile/library/common/network/connectivity_manager.cc @@ -102,6 +102,24 @@ envoy_netconf_t ConnectivityManager::setPreferredNetwork(envoy_network_t network return network_state_.configuration_key_; } +void ConnectivityManager::setProxySettings(ProxySettingsConstSharedPtr new_proxy_settings) { + if (proxy_settings_ == nullptr && new_proxy_settings != nullptr) { + ENVOY_LOG_EVENT(info, "netconf_proxy_change", new_proxy_settings->asString()); + proxy_settings_ = new_proxy_settings; + } else if (proxy_settings_ != nullptr && new_proxy_settings == nullptr) { + ENVOY_LOG_EVENT(info, "netconf_proxy_change", "no_proxy_configured"); + proxy_settings_ = new_proxy_settings; + } else if (proxy_settings_ != nullptr && new_proxy_settings != nullptr && + *proxy_settings_ != *new_proxy_settings) { + ENVOY_LOG_EVENT(info, "netconf_proxy_change", new_proxy_settings->asString()); + proxy_settings_ = new_proxy_settings; + } + + return; +} + +ProxySettingsConstSharedPtr ConnectivityManager::getProxySettings() { return proxy_settings_; } + envoy_network_t ConnectivityManager::getPreferredNetwork() { Thread::LockGuard lock{network_state_.mutex_}; return network_state_.network_; diff --git a/mobile/library/common/network/connectivity_manager.h b/mobile/library/common/network/connectivity_manager.h index ca6492c5d2a6..182ca30e09a5 100644 --- a/mobile/library/common/network/connectivity_manager.h +++ b/mobile/library/common/network/connectivity_manager.h @@ -10,6 +10,7 @@ #include "source/extensions/common/dynamic_forward_proxy/dns_cache.h" #include "source/extensions/common/dynamic_forward_proxy/dns_cache_impl.h" +#include "library/common/network/proxy_settings.h" #include "library/common/types/c_types.h" /** @@ -128,6 +129,12 @@ class ConnectivityManager */ envoy_netconf_t getConfigurationKey(); + /** + * + * @return the current proxy settings. + */ + Envoy::Network::ProxySettingsConstSharedPtr getProxySettings(); + /** * Call to report on the current viability of the passed network configuration after an attempt * at transmission (e.g., an HTTP request). @@ -143,6 +150,13 @@ class ConnectivityManager */ static envoy_netconf_t setPreferredNetwork(envoy_network_t network); + /** + * @brief Sets the current proxy settings. + * + * @param host The proxy settings. `nullptr` if there is no proxy configured on a device. + */ + void setProxySettings(ProxySettingsConstSharedPtr proxy_settings); + /** * Configure whether connections should be drained after a triggered DNS refresh. Currently this * may happen either due to an external call to refreshConnectivityState or an update to @@ -211,6 +225,7 @@ class ConnectivityManager dns_callbacks_handle_{nullptr}; Upstream::ClusterManager& cluster_manager_; DnsCacheManagerSharedPtr dns_cache_manager_; + ProxySettingsConstSharedPtr proxy_settings_; static NetworkState network_state_; }; diff --git a/mobile/library/common/network/proxy_settings.h b/mobile/library/common/network/proxy_settings.h new file mode 100644 index 000000000000..196273c111ad --- /dev/null +++ b/mobile/library/common/network/proxy_settings.h @@ -0,0 +1,83 @@ +#pragma once + +#include "source/common/network/utility.h" + +namespace Envoy { +namespace Network { + +struct ProxySettings; +using ProxySettingsConstSharedPtr = std::shared_ptr; + +/** + * Proxy settings coming from platform specific APIs, i.e. ConnectivityManager in + * the case of Android platform. + * + */ +struct ProxySettings { + /** + * @brief Construct a new Proxy Settings object. + * + * @param host The proxy host defined as a hostname or an IP address. Some platforms + * (i.e., Android) allow users to specify proxy using either one of these. + * @param port The proxy port. + */ + ProxySettings(const std::string& host, const uint16_t port) + : address_(Envoy::Network::Utility::parseInternetAddressNoThrow(host, port)) {} + + /** + * @brief Parses given host and domain and creates proxy settings. Returns nullptr + * for an empty host and a port equal to 0 as they are passed to c++ native layer + * as a synonym of the lack of proxy settings configured on a device. + * + * @param host The proxy host defined as a hostname or an IP address. Some platforms + * (i.e., Android) allow users to specify proxy using either one of these. + * @param port The proxy port. + * @return The created proxy settings, nullptr if the passed host is an empty string and + * port is equal to 0. + */ + static const ProxySettingsConstSharedPtr parseHostAndPort(const std::string& host, + const uint16_t port) { + if (host == "" && port == 0) { + return nullptr; + } + return std::make_shared(host, port); + } + + /** + * @brief Returns an address of a proxy. This method returns nullptr for proxy settings + * that are initialized with anything other than an IP address. + * + * @return Address of a proxy or nullptr if proxy address is incorrect or host is + * defined using a hostname and not an IP address. + */ + const Envoy::Network::Address::InstanceConstSharedPtr& address() const { return address_; } + + /** + * @brief Returns a human readable representation of the proxy settings represented + * by the receiver + * + * @return const A human readable representation of the receiver. + */ + const std::string asString() const { + if (address_ != nullptr) { + return address_->asString(); + } + return "no_proxy_configured"; + } + + bool operator==(ProxySettings const& rhs) const { + if (this->address() == nullptr || rhs.address() == nullptr) { + return this->address() == nullptr && rhs.address() == nullptr; + } + + return this->address()->asString() == rhs.address()->asString(); + } + + bool operator!=(ProxySettings const& rhs) const { return !(*this == rhs); } + +private: + Envoy::Network::Address::InstanceConstSharedPtr address_; +}; + +} // namespace Network +} // namespace Envoy diff --git a/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidEngineImpl.java b/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidEngineImpl.java index 5434f327e95e..596cbb1a8d5c 100644 --- a/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidEngineImpl.java +++ b/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidEngineImpl.java @@ -18,10 +18,14 @@ public class AndroidEngineImpl implements EnvoyEngine { * @param runningCallback Called when the engine finishes its async startup and begins running. */ public AndroidEngineImpl(Context context, EnvoyOnEngineRunning runningCallback, - EnvoyLogger logger, EnvoyEventTracker eventTracker) { + EnvoyLogger logger, EnvoyEventTracker eventTracker, + Boolean enableProxying) { this.envoyEngine = new EnvoyEngineImpl(runningCallback, logger, eventTracker); AndroidJniLibrary.load(context); AndroidNetworkMonitor.load(context, envoyEngine); + if (enableProxying) { + AndroidProxyMonitor.load(context, envoyEngine); + } } @Override @@ -98,4 +102,6 @@ public void resetConnectivityState() { public void setPreferredNetwork(EnvoyNetworkType network) { envoyEngine.setPreferredNetwork(network); } + + public void setProxySettings(String host, int port) { envoyEngine.setProxySettings(host, port); } } diff --git a/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidProxyMonitor.java b/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidProxyMonitor.java new file mode 100644 index 000000000000..d912b4e5becb --- /dev/null +++ b/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidProxyMonitor.java @@ -0,0 +1,59 @@ +package io.envoyproxy.envoymobile.engine; + +import android.annotation.TargetApi; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.Proxy; +import android.net.ProxyInfo; +import android.os.Build; + +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class AndroidProxyMonitor extends BroadcastReceiver { + private static volatile AndroidProxyMonitor instance = null; + private ConnectivityManager connectivityManager; + private EnvoyEngine envoyEngine; + + public static void load(Context context, EnvoyEngine envoyEngine) { + if (instance != null) { + return; + } + + synchronized (AndroidProxyMonitor.class) { + if (instance != null) { + return; + } + instance = new AndroidProxyMonitor(context, envoyEngine); + } + } + + private AndroidProxyMonitor(Context context, EnvoyEngine envoyEngine) { + this.envoyEngine = envoyEngine; + this.connectivityManager = + (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); + registerReceiver(context); + this.handleProxyChange(); + } + + private void registerReceiver(Context context) { + context.getApplicationContext().registerReceiver(this, new IntentFilter() { + { addAction(Proxy.PROXY_CHANGE_ACTION); } + }); + } + + @Override + public void onReceive(Context context, Intent intent) { + handleProxyChange(); + } + + private void handleProxyChange() { + ProxyInfo info = connectivityManager.getDefaultProxy(); + if (info == null) { + envoyEngine.setProxySettings("", 0); + } else { + envoyEngine.setProxySettings(info.getHost(), info.getPort()); + } + } +} diff --git a/mobile/library/java/io/envoyproxy/envoymobile/engine/BUILD b/mobile/library/java/io/envoyproxy/envoymobile/engine/BUILD index be4f2a83805c..d42d805fdf20 100644 --- a/mobile/library/java/io/envoyproxy/envoymobile/engine/BUILD +++ b/mobile/library/java/io/envoyproxy/envoymobile/engine/BUILD @@ -9,6 +9,7 @@ android_library( "AndroidEngineImpl.java", "AndroidJniLibrary.java", "AndroidNetworkMonitor.java", + "AndroidProxyMonitor.java", ], custom_package = "io.envoyproxy.envoymobile.engine", manifest = "AndroidEngineManifest.xml", diff --git a/mobile/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngine.java b/mobile/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngine.java index 7b9e18036b8f..dd65122fe299 100644 --- a/mobile/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngine.java +++ b/mobile/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngine.java @@ -127,4 +127,13 @@ int runWithTemplate(String configurationYAML, EnvoyConfiguration envoyConfigurat * @param network The network to be preferred for new streams. */ void setPreferredNetwork(EnvoyNetworkType network); + + /** + * Update proxy settings. + * + * @param host The proxy host defined as a hostname or an IP address. Android + * allow users to specify proxy using either one of these. + * @param port The proxy port. + */ + void setProxySettings(String host, int port); } diff --git a/mobile/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngineImpl.java b/mobile/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngineImpl.java index 63c4ef9122b5..1d180d580030 100644 --- a/mobile/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngineImpl.java +++ b/mobile/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngineImpl.java @@ -233,4 +233,8 @@ public void setPreferredNetwork(EnvoyNetworkType network) { return; } } + + public void setProxySettings(String host, int port) { + JniLibrary.setProxySettings(engineHandle, host, port); + } } diff --git a/mobile/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java b/mobile/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java index b4412f95ad26..54d6add8b1ef 100644 --- a/mobile/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java +++ b/mobile/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java @@ -356,6 +356,16 @@ protected static native int registerStringAccessor(String accessorName, */ protected static native int setPreferredNetwork(long engine, int network); + /** + * Update the proxy settings. + * + * @param engine Handle to the engine whose proxy settings should be updated. + * @param host The proxy host. + * @param port The proxy port. + * @return The resulting status of the operation. + */ + protected static native int setProxySettings(long engine, String host, int port); + /** * Mimic a call to AndroidNetworkLibrary#verifyServerCertificates from native code. * To be used for testing only. diff --git a/mobile/library/java/org/chromium/net/impl/NativeCronetEngineBuilderImpl.java b/mobile/library/java/org/chromium/net/impl/NativeCronetEngineBuilderImpl.java index c9852f8408fb..ef6e28a7e6e7 100644 --- a/mobile/library/java/org/chromium/net/impl/NativeCronetEngineBuilderImpl.java +++ b/mobile/library/java/org/chromium/net/impl/NativeCronetEngineBuilderImpl.java @@ -49,6 +49,7 @@ public class NativeCronetEngineBuilderImpl extends CronetEngineBuilderImpl { private boolean mEnableSocketTag = true; private boolean mEnableHappyEyeballs = false; private boolean mEnableInterfaceBinding = false; + private boolean mEnableProxying = false; private int mH2ConnectionKeepaliveIdleIntervalMilliseconds = 100000000; private int mH2ConnectionKeepaliveTimeoutSeconds = 10; private boolean mH2ExtendKeepaliveTimeout = false; @@ -89,8 +90,8 @@ public ExperimentalCronetEngine build() { } EnvoyEngine createEngine(EnvoyOnEngineRunning onEngineRunning) { - AndroidEngineImpl engine = - new AndroidEngineImpl(getContext(), onEngineRunning, mEnvoyLogger, mEnvoyEventTracker); + AndroidEngineImpl engine = new AndroidEngineImpl(getContext(), onEngineRunning, mEnvoyLogger, + mEnvoyEventTracker, mEnableProxying); AndroidJniLibrary.load(getContext()); AndroidNetworkMonitor.load(getContext(), engine); engine.runWithConfig(createEnvoyConfiguration(), getLogLevel()); diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/AndroidEngineBuilder.kt b/mobile/library/kotlin/io/envoyproxy/envoymobile/AndroidEngineBuilder.kt index 80972b24d066..58145ec68f5f 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/AndroidEngineBuilder.kt +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/AndroidEngineBuilder.kt @@ -12,6 +12,8 @@ class AndroidEngineBuilder @JvmOverloads constructor( baseConfiguration: BaseConfiguration = Standard() ) : EngineBuilder(baseConfiguration) { init { - addEngineType { AndroidEngineImpl(context, onEngineRunning, logger, eventTracker) } + addEngineType { + AndroidEngineImpl(context, onEngineRunning, logger, eventTracker, enableProxying) + } } } diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/EngineBuilder.kt b/mobile/library/kotlin/io/envoyproxy/envoymobile/EngineBuilder.kt index d414c385b4fc..6b1f37273edd 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/EngineBuilder.kt +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/EngineBuilder.kt @@ -1,4 +1,4 @@ -package io.envoyproxy.envoymobile + package io.envoyproxy.envoymobile import io.envoyproxy.envoymobile.engine.EnvoyConfiguration import io.envoyproxy.envoymobile.engine.EnvoyConfiguration.TrustChainVerification @@ -36,6 +36,7 @@ open class EngineBuilder( protected var onEngineRunning: (() -> Unit) = {} protected var logger: ((String) -> Unit)? = null protected var eventTracker: ((Map) -> Unit)? = null + protected var enableProxying = false private var engineType: () -> EnvoyEngine = { EnvoyEngineImpl(onEngineRunning, logger, eventTracker) } @@ -326,6 +327,24 @@ open class EngineBuilder( return this } + /** + * Specify whether system proxy settings should be respected. If yes, Envoy Mobile will + * use Android APIs to query Android Proxy settings configured on a device and will + * respect these settings when establishing connections with remote services. + * + * The method is introduced for experimentation purposes and as a safety guard against + * critical issues in the implementation of the proxying feature. It's intended to be removed + * after it's confirmed that proxies on Android work as expected. + * + * @param enableProxying whether to enable Envoy's support for proxies. + * + * @return This builder. + */ + fun enableProxying(enableProxying: Boolean): EngineBuilder { + this.enableProxying = enableProxying + return this + } + /** * Add a rate at which to ping h2 connections on new stream creation if the connection has * sat idle. Defaults to 1 millisecond which effectively enables h2 ping functionality diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/mocks/MockEnvoyEngine.kt b/mobile/library/kotlin/io/envoyproxy/envoymobile/mocks/MockEnvoyEngine.kt index fcbbfe18b416..d578b352ba19 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/mocks/MockEnvoyEngine.kt +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/mocks/MockEnvoyEngine.kt @@ -49,4 +49,6 @@ internal class MockEnvoyEngine : EnvoyEngine { override fun resetConnectivityState() = Unit override fun setPreferredNetwork(network: EnvoyNetworkType) = Unit + + override fun setProxySettings(host: String, port: Int) = Unit } diff --git a/mobile/test/common/network/BUILD b/mobile/test/common/network/BUILD index e0ee38f03569..050e0386a5b2 100644 --- a/mobile/test/common/network/BUILD +++ b/mobile/test/common/network/BUILD @@ -15,6 +15,15 @@ envoy_cc_test( ], ) +envoy_cc_test( + name = "proxy_settings_test", + srcs = ["proxy_settings_test.cc"], + repository = "@envoy", + deps = [ + "//library/common/network:connectivity_manager_lib", + ], +) + envoy_cc_test( name = "src_addr_socket_option_impl_test", srcs = ["src_addr_socket_option_impl_test.cc"], diff --git a/mobile/test/common/network/connectivity_manager_test.cc b/mobile/test/common/network/connectivity_manager_test.cc index 9545ff4ad483..e8e8bd478d7c 100644 --- a/mobile/test/common/network/connectivity_manager_test.cc +++ b/mobile/test/common/network/connectivity_manager_test.cc @@ -205,5 +205,42 @@ TEST_F(ConnectivityManagerTest, EnumerateInterfacesFiltersByFlags) { EXPECT_EQ(empty.size(), 0); } +TEST_F(ConnectivityManagerTest, OverridesNoProxySettingsWithNewProxySettings) { + EXPECT_EQ(nullptr, connectivity_manager_->getProxySettings()); + + const auto proxy_settings = ProxySettings::parseHostAndPort("127.0.0.1", 9999); + connectivity_manager_->setProxySettings(proxy_settings); + EXPECT_EQ("127.0.0.1:9999", connectivity_manager_->getProxySettings()->asString()); +} + +TEST_F(ConnectivityManagerTest, OverridesCurrentProxySettingsWithNoProxySettings) { + const auto proxy_settings = ProxySettings::parseHostAndPort("127.0.0.1", 9999); + connectivity_manager_->setProxySettings(proxy_settings); + EXPECT_EQ("127.0.0.1:9999", connectivity_manager_->getProxySettings()->asString()); + + connectivity_manager_->setProxySettings(nullptr); + EXPECT_EQ(nullptr, connectivity_manager_->getProxySettings()); +} + +TEST_F(ConnectivityManagerTest, OverridesCurrentProxySettingsWithNewProxySettings) { + const auto proxy_settings1 = ProxySettings::parseHostAndPort("127.0.0.1", 9999); + connectivity_manager_->setProxySettings(proxy_settings1); + EXPECT_EQ("127.0.0.1:9999", connectivity_manager_->getProxySettings()->asString()); + + const auto proxy_settings2 = ProxySettings::parseHostAndPort("127.0.0.1", 8888); + connectivity_manager_->setProxySettings(proxy_settings2); + EXPECT_EQ(proxy_settings2, connectivity_manager_->getProxySettings()); +} + +TEST_F(ConnectivityManagerTest, IgnoresDuplicatedProxySettingsUpdates) { + const auto proxy_settings1 = ProxySettings::parseHostAndPort("127.0.0.1", 9999); + connectivity_manager_->setProxySettings(proxy_settings1); + EXPECT_EQ("127.0.0.1:9999", connectivity_manager_->getProxySettings()->asString()); + + const auto proxy_settings2 = ProxySettings::parseHostAndPort("127.0.0.1", 9999); + connectivity_manager_->setProxySettings(proxy_settings2); + EXPECT_EQ(proxy_settings1, connectivity_manager_->getProxySettings()); +} + } // namespace Network } // namespace Envoy diff --git a/mobile/test/common/network/proxy_settings_test.cc b/mobile/test/common/network/proxy_settings_test.cc new file mode 100644 index 000000000000..cf8ff6e672cf --- /dev/null +++ b/mobile/test/common/network/proxy_settings_test.cc @@ -0,0 +1,30 @@ +#include "gtest/gtest.h" +#include "library/common/network/proxy_settings.h" + +namespace Envoy { +namespace Network { + +class ProxySettingsTest : public testing::Test { +public: + ProxySettingsTest() {} +}; + +TEST_F(ProxySettingsTest, SameIPv4AddressesAndPortsAreEqual) { + EXPECT_EQ(ProxySettings("127.0.0.1", 2222), ProxySettings("127.0.0.1", 2222)); +} + +TEST_F(ProxySettingsTest, DifferentPortsAreNotEqual) { + EXPECT_NE(ProxySettings("127.0.0.1", 1111), ProxySettings("127.0.0.1", 2222)); +} + +TEST_F(ProxySettingsTest, DifferentAddressesAreNotEqual) { + EXPECT_NE(ProxySettings("127.0.0.2", 1111), ProxySettings("127.0.0.1", 1111)); +} + +TEST_F(ProxySettingsTest, EmptyAddressStringResultsInNullAddress) { + EXPECT_EQ(ProxySettings("", 0).address(), nullptr); + EXPECT_EQ(ProxySettings("", 0).asString(), "no_proxy_configured"); +} + +} // namespace Network +} // namespace Envoy diff --git a/mobile/test/kotlin/integration/proxying/BUILD b/mobile/test/kotlin/integration/proxying/BUILD new file mode 100644 index 000000000000..737e9a5e32a5 --- /dev/null +++ b/mobile/test/kotlin/integration/proxying/BUILD @@ -0,0 +1,54 @@ +load("@envoy_mobile//bazel:kotlin_lib.bzl", "envoy_mobile_kt_library") +load("@envoy_mobile//bazel:kotlin_test.bzl", "envoy_mobile_android_test") + +envoy_mobile_kt_library( + name = "proxy_lib", + srcs = ["Proxy.kt"], + visibility = ["//visibility:public"], + deps = [ + "//library/kotlin/io/envoyproxy/envoymobile:envoy_interfaces_lib", + "//library/kotlin/io/envoyproxy/envoymobile:envoy_lib", + ], +) + +envoy_mobile_android_test( + name = "perform_http_request_using_proxy_test", + srcs = [ + "PerformHTTPRequestUsingProxyTest.kt", + ], + exec_properties = { + # TODO(lfpino): Remove this once the sandboxNetwork=off works for ipv4 localhost addresses. + "sandboxNetwork": "standard", + "dockerNetwork": "standard", + }, + native_deps = [ + "//library/common/jni:libndk_envoy_jni.so", + "//library/common/jni:libndk_envoy_jni.jnilib", + ], + deps = [ + "//library/kotlin/io/envoyproxy/envoymobile:envoy_interfaces_lib", + "//library/kotlin/io/envoyproxy/envoymobile:envoy_lib", + "//test/kotlin/integration/proxying:proxy_lib", + ], +) + +envoy_mobile_android_test( + name = "perform_https_request_using_proxy_test", + srcs = [ + "PerformHTTPSRequestUsingProxyTest.kt", + ], + exec_properties = { + # TODO(lfpino): Remove this once the sandboxNetwork=off works for ipv4 localhost addresses. + "sandboxNetwork": "standard", + "dockerNetwork": "standard", + }, + native_deps = [ + "//library/common/jni:libndk_envoy_jni.so", + "//library/common/jni:libndk_envoy_jni.jnilib", + ], + deps = [ + "//library/kotlin/io/envoyproxy/envoymobile:envoy_interfaces_lib", + "//library/kotlin/io/envoyproxy/envoymobile:envoy_lib", + "//test/kotlin/integration/proxying:proxy_lib", + ], +) diff --git a/mobile/test/kotlin/integration/proxying/PerformHTTPRequestUsingProxyTest.kt b/mobile/test/kotlin/integration/proxying/PerformHTTPRequestUsingProxyTest.kt new file mode 100644 index 000000000000..e9fbe38ba6ee --- /dev/null +++ b/mobile/test/kotlin/integration/proxying/PerformHTTPRequestUsingProxyTest.kt @@ -0,0 +1,106 @@ +package test.kotlin.integration.proxying + +import android.content.Context +import android.net.ConnectivityManager +import android.net.ProxyInfo +import androidx.test.core.app.ApplicationProvider + +import io.envoyproxy.envoymobile.AndroidEngineBuilder +import io.envoyproxy.envoymobile.Custom +import io.envoyproxy.envoymobile.Engine +import io.envoyproxy.envoymobile.engine.JniLibrary +import io.envoyproxy.envoymobile.LogLevel +import io.envoyproxy.envoymobile.RequestHeadersBuilder +import io.envoyproxy.envoymobile.RequestMethod +import io.envoyproxy.envoymobile.ResponseHeaders +import io.envoyproxy.envoymobile.StreamIntel + +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.robolectric.RobolectricTestRunner + +// ┌──────────────────┐ +// │ Proxy Engine │ +// │ ┌──────────────┐ │ +// ┌────────────────────────┐ ┌─┼─►listener_proxy│ │ +// │http://api.lyft.com/ping│ ┌──────────────┬┘ │ └──────┬───────┘ │ ┌────────────┐ +// │ Request ├──►Android Engine│ │ │ │ │api.lyft.com│ +// └────────────────────────┘ └──────────────┘ │ ┌──────▼──────┐ │ └──────▲─────┘ +// │ │cluster_proxy│ │ │ +// │ └─────────────┴──┼────────┘ +// │ │ +// └──────────────────┘ +@RunWith(RobolectricTestRunner::class) +class PerformHTTPRequestUsingProxy { + init { + JniLibrary.loadTestLibrary() + } + + @Test + fun `performs an HTTP request through a proxy`() { + val port = (10001..11000).random() + + val mockContext = Mockito.mock(Context::class.java) + Mockito.`when`(mockContext.getApplicationContext()).thenReturn(mockContext) + val mockConnectivityManager = Mockito.mock(ConnectivityManager::class.java) + Mockito.`when`(mockContext.getSystemService(Mockito.anyString())).thenReturn(mockConnectivityManager) + Mockito.`when`(mockConnectivityManager.getDefaultProxy()).thenReturn(ProxyInfo.buildDirectProxy("127.0.0.1", port)) + + val onProxyEngineRunningLatch = CountDownLatch(1) + val onEngineRunningLatch = CountDownLatch(1) + val onRespondeHeadersLatch = CountDownLatch(1) + + val proxyEngineBuilder = Proxy(ApplicationProvider.getApplicationContext(), port) + .http() + val proxyEngine = proxyEngineBuilder + .addLogLevel(LogLevel.DEBUG) + .setOnEngineRunning { onProxyEngineRunningLatch.countDown() } + .build() + + onProxyEngineRunningLatch.await(10, TimeUnit.SECONDS) + assertThat(onProxyEngineRunningLatch.count).isEqualTo(0) + + val builder = AndroidEngineBuilder(mockContext) + val engine = builder + .addLogLevel(LogLevel.DEBUG) + .enableProxying(true) + .setOnEngineRunning { onEngineRunningLatch.countDown() } + .build() + + onEngineRunningLatch.await(10, TimeUnit.SECONDS) + assertThat(onEngineRunningLatch.count).isEqualTo(0) + + val requestHeaders = RequestHeadersBuilder( + method = RequestMethod.GET, + scheme = "http", + authority = "api.lyft.com", + path = "/ping" + ) + .build() + + engine + .streamClient() + .newStreamPrototype() + .setOnResponseHeaders { responseHeaders, _, _ -> + val status = responseHeaders.httpStatus ?: 0L + assertThat(status).isEqualTo(301) + assertThat(responseHeaders.value("x-proxy-response")).isEqualTo(listOf("true")) + onRespondeHeadersLatch.countDown() + } + .start(Executors.newSingleThreadExecutor()) + .sendHeaders(requestHeaders, true) + + onRespondeHeadersLatch.await(15, TimeUnit.SECONDS) + assertThat(onRespondeHeadersLatch.count).isEqualTo(0) + + engine.terminate() + proxyEngine.terminate() + } +} diff --git a/mobile/test/kotlin/integration/proxying/PerformHTTPSRequestUsingProxyTest.kt b/mobile/test/kotlin/integration/proxying/PerformHTTPSRequestUsingProxyTest.kt new file mode 100644 index 000000000000..af260b5dfc65 --- /dev/null +++ b/mobile/test/kotlin/integration/proxying/PerformHTTPSRequestUsingProxyTest.kt @@ -0,0 +1,107 @@ +package test.kotlin.integration.proxying + + +import android.content.Context +import android.net.ConnectivityManager +import android.net.ProxyInfo +import androidx.test.core.app.ApplicationProvider + +import io.envoyproxy.envoymobile.LogLevel +import io.envoyproxy.envoymobile.Custom +import io.envoyproxy.envoymobile.Engine +import io.envoyproxy.envoymobile.UpstreamHttpProtocol +import io.envoyproxy.envoymobile.AndroidEngineBuilder +import io.envoyproxy.envoymobile.RequestHeadersBuilder +import io.envoyproxy.envoymobile.RequestMethod +import io.envoyproxy.envoymobile.ResponseHeaders +import io.envoyproxy.envoymobile.StreamIntel +import io.envoyproxy.envoymobile.engine.JniLibrary + +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.robolectric.RobolectricTestRunner + +// ┌──────────────────┐ +// │ Proxy Engine │ +// │ ┌──────────────┐ │ +// ┌─────────────────────────┐ ┌─┼─►listener_proxy│ │ +// │https://api.lyft.com/ping│ ┌──────────────┬┘ │ └──────┬───────┘ │ ┌────────────┐ +// │ Request ├──►Android Engine│ │ │ │ │api.lyft.com│ +// └─────────────────────────┘ └──────────────┘ │ ┌──────▼──────┐ │ └──────▲─────┘ +// │ │cluster_proxy│ │ │ +// │ └─────────────┴──┼────────┘ +// │ │ +// └──────────────────┘ +@RunWith(RobolectricTestRunner::class) +class PerformHTTPSRequestUsingProxy { + init { + JniLibrary.loadTestLibrary() + } + + @Test + fun `performs an HTTPs request through a proxy`() { + val port = (10001..11000).random() + + val mockContext = Mockito.mock(Context::class.java) + Mockito.`when`(mockContext.getApplicationContext()).thenReturn(mockContext) + val mockConnectivityManager = Mockito.mock(ConnectivityManager::class.java) + Mockito.`when`(mockContext.getSystemService(Mockito.anyString())).thenReturn(mockConnectivityManager) + Mockito.`when`(mockConnectivityManager.getDefaultProxy()).thenReturn(ProxyInfo.buildDirectProxy("127.0.0.1", port)) + + val onEngineRunningLatch = CountDownLatch(1) + val onProxyEngineRunningLatch = CountDownLatch(1) + val onRespondeHeadersLatch = CountDownLatch(1) + + val proxyEngineBuilder = Proxy(ApplicationProvider.getApplicationContext(), port).https() + val proxyEngine = proxyEngineBuilder + .addLogLevel(LogLevel.DEBUG) + .setOnEngineRunning { onProxyEngineRunningLatch.countDown() } + .build() + + onProxyEngineRunningLatch.await(10, TimeUnit.SECONDS) + assertThat(onProxyEngineRunningLatch.count).isEqualTo(0) + + val builder = AndroidEngineBuilder(mockContext) + val engine = builder + .addLogLevel(LogLevel.DEBUG) + .enableProxying(true) + .setOnEngineRunning { onEngineRunningLatch.countDown() } + .build() + + onEngineRunningLatch.await(10, TimeUnit.SECONDS) + assertThat(onEngineRunningLatch.count).isEqualTo(0) + + val requestHeaders = RequestHeadersBuilder( + method = RequestMethod.GET, + scheme = "https", + authority = "api.lyft.com", + path = "/ping" + ) + .build() + + engine + .streamClient() + .newStreamPrototype() + .setOnResponseHeaders { responseHeaders, _, _ -> + val status = responseHeaders.httpStatus ?: 0L + assertThat(status).isEqualTo(200) + assertThat(responseHeaders.value("x-response-header-that-should-be-stripped")).isNull() + onRespondeHeadersLatch.countDown() + } + .start(Executors.newSingleThreadExecutor()) + .sendHeaders(requestHeaders, true) + + onRespondeHeadersLatch.await(15, TimeUnit.SECONDS) + assertThat(onRespondeHeadersLatch.count).isEqualTo(0) + + engine.terminate() + proxyEngine.terminate() + } +} diff --git a/mobile/test/kotlin/integration/proxying/Proxy.kt b/mobile/test/kotlin/integration/proxying/Proxy.kt new file mode 100644 index 000000000000..26f5a31edb63 --- /dev/null +++ b/mobile/test/kotlin/integration/proxying/Proxy.kt @@ -0,0 +1,192 @@ +package test.kotlin.integration.proxying + +import android.content.Context + +import io.envoyproxy.envoymobile.AndroidEngineBuilder +import io.envoyproxy.envoymobile.Custom +import io.envoyproxy.envoymobile.EngineBuilder + +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +// A convenient wrapper for creating an builder for an engine that +// proxies network requests. +class Proxy constructor(val context: Context, val port: Int) { + fun http(): EngineBuilder { + val config = """ +static_resources: + listeners: + - name: base_api_listener + address: + socket_address: { protocol: TCP, address: 127.0.0.1, port_value: 10000 } + api_listener: + api_listener: + "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.EnvoyMobileHttpConnectionManager" + config: + stat_prefix: api_hcm + route_config: + name: api_router + virtual_hosts: + - name: api + domains: ["*"] + routes: + - match: { prefix: "/" } + direct_response: { status: 400, body: { inline_string: "not found" } } + - name: listener_proxy + address: + socket_address: { address: 127.0.0.1, port_value: $port } + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: remote_hcm + route_config: + name: remote_route + virtual_hosts: + - name: remote_service + domains: ["*"] + routes: + - match: { prefix: "/" } + route: { cluster: cluster_proxy } + response_headers_to_add: + - append_action: OVERWRITE_IF_EXISTS_OR_ADD + header: + key: x-proxy-response + value: 'true' + http_filters: + - name: envoy.filters.http.local_error + typed_config: + "@type": type.googleapis.com/envoymobile.extensions.filters.http.local_error.LocalError + - name: envoy.filters.http.dynamic_forward_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.dynamic_forward_proxy.v3.FilterConfig + dns_cache_config: &dns_cache_config + name: base_dns_cache + dns_lookup_family: ALL + host_ttl: 86400s + dns_min_refresh_rate: 20s + dns_refresh_rate: 60s + dns_failure_refresh_rate: + base_interval: 2s + max_interval: 10s + dns_query_timeout: 25s + typed_dns_resolver_config: + name: envoy.network.dns_resolver.getaddrinfo + typed_config: {"@type":"type.googleapis.com/envoy.extensions.network.dns_resolver.getaddrinfo.v3.GetAddrInfoDnsResolverConfig"} + - name: envoy.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + clusters: + - name: cluster_proxy + connect_timeout: 30s + lb_policy: CLUSTER_PROVIDED + dns_lookup_family: ALL + cluster_type: + name: envoy.clusters.dynamic_forward_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.dynamic_forward_proxy.v3.ClusterConfig + dns_cache_config: *dns_cache_config +layered_runtime: + layers: + - name: static_layer_0 + static_layer: + envoy: + # This disables envoy bug stats, which are filtered out of our stats inclusion list anyway + # Global stats do not play well with engines with limited lifetimes + disallow_global_stats: true +""" + return AndroidEngineBuilder(context, Custom(config)) + } + + fun https(): EngineBuilder { + val config = """ +static_resources: + listeners: + - name: base_api_listener + address: + socket_address: { protocol: TCP, address: 127.0.0.1, port_value: 10000 } + api_listener: + api_listener: + "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.EnvoyMobileHttpConnectionManager" + config: + stat_prefix: api_hcm + route_config: + name: api_router + virtual_hosts: + - name: api + domains: ["*"] + routes: + - match: { prefix: "/" } + direct_response: { status: 400, body: { inline_string: "not found" } } + - name: listener_proxy + address: + socket_address: { address: 127.0.0.1, port_value: $port } + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: remote_hcm + route_config: + name: remote_route + virtual_hosts: + - name: remote_service + domains: ["*"] + routes: + - match: { connect_matcher: {} } + route: + cluster: cluster_proxy + upgrade_configs: + - upgrade_type: CONNECT + connect_config: + response_headers_to_add: + - append_action: OVERWRITE_IF_EXISTS_OR_ADD + header: + key: x-response-header-that-should-be-stripped + value: 'true' + http_filters: + - name: envoy.filters.http.local_error + typed_config: + "@type": type.googleapis.com/envoymobile.extensions.filters.http.local_error.LocalError + - name: envoy.filters.http.dynamic_forward_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.dynamic_forward_proxy.v3.FilterConfig + dns_cache_config: &dns_cache_config + name: base_dns_cache + dns_lookup_family: ALL + host_ttl: 86400s + dns_min_refresh_rate: 20s + dns_refresh_rate: 60s + dns_failure_refresh_rate: + base_interval: 2s + max_interval: 10s + dns_query_timeout: 25s + typed_dns_resolver_config: + name: envoy.network.dns_resolver.getaddrinfo + typed_config: {"@type":"type.googleapis.com/envoy.extensions.network.dns_resolver.getaddrinfo.v3.GetAddrInfoDnsResolverConfig"} + - name: envoy.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + clusters: + - name: cluster_proxy + connect_timeout: 30s + lb_policy: CLUSTER_PROVIDED + dns_lookup_family: ALL + cluster_type: + name: envoy.clusters.dynamic_forward_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.dynamic_forward_proxy.v3.ClusterConfig + dns_cache_config: *dns_cache_config +layered_runtime: + layers: + - name: static_layer_0 + static_layer: + envoy: + # This disables envoy bug stats, which are filtered out of our stats inclusion list anyway + # Global stats do not play well with engines with limited lifetimes + disallow_global_stats: true +""" + return AndroidEngineBuilder(context, Custom(config)) + } +}