From 25443217c20e7a30e3eb1c36ed7b4f7abdad4122 Mon Sep 17 00:00:00 2001 From: kylegoetz Date: Wed, 7 Dec 2022 12:29:18 -0600 Subject: [PATCH] 2952/proxy (#2961) * replace hard-coded strings with existing constants * proxy port * * custom proxy port and hostname inputs * typesafety, refactor, linting, unit tests * relocate ProxyConfiguration in app structure * remove unused editTextPreference fn * allow preference category to have no title * refactor proxy prefs hierarchy/dependency --- .../preference/ProxyPreferencesFragment.kt | 37 ++++++++++----- .../keylesspalace/tusky/di/NetworkModule.kt | 25 +++++++--- .../tusky/settings/ProxyConfiguration.kt | 32 +++++++++++++ .../tusky/settings/SettingsDSL.kt | 23 ++++++++-- app/src/main/res/values/strings.xml | 1 + .../tusky/entity/ProxyConfigurationTest.kt | 46 +++++++++++++++++++ 6 files changed, 143 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/settings/ProxyConfiguration.kt create mode 100644 app/src/test/java/com/keylesspalace/tusky/entity/ProxyConfigurationTest.kt diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/ProxyPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/ProxyPreferencesFragment.kt index 322b0c1d..3e11738c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/ProxyPreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/ProxyPreferencesFragment.kt @@ -19,9 +19,11 @@ import android.os.Bundle import androidx.preference.PreferenceFragmentCompat import com.keylesspalace.tusky.R import com.keylesspalace.tusky.settings.PrefKeys -import com.keylesspalace.tusky.settings.editTextPreference +import com.keylesspalace.tusky.settings.ProxyConfiguration import com.keylesspalace.tusky.settings.makePreferenceScreen +import com.keylesspalace.tusky.settings.preferenceCategory import com.keylesspalace.tusky.settings.switchPreference +import com.keylesspalace.tusky.settings.validatedEditTextPreference import kotlin.system.exitProcess class ProxyPreferencesFragment : PreferenceFragmentCompat() { @@ -36,18 +38,29 @@ class ProxyPreferencesFragment : PreferenceFragmentCompat() { setDefaultValue(false) } - editTextPreference { - setTitle(R.string.pref_title_http_proxy_server) - key = PrefKeys.HTTP_PROXY_SERVER - isIconSpaceReserved = false - setSummaryProvider { text } - } + preferenceCategory { category -> + category.dependency = PrefKeys.HTTP_PROXY_ENABLED + category.isIconSpaceReserved = false - editTextPreference { - setTitle(R.string.pref_title_http_proxy_port) - key = PrefKeys.HTTP_PROXY_PORT - isIconSpaceReserved = false - setSummaryProvider { text } + validatedEditTextPreference(null, ProxyConfiguration::isValidHostname) { + setTitle(R.string.pref_title_http_proxy_server) + key = PrefKeys.HTTP_PROXY_SERVER + isIconSpaceReserved = false + setSummaryProvider { text } + } + + val portErrorMessage = getString( + R.string.pref_title_http_proxy_port_message, + ProxyConfiguration.MIN_PROXY_PORT, + ProxyConfiguration.MAX_PROXY_PORT + ) + + validatedEditTextPreference(portErrorMessage, ProxyConfiguration::isValidProxyPort) { + setTitle(R.string.pref_title_http_proxy_port) + key = PrefKeys.HTTP_PROXY_PORT + isIconSpaceReserved = false + setSummaryProvider { text } + } } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt index 8250e61f..03b4ad39 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt @@ -18,6 +18,7 @@ package com.keylesspalace.tusky.di import android.content.Context import android.content.SharedPreferences import android.os.Build +import android.util.Log import at.connyduck.calladapter.networkresult.NetworkResultCallAdapterFactory import com.google.gson.Gson import com.google.gson.GsonBuilder @@ -27,6 +28,10 @@ import com.keylesspalace.tusky.json.Rfc3339DateJsonAdapter import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MediaUploadApi +import com.keylesspalace.tusky.settings.PrefKeys.HTTP_PROXY_ENABLED +import com.keylesspalace.tusky.settings.PrefKeys.HTTP_PROXY_PORT +import com.keylesspalace.tusky.settings.PrefKeys.HTTP_PROXY_SERVER +import com.keylesspalace.tusky.settings.ProxyConfiguration import com.keylesspalace.tusky.util.getNonNullString import dagger.Module import dagger.Provides @@ -38,6 +43,7 @@ import retrofit2.Retrofit import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory import retrofit2.converter.gson.GsonConverterFactory import retrofit2.create +import java.net.IDN import java.net.InetSocketAddress import java.net.Proxy import java.util.Date @@ -64,9 +70,9 @@ class NetworkModule { context: Context, preferences: SharedPreferences ): OkHttpClient { - val httpProxyEnabled = preferences.getBoolean("httpProxyEnabled", false) - val httpServer = preferences.getNonNullString("httpProxyServer", "") - val httpPort = preferences.getNonNullString("httpProxyPort", "-1").toIntOrNull() ?: -1 + val httpProxyEnabled = preferences.getBoolean(HTTP_PROXY_ENABLED, false) + val httpServer = preferences.getNonNullString(HTTP_PROXY_SERVER, "") + val httpPort = preferences.getNonNullString(HTTP_PROXY_PORT, "-1").toIntOrNull() ?: -1 val cacheSize = 25 * 1024 * 1024L // 25 MiB val builder = OkHttpClient.Builder() .addInterceptor { chain -> @@ -87,10 +93,13 @@ class NetworkModule { .writeTimeout(30, TimeUnit.SECONDS) .cache(Cache(context.cacheDir, cacheSize)) - if (httpProxyEnabled && httpServer.isNotEmpty() && httpPort > 0 && httpPort < 65535) { - val address = InetSocketAddress.createUnresolved(httpServer, httpPort) - builder.proxy(Proxy(Proxy.Type.HTTP, address)) + if (httpProxyEnabled) { + ProxyConfiguration.create(httpServer, httpPort)?.also { conf -> + val address = InetSocketAddress.createUnresolved(IDN.toASCII(conf.hostname), conf.port) + builder.proxy(Proxy(Proxy.Type.HTTP, address)) + } ?: Log.w(TAG, "Invalid proxy configuration: ($httpServer, $httpPort)") } + return builder .apply { addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) @@ -132,4 +141,8 @@ class NetworkModule { .build() .create() } + + companion object { + private const val TAG = "NetworkModule" + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/ProxyConfiguration.kt b/app/src/main/java/com/keylesspalace/tusky/settings/ProxyConfiguration.kt new file mode 100644 index 00000000..903a2c6a --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/settings/ProxyConfiguration.kt @@ -0,0 +1,32 @@ +package com.keylesspalace.tusky.settings + +import java.net.IDN + +class ProxyConfiguration private constructor( + val hostname: String, + val port: Int +) { + companion object { + fun create(hostname: String, port: Int): ProxyConfiguration? { + if (isValidHostname(IDN.toASCII(hostname)) && isValidProxyPort(port)) { + return ProxyConfiguration(hostname, port) + } + return null + } + fun isValidProxyPort(value: Any): Boolean = when (value) { + is String -> if (value == "") true else value.runCatching(String::toInt).map( + PROXY_RANGE::contains + ).getOrDefault(false) + is Int -> PROXY_RANGE.contains(value) + else -> false + } + fun isValidHostname(hostname: String): Boolean = + IP_ADDRESS_REGEX.matches(hostname) || HOSTNAME_REGEX.matches(hostname) + const val MIN_PROXY_PORT = 0 + const val MAX_PROXY_PORT = 65535 + } +} + +private val PROXY_RANGE = IntRange(ProxyConfiguration.MIN_PROXY_PORT, ProxyConfiguration.MAX_PROXY_PORT) +private val IP_ADDRESS_REGEX = Regex("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$") +private val HOSTNAME_REGEX = Regex("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$") diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt index 85270081..fc7a51c5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt +++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt @@ -1,8 +1,10 @@ package com.keylesspalace.tusky.settings import android.content.Context +import android.widget.Button import androidx.activity.result.ActivityResultRegistryOwner import androidx.annotation.StringRes +import androidx.core.widget.doAfterTextChanged import androidx.lifecycle.LifecycleOwner import androidx.preference.CheckBoxPreference import androidx.preference.EditTextPreference @@ -50,10 +52,25 @@ inline fun PreferenceParent.switchPreference( return pref } -inline fun PreferenceParent.editTextPreference( +inline fun PreferenceParent.validatedEditTextPreference( + errorMessage: String?, + crossinline isValid: (a: String) -> Boolean, builder: EditTextPreference.() -> Unit ): EditTextPreference { val pref = EditTextPreference(context) + pref.setOnBindEditTextListener { editText -> + editText.doAfterTextChanged { editable -> + requireNotNull(editable) + val btn = editText.rootView.findViewById