Skip to content

Commit

Permalink
Feat: Add support for http and https proxies (#623)
Browse files Browse the repository at this point in the history
Fixes  ooni/probe#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)
  • Loading branch information
aanorbel authored Oct 18, 2023
1 parent 5336624 commit ef361e4
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 102 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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();
Expand Down Expand Up @@ -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");
Expand All @@ -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");
Expand All @@ -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);
}
});
}
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
public enum ProxyProtocol {
NONE("none"),
PSIPHON("psiphon"),
SOCKS5("socks5");
SOCKS5("socks5"),
HTTP("http"),
HTTPS("https");

private String protocol;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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();
Expand Down
36 changes: 22 additions & 14 deletions app/src/main/res/layout/activity_proxy.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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">

<RadioButton
android:id="@+id/customProxySOCKS5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/color_black"
android:text="@string/SOCKS5"/>
android:layout_marginStart="20dp">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:orientation="horizontal">

<com.google.android.material.textfield.TextInputLayout
android:hint="@string/proxy_protocol"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_weight="0.3"
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.ExposedDropdownMenu">
<AutoCompleteTextView
android:id="@+id/customProxyProtocol"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:textSize="12sp"
android:textAppearance="?android:attr/textAppearanceListItemSmall"
android:maxLines="1"
app:simpleItems="@array/proxy_protocol_list"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/customProxyHostname"
android:layout_width="0dp"
Expand All @@ -76,10 +84,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/Settings_Proxy_Custom_Hostname"
android:textSize="12sp"
android:textColorHint="@color/color_gray6"
android:textColor="@android:color/black"
android:inputType="textUri"
/>
android:inputType="textUri"/>

</com.google.android.material.textfield.TextInputLayout>

Expand All @@ -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"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</RadioGroup>
Expand Down
Loading

0 comments on commit ef361e4

Please sign in to comment.