Add support for I2P and custom proxies. https://redmine.stoutner.com/issues/355
[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.net.Proxy;
26 import android.os.Build;
27 import android.os.Parcelable;
28 import android.util.ArrayMap;
29 import android.util.Log;
30
31 import com.stoutner.privacybrowser.activities.MainWebViewActivity;
32
33 import java.lang.reflect.Field;
34 import java.lang.reflect.InvocationTargetException;
35 import java.lang.reflect.Method;
36
37 public class ProxyHelper {
38     public static final String NONE = "None";
39     public static final String TOR = "Tor";
40     public static final String I2P = "I2P";
41     public static final String CUSTOM = "Custom";
42
43     public static void setProxy(Context context, String mode) {
44         // Initialize the proxy host and port strings.
45         String proxyHost = "0";
46         String proxyPort = "0";
47
48         // Run the commands that correlate to the proxy mode.
49         switch (mode) {
50             case NONE:
51                 // Clear the proxy values.
52                 System.clearProperty("proxyHost");
53                 System.clearProperty("proxyHost");
54                 break;
55
56             case TOR:
57                 // Update the proxy host and port strings.
58                 proxyHost = "localhost";
59                 proxyPort = "8118";
60
61                 // Set the proxy values
62                 System.setProperty("proxyHost", proxyHost);
63                 System.setProperty("proxyPort", proxyPort);
64
65                 // Ask Orbot to connect if its current status is not `"ON"`.
66                 if (!MainWebViewActivity.orbotStatus.equals("ON")) {
67                     // Request Orbot to start.
68                     Intent orbotIntent = new Intent("org.torproject.android.intent.action.START");
69
70                     // Send the intent to the Orbot package.
71                     orbotIntent.setPackage("org.torproject.android");
72
73                     // Request a status response be sent back to this package.
74                     orbotIntent.putExtra("org.torproject.android.intent.extra.PACKAGE_NAME", context.getPackageName());
75
76                     // Make it so.
77                     context.sendBroadcast(orbotIntent);
78                 }
79                 break;
80
81             case I2P:
82                 // Update the proxy host and port strings.
83                 proxyHost = "localhost";
84                 proxyPort = "4444";
85
86                 // Set the proxy values
87                 System.setProperty("proxyHost", proxyHost);
88                 System.setProperty("proxyPort", proxyPort);
89                 break;
90
91             case CUSTOM:
92                 // Update the proxy host and port strings.
93                 proxyHost = "0";
94                 proxyPort = "0";
95
96                 // Set the proxy values
97                 System.setProperty("proxyHost", proxyHost);
98                 System.setProperty("proxyPort", proxyPort);
99                 break;
100         }
101
102         // Use reflection to apply the new proxy values.
103         try {
104             // Get the application and APK classes.  Suppress the lint warning that reflection may not always work in the future and on all devices.
105             Class applicationClass = Class.forName("android.app.Application");
106             @SuppressLint("PrivateApi") Class loadedApkClass = Class.forName("android.app.LoadedApk");
107
108             // Get the declared fields.  Suppress the lint warning that `mLoadedApk` cannot be resolved.
109             @SuppressWarnings("JavaReflectionMemberAccess") Field methodLoadedApkField = applicationClass.getDeclaredField("mLoadedApk");
110             Field methodReceiversField = loadedApkClass.getDeclaredField("mReceivers");
111
112             // Allow the values to be changed.
113             methodLoadedApkField.setAccessible(true);
114             methodReceiversField.setAccessible(true);
115
116             // Get the APK object.
117             Object methodLoadedApkObject = methodLoadedApkField.get(context);
118
119             // Get an array map of the receivers.
120             ArrayMap receivers = (ArrayMap) methodReceiversField.get(methodLoadedApkObject);
121
122             // Set the proxy if the receivers has at least one entry.
123             if (receivers != null) {
124                 for (Object receiverMap : receivers.values()) {
125                     for (Object receiver : ((ArrayMap) receiverMap).keySet()) {
126                         // Get the receiver class.
127                         // `Class<?>`, which is an `unbounded wildcard parameterized type`, must be used instead of `Class`, which is a `raw type`.  Otherwise, `receiveClass.getDeclaredMethod()` is unhappy.
128                         Class<?> receiverClass = receiver.getClass();
129
130                         // Apply the new proxy settings to any classes whose names contain `ProxyChangeListener`.
131                         if (receiverClass.getName().contains("ProxyChangeListener")) {
132                             // Get the `onReceive` method from the class.
133                             Method onReceiveMethod = receiverClass.getDeclaredMethod("onReceive", Context.class, Intent.class);
134
135                             // Create a proxy change intent.
136                             Intent proxyChangeIntent = new Intent(Proxy.PROXY_CHANGE_ACTION);
137
138                             if (Build.VERSION.SDK_INT >= 21) {
139                                 // Get a proxy info class.
140                                 // `Class<?>`, which is an `unbounded wildcard parameterized type`, must be used instead of `Class`, which is a `raw type`.  Otherwise, `proxyInfoClass.getMethod()` is unhappy.
141                                 Class<?> proxyInfoClass = Class.forName("android.net.ProxyInfo");
142
143                                 // Get the build direct proxy method from the proxy info class.
144                                 Method buildDirectProxyMethod = proxyInfoClass.getMethod("buildDirectProxy", String.class, Integer.TYPE);
145
146                                 // Populate a proxy info object with the new proxy information.
147                                 Object proxyInfoObject = buildDirectProxyMethod.invoke(proxyInfoClass, proxyHost, Integer.valueOf(proxyPort));
148
149                                 // Add the proxy info object into the proxy change intent.
150                                 proxyChangeIntent.putExtra("proxy", (Parcelable) proxyInfoObject);
151                             }
152
153                             // Pass the proxy change intent to the `onReceive` method of the receiver class.
154                             onReceiveMethod.invoke(receiver, context, proxyChangeIntent);
155                         }
156                     }
157                 }
158             }
159         } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException | NoSuchMethodException | InvocationTargetException exception) {
160             Log.d("enableProxyThroughOrbot", "Exception: " + exception);
161         }
162     }
163 }