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