From 90a673813dcc51e4edcbe93a454297aa6c673b6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Augustyniak?= Date: Tue, 4 Oct 2022 16:25:18 -0400 Subject: [PATCH] android proxy: add support for PAC proxies (#2591) Description: Add support for PAC proxies. Fixes https://github.com/envoyproxy/envoy-mobile/issues/2531. Risk Level: Low, the change should be additive and it's guarded with an engine builder flag (`enableProxying(true/false)` that's disabled by default. Testing: Manual testing for PAC proxies, integrations tests for other types of proxies. Docs Changes: N/A Release Notes: WIP Signed-off-by: Rafal Augustyniak --- .../engine/AndroidProxyMonitor.java | 44 ++++++++++++++++--- .../PerformHTTPRequestUsingProxyTest.kt | 17 ++++--- .../PerformHTTPSRequestBadHostname.kt | 18 ++++---- .../PerformHTTPSRequestUsingAsyncProxyTest.kt | 16 ++++--- .../PerformHTTPSRequestUsingProxyTest.kt | 18 ++++---- 5 files changed, 76 insertions(+), 37 deletions(-) diff --git a/library/java/io/envoyproxy/envoymobile/engine/AndroidProxyMonitor.java b/library/java/io/envoyproxy/envoymobile/engine/AndroidProxyMonitor.java index d912b4e5be..563194dd37 100644 --- a/library/java/io/envoyproxy/envoymobile/engine/AndroidProxyMonitor.java +++ b/library/java/io/envoyproxy/envoymobile/engine/AndroidProxyMonitor.java @@ -8,15 +8,17 @@ import android.net.ConnectivityManager; import android.net.Proxy; import android.net.ProxyInfo; +import android.net.Uri; import android.os.Build; +import android.os.Bundle; @TargetApi(Build.VERSION_CODES.LOLLIPOP) -public class AndroidProxyMonitor extends BroadcastReceiver { - private static volatile AndroidProxyMonitor instance = null; +class AndroidProxyMonitor extends BroadcastReceiver { + static volatile AndroidProxyMonitor instance = null; private ConnectivityManager connectivityManager; private EnvoyEngine envoyEngine; - public static void load(Context context, EnvoyEngine envoyEngine) { + static void load(Context context, EnvoyEngine envoyEngine) { if (instance != null) { return; } @@ -34,7 +36,6 @@ private AndroidProxyMonitor(Context context, EnvoyEngine envoyEngine) { this.connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); registerReceiver(context); - this.handleProxyChange(); } private void registerReceiver(Context context) { @@ -45,15 +46,44 @@ private void registerReceiver(Context context) { @Override public void onReceive(Context context, Intent intent) { - handleProxyChange(); + handleProxyChange(intent); } - private void handleProxyChange() { - ProxyInfo info = connectivityManager.getDefaultProxy(); + private void handleProxyChange(final Intent intent) { + ProxyInfo info = this.extractProxyInfo(intent); + if (info == null) { envoyEngine.setProxySettings("", 0); } else { envoyEngine.setProxySettings(info.getHost(), info.getPort()); } } + + private ProxyInfo extractProxyInfo(final Intent intent) { + ProxyInfo info = connectivityManager.getDefaultProxy(); + if (info == null) { + return null; + } + + // If a proxy is configured using the PAC file use + // Android's injected localhost HTTP proxy. + // + // Android's injected localhost proxy can be accessed using a proxy host + // equal to `localhost` and a proxy port retrieved from intent's 'extras'. + // We cannot take a proxy port from the ProxyInfo object that's exposed by + // the connectivity manager as it's always equal to -1 for cases when PAC + // proxy is configured. + // + // See https://github.com/envoyproxy/envoy-mobile/issues/2531 for more details. + if (info.getPacFileUrl() != null && info.getPacFileUrl() != Uri.EMPTY) { + Bundle extras = intent.getExtras(); + if (extras == null) { + return null; + } + + info = (ProxyInfo)extras.get("android.intent.extra.PROXY_INFO"); + } + + return info; + } } diff --git a/test/kotlin/integration/proxying/PerformHTTPRequestUsingProxyTest.kt b/test/kotlin/integration/proxying/PerformHTTPRequestUsingProxyTest.kt index e9fbe38ba6..b651c34f74 100644 --- a/test/kotlin/integration/proxying/PerformHTTPRequestUsingProxyTest.kt +++ b/test/kotlin/integration/proxying/PerformHTTPRequestUsingProxyTest.kt @@ -1,7 +1,9 @@ package test.kotlin.integration.proxying +import android.content.Intent import android.content.Context import android.net.ConnectivityManager +import android.net.Proxy import android.net.ProxyInfo import androidx.test.core.app.ApplicationProvider @@ -47,17 +49,16 @@ class PerformHTTPRequestUsingProxy { 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 context = Mockito.spy(ApplicationProvider.getApplicationContext()) + val connectivityManager: ConnectivityManager = Mockito.mock(ConnectivityManager::class.java) + Mockito.doReturn(connectivityManager).`when`(context).getSystemService(Context.CONNECTIVITY_SERVICE) + Mockito.`when`(connectivityManager.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) + val proxyEngineBuilder = Proxy(context, port) .http() val proxyEngine = proxyEngineBuilder .addLogLevel(LogLevel.DEBUG) @@ -67,7 +68,9 @@ class PerformHTTPRequestUsingProxy { onProxyEngineRunningLatch.await(10, TimeUnit.SECONDS) assertThat(onProxyEngineRunningLatch.count).isEqualTo(0) - val builder = AndroidEngineBuilder(mockContext) + context.sendStickyBroadcast(Intent(Proxy.PROXY_CHANGE_ACTION)) + + val builder = AndroidEngineBuilder(context) val engine = builder .addLogLevel(LogLevel.DEBUG) .enableProxying(true) diff --git a/test/kotlin/integration/proxying/PerformHTTPSRequestBadHostname.kt b/test/kotlin/integration/proxying/PerformHTTPSRequestBadHostname.kt index 61aa7637d5..2b55c04350 100644 --- a/test/kotlin/integration/proxying/PerformHTTPSRequestBadHostname.kt +++ b/test/kotlin/integration/proxying/PerformHTTPSRequestBadHostname.kt @@ -1,8 +1,9 @@ package test.kotlin.integration.proxying - +import android.content.Intent import android.content.Context import android.net.ConnectivityManager +import android.net.Proxy import android.net.ProxyInfo import androidx.test.core.app.ApplicationProvider @@ -49,17 +50,16 @@ class PerformHTTPSRequestBadHostname { fun `attempts an HTTPs request through a proxy using an async DNS resolution that fails`() { 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("loopback", port)) + val context = Mockito.spy(ApplicationProvider.getApplicationContext()) + val connectivityManager: ConnectivityManager = Mockito.mock(ConnectivityManager::class.java) + Mockito.doReturn(connectivityManager).`when`(context).getSystemService(Context.CONNECTIVITY_SERVICE) + Mockito.`when`(connectivityManager.getDefaultProxy()).thenReturn(ProxyInfo.buildDirectProxy("loopback", port)) val onEngineRunningLatch = CountDownLatch(1) val onProxyEngineRunningLatch = CountDownLatch(1) val onErrorLatch = CountDownLatch(1) - val proxyEngineBuilder = Proxy(ApplicationProvider.getApplicationContext(), port).https() + val proxyEngineBuilder = Proxy(context, port).https() val proxyEngine = proxyEngineBuilder .addLogLevel(LogLevel.DEBUG) .addDNSQueryTimeoutSeconds(2) @@ -69,7 +69,9 @@ class PerformHTTPSRequestBadHostname { onProxyEngineRunningLatch.await(10, TimeUnit.SECONDS) assertThat(onProxyEngineRunningLatch.count).isEqualTo(0) - val builder = AndroidEngineBuilder(mockContext) + context.sendStickyBroadcast(Intent(Proxy.PROXY_CHANGE_ACTION)) + + val builder = AndroidEngineBuilder(context) val engine = builder .addLogLevel(LogLevel.DEBUG) .enableProxying(true) diff --git a/test/kotlin/integration/proxying/PerformHTTPSRequestUsingAsyncProxyTest.kt b/test/kotlin/integration/proxying/PerformHTTPSRequestUsingAsyncProxyTest.kt index 4544eb87c3..7d88f91511 100644 --- a/test/kotlin/integration/proxying/PerformHTTPSRequestUsingAsyncProxyTest.kt +++ b/test/kotlin/integration/proxying/PerformHTTPSRequestUsingAsyncProxyTest.kt @@ -1,8 +1,9 @@ package test.kotlin.integration.proxying - +import android.content.Intent import android.content.Context import android.net.ConnectivityManager +import android.net.Proxy import android.net.ProxyInfo import androidx.test.core.app.ApplicationProvider @@ -49,11 +50,10 @@ class PerformHTTPSRequestUsingAsyncProxyTest { fun `performs an HTTPs request through a proxy using async DNS resolution`() { 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("localhost", port)) + val context = Mockito.spy(ApplicationProvider.getApplicationContext()) + val connectivityManager: ConnectivityManager = Mockito.mock(ConnectivityManager::class.java) + Mockito.doReturn(connectivityManager).`when`(context).getSystemService(Context.CONNECTIVITY_SERVICE) + Mockito.`when`(connectivityManager.getDefaultProxy()).thenReturn(ProxyInfo.buildDirectProxy("localhost", port)) val onEngineRunningLatch = CountDownLatch(1) val onProxyEngineRunningLatch = CountDownLatch(1) @@ -68,7 +68,9 @@ class PerformHTTPSRequestUsingAsyncProxyTest { onProxyEngineRunningLatch.await(10, TimeUnit.SECONDS) assertThat(onProxyEngineRunningLatch.count).isEqualTo(0) - val builder = AndroidEngineBuilder(mockContext) + context.sendStickyBroadcast(Intent(Proxy.PROXY_CHANGE_ACTION)) + + val builder = AndroidEngineBuilder(context) val engine = builder .addLogLevel(LogLevel.DEBUG) .enableProxying(true) diff --git a/test/kotlin/integration/proxying/PerformHTTPSRequestUsingProxyTest.kt b/test/kotlin/integration/proxying/PerformHTTPSRequestUsingProxyTest.kt index af260b5dfc..4ce45b6f04 100644 --- a/test/kotlin/integration/proxying/PerformHTTPSRequestUsingProxyTest.kt +++ b/test/kotlin/integration/proxying/PerformHTTPSRequestUsingProxyTest.kt @@ -1,8 +1,9 @@ package test.kotlin.integration.proxying - +import android.content.Intent import android.content.Context import android.net.ConnectivityManager +import android.net.Proxy import android.net.ProxyInfo import androidx.test.core.app.ApplicationProvider @@ -49,17 +50,16 @@ class PerformHTTPSRequestUsingProxy { 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 context = Mockito.spy(ApplicationProvider.getApplicationContext()) + val connectivityManager: ConnectivityManager = Mockito.mock(ConnectivityManager::class.java) + Mockito.doReturn(connectivityManager).`when`(context).getSystemService(Context.CONNECTIVITY_SERVICE) + Mockito.`when`(connectivityManager.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 proxyEngineBuilder = Proxy(context, port).https() val proxyEngine = proxyEngineBuilder .addLogLevel(LogLevel.DEBUG) .setOnEngineRunning { onProxyEngineRunningLatch.countDown() } @@ -68,7 +68,9 @@ class PerformHTTPSRequestUsingProxy { onProxyEngineRunningLatch.await(10, TimeUnit.SECONDS) assertThat(onProxyEngineRunningLatch.count).isEqualTo(0) - val builder = AndroidEngineBuilder(mockContext) + context.sendStickyBroadcast(Intent(Proxy.PROXY_CHANGE_ACTION)) + + val builder = AndroidEngineBuilder(context) val engine = builder .addLogLevel(LogLevel.DEBUG) .enableProxying(true)