/* * Copyright © 2016-2021 Soren Stoutner . * * This file is part of 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 . */ 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 } }