From 74c6e768086ac9fd344df4b52c6af81d7c67e363 Mon Sep 17 00:00:00 2001 From: andrew-signal Date: Fri, 11 Apr 2025 18:58:07 -0500 Subject: [PATCH] Add system HTTP proxy support to libsignal-net. Co-authored-by: Cody Henthorne --- ...umentationApplicationDependencyProvider.kt | 1 + .../securesms/dependencies/AppDependencies.kt | 12 ++++++++ .../messages/IncomingMessageObserver.kt | 30 ++++++++++++++----- .../messages/NetworkConnectionListener.kt | 10 ++++++- .../push/SignalServiceNetworkAccess.kt | 30 +++++++++++++++++++ .../internal/configuration/HttpProxy.kt | 6 ++++ .../SignalServiceConfiguration.kt | 1 + .../websocket/LibSignalNetworkExtensions.kt | 29 ++++++++++++------ 8 files changed, 101 insertions(+), 18 deletions(-) create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/internal/configuration/HttpProxy.kt diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/dependencies/InstrumentationApplicationDependencyProvider.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/dependencies/InstrumentationApplicationDependencyProvider.kt index 1b71cfce35..f7162da0f4 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/dependencies/InstrumentationApplicationDependencyProvider.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/dependencies/InstrumentationApplicationDependencyProvider.kt @@ -97,6 +97,7 @@ class InstrumentationApplicationDependencyProvider(val application: Application, networkInterceptors = emptyList(), dns = Optional.of(SignalServiceNetworkAccess.DNS), signalProxy = Optional.empty(), + systemHttpProxy = Optional.empty(), zkGroupServerPublicParams = Base64.decode(BuildConfig.ZKGROUP_SERVER_PUBLIC_PARAMS), genericServerPublicParams = Base64.decode(BuildConfig.GENERIC_SERVER_PUBLIC_PARAMS), backupServerPublicParams = Base64.decode(BuildConfig.BACKUP_SERVER_PUBLIC_PARAMS), diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt index a092913279..b7e7a323cb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt @@ -7,6 +7,7 @@ import okhttp3.OkHttpClient import org.signal.core.util.billing.BillingApi import org.signal.core.util.concurrent.DeadlockDetector import org.signal.core.util.concurrent.LatestValueObservable +import org.signal.core.util.orNull import org.signal.core.util.resettableLazy import org.signal.libsignal.net.Network import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations @@ -16,6 +17,7 @@ import org.thoughtcrime.securesms.components.TypingStatusSender import org.thoughtcrime.securesms.crypto.storage.SignalServiceDataStoreImpl import org.thoughtcrime.securesms.database.DatabaseObserver import org.thoughtcrime.securesms.database.PendingRetryReceiptCache +import org.thoughtcrime.securesms.dependencies.AppDependencies.authWebSocket import org.thoughtcrime.securesms.groups.GroupsV2Authorization import org.thoughtcrime.securesms.jobmanager.JobManager import org.thoughtcrime.securesms.megaphone.MegaphoneRepository @@ -378,6 +380,16 @@ object AppDependencies { networkModule.openConnections() } + fun onSystemHttpProxyChange(host: String?, port: Int?): Boolean { + val currentSystemProxy = signalServiceNetworkAccess.getConfiguration().systemHttpProxy.orNull() + return if (currentSystemProxy?.host != host || currentSystemProxy?.port != port) { + resetNetwork() + true + } else { + false + } + } + interface Provider { fun providePushServiceSocket(signalServiceConfiguration: SignalServiceConfiguration, groupsV2Operations: GroupsV2Operations): PushServiceSocket fun provideGroupsV2Operations(signalServiceConfiguration: SignalServiceConfiguration): GroupsV2Operations diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt index e50fdf45ec..6cbd704120 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt @@ -4,6 +4,7 @@ import android.app.Application import android.app.Service import android.content.Context import android.content.Intent +import android.net.ProxyInfo import android.os.IBinder import androidx.annotation.VisibleForTesting import androidx.core.app.NotificationCompat @@ -86,16 +87,29 @@ class IncomingMessageObserver(private val context: Application, private val auth private val lock: ReentrantLock = ReentrantLock() private val connectionNecessarySemaphore = Semaphore(0) - private val networkConnectionListener = NetworkConnectionListener(context) { isNetworkUnavailable -> - lock.withLock { - AppDependencies.libsignalNetwork.onNetworkChange() - if (isNetworkUnavailable()) { - Log.w(TAG, "Lost network connection. Resetting the drained state.") - decryptionDrained = false + private var previousProxyInfo: ProxyInfo? = null + private val networkConnectionListener = NetworkConnectionListener( + context, + { isNetworkUnavailable -> + lock.withLock { + AppDependencies.libsignalNetwork.onNetworkChange() + if (isNetworkUnavailable()) { + Log.w(TAG, "Lost network connection. Resetting the drained state.") + decryptionDrained = false + } + connectionNecessarySemaphore.release() } - connectionNecessarySemaphore.release() + }, + { proxyInfo -> + if (proxyInfo != previousProxyInfo) { + val networkReset = AppDependencies.onSystemHttpProxyChange(proxyInfo?.host, proxyInfo?.port) + if (networkReset) { + Log.i(TAG, "System proxy configuration changed, network reset.") + } + } + previousProxyInfo = proxyInfo } - } + ) private val messageContentProcessor = MessageContentProcessor(context) diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/NetworkConnectionListener.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/NetworkConnectionListener.kt index 5878d212d6..e5854e6221 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/NetworkConnectionListener.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/NetworkConnectionListener.kt @@ -10,7 +10,9 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.net.ConnectivityManager +import android.net.LinkProperties import android.net.Network +import android.net.ProxyInfo import android.os.Build import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint @@ -24,7 +26,7 @@ import org.thoughtcrime.securesms.util.ServiceUtil * API 28+ only runs on lost networks, so it provides a conditional that's always true because that is guaranteed by the call site. * Earlier versions use [NetworkConstraint.isMet] to query the current network state upon receiving the broadcast. */ -class NetworkConnectionListener(private val context: Context, private val onNetworkLost: (() -> Boolean) -> Unit) { +class NetworkConnectionListener(private val context: Context, private val onNetworkLost: (() -> Boolean) -> Unit, private val onProxySettingsChanged: ((ProxyInfo?) -> Unit)) { companion object { private val TAG = Log.tag(NetworkConnectionListener::class.java) } @@ -55,6 +57,12 @@ class NetworkConnectionListener(private val context: Context, private val onNetw Log.d(TAG, "ConnectivityManager.NetworkCallback onLost()") onNetworkLost { true } } + + override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) { + super.onLinkPropertiesChanged(network, linkProperties) + Log.d(TAG, "ConnectivityManager.NetworkCallback onLinkPropertiesChanged()") + onProxySettingsChanged(linkProperties.httpProxy) + } } private val connectionReceiver = object : BroadcastReceiver() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt index 6d75dc7348..f6a8849122 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt @@ -1,6 +1,9 @@ package org.thoughtcrime.securesms.push import android.content.Context +import android.net.ConnectivityManager +import android.os.Build +import androidx.core.content.ContextCompat import com.google.i18n.phonenumbers.PhoneNumberUtil import okhttp3.CipherSuite import okhttp3.ConnectionSpec @@ -20,6 +23,7 @@ import org.thoughtcrime.securesms.net.SequentialDns import org.thoughtcrime.securesms.net.StandardUserAgentInterceptor import org.thoughtcrime.securesms.net.StaticDns import org.whispersystems.signalservice.api.push.TrustStore +import org.whispersystems.signalservice.internal.configuration.HttpProxy import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration @@ -28,6 +32,7 @@ import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url import java.io.IOException import java.util.Optional +import android.net.Proxy as AndroidProxy /** * Provides a [SignalServiceConfiguration] to be used with our service layer. @@ -133,6 +138,28 @@ class SignalServiceNetworkAccess(context: Context) { .build() private val APP_CONNECTION_SPEC = ConnectionSpec.MODERN_TLS + + @Suppress("DEPRECATION") + private fun getSystemHttpProxy(context: Context): HttpProxy? { + return if (Build.VERSION.SDK_INT >= 23) { + val connectivityManager = ContextCompat.getSystemService(context, ConnectivityManager::class.java) ?: return null + + connectivityManager + .activeNetwork + ?.let { connectivityManager.getLinkProperties(it)?.httpProxy } + ?.takeIf { !it.exclusionList.contains(BuildConfig.SIGNAL_URL.stripProtocol()) } + ?.let { proxy -> HttpProxy(proxy.host, proxy.port) } + } else { + val host: String? = AndroidProxy.getHost(context) + val port: Int = AndroidProxy.getPort(context) + + if (host != null) { + HttpProxy(host, port) + } else { + null + } + } + } } private val serviceTrustStore: TrustStore = SignalServiceTrustStore(context) @@ -187,6 +214,7 @@ class SignalServiceNetworkAccess(context: Context) { networkInterceptors = interceptors, dns = Optional.of(DNS), signalProxy = Optional.empty(), + systemHttpProxy = Optional.empty(), zkGroupServerPublicParams = zkGroupServerPublicParams, genericServerPublicParams = genericServerPublicParams, backupServerPublicParams = backupServerPublicParams, @@ -246,6 +274,7 @@ class SignalServiceNetworkAccess(context: Context) { networkInterceptors = interceptors, dns = Optional.of(DNS), signalProxy = if (SignalStore.proxy.isProxyEnabled) Optional.ofNullable(SignalStore.proxy.proxy) else Optional.empty(), + systemHttpProxy = Optional.ofNullable(getSystemHttpProxy(context)), zkGroupServerPublicParams = zkGroupServerPublicParams, genericServerPublicParams = genericServerPublicParams, backupServerPublicParams = backupServerPublicParams, @@ -316,6 +345,7 @@ class SignalServiceNetworkAccess(context: Context) { networkInterceptors = interceptors, dns = Optional.of(DNS), signalProxy = Optional.empty(), + systemHttpProxy = Optional.empty(), zkGroupServerPublicParams = zkGroupServerPublicParams, genericServerPublicParams = genericServerPublicParams, backupServerPublicParams = backupServerPublicParams, diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/configuration/HttpProxy.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/configuration/HttpProxy.kt new file mode 100644 index 0000000000..da28349ad5 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/configuration/HttpProxy.kt @@ -0,0 +1,6 @@ +package org.whispersystems.signalservice.internal.configuration + +/** + * HTTP Proxy configuration from Android OS configuration. + */ +class HttpProxy(val host: String, val port: Int) diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.kt index dfad0111e8..3c4c70607e 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.kt @@ -17,6 +17,7 @@ data class SignalServiceConfiguration( val networkInterceptors: List, val dns: Optional, val signalProxy: Optional, + val systemHttpProxy: Optional, val zkGroupServerPublicParams: ByteArray, val genericServerPublicParams: ByteArray, val backupServerPublicParams: ByteArray, diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/websocket/LibSignalNetworkExtensions.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/websocket/LibSignalNetworkExtensions.kt index ae2c01731b..98bf4f7b30 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/websocket/LibSignalNetworkExtensions.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/websocket/LibSignalNetworkExtensions.kt @@ -30,16 +30,27 @@ fun Network.transformAndSetRemoteConfig(remoteConfig: Map) { * Helper method to apply settings from the SignalServiceConfiguration. */ fun Network.applyConfiguration(config: SignalServiceConfiguration) { - val proxy = config.signalProxy.orNull() + val signalProxy = config.signalProxy.orNull() + val systemHttpProxy = config.systemHttpProxy.orNull() - if (proxy == null) { - this.clearProxy() - } else { - try { - this.setProxy(proxy.host, proxy.port) - } catch (e: IOException) { - Log.e(TAG, "Invalid proxy configuration set! Failing connections until changed.") - this.setInvalidProxy() + when { + (signalProxy != null) -> { + try { + this.setProxy(signalProxy.host, signalProxy.port) + } catch (e: IOException) { + Log.e(TAG, "Invalid proxy configuration set! Failing connections until changed.") + this.setInvalidProxy() + } + } + (systemHttpProxy != null) -> { + try { + this.setProxy("http", systemHttpProxy.host, systemHttpProxy.port, "", "") + } catch (e: IOException) { + // The Android settings screen where this is set explicitly calls out that apps are allowed to + // ignore the HTTP Proxy setting, so if using the specified proxy would cause us to break, let's + // try just ignoring it and seeing if that still lets us connect. + Log.w(TAG, "Failed to set system HTTP proxy, ignoring and continuing...") + } } }