From 7c83e0f87d7de5c84cddfa7eddfa2d9f653d02b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20L=C3=B3pez?= Date: Tue, 26 Dec 2017 21:45:08 +0100 Subject: [PATCH] implement support for HTTP proxy (#489) This change allows the user to manually enter an unauthenticated proxy configuration to be used for all API connections. This is mainly intended for using Tusky with Tor (via Orbot or a local proxy). --- .../com/keylesspalace/tusky/BaseActivity.java | 4 +- .../keylesspalace/tusky/LoginActivity.java | 2 +- .../tusky/NotificationPullJobCreator.java | 4 +- .../keylesspalace/tusky/TuskyApplication.java | 5 +- .../tusky/fragment/PreferencesFragment.java | 92 ++++++++++++++++++- .../keylesspalace/tusky/util/OkHttpUtils.java | 19 +++- app/src/main/res/values/strings.xml | 5 + .../main/res/xml/http_proxy_preferences.xml | 18 ++++ app/src/main/res/xml/preferences.xml | 8 ++ 9 files changed, 149 insertions(+), 8 deletions(-) create mode 100644 app/src/main/res/xml/http_proxy_preferences.xml diff --git a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java index 06d0529fc..1bb2c57d3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java @@ -137,8 +137,10 @@ public abstract class BaseActivity extends AppCompatActivity { .registerTypeAdapter(Spanned.class, new SpannedTypeAdapter()) .create(); + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + OkHttpClient.Builder okBuilder = - OkHttpUtils.getCompatibleClientBuilder() + OkHttpUtils.getCompatibleClientBuilder(preferences) .addInterceptor(new AuthInterceptor(this)) .dispatcher(mastodonApiDispatcher); diff --git a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java index 0c8c0a52b..4449fc469 100644 --- a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java @@ -152,7 +152,7 @@ public class LoginActivity extends AppCompatActivity { private MastodonApi getApiFor(String domain) { Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://" + domain) - .client(OkHttpUtils.getCompatibleClient()) + .client(OkHttpUtils.getCompatibleClient(preferences)) .addConverterFactory(GsonConverterFactory.create()) .build(); diff --git a/app/src/main/java/com/keylesspalace/tusky/NotificationPullJobCreator.java b/app/src/main/java/com/keylesspalace/tusky/NotificationPullJobCreator.java index 4fe10bc8f..3b35255d4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/NotificationPullJobCreator.java +++ b/app/src/main/java/com/keylesspalace/tusky/NotificationPullJobCreator.java @@ -76,8 +76,10 @@ public final class NotificationPullJobCreator implements JobCreator { } private static MastodonApi createMastodonApi(String domain, Context context) { + SharedPreferences preferences = context.getSharedPreferences( + context.getString(R.string.preferences_file_key), Context.MODE_PRIVATE); - OkHttpClient okHttpClient = OkHttpUtils.getCompatibleClientBuilder() + OkHttpClient okHttpClient = OkHttpUtils.getCompatibleClientBuilder(preferences) .addInterceptor(new AuthInterceptor(context)) .build(); diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java index fee6dc8b2..abcaecade 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java +++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java @@ -17,6 +17,8 @@ package com.keylesspalace.tusky; import android.app.Application; import android.arch.persistence.room.Room; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; import android.support.v7.app.AppCompatDelegate; import com.evernote.android.job.JobManager; @@ -37,7 +39,8 @@ public class TuskyApplication extends Application { super.onCreate(); // Initialize Picasso configuration Picasso.Builder builder = new Picasso.Builder(this); - builder.downloader(new OkHttp3Downloader(OkHttpUtils.getCompatibleClient())); + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + builder.downloader(new OkHttp3Downloader(OkHttpUtils.getCompatibleClient(preferences))); if (BuildConfig.DEBUG) { builder.listener((picasso, uri, exception) -> exception.printStackTrace()); } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/PreferencesFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/PreferencesFragment.java index 9e96d5acd..7fab0c390 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/PreferencesFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/PreferencesFragment.java @@ -16,8 +16,10 @@ package com.keylesspalace.tusky.fragment; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; +import android.preference.EditTextPreference; import android.preference.Preference; import android.preference.PreferenceFragment; import android.support.annotation.XmlRes; @@ -27,7 +29,10 @@ import com.keylesspalace.tusky.PreferencesActivity; import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.util.NotificationManager; -public class PreferencesFragment extends PreferenceFragment { +public class PreferencesFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener { + SharedPreferences sharedPreferences; + static boolean httpProxyChanged = false; + static boolean pendingRestart = false; public static PreferencesFragment newInstance(@XmlRes int preference) { PreferencesFragment fragment = new PreferencesFragment(); @@ -101,6 +106,91 @@ public class PreferencesFragment extends PreferenceFragment { }); } + Preference httpProxyPreferences = findPreference("httpProxyPreferences"); + if(httpProxyPreferences != null) { + httpProxyPreferences.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + PreferencesActivity activity = (PreferencesActivity) getActivity(); + if (activity != null) { + pendingRestart = false; + activity.showFragment(R.xml.http_proxy_preferences, R.string.pref_title_http_proxy_settings); + } + + return true; + } + }); + } + } + @Override + public void onResume() { + super.onResume(); + + sharedPreferences = getPreferenceManager().getSharedPreferences(); + sharedPreferences.registerOnSharedPreferenceChangeListener(this); + + updateSummary("httpProxyServer"); + updateSummary("httpProxyPort"); + updateHttpProxySummary(); + } + + @Override + public void onPause() { + sharedPreferences.unregisterOnSharedPreferenceChangeListener(this); + super.onPause(); + if (pendingRestart) { + pendingRestart = false; + httpProxyChanged = false; + System.exit(0); + } + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, + String key) { + switch (key) { + case "httpProxyServer": + case "httpProxyPort": + updateSummary(key); + case "httpProxyEnabled": + httpProxyChanged = true; + break; + default: + } + } + + private void updateSummary(String key) { + switch (key) { + case "httpProxyServer": + case "httpProxyPort": + EditTextPreference editTextPreference = (EditTextPreference) findPreference(key); + if (editTextPreference != null) { + editTextPreference.setSummary(editTextPreference.getText()); + } + break; + default: + } + } + + private void updateHttpProxySummary() { + Preference httpProxyPref = findPreference("httpProxyPreferences"); + if (httpProxyPref != null) { + if (httpProxyChanged) { + pendingRestart = true; + } + + Boolean httpProxyEnabled = sharedPreferences.getBoolean("httpProxyEnabled", false); + + String httpServer = sharedPreferences.getString("httpProxyServer", ""); + int httpPort = Integer.parseInt(sharedPreferences.getString("httpProxyPort", "-1")); + + if (httpProxyEnabled && !httpServer.isEmpty() && (httpPort > 0 && httpPort < 65535)) { + httpProxyPref.setSummary(httpServer + ":" + httpPort); + } else { + httpProxyPref.setSummary(""); + } + } + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.java index 9f4fb4367..0873b2189 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.java @@ -15,7 +15,9 @@ package com.keylesspalace.tusky.util; +import android.content.SharedPreferences; import android.os.Build; +import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.util.Log; @@ -23,6 +25,8 @@ import com.keylesspalace.tusky.BuildConfig; import java.io.IOException; import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Proxy; import java.net.Socket; import java.security.KeyManagementException; import java.security.KeyStore; @@ -62,7 +66,11 @@ public class OkHttpUtils { * TLS 1.1 and 1.2 have to be manually enabled on API levels 16-20. */ @NonNull - public static OkHttpClient.Builder getCompatibleClientBuilder() { + public static OkHttpClient.Builder getCompatibleClientBuilder(SharedPreferences preferences) { + boolean httpProxyEnabled = preferences.getBoolean("httpProxyEnabled", false); + String httpServer = preferences.getString("httpProxyServer", ""); + int httpPort = Integer.parseInt(preferences.getString("httpProxyPort", "-1")); + ConnectionSpec fallback = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) .allEnabledCipherSuites() .supportsTlsExtensions(true) @@ -80,12 +88,17 @@ public class OkHttpUtils { .writeTimeout(30, TimeUnit.SECONDS) .connectionSpecs(specList); + if (httpProxyEnabled && !httpServer.isEmpty() && (httpPort > 0) && (httpPort < 65535)) { + InetSocketAddress address = InetSocketAddress.createUnresolved(httpServer, httpPort); + builder.proxy(new Proxy(Proxy.Type.HTTP, address)); + } + return enableHigherTlsOnPreLollipop(builder); } @NonNull - public static OkHttpClient getCompatibleClient() { - return getCompatibleClientBuilder().build(); + public static OkHttpClient getCompatibleClient(SharedPreferences preferences) { + return getCompatibleClientBuilder(preferences).build(); } /** diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d90719fc1..b2c20f6da 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -177,6 +177,11 @@ Show boosts Show replies Show media previews + Proxy + HTTP proxy + Enable HTTP proxy + HTTP proxy server + HTTP proxy port 15 minutes diff --git a/app/src/main/res/xml/http_proxy_preferences.xml b/app/src/main/res/xml/http_proxy_preferences.xml new file mode 100644 index 000000000..051835356 --- /dev/null +++ b/app/src/main/res/xml/http_proxy_preferences.xml @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index d94292144..d005a25c7 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -74,4 +74,12 @@ android:title="@string/pref_title_edit_notification_settings" /> + + + + +