Switch to using AndroidX's ProxyController. https://redmine.stoutner.com/issues/486
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / helpers / ProxyHelper.java
1 /*
2  * Copyright © 2016-2019 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
5  *
6  * Privacy Browser is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Privacy Browser is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Privacy Browser.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 package com.stoutner.privacybrowser.helpers;
21
22 import android.annotation.SuppressLint;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.SharedPreferences;
26 import android.net.Proxy;
27 import android.net.Uri;
28 import android.os.Build;
29 import android.os.Parcelable;
30 import android.util.ArrayMap;
31 import android.util.Log;
32 import android.view.View;
33
34 import androidx.preference.PreferenceManager;
35 import androidx.webkit.ProxyConfig;
36 import androidx.webkit.ProxyController;
37 import androidx.webkit.WebViewFeature;
38
39 import com.google.android.material.snackbar.Snackbar;
40
41 import com.stoutner.privacybrowser.R;
42 import com.stoutner.privacybrowser.activities.MainWebViewActivity;
43
44 import java.lang.reflect.Field;
45 import java.lang.reflect.InvocationTargetException;
46 import java.lang.reflect.Method;
47 import java.util.concurrent.Executor;
48
49 public class ProxyHelper {
50     public static final String NONE = "None";
51     public static final String TOR = "Tor";
52     public static final String I2P = "I2P";
53     public static final String CUSTOM = "Custom";
54
55     public static void setProxy(Context context, View activityView, String proxyMode) {
56         // Initialize the proxy host and port strings.
57         String proxyHost = "0";
58         String proxyPort = "0";
59
60         // Create a proxy config builder.
61         ProxyConfig.Builder proxyConfigBuilder = new ProxyConfig.Builder();
62
63         // Run the commands that correlate to the proxy mode.
64         switch (proxyMode) {
65             case NONE:
66                 // Clear the proxy values.
67                 System.clearProperty("proxyHost");
68                 System.clearProperty("proxyHost");
69                 break;
70
71             case TOR:
72                 // Update the proxy host and port strings.  These can be removed once the minimum API >= 21.
73                 proxyHost = "localhost";
74                 proxyPort = "8118";
75
76                 // Set the proxy values.  These can be removed once the minimum API >= 21.
77                 System.setProperty("proxyHost", proxyHost);
78                 System.setProperty("proxyPort", proxyPort);
79
80                 // Add the proxy to the builder.  The proxy config builder can use a SOCKS proxy.
81                 proxyConfigBuilder.addProxyRule("socks://localhost:9050");
82
83                 // Ask Orbot to connect if its current status is not `"ON"`.
84                 if (!MainWebViewActivity.orbotStatus.equals("ON")) {
85                     // Create an intent to request Orbot to start.
86                     Intent orbotIntent = new Intent("org.torproject.android.intent.action.START");
87
88                     // Send the intent to the Orbot package.
89                     orbotIntent.setPackage("org.torproject.android");
90
91                     // Request a status response be sent back to this package.
92                     orbotIntent.putExtra("org.torproject.android.intent.extra.PACKAGE_NAME", context.getPackageName());
93
94                     // Make it so.
95                     context.sendBroadcast(orbotIntent);
96                 }
97                 break;
98
99             case I2P:
100                 // Update the proxy host and port strings.  These can be removed once the minimum API >= 21.
101                 proxyHost = "localhost";
102                 proxyPort = "4444";
103
104                 // Set the proxy values.  These can be removed once the minimum API >= 21.
105                 System.setProperty("proxyHost", proxyHost);
106                 System.setProperty("proxyPort", proxyPort);
107
108                 // Add the proxy to the builder.
109                 proxyConfigBuilder.addProxyRule("http://localhost:4444");
110                 break;
111
112             case CUSTOM:
113                 // Get a handle for the shared preferences.
114                 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
115
116                 // Get the custom proxy URL string.
117                 String customProxyUrlString = sharedPreferences.getString("proxy_custom_url", context.getString(R.string.proxy_custom_url_default_value));
118
119                 // Parse the custom proxy URL.
120                 try {
121                     // Convert the custom proxy URL string to a URI.
122                     Uri customProxyUri = Uri.parse(customProxyUrlString);
123
124                     // Get the proxy host and port strings from the shared preferences.  These can be removed once the minimum API >= 21.
125                     proxyHost = customProxyUri.getHost();
126                     proxyPort = String.valueOf(customProxyUri.getPort());
127
128                     // Set the proxy values.  These can be removed once the minimum API >= 21.
129                     System.setProperty("proxyHost", proxyHost);
130                     System.setProperty("proxyPort", proxyPort);
131
132                     // Add the proxy to the builder.
133                     proxyConfigBuilder.addProxyRule(customProxyUrlString);
134                 } catch (Exception exception){  // The custom proxy URL is invalid.
135                     // Display a Snackbar.
136                     Snackbar.make(activityView, R.string.custom_proxy_invalid, Snackbar.LENGTH_LONG).show();
137                 }
138                 break;
139         }
140
141         // Apply the proxy settings
142         if (WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) {  // The fancy new proxy config can be used because the API >= 21.
143             // Convert the proxy config builder into a proxy config.
144             ProxyConfig proxyConfig = proxyConfigBuilder.build();
145
146             // Get the proxy controller.
147             ProxyController proxyController = ProxyController.getInstance();
148
149             // Applying a proxy requires an executor.
150             Executor executor = runnable -> {
151                 // Do nothing.
152             };
153
154             // Applying a proxy requires a runnable.
155             Runnable runnable = () -> {
156                 // Do nothing.
157             };
158
159             // Apply the proxy settings.
160             if (proxyMode.equals(NONE)) {  // Remove the proxy.
161                 proxyController.clearProxyOverride(executor, runnable);
162             } else {  // Apply the proxy.
163                 try {
164                     // Apply the proxy.
165                     proxyController.setProxyOverride(proxyConfig, executor, runnable);
166                 } catch (IllegalArgumentException exception) {  // The proxy config is invalid.
167                     // Display a Snackbar.
168                     Snackbar.make(activityView, R.string.custom_proxy_invalid, Snackbar.LENGTH_LONG).show();
169                 }
170             }
171         } else {  // The old proxy method must be used, either because an old WebView is installed or because the API == 19;
172             // Get a handle for the shared preferences.
173             SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
174
175             // Check to make sure a SOCKS proxy is not selected.
176             if (proxyMode.equals(CUSTOM) && sharedPreferences.getString("proxy_custom_url", context.getString(R.string.proxy_custom_url_default_value)).startsWith("socks://")) {
177                 // Display a Snackbar.
178                 Snackbar.make(activityView, R.string.socks_proxies_do_not_work_on_kitkat, Snackbar.LENGTH_LONG).show();
179             } else {  // Use reflection to apply the new proxy values.
180                 try {
181                     // Get the application and APK classes.  Suppress the lint warning that reflection may not always work in the future and on all devices.
182                     Class applicationClass = Class.forName("android.app.Application");
183                     @SuppressLint("PrivateApi") Class loadedApkClass = Class.forName("android.app.LoadedApk");
184
185                     // Get the declared fields.  Suppress the lint warning that `mLoadedApk` cannot be resolved.
186                     @SuppressWarnings("JavaReflectionMemberAccess") Field methodLoadedApkField = applicationClass.getDeclaredField("mLoadedApk");
187                     Field methodReceiversField = loadedApkClass.getDeclaredField("mReceivers");
188
189                     // Allow the values to be changed.
190                     methodLoadedApkField.setAccessible(true);
191                     methodReceiversField.setAccessible(true);
192
193                     // Get the APK object.
194                     Object methodLoadedApkObject = methodLoadedApkField.get(context);
195
196                     // Get an array map of the receivers.
197                     ArrayMap receivers = (ArrayMap) methodReceiversField.get(methodLoadedApkObject);
198
199                     // Set the proxy if the receivers has at least one entry.
200                     if (receivers != null) {
201                         for (Object receiverMap : receivers.values()) {
202                             for (Object receiver : ((ArrayMap) receiverMap).keySet()) {
203                                 // Get the receiver class.
204                                 // `Class<?>`, which is an `unbounded wildcard parameterized type`, must be used instead of `Class`, which is a `raw type`.  Otherwise, `receiveClass.getDeclaredMethod()` is unhappy.
205                                 Class<?> receiverClass = receiver.getClass();
206
207                                 // Apply the new proxy settings to any classes whose names contain `ProxyChangeListener`.
208                                 if (receiverClass.getName().contains("ProxyChangeListener")) {
209                                     // Get the `onReceive` method from the class.
210                                     Method onReceiveMethod = receiverClass.getDeclaredMethod("onReceive", Context.class, Intent.class);
211
212                                     // Create a proxy change intent.
213                                     Intent proxyChangeIntent = new Intent(Proxy.PROXY_CHANGE_ACTION);
214
215                                     if (Build.VERSION.SDK_INT >= 21) {
216                                         // Get a proxy info class.
217                                         // `Class<?>`, which is an `unbounded wildcard parameterized type`, must be used instead of `Class`, which is a `raw type`.  Otherwise, `proxyInfoClass.getMethod()` is unhappy.
218                                         Class<?> proxyInfoClass = Class.forName("android.net.ProxyInfo");
219
220                                         // Get the build direct proxy method from the proxy info class.
221                                         Method buildDirectProxyMethod = proxyInfoClass.getMethod("buildDirectProxy", String.class, Integer.TYPE);
222
223                                         // Populate a proxy info object with the new proxy information.
224                                         Object proxyInfoObject = buildDirectProxyMethod.invoke(proxyInfoClass, proxyHost, Integer.valueOf(proxyPort));
225
226                                         // Add the proxy info object into the proxy change intent.
227                                         proxyChangeIntent.putExtra("proxy", (Parcelable) proxyInfoObject);
228                                     }
229
230                                     // Pass the proxy change intent to the `onReceive` method of the receiver class.
231                                     onReceiveMethod.invoke(receiver, context, proxyChangeIntent);
232                                 }
233                             }
234                         }
235                     }
236                 } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException | NoSuchMethodException | InvocationTargetException exception) {
237                     Log.d("enableProxyThroughOrbot", "Exception: " + exception);
238                 }
239             }
240         }
241     }
242 }