Skip to content

Commit

Permalink
android proxy: add support for PAC proxies (#2591)
Browse files Browse the repository at this point in the history
Description: Add support for PAC proxies. Fixes #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 <[email protected]>
  • Loading branch information
Augustyniak authored Oct 4, 2022
1 parent 58340da commit 90a6738
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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) {
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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<Context>())
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)
Expand All @@ -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)
Expand Down
18 changes: 10 additions & 8 deletions test/kotlin/integration/proxying/PerformHTTPSRequestBadHostname.kt
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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<Context>())
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)
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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<Context>())
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)
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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<Context>())
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() }
Expand All @@ -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)
Expand Down

0 comments on commit 90a6738

Please sign in to comment.