]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blobdiff - app/src/main/java/com/stoutner/privacybrowser/helpers/ProxyHelper.kt
Fix scrolling of the bottom app bar. https://redmine.stoutner.com/issues/791
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / helpers / ProxyHelper.kt
diff --git a/app/src/main/java/com/stoutner/privacybrowser/helpers/ProxyHelper.kt b/app/src/main/java/com/stoutner/privacybrowser/helpers/ProxyHelper.kt
new file mode 100644 (file)
index 0000000..0cc5105
--- /dev/null
@@ -0,0 +1,311 @@
+/*
+ * Copyright © 2016-2021 Soren Stoutner <soren@stoutner.com>.
+ *
+ * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
+ *
+ * Privacy Browser is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Privacy Browser is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Privacy Browser.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.stoutner.privacybrowser.helpers
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Parcelable
+import android.util.ArrayMap
+import android.view.View
+
+import androidx.preference.PreferenceManager
+import androidx.webkit.ProxyConfig
+import androidx.webkit.ProxyController
+import androidx.webkit.WebViewFeature
+
+import com.stoutner.privacybrowser.R
+import com.stoutner.privacybrowser.activities.MainWebViewActivity
+
+import com.google.android.material.snackbar.Snackbar
+
+import java.lang.Exception
+import java.lang.IllegalArgumentException
+import java.lang.reflect.InvocationTargetException
+import java.net.InetSocketAddress
+import java.net.Proxy
+import java.net.SocketAddress
+
+class ProxyHelper {
+    companion object {
+        // Define the public companion object constants.  These can be moved to public class constants once the entire project has migrated to Kotlin.
+        const val NONE = "None"
+        const val TOR = "Tor"
+        const val I2P = "I2P"
+        const val CUSTOM = "Custom"
+        const val ORBOT_STATUS_ON = "ON"
+    }
+
+    fun setProxy(context: Context, activityView: View, proxyMode: String) {
+        // Initialize the proxy host and port strings.
+        var proxyHost = "0"
+        var proxyPort = "0"
+
+        // Create a proxy config builder.
+        val proxyConfigBuilder = ProxyConfig.Builder()
+
+        // Run the commands that correlate to the proxy mode.
+        when (proxyMode) {
+            NONE -> {
+                // Clear the proxy values.
+                System.clearProperty("proxyHost")
+                System.clearProperty("proxyPort")
+            }
+
+            TOR -> {
+                // Update the proxy host and port strings.  These can be removed once the minimum API >= 21.
+                proxyHost = "localhost"
+                proxyPort = "8118"
+
+                // Set the proxy values.  These can be removed once the minimum API >= 21.
+                System.setProperty("proxyHost", proxyHost)
+                System.setProperty("proxyPort", proxyPort)
+
+                // Add the proxy to the builder.  The proxy config builder can use a SOCKS proxy.
+                proxyConfigBuilder.addProxyRule("socks://localhost:9050")
+
+                // Ask Orbot to connect if its current status is not `"ON"`.
+                if (MainWebViewActivity.orbotStatus != ORBOT_STATUS_ON) {
+                    // Create an intent to request Orbot to start.
+                    val orbotIntent = Intent("org.torproject.android.intent.action.START")
+
+                    // Send the intent to the Orbot package.
+                    orbotIntent.setPackage("org.torproject.android")
+
+                    // Request a status response be sent back to this package.
+                    orbotIntent.putExtra("org.torproject.android.intent.extra.PACKAGE_NAME", context.packageName)
+
+                    // Make it so.
+                    context.sendBroadcast(orbotIntent)
+                }
+            }
+
+            I2P -> {
+                // Update the proxy host and port strings.  These can be removed once the minimum API >= 21.
+                proxyHost = "localhost"
+                proxyPort = "4444"
+
+                // Set the proxy values.  These can be removed once the minimum API >= 21.
+                System.setProperty("proxyHost", proxyHost)
+                System.setProperty("proxyPort", proxyPort)
+
+                // Add the proxy to the builder.
+                proxyConfigBuilder.addProxyRule("http://localhost:4444")
+            }
+
+            CUSTOM -> {
+                // Get a handle for the shared preferences.
+                val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
+
+                // Get the custom proxy URL string.
+                val customProxyUrlString = sharedPreferences.getString(context.getString(R.string.proxy_custom_url_key), context.getString(R.string.proxy_custom_url_default_value))
+
+                // Parse the custom proxy URL.
+                try {
+                    // Convert the custom proxy URL string to a URI.
+                    val customProxyUri = Uri.parse(customProxyUrlString)
+
+                    // Get the proxy host and port strings from the shared preferences.  These can be removed once the minimum API >= 21.
+                    proxyHost = customProxyUri.host!!
+                    proxyPort = customProxyUri.port.toString()
+
+                    // Set the proxy values.  These can be removed once the minimum API >= 21.
+                    System.setProperty("proxyHost", proxyHost)
+                    System.setProperty("proxyPort", proxyPort)
+
+                    // Add the proxy to the builder.
+                    proxyConfigBuilder.addProxyRule(customProxyUrlString!!)
+                } catch (exception: Exception) {  // The custom proxy URL is invalid.
+                    // Display a Snackbar.
+                    Snackbar.make(activityView, R.string.custom_proxy_invalid, Snackbar.LENGTH_LONG).show()
+                }
+            }
+        }
+
+        // Apply the proxy settings
+        if (WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) {  // The fancy new proxy config can be used because the API >= 21.
+            // Convert the proxy config builder into a proxy config.
+            val proxyConfig = proxyConfigBuilder.build()
+
+            // Get the proxy controller.
+            val proxyController = ProxyController.getInstance()
+
+            // Apply the proxy settings.
+            if (proxyMode == NONE) {  // Remove the proxy.  A default executor and runnable are used.
+                proxyController.clearProxyOverride({}, {})
+            } else {  // Apply the proxy.
+                try {
+                    // Apply the proxy.  A default executor and runnable are used.
+                    proxyController.setProxyOverride(proxyConfig, {}, {})
+                } catch (exception: IllegalArgumentException) {  // The proxy config is invalid.
+                    // Display a Snackbar.
+                    Snackbar.make(activityView, R.string.custom_proxy_invalid, Snackbar.LENGTH_LONG).show()
+                }
+            }
+        } else {  // The old proxy method must be used, either because an old WebView is installed or because the API == 19;
+            // Get a handle for the shared preferences.
+            val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
+
+            // Check to make sure a SOCKS proxy is not selected.
+            if ((proxyMode == CUSTOM) &&
+                sharedPreferences.getString(context.getString(R.string.proxy_custom_url_key), context.getString(R.string.proxy_custom_url_default_value))!!.startsWith("socks://")) {
+                // Display a Snackbar.
+                Snackbar.make(activityView, R.string.socks_proxies_do_not_work_on_kitkat, Snackbar.LENGTH_LONG).show()
+            } else {  // Use reflection to apply the new proxy values.
+                try {
+                    // Get the application and APK classes.
+                    val applicationClass = Class.forName("android.app.Application")
+
+                    // Suppress the lint warning that reflection may not always work in the future and on all devices.
+                    @SuppressLint("PrivateApi") val loadedApkClass = Class.forName("android.app.LoadedApk")
+
+                    // Get the declared fields.  Suppress the lint that it is discouraged to access private APIs.
+                    @SuppressLint("DiscouragedPrivateApi") val methodLoadedApkField = applicationClass.getDeclaredField("mLoadedApk")
+                    @SuppressLint("DiscouragedPrivateApi") val methodReceiversField = loadedApkClass.getDeclaredField("mReceivers")
+
+                    // Allow the values to be changed.
+                    methodLoadedApkField.isAccessible = true
+                    methodReceiversField.isAccessible = true
+
+                    // Get the APK object.
+                    val methodLoadedApkObject = methodLoadedApkField[context]
+
+                    // Get an array map of the receivers.
+                    val receivers = methodReceiversField[methodLoadedApkObject] as ArrayMap<*, *>
+
+                    // Set the proxy.
+                    for (receiverMap in receivers.values) {
+                        for (receiver in (receiverMap as ArrayMap<*, *>).keys) {
+                            // Get the receiver class.
+                            // `Class<*>`, which is an `unbounded wildcard parameterized type`, must be used instead of `Class`, which is a `raw type`.  Otherwise, `receiveClass.getDeclaredMethod()` is unhappy.
+                            val receiverClass: Class<*> = receiver.javaClass
+
+                            // Apply the new proxy settings to any classes whose names contain `ProxyChangeListener`.
+                            if (receiverClass.name.contains("ProxyChangeListener")) {
+                                // Get the `onReceive` method from the class.
+                                val onReceiveMethod = receiverClass.getDeclaredMethod("onReceive", Context::class.java, Intent::class.java)
+
+                                // Create a proxy change intent.
+                                val proxyChangeIntent = Intent(android.net.Proxy.PROXY_CHANGE_ACTION)
+
+                                // Set the proxy for API >= 21.
+                                if (Build.VERSION.SDK_INT >= 21) {
+                                    // Get the proxy info class.
+                                    val proxyInfoClass = Class.forName("android.net.ProxyInfo")
+
+                                    // Get the build direct proxy method from the proxy info class.
+                                    val buildDirectProxyMethod = proxyInfoClass.getMethod("buildDirectProxy", String::class.java, Integer.TYPE)
+
+                                    // Populate a proxy info object with the new proxy information.
+                                    val proxyInfoObject = buildDirectProxyMethod.invoke(proxyInfoClass, proxyHost, Integer.valueOf(proxyPort))
+
+                                    // Add the proxy info object into the proxy change intent.
+                                    proxyChangeIntent.putExtra("proxy", proxyInfoObject as Parcelable)
+                                }
+
+                                // Pass the proxy change intent to the `onReceive` method of the receiver class.
+                                onReceiveMethod.invoke(receiver, context, proxyChangeIntent)
+                            }
+                        }
+                    }
+                } catch (exception: ClassNotFoundException) {
+                    // Do nothing.
+                } catch (exception: NoSuchFieldException) {
+                    // Do nothing.
+                } catch (exception: IllegalAccessException) {
+                    // Do nothing.
+                } catch (exception: NoSuchMethodException) {
+                    // Do nothing.
+                } catch (exception: InvocationTargetException) {
+                    // Do nothing.
+                }
+            }
+        }
+    }
+
+    fun getCurrentProxy(context: Context): Proxy {
+        // Get the proxy according to the current proxy mode.
+        val proxy = when (MainWebViewActivity.proxyMode) {
+            TOR -> if (Build.VERSION.SDK_INT >= 21) {
+                // Use localhost port 9050 as the socket address.
+                val torSocketAddress: SocketAddress = InetSocketAddress.createUnresolved("localhost", 9050)
+
+                // Create a SOCKS proxy.
+                Proxy(Proxy.Type.SOCKS, torSocketAddress)
+            } else {
+                // Use localhost port 8118 as the socket address.
+                val oldTorSocketAddress: SocketAddress = InetSocketAddress.createUnresolved("localhost", 8118)
+
+                // Create an HTTP proxy.
+                Proxy(Proxy.Type.HTTP, oldTorSocketAddress)
+            }
+
+            I2P -> {
+                // Use localhost port 4444 as the socket address.
+                val i2pSocketAddress: SocketAddress = InetSocketAddress.createUnresolved("localhost", 4444)
+
+                // Create an HTTP proxy.
+                Proxy(Proxy.Type.HTTP, i2pSocketAddress)
+            }
+
+            CUSTOM -> {
+                // Get the shared preferences.
+                val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
+
+                // Get the custom proxy URL string.
+                val customProxyUrlString = sharedPreferences.getString(context.getString(R.string.proxy_custom_url_key), context.getString(R.string.proxy_custom_url_default_value))
+
+                // Parse the custom proxy URL.
+                try {
+                    // Convert the custom proxy URL string to a URI.
+                    val customProxyUri = Uri.parse(customProxyUrlString)
+
+                    // Get the custom socket address.
+                    val customSocketAddress: SocketAddress = InetSocketAddress.createUnresolved(customProxyUri.host, customProxyUri.port)
+
+                    // Get the custom proxy scheme.
+                    val customProxyScheme = customProxyUri.scheme
+
+                    // Create a proxy according to the scheme.
+                    if (customProxyScheme != null && customProxyScheme.startsWith("socks")) {  // A SOCKS proxy is specified.
+                        // Create a SOCKS proxy.
+                        Proxy(Proxy.Type.SOCKS, customSocketAddress)
+                    } else {  // A SOCKS proxy is not specified.
+                        // Create an HTTP proxy.
+                        Proxy(Proxy.Type.HTTP, customSocketAddress)
+                    }
+                } catch (exception: Exception) {  // The custom proxy cannot be parsed.
+                    // Disable the proxy.
+                    Proxy.NO_PROXY
+                }
+            }
+
+            else -> {
+                // Create a direct proxy.
+                Proxy.NO_PROXY
+            }
+        }
+
+        // Return the proxy.
+        return proxy
+    }
+}
\ No newline at end of file