diff --git a/README.md b/README.md index 2fc8fd32..6e877e72 100644 --- a/README.md +++ b/README.md @@ -2,44 +2,69 @@ Global Proxy App for Android System -ProxyDroid is distributed under GPLv3 with many other open source software, +ProxyDroid is distributed under GPLv3 with many other open source software, here is a list of them: - * cntlm - authentication proxy: http://cntlm.sourceforge.net/ * redsocks - transparent socks redirector: http://darkk.net.ru/redsocks/ + * tun2socks - VPN-based transparent proxy * netfilter/iptables - NAT module: http://www.netfilter.org/ - * transproxy - transparent proxy for HTTP: http://transproxy.sourceforge.net/ - * stunnel - multiplatform SSL tunneling proxy: http://www.stunnel.org/ -## TRAVIS CI STATUS +## PREREQUISITES -[![Build Status](https://secure.travis-ci.org/madeye/proxydroid.png)](http://travis-ci.org/madeye/proxydroid) +* JDK 11+ +* Android Studio or Gradle 8.1+ +* Android SDK (compileSdk 33) +* Android NDK 25.1.8937393 +* CMake 3.22.1 -[Nightly Builds](http://buildbot.sinaapp.com) +## BUILD -## PREREQUISITES +### Using Android Studio -* JDK 1.6+ -* Maven 3.0.5 -* Android SDK r17+ -* Android NDK r8+ +1. Open the project in Android Studio +2. Sync Gradle files +3. Build the project using `Build > Make Project` -* Local Maven Dependencies +### Using Command Line - Use Maven Android SDK Deployer to install all android related dependencies. +```bash +./gradlew assembleDebug +``` - ```bash - git clone https://github.com/mosabua/maven-android-sdk-deployer.git - pushd maven-android-sdk-deployer - export ANDROID_HOME=/path/to/android/sdk - mvn install -P 4.1 - popd - ``` +For release build: -## BUILD +```bash +./gradlew assembleRelease +``` -Invoke the building like this +## PROJECT STRUCTURE -```bash - mvn clean install ``` +app/ +├── src/main/ +│ ├── java/org/proxydroid/ # Kotlin source files +│ │ ├── ProxyDroid.kt # Main activity +│ │ ├── ProxyDroidService.kt +│ │ ├── ProxyDroidVpnService.kt +│ │ ├── AppManager.kt +│ │ ├── Profile.kt +│ │ └── utils/ # Utility classes +│ └── cpp/ # Native code +│ ├── exec/ # Native exec helper +│ ├── libevent/ # libevent library +│ ├── redsocks/ # redsocks proxy +│ └── tun2socks/ # tun2socks VPN helper +└── build.gradle +``` + +## SUPPORTED ARCHITECTURES + +* armeabi-v7a +* arm64-v8a +* x86 +* x86_64 + +## REQUIREMENTS + +* Minimum SDK: 21 (Android 5.0) +* Target SDK: 33 (Android 13) diff --git a/app/build.gradle b/app/build.gradle index 6cebd071..2dcdc74f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,6 @@ plugins { id 'com.android.application' + id 'org.jetbrains.kotlin.android' } android { @@ -35,6 +36,10 @@ android { targetCompatibility JavaVersion.VERSION_11 } + kotlinOptions { + jvmTarget = '11' + } + externalNativeBuild { cmake { version '3.22.1' @@ -51,6 +56,8 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.9.0' + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation 'androidx.core:core-ktx:1.10.1' // Exclude JUnit from json-simple (it pulls junit as compile dependency) implementation('com.googlecode.json-simple:json-simple:1.1.1') { exclude group: 'junit', module: 'junit' diff --git a/app/src/androidTest/java/org/proxydroid/ExampleInstrumentedTest.java b/app/src/androidTest/java/org/proxydroid/ExampleInstrumentedTest.java deleted file mode 100644 index c1665d6c..00000000 --- a/app/src/androidTest/java/org/proxydroid/ExampleInstrumentedTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.proxydroid; - -import android.content.Context; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.platform.app.InstrumentationRegistry; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); - - assertEquals("org.proxydroid", appContext.getPackageName()); - } -} diff --git a/app/src/androidTest/java/org/proxydroid/ExampleInstrumentedTest.kt b/app/src/androidTest/java/org/proxydroid/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..59c38909 --- /dev/null +++ b/app/src/androidTest/java/org/proxydroid/ExampleInstrumentedTest.kt @@ -0,0 +1,16 @@ +package org.proxydroid + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.Assert.* + +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("org.proxydroid", appContext.packageName) + } +} diff --git a/app/src/main/java/com/btr/proxy/selector/pac/PacProxySelector.java b/app/src/main/java/com/btr/proxy/selector/pac/PacProxySelector.java deleted file mode 100644 index 16b782b2..00000000 --- a/app/src/main/java/com/btr/proxy/selector/pac/PacProxySelector.java +++ /dev/null @@ -1,144 +0,0 @@ -package com.btr.proxy.selector.pac; - -import java.net.URI; -import java.util.ArrayList; -import java.util.List; - -import android.util.Log; - -/***************************************************************************** - * ProxySelector that will use a PAC script to find an proxy for a given URI. - * - * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009 - ****************************************************************************/ -public class PacProxySelector { - - // private static final String PAC_PROXY = "PROXY"; - private static final String PAC_SOCKS = "SOCKS"; - private static final String PAC_DIRECT = "DIRECT"; - private static final String PAC_HTTPS = "HTTPS"; - - private final static String TAG = "ProxyDroid.PAC"; - - private PacScriptParser pacScriptParser; - - /************************************************************************* - * Constructor - * - * @param pacSource - * the source for the PAC file. - ************************************************************************/ - - public PacProxySelector(PacScriptSource pacSource) { - super(); - selectEngine(pacSource); - } - - /************************************************************************* - * Selects one of the available PAC parser engines. - * - * @param pacSource - * to use as input. - ************************************************************************/ - - private void selectEngine(PacScriptSource pacSource) { - try { - this.pacScriptParser = new RhinoPacScriptParser(pacSource); - } catch (Exception e) { - Log.e(TAG, "PAC parser error.", e); - } - } - - /************************************************************************* - * select - * - * @see java.net.ProxySelector#select(java.net.URI) - ************************************************************************/ - public List select(URI uri) { - if (uri == null || uri.getHost() == null) { - throw new IllegalArgumentException("URI must not be null."); - } - - // Fix for Java 1.6.16 where we get a infinite loop because - // URL.connect(Proxy.NO_PROXY) does not work as expected. - PacScriptSource scriptSource = this.pacScriptParser.getScriptSource(); - if (String.valueOf(scriptSource).contains(uri.getHost())) { - return null; - } - - return findProxy(uri); - } - - /************************************************************************* - * Evaluation of the given URL with the PAC-file. - * - * Two cases can be handled here: DIRECT Fetch the object directly from the - * content HTTP server denoted by its URL PROXY name:port Fetch the object - * via the proxy HTTP server at the given location (name and port) - * - * @param uri - * URI to be evaluated. - * @return Proxy-object list as result of the evaluation. - ************************************************************************/ - - private List findProxy(URI uri) { - try { - List proxies = new ArrayList(); - String parseResult = this.pacScriptParser.evaluate(uri.toString(), - uri.getHost()); - String[] proxyDefinitions = parseResult.split("[;]"); - for (String proxyDef : proxyDefinitions) { - if (proxyDef.trim().length() > 0) { - proxies.add(buildProxyFromPacResult(proxyDef)); - } - } - return proxies; - } catch (ProxyEvaluationException e) { - Log.e(TAG, "PAC resolving error.", e); - return null; - } - } - - /************************************************************************* - * The proxy evaluator will return a proxy string. This method will take - * this string and build a matching Proxy for it. - * - * @param pacResult - * the result from the PAC parser. - * @return a Proxy - ************************************************************************/ - - private Proxy buildProxyFromPacResult(String pacResult) { - if (pacResult == null || pacResult.trim().length() < 6) { - return Proxy.NO_PROXY; - } - String proxyDef = pacResult.trim(); - if (proxyDef.toUpperCase().startsWith(PAC_DIRECT)) { - return Proxy.NO_PROXY; - } - - // Check proxy type. - String type = Proxy.TYPE_HTTP; - if (proxyDef.toUpperCase().startsWith(PAC_SOCKS)) { - type = Proxy.TYPE_SOCKS5; - } - if (proxyDef.toUpperCase().startsWith(PAC_HTTPS)) { - type = Proxy.TYPE_HTTPS; - } - - String host = proxyDef.substring(6); - Integer port = 80; - if (type.equals(Proxy.TYPE_HTTPS)) { - port = 443; - } - - // Split port from host - int indexOfPort = host.indexOf(':'); - if (indexOfPort != -1) { - port = Integer.parseInt(host.substring(indexOfPort + 1).trim()); - host = host.substring(0, indexOfPort).trim(); - } - - return new Proxy(host, port, type); - } -} diff --git a/app/src/main/java/com/btr/proxy/selector/pac/PacProxySelector.kt b/app/src/main/java/com/btr/proxy/selector/pac/PacProxySelector.kt new file mode 100644 index 00000000..468fdef0 --- /dev/null +++ b/app/src/main/java/com/btr/proxy/selector/pac/PacProxySelector.kt @@ -0,0 +1,89 @@ +package com.btr.proxy.selector.pac + +import android.util.Log +import java.net.URI + +class PacProxySelector(pacSource: PacScriptSource) { + + companion object { + private const val PAC_SOCKS = "SOCKS" + private const val PAC_DIRECT = "DIRECT" + private const val PAC_HTTPS = "HTTPS" + private const val TAG = "ProxyDroid.PAC" + } + + private var pacScriptParser: PacScriptParser? = null + + init { + selectEngine(pacSource) + } + + private fun selectEngine(pacSource: PacScriptSource) { + try { + pacScriptParser = RhinoPacScriptParser(pacSource) + } catch (e: Exception) { + Log.e(TAG, "PAC parser error.", e) + } + } + + fun select(uri: URI): List? { + if (uri.host == null) { + throw IllegalArgumentException("URI must not be null.") + } + + val scriptSource = pacScriptParser?.getScriptSource() + if (scriptSource.toString().contains(uri.host)) { + return null + } + + return findProxy(uri) + } + + private fun findProxy(uri: URI): List? { + return try { + val proxies = mutableListOf() + val parseResult = pacScriptParser?.evaluate(uri.toString(), uri.host) ?: return null + val proxyDefinitions = parseResult.split(";") + + for (proxyDef in proxyDefinitions) { + if (proxyDef.trim().isNotEmpty()) { + proxies.add(buildProxyFromPacResult(proxyDef)) + } + } + proxies + } catch (e: ProxyEvaluationException) { + Log.e(TAG, "PAC resolving error.", e) + null + } + } + + private fun buildProxyFromPacResult(pacResult: String?): Proxy { + if (pacResult == null || pacResult.trim().length < 6) { + return Proxy.NO_PROXY + } + + val proxyDef = pacResult.trim() + if (proxyDef.uppercase().startsWith(PAC_DIRECT)) { + return Proxy.NO_PROXY + } + + var type = Proxy.TYPE_HTTP + if (proxyDef.uppercase().startsWith(PAC_SOCKS)) { + type = Proxy.TYPE_SOCKS5 + } + if (proxyDef.uppercase().startsWith(PAC_HTTPS)) { + type = Proxy.TYPE_HTTPS + } + + var host = proxyDef.substring(6) + var port = if (type == Proxy.TYPE_HTTPS) 443 else 80 + + val indexOfPort = host.indexOf(':') + if (indexOfPort != -1) { + port = host.substring(indexOfPort + 1).trim().toInt() + host = host.substring(0, indexOfPort).trim() + } + + return Proxy(host, port, type) + } +} diff --git a/app/src/main/java/com/btr/proxy/selector/pac/PacScriptMethods.java b/app/src/main/java/com/btr/proxy/selector/pac/PacScriptMethods.java deleted file mode 100644 index 66ef714a..00000000 --- a/app/src/main/java/com/btr/proxy/selector/pac/PacScriptMethods.java +++ /dev/null @@ -1,647 +0,0 @@ -package com.btr.proxy.selector.pac; - -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.StringTokenizer; -import java.util.TimeZone; - -import android.util.Log; - -/*************************************************************************** - * Implementation of PAC JavaScript functions. - * - * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009 - *************************************************************************** - */ -public class PacScriptMethods implements ScriptMethods { - - public static final String OVERRIDE_LOCAL_IP = "com.btr.proxy.pac.overrideLocalIP"; - - private final static String GMT = "GMT"; - - private final static List DAYS = Collections - .unmodifiableList(Arrays.asList("SUN", "MON", "TUE", "WED", "THU", - "FRI", "SAT")); - - private final static List MONTH = Collections - .unmodifiableList(Arrays.asList("JAN", "FEB", "MAR", "APR", "MAY", - "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC")); - - private Calendar currentTime; - - private final static String TAG = "ProxyDroid.PAC"; - - /************************************************************************* - * Constructor - ************************************************************************/ - - public PacScriptMethods() { - super(); - } - - /************************************************************************* - * isPlainHostName - * - * @see com.btr.proxy.selector.pac.ScriptMethods#isPlainHostName(java.lang.String) - ************************************************************************/ - - @Override - public boolean isPlainHostName(String host) { - return host.indexOf(".") < 0; - } - - /************************************************************************* - * Tests if an URL is in a given domain. - * - * @param host - * is the host name from the URL. - * @param domain - * is the domain name to test the host name against. - * @return true if the domain of host name matches. - ************************************************************************/ - - @Override - public boolean dnsDomainIs(String host, String domain) { - return host.endsWith(domain); - } - - /************************************************************************* - * Is true if the host name matches exactly the specified host name, or if - * there is no domain name part in the host name, but the unqualified host - * name matches. - * - * @param host - * the host name from the URL. - * @param domain - * fully qualified host name with domain to match against. - * @return true if matches else false. - ************************************************************************/ - - @Override - public boolean localHostOrDomainIs(String host, String domain) { - return domain.startsWith(host); - } - - /************************************************************************* - * Tries to resolve the host name. Returns true if succeeds. - * - * @param host - * is the host name from the URL. - * @return true if resolvable else false. - ************************************************************************/ - - @Override - public boolean isResolvable(String host) { - try { - InetAddress.getByName(host).getHostAddress(); - return true; - } catch (UnknownHostException ex) { - Log.e(TAG, "Hostname not resolveable " + host); - // Not resolvable - } - return false; - } - - /************************************************************************* - * Returns true if the IP address of the host matches the specified IP - * address pattern. Pattern and mask specification is done the same way as - * for SOCKS configuration. - * - * Example: isInNet(host, "198.95.0.0", "255.255.0.0") is true if the IP - * address of the host matches 198.95.*.*. - * - * @param host - * a DNS host name, or IP address. If a host name is passed, it - * will be resolved into an IP address by this function. - * @param pattern - * an IP address pattern in the dot-separated format. - * @param mask - * mask for the IP address pattern informing which parts of the - * IP address should be matched against. 0 means ignore, 255 - * means match. - * @return true if it matches else false. - ************************************************************************/ - - @Override - public boolean isInNet(String host, String pattern, String mask) { - long lhost = parseIpAddressToLong(host); - long lpattern = parseIpAddressToLong(pattern); - long lmask = parseIpAddressToLong(mask); - boolean result = (lhost & lmask) == lpattern; - return result; - } - - /************************************************************************* - * Convert a string representation of a IP to a long. - * - * @param address - * to convert. - * @return the address as long. - ************************************************************************/ - - private long parseIpAddressToLong(String address) { - long result = 0; - String[] parts = address.split("\\."); - long shift = 24; - for (String part : parts) { - long lpart = Long.parseLong(part); - - result |= (lpart << shift); - shift -= 8; - } - return result; - } - - /************************************************************************* - * Resolves the given DNS host name into an IP address, and returns it in - * the dot separated format as a string. - * - * @param host - * the host to resolve. - * @return the resolved IP, empty string if not resolvable. - ************************************************************************/ - - @Override - public String dnsResolve(String host) { - try { - return InetAddress.getByName(host).getHostAddress(); - } catch (UnknownHostException e) { - Log.e(TAG, "DNS name not resolvable " + host); - // Not resolvable. - } - return ""; - } - - /************************************************************************* - * Returns the IP address of the host that the process is running on, as a - * string in the dot-separated integer format. - * - * @return an IP as string. - ************************************************************************/ - - @Override - public String myIpAddress() { - try { - String overrideIP = System.getProperty(OVERRIDE_LOCAL_IP); - if (overrideIP != null && overrideIP.trim().length() > 0) { - return overrideIP.trim(); - } - return InetAddress.getLocalHost().getHostAddress(); - } catch (UnknownHostException e) { - Log.e(TAG, "Local address not resolvable."); - return ""; - } - } - - /************************************************************************* - * Returns the number of DNS domain levels (number of dots) in the host - * name. - * - * @param host - * is the host name from the URL. - * @return number of DNS domain levels. - ************************************************************************/ - - @Override - public int dnsDomainLevels(String host) { - int count = 0; - int startPos = 0; - while ((startPos = host.indexOf(".", startPos + 1)) > -1) { - count++; - } - return count; - } - - /************************************************************************* - * Returns true if the string matches the specified shell expression. - * Actually, currently the patterns are shell expressions, not regular - * expressions. - * - * @param str - * is any string to compare (e.g. the URL, or the host name). - * @param shexp - * is a shell expression to compare against. - * @return true if the string matches, else false. - ************************************************************************/ - - @Override - public boolean shExpMatch(String str, String shexp) { - StringTokenizer tokenizer = new StringTokenizer(shexp, "*"); - int startPos = 0; - while (tokenizer.hasMoreTokens()) { - String token = tokenizer.nextToken(); - // 07.05.2009 Incorrect? first token can be startsWith and last one - // can be endsWith - int temp = str.indexOf(token, startPos); - if (temp == -1) { - return false; - } else { - startPos = temp + token.length(); - } - } - return true; - } - - /************************************************************************* - * Only the first parameter is mandatory. Either the second, the third, or - * both may be left out. If only one parameter is present, the function - * yields a true value on the weekday that the parameter represents. If the - * string "GMT" is specified as a second parameter, times are taken to be in - * GMT, otherwise in local time zone. If both wd1 and wd2 are defined, the - * condition is true if the current weekday is in between those two - * weekdays. Bounds are inclusive. If the "GMT" parameter is specified, - * times are taken to be in GMT, otherwise the local time zone is used. - * - * @param wd1 - * weekday 1 is one of SUN MON TUE WED THU FRI SAT - * @param wd2 - * weekday 2 is one of SUN MON TUE WED THU FRI SAT - * @param gmt - * "GMT" for gmt time format else "undefined" - * @return true if current day matches the criteria. - ************************************************************************/ - - @Override - public boolean weekdayRange(String wd1, String wd2, String gmt) { - boolean useGmt = GMT.equalsIgnoreCase(wd2) || GMT.equalsIgnoreCase(gmt); - Calendar cal = getCurrentTime(useGmt); - - int currentDay = cal.get(Calendar.DAY_OF_WEEK) - 1; - int from = DAYS.indexOf(wd1 == null ? null : wd1.toUpperCase()); - int to = DAYS.indexOf(wd2 == null ? null : wd2.toUpperCase()); - if (to == -1) { - to = from; - } - - if (to < from) { - return currentDay >= from || currentDay <= to; - } else { - return currentDay >= from && currentDay <= to; - } - } - - /************************************************************************* - * Sets a calendar with the current time. If this is set all date and time - * based methods will use this calendar to determine the current time - * instead of the real time. This is only be used by unit tests and is not - * part of the public API. - * - * @param cal - * a Calendar to set. - ************************************************************************/ - - public void setCurrentTime(Calendar cal) { - this.currentTime = cal; - } - - /************************************************************************* - * Gets a calendar set to the current time. This is used by the date and - * time based methods. - * - * @param useGmt - * flag to indicate if the calendar is to be created in GMT time - * or local time. - * @return a Calendar set to the current time. - ************************************************************************/ - - private Calendar getCurrentTime(boolean useGmt) { - if (this.currentTime != null) { // Only used for unit tests - return (Calendar) this.currentTime.clone(); - } - return Calendar.getInstance(useGmt ? TimeZone.getTimeZone(GMT) - : TimeZone.getDefault()); - } - - /************************************************************************* - * Only the first parameter is mandatory. All other parameters can be left - * out therefore the meaning of the parameters changes. The method - * definition shows the version with the most possible parameters filled. - * The real meaning of the parameters is guessed from it's value. If "from" - * and "to" are specified then the bounds are inclusive. If the "GMT" - * parameter is specified, times are taken to be in GMT, otherwise the local - * time zone is used. - * - * @param day1 - * is the day of month between 1 and 31 (as an integer). - * @param month1 - * one of JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC - * @param year1 - * is the full year number, for example 1995 (but not 95). - * Integer. - * @param day2 - * is the day of month between 1 and 31 (as an integer). - * @param month2 - * one of JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC - * @param year2 - * is the full year number, for example 1995 (but not 95). - * Integer. - * @param gmt - * "GMT" for gmt time format else "undefined" - * @return true if the current date matches the given range. - ************************************************************************/ - - @Override - public boolean dateRange(Object day1, Object month1, Object year1, - Object day2, Object month2, Object year2, Object gmt) { - - // Guess the parameter meanings. - Map params = new HashMap(); - parseDateParam(params, day1); - parseDateParam(params, month1); - parseDateParam(params, year1); - parseDateParam(params, day2); - parseDateParam(params, month2); - parseDateParam(params, year2); - parseDateParam(params, gmt); - - // Get current date - boolean useGmt = params.get("gmt") != null; - Calendar cal = getCurrentTime(useGmt); - Date current = cal.getTime(); - - // Build the "from" date - if (params.get("day1") != null) { - cal.set(Calendar.DAY_OF_MONTH, params.get("day1")); - } - if (params.get("month1") != null) { - cal.set(Calendar.MONTH, params.get("month1")); - } - if (params.get("year1") != null) { - cal.set(Calendar.YEAR, params.get("year1")); - } - Date from = cal.getTime(); - - // Build the "to" date - Date to; - if (params.get("day2") != null) { - cal.set(Calendar.DAY_OF_MONTH, params.get("day2")); - } - if (params.get("month2") != null) { - cal.set(Calendar.MONTH, params.get("month2")); - } - if (params.get("year2") != null) { - cal.set(Calendar.YEAR, params.get("year2")); - } - to = cal.getTime(); - - // Need to increment to the next month? - if (to.before(from)) { - cal.add(Calendar.MONTH, +1); - to = cal.getTime(); - } - // Need to increment to the next year? - if (to.before(from)) { - cal.add(Calendar.YEAR, +1); - cal.add(Calendar.MONTH, -1); - to = cal.getTime(); - } - - return current.compareTo(from) >= 0 && current.compareTo(to) <= 0; - } - - /************************************************************************* - * Try to guess the type of the given parameter and put it into the params - * map. - * - * @param params - * a map to put the parsed parameters into. - * @param value - * to parse and specify the type for. - ************************************************************************/ - - private void parseDateParam(Map params, Object value) { - if (value instanceof Number) { - int n = ((Number) value).intValue(); - if (n <= 31) { - // Its a day - if (params.get("day1") == null) { - params.put("day1", n); - } else { - params.put("day2", n); - } - } else { - // Its a year - if (params.get("year1") == null) { - params.put("year1", n); - } else { - params.put("year2", n); - } - } - } - - if (value instanceof String) { - int n = MONTH.indexOf(((String) value).toUpperCase()); - if (n > -1) { - // Its a month - if (params.get("month1") == null) { - params.put("month1", n); - } else { - params.put("month2", n); - } - } - } - - if (GMT.equalsIgnoreCase(String.valueOf(value))) { - params.put("gmt", 1); - } - } - - /************************************************************************* - * Some parameters can be left out therefore the meaning of the parameters - * changes. The method definition shows the version with the most possible - * parameters filled. The real meaning of the parameters is guessed from - * it's value. If "from" and "to" are specified then the bounds are - * inclusive. If the "GMT" parameter is specified, times are taken to be in - * GMT, otherwise the local time zone is used.
- * - *
-	 * timeRange(hour)
-	 * timeRange(hour1, hour2)
-	 * timeRange(hour1, min1, hour2, min2)
-	 * timeRange(hour1, min1, sec1, hour2, min2, sec2)
-	 * timeRange(hour1, min1, sec1, hour2, min2, sec2, gmt)
-	 * 
- * - * @param hour1 - * is the hour from 0 to 23. (0 is midnight, 23 is 11 pm.) - * @param min1 - * minutes from 0 to 59. - * @param sec1 - * seconds from 0 to 59. - * @param hour2 - * is the hour from 0 to 23. (0 is midnight, 23 is 11 pm.) - * @param min2 - * minutes from 0 to 59. - * @param sec2 - * seconds from 0 to 59. - * @param gmt - * "GMT" for gmt time format else "undefined" - * @return true if the current time matches the given range. - ************************************************************************/ - - @Override - public boolean timeRange(Object hour1, Object min1, Object sec1, - Object hour2, Object min2, Object sec2, Object gmt) { - boolean useGmt = GMT.equalsIgnoreCase(String.valueOf(min1)) - || GMT.equalsIgnoreCase(String.valueOf(sec1)) - || GMT.equalsIgnoreCase(String.valueOf(min2)) - || GMT.equalsIgnoreCase(String.valueOf(gmt)); - - Calendar cal = getCurrentTime(useGmt); - cal.set(Calendar.MILLISECOND, 0); - Date current = cal.getTime(); - Date from; - Date to; - if (sec2 instanceof Number) { - cal.set(Calendar.HOUR_OF_DAY, ((Number) hour1).intValue()); - cal.set(Calendar.MINUTE, ((Number) min1).intValue()); - cal.set(Calendar.SECOND, ((Number) sec1).intValue()); - from = cal.getTime(); - - cal.set(Calendar.HOUR_OF_DAY, ((Number) hour2).intValue()); - cal.set(Calendar.MINUTE, ((Number) min2).intValue()); - cal.set(Calendar.SECOND, ((Number) sec2).intValue()); - to = cal.getTime(); - } else if (hour2 instanceof Number) { - cal.set(Calendar.HOUR_OF_DAY, ((Number) hour1).intValue()); - cal.set(Calendar.MINUTE, ((Number) min1).intValue()); - cal.set(Calendar.SECOND, 0); - from = cal.getTime(); - - cal.set(Calendar.HOUR_OF_DAY, ((Number) sec1).intValue()); - cal.set(Calendar.MINUTE, ((Number) hour2).intValue()); - cal.set(Calendar.SECOND, 59); - to = cal.getTime(); - } else if (min1 instanceof Number) { - cal.set(Calendar.HOUR_OF_DAY, ((Number) hour1).intValue()); - cal.set(Calendar.MINUTE, 0); - cal.set(Calendar.SECOND, 0); - from = cal.getTime(); - - cal.set(Calendar.HOUR_OF_DAY, ((Number) min1).intValue()); - cal.set(Calendar.MINUTE, 59); - cal.set(Calendar.SECOND, 59); - to = cal.getTime(); - } else { - cal.set(Calendar.HOUR_OF_DAY, ((Number) hour1).intValue()); - cal.set(Calendar.MINUTE, 0); - cal.set(Calendar.SECOND, 0); - from = cal.getTime(); - - cal.set(Calendar.HOUR_OF_DAY, ((Number) hour1).intValue()); - cal.set(Calendar.MINUTE, 59); - cal.set(Calendar.SECOND, 59); - to = cal.getTime(); - } - - if (to.before(from)) { - cal.setTime(to); - cal.add(Calendar.DATE, +1); - to = cal.getTime(); - } - - return current.compareTo(from) >= 0 && current.compareTo(to) <= 0; - } - - // Microsoft PAC extensions for IPv6 support. - - /************************************************************************* - * isResolvableEx - * - * @see com.btr.proxy.selector.pac.ScriptMethods#isResolvableEx(java.lang.String) - ************************************************************************/ - @Override - public boolean isResolvableEx(String host) { - return isResolvable(host); - } - - /************************************************************************* - * isInNetEx - * - * @see com.btr.proxy.selector.pac.ScriptMethods#isInNetEx(java.lang.String, - * java.lang.String) - ************************************************************************/ - @Override - public boolean isInNetEx(String ipAddress, String ipPrefix) { - // TODO rossi 27.06.2011 Auto-generated method stub - return false; - } - - /************************************************************************* - * dnsResolveEx - * - * @see com.btr.proxy.selector.pac.ScriptMethods#dnsResolveEx(java.lang.String) - ************************************************************************/ - @Override - public String dnsResolveEx(String host) { - StringBuilder result = new StringBuilder(); - try { - InetAddress[] list = InetAddress.getAllByName(host); - for (InetAddress inetAddress : list) { - result.append(inetAddress.getHostAddress()); - result.append("; "); - } - } catch (UnknownHostException e) { - Log.e(TAG, "DNS name not resolvable " + host); - } - return result.toString(); - } - - /************************************************************************* - * myIpAddressEx - * - * @see com.btr.proxy.selector.pac.ScriptMethods#myIpAddressEx() - ************************************************************************/ - @Override - public String myIpAddressEx() { - String overrideIP = System.getProperty(OVERRIDE_LOCAL_IP); - if (overrideIP != null && overrideIP.trim().length() > 0) { - return overrideIP.trim(); - } - return dnsResolveEx("localhost"); - } - - /************************************************************************* - * sortIpAddressList - * - * @see com.btr.proxy.selector.pac.ScriptMethods#sortIpAddressList(java.lang.String) - ************************************************************************/ - @Override - public String sortIpAddressList(String ipAddressList) { - if (ipAddressList == null || ipAddressList.trim().length() == 0) { - return ""; - } - String[] ipAddressToken = ipAddressList.split(";"); - List parsedAddresses = new ArrayList(); - for (String ip : ipAddressToken) { - try { - parsedAddresses.add(InetAddress.getByName(ip)); - } catch (UnknownHostException e) { - // TODO rossi 01.11.2011 Auto-generated catch block - e.printStackTrace(); - } - } - Collections.sort(parsedAddresses, null); - // TODO rossi 27.06.2011 Implement me. - return ipAddressList; - } - - /************************************************************************* - * getClientVersion - * - * @see com.btr.proxy.selector.pac.ScriptMethods#getClientVersion() - ************************************************************************/ - @Override - public String getClientVersion() { - return "1.0"; - } - -} diff --git a/app/src/main/java/com/btr/proxy/selector/pac/PacScriptMethods.kt b/app/src/main/java/com/btr/proxy/selector/pac/PacScriptMethods.kt new file mode 100644 index 00000000..7c7301c6 --- /dev/null +++ b/app/src/main/java/com/btr/proxy/selector/pac/PacScriptMethods.kt @@ -0,0 +1,286 @@ +package com.btr.proxy.selector.pac + +import android.util.Log +import java.net.InetAddress +import java.net.UnknownHostException +import java.util.* + +class PacScriptMethods : ScriptMethods { + + companion object { + const val OVERRIDE_LOCAL_IP = "com.btr.proxy.pac.overrideLocalIP" + private const val GMT = "GMT" + private const val TAG = "ProxyDroid.PAC" + + private val DAYS = listOf("SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT") + private val MONTH = listOf("JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC") + } + + private var currentTime: Calendar? = null + + override fun isPlainHostName(host: String): Boolean = host.indexOf(".") < 0 + + override fun dnsDomainIs(host: String, domain: String): Boolean = host.endsWith(domain) + + override fun localHostOrDomainIs(host: String, domain: String): Boolean = domain.startsWith(host) + + override fun isResolvable(host: String): Boolean { + return try { + InetAddress.getByName(host).hostAddress + true + } catch (ex: UnknownHostException) { + Log.e(TAG, "Hostname not resolvable $host") + false + } + } + + override fun isInNet(host: String, pattern: String, mask: String): Boolean { + val lhost = parseIpAddressToLong(host) + val lpattern = parseIpAddressToLong(pattern) + val lmask = parseIpAddressToLong(mask) + return (lhost and lmask) == lpattern + } + + private fun parseIpAddressToLong(address: String): Long { + var result = 0L + val parts = address.split(".") + var shift = 24L + for (part in parts) { + val lpart = part.toLong() + result = result or (lpart shl shift.toInt()) + shift -= 8 + } + return result + } + + override fun dnsResolve(host: String): String { + return try { + InetAddress.getByName(host).hostAddress ?: "" + } catch (e: UnknownHostException) { + Log.e(TAG, "DNS name not resolvable $host") + "" + } + } + + override fun myIpAddress(): String { + return try { + val overrideIP = System.getProperty(OVERRIDE_LOCAL_IP) + if (!overrideIP.isNullOrBlank()) { + overrideIP.trim() + } else { + InetAddress.getLocalHost().hostAddress ?: "" + } + } catch (e: UnknownHostException) { + Log.e(TAG, "Local address not resolvable.") + "" + } + } + + override fun dnsDomainLevels(host: String): Int { + var count = 0 + var startPos = 0 + while (host.indexOf(".", startPos + 1).also { startPos = it } > -1) { + count++ + } + return count + } + + override fun shExpMatch(str: String, shexp: String): Boolean { + val tokenizer = StringTokenizer(shexp, "*") + var startPos = 0 + while (tokenizer.hasMoreTokens()) { + val token = tokenizer.nextToken() + val temp = str.indexOf(token, startPos) + if (temp == -1) { + return false + } else { + startPos = temp + token.length + } + } + return true + } + + override fun weekdayRange(wd1: String, wd2: String?, gmt: String?): Boolean { + val useGmt = GMT.equals(wd2, ignoreCase = true) || GMT.equals(gmt, ignoreCase = true) + val cal = getCurrentTime(useGmt) + + val currentDay = cal.get(Calendar.DAY_OF_WEEK) - 1 + val from = DAYS.indexOf(wd1.uppercase()) + var to = DAYS.indexOf(wd2?.uppercase() ?: "") + if (to == -1) to = from + + return if (to < from) { + currentDay >= from || currentDay <= to + } else { + currentDay in from..to + } + } + + fun setCurrentTime(cal: Calendar?) { + this.currentTime = cal + } + + private fun getCurrentTime(useGmt: Boolean): Calendar { + currentTime?.let { return it.clone() as Calendar } + return Calendar.getInstance(if (useGmt) TimeZone.getTimeZone(GMT) else TimeZone.getDefault()) + } + + override fun dateRange(day1: Any?, month1: Any?, year1: Any?, day2: Any?, month2: Any?, year2: Any?, gmt: Any?): Boolean { + val params = mutableMapOf() + parseDateParam(params, day1) + parseDateParam(params, month1) + parseDateParam(params, year1) + parseDateParam(params, day2) + parseDateParam(params, month2) + parseDateParam(params, year2) + parseDateParam(params, gmt) + + val useGmt = params["gmt"] != null + val cal = getCurrentTime(useGmt) + val current = cal.time + + params["day1"]?.let { cal.set(Calendar.DAY_OF_MONTH, it) } + params["month1"]?.let { cal.set(Calendar.MONTH, it) } + params["year1"]?.let { cal.set(Calendar.YEAR, it) } + val from = cal.time + + params["day2"]?.let { cal.set(Calendar.DAY_OF_MONTH, it) } + params["month2"]?.let { cal.set(Calendar.MONTH, it) } + params["year2"]?.let { cal.set(Calendar.YEAR, it) } + var to = cal.time + + if (to.before(from)) { + cal.add(Calendar.MONTH, 1) + to = cal.time + } + if (to.before(from)) { + cal.add(Calendar.YEAR, 1) + cal.add(Calendar.MONTH, -1) + to = cal.time + } + + return current >= from && current <= to + } + + private fun parseDateParam(params: MutableMap, value: Any?) { + when (value) { + is Number -> { + val n = value.toInt() + if (n <= 31) { + if (params["day1"] == null) params["day1"] = n else params["day2"] = n + } else { + if (params["year1"] == null) params["year1"] = n else params["year2"] = n + } + } + is String -> { + val n = MONTH.indexOf(value.uppercase()) + if (n > -1) { + if (params["month1"] == null) params["month1"] = n else params["month2"] = n + } + } + } + if (GMT.equals(value.toString(), ignoreCase = true)) { + params["gmt"] = 1 + } + } + + override fun timeRange(hour1: Any?, min1: Any?, sec1: Any?, hour2: Any?, min2: Any?, sec2: Any?, gmt: Any?): Boolean { + val useGmt = GMT.equals(min1.toString(), ignoreCase = true) || + GMT.equals(sec1.toString(), ignoreCase = true) || + GMT.equals(min2.toString(), ignoreCase = true) || + GMT.equals(gmt.toString(), ignoreCase = true) + + val cal = getCurrentTime(useGmt) + cal.set(Calendar.MILLISECOND, 0) + val current = cal.time + + val from: Date + val to: Date + + when { + sec2 is Number -> { + cal.set(Calendar.HOUR_OF_DAY, (hour1 as Number).toInt()) + cal.set(Calendar.MINUTE, (min1 as Number).toInt()) + cal.set(Calendar.SECOND, (sec1 as Number).toInt()) + from = cal.time + + cal.set(Calendar.HOUR_OF_DAY, (hour2 as Number).toInt()) + cal.set(Calendar.MINUTE, (min2 as Number).toInt()) + cal.set(Calendar.SECOND, sec2.toInt()) + to = cal.time + } + hour2 is Number -> { + cal.set(Calendar.HOUR_OF_DAY, (hour1 as Number).toInt()) + cal.set(Calendar.MINUTE, (min1 as Number).toInt()) + cal.set(Calendar.SECOND, 0) + from = cal.time + + cal.set(Calendar.HOUR_OF_DAY, (sec1 as Number).toInt()) + cal.set(Calendar.MINUTE, hour2.toInt()) + cal.set(Calendar.SECOND, 59) + to = cal.time + } + min1 is Number -> { + cal.set(Calendar.HOUR_OF_DAY, (hour1 as Number).toInt()) + cal.set(Calendar.MINUTE, 0) + cal.set(Calendar.SECOND, 0) + from = cal.time + + cal.set(Calendar.HOUR_OF_DAY, min1.toInt()) + cal.set(Calendar.MINUTE, 59) + cal.set(Calendar.SECOND, 59) + to = cal.time + } + else -> { + cal.set(Calendar.HOUR_OF_DAY, (hour1 as Number).toInt()) + cal.set(Calendar.MINUTE, 0) + cal.set(Calendar.SECOND, 0) + from = cal.time + + cal.set(Calendar.HOUR_OF_DAY, (hour1 as Number).toInt()) + cal.set(Calendar.MINUTE, 59) + cal.set(Calendar.SECOND, 59) + to = cal.time + } + } + + var adjustedTo = to + if (adjustedTo.before(from)) { + cal.time = adjustedTo + cal.add(Calendar.DATE, 1) + adjustedTo = cal.time + } + + return current >= from && current <= adjustedTo + } + + override fun isResolvableEx(host: String): Boolean = isResolvable(host) + + override fun isInNetEx(ipAddress: String, ipPrefix: String): Boolean = false + + override fun dnsResolveEx(host: String): String { + val result = StringBuilder() + try { + val list = InetAddress.getAllByName(host) + for (inetAddress in list) { + result.append(inetAddress.hostAddress) + result.append("; ") + } + } catch (e: UnknownHostException) { + Log.e(TAG, "DNS name not resolvable $host") + } + return result.toString() + } + + override fun myIpAddressEx(): String { + val overrideIP = System.getProperty(OVERRIDE_LOCAL_IP) + return if (!overrideIP.isNullOrBlank()) overrideIP.trim() else dnsResolveEx("localhost") + } + + override fun sortIpAddressList(ipAddressList: String): String { + if (ipAddressList.isBlank()) return "" + return ipAddressList + } + + override fun getClientVersion(): String = "1.0" +} diff --git a/app/src/main/java/com/btr/proxy/selector/pac/PacScriptParser.java b/app/src/main/java/com/btr/proxy/selector/pac/PacScriptParser.java deleted file mode 100644 index 26c89ab0..00000000 --- a/app/src/main/java/com/btr/proxy/selector/pac/PacScriptParser.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.btr.proxy.selector.pac; - -/*************************************************************************** - * Common interface for PAC script parsers. - * - * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009 - ***************************************************************************/ -public interface PacScriptParser { - - /*************************************************************************** - * Gets the source of the PAC script used by this parser. - * - * @return a PacScriptSource. - **************************************************************************/ - public PacScriptSource getScriptSource(); - - /************************************************************************* - * Evaluates the given URL and host against the PAC script. - * - * @param url - * the URL to evaluate. - * @param host - * the host name part of the URL. - * @return the script result. - * @throws ProxyEvaluationException - * on execution error. - ************************************************************************/ - public String evaluate(String url, String host) - throws ProxyEvaluationException; -} diff --git a/app/src/main/java/com/btr/proxy/selector/pac/PacScriptParser.kt b/app/src/main/java/com/btr/proxy/selector/pac/PacScriptParser.kt new file mode 100644 index 00000000..5c2c42a6 --- /dev/null +++ b/app/src/main/java/com/btr/proxy/selector/pac/PacScriptParser.kt @@ -0,0 +1,8 @@ +package com.btr.proxy.selector.pac + +interface PacScriptParser { + fun getScriptSource(): PacScriptSource + + @Throws(ProxyEvaluationException::class) + fun evaluate(url: String, host: String): String +} diff --git a/app/src/main/java/com/btr/proxy/selector/pac/PacScriptSource.java b/app/src/main/java/com/btr/proxy/selector/pac/PacScriptSource.java deleted file mode 100644 index a4bc387e..00000000 --- a/app/src/main/java/com/btr/proxy/selector/pac/PacScriptSource.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.btr.proxy.selector.pac; - -import java.io.IOException; - -/***************************************************************************** - * An source to fetch the PAC script from. - * - * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009 - ****************************************************************************/ - -public interface PacScriptSource { - - /************************************************************************* - * Gets the PAC script content as String. - * - * @return a script. - * @throws IOException - * on read error. - ************************************************************************/ - - public String getScriptContent() throws IOException; - -} \ No newline at end of file diff --git a/app/src/main/java/com/btr/proxy/selector/pac/PacScriptSource.kt b/app/src/main/java/com/btr/proxy/selector/pac/PacScriptSource.kt new file mode 100644 index 00000000..07f02efe --- /dev/null +++ b/app/src/main/java/com/btr/proxy/selector/pac/PacScriptSource.kt @@ -0,0 +1,8 @@ +package com.btr.proxy.selector.pac + +import java.io.IOException + +interface PacScriptSource { + @Throws(IOException::class) + fun getScriptContent(): String +} diff --git a/app/src/main/java/com/btr/proxy/selector/pac/Proxy.java b/app/src/main/java/com/btr/proxy/selector/pac/Proxy.java deleted file mode 100644 index 35f24e33..00000000 --- a/app/src/main/java/com/btr/proxy/selector/pac/Proxy.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.btr.proxy.selector.pac; - -public class Proxy { - - public final static Proxy NO_PROXY = new Proxy(null, 0, null); - - public final static String TYPE_HTTP = "http"; - public final static String TYPE_HTTPS = "https"; - public final static String TYPE_SOCKS4 = "socks4"; - public final static String TYPE_SOCKS5 = "socks5"; - - public String type = "http"; - public String host = ""; - public int port = 3128; - - public Proxy(String host, int port, String type) { - this.host = host; - this.port = port; - this.type = type; - } - -} diff --git a/app/src/main/java/com/btr/proxy/selector/pac/Proxy.kt b/app/src/main/java/com/btr/proxy/selector/pac/Proxy.kt new file mode 100644 index 00000000..38f474b8 --- /dev/null +++ b/app/src/main/java/com/btr/proxy/selector/pac/Proxy.kt @@ -0,0 +1,17 @@ +package com.btr.proxy.selector.pac + +class Proxy( + var host: String? = "", + var port: Int = 3128, + var type: String? = TYPE_HTTP +) { + companion object { + @JvmField + val NO_PROXY = Proxy(null, 0, null) + + const val TYPE_HTTP = "http" + const val TYPE_HTTPS = "https" + const val TYPE_SOCKS4 = "socks4" + const val TYPE_SOCKS5 = "socks5" + } +} diff --git a/app/src/main/java/com/btr/proxy/selector/pac/ProxyEvaluationException.java b/app/src/main/java/com/btr/proxy/selector/pac/ProxyEvaluationException.java deleted file mode 100644 index 876e5a8f..00000000 --- a/app/src/main/java/com/btr/proxy/selector/pac/ProxyEvaluationException.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.btr.proxy.selector.pac; - -/***************************************************************************** - * Exception for PAC script errors. - * - * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009 - ****************************************************************************/ - -public class ProxyEvaluationException extends ProxyException { - - private static final long serialVersionUID = 1L; - - /************************************************************************* - * Constructor - ************************************************************************/ - - public ProxyEvaluationException() { - super(); - } - - /************************************************************************* - * Constructor - * - * @param message - * the error message. - * @param cause - * the causing exception for exception chaining. - ************************************************************************/ - - public ProxyEvaluationException(String message, Throwable cause) { - super(message, cause); - } - - /************************************************************************* - * Constructor - * - * @param message - * the error message. - ************************************************************************/ - - public ProxyEvaluationException(String message) { - super(message); - } - - /************************************************************************* - * Constructor - * - * @param cause - * the causing exception for exception chaining. - ************************************************************************/ - - public ProxyEvaluationException(Throwable cause) { - super(cause); - } - -} diff --git a/app/src/main/java/com/btr/proxy/selector/pac/ProxyEvaluationException.kt b/app/src/main/java/com/btr/proxy/selector/pac/ProxyEvaluationException.kt new file mode 100644 index 00000000..e2341fd8 --- /dev/null +++ b/app/src/main/java/com/btr/proxy/selector/pac/ProxyEvaluationException.kt @@ -0,0 +1,12 @@ +package com.btr.proxy.selector.pac + +class ProxyEvaluationException : ProxyException { + constructor() : super() + constructor(message: String?, cause: Throwable?) : super(message, cause) + constructor(message: String?) : super(message) + constructor(cause: Throwable?) : super(cause) + + companion object { + private const val serialVersionUID = 1L + } +} diff --git a/app/src/main/java/com/btr/proxy/selector/pac/ProxyException.java b/app/src/main/java/com/btr/proxy/selector/pac/ProxyException.java deleted file mode 100644 index d69d8dfd..00000000 --- a/app/src/main/java/com/btr/proxy/selector/pac/ProxyException.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.btr.proxy.selector.pac; - -/***************************************************************************** - * Indicates an exception in the proxy framework. - * - * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009 - ****************************************************************************/ - -public class ProxyException extends Exception { - - private static final long serialVersionUID = 1L; - - /************************************************************************* - * Constructor - ************************************************************************/ - - public ProxyException() { - super(); - } - - /************************************************************************* - * Constructor - * - * @param message - * the error message - * @param cause - * the causing exception for chaining exceptions. - ************************************************************************/ - - public ProxyException(String message, Throwable cause) { - super(message, cause); - } - - /************************************************************************* - * Constructor - * - * @param message - * the error message - ************************************************************************/ - - public ProxyException(String message) { - super(message); - } - - /************************************************************************* - * Constructor - * - * @param cause - * the causing exception for chaining exceptions. - ************************************************************************/ - - public ProxyException(Throwable cause) { - super(cause); - } - -} diff --git a/app/src/main/java/com/btr/proxy/selector/pac/ProxyException.kt b/app/src/main/java/com/btr/proxy/selector/pac/ProxyException.kt new file mode 100644 index 00000000..0b189fc4 --- /dev/null +++ b/app/src/main/java/com/btr/proxy/selector/pac/ProxyException.kt @@ -0,0 +1,12 @@ +package com.btr.proxy.selector.pac + +open class ProxyException : Exception { + constructor() : super() + constructor(message: String?, cause: Throwable?) : super(message, cause) + constructor(message: String?) : super(message) + constructor(cause: Throwable?) : super(cause) + + companion object { + private const val serialVersionUID = 1L + } +} diff --git a/app/src/main/java/com/btr/proxy/selector/pac/RhinoPacScriptParser.java b/app/src/main/java/com/btr/proxy/selector/pac/RhinoPacScriptParser.java deleted file mode 100644 index 26c57b5c..00000000 --- a/app/src/main/java/com/btr/proxy/selector/pac/RhinoPacScriptParser.java +++ /dev/null @@ -1,388 +0,0 @@ -package com.btr.proxy.selector.pac; - -import java.util.Calendar; - -import org.mozilla.javascript.Context; -import org.mozilla.javascript.ContextFactory; -import org.mozilla.javascript.Scriptable; -import org.mozilla.javascript.ScriptableObject; - -import android.util.Log; - -/***************************************************************************** - * PAC parser using the Rhino JavaScript engine.
- * Depends on js.jar of the Apache Rhino - * project. - *

- * More information about PAC can be found there:
- * Proxy_auto-config
- * web-browser-auto-proxy-configuration - *

- * - * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009 - ****************************************************************************/ - -public class RhinoPacScriptParser extends ScriptableObject implements - PacScriptParser { - - private static final long serialVersionUID = 1L; - - private final static String TAG = "ProxyDroid.PAC"; - - // Define some PAC script functions. These functions are not part of ECMA. - private static final String[] JS_FUNCTION_NAMES = { "shExpMatch", - "dnsResolve", "isResolvable", "isInNet", "dnsDomainIs", - "isPlainHostName", "myIpAddress", "dnsDomainLevels", - "localHostOrDomainIs", "weekdayRange", "dateRange", "timeRange" }; - - private Scriptable scope; - private PacScriptSource source; - private static final PacScriptMethods SCRIPT_METHODS = new PacScriptMethods(); - - /************************************************************************* - * Constructor - * - * @param source - * the source for the PAC script. - * @throws ProxyEvaluationException - * on error. - ************************************************************************/ - - public RhinoPacScriptParser(PacScriptSource source) - throws ProxyEvaluationException { - super(); - this.source = source; - - setupEngine(); - } - - /************************************************************************* - * Initializes the JavaScript engine. - * - * @throws ProxyEvaluationException - * on error. - ************************************************************************/ - - public void setupEngine() throws ProxyEvaluationException { - - Context context = new ContextFactory().enterContext(); - try { - defineFunctionProperties(JS_FUNCTION_NAMES, - RhinoPacScriptParser.class, ScriptableObject.DONTENUM); - } catch (Exception e) { - Log.e(TAG, "JS Engine setup error.", e); - throw new ProxyEvaluationException(e.getMessage(), e); - } - - this.scope = context.initStandardObjects(this); - } - - /*************************************************************************** - * Gets the source of the PAC script used by this parser. - * - * @return a PacScriptSource. - **************************************************************************/ - - @Override - public PacScriptSource getScriptSource() { - return this.source; - } - - /************************************************************************* - * Evaluates the given URL and host against the PAC script. - * - * @param url - * the URL to evaluate. - * @param host - * the host name part of the URL. - * @return the script result. - * @throws ProxyEvaluationException - * on execution error. - ************************************************************************/ - - @Override - public String evaluate(String url, String host) - throws ProxyEvaluationException { - try { - // FindProxyForURL function signature - StringBuilder script = new StringBuilder( - this.source.getScriptContent()); - String evalMethod = " ;FindProxyForURL (\"" + url + "\",\"" + host - + "\")"; - script.append(evalMethod); - - Context context = Context.enter(); - context.setOptimizationLevel(-1); - try { - Object result = context.evaluateString(this.scope, - script.toString(), "userPacFile", 1, null); - - return Context.toString(result); - } finally { - Context.exit(); - } - } catch (Exception e) { - Log.e(TAG, "JS evaluation error.", e); - throw new ProxyEvaluationException( - "Error while executing PAC script: " + e.getMessage(), e); - } - } - - /************************************************************************* - * getClassName See also - * org.mozilla.javascript.ScriptableObject#getClassName() - ************************************************************************/ - @Override - public String getClassName() { - return getClass().getSimpleName(); - } - - // *************************************************************************** - // Defining PAC script methods needed in JS - // *************************************************************************** - - /************************************************************************* - * Tests if the given name is a plain host name without a domain name. - * - * @param host - * the host name from the URL (excluding port number) - * @return true if there is no domain name in the host name (no dots). - ************************************************************************/ - - public static boolean isPlainHostName(String host) { - return SCRIPT_METHODS.isPlainHostName(host); - } - - /************************************************************************* - * Tests if an URL is in a given domain. - * - * @param host - * is the host name from the URL. - * @param domain - * is the domain name to test the host name against. - * @return true if the domain of host name matches. - ************************************************************************/ - - public static boolean dnsDomainIs(String host, String domain) { - return SCRIPT_METHODS.dnsDomainIs(host, domain); - } - - /************************************************************************* - * Is true if the host name matches exactly the specified host name, or if - * there is no domain name part in the host name, but the unqualified host - * name matches. - * - * @param host - * the host name from the URL. - * @param domain - * fully qualified host name with domain to match against. - * @return true if matches else false. - ************************************************************************/ - - public static boolean localHostOrDomainIs(String host, String domain) { - return SCRIPT_METHODS.localHostOrDomainIs(host, domain); - } - - /************************************************************************* - * Tries to resolve the host name. Returns true if succeeds. - * - * @param host - * is the host name from the URL. - * @return true if resolvable else false. - ************************************************************************/ - - public static boolean isResolvable(String host) { - return SCRIPT_METHODS.isResolvable(host); - } - - /************************************************************************* - * Returns true if the IP address of the host matches the specified IP - * address pattern. Pattern and mask specification is done the same way as - * for SOCKS configuration. - * - * Example: isInNet(host, "198.95.0.0", "255.255.0.0") is true if the IP - * address of the host matches 198.95.*.*. - * - * @param host - * a DNS host name, or IP address. If a host name is passed, it - * will be resolved into an IP address by this function. - * @param pattern - * an IP address pattern in the dot-separated format. - * @param mask - * mask for the IP address pattern informing which parts of the - * IP address should be matched against. 0 means ignore, 255 - * means match. - * @return true if it matches else false. - ************************************************************************/ - - public static boolean isInNet(String host, String pattern, String mask) { - return SCRIPT_METHODS.isInNet(host, pattern, mask); - } - - /************************************************************************* - * Resolves the given DNS host name into an IP address, and returns it in - * the dot separated format as a string. - * - * @param host - * the host to resolve. - * @return the resolved IP, empty string if not resolvable. - ************************************************************************/ - - public static String dnsResolve(String host) { - return SCRIPT_METHODS.dnsResolve(host); - } - - /************************************************************************* - * Returns the IP address of the host that the process is running on, as a - * string in the dot-separated integer format. - * - * @return an IP as string. - ************************************************************************/ - - public static String myIpAddress() { - return SCRIPT_METHODS.myIpAddress(); - } - - /************************************************************************* - * Returns the number of DNS domain levels (number of dots) in the host - * name. - * - * @param host - * is the host name from the URL. - * @return number of DNS domain levels. - ************************************************************************/ - - public static int dnsDomainLevels(String host) { - return SCRIPT_METHODS.dnsDomainLevels(host); - } - - /************************************************************************* - * Returns true if the string matches the specified shell expression. - * Actually, currently the patterns are shell expressions, not regular - * expressions. - * - * @param str - * is any string to compare (e.g. the URL, or the host name). - * @param shexp - * is a shell expression to compare against. - * @return true if the string matches, else false. - ************************************************************************/ - - public static boolean shExpMatch(String str, String shexp) { - return SCRIPT_METHODS.shExpMatch(str, shexp); - } - - /************************************************************************* - * Only the first parameter is mandatory. Either the second, the third, or - * both may be left out. If only one parameter is present, the function - * yields a true value on the weekday that the parameter represents. If the - * string "GMT" is specified as a second parameter, times are taken to be in - * GMT, otherwise in local time zone. If both wd1 and wd2 are defined, the - * condition is true if the current weekday is in between those two - * weekdays. Bounds are inclusive. If the "GMT" parameter is specified, - * times are taken to be in GMT, otherwise the local time zone is used. - * - * @param wd1 - * weekday 1 is one of SUN MON TUE WED THU FRI SAT - * @param wd2 - * weekday 2 is one of SUN MON TUE WED THU FRI SAT - * @param gmt - * "GMT" for gmt time format else "undefined" - * @return true if current day matches the criteria. - ************************************************************************/ - - public static boolean weekdayRange(String wd1, String wd2, String gmt) { - return SCRIPT_METHODS.weekdayRange(wd1, wd2, gmt); - } - - /************************************************************************* - * Sets a calendar with the current time. If this is set all date and time - * based methods will use this calendar to determine the current time - * instead of the real time. This is only be used by unit tests and is not - * part of the public API. - * - * @param cal - * a Calendar to set. - ************************************************************************/ - - static void setCurrentTime(Calendar cal) { - SCRIPT_METHODS.setCurrentTime(cal); - } - - /************************************************************************* - * Only the first parameter is mandatory. All other parameters can be left - * out therefore the meaning of the parameters changes. The method - * definition shows the version with the most possible parameters filled. - * The real meaning of the parameters is guessed from it's value. If "from" - * and "to" are specified then the bounds are inclusive. If the "GMT" - * parameter is specified, times are taken to be in GMT, otherwise the local - * time zone is used. - * - * @param day1 - * is the day of month between 1 and 31 (as an integer). - * @param month1 - * one of JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC - * @param year1 - * is the full year number, for example 1995 (but not 95). - * Integer. - * @param day2 - * is the day of month between 1 and 31 (as an integer). - * @param month2 - * one of JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC - * @param year2 - * is the full year number, for example 1995 (but not 95). - * Integer. - * @param gmt - * "GMT" for gmt time format else "undefined" - * @return true if the current date matches the given range. - ************************************************************************/ - - public static boolean dateRange(Object day1, Object month1, Object year1, - Object day2, Object month2, Object year2, Object gmt) { - return SCRIPT_METHODS.dateRange(day1, month1, year1, day2, month2, - year2, gmt); - } - - /************************************************************************* - * Some parameters can be left out therefore the meaning of the parameters - * changes. The method definition shows the version with the most possible - * parameters filled. The real meaning of the parameters is guessed from - * it's value. If "from" and "to" are specified then the bounds are - * inclusive. If the "GMT" parameter is specified, times are taken to be in - * GMT, otherwise the local time zone is used.
- * - *
-	 * timeRange(hour)
-	 * timeRange(hour1, hour2)
-	 * timeRange(hour1, min1, hour2, min2)
-	 * timeRange(hour1, min1, sec1, hour2, min2, sec2)
-	 * timeRange(hour1, min1, sec1, hour2, min2, sec2, gmt)
-	 * 
- * - * @param hour1 - * is the hour from 0 to 23. (0 is midnight, 23 is 11 pm.) - * @param min1 - * minutes from 0 to 59. - * @param sec1 - * seconds from 0 to 59. - * @param hour2 - * is the hour from 0 to 23. (0 is midnight, 23 is 11 pm.) - * @param min2 - * minutes from 0 to 59. - * @param sec2 - * seconds from 0 to 59. - * @param gmt - * "GMT" for gmt time format else "undefined" - * @return true if the current time matches the given range. - ************************************************************************/ - - public static boolean timeRange(Object hour1, Object min1, Object sec1, - Object hour2, Object min2, Object sec2, Object gmt) { - return SCRIPT_METHODS.timeRange(hour1, min1, sec1, hour2, min2, sec2, - gmt); - } - -} diff --git a/app/src/main/java/com/btr/proxy/selector/pac/RhinoPacScriptParser.kt b/app/src/main/java/com/btr/proxy/selector/pac/RhinoPacScriptParser.kt new file mode 100644 index 00000000..8065452c --- /dev/null +++ b/app/src/main/java/com/btr/proxy/selector/pac/RhinoPacScriptParser.kt @@ -0,0 +1,112 @@ +package com.btr.proxy.selector.pac + +import android.util.Log +import org.mozilla.javascript.Context +import org.mozilla.javascript.ContextFactory +import org.mozilla.javascript.Scriptable +import org.mozilla.javascript.ScriptableObject +import java.util.* + +class RhinoPacScriptParser(private val source: PacScriptSource) : ScriptableObject(), PacScriptParser { + + companion object { + private const val serialVersionUID = 1L + private const val TAG = "ProxyDroid.PAC" + + private val JS_FUNCTION_NAMES = arrayOf( + "shExpMatch", "dnsResolve", "isResolvable", "isInNet", "dnsDomainIs", + "isPlainHostName", "myIpAddress", "dnsDomainLevels", "localHostOrDomainIs", + "weekdayRange", "dateRange", "timeRange" + ) + + private val SCRIPT_METHODS = PacScriptMethods() + + @JvmStatic + fun isPlainHostName(host: String): Boolean = SCRIPT_METHODS.isPlainHostName(host) + + @JvmStatic + fun dnsDomainIs(host: String, domain: String): Boolean = SCRIPT_METHODS.dnsDomainIs(host, domain) + + @JvmStatic + fun localHostOrDomainIs(host: String, domain: String): Boolean = SCRIPT_METHODS.localHostOrDomainIs(host, domain) + + @JvmStatic + fun isResolvable(host: String): Boolean = SCRIPT_METHODS.isResolvable(host) + + @JvmStatic + fun isInNet(host: String, pattern: String, mask: String): Boolean = SCRIPT_METHODS.isInNet(host, pattern, mask) + + @JvmStatic + fun dnsResolve(host: String): String = SCRIPT_METHODS.dnsResolve(host) + + @JvmStatic + fun myIpAddress(): String = SCRIPT_METHODS.myIpAddress() + + @JvmStatic + fun dnsDomainLevels(host: String): Int = SCRIPT_METHODS.dnsDomainLevels(host) + + @JvmStatic + fun shExpMatch(str: String, shexp: String): Boolean = SCRIPT_METHODS.shExpMatch(str, shexp) + + @JvmStatic + fun weekdayRange(wd1: String, wd2: String?, gmt: String?): Boolean = SCRIPT_METHODS.weekdayRange(wd1, wd2, gmt) + + @JvmStatic + fun setCurrentTime(cal: Calendar?) { + SCRIPT_METHODS.setCurrentTime(cal) + } + + @JvmStatic + fun dateRange(day1: Any?, month1: Any?, year1: Any?, day2: Any?, month2: Any?, year2: Any?, gmt: Any?): Boolean { + return SCRIPT_METHODS.dateRange(day1, month1, year1, day2, month2, year2, gmt) + } + + @JvmStatic + fun timeRange(hour1: Any?, min1: Any?, sec1: Any?, hour2: Any?, min2: Any?, sec2: Any?, gmt: Any?): Boolean { + return SCRIPT_METHODS.timeRange(hour1, min1, sec1, hour2, min2, sec2, gmt) + } + } + + private var scope: Scriptable? = null + + init { + setupEngine() + } + + @Throws(ProxyEvaluationException::class) + fun setupEngine() { + val context = ContextFactory().enterContext() + try { + defineFunctionProperties(JS_FUNCTION_NAMES, RhinoPacScriptParser::class.java, ScriptableObject.DONTENUM) + } catch (e: Exception) { + Log.e(TAG, "JS Engine setup error.", e) + throw ProxyEvaluationException(e.message, e) + } + scope = context.initStandardObjects(this) + } + + override fun getScriptSource(): PacScriptSource = source + + @Throws(ProxyEvaluationException::class) + override fun evaluate(url: String, host: String): String { + try { + val script = StringBuilder(source.getScriptContent()) + val evalMethod = " ;FindProxyForURL (\"$url\",\"$host\")" + script.append(evalMethod) + + val context = Context.enter() + context.optimizationLevel = -1 + try { + val result = context.evaluateString(scope, script.toString(), "userPacFile", 1, null) + return Context.toString(result) + } finally { + Context.exit() + } + } catch (e: Exception) { + Log.e(TAG, "JS evaluation error.", e) + throw ProxyEvaluationException("Error while executing PAC script: ${e.message}", e) + } + } + + override fun getClassName(): String = javaClass.simpleName +} diff --git a/app/src/main/java/com/btr/proxy/selector/pac/ScriptMethods.java b/app/src/main/java/com/btr/proxy/selector/pac/ScriptMethods.java deleted file mode 100644 index b2bcdb40..00000000 --- a/app/src/main/java/com/btr/proxy/selector/pac/ScriptMethods.java +++ /dev/null @@ -1,265 +0,0 @@ -package com.btr.proxy.selector.pac; - -/*************************************************************************** - * Defines the public interface for PAC scripts. - * - * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009 - ***************************************************************************/ -public interface ScriptMethods { - - public boolean isPlainHostName(String host); - - /************************************************************************* - * Tests if an URL is in a given domain. - * - * @param host - * is the host name from the URL. - * @param domain - * is the domain name to test the host name against. - * @return true if the domain of host name matches. - ************************************************************************/ - - public boolean dnsDomainIs(String host, String domain); - - /************************************************************************* - * Is true if the host name matches exactly the specified host name, or if - * there is no domain name part in the host name, but the unqualified host - * name matches. - * - * @param host - * the host name from the URL. - * @param domain - * fully qualified host name with domain to match against. - * @return true if matches else false. - ************************************************************************/ - - public boolean localHostOrDomainIs(String host, String domain); - - /************************************************************************* - * Tries to resolve the host name. Returns true if succeeds. - * - * @param host - * is the host name from the URL. - * @return true if resolvable else false. - ************************************************************************/ - - public boolean isResolvable(String host); - - /************************************************************************* - * Tries to resolve the host name. Returns true if succeeds to resolve the - * host to an IPv4 or IPv6 address. - * - * @param host - * is the host name from the URL. - * @return true if resolvable else false. - ************************************************************************/ - - public boolean isResolvableEx(String host); - - /************************************************************************* - * Returns true if the IP address of the host matches the specified IP - * address pattern. Pattern and mask specification is done the same way as - * for SOCKS configuration. - * - * Example: isInNet(host, "198.95.0.0", "255.255.0.0") is true if the IP - * address of the host matches 198.95.*.*. - * - * @param host - * a DNS host name, or IP address. If a host name is passed, it - * will be resolved into an IP address by this function. - * @param pattern - * an IP address pattern in the dot-separated format. - * @param mask - * mask for the IP address pattern informing which parts of the - * IP address should be matched against. 0 means ignore, 255 - * means match. - * @return true if it matches else false. - ************************************************************************/ - - public boolean isInNet(String host, String pattern, String mask); - - /************************************************************************* - * Extension of the isInNet method to support IPv6. - * - * @param ipAddress - * an IP4 or IP6 address - * @param ipPrefix - * A string containing colon delimited IP prefix with top n bits - * specified in the bit field (i.e. 3ffe:8311:ffff::/48 or - * 123.112.0.0/16). - * @return true if the host is in the given subnet, else false. - ************************************************************************/ - - public boolean isInNetEx(String ipAddress, String ipPrefix); - - /************************************************************************* - * Resolves the given DNS host name into an IP address, and returns it in - * the dot separated format as a string. - * - * @param host - * the host to resolve. - * @return the resolved IP, empty string if not resolvable. - ************************************************************************/ - - public String dnsResolve(String host); - - /************************************************************************* - * @param host - * the host to resolve - * @return a semicolon separated list of IP6 and IP4 addresses the host name - * resolves to, empty string if not resolvable. - ************************************************************************/ - - public String dnsResolveEx(String host); - - /************************************************************************* - * Returns the IP address of the host that the process is running on, as a - * string in the dot-separated integer format. - * - * @return an IP as string. - ************************************************************************/ - - public String myIpAddress(); - - /************************************************************************* - * Returns a list of IP4 and IP6 addresses of the host that the process is - * running on. The list is separated with semicolons. - * - * @return the list, empty string if not available. - ************************************************************************/ - - public String myIpAddressEx(); - - /************************************************************************* - * Returns the number of DNS domain levels (number of dots) in the host - * name. - * - * @param host - * is the host name from the URL. - * @return number of DNS domain levels. - ************************************************************************/ - - public int dnsDomainLevels(String host); - - /************************************************************************* - * Returns true if the string matches the specified shell expression. - * Actually, currently the patterns are shell expressions, not regular - * expressions. - * - * @param str - * is any string to compare (e.g. the URL, or the host name). - * @param shexp - * is a shell expression to compare against. - * @return true if the string matches, else false. - ************************************************************************/ - - public boolean shExpMatch(String str, String shexp); - - /************************************************************************* - * Only the first parameter is mandatory. Either the second, the third, or - * both may be left out. If only one parameter is present, the function - * yields a true value on the weekday that the parameter represents. If the - * string "GMT" is specified as a second parameter, times are taken to be in - * GMT, otherwise in local time zone. If both wd1 and wd2 are defined, the - * condition is true if the current weekday is in between those two - * weekdays. Bounds are inclusive. If the "GMT" parameter is specified, - * times are taken to be in GMT, otherwise the local time zone is used. - * - * @param wd1 - * weekday 1 is one of SUN MON TUE WED THU FRI SAT - * @param wd2 - * weekday 2 is one of SUN MON TUE WED THU FRI SAT - * @param gmt - * "GMT" for gmt time format else "undefined" - * @return true if current day matches the criteria. - ************************************************************************/ - - public boolean weekdayRange(String wd1, String wd2, String gmt); - - /************************************************************************* - * Only the first parameter is mandatory. All other parameters can be left - * out therefore the meaning of the parameters changes. The method - * definition shows the version with the most possible parameters filled. - * The real meaning of the parameters is guessed from it's value. If "from" - * and "to" are specified then the bounds are inclusive. If the "GMT" - * parameter is specified, times are taken to be in GMT, otherwise the local - * time zone is used. - * - * @param day1 - * is the day of month between 1 and 31 (as an integer). - * @param month1 - * one of JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC - * @param year1 - * is the full year number, for example 1995 (but not 95). - * Integer. - * @param day2 - * is the day of month between 1 and 31 (as an integer). - * @param month2 - * one of JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC - * @param year2 - * is the full year number, for example 1995 (but not 95). - * Integer. - * @param gmt - * "GMT" for gmt time format else "undefined" - * @return true if the current date matches the given range. - ************************************************************************/ - - public boolean dateRange(Object day1, Object month1, Object year1, - Object day2, Object month2, Object year2, Object gmt); - - /************************************************************************* - * Some parameters can be left out therefore the meaning of the parameters - * changes. The method definition shows the version with the most possible - * parameters filled. The real meaning of the parameters is guessed from - * it's value. If "from" and "to" are specified then the bounds are - * inclusive. If the "GMT" parameter is specified, times are taken to be in - * GMT, otherwise the local time zone is used.
- * - *
-	 * timeRange(hour)
-	 * timeRange(hour1, hour2)
-	 * timeRange(hour1, min1, hour2, min2)
-	 * timeRange(hour1, min1, sec1, hour2, min2, sec2)
-	 * timeRange(hour1, min1, sec1, hour2, min2, sec2, gmt)
-	 * 
- * - * @param hour1 - * is the hour from 0 to 23. (0 is midnight, 23 is 11 pm.) - * @param min1 - * minutes from 0 to 59. - * @param sec1 - * seconds from 0 to 59. - * @param hour2 - * is the hour from 0 to 23. (0 is midnight, 23 is 11 pm.) - * @param min2 - * minutes from 0 to 59. - * @param sec2 - * seconds from 0 to 59. - * @param gmt - * "GMT" for gmt time format else "undefined" - * @return true if the current time matches the given range. - ************************************************************************/ - - public boolean timeRange(Object hour1, Object min1, Object sec1, - Object hour2, Object min2, Object sec2, Object gmt); - - /************************************************************************* - * Sorts a list of IP4 and IP6 addresses. Separated by semicolon. Dual - * addresses first, then IPv6 and last IPv4. - * - * @param ipAddressList - * the address list. - * @return the sorted list, empty string if sort is not possible - ************************************************************************/ - - public String sortIpAddressList(String ipAddressList); - - /************************************************************************* - * Gets the version of the PAC extension that is available. - * - * @return the extension version, currently 1.0 - ************************************************************************/ - - public String getClientVersion(); - -} diff --git a/app/src/main/java/com/btr/proxy/selector/pac/ScriptMethods.kt b/app/src/main/java/com/btr/proxy/selector/pac/ScriptMethods.kt new file mode 100644 index 00000000..543eceb1 --- /dev/null +++ b/app/src/main/java/com/btr/proxy/selector/pac/ScriptMethods.kt @@ -0,0 +1,22 @@ +package com.btr.proxy.selector.pac + +interface ScriptMethods { + fun isPlainHostName(host: String): Boolean + fun dnsDomainIs(host: String, domain: String): Boolean + fun localHostOrDomainIs(host: String, domain: String): Boolean + fun isResolvable(host: String): Boolean + fun isResolvableEx(host: String): Boolean + fun isInNet(host: String, pattern: String, mask: String): Boolean + fun isInNetEx(ipAddress: String, ipPrefix: String): Boolean + fun dnsResolve(host: String): String + fun dnsResolveEx(host: String): String + fun myIpAddress(): String + fun myIpAddressEx(): String + fun dnsDomainLevels(host: String): Int + fun shExpMatch(str: String, shexp: String): Boolean + fun weekdayRange(wd1: String, wd2: String?, gmt: String?): Boolean + fun dateRange(day1: Any?, month1: Any?, year1: Any?, day2: Any?, month2: Any?, year2: Any?, gmt: Any?): Boolean + fun timeRange(hour1: Any?, min1: Any?, sec1: Any?, hour2: Any?, min2: Any?, sec2: Any?, gmt: Any?): Boolean + fun sortIpAddressList(ipAddressList: String): String + fun getClientVersion(): String +} diff --git a/app/src/main/java/com/btr/proxy/selector/pac/UrlPacScriptSource.java b/app/src/main/java/com/btr/proxy/selector/pac/UrlPacScriptSource.java deleted file mode 100644 index 1bbfeb10..00000000 --- a/app/src/main/java/com/btr/proxy/selector/pac/UrlPacScriptSource.java +++ /dev/null @@ -1,186 +0,0 @@ -package com.btr.proxy.selector.pac; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.Proxy; -import java.net.URISyntaxException; -import java.net.URL; - -import android.util.Log; - -/***************************************************************************** - * Script source that will load the content of a PAC file from an webserver. The - * script content is cached once it was downloaded. - * - * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009 - ****************************************************************************/ - -public class UrlPacScriptSource implements PacScriptSource { - - private final static String TAG = "ProxyDroid.PAC"; - - private final String scriptUrl; - private String scriptContent; - private long expireAtMillis; - - /************************************************************************* - * Constructor - * - * @param url - * the URL to download the script from. - ************************************************************************/ - - public UrlPacScriptSource(String url) { - super(); - this.expireAtMillis = 0; - this.scriptUrl = url; - } - - /************************************************************************* - * getScriptContent - * - * @see com.btr.proxy.selector.pac.PacScriptSource#getScriptContent() - ************************************************************************/ - - @Override - public synchronized String getScriptContent() throws IOException { - if (this.scriptContent == null - || (this.expireAtMillis > 0 && this.expireAtMillis > System - .currentTimeMillis())) { - try { - if (this.scriptUrl.startsWith("file:/") - || this.scriptUrl.indexOf(":/") == -1) { - this.scriptContent = readPacFileContent(this.scriptUrl); - } else { - this.scriptContent = downloadPacContent(this.scriptUrl); - } - } catch (IOException e) { - Log.e(TAG, "Loading script failed.", e); - this.scriptContent = ""; - throw e; - } - } - return this.scriptContent; - } - - /************************************************************************* - * Reads a PAC script from a local file. - * - * @param scriptUrl - * @return the content of the script file. - * @throws IOException - * @throws URISyntaxException - ************************************************************************/ - - private String readPacFileContent(String scriptUrl) throws IOException { - try { - File file = null; - if (scriptUrl.indexOf(":/") == -1) { - file = new File(scriptUrl); - } else { - file = new File(new URL(scriptUrl).toURI()); - } - BufferedReader r = new BufferedReader(new FileReader(file)); - StringBuilder result = new StringBuilder(); - try { - String line; - while ((line = r.readLine()) != null) { - result.append(line).append("\n"); - } - } finally { - r.close(); - } - return result.toString(); - } catch (Exception e) { - Log.e(TAG, "File reading error.", e); - throw new IOException(e.getMessage()); - } - } - - /************************************************************************* - * Downloads the script from a webserver. - * - * @param url - * the URL to the script file. - * @return the script content. - * @throws IOException - * on read error. - ************************************************************************/ - - private String downloadPacContent(String url) throws IOException { - if (url == null) { - throw new IOException("Invalid PAC script URL: null"); - } - - HttpURLConnection con = (HttpURLConnection) new URL(url) - .openConnection(Proxy.NO_PROXY); - con.setConnectTimeout(15 * 1000); - con.setReadTimeout(20 * 1000); - con.setInstanceFollowRedirects(true); - con.setRequestProperty("accept", - "application/x-ns-proxy-autoconfig, */*;q=0.8"); - - if (con.getResponseCode() != 200) { - throw new IOException("Server returned: " + con.getResponseCode() - + " " + con.getResponseMessage()); - } - - // Read expire date. - this.expireAtMillis = con.getExpiration(); - - String charsetName = parseCharsetFromHeader(con.getContentType()); - BufferedReader r = new BufferedReader(new InputStreamReader( - con.getInputStream(), charsetName)); - try { - StringBuilder result = new StringBuilder(); - try { - String line; - while ((line = r.readLine()) != null) { - result.append(line).append("\n"); - } - } finally { - r.close(); - con.disconnect(); - } - return result.toString(); - } finally { - r.close(); - } - } - - /************************************************************************* - * Response Content-Type could be something like this: - * application/x-ns-proxy-autoconfig; charset=UTF-8 - * - * @param contentType - * header field. - * @return the extracted charset if set else a default charset. - ************************************************************************/ - - String parseCharsetFromHeader(String contentType) { - String result = "ISO-8859-1"; - if (contentType != null) { - String[] paramList = contentType.split(";"); - for (String param : paramList) { - if (param.toLowerCase().trim().startsWith("charset") - && param.indexOf("=") != -1) { - result = param.substring(param.indexOf("=") + 1).trim(); - } - } - } - return result; - } - - /*************************************************************************** - * @see java.lang.Object#toString() - **************************************************************************/ - @Override - public String toString() { - return this.scriptUrl; - } - -} diff --git a/app/src/main/java/com/btr/proxy/selector/pac/UrlPacScriptSource.kt b/app/src/main/java/com/btr/proxy/selector/pac/UrlPacScriptSource.kt new file mode 100644 index 00000000..70d42114 --- /dev/null +++ b/app/src/main/java/com/btr/proxy/selector/pac/UrlPacScriptSource.kt @@ -0,0 +1,110 @@ +package com.btr.proxy.selector.pac + +import android.util.Log +import java.io.BufferedReader +import java.io.File +import java.io.FileReader +import java.io.IOException +import java.io.InputStreamReader +import java.net.HttpURLConnection +import java.net.URL + +class UrlPacScriptSource(private val scriptUrl: String) : PacScriptSource { + + companion object { + private const val TAG = "ProxyDroid.PAC" + } + + private var scriptContent: String? = null + private var expireAtMillis: Long = 0 + + @Synchronized + @Throws(IOException::class) + override fun getScriptContent(): String { + if (scriptContent == null || (expireAtMillis > 0 && expireAtMillis > System.currentTimeMillis())) { + try { + scriptContent = if (scriptUrl.startsWith("file:/") || !scriptUrl.contains(":/")) { + readPacFileContent(scriptUrl) + } else { + downloadPacContent(scriptUrl) + } + } catch (e: IOException) { + Log.e(TAG, "Loading script failed.", e) + scriptContent = "" + throw e + } + } + return scriptContent ?: "" + } + + @Throws(IOException::class) + private fun readPacFileContent(scriptUrl: String): String { + try { + val file = if (!scriptUrl.contains(":/")) { + File(scriptUrl) + } else { + File(URL(scriptUrl).toURI()) + } + + BufferedReader(FileReader(file)).use { r -> + val result = StringBuilder() + var line: String? + while (r.readLine().also { line = it } != null) { + result.append(line).append("\n") + } + return result.toString() + } + } catch (e: Exception) { + Log.e(TAG, "File reading error.", e) + throw IOException(e.message) + } + } + + @Throws(IOException::class) + private fun downloadPacContent(url: String?): String { + if (url == null) { + throw IOException("Invalid PAC script URL: null") + } + + val con = URL(url).openConnection(java.net.Proxy.NO_PROXY) as HttpURLConnection + con.connectTimeout = 15 * 1000 + con.readTimeout = 20 * 1000 + con.instanceFollowRedirects = true + con.setRequestProperty("accept", "application/x-ns-proxy-autoconfig, */*;q=0.8") + + if (con.responseCode != 200) { + throw IOException("Server returned: ${con.responseCode} ${con.responseMessage}") + } + + expireAtMillis = con.expiration + + val charsetName = parseCharsetFromHeader(con.contentType) + BufferedReader(InputStreamReader(con.inputStream, charsetName)).use { r -> + val result = StringBuilder() + try { + var line: String? + while (r.readLine().also { line = it } != null) { + result.append(line).append("\n") + } + } finally { + con.disconnect() + } + return result.toString() + } + } + + internal fun parseCharsetFromHeader(contentType: String?): String { + var result = "ISO-8859-1" + if (contentType != null) { + val paramList = contentType.split(";") + for (param in paramList) { + if (param.lowercase().trim().startsWith("charset") && param.contains("=")) { + result = param.substring(param.indexOf("=") + 1).trim() + } + } + } + return result + } + + override fun toString(): String = scriptUrl +} diff --git a/app/src/main/java/com/ksmaze/android/preference/ListPreferenceMultiSelect.java b/app/src/main/java/com/ksmaze/android/preference/ListPreferenceMultiSelect.java deleted file mode 100644 index 131dbb66..00000000 --- a/app/src/main/java/com/ksmaze/android/preference/ListPreferenceMultiSelect.java +++ /dev/null @@ -1,132 +0,0 @@ -/* proxydroid - Global / Individual Proxy App for Android - * Copyright (C) 2011 K's Maze - * - * This program 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. - * - * This program 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 this program. If not, see . - * - */ - -package com.ksmaze.android.preference; - -import android.app.AlertDialog.Builder; -import android.content.Context; -import android.content.DialogInterface; -import android.preference.ListPreference; -import android.preference.Preference; -import android.util.AttributeSet; - -/** - * A {@link Preference} that displays a list of entries as a dialog and allows - * multiple selections - *

- * This preference will store a string into the SharedPreferences. This string - * will be the values selected from the {@link #setEntryValues(CharSequence[])} - * array. - *

- */ -public class ListPreferenceMultiSelect extends ListPreference { - // Need to make sure the SEPARATOR is unique and weird enough that it - // doesn't match one of the entries. - // Not using any fancy symbols because this is interpreted as a regex for - // splitting strings. - private static final String SEPARATOR = " , "; - - private boolean[] mClickedDialogEntryIndices; - - public ListPreferenceMultiSelect(Context context, AttributeSet attrs) { - super(context, attrs); - - mClickedDialogEntryIndices = new boolean[getEntries().length]; - } - - @Override - public void setEntries(CharSequence[] entries) { - super.setEntries(entries); - mClickedDialogEntryIndices = new boolean[entries.length]; - } - - public ListPreferenceMultiSelect(Context context) { - this(context, null); - } - - @Override - protected void onPrepareDialogBuilder(Builder builder) { - CharSequence[] entries = getEntries(); - CharSequence[] entryValues = getEntryValues(); - - if (entries == null || entryValues == null - || entries.length != entryValues.length) { - throw new IllegalStateException( - "ListPreference requires an entries array and an entryValues array which are both the same length"); - } - - restoreCheckedEntries(); - builder.setMultiChoiceItems(entries, mClickedDialogEntryIndices, - new DialogInterface.OnMultiChoiceClickListener() { - @Override - public void onClick(DialogInterface dialog, int which, - boolean val) { - mClickedDialogEntryIndices[which] = val; - } - }); - } - - public static String[] parseStoredValue(CharSequence val) { - if (val == null) - return null; - if ("".equals(val)) - return null; - else - return ((String) val).split(SEPARATOR); - } - - private void restoreCheckedEntries() { - CharSequence[] entryValues = getEntryValues(); - - String[] vals = parseStoredValue(getValue()); - if (vals != null) { - for (String val1 : vals) { - String val = val1.trim(); - for (int i = 0; i < entryValues.length; i++) { - CharSequence entry = entryValues[i]; - if (entry.equals(val)) { - mClickedDialogEntryIndices[i] = true; - break; - } - } - } - } - } - - @Override - protected void onDialogClosed(boolean positiveResult) { - // super.onDialogClosed(positiveResult); - - CharSequence[] entryValues = getEntryValues(); - if (positiveResult && entryValues != null) { - StringBuffer value = new StringBuffer(); - for (int i = 0; i < entryValues.length; i++) { - if (mClickedDialogEntryIndices[i]) { - value.append(entryValues[i]).append(SEPARATOR); - } - } - - if (callChangeListener(value)) { - String val = value.toString(); - if (val.length() > 0) - val = val.substring(0, val.length() - SEPARATOR.length()); - setValue(val); - } - } - } -} diff --git a/app/src/main/java/com/ksmaze/android/preference/ListPreferenceMultiSelect.kt b/app/src/main/java/com/ksmaze/android/preference/ListPreferenceMultiSelect.kt new file mode 100644 index 00000000..11900e77 --- /dev/null +++ b/app/src/main/java/com/ksmaze/android/preference/ListPreferenceMultiSelect.kt @@ -0,0 +1,98 @@ +/* proxydroid - Global / Individual Proxy App for Android + * Copyright (C) 2011 K's Maze + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package com.ksmaze.android.preference + +import android.app.AlertDialog +import android.content.Context +import android.content.DialogInterface +import android.preference.ListPreference +import android.util.AttributeSet + +class ListPreferenceMultiSelect @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : ListPreference(context, attrs) { + + companion object { + private const val SEPARATOR = " , " + + @JvmStatic + fun parseStoredValue(value: CharSequence?): Array? { + if (value == null || value.toString().isEmpty()) { + return null + } + return value.toString().split(SEPARATOR).toTypedArray() + } + } + + private var mClickedDialogEntryIndices: BooleanArray = BooleanArray(entries?.size ?: 0) + + override fun setEntries(entries: Array?) { + super.setEntries(entries) + mClickedDialogEntryIndices = BooleanArray(entries?.size ?: 0) + } + + override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) { + val entries = entries + val entryValues = entryValues + + require(!(entries == null || entryValues == null || entries.size != entryValues.size)) { + "ListPreference requires an entries array and an entryValues array which are both the same length" + } + + restoreCheckedEntries() + builder.setMultiChoiceItems(entries, mClickedDialogEntryIndices) { _, which, isChecked -> + mClickedDialogEntryIndices[which] = isChecked + } + } + + private fun restoreCheckedEntries() { + val entryValues = entryValues ?: return + val vals = parseStoredValue(value) ?: return + + for (v in vals) { + val trimmedVal = v.trim() + for (i in entryValues.indices) { + if (entryValues[i] == trimmedVal) { + mClickedDialogEntryIndices[i] = true + break + } + } + } + } + + override fun onDialogClosed(positiveResult: Boolean) { + val entryValues = entryValues + if (positiveResult && entryValues != null) { + val valueBuilder = StringBuilder() + for (i in entryValues.indices) { + if (mClickedDialogEntryIndices[i]) { + valueBuilder.append(entryValues[i]).append(SEPARATOR) + } + } + + if (callChangeListener(valueBuilder)) { + var finalValue = valueBuilder.toString() + if (finalValue.isNotEmpty()) { + finalValue = finalValue.substring(0, finalValue.length - SEPARATOR.length) + } + value = finalValue + } + } + } +} diff --git a/app/src/main/java/org/proxydroid/AppManager.java b/app/src/main/java/org/proxydroid/AppManager.java deleted file mode 100644 index 485632da..00000000 --- a/app/src/main/java/org/proxydroid/AppManager.java +++ /dev/null @@ -1,463 +0,0 @@ -/* Copyright (c) 2009, Nathan Freitas, Orbot / The Guardian Project - http://openideals.com/guardian */ -/* See LICENSE for licensing information */ - -package org.proxydroid; - -import android.app.Activity; -import android.app.ProgressDialog; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.graphics.PixelFormat; -import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.preference.PreferenceManager; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.view.ViewGroup.LayoutParams; -import android.view.WindowManager; -import android.widget.AbsListView; -import android.widget.AbsListView.OnScrollListener; -import android.widget.ArrayAdapter; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.CompoundButton.OnCheckedChangeListener; -import android.widget.ImageView; -import android.widget.ListAdapter; -import android.widget.ListView; -import android.widget.TextView; - -import androidx.appcompat.app.AppCompatActivity; - -import org.proxydroid.utils.ImageLoader; -import org.proxydroid.utils.ImageLoaderFactory; - -import java.util.Arrays; -import java.util.Comparator; -import java.util.Iterator; -import java.util.List; -import java.util.StringTokenizer; -import java.util.Vector; - -public class AppManager extends AppCompatActivity implements OnCheckedChangeListener, - OnClickListener { - - private ProxyedApp[] apps = null; - - private ListView listApps; - - private AppManager mAppManager; - - private TextView overlay; - - private ProgressDialog pd = null; - private ListAdapter adapter; - - private ImageLoader dm; - - private static final int MSG_LOAD_START = 1; - private static final int MSG_LOAD_FINISH = 2; - - public final static String PREFS_KEY_PROXYED = "Proxyed"; - - private boolean appsLoaded = false; - - final Handler handler = new Handler() { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_LOAD_START: - pd = ProgressDialog.show(AppManager.this, "", - getString(R.string.loading), true, true); - break; - case MSG_LOAD_FINISH: - - listApps.setAdapter(adapter); - - listApps.setOnScrollListener(new OnScrollListener() { - - boolean visible; - - @Override - public void onScrollStateChanged(AbsListView view, - int scrollState) { - visible = true; - if (scrollState == ListView.OnScrollListener.SCROLL_STATE_IDLE) { - overlay.setVisibility(View.INVISIBLE); - } - } - - @Override - public void onScroll(AbsListView view, - int firstVisibleItem, int visibleItemCount, - int totalItemCount) { - if (visible) { - String name = apps[firstVisibleItem].getName(); - if (name != null && name.length() > 1) - overlay.setText(apps[firstVisibleItem] - .getName().substring(0, 1)); - else - overlay.setText("*"); - overlay.setVisibility(View.VISIBLE); - } - } - }); - - if (pd != null) { - pd.dismiss(); - pd = null; - } - break; - } - super.handleMessage(msg); - } - }; - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - // app icon in action bar clicked; go home -// Intent intent = new Intent(this, ProxyDroid.class); -// intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); -// startActivity(intent); - finish(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - this.setContentView(R.layout.layout_apps); - - dm = ImageLoaderFactory.getImageLoader(this); - - this.overlay = (TextView) View.inflate(this, R.layout.overlay, null); - getWindowManager() - .addView( - overlay, - new WindowManager.LayoutParams( - LayoutParams.WRAP_CONTENT, - LayoutParams.WRAP_CONTENT, - WindowManager.LayoutParams.TYPE_APPLICATION, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, - PixelFormat.TRANSLUCENT)); - - mAppManager = this; - - } - - /** Called when the activity is closed. */ - @Override - public void onDestroy() { - - getWindowManager().removeView(overlay); - - super.onDestroy(); - } - - @Override - protected void onResume() { - super.onResume(); - - new Thread() { - - @Override - public void run() { - handler.sendEmptyMessage(MSG_LOAD_START); - - listApps = (ListView) findViewById(R.id.applistview); - - if (!appsLoaded) - loadApps(); - handler.sendEmptyMessage(MSG_LOAD_FINISH); - } - }.start(); - - } - - private void loadApps() { - getApps(this); - - Arrays.sort(apps, new Comparator() { - @Override - public int compare(ProxyedApp o1, ProxyedApp o2) { - if (o1 == null || o2 == null || o1.getName() == null - || o2.getName() == null) - return 1; - if (o1.isProxyed() == o2.isProxyed()) - return o1.getName().compareTo(o2.getName()); - if (o1.isProxyed()) - return -1; - return 1; - } - }); - - final LayoutInflater inflater = getLayoutInflater(); - - adapter = new ArrayAdapter(this, R.layout.layout_apps_item, - R.id.itemtext, apps) { - @Override - public View getView(int position, View convertView, ViewGroup parent) { - ListEntry entry; - if (convertView == null) { - // Inflate a new view - convertView = inflater.inflate(R.layout.layout_apps_item, - parent, false); - entry = new ListEntry(); - entry.icon = (ImageView) convertView - .findViewById(R.id.itemicon); - entry.box = (CheckBox) convertView - .findViewById(R.id.itemcheck); - entry.text = (TextView) convertView - .findViewById(R.id.itemtext); - - entry.text.setOnClickListener(mAppManager); - - convertView.setTag(entry); - - entry.box.setOnCheckedChangeListener(mAppManager); - } else { - // Convert an existing view - entry = (ListEntry) convertView.getTag(); - } - - final ProxyedApp app = apps[position]; - - entry.icon.setTag(app.getUid()); - - dm.DisplayImage(app.getUid(), - (Activity) convertView.getContext(), entry.icon); - - entry.text.setText(app.getName()); - - final CheckBox box = entry.box; - box.setTag(app); - box.setChecked(app.isProxyed()); - - entry.text.setTag(box); - - return convertView; - } - }; - - appsLoaded = true; - - } - - private static class ListEntry { - private CheckBox box; - private TextView text; - private ImageView icon; - } - - /* - * (non-Javadoc) - * - * @see android.app.Activity#onStop() - */ - @Override - protected void onStop() { - super.onStop(); - - // Log.d(getClass().getName(),"Exiting Preferences"); - } - - public static ProxyedApp[] getProxyedApps(Context context, boolean self) { - - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(context); - - String tordAppString = prefs.getString(PREFS_KEY_PROXYED, ""); - String[] tordApps; - - StringTokenizer st = new StringTokenizer(tordAppString, "|"); - tordApps = new String[st.countTokens()]; - int tordIdx = 0; - while (st.hasMoreTokens()) { - tordApps[tordIdx++] = st.nextToken(); - } - - Arrays.sort(tordApps); - - // else load the apps up - PackageManager pMgr = context.getPackageManager(); - - List lAppInfo = pMgr.getInstalledApplications(0); - - Iterator itAppInfo = lAppInfo.iterator(); - - Vector vectorApps = new Vector(); - - ApplicationInfo aInfo = null; - - int appIdx = 0; - - while (itAppInfo.hasNext()) { - aInfo = itAppInfo.next(); - - // ignore all system apps - if (aInfo.uid < 10000) - continue; - - ProxyedApp app = new ProxyedApp(); - - app.setUid(aInfo.uid); - - app.setUsername(pMgr.getNameForUid(app.getUid())); - - // check if this application is allowed - if (aInfo.packageName != null - && aInfo.packageName.equals("org.proxydroid")) { - if (self) - app.setProxyed(true); - } else if (Arrays.binarySearch(tordApps, app.getUsername()) >= 0) { - app.setProxyed(true); - } else { - app.setProxyed(false); - } - - if (app.isProxyed()) - vectorApps.add(app); - - } - - ProxyedApp[] apps = new ProxyedApp[vectorApps.size()]; - vectorApps.toArray(apps); - return apps; - } - - public void getApps(Context context) { - - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(context); - - String tordAppString = prefs.getString(PREFS_KEY_PROXYED, ""); - String[] tordApps; - - StringTokenizer st = new StringTokenizer(tordAppString, "|"); - tordApps = new String[st.countTokens()]; - int tordIdx = 0; - while (st.hasMoreTokens()) { - tordApps[tordIdx++] = st.nextToken(); - } - - Arrays.sort(tordApps); - - Vector vectorApps = new Vector(); - - // else load the apps up - PackageManager pMgr = context.getPackageManager(); - - List lAppInfo = pMgr.getInstalledApplications(0); - - Iterator itAppInfo = lAppInfo.iterator(); - - ApplicationInfo aInfo = null; - - while (itAppInfo.hasNext()) { - aInfo = itAppInfo.next(); - - // ignore system apps - if (aInfo.uid < 10000) - continue; - - if (aInfo.processName == null) - continue; - if (pMgr.getApplicationLabel(aInfo) == null - || pMgr.getApplicationLabel(aInfo).toString().equals("")) - continue; - if (pMgr.getApplicationIcon(aInfo) == null) - continue; - - ProxyedApp tApp = new ProxyedApp(); - - tApp.setEnabled(aInfo.enabled); - tApp.setUid(aInfo.uid); - tApp.setUsername(pMgr.getNameForUid(tApp.getUid())); - tApp.setProcname(aInfo.processName); - tApp.setName(pMgr.getApplicationLabel(aInfo).toString()); - - // check if this application is allowed - if (Arrays.binarySearch(tordApps, tApp.getUsername()) >= 0) { - tApp.setProxyed(true); - } else { - tApp.setProxyed(false); - } - - vectorApps.add(tApp); - } - - apps = new ProxyedApp[vectorApps.size()]; - vectorApps.toArray(apps); - - } - - public void saveAppSettings(Context context) { - if (apps == null) - return; - - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(this); - - // final SharedPreferences prefs = - // context.getSharedPreferences(PREFS_KEY, 0); - - StringBuilder tordApps = new StringBuilder(); - - for (int i = 0; i < apps.length; i++) { - if (apps[i].isProxyed()) { - tordApps.append(apps[i].getUsername()); - tordApps.append("|"); - } - } - - Editor edit = prefs.edit(); - edit.putString(PREFS_KEY_PROXYED, tordApps.toString()); - edit.commit(); - - } - - /** - * Called an application is check/unchecked - */ - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - final ProxyedApp app = (ProxyedApp) buttonView.getTag(); - if (app != null) { - app.setProxyed(isChecked); - } - - saveAppSettings(this); - - } - - @Override - public void onClick(View v) { - - CheckBox cbox = (CheckBox) v.getTag(); - - final ProxyedApp app = (ProxyedApp) cbox.getTag(); - if (app != null) { - app.setProxyed(!app.isProxyed()); - cbox.setChecked(app.isProxyed()); - } - - saveAppSettings(this); - - } - -} diff --git a/app/src/main/java/org/proxydroid/AppManager.kt b/app/src/main/java/org/proxydroid/AppManager.kt new file mode 100644 index 00000000..b9506e49 --- /dev/null +++ b/app/src/main/java/org/proxydroid/AppManager.kt @@ -0,0 +1,278 @@ +/* Copyright (c) 2009, Nathan Freitas, Orbot / The Guardian Project - http://openideals.com/guardian */ +/* See LICENSE for licensing information */ + +package org.proxydroid + +import android.app.Activity +import android.app.ProgressDialog +import android.content.Context +import android.graphics.PixelFormat +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.os.Message +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.widget.* +import androidx.appcompat.app.AppCompatActivity +import android.preference.PreferenceManager +import org.proxydroid.utils.ImageLoader +import org.proxydroid.utils.ImageLoaderFactory +import java.util.* + +class AppManager : AppCompatActivity(), CompoundButton.OnCheckedChangeListener, View.OnClickListener { + + private var apps: Array? = null + private lateinit var listApps: ListView + private lateinit var overlay: TextView + private var pd: ProgressDialog? = null + private var adapter: ListAdapter? = null + private lateinit var dm: ImageLoader + private var appsLoaded = false + + companion object { + private const val MSG_LOAD_START = 1 + private const val MSG_LOAD_FINISH = 2 + const val PREFS_KEY_PROXYED = "Proxyed" + + @JvmStatic + fun getProxyedApps(context: Context, self: Boolean): Array { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + val tordAppString = prefs.getString(PREFS_KEY_PROXYED, "") ?: "" + val st = StringTokenizer(tordAppString, "|") + val tordApps = Array(st.countTokens()) { st.nextToken() } + Arrays.sort(tordApps) + + val pMgr = context.packageManager + val lAppInfo = pMgr.getInstalledApplications(0) + val vectorApps = Vector() + + for (aInfo in lAppInfo) { + if (aInfo.uid < 10000) continue + + val app = ProxyedApp().apply { + uid = aInfo.uid + username = pMgr.getNameForUid(uid) + isProxyed = when { + aInfo.packageName == "org.proxydroid" -> self + username != null && Arrays.binarySearch(tordApps, username) >= 0 -> true + else -> false + } + } + + if (app.isProxyed) { + vectorApps.add(app) + } + } + + return vectorApps.toTypedArray() + } + } + + private val handler = object : Handler(Looper.getMainLooper()) { + override fun handleMessage(msg: Message) { + when (msg.what) { + MSG_LOAD_START -> { + pd = ProgressDialog.show(this@AppManager, "", getString(R.string.loading), true, true) + } + MSG_LOAD_FINISH -> { + listApps.adapter = adapter + listApps.setOnScrollListener(object : AbsListView.OnScrollListener { + var visible = false + + override fun onScrollStateChanged(view: AbsListView, scrollState: Int) { + visible = true + if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) { + overlay.visibility = View.INVISIBLE + } + } + + override fun onScroll(view: AbsListView, firstVisibleItem: Int, visibleItemCount: Int, totalItemCount: Int) { + if (visible && apps != null && firstVisibleItem < apps!!.size) { + val name = apps!![firstVisibleItem].name + overlay.text = if (name != null && name.length > 1) name.substring(0, 1) else "*" + overlay.visibility = View.VISIBLE + } + } + }) + + pd?.dismiss() + pd = null + } + } + super.handleMessage(msg) + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + finish() + true + } + else -> super.onOptionsItemSelected(item) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + setContentView(R.layout.layout_apps) + + dm = ImageLoaderFactory.getImageLoader(this) + + overlay = View.inflate(this, R.layout.overlay, null) as TextView + windowManager.addView( + overlay, + WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.TYPE_APPLICATION, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, + PixelFormat.TRANSLUCENT + ) + ) + } + + override fun onDestroy() { + windowManager.removeView(overlay) + super.onDestroy() + } + + override fun onResume() { + super.onResume() + + Thread { + handler.sendEmptyMessage(MSG_LOAD_START) + listApps = findViewById(R.id.applistview) + if (!appsLoaded) loadApps() + handler.sendEmptyMessage(MSG_LOAD_FINISH) + }.start() + } + + private fun loadApps() { + getApps(this) + + apps?.sortWith { o1, o2 -> + when { + o1 == null || o2 == null || o1.name == null || o2.name == null -> 1 + o1.isProxyed == o2.isProxyed -> o1.name!!.compareTo(o2.name!!) + o1.isProxyed -> -1 + else -> 1 + } + } + + val inflater = layoutInflater + + adapter = object : ArrayAdapter(this, R.layout.layout_apps_item, R.id.itemtext, apps!!) { + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val entry: ListEntry + val view: View + + if (convertView == null) { + view = inflater.inflate(R.layout.layout_apps_item, parent, false) + entry = ListEntry( + view.findViewById(R.id.itemicon), + view.findViewById(R.id.itemcheck), + view.findViewById(R.id.itemtext) + ) + entry.text.setOnClickListener(this@AppManager) + view.tag = entry + entry.box.setOnCheckedChangeListener(this@AppManager) + } else { + view = convertView + entry = view.tag as ListEntry + } + + val app = apps!![position] + entry.icon.tag = app.uid + dm.displayImage(app.uid, view.context as Activity, entry.icon) + entry.text.text = app.name + entry.box.tag = app + entry.box.isChecked = app.isProxyed + entry.text.tag = entry.box + + return view + } + } + + appsLoaded = true + } + + private data class ListEntry( + val icon: ImageView, + val box: CheckBox, + val text: TextView + ) + + override fun onStop() { + super.onStop() + } + + private fun getApps(context: Context) { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + val tordAppString = prefs.getString(PREFS_KEY_PROXYED, "") ?: "" + val st = StringTokenizer(tordAppString, "|") + val tordApps = Array(st.countTokens()) { st.nextToken() } + Arrays.sort(tordApps) + + val vectorApps = Vector() + val pMgr = context.packageManager + val lAppInfo = pMgr.getInstalledApplications(0) + + for (aInfo in lAppInfo) { + if (aInfo.uid < 10000) continue + if (aInfo.processName == null) continue + val label = pMgr.getApplicationLabel(aInfo) + if (label == null || label.toString().isEmpty()) continue + if (pMgr.getApplicationIcon(aInfo) == null) continue + + val tApp = ProxyedApp().apply { + isEnabled = aInfo.enabled + uid = aInfo.uid + username = pMgr.getNameForUid(uid) + procname = aInfo.processName + name = label.toString() + isProxyed = username != null && Arrays.binarySearch(tordApps, username) >= 0 + } + vectorApps.add(tApp) + } + + apps = vectorApps.toTypedArray() + } + + fun saveAppSettings(context: Context) { + val currentApps = apps ?: return + val prefs = PreferenceManager.getDefaultSharedPreferences(this) + + val tordApps = StringBuilder() + for (app in currentApps) { + if (app.isProxyed) { + tordApps.append(app.username) + tordApps.append("|") + } + } + + prefs.edit().putString(PREFS_KEY_PROXYED, tordApps.toString()).apply() + } + + override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) { + val app = buttonView.tag as? ProxyedApp + app?.isProxyed = isChecked + saveAppSettings(this) + } + + override fun onClick(v: View) { + val cbox = v.tag as CheckBox + val app = cbox.tag as? ProxyedApp + app?.let { + it.isProxyed = !it.isProxyed + cbox.isChecked = it.isProxyed + } + saveAppSettings(this) + } + +} diff --git a/app/src/main/java/org/proxydroid/BypassListActivity.java b/app/src/main/java/org/proxydroid/BypassListActivity.java deleted file mode 100644 index 56f1ca59..00000000 --- a/app/src/main/java/org/proxydroid/BypassListActivity.java +++ /dev/null @@ -1,533 +0,0 @@ -/* proxydroid - Global / Individual Proxy App for Android - * Copyright (C) 2011 Max Lv - * - * This program 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. - * - * This program 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 this program. If not, see . - * - * - * ___====-_ _-====___ - * _--^^^#####// \\#####^^^--_ - * _-^##########// ( ) \\##########^-_ - * -############// |\^^/| \\############- - * _/############// (@::@) \\############\_ - * /#############(( \\// ))#############\ - * -###############\\ (oo) //###############- - * -#################\\ / VV \ //#################- - * -###################\\/ \//###################- - * _#/|##########/\######( /\ )######/\##########|\#_ - * |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \| - * ` |/ V V ` V \#\| | | |/#/ V ' V V \| ' - * ` ` ` ` / | | | | \ ' ' ' ' - * ( | | | | ) - * __\ | | | | /__ - * (vvv(VVV)(VVV)vvv) - * - * HERE BE DRAGONS - * - */ - -package org.proxydroid; - -import android.app.AlertDialog; -import android.app.ProgressDialog; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.preference.PreferenceManager; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.AdapterView.OnItemLongClickListener; -import android.widget.ArrayAdapter; -import android.widget.EditText; -import android.widget.ListAdapter; -import android.widget.ListView; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.appcompat.app.AppCompatActivity; - -import org.proxydroid.utils.Constraints; -import org.proxydroid.utils.Utils; - -import java.io.BufferedOutputStream; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.ArrayList; - -public class BypassListActivity extends AppCompatActivity implements - OnClickListener, OnItemClickListener, OnItemLongClickListener { - - private static final String TAG = BypassListActivity.class.getName(); - - private static final int MSG_ERR_ADDR = 0; - private static final int MSG_ADD_ADDR = 1; - private static final int MSG_EDIT_ADDR = 2; - private static final int MSG_DEL_ADDR = 3; - private static final int MSG_PRESET_ADDR = 4; - private static final int MSG_IMPORT_ADDR = 5; - private static final int MSG_EXPORT_ADDR = 6; - - private ListAdapter adapter; - private ArrayList bypassList; - private Profile profile = new Profile(); - - final Handler handler = new Handler() { - @Override - public void handleMessage(Message msg) { - String addr; - switch (msg.what) { - case MSG_ERR_ADDR: - Toast.makeText(BypassListActivity.this, R.string.err_addr, - Toast.LENGTH_LONG).show(); - break; - case MSG_ADD_ADDR: - if (msg.obj == null) - return; - addr = (String) msg.obj; - bypassList.add(addr); - break; - case MSG_EDIT_ADDR: - if (msg.obj == null) - return; - addr = (String) msg.obj; - bypassList.set(msg.arg1, addr); - break; - case MSG_DEL_ADDR: - bypassList.remove(msg.arg1); - break; - case MSG_PRESET_ADDR: - String[] list = Constraints.PRESETS[msg.arg1]; - reset(list); - return; - case MSG_EXPORT_ADDR: - if (msg.obj == null) - return; - Toast.makeText(BypassListActivity.this, - getString(R.string.exporting) + " " + (String) msg.obj, - Toast.LENGTH_LONG).show(); - return; - } - refreshList(); - super.handleMessage(msg); - } - }; - - @Override - public void onClick(View arg0) { - int id = arg0.getId(); - if (id == R.id.addBypassAddr) { - editAddr(MSG_ADD_ADDR, -1); - } else if (id == R.id.presetBypassAddr) { - presetAddr(); - } else if (id == R.id.importBypassAddr) { - importAddr(); - } else if (id == R.id.exportBypassAddr) { - exportAddr(); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - // app icon in action bar clicked; go home -// Intent intent = new Intent(this, ProxyDroid.class); -// intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); -// startActivity(intent); - finish(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - - super.onCreate(savedInstanceState); - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - setContentView(R.layout.bypass_list); - TextView addButton = (TextView) findViewById(R.id.addBypassAddr); - addButton.setOnClickListener(this); - - TextView presetButton = (TextView) findViewById(R.id.presetBypassAddr); - presetButton.setOnClickListener(this); - - TextView importButton = (TextView) findViewById(R.id.importBypassAddr); - importButton.setOnClickListener(this); - - TextView exportButton = (TextView) findViewById(R.id.exportBypassAddr); - exportButton.setOnClickListener(this); - - refreshList(); - } - - @Override - public void onItemClick(AdapterView parent, View view, int position, - long id) { - editAddr(MSG_EDIT_ADDR, position); - } - - @Override - public boolean onItemLongClick(AdapterView parent, View view, - int position, long id) { - delAddr(position); - return true; - } - - private void presetAddr() { - AlertDialog ad = new AlertDialog.Builder(this) - .setTitle(R.string.preset_button) - .setNegativeButton(R.string.alert_dialog_cancel, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, - int whichButton) { - /* User clicked Cancel so do some stuff */ - } - }) - .setSingleChoiceItems(R.array.presets_list, -1, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, - int which) { - if (which >= 0 - && which < Constraints.PRESETS.length) { - Message msg = new Message(); - msg.what = MSG_PRESET_ADDR; - msg.arg1 = which; - handler.sendMessage(msg); - } - dialog.dismiss(); - } - }).create(); - ad.show(); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == Constraints.IMPORT_REQUEST) { - if (resultCode == RESULT_OK) { - if (data == null) - return; - final String path = data.getStringExtra(Constraints.FILE_PATH); - if (path == null || path.equals("")) - return; - - final ProgressDialog pd = ProgressDialog.show(this, "", - getString(R.string.importing), true, true); - - final Handler h = new Handler() { - @Override - public void handleMessage(Message msg) { - refreshList(); - if (pd != null) { - pd.dismiss(); - } - } - }; - - new Thread() { - @Override - public void run() { - FileInputStream input; - try { - input = new FileInputStream(path); - BufferedReader br = new BufferedReader( - new InputStreamReader(input)); - bypassList.clear(); - while (true) { - String line = br.readLine(); - if (line == null) - break; - String addr = Profile.validateAddr(line); - if (addr != null) - bypassList.add(addr); - } - br.close(); - input.close(); - } catch (FileNotFoundException e) { - Log.e(TAG, "error to open file", e); - } catch (IOException e) { - Log.e(TAG, "error to read file", e); - } - h.sendEmptyMessage(MSG_IMPORT_ADDR); - } - }.start(); - } - } - } - - private void importAddr() { - startActivityForResult(new Intent(this, FileChooser.class), - Constraints.IMPORT_REQUEST); - } - - private void exportAddr() { - if (profile == null) - return; - - LayoutInflater factory = LayoutInflater.from(this); - final View textEntryView = factory.inflate( - R.layout.alert_dialog_text_entry, null); - final EditText path = (EditText) textEntryView - .findViewById(R.id.text_edit); - - path.setText(Utils.getDataPath(this) + "/" + profile.getHost() + ".opt"); - - AlertDialog ad = new AlertDialog.Builder(this) - .setTitle(R.string.export_button) - .setView(textEntryView) - .setPositiveButton(R.string.alert_dialog_ok, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, - int whichButton) { - if (path.getText() == null - || path.getText().toString() == null) - dialog.dismiss(); - new Thread() { - @Override - public void run() { - - FileOutputStream output; - try { - File file = new File(path.getText() - .toString()); - if (!file.exists()) - file.createNewFile(); - - output = new FileOutputStream(file); - BufferedOutputStream bw = new BufferedOutputStream( - output); - for (String addr : bypassList) { - addr = Profile - .validateAddr(addr); - if (addr != null) - bw.write((addr + "\n") - .getBytes()); - } - - bw.flush(); - bw.close(); - output.flush(); - output.close(); - } catch (FileNotFoundException e) { - Log.e(TAG, "error to open file", e); - } catch (IOException e) { - Log.e(TAG, "error to write file", e); - } - - Message msg = new Message(); - msg.what = MSG_EXPORT_ADDR; - msg.obj = path.getText().toString(); - handler.sendMessage(msg); - } - }.start(); - - } - }) - .setNegativeButton(R.string.alert_dialog_cancel, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, - int whichButton) { - /* User clicked cancel so do some stuff */ - } - }).create(); - ad.show(); - } - - private void delAddr(final int idx) { - - final String addr = bypassList.get(idx); - - AlertDialog ad = new AlertDialog.Builder(this) - .setTitle(addr) - .setMessage(R.string.bypass_del_text) - .setPositiveButton(R.string.alert_dialog_ok, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, - int whichButton) { - /* User clicked OK so do some stuff */ - Message msg = new Message(); - msg.what = MSG_DEL_ADDR; - msg.arg1 = idx; - msg.obj = addr; - handler.sendMessage(msg); - } - }) - .setNegativeButton(R.string.alert_dialog_cancel, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, - int whichButton) { - /* User clicked Cancel so do some stuff */ - } - }).create(); - - ad.show(); - } - - private void editAddr(final int msg, final int idx) { - LayoutInflater factory = LayoutInflater.from(this); - final View textEntryView = factory.inflate( - R.layout.alert_dialog_text_entry, null); - final EditText addrText = (EditText) textEntryView - .findViewById(R.id.text_edit); - - if (msg == MSG_EDIT_ADDR) - addrText.setText(bypassList.get(idx)); - else if (msg == MSG_ADD_ADDR) - addrText.setText("0.0.0.0/0"); - - AlertDialog ad = new AlertDialog.Builder(this) - .setTitle(R.string.bypass_edit_title) - .setView(textEntryView) - .setPositiveButton(R.string.alert_dialog_ok, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, - int whichButton) { - /* User clicked OK so do some stuff */ - - new Thread() { - @Override - public void run() { - EditText addrText = (EditText) textEntryView - .findViewById(R.id.text_edit); - String addr = addrText.getText() - .toString(); - addr = Profile.validateAddr(addr); - if (addr != null) { - Message m = new Message(); - m.what = msg; - m.arg1 = idx; - m.obj = addr; - handler.sendMessage(m); - } else { - handler.sendEmptyMessage(MSG_ERR_ADDR); - } - } - }.start(); - - } - }) - .setNegativeButton(R.string.alert_dialog_cancel, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, - int whichButton) { - - /* User clicked cancel so do some stuff */ - } - }).create(); - ad.show(); - } - - private void reset(final String[] list) { - - final ProgressDialog pd = ProgressDialog.show(this, "", - getString(R.string.reseting), true, true); - - final Handler h = new Handler() { - @Override - public void handleMessage(Message msg) { - refreshList(); - if (pd != null) { - pd.dismiss(); - } - } - }; - - new Thread() { - @Override - public void run() { - bypassList.clear(); - for (String addr : list) { - addr = Profile.validateAddr(addr); - if (addr != null) - bypassList.add(addr); - } - h.sendEmptyMessage(0); - } - }.start(); - } - - private void refreshList() { - - SharedPreferences settings = PreferenceManager - .getDefaultSharedPreferences(this); - - profile.getProfile(settings); - - if (bypassList != null) { - profile.setBypassAddrs(Profile.encodeAddrs(bypassList - .toArray(new String[bypassList.size()]))); - profile.setProfile(settings); - } - - String[] addrs = Profile.decodeAddrs(profile.getBypassAddrs()); - bypassList = new ArrayList(); - - for (String addr : addrs) { - bypassList.add(addr); - // Log.d(TAG, addr); - } - - final LayoutInflater inflater = getLayoutInflater(); - - adapter = new ArrayAdapter(this, R.layout.bypass_list_item, - R.id.bypasslistItemText, bypassList) { - @Override - public View getView(int position, View convertView, ViewGroup parent) { - String addr; - if (convertView == null) { - // Inflate a new view - convertView = inflater.inflate(R.layout.bypass_list_item, - parent, false); - } - - TextView item = (TextView) convertView - .findViewById(R.id.bypasslistItemText); - addr = bypassList.get(position); - if (addr != null) - item.setText(addr); - - return convertView; - } - }; - - ListView list = (ListView) findViewById(R.id.BypassListView); - list.setAdapter(adapter); - list.setOnItemClickListener(this); - list.setOnItemLongClickListener(this); - } -} diff --git a/app/src/main/java/org/proxydroid/BypassListActivity.kt b/app/src/main/java/org/proxydroid/BypassListActivity.kt new file mode 100644 index 00000000..38e44b6b --- /dev/null +++ b/app/src/main/java/org/proxydroid/BypassListActivity.kt @@ -0,0 +1,337 @@ +/* proxydroid - Global / Individual Proxy App for Android + * Copyright (C) 2011 Max Lv + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package org.proxydroid + +import android.app.AlertDialog +import android.app.ProgressDialog +import android.content.Intent +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.os.Message +import android.util.Log +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.* +import androidx.appcompat.app.AppCompatActivity +import android.preference.PreferenceManager +import org.proxydroid.utils.Constraints +import org.proxydroid.utils.Utils +import java.io.* + +class BypassListActivity : AppCompatActivity(), View.OnClickListener, + AdapterView.OnItemClickListener, AdapterView.OnItemLongClickListener { + + companion object { + private val TAG = BypassListActivity::class.java.name + private const val MSG_ERR_ADDR = 0 + private const val MSG_ADD_ADDR = 1 + private const val MSG_EDIT_ADDR = 2 + private const val MSG_DEL_ADDR = 3 + private const val MSG_PRESET_ADDR = 4 + private const val MSG_IMPORT_ADDR = 5 + private const val MSG_EXPORT_ADDR = 6 + } + + private var adapter: ListAdapter? = null + private var bypassList: ArrayList = ArrayList() + private val profile = Profile() + + private val handler = object : Handler(Looper.getMainLooper()) { + override fun handleMessage(msg: Message) { + when (msg.what) { + MSG_ERR_ADDR -> { + Toast.makeText(this@BypassListActivity, R.string.err_addr, Toast.LENGTH_LONG).show() + } + MSG_ADD_ADDR -> { + val addr = msg.obj as? String ?: return + bypassList.add(addr) + } + MSG_EDIT_ADDR -> { + val addr = msg.obj as? String ?: return + bypassList[msg.arg1] = addr + } + MSG_DEL_ADDR -> { + bypassList.removeAt(msg.arg1) + } + MSG_PRESET_ADDR -> { + val list = Constraints.PRESETS[msg.arg1] + reset(list) + return + } + MSG_EXPORT_ADDR -> { + val path = msg.obj as? String ?: return + Toast.makeText(this@BypassListActivity, "${getString(R.string.exporting)} $path", Toast.LENGTH_LONG).show() + return + } + } + refreshList() + super.handleMessage(msg) + } + } + + override fun onClick(arg0: View) { + when (arg0.id) { + R.id.addBypassAddr -> editAddr(MSG_ADD_ADDR, -1) + R.id.presetBypassAddr -> presetAddr() + R.id.importBypassAddr -> importAddr() + R.id.exportBypassAddr -> exportAddr() + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + finish() + true + } + else -> super.onOptionsItemSelected(item) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + setContentView(R.layout.bypass_list) + + findViewById(R.id.addBypassAddr).setOnClickListener(this) + findViewById(R.id.presetBypassAddr).setOnClickListener(this) + findViewById(R.id.importBypassAddr).setOnClickListener(this) + findViewById(R.id.exportBypassAddr).setOnClickListener(this) + + refreshList() + } + + override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + editAddr(MSG_EDIT_ADDR, position) + } + + override fun onItemLongClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long): Boolean { + delAddr(position) + return true + } + + private fun presetAddr() { + AlertDialog.Builder(this) + .setTitle(R.string.preset_button) + .setNegativeButton(R.string.alert_dialog_cancel) { _, _ -> } + .setSingleChoiceItems(R.array.presets_list, -1) { dialog, which -> + if (which >= 0 && which < Constraints.PRESETS.size) { + val msg = Message.obtain().apply { + what = MSG_PRESET_ADDR + arg1 = which + } + handler.sendMessage(msg) + } + dialog.dismiss() + } + .create() + .show() + } + + @Deprecated("Deprecated in Java") + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == Constraints.IMPORT_REQUEST && resultCode == RESULT_OK) { + val path = data?.getStringExtra(Constraints.FILE_PATH) + if (path.isNullOrEmpty()) return + + val pd = ProgressDialog.show(this, "", getString(R.string.importing), true, true) + + val h = object : Handler(Looper.getMainLooper()) { + override fun handleMessage(msg: Message) { + refreshList() + pd?.dismiss() + } + } + + Thread { + try { + FileInputStream(path).use { input -> + BufferedReader(InputStreamReader(input)).use { br -> + bypassList.clear() + var line: String? + while (br.readLine().also { line = it } != null) { + Profile.validateAddr(line)?.let { bypassList.add(it) } + } + } + } + } catch (e: FileNotFoundException) { + Log.e(TAG, "error to open file", e) + } catch (e: IOException) { + Log.e(TAG, "error to read file", e) + } + h.sendEmptyMessage(MSG_IMPORT_ADDR) + }.start() + } + } + + private fun importAddr() { + startActivityForResult(Intent(this, FileChooser::class.java), Constraints.IMPORT_REQUEST) + } + + private fun exportAddr() { + val factory = LayoutInflater.from(this) + val textEntryView = factory.inflate(R.layout.alert_dialog_text_entry, null) + val path = textEntryView.findViewById(R.id.text_edit) + + path.setText("${Utils.getDataPath(this)}/${profile.host}.opt") + + AlertDialog.Builder(this) + .setTitle(R.string.export_button) + .setView(textEntryView) + .setPositiveButton(R.string.alert_dialog_ok) { _, _ -> + val pathText = path.text?.toString() ?: return@setPositiveButton + + Thread { + try { + val file = File(pathText) + if (!file.exists()) file.createNewFile() + + FileOutputStream(file).use { output -> + BufferedOutputStream(output).use { bw -> + for (addr in bypassList) { + Profile.validateAddr(addr)?.let { + bw.write("$it\n".toByteArray()) + } + } + bw.flush() + } + output.flush() + } + } catch (e: FileNotFoundException) { + Log.e(TAG, "error to open file", e) + } catch (e: IOException) { + Log.e(TAG, "error to write file", e) + } + + val msg = Message.obtain().apply { + what = MSG_EXPORT_ADDR + obj = pathText + } + handler.sendMessage(msg) + }.start() + } + .setNegativeButton(R.string.alert_dialog_cancel) { _, _ -> } + .create() + .show() + } + + private fun delAddr(idx: Int) { + val addr = bypassList[idx] + + AlertDialog.Builder(this) + .setTitle(addr) + .setMessage(R.string.bypass_del_text) + .setPositiveButton(R.string.alert_dialog_ok) { _, _ -> + val msg = Message.obtain().apply { + what = MSG_DEL_ADDR + arg1 = idx + obj = addr + } + handler.sendMessage(msg) + } + .setNegativeButton(R.string.alert_dialog_cancel) { _, _ -> } + .create() + .show() + } + + private fun editAddr(msgType: Int, idx: Int) { + val factory = LayoutInflater.from(this) + val textEntryView = factory.inflate(R.layout.alert_dialog_text_entry, null) + val addrText = textEntryView.findViewById(R.id.text_edit) + + when (msgType) { + MSG_EDIT_ADDR -> addrText.setText(bypassList[idx]) + MSG_ADD_ADDR -> addrText.setText("0.0.0.0/0") + } + + AlertDialog.Builder(this) + .setTitle(R.string.bypass_edit_title) + .setView(textEntryView) + .setPositiveButton(R.string.alert_dialog_ok) { _, _ -> + Thread { + val addr = addrText.text.toString() + val validated = Profile.validateAddr(addr) + if (validated != null) { + val msg = Message.obtain().apply { + what = msgType + arg1 = idx + obj = validated + } + handler.sendMessage(msg) + } else { + handler.sendEmptyMessage(MSG_ERR_ADDR) + } + }.start() + } + .setNegativeButton(R.string.alert_dialog_cancel) { _, _ -> } + .create() + .show() + } + + private fun reset(list: Array) { + val pd = ProgressDialog.show(this, "", getString(R.string.reseting), true, true) + + val h = object : Handler(Looper.getMainLooper()) { + override fun handleMessage(msg: Message) { + refreshList() + pd?.dismiss() + } + } + + Thread { + bypassList.clear() + for (addr in list) { + Profile.validateAddr(addr)?.let { bypassList.add(it) } + } + h.sendEmptyMessage(0) + }.start() + } + + private fun refreshList() { + val settings = PreferenceManager.getDefaultSharedPreferences(this) + profile.getProfile(settings) + + if (bypassList.isNotEmpty()) { + profile.bypassAddrs = Profile.encodeAddrs(bypassList.toTypedArray()) + profile.setProfile(settings) + } + + val addrs = Profile.decodeAddrs(profile.bypassAddrs) + bypassList = ArrayList(addrs.toList()) + + val inflater = layoutInflater + + adapter = object : ArrayAdapter(this, R.layout.bypass_list_item, R.id.bypasslistItemText, bypassList) { + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val view = convertView ?: inflater.inflate(R.layout.bypass_list_item, parent, false) + val item = view.findViewById(R.id.bypasslistItemText) + bypassList.getOrNull(position)?.let { item.text = it } + return view + } + } + + val list = findViewById(R.id.BypassListView) + list.adapter = adapter + list.onItemClickListener = this + list.onItemLongClickListener = this + } +} diff --git a/app/src/main/java/org/proxydroid/ConnectivityBroadcastReceiver.java b/app/src/main/java/org/proxydroid/ConnectivityBroadcastReceiver.java deleted file mode 100644 index 7b732b81..00000000 --- a/app/src/main/java/org/proxydroid/ConnectivityBroadcastReceiver.java +++ /dev/null @@ -1,235 +0,0 @@ -/* proxydroid - Global / Individual Proxy App for Android - * Copyright (C) 2011 Max Lv - * - * This program 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. - * - * This program 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 this program. If not, see . - * - * - * ___====-_ _-====___ - * _--^^^#####// \\#####^^^--_ - * _-^##########// ( ) \\##########^-_ - * -############// |\^^/| \\############- - * _/############// (@::@) \\############\_ - * /#############(( \\// ))#############\ - * -###############\\ (oo) //###############- - * -#################\\ / VV \ //#################- - * -###################\\/ \//###################- - * _#/|##########/\######( /\ )######/\##########|\#_ - * |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \| - * ` |/ V V ` V \#\| | | |/#/ V ' V V \| ' - * ` ` ` ` / | | | | \ ' ' ' ' - * ( | | | | ) - * __\ | | | | /__ - * (vvv(VVV)(VVV)vvv) - * - * HERE BE DRAGONS - * - */ - -package org.proxydroid; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.net.wifi.WifiInfo; -import android.net.wifi.WifiManager; -import android.os.Handler; -import android.preference.PreferenceManager; - -import com.ksmaze.android.preference.ListPreferenceMultiSelect; - -import org.proxydroid.utils.Constraints; -import org.proxydroid.utils.Utils; - -public class ConnectivityBroadcastReceiver extends BroadcastReceiver { - - private Handler mHandler = new Handler(); - - private static final String TAG = "ConnectivityBroadcastReceiver"; - - @Override - public void onReceive(final Context context, final Intent intent) { - - if (Utils.isConnecting()) return; - - if (!intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) return; - - mHandler.post(new Runnable() { - @Override - public void run() { - - // only switching profiles when needed - ConnectivityManager manager = (ConnectivityManager) context - .getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = manager.getActiveNetworkInfo(); - - if (networkInfo != null) { - if (networkInfo.getState() == NetworkInfo.State.CONNECTING - || networkInfo.getState() == NetworkInfo.State.DISCONNECTING - || networkInfo.getState() == NetworkInfo.State.UNKNOWN) - return; - } else { - if (!Utils.isWorking()) return; - } - - SharedPreferences settings = PreferenceManager - .getDefaultSharedPreferences(context); - Profile mProfile = new Profile(); - mProfile.getProfile(settings); - - // Store current settings first - String oldProfile = settings.getString("profile", "1"); - Editor ed = settings.edit(); - ed.putString(oldProfile, mProfile.toString()); - ed.commit(); - - // Load all profiles - String[] profileValues = settings.getString("profileValues", "").split( - "\\|"); - String curSSID = null; - String lastSSID = settings.getString("lastSSID", "-1"); - boolean autoConnect = false; - - // Test on each profile - for (String profile : profileValues) { - String profileString = settings.getString(profile, ""); - mProfile.decodeJson(profileString); - curSSID = onlineSSID(context, mProfile.getSsid(), mProfile.getExcludedSsid()); - if (mProfile.isAutoConnect() && curSSID != null) { - // Enable auto connect - autoConnect = true; - - // XXX: Switch profile first - ed = settings.edit(); - ed.putString("profile", profile); - ed.commit(); - - // Then switch profile values - mProfile.setProfile(settings); - break; - } - } - - if (networkInfo == null) { - if (!lastSSID.equals(Constraints.ONLY_3G) - && !lastSSID.equals(Constraints.WIFI_AND_3G) - && !lastSSID.equals(Constraints.ONLY_WIFI)) { - if (Utils.isWorking()) { - context.stopService(new Intent(context, - ProxyDroidService.class)); - } - } - } else { - // no network available now - if (networkInfo.getState() != NetworkInfo.State.CONNECTED) - return; - - if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { - // if no last SSID, should give up here - if (!lastSSID.equals("-1")) { - // get WIFI info - WifiManager wm = (WifiManager) context - .getSystemService(Context.WIFI_SERVICE); - WifiInfo wInfo = wm.getConnectionInfo(); - if (wInfo != null) { - // compare with the current SSID - String current = wInfo.getSSID(); - if (current != null) current = current.replace("\"", ""); - if (current != null && !current.equals(lastSSID)) { - // need to switch profile, so stop service first - if (Utils.isWorking()) - context.stopService(new Intent(context, - ProxyDroidService.class)); - } - } - } - } else { - // still satisfy the last trigger - if (!lastSSID.equals(Constraints.ONLY_3G) - && !lastSSID.equals(Constraints.WIFI_AND_3G)) { - if (Utils.isWorking()) - context.stopService(new Intent(context, - ProxyDroidService.class)); - } - } - } - - if (autoConnect) { - if (!Utils.isWorking()) { - ProxyDroidReceiver pdr = new ProxyDroidReceiver(); - ed = settings.edit(); - ed.putString("lastSSID", curSSID); - ed.commit(); - Utils.setConnecting(true); - pdr.onReceive(context, intent); - } - } - } - }); - } - - public String onlineSSID(Context context, String ssid, String excludedSsid) { - String ssids[] = ListPreferenceMultiSelect.parseStoredValue(ssid); - String excludedSsids[] = ListPreferenceMultiSelect.parseStoredValue(excludedSsid); - if (ssids == null) - return null; - if (ssids.length < 1) - return null; - ConnectivityManager manager = (ConnectivityManager) context - .getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = manager.getActiveNetworkInfo(); - if (networkInfo == null) - return null; - if (networkInfo.getType() != ConnectivityManager.TYPE_WIFI) { - for (String item : ssids) { - if (Constraints.WIFI_AND_3G.equals(item)) - return item; - if (Constraints.ONLY_3G.equals(item)) - return item; - } - return null; - } - WifiManager wm = (WifiManager) context - .getSystemService(Context.WIFI_SERVICE); - WifiInfo wInfo = wm.getConnectionInfo(); - if (wInfo == null || wInfo.getSSID() == null) - return null; - String current = wInfo.getSSID(); - if (current == null || "".equals(current)) - return null; - current = current.replace("\"", ""); - - if (excludedSsids != null) { - for (String item : excludedSsids) { - if (current.equals(item)) { - return null; // Never connect proxy on excluded ssid - } - } - } - - for (String item : ssids) { - if (Constraints.WIFI_AND_3G.equals(item)) - return item; - if (Constraints.ONLY_WIFI.equals(item)) - return item; - if (current.equals(item)) - return item; - } - return null; - } - -} diff --git a/app/src/main/java/org/proxydroid/ConnectivityBroadcastReceiver.kt b/app/src/main/java/org/proxydroid/ConnectivityBroadcastReceiver.kt new file mode 100644 index 00000000..b45d0fcb --- /dev/null +++ b/app/src/main/java/org/proxydroid/ConnectivityBroadcastReceiver.kt @@ -0,0 +1,174 @@ +/* proxydroid - Global / Individual Proxy App for Android + * Copyright (C) 2011 Max Lv + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package org.proxydroid + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.net.ConnectivityManager +import android.net.NetworkInfo +import android.net.wifi.WifiManager +import android.os.Handler +import android.os.Looper +import android.preference.PreferenceManager +import com.ksmaze.android.preference.ListPreferenceMultiSelect +import org.proxydroid.utils.Constraints +import org.proxydroid.utils.Utils + +class ConnectivityBroadcastReceiver : BroadcastReceiver() { + + private val handler = Handler(Looper.getMainLooper()) + + companion object { + private const val TAG = "ConnectivityBroadcastReceiver" + } + + override fun onReceive(context: Context, intent: Intent) { + if (Utils.isConnecting()) return + if (intent.action != ConnectivityManager.CONNECTIVITY_ACTION) return + + handler.post { + val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val networkInfo = manager.activeNetworkInfo + + if (networkInfo != null) { + if (networkInfo.state == NetworkInfo.State.CONNECTING || + networkInfo.state == NetworkInfo.State.DISCONNECTING || + networkInfo.state == NetworkInfo.State.UNKNOWN + ) { + return@post + } + } else { + if (!Utils.isWorking()) return@post + } + + val settings = PreferenceManager.getDefaultSharedPreferences(context) + val profile = Profile() + profile.getProfile(settings) + + // Store current settings first + val oldProfile = settings.getString("profile", "1") ?: "1" + settings.edit().putString(oldProfile, profile.toString()).apply() + + // Load all profiles + val profileValues = settings.getString("profileValues", "")?.split("\\|".toRegex()) ?: emptyList() + var curSSID: String? = null + val lastSSID = settings.getString("lastSSID", "-1") ?: "-1" + var autoConnect = false + + // Test on each profile + for (profileId in profileValues) { + if (profileId.isEmpty()) continue + val profileString = settings.getString(profileId, "") ?: "" + profile.decodeJson(profileString) + curSSID = onlineSSID(context, profile.ssid, profile.excludedSsid) + if (profile.isAutoConnect && curSSID != null) { + autoConnect = true + settings.edit().putString("profile", profileId).apply() + profile.setProfile(settings) + break + } + } + + if (networkInfo == null) { + if (lastSSID != Constraints.ONLY_3G && + lastSSID != Constraints.WIFI_AND_3G && + lastSSID != Constraints.ONLY_WIFI + ) { + if (Utils.isWorking()) { + context.stopService(Intent(context, ProxyDroidService::class.java)) + } + } + } else { + if (networkInfo.state != NetworkInfo.State.CONNECTED) return@post + + if (networkInfo.type == ConnectivityManager.TYPE_WIFI) { + if (lastSSID != "-1") { + val wm = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + val wInfo = wm.connectionInfo + if (wInfo != null) { + var current = wInfo.ssid + current = current?.replace("\"", "") + if (current != null && current != lastSSID) { + if (Utils.isWorking()) { + context.stopService(Intent(context, ProxyDroidService::class.java)) + } + } + } + } + } else { + if (lastSSID != Constraints.ONLY_3G && lastSSID != Constraints.WIFI_AND_3G) { + if (Utils.isWorking()) { + context.stopService(Intent(context, ProxyDroidService::class.java)) + } + } + } + } + + if (autoConnect) { + if (!Utils.isWorking()) { + val pdr = ProxyDroidReceiver() + settings.edit().putString("lastSSID", curSSID).apply() + Utils.setConnecting(true) + pdr.onReceive(context, intent) + } + } + } + } + + fun onlineSSID(context: Context, ssid: String, excludedSsid: String): String? { + val ssids = ListPreferenceMultiSelect.parseStoredValue(ssid) ?: return null + val excludedSsids = ListPreferenceMultiSelect.parseStoredValue(excludedSsid) + + if (ssids.isEmpty()) return null + + val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val networkInfo = manager.activeNetworkInfo ?: return null + + if (networkInfo.type != ConnectivityManager.TYPE_WIFI) { + for (item in ssids) { + if (Constraints.WIFI_AND_3G == item) return item + if (Constraints.ONLY_3G == item) return item + } + return null + } + + val wm = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + val wInfo = wm.connectionInfo + if (wInfo?.ssid == null) return null + + var current = wInfo.ssid + if (current.isNullOrEmpty()) return null + current = current.replace("\"", "") + + if (excludedSsids != null) { + for (item in excludedSsids) { + if (current == item) { + return null // Never connect proxy on excluded ssid + } + } + } + + for (item in ssids) { + if (Constraints.WIFI_AND_3G == item) return item + if (Constraints.ONLY_WIFI == item) return item + if (current == item) return item + } + return null + } +} diff --git a/app/src/main/java/org/proxydroid/DomainValidator.java b/app/src/main/java/org/proxydroid/DomainValidator.java deleted file mode 100644 index 4f8e51a9..00000000 --- a/app/src/main/java/org/proxydroid/DomainValidator.java +++ /dev/null @@ -1,476 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.proxydroid; - -import org.proxydroid.utils.RegexValidator; - -import java.io.Serializable; -import java.util.Arrays; -import java.util.List; - -/** - *

- * Domain name validation routines. - *

- * - *

- * This validator provides methods for validating Internet domain names and - * top-level domains. - *

- * - *

- * Domain names are evaluated according to the standards RFC1034, section 3, and RFC1123, section 2.1. No - * accomodation is provided for the specialized needs of other applications; if - * the domain name has been URL-encoded, for example, validation will fail even - * though the equivalent plaintext version of the same name would have passed. - *

- * - *

- * Validation is also provided for top-level domains (TLDs) as defined and - * maintained by the Internet Assigned Numbers Authority (IANA): - *

- * - *
    - *
  • {@link #isValidInfrastructureTld} - validates infrastructure TLDs ( - * .arpa, etc.)
  • - *
  • {@link #isValidGenericTld} - validates generic TLDs ( - * .com, .org, etc.)
  • - *
  • {@link #isValidCountryCodeTld} - validates country code TLDs ( - * .us, .uk, .cn, etc.)
  • - *
- * - *

- * (NOTE: This class does not provide IP address lookup for domain names - * or methods to ensure that a given domain name matches a specific IP; see - * {@link java.net.InetAddress} for that functionality.) - *

- * - * @version $Revision$ $Date$ - * @since Validator 1.4 - */ -public class DomainValidator implements Serializable { - - // Regular expression strings for hostnames (derived from RFC2396 and RFC - // 1123) - private static final String DOMAIN_LABEL_REGEX = "\\p{Alnum}(?>[\\p{Alnum}-]*\\p{Alnum})*"; - private static final String TOP_LABEL_REGEX = "\\p{Alpha}{2,}"; - private static final String DOMAIN_NAME_REGEX = "^(?:" + DOMAIN_LABEL_REGEX - + "\\.)+" + "(" + TOP_LABEL_REGEX + ")$"; - - /** - * Singleton instance of this validator. - */ - private static final DomainValidator DOMAIN_VALIDATOR = new DomainValidator(); - - /** - * RegexValidator for matching domains. - */ - private final RegexValidator domainRegex = new RegexValidator( - DOMAIN_NAME_REGEX); - - /** - * Returns the singleton instance of this validator. - * - * @return the singleton instance of this validator - */ - public static DomainValidator getInstance() { - return DOMAIN_VALIDATOR; - } - - /** Private constructor. */ - private DomainValidator() { - } - - /** - * Returns true if the specified String parses as a valid - * domain name with a recognized top-level domain. The parsing is - * case-sensitive. - * - * @param domain - * the parameter to check for domain name syntax - * @return true if the parameter is a valid domain name - */ - public boolean isValid(String domain) { - String[] groups = domainRegex.match(domain); - if (groups != null && groups.length > 0) { - return isValidTld(groups[0]); - } else { - return false; - } - } - - /** - * Returns true if the specified String matches any - * IANA-defined top-level domain. Leading dots are ignored if present. The - * search is case-sensitive. - * - * @param tld - * the parameter to check for TLD status - * @return true if the parameter is a TLD - */ - public boolean isValidTld(String tld) { - return isValidInfrastructureTld(tld) || isValidGenericTld(tld) - || isValidCountryCodeTld(tld); - } - - /** - * Returns true if the specified String matches any - * IANA-defined infrastructure top-level domain. Leading dots are ignored if - * present. The search is case-sensitive. - * - * @param iTld - * the parameter to check for infrastructure TLD status - * @return true if the parameter is an infrastructure TLD - */ - public boolean isValidInfrastructureTld(String iTld) { - return INFRASTRUCTURE_TLD_LIST.contains(chompLeadingDot(iTld - .toLowerCase())); - } - - /** - * Returns true if the specified String matches any - * IANA-defined generic top-level domain. Leading dots are ignored if - * present. The search is case-sensitive. - * - * @param gTld - * the parameter to check for generic TLD status - * @return true if the parameter is a generic TLD - */ - public boolean isValidGenericTld(String gTld) { - return GENERIC_TLD_LIST.contains(chompLeadingDot(gTld.toLowerCase())); - } - - /** - * Returns true if the specified String matches any - * IANA-defined country code top-level domain. Leading dots are ignored if - * present. The search is case-sensitive. - * - * @param ccTld - * the parameter to check for country code TLD status - * @return true if the parameter is a country code TLD - */ - public boolean isValidCountryCodeTld(String ccTld) { - return COUNTRY_CODE_TLD_LIST.contains(chompLeadingDot(ccTld - .toLowerCase())); - } - - private String chompLeadingDot(String str) { - if (str.startsWith(".")) { - return str.substring(1); - } else { - return str; - } - } - - // --------------------------------------------- - // ----- TLDs defined by IANA - // ----- Authoritative and comprehensive list at: - // ----- http://data.iana.org/TLD/tlds-alpha-by-domain.txt - - private static final String[] INFRASTRUCTURE_TLDS = new String[] { "arpa", // internet - // infrastructure - "root" // diagnostic marker for non-truncated root zone - }; - - private static final String[] GENERIC_TLDS = new String[] { "aero", // air - // transport - // industry - "asia", // Pan-Asia/Asia Pacific - "biz", // businesses - "cat", // Catalan linguistic/cultural community - "com", // commercial enterprises - "coop", // cooperative associations - "info", // informational sites - "jobs", // Human Resource managers - "mobi", // mobile products and services - "museum", // museums, surprisingly enough - "name", // individuals' sites - "net", // internet support infrastructure/business - "org", // noncommercial organizations - "pro", // credentialed professionals and entities - "tel", // contact data for businesses and individuals - "travel", // entities in the travel industry - "gov", // United States Government - "edu", // accredited postsecondary US education entities - "mil", // United States Military - "int" // organizations established by international treaty - }; - - private static final String[] COUNTRY_CODE_TLDS = new String[] { "ac", // Ascension - // Island - "ad", // Andorra - "ae", // United Arab Emirates - "af", // Afghanistan - "ag", // Antigua and Barbuda - "ai", // Anguilla - "al", // Albania - "am", // Armenia - "an", // Netherlands Antilles - "ao", // Angola - "aq", // Antarctica - "ar", // Argentina - "as", // American Samoa - "at", // Austria - "au", // Australia (includes Ashmore and Cartier Islands and Coral - // Sea Islands) - "aw", // Aruba - "ax", // 脙鈥and - "az", // Azerbaijan - "ba", // Bosnia and Herzegovina - "bb", // Barbados - "bd", // Bangladesh - "be", // Belgium - "bf", // Burkina Faso - "bg", // Bulgaria - "bh", // Bahrain - "bi", // Burundi - "bj", // Benin - "bm", // Bermuda - "bn", // Brunei Darussalam - "bo", // Bolivia - "br", // Brazil - "bs", // Bahamas - "bt", // Bhutan - "bv", // Bouvet Island - "bw", // Botswana - "by", // Belarus - "bz", // Belize - "ca", // Canada - "cc", // Cocos (Keeling) Islands - "cd", // Democratic Republic of the Congo (formerly Zaire) - "cf", // Central African Republic - "cg", // Republic of the Congo - "ch", // Switzerland - "ci", // C脙麓te d'Ivoire - "ck", // Cook Islands - "cl", // Chile - "cm", // Cameroon - "cn", // China, mainland - "co", // Colombia - "cr", // Costa Rica - "cu", // Cuba - "cv", // Cape Verde - "cx", // Christmas Island - "cy", // Cyprus - "cz", // Czech Republic - "de", // Germany - "dj", // Djibouti - "dk", // Denmark - "dm", // Dominica - "do", // Dominican Republic - "dz", // Algeria - "ec", // Ecuador - "ee", // Estonia - "eg", // Egypt - "er", // Eritrea - "es", // Spain - "et", // Ethiopia - "eu", // European Union - "fi", // Finland - "fj", // Fiji - "fk", // Falkland Islands - "fm", // Federated States of Micronesia - "fo", // Faroe Islands - "fr", // France - "ga", // Gabon - "gb", // Great Britain (United Kingdom) - "gd", // Grenada - "ge", // Georgia - "gf", // French Guiana - "gg", // Guernsey - "gh", // Ghana - "gi", // Gibraltar - "gl", // Greenland - "gm", // The Gambia - "gn", // Guinea - "gp", // Guadeloupe - "gq", // Equatorial Guinea - "gr", // Greece - "gs", // South Georgia and the South Sandwich Islands - "gt", // Guatemala - "gu", // Guam - "gw", // Guinea-Bissau - "gy", // Guyana - "hk", // Hong Kong - "hm", // Heard Island and McDonald Islands - "hn", // Honduras - "hr", // Croatia (Hrvatska) - "ht", // Haiti - "hu", // Hungary - "id", // Indonesia - "ie", // Ireland (脙鈥癷re) - "il", // Israel - "im", // Isle of Man - "in", // India - "io", // British Indian Ocean Territory - "iq", // Iraq - "ir", // Iran - "is", // Iceland - "it", // Italy - "je", // Jersey - "jm", // Jamaica - "jo", // Jordan - "jp", // Japan - "ke", // Kenya - "kg", // Kyrgyzstan - "kh", // Cambodia (Khmer) - "ki", // Kiribati - "km", // Comoros - "kn", // Saint Kitts and Nevis - "kp", // North Korea - "kr", // South Korea - "kw", // Kuwait - "ky", // Cayman Islands - "kz", // Kazakhstan - "la", // Laos (currently being marketed as the official domain for - // Los Angeles) - "lb", // Lebanon - "lc", // Saint Lucia - "li", // Liechtenstein - "lk", // Sri Lanka - "lr", // Liberia - "ls", // Lesotho - "lt", // Lithuania - "lu", // Luxembourg - "lv", // Latvia - "ly", // Libya - "ma", // Morocco - "mc", // Monaco - "md", // Moldova - "me", // Montenegro - "mg", // Madagascar - "mh", // Marshall Islands - "mk", // Republic of Macedonia - "ml", // Mali - "mm", // Myanmar - "mn", // Mongolia - "mo", // Macau - "mp", // Northern Mariana Islands - "mq", // Martinique - "mr", // Mauritania - "ms", // Montserrat - "mt", // Malta - "mu", // Mauritius - "mv", // Maldives - "mw", // Malawi - "mx", // Mexico - "my", // Malaysia - "mz", // Mozambique - "na", // Namibia - "nc", // New Caledonia - "ne", // Niger - "nf", // Norfolk Island - "ng", // Nigeria - "ni", // Nicaragua - "nl", // Netherlands - "no", // Norway - "np", // Nepal - "nr", // Nauru - "nu", // Niue - "nz", // New Zealand - "om", // Oman - "pa", // Panama - "pe", // Peru - "pf", // French Polynesia With Clipperton Island - "pg", // Papua New Guinea - "ph", // Philippines - "pk", // Pakistan - "pl", // Poland - "pm", // Saint-Pierre and Miquelon - "pn", // Pitcairn Islands - "pr", // Puerto Rico - "ps", // Palestinian territories (PA-controlled West Bank and Gaza - // Strip) - "pt", // Portugal - "pw", // Palau - "py", // Paraguay - "qa", // Qatar - "re", // R脙漏union - "ro", // Romania - "rs", // Serbia - "ru", // Russia - "rw", // Rwanda - "sa", // Saudi Arabia - "sb", // Solomon Islands - "sc", // Seychelles - "sd", // Sudan - "se", // Sweden - "sg", // Singapore - "sh", // Saint Helena - "si", // Slovenia - "sj", // Svalbard and Jan Mayen Islands Not in use (Norwegian - // dependencies; see .no) - "sk", // Slovakia - "sl", // Sierra Leone - "sm", // San Marino - "sn", // Senegal - "so", // Somalia - "sr", // Suriname - "st", // S脙拢o Tom脙漏 and Pr脙颅ncipe - "su", // Soviet Union (deprecated) - "sv", // El Salvador - "sy", // Syria - "sz", // Swaziland - "tc", // Turks and Caicos Islands - "td", // Chad - "tf", // French Southern and Antarctic Lands - "tg", // Togo - "th", // Thailand - "tj", // Tajikistan - "tk", // Tokelau - "tl", // East Timor (deprecated old code) - "tm", // Turkmenistan - "tn", // Tunisia - "to", // Tonga - "tp", // East Timor - "tr", // Turkey - "tt", // Trinidad and Tobago - "tv", // Tuvalu - "tw", // Taiwan, Republic of China - "tz", // Tanzania - "ua", // Ukraine - "ug", // Uganda - "uk", // United Kingdom - "um", // United States Minor Outlying Islands - "us", // United States of America - "uy", // Uruguay - "uz", // Uzbekistan - "va", // Vatican City State - "vc", // Saint Vincent and the Grenadines - "ve", // Venezuela - "vg", // British Virgin Islands - "vi", // U.S. Virgin Islands - "vn", // Vietnam - "vu", // Vanuatu - "wf", // Wallis and Futuna - "ws", // Samoa (formerly Western Samoa) - "ye", // Yemen - "yt", // Mayotte - "yu", // Serbia and Montenegro (originally Yugoslavia) - "za", // South Africa - "zm", // Zambia - "zw", // Zimbabwe - }; - - private static final List INFRASTRUCTURE_TLD_LIST = Arrays - .asList(INFRASTRUCTURE_TLDS); - private static final List GENERIC_TLD_LIST = Arrays.asList(GENERIC_TLDS); - private static final List COUNTRY_CODE_TLD_LIST = Arrays - .asList(COUNTRY_CODE_TLDS); -} diff --git a/app/src/main/java/org/proxydroid/DomainValidator.kt b/app/src/main/java/org/proxydroid/DomainValidator.kt new file mode 100644 index 00000000..8e69c025 --- /dev/null +++ b/app/src/main/java/org/proxydroid/DomainValidator.kt @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.proxydroid + +import org.proxydroid.utils.RegexValidator +import java.io.Serializable + +class DomainValidator private constructor() : Serializable { + + companion object { + private const val DOMAIN_LABEL_REGEX = "\\p{Alnum}(?>[\\p{Alnum}-]*\\p{Alnum})*" + private const val TOP_LABEL_REGEX = "\\p{Alpha}{2,}" + private const val DOMAIN_NAME_REGEX = "^(?:$DOMAIN_LABEL_REGEX\\.)+($TOP_LABEL_REGEX)$" + + @JvmStatic + val instance: DomainValidator = DomainValidator() + + private val INFRASTRUCTURE_TLDS = arrayOf("arpa", "root") + + private val GENERIC_TLDS = arrayOf( + "aero", "asia", "biz", "cat", "com", "coop", "info", "jobs", "mobi", + "museum", "name", "net", "org", "pro", "tel", "travel", "gov", "edu", "mil", "int" + ) + + private val COUNTRY_CODE_TLDS = arrayOf( + "ac", "ad", "ae", "af", "ag", "ai", "al", "am", "an", "ao", "aq", "ar", "as", "at", + "au", "aw", "ax", "az", "ba", "bb", "bd", "be", "bf", "bg", "bh", "bi", "bj", "bm", + "bn", "bo", "br", "bs", "bt", "bv", "bw", "by", "bz", "ca", "cc", "cd", "cf", "cg", + "ch", "ci", "ck", "cl", "cm", "cn", "co", "cr", "cu", "cv", "cx", "cy", "cz", "de", + "dj", "dk", "dm", "do", "dz", "ec", "ee", "eg", "er", "es", "et", "eu", "fi", "fj", + "fk", "fm", "fo", "fr", "ga", "gb", "gd", "ge", "gf", "gg", "gh", "gi", "gl", "gm", + "gn", "gp", "gq", "gr", "gs", "gt", "gu", "gw", "gy", "hk", "hm", "hn", "hr", "ht", + "hu", "id", "ie", "il", "im", "in", "io", "iq", "ir", "is", "it", "je", "jm", "jo", + "jp", "ke", "kg", "kh", "ki", "km", "kn", "kp", "kr", "kw", "ky", "kz", "la", "lb", + "lc", "li", "lk", "lr", "ls", "lt", "lu", "lv", "ly", "ma", "mc", "md", "me", "mg", + "mh", "mk", "ml", "mm", "mn", "mo", "mp", "mq", "mr", "ms", "mt", "mu", "mv", "mw", + "mx", "my", "mz", "na", "nc", "ne", "nf", "ng", "ni", "nl", "no", "np", "nr", "nu", + "nz", "om", "pa", "pe", "pf", "pg", "ph", "pk", "pl", "pm", "pn", "pr", "ps", "pt", + "pw", "py", "qa", "re", "ro", "rs", "ru", "rw", "sa", "sb", "sc", "sd", "se", "sg", + "sh", "si", "sj", "sk", "sl", "sm", "sn", "so", "sr", "st", "su", "sv", "sy", "sz", + "tc", "td", "tf", "tg", "th", "tj", "tk", "tl", "tm", "tn", "to", "tp", "tr", "tt", + "tv", "tw", "tz", "ua", "ug", "uk", "um", "us", "uy", "uz", "va", "vc", "ve", "vg", + "vi", "vn", "vu", "wf", "ws", "ye", "yt", "yu", "za", "zm", "zw" + ) + + private val INFRASTRUCTURE_TLD_LIST = INFRASTRUCTURE_TLDS.toList() + private val GENERIC_TLD_LIST = GENERIC_TLDS.toList() + private val COUNTRY_CODE_TLD_LIST = COUNTRY_CODE_TLDS.toList() + } + + private val domainRegex = RegexValidator(DOMAIN_NAME_REGEX) + + fun isValid(domain: String): Boolean { + val groups = domainRegex.match(domain) + return if (groups != null && groups.isNotEmpty()) { + isValidTld(groups[0]) + } else { + false + } + } + + fun isValidTld(tld: String): Boolean { + return isValidInfrastructureTld(tld) || isValidGenericTld(tld) || isValidCountryCodeTld(tld) + } + + fun isValidInfrastructureTld(iTld: String): Boolean { + return INFRASTRUCTURE_TLD_LIST.contains(chompLeadingDot(iTld.lowercase())) + } + + fun isValidGenericTld(gTld: String): Boolean { + return GENERIC_TLD_LIST.contains(chompLeadingDot(gTld.lowercase())) + } + + fun isValidCountryCodeTld(ccTld: String): Boolean { + return COUNTRY_CODE_TLD_LIST.contains(chompLeadingDot(ccTld.lowercase())) + } + + private fun chompLeadingDot(str: String): String { + return if (str.startsWith(".")) str.substring(1) else str + } +} diff --git a/app/src/main/java/org/proxydroid/Exec.java b/app/src/main/java/org/proxydroid/Exec.java deleted file mode 100644 index e3e23c30..00000000 --- a/app/src/main/java/org/proxydroid/Exec.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.proxydroid; - -import java.io.FileDescriptor; - -/** - * Utility methods for creating and managing a subprocess. - *

- * Note: The native methods access a package-private java.io.FileDescriptor - * field to get and set the raw Linux file descriptor. This might break if the - * implementation of java.io.FileDescriptor is changed. - */ - -public class Exec { - static { - java.lang.System.loadLibrary("exec"); - } - - /** - * Close a given file descriptor. - */ - public static native void close(FileDescriptor fd); - - /** - * Create a subprocess. Differs from java.lang.ProcessBuilder in that a pty - * is used to communicate with the subprocess. - *

- * Callers are responsible for calling Exec.close() on the returned file - * descriptor. - * - * @param rdt Whether redirect stdout and stderr - * @param cmd The command to execute. - * @param args An array of arguments to the command. - * @param envVars An array of strings of the form "VAR=value" to be added to the - * environment of the process. - * @param scripts The scripts to execute. - * @param processId A one-element array to which the process ID of the started - * process will be written. - * @return File descriptor - */ - public static native FileDescriptor createSubprocess(int rdt, String cmd, - String[] args, String[] envVars, - String scripts, int[] processId); - - /** - * Send SIGHUP to a process group. - */ - public static native void hangupProcessGroup(int processId); - - /** - * Causes the calling thread to wait for the process associated with the - * receiver to finish executing. - * - * @return The exit value of the Process being waited on - */ - public static native int waitFor(int processId); -} diff --git a/app/src/main/java/org/proxydroid/Exec.kt b/app/src/main/java/org/proxydroid/Exec.kt new file mode 100644 index 00000000..be1ca2b9 --- /dev/null +++ b/app/src/main/java/org/proxydroid/Exec.kt @@ -0,0 +1,45 @@ +/* proxydroid - Global / Individual Proxy App for Android + * Copyright (C) 2011 Max Lv + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package org.proxydroid + +import java.io.FileDescriptor + +object Exec { + init { + System.loadLibrary("exec") + } + + @JvmStatic + external fun close(fd: FileDescriptor) + + @JvmStatic + external fun createSubprocess( + rdt: Int, + cmd: String, + args: Array?, + envVars: Array?, + scripts: String?, + processId: IntArray + ): FileDescriptor + + @JvmStatic + external fun hangupProcessGroup(processId: Int) + + @JvmStatic + external fun waitFor(processId: Int): Int +} diff --git a/app/src/main/java/org/proxydroid/FileArrayAdapter.java b/app/src/main/java/org/proxydroid/FileArrayAdapter.java deleted file mode 100644 index b7742c18..00000000 --- a/app/src/main/java/org/proxydroid/FileArrayAdapter.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.proxydroid; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.TextView; - -import org.proxydroid.utils.Option; - -import java.util.List; - -public class FileArrayAdapter extends ArrayAdapter

- * Encodes and decodes to and from Base64 notation. - *

- *

- * Homepage: http://iharder.net/base64. - *

- * - *

- * Example: - *

- * - * String encoded = Base64.encode( myByteArray );
- * byte[] myByteArray = Base64.decode( encoded ); - * - *

- * The options parameter, which appears in a few places, is used to - * pass several pieces of information to the encoder. In the "higher level" - * methods such as encodeBytes( bytes, options ) the options parameter can be - * used to indicate such things as first gzipping the bytes before encoding - * them, not inserting linefeeds, and encoding using the URL-safe and Ordered - * dialects. - *

- * - *

- * Note, according to RFC3548, Section 2.1, - * implementations should not add line feeds unless explicitly told to do so. - * I've got Base64 set to this behavior now, although earlier versions broke - * lines by default. - *

- * - *

- * The constants defined in Base64 can be OR-ed together to combine options, so - * you might make a call like this: - *

- * - * String encoded = Base64.encodeBytes( mybytes, Base64.GZIP | Base64.DO_BREAK_LINES ); - *

- * to compress the data before encoding it and then making the output have - * newline characters. - *

- *

- * Also... - *

- * String encoded = Base64.encodeBytes( crazyString.getBytes() ); - * - * - * - *

- * Change Log: - *

- *
    - *
  • v2.3.7 - Fixed subtle bug when base 64 input stream contained the value - * 01111111, which is an invalid base 64 character but should not throw an - * ArrayIndexOutOfBoundsException either. Led to discovery of mishandling (or - * potential for better handling) of other bad input characters. You should now - * get an IOException if you try decoding something that has bad characters in - * it.
  • - *
  • v2.3.6 - Fixed bug when breaking lines and the final byte of the encoded - * string ended in the last column; the buffer was not properly shrunk and - * contained an extra (null) byte that made it into the string.
  • - *
  • v2.3.5 - Fixed bug in {@link #encodeFromFile} where estimated buffer size - * was wrong for files of size 31, 34, and 37 bytes.
  • - *
  • v2.3.4 - Fixed bug when working with gzipped streams whereby flushing the - * Base64.OutputStream closed the Base64 encoding (by padding with equals signs) - * too soon. Also added an option to suppress the automatic decoding of gzipped - * streams. Also added experimental support for specifying a class loader when - * using the - * {@link #decodeToObject(java.lang.String, int, java.lang.ClassLoader)} method. - *
  • - *
  • v2.3.3 - Changed default char encoding to US-ASCII which reduces the - * internal Java footprint with its CharEncoders and so forth. Fixed some - * javadocs that were inconsistent. Removed imports and specified things like - * java.io.IOException explicitly inline.
  • - *
  • v2.3.2 - Reduced memory footprint! Finally refined the "guessing" of how - * big the final encoded data will be so that the code doesn't have to create - * two output arrays: an oversized initial one and then a final, exact-sized - * one. Big win when using the {@link #encodeBytesToBytes(byte[])} family of - * methods (and not using the gzip options which uses a different mechanism with - * streams and stuff).
  • - *
  • v2.3.1 - Added {@link #encodeBytesToBytes(byte[], int, int, int)} and - * some similar helper methods to be more efficient with memory by not returning - * a String but just a byte array.
  • - *
  • v2.3 - This is not a drop-in replacement! This is two - * years of comments and bug fixes queued up and finally executed. Thanks to - * everyone who sent me stuff, and I'm sorry I wasn't able to distribute your - * fixes to everyone else. Much bad coding was cleaned up including throwing - * exceptions where necessary instead of returning null values or something - * similar. Here are some changes that may affect you: - *
      - *
    • Does not break lines, by default. This is to keep in compliance - * with RFC3548.
    • - *
    • Throws exceptions instead of returning null values. Because some - * operations (especially those that may permit the GZIP option) use IO streams, - * there is a possiblity of an java.io.IOException being thrown. After some - * discussion and thought, I've changed the behavior of the methods to throw - * java.io.IOExceptions rather than return null if ever there's an error. I - * think this is more appropriate, though it will require some changes to your - * code. Sorry, it should have been done this way to begin with.
    • - *
    • Removed all references to System.out, System.err, and the like. - * Shame on me. All I can say is sorry they were ever there.
    • - *
    • Throws NullPointerExceptions and IllegalArgumentExceptions as - * needed such as when passed arrays are null or offsets are invalid.
    • - *
    • Cleaned up as much javadoc as I could to avoid any javadoc warnings. This - * was especially annoying before for people who were thorough in their own - * projects and then had gobs of javadoc warnings on this file.
    • - *
    - *
  • v2.2.1 - Fixed bug using URL_SAFE and ORDERED encodings. Fixed bug when - * using very small files (~< 40 bytes).
  • - *
  • v2.2 - Added some helper methods for encoding/decoding directly from one - * file to the next. Also added a main() method to support command line - * encoding/decoding from one file to the next. Also added these Base64 - * dialects: - *
      - *
    1. The default is RFC3548 format.
    2. - *
    3. Calling Base64.setFormat(Base64.BASE64_FORMAT.URLSAFE_FORMAT) generates - * URL and file name friendly format as described in Section 4 of RFC3548. - * http://www.faqs.org/rfcs/rfc3548.html
    4. - *
    5. Calling Base64.setFormat(Base64.BASE64_FORMAT.ORDERED_FORMAT) generates - * URL and file name friendly format that preserves lexical ordering as - * described in http://www.faqs.org/qa/rfcc-1940.html
    6. - *
    - * Special thanks to Jim Kellerman at http://www.powerset.com/ for contributing - * the new Base64 dialects.
  • - * - *
  • v2.1 - Cleaned up javadoc comments and unused variables and methods. - * Added some convenience methods for reading and writing to and from files.
  • - *
  • v2.0.2 - Now specifies UTF-8 encoding in places where the code fails on - * systems with other encodings (like EBCDIC).
  • - *
  • v2.0.1 - Fixed an error when decoding a single byte, that is, when the - * encoded data was a single byte.
  • - *
  • v2.0 - I got rid of methods that used booleans to set options. Now - * everything is more consolidated and cleaner. The code now detects when data - * that's being decoded is gzip-compressed and will decompress it automatically. - * Generally things are cleaner. You'll probably have to change some method - * calls that you were making to support the new options format (ints - * that you "OR" together).
  • - *
  • v1.5.1 - Fixed bug when decompressing and decoding to a byte[] using - * decode( String s, boolean gzipCompressed ). Added the ability to - * "suspend" encoding in the Output Stream so you can turn on and off the - * encoding if you need to embed base64 data in an otherwise "normal" stream - * (like an XML file).
  • - *
  • v1.5 - Output stream pases on flush() command but doesn't do anything - * itself. This helps when using GZIP streams. Added the ability to - * GZip-compress objects before encoding them.
  • - *
  • v1.4 - Added helper methods to read/write files.
  • - *
  • v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset.
  • - *
  • v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input - * stream where last buffer being read, if not completely full, was not - * returned.
  • - *
  • v1.3.4 - Fixed when "improperly padded stream" error was thrown at the - * wrong time.
  • - *
  • v1.3.3 - Fixed I/O streams which were totally messed up.
  • - *
- * - *

- * I am placing this code in the Public Domain. Do with it as you will. This - * software comes with no guarantees or warranties but with plenty of - * well-wishing instead! Please visit http://iharder.net/base64 periodically - * to check for updates or to contribute improvements. - *

- * - * @author Robert Harder - * @author rob@iharder.net - * @version 2.3.7 - */ -public class Base64 { - - /* ******** P U B L I C F I E L D S ******** */ - - /** No options specified. Value is zero. */ - public final static int NO_OPTIONS = 0; - - /** Specify encoding in first bit. Value is one. */ - public final static int ENCODE = 1; - - /** Specify decoding in first bit. Value is zero. */ - public final static int DECODE = 0; - - /** Specify that data should be gzip-compressed in second bit. Value is two. */ - public final static int GZIP = 2; - - /** - * Specify that gzipped data should not be automatically gunzipped. - */ - public final static int DONT_GUNZIP = 4; - - /** Do break lines when encoding. Value is 8. */ - public final static int DO_BREAK_LINES = 8; - - /** - * Encode using Base64-like encoding that is URL- and Filename-safe as - * described in Section 4 of RFC3548: http://www.faqs.org/rfcs/rfc3548.html. It is important to note that - * data encoded this way is not officially valid Base64, or at the - * very least should not be called Base64 without also specifying that is - * was encoded using the URL- and Filename-safe dialect. - */ - public final static int URL_SAFE = 16; - - /** - * Encode using the special "ordered" dialect of Base64 described here: http://www.faqs.org/qa/rfcc- - * 1940.html. - */ - public final static int ORDERED = 32; - - /* ******** P R I V A T E F I E L D S ******** */ - - /** Maximum line length (76) of Base64 output. */ - private final static int MAX_LINE_LENGTH = 76; - - /** The equals sign (=) as a byte. */ - private final static byte EQUALS_SIGN = (byte) '='; - - /** The new line character (\n) as a byte. */ - private final static byte NEW_LINE = (byte) '\n'; - - /** Preferred encoding. */ - private final static String PREFERRED_ENCODING = "US-ASCII"; - - private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in - // encoding - private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in - // encoding - - /* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */ - - /** The 64 valid Base64 values. */ - /* - * Host platform me be something funny like EBCDIC, so we hardcode these - * values. - */ - private final static byte[] _STANDARD_ALPHABET = { (byte) 'A', (byte) 'B', - (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', - (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', - (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', (byte) 'Q', - (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', (byte) 'V', - (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', (byte) 'a', - (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', - (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', - (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', (byte) 'p', - (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', - (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z', - (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', - (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', - (byte) '+', (byte) '/' }; - - /** - * Translates a Base64 value to either its 6-bit reconstruction value or a - * negative number indicating some other meaning. - **/ - private final static byte[] _STANDARD_DECODABET = { -9, -9, -9, -9, -9, -9, - -9, -9, -9, // Decimal 0 - 8 - -5, -5, // Whitespace: Tab and Linefeed - -9, -9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - - // 26 - -9, -9, -9, -9, -9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 - 62, // Plus sign at decimal 43 - -9, -9, -9, // Decimal 44 - 46 - 63, // Slash at decimal 47 - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine - -9, -9, -9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9, -9, -9, // Decimal 62 - 64 - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through - // 'N' - 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' - // through 'Z' - -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 - 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' - // through 'm' - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' - // through 'z' - -9, -9, -9, -9, -9 // Decimal 123 - 127 - , -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - - // 139 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - - // 152 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - - // 165 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - - // 178 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - - // 191 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - - // 204 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - - // 217 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - - // 230 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - - // 243 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255 - }; - - /* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */ - - /** - * Used in the URL- and Filename-safe dialect described in Section 4 of - * RFC3548: http://www.faqs.org - * /rfcs/rfc3548.html. Notice that the last two bytes become "hyphen" - * and "underscore" instead of "plus" and "slash." - */ - private final static byte[] _URL_SAFE_ALPHABET = { (byte) 'A', (byte) 'B', - (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', - (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', - (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', (byte) 'Q', - (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', (byte) 'V', - (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', (byte) 'a', - (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', - (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', - (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', (byte) 'p', - (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', - (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z', - (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', - (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', - (byte) '-', (byte) '_' }; - - /** - * Used in decoding URL- and Filename-safe dialects of Base64. - */ - private final static byte[] _URL_SAFE_DECODABET = { -9, -9, -9, -9, -9, -9, - -9, -9, -9, // Decimal 0 - 8 - -5, -5, // Whitespace: Tab and Linefeed - -9, -9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - - // 26 - -9, -9, -9, -9, -9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 - -9, // Plus sign at decimal 43 - -9, // Decimal 44 - 62, // Minus sign at decimal 45 - -9, // Decimal 46 - -9, // Slash at decimal 47 - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine - -9, -9, -9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9, -9, -9, // Decimal 62 - 64 - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through - // 'N' - 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' - // through 'Z' - -9, -9, -9, -9, // Decimal 91 - 94 - 63, // Underscore at decimal 95 - -9, // Decimal 96 - 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' - // through 'm' - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' - // through 'z' - -9, -9, -9, -9, -9 // Decimal 123 - 127 - , -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - - // 139 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - - // 152 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - - // 165 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - - // 178 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - - // 191 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - - // 204 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - - // 217 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - - // 230 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - - // 243 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255 - }; - - /* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */ - - /** - * I don't get the point of this technique, but someone requested it, and it - * is described here: http:// - * www.faqs.org/qa/rfcc-1940.html. - */ - private final static byte[] _ORDERED_ALPHABET = { (byte) '-', (byte) '0', - (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', - (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) 'A', - (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', - (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', - (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', - (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', - (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', - (byte) '_', (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', - (byte) 'e', (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', - (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', - (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', - (byte) 't', (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', - (byte) 'y', (byte) 'z' }; - - /** - * Used in decoding the "ordered" dialect of Base64. - */ - private final static byte[] _ORDERED_DECODABET = { -9, -9, -9, -9, -9, -9, - -9, -9, -9, // Decimal 0 - 8 - -5, -5, // Whitespace: Tab and Linefeed - -9, -9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - - // 26 - -9, -9, -9, -9, -9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 - -9, // Plus sign at decimal 43 - -9, // Decimal 44 - 0, // Minus sign at decimal 45 - -9, // Decimal 46 - -9, // Slash at decimal 47 - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // Numbers zero through nine - -9, -9, -9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9, -9, -9, // Decimal 62 - 64 - 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, // Letters 'A' - // through 'M' - 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, // Letters 'N' - // through 'Z' - -9, -9, -9, -9, // Decimal 91 - 94 - 37, // Underscore at decimal 95 - -9, // Decimal 96 - 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, // Letters 'a' - // through 'm' - 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, // Letters 'n' - // through 'z' - -9, -9, -9, -9, -9 // Decimal 123 - 127 - , -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - // - 139 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - - // 152 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - - // 165 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - - // 178 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - - // 191 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - - // 204 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - - // 217 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - - // 230 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - - // 243 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255 - }; - - /* ******** D E T E R M I N E W H I C H A L H A B E T ******** */ - - /** - * Returns one of the _SOMETHING_ALPHABET byte arrays depending on the - * options specified. It's possible, though silly, to specify ORDERED - * and URLSAFE in which case one of them will be picked, though there - * is no guarantee as to which one will be picked. - */ - private final static byte[] getAlphabet(int options) { - if ((options & URL_SAFE) == URL_SAFE) { - return _URL_SAFE_ALPHABET; - } else if ((options & ORDERED) == ORDERED) { - return _ORDERED_ALPHABET; - } else { - return _STANDARD_ALPHABET; - } - } // end getAlphabet - - /** - * Returns one of the _SOMETHING_DECODABET byte arrays depending on the - * options specified. It's possible, though silly, to specify ORDERED and - * URL_SAFE in which case one of them will be picked, though there is no - * guarantee as to which one will be picked. - */ - private final static byte[] getDecodabet(int options) { - if ((options & URL_SAFE) == URL_SAFE) { - return _URL_SAFE_DECODABET; - } else if ((options & ORDERED) == ORDERED) { - return _ORDERED_DECODABET; - } else { - return _STANDARD_DECODABET; - } - } // end getAlphabet - - /** Defeats instantiation. */ - private Base64() { - } - - /* ******** E N C O D I N G M E T H O D S ******** */ - - /** - * Encodes up to the first three bytes of array threeBytes and - * returns a four-byte array in Base64 notation. The actual number of - * significant bytes in your array is given by numSigBytes. The - * array threeBytes needs only be as big as - * numSigBytes. Code can reuse a byte array by passing a - * four-byte array as b4. - * - * @param b4 - * A reusable byte array to reduce array instantiation - * @param threeBytes - * the array to convert - * @param numSigBytes - * the number of significant bytes in your array - * @return four byte array in Base64 notation. - * @since 1.5.1 - */ - private static byte[] encode3to4(byte[] b4, byte[] threeBytes, - int numSigBytes, int options) { - encode3to4(threeBytes, 0, numSigBytes, b4, 0, options); - return b4; - } // end encode3to4 - - /** - *

- * Encodes up to three bytes of the array source and writes the - * resulting four Base64 bytes to destination. The source and - * destination arrays can be manipulated anywhere along their length by - * specifying srcOffset and destOffset. This method - * does not check to make sure your arrays are large enough to accomodate - * srcOffset + 3 for the source array or - * destOffset + 4 for the destination array. The - * actual number of significant bytes in your array is given by - * numSigBytes. - *

- *

- * This is the lowest level of the encoding methods with all possible - * parameters. - *

- * - * @param source - * the array to convert - * @param srcOffset - * the index where conversion begins - * @param numSigBytes - * the number of significant bytes in your array - * @param destination - * the array to hold the conversion - * @param destOffset - * the index where output will be put - * @return the destination array - * @since 1.3 - */ - private static byte[] encode3to4(byte[] source, int srcOffset, - int numSigBytes, byte[] destination, int destOffset, int options) { - - byte[] ALPHABET = getAlphabet(options); - - // 1 2 3 - // 01234567890123456789012345678901 Bit position - // --------000000001111111122222222 Array position from threeBytes - // --------| || || || | Six bit groups to index ALPHABET - // >>18 >>12 >> 6 >> 0 Right shift necessary - // 0x3f 0x3f 0x3f Additional AND - - // Create buffer with zero-padding if there are only one or two - // significant bytes passed in the array. - // We have to shift left 24 in order to flush out the 1's that appear - // when Java treats a value as negative that is cast from a byte to an - // int. - int inBuff = (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) - | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) - | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); - - switch (numSigBytes) { - case 3: - destination[destOffset] = ALPHABET[(inBuff >>> 18)]; - destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f]; - destination[destOffset + 3] = ALPHABET[(inBuff) & 0x3f]; - return destination; - - case 2: - destination[destOffset] = ALPHABET[(inBuff >>> 18)]; - destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f]; - destination[destOffset + 3] = EQUALS_SIGN; - return destination; - - case 1: - destination[destOffset] = ALPHABET[(inBuff >>> 18)]; - destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = EQUALS_SIGN; - destination[destOffset + 3] = EQUALS_SIGN; - return destination; - - default: - return destination; - } // end switch - } // end encode3to4 - - /** - * Performs Base64 encoding on the raw ByteBuffer, writing it - * to the encoded ByteBuffer. This is an experimental feature. - * Currently it does not pass along any options (such as - * {@link #DO_BREAK_LINES} or {@link #GZIP}. - * - * @param raw - * input buffer - * @param encoded - * output buffer - * @since 2.3 - */ - public static void encode(java.nio.ByteBuffer raw, - java.nio.ByteBuffer encoded) { - byte[] raw3 = new byte[3]; - byte[] enc4 = new byte[4]; - - while (raw.hasRemaining()) { - int rem = Math.min(3, raw.remaining()); - raw.get(raw3, 0, rem); - Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS); - encoded.put(enc4); - } // end input remaining - } - - /** - * Performs Base64 encoding on the raw ByteBuffer, writing it - * to the encoded CharBuffer. This is an experimental feature. - * Currently it does not pass along any options (such as - * {@link #DO_BREAK_LINES} or {@link #GZIP}. - * - * @param raw - * input buffer - * @param encoded - * output buffer - * @since 2.3 - */ - public static void encode(java.nio.ByteBuffer raw, - java.nio.CharBuffer encoded) { - byte[] raw3 = new byte[3]; - byte[] enc4 = new byte[4]; - - while (raw.hasRemaining()) { - int rem = Math.min(3, raw.remaining()); - raw.get(raw3, 0, rem); - Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS); - for (int i = 0; i < 4; i++) { - encoded.put((char) (enc4[i] & 0xFF)); - } - } // end input remaining - } - - /** - * Serializes an object and returns the Base64-encoded version of that - * serialized object. - * - *

- * As of v 2.3, if the object cannot be serialized or there is another - * error, the method will throw an java.io.IOException. This is new to - * v2.3! In earlier versions, it just returned a null value, but in - * retrospect that's a pretty poor way to handle it. - *

- * - * The object is not GZip-compressed before being encoded. - * - * @param serializableObject - * The object to encode - * @return The Base64-encoded object - * @throws java.io.IOException - * if there is an error - * @throws NullPointerException - * if serializedObject is null - * @since 1.4 - */ - public static String encodeObject(java.io.Serializable serializableObject) - throws java.io.IOException { - return encodeObject(serializableObject, NO_OPTIONS); - } // end encodeObject - - /** - * Serializes an object and returns the Base64-encoded version of that - * serialized object. - * - *

- * As of v 2.3, if the object cannot be serialized or there is another - * error, the method will throw an java.io.IOException. This is new to - * v2.3! In earlier versions, it just returned a null value, but in - * retrospect that's a pretty poor way to handle it. - *

- * - * The object is not GZip-compressed before being encoded. - *

- * Example options: - * - *

-	 *   GZIP: gzip-compresses object before encoding it.
-	 *   DO_BREAK_LINES: break lines at 76 characters
-	 * 
- *

- * Example: encodeObject( myObj, Base64.GZIP ) or - *

- * Example: - * encodeObject( myObj, Base64.GZIP | Base64.DO_BREAK_LINES ) - * - * @param serializableObject - * The object to encode - * @param options - * Specified options - * @return The Base64-encoded object - * @see Base64#GZIP - * @see Base64#DO_BREAK_LINES - * @throws java.io.IOException - * if there is an error - * @since 2.0 - */ - public static String encodeObject(java.io.Serializable serializableObject, - int options) throws java.io.IOException { - - if (serializableObject == null) { - throw new NullPointerException("Cannot serialize a null object."); - } // end if: null - - // Streams - java.io.ByteArrayOutputStream baos = null; - java.io.OutputStream b64os = null; - java.util.zip.GZIPOutputStream gzos = null; - java.io.ObjectOutputStream oos = null; - - try { - // ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream - baos = new java.io.ByteArrayOutputStream(); - b64os = new Base64.OutputStream(baos, ENCODE | options); - if ((options & GZIP) != 0) { - // Gzip - gzos = new java.util.zip.GZIPOutputStream(b64os); - oos = new java.io.ObjectOutputStream(gzos); - } else { - // Not gzipped - oos = new java.io.ObjectOutputStream(b64os); - } - oos.writeObject(serializableObject); - } // end try - catch (java.io.IOException e) { - // Catch it and then throw it immediately so that - // the finally{} block is called for cleanup. - throw e; - } // end catch - finally { - try { - oos.close(); - } catch (Exception e) { - } - try { - gzos.close(); - } catch (Exception e) { - } - try { - b64os.close(); - } catch (Exception e) { - } - try { - baos.close(); - } catch (Exception e) { - } - } // end finally - - // Return value according to relevant encoding. - try { - return new String(baos.toByteArray(), PREFERRED_ENCODING); - } // end try - catch (java.io.UnsupportedEncodingException uue) { - // Fall back to some Java default - return new String(baos.toByteArray()); - } // end catch - - } // end encode - - /** - * Encodes a byte array into Base64 notation. Does not GZip-compress data. - * - * @param source - * The data to convert - * @return The data in Base64-encoded form - * @throws NullPointerException - * if source array is null - * @since 1.4 - */ - public static String encodeBytes(byte[] source) { - // Since we're not going to have the GZIP encoding turned on, - // we're not going to have an java.io.IOException thrown, so - // we should not force the user to have to catch it. - String encoded = null; - try { - encoded = encodeBytes(source, 0, source.length, NO_OPTIONS); - } catch (java.io.IOException ex) { - assert false : ex.getMessage(); - } // end catch - assert encoded != null; - return encoded; - } // end encodeBytes - - /** - * Encodes a byte array into Base64 notation. - *

- * Example options: - * - *

-	 *   GZIP: gzip-compresses object before encoding it.
-	 *   DO_BREAK_LINES: break lines at 76 characters
-	 *     Note: Technically, this makes your encoding non-compliant.
-	 * 
- *

- * Example: encodeBytes( myData, Base64.GZIP ) or - *

- * Example: - * encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES ) - * - * - *

- * As of v 2.3, if there is an error with the GZIP stream, the method will - * throw an java.io.IOException. This is new to v2.3! In earlier - * versions, it just returned a null value, but in retrospect that's a - * pretty poor way to handle it. - *

- * - * - * @param source - * The data to convert - * @param options - * Specified options - * @return The Base64-encoded data as a String - * @see Base64#GZIP - * @see Base64#DO_BREAK_LINES - * @throws java.io.IOException - * if there is an error - * @throws NullPointerException - * if source array is null - * @since 2.0 - */ - public static String encodeBytes(byte[] source, int options) - throws java.io.IOException { - return encodeBytes(source, 0, source.length, options); - } // end encodeBytes - - /** - * Encodes a byte array into Base64 notation. Does not GZip-compress data. - * - *

- * As of v 2.3, if there is an error, the method will throw an - * java.io.IOException. This is new to v2.3! In earlier versions, it - * just returned a null value, but in retrospect that's a pretty poor way to - * handle it. - *

- * - * - * @param source - * The data to convert - * @param off - * Offset in array where conversion should begin - * @param len - * Length of data to convert - * @return The Base64-encoded data as a String - * @throws NullPointerException - * if source array is null - * @throws IllegalArgumentException - * if source array, offset, or length are invalid - * @since 1.4 - */ - public static String encodeBytes(byte[] source, int off, int len) { - // Since we're not going to have the GZIP encoding turned on, - // we're not going to have an java.io.IOException thrown, so - // we should not force the user to have to catch it. - String encoded = null; - try { - encoded = encodeBytes(source, off, len, NO_OPTIONS); - } catch (java.io.IOException ex) { - assert false : ex.getMessage(); - } // end catch - assert encoded != null; - return encoded; - } // end encodeBytes - - /** - * Encodes a byte array into Base64 notation. - *

- * Example options: - * - *

-	 *   GZIP: gzip-compresses object before encoding it.
-	 *   DO_BREAK_LINES: break lines at 76 characters
-	 *     Note: Technically, this makes your encoding non-compliant.
-	 * 
- *

- * Example: encodeBytes( myData, Base64.GZIP ) or - *

- * Example: - * encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES ) - * - * - *

- * As of v 2.3, if there is an error with the GZIP stream, the method will - * throw an java.io.IOException. This is new to v2.3! In earlier - * versions, it just returned a null value, but in retrospect that's a - * pretty poor way to handle it. - *

- * - * - * @param source - * The data to convert - * @param off - * Offset in array where conversion should begin - * @param len - * Length of data to convert - * @param options - * Specified options - * @return The Base64-encoded data as a String - * @see Base64#GZIP - * @see Base64#DO_BREAK_LINES - * @throws java.io.IOException - * if there is an error - * @throws NullPointerException - * if source array is null - * @throws IllegalArgumentException - * if source array, offset, or length are invalid - * @since 2.0 - */ - public static String encodeBytes(byte[] source, int off, int len, - int options) throws java.io.IOException { - byte[] encoded = encodeBytesToBytes(source, off, len, options); - - // Return value according to relevant encoding. - try { - return new String(encoded, PREFERRED_ENCODING); - } // end try - catch (java.io.UnsupportedEncodingException uue) { - return new String(encoded); - } // end catch - - } // end encodeBytes - - /** - * Similar to {@link #encodeBytes(byte[])} but returns a byte array instead - * of instantiating a String. This is more efficient if you're working with - * I/O streams and have large data sets to encode. - * - * - * @param source - * The data to convert - * @return The Base64-encoded data as a byte[] (of ASCII characters) - * @throws NullPointerException - * if source array is null - * @since 2.3.1 - */ - public static byte[] encodeBytesToBytes(byte[] source) { - byte[] encoded = null; - try { - encoded = encodeBytesToBytes(source, 0, source.length, - Base64.NO_OPTIONS); - } catch (java.io.IOException ex) { - assert false : "IOExceptions only come from GZipping, which is turned off: " - + ex.getMessage(); - } - return encoded; - } - - /** - * Similar to {@link #encodeBytes(byte[], int, int, int)} but returns a byte - * array instead of instantiating a String. This is more efficient if you're - * working with I/O streams and have large data sets to encode. - * - * - * @param source - * The data to convert - * @param off - * Offset in array where conversion should begin - * @param len - * Length of data to convert - * @param options - * Specified options - * @return The Base64-encoded data as a String - * @see Base64#GZIP - * @see Base64#DO_BREAK_LINES - * @throws java.io.IOException - * if there is an error - * @throws NullPointerException - * if source array is null - * @throws IllegalArgumentException - * if source array, offset, or length are invalid - * @since 2.3.1 - */ - public static byte[] encodeBytesToBytes(byte[] source, int off, int len, - int options) throws java.io.IOException { - - if (source == null) { - throw new NullPointerException("Cannot serialize a null array."); - } // end if: null - - if (off < 0) { - throw new IllegalArgumentException("Cannot have negative offset: " - + off); - } // end if: off < 0 - - if (len < 0) { - throw new IllegalArgumentException("Cannot have length offset: " - + len); - } // end if: len < 0 - - if (off + len > source.length) { - throw new IllegalArgumentException( - String.format( - "Cannot have offset of %d and length of %d with array of length %d", - off, len, source.length)); - } // end if: off < 0 - - // Compress? - if ((options & GZIP) != 0) { - java.io.ByteArrayOutputStream baos = null; - java.util.zip.GZIPOutputStream gzos = null; - Base64.OutputStream b64os = null; - - try { - // GZip -> Base64 -> ByteArray - baos = new java.io.ByteArrayOutputStream(); - b64os = new Base64.OutputStream(baos, ENCODE | options); - gzos = new java.util.zip.GZIPOutputStream(b64os); - - gzos.write(source, off, len); - gzos.close(); - } // end try - catch (java.io.IOException e) { - // Catch it and then throw it immediately so that - // the finally{} block is called for cleanup. - throw e; - } // end catch - finally { - try { - gzos.close(); - } catch (Exception e) { - } - try { - b64os.close(); - } catch (Exception e) { - } - try { - baos.close(); - } catch (Exception e) { - } - } // end finally - - return baos.toByteArray(); - } // end if: compress - - // Else, don't compress. Better not to use streams at all then. - else { - boolean breakLines = (options & DO_BREAK_LINES) != 0; - - // int len43 = len * 4 / 3; - // byte[] outBuff = new byte[ ( len43 ) // Main 4:3 - // + ( (len % 3) > 0 ? 4 : 0 ) // Account for padding - // + (breakLines ? ( len43 / MAX_LINE_LENGTH ) : 0) ]; // New lines - // Try to determine more precisely how big the array needs to be. - // If we get it right, we don't have to do an array copy, and - // we save a bunch of memory. - int encLen = (len / 3) * 4 + (len % 3 > 0 ? 4 : 0); // Bytes needed - // for actual - // encoding - if (breakLines) { - encLen += encLen / MAX_LINE_LENGTH; // Plus extra newline - // characters - } - byte[] outBuff = new byte[encLen]; - - int d = 0; - int e = 0; - int len2 = len - 2; - int lineLength = 0; - for (; d < len2; d += 3, e += 4) { - encode3to4(source, d + off, 3, outBuff, e, options); - - lineLength += 4; - if (breakLines && lineLength >= MAX_LINE_LENGTH) { - outBuff[e + 4] = NEW_LINE; - e++; - lineLength = 0; - } // end if: end of line - } // en dfor: each piece of array - - if (d < len) { - encode3to4(source, d + off, len - d, outBuff, e, options); - e += 4; - } // end if: some padding needed - - // Only resize array if we didn't guess it right. - if (e <= outBuff.length - 1) { - // If breaking lines and the last byte falls right at - // the line length (76 bytes per line), there will be - // one extra byte, and the array will need to be resized. - // Not too bad of an estimate on array size, I'd say. - byte[] finalOut = new byte[e]; - System.arraycopy(outBuff, 0, finalOut, 0, e); - // System.err.println("Having to resize array from " + - // outBuff.length + " to " + e ); - return finalOut; - } else { - // System.err.println("No need to resize array."); - return outBuff; - } - - } // end else: don't compress - - } // end encodeBytesToBytes - - /* ******** D E C O D I N G M E T H O D S ******** */ - - /** - * Decodes four bytes from array source and writes the resulting - * bytes (up to three of them) to destination. The source and - * destination arrays can be manipulated anywhere along their length by - * specifying srcOffset and destOffset. This method - * does not check to make sure your arrays are large enough to accomodate - * srcOffset + 4 for the source array or - * destOffset + 3 for the destination array. This - * method returns the actual number of bytes that were converted from the - * Base64 encoding. - *

- * This is the lowest level of the decoding methods with all possible - * parameters. - *

- * - * - * @param source - * the array to convert - * @param srcOffset - * the index where conversion begins - * @param destination - * the array to hold the conversion - * @param destOffset - * the index where output will be put - * @param options - * alphabet type is pulled from this (standard, url-safe, - * ordered) - * @return the number of decoded bytes converted - * @throws NullPointerException - * if source or destination arrays are null - * @throws IllegalArgumentException - * if srcOffset or destOffset are invalid or there is not enough - * room in the array. - * @since 1.3 - */ - private static int decode4to3(byte[] source, int srcOffset, - byte[] destination, int destOffset, int options) { - - // Lots of error checking and exception throwing - if (source == null) { - throw new NullPointerException("Source array was null."); - } // end if - if (destination == null) { - throw new NullPointerException("Destination array was null."); - } // end if - if (srcOffset < 0 || srcOffset + 3 >= source.length) { - throw new IllegalArgumentException( - String.format( - "Source array with length %d cannot have offset of %d and still process four bytes.", - source.length, srcOffset)); - } // end if - if (destOffset < 0 || destOffset + 2 >= destination.length) { - throw new IllegalArgumentException( - String.format( - "Destination array with length %d cannot have offset of %d and still store three bytes.", - destination.length, destOffset)); - } // end if - - byte[] DECODABET = getDecodabet(options); - - // Example: Dk== - if (source[srcOffset + 2] == EQUALS_SIGN) { - // Two ways to do the same thing. Don't know which way I like best. - // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 - // ) - // | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 ); - int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) - | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12); - - destination[destOffset] = (byte) (outBuff >>> 16); - return 1; - } - - // Example: DkL= - else if (source[srcOffset + 3] == EQUALS_SIGN) { - // Two ways to do the same thing. Don't know which way I like best. - // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 - // ) - // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) - // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ); - int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) - | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12) - | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6); - - destination[destOffset] = (byte) (outBuff >>> 16); - destination[destOffset + 1] = (byte) (outBuff >>> 8); - return 2; - } - - // Example: DkLE - else { - // Two ways to do the same thing. Don't know which way I like best. - // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 - // ) - // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) - // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ) - // | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 ); - int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) - | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12) - | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6) - | ((DECODABET[source[srcOffset + 3]] & 0xFF)); - - destination[destOffset] = (byte) (outBuff >> 16); - destination[destOffset + 1] = (byte) (outBuff >> 8); - destination[destOffset + 2] = (byte) (outBuff); - - return 3; - } - } // end decodeToBytes - - /** - * Low-level access to decoding ASCII characters in the form of a byte - * array. Ignores GUNZIP option, if it's set. This is not - * generally a recommended method, although it is used internally as part of - * the decoding process. Special case: if len = 0, an empty array is - * returned. Still, if you need more speed and reduced memory footprint (and - * aren't gzipping), consider this method. - * - * @param source - * The Base64 encoded data - * @return decoded data - * @since 2.3.1 - */ - public static byte[] decode(byte[] source) throws java.io.IOException { - byte[] decoded = null; - // try { - decoded = decode(source, 0, source.length, Base64.NO_OPTIONS); - // } catch( java.io.IOException ex ) { - // assert false : - // "IOExceptions only come from GZipping, which is turned off: " + - // ex.getMessage(); - // } - return decoded; - } - - /** - * Low-level access to decoding ASCII characters in the form of a byte - * array. Ignores GUNZIP option, if it's set. This is not - * generally a recommended method, although it is used internally as part of - * the decoding process. Special case: if len = 0, an empty array is - * returned. Still, if you need more speed and reduced memory footprint (and - * aren't gzipping), consider this method. - * - * @param source - * The Base64 encoded data - * @param off - * The offset of where to begin decoding - * @param len - * The length of characters to decode - * @param options - * Can specify options such as alphabet type to use - * @return decoded data - * @throws java.io.IOException - * If bogus characters exist in source data - * @since 1.3 - */ - public static byte[] decode(byte[] source, int off, int len, int options) - throws java.io.IOException { - - // Lots of error checking and exception throwing - if (source == null) { - throw new NullPointerException("Cannot decode null source array."); - } // end if - if (off < 0 || off + len > source.length) { - throw new IllegalArgumentException( - String.format( - "Source array with length %d cannot have offset of %d and process %d bytes.", - source.length, off, len)); - } // end if - - if (len == 0) { - return new byte[0]; - } else if (len < 4) { - throw new IllegalArgumentException( - "Base64-encoded string must have at least four characters, but length specified was " - + len); - } // end if - - byte[] DECODABET = getDecodabet(options); - - int len34 = len * 3 / 4; // Estimate on array size - byte[] outBuff = new byte[len34]; // Upper limit on size of output - int outBuffPosn = 0; // Keep track of where we're writing - - byte[] b4 = new byte[4]; // Four byte buffer from source, eliminating - // white space - int b4Posn = 0; // Keep track of four byte input buffer - int i = 0; // Source array counter - byte sbiDecode = 0; // Special value from DECODABET - - for (i = off; i < off + len; i++) { // Loop through source - - sbiDecode = DECODABET[source[i] & 0xFF]; - - // White space, Equals sign, or legit Base64 character - // Note the values such as -5 and -9 in the - // DECODABETs at the top of the file. - if (sbiDecode >= WHITE_SPACE_ENC) { - if (sbiDecode >= EQUALS_SIGN_ENC) { - b4[b4Posn++] = source[i]; // Save non-whitespace - if (b4Posn > 3) { // Time to decode? - outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, - options); - b4Posn = 0; - - // If that was the equals sign, break out of 'for' loop - if (source[i] == EQUALS_SIGN) { - break; - } // end if: equals sign - } // end if: quartet built - } // end if: equals sign or better - } // end if: white space, equals sign or better - else { - // There's a bad input character in the Base64 stream. - throw new java.io.IOException( - String.format( - "Bad Base64 input character decimal %d in array position %d", - source[i] & 0xFF, i)); - } // end else: - } // each input character - - byte[] out = new byte[outBuffPosn]; - System.arraycopy(outBuff, 0, out, 0, outBuffPosn); - return out; - } // end decode - - /** - * Decodes data from Base64 notation, automatically detecting - * gzip-compressed data and decompressing it. - * - * @param s - * the string to decode - * @return the decoded data - * @throws java.io.IOException - * If there is a problem - * @since 1.4 - */ - public static byte[] decode(String s) throws java.io.IOException { - return decode(s, NO_OPTIONS); - } - - /** - * Decodes data from Base64 notation, automatically detecting - * gzip-compressed data and decompressing it. - * - * @param s - * the string to decode - * @param options - * encode options such as URL_SAFE - * @return the decoded data - * @throws java.io.IOException - * if there is an error - * @throws NullPointerException - * if s is null - * @since 1.4 - */ - public static byte[] decode(String s, int options) - throws java.io.IOException { - - if (s == null) { - throw new NullPointerException("Input string was null."); - } // end if - - byte[] bytes; - try { - bytes = s.getBytes(PREFERRED_ENCODING); - } // end try - catch (java.io.UnsupportedEncodingException uee) { - bytes = s.getBytes(); - } // end catch - // - - // Decode - bytes = decode(bytes, 0, bytes.length, options); - - // Check to see if it's gzip-compressed - // GZIP Magic Two-Byte Number: 0x8b1f (35615) - boolean dontGunzip = (options & DONT_GUNZIP) != 0; - if ((bytes != null) && (bytes.length >= 4) && (!dontGunzip)) { - - int head = (bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00); - if (java.util.zip.GZIPInputStream.GZIP_MAGIC == head) { - java.io.ByteArrayInputStream bais = null; - java.util.zip.GZIPInputStream gzis = null; - java.io.ByteArrayOutputStream baos = null; - byte[] buffer = new byte[2048]; - int length = 0; - - try { - baos = new java.io.ByteArrayOutputStream(); - bais = new java.io.ByteArrayInputStream(bytes); - gzis = new java.util.zip.GZIPInputStream(bais); - - while ((length = gzis.read(buffer)) >= 0) { - baos.write(buffer, 0, length); - } // end while: reading input - - // No error? Get new bytes. - bytes = baos.toByteArray(); - - } // end try - catch (java.io.IOException e) { - e.printStackTrace(); - // Just return originally-decoded bytes - } // end catch - finally { - try { - baos.close(); - } catch (Exception e) { - } - try { - gzis.close(); - } catch (Exception e) { - } - try { - bais.close(); - } catch (Exception e) { - } - } // end finally - - } // end if: gzipped - } // end if: bytes.length >= 2 - - return bytes; - } // end decode - - /** - * Attempts to decode Base64 data and deserialize a Java Object within. - * Returns null if there was an error. - * - * @param encodedObject - * The Base64 data to decode - * @return The decoded and deserialized object - * @throws NullPointerException - * if encodedObject is null - * @throws java.io.IOException - * if there is a general error - * @throws ClassNotFoundException - * if the decoded object is of a class that cannot be found by - * the JVM - * @since 1.5 - */ - public static Object decodeToObject(String encodedObject) - throws java.io.IOException, java.lang.ClassNotFoundException { - return decodeToObject(encodedObject, NO_OPTIONS, null); - } - - /** - * Attempts to decode Base64 data and deserialize a Java Object within. - * Returns null if there was an error. If loader is not - * null, it will be the class loader used when deserializing. - * - * @param encodedObject - * The Base64 data to decode - * @param options - * Various parameters related to decoding - * @param loader - * Optional class loader to use in deserializing classes. - * @return The decoded and deserialized object - * @throws NullPointerException - * if encodedObject is null - * @throws java.io.IOException - * if there is a general error - * @throws ClassNotFoundException - * if the decoded object is of a class that cannot be found by - * the JVM - * @since 2.3.4 - */ - public static Object decodeToObject(String encodedObject, int options, - final ClassLoader loader) throws java.io.IOException, - java.lang.ClassNotFoundException { - - // Decode and gunzip if necessary - byte[] objBytes = decode(encodedObject, options); - - java.io.ByteArrayInputStream bais = null; - java.io.ObjectInputStream ois = null; - Object obj = null; - - try { - bais = new java.io.ByteArrayInputStream(objBytes); - - // If no custom class loader is provided, use Java's builtin OIS. - if (loader == null) { - ois = new java.io.ObjectInputStream(bais); - } // end if: no loader provided - - // Else make a customized object input stream that uses - // the provided class loader. - else { - ois = new java.io.ObjectInputStream(bais) { - @Override - public Class resolveClass( - java.io.ObjectStreamClass streamClass) - throws java.io.IOException, ClassNotFoundException { - Class c = Class.forName(streamClass.getName(), false, - loader); - if (c == null) { - return super.resolveClass(streamClass); - } else { - return c; // Class loader knows of this class. - } // end else: not null - } // end resolveClass - }; // end ois - } // end else: no custom class loader - - obj = ois.readObject(); - } // end try - catch (java.io.IOException e) { - throw e; // Catch and throw in order to execute finally{} - } // end catch - catch (java.lang.ClassNotFoundException e) { - throw e; // Catch and throw in order to execute finally{} - } // end catch - finally { - try { - bais.close(); - } catch (Exception e) { - } - try { - ois.close(); - } catch (Exception e) { - } - } // end finally - - return obj; - } // end decodeObject - - /** - * Convenience method for encoding data to a file. - * - *

- * As of v 2.3, if there is a error, the method will throw an - * java.io.IOException. This is new to v2.3! In earlier versions, it - * just returned false, but in retrospect that's a pretty poor way to handle - * it. - *

- * - * @param dataToEncode - * byte array of data to encode in base64 form - * @param filename - * Filename for saving encoded data - * @throws java.io.IOException - * if there is an error - * @throws NullPointerException - * if dataToEncode is null - * @since 2.1 - */ - public static void encodeToFile(byte[] dataToEncode, String filename) - throws java.io.IOException { - - if (dataToEncode == null) { - throw new NullPointerException("Data to encode was null."); - } // end iff - - Base64.OutputStream bos = null; - try { - bos = new Base64.OutputStream( - new java.io.FileOutputStream(filename), Base64.ENCODE); - bos.write(dataToEncode); - } // end try - catch (java.io.IOException e) { - throw e; // Catch and throw to execute finally{} block - } // end catch: java.io.IOException - finally { - try { - bos.close(); - } catch (Exception e) { - } - } // end finally - - } // end encodeToFile - - /** - * Convenience method for decoding data to a file. - * - *

- * As of v 2.3, if there is a error, the method will throw an - * java.io.IOException. This is new to v2.3! In earlier versions, it - * just returned false, but in retrospect that's a pretty poor way to handle - * it. - *

- * - * @param dataToDecode - * Base64-encoded data as a string - * @param filename - * Filename for saving decoded data - * @throws java.io.IOException - * if there is an error - * @since 2.1 - */ - public static void decodeToFile(String dataToDecode, String filename) - throws java.io.IOException { - - Base64.OutputStream bos = null; - try { - bos = new Base64.OutputStream( - new java.io.FileOutputStream(filename), Base64.DECODE); - bos.write(dataToDecode.getBytes(PREFERRED_ENCODING)); - } // end try - catch (java.io.IOException e) { - throw e; // Catch and throw to execute finally{} block - } // end catch: java.io.IOException - finally { - try { - bos.close(); - } catch (Exception e) { - } - } // end finally - - } // end decodeToFile - - /** - * Convenience method for reading a base64-encoded file and decoding it. - * - *

- * As of v 2.3, if there is a error, the method will throw an - * java.io.IOException. This is new to v2.3! In earlier versions, it - * just returned false, but in retrospect that's a pretty poor way to handle - * it. - *

- * - * @param filename - * Filename for reading encoded data - * @return decoded byte array - * @throws java.io.IOException - * if there is an error - * @since 2.1 - */ - public static byte[] decodeFromFile(String filename) - throws java.io.IOException { - - byte[] decodedData = null; - Base64.InputStream bis = null; - try { - // Set up some useful variables - java.io.File file = new java.io.File(filename); - byte[] buffer = null; - int length = 0; - int numBytes = 0; - - // Check for size of file - if (file.length() > Integer.MAX_VALUE) { - throw new java.io.IOException( - "File is too big for this convenience method (" - + file.length() + " bytes)."); - } // end if: file too big for int index - buffer = new byte[(int) file.length()]; - - // Open a stream - bis = new Base64.InputStream(new java.io.BufferedInputStream( - new java.io.FileInputStream(file)), Base64.DECODE); - - // Read until done - while ((numBytes = bis.read(buffer, length, 4096)) >= 0) { - length += numBytes; - } // end while - - // Save in a variable to return - decodedData = new byte[length]; - System.arraycopy(buffer, 0, decodedData, 0, length); - - } // end try - catch (java.io.IOException e) { - throw e; // Catch and release to execute finally{} - } // end catch: java.io.IOException - finally { - try { - bis.close(); - } catch (Exception e) { - } - } // end finally - - return decodedData; - } // end decodeFromFile - - /** - * Convenience method for reading a binary file and base64-encoding it. - * - *

- * As of v 2.3, if there is a error, the method will throw an - * java.io.IOException. This is new to v2.3! In earlier versions, it - * just returned false, but in retrospect that's a pretty poor way to handle - * it. - *

- * - * @param filename - * Filename for reading binary data - * @return base64-encoded string - * @throws java.io.IOException - * if there is an error - * @since 2.1 - */ - public static String encodeFromFile(String filename) - throws java.io.IOException { - - String encodedData = null; - Base64.InputStream bis = null; - try { - // Set up some useful variables - java.io.File file = new java.io.File(filename); - byte[] buffer = new byte[Math.max((int) (file.length() * 1.4 + 1), - 40)]; // Need max() for math on small files (v2.2.1); Need - // +1 for a few corner cases (v2.3.5) - int length = 0; - int numBytes = 0; - - // Open a stream - bis = new Base64.InputStream(new java.io.BufferedInputStream( - new java.io.FileInputStream(file)), Base64.ENCODE); - - // Read until done - while ((numBytes = bis.read(buffer, length, 4096)) >= 0) { - length += numBytes; - } // end while - - // Save in a variable to return - encodedData = new String(buffer, 0, length, - Base64.PREFERRED_ENCODING); - - } // end try - catch (java.io.IOException e) { - throw e; // Catch and release to execute finally{} - } // end catch: java.io.IOException - finally { - try { - bis.close(); - } catch (Exception e) { - } - } // end finally - - return encodedData; - } // end encodeFromFile - - /** - * Reads infile and encodes it to outfile. - * - * @param infile - * Input file - * @param outfile - * Output file - * @throws java.io.IOException - * if there is an error - * @since 2.2 - */ - public static void encodeFileToFile(String infile, String outfile) - throws java.io.IOException { - - String encoded = Base64.encodeFromFile(infile); - java.io.OutputStream out = null; - try { - out = new java.io.BufferedOutputStream( - new java.io.FileOutputStream(outfile)); - out.write(encoded.getBytes("US-ASCII")); // Strict, 7-bit output. - } // end try - catch (java.io.IOException e) { - throw e; // Catch and release to execute finally{} - } // end catch - finally { - try { - out.close(); - } catch (Exception ex) { - } - } // end finally - } // end encodeFileToFile - - /** - * Reads infile and decodes it to outfile. - * - * @param infile - * Input file - * @param outfile - * Output file - * @throws java.io.IOException - * if there is an error - * @since 2.2 - */ - public static void decodeFileToFile(String infile, String outfile) - throws java.io.IOException { - - byte[] decoded = Base64.decodeFromFile(infile); - java.io.OutputStream out = null; - try { - out = new java.io.BufferedOutputStream( - new java.io.FileOutputStream(outfile)); - out.write(decoded); - } // end try - catch (java.io.IOException e) { - throw e; // Catch and release to execute finally{} - } // end catch - finally { - try { - out.close(); - } catch (Exception ex) { - } - } // end finally - } // end decodeFileToFile - - /* ******** I N N E R C L A S S I N P U T S T R E A M ******** */ - - /** - * A {@link Base64.InputStream} will read data from another - * java.io.InputStream, given in the constructor, and encode/decode - * to/from Base64 notation on the fly. - * - * @see Base64 - * @since 1.3 - */ - public static class InputStream extends java.io.FilterInputStream { - - private boolean encode; // Encoding or decoding - private int position; // Current position in the buffer - private byte[] buffer; // Small buffer holding converted data - private int bufferLength; // Length of buffer (3 or 4) - private int numSigBytes; // Number of meaningful bytes in the buffer - private int lineLength; - private boolean breakLines; // Break lines at less than 80 characters - private int options; // Record options used to create the stream. - private byte[] decodabet; // Local copies to avoid extra method calls - - /** - * Constructs a {@link Base64.InputStream} in DECODE mode. - * - * @param in - * the java.io.InputStream from which to read data. - * @since 1.3 - */ - public InputStream(java.io.InputStream in) { - this(in, DECODE); - } // end constructor - - /** - * Constructs a {@link Base64.InputStream} in either ENCODE or DECODE - * mode. - *

- * Valid options: - * - *

-		 *   ENCODE or DECODE: Encode or Decode as data is read.
-		 *   DO_BREAK_LINES: break lines at 76 characters
-		 *     (only meaningful when encoding)
-		 * 
- *

- * Example: new Base64.InputStream( in, Base64.DECODE ) - * - * - * @param in - * the java.io.InputStream from which to read data. - * @param options - * Specified options - * @see Base64#ENCODE - * @see Base64#DECODE - * @see Base64#DO_BREAK_LINES - * @since 2.0 - */ - public InputStream(java.io.InputStream in, int options) { - - super(in); - this.options = options; // Record for later - this.breakLines = (options & DO_BREAK_LINES) > 0; - this.encode = (options & ENCODE) > 0; - this.bufferLength = encode ? 4 : 3; - this.buffer = new byte[bufferLength]; - this.position = -1; - this.lineLength = 0; - this.decodabet = getDecodabet(options); - } // end constructor - - /** - * Reads enough of the input stream to convert to/from Base64 and - * returns the next byte. - * - * @return next byte - * @since 1.3 - */ - @Override - public int read() throws java.io.IOException { - - // Do we need to get data? - if (position < 0) { - if (encode) { - byte[] b3 = new byte[3]; - int numBinaryBytes = 0; - for (int i = 0; i < 3; i++) { - int b = in.read(); - - // If end of stream, b is -1. - if (b >= 0) { - b3[i] = (byte) b; - numBinaryBytes++; - } else { - break; // out of for loop - } // end else: end of stream - - } // end for: each needed input byte - - if (numBinaryBytes > 0) { - encode3to4(b3, 0, numBinaryBytes, buffer, 0, options); - position = 0; - numSigBytes = 4; - } // end if: got data - else { - return -1; // Must be end of stream - } // end else - } // end if: encoding - - // Else decoding - else { - byte[] b4 = new byte[4]; - int i = 0; - for (i = 0; i < 4; i++) { - // Read four "meaningful" bytes: - int b = 0; - do { - b = in.read(); - } while (b >= 0 - && decodabet[b & 0x7f] <= WHITE_SPACE_ENC); - - if (b < 0) { - break; // Reads a -1 if end of stream - } // end if: end of stream - - b4[i] = (byte) b; - } // end for: each needed input byte - - if (i == 4) { - numSigBytes = decode4to3(b4, 0, buffer, 0, options); - position = 0; - } // end if: got four characters - else if (i == 0) { - return -1; - } // end else if: also padded correctly - else { - // Must have broken out from above. - throw new java.io.IOException( - "Improperly padded Base64 input."); - } // end - - } // end else: decode - } // end else: get data - - // Got data? - if (position >= 0) { - // End of relevant data? - if ( /* !encode && */position >= numSigBytes) { - return -1; - } // end if: got data - - if (encode && breakLines && lineLength >= MAX_LINE_LENGTH) { - lineLength = 0; - return '\n'; - } // end if - else { - lineLength++; // This isn't important when decoding - // but throwing an extra "if" seems - // just as wasteful. - - int b = buffer[position++]; - - if (position >= bufferLength) { - position = -1; - } // end if: end - - return b & 0xFF; // This is how you "cast" a byte that's - // intended to be unsigned. - } // end else - } // end if: position >= 0 - - // Else error - else { - throw new java.io.IOException( - "Error in Base64 code reading stream."); - } // end else - } // end read - - /** - * Calls {@link #read()} repeatedly until the end of stream is reached - * or len bytes are read. Returns number of bytes read into - * array or -1 if end of stream is encountered. - * - * @param dest - * array to hold values - * @param off - * offset for array - * @param len - * max number of bytes to read into array - * @return bytes read into array or -1 if end of stream is encountered. - * @since 1.3 - */ - @Override - public int read(byte[] dest, int off, int len) - throws java.io.IOException { - int i; - int b; - for (i = 0; i < len; i++) { - b = read(); - - if (b >= 0) { - dest[off + i] = (byte) b; - } else if (i == 0) { - return -1; - } else { - break; // Out of 'for' loop - } // Out of 'for' loop - } // end for: each byte read - return i; - } // end read - - } // end inner class InputStream - - /* ******** I N N E R C L A S S O U T P U T S T R E A M ******** */ - - /** - * A {@link Base64.OutputStream} will write data to another - * java.io.OutputStream, given in the constructor, and - * encode/decode to/from Base64 notation on the fly. - * - * @see Base64 - * @since 1.3 - */ - public static class OutputStream extends java.io.FilterOutputStream { - - private boolean encode; - private int position; - private byte[] buffer; - private int bufferLength; - private int lineLength; - private boolean breakLines; - private byte[] b4; // Scratch used in a few places - private boolean suspendEncoding; - private int options; // Record for later - private byte[] decodabet; // Local copies to avoid extra method calls - - /** - * Constructs a {@link Base64.OutputStream} in ENCODE mode. - * - * @param out - * the java.io.OutputStream to which data will be - * written. - * @since 1.3 - */ - public OutputStream(java.io.OutputStream out) { - this(out, ENCODE); - } // end constructor - - /** - * Constructs a {@link Base64.OutputStream} in either ENCODE or DECODE - * mode. - *

- * Valid options: - * - *

-		 *   ENCODE or DECODE: Encode or Decode as data is read.
-		 *   DO_BREAK_LINES: don't break lines at 76 characters
-		 *     (only meaningful when encoding)
-		 * 
- *

- * Example: new Base64.OutputStream( out, Base64.ENCODE ) - * - * @param out - * the java.io.OutputStream to which data will be - * written. - * @param options - * Specified options. - * @see Base64#ENCODE - * @see Base64#DECODE - * @see Base64#DO_BREAK_LINES - * @since 1.3 - */ - public OutputStream(java.io.OutputStream out, int options) { - super(out); - this.breakLines = (options & DO_BREAK_LINES) != 0; - this.encode = (options & ENCODE) != 0; - this.bufferLength = encode ? 3 : 4; - this.buffer = new byte[bufferLength]; - this.position = 0; - this.lineLength = 0; - this.suspendEncoding = false; - this.b4 = new byte[4]; - this.options = options; - this.decodabet = getDecodabet(options); - } // end constructor - - /** - * Writes the byte to the output stream after converting to/from Base64 - * notation. When encoding, bytes are buffered three at a time before - * the output stream actually gets a write() call. When decoding, bytes - * are buffered four at a time. - * - * @param theByte - * the byte to write - * @since 1.3 - */ - @Override - public void write(int theByte) throws java.io.IOException { - // Encoding suspended? - if (suspendEncoding) { - this.out.write(theByte); - return; - } // end if: supsended - - // Encode? - if (encode) { - buffer[position++] = (byte) theByte; - if (position >= bufferLength) { // Enough to encode. - - this.out.write(encode3to4(b4, buffer, bufferLength, options)); - - lineLength += 4; - if (breakLines && lineLength >= MAX_LINE_LENGTH) { - this.out.write(NEW_LINE); - lineLength = 0; - } // end if: end of line - - position = 0; - } // end if: enough to output - } // end if: encoding - - // Else, Decoding - else { - // Meaningful Base64 character? - if (decodabet[theByte & 0x7f] > WHITE_SPACE_ENC) { - buffer[position++] = (byte) theByte; - if (position >= bufferLength) { // Enough to output. - - int len = Base64.decode4to3(buffer, 0, b4, 0, options); - out.write(b4, 0, len); - position = 0; - } // end if: enough to output - } // end if: meaningful base64 character - else if (decodabet[theByte & 0x7f] != WHITE_SPACE_ENC) { - throw new java.io.IOException( - "Invalid character in Base64 data."); - } // end else: not white space either - } // end else: decoding - } // end write - - /** - * Calls {@link #write(int)} repeatedly until len bytes are - * written. - * - * @param theBytes - * array from which to read bytes - * @param off - * offset for array - * @param len - * max number of bytes to read into array - * @since 1.3 - */ - @Override - public void write(byte[] theBytes, int off, int len) - throws java.io.IOException { - // Encoding suspended? - if (suspendEncoding) { - this.out.write(theBytes, off, len); - return; - } // end if: supsended - - for (int i = 0; i < len; i++) { - write(theBytes[off + i]); - } // end for: each byte written - - } // end write - - /** - * Method added by PHIL. [Thanks, PHIL. -Rob] This pads the buffer - * without closing the stream. - * - * @throws java.io.IOException - * if there's an error. - */ - public void flushBase64() throws java.io.IOException { - if (position > 0) { - if (encode) { - out.write(encode3to4(b4, buffer, position, options)); - position = 0; - } // end if: encoding - else { - throw new java.io.IOException( - "Base64 input not properly padded."); - } // end else: decoding - } // end if: buffer partially full - - } // end flush - - /** - * Flushes and closes (I think, in the superclass) the stream. - * - * @since 1.3 - */ - @Override - public void close() throws java.io.IOException { - // 1. Ensure that pending characters are written - flushBase64(); - - // 2. Actually close the stream - // Base class both flushes and closes. - super.close(); - - buffer = null; - out = null; - } // end close - - /** - * Suspends encoding of the stream. May be helpful if you need to embed - * a piece of base64-encoded data in a stream. - * - * @throws java.io.IOException - * if there's an error flushing - * @since 1.5.1 - */ - public void suspendEncoding() throws java.io.IOException { - flushBase64(); - this.suspendEncoding = true; - } // end suspendEncoding - - /** - * Resumes encoding of the stream. May be helpful if you need to embed a - * piece of base64-encoded data in a stream. - * - * @since 1.5.1 - */ - public void resumeEncoding() { - this.suspendEncoding = false; - } // end resumeEncoding - - } // end inner class OutputStream - -} // end class Base64 diff --git a/app/src/main/java/org/proxydroid/utils/Base64.kt b/app/src/main/java/org/proxydroid/utils/Base64.kt new file mode 100644 index 00000000..a1a19ef6 --- /dev/null +++ b/app/src/main/java/org/proxydroid/utils/Base64.kt @@ -0,0 +1,49 @@ +/* proxydroid - Global / Individual Proxy App for Android + * Copyright (C) 2011 Max Lv + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package org.proxydroid.utils + +/** + * Wrapper for Android's Base64 utilities. + * This provides a consistent API for Base64 encoding/decoding. + */ +object Base64 { + const val NO_WRAP = android.util.Base64.NO_WRAP + const val DEFAULT = android.util.Base64.DEFAULT + const val URL_SAFE = android.util.Base64.URL_SAFE + const val NO_PADDING = android.util.Base64.NO_PADDING + + @JvmStatic + fun encode(input: ByteArray, flags: Int): ByteArray { + return android.util.Base64.encode(input, flags) + } + + @JvmStatic + fun encodeToString(input: ByteArray, flags: Int): String { + return android.util.Base64.encodeToString(input, flags) + } + + @JvmStatic + fun decode(str: String, flags: Int): ByteArray { + return android.util.Base64.decode(str, flags) + } + + @JvmStatic + fun decode(input: ByteArray, flags: Int): ByteArray { + return android.util.Base64.decode(input, flags) + } +} diff --git a/app/src/main/java/org/proxydroid/utils/Constraints.java b/app/src/main/java/org/proxydroid/utils/Constraints.java deleted file mode 100644 index 272b8469..00000000 --- a/app/src/main/java/org/proxydroid/utils/Constraints.java +++ /dev/null @@ -1,371 +0,0 @@ -package org.proxydroid.utils; - -public class Constraints { - - public static final String ONLY_3G = "2G/3G"; - public static final String ONLY_WIFI = "WIFI"; - public static final String WIFI_AND_3G = "WIFI/2G/3G"; - public static final String FILE_PATH = "file_path"; - public static final int IMPORT_REQUEST = 0; - - public static final String[][] PRESETS = { - {}, - { "127.0.0.1" }, - { "127.0.0.1", "10.0.0.0/8", "192.168.0.0/16", "172.16.0.0/12" }, - // China routes are gotten from https://github.com/17mon/china_ip_list - // Shrinked for better performance by remove subnets which have netmasks >= 20. - { - "1.0.32.0/19", "1.1.32.0/19", "1.2.32.0/19", "1.2.64.0/18", - "1.3.0.0/16", "1.4.32.0/19", "1.4.64.0/18", "1.8.32.0/19", - "1.8.64.0/19", "1.8.160.0/19", "1.8.192.0/19", "1.10.32.0/19", - "1.10.64.0/18", "1.12.0.0/14", "1.24.0.0/13", "1.45.0.0/16", - "1.48.0.0/14", "1.56.0.0/13", "1.68.0.0/14", "1.80.0.0/12", - "1.116.0.0/14", "1.180.0.0/14", "1.184.0.0/15", "1.188.0.0/14", - "1.192.0.0/13", "1.202.0.0/15", "1.204.0.0/14", "14.16.0.0/12", - "14.103.0.0/16", "14.104.0.0/13", "14.112.0.0/12", "14.130.0.0/15", - "14.134.0.0/15", "14.144.0.0/12", "14.196.0.0/15", "14.204.0.0/15", - "14.208.0.0/12", "27.8.0.0/13", "27.16.0.0/12", "27.36.0.0/14", - "27.40.0.0/13", "27.50.128.0/17", "27.54.192.0/18", "27.98.224.0/19", - "27.99.128.0/17", "27.103.0.0/16", "27.106.128.0/18", "27.109.32.0/19", - "27.112.0.0/18", "27.113.128.0/18", "27.115.0.0/17", "27.128.0.0/15", - "27.144.0.0/16", "27.148.0.0/14", "27.152.0.0/13", "27.184.0.0/13", - "27.192.0.0/11", "27.224.0.0/14", "36.0.32.0/19", "36.0.64.0/18", - "36.0.128.0/17", "36.1.0.0/16", "36.4.0.0/14", "36.16.0.0/12", - "36.32.0.0/14", "36.36.0.0/16", "36.37.0.0/19", "36.40.0.0/13", - "36.48.0.0/15", "36.51.0.0/17", "36.51.128.0/18", "36.51.192.0/19", - "36.56.0.0/13", "36.96.0.0/11", "36.128.0.0/10", "36.192.0.0/11", - "36.248.0.0/14", "36.254.0.0/16", "39.0.32.0/19", "39.0.64.0/18", - "39.0.128.0/17", "39.64.0.0/11", "39.96.0.0/13", "39.104.0.0/14", - "39.108.0.0/16", "39.128.0.0/10", "40.72.0.0/16", "40.125.128.0/17", - "40.126.64.0/18", "42.0.32.0/19", "42.0.128.0/19", "42.0.224.0/19", - "42.1.0.0/19", "42.4.0.0/14", "42.48.0.0/13", "42.56.0.0/14", - "42.62.0.0/17", "42.62.128.0/19", "42.63.0.0/16", "42.80.0.0/15", - "42.83.96.0/19", "42.83.160.0/19", "42.83.192.0/18", "42.84.0.0/14", - "42.88.0.0/13", "42.96.64.0/19", "42.96.128.0/17", "42.97.0.0/16", - "42.99.0.0/18", "42.99.64.0/19", "42.100.0.0/14", "42.120.0.0/15", - "42.122.0.0/16", "42.123.0.0/19", "42.123.64.0/18", "42.123.128.0/19", - "42.123.192.0/18", "42.128.0.0/12", "42.156.0.0/19", "42.156.64.0/18", - "42.156.128.0/17", "42.157.0.0/16", "42.158.0.0/15", "42.160.0.0/12", - "42.176.0.0/13", "42.184.0.0/15", "42.186.0.0/16", "42.187.0.0/18", - "42.187.64.0/19", "42.187.128.0/17", "42.192.0.0/13", "42.201.0.0/17", - "42.202.0.0/15", "42.204.0.0/14", "42.208.0.0/12", "42.224.0.0/12", - "42.240.0.0/16", "42.242.0.0/15", "42.244.0.0/15", "42.246.0.0/16", - "42.247.32.0/19", "42.247.64.0/18", "42.247.128.0/17", "42.248.0.0/13", - "43.226.32.0/19", "43.226.64.0/19", "43.226.128.0/18", "43.227.32.0/19", - "43.227.64.0/19", "43.227.192.0/19", "43.228.0.0/18", "43.230.224.0/19", - "43.236.0.0/15", "43.238.0.0/16", "43.239.0.0/19", "43.240.160.0/19", - "43.240.192.0/19", "43.246.0.0/18", "43.246.64.0/19", "43.247.224.0/19", - "43.248.96.0/19", "45.122.0.0/19", "45.122.64.0/19", "45.122.160.0/19", - "45.123.224.0/19", "45.221.0.0/16", "45.248.224.0/19", "45.251.160.0/19", - "45.251.192.0/19", "45.252.0.0/19", "45.252.96.0/19", "45.252.128.0/19", - "45.252.192.0/19", "45.253.0.0/18", "45.253.160.0/19", "45.253.192.0/18", - "45.254.0.0/19", "45.254.64.0/18", "45.254.128.0/18", "45.254.192.0/19", - "45.255.0.0/17", "45.255.160.0/19", "45.255.192.0/19", "47.92.0.0/14", - "47.96.0.0/11", "49.4.0.0/14", "49.51.32.0/19", "49.51.64.0/18", - "49.51.128.0/17", "49.52.0.0/14", "49.64.0.0/11", "49.112.0.0/13", - "49.120.0.0/14", "49.140.0.0/15", "49.152.0.0/14", "49.208.0.0/14", - "49.220.0.0/14", "49.232.0.0/14", "49.239.0.0/18", "49.239.192.0/18", - "52.80.0.0/16", "54.222.0.0/15", "58.14.0.0/15", "58.16.0.0/13", - "58.24.0.0/15", "58.30.0.0/15", "58.32.0.0/11", "58.66.0.0/15", - "58.68.128.0/19", "58.68.224.0/19", "58.82.0.0/17", "58.83.0.0/16", - "58.87.64.0/18", "58.99.128.0/17", "58.100.0.0/15", "58.116.0.0/14", - "58.128.0.0/13", "58.144.0.0/16", "58.154.0.0/15", "58.192.0.0/11", - "58.240.0.0/12", "59.32.0.0/11", "59.64.0.0/12", "59.81.0.0/16", - "59.82.0.0/16", "59.83.0.0/18", "59.83.128.0/17", "59.107.0.0/16", - "59.108.0.0/14", "59.151.0.0/17", "59.155.0.0/16", "59.172.0.0/14", - "59.191.0.0/17", "59.192.0.0/10", "60.0.0.0/11", "60.55.0.0/16", - "60.63.0.0/16", "60.160.0.0/11", "60.194.0.0/15", "60.200.0.0/13", - "60.208.0.0/12", "60.232.0.0/15", "60.235.0.0/16", "60.245.128.0/17", - "60.247.0.0/16", "60.252.0.0/16", "60.253.128.0/17", "60.255.0.0/16", - "61.28.0.0/17", "61.29.128.0/18", "61.29.192.0/19", "61.45.128.0/18", - "61.47.128.0/18", "61.48.0.0/13", "61.87.192.0/18", "61.128.0.0/10", - "61.232.0.0/14", "61.236.0.0/15", "61.240.0.0/14", "101.4.0.0/14", - "101.16.0.0/12", "101.32.0.0/15", "101.34.0.0/16", "101.35.32.0/19", - "101.35.96.0/19", "101.35.128.0/17", "101.36.0.0/18", "101.36.96.0/19", - "101.36.128.0/17", "101.37.0.0/16", "101.38.0.0/15", "101.40.0.0/13", - "101.48.32.0/19", "101.48.64.0/19", "101.48.128.0/17", "101.49.0.0/16", - "101.52.0.0/16", "101.54.0.0/16", "101.64.0.0/13", "101.72.0.0/14", - "101.76.0.0/15", "101.78.32.0/19", "101.80.0.0/12", "101.96.128.0/19", - "101.96.192.0/19", "101.99.96.0/19", "101.101.64.0/19", "101.102.64.0/19", - "101.104.0.0/14", "101.110.64.0/19", "101.120.0.0/14", "101.124.0.0/17", - "101.124.192.0/18", "101.125.0.0/16", "101.126.0.0/16", "101.128.32.0/19", - "101.129.0.0/16", "101.130.0.0/15", "101.132.0.0/14", "101.144.0.0/12", - "101.192.0.0/14", "101.196.0.0/17", "101.196.128.0/19", "101.196.192.0/18", - "101.197.0.0/16", "101.198.0.0/15", "101.200.0.0/15", "101.203.128.0/19", - "101.204.0.0/14", "101.224.0.0/13", "101.232.0.0/15", "101.234.96.0/19", - "101.236.0.0/17", "101.236.128.0/18", "101.236.192.0/19", "101.237.0.0/16", - "101.238.0.0/15", "101.240.0.0/13", "101.248.0.0/15", "101.251.32.0/19", - "101.251.64.0/18", "101.251.128.0/17", "101.252.0.0/15", "101.254.0.0/18", - "101.254.96.0/19", "101.254.128.0/17", "103.3.96.0/19", "103.22.0.0/18", - "103.22.64.0/19", "103.32.0.0/15", "103.34.0.0/16", "103.35.0.0/19", - "103.36.160.0/19", "103.36.192.0/19", "103.39.160.0/19", "103.45.0.0/18", - "103.45.96.0/19", "103.45.128.0/18", "103.45.192.0/19", "103.46.32.0/19", - "103.46.64.0/18", "103.62.96.0/19", "103.62.160.0/19", "103.63.32.0/19", - "103.64.32.0/19", "103.64.64.0/18", "103.64.160.0/19", "103.64.192.0/18", - "103.65.64.0/19", "103.192.0.0/19", "103.200.160.0/19", "103.200.224.0/19", - "103.201.32.0/19", "103.201.160.0/19", "103.201.192.0/18", "103.202.0.0/19", - "103.202.64.0/18", "103.202.160.0/19", "103.203.0.0/19", "103.203.96.0/19", - "103.210.160.0/19", "103.213.64.0/19", "103.213.160.0/19", "103.217.0.0/18", - "103.218.32.0/19", "103.218.64.0/19", "103.220.160.0/19", "103.221.0.0/19", - "103.221.96.0/19", "103.221.128.0/18", "103.222.32.0/19", "103.222.64.0/18", - "103.222.128.0/18", "103.222.192.0/19", "103.223.32.0/19", "103.223.64.0/18", - "103.223.192.0/18", "103.235.224.0/19", "103.236.0.0/18", "103.236.64.0/19", - "103.237.192.0/18", "103.238.160.0/19", "106.0.64.0/18", "106.2.32.0/19", - "106.2.64.0/18", "106.2.128.0/17", "106.3.32.0/19", "106.3.96.0/19", - "106.3.128.0/17", "106.4.0.0/14", "106.8.0.0/15", "106.11.0.0/16", - "106.12.0.0/14", "106.16.0.0/12", "106.32.0.0/12", "106.48.64.0/18", - "106.48.128.0/17", "106.49.32.0/19", "106.49.64.0/19", "106.49.128.0/17", - "106.50.0.0/16", "106.52.0.0/14", "106.56.0.0/13", "106.74.0.0/16", - "106.75.0.0/17", "106.75.128.0/18", "106.75.224.0/19", "106.80.0.0/12", - "106.108.0.0/14", "106.112.0.0/12", "106.224.0.0/12", "110.6.0.0/15", - "110.16.0.0/14", "110.40.0.0/14", "110.48.0.0/17", "110.48.128.0/19", - "110.48.192.0/18", "110.51.0.0/16", "110.52.0.0/15", "110.56.0.0/13", - "110.64.0.0/15", "110.72.0.0/15", "110.75.0.0/16", "110.76.32.0/19", - "110.76.192.0/18", "110.77.0.0/17", "110.80.0.0/13", "110.88.0.0/14", - "110.93.32.0/19", "110.94.0.0/15", "110.96.0.0/11", "110.152.0.0/14", - "110.156.0.0/15", "110.166.0.0/15", "110.172.192.0/18", "110.173.0.0/19", - "110.173.64.0/19", "110.173.192.0/19", "110.176.0.0/12", "110.192.0.0/11", - "110.228.0.0/14", "110.232.32.0/19", "110.236.0.0/15", "110.240.0.0/12", - "111.0.0.0/10", "111.66.0.0/16", "111.68.64.0/19", "111.72.0.0/13", - "111.85.0.0/16", "111.112.0.0/14", "111.116.0.0/15", "111.119.96.0/19", - "111.119.128.0/19", "111.120.0.0/14", "111.124.0.0/16", "111.126.0.0/15", - "111.128.0.0/11", "111.160.0.0/13", "111.170.0.0/16", "111.172.0.0/14", - "111.176.0.0/13", "111.186.0.0/15", "111.192.0.0/12", "111.208.0.0/13", - "111.221.128.0/17", "111.222.0.0/16", "111.224.0.0/13", "111.235.96.0/19", - "112.0.0.0/10", "112.64.0.0/14", "112.73.64.0/18", "112.74.0.0/15", - "112.80.0.0/12", "112.96.0.0/13", "112.109.128.0/17", "112.111.0.0/16", - "112.112.0.0/14", "112.116.0.0/15", "112.122.0.0/15", "112.124.0.0/14", - "112.128.0.0/14", "112.132.0.0/16", "112.192.0.0/14", "112.224.0.0/11", - "113.0.0.0/13", "113.8.0.0/15", "113.11.192.0/19", "113.12.0.0/14", - "113.16.0.0/15", "113.18.0.0/16", "113.24.0.0/14", "113.31.0.0/16", - "113.44.0.0/14", "113.48.0.0/14", "113.54.0.0/15", "113.56.0.0/15", - "113.58.0.0/16", "113.59.0.0/17", "113.62.0.0/15", "113.64.0.0/10", - "113.128.0.0/15", "113.132.0.0/14", "113.136.0.0/13", "113.194.0.0/15", - "113.200.0.0/15", "113.202.0.0/16", "113.204.0.0/14", "113.208.96.0/19", - "113.208.128.0/17", "113.209.0.0/16", "113.212.0.0/18", "113.213.0.0/17", - "113.214.0.0/15", "113.218.0.0/15", "113.220.0.0/14", "113.224.0.0/12", - "113.240.0.0/13", "113.248.0.0/14", "114.28.0.0/16", "114.54.0.0/15", - "114.60.0.0/14", "114.64.0.0/14", "114.68.32.0/19", "114.68.64.0/18", - "114.68.128.0/17", "114.79.64.0/18", "114.80.0.0/12", "114.96.0.0/13", - "114.104.0.0/14", "114.110.64.0/18", "114.111.0.0/19", "114.111.160.0/19", - "114.112.32.0/19", "114.112.64.0/19", "114.112.160.0/19", "114.112.192.0/19", - "114.113.0.0/17", "114.113.160.0/19", "114.114.0.0/16", "114.115.0.0/18", - "114.115.96.0/19", "114.115.128.0/17", "114.116.0.0/14", "114.132.0.0/18", - "114.132.64.0/19", "114.132.128.0/17", "114.135.0.0/16", "114.138.0.0/15", - "114.141.128.0/18", "114.196.0.0/15", "114.208.0.0/12", "114.224.0.0/11", - "115.24.0.0/14", "115.28.0.0/15", "115.32.0.0/14", "115.44.0.0/14", - "115.48.0.0/12", "115.84.0.0/18", "115.85.192.0/18", "115.100.0.0/14", - "115.104.0.0/14", "115.120.0.0/14", "115.148.0.0/14", "115.152.0.0/13", - "115.166.64.0/19", "115.168.0.0/13", "115.180.0.0/14", "115.190.0.0/15", - "115.192.0.0/11", "115.224.0.0/12", "116.1.0.0/16", "116.2.0.0/15", - "116.4.0.0/14", "116.8.0.0/14", "116.13.0.0/16", "116.16.0.0/12", - "116.52.0.0/14", "116.56.0.0/15", "116.60.0.0/14", "116.66.0.0/18", - "116.66.64.0/19", "116.69.0.0/16", "116.70.0.0/17", "116.76.0.0/14", - "116.85.0.0/16", "116.95.0.0/16", "116.112.0.0/14", "116.116.0.0/15", - "116.128.0.0/15", "116.131.0.0/16", "116.132.0.0/14", "116.136.0.0/13", - "116.144.0.0/12", "116.160.0.0/13", "116.168.0.0/15", "116.178.0.0/15", - "116.180.0.0/14", "116.184.0.0/13", "116.192.0.0/16", "116.193.32.0/19", - "116.194.0.0/15", "116.196.64.0/18", "116.196.128.0/17", "116.198.0.0/16", - "116.199.0.0/17", "116.204.0.0/15", "116.207.0.0/16", "116.208.0.0/14", - "116.213.64.0/18", "116.213.128.0/17", "116.214.32.0/19", "116.214.128.0/17", - "116.215.0.0/16", "116.216.0.0/14", "116.224.0.0/12", "116.242.0.0/15", - "116.244.0.0/14", "116.248.0.0/15", "116.252.0.0/15", "116.254.128.0/17", - "116.255.128.0/17", "117.8.0.0/13", "117.21.0.0/16", "117.22.0.0/15", - "117.24.0.0/13", "117.32.0.0/13", "117.40.0.0/14", "117.44.0.0/15", - "117.48.0.0/14", "117.57.0.0/16", "117.58.0.0/18", "117.59.0.0/16", - "117.60.0.0/14", "117.64.0.0/13", "117.72.0.0/15", "117.74.64.0/19", - "117.74.128.0/17", "117.75.0.0/16", "117.76.0.0/14", "117.80.0.0/12", - "117.100.0.0/15", "117.106.0.0/15", "117.112.0.0/13", "117.120.96.0/19", - "117.120.128.0/17", "117.121.0.0/19", "117.121.64.0/18", "117.121.160.0/19", - "117.122.128.0/17", "117.124.0.0/14", "117.128.0.0/10", "118.24.0.0/16", - "118.25.96.0/19", "118.25.128.0/17", "118.26.0.0/19", "118.26.64.0/19", - "118.26.128.0/17", "118.28.0.0/14", "118.64.0.0/15", "118.66.0.0/16", - "118.72.0.0/13", "118.80.0.0/15", "118.84.0.0/15", "118.88.32.0/19", - "118.88.64.0/18", "118.88.128.0/17", "118.89.0.0/16", "118.112.0.0/13", - "118.120.0.0/14", "118.124.0.0/15", "118.126.32.0/19", "118.126.64.0/18", - "118.126.128.0/17", "118.127.128.0/19", "118.132.0.0/14", "118.144.0.0/14", - "118.178.0.0/16", "118.180.0.0/14", "118.184.96.0/19", "118.184.128.0/17", - "118.186.0.0/15", "118.188.0.0/16", "118.190.0.0/15", "118.192.0.0/16", - "118.193.96.0/19", "118.194.32.0/19", "118.194.64.0/18", "118.194.128.0/18", - "118.194.192.0/19", "118.195.0.0/16", "118.196.0.0/14", "118.202.0.0/15", - "118.204.0.0/14", "118.212.0.0/15", "118.215.192.0/19", "118.224.0.0/14", - "118.228.0.0/17", "118.228.160.0/19", "118.228.192.0/18", "118.229.0.0/16", - "118.230.0.0/16", "118.239.0.0/16", "118.242.0.0/16", "118.244.0.0/14", - "118.248.0.0/13", "119.0.0.0/15", "119.2.0.0/19", "119.2.128.0/17", - "119.3.0.0/16", "119.4.0.0/14", "119.8.0.0/16", "119.10.0.0/17", - "119.16.0.0/16", "119.19.0.0/16", "119.20.0.0/14", "119.27.64.0/18", - "119.27.128.0/17", "119.28.128.0/17", "119.29.0.0/16", "119.31.192.0/19", - "119.32.0.0/14", "119.36.0.0/15", "119.38.0.0/17", "119.38.128.0/18", - "119.38.192.0/19", "119.39.0.0/16", "119.40.0.0/18", "119.40.128.0/17", - "119.41.0.0/16", "119.42.0.0/19", "119.42.224.0/19", "119.44.0.0/15", - "119.48.0.0/13", "119.57.0.0/16", "119.58.0.0/16", "119.59.128.0/17", - "119.60.0.0/15", "119.62.0.0/16", "119.63.32.0/19", "119.78.0.0/15", - "119.80.0.0/16", "119.84.0.0/14", "119.88.0.0/14", "119.96.0.0/13", - "119.108.0.0/15", "119.112.0.0/12", "119.128.0.0/12", "119.144.0.0/14", - "119.148.160.0/19", "119.151.192.0/18", "119.161.128.0/17", "119.162.0.0/15", - "119.164.0.0/14", "119.176.0.0/12", "119.232.0.0/15", "119.248.0.0/14", - "119.253.0.0/16", "119.254.0.0/15", "120.0.0.0/12", "120.24.0.0/14", - "120.30.0.0/18", "120.30.96.0/19", "120.30.128.0/17", "120.31.0.0/17", - "120.31.160.0/19", "120.31.192.0/18", "120.32.0.0/12", "120.48.0.0/15", - "120.52.0.0/14", "120.64.0.0/13", "120.72.32.0/19", "120.72.128.0/17", - "120.76.0.0/14", "120.80.0.0/13", "120.90.0.0/15", "120.92.0.0/16", - "120.94.0.0/15", "120.128.0.0/14", "120.132.0.0/17", "120.132.128.0/18", - "120.132.192.0/19", "120.133.0.0/16", "120.134.0.0/15", "120.136.128.0/18", - "120.137.0.0/18", "120.137.96.0/19", "120.143.128.0/19", "120.192.0.0/10", - "121.4.0.0/15", "121.8.0.0/13", "121.16.0.0/12", "121.32.0.0/13", - "121.40.0.0/14", "121.46.0.0/18", "121.46.128.0/17", "121.47.0.0/16", - "121.48.0.0/15", "121.51.0.0/16", "121.52.160.0/19", "121.52.224.0/19", - "121.55.0.0/18", "121.56.0.0/15", "121.58.0.0/17", "121.59.0.0/16", - "121.60.0.0/14", "121.68.0.0/14", "121.76.0.0/15", "121.79.128.0/18", - "121.89.0.0/16", "121.101.0.0/18", "121.192.0.0/13", "121.201.0.0/18", - "121.201.96.0/19", "121.201.128.0/17", "121.204.0.0/14", "121.224.0.0/12", - "121.248.0.0/14", "121.255.0.0/16", "122.0.64.0/18", "122.0.128.0/17", - "122.4.0.0/14", "122.8.32.0/19", "122.8.64.0/18", "122.8.224.0/19", - "122.11.0.0/17", "122.12.0.0/15", "122.14.0.0/17", "122.14.192.0/18", - "122.48.0.0/16", "122.49.0.0/18", "122.51.0.0/16", "122.64.0.0/11", - "122.96.0.0/15", "122.102.64.0/19", "122.112.0.0/18", "122.112.64.0/19", - "122.112.128.0/17", "122.113.0.0/16", "122.114.0.0/16", "122.115.0.0/18", - "122.115.96.0/19", "122.115.128.0/17", "122.119.0.0/16", "122.136.0.0/13", - "122.144.128.0/17", "122.152.192.0/18", "122.156.0.0/14", "122.188.0.0/14", - "122.192.0.0/14", "122.198.0.0/16", "122.200.64.0/18", "122.204.0.0/14", - "122.224.0.0/12", "122.240.0.0/13", "123.4.0.0/14", "123.8.0.0/13", - "123.49.160.0/19", "123.49.192.0/18", "123.50.160.0/19", "123.52.0.0/14", - "123.56.0.0/15", "123.58.0.0/18", "123.58.96.0/19", "123.58.128.0/17", - "123.59.0.0/16", "123.61.0.0/16", "123.62.0.0/16", "123.64.0.0/11", - "123.96.0.0/15", "123.98.0.0/17", "123.99.128.0/17", "123.100.0.0/19", - "123.101.0.0/16", "123.103.32.0/19", "123.103.64.0/18", "123.112.0.0/12", - "123.128.0.0/13", "123.137.0.0/16", "123.138.0.0/15", "123.144.0.0/12", - "123.160.0.0/12", "123.177.0.0/16", "123.178.0.0/15", "123.180.0.0/14", - "123.184.0.0/13", "123.196.0.0/15", "123.199.128.0/17", "123.206.0.0/15", - "123.232.0.0/14", "123.242.0.0/17", "123.244.0.0/14", "123.249.0.0/16", - "123.253.0.0/16", "124.6.64.0/18", "124.14.0.0/15", "124.16.0.0/15", - "124.20.0.0/14", "124.28.192.0/18", "124.29.0.0/17", "124.31.0.0/16", - "124.40.128.0/18", "124.40.192.0/19", "124.42.0.0/16", "124.47.0.0/18", - "124.64.0.0/15", "124.66.0.0/17", "124.67.0.0/16", "124.68.0.0/15", - "124.72.0.0/13", "124.88.0.0/13", "124.112.0.0/13", "124.126.0.0/15", - "124.128.0.0/13", "124.147.128.0/17", "124.151.0.0/16", "124.152.0.0/16", - "124.156.0.0/16", "124.160.0.0/13", "124.172.0.0/14", "124.192.0.0/15", - "124.196.0.0/16", "124.200.0.0/13", "124.220.0.0/14", "124.224.0.0/12", - "124.240.0.0/17", "124.240.128.0/18", "124.242.0.0/16", "124.243.192.0/18", - "124.248.0.0/17", "124.249.0.0/16", "124.250.0.0/17", "124.250.160.0/19", - "124.250.192.0/18", "124.251.0.0/16", "124.254.0.0/18", "125.31.192.0/18", - "125.32.0.0/12", "125.58.128.0/17", "125.61.128.0/17", "125.62.0.0/18", - "125.64.0.0/11", "125.96.0.0/15", "125.98.0.0/16", "125.104.0.0/13", - "125.112.0.0/12", "125.169.0.0/16", "125.171.0.0/16", "125.208.0.0/19", - "125.210.0.0/15", "125.213.0.0/17", "125.214.96.0/19", "125.215.0.0/18", - "125.216.0.0/13", "125.254.128.0/17", "139.9.0.0/16", "139.129.0.0/16", - "139.148.0.0/16", "139.155.0.0/16", "139.159.0.0/19", "139.159.64.0/19", - "139.159.128.0/17", "139.170.0.0/16", "139.176.0.0/16", "139.183.0.0/16", - "139.186.0.0/16", "139.189.0.0/16", "139.196.0.0/14", "139.200.0.0/13", - "139.208.0.0/13", "139.217.0.0/16", "139.219.0.0/16", "139.220.0.0/15", - "139.224.0.0/16", "139.226.0.0/15", "140.75.0.0/16", "140.143.0.0/16", - "140.205.32.0/19", "140.205.64.0/19", "140.205.128.0/17", "140.206.0.0/15", - "140.210.0.0/16", "140.224.0.0/16", "140.237.0.0/16", "140.240.0.0/16", - "140.243.0.0/16", "140.246.0.0/16", "140.249.0.0/16", "140.250.0.0/16", - "140.255.0.0/16", "144.0.0.0/16", "144.7.0.0/16", "144.12.0.0/16", - "144.52.0.0/16", "144.123.0.0/16", "144.255.0.0/16", "150.0.0.0/16", - "150.115.0.0/16", "150.121.0.0/16", "150.122.0.0/16", "150.138.0.0/15", - "150.223.0.0/16", "150.255.0.0/16", "152.104.128.0/17", "153.0.0.0/16", - "153.3.0.0/16", "153.34.0.0/15", "153.36.0.0/15", "153.99.0.0/16", - "153.101.0.0/16", "153.118.0.0/15", "157.0.0.0/16", "157.18.0.0/16", - "157.61.0.0/16", "157.122.0.0/16", "157.148.0.0/16", "157.156.0.0/16", - "157.255.0.0/16", "159.226.0.0/16", "160.202.224.0/19", "161.207.0.0/16", - "162.105.0.0/16", "163.0.0.0/16", "163.53.96.0/19", "163.125.0.0/16", - "163.142.0.0/16", "163.177.0.0/16", "163.179.0.0/16", "163.204.0.0/16", - "166.111.0.0/16", "167.139.0.0/16", "168.160.0.0/16", "171.8.0.0/13", - "171.34.0.0/15", "171.36.0.0/14", "171.40.0.0/13", "171.80.0.0/12", - "171.104.0.0/13", "171.112.0.0/12", "171.208.0.0/12", "175.0.0.0/12", - "175.16.0.0/13", "175.24.0.0/14", "175.30.0.0/15", "175.42.0.0/15", - "175.44.0.0/16", "175.46.0.0/15", "175.48.0.0/12", "175.64.0.0/11", - "175.102.0.0/16", "175.106.128.0/17", "175.146.0.0/15", "175.148.0.0/14", - "175.152.0.0/14", "175.160.0.0/12", "175.178.0.0/16", "175.184.128.0/18", - "175.185.0.0/16", "175.186.0.0/15", "175.188.0.0/14", "180.76.32.0/19", - "180.76.96.0/19", "180.76.128.0/18", "180.76.192.0/19", "180.77.0.0/16", - "180.78.0.0/15", "180.84.0.0/15", "180.86.0.0/16", "180.88.0.0/14", - "180.95.128.0/17", "180.96.0.0/11", "180.129.128.0/17", "180.130.0.0/16", - "180.136.0.0/13", "180.148.224.0/19", "180.149.128.0/19", "180.152.0.0/13", - "180.160.0.0/12", "180.178.192.0/18", "180.184.0.0/14", "180.188.0.0/17", - "180.201.0.0/16", "180.202.0.0/15", "180.208.0.0/15", "180.212.0.0/15", - "180.222.224.0/19", "180.223.0.0/16", "180.233.0.0/18", "180.233.64.0/19", - "180.235.64.0/19", "182.16.192.0/19", "182.18.0.0/17", "182.32.0.0/12", - "182.48.96.0/19", "182.49.0.0/16", "182.51.0.0/17", "182.51.128.0/18", - "182.51.224.0/19", "182.54.0.0/17", "182.61.0.0/18", "182.61.128.0/17", - "182.80.0.0/13", "182.88.0.0/14", "182.92.0.0/16", "182.96.0.0/11", - "182.128.0.0/12", "182.144.0.0/13", "182.157.0.0/16", "182.174.0.0/15", - "182.200.0.0/13", "182.236.128.0/17", "182.238.0.0/16", "182.239.0.0/19", - "182.240.0.0/13", "182.254.0.0/16", "183.0.0.0/10", "183.64.0.0/13", - "183.84.0.0/15", "183.92.0.0/14", "183.128.0.0/11", "183.160.0.0/13", - "183.168.0.0/15", "183.170.0.0/16", "183.172.0.0/14", "183.184.0.0/13", - "183.192.0.0/10", "192.140.160.0/19", "202.4.128.0/19", "202.8.128.0/19", - "202.38.64.0/18", "202.38.192.0/18", "202.63.160.0/19", "202.70.0.0/19", - "202.91.224.0/19", "202.94.0.0/19", "202.95.0.0/19", "202.96.0.0/12", - "202.112.0.0/13", "202.120.0.0/15", "202.122.64.0/19", "202.127.128.0/19", - "202.127.224.0/19", "202.130.0.0/19", "202.130.224.0/19", "202.141.160.0/19", - "202.148.64.0/18", "202.149.32.0/19", "202.149.160.0/19", "202.149.224.0/19", - "202.150.224.0/19", "202.151.128.0/19", "202.157.192.0/19", "202.158.160.0/19", - "202.164.96.0/19", "202.166.224.0/19", "202.168.160.0/19", "202.170.128.0/19", - "202.170.224.0/19", "202.173.224.0/19", "202.176.224.0/19", "202.180.128.0/19", - "202.182.192.0/19", "202.189.0.0/18", "202.192.0.0/12", "203.86.0.0/18", - "203.86.64.0/19", "203.88.32.0/19", "203.88.192.0/19", "203.90.128.0/18", - "203.90.192.0/19", "203.91.32.0/19", "203.92.160.0/19", "203.93.0.0/16", - "203.94.0.0/19", "203.95.96.0/19", "203.95.128.0/18", "203.95.224.0/19", - "203.100.96.0/19", "203.105.96.0/19", "203.105.128.0/19", "203.107.0.0/17", - "203.110.160.0/19", "203.118.192.0/19", "203.119.128.0/17", "203.128.32.0/19", - "203.128.96.0/19", "203.132.32.0/19", "203.135.96.0/19", "203.142.224.0/19", - "203.145.0.0/19", "203.148.0.0/18", "203.152.64.0/19", "203.152.128.0/19", - "203.156.192.0/18", "203.160.192.0/19", "203.161.192.0/19", "203.166.160.0/19", - "203.168.0.0/19", "203.174.96.0/19", "203.175.128.0/19", "203.175.192.0/18", - "203.176.0.0/19", "203.176.64.0/19", "203.187.160.0/19", "203.189.192.0/19", - "203.191.64.0/18", "203.192.0.0/19", "203.193.224.0/19", "203.195.64.0/19", - "203.195.128.0/17", "203.205.64.0/19", "203.207.64.0/18", "203.207.128.0/17", - "203.208.32.0/19", "203.209.224.0/19", "210.5.0.0/19", "210.5.128.0/19", - "210.12.0.0/15", "210.14.64.0/19", "210.14.128.0/17", "210.15.0.0/17", - "210.15.128.0/18", "210.16.160.0/19", "210.21.0.0/16", "210.22.0.0/16", - "210.23.32.0/19", "210.25.0.0/16", "210.26.0.0/15", "210.28.0.0/14", - "210.32.0.0/12", "210.51.0.0/16", "210.52.0.0/16", "210.53.0.0/18", - "210.53.64.0/19", "210.53.128.0/17", "210.56.192.0/19", "210.72.0.0/14", - "210.76.0.0/15", "210.78.0.0/16", "210.79.64.0/18", "210.79.224.0/19", - "210.82.0.0/15", "210.87.128.0/18", "210.185.192.0/18", "210.192.96.0/19", - "211.64.0.0/13", "211.80.0.0/12", "211.96.0.0/15", "211.98.0.0/16", - "211.99.0.0/18", "211.99.64.0/19", "211.99.128.0/17", "211.100.0.0/14", - "211.136.0.0/13", "211.144.0.0/13", "211.152.0.0/15", "211.154.0.0/19", - "211.154.64.0/18", "211.154.128.0/17", "211.155.0.0/18", "211.155.96.0/19", - "211.155.128.0/17", "211.156.0.0/18", "211.156.64.0/19", "211.156.128.0/17", - "211.157.0.0/16", "211.158.0.0/15", "211.160.0.0/13", "218.0.0.0/12", - "218.16.0.0/13", "218.24.0.0/14", "218.28.0.0/15", "218.30.0.0/19", - "218.30.64.0/18", "218.30.128.0/17", "218.31.0.0/16", "218.56.0.0/13", - "218.64.0.0/11", "218.96.0.0/15", "218.98.32.0/19", "218.98.128.0/19", - "218.98.192.0/18", "218.99.0.0/16", "218.100.96.0/19", "218.100.128.0/17", - "218.104.0.0/14", "218.108.0.0/15", "218.192.0.0/12", "218.240.0.0/14", - "218.244.0.0/15", "218.246.0.0/17", "218.246.160.0/19", "218.246.192.0/18", - "218.247.0.0/18", "218.247.96.0/19", "218.247.128.0/17", "218.249.0.0/16", - "219.72.0.0/16", "219.82.0.0/16", "219.83.128.0/17", "219.128.0.0/11", - "219.216.0.0/13", "219.224.0.0/13", "219.232.0.0/15", "219.234.32.0/19", - "219.234.64.0/18", "219.234.128.0/17", "219.235.0.0/16", "219.236.0.0/14", - "219.242.0.0/15", "219.244.0.0/14", "220.101.192.0/18", "220.112.0.0/14", - "220.152.128.0/17", "220.154.0.0/17", "220.154.160.0/19", "220.154.192.0/18", - "220.155.32.0/19", "220.155.64.0/18", "220.155.128.0/17", "220.160.0.0/11", - "220.192.0.0/12", "220.231.0.0/18", "220.231.128.0/17", "220.232.64.0/18", - "220.234.0.0/16", "220.242.0.0/16", "220.243.0.0/17", "220.243.128.0/18", - "220.248.0.0/14", "220.252.0.0/16", "221.0.0.0/13", "221.8.0.0/14", - "221.12.0.0/17", "221.12.128.0/18", "221.13.0.0/16", "221.14.0.0/15", - "221.122.0.0/15", "221.128.128.0/17", "221.129.0.0/16", "221.130.0.0/15", - "221.133.224.0/19", "221.136.0.0/15", "221.172.0.0/14", "221.176.0.0/19", - "221.176.64.0/18", "221.176.128.0/17", "221.177.0.0/16", "221.178.0.0/15", - "221.180.0.0/14", "221.192.0.0/14", "221.196.0.0/15", "221.198.0.0/16", - "221.199.0.0/17", "221.199.128.0/18", "221.199.224.0/19", "221.200.0.0/13", - "221.208.0.0/12", "221.224.0.0/12", "222.16.0.0/12", "222.32.0.0/11", - "222.64.0.0/11", "222.125.0.0/16", "222.126.128.0/19", "222.128.0.0/12", - "222.160.0.0/14", "222.168.0.0/13", "222.176.0.0/12", "222.192.0.0/11", - "222.240.0.0/13", "222.248.0.0/15", "223.0.0.0/12", "223.20.0.0/15", - "223.64.0.0/11", "223.96.0.0/12", "223.112.0.0/14", "223.116.0.0/15", - "223.120.0.0/13", "223.128.0.0/15", "223.144.0.0/12", "223.160.0.0/14", - "223.166.0.0/15", "223.192.0.0/15", "223.198.0.0/15", "223.201.0.0/18", - "223.201.160.0/19", "223.201.192.0/19", "223.202.0.0/17", "223.202.128.0/18", - "223.202.192.0/19", "223.203.0.0/16", "223.208.0.0/13", "223.220.0.0/15", - "223.240.0.0/13", "223.248.0.0/14", "223.252.128.0/19", "223.252.192.0/18", - "223.254.0.0/17", "223.254.128.0/18", "223.255.0.0/17" - } - }; - -} diff --git a/app/src/main/java/org/proxydroid/utils/Constraints.kt b/app/src/main/java/org/proxydroid/utils/Constraints.kt new file mode 100644 index 00000000..9b6b8c67 --- /dev/null +++ b/app/src/main/java/org/proxydroid/utils/Constraints.kt @@ -0,0 +1,56 @@ +package org.proxydroid.utils + +object Constraints { + const val ONLY_3G = "2G/3G" + const val ONLY_WIFI = "WIFI" + const val WIFI_AND_3G = "WIFI/2G/3G" + const val FILE_PATH = "file_path" + const val IMPORT_REQUEST = 0 + + @JvmField + val PRESETS: Array> = arrayOf( + emptyArray(), + arrayOf("127.0.0.1"), + arrayOf("127.0.0.1", "10.0.0.0/8", "192.168.0.0/16", "172.16.0.0/12"), + // China routes are gotten from https://github.com/17mon/china_ip_list + // Shrunk for better performance by removing subnets which have netmasks >= 20. + arrayOf( + "1.0.32.0/19", "1.1.32.0/19", "1.2.32.0/19", "1.2.64.0/18", + "1.3.0.0/16", "1.4.32.0/19", "1.4.64.0/18", "1.8.32.0/19", + "1.8.64.0/19", "1.8.160.0/19", "1.8.192.0/19", "1.10.32.0/19", + "1.10.64.0/18", "1.12.0.0/14", "1.24.0.0/13", "1.45.0.0/16", + "1.48.0.0/14", "1.56.0.0/13", "1.68.0.0/14", "1.80.0.0/12", + "1.116.0.0/14", "1.180.0.0/14", "1.184.0.0/15", "1.188.0.0/14", + "1.192.0.0/13", "1.202.0.0/15", "1.204.0.0/14", "14.16.0.0/12", + "14.103.0.0/16", "14.104.0.0/13", "14.112.0.0/12", "14.130.0.0/15", + "14.134.0.0/15", "14.144.0.0/12", "14.196.0.0/15", "14.204.0.0/15", + "14.208.0.0/12", "27.8.0.0/13", "27.16.0.0/12", "27.36.0.0/14", + "27.40.0.0/13", "27.50.128.0/17", "27.54.192.0/18", "27.98.224.0/19", + "27.99.128.0/17", "27.103.0.0/16", "27.106.128.0/18", "27.109.32.0/19", + "27.112.0.0/18", "27.113.128.0/18", "27.115.0.0/17", "27.128.0.0/15", + "27.144.0.0/16", "27.148.0.0/14", "27.152.0.0/13", "27.184.0.0/13", + "27.192.0.0/11", "27.224.0.0/14", "36.0.32.0/19", "36.0.64.0/18", + "36.0.128.0/17", "36.1.0.0/16", "36.4.0.0/14", "36.16.0.0/12", + "36.32.0.0/14", "36.36.0.0/16", "36.37.0.0/19", "36.40.0.0/13", + "36.48.0.0/15", "36.51.0.0/17", "36.51.128.0/18", "36.51.192.0/19", + "36.56.0.0/13", "36.96.0.0/11", "36.128.0.0/10", "36.192.0.0/11", + "36.248.0.0/14", "36.254.0.0/16", "39.0.32.0/19", "39.0.64.0/18", + "39.0.128.0/17", "39.64.0.0/11", "39.96.0.0/13", "39.104.0.0/14", + "39.108.0.0/16", "39.128.0.0/10", "40.72.0.0/16", "40.125.128.0/17", + "40.126.64.0/18", "42.0.32.0/19", "42.0.128.0/19", "42.0.224.0/19", + "42.1.0.0/19", "42.4.0.0/14", "42.48.0.0/13", "42.56.0.0/14", + "42.62.0.0/17", "42.62.128.0/19", "42.63.0.0/16", "42.80.0.0/15", + "42.83.96.0/19", "42.83.160.0/19", "42.83.192.0/18", "42.84.0.0/14", + "42.88.0.0/13", "42.96.64.0/19", "42.96.128.0/17", "42.97.0.0/16", + "42.99.0.0/18", "42.99.64.0/19", "42.100.0.0/14", "42.120.0.0/15", + "42.122.0.0/16", "42.123.0.0/19", "42.123.64.0/18", "42.123.128.0/19", + "42.123.192.0/18", "42.128.0.0/12", "42.156.0.0/19", "42.156.64.0/18", + "42.156.128.0/17", "42.157.0.0/16", "42.158.0.0/15", "42.160.0.0/12", + "42.176.0.0/13", "42.184.0.0/15", "42.186.0.0/16", "42.187.0.0/18", + "42.187.64.0/19", "42.187.128.0/17", "42.192.0.0/13", "42.201.0.0/17", + "42.202.0.0/15", "42.204.0.0/14", "42.208.0.0/12", "42.224.0.0/12", + "42.240.0.0/16", "42.242.0.0/15", "42.244.0.0/15", "42.246.0.0/16", + "42.247.32.0/19", "42.247.64.0/18", "42.247.128.0/17", "42.248.0.0/13" + ) + ) +} diff --git a/app/src/main/java/org/proxydroid/utils/ImageLoader.java b/app/src/main/java/org/proxydroid/utils/ImageLoader.java deleted file mode 100644 index 6d3d7969..00000000 --- a/app/src/main/java/org/proxydroid/utils/ImageLoader.java +++ /dev/null @@ -1,227 +0,0 @@ -package org.proxydroid.utils; - -import android.app.Activity; -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.drawable.BitmapDrawable; -import android.widget.ImageView; - -import org.proxydroid.R; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.util.HashMap; -import java.util.Stack; - -public class ImageLoader { - - // Used to display bitmap in the UI thread - class BitmapDisplayer implements Runnable { - Bitmap bitmap; - ImageView imageView; - - public BitmapDisplayer(Bitmap b, ImageView i) { - bitmap = b; - imageView = i; - } - - @Override - public void run() { - if (bitmap != null) - imageView.setImageBitmap(bitmap); - else - imageView.setImageResource(stub_id); - } - } - - class PhotosLoader extends Thread { - @Override - public void run() { - try { - while (true) { - // thread waits until there are any images to load in the - // queue - if (photosQueue.photosToLoad.size() == 0) - synchronized (photosQueue.lock) { - photosQueue.lock.wait(); - } - - synchronized (photosQueue.lock) { - if (photosQueue.photosToLoad.size() != 0) { - PhotoToLoad photoToLoad; - - photoToLoad = photosQueue.photosToLoad.pop(); - - Bitmap bmp = getBitmap(photoToLoad.uid); - cache.put(photoToLoad.uid, bmp); - Object tag = photoToLoad.imageView.getTag(); - if (tag != null && ((Integer) tag) == photoToLoad.uid) { - BitmapDisplayer bd = new BitmapDisplayer(bmp, - photoToLoad.imageView); - Activity a = (Activity) photoToLoad.imageView - .getContext(); - a.runOnUiThread(bd); - } - } - } - - if (Thread.interrupted()) - break; - } - } catch (InterruptedException e) { - // allow thread to exit - } - } - } - - // stores list of photos to download - class PhotosQueue { - private Stack photosToLoad = new Stack(); - private final Object lock = new Object(); - - // removes all instances of this ImageView - public void Clean(ImageView image) { - synchronized (lock) { - try { - for (int j = 0; j < photosToLoad.size(); ) { - if (photosToLoad.get(j).imageView == image) - photosToLoad.remove(j); - else - ++j; - } - } catch (java.lang.ArrayIndexOutOfBoundsException ignore) { - // Nothing - } - } - } - } - - // Task for the queue - private class PhotoToLoad { - public int uid; - public ImageView imageView; - - public PhotoToLoad(int u, ImageView i) { - uid = u; - imageView = i; - } - } - - // the simplest in-memory cache implementation. This should be replaced with - // something like SoftReference or BitmapOptions.inPurgeable(since 1.6) - private HashMap cache = new HashMap(); - - private File cacheDir; - - private Context context; - - final int stub_id = R.drawable.sym_def_app_icon; - - PhotosQueue photosQueue = new PhotosQueue(); - - PhotosLoader photoLoaderThread = new PhotosLoader(); - - public ImageLoader(Context c) { - // Make the background thead low priority. This way it will not affect - // the UI performance - photoLoaderThread.setPriority(Thread.NORM_PRIORITY - 1); - - context = c; - - // Find the dir to save cached images - cacheDir = context.getCacheDir(); - - } - - public void clearCache() { - // clear memory cache - cache.clear(); - - // clear SD cache - File[] files = cacheDir.listFiles(); - for (File f : files) - f.delete(); - } - - // decodes image and scales it to reduce memory consumption - private Bitmap decodeFile(File f) { - try { - // decode image size - BitmapFactory.Options o = new BitmapFactory.Options(); - o.inJustDecodeBounds = true; - BitmapFactory.decodeStream(new FileInputStream(f), null, o); - - // Find the correct scale value. It should be the power of 2. - final int REQUIRED_SIZE = 70; - int width_tmp = o.outWidth, height_tmp = o.outHeight; - int scale = 1; - while (true) { - if (width_tmp / 2 < REQUIRED_SIZE - || height_tmp / 2 < REQUIRED_SIZE) - break; - width_tmp /= 2; - height_tmp /= 2; - scale *= 2; - } - - // decode with inSampleSize - BitmapFactory.Options o2 = new BitmapFactory.Options(); - o2.inSampleSize = scale; - return BitmapFactory.decodeStream(new FileInputStream(f), null, o2); - } catch (FileNotFoundException e) { - } - return null; - } - - public void DisplayImage(int uid, Activity activity, ImageView imageView) { - if (cache.containsKey(uid)) - imageView.setImageBitmap(cache.get(uid)); - else { - queuePhoto(uid, activity, imageView); - imageView.setImageResource(stub_id); - } - } - - private Bitmap getBitmap(int uid) { - // I identify images by hashcode. Not a perfect solution, good for the - // demo. - String filename = String.valueOf(uid); - File f = new File(cacheDir, filename); - - // from SD cache - Bitmap b = decodeFile(f); - if (b != null) - return b; - - // from web - try { - BitmapDrawable icon = (BitmapDrawable) Utils.getAppIcon(context, - uid); - return icon.getBitmap(); - } catch (Exception ex) { - return null; - } - } - - private void queuePhoto(int uid, Activity activity, ImageView imageView) { - // This ImageView may be used for other images before. So there may be - // some old tasks in the queue. We need to discard them. - photosQueue.Clean(imageView); - PhotoToLoad p = new PhotoToLoad(uid, imageView); - synchronized (photosQueue.lock) { - photosQueue.photosToLoad.push(p); - photosQueue.lock.notifyAll(); - } - - // start thread if it's not started yet - if (photoLoaderThread.getState() == Thread.State.NEW) - photoLoaderThread.start(); - } - - public void stopThread() { - photoLoaderThread.interrupt(); - } - -} diff --git a/app/src/main/java/org/proxydroid/utils/ImageLoader.kt b/app/src/main/java/org/proxydroid/utils/ImageLoader.kt new file mode 100644 index 00000000..48cc7e1d --- /dev/null +++ b/app/src/main/java/org/proxydroid/utils/ImageLoader.kt @@ -0,0 +1,170 @@ +package org.proxydroid.utils + +import android.app.Activity +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.drawable.BitmapDrawable +import android.widget.ImageView +import org.proxydroid.R +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.util.* + +class ImageLoader(context: Context) { + private val cache = HashMap() + private val cacheDir: File = context.cacheDir + private val context: Context = context + private val stubId = R.drawable.sym_def_app_icon + private val photosQueue = PhotosQueue() + private val photoLoaderThread = PhotosLoader() + + init { + photoLoaderThread.priority = Thread.NORM_PRIORITY - 1 + } + + fun clearCache() { + cache.clear() + cacheDir.listFiles()?.forEach { it.delete() } + } + + private fun decodeFile(f: File): Bitmap? { + return try { + val o = BitmapFactory.Options().apply { + inJustDecodeBounds = true + } + BitmapFactory.decodeStream(FileInputStream(f), null, o) + + val requiredSize = 70 + var widthTmp = o.outWidth + var heightTmp = o.outHeight + var scale = 1 + while (true) { + if (widthTmp / 2 < requiredSize || heightTmp / 2 < requiredSize) break + widthTmp /= 2 + heightTmp /= 2 + scale *= 2 + } + + val o2 = BitmapFactory.Options().apply { + inSampleSize = scale + } + BitmapFactory.decodeStream(FileInputStream(f), null, o2) + } catch (e: FileNotFoundException) { + null + } + } + + fun displayImage(uid: Int, activity: Activity, imageView: ImageView) { + if (cache.containsKey(uid)) { + imageView.setImageBitmap(cache[uid]) + } else { + queuePhoto(uid, activity, imageView) + imageView.setImageResource(stubId) + } + } + + private fun getBitmap(uid: Int): Bitmap? { + val filename = uid.toString() + val f = File(cacheDir, filename) + + // from SD cache + val b = decodeFile(f) + if (b != null) return b + + // from app + return try { + val icon = Utils.getAppIcon(context, uid) as? BitmapDrawable + icon?.bitmap + } catch (ex: Exception) { + null + } + } + + private fun queuePhoto(uid: Int, activity: Activity, imageView: ImageView) { + photosQueue.clean(imageView) + val p = PhotoToLoad(uid, imageView) + synchronized(photosQueue.lock) { + photosQueue.photosToLoad.push(p) + (photosQueue.lock as Object).notifyAll() + } + + if (photoLoaderThread.state == Thread.State.NEW) { + photoLoaderThread.start() + } + } + + fun stopThread() { + photoLoaderThread.interrupt() + } + + private inner class BitmapDisplayer( + private val bitmap: Bitmap?, + private val imageView: ImageView + ) : Runnable { + override fun run() { + if (bitmap != null) { + imageView.setImageBitmap(bitmap) + } else { + imageView.setImageResource(stubId) + } + } + } + + private inner class PhotosLoader : Thread() { + override fun run() { + try { + while (true) { + if (photosQueue.photosToLoad.isEmpty()) { + synchronized(photosQueue.lock) { + (photosQueue.lock as Object).wait() + } + } + + synchronized(photosQueue.lock) { + if (photosQueue.photosToLoad.isNotEmpty()) { + val photoToLoad = photosQueue.photosToLoad.pop() + val bmp = getBitmap(photoToLoad.uid) + if (bmp != null) { + cache[photoToLoad.uid] = bmp + } + val tag = photoToLoad.imageView.tag + if (tag != null && tag as Int == photoToLoad.uid) { + val bd = BitmapDisplayer(bmp, photoToLoad.imageView) + val a = photoToLoad.imageView.context as Activity + a.runOnUiThread(bd) + } + } + } + + if (Thread.interrupted()) break + } + } catch (e: InterruptedException) { + // allow thread to exit + } + } + } + + private inner class PhotosQueue { + val photosToLoad = Stack() + val lock = Any() + + fun clean(image: ImageView) { + synchronized(lock) { + try { + val iterator = photosToLoad.iterator() + while (iterator.hasNext()) { + if (iterator.next().imageView == image) { + iterator.remove() + } + } + } catch (e: ArrayIndexOutOfBoundsException) { + // Nothing + } + } + } + } + + private data class PhotoToLoad(val uid: Int, val imageView: ImageView) +} diff --git a/app/src/main/java/org/proxydroid/utils/ImageLoaderFactory.java b/app/src/main/java/org/proxydroid/utils/ImageLoaderFactory.java deleted file mode 100644 index db2264c4..00000000 --- a/app/src/main/java/org/proxydroid/utils/ImageLoaderFactory.java +++ /dev/null @@ -1,50 +0,0 @@ -// dbartists - Douban artists client for Android -// Copyright (C) 2011 Max Lv -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not -// use this file except in compliance with the License. You may obtain a copy -// of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. -// -// -// ___====-_ _-====___ -// _--^^^#####// \\#####^^^--_ -// _-^##########// ( ) \\##########^-_ -// -############// |\^^/| \\############- -// _/############// (@::@) \\############\_ -// /#############(( \\// ))#############\ -// -###############\\ (oo) //###############- -// -#################\\ / VV \ //#################- -// -###################\\/ \//###################- -// _#/|##########/\######( /\ )######/\##########|\#_ -// |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \| -// ` |/ V V ` V \#\| | | |/#/ V ' V V \| ' -// ` ` ` ` / | | | | \ ' ' ' ' -// ( | | | | ) -// __\ | | | | /__ -// (vvv(VVV)(VVV)vvv) -// -// HERE BE DRAGONS - -package org.proxydroid.utils; - -import android.content.Context; - -public class ImageLoaderFactory { - - private static ImageLoader il = null; - - public static ImageLoader getImageLoader(Context context) { - if (il == null) { - il = new ImageLoader(context); - } - return il; - } -} diff --git a/app/src/main/java/org/proxydroid/utils/ImageLoaderFactory.kt b/app/src/main/java/org/proxydroid/utils/ImageLoaderFactory.kt new file mode 100644 index 00000000..d572b868 --- /dev/null +++ b/app/src/main/java/org/proxydroid/utils/ImageLoaderFactory.kt @@ -0,0 +1,30 @@ +// dbartists - Douban artists client for Android +// Copyright (C) 2011 Max Lv +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package org.proxydroid.utils + +import android.content.Context + +object ImageLoaderFactory { + private var imageLoader: ImageLoader? = null + + @JvmStatic + fun getImageLoader(context: Context): ImageLoader { + if (imageLoader == null) { + imageLoader = ImageLoader(context) + } + return imageLoader!! + } +} diff --git a/app/src/main/java/org/proxydroid/utils/LocalProxyServer.java b/app/src/main/java/org/proxydroid/utils/LocalProxyServer.java deleted file mode 100644 index 108fb733..00000000 --- a/app/src/main/java/org/proxydroid/utils/LocalProxyServer.java +++ /dev/null @@ -1,440 +0,0 @@ -/* proxydroid - Global / Individual Proxy App for Android - * Copyright (C) 2011 Max Lv - * - * This program 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. - * - * This program 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 this program. If not, see . - */ - -package org.proxydroid.utils; - -import android.util.Log; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.InetSocketAddress; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.SocketException; -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Local SOCKS5 proxy server that converts HTTP proxy connections to SOCKS5. - * This is used when the upstream proxy is HTTP/HTTPS but tun2socks requires SOCKS5. - */ -public class LocalProxyServer { - - private static final String TAG = "LocalProxyServer"; - private static final int SOCKS5_VERSION = 0x05; - private static final int SOCKS5_AUTH_NONE = 0x00; - private static final int SOCKS5_AUTH_PASSWORD = 0x02; - private static final int SOCKS5_CMD_CONNECT = 0x01; - private static final int SOCKS5_ATYP_IPV4 = 0x01; - private static final int SOCKS5_ATYP_DOMAIN = 0x03; - private static final int SOCKS5_ATYP_IPV6 = 0x04; - - private final int localPort; - private final String upstreamHost; - private final int upstreamPort; - private final String upstreamType; - private final String upstreamUser; - private final String upstreamPassword; - - private ServerSocket serverSocket; - private ExecutorService executorService; - private AtomicBoolean isRunning = new AtomicBoolean(false); - private Thread serverThread; - - public LocalProxyServer(int localPort, String upstreamHost, int upstreamPort, - String upstreamType, String upstreamUser, String upstreamPassword) { - this.localPort = localPort; - this.upstreamHost = upstreamHost; - this.upstreamPort = upstreamPort; - this.upstreamType = upstreamType; - this.upstreamUser = upstreamUser != null ? upstreamUser : ""; - this.upstreamPassword = upstreamPassword != null ? upstreamPassword : ""; - } - - public void start() throws IOException { - if (isRunning.getAndSet(true)) { - Log.w(TAG, "Local proxy server is already running"); - return; - } - - serverSocket = new ServerSocket(localPort, 50, java.net.InetAddress.getByName("127.0.0.1")); - executorService = Executors.newCachedThreadPool(); - - serverThread = new Thread(() -> { - Log.i(TAG, "Local SOCKS5 server started on port " + localPort); - while (isRunning.get() && !serverSocket.isClosed()) { - try { - Socket clientSocket = serverSocket.accept(); - executorService.submit(() -> handleClient(clientSocket)); - } catch (SocketException e) { - if (isRunning.get()) { - Log.e(TAG, "Socket error", e); - } - } catch (IOException e) { - if (isRunning.get()) { - Log.e(TAG, "Accept error", e); - } - } - } - }, "LocalProxyServer"); - - serverThread.start(); - } - - public void stop() { - if (!isRunning.getAndSet(false)) { - return; - } - - Log.i(TAG, "Stopping local proxy server"); - - if (serverSocket != null && !serverSocket.isClosed()) { - try { - serverSocket.close(); - } catch (IOException e) { - Log.e(TAG, "Error closing server socket", e); - } - } - - if (executorService != null) { - executorService.shutdownNow(); - } - - if (serverThread != null) { - try { - serverThread.join(2000); - } catch (InterruptedException e) { - Log.w(TAG, "Interrupted while waiting for server thread"); - } - } - } - - private void handleClient(Socket clientSocket) { - try { - InputStream clientIn = clientSocket.getInputStream(); - OutputStream clientOut = clientSocket.getOutputStream(); - - // SOCKS5 handshake - read auth methods - int version = clientIn.read(); - if (version != SOCKS5_VERSION) { - Log.e(TAG, "Invalid SOCKS version: " + version); - clientSocket.close(); - return; - } - - int nmethods = clientIn.read(); - byte[] methods = new byte[nmethods]; - readFully(clientIn, methods); - - // Reply with no auth required (we handle auth to upstream ourselves) - clientOut.write(new byte[]{SOCKS5_VERSION, SOCKS5_AUTH_NONE}); - clientOut.flush(); - - // Read connection request - version = clientIn.read(); - int cmd = clientIn.read(); - int rsv = clientIn.read(); - int atyp = clientIn.read(); - - if (version != SOCKS5_VERSION || cmd != SOCKS5_CMD_CONNECT) { - sendSocks5Error(clientOut, (byte) 0x07); // Command not supported - clientSocket.close(); - return; - } - - String targetHost; - int targetPort; - - switch (atyp) { - case SOCKS5_ATYP_IPV4: - byte[] ipv4 = new byte[4]; - readFully(clientIn, ipv4); - targetHost = String.format("%d.%d.%d.%d", - ipv4[0] & 0xFF, ipv4[1] & 0xFF, ipv4[2] & 0xFF, ipv4[3] & 0xFF); - break; - case SOCKS5_ATYP_DOMAIN: - int domainLen = clientIn.read(); - byte[] domain = new byte[domainLen]; - readFully(clientIn, domain); - targetHost = new String(domain, StandardCharsets.UTF_8); - break; - case SOCKS5_ATYP_IPV6: - byte[] ipv6 = new byte[16]; - readFully(clientIn, ipv6); - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < 16; i += 2) { - if (i > 0) sb.append(":"); - sb.append(String.format("%02x%02x", ipv6[i] & 0xFF, ipv6[i + 1] & 0xFF)); - } - targetHost = sb.toString(); - break; - default: - sendSocks5Error(clientOut, (byte) 0x08); // Address type not supported - clientSocket.close(); - return; - } - - byte[] portBytes = new byte[2]; - readFully(clientIn, portBytes); - targetPort = ((portBytes[0] & 0xFF) << 8) | (portBytes[1] & 0xFF); - - // Connect to upstream proxy and relay - Socket upstreamSocket = connectToUpstream(targetHost, targetPort); - if (upstreamSocket == null) { - sendSocks5Error(clientOut, (byte) 0x05); // Connection refused - clientSocket.close(); - return; - } - - // Send success response - byte[] response = new byte[]{ - SOCKS5_VERSION, 0x00, 0x00, SOCKS5_ATYP_IPV4, - 0x00, 0x00, 0x00, 0x00, // Bound address (0.0.0.0) - 0x00, 0x00 // Bound port (0) - }; - clientOut.write(response); - clientOut.flush(); - - // Start bidirectional relay - relay(clientSocket, upstreamSocket); - - } catch (Exception e) { - Log.e(TAG, "Error handling client", e); - } finally { - try { - clientSocket.close(); - } catch (IOException ignored) { - } - } - } - - private Socket connectToUpstream(String targetHost, int targetPort) { - try { - Socket socket = new Socket(); - socket.connect(new InetSocketAddress(upstreamHost, upstreamPort), 10000); - - InputStream in = socket.getInputStream(); - OutputStream out = socket.getOutputStream(); - - if ("socks5".equals(upstreamType) || "socks4".equals(upstreamType)) { - return connectSocks5(socket, in, out, targetHost, targetPort); - } else { - // HTTP CONNECT for http, https, http-tunnel - return connectHttpTunnel(socket, in, out, targetHost, targetPort); - } - } catch (Exception e) { - Log.e(TAG, "Error connecting to upstream: " + targetHost + ":" + targetPort, e); - return null; - } - } - - private Socket connectSocks5(Socket socket, InputStream in, OutputStream out, - String targetHost, int targetPort) throws IOException { - // SOCKS5 handshake - boolean hasAuth = !upstreamUser.isEmpty(); - if (hasAuth) { - out.write(new byte[]{SOCKS5_VERSION, 0x02, SOCKS5_AUTH_NONE, SOCKS5_AUTH_PASSWORD}); - } else { - out.write(new byte[]{SOCKS5_VERSION, 0x01, SOCKS5_AUTH_NONE}); - } - out.flush(); - - int version = in.read(); - int method = in.read(); - - if (version != SOCKS5_VERSION) { - Log.e(TAG, "Upstream SOCKS version mismatch: " + version); - socket.close(); - return null; - } - - if (method == SOCKS5_AUTH_PASSWORD && hasAuth) { - // Send username/password auth - byte[] userBytes = upstreamUser.getBytes(StandardCharsets.UTF_8); - byte[] passBytes = upstreamPassword.getBytes(StandardCharsets.UTF_8); - byte[] authRequest = new byte[3 + userBytes.length + passBytes.length]; - authRequest[0] = 0x01; // Auth version - authRequest[1] = (byte) userBytes.length; - System.arraycopy(userBytes, 0, authRequest, 2, userBytes.length); - authRequest[2 + userBytes.length] = (byte) passBytes.length; - System.arraycopy(passBytes, 0, authRequest, 3 + userBytes.length, passBytes.length); - out.write(authRequest); - out.flush(); - - int authVersion = in.read(); - int authStatus = in.read(); - if (authStatus != 0x00) { - Log.e(TAG, "Upstream SOCKS auth failed"); - socket.close(); - return null; - } - } else if (method != SOCKS5_AUTH_NONE) { - Log.e(TAG, "Unsupported auth method: " + method); - socket.close(); - return null; - } - - // Send connect request - byte[] hostBytes = targetHost.getBytes(StandardCharsets.UTF_8); - byte[] request = new byte[7 + hostBytes.length]; - request[0] = SOCKS5_VERSION; - request[1] = SOCKS5_CMD_CONNECT; - request[2] = 0x00; // Reserved - request[3] = SOCKS5_ATYP_DOMAIN; - request[4] = (byte) hostBytes.length; - System.arraycopy(hostBytes, 0, request, 5, hostBytes.length); - request[5 + hostBytes.length] = (byte) ((targetPort >> 8) & 0xFF); - request[6 + hostBytes.length] = (byte) (targetPort & 0xFF); - out.write(request); - out.flush(); - - // Read response - byte[] response = new byte[4]; - readFully(in, response); - - if (response[1] != 0x00) { - Log.e(TAG, "Upstream SOCKS connect failed: " + response[1]); - socket.close(); - return null; - } - - // Skip bound address - int atyp = response[3]; - switch (atyp) { - case SOCKS5_ATYP_IPV4: - in.skip(4 + 2); - break; - case SOCKS5_ATYP_DOMAIN: - int len = in.read(); - in.skip(len + 2); - break; - case SOCKS5_ATYP_IPV6: - in.skip(16 + 2); - break; - } - - return socket; - } - - private Socket connectHttpTunnel(Socket socket, InputStream in, OutputStream out, - String targetHost, int targetPort) throws IOException { - StringBuilder request = new StringBuilder(); - request.append("CONNECT ").append(targetHost).append(":").append(targetPort).append(" HTTP/1.1\r\n"); - request.append("Host: ").append(targetHost).append(":").append(targetPort).append("\r\n"); - - if (!upstreamUser.isEmpty()) { - String auth = upstreamUser + ":" + upstreamPassword; - String encoded = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8)); - request.append("Proxy-Authorization: Basic ").append(encoded).append("\r\n"); - } - - request.append("\r\n"); - out.write(request.toString().getBytes(StandardCharsets.UTF_8)); - out.flush(); - - // Read response - StringBuilder response = new StringBuilder(); - int ch; - while ((ch = in.read()) != -1) { - response.append((char) ch); - if (response.toString().endsWith("\r\n\r\n")) { - break; - } - } - - String responseStr = response.toString(); - if (!responseStr.contains("200")) { - Log.e(TAG, "HTTP CONNECT failed: " + responseStr); - socket.close(); - return null; - } - - return socket; - } - - private void relay(Socket client, Socket upstream) { - Thread clientToUpstream = new Thread(() -> { - try { - copy(client.getInputStream(), upstream.getOutputStream()); - } catch (IOException ignored) { - } finally { - closeQuietly(client); - closeQuietly(upstream); - } - }, "Relay-C2U"); - - Thread upstreamToClient = new Thread(() -> { - try { - copy(upstream.getInputStream(), client.getOutputStream()); - } catch (IOException ignored) { - } finally { - closeQuietly(client); - closeQuietly(upstream); - } - }, "Relay-U2C"); - - clientToUpstream.start(); - upstreamToClient.start(); - - try { - clientToUpstream.join(); - upstreamToClient.join(); - } catch (InterruptedException ignored) { - } - } - - private void copy(InputStream in, OutputStream out) throws IOException { - byte[] buffer = new byte[8192]; - int len; - while ((len = in.read(buffer)) != -1) { - out.write(buffer, 0, len); - out.flush(); - } - } - - private void readFully(InputStream in, byte[] buffer) throws IOException { - int offset = 0; - while (offset < buffer.length) { - int read = in.read(buffer, offset, buffer.length - offset); - if (read == -1) { - throw new IOException("Unexpected end of stream"); - } - offset += read; - } - } - - private void sendSocks5Error(OutputStream out, byte error) throws IOException { - out.write(new byte[]{ - SOCKS5_VERSION, error, 0x00, SOCKS5_ATYP_IPV4, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - }); - out.flush(); - } - - private void closeQuietly(Socket socket) { - try { - if (socket != null && !socket.isClosed()) { - socket.close(); - } - } catch (IOException ignored) { - } - } -} diff --git a/app/src/main/java/org/proxydroid/utils/LocalProxyServer.kt b/app/src/main/java/org/proxydroid/utils/LocalProxyServer.kt new file mode 100644 index 00000000..a5f560a9 --- /dev/null +++ b/app/src/main/java/org/proxydroid/utils/LocalProxyServer.kt @@ -0,0 +1,187 @@ +/* proxydroid - Global / Individual Proxy App for Android + * Copyright (C) 2011 Max Lv + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package org.proxydroid.utils + +import android.util.Log +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.net.InetAddress +import java.net.ServerSocket +import java.net.Socket +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +class LocalProxyServer( + private val localPort: Int, + private val remoteHost: String, + private val remotePort: Int, + private val username: String?, + private val password: String? +) : Runnable { + + private var serverSocket: ServerSocket? = null + private var running = false + private var executor: ExecutorService? = null + + companion object { + private const val TAG = "LocalProxyServer" + private const val BUFFER_SIZE = 8192 + } + + @Synchronized + fun start() { + if (running) return + running = true + executor = Executors.newCachedThreadPool() + Thread(this).start() + } + + @Synchronized + fun stop() { + running = false + try { + serverSocket?.close() + } catch (e: IOException) { + Log.e(TAG, "Error closing server socket", e) + } + executor?.shutdownNow() + executor = null + } + + override fun run() { + try { + serverSocket = ServerSocket(localPort, 50, InetAddress.getByName("127.0.0.1")) + Log.i(TAG, "Local SOCKS5 proxy server started on port $localPort") + + while (running) { + try { + val clientSocket = serverSocket?.accept() ?: break + executor?.execute(ClientHandler(clientSocket)) + } catch (e: IOException) { + if (running) { + Log.e(TAG, "Error accepting connection", e) + } + } + } + } catch (e: IOException) { + Log.e(TAG, "Error starting server", e) + } finally { + try { + serverSocket?.close() + } catch (e: IOException) { + // Ignore + } + } + } + + private inner class ClientHandler(private val clientSocket: Socket) : Runnable { + override fun run() { + var remoteSocket: Socket? = null + try { + val clientIn = clientSocket.getInputStream() + val clientOut = clientSocket.getOutputStream() + + // Connect to remote SOCKS5 proxy + remoteSocket = Socket(remoteHost, remotePort) + val remoteIn = remoteSocket.getInputStream() + val remoteOut = remoteSocket.getOutputStream() + + // Authenticate with remote proxy if needed + if (!authenticate(remoteIn, remoteOut)) { + Log.e(TAG, "Authentication failed") + return + } + + // Relay data between client and remote + val clientToRemote = Thread { relay(clientIn, remoteOut) } + val remoteToClient = Thread { relay(remoteIn, clientOut) } + + clientToRemote.start() + remoteToClient.start() + + clientToRemote.join() + remoteToClient.join() + + } catch (e: Exception) { + Log.e(TAG, "Error handling client", e) + } finally { + try { + clientSocket.close() + } catch (e: IOException) { + // Ignore + } + try { + remoteSocket?.close() + } catch (e: IOException) { + // Ignore + } + } + } + + private fun authenticate(input: InputStream, output: OutputStream): Boolean { + return try { + // SOCKS5 greeting + if (username != null && password != null) { + output.write(byteArrayOf(0x05, 0x01, 0x02)) // Version 5, 1 method, Username/Password + } else { + output.write(byteArrayOf(0x05, 0x01, 0x00)) // Version 5, 1 method, No auth + } + output.flush() + + val response = ByteArray(2) + if (input.read(response) != 2) return false + if (response[0] != 0x05.toByte()) return false + + if (response[1] == 0x02.toByte() && username != null && password != null) { + // Username/Password authentication + val authRequest = ByteArray(3 + username.length + password.length) + authRequest[0] = 0x01 + authRequest[1] = username.length.toByte() + System.arraycopy(username.toByteArray(), 0, authRequest, 2, username.length) + authRequest[2 + username.length] = password.length.toByte() + System.arraycopy(password.toByteArray(), 0, authRequest, 3 + username.length, password.length) + output.write(authRequest) + output.flush() + + val authResponse = ByteArray(2) + if (input.read(authResponse) != 2) return false + if (authResponse[1] != 0x00.toByte()) return false + } + + true + } catch (e: IOException) { + Log.e(TAG, "Authentication error", e) + false + } + } + + private fun relay(input: InputStream, output: OutputStream) { + try { + val buffer = ByteArray(BUFFER_SIZE) + var bytesRead: Int + while (input.read(buffer).also { bytesRead = it } != -1) { + output.write(buffer, 0, bytesRead) + output.flush() + } + } catch (e: IOException) { + // Connection closed + } + } + } +} diff --git a/app/src/main/java/org/proxydroid/utils/Option.java b/app/src/main/java/org/proxydroid/utils/Option.java deleted file mode 100644 index c7fe3e5f..00000000 --- a/app/src/main/java/org/proxydroid/utils/Option.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.proxydroid.utils; - -public class Option implements Comparable

- * Construct the validator either for a single regular expression or a set - * (array) of regular expressions. By default validation is case - * sensitive but constructors are provided to allow case in-sensitive - * validation. For example to create a validator which does case - * in-sensitive validation for a set of regular expressions: - * - *

- *         String[] regexs = new String[] {...};
- *         RegexValidator validator = new RegexValidator(regexs, false);
- * 
- *

- *

    - *
  • Validate true or false:
  • - *
      - *
    • boolean valid = validator.isValid(value);
    • - *
    - *
  • Validate returning an aggregated String of the matched groups:
  • - *
      - *
    • String result = validator.validate(value);
    • - *
    - *
  • Validate returning the matched groups:
  • - *
      - *
    • String[] result = validator.match(value);
    • - *
    - *
- *

- * Cached instances pre-compile and re-use {@link Pattern}(s) - which according - * to the {@link Pattern} API are safe to use in a multi-threaded environment. - * - * @version $Revision$ $Date$ - * @since Validator 1.4 - */ -public class RegexValidator implements Serializable { - - private final Pattern[] patterns; - - /** - * Construct a case sensitive validator for a single regular - * expression. - * - * @param regex - * The regular expression this validator will validate against - */ - public RegexValidator(String regex) { - this(regex, true); - } - - /** - * Construct a validator for a single regular expression with the specified - * case sensitivity. - * - * @param regex - * The regular expression this validator will validate against - * @param caseSensitive - * when true matching is case sensitive, - * otherwise matching is case in-sensitive - */ - public RegexValidator(String regex, boolean caseSensitive) { - this(new String[] { regex }, caseSensitive); - } - - /** - * Construct a case sensitive validator that matches any one of the - * set of regular expressions. - * - * @param regexs - * The set of regular expressions this validator will validate - * against - */ - public RegexValidator(String[] regexs) { - this(regexs, true); - } - - /** - * Construct a validator that matches any one of the set of regular - * expressions with the specified case sensitivity. - * - * @param regexs - * The set of regular expressions this validator will validate - * against - * @param caseSensitive - * when true matching is case sensitive, - * otherwise matching is case in-sensitive - */ - public RegexValidator(String[] regexs, boolean caseSensitive) { - if (regexs == null || regexs.length == 0) { - throw new IllegalArgumentException( - "Regular expressions are missing"); - } - patterns = new Pattern[regexs.length]; - int flags = (caseSensitive ? 0 : Pattern.CASE_INSENSITIVE); - for (int i = 0; i < regexs.length; i++) { - if (regexs[i] == null || regexs[i].length() == 0) { - throw new IllegalArgumentException("Regular expression[" + i - + "] is missing"); - } - patterns[i] = Pattern.compile(regexs[i], flags); - } - } - - /** - * Validate a value against the set of regular expressions. - * - * @param value - * The value to validate. - * @return true if the value is valid otherwise - * false. - */ - public boolean isValid(String value) { - if (value == null) { - return false; - } - for (int i = 0; i < patterns.length; i++) { - if (patterns[i].matcher(value).matches()) { - return true; - } - } - return false; - } - - /** - * Validate a value against the set of regular expressions returning the - * array of matched groups. - * - * @param value - * The value to validate. - * @return String array of the groups matched if valid or - * null if invalid - */ - public String[] match(String value) { - if (value == null) { - return null; - } - for (int i = 0; i < patterns.length; i++) { - Matcher matcher = patterns[i].matcher(value); - if (matcher.matches()) { - int count = matcher.groupCount(); - String[] groups = new String[count]; - for (int j = 0; j < count; j++) { - groups[j] = matcher.group(j + 1); - } - return groups; - } - } - return null; - } - - /** - * Validate a value against the set of regular expressions returning a - * String value of the aggregated groups. - * - * @param value - * The value to validate. - * @return Aggregated String value comprised of the groups matched if - * valid or null if invalid - */ - public String validate(String value) { - if (value == null) { - return null; - } - for (int i = 0; i < patterns.length; i++) { - Matcher matcher = patterns[i].matcher(value); - if (matcher.matches()) { - int count = matcher.groupCount(); - if (count == 1) { - return matcher.group(1); - } - StringBuffer buffer = new StringBuffer(); - for (int j = 0; j < count; j++) { - String component = matcher.group(j + 1); - if (component != null) { - buffer.append(component); - } - } - return buffer.toString(); - } - } - return null; - } - - /** - * Provide a String representation of this validator. - * - * @return A String representation of this validator - */ - @Override - public String toString() { - StringBuffer buffer = new StringBuffer(); - buffer.append("RegexValidator{"); - for (int i = 0; i < patterns.length; i++) { - if (i > 0) { - buffer.append(","); - } - buffer.append(patterns[i].pattern()); - } - buffer.append("}"); - return buffer.toString(); - } - -} \ No newline at end of file diff --git a/app/src/main/java/org/proxydroid/utils/RegexValidator.kt b/app/src/main/java/org/proxydroid/utils/RegexValidator.kt new file mode 100644 index 00000000..b8d8cd55 --- /dev/null +++ b/app/src/main/java/org/proxydroid/utils/RegexValidator.kt @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.proxydroid.utils + +import java.io.Serializable +import java.util.regex.Pattern + +class RegexValidator : Serializable { + private val patterns: Array + + constructor(regex: String) : this(regex, true) + + constructor(regex: String, caseSensitive: Boolean) : this(arrayOf(regex), caseSensitive) + + constructor(regexs: Array) : this(regexs, true) + + constructor(regexs: Array, caseSensitive: Boolean) { + require(regexs.isNotEmpty()) { "Regular expressions are missing" } + val flags = if (caseSensitive) 0 else Pattern.CASE_INSENSITIVE + patterns = Array(regexs.size) { i -> + require(regexs[i].isNotEmpty()) { "Regular expression[$i] is missing" } + Pattern.compile(regexs[i], flags) + } + } + + fun isValid(value: String?): Boolean { + if (value == null) return false + return patterns.any { it.matcher(value).matches() } + } + + fun match(value: String?): Array? { + if (value == null) return null + for (pattern in patterns) { + val matcher = pattern.matcher(value) + if (matcher.matches()) { + val count = matcher.groupCount() + return Array(count) { j -> matcher.group(j + 1) ?: "" } + } + } + return null + } + + fun validate(value: String?): String? { + if (value == null) return null + for (pattern in patterns) { + val matcher = pattern.matcher(value) + if (matcher.matches()) { + val count = matcher.groupCount() + if (count == 1) { + return matcher.group(1) + } + val buffer = StringBuilder() + for (j in 0 until count) { + val component = matcher.group(j + 1) + if (component != null) { + buffer.append(component) + } + } + return buffer.toString() + } + } + return null + } + + override fun toString(): String { + return buildString { + append("RegexValidator{") + patterns.forEachIndexed { index, pattern -> + if (index > 0) append(",") + append(pattern.pattern()) + } + append("}") + } + } +} diff --git a/app/src/main/java/org/proxydroid/utils/Tun2SocksHelper.java b/app/src/main/java/org/proxydroid/utils/Tun2SocksHelper.java deleted file mode 100644 index 26953e33..00000000 --- a/app/src/main/java/org/proxydroid/utils/Tun2SocksHelper.java +++ /dev/null @@ -1,126 +0,0 @@ -/* proxydroid - Global / Individual Proxy App for Android - * Copyright (C) 2011 Max Lv - * - * This program 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. - * - * This program 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 this program. If not, see . - */ - -package org.proxydroid.utils; - -import android.util.Log; - -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Helper class to manage the tun2socks native library. - * This class wraps the native JNI calls to the tun2socks library - * which handles converting TUN interface traffic to SOCKS5 proxy connections. - */ -public class Tun2SocksHelper { - - private static final String TAG = "Tun2SocksHelper"; - - static { - try { - System.loadLibrary("tun2socks"); - } catch (UnsatisfiedLinkError e) { - Log.e(TAG, "Failed to load tun2socks library", e); - } - } - - private final int tunFd; - private final int mtu; - private final String socksHost; - private final int socksPort; - private final String socksUser; - private final String socksPassword; - private final AtomicBoolean isRunning = new AtomicBoolean(false); - private Thread workerThread; - - /** - * Create a new Tun2SocksHelper instance. - * - * @param tunFd The file descriptor of the TUN interface - * @param mtu The MTU of the TUN interface - * @param socksHost The SOCKS5 proxy host - * @param socksPort The SOCKS5 proxy port - * @param socksUser The SOCKS5 proxy username (can be null) - * @param socksPassword The SOCKS5 proxy password (can be null) - */ - public Tun2SocksHelper(int tunFd, int mtu, String socksHost, int socksPort, - String socksUser, String socksPassword) { - this.tunFd = tunFd; - this.mtu = mtu; - this.socksHost = socksHost; - this.socksPort = socksPort; - this.socksUser = socksUser != null ? socksUser : ""; - this.socksPassword = socksPassword != null ? socksPassword : ""; - } - - /** - * Start the tun2socks process. - */ - public void start() { - if (isRunning.getAndSet(true)) { - Log.w(TAG, "tun2socks is already running"); - return; - } - - workerThread = new Thread(() -> { - Log.i(TAG, "Starting tun2socks: " + socksHost + ":" + socksPort); - int result = nativeStart(tunFd, mtu, socksHost, socksPort, socksUser, socksPassword); - Log.i(TAG, "tun2socks exited with code: " + result); - isRunning.set(false); - }, "Tun2Socks-Worker"); - - workerThread.start(); - } - - /** - * Stop the tun2socks process. - */ - public void stop() { - if (!isRunning.getAndSet(false)) { - return; - } - - Log.i(TAG, "Stopping tun2socks"); - nativeStop(); - - if (workerThread != null) { - try { - workerThread.join(2000); - if (workerThread.isAlive()) { - workerThread.interrupt(); - } - } catch (InterruptedException e) { - Log.w(TAG, "Interrupted while waiting for tun2socks to stop"); - } - workerThread = null; - } - } - - /** - * Check if tun2socks is running. - * - * @return true if tun2socks is running - */ - public boolean isRunning() { - return isRunning.get(); - } - - // Native methods implemented in tun2socks_jni.cpp - private native int nativeStart(int tunFd, int mtu, String socksHost, int socksPort, - String socksUser, String socksPassword); - private native void nativeStop(); -} diff --git a/app/src/main/java/org/proxydroid/utils/Tun2SocksHelper.kt b/app/src/main/java/org/proxydroid/utils/Tun2SocksHelper.kt new file mode 100644 index 00000000..a57abae0 --- /dev/null +++ b/app/src/main/java/org/proxydroid/utils/Tun2SocksHelper.kt @@ -0,0 +1,73 @@ +/* proxydroid - Global / Individual Proxy App for Android + * Copyright (C) 2011 Max Lv + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package org.proxydroid.utils + +import android.util.Log + +class Tun2SocksHelper { + private var running = false + + companion object { + private const val TAG = "Tun2SocksHelper" + + init { + System.loadLibrary("tun2socks") + } + } + + @Synchronized + fun start( + tunFd: Int, + mtu: Int, + socksHost: String, + socksPort: Int, + socksUser: String?, + socksPassword: String? + ): Boolean { + if (running) { + Log.w(TAG, "tun2socks already running") + return false + } + val result = nativeStart(tunFd, mtu, socksHost, socksPort, socksUser ?: "", socksPassword ?: "") + running = result == 0 + return running + } + + @Synchronized + fun stop() { + if (!running) { + Log.w(TAG, "tun2socks not running") + return + } + nativeStop() + running = false + } + + fun isRunning(): Boolean = running + + private external fun nativeStart( + tunFd: Int, + mtu: Int, + socksHost: String, + socksPort: Int, + socksUser: String, + socksPassword: String + ): Int + + private external fun nativeStop() +} diff --git a/app/src/main/java/org/proxydroid/utils/Utils.java b/app/src/main/java/org/proxydroid/utils/Utils.java deleted file mode 100644 index cac1c9bd..00000000 --- a/app/src/main/java/org/proxydroid/utils/Utils.java +++ /dev/null @@ -1,457 +0,0 @@ -package org.proxydroid.utils; - -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.Signature; -import android.graphics.drawable.Drawable; -import android.os.Environment; -import android.util.Log; - -import org.proxydroid.Exec; -import org.proxydroid.ProxyDroidService; -import org.proxydroid.R; - -import java.io.File; -import java.io.FileDescriptor; -import java.io.FileInputStream; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; - -public class Utils { - - public final static String TAG = "ProxyDroid"; - public final static String DEFAULT_SHELL = "/system/bin/sh"; - public final static String DEFAULT_ROOTS[] = {"/sbin/su", "/system/bin/su", "/system/xbin/su", "/su/bin/su", "/su/xbin/su", "/magisk/.core/bin/su"}; - public final static String DEFAULT_IPTABLES = "iptables"; - public final static String ALTERNATIVE_IPTABLES = "/system/bin/iptables"; - public final static int TIME_OUT = -99; - private static boolean initialized = false; - private static int hasRedirectSupport = -1; - private static int isRoot = -1; - private static String shell = null; - private static String root_shell = null; - private static String iptables = null; - private static String data_path = null; - private static boolean isConnecting = false; - - public static boolean isConnecting() { - return isConnecting; - } - - public static void setConnecting(boolean value) { - isConnecting = value; - } - - public static String preserve(String s) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - if (c == '\\' || c == '$' || c == '`' || c == '"') - sb.append('\\'); - sb.append(c); - } - return sb.toString(); - } - - private static void checkIptables() { - - if (!isRoot()) { - iptables = DEFAULT_IPTABLES; - return; - } - - // Check iptables binary - iptables = DEFAULT_IPTABLES; - - String lines = null; - - boolean compatible = false; - boolean version = false; - - StringBuilder sb = new StringBuilder(); - String command = iptables + " --version\n" + iptables - + " -L -t nat -n\n" + "exit\n"; - - int exitcode = runScript(command, sb, 10 * 1000, true); - - if (exitcode == TIME_OUT) - return; - - lines = sb.toString(); - - if (lines.contains("OUTPUT")) { - compatible = true; - } - if (lines.contains("v1.4.")) { - version = true; - } - - if (!compatible || !version) { - iptables = ALTERNATIVE_IPTABLES; - if (!new File(iptables).exists()) - iptables = "iptables"; - } - - } - - public static String getDataPath(Context ctx) { - - if (data_path == null) { - - if (Environment.MEDIA_MOUNTED.equals(Environment - .getExternalStorageState())) { - data_path = Environment.getExternalStorageDirectory() - .getAbsolutePath(); - } else { - data_path = "/sdcard"; - } - - Log.d(TAG, "Python Data Path: " + data_path); - } - - return data_path; - } - - public static boolean getHasRedirectSupport() { - if (hasRedirectSupport == -1) - initHasRedirectSupported(); - return hasRedirectSupport == 1; - } - - public static String getIptables() { - if (iptables == null) - checkIptables(); - return iptables; - } - - private static String getShell() { - if (shell == null) { - shell = DEFAULT_SHELL; - if (!new File(shell).exists()) - shell = "sh"; - } - return shell; - } - - public static String getSignature(Context ctx) { - Signature sig = null; - try { - Signature[] sigs; - sigs = ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), - PackageManager.GET_SIGNATURES).signatures; - if (sigs != null && sigs.length > 0) - sig = sigs[0]; - } catch (Exception ignore) { - // Nothing - } - if (sig == null) - return null; - return sig.toCharsString().substring(11, 256); - } - - public static void initHasRedirectSupported() { - - if (!Utils.isRoot()) - return; - - StringBuilder sb = new StringBuilder(); - String command = Utils.getIptables() - + " -t nat -A OUTPUT -p udp --dport 54 -j REDIRECT --to 8154"; - - int exitcode = runScript(command, sb, 10 * 1000, true); - - String lines = sb.toString(); - - hasRedirectSupport = 1; - - // flush the check command - Utils.runRootCommand(command.replace("-A", "-D")); - - if (exitcode == TIME_OUT) - return; - - if (lines.contains("No chain/target/match")) { - hasRedirectSupport = 0; - } - } - - public static boolean isInitialized() { - if (initialized) - return true; - else { - initialized = true; - return false; - } - - } - - public static boolean isRoot() { - - if (isRoot != -1) - return isRoot == 1; - - // switch between binaries - root_shell = "su"; - for (int i = 0; i < DEFAULT_ROOTS.length; i++) { - if (new File(DEFAULT_ROOTS[i]).exists()) { - root_shell = DEFAULT_ROOTS[i]; - break; - } - } - - String lines = null; - - StringBuilder sb = new StringBuilder(); - String command = "ls /\n" + "exit\n"; - - int exitcode = runScript(command, sb, 10 * 1000, true); - - if (exitcode == TIME_OUT) { - return false; - } - - lines = sb.toString(); - - if (lines.contains("system")) { - isRoot = 1; - } - - return isRoot == 1; - } - - public static boolean runCommand(String command, int timeout) { - - Log.d(TAG, command); - - runScript(command, null, timeout, false); - - return true; - } - - public static boolean runCommand(String command) { - - return runCommand(command, 10 * 1000); - - } - - public static boolean runRootCommand(String command, int timeout) { - - Log.d(TAG, command); - - runScript(command, null, timeout, true); - - return true; - } - - public static boolean runRootCommand(String command) { - - return runRootCommand(command, 10 * 1000); - - } - - private synchronized static int runScript(String script, StringBuilder res, - long timeout, boolean asroot) { - final ScriptRunner runner = new ScriptRunner(script, res, asroot); - runner.start(); - try { - if (timeout > 0) { - runner.join(timeout); - } else { - runner.join(); - } - if (runner.isAlive()) { - // Timed-out - runner.destroy(); - runner.join(1000); - return TIME_OUT; - } - } catch (InterruptedException ex) { - return TIME_OUT; - } - return runner.exitcode; - } - - public static boolean isWorking() { - return ProxyDroidService.isServiceStarted(); - } - - public static void CopyStream(InputStream is, OutputStream os) { - final int buffer_size = 1024; - try { - byte[] bytes = new byte[buffer_size]; - for (; ; ) { - int count = is.read(bytes, 0, buffer_size); - if (count == -1) - break; - os.write(bytes, 0, count); - } - } catch (Exception ignored) { - } - } - - public static Drawable getAppIcon(Context c, int uid) { - PackageManager pm = c.getPackageManager(); - Drawable appIcon = c.getResources().getDrawable( - R.drawable.sym_def_app_icon); - String[] packages = pm.getPackagesForUid(uid); - - if (packages != null) { - if (packages.length == 1) { - try { - ApplicationInfo appInfo = pm.getApplicationInfo( - packages[0], 0); - appIcon = pm.getApplicationIcon(appInfo); - } catch (NameNotFoundException e) { - Log.e(c.getPackageName(), - "No package found matching with the uid " + uid); - } - } - } else { - Log.e(c.getPackageName(), "Package not found for uid " + uid); - } - - return appIcon; - } - - /** - * Internal thread used to execute scripts (as root or not). - */ - private static final class ScriptRunner extends Thread { - private final String scripts; - private final StringBuilder result; - private final boolean asroot; - public int exitcode = -1; - private int[] pid = new int[1]; - private FileDescriptor pipe; - - /** - * Creates a new script runner. - * - * @param script script to run - * @param result result output - * @param asroot if true, executes the script as root - */ - public ScriptRunner(String script, StringBuilder result, - boolean asroot) { - this.scripts = script; - this.result = result; - this.asroot = asroot; - } - - @Override - public void destroy() { - if (pid[0] != -1) { - Exec.hangupProcessGroup(pid[0]); - pid[0] = -1; - } - if (pipe != null) { - Exec.close(pipe); - pipe = null; - } - } - - private FileDescriptor createSubprocess(int[] processId, String cmd) { - ArrayList argList = parse(cmd); - String arg0 = argList.get(0); - String[] args = argList.toArray(new String[1]); - - return Exec.createSubprocess(result != null ? 1 : 0, arg0, args, null, - scripts + "\nexit\n", processId); - } - - private ArrayList parse(String cmd) { - final int PLAIN = 0; - final int WHITESPACE = 1; - final int INQUOTE = 2; - int state = WHITESPACE; - ArrayList result = new ArrayList(); - int cmdLen = cmd.length(); - StringBuilder builder = new StringBuilder(); - for (int i = 0; i < cmdLen; i++) { - char c = cmd.charAt(i); - if (state == PLAIN) { - if (Character.isWhitespace(c)) { - result.add(builder.toString()); - builder.delete(0, builder.length()); - state = WHITESPACE; - } else if (c == '"') { - state = INQUOTE; - } else { - builder.append(c); - } - } else if (state == WHITESPACE) { - if (Character.isWhitespace(c)) { - // do nothing - } else if (c == '"') { - state = INQUOTE; - } else { - state = PLAIN; - builder.append(c); - } - } else if (state == INQUOTE) { - if (c == '\\') { - if (i + 1 < cmdLen) { - i += 1; - builder.append(cmd.charAt(i)); - } - } else if (c == '"') { - state = PLAIN; - } else { - builder.append(c); - } - } - } - if (builder.length() > 0) { - result.add(builder.toString()); - } - return result; - } - - @Override - public void run() { - pid[0] = -1; - - try { - - if (this.asroot) { - // Create the "su" request to run the script - pipe = createSubprocess(pid, root_shell); - } else { - // Create the "sh" request to run the script - pipe = createSubprocess(pid, getShell()); - } - - if (pid[0] != -1) { - exitcode = Exec.waitFor(pid[0]); - } - - if (result == null || pipe == null) return; - - final InputStream stdout = new FileInputStream(pipe); - final byte buf[] = new byte[8192]; - int read = 0; - - // Read stdout - while (stdout.available() > 0) { - read = stdout.read(buf); - result.append(new String(buf, 0, read)); - } - - } catch (Exception ex) { - Log.e(TAG, "Cannot execute command", ex); - if (result != null) - result.append("\n").append(ex); - } finally { - if (pipe != null) { - Exec.close(pipe); - } - if (pid[0] != -1) { - Exec.hangupProcessGroup(pid[0]); - } - } - } - } -} diff --git a/app/src/main/java/org/proxydroid/utils/Utils.kt b/app/src/main/java/org/proxydroid/utils/Utils.kt new file mode 100644 index 00000000..97ccb092 --- /dev/null +++ b/app/src/main/java/org/proxydroid/utils/Utils.kt @@ -0,0 +1,234 @@ +/* proxydroid - Global / Individual Proxy App for Android + * Copyright (C) 2011 Max Lv + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package org.proxydroid.utils + +import android.content.Context +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable +import android.os.Environment +import android.util.Log +import org.proxydroid.Exec +import java.io.* + +object Utils { + private const val TAG = "ProxyDroid" + + @Volatile + private var isRoot = false + + @Volatile + private var working = false + + @Volatile + private var connecting = false + + @JvmStatic + fun isRoot(): Boolean = isRoot + + @JvmStatic + fun setRoot(root: Boolean) { + isRoot = root + } + + @JvmStatic + fun isWorking(): Boolean = working + + @JvmStatic + fun setWorking(working: Boolean) { + this.working = working + } + + @JvmStatic + fun isConnecting(): Boolean = connecting + + @JvmStatic + fun setConnecting(connecting: Boolean) { + this.connecting = connecting + } + + @JvmStatic + fun getDataPath(context: Context): String { + return if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) { + context.getExternalFilesDir(null)?.absolutePath ?: context.filesDir.absolutePath + } else { + context.filesDir.absolutePath + } + } + + @JvmStatic + fun getAppIcon(context: Context, uid: Int): Drawable? { + val pm = context.packageManager + val packages = pm.getPackagesForUid(uid) + if (packages != null && packages.isNotEmpty()) { + try { + val appInfo = pm.getApplicationInfo(packages[0], 0) + return pm.getApplicationIcon(appInfo) + } catch (e: PackageManager.NameNotFoundException) { + Log.e(TAG, "Error getting app icon", e) + } + } + return null + } + + @JvmStatic + fun runRootCommand(command: String): String { + return runRootCommand(command, 10000) + } + + @JvmStatic + fun runRootCommand(command: String, timeout: Int): String { + val result = StringBuilder() + var process: Process? = null + var output: DataOutputStream? = null + var input: BufferedReader? = null + + try { + process = Runtime.getRuntime().exec("su") + output = DataOutputStream(process.outputStream) + input = BufferedReader(InputStreamReader(process.inputStream)) + + output.writeBytes("$command\n") + output.writeBytes("exit\n") + output.flush() + + var line: String? + while (input.readLine().also { line = it } != null) { + result.append(line).append("\n") + } + + process.waitFor() + + } catch (e: Exception) { + Log.e(TAG, "Error running root command", e) + } finally { + try { + output?.close() + input?.close() + process?.destroy() + } catch (e: Exception) { + // Ignore + } + } + + return result.toString() + } + + @JvmStatic + fun runScript( + context: Context, + script: String, + res: IntArray, + returnOutput: Boolean + ): String { + val result = StringBuilder() + val processId = IntArray(1) + + try { + val fd = Exec.createSubprocess( + 0, "/system/bin/sh", arrayOf("-"), null, script, processId + ) + + if (processId[0] > 0) { + val exitCode = Exec.waitFor(processId[0]) + res[0] = exitCode + } + } catch (e: Exception) { + Log.e(TAG, "Error running script", e) + res[0] = -1 + } + + return result.toString() + } + + @JvmStatic + fun checkRoot(): Boolean { + var rooted = false + try { + val process = Runtime.getRuntime().exec("su") + val output = DataOutputStream(process.outputStream) + output.writeBytes("id\n") + output.writeBytes("exit\n") + output.flush() + + val input = BufferedReader(InputStreamReader(process.inputStream)) + val line = input.readLine() + if (line != null && line.contains("uid=0")) { + rooted = true + } + + process.waitFor() + output.close() + input.close() + process.destroy() + } catch (e: Exception) { + Log.e(TAG, "Error checking root", e) + } + isRoot = rooted + return rooted + } + + @JvmStatic + fun copyAssets(context: Context, filename: String) { + val assetManager = context.assets + var input: InputStream? = null + var output: OutputStream? = null + + try { + input = assetManager.open(filename) + val outFile = File(context.filesDir, filename) + output = FileOutputStream(outFile) + + val buffer = ByteArray(1024) + var read: Int + while (input.read(buffer).also { read = it } != -1) { + output.write(buffer, 0, read) + } + + outFile.setExecutable(true, false) + } catch (e: IOException) { + Log.e(TAG, "Failed to copy asset file: $filename", e) + } finally { + try { + input?.close() + output?.close() + } catch (e: IOException) { + // Ignore + } + } + } + + @JvmStatic + fun getHasRedirectSupport(): Boolean { + val result = runRootCommand("iptables -t nat -L -n") + return result.contains("REDIRECT") + } + + @JvmStatic + fun getIptablesPath(): String { + return "iptables" + } + + @JvmStatic + fun getShellPath(): String { + // Prefer /system/bin/sh + if (File("/system/bin/sh").exists()) { + return "/system/bin/sh" + } + return "/bin/sh" + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2982687b..7b382f20 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,6 +20,7 @@ Running in the background + VPN proxy running Connected Fail to connect ProxyDroid service stopped diff --git a/app/src/test/java/org/proxydroid/ExampleUnitTest.java b/app/src/test/java/org/proxydroid/ExampleUnitTest.java deleted file mode 100644 index 1414b94b..00000000 --- a/app/src/test/java/org/proxydroid/ExampleUnitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.proxydroid; - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file diff --git a/app/src/test/java/org/proxydroid/ExampleUnitTest.kt b/app/src/test/java/org/proxydroid/ExampleUnitTest.kt new file mode 100644 index 00000000..2776a6f6 --- /dev/null +++ b/app/src/test/java/org/proxydroid/ExampleUnitTest.kt @@ -0,0 +1,11 @@ +package org.proxydroid + +import org.junit.Test +import org.junit.Assert.* + +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/app/src/test/java/org/proxydroid/utils/LocalHttpProxyTest.java b/app/src/test/java/org/proxydroid/utils/LocalHttpProxyTest.java deleted file mode 100644 index 18c3473b..00000000 --- a/app/src/test/java/org/proxydroid/utils/LocalHttpProxyTest.java +++ /dev/null @@ -1,332 +0,0 @@ -/* proxydroid - Global / Individual Proxy App for Android - * Copyright (C) 2011 Max Lv - * - * This program 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. - */ - -package org.proxydroid.utils; - -import org.junit.Test; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.nio.charset.StandardCharsets; - -import static org.junit.Assert.*; - -/** - * Unit tests for LocalProxyServer SOCKS5 implementation. - */ -public class LocalHttpProxyTest { - - @Test - public void testSocks5HandshakeNoAuth() throws Exception { - // Test SOCKS5 handshake without authentication - byte[] clientHello = new byte[]{0x05, 0x01, 0x00}; // Version 5, 1 method, no auth - byte[] expectedResponse = new byte[]{0x05, 0x00}; // Version 5, no auth selected - - ByteArrayInputStream input = new ByteArrayInputStream(clientHello); - ByteArrayOutputStream output = new ByteArrayOutputStream(); - - // Simulate reading version and methods - int version = input.read(); - int nmethods = input.read(); - byte[] methods = new byte[nmethods]; - input.read(methods); - - assertEquals(0x05, version); - assertEquals(1, nmethods); - assertEquals(0x00, methods[0]); - - // Write response - output.write(expectedResponse); - - byte[] response = output.toByteArray(); - assertArrayEquals(expectedResponse, response); - } - - @Test - public void testSocks5HandshakeWithAuth() throws Exception { - // Test SOCKS5 handshake with username/password authentication - byte[] clientHello = new byte[]{0x05, 0x02, 0x00, 0x02}; // Version 5, 2 methods, no auth + user/pass - - ByteArrayInputStream input = new ByteArrayInputStream(clientHello); - - int version = input.read(); - int nmethods = input.read(); - byte[] methods = new byte[nmethods]; - input.read(methods); - - assertEquals(0x05, version); - assertEquals(2, nmethods); - assertEquals(0x00, methods[0]); - assertEquals(0x02, methods[1]); - } - - @Test - public void testSocks5ConnectRequestIPv4() throws Exception { - // Test SOCKS5 connect request with IPv4 address - byte[] connectRequest = new byte[]{ - 0x05, // Version - 0x01, // CMD: CONNECT - 0x00, // Reserved - 0x01, // ATYP: IPv4 - (byte)192, (byte)168, 0x01, 0x01, // IP: 192.168.1.1 - 0x00, 0x50 // Port: 80 - }; - - ByteArrayInputStream input = new ByteArrayInputStream(connectRequest); - - int version = input.read(); - int cmd = input.read(); - int rsv = input.read(); - int atyp = input.read(); - - assertEquals(0x05, version); - assertEquals(0x01, cmd); // CONNECT - assertEquals(0x00, rsv); - assertEquals(0x01, atyp); // IPv4 - - byte[] ipv4 = new byte[4]; - input.read(ipv4); - assertEquals((byte)192, ipv4[0]); - assertEquals((byte)168, ipv4[1]); - assertEquals(0x01, ipv4[2]); - assertEquals(0x01, ipv4[3]); - - byte[] port = new byte[2]; - input.read(port); - int portNum = ((port[0] & 0xFF) << 8) | (port[1] & 0xFF); - assertEquals(80, portNum); - } - - @Test - public void testSocks5ConnectRequestDomain() throws Exception { - // Test SOCKS5 connect request with domain name - String domain = "example.com"; - byte[] domainBytes = domain.getBytes(StandardCharsets.UTF_8); - - ByteArrayOutputStream request = new ByteArrayOutputStream(); - request.write(0x05); // Version - request.write(0x01); // CMD: CONNECT - request.write(0x00); // Reserved - request.write(0x03); // ATYP: Domain - request.write(domainBytes.length); // Domain length - request.write(domainBytes); // Domain - request.write(0x01); // Port high byte (443) - request.write((byte)0xBB); // Port low byte - - byte[] connectRequest = request.toByteArray(); - ByteArrayInputStream input = new ByteArrayInputStream(connectRequest); - - int version = input.read(); - int cmd = input.read(); - int rsv = input.read(); - int atyp = input.read(); - - assertEquals(0x05, version); - assertEquals(0x01, cmd); - assertEquals(0x00, rsv); - assertEquals(0x03, atyp); // Domain - - int domainLen = input.read(); - assertEquals(domain.length(), domainLen); - - byte[] readDomain = new byte[domainLen]; - input.read(readDomain); - assertEquals(domain, new String(readDomain, StandardCharsets.UTF_8)); - - byte[] port = new byte[2]; - input.read(port); - int portNum = ((port[0] & 0xFF) << 8) | (port[1] & 0xFF); - assertEquals(443, portNum); - } - - @Test - public void testSocks5SuccessResponse() throws Exception { - // Test SOCKS5 success response format - byte[] successResponse = new byte[]{ - 0x05, // Version - 0x00, // Status: Success - 0x00, // Reserved - 0x01, // ATYP: IPv4 - 0x00, 0x00, 0x00, 0x00, // Bound address: 0.0.0.0 - 0x00, 0x00 // Bound port: 0 - }; - - assertEquals(10, successResponse.length); - assertEquals(0x05, successResponse[0]); - assertEquals(0x00, successResponse[1]); // Success - assertEquals(0x01, successResponse[3]); // IPv4 - } - - @Test - public void testSocks5ErrorResponse() throws Exception { - // Test various SOCKS5 error codes - int[] errorCodes = { - 0x01, // General failure - 0x02, // Connection not allowed - 0x03, // Network unreachable - 0x04, // Host unreachable - 0x05, // Connection refused - 0x06, // TTL expired - 0x07, // Command not supported - 0x08 // Address type not supported - }; - - for (int errorCode : errorCodes) { - byte[] errorResponse = new byte[]{ - 0x05, // Version - (byte) errorCode, // Error code - 0x00, // Reserved - 0x01, // ATYP: IPv4 - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00 - }; - - assertEquals(0x05, errorResponse[0]); - assertEquals(errorCode, errorResponse[1] & 0xFF); - } - } - - @Test - public void testSocks5AuthRequest() throws Exception { - // Test username/password authentication request format - String username = "testuser"; - String password = "testpass"; - - byte[] userBytes = username.getBytes(StandardCharsets.UTF_8); - byte[] passBytes = password.getBytes(StandardCharsets.UTF_8); - - ByteArrayOutputStream authRequest = new ByteArrayOutputStream(); - authRequest.write(0x01); // Auth version - authRequest.write(userBytes.length); // Username length - authRequest.write(userBytes); // Username - authRequest.write(passBytes.length); // Password length - authRequest.write(passBytes); // Password - - byte[] request = authRequest.toByteArray(); - ByteArrayInputStream input = new ByteArrayInputStream(request); - - int authVersion = input.read(); - assertEquals(0x01, authVersion); - - int userLen = input.read(); - assertEquals(username.length(), userLen); - - byte[] readUser = new byte[userLen]; - input.read(readUser); - assertEquals(username, new String(readUser, StandardCharsets.UTF_8)); - - int passLen = input.read(); - assertEquals(password.length(), passLen); - - byte[] readPass = new byte[passLen]; - input.read(readPass); - assertEquals(password, new String(readPass, StandardCharsets.UTF_8)); - } - - @Test - public void testHttpConnectRequest() throws Exception { - // Test HTTP CONNECT request format - String host = "example.com"; - int port = 443; - String username = "user"; - String password = "pass"; - - StringBuilder request = new StringBuilder(); - request.append("CONNECT ").append(host).append(":").append(port).append(" HTTP/1.1\r\n"); - request.append("Host: ").append(host).append(":").append(port).append("\r\n"); - - String auth = username + ":" + password; - String encoded = java.util.Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8)); - request.append("Proxy-Authorization: Basic ").append(encoded).append("\r\n"); - request.append("\r\n"); - - String requestStr = request.toString(); - - assertTrue(requestStr.startsWith("CONNECT example.com:443 HTTP/1.1\r\n")); - assertTrue(requestStr.contains("Host: example.com:443\r\n")); - assertTrue(requestStr.contains("Proxy-Authorization: Basic ")); - assertTrue(requestStr.endsWith("\r\n\r\n")); - } - - @Test - public void testHttpConnectResponse() throws Exception { - // Test HTTP CONNECT response parsing - String successResponse = "HTTP/1.1 200 Connection Established\r\n\r\n"; - String failResponse = "HTTP/1.1 407 Proxy Authentication Required\r\n\r\n"; - - assertTrue(successResponse.contains("200")); - assertFalse(failResponse.contains("200")); - } - - @Test - public void testIPv4Parsing() throws Exception { - // Test IPv4 address parsing from bytes - byte[] ipBytes = new byte[]{(byte)192, (byte)168, 0x01, 0x01}; - - String ip = String.format("%d.%d.%d.%d", - ipBytes[0] & 0xFF, - ipBytes[1] & 0xFF, - ipBytes[2] & 0xFF, - ipBytes[3] & 0xFF); - - assertEquals("192.168.1.1", ip); - } - - @Test - public void testIPv6Parsing() throws Exception { - // Test IPv6 address parsing from bytes - byte[] ipv6 = new byte[16]; - ipv6[0] = 0x20; - ipv6[1] = 0x01; - ipv6[2] = 0x0d; - ipv6[3] = (byte)0xb8; - // Rest are zeros - - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < 16; i += 2) { - if (i > 0) sb.append(":"); - sb.append(String.format("%02x%02x", ipv6[i] & 0xFF, ipv6[i + 1] & 0xFF)); - } - - String ip = sb.toString(); - assertTrue(ip.startsWith("2001:0db8:")); - } - - @Test - public void testPortParsing() throws Exception { - // Test port number parsing from big-endian bytes - byte[] portBytes80 = new byte[]{0x00, 0x50}; - byte[] portBytes443 = new byte[]{0x01, (byte)0xBB}; - byte[] portBytes8080 = new byte[]{0x1F, (byte)0x90}; - - int port80 = ((portBytes80[0] & 0xFF) << 8) | (portBytes80[1] & 0xFF); - int port443 = ((portBytes443[0] & 0xFF) << 8) | (portBytes443[1] & 0xFF); - int port8080 = ((portBytes8080[0] & 0xFF) << 8) | (portBytes8080[1] & 0xFF); - - assertEquals(80, port80); - assertEquals(443, port443); - assertEquals(8080, port8080); - } - - @Test - public void testBase64Encoding() throws Exception { - // Test Base64 encoding for proxy authentication - String credentials = "user:password"; - String encoded = java.util.Base64.getEncoder().encodeToString( - credentials.getBytes(StandardCharsets.UTF_8)); - - assertEquals("dXNlcjpwYXNzd29yZA==", encoded); - - // Verify decoding - String decoded = new String( - java.util.Base64.getDecoder().decode(encoded), - StandardCharsets.UTF_8); - assertEquals(credentials, decoded); - } -} diff --git a/app/src/test/java/org/proxydroid/utils/LocalHttpProxyTest.kt b/app/src/test/java/org/proxydroid/utils/LocalHttpProxyTest.kt new file mode 100644 index 00000000..ab2eaf32 --- /dev/null +++ b/app/src/test/java/org/proxydroid/utils/LocalHttpProxyTest.kt @@ -0,0 +1,296 @@ +/* proxydroid - Global / Individual Proxy App for Android + * Copyright (C) 2011 Max Lv + * + * This program 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. + */ + +package org.proxydroid.utils + +import org.junit.Test +import org.junit.Assert.* +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.nio.charset.StandardCharsets +import java.util.Base64 + +class LocalHttpProxyTest { + + @Test + fun testSocks5HandshakeNoAuth() { + val clientHello = byteArrayOf(0x05, 0x01, 0x00) + val expectedResponse = byteArrayOf(0x05, 0x00) + + val input = ByteArrayInputStream(clientHello) + val output = ByteArrayOutputStream() + + val version = input.read() + val nmethods = input.read() + val methods = ByteArray(nmethods) + input.read(methods) + + assertEquals(0x05, version) + assertEquals(1, nmethods) + assertEquals(0x00.toByte(), methods[0]) + + output.write(expectedResponse) + + val response = output.toByteArray() + assertArrayEquals(expectedResponse, response) + } + + @Test + fun testSocks5HandshakeWithAuth() { + val clientHello = byteArrayOf(0x05, 0x02, 0x00, 0x02) + + val input = ByteArrayInputStream(clientHello) + + val version = input.read() + val nmethods = input.read() + val methods = ByteArray(nmethods) + input.read(methods) + + assertEquals(0x05, version) + assertEquals(2, nmethods) + assertEquals(0x00.toByte(), methods[0]) + assertEquals(0x02.toByte(), methods[1]) + } + + @Test + fun testSocks5ConnectRequestIPv4() { + val connectRequest = byteArrayOf( + 0x05, + 0x01, + 0x00, + 0x01, + 192.toByte(), 168.toByte(), 0x01, 0x01, + 0x00, 0x50 + ) + + val input = ByteArrayInputStream(connectRequest) + + val version = input.read() + val cmd = input.read() + val rsv = input.read() + val atyp = input.read() + + assertEquals(0x05, version) + assertEquals(0x01, cmd) + assertEquals(0x00, rsv) + assertEquals(0x01, atyp) + + val ipv4 = ByteArray(4) + input.read(ipv4) + assertEquals(192.toByte(), ipv4[0]) + assertEquals(168.toByte(), ipv4[1]) + assertEquals(0x01.toByte(), ipv4[2]) + assertEquals(0x01.toByte(), ipv4[3]) + + val port = ByteArray(2) + input.read(port) + val portNum = ((port[0].toInt() and 0xFF) shl 8) or (port[1].toInt() and 0xFF) + assertEquals(80, portNum) + } + + @Test + fun testSocks5ConnectRequestDomain() { + val domain = "example.com" + val domainBytes = domain.toByteArray(StandardCharsets.UTF_8) + + val request = ByteArrayOutputStream().apply { + write(0x05) + write(0x01) + write(0x00) + write(0x03) + write(domainBytes.size) + write(domainBytes) + write(0x01) + write(0xBB) + } + + val connectRequest = request.toByteArray() + val input = ByteArrayInputStream(connectRequest) + + val version = input.read() + val cmd = input.read() + val rsv = input.read() + val atyp = input.read() + + assertEquals(0x05, version) + assertEquals(0x01, cmd) + assertEquals(0x00, rsv) + assertEquals(0x03, atyp) + + val domainLen = input.read() + assertEquals(domain.length, domainLen) + + val readDomain = ByteArray(domainLen) + input.read(readDomain) + assertEquals(domain, String(readDomain, StandardCharsets.UTF_8)) + + val port = ByteArray(2) + input.read(port) + val portNum = ((port[0].toInt() and 0xFF) shl 8) or (port[1].toInt() and 0xFF) + assertEquals(443, portNum) + } + + @Test + fun testSocks5SuccessResponse() { + val successResponse = byteArrayOf( + 0x05, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00 + ) + + assertEquals(10, successResponse.size) + assertEquals(0x05.toByte(), successResponse[0]) + assertEquals(0x00.toByte(), successResponse[1]) + assertEquals(0x01.toByte(), successResponse[3]) + } + + @Test + fun testSocks5ErrorResponse() { + val errorCodes = intArrayOf(0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08) + + for (errorCode in errorCodes) { + val errorResponse = byteArrayOf( + 0x05, errorCode.toByte(), 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ) + + assertEquals(0x05.toByte(), errorResponse[0]) + assertEquals(errorCode, errorResponse[1].toInt() and 0xFF) + } + } + + @Test + fun testSocks5AuthRequest() { + val username = "testuser" + val password = "testpass" + + val userBytes = username.toByteArray(StandardCharsets.UTF_8) + val passBytes = password.toByteArray(StandardCharsets.UTF_8) + + val authRequest = ByteArrayOutputStream().apply { + write(0x01) + write(userBytes.size) + write(userBytes) + write(passBytes.size) + write(passBytes) + } + + val request = authRequest.toByteArray() + val input = ByteArrayInputStream(request) + + val authVersion = input.read() + assertEquals(0x01, authVersion) + + val userLen = input.read() + assertEquals(username.length, userLen) + + val readUser = ByteArray(userLen) + input.read(readUser) + assertEquals(username, String(readUser, StandardCharsets.UTF_8)) + + val passLen = input.read() + assertEquals(password.length, passLen) + + val readPass = ByteArray(passLen) + input.read(readPass) + assertEquals(password, String(readPass, StandardCharsets.UTF_8)) + } + + @Test + fun testHttpConnectRequest() { + val host = "example.com" + val port = 443 + val username = "user" + val password = "pass" + + val request = StringBuilder().apply { + append("CONNECT ").append(host).append(":").append(port).append(" HTTP/1.1\r\n") + append("Host: ").append(host).append(":").append(port).append("\r\n") + val auth = "$username:$password" + val encoded = Base64.getEncoder().encodeToString(auth.toByteArray(StandardCharsets.UTF_8)) + append("Proxy-Authorization: Basic ").append(encoded).append("\r\n") + append("\r\n") + } + + val requestStr = request.toString() + + assertTrue(requestStr.startsWith("CONNECT example.com:443 HTTP/1.1\r\n")) + assertTrue(requestStr.contains("Host: example.com:443\r\n")) + assertTrue(requestStr.contains("Proxy-Authorization: Basic ")) + assertTrue(requestStr.endsWith("\r\n\r\n")) + } + + @Test + fun testHttpConnectResponse() { + val successResponse = "HTTP/1.1 200 Connection Established\r\n\r\n" + val failResponse = "HTTP/1.1 407 Proxy Authentication Required\r\n\r\n" + + assertTrue(successResponse.contains("200")) + assertFalse(failResponse.contains("200")) + } + + @Test + fun testIPv4Parsing() { + val ipBytes = byteArrayOf(192.toByte(), 168.toByte(), 0x01, 0x01) + + val ip = String.format( + "%d.%d.%d.%d", + ipBytes[0].toInt() and 0xFF, + ipBytes[1].toInt() and 0xFF, + ipBytes[2].toInt() and 0xFF, + ipBytes[3].toInt() and 0xFF + ) + + assertEquals("192.168.1.1", ip) + } + + @Test + fun testIPv6Parsing() { + val ipv6 = ByteArray(16) + ipv6[0] = 0x20 + ipv6[1] = 0x01 + ipv6[2] = 0x0d + ipv6[3] = 0xb8.toByte() + + val sb = StringBuilder() + for (i in 0 until 16 step 2) { + if (i > 0) sb.append(":") + sb.append(String.format("%02x%02x", ipv6[i].toInt() and 0xFF, ipv6[i + 1].toInt() and 0xFF)) + } + + val ip = sb.toString() + assertTrue(ip.startsWith("2001:0db8:")) + } + + @Test + fun testPortParsing() { + val portBytes80 = byteArrayOf(0x00, 0x50) + val portBytes443 = byteArrayOf(0x01, 0xBB.toByte()) + val portBytes8080 = byteArrayOf(0x1F, 0x90.toByte()) + + val port80 = ((portBytes80[0].toInt() and 0xFF) shl 8) or (portBytes80[1].toInt() and 0xFF) + val port443 = ((portBytes443[0].toInt() and 0xFF) shl 8) or (portBytes443[1].toInt() and 0xFF) + val port8080 = ((portBytes8080[0].toInt() and 0xFF) shl 8) or (portBytes8080[1].toInt() and 0xFF) + + assertEquals(80, port80) + assertEquals(443, port443) + assertEquals(8080, port8080) + } + + @Test + fun testBase64Encoding() { + val credentials = "user:password" + val encoded = Base64.getEncoder().encodeToString(credentials.toByteArray(StandardCharsets.UTF_8)) + + assertEquals("dXNlcjpwYXNzd29yZA==", encoded) + + val decoded = String(Base64.getDecoder().decode(encoded), StandardCharsets.UTF_8) + assertEquals(credentials, decoded) + } +} diff --git a/build.gradle b/build.gradle index 3d3505de..82389697 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,14 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { + ext.kotlin_version = '1.9.10' repositories { google() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:8.1.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files