From ef361e44be7347d8761f0169dd5d50a4764cd2b6 Mon Sep 17 00:00:00 2001 From: Norbel AMBANUMBEN Date: Wed, 18 Oct 2023 08:25:02 +0100 Subject: [PATCH] Feat: Add support for `http` and `https` proxies (#623) Fixes https://github.com/ooni/probe/issues/2561 ## Proposed Changes - Update support for socks proxy to use dropdown for selecting a scheme. - Update code to save and retrieve the various proxies. ![Screenshot_20231013_171055](https://github.com/ooni/probe-android/assets/17911892/2bad783f-6570-478d-9754-f07a17184664) --- .../ooniprobe/activity/ProxyActivity.java | 117 ++++++------------ .../ooniprobe/common/ProxyProtocol.java | 4 +- .../ooniprobe/common/ProxySettings.java | 18 +-- app/src/main/res/layout/activity_proxy.xml | 36 +++--- app/src/main/res/values/untraslatable.xml | 9 +- 5 files changed, 82 insertions(+), 102 deletions(-) diff --git a/app/src/main/java/org/openobservatory/ooniprobe/activity/ProxyActivity.java b/app/src/main/java/org/openobservatory/ooniprobe/activity/ProxyActivity.java index 46ced49d6..902fec4c8 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/activity/ProxyActivity.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/activity/ProxyActivity.java @@ -4,29 +4,23 @@ import android.util.Log; import android.view.KeyEvent; import android.view.MenuItem; -import android.widget.RadioButton; -import android.widget.RadioGroup; -import android.widget.TextView; - import androidx.annotation.NonNull; - import com.google.android.material.textfield.TextInputLayout; import com.google.common.net.InetAddresses; import com.google.common.net.InternetDomainName; - import org.openobservatory.ooniprobe.R; import org.openobservatory.ooniprobe.common.AppLogger; import org.openobservatory.ooniprobe.common.PreferenceManager; import org.openobservatory.ooniprobe.common.ProxyProtocol; import org.openobservatory.ooniprobe.common.ProxySettings; +import org.openobservatory.ooniprobe.databinding.ActivityProxyBinding; +import ru.noties.markwon.Markwon; +import javax.inject.Inject; import java.net.URISyntaxException; +import java.util.Arrays; import java.util.Objects; -import javax.inject.Inject; - -import ru.noties.markwon.Markwon; - /** * The ProxyActivity is part of the Settings. It allows users to * configure the proxy for speaking with OONI's backends. @@ -103,36 +97,8 @@ public class ProxyActivity extends AbstractActivity { // The following radio group describes the top level choice // in terms of proxying: no proxy, psiphon, or custom. - // proxyRadioGroup is the top-level radio group. - private RadioGroup proxyRadioGroup; - - // proxyNoneRB is the radio button selecting the "none" proxy. - private RadioButton proxyNoneRB; - - // proxyPsiphonRB is the radio button selecting the "psiphon" proxy. - private RadioButton proxyPsiphonRB; - - // proxyCustomRB is the radio button for the "custom" proxy. - private RadioButton proxyCustomRB; - - // The following radio group allows users to choose which specific - // custom proxy they would like to use. When writing this documentation, - // only socks5 is available but we will add more options. - - // customProxyRadioGroup allows you to choose among the different - // kinds of custom proxies that are available. - private RadioGroup customProxyRadioGroup; - - // customProxySOCKS5 selects the custom SOCKS5 proxy type. - private RadioButton customProxySOCKS5; - - // The following settings allow users to configure the custom proxy. - - // customProxyHostname is the hostname for the custom proxy. - private TextInputLayout customProxyHostname; + ActivityProxyBinding binding; - // customProxyPort is the port for the custom proxy. - private TextInputLayout customProxyPort; // settings contains a representation of the proxy settings // loaded from the preference manager. @@ -147,21 +113,13 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getActivityComponent().inject(this); + binding = ActivityProxyBinding.inflate(getLayoutInflater()); // We draw the view and store references to objects needed // when configuring the initial view or modifying it. - setContentView(R.layout.activity_proxy); - proxyRadioGroup = findViewById(R.id.proxyRadioGroup); - proxyNoneRB = findViewById(R.id.proxyNone); - proxyPsiphonRB = findViewById(R.id.proxyPsiphon); - proxyCustomRB = findViewById(R.id.proxyCustom); - customProxyRadioGroup = findViewById(R.id.customProxyRadioGroup); - customProxySOCKS5 = findViewById(R.id.customProxySOCKS5); - customProxyHostname = findViewById(R.id.customProxyHostname); - customProxyPort = findViewById(R.id.customProxyPort); + setContentView(binding.getRoot()); // We fill the footer that helps users to understand this settings screen. - TextView proxyFooter = findViewById(R.id.proxyFooter); - Markwon.setMarkdown(proxyFooter, getString(R.string.Settings_Proxy_Footer)); + Markwon.setMarkdown(binding.proxyFooter, getString(R.string.Settings_Proxy_Footer)); // We read settings and configure the initial view. loadSettingsAndConfigureInitialView(); @@ -193,11 +151,12 @@ private void configureInitialViewWithSettings(ProxySettings settings) { // Inspect the scheme and use the scheme to choose among the // top-level radio buttons describing the proxy type. if (settings.protocol == ProxyProtocol.NONE) { - proxyNoneRB.setChecked(true); + binding.proxyNone.setChecked(true); } else if (settings.protocol == ProxyProtocol.PSIPHON) { - proxyPsiphonRB.setChecked(true); - } else if (settings.protocol == ProxyProtocol.SOCKS5) { - proxyCustomRB.setChecked(true); + binding.proxyPsiphon.setChecked(true); + } else if (Arrays.asList(getResources().getStringArray(R.array.proxy_protocol_list)).contains(settings.protocol.getProtocol())) { + binding.customProxyProtocol.setText(settings.protocol.getProtocol(),false); + binding.proxyCustom.setChecked(true); } else { // TODO(bassosimone): this should also be reported as a bug. Log.w(TAG, "got an unhandled proxy scheme"); @@ -208,28 +167,28 @@ private void configureInitialViewWithSettings(ProxySettings settings) { // If the scheme is custom, then we need to enable the // part of the view related to custom proxies. customProxySetEnabled(isSchemeCustom(settings.protocol)); - customProxySOCKS5.setChecked(isSchemeCustom(settings.protocol)); // Populate all the editable fields _anyway_ so the user // has the feeling that everything was just as before + Log.d(TAG, "(from preferences) protocol: " + settings.protocol); + logger.i(TAG, "(from preferences) protocol: " + settings.protocol); Log.d(TAG, "(from preferences) hostname: " + settings.hostname); logger.i(TAG, "(from preferences) hostname: " + settings.hostname); Log.d(TAG, "(from preferences) port: " + settings.port); logger.i(TAG, "(from preferences) port: " + settings.port); - Objects.requireNonNull(customProxyHostname.getEditText()).setText(settings.hostname); - Objects.requireNonNull(customProxyPort.getEditText()).setText(settings.port); + Objects.requireNonNull(binding.customProxyHostname.getEditText()).setText(settings.hostname); + Objects.requireNonNull(binding.customProxyPort.getEditText()).setText(settings.port); // Now we need to make the top level proxy radio group interactive: when // we change what is selected, we need the view to adapt. - proxyRadioGroup.setOnCheckedChangeListener((group, checkedId) -> { + binding.proxyRadioGroup.setOnCheckedChangeListener((group, checkedId) -> { if (checkedId == R.id.proxyNone) { customProxySetEnabled(false); } else if (checkedId == R.id.proxyPsiphon) { customProxySetEnabled(false); } else if (checkedId == R.id.proxyCustom) { customProxySetEnabled(true); - customProxyRadioGroup.clearCheck(); - customProxySOCKS5.setChecked(true); + binding.customProxyRadioGroup.clearCheck(); } else { // TODO(bassosimone): this should also be reported as a bug. Log.w(TAG, "unexpected state in setOnCheckedChangeListener"); @@ -238,14 +197,14 @@ private void configureInitialViewWithSettings(ProxySettings settings) { }); // When we change the focus of text fields, clear any lingering error text. - Objects.requireNonNull(customProxyHostname.getEditText()).setOnFocusChangeListener((v, hasFocus) -> { + Objects.requireNonNull(binding.customProxyHostname.getEditText()).setOnFocusChangeListener((v, hasFocus) -> { if (!hasFocus) { - customProxyHostname.setError(null); + binding.customProxyHostname.setError(null); } }); - Objects.requireNonNull(customProxyPort.getEditText()).setOnFocusChangeListener((v, hasFocus) -> { + Objects.requireNonNull(binding.customProxyPort.getEditText()).setOnFocusChangeListener((v, hasFocus) -> { if (!hasFocus) { - customProxyHostname.setError(null); + binding.customProxyHostname.setError(null); } }); } @@ -254,7 +213,7 @@ private void configureInitialViewWithSettings(ProxySettings settings) { private boolean isSchemeCustom(ProxyProtocol protocol) { // This is where we need to extend the implementation of we add a new scheme // that will not be related to a custom proxy type. - return protocol == ProxyProtocol.SOCKS5; + return protocol == ProxyProtocol.SOCKS5 || protocol == ProxyProtocol.HTTP || protocol == ProxyProtocol.HTTPS; } // customProxyTextInputSetEnabled is a helper function that changes the @@ -268,9 +227,8 @@ private void customProxyTextInputSetEnabled(@NonNull TextInputLayout input, bool // customProxySetEnabled reacts to the enabling or disabling of the custom // proxy group and changes the view accordingly to that. private void customProxySetEnabled(boolean flag) { - customProxySOCKS5.setEnabled(flag); - customProxyTextInputSetEnabled(customProxyHostname, flag); - customProxyTextInputSetEnabled(customProxyPort, flag); + customProxyTextInputSetEnabled(binding.customProxyHostname, flag); + customProxyTextInputSetEnabled(binding.customProxyPort, flag); } // isValidHostnameOrIP validates its input as an IP address or hostname. @@ -341,14 +299,14 @@ public void onBackPressed() { logger.i(TAG, "onBackPressed: about to save proxy settings"); // Get the hostname and port for the custom proxy. - String hostname = Objects.requireNonNull(customProxyHostname.getEditText()).getText().toString(); - String port = Objects.requireNonNull(customProxyPort.getEditText()).getText().toString(); + String hostname = Objects.requireNonNull(binding.customProxyHostname.getEditText()).getText().toString(); + String port = Objects.requireNonNull(binding.customProxyPort.getEditText()).getText().toString(); settings.hostname = hostname; settings.port = port; // If no proxy is selected then just write an empty proxy // configuration into the settings and move on. - if (proxyNoneRB.isChecked()) { + if (binding.proxyNone.isChecked()) { settings.protocol = ProxyProtocol.NONE; saveSettings(); super.onBackPressed(); @@ -357,7 +315,7 @@ public void onBackPressed() { // If the psiphon proxy is checked then write back the right // proxy configuration for psiphon and move on. - if (proxyPsiphonRB.isChecked()) { + if (binding.proxyPsiphon.isChecked()) { settings.protocol = ProxyProtocol.PSIPHON; saveSettings(); super.onBackPressed(); @@ -366,26 +324,27 @@ public void onBackPressed() { // validate the hostname for the custom proxy. if (!isValidHostnameOrIP(hostname)) { - customProxyHostname.setError("not a valid hostname or IP"); + binding.customProxyHostname.setError("not a valid hostname or IP"); return; } // validate the port for the custom proxy. if (!isValidPort(port)) { - customProxyPort.setError("not a valid network port"); + binding.customProxyPort.setError("not a valid network port"); return; } - // At this point we're going to assume that this is a socks5 proxy. We will - // need to change the code in here when we add support for http proxies. - settings.protocol = ProxyProtocol.SOCKS5; + // At this point we're going to assume that this is a socks5,http,https proxy. + // ProxyProtocol.valueOf will only accept one of the values in ProxyProtocol + // as in the enum definition(uppercase). try { + settings.protocol = ProxyProtocol.valueOf(binding.customProxyProtocol.getText().toString().toUpperCase()); settings.getProxyString(); } catch (URISyntaxException e) { // okay, then, notwithstanding our efforts it still seems that we // have not obtained a valid URL, so let's not proceed. - customProxyHostname.setError("cannot construct a valid URL"); - customProxyPort.setError("cannot construct a valid URL"); + binding.customProxyHostname.setError("cannot construct a valid URL"); + binding.customProxyPort.setError("cannot construct a valid URL"); return; } diff --git a/app/src/main/java/org/openobservatory/ooniprobe/common/ProxyProtocol.java b/app/src/main/java/org/openobservatory/ooniprobe/common/ProxyProtocol.java index ed5742a2e..6bd26432e 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/common/ProxyProtocol.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/common/ProxyProtocol.java @@ -4,7 +4,9 @@ public enum ProxyProtocol { NONE("none"), PSIPHON("psiphon"), - SOCKS5("socks5"); + SOCKS5("socks5"), + HTTP("http"), + HTTPS("https"); private String protocol; diff --git a/app/src/main/java/org/openobservatory/ooniprobe/common/ProxySettings.java b/app/src/main/java/org/openobservatory/ooniprobe/common/ProxySettings.java index e19867f94..65088ccc1 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/common/ProxySettings.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/common/ProxySettings.java @@ -33,8 +33,10 @@ public static ProxySettings newProxySettings(PreferenceManager pm) throws Invali settings.protocol = ProxyProtocol.NONE; } else if (protocol.equals(ProxyProtocol.PSIPHON.getProtocol())) { settings.protocol = ProxyProtocol.PSIPHON; - } else if (protocol.equals(ProxyProtocol.SOCKS5.getProtocol())) { - settings.protocol = ProxyProtocol.SOCKS5; + } else if (protocol.equals(ProxyProtocol.SOCKS5.getProtocol()) || protocol.equals(ProxyProtocol.HTTP.getProtocol()) || protocol.equals(ProxyProtocol.HTTPS.getProtocol())) { + // ProxyProtocol.valueOf will only accept one of the values in ProxyProtocol + // as in the enum definition(uppercase). + settings.protocol = ProxyProtocol.valueOf(protocol.toUpperCase()); } else { // This is where we will extend the code to add support for // more proxies, e.g., HTTP proxies. @@ -72,16 +74,18 @@ private boolean isIPv6(String hostname) { /** getProxyString returns to you the proxy string you should pass to oonimkall. */ public String getProxyString() throws URISyntaxException { - if (protocol == ProxyProtocol.NONE) + if (protocol == ProxyProtocol.NONE) { return ""; - if (protocol == ProxyProtocol.PSIPHON) + } + if (protocol == ProxyProtocol.PSIPHON) { return "psiphon://"; - if (protocol == ProxyProtocol.SOCKS5) { + } + if (protocol == ProxyProtocol.SOCKS5||protocol == ProxyProtocol.HTTP||protocol == ProxyProtocol.HTTPS) { // Alright, we now need to construct a new SOCKS5 URL. We are going to defer // doing that to the Java standard library (er, the Android stdlib). - String urlStr = "socks5://" + hostname + ":" + port + "/"; + String urlStr = protocol.getProtocol()+"://" + hostname + ":" + port + "/"; if (isIPv6(hostname)) { - urlStr = "socks5://[" + hostname + "]:" + port + "/"; // IPv6 must be quoted in URLs + urlStr = protocol.getProtocol()+"://[" + hostname + "]:" + port + "/"; // IPv6 must be quoted in URLs } URI url = new URI(urlStr); return url.toASCIIString(); diff --git a/app/src/main/res/layout/activity_proxy.xml b/app/src/main/res/layout/activity_proxy.xml index d3c565ed4..7f49e920d 100644 --- a/app/src/main/res/layout/activity_proxy.xml +++ b/app/src/main/res/layout/activity_proxy.xml @@ -49,22 +49,30 @@ android:id="@+id/customProxyRadioGroup" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_marginStart="20dp" - android:checkedButton="@id/customProxySOCKS5"> - - + android:layout_marginStart="20dp"> - + + + + android:inputType="textUri"/> @@ -93,10 +101,10 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/Settings_Proxy_Custom_Port" + android:textSize="12sp" android:textColorHint="@color/color_gray6" android:textColor="@android:color/black" - android:inputType="number" - /> + android:inputType="number"/> diff --git a/app/src/main/res/values/untraslatable.xml b/app/src/main/res/values/untraslatable.xml index 8e52cf58e..f3490ad0d 100644 --- a/app/src/main/res/values/untraslatable.xml +++ b/app/src/main/res/values/untraslatable.xml @@ -19,7 +19,9 @@ proxy_protocol proxy_hostname proxy_port - SOCKS5 + socks5 + http + https middle_boxes performance @@ -313,6 +315,11 @@ vi my + + @string/SOCKS5 + @string/HTTP + @string/HTTPS + org.openobservatory.ooniprobe.activity.InfoActivity org.openobservatory.ooniprobe.activity.ProxyActivity org.openobservatory.ooniprobe.activity.LogActivity