Fix a crash in View Source when using a proxy. https://redmine.stoutner.com/issues/725
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / helpers / ProxyHelper.java
1 /*
2  * Copyright © 2016-2020 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.Uri;
27 import android.os.Build;
28 import android.os.Parcelable;
29 import android.util.ArrayMap;
30 import android.view.View;
31
32 import androidx.preference.PreferenceManager;
33 import androidx.webkit.ProxyConfig;
34 import androidx.webkit.ProxyController;
35 import androidx.webkit.WebViewFeature;
36
37 import com.google.android.material.snackbar.Snackbar;
38
39 import com.stoutner.privacybrowser.R;
40 import com.stoutner.privacybrowser.activities.MainWebViewActivity;
41
42 import java.lang.reflect.Field;
43 import java.lang.reflect.InvocationTargetException;
44 import java.lang.reflect.Method;
45 import java.net.InetSocketAddress;
46 import java.net.Proxy;
47 import java.net.SocketAddress;
48 import java.util.concurrent.Executor;
49
50 public class ProxyHelper {
51     public static final String NONE = "None";
52     public static final String TOR = "Tor";
53     public static final String I2P = "I2P";
54     public static final String CUSTOM = "Custom";
55
56     public static void setProxy(Context context, View activityView, String proxyMode) {
57         // Initialize the proxy host and port strings.
58         String proxyHost = "0";
59         String proxyPort = "0";
60
61         // Create a proxy config builder.
62         ProxyConfig.Builder proxyConfigBuilder = new ProxyConfig.Builder();
63
64         // Run the commands that correlate to the proxy mode.
65         switch (proxyMode) {
66             case NONE:
67                 // Clear the proxy values.
68                 System.clearProperty("proxyHost");
69                 System.clearProperty("proxyHost");
70                 break;
71
72             case TOR:
73                 // Update the proxy host and port strings.  These can be removed once the minimum API >= 21.
74                 proxyHost = "localhost";
75                 proxyPort = "8118";
76
77                 // Set the proxy values.  These can be removed once the minimum API >= 21.
78                 System.setProperty("proxyHost", proxyHost);
79                 System.setProperty("proxyPort", proxyPort);
80
81                 // Add the proxy to the builder.  The proxy config builder can use a SOCKS proxy.
82                 proxyConfigBuilder.addProxyRule("socks://localhost:9050");
83
84                 // Ask Orbot to connect if its current status is not `"ON"`.
85                 if (!MainWebViewActivity.orbotStatus.equals("ON")) {
86                     // Create an intent to request Orbot to start.
87                     Intent orbotIntent = new Intent("org.torproject.android.intent.action.START");
88
89                     // Send the intent to the Orbot package.
90                     orbotIntent.setPackage("org.torproject.android");
91
92                     // Request a status response be sent back to this package.
93                     orbotIntent.putExtra("org.torproject.android.intent.extra.PACKAGE_NAME", context.getPackageName());
94
95                     // Make it so.
96                     context.sendBroadcast(orbotIntent);
97                 }
98                 break;
99
100             case I2P:
101                 // Update the proxy host and port strings.  These can be removed once the minimum API >= 21.
102                 proxyHost = "localhost";
103                 proxyPort = "4444";
104
105                 // Set the proxy values.  These can be removed once the minimum API >= 21.
106                 System.setProperty("proxyHost", proxyHost);
107                 System.setProperty("proxyPort", proxyPort);
108
109                 // Add the proxy to the builder.
110                 proxyConfigBuilder.addProxyRule("http://localhost:4444");
111                 break;
112
113             case CUSTOM:
114                 // Get a handle for the shared preferences.
115                 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
116
117                 // Get the custom proxy URL string.
118                 String customProxyUrlString = sharedPreferences.getString("proxy_custom_url", context.getString(R.string.proxy_custom_url_default_value));
119
120                 // Parse the custom proxy URL.
121                 try {
122                     // Convert the custom proxy URL string to a URI.
123                     Uri customProxyUri = Uri.parse(customProxyUrlString);
124
125                     // Get the proxy host and port strings from the shared preferences.  These can be removed once the minimum API >= 21.
126                     proxyHost = customProxyUri.getHost();
127                     proxyPort = String.valueOf(customProxyUri.getPort());
128
129                     // Set the proxy values.  These can be removed once the minimum API >= 21.
130                     System.setProperty("proxyHost", proxyHost);
131                     System.setProperty("proxyPort", proxyPort);
132
133                     // Add the proxy to the builder.
134                     proxyConfigBuilder.addProxyRule(customProxyUrlString);
135                 } catch (Exception exception){  // The custom proxy URL is invalid.
136                     // Display a Snackbar.
137                     Snackbar.make(activityView, R.string.custom_proxy_invalid, Snackbar.LENGTH_LONG).show();
138                 }
139                 break;
140         }
141
142         // Apply the proxy settings
143         if (WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) {  // The fancy new proxy config can be used because the API >= 21.
144             // Convert the proxy config builder into a proxy config.
145             ProxyConfig proxyConfig = proxyConfigBuilder.build();
146
147             // Get the proxy controller.
148             ProxyController proxyController = ProxyController.getInstance();
149
150             // Applying a proxy requires an executor.
151             Executor executor = runnable -> {
152                 // Do nothing.
153             };
154
155             // Applying a proxy requires a runnable.
156             Runnable runnable = () -> {
157                 // Do nothing.
158             };
159
160             // Apply the proxy settings.
161             if (proxyMode.equals(NONE)) {  // Remove the proxy.
162                 proxyController.clearProxyOverride(executor, runnable);
163             } else {  // Apply the proxy.
164                 try {
165                     // Apply the proxy.
166                     proxyController.setProxyOverride(proxyConfig, executor, runnable);
167                 } catch (IllegalArgumentException exception) {  // The proxy config is invalid.
168                     // Display a Snackbar.
169                     Snackbar.make(activityView, R.string.custom_proxy_invalid, Snackbar.LENGTH_LONG).show();
170                 }
171             }
172         } else {  // The old proxy method must be used, either because an old WebView is installed or because the API == 19;
173             // Get a handle for the shared preferences.
174             SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
175
176             // Check to make sure a SOCKS proxy is not selected.
177             if (proxyMode.equals(CUSTOM) && sharedPreferences.getString("proxy_custom_url", context.getString(R.string.proxy_custom_url_default_value)).startsWith("socks://")) {
178                 // Display a Snackbar.
179                 Snackbar.make(activityView, R.string.socks_proxies_do_not_work_on_kitkat, Snackbar.LENGTH_LONG).show();
180             } else {  // Use reflection to apply the new proxy values.
181                 try {
182                     // Get the application and APK classes.  Suppress the lint warning that reflection may not always work in the future and on all devices.
183                     Class applicationClass = Class.forName("android.app.Application");
184                     @SuppressLint("PrivateApi") Class loadedApkClass = Class.forName("android.app.LoadedApk");
185
186                     // Get the declared fields.  Suppress the lint warning that `mLoadedApk` cannot be resolved.
187                     @SuppressWarnings("JavaReflectionMemberAccess") Field methodLoadedApkField = applicationClass.getDeclaredField("mLoadedApk");
188                     Field methodReceiversField = loadedApkClass.getDeclaredField("mReceivers");
189
190                     // Allow the values to be changed.
191                     methodLoadedApkField.setAccessible(true);
192                     methodReceiversField.setAccessible(true);
193
194                     // Get the APK object.
195                     Object methodLoadedApkObject = methodLoadedApkField.get(context);
196
197                     // Get an array map of the receivers.
198                     ArrayMap receivers = (ArrayMap) methodReceiversField.get(methodLoadedApkObject);
199
200                     // Set the proxy if the receivers has at least one entry.
201                     if (receivers != null) {
202                         for (Object receiverMap : receivers.values()) {
203                             for (Object receiver : ((ArrayMap) receiverMap).keySet()) {
204                                 // Get the receiver class.
205                                 // `Class<?>`, which is an `unbounded wildcard parameterized type`, must be used instead of `Class`, which is a `raw type`.  Otherwise, `receiveClass.getDeclaredMethod()` is unhappy.
206                                 Class<?> receiverClass = receiver.getClass();
207
208                                 // Apply the new proxy settings to any classes whose names contain `ProxyChangeListener`.
209                                 if (receiverClass.getName().contains("ProxyChangeListener")) {
210                                     // Get the `onReceive` method from the class.
211                                     Method onReceiveMethod = receiverClass.getDeclaredMethod("onReceive", Context.class, Intent.class);
212
213                                     // Create a proxy change intent.
214                                     Intent proxyChangeIntent = new Intent(android.net.Proxy.PROXY_CHANGE_ACTION);
215
216                                     if (Build.VERSION.SDK_INT >= 21) {
217                                         // Get a proxy info class.
218                                         // `Class<?>`, which is an `unbounded wildcard parameterized type`, must be used instead of `Class`, which is a `raw type`.  Otherwise, `proxyInfoClass.getMethod()` is unhappy.
219                                         Class<?> proxyInfoClass = Class.forName("android.net.ProxyInfo");
220
221                                         // Get the build direct proxy method from the proxy info class.
222                                         Method buildDirectProxyMethod = proxyInfoClass.getMethod("buildDirectProxy", String.class, Integer.TYPE);
223
224                                         // Populate a proxy info object with the new proxy information.
225                                         Object proxyInfoObject = buildDirectProxyMethod.invoke(proxyInfoClass, proxyHost, Integer.valueOf(proxyPort));
226
227                                         // Add the proxy info object into the proxy change intent.
228                                         proxyChangeIntent.putExtra("proxy", (Parcelable) proxyInfoObject);
229                                     }
230
231                                     // Pass the proxy change intent to the `onReceive` method of the receiver class.
232                                     onReceiveMethod.invoke(receiver, context, proxyChangeIntent);
233                                 }
234                             }
235                         }
236                     }
237                 } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException | NoSuchMethodException | InvocationTargetException exception) {
238                     // Do nothing.
239                 }
240             }
241         }
242     }
243
244     public Proxy getCurrentProxy(Context context) {
245         // Define a proxy variable.
246         Proxy proxy;
247
248         // Get the proxy according to the current proxy mode.
249         switch (MainWebViewActivity.proxyMode) {
250             case (ProxyHelper.TOR):
251                 if (Build.VERSION.SDK_INT >= 21) {
252                     // Use localhost port 9050 as the socket address.
253                     SocketAddress torSocketAddress = InetSocketAddress.createUnresolved("localhost", 9050);
254
255                     // Create a SOCKS proxy.
256                     proxy = new Proxy(Proxy.Type.SOCKS, torSocketAddress);
257                 } else {
258                     // Use localhost port 8118 as the socket address.
259                     SocketAddress oldTorSocketAddress = InetSocketAddress.createUnresolved("localhost", 8118);
260
261                     // Create an HTTP proxy.
262                     proxy = new Proxy(Proxy.Type.HTTP, oldTorSocketAddress);
263                 }
264                 break;
265
266             case (ProxyHelper.I2P):
267                 // Use localhost port 4444 as the socket address.
268                 SocketAddress i2pSocketAddress = InetSocketAddress.createUnresolved("localhost", 4444);
269
270                 // Create an HTTP proxy.
271                 proxy = new Proxy(Proxy.Type.HTTP, i2pSocketAddress);
272                 break;
273
274             case (ProxyHelper.CUSTOM):
275                 // Get the shared preferences.
276                 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
277
278                 // Get the custom proxy URL string.
279                 String customProxyUrlString = sharedPreferences.getString("proxy_custom_url", context.getString(R.string.proxy_custom_url_default_value));
280
281                 // Parse the custom proxy URL.
282                 try {
283                     // Convert the custom proxy URL string to a URI.
284                     Uri customProxyUri = Uri.parse(customProxyUrlString);
285
286                     // Get the custom socket address.
287                     SocketAddress customSocketAddress = InetSocketAddress.createUnresolved(customProxyUri.getHost(), customProxyUri.getPort());
288
289                     // Get the custom proxy scheme.
290                     String customProxyScheme = customProxyUri.getScheme();
291
292                     // Create a proxy according to the scheme.
293                     if ((customProxyScheme != null) && customProxyScheme.startsWith("socks")) {  // A SOCKS proxy is specified.
294                         // Create a SOCKS proxy.
295                         proxy = new Proxy(Proxy.Type.SOCKS, customSocketAddress);
296                     } else {  // A SOCKS proxy is not specified.
297                         // Create an HTTP proxy.
298                         proxy = new Proxy(Proxy.Type.HTTP, customSocketAddress);
299                     }
300                 } catch (Exception exception) {  // The custom proxy cannot be parsed.
301                     // Disable the proxy.
302                     proxy = Proxy.NO_PROXY;
303                 }
304                 break;
305
306             default:  // No proxy is in use.
307                 // Create a direct proxy.
308                 proxy = Proxy.NO_PROXY;
309                 break;
310         }
311
312         // Return the proxy.
313         return proxy;
314     }
315 }