diff --git a/app/build.gradle b/app/build.gradle index ef526c2..0c66820 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,38 +1,23 @@ import groovy.swing.SwingBuilder -buildscript { - repositories { - jcenter() - maven { url 'https://maven.fabric.io/public' } - } - - dependencies { - classpath 'io.fabric.tools:gradle:1.19.1' - } -} - plugins { id 'com.github.ben-manes.versions' version '0.13.0' } apply plugin: 'com.android.application' -apply plugin: 'io.fabric' - -repositories { - maven { url 'https://maven.fabric.io/public' } -} android { - compileSdkVersion 24 - buildToolsVersion "24.0.0" + compileSdkVersion 29 + buildToolsVersion "29.0.0" defaultConfig { - applicationId "ht.vpn.android.app" - minSdkVersion 15 - targetSdkVersion 24 - versionCode 15 - versionName "1.3.0" + applicationId "ht.vpn.android" + minSdkVersion 23 + targetSdkVersion 29 + versionCode 16 + versionName "1.4.0" resValue "string", "geokey", System.getenv("VPNHT_GEO_KEY").toString() + buildConfigField("boolean", "openvpn3", "true") } signingConfigs { @@ -54,30 +39,26 @@ android { applicationIdSuffix ".dev" } } -} - -ext { - supportLibVersion = "24.0.0" + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } // ./gradlew(.bat) dependencyUpdates -Drevision=release dependencies { - compile fileTree(dir: 'libs', include: ['*.jar']) - compile "com.android.support:support-v13:${supportLibVersion}" - compile "com.android.support:appcompat-v7:${supportLibVersion}" - compile "com.android.support:cardview-v7:${supportLibVersion}" - compile "com.android.support:recyclerview-v7:${supportLibVersion}" - compile "com.android.support:design:${supportLibVersion}" - compile "com.android.support:support-annotations:${supportLibVersion}" - compile 'com.google.android.gms:play-services-maps:9.2.1' - compile 'com.github.johnkil.android-robototextview:robototextview:2.4.0' - compile 'com.jakewharton:butterknife:7.0.1' - compile 'com.mobsandgeeks:android-saripaar:2.0.2' - compile 'com.squareup.retrofit:retrofit:1.9.0' - compile 'com.jakewharton.timber:timber:3.1.0' - compile('com.crashlytics.sdk.android:crashlytics:2.5.2@aar') { - transitive = true; - } + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "androidx.appcompat:appcompat:1.1.0" + implementation "androidx.recyclerview:recyclerview:1.1.0" + implementation "androidx.cardview:cardview:1.0.0" + implementation "androidx.annotation:annotation:1.1.0" + implementation "com.google.android.material:material:1.1.0" + implementation 'com.google.android.gms:play-services-maps:17.0.0' + implementation 'com.mobsandgeeks:android-saripaar:2.0.2' + implementation 'com.squareup.retrofit:retrofit:1.9.0' + implementation 'com.jakewharton.timber:timber:3.1.0' + implementation 'com.jakewharton:butterknife:10.2.1' + annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.1' } gradle.taskGraph.whenReady { taskGraph -> diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3cef701..ee3c8ff 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,16 +2,25 @@ xmlns:tools="http://schemas.android.com/tools" package="ht.vpn.android"> + + + + @@ -53,7 +62,7 @@ tools:ignore="ExportedActivity"> - - + @@ -65,12 +74,18 @@ + android:permission="android.permission.BIND_VPN_SERVICE" + android:process=":openvpn"> + + diff --git a/app/src/main/aidl/de/blinkt/openvpn/core/ConnectionStatus.aidl b/app/src/main/aidl/de/blinkt/openvpn/core/ConnectionStatus.aidl new file mode 100644 index 0000000..f37c310 --- /dev/null +++ b/app/src/main/aidl/de/blinkt/openvpn/core/ConnectionStatus.aidl @@ -0,0 +1,3 @@ +package de.blinkt.openvpn.core; + +parcelable ConnectionStatus; \ No newline at end of file diff --git a/app/src/main/aidl/de/blinkt/openvpn/core/IOpenVPNServiceInternal.aidl b/app/src/main/aidl/de/blinkt/openvpn/core/IOpenVPNServiceInternal.aidl new file mode 100644 index 0000000..1c1df7d --- /dev/null +++ b/app/src/main/aidl/de/blinkt/openvpn/core/IOpenVPNServiceInternal.aidl @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.core; + +/** + * Created by arne on 15.11.16. + */ + +interface IOpenVPNServiceInternal { + + boolean protect(int fd); + + void userPause(boolean b); + + /** + * @param replaceConnection True if the VPN is connected by a new connection. + * @return true if there was a process that has been send a stop signal + */ + boolean stopVPN(boolean replaceConnection); + + void addAllowedExternalApp(String packagename); + + boolean isAllowedExternalApp(String packagename); + + void challengeResponse(String repsonse); +} diff --git a/app/src/main/aidl/de/blinkt/openvpn/core/IServiceStatus.aidl b/app/src/main/aidl/de/blinkt/openvpn/core/IServiceStatus.aidl new file mode 100644 index 0000000..5a5cbdb --- /dev/null +++ b/app/src/main/aidl/de/blinkt/openvpn/core/IServiceStatus.aidl @@ -0,0 +1,36 @@ +// StatusIPC.aidl +package de.blinkt.openvpn.core; + +// Declare any non-default types here with import statements +import de.blinkt.openvpn.core.IStatusCallbacks; +import android.os.ParcelFileDescriptor; +import de.blinkt.openvpn.core.TrafficHistory; + + +interface IServiceStatus { + /** + * Registers to receive OpenVPN Status Updates and gets a + * ParcelFileDescript back that contains the log up to that point + */ + ParcelFileDescriptor registerStatusCallback(in IStatusCallbacks cb); + + /** + * Remove a previously registered callback interface. + */ + void unregisterStatusCallback(in IStatusCallbacks cb); + + /** + * Returns the last connedcted VPN + */ + String getLastConnectedVPN(); + + /** + * Sets a cached password + */ + void setCachedPassword(in String uuid, int type, String password); + + /** + * Gets the traffic history + */ + TrafficHistory getTrafficHistory(); +} diff --git a/app/src/main/aidl/de/blinkt/openvpn/core/IStatusCallbacks.aidl b/app/src/main/aidl/de/blinkt/openvpn/core/IStatusCallbacks.aidl new file mode 100644 index 0000000..286a504 --- /dev/null +++ b/app/src/main/aidl/de/blinkt/openvpn/core/IStatusCallbacks.aidl @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.core; + +import de.blinkt.openvpn.core.LogItem; +import de.blinkt.openvpn.core.ConnectionStatus; + + + +interface IStatusCallbacks { + /** + * Called when the service has a new status for you. + */ + oneway void newLogItem(in LogItem item); + + oneway void updateStateString(in String state, in String msg, in int resid, in ConnectionStatus level, in Intent intent); + + oneway void updateByteCount(long inBytes, long outBytes); + + oneway void connectedVPN(String uuid); +} diff --git a/app/src/main/aidl/de/blinkt/openvpn/core/LogItem.aidl b/app/src/main/aidl/de/blinkt/openvpn/core/LogItem.aidl new file mode 100644 index 0000000..9a7291a --- /dev/null +++ b/app/src/main/aidl/de/blinkt/openvpn/core/LogItem.aidl @@ -0,0 +1,3 @@ +package de.blinkt.openvpn.core; + +parcelable LogItem; \ No newline at end of file diff --git a/app/src/main/aidl/de/blinkt/openvpn/core/TrafficHistory.aidl b/app/src/main/aidl/de/blinkt/openvpn/core/TrafficHistory.aidl new file mode 100644 index 0000000..5bd255f --- /dev/null +++ b/app/src/main/aidl/de/blinkt/openvpn/core/TrafficHistory.aidl @@ -0,0 +1,4 @@ +package de.blinkt.openvpn.core; + + +parcelable TrafficHistory; diff --git a/app/src/main/aidl/ht/vpn/android/api/ExternalCertificateProvider.aidl b/app/src/main/aidl/ht/vpn/android/api/ExternalCertificateProvider.aidl new file mode 100644 index 0000000..3acd846 --- /dev/null +++ b/app/src/main/aidl/ht/vpn/android/api/ExternalCertificateProvider.aidl @@ -0,0 +1,39 @@ +// ExternalCertificateProvider.aidl +package ht.vpn.android.api; + + +/* + * This is very simple interface that is specialised to have only the minimal set of crypto + * operation that are needed for OpenVPN to authenticate with an external certificate + */ +interface ExternalCertificateProvider { + /** + * Requests signing the data with RSA/ECB/PKCS1PADDING + * for RSA certficate and with NONEwithECDSA for EC certificates + * @parm alias the parameter that + */ + byte[] getSignedData(in String alias, in byte[] data); + + /** + * Requests the certificate chain for the selected alias + * The first certifcate returned is assumed to be + * the user certificate + */ + byte[] getCertificateChain(in String alias); + + /** + * This function is called for the app to get additional meta information from the + * external provider and will be called with the stored alias in the app + * + * For external app provider that do not provide an activity to configure them, this + * is used to get the alias that should be used. + * The format is the same as the activity should return, i.e. + * + * EXTRA_ALIAS = "ht.vpn.android.api.KEY_ALIAS" + * EXTRA_DESCRIPTION = "ht.vpn.android.api.KEY_DESCRIPTION" + * + * as the keys for the bundle. + * + */ + Bundle getCertificateMetaData(in String alias); +} diff --git a/app/src/main/assets/fonts/Roboto-Bold.ttf b/app/src/main/assets/fonts/Roboto-Bold.ttf new file mode 100644 index 0000000..d998cf5 Binary files /dev/null and b/app/src/main/assets/fonts/Roboto-Bold.ttf differ diff --git a/app/src/main/assets/fonts/Roboto-Light.ttf b/app/src/main/assets/fonts/Roboto-Light.ttf new file mode 100644 index 0000000..3526798 Binary files /dev/null and b/app/src/main/assets/fonts/Roboto-Light.ttf differ diff --git a/app/src/main/assets/fonts/Roboto-Medium.ttf b/app/src/main/assets/fonts/Roboto-Medium.ttf new file mode 100644 index 0000000..f714a51 Binary files /dev/null and b/app/src/main/assets/fonts/Roboto-Medium.ttf differ diff --git a/app/src/main/assets/fonts/Roboto-Regular.ttf b/app/src/main/assets/fonts/Roboto-Regular.ttf new file mode 100644 index 0000000..2b6392f Binary files /dev/null and b/app/src/main/assets/fonts/Roboto-Regular.ttf differ diff --git a/app/src/main/assets/nopie_openvpn.arm64-v8a b/app/src/main/assets/nopie_openvpn.arm64-v8a old mode 100755 new mode 100644 index 2a0ea1d..e8acca0 Binary files a/app/src/main/assets/nopie_openvpn.arm64-v8a and b/app/src/main/assets/nopie_openvpn.arm64-v8a differ diff --git a/app/src/main/assets/nopie_openvpn.armeabi b/app/src/main/assets/nopie_openvpn.armeabi deleted file mode 100755 index 022978e..0000000 Binary files a/app/src/main/assets/nopie_openvpn.armeabi and /dev/null differ diff --git a/app/src/main/assets/nopie_openvpn.armeabi-v7a b/app/src/main/assets/nopie_openvpn.armeabi-v7a old mode 100755 new mode 100644 index 4f3f709..b573c26 Binary files a/app/src/main/assets/nopie_openvpn.armeabi-v7a and b/app/src/main/assets/nopie_openvpn.armeabi-v7a differ diff --git a/app/src/main/assets/nopie_openvpn.mips b/app/src/main/assets/nopie_openvpn.mips deleted file mode 100755 index 780fa9d..0000000 Binary files a/app/src/main/assets/nopie_openvpn.mips and /dev/null differ diff --git a/app/src/main/assets/nopie_openvpn.x86 b/app/src/main/assets/nopie_openvpn.x86 old mode 100755 new mode 100644 index 220a5e0..5a2979a Binary files a/app/src/main/assets/nopie_openvpn.x86 and b/app/src/main/assets/nopie_openvpn.x86 differ diff --git a/app/src/main/assets/nopie_openvpn.x86_64 b/app/src/main/assets/nopie_openvpn.x86_64 old mode 100755 new mode 100644 index 06c9602..606a3e9 Binary files a/app/src/main/assets/nopie_openvpn.x86_64 and b/app/src/main/assets/nopie_openvpn.x86_64 differ diff --git a/app/src/main/assets/pie_openvpn.arm64-v8a b/app/src/main/assets/pie_openvpn.arm64-v8a old mode 100755 new mode 100644 index 2a0ea1d..e8acca0 Binary files a/app/src/main/assets/pie_openvpn.arm64-v8a and b/app/src/main/assets/pie_openvpn.arm64-v8a differ diff --git a/app/src/main/assets/pie_openvpn.armeabi b/app/src/main/assets/pie_openvpn.armeabi deleted file mode 100755 index 1ebd0f0..0000000 Binary files a/app/src/main/assets/pie_openvpn.armeabi and /dev/null differ diff --git a/app/src/main/assets/pie_openvpn.armeabi-v7a b/app/src/main/assets/pie_openvpn.armeabi-v7a old mode 100755 new mode 100644 index 9ca9f67..b573c26 Binary files a/app/src/main/assets/pie_openvpn.armeabi-v7a and b/app/src/main/assets/pie_openvpn.armeabi-v7a differ diff --git a/app/src/main/assets/pie_openvpn.mips b/app/src/main/assets/pie_openvpn.mips deleted file mode 100755 index 9192dac..0000000 Binary files a/app/src/main/assets/pie_openvpn.mips and /dev/null differ diff --git a/app/src/main/assets/pie_openvpn.x86 b/app/src/main/assets/pie_openvpn.x86 old mode 100755 new mode 100644 index 4f6015d..5a2979a Binary files a/app/src/main/assets/pie_openvpn.x86 and b/app/src/main/assets/pie_openvpn.x86 differ diff --git a/app/src/main/assets/pie_openvpn.x86_64 b/app/src/main/assets/pie_openvpn.x86_64 old mode 100755 new mode 100644 index 06c9602..606a3e9 Binary files a/app/src/main/assets/pie_openvpn.x86_64 and b/app/src/main/assets/pie_openvpn.x86_64 differ diff --git a/app/src/main/java/ht/vpn/android/VpnProfile.java b/app/src/main/java/de/blinkt/openvpn/VpnProfile.java similarity index 52% rename from app/src/main/java/ht/vpn/android/VpnProfile.java rename to app/src/main/java/de/blinkt/openvpn/VpnProfile.java index 8088133..f249adf 100644 --- a/app/src/main/java/ht/vpn/android/VpnProfile.java +++ b/app/src/main/java/de/blinkt/openvpn/VpnProfile.java @@ -1,24 +1,26 @@ /* - * Copyright (c) 2012-2014 Arne Schwabe + * Copyright (c) 2012-2016 Arne Schwabe * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt */ -package ht.vpn.android; +package de.blinkt.openvpn; import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; import android.preference.PreferenceManager; import android.security.KeyChain; import android.security.KeyChainException; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import android.text.TextUtils; import android.util.Base64; +import de.blinkt.openvpn.core.*; import org.spongycastle.util.io.pem.PemObject; import org.spongycastle.util.io.pem.PemWriter; @@ -31,9 +33,7 @@ import java.io.StringWriter; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; +import java.security.*; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; @@ -48,12 +48,8 @@ import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; -import de.blinkt.openvpn.core.Connection; -import de.blinkt.openvpn.core.NativeUtils; -import de.blinkt.openvpn.core.OpenVPNService; -import de.blinkt.openvpn.core.VPNLaunchHelper; -import de.blinkt.openvpn.core.VpnStatus; -import de.blinkt.openvpn.core.X509Utils; +import ht.vpn.android.BuildConfig; +import ht.vpn.android.R; public class VpnProfile implements Serializable, Cloneable { // Note that this class cannot be moved to core where it belongs since @@ -66,18 +62,9 @@ public class VpnProfile implements Serializable, Cloneable { public static final String EXTRA_PROFILEUUID = "de.blinkt.openvpn.profileUUID"; public static final String INLINE_TAG = "[[INLINE]]"; public static final String DISPLAYNAME_TAG = "[[NAME]]"; - - private static final long serialVersionUID = 7085688938959334563L; public static final int MAXLOGLEVEL = 4; - public static final int CURRENT_PROFILE_VERSION = 5; - public static final int DEFAULT_MSSFIX_SIZE = 1450; - public static String DEFAULT_DNS1 = "8.8.8.8"; - public static String DEFAULT_DNS2 = "8.8.4.4"; - - public transient String mTransientPW = null; - public transient String mTransientPCKS12PW = null; - - + public static final int CURRENT_PROFILE_VERSION = 8; + public static final int DEFAULT_MSSFIX_SIZE = 1280; public static final int TYPE_CERTIFICATES = 0; public static final int TYPE_PKCS12 = 1; public static final int TYPE_KEYSTORE = 2; @@ -86,11 +73,20 @@ public class VpnProfile implements Serializable, Cloneable { public static final int TYPE_USERPASS_CERTIFICATES = 5; public static final int TYPE_USERPASS_PKCS12 = 6; public static final int TYPE_USERPASS_KEYSTORE = 7; + public static final int TYPE_EXTERNAL_APP = 8; public static final int X509_VERIFY_TLSREMOTE = 0; public static final int X509_VERIFY_TLSREMOTE_COMPAT_NOREMAPPING = 1; public static final int X509_VERIFY_TLSREMOTE_DN = 2; public static final int X509_VERIFY_TLSREMOTE_RDN = 3; public static final int X509_VERIFY_TLSREMOTE_RDN_PREFIX = 4; + public static final int AUTH_RETRY_NONE_FORGET = 0; + public static final int AUTH_RETRY_NOINTERACT = 2; + public static final boolean mIsOpenVPN22 = false; + private static final long serialVersionUID = 7085688938959334563L; + private static final int AUTH_RETRY_NONE_KEEP = 1; + private static final int AUTH_RETRY_INTERACT = 3; + public static String DEFAULT_DNS1 = "8.8.8.8"; + public static String DEFAULT_DNS2 = "8.8.4.4"; // variable named wrong and should haven beeen transient // but needs to keep wrong name to guarante loading of old // profiles @@ -107,7 +103,6 @@ public class VpnProfile implements Serializable, Cloneable { public String mPKCS12Filename; public String mPKCS12Password; public boolean mUseTLSAuth = false; - public String mDNS1 = DEFAULT_DNS1; public String mDNS2 = DEFAULT_DNS2; public String mIPv4Address; @@ -129,36 +124,49 @@ public class VpnProfile implements Serializable, Cloneable { public String mCustomConfigOptions = ""; public String mVerb = "1"; //ignored public String mCipher = ""; - public boolean mNobind = false; + public boolean mNobind = true; public boolean mUseDefaultRoutev6 = true; public String mCustomRoutesv6 = ""; public String mKeyPassword = ""; public boolean mPersistTun = false; - public String mConnectRetryMax = "5"; - public String mConnectRetry = "5"; + public String mConnectRetryMax = "-1"; + public String mConnectRetry = "2"; + public String mConnectRetryMaxTime = "300"; public boolean mUserEditable = true; public String mAuth = ""; public int mX509AuthType = X509_VERIFY_TLSREMOTE_RDN; - private transient PrivateKey mPrivateKey; - // Public attributes, since I got mad with getter/setter - // set members to default values - private UUID mUuid; + public String mx509UsernameField = null; public boolean mAllowLocalLAN; - private int mProfileVersion; public String mExcludedRoutes; public String mExcludedRoutesv6; public int mMssFix = 0; // -1 is default, public Connection[] mConnections = new Connection[0]; public boolean mRemoteRandom = false; - public HashSet mAllowedAppsVpn = new HashSet(); + public HashSet mAllowedAppsVpn = new HashSet<>(); public boolean mAllowedAppsVpnAreDisallowed = true; + public boolean mAllowAppVpnBypass = false; + public String mCrlFilename; public String mProfileCreator; - - /* Options no long used in new profiles */ - public String mServerName = "openvpn.blinkt.de"; + public String mExternalAuthenticator; + public int mAuthRetry = AUTH_RETRY_NONE_FORGET; + public int mTunMtu; + public boolean mPushPeerInfo = false; + public int mVersion = 0; + // timestamp when the profile was last used + public long mLastUsed; + public String importedProfileHash; + /* Options no longer used in new profiles */ + public String mServerName = "openvpn.example.com"; public String mServerPort = "1194"; public boolean mUseUdp = true; + public boolean mTemporaryProfile = false; + private transient PrivateKey mPrivateKey; + // Public attributes, since I got mad with getter/setter + // set members to default values + private UUID mUuid; + private int mProfileVersion; + public boolean mBlockUnusedAddressFamilies =true; public VpnProfile(String name) { mUuid = UUID.randomUUID(); @@ -167,6 +175,7 @@ public VpnProfile(String name) { mConnections = new Connection[1]; mConnections[0] = new Connection(); + mLastUsed = System.currentTimeMillis(); } public static String openVpnEscape(String unescaped) { @@ -184,6 +193,59 @@ public static String openVpnEscape(String unescaped) { return '"' + escapedString + '"'; } + public static boolean doUseOpenVPN3(Context c) { + SharedPreferences prefs = Preferences.getDefaultSharedPreferences(c); + boolean useOpenVPN3 = prefs.getBoolean("ovpn3", false); + if (!BuildConfig.openvpn3) + useOpenVPN3 = false; + return useOpenVPN3; + } + + //! Put inline data inline and other data as normal escaped filename + public static String insertFileData(String cfgentry, String filedata) { + if (filedata == null) { + return String.format("%s %s\n", cfgentry, "file missing in config profile"); + } else if (isEmbedded(filedata)) { + String dataWithOutHeader = getEmbeddedContent(filedata); + return String.format(Locale.ENGLISH, "<%s>\n%s\n\n", cfgentry, dataWithOutHeader, cfgentry); + } else { + return String.format(Locale.ENGLISH, "%s %s\n", cfgentry, openVpnEscape(filedata)); + } + } + + public static String getDisplayName(String embeddedFile) { + int start = DISPLAYNAME_TAG.length(); + int end = embeddedFile.indexOf(INLINE_TAG); + return embeddedFile.substring(start, end); + } + + public static String getEmbeddedContent(String data) { + if (!data.contains(INLINE_TAG)) + return data; + + int start = data.indexOf(INLINE_TAG) + INLINE_TAG.length(); + return data.substring(start); + } + + public static boolean isEmbedded(String data) { + if (data == null) + return false; + if (data.startsWith(INLINE_TAG) || data.startsWith(DISPLAYNAME_TAG)) + return true; + else + return false; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof VpnProfile) { + VpnProfile vpnProfile = (VpnProfile) obj; + return mUuid.equals(vpnProfile.mUuid); + } else { + return false; + } + } + public void clearDefaults() { mServerName = "unknown"; mUsePull = false; @@ -194,7 +256,9 @@ public void clearDefaults() { mCheckRemoteCN = false; mPersistTun = false; mAllowLocalLAN = true; + mPushPeerInfo = false; mMssFix = 0; + mNobind = false; } public UUID getUUID() { @@ -202,26 +266,49 @@ public UUID getUUID() { } + // Only used for the special case of managed profiles + public void setUUID(UUID uuid) { + mUuid = uuid; + } + public String getName() { - if (mName == null) + if (TextUtils.isEmpty(mName)) return "No profile name"; return mName; } public void upgradeProfile() { - if (mProfileVersion < 2) { - /* default to the behaviour the OS used */ - mAllowLocalLAN = Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT; - } - if (mProfileVersion < 4) { - moveOptionsToConnection(); - mAllowedAppsVpnAreDisallowed = true; + /* Fallthrough is intended here */ + switch(mProfileVersion) { + case 0: + case 1: + /* default to the behaviour the OS used */ + mAllowLocalLAN = Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT; + case 2: + case 3: + moveOptionsToConnection(); + mAllowedAppsVpnAreDisallowed = true; + + if (mAllowedAppsVpn == null) + mAllowedAppsVpn = new HashSet<>(); + + if (mConnections == null) + mConnections = new Connection[0]; + case 4: + case 5: + + if (TextUtils.isEmpty(mProfileCreator)) + mUserEditable = true; + case 6: + for (Connection c : mConnections) + if (c.mProxyType == null) + c.mProxyType = Connection.ProxyType.NONE; + case 7: + if (mAllowAppVpnBypass) + mBlockUnusedAddressFamilies = !mAllowAppVpnBypass; + default: } - if (mAllowedAppsVpn == null) - mAllowedAppsVpn = new HashSet(); - if (mConnections == null) - mConnections = new Connection[0]; mProfileVersion = CURRENT_PROFILE_VERSION; @@ -243,78 +330,96 @@ private void moveOptionsToConnection() { public String getConfigFile(Context context, boolean configForOvpn3) { File cacheDir = context.getCacheDir(); - String cfg = ""; - - // Enable management interface - cfg += "# Enables connection to GUI\n"; - cfg += "management "; - - cfg += cacheDir.getAbsolutePath() + "/" + "mgmtsocket"; - cfg += " unix\n"; - cfg += "management-client\n"; - // Not needed, see updated man page in 2.3 - //cfg += "management-signal\n"; - cfg += "management-query-passwords\n"; - cfg += "management-hold\n\n"; - - if (!configForOvpn3) - cfg += String.format("setenv IV_GUI_VER %s \n", openVpnEscape(getVersionEnvString(context))); + StringBuilder cfg = new StringBuilder(); + + if (!configForOvpn3) { + // Enable management interface + cfg.append("# Config for OpenVPN 2.x\n"); + cfg.append("# Enables connection to GUI\n"); + cfg.append("management "); + + cfg.append(cacheDir.getAbsolutePath()).append("/").append("mgmtsocket"); + cfg.append(" unix\n"); + cfg.append("management-client\n"); + // Not needed, see updated man page in 2.3 + //cfg += "management-signal\n"; + cfg.append("management-query-passwords\n"); + cfg.append("management-hold\n\n"); + + cfg.append(String.format("setenv IV_GUI_VER %s \n", openVpnEscape(getVersionEnvString(context)))); + cfg.append("setenv IV_SSO openurl,crtext\n"); + String versionString = getPlatformVersionEnvString(); + cfg.append(String.format("setenv IV_PLAT_VER %s\n", openVpnEscape(versionString))); + } else { + cfg.append("# Config for OpenVPN 3 C++\n"); + } - cfg += "machine-readable-output\n"; - // Users are confused by warnings that are misleading... - cfg += "ifconfig-nowarn\n"; + if (!configForOvpn3) { + cfg.append("machine-readable-output\n"); + if (!mIsOpenVPN22) + cfg.append("allow-recursive-routing\n"); + // Users are confused by warnings that are misleading... + cfg.append("ifconfig-nowarn\n"); + } boolean useTLSClient = (mAuthenticationType != TYPE_STATICKEYS); if (useTLSClient && mUsePull) - cfg += "client\n"; + cfg.append("client\n"); else if (mUsePull) - cfg += "pull\n"; + cfg.append("pull\n"); else if (useTLSClient) - cfg += "tls-client\n"; + cfg.append("tls-client\n"); //cfg += "verb " + mVerb + "\n"; - cfg += "verb " + MAXLOGLEVEL + "\n"; + cfg.append("verb " + MAXLOGLEVEL + "\n"); if (mConnectRetryMax == null) { - mConnectRetryMax = "5"; + mConnectRetryMax = "-1"; } if (!mConnectRetryMax.equals("-1")) - cfg += "connect-retry-max " + mConnectRetryMax + "\n"; + cfg.append("connect-retry-max ").append(mConnectRetryMax).append("\n"); + + if (TextUtils.isEmpty(mConnectRetry)) + mConnectRetry = "2"; - if (mConnectRetry == null) - mConnectRetry = "5"; + if (TextUtils.isEmpty(mConnectRetryMaxTime)) + mConnectRetryMaxTime = "300"; - cfg += "connect-retry " + mConnectRetry + "\n"; + if (!mIsOpenVPN22) + cfg.append("connect-retry ").append(mConnectRetry).append(" ").append(mConnectRetryMaxTime).append("\n"); + else if (mIsOpenVPN22 && !mUseUdp) + cfg.append("connect-retry ").append(mConnectRetry).append("\n"); - cfg += "resolv-retry 60\n"; + + cfg.append("resolv-retry 60\n"); // We cannot use anything else than tun - cfg += "dev tun\n"; + cfg.append("dev tun\n"); boolean canUsePlainRemotes = true; if (mConnections.length == 1) { - cfg += mConnections[0].getConnectionBlock(); + cfg.append(mConnections[0].getConnectionBlock(configForOvpn3)); } else { for (Connection conn : mConnections) { canUsePlainRemotes = canUsePlainRemotes && conn.isOnlyRemote(); } if (mRemoteRandom) - cfg += "remote-random\n"; + cfg.append("remote-random\n"); if (canUsePlainRemotes) { for (Connection conn : mConnections) { if (conn.mEnabled) { - cfg += conn.getConnectionBlock(); + cfg.append(conn.getConnectionBlock(configForOvpn3)); } } } @@ -323,75 +428,104 @@ else if (useTLSClient) switch (mAuthenticationType) { case VpnProfile.TYPE_USERPASS_CERTIFICATES: - cfg += "auth-user-pass\n"; + cfg.append("auth-user-pass\n"); case VpnProfile.TYPE_CERTIFICATES: // Ca - cfg += insertFileData("ca", mCaFilename); + cfg.append(insertFileData("ca", mCaFilename)); // Client Cert + Key - cfg += insertFileData("key", mClientKeyFilename); - cfg += insertFileData("cert", mClientCertFilename); + cfg.append(insertFileData("key", mClientKeyFilename)); + cfg.append(insertFileData("cert", mClientCertFilename)); break; case VpnProfile.TYPE_USERPASS_PKCS12: - cfg += "auth-user-pass\n"; + cfg.append("auth-user-pass\n"); case VpnProfile.TYPE_PKCS12: - cfg += insertFileData("pkcs12", mPKCS12Filename); + cfg.append(insertFileData("pkcs12", mPKCS12Filename)); + + if (!TextUtils.isEmpty(mCaFilename)) + { + cfg.append(insertFileData("ca", mCaFilename)); + } break; case VpnProfile.TYPE_USERPASS_KEYSTORE: - cfg += "auth-user-pass\n"; + cfg.append("auth-user-pass\n"); case VpnProfile.TYPE_KEYSTORE: + case VpnProfile.TYPE_EXTERNAL_APP: if (!configForOvpn3) { - String[] ks = getKeyStoreCertificates(context); - cfg += "### From Keystore ####\n"; + String[] ks = getExternalCertificates(context); + cfg.append("### From Keystore/ext auth app ####\n"); if (ks != null) { - cfg += "\n" + ks[0] + "\n\n"; - if (ks[1] != null) - cfg += "\n" + ks[1] + "\n\n"; - cfg += "\n" + ks[2] + "\n\n"; - cfg += "management-external-key\n"; + cfg.append("\n").append(ks[0]).append("\n\n"); + if (!TextUtils.isEmpty(ks[1])) + cfg.append("\n").append(ks[1]).append("\n\n"); + cfg.append("\n").append(ks[2]).append("\n\n"); + cfg.append("management-external-key nopadding\n"); } else { - cfg += context.getString(R.string.keychain_access) + "\n"; + cfg.append(context.getString(R.string.keychain_access)).append("\n"); if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) if (!mAlias.matches("^[a-zA-Z0-9]$")) - cfg += context.getString(R.string.jelly_keystore_alphanumeric_bug) + "\n"; + cfg.append(context.getString(R.string.jelly_keystore_alphanumeric_bug)).append("\n"); } } break; case VpnProfile.TYPE_USERPASS: - cfg += "auth-user-pass\n"; - cfg += insertFileData("ca", mCaFilename); + cfg.append("auth-user-pass\n"); + cfg.append(insertFileData("ca", mCaFilename)); + if (configForOvpn3) { + // OpenVPN 3 needs to be told that a client certificate is not required + cfg.append("client-cert-not-required\n"); + } + } + + if (isUserPWAuth()) { + if (mAuthRetry == AUTH_RETRY_NOINTERACT) + cfg.append("auth-retry nointeract\n"); } + if (!TextUtils.isEmpty(mCrlFilename)) + cfg.append(insertFileData("crl-verify", mCrlFilename)); + if (mUseLzo) { - cfg += "comp-lzo\n"; + cfg.append("comp-lzo\n"); } if (mUseTLSAuth) { + boolean useTlsCrypt = mTLSAuthDirection.equals("tls-crypt"); + boolean useTlsCrypt2 = mTLSAuthDirection.equals("tls-crypt-v2"); + if (mAuthenticationType == TYPE_STATICKEYS) - cfg += insertFileData("secret", mTLSAuthFilename); + cfg.append(insertFileData("secret", mTLSAuthFilename)); + else if (useTlsCrypt) + cfg.append(insertFileData("tls-crypt", mTLSAuthFilename)); + else if (useTlsCrypt2) + cfg.append(insertFileData("tls-crypt-v2", mTLSAuthFilename)); else - cfg += insertFileData("tls-auth", mTLSAuthFilename); + cfg.append(insertFileData("tls-auth", mTLSAuthFilename)); - if (!TextUtils.isEmpty(mTLSAuthDirection)) { - cfg += "key-direction "; - cfg += mTLSAuthDirection; - cfg += "\n"; + if (!TextUtils.isEmpty(mTLSAuthDirection) && !useTlsCrypt) { + cfg.append("key-direction "); + cfg.append(mTLSAuthDirection); + cfg.append("\n"); } } if (!mUsePull) { if (!TextUtils.isEmpty(mIPv4Address)) - cfg += "ifconfig " + cidrToIPAndNetmask(mIPv4Address) + "\n"; + cfg.append("ifconfig ").append(cidrToIPAndNetmask(mIPv4Address)).append("\n"); + + if (!TextUtils.isEmpty(mIPv6Address)) { + // Use our own ip as gateway since we ignore it anyway + String fakegw = mIPv6Address.split("/", 2)[0]; + cfg.append("ifconfig-ipv6 ").append(mIPv6Address).append(" ").append(fakegw).append("\n"); + } - if (!TextUtils.isEmpty(mIPv6Address)) - cfg += "ifconfig-ipv6 " + mIPv6Address + "\n"; } if (mUsePull && mRoutenopull) - cfg += "route-nopull\n"; + cfg.append("route-nopull\n"); String routes = ""; @@ -409,117 +543,137 @@ else if (useTLSClient) if (mUseDefaultRoutev6) - cfg += "route-ipv6 ::/0\n"; + cfg.append("route-ipv6 ::/0\n"); else for (String route : getCustomRoutesv6(mCustomRoutesv6)) { routes += "route-ipv6 " + route + "\n"; } - cfg += routes; + cfg.append(routes); if (mOverrideDNS || !mUsePull) { - if (!TextUtils.isEmpty(mDNS1)) - cfg += "dhcp-option DNS " + mDNS1 + "\n"; - if (!TextUtils.isEmpty(mDNS2)) - cfg += "dhcp-option DNS " + mDNS2 + "\n"; + if (!TextUtils.isEmpty(mDNS1)) { + cfg.append("dhcp-option DNS ").append(mDNS1).append("\n"); + } + if (!TextUtils.isEmpty(mDNS2)) { + cfg.append("dhcp-option DNS ").append(mDNS2).append("\n"); + } if (!TextUtils.isEmpty(mSearchDomain)) - cfg += "dhcp-option DOMAIN " + mSearchDomain + "\n"; + cfg.append("dhcp-option DOMAIN ").append(mSearchDomain).append("\n"); } if (mMssFix != 0) { - if (mMssFix != 1450) - cfg += String.format("mssfix %d\n", mMssFix, Locale.US); - else - cfg += "mssfix\n"; + if (mMssFix != 1450) { + if (configForOvpn3) + cfg.append(String.format(Locale.US, "mssfix %d mtu\n", mMssFix)); + else + cfg.append(String.format(Locale.US, "mssfix %d\n", mMssFix)); + } else + cfg.append("mssfix\n"); + } + + if (mTunMtu >= 48 && mTunMtu != 1500) { + cfg.append(String.format(Locale.US, "tun-mtu %d\n", mTunMtu)); } if (mNobind) - cfg += "nobind\n"; + cfg.append("nobind\n"); // Authentication if (mAuthenticationType != TYPE_STATICKEYS) { if (mCheckRemoteCN) { if (mRemoteCN == null || mRemoteCN.equals("")) - cfg += "verify-x509-name " + mConnections[0].mServerName + " name\n"; + cfg.append("verify-x509-name ").append(openVpnEscape(mConnections[0].mServerName)).append(" name\n"); else switch (mX509AuthType) { // 2.2 style x509 checks case X509_VERIFY_TLSREMOTE_COMPAT_NOREMAPPING: - cfg += "compat-names no-remapping\n"; + cfg.append("compat-names no-remapping\n"); case X509_VERIFY_TLSREMOTE: - cfg += "tls-remote " + openVpnEscape(mRemoteCN) + "\n"; + cfg.append("tls-remote ").append(openVpnEscape(mRemoteCN)).append("\n"); break; case X509_VERIFY_TLSREMOTE_RDN: - cfg += "verify-x509-name " + openVpnEscape(mRemoteCN) + " name\n"; + cfg.append("verify-x509-name ").append(openVpnEscape(mRemoteCN)).append(" name\n"); break; case X509_VERIFY_TLSREMOTE_RDN_PREFIX: - cfg += "verify-x509-name " + openVpnEscape(mRemoteCN) + " name-prefix\n"; + cfg.append("verify-x509-name ").append(openVpnEscape(mRemoteCN)).append(" name-prefix\n"); break; case X509_VERIFY_TLSREMOTE_DN: - cfg += "verify-x509-name " + openVpnEscape(mRemoteCN) + "\n"; + cfg.append("verify-x509-name ").append(openVpnEscape(mRemoteCN)).append("\n"); break; } + if (!TextUtils.isEmpty(mx509UsernameField)) + cfg.append("x509-username-field ").append(openVpnEscape(mx509UsernameField)).append("\n"); } if (mExpectTLSCert) - cfg += "remote-cert-tls server\n"; + cfg.append("remote-cert-tls server\n"); } if (!TextUtils.isEmpty(mCipher)) { - cfg += "cipher " + mCipher + "\n"; + cfg.append("cipher ").append(mCipher).append("\n"); } if (!TextUtils.isEmpty(mAuth)) { - cfg += "auth " + mAuth + "\n"; + cfg.append("auth ").append(mAuth).append("\n"); } // Obscure Settings dialog if (mUseRandomHostname) - cfg += "#my favorite options :)\nremote-random-hostname\n"; + cfg.append("#my favorite options :)\nremote-random-hostname\n"); if (mUseFloat) - cfg += "float\n"; + cfg.append("float\n"); if (mPersistTun) { - cfg += "persist-tun\n"; - cfg += "# persist-tun also enables pre resolving to avoid DNS resolve problem\n"; - cfg += "preresolve\n"; + cfg.append("persist-tun\n"); + cfg.append("# persist-tun also enables pre resolving to avoid DNS resolve problem\n"); + if (!mIsOpenVPN22) + cfg.append("preresolve\n"); } + if (mPushPeerInfo) + cfg.append("push-peer-info\n"); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); boolean usesystemproxy = prefs.getBoolean("usesystemproxy", true); - if (usesystemproxy) { - cfg += "# Use system proxy setting\n"; - cfg += "management-query-proxy\n"; + if (usesystemproxy && !mIsOpenVPN22 && !configForOvpn3 && !usesExtraProxyOptions()) { + cfg.append("# Use system proxy setting\n"); + cfg.append("management-query-proxy\n"); } if (mUseCustomConfig) { - cfg += "# Custom configuration options\n"; - cfg += "# You are on your on own here :)\n"; - cfg += mCustomConfigOptions; - cfg += "\n"; + cfg.append("# Custom configuration options\n"); + cfg.append("# You are on your on own here :)\n"); + cfg.append(mCustomConfigOptions); + cfg.append("\n"); } if (!canUsePlainRemotes) { - cfg += "# Connection Options are at the end to allow global options (and global custom options) to influence connection blocks\n"; + cfg.append("# Connection Options are at the end to allow global options (and global custom options) to influence connection blocks\n"); for (Connection conn : mConnections) { if (conn.mEnabled) { - cfg += "\n"; - cfg += conn.getConnectionBlock(); - cfg += "\n"; + cfg.append("\n"); + cfg.append(conn.getConnectionBlock(configForOvpn3)); + cfg.append("\n"); } } } - return cfg; + return cfg.toString(); + } + + public String getPlatformVersionEnvString() { + return String.format(Locale.US, "%d %s %s %s %s %s", Build.VERSION.SDK_INT, Build.VERSION.RELEASE, + NativeUtils.getNativeAPI(), Build.BRAND, Build.BOARD, Build.MODEL); } public String getVersionEnvString(Context c) { @@ -534,21 +688,9 @@ public String getVersionEnvString(Context c) { } - //! Put inline data inline and other data as normal escaped filename - public static String insertFileData(String cfgentry, String filedata) { - if (filedata == null) { - // TODO: generate good error - return String.format("%s %s\n", cfgentry, "missing"); - } else if (isEmbedded(filedata)) { - String dataWithOutHeader = getEmbeddedContent(filedata); - return String.format(Locale.ENGLISH, "<%s>\n%s\n\n", cfgentry, dataWithOutHeader, cfgentry); - } else { - return String.format(Locale.ENGLISH, "%s %s\n", cfgentry, openVpnEscape(filedata)); - } - } - + @NonNull private Collection getCustomRoutes(String routes) { - Vector cidrRoutes = new Vector(); + Vector cidrRoutes = new Vector<>(); if (routes == null) { // No routes set, return empty vector return cidrRoutes; @@ -557,7 +699,7 @@ private Collection getCustomRoutes(String routes) { if (!route.equals("")) { String cidrroute = cidrToIPAndNetmask(route); if (cidrroute == null) - return null; + return cidrRoutes; cidrRoutes.add(cidrroute); } @@ -567,7 +709,7 @@ private Collection getCustomRoutes(String routes) { } private Collection getCustomRoutesv6(String routes) { - Vector cidrRoutes = new Vector(); + Vector cidrRoutes = new Vector<>(); if (routes == null) { // No routes set, return empty vector return cidrRoutes; @@ -600,75 +742,42 @@ private String cidrToIPAndNetmask(String route) { return null; - long nm = 0xffffffffl; - nm = (nm << (32 - len)) & 0xffffffffl; + long nm = 0xffffffffL; + nm = (nm << (32 - len)) & 0xffffffffL; String netmask = String.format(Locale.ENGLISH, "%d.%d.%d.%d", (nm & 0xff000000) >> 24, (nm & 0xff0000) >> 16, (nm & 0xff00) >> 8, nm & 0xff); return parts[0] + " " + netmask; } - public Intent prepareStartService(Context context) { Intent intent = getStartServiceIntent(context); + // TODO: Handle this?! +// if (mAuthenticationType == VpnProfile.TYPE_KEYSTORE || mAuthenticationType == VpnProfile.TYPE_USERPASS_KEYSTORE) { +// if (getKeyStoreCertificates(context) == null) +// return null; +// } - if (mAuthenticationType == VpnProfile.TYPE_KEYSTORE || mAuthenticationType == VpnProfile.TYPE_USERPASS_KEYSTORE) { - if (getKeyStoreCertificates(context) == null) - return null; - } - + return intent; + } - try { - FileWriter cfg = new FileWriter(VPNLaunchHelper.getConfigFilePath(context)); - cfg.write(getConfigFile(context, false)); - cfg.flush(); - cfg.close(); - } catch (IOException e) { - VpnStatus.logException(e); - } + public void writeConfigFile(Context context) throws IOException { + FileWriter cfg = new FileWriter(VPNLaunchHelper.getConfigFilePath(context)); + cfg.write(getConfigFile(context, false)); + cfg.flush(); + cfg.close(); - return intent; } public Intent getStartServiceIntent(Context context) { String prefix = context.getPackageName(); Intent intent = new Intent(context, OpenVPNService.class); - intent.putExtra(prefix + ".ARGV", VPNLaunchHelper.buildOpenvpnArgv(context)); intent.putExtra(prefix + ".profileUUID", mUuid.toString()); - - ApplicationInfo info = context.getApplicationInfo(); - intent.putExtra(prefix + ".nativelib", info.nativeLibraryDir); + intent.putExtra(prefix + ".profileVersion", mVersion); return intent; } - public String[] getKeyStoreCertificates(Context context) { - return getKeyStoreCertificates(context, 5); - } - - public static String getDisplayName(String embeddedFile) { - int start = DISPLAYNAME_TAG.length(); - int end = embeddedFile.indexOf(INLINE_TAG); - return embeddedFile.substring(start, end); - } - - public static String getEmbeddedContent(String data) { - if (!data.contains(INLINE_TAG)) - return data; - - int start = data.indexOf(INLINE_TAG) + INLINE_TAG.length(); - return data.substring(start); - } - - public static boolean isEmbedded(String data) { - if (data == null) - return false; - if (data.startsWith(INLINE_TAG) || data.startsWith(DISPLAYNAME_TAG)) - return true; - else - return false; - } - public void checkForRestart(final Context context) { /* This method is called when OpenVPNService is restarted */ @@ -677,7 +786,7 @@ public void checkForRestart(final Context context) { new Thread(new Runnable() { @Override public void run() { - getKeyStoreCertificates(context); + getExternalCertificates(context); } }).start(); @@ -699,7 +808,7 @@ protected VpnProfile clone() throws CloneNotSupportedException { public VpnProfile copy(String name) { try { - VpnProfile copy = (VpnProfile) clone(); + VpnProfile copy = clone(); copy.mName = name; return copy; @@ -709,25 +818,44 @@ public VpnProfile copy(String name) { } } + public void pwDidFail(Context c) { - class NoCertReturnedException extends Exception { - public NoCertReturnedException(String msg) { - super(msg); - } } - synchronized String[] getKeyStoreCertificates(Context context, int tries) { - PrivateKey privateKey = null; - X509Certificate[] caChain; - Exception exp; - try { - privateKey = KeyChain.getPrivateKey(context, mAlias); - mPrivateKey = privateKey; + private X509Certificate[] getKeyStoreCertificates(Context context) throws KeyChainException, InterruptedException { + PrivateKey privateKey = KeyChain.getPrivateKey(context, mAlias); + mPrivateKey = privateKey; - String keystoreChain = null; + + X509Certificate[] caChain = KeyChain.getCertificateChain(context, mAlias); + return caChain; + } + + private X509Certificate[] getExtAppCertificates(Context context) throws KeyChainException { + if (mExternalAuthenticator == null || mAlias == null) + throw new KeyChainException("Alias or external auth provider name not set"); + return ExtAuthHelper.getCertificateChain(context, mExternalAuthenticator, mAlias); + } + + public String[] getExternalCertificates(Context context) { + return getExternalCertificates(context, 5); + } - caChain = KeyChain.getCertificateChain(context, mAlias); + synchronized String[] getExternalCertificates(Context context, int tries) { + // Force application context- KeyChain methods will block long enough that by the time they + // are finished and try to unbind, the original activity context might have been destroyed. + context = context.getApplicationContext(); + + try { + String keystoreChain = null; + + X509Certificate caChain[]; + if (mAuthenticationType == TYPE_EXTERNAL_APP) { + caChain = getExtAppCertificates(context); + } else { + caChain = getKeyStoreCertificates(context); + } if (caChain == null) throw new NoCertReturnedException("No certificate returned from Keystore"); @@ -749,11 +877,12 @@ synchronized String[] getKeyStoreCertificates(Context context, int tries) { String caout = null; if (!TextUtils.isEmpty(mCaFilename)) { try { - Certificate cacert = X509Utils.getCertificateFromFile(mCaFilename); + Certificate[] cacerts = X509Utils.getCertificatesFromFile(mCaFilename); StringWriter caoutWriter = new StringWriter(); PemWriter pw = new PemWriter(caoutWriter); - pw.writeObject(new PemObject("CERTIFICATE", cacert.getEncoded())); + for (Certificate cert : cacerts) + pw.writeObject(new PemObject("CERTIFICATE", cert.getEncoded())); pw.close(); caout = caoutWriter.toString(); @@ -787,20 +916,19 @@ synchronized String[] getKeyStoreCertificates(Context context, int tries) { } return new String[]{ca, extra, user}; - } catch (InterruptedException e) { - exp = e; - } catch (FileNotFoundException e) { - exp = e; - } catch (CertificateException e) { - exp = e; - } catch (IOException e) { - exp = e; - } catch (KeyChainException e) { - exp = e; - } catch (NoCertReturnedException e) { - exp = e; - } catch (IllegalArgumentException e) { - exp = e; + } catch (InterruptedException | IOException | KeyChainException | NoCertReturnedException | IllegalArgumentException + | CertificateException e) { + e.printStackTrace(); + VpnStatus.logError(R.string.keyChainAccessError, e.getLocalizedMessage()); + + VpnStatus.logError(R.string.keychain_access); + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) { + if (!mAlias.matches("^[a-zA-Z0-9]$")) { + VpnStatus.logError(R.string.jelly_keystore_alphanumeric_bug); + } + } + return null; + } catch (AssertionError e) { if (tries == 0) return null; @@ -810,43 +938,84 @@ synchronized String[] getKeyStoreCertificates(Context context, int tries) { } catch (InterruptedException e1) { VpnStatus.logException(e1); } - return getKeyStoreCertificates(context, tries - 1); + return getExternalCertificates(context, tries - 1); } - exp.printStackTrace(); - VpnStatus.logError(R.string.keyChainAccessError, exp.getLocalizedMessage()); + } - VpnStatus.logError(R.string.keychain_access); - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) { - if (!mAlias.matches("^[a-zA-Z0-9]$")) { - VpnStatus.logError(R.string.jelly_keystore_alphanumeric_bug); - } - } - return null; + public int checkProfile(Context c) { + return checkProfile(c, doUseOpenVPN3(c)); } - //! Return an error if somethign is wrong - public int checkProfile(Context context) { - if (mAuthenticationType == TYPE_KEYSTORE || mAuthenticationType == TYPE_USERPASS_KEYSTORE) { + //! Return an error if something is wrong + public int checkProfile(Context context, boolean useOpenVPN3) { + if (mAuthenticationType == TYPE_KEYSTORE || mAuthenticationType == TYPE_USERPASS_KEYSTORE || mAuthenticationType == TYPE_EXTERNAL_APP) { if (mAlias == null) return R.string.no_keystore_cert_selected; + } else if (mAuthenticationType == TYPE_CERTIFICATES || mAuthenticationType == TYPE_USERPASS_CERTIFICATES) { + if (TextUtils.isEmpty(mCaFilename)) + return R.string.no_ca_cert_selected; } + if (mCheckRemoteCN && mX509AuthType == X509_VERIFY_TLSREMOTE) + return R.string.deprecated_tls_remote; + if (!mUsePull || mAuthenticationType == TYPE_STATICKEYS) { if (mIPv4Address == null || cidrToIPAndNetmask(mIPv4Address) == null) return R.string.ipv4_format_error; } - if (!mUseDefaultRoute && (getCustomRoutes(mCustomRoutes) == null || getCustomRoutes(mExcludedRoutes) == null)) - return R.string.custom_route_format_error; + if (!mUseDefaultRoute) { + if (!TextUtils.isEmpty(mCustomRoutes) && getCustomRoutes(mCustomRoutes).size() == 0) + return R.string.custom_route_format_error; + + if (!TextUtils.isEmpty(mExcludedRoutes) && getCustomRoutes(mExcludedRoutes).size() == 0) + return R.string.custom_route_format_error; + + } + + if (mUseTLSAuth && TextUtils.isEmpty(mTLSAuthFilename)) + return R.string.missing_tlsauth; + + if ((mAuthenticationType == TYPE_USERPASS_CERTIFICATES || mAuthenticationType == TYPE_CERTIFICATES) + && (TextUtils.isEmpty(mClientCertFilename) || TextUtils.isEmpty(mClientKeyFilename))) + return R.string.missing_certificates; + + if ((mAuthenticationType == TYPE_CERTIFICATES || mAuthenticationType == TYPE_USERPASS_CERTIFICATES) + && TextUtils.isEmpty(mCaFilename)) + return R.string.missing_ca_certificate; + boolean noRemoteEnabled = true; - for (Connection c : mConnections) + for (Connection c : mConnections) { if (c.mEnabled) noRemoteEnabled = false; + } if (noRemoteEnabled) return R.string.remote_no_server_selected; + if (useOpenVPN3) { + if (mAuthenticationType == TYPE_STATICKEYS) { + return R.string.openvpn3_nostatickeys; + } + if (mAuthenticationType == TYPE_PKCS12 || mAuthenticationType == TYPE_USERPASS_PKCS12) { + return R.string.openvpn3_pkcs12; + } + for (Connection conn : mConnections) { + if (conn.mProxyType == Connection.ProxyType.ORBOT || conn.mProxyType == Connection.ProxyType.SOCKS5) + return R.string.openvpn3_socksproxy; + } + } + for (Connection c : mConnections) { + if (c.mProxyType == Connection.ProxyType.ORBOT) { + if (usesExtraProxyOptions()) + return R.string.error_orbot_and_proxy_options; + if (!OrbotHelper.checkTorReceier(context)) + return R.string.no_orbotfound; + } + } + + // Everything okay return R.string.no_error_found; @@ -855,10 +1024,9 @@ public int checkProfile(Context context) { //! Openvpn asks for a "Private Key", this should be pkcs12 key // public String getPasswordPrivateKey() { - if (mTransientPCKS12PW != null) { - String pwcopy = mTransientPCKS12PW; - mTransientPCKS12PW = null; - return pwcopy; + String cachedPw = PasswordCache.getPKCS12orCertificatePassword(mUuid, true); + if (cachedPw != null) { + return cachedPw; } switch (mAuthenticationType) { case TYPE_PKCS12: @@ -923,33 +1091,32 @@ else if (data.contains("-----BEGIN ENCRYPTED PRIVATE KEY-----")) return false; } - public int needUserPWInput(boolean ignoreTransient) { + public int needUserPWInput(String transientCertOrPkcs12PW, String mTransientAuthPW) { if ((mAuthenticationType == TYPE_PKCS12 || mAuthenticationType == TYPE_USERPASS_PKCS12) && (mPKCS12Password == null || mPKCS12Password.equals(""))) { - if (ignoreTransient || mTransientPCKS12PW == null) + if (transientCertOrPkcs12PW == null) return R.string.pkcs12_file_encryption_key; } if (mAuthenticationType == TYPE_CERTIFICATES || mAuthenticationType == TYPE_USERPASS_CERTIFICATES) { if (requireTLSKeyPassword() && TextUtils.isEmpty(mKeyPassword)) - if (ignoreTransient || mTransientPCKS12PW == null) { + if (transientCertOrPkcs12PW == null) { return R.string.private_key_password; } } if (isUserPWAuth() && (TextUtils.isEmpty(mUsername) || - (TextUtils.isEmpty(mPassword) && (mTransientPW == null || ignoreTransient)))) { + (TextUtils.isEmpty(mPassword) && mTransientAuthPW == null))) { return R.string.password; } return 0; } public String getPasswordAuth() { - if (mTransientPW != null) { - String pwcopy = mTransientPW; - mTransientPW = null; - return pwcopy; + String cachedPw = PasswordCache.getAuthPassword(mUuid, true); + if (cachedPw != null) { + return cachedPw; } else { return mPassword; } @@ -962,59 +1129,85 @@ public String toString() { } public String getUUIDString() { - return mUuid.toString(); + return mUuid.toString().toLowerCase(Locale.ENGLISH); } public PrivateKey getKeystoreKey() { return mPrivateKey; } - public String getSignedData(String b64data) { - PrivateKey privkey = getKeystoreKey(); - Exception err; - + @Nullable + public String getSignedData(Context c, String b64data, boolean pkcs1padding) { byte[] data = Base64.decode(b64data, Base64.DEFAULT); + byte[] signed_bytes; + if (mAuthenticationType == TYPE_EXTERNAL_APP) + signed_bytes = getExtAppSignedData(c, data); + else + signed_bytes = getKeyChainSignedData(data, pkcs1padding); + + if (signed_bytes != null) + return Base64.encodeToString(signed_bytes, Base64.NO_WRAP); + else + return null; + } + + private byte[] getExtAppSignedData(Context c, byte[] data) { + if (TextUtils.isEmpty(mExternalAuthenticator)) + return null; + try { + return ExtAuthHelper.signData(c, mExternalAuthenticator, mAlias, data); + } catch (KeyChainException | InterruptedException e) { + VpnStatus.logError(R.string.error_extapp_sign, mExternalAuthenticator, e.getClass().toString(), e.getLocalizedMessage()); + return null; + } + } + + private byte[] getKeyChainSignedData(byte[] data, boolean pkcs1padding) { + PrivateKey privkey = getKeystoreKey(); // The Jelly Bean *evil* Hack // 4.2 implements the RSA/ECB/PKCS1PADDING in the OpenSSLprovider if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) { - return processSignJellyBeans(privkey, data); + return processSignJellyBeans(privkey, data, pkcs1padding); } try { + @SuppressLint("GetInstance") + String keyalgorithm = privkey.getAlgorithm(); + + byte[] signed_bytes; + if (keyalgorithm.equals("EC")) { + Signature signer = Signature.getInstance("NONEwithECDSA"); + + signer.initSign(privkey); + signer.update(data); + signed_bytes = signer.sign(); + } else { /* ECB is perfectly fine in this special case, since we are using it for the public/private part in the TLS exchange */ - @SuppressLint("GetInstance") - Cipher rsaSigner = Cipher.getInstance("RSA/ECB/PKCS1PADDING"); + Cipher signer; + if (pkcs1padding) + signer = Cipher.getInstance("RSA/ECB/PKCS1PADDING"); + else + signer = Cipher.getInstance("RSA/ECB/NoPadding"); - rsaSigner.init(Cipher.ENCRYPT_MODE, privkey); - byte[] signed_bytes = rsaSigner.doFinal(data); - return Base64.encodeToString(signed_bytes, Base64.NO_WRAP); + signer.init(Cipher.ENCRYPT_MODE, privkey); - } catch (NoSuchAlgorithmException e) { - err = e; - } catch (InvalidKeyException e) { - err = e; - } catch (NoSuchPaddingException e) { - err = e; - } catch (IllegalBlockSizeException e) { - err = e; - } catch (BadPaddingException e) { - err = e; + signed_bytes = signer.doFinal(data); + } + return signed_bytes; + } catch (NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException + | BadPaddingException | NoSuchPaddingException | SignatureException e) { + VpnStatus.logError(R.string.error_rsa_sign, e.getClass().toString(), e.getLocalizedMessage()); + return null; } - - VpnStatus.logError(R.string.error_rsa_sign, err.getClass().toString(), err.getLocalizedMessage()); - - return null; - } - private String processSignJellyBeans(PrivateKey privkey, byte[] data) { - Exception err; + private byte[] processSignJellyBeans(PrivateKey privkey, byte[] data, boolean pkcs1padding) { try { Method getKey = privkey.getClass().getSuperclass().getDeclaredMethod("getOpenSSLKey"); getKey.setAccessible(true); @@ -1032,25 +1225,33 @@ private String processSignJellyBeans(PrivateKey privkey, byte[] data) { getPkeyContext.setAccessible(false); // 112 with TLS 1.2 (172 back with 4.3), 36 with TLS 1.0 - byte[] signed_bytes = NativeUtils.rsasign(data, pkey); - return Base64.encodeToString(signed_bytes, Base64.NO_WRAP); + return NativeUtils.rsasign(data, pkey, pkcs1padding); - } catch (NoSuchMethodException e) { - err = e; - } catch (IllegalArgumentException e) { - err = e; - } catch (IllegalAccessException e) { - err = e; - } catch (InvocationTargetException e) { - err = e; - } catch (InvalidKeyException e) { - err = e; + } catch (NoSuchMethodException | InvalidKeyException | InvocationTargetException | IllegalAccessException | IllegalArgumentException e) { + VpnStatus.logError(R.string.error_rsa_sign, e.getClass().toString(), e.getLocalizedMessage()); + return null; } - VpnStatus.logError(R.string.error_rsa_sign, err.getClass().toString(), err.getLocalizedMessage()); + } - return null; + private boolean usesExtraProxyOptions() { + if (mUseCustomConfig && mCustomConfigOptions != null && mCustomConfigOptions.contains("http-proxy-option ")) + return true; + for (Connection c : mConnections) + if (c.usesExtraProxyOptions()) + return true; + return false; + } + + class NoCertReturnedException extends Exception { + public NoCertReturnedException(String msg) { + super(msg); + } } -} \ No newline at end of file +} + + + + diff --git a/app/src/main/java/de/blinkt/openvpn/core/CIDRIP.java b/app/src/main/java/de/blinkt/openvpn/core/CIDRIP.java index 94ed8a0..ca3d116 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/CIDRIP.java +++ b/app/src/main/java/de/blinkt/openvpn/core/CIDRIP.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2014 Arne Schwabe + * Copyright (c) 2012-2016 Arne Schwabe * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt */ @@ -14,6 +14,11 @@ class CIDRIP { public CIDRIP(String ip, String mask) { mIp = ip; + len = calculateLenFromMask(mask); + + } + + public static int calculateLenFromMask(String mask) { long netmask = getInt(mask); // Add 33. bit to ensure the loop terminates @@ -24,6 +29,7 @@ public CIDRIP(String ip, String mask) { lenZeros++; netmask = netmask >> 1; } + int len; // Check if rest of netmask is only 1s if (netmask != (0x1ffffffffl >> lenZeros)) { // Asume no CIDR, set /32 @@ -31,7 +37,7 @@ public CIDRIP(String ip, String mask) { } else { len = 32 - lenZeros; } - + return len; } public CIDRIP(String address, int prefix_length) { @@ -47,9 +53,9 @@ public String toString() { public boolean normalise() { long ip = getInt(mIp); - long newip = ip & (0xffffffffl << (32 - len)); + long newip = ip & (0xffffffffL << (32 - len)); if (newip != ip) { - mIp = String.format("%d.%d.%d.%d", (newip & 0xff000000) >> 24, (newip & 0xff0000) >> 16, (newip & 0xff00) >> 8, newip & 0xff); + mIp = String.format(Locale.US,"%d.%d.%d.%d", (newip & 0xff000000) >> 24, (newip & 0xff0000) >> 16, (newip & 0xff00) >> 8, newip & 0xff); return true; } else { return false; diff --git a/app/src/main/java/de/blinkt/openvpn/core/ConfigParser.java b/app/src/main/java/de/blinkt/openvpn/core/ConfigParser.java index 843188b..340853d 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/ConfigParser.java +++ b/app/src/main/java/de/blinkt/openvpn/core/ConfigParser.java @@ -1,23 +1,21 @@ /* - * Copyright (c) 2012-2014 Arne Schwabe + * Copyright (c) 2012-2016 Arne Schwabe * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt */ package de.blinkt.openvpn.core; +import android.os.Build; +import androidx.core.util.Pair; import android.text.TextUtils; -import android.util.Pair; import java.io.BufferedReader; import java.io.IOException; import java.io.Reader; import java.io.StringReader; -import java.util.Collections; -import java.util.HashMap; -import java.util.Locale; -import java.util.Vector; +import java.util.*; -import ht.vpn.android.VpnProfile; +import de.blinkt.openvpn.VpnProfile; //! Openvpn Config FIle Parser, probably not 100% accurate but close enough @@ -29,14 +27,132 @@ public class ConfigParser { public static final String CONVERTED_PROFILE = "converted Profile"; - private HashMap>> options = new HashMap>>(); + final String[] unsupportedOptions = {"config", + "tls-server" + + }; + // Ignore all scripts + // in most cases these won't work and user who wish to execute scripts will + // figure out themselves + private final String[] ignoreOptions = {"tls-client", + "allow-recursive-routing", + "askpass", + "auth-nocache", + "up", + "down", + "route-up", + "ipchange", + "route-pre-down", + "auth-user-pass-verify", + "block-outside-dns", + "client-cert-not-required", + "dhcp-release", + "dhcp-renew", + "dh", + "group", + "ip-win32", + "ifconfig-nowarn", + "management-hold", + "management", + "management-client", + "management-query-remote", + "management-query-passwords", + "management-query-proxy", + "management-external-key", + "management-forget-disconnect", + "management-signal", + "management-log-cache", + "management-up-down", + "management-client-user", + "management-client-group", + "pause-exit", + "preresolve", + "plugin", + "machine-readable-output", + "persist-key", + "push", + "register-dns", + "route-delay", + "route-gateway", + "route-metric", + "route-method", + "status", + "script-security", + "show-net-up", + "suppress-timestamps", + "tap-sleep", + "tmp-dir", + "tun-ipv6", + "topology", + "user", + "win-sys", + }; + private final String[][] ignoreOptionsWithArg = + { + {"setenv", "IV_GUI_VER"}, + {"setenv", "IV_SSO"}, + {"setenv", "IV_PLAT_VER"}, + {"setenv", "IV_OPENVPN_GUI_VERSION"}, + {"engine", "dynamic"}, + {"setenv", "CLIENT_CERT"}, + {"resolv-retry", "60"} + }; + private final String[] connectionOptions = { + "local", + "remote", + "float", + "port", + "connect-retry", + "connect-timeout", + "connect-retry-max", + "link-mtu", + "tun-mtu", + "tun-mtu-extra", + "fragment", + "mtu-disc", + "local-port", + "remote-port", + "bind", + "nobind", + "proto", + "http-proxy", + "http-proxy-retry", + "http-proxy-timeout", + "http-proxy-option", + "socks-proxy", + "socks-proxy-retry", + "http-proxy-user-pass", + "explicit-exit-notify", + }; + private HashSet connectionOptionsSet = new HashSet<>(Arrays.asList(connectionOptions)); + + private HashMap>> options = new HashMap<>(); private HashMap> meta = new HashMap>(); private String auth_user_pass_file; - private String crl_verify_file; + static public void useEmbbedUserAuth(VpnProfile np, String inlinedata) { + String data = VpnProfile.getEmbeddedContent(inlinedata); + String[] parts = data.split("\n"); + if (parts.length >= 2) { + np.mUsername = parts[0]; + np.mPassword = parts[1]; + } + } + + static public void useEmbbedHttpAuth(Connection c, String inlinedata) { + String data = VpnProfile.getEmbeddedContent(inlinedata); + String[] parts = data.split("\n"); + if (parts.length >= 2) { + c.mProxyAuthUser = parts[0]; + c.mProxyAuthPassword = parts[1]; + c.mUseProxyAuth = true; + } + } public void parseConfig(Reader reader) throws IOException, ConfigParseError { + HashMap optionAliases = new HashMap<>(); + optionAliases.put("server-poll-timeout", "timeout-connect"); BufferedReader br = new BufferedReader(reader); @@ -76,12 +192,15 @@ public void parseConfig(Reader reader) throws IOException, ConfigParseError { checkinlinefile(args, br); String optionname = args.get(0); + if (optionAliases.get(optionname) != null) + optionname = optionAliases.get(optionname); + if (!options.containsKey(optionname)) { options.put(optionname, new Vector>()); } options.get(optionname).add(args); } - } catch (OutOfMemoryError memoryError) { + } catch (java.lang.OutOfMemoryError memoryError) { throw new ConfigParseError("File too large to parse: " + memoryError.getLocalizedMessage()); } } @@ -116,6 +235,9 @@ private void checkinlinefile(Vector args, BufferedReader br) throws IOEx } } while (true); + if (inlinefile.endsWith("\n")) + inlinefile = inlinefile.substring(0, inlinefile.length() - 1); + args.clear(); args.add(argname); args.add(inlinefile); @@ -127,15 +249,6 @@ public String getAuthUserPassFile() { return auth_user_pass_file; } - public String getCrlVerifyFile() { - return crl_verify_file; - } - - enum linestate { - initial, - readin_single_quote, reading_quoted, reading_unquoted, done - } - private boolean space(char c) { // I really hope nobody is using zero bytes inside his/her config file // to sperate parameter but here we go: @@ -143,15 +256,6 @@ private boolean space(char c) { } - public class ConfigParseError extends Exception { - private static final long serialVersionUID = -60L; - - public ConfigParseError(String msg) { - super(msg); - } - } - - // adapted openvpn's parse function to java private Vector parseline(String line) throws ConfigParseError { Vector parameters = new Vector(); @@ -224,7 +328,7 @@ else if (!backslash && in == '\'') backslash = false; } - /* store parameter character */ + /* store parameter character */ if (out != 0) { currentarg += out; } @@ -233,99 +337,6 @@ else if (!backslash && in == '\'') return parameters; } - - final String[] unsupportedOptions = {"config", - "tls-server" - - }; - - // Ignore all scripts - // in most cases these won't work and user who wish to execute scripts will - // figure out themselves - final String[] ignoreOptions = {"tls-client", - "askpass", - "auth-nocache", - "up", - "down", - "route-up", - "ipchange", - "route-up", - "route-pre-down", - "auth-user-pass-verify", - "dhcp-release", - "dhcp-renew", - "dh", - "group", - "ip-win32", - "management-hold", - "management", - "management-client", - "management-query-remote", - "management-query-passwords", - "management-query-proxy", - "management-external-key", - "management-forget-disconnect", - "management-signal", - "management-log-cache", - "management-up-down", - "management-client-user", - "management-client-group", - "pause-exit", - "plugin", - "machine-readable-output", - "persist-key", - "register-dns", - "route-delay", - "route-gateway", - "route-metric", - "route-method", - "status", - "script-security", - "show-net-up", - "suppress-timestamps", - "tmp-dir", - "tun-ipv6", - "topology", - "user", - "win-sys", - }; - - final String[][] ignoreOptionsWithArg = - { - {"setenv", "IV_GUI_VER"}, - {"setenv", "IV_OPENVPN_GUI_VERSION"}, - {"engine", "dynamic"} - }; - - final String[] connectionOptions = { - "local", - "remote", - "float", - "port", - "connect-retry", - "connect-timeout", - "connect-retry-max", - "link-mtu", - "tun-mtu", - "tun-mtu-extra", - "fragment", - "mtu-disc", - "local-port", - "remote-port", - "bind", - "nobind", - "proto", - "http-proxy", - "http-proxy-retry", - "http-proxy-timeout", - "http-proxy-option", - "socks-proxy", - "socks-proxy-retry", - "explicit-exit-notify", - "mssfix" - }; - - // This method is far too long @SuppressWarnings("ConstantConditions") public VpnProfile convertProfile() throws ConfigParseError, IOException { @@ -394,6 +405,10 @@ public VpnProfile convertProfile() throws ConfigParseError, IOException { np.mCustomRoutesv6 = customIPv6Routes; } + Vector routeNoPull = getOption("route-nopull", 0, 0); + if (routeNoPull != null) + np.mRoutenopull = true; + // Also recognize tls-auth [inline] direction ... Vector> tlsauthoptions = getAllOption("tls-auth", 1, 2); if (tlsauthoptions != null) { @@ -413,15 +428,23 @@ public VpnProfile convertProfile() throws ConfigParseError, IOException { if (direction != null) np.mTLSAuthDirection = direction.get(1); - Vector> defgw = getAllOption("redirect-gateway", 0, 5); + for (String crypt: new String[]{"tls-crypt", "tls-crypt-v2"}) { + Vector tlscrypt = getOption(crypt, 1, 1); + if (tlscrypt != null) { + np.mUseTLSAuth = true; + np.mTLSAuthFilename = tlscrypt.get(1); + np.mTLSAuthDirection = crypt; + } + } + + Vector> defgw = getAllOption("redirect-gateway", 0, 7); if (defgw != null) { - np.mUseDefaultRoute = true; - checkRedirectParameters(np, defgw); + checkRedirectParameters(np, defgw, true); } Vector> redirectPrivate = getAllOption("redirect-private", 0, 5); if (redirectPrivate != null) { - checkRedirectParameters(np, redirectPrivate); + checkRedirectParameters(np, redirectPrivate, false); } Vector dev = getOption("dev", 1, 1); Vector devtype = getOption("dev-type", 1, 1); @@ -434,7 +457,7 @@ public VpnProfile convertProfile() throws ConfigParseError, IOException { throw new ConfigParseError("Sorry. Only tun mode is supported. See the FAQ for more detail"); } - Vector mssfix = getOption("mssfix", 0, 1); + Vector mssfix = getOption("mssfix", 0, 2); if (mssfix != null) { if (mssfix.size() >= 2) { @@ -444,7 +467,22 @@ public VpnProfile convertProfile() throws ConfigParseError, IOException { throw new ConfigParseError("Argument to --mssfix has to be an integer"); } } else { - np.mMssFix = VpnProfile.DEFAULT_MSSFIX_SIZE; + np.mMssFix = 1450; // OpenVPN default size + } + // Ignore mtu argument of OpenVPN3 and report error otherwise + if (mssfix.size() >= 3 && !(mssfix.get(2).equals("mtu"))) { + throw new ConfigParseError("Second argument to --mssfix unkonwn"); + } + } + + + Vector tunmtu = getOption("tun-mtu", 1, 1); + + if (tunmtu != null) { + try { + np.mTunMtu = Integer.parseInt(tunmtu.get(1)); + } catch (NumberFormatException e) { + throw new ConfigParseError("Argument to --tun-mtu has to be an integer"); } } @@ -550,16 +588,23 @@ public VpnProfile convertProfile() throws ConfigParseError, IOException { if (verifyx509name.size() > 2) { if (verifyx509name.get(2).equals("name")) np.mX509AuthType = VpnProfile.X509_VERIFY_TLSREMOTE_RDN; + else if (verifyx509name.get(2).equals("subject")) + np.mX509AuthType = VpnProfile.X509_VERIFY_TLSREMOTE_DN; else if (verifyx509name.get(2).equals("name-prefix")) np.mX509AuthType = VpnProfile.X509_VERIFY_TLSREMOTE_RDN_PREFIX; else - throw new ConfigParseError("Unknown parameter to x509-verify-name: " + verifyx509name.get(2)); + throw new ConfigParseError("Unknown parameter to verify-x509-name: " + verifyx509name.get(2)); } else { np.mX509AuthType = VpnProfile.X509_VERIFY_TLSREMOTE_DN; } } + Vector x509usernamefield = getOption("x509-username-field", 1, 1); + if (x509usernamefield != null) { + np.mx509UsernameField = x509usernamefield.get(1); + } + Vector verb = getOption("verb", 1, 1); if (verb != null) { @@ -573,9 +618,15 @@ else if (verifyx509name.get(2).equals("name-prefix")) if (getOption("persist-tun", 0, 0) != null) np.mPersistTun = true; - Vector connectretry = getOption("connect-retry", 1, 1); - if (connectretry != null) + if (getOption("push-peer-info", 0, 0) != null) + np.mPushPeerInfo = true; + + Vector connectretry = getOption("connect-retry", 1, 2); + if (connectretry != null) { np.mConnectRetry = connectretry.get(1); + if (connectretry.size() > 2) + np.mConnectRetryMaxTime = connectretry.get(2); + } Vector connectretrymax = getOption("connect-retry-max", 1, 1); if (connectretrymax != null) @@ -606,14 +657,28 @@ else if (verifyx509name.get(2).equals("name-prefix")) } } + Vector authretry = getOption("auth-retry", 1, 1); + if (authretry != null) { + if (authretry.get(1).equals("none")) + np.mAuthRetry = VpnProfile.AUTH_RETRY_NONE_FORGET; + else if (authretry.get(1).equals("nointeract")) + np.mAuthRetry = VpnProfile.AUTH_RETRY_NOINTERACT; + else if (authretry.get(1).equals("interact")) + np.mAuthRetry = VpnProfile.AUTH_RETRY_NOINTERACT; + else + throw new ConfigParseError("Unknown parameter to auth-retry: " + authretry.get(2)); + } + + Vector crlfile = getOption("crl-verify", 1, 2); if (crlfile != null) { // If the 'dir' parameter is present just add it as custom option .. - np.mCustomConfigOptions += TextUtils.join(" ", crlfile) + "\n"; - if (crlfile.size() == 2) { + if (crlfile.size() == 3 && crlfile.get(2).equals("dir")) + np.mCustomConfigOptions += join(" ", crlfile) + "\n"; + else // Save the filename for the config converter to add later - crl_verify_file = crlfile.get(1); - } + np.mCrlFilename = crlfile.get(1); + } @@ -660,9 +725,11 @@ else if (protoToDisable.equals("tcp")) } // Parse OpenVPN Access Server extra - Vector friendlyname = meta.get("FRIENDLY_NAME"); - if (friendlyname != null && friendlyname.size() > 1) - np.mName = friendlyname.get(1); + for (String as_name_directive: new String[]{"PROFILE", "FRIENDLY_NAME"}) { + Vector friendlyname = meta.get(as_name_directive); + if (friendlyname != null && friendlyname.size() > 1) + np.mName = friendlyname.get(1); + } Vector ocusername = meta.get("USERNAME"); @@ -675,6 +742,13 @@ else if (protoToDisable.equals("tcp")) return np; } + private String join(String s, Vector str) { + if (Build.VERSION.SDK_INT > 26) + return String.join(s, str); + else + return TextUtils.join(s, str); + } + private Pair parseConnection(String connection, Connection defaultValues) throws IOException, ConfigParseError { // Parse a connection Block as a new configuration file @@ -715,21 +789,59 @@ private Pair parseConnectionOptions(Connection connDef conn.mUseUdp = isUdpProto(proto.get(1)); } + Vector connectTimeout = getOption("connect-timeout", 1, 1); + if (connectTimeout != null) { + try { + conn.mConnectTimeout = Integer.parseInt(connectTimeout.get(1)); + } catch (NumberFormatException nfe) { + throw new ConfigParseError(String.format("Argument to connect-timeout (%s) must to be an integer: %s", + connectTimeout.get(1), nfe.getLocalizedMessage())); + + } + } + + Vector proxy = getOption("socks-proxy", 1, 2); + if (proxy == null) + proxy = getOption("http-proxy", 2, 2); + + if (proxy != null) { + if (proxy.get(0).equals("socks-proxy")) { + conn.mProxyType = Connection.ProxyType.SOCKS5; + // socks defaults to 1080, http always sets port + conn.mProxyPort = "1080"; + } else { + conn.mProxyType = Connection.ProxyType.HTTP; + } + + conn.mProxyName = proxy.get(1); + if (proxy.size() >= 3) + conn.mProxyPort = proxy.get(2); + } + + Vector httpproxyauthhttp = getOption("http-proxy-user-pass", 1, 1); + if (httpproxyauthhttp != null) + useEmbbedHttpAuth(conn, httpproxyauthhttp.get(1)); + // Parse remote config Vector> remotes = getAllOption("remote", 1, 3); - // Assume that we need custom options if connectionDefault are set - if (connDefault != null) { - for (Vector> option : options.values()) { - - conn.mCustomConfiguration += getOptionStrings(option); + Vector optionsToRemove = new Vector<>(); + // Assume that we need custom options if connectionDefault are set or in the connection specific set + for (Map.Entry>> option : options.entrySet()) { + if (connDefault != null || connectionOptionsSet.contains(option.getKey())) { + conn.mCustomConfiguration += getOptionStrings(option.getValue()); + optionsToRemove.add(option.getKey()); } - if (!TextUtils.isEmpty(conn.mCustomConfiguration)) - conn.mUseCustomConfig = true; } + for (String o: optionsToRemove) + options.remove(o); + + if (!(conn.mCustomConfiguration == null || "".equals(conn.mCustomConfiguration.trim()))) + conn.mUseCustomConfig = true; + // Make remotes empty to simplify code if (remotes == null) remotes = new Vector>(); @@ -754,26 +866,39 @@ private Pair parseConnectionOptions(Connection connDef } i++; } + return Pair.create(conn, connections); } - private void checkRedirectParameters(VpnProfile np, Vector> defgw) { - for (Vector redirect : defgw) - for (int i = 1; i < redirect.size(); i++) { - if (redirect.get(i).equals("block-local")) - np.mAllowLocalLAN = false; - else if (redirect.get(i).equals("unblock-local")) - np.mAllowLocalLAN = true; - } + private void checkRedirectParameters(VpnProfile np, Vector> defgw, boolean defaultRoute) { + + boolean noIpv4 = false; + if (defaultRoute) + + for (Vector redirect : defgw) + for (int i = 1; i < redirect.size(); i++) { + if (redirect.get(i).equals("block-local")) + np.mAllowLocalLAN = false; + else if (redirect.get(i).equals("unblock-local")) + np.mAllowLocalLAN = true; + else if (redirect.get(i).equals("!ipv4")) + noIpv4 = true; + else if (redirect.get(i).equals("ipv6")) + np.mUseDefaultRoutev6 = true; + } + if (defaultRoute && !noIpv4) + np.mUseDefaultRoute = true; } private boolean isUdpProto(String proto) throws ConfigParseError { boolean isudp; - if (proto.equals("udp") || proto.equals("udp6")) + if (proto.equals("udp") || proto.equals("udp4") || proto.equals("udp6")) isudp = true; else if (proto.equals("tcp-client") || proto.equals("tcp") || + proto.equals("tcp4") || + proto.endsWith("tcp4-client") || proto.equals("tcp6") || proto.endsWith("tcp6-client")) isudp = false; @@ -782,25 +907,6 @@ else if (proto.equals("tcp-client") || return isudp; } - static public void useEmbbedUserAuth(VpnProfile np, String inlinedata) { - String data = VpnProfile.getEmbeddedContent(inlinedata); - String[] parts = data.split("\n"); - if (parts.length >= 2) { - np.mUsername = parts[0]; - np.mPassword = parts[1]; - } - } - - public static void removeCRLCustomOption(VpnProfile np) { - String lines[] = np.mCustomConfigOptions.split("\\r?\\n"); - Vector keeplines = new Vector<>(); - for (String l : lines) { - if (!l.startsWith("crl-verify ")) - keeplines.add(l); - } - np.mCustomConfigOptions = TextUtils.join("\n", keeplines); - } - private void checkIgnoreAndInvalidOptions(VpnProfile np) throws ConfigParseError { for (String option : unsupportedOptions) if (options.containsKey(option)) @@ -811,7 +917,16 @@ private void checkIgnoreAndInvalidOptions(VpnProfile np) throws ConfigParseError options.remove(option); - if (options.size() > 0) { + boolean customOptions=false; + for (Vector> option: options.values()) + { + for (Vector optionsline : option) { + if (!ignoreThisOption(optionsline)) { + customOptions = true; + } + } + } + if (customOptions) { np.mCustomConfigOptions = "# These options found in the config file do not map to config settings:\n" + np.mCustomConfigOptions; @@ -825,7 +940,6 @@ private void checkIgnoreAndInvalidOptions(VpnProfile np) throws ConfigParseError } } - boolean ignoreThisOption(Vector option) { for (String[] ignoreOption : ignoreOptionsWithArg) { @@ -850,10 +964,9 @@ private String getOptionStrings(Vector> option) { for (Vector optionsline : option) { if (!ignoreThisOption(optionsline)) { // Check if option had been inlined and inline again - if (optionsline.size() == 2 && "extra-certs".equals(optionsline.get(0)) ) { + if (optionsline.size() == 2 && + "extra-certs".equals(optionsline.get(0))) { custom += VpnProfile.insertFileData(optionsline.get(0), optionsline.get(1)); - - } else { for (String arg : optionsline) custom += VpnProfile.openVpnEscape(arg) + " "; @@ -864,7 +977,6 @@ private String getOptionStrings(Vector> option) { return custom; } - private void fixup(VpnProfile np) { if (np.mRemoteCN.equals(np.mServerName)) { np.mRemoteCN = ""; @@ -879,7 +991,6 @@ private Vector getOption(String option, int minarg, int maxarg) throws C return alloptions.lastElement(); } - private Vector> getAllOption(String option, int minarg, int maxarg) throws ConfigParseError { Vector> args = options.get(option); if (args == null) @@ -896,6 +1007,19 @@ private Vector> getAllOption(String option, int minarg, int maxar return args; } + enum linestate { + initial, + readin_single_quote, reading_quoted, reading_unquoted, done + } + + public static class ConfigParseError extends Exception { + private static final long serialVersionUID = -60L; + + public ConfigParseError(String msg) { + super(msg); + } + } + } diff --git a/app/src/main/java/de/blinkt/openvpn/core/Connection.java b/app/src/main/java/de/blinkt/openvpn/core/Connection.java index b10664c..df07189 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/Connection.java +++ b/app/src/main/java/de/blinkt/openvpn/core/Connection.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2014 Arne Schwabe + * Copyright (c) 2012-2016 Arne Schwabe * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt */ @@ -8,20 +8,37 @@ import android.text.TextUtils; import java.io.Serializable; +import java.util.Locale; public class Connection implements Serializable, Cloneable { - public String mServerName = "openvpn.blinkt.de"; + public String mServerName = "openvpn.example.com"; public String mServerPort = "1194"; public boolean mUseUdp = true; - public String mCustomConfiguration=""; - public boolean mUseCustomConfig=false; - public boolean mEnabled=true; + public String mCustomConfiguration = ""; + public boolean mUseCustomConfig = false; + public boolean mEnabled = true; + public int mConnectTimeout = 0; + public static final int CONNECTION_DEFAULT_TIMEOUT = 120; + public ProxyType mProxyType = ProxyType.NONE; + public String mProxyName = "proxy.example.com"; + public String mProxyPort = "8080"; + + public boolean mUseProxyAuth; + public String mProxyAuthUser = null; + public String mProxyAuthPassword = null; + + public enum ProxyType { + NONE, + HTTP, + SOCKS5, + ORBOT + } private static final long serialVersionUID = 92031902903829089L; - public String getConnectionBlock() { - String cfg=""; + public String getConnectionBlock(boolean isOpenVPN3) { + String cfg = ""; // Server Address cfg += "remote "; @@ -33,13 +50,34 @@ public String getConnectionBlock() { else cfg += " tcp-client\n"; + if (mConnectTimeout != 0) + cfg += String.format(Locale.US, " connect-timeout %d\n", mConnectTimeout); + + // OpenVPN 2.x manages proxy connection via management interface + if ((isOpenVPN3 || usesExtraProxyOptions()) && mProxyType == ProxyType.HTTP) + { + cfg+=String.format(Locale.US,"http-proxy %s %s\n", mProxyName, mProxyPort); + if (mUseProxyAuth) + cfg+=String.format(Locale.US, "\n%s\n%s\n\n", mProxyAuthUser, mProxyAuthPassword); + } + if (usesExtraProxyOptions() && mProxyType == ProxyType.SOCKS5) { + cfg+=String.format(Locale.US,"socks-proxy %s %s\n", mProxyName, mProxyPort); + } + if (!TextUtils.isEmpty(mCustomConfiguration) && mUseCustomConfig) { cfg += mCustomConfiguration; cfg += "\n"; } + + return cfg; } + public boolean usesExtraProxyOptions() { + return (mUseCustomConfig && mCustomConfiguration.contains("http-proxy-option ")); + } + + @Override public Connection clone() throws CloneNotSupportedException { return (Connection) super.clone(); @@ -48,4 +86,11 @@ public Connection clone() throws CloneNotSupportedException { public boolean isOnlyRemote() { return TextUtils.isEmpty(mCustomConfiguration) || !mUseCustomConfig; } + + public int getTimeout() { + if (mConnectTimeout <= 0) + return CONNECTION_DEFAULT_TIMEOUT; + else + return mConnectTimeout; + } } diff --git a/app/src/main/java/de/blinkt/openvpn/core/ConnectionStatus.java b/app/src/main/java/de/blinkt/openvpn/core/ConnectionStatus.java new file mode 100644 index 0000000..03d842e --- /dev/null +++ b/app/src/main/java/de/blinkt/openvpn/core/ConnectionStatus.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.core; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Created by arne on 08.11.16. + */ +public enum ConnectionStatus implements Parcelable { + LEVEL_CONNECTED, + LEVEL_VPNPAUSED, + LEVEL_CONNECTING_SERVER_REPLIED, + LEVEL_CONNECTING_NO_SERVER_REPLY_YET, + LEVEL_NONETWORK, + LEVEL_NOTCONNECTED, + LEVEL_START, + LEVEL_AUTH_FAILED, + LEVEL_WAITING_FOR_USER_INPUT, + UNKNOWN_LEVEL; + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(ordinal()); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public ConnectionStatus createFromParcel(Parcel in) { + return ConnectionStatus.values()[in.readInt()]; + } + + @Override + public ConnectionStatus[] newArray(int size) { + return new ConnectionStatus[size]; + } + }; +} diff --git a/app/src/main/java/de/blinkt/openvpn/core/DeviceStateReceiver.java b/app/src/main/java/de/blinkt/openvpn/core/DeviceStateReceiver.java index 7e2f801..d618c0f 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/DeviceStateReceiver.java +++ b/app/src/main/java/de/blinkt/openvpn/core/DeviceStateReceiver.java @@ -1,290 +1,300 @@ -/* - * Copyright (c) 2012-2014 Arne Schwabe - * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt - */ - -package de.blinkt.openvpn.core; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.net.NetworkInfo.State; -import android.os.Handler; -import android.preference.PreferenceManager; - -import java.util.LinkedList; - -import de.blinkt.openvpn.core.VpnStatus.ByteCountListener; -import ht.vpn.android.R; - -import static de.blinkt.openvpn.core.OpenVPNManagement.pauseReason; - -public class DeviceStateReceiver extends BroadcastReceiver implements ByteCountListener { - private final Handler mDisconnectHandler; - private int lastNetwork = -1; - private OpenVPNManagement mManagement; - - // Window time in s - private final int TRAFFIC_WINDOW = 60; - // Data traffic limit in bytes - private final long TRAFFIC_LIMIT = 64 * 1024; - - // Time to wait after network disconnect to pause the VPN - private final int DISCONNECT_WAIT = 20; - - - connectState network = connectState.DISCONNECTED; - connectState screen = connectState.SHOULDBECONNECTED; - connectState userpause = connectState.SHOULDBECONNECTED; - - private String lastStateMsg = null; - private Runnable mDelayDisconnectRunnable = new Runnable() { - @Override - public void run() { - if (!(network == connectState.PENDINGDISCONNECT)) - return; - - network = connectState.DISCONNECTED; - - // Set screen state to be disconnected if disconnect pending - if (screen == connectState.PENDINGDISCONNECT) - screen = connectState.DISCONNECTED; - - mManagement.pause(getPauseReason()); - } - }; - private NetworkInfo lastConnectedNetwork; - - enum connectState { - SHOULDBECONNECTED, - PENDINGDISCONNECT, - DISCONNECTED - } - - static class Datapoint { - private Datapoint(long t, long d) { - timestamp = t; - data = d; - } - - long timestamp; - long data; - } - - LinkedList trafficdata = new LinkedList(); - - - @Override - public void updateByteCount(long in, long out, long diffIn, long diffOut) { - if (screen != connectState.PENDINGDISCONNECT) - return; - - long total = diffIn + diffOut; - trafficdata.add(new Datapoint(System.currentTimeMillis(), total)); - - while (trafficdata.getFirst().timestamp <= (System.currentTimeMillis() - TRAFFIC_WINDOW * 1000)) { - trafficdata.removeFirst(); - } - - long windowtraffic = 0; - for (Datapoint dp : trafficdata) - windowtraffic += dp.data; - - if (windowtraffic < TRAFFIC_LIMIT) { - screen = connectState.DISCONNECTED; - VpnStatus.logInfo(R.string.screenoff_pause, - OpenVPNService.humanReadableByteCount(TRAFFIC_LIMIT, false), TRAFFIC_WINDOW); - - mManagement.pause(getPauseReason()); - } - } - - - public void userPause(boolean pause) { - if (pause) { - userpause = connectState.DISCONNECTED; - // Check if we should disconnect - mManagement.pause(getPauseReason()); - } else { - boolean wereConnected = shouldBeConnected(); - userpause = connectState.SHOULDBECONNECTED; - if (shouldBeConnected() && !wereConnected) - mManagement.resume(); - else - // Update the reason why we currently paused - mManagement.pause(getPauseReason()); - } - } - - public DeviceStateReceiver(OpenVPNManagement magnagement) { - super(); - mManagement = magnagement; - mDisconnectHandler = new Handler(); - } - - - @Override - public void onReceive(Context context, Intent intent) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - - - if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) { - networkStateChange(context); - } else if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) { - boolean screenOffPause = prefs.getBoolean("screenoff", false); - - if (screenOffPause) { - if (ProfileManager.getLastConnectedVpn() != null && !ProfileManager.getLastConnectedVpn().mPersistTun) - VpnStatus.logError(R.string.screen_nopersistenttun); - - screen = connectState.PENDINGDISCONNECT; - fillTrafficData(); - if (network == connectState.DISCONNECTED || userpause == connectState.DISCONNECTED) - screen = connectState.DISCONNECTED; - } - } else if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) { - // Network was disabled because screen off - boolean connected = shouldBeConnected(); - screen = connectState.SHOULDBECONNECTED; - - /* We should connect now, cancel any outstanding disconnect timer */ - mDisconnectHandler.removeCallbacks(mDelayDisconnectRunnable); - /* should be connected has changed because the screen is on now, connect the VPN */ - if (shouldBeConnected() != connected) - mManagement.resume(); - else if (!shouldBeConnected()) - /*Update the reason why we are still paused */ - mManagement.pause(getPauseReason()); - - } - } - - - private void fillTrafficData() { - trafficdata.add(new Datapoint(System.currentTimeMillis(), TRAFFIC_LIMIT)); - } - public static boolean equalsObj(Object a, Object b) { - return (a == null) ? (b == null) : a.equals(b); - } - - - - public void networkStateChange(Context context) { - NetworkInfo networkInfo = getCurrentNetworkInfo(context); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - boolean sendusr1 = prefs.getBoolean("netchangereconnect", true); - - - String netstatestring; - if (networkInfo == null) { - netstatestring = "not connected"; - } else { - String subtype = networkInfo.getSubtypeName(); - if (subtype == null) - subtype = ""; - String extrainfo = networkInfo.getExtraInfo(); - if (extrainfo == null) - extrainfo = ""; - - /* - if(networkInfo.getType()==android.net.ConnectivityManager.TYPE_WIFI) { - WifiManager wifiMgr = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); - WifiInfo wifiinfo = wifiMgr.getConnectionInfo(); - extrainfo+=wifiinfo.getBSSID(); - - subtype += wifiinfo.getNetworkId(); - }*/ - - - netstatestring = String.format("%2$s %4$s to %1$s %3$s", networkInfo.getTypeName(), - networkInfo.getDetailedState(), extrainfo, subtype); - } - - if (networkInfo != null && networkInfo.getState() == State.CONNECTED) { - int newnet = networkInfo.getType(); - - boolean pendingDisconnect = (network == connectState.PENDINGDISCONNECT); - network = connectState.SHOULDBECONNECTED; - - boolean sameNetwork; - if (lastConnectedNetwork == null - || lastConnectedNetwork.getType() != networkInfo.getType() - || !equalsObj(lastConnectedNetwork.getExtraInfo(), networkInfo.getExtraInfo()) - ) - sameNetwork = false; - else - sameNetwork = true; - - /* Same network, connection still 'established' */ - if (pendingDisconnect && sameNetwork) { - mDisconnectHandler.removeCallbacks(mDelayDisconnectRunnable); - // Reprotect the sockets just be sure - mManagement.networkChange(true); - } else { - /* Different network or connection not established anymore */ - - if (screen == connectState.PENDINGDISCONNECT) - screen = connectState.DISCONNECTED; - - if (shouldBeConnected()) { - mDisconnectHandler.removeCallbacks(mDelayDisconnectRunnable); - - if (pendingDisconnect) - mManagement.networkChange(false); - else - mManagement.resume(); - } - - lastNetwork = newnet; - lastConnectedNetwork = networkInfo; - } - } else if (networkInfo == null) { - // Not connected, stop openvpn, set last connected network to no network - lastNetwork = -1; - if (sendusr1) { - network = connectState.PENDINGDISCONNECT; - mDisconnectHandler.postDelayed(mDelayDisconnectRunnable, DISCONNECT_WAIT * 1000); - - } - } - - - if (!netstatestring.equals(lastStateMsg)) - VpnStatus.logInfo(R.string.netstatus, netstatestring); - lastStateMsg = netstatestring; - - } - - - public boolean isUserPaused() { - return userpause == connectState.DISCONNECTED; - } - - private boolean shouldBeConnected() { - return (screen == connectState.SHOULDBECONNECTED && userpause == connectState.SHOULDBECONNECTED && - network == connectState.SHOULDBECONNECTED); - } - - private pauseReason getPauseReason() { - if (userpause == connectState.DISCONNECTED) - return pauseReason.userPause; - - if (screen == connectState.DISCONNECTED) - return pauseReason.screenOff; - - if (network == connectState.DISCONNECTED) - return pauseReason.noNetwork; - - return pauseReason.userPause; - } - - private NetworkInfo getCurrentNetworkInfo(Context context) { - ConnectivityManager conn = (ConnectivityManager) - context.getSystemService(Context.CONNECTIVITY_SERVICE); - - return conn.getActiveNetworkInfo(); - } -} +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.core; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.NetworkInfo.State; +import android.os.Handler; +import android.preference.PreferenceManager; + +import ht.vpn.android.R; +import de.blinkt.openvpn.core.VpnStatus.ByteCountListener; + +import java.util.LinkedList; +import java.util.Objects; +import java.util.StringTokenizer; + +import static de.blinkt.openvpn.core.OpenVPNManagement.pauseReason; + +public class DeviceStateReceiver extends BroadcastReceiver implements ByteCountListener, OpenVPNManagement.PausedStateCallback { + private final Handler mDisconnectHandler; + private int lastNetwork = -1; + private OpenVPNManagement mManagement; + + // Window time in s + private final int TRAFFIC_WINDOW = 60; + // Data traffic limit in bytes + private final long TRAFFIC_LIMIT = 64 * 1024; + + // Time to wait after network disconnect to pause the VPN + private final int DISCONNECT_WAIT = 20; + + + connectState network = connectState.DISCONNECTED; + connectState screen = connectState.SHOULDBECONNECTED; + connectState userpause = connectState.SHOULDBECONNECTED; + + private String lastStateMsg = null; + private java.lang.Runnable mDelayDisconnectRunnable = new Runnable() { + @Override + public void run() { + if (!(network == connectState.PENDINGDISCONNECT)) + return; + + network = connectState.DISCONNECTED; + + // Set screen state to be disconnected if disconnect pending + if (screen == connectState.PENDINGDISCONNECT) + screen = connectState.DISCONNECTED; + + mManagement.pause(getPauseReason()); + } + }; + private NetworkInfo lastConnectedNetwork; + + @Override + public boolean shouldBeRunning() { + return shouldBeConnected(); + } + + private enum connectState { + SHOULDBECONNECTED, + PENDINGDISCONNECT, + DISCONNECTED + } + + private static class Datapoint { + private Datapoint(long t, long d) { + timestamp = t; + data = d; + } + + long timestamp; + long data; + } + + private LinkedList trafficdata = new LinkedList<>(); + + + @Override + public void updateByteCount(long in, long out, long diffIn, long diffOut) { + if (screen != connectState.PENDINGDISCONNECT) + return; + + long total = diffIn + diffOut; + trafficdata.add(new Datapoint(System.currentTimeMillis(), total)); + + while (trafficdata.getFirst().timestamp <= (System.currentTimeMillis() - TRAFFIC_WINDOW * 1000)) { + trafficdata.removeFirst(); + } + + long windowtraffic = 0; + for (Datapoint dp : trafficdata) + windowtraffic += dp.data; + + if (windowtraffic < TRAFFIC_LIMIT) { + screen = connectState.DISCONNECTED; + VpnStatus.logInfo(R.string.screenoff_pause, + "64 kB", TRAFFIC_WINDOW); + + mManagement.pause(getPauseReason()); + } + } + + + public void userPause(boolean pause) { + if (pause) { + userpause = connectState.DISCONNECTED; + // Check if we should disconnect + mManagement.pause(getPauseReason()); + } else { + boolean wereConnected = shouldBeConnected(); + userpause = connectState.SHOULDBECONNECTED; + if (shouldBeConnected() && !wereConnected) + mManagement.resume(); + else + // Update the reason why we currently paused + mManagement.pause(getPauseReason()); + } + } + + public DeviceStateReceiver(OpenVPNManagement magnagement) { + super(); + mManagement = magnagement; + mManagement.setPauseCallback(this); + mDisconnectHandler = new Handler(); + } + + + @Override + public void onReceive(Context context, Intent intent) { + SharedPreferences prefs = Preferences.getDefaultSharedPreferences(context); + + + if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) { + networkStateChange(context); + } else if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) { + boolean screenOffPause = prefs.getBoolean("screenoff", false); + + if (screenOffPause) { + if (ProfileManager.getLastConnectedVpn() != null && !ProfileManager.getLastConnectedVpn().mPersistTun) + VpnStatus.logError(R.string.screen_nopersistenttun); + + screen = connectState.PENDINGDISCONNECT; + fillTrafficData(); + if (network == connectState.DISCONNECTED || userpause == connectState.DISCONNECTED) + screen = connectState.DISCONNECTED; + } + } else if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) { + // Network was disabled because screen off + boolean connected = shouldBeConnected(); + screen = connectState.SHOULDBECONNECTED; + + /* We should connect now, cancel any outstanding disconnect timer */ + mDisconnectHandler.removeCallbacks(mDelayDisconnectRunnable); + /* should be connected has changed because the screen is on now, connect the VPN */ + if (shouldBeConnected() != connected) + mManagement.resume(); + else if (!shouldBeConnected()) + /*Update the reason why we are still paused */ + mManagement.pause(getPauseReason()); + + } + } + + + private void fillTrafficData() { + trafficdata.add(new Datapoint(System.currentTimeMillis(), TRAFFIC_LIMIT)); + } + + public static boolean equalsObj(Object a, Object b) { + return (a == null) ? (b == null) : a.equals(b); + } + + + public void networkStateChange(Context context) { + NetworkInfo networkInfo = getCurrentNetworkInfo(context); + SharedPreferences prefs = Preferences.getDefaultSharedPreferences(context); + boolean sendusr1 = prefs.getBoolean("netchangereconnect", true); + + + String netstatestring; + if (networkInfo == null) { + netstatestring = "not connected"; + } else { + String subtype = networkInfo.getSubtypeName(); + if (subtype == null) + subtype = ""; + String extrainfo = networkInfo.getExtraInfo(); + if (extrainfo == null) + extrainfo = ""; + + /* + if(networkInfo.getType()==android.net.ConnectivityManager.TYPE_WIFI) { + WifiManager wifiMgr = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + WifiInfo wifiinfo = wifiMgr.getConnectionInfo(); + extrainfo+=wifiinfo.getBSSID(); + + subtype += wifiinfo.getNetworkId(); + }*/ + + + netstatestring = String.format("%2$s %4$s to %1$s %3$s", networkInfo.getTypeName(), + networkInfo.getDetailedState(), extrainfo, subtype); + } + + if (networkInfo != null && networkInfo.getState() == State.CONNECTED) { + int newnet = networkInfo.getType(); + + boolean pendingDisconnect = (network == connectState.PENDINGDISCONNECT); + network = connectState.SHOULDBECONNECTED; + + boolean sameNetwork; + if (lastConnectedNetwork == null + || lastConnectedNetwork.getType() != networkInfo.getType() + || !equalsObj(lastConnectedNetwork.getExtraInfo(), networkInfo.getExtraInfo()) + ) + sameNetwork = false; + else + sameNetwork = true; + + /* Same network, connection still 'established' */ + if (pendingDisconnect && sameNetwork) { + mDisconnectHandler.removeCallbacks(mDelayDisconnectRunnable); + // Reprotect the sockets just be sure + mManagement.networkChange(true); + } else { + /* Different network or connection not established anymore */ + + if (screen == connectState.PENDINGDISCONNECT) + screen = connectState.DISCONNECTED; + + if (shouldBeConnected()) { + mDisconnectHandler.removeCallbacks(mDelayDisconnectRunnable); + + if (pendingDisconnect || !sameNetwork) + mManagement.networkChange(sameNetwork); + else + mManagement.resume(); + } + + lastNetwork = newnet; + lastConnectedNetwork = networkInfo; + } + } else if (networkInfo == null) { + // Not connected, stop openvpn, set last connected network to no network + lastNetwork = -1; + if (sendusr1) { + network = connectState.PENDINGDISCONNECT; + mDisconnectHandler.postDelayed(mDelayDisconnectRunnable, DISCONNECT_WAIT * 1000); + + } + } + + + if (!netstatestring.equals(lastStateMsg)) + VpnStatus.logInfo(R.string.netstatus, netstatestring); + VpnStatus.logDebug(String.format("Debug state info: %s, pause: %s, shouldbeconnected: %s, network: %s ", + netstatestring, getPauseReason(), shouldBeConnected(), network)); + lastStateMsg = netstatestring; + + } + + + public boolean isUserPaused() { + return userpause == connectState.DISCONNECTED; + } + + private boolean shouldBeConnected() { + return (screen == connectState.SHOULDBECONNECTED && userpause == connectState.SHOULDBECONNECTED && + network == connectState.SHOULDBECONNECTED); + } + + private pauseReason getPauseReason() { + if (userpause == connectState.DISCONNECTED) + return pauseReason.userPause; + + if (screen == connectState.DISCONNECTED) + return pauseReason.screenOff; + + if (network == connectState.DISCONNECTED) + return pauseReason.noNetwork; + + return pauseReason.userPause; + } + + private NetworkInfo getCurrentNetworkInfo(Context context) { + ConnectivityManager conn = (ConnectivityManager) + context.getSystemService(Context.CONNECTIVITY_SERVICE); + + return conn.getActiveNetworkInfo(); + } +} diff --git a/app/src/main/java/de/blinkt/openvpn/core/ExtAuthHelper.java b/app/src/main/java/de/blinkt/openvpn/core/ExtAuthHelper.java new file mode 100644 index 0000000..05f83f9 --- /dev/null +++ b/app/src/main/java/de/blinkt/openvpn/core/ExtAuthHelper.java @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2012-2018 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.core; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.*; +import android.security.KeyChainException; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; +import android.widget.ArrayAdapter; +import android.widget.Spinner; +import android.widget.SpinnerAdapter; + +import java.io.ByteArrayInputStream; +import java.io.Closeable; +import java.io.UnsupportedEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import ht.vpn.android.api.ExternalCertificateProvider; + +public class ExtAuthHelper { + + public static final String ACTION_CERT_CONFIGURATION = "de.blinkt.openvpn.api.ExternalCertificateConfiguration"; + public static final String ACTION_CERT_PROVIDER = "de.blinkt.openvpn.api.ExternalCertificateProvider"; + + public static final String EXTRA_ALIAS = "de.blinkt.openvpn.api.KEY_ALIAS"; + public static final String EXTRA_DESCRIPTION = "de.blinkt.openvpn.api.KEY_DESCRIPTION"; + + + public static void setExternalAuthProviderSpinnerList(Spinner spinner, String selectedApp) { + Context c = spinner.getContext(); + final PackageManager pm = c.getPackageManager(); + ArrayList extProviders = getExternalAuthProviderList(c); + + int selectedPos = -1; + + if (extProviders.size() ==0) + { + selectedApp = ""; + ExternalAuthProvider noauthprovider = new ExternalAuthProvider(); + noauthprovider.label = "No external auth provider found"; + noauthprovider.packageName = selectedApp; + noauthprovider.configurable = false; + extProviders.add(noauthprovider); + } + + + for (int i = 0; i < extProviders.size(); i++) { + if (extProviders.get(i).packageName.equals(selectedApp)) + selectedPos = i; + } + SpinnerAdapter extAppAdapter = new ArrayAdapter(c, android.R.layout.simple_spinner_item, android.R.id.text1, extProviders); + spinner.setAdapter(extAppAdapter); + if (selectedPos != -1) + spinner.setSelection(selectedPos); + } + + static ArrayList getExternalAuthProviderList(Context c) { + Intent configureExtAuth = new Intent(ACTION_CERT_CONFIGURATION); + + final PackageManager packageManager = c.getPackageManager(); + List configureList = + packageManager.queryIntentActivities(configureExtAuth, 0); + + Intent serviceExtAuth = new Intent(ACTION_CERT_PROVIDER); + + List serviceList = + packageManager.queryIntentServices(serviceExtAuth, 0); + + + // For now only list those who appear in both lists + + ArrayList providers = new ArrayList(); + + for (ResolveInfo service : serviceList) { + ExternalAuthProvider ext = new ExternalAuthProvider(); + ext.packageName = service.serviceInfo.packageName; + + ext.label = (String) service.serviceInfo.applicationInfo.loadLabel(packageManager); + + for (ResolveInfo activity : configureList) { + if (service.serviceInfo.packageName.equals(activity.activityInfo.packageName)) { + ext.configurable = true; + } + } + providers.add(ext); + + } + return providers; + + } + + @Nullable + @WorkerThread + public static byte[] signData(@NonNull Context context, + @NonNull String extAuthPackageName, + @NonNull String alias, + @NonNull byte[] data + ) throws KeyChainException, InterruptedException + + { + + + try (ExternalAuthProviderConnection authProviderConnection = bindToExtAuthProvider(context.getApplicationContext(), extAuthPackageName)) { + ExternalCertificateProvider externalAuthProvider = authProviderConnection.getService(); + return externalAuthProvider.getSignedData(alias, data); + + } catch (RemoteException e) { + throw new KeyChainException(e); + } + } + + @Nullable + @WorkerThread + public static X509Certificate[] getCertificateChain(@NonNull Context context, + @NonNull String extAuthPackageName, + @NonNull String alias) throws KeyChainException { + + final byte[] certificateBytes; + try (ExternalAuthProviderConnection authProviderConnection = bindToExtAuthProvider(context.getApplicationContext(), extAuthPackageName)) { + ExternalCertificateProvider externalAuthProvider = authProviderConnection.getService(); + certificateBytes = externalAuthProvider.getCertificateChain(alias); + if (certificateBytes == null) { + return null; + } + Collection chain = toCertificates(certificateBytes); + return chain.toArray(new X509Certificate[chain.size()]); + + } catch (RemoteException | RuntimeException | InterruptedException e) { + throw new KeyChainException(e); + } + } + + public static Bundle getCertificateMetaData(@NonNull Context context, + @NonNull String extAuthPackageName, + String alias) throws KeyChainException + { + try (ExternalAuthProviderConnection authProviderConnection = bindToExtAuthProvider(context.getApplicationContext(), extAuthPackageName)) { + ExternalCertificateProvider externalAuthProvider = authProviderConnection.getService(); + return externalAuthProvider.getCertificateMetaData(alias); + + } catch (RemoteException | RuntimeException | InterruptedException e) { + throw new KeyChainException(e); + } + } + + public static Collection toCertificates(@NonNull byte[] bytes) { + final String BEGINCERT = "-----BEGIN CERTIFICATE-----"; + try { + Vector retCerts = new Vector<>(); + // Java library is broken, although the javadoc says it will extract all certificates from a byte array + // it only extracts the first one + String allcerts = new String(bytes, "iso8859-1"); + String[] certstrings = allcerts.split(BEGINCERT); + for (String certstring: certstrings) { + certstring = BEGINCERT + certstring; + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + retCerts.addAll((Collection) certFactory.generateCertificates( + new ByteArrayInputStream((certstring.getBytes("iso8859-1"))))); + + } + return retCerts; + + } catch (CertificateException e) { + throw new AssertionError(e); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } + } + + // adapted form Keychain + @WorkerThread + public static ExternalAuthProviderConnection bindToExtAuthProvider(@NonNull Context context, String packagename) throws KeyChainException, InterruptedException { + ensureNotOnMainThread(context); + final BlockingQueue q = new LinkedBlockingQueue<>(1); + ServiceConnection extAuthServiceConnection = new ServiceConnection() { + volatile boolean mConnectedAtLeastOnce = false; + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + if (!mConnectedAtLeastOnce) { + mConnectedAtLeastOnce = true; + try { + q.put(ExternalCertificateProvider.Stub.asInterface(service)); + } catch (InterruptedException e) { + // will never happen, since the queue starts with one available slot + } + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + } + }; + Intent intent = new Intent(ACTION_CERT_PROVIDER); + intent.setPackage(packagename); + + if (!context.bindService(intent, extAuthServiceConnection, Context.BIND_AUTO_CREATE)) { + throw new KeyChainException("could not bind to external authticator app: " + packagename); + } + return new ExternalAuthProviderConnection(context, extAuthServiceConnection, q.take()); + } + + private static void ensureNotOnMainThread(@NonNull Context context) { + Looper looper = Looper.myLooper(); + if (looper != null && looper == context.getMainLooper()) { + throw new IllegalStateException( + "calling this from your main thread can lead to deadlock"); + } + } + + public static class ExternalAuthProvider { + + public String packageName; + public boolean configurable = false; + private String label; + + @Override + public String toString() { + return label; + } + } + + public static class ExternalAuthProviderConnection implements Closeable { + private final Context context; + private final ServiceConnection serviceConnection; + private final ExternalCertificateProvider service; + + protected ExternalAuthProviderConnection(Context context, + ServiceConnection serviceConnection, + ExternalCertificateProvider service) { + this.context = context; + this.serviceConnection = serviceConnection; + this.service = service; + } + + @Override + public void close() { + context.unbindService(serviceConnection); + } + + public ExternalCertificateProvider getService() { + return service; + } + } +} diff --git a/app/src/main/java/de/blinkt/openvpn/core/ICSOpenVPNApplication.java b/app/src/main/java/de/blinkt/openvpn/core/ICSOpenVPNApplication.java new file mode 100644 index 0000000..845fef1 --- /dev/null +++ b/app/src/main/java/de/blinkt/openvpn/core/ICSOpenVPNApplication.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.core; + +import android.annotation.TargetApi; +import android.app.Application; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.graphics.Color; +import android.os.Build; + +import android.os.StrictMode; +import ht.vpn.android.BuildConfig; +import ht.vpn.android.R; + +public class ICSOpenVPNApplication extends Application { + private StatusListener mStatus; + + @Override + public void onCreate() { + if("robolectric".equals(Build.FINGERPRINT)) + return; + + super.onCreate(); + PRNGFixes.apply(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + createNotificationChannels(); + mStatus = new StatusListener(); + mStatus.init(getApplicationContext()); + } + + @TargetApi(Build.VERSION_CODES.O) + private void createNotificationChannels() { + NotificationManager mNotificationManager = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + + // Background message + CharSequence name = getString(R.string.channel_name_background); + NotificationChannel mChannel = new NotificationChannel(OpenVPNService.NOTIFICATION_CHANNEL_BG_ID, + name, NotificationManager.IMPORTANCE_MIN); + + mChannel.setDescription(getString(R.string.channel_description_background)); + mChannel.enableLights(false); + + mChannel.setLightColor(Color.DKGRAY); + mNotificationManager.createNotificationChannel(mChannel); + + // Connection status change messages + + name = getString(R.string.channel_name_status); + mChannel = new NotificationChannel(OpenVPNService.NOTIFICATION_CHANNEL_NEWSTATUS_ID, + name, NotificationManager.IMPORTANCE_LOW); + + mChannel.setDescription(getString(R.string.channel_description_status)); + mChannel.enableLights(true); + + mChannel.setLightColor(Color.BLUE); + mNotificationManager.createNotificationChannel(mChannel); + + + // Urgent requests, e.g. two factor auth + name = getString(R.string.channel_name_userreq); + mChannel = new NotificationChannel(OpenVPNService.NOTIFICATION_CHANNEL_USERREQ_ID, + name, NotificationManager.IMPORTANCE_HIGH); + mChannel.setDescription(getString(R.string.channel_description_userreq)); + mChannel.enableVibration(true); + mChannel.setLightColor(Color.CYAN); + mNotificationManager.createNotificationChannel(mChannel); + } +} diff --git a/app/src/main/java/de/blinkt/openvpn/core/LogFileHandler.java b/app/src/main/java/de/blinkt/openvpn/core/LogFileHandler.java new file mode 100644 index 0000000..42ec3a5 --- /dev/null +++ b/app/src/main/java/de/blinkt/openvpn/core/LogFileHandler.java @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2012-2015 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.core; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.util.Locale; + +import ht.vpn.android.R; + +/** + * Created by arne on 23.01.16. + */ +class LogFileHandler extends Handler { + static final int TRIM_LOG_FILE = 100; + static final int FLUSH_TO_DISK = 101; + static final int LOG_INIT = 102; + public static final int LOG_MESSAGE = 103; + public static final int MAGIC_BYTE = 0x55; + protected OutputStream mLogFile; + + public static final String LOGFILE_NAME = "logcache.dat"; + + + public LogFileHandler(Looper looper) { + super(looper); + } + + + @Override + public void handleMessage(Message msg) { + try { + if (msg.what == LOG_INIT) { + if (mLogFile != null) + throw new RuntimeException("mLogFile not null"); + readLogCache((File) msg.obj); + openLogFile((File) msg.obj); + } else if (msg.what == LOG_MESSAGE && msg.obj instanceof LogItem) { + // Ignore log messages if not yet initialized + if (mLogFile == null) + return; + writeLogItemToDisk((LogItem) msg.obj); + } else if (msg.what == TRIM_LOG_FILE) { + trimLogFile(); + for (LogItem li : VpnStatus.getlogbuffer()) + writeLogItemToDisk(li); + } else if (msg.what == FLUSH_TO_DISK) { + flushToDisk(); + } + + } catch (IOException | BufferOverflowException e) { + e.printStackTrace(); + VpnStatus.logError("Error during log cache: " + msg.what); + VpnStatus.logException(e); + } + + } + + private void flushToDisk() throws IOException { + mLogFile.flush(); + } + + private void trimLogFile() { + try { + mLogFile.flush(); + ((FileOutputStream) mLogFile).getChannel().truncate(0); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void writeLogItemToDisk(LogItem li) throws IOException { + + // We do not really care if the log cache breaks between Android upgrades, + // write binary format to disc + + byte[] liBytes = li.getMarschaledBytes(); + + writeEscapedBytes(liBytes); + } + + public void writeEscapedBytes(byte[] bytes) throws IOException { + int magic = 0; + for (byte b : bytes) + if (b == MAGIC_BYTE || b == MAGIC_BYTE + 1) + magic++; + + byte eBytes[] = new byte[bytes.length + magic]; + + int i = 0; + for (byte b : bytes) { + if (b == MAGIC_BYTE || b == MAGIC_BYTE + 1) { + eBytes[i++] = MAGIC_BYTE + 1; + eBytes[i++] = (byte) (b - MAGIC_BYTE); + } else { + eBytes[i++] = b; + } + } + + byte[] lenBytes = ByteBuffer.allocate(4).putInt(bytes.length).array(); + synchronized (mLogFile) { + mLogFile.write(MAGIC_BYTE); + mLogFile.write(lenBytes); + mLogFile.write(eBytes); + } + } + + private void openLogFile(File cacheDir) throws FileNotFoundException { + File logfile = new File(cacheDir, LOGFILE_NAME); + mLogFile = new FileOutputStream(logfile); + } + + private void readLogCache(File cacheDir) { + try { + File logfile = new File(cacheDir, LOGFILE_NAME); + + + if (!logfile.exists() || !logfile.canRead()) + return; + + FileInputStream log = new FileInputStream(logfile); + readCacheContents(log); + log.close(); + + } catch (java.io.IOException | java.lang.RuntimeException e) { + VpnStatus.logError("Reading cached logfile failed"); + VpnStatus.logException(e); + e.printStackTrace(); + // ignore reading file error + } finally { + synchronized (VpnStatus.readFileLock) { + VpnStatus.readFileLog = true; + VpnStatus.readFileLock.notifyAll(); + } + } + } + + + protected void readCacheContents(InputStream in) throws IOException { + BufferedInputStream logFile = new BufferedInputStream(in); + + byte[] buf = new byte[16384]; + int read = logFile.read(buf, 0, 5); + int itemsRead = 0; + + + readloop: + while (read >= 5) { + int skipped = 0; + while (buf[skipped] != MAGIC_BYTE) { + skipped++; + if (!(logFile.read(buf, skipped + 4, 1) == 1) || skipped + 10 > buf.length) { + VpnStatus.logDebug(String.format(Locale.US, "Skipped %d bytes and no a magic byte found", skipped)); + break readloop; + } + } + if (skipped > 0) + VpnStatus.logDebug(String.format(Locale.US, "Skipped %d bytes before finding a magic byte", skipped)); + + int len = ByteBuffer.wrap(buf, skipped + 1, 4).asIntBuffer().get(); + + // Marshalled LogItem + int pos = 0; + byte buf2[] = new byte[buf.length]; + + while (pos < len) { + byte b = (byte) logFile.read(); + if (b == MAGIC_BYTE) { + VpnStatus.logDebug(String.format(Locale.US, "Unexpected magic byte found at pos %d, abort current log item", pos)); + read = logFile.read(buf, 1, 4) + 1; + continue readloop; + } else if (b == MAGIC_BYTE + 1) { + b = (byte) logFile.read(); + if (b == 0) + b = MAGIC_BYTE; + else if (b == 1) + b = MAGIC_BYTE + 1; + else { + VpnStatus.logDebug(String.format(Locale.US, "Escaped byte not 0 or 1: %d", b)); + read = logFile.read(buf, 1, 4) + 1; + continue readloop; + } + } + buf2[pos++] = b; + } + + restoreLogItem(buf2, len); + + //Next item + read = logFile.read(buf, 0, 5); + itemsRead++; + if (itemsRead > 2 * VpnStatus.MAXLOGENTRIES) { + VpnStatus.logError("Too many logentries read from cache, aborting."); + read = 0; + } + + } + VpnStatus.logDebug(R.string.reread_log, itemsRead); + } + + protected void restoreLogItem(byte[] buf, int len) throws UnsupportedEncodingException { + + LogItem li = new LogItem(buf, len); + if (li.verify()) { + VpnStatus.newLogItem(li, true); + } else { + VpnStatus.logError(String.format(Locale.getDefault(), + "Could not read log item from file: %d: %s", + len, bytesToHex(buf, Math.max(len, 80)))); + } + } + + private final static char[] hexArray = "0123456789ABCDEF".toCharArray(); + + public static String bytesToHex(byte[] bytes, int len) { + len = Math.min(bytes.length, len); + char[] hexChars = new char[len * 2]; + for (int j = 0; j < len; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + + +} diff --git a/app/src/main/java/de/blinkt/openvpn/core/LogItem.java b/app/src/main/java/de/blinkt/openvpn/core/LogItem.java new file mode 100644 index 0000000..ec157e9 --- /dev/null +++ b/app/src/main/java/de/blinkt/openvpn/core/LogItem.java @@ -0,0 +1,375 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.core; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.Signature; +import android.os.Parcel; +import android.os.Parcelable; + +import java.io.ByteArrayInputStream; +import java.io.UnsupportedEncodingException; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.FormatFlagsConversionMismatchException; +import java.util.Locale; +import java.util.UnknownFormatConversionException; + +import ht.vpn.android.R; + +/** + * Created by arne on 24.04.16. + */ +public class LogItem implements Parcelable { + private Object[] mArgs = null; + private String mMessage = null; + private int mRessourceId; + // Default log priority + VpnStatus.LogLevel mLevel = VpnStatus.LogLevel.INFO; + private long logtime = System.currentTimeMillis(); + private int mVerbosityLevel = -1; + + private LogItem(int ressourceId, Object[] args) { + mRessourceId = ressourceId; + mArgs = args; + } + + public LogItem(VpnStatus.LogLevel level, int verblevel, String message) { + mMessage = message; + mLevel = level; + mVerbosityLevel = verblevel; + } + + @Override + public int describeContents() { + return 0; + } + + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeArray(mArgs); + dest.writeString(mMessage); + dest.writeInt(mRessourceId); + dest.writeInt(mLevel.getInt()); + dest.writeInt(mVerbosityLevel); + + dest.writeLong(logtime); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof LogItem)) + return obj.equals(this); + LogItem other = (LogItem) obj; + + return Arrays.equals(mArgs, other.mArgs) && + ((other.mMessage == null && mMessage == other.mMessage) || + mMessage.equals(other.mMessage)) && + mRessourceId == other.mRessourceId && + ((mLevel == null && other.mLevel == mLevel) || + other.mLevel.equals(mLevel)) && + mVerbosityLevel == other.mVerbosityLevel && + logtime == other.logtime; + + + } + + public byte[] getMarschaledBytes() throws UnsupportedEncodingException, BufferOverflowException { + ByteBuffer bb = ByteBuffer.allocate(16384); + + + bb.put((byte) 0x0); //version + bb.putLong(logtime); //8 + bb.putInt(mVerbosityLevel); //4 + bb.putInt(mLevel.getInt()); + bb.putInt(mRessourceId); + if (mMessage == null || mMessage.length() == 0) { + bb.putInt(0); + } else { + marschalString(mMessage, bb); + } + if (mArgs == null || mArgs.length == 0) { + bb.putInt(0); + } else { + bb.putInt(mArgs.length); + for (Object o : mArgs) { + if (o instanceof String) { + bb.putChar('s'); + marschalString((String) o, bb); + } else if (o instanceof Integer) { + bb.putChar('i'); + bb.putInt((Integer) o); + } else if (o instanceof Float) { + bb.putChar('f'); + bb.putFloat((Float) o); + } else if (o instanceof Double) { + bb.putChar('d'); + bb.putDouble((Double) o); + } else if (o instanceof Long) { + bb.putChar('l'); + bb.putLong((Long) o); + } else if (o == null) { + bb.putChar('0'); + } else { + VpnStatus.logDebug("Unknown object for LogItem marschaling " + o); + bb.putChar('s'); + marschalString(o.toString(), bb); + } + + } + } + + int pos = bb.position(); + bb.rewind(); + return Arrays.copyOf(bb.array(), pos); + + } + + public LogItem(byte[] in, int length) throws UnsupportedEncodingException { + ByteBuffer bb = ByteBuffer.wrap(in, 0, length); + bb.get(); // ignore version + logtime = bb.getLong(); + mVerbosityLevel = bb.getInt(); + mLevel = VpnStatus.LogLevel.getEnumByValue(bb.getInt()); + mRessourceId = bb.getInt(); + int len = bb.getInt(); + if (len == 0) { + mMessage = null; + } else { + if (len > bb.remaining()) + throw new IndexOutOfBoundsException("String length " + len + " is bigger than remaining bytes " + bb.remaining()); + byte[] utf8bytes = new byte[len]; + bb.get(utf8bytes); + mMessage = new String(utf8bytes, "UTF-8"); + } + int numArgs = bb.getInt(); + if (numArgs > 30) { + throw new IndexOutOfBoundsException("Too many arguments for Logitem to unmarschal"); + } + if (numArgs == 0) { + mArgs = null; + } else { + mArgs = new Object[numArgs]; + for (int i = 0; i < numArgs; i++) { + char type = bb.getChar(); + switch (type) { + case 's': + mArgs[i] = unmarschalString(bb); + break; + case 'i': + mArgs[i] = bb.getInt(); + break; + case 'd': + mArgs[i] = bb.getDouble(); + break; + case 'f': + mArgs[i] = bb.getFloat(); + break; + case 'l': + mArgs[i] = bb.getLong(); + break; + case '0': + mArgs[i] = null; + break; + default: + throw new UnsupportedEncodingException("Unknown format type: " + type); + } + } + } + if (bb.hasRemaining()) + throw new UnsupportedEncodingException(bb.remaining() + " bytes left after unmarshaling everything"); + } + + private void marschalString(String str, ByteBuffer bb) throws UnsupportedEncodingException { + byte[] utf8bytes = str.getBytes("UTF-8"); + bb.putInt(utf8bytes.length); + bb.put(utf8bytes); + } + + private String unmarschalString(ByteBuffer bb) throws UnsupportedEncodingException { + int len = bb.getInt(); + byte[] utf8bytes = new byte[len]; + bb.get(utf8bytes); + return new String(utf8bytes, "UTF-8"); + } + + + public LogItem(Parcel in) { + mArgs = in.readArray(Object.class.getClassLoader()); + mMessage = in.readString(); + mRessourceId = in.readInt(); + mLevel = VpnStatus.LogLevel.getEnumByValue(in.readInt()); + mVerbosityLevel = in.readInt(); + logtime = in.readLong(); + } + + public static final Creator CREATOR + = new Creator() { + public LogItem createFromParcel(Parcel in) { + return new LogItem(in); + } + + public LogItem[] newArray(int size) { + return new LogItem[size]; + } + }; + + public LogItem(VpnStatus.LogLevel loglevel, int ressourceId, Object... args) { + mRessourceId = ressourceId; + mArgs = args; + mLevel = loglevel; + } + + + public LogItem(VpnStatus.LogLevel loglevel, String msg) { + mLevel = loglevel; + mMessage = msg; + } + + + public LogItem(VpnStatus.LogLevel loglevel, int ressourceId) { + mRessourceId = ressourceId; + mLevel = loglevel; + } + + public String getString(Context c) { + try { + if (mMessage != null) { + return mMessage; + } else { + if (c != null) { + if (mRessourceId == R.string.mobile_info) + return getMobileInfoString(c); + if (mArgs == null) + return c.getString(mRessourceId); + else + return c.getString(mRessourceId, mArgs); + } else { + String str = String.format(Locale.ENGLISH, "Log (no context) resid %d", mRessourceId); + if (mArgs != null) + str += join("|", mArgs); + + return str; + } + } + } catch (UnknownFormatConversionException e) { + if (c != null) + throw new UnknownFormatConversionException(e.getLocalizedMessage() + getString(null)); + else + throw e; + } catch (java.util.FormatFlagsConversionMismatchException e) { + if (c != null) + throw new FormatFlagsConversionMismatchException(e.getLocalizedMessage() + getString(null), e.getConversion()); + else + throw e; + } + + } + + + // TextUtils.join will cause not macked exeception in tests .... + public static String join(CharSequence delimiter, Object[] tokens) { + StringBuilder sb = new StringBuilder(); + boolean firstTime = true; + for (Object token : tokens) { + if (firstTime) { + firstTime = false; + } else { + sb.append(delimiter); + } + sb.append(token); + } + return sb.toString(); + } + + + public VpnStatus.LogLevel getLogLevel() { + return mLevel; + } + + + @Override + public String toString() { + return getString(null); + } + + // The lint is wrong here + @SuppressLint("StringFormatMatches") + private String getMobileInfoString(Context c) { + c.getPackageManager(); + String apksign = "error getting package signature"; + + String version = "error getting version"; + try { + @SuppressLint("PackageManagerGetSignatures") + Signature raw = c.getPackageManager().getPackageInfo(c.getPackageName(), PackageManager.GET_SIGNATURES).signatures[0]; + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + X509Certificate cert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(raw.toByteArray())); + MessageDigest md = MessageDigest.getInstance("SHA-1"); + byte[] der = cert.getEncoded(); + md.update(der); + byte[] digest = md.digest(); + + if (Arrays.equals(digest, VpnStatus.officalkey)) + apksign = c.getString(R.string.official_build); + else if (Arrays.equals(digest, VpnStatus.officaldebugkey)) + apksign = c.getString(R.string.debug_build); + else if (Arrays.equals(digest, VpnStatus.amazonkey)) + apksign = "amazon version"; + else if (Arrays.equals(digest, VpnStatus.fdroidkey)) + apksign = "F-Droid built and signed version"; + else + apksign = c.getString(R.string.built_by, cert.getSubjectX500Principal().getName()); + + PackageInfo packageinfo = c.getPackageManager().getPackageInfo(c.getPackageName(), 0); + version = packageinfo.versionName; + + } catch (PackageManager.NameNotFoundException | CertificateException | + NoSuchAlgorithmException ignored) { + } + + Object[] argsext = Arrays.copyOf(mArgs, mArgs.length); + argsext[argsext.length - 1] = apksign; + argsext[argsext.length - 2] = version; + + return c.getString(R.string.mobile_info, argsext); + + } + + public long getLogtime() { + return logtime; + } + + + public int getVerbosityLevel() { + if (mVerbosityLevel == -1) { + // Hack: + // For message not from OpenVPN, report the status level as log level + return mLevel.getInt(); + } + return mVerbosityLevel; + } + + public boolean verify() { + if (mLevel == null) + return false; + + if (mMessage == null && mRessourceId == 0) + return false; + + return true; + } +} diff --git a/app/src/main/java/de/blinkt/openvpn/core/LollipopDeviceStateListener.java b/app/src/main/java/de/blinkt/openvpn/core/LollipopDeviceStateListener.java index 440458e..04e4e8b 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/LollipopDeviceStateListener.java +++ b/app/src/main/java/de/blinkt/openvpn/core/LollipopDeviceStateListener.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2014 Arne Schwabe + * Copyright (c) 2012-2016 Arne Schwabe * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt */ diff --git a/app/src/main/java/de/blinkt/openvpn/core/NativeUtils.java b/app/src/main/java/de/blinkt/openvpn/core/NativeUtils.java index 42f5b85..e81bb55 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/NativeUtils.java +++ b/app/src/main/java/de/blinkt/openvpn/core/NativeUtils.java @@ -1,22 +1,49 @@ /* - * Copyright (c) 2012-2014 Arne Schwabe + * Copyright (c) 2012-2016 Arne Schwabe * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt */ package de.blinkt.openvpn.core; import android.os.Build; +import ht.vpn.android.BuildConfig; import java.security.InvalidKeyException; public class NativeUtils { - public static native byte[] rsasign(byte[] input,int pkey) throws InvalidKeyException; - public static native String[] getIfconfig() throws IllegalArgumentException; - static native void jniclose(int fdint); - - static { - System.loadLibrary("opvpnutil"); - if (Build.VERSION.SDK_INT== Build.VERSION_CODES.JELLY_BEAN) - System.loadLibrary("jbcrypto"); - } -} + public static native byte[] rsasign(byte[] input, int pkey, boolean pkcs1padding) throws InvalidKeyException; + + public static native String[] getIfconfig() throws IllegalArgumentException; + + static native void jniclose(int fdint); + + public static String getNativeAPI() + { + if (isRoboUnitTest()) + return "ROBO"; + else + return getJNIAPI(); + } + + private static native String getJNIAPI(); + + public static native String getOpenVPN2GitVersion(); + + public static native String getOpenVPN3GitVersion(); + + public final static int[] openSSLlengths = { + 16, 64, 256, 1024, 8 * 1024, 16 * 1024 + }; + + public static native double[] getOpenSSLSpeed(String algorithm, int testnum); + + static { + if (!isRoboUnitTest()) { + System.loadLibrary("opvpnutil"); + } + } + + public static boolean isRoboUnitTest() { + return "robolectric".equals(Build.FINGERPRINT); } + +} \ No newline at end of file diff --git a/app/src/main/java/de/blinkt/openvpn/core/NetworkSpace.java b/app/src/main/java/de/blinkt/openvpn/core/NetworkSpace.java index 26219e0..c0fe723 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/NetworkSpace.java +++ b/app/src/main/java/de/blinkt/openvpn/core/NetworkSpace.java @@ -1,15 +1,12 @@ /* - * Copyright (c) 2012-2014 Arne Schwabe + * Copyright (c) 2012-2016 Arne Schwabe * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt */ package de.blinkt.openvpn.core; import android.os.Build; -import android.support.annotation.NonNull; -import android.text.TextUtils; - -import junit.framework.Assert; +import androidx.annotation.NonNull; import java.math.BigInteger; import java.net.Inet6Address; @@ -21,12 +18,17 @@ import ht.vpn.android.BuildConfig; -public class NetworkSpace { +public class NetworkSpace { + static void assertTrue(boolean f) + { + if (!f) + throw new IllegalStateException(); + } - static class ipAddress implements Comparable { + static class IpAddress implements Comparable { private BigInteger netAddress; public int networkMask; private boolean included; @@ -37,11 +39,11 @@ static class ipAddress implements Comparable { /** * sorts the networks with following criteria: - * 1. compares first 1 of the network - * 2. smaller networks are returned as smaller + * 1. compares first 1 of the network + * 2. smaller networks are returned as smaller */ @Override - public int compareTo(@NonNull ipAddress another) { + public int compareTo(@NonNull IpAddress another) { int comp = getFirstAddress().compareTo(another.getFirstAddress()); if (comp != 0) return comp; @@ -58,27 +60,26 @@ else if (another.networkMask == networkMask) /** * Warning ignores the included integer * - * @param o - * the object to compare this instance with. + * @param o the object to compare this instance with. */ @Override public boolean equals(Object o) { - if (!(o instanceof ipAddress)) + if (!(o instanceof IpAddress)) return super.equals(o); - ipAddress on = (ipAddress) o; + IpAddress on = (IpAddress) o; return (networkMask == on.networkMask) && on.getFirstAddress().equals(getFirstAddress()); } - public ipAddress(CIDRIP ip, boolean include) { + public IpAddress(CIDRIP ip, boolean include) { included = include; netAddress = BigInteger.valueOf(ip.getInt()); networkMask = ip.len; isV4 = true; } - public ipAddress(Inet6Address address, int mask, boolean include) { + public IpAddress(Inet6Address address, int mask, boolean include) { networkMask = mask; included = include; @@ -92,15 +93,15 @@ public ipAddress(Inet6Address address, int mask, boolean include) { } public BigInteger getLastAddress() { - if(lastAddress ==null) + if (lastAddress == null) lastAddress = getMaskedAddress(true); return lastAddress; } public BigInteger getFirstAddress() { - if (firstAddress ==null) - firstAddress =getMaskedAddress(false); + if (firstAddress == null) + firstAddress = getMaskedAddress(false); return firstAddress; } @@ -129,12 +130,12 @@ private BigInteger getMaskedAddress(boolean one) { public String toString() { //String in = included ? "+" : "-"; if (isV4) - return String.format(Locale.US,"%s/%d", getIPv4Address(), networkMask); + return String.format(Locale.US, "%s/%d", getIPv4Address(), networkMask); else return String.format(Locale.US, "%s/%d", getIPv6Address(), networkMask); } - ipAddress(BigInteger baseAddress, int mask, boolean included, boolean isV4) { + IpAddress(BigInteger baseAddress, int mask, boolean included, boolean isV4) { this.netAddress = baseAddress; this.networkMask = mask; this.included = included; @@ -142,39 +143,55 @@ public String toString() { } - public ipAddress[] split() { - ipAddress firstHalf = new ipAddress(getFirstAddress(), networkMask + 1, included, isV4); - ipAddress secondHalf = new ipAddress(firstHalf.getLastAddress().add(BigInteger.ONE), networkMask + 1, included, isV4); - if (BuildConfig.DEBUG) Assert.assertTrue(secondHalf.getLastAddress().equals(getLastAddress())); - return new ipAddress[]{firstHalf, secondHalf}; + public IpAddress[] split() { + IpAddress firstHalf = new IpAddress(getFirstAddress(), networkMask + 1, included, isV4); + IpAddress secondHalf = new IpAddress(firstHalf.getLastAddress().add(BigInteger.ONE), networkMask + 1, included, isV4); + if (BuildConfig.DEBUG) + assertTrue(secondHalf.getLastAddress().equals(getLastAddress())); + return new IpAddress[]{firstHalf, secondHalf}; } String getIPv4Address() { if (BuildConfig.DEBUG) { - Assert.assertTrue (isV4); - Assert.assertTrue (netAddress.longValue() <= 0xffffffffl); - Assert.assertTrue (netAddress.longValue() >= 0); + assertTrue(isV4); + assertTrue(netAddress.longValue() <= 0xffffffffl); + assertTrue(netAddress.longValue() >= 0); } long ip = netAddress.longValue(); return String.format(Locale.US, "%d.%d.%d.%d", (ip >> 24) % 256, (ip >> 16) % 256, (ip >> 8) % 256, ip % 256); } String getIPv6Address() { - if (BuildConfig.DEBUG) Assert.assertTrue (!isV4); + if (BuildConfig.DEBUG) assertTrue(!isV4); BigInteger r = netAddress; - if (r.compareTo(BigInteger.ZERO)==0 && networkMask==0) - return "::"; - Vector parts = new Vector(); + String ipv6str = null; + boolean lastPart = true; + while (r.compareTo(BigInteger.ZERO) == 1) { - parts.add(0, String.format(Locale.US, "%x", r.mod(BigInteger.valueOf(0x10000)).longValue())); + + long part = r.mod(BigInteger.valueOf(0x10000)).longValue(); + if (ipv6str != null || part != 0) { + if (ipv6str == null && !lastPart) + ipv6str = ":"; + + if (lastPart) + ipv6str = String.format(Locale.US, "%x", part, ipv6str); + else + ipv6str = String.format(Locale.US, "%x:%s", part, ipv6str); + } + r = r.shiftRight(16); + lastPart = false; } + if (ipv6str == null) + return "::"; + - return TextUtils.join(":", parts); + return ipv6str; } - public boolean containsNet(ipAddress network) { + public boolean containsNet(IpAddress network) { // this.first >= net.first && this.last <= net.last BigInteger ourFirst = getFirstAddress(); BigInteger ourLast = getLastAddress(); @@ -182,19 +199,19 @@ public boolean containsNet(ipAddress network) { BigInteger netLast = network.getLastAddress(); boolean a = ourFirst.compareTo(netFirst) != 1; - boolean b = ourLast.compareTo(netLast) != -1; - return a && b; + boolean b = ourLast.compareTo(netLast) != -1; + return a && b; } } - TreeSet mIpAddresses = new TreeSet(); + TreeSet mIpAddresses = new TreeSet(); - public Collection getNetworks(boolean included) { - Vector ips = new Vector(); - for (ipAddress ip : mIpAddresses) { + public Collection getNetworks(boolean included) { + Vector ips = new Vector(); + for (IpAddress ip : mIpAddresses) { if (ip.included == included) ips.add(ip); } @@ -208,36 +225,36 @@ public void clear() { void addIP(CIDRIP cidrIp, boolean include) { - mIpAddresses.add(new ipAddress(cidrIp, include)); + mIpAddresses.add(new IpAddress(cidrIp, include)); } public void addIPSplit(CIDRIP cidrIp, boolean include) { - ipAddress newIP = new ipAddress(cidrIp, include); - ipAddress[] splitIps = newIP.split(); - for (ipAddress split: splitIps) + IpAddress newIP = new IpAddress(cidrIp, include); + IpAddress[] splitIps = newIP.split(); + for (IpAddress split : splitIps) mIpAddresses.add(split); } void addIPv6(Inet6Address address, int mask, boolean included) { - mIpAddresses.add(new ipAddress(address, mask, included)); + mIpAddresses.add(new IpAddress(address, mask, included)); } - TreeSet generateIPList() { + TreeSet generateIPList() { - PriorityQueue networks = new PriorityQueue(mIpAddresses); + PriorityQueue networks = new PriorityQueue(mIpAddresses); - TreeSet ipsDone = new TreeSet(); + TreeSet ipsDone = new TreeSet(); - ipAddress currentNet = networks.poll(); - if (currentNet==null) + IpAddress currentNet = networks.poll(); + if (currentNet == null) return ipsDone; - while (currentNet!=null) { + while (currentNet != null) { // Check if it and the next of it are compatible - ipAddress nextNet = networks.poll(); + IpAddress nextNet = networks.poll(); - if (BuildConfig.DEBUG) Assert.assertNotNull(currentNet); - if (nextNet== null || currentNet.getLastAddress().compareTo(nextNet.getFirstAddress()) == -1) { + if (BuildConfig.DEBUG) assertTrue(currentNet!=null); + if (nextNet == null || currentNet.getLastAddress().compareTo(nextNet.getFirstAddress()) == -1) { // Everything good, no overlapping nothing to do ipsDone.add(currentNet); @@ -248,10 +265,10 @@ TreeSet generateIPList() { if (currentNet.included == nextNet.included) { // Included in the next next and same type // Simply forget our current network - currentNet=nextNet; + currentNet = nextNet; } else { // our currentNet is included in next and types differ. Need to split the next network - ipAddress[] newNets = nextNet.split(); + IpAddress[] newNets = nextNet.split(); // TODO: The contains method of the Priority is stupid linear search @@ -261,7 +278,8 @@ TreeSet generateIPList() { networks.add(newNets[1]); if (newNets[0].getLastAddress().equals(currentNet.getLastAddress())) { - if (BuildConfig.DEBUG) Assert.assertEquals (newNets[0].networkMask, currentNet.networkMask); + if (BuildConfig.DEBUG) + assertTrue(newNets[0].networkMask == currentNet.networkMask); // Don't add the lower half that would conflict with currentNet } else { if (!networks.contains(newNets[0])) @@ -271,9 +289,9 @@ TreeSet generateIPList() { } } else { if (BuildConfig.DEBUG) { - Assert.assertTrue(currentNet.networkMask < nextNet.networkMask); - Assert.assertTrue (nextNet.getFirstAddress().compareTo(currentNet.getFirstAddress()) == 1); - Assert.assertTrue (currentNet.getLastAddress().compareTo(nextNet.getLastAddress()) != -1); + assertTrue(currentNet.networkMask < nextNet.networkMask); + assertTrue(nextNet.getFirstAddress().compareTo(currentNet.getFirstAddress()) == 1); + assertTrue(currentNet.getLastAddress().compareTo(nextNet.getLastAddress()) != -1); } // This network is bigger than the next and last ip of current >= next @@ -283,13 +301,13 @@ TreeSet generateIPList() { // simply ignore the next and move on } else { // We need to split our network - ipAddress[] newNets = currentNet.split(); + IpAddress[] newNets = currentNet.split(); if (newNets[1].networkMask == nextNet.networkMask) { if (BuildConfig.DEBUG) { - Assert.assertTrue (newNets[1].getFirstAddress().equals(nextNet.getFirstAddress())); - Assert.assertTrue (newNets[1].getLastAddress().equals(currentNet.getLastAddress())); + assertTrue(newNets[1].getFirstAddress().equals(nextNet.getFirstAddress())); + assertTrue(newNets[1].getLastAddress().equals(currentNet.getLastAddress())); // split second equal the next network, do not add it } networks.add(nextNet); @@ -309,11 +327,11 @@ TreeSet generateIPList() { return ipsDone; } - Collection getPositiveIPList() { - TreeSet ipsSorted = generateIPList(); + Collection getPositiveIPList() { + TreeSet ipsSorted = generateIPList(); - Vector ips = new Vector(); - for (ipAddress ia : ipsSorted) { + Vector ips = new Vector(); + for (IpAddress ia : ipsSorted) { if (ia.included) ips.add(ia); } @@ -321,20 +339,20 @@ Collection getPositiveIPList() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { // Include postive routes from the original set under < 4.4 since these might overrule the local // network but only if no smaller negative route exists - for(ipAddress origIp: mIpAddresses){ + for (IpAddress origIp : mIpAddresses) { if (!origIp.included) continue; // The netspace exists - if(ipsSorted.contains(origIp)) + if (ipsSorted.contains(origIp)) continue; - boolean skipIp=false; + boolean skipIp = false; // If there is any smaller net that is excluded we may not add the positive route back - for (ipAddress calculatedIp: ipsSorted) { - if(!calculatedIp.included && origIp.containsNet(calculatedIp)) { - skipIp=true; + for (IpAddress calculatedIp : ipsSorted) { + if (!calculatedIp.included && origIp.containsNet(calculatedIp)) { + skipIp = true; break; } } diff --git a/app/src/main/java/de/blinkt/openvpn/core/NetworkUtils.java b/app/src/main/java/de/blinkt/openvpn/core/NetworkUtils.java new file mode 100644 index 0000000..4044911 --- /dev/null +++ b/app/src/main/java/de/blinkt/openvpn/core/NetworkUtils.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2012-2018 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.core; + +import android.content.Context; +import android.net.*; +import android.os.Build; +import android.text.TextUtils; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.util.Vector; + +public class NetworkUtils { + + public static Vector getLocalNetworks(Context c, boolean ipv6) { + Vector nets = new Vector<>(); + ConnectivityManager conn = (ConnectivityManager) c.getSystemService(Context.CONNECTIVITY_SERVICE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Network[] networks = conn.getAllNetworks(); + for (Network network : networks) { + NetworkInfo ni = conn.getNetworkInfo(network); + LinkProperties li = conn.getLinkProperties(network); + + NetworkCapabilities nc = conn.getNetworkCapabilities(network); + + // Skip VPN networks like ourselves + if (nc.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) + continue; + + // Also skip mobile networks + if (nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) + continue; + + + for (LinkAddress la : li.getLinkAddresses()) { + if ((la.getAddress() instanceof Inet4Address && !ipv6) || + (la.getAddress() instanceof Inet6Address && ipv6)) + nets.add(la.toString()); + } + } + } else { + // Old Android Version, use native utils via ifconfig instead + // Add local network interfaces + if (ipv6) + return nets; + + String[] localRoutes = NativeUtils.getIfconfig(); + + // The format of mLocalRoutes is kind of broken because I don't really like JNI + for (int i = 0; i < localRoutes.length; i += 3) { + String intf = localRoutes[i]; + String ipAddr = localRoutes[i + 1]; + String netMask = localRoutes[i + 2]; + + if (intf == null || intf.equals("lo") || + intf.startsWith("tun") || intf.startsWith("rmnet")) + continue; + + if (ipAddr == null || netMask == null) { + VpnStatus.logError("Local routes are broken?! (Report to author) " + TextUtils.join("|", localRoutes)); + continue; + } + nets.add(ipAddr + "/" + CIDRIP.calculateLenFromMask(netMask)); + + } + + } + return nets; + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNManagement.java b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNManagement.java index d766693..46631e9 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNManagement.java +++ b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNManagement.java @@ -1,29 +1,45 @@ /* - * Copyright (c) 2012-2014 Arne Schwabe + * Copyright (c) 2012-2016 Arne Schwabe * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt */ package de.blinkt.openvpn.core; public interface OpenVPNManagement { + interface PausedStateCallback { + boolean shouldBeRunning(); + } + enum pauseReason { noNetwork, userPause, - screenOff + screenOff, } - int mBytecountInterval =2; + int mBytecountInterval = 2; - void reconnect(); + void reconnect(); - void pause(pauseReason reason); + void pause(pauseReason reason); - void resume(); + void resume(); - boolean stopVPN(); + /** + * @param replaceConnection True if the VPN is connected by a new connection. + * @return true if there was a process that has been send a stop signal + */ + boolean stopVPN(boolean replaceConnection); /* * Rebind the interface */ void networkChange(boolean sameNetwork); + + void setPauseCallback(PausedStateCallback callback); + + /** + * Send the response to a challenge response + * @param response Base64 encoded response + */ + void sendCRResponse(String response); } diff --git a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java index 72bb54a..97ed932 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java +++ b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2014 Arne Schwabe + * Copyright (c) 2012-2016 Arne Schwabe * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt */ @@ -7,64 +7,88 @@ import android.Manifest.permission; import android.annotation.TargetApi; +import android.app.Activity; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; +import android.app.UiModeManager; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.content.pm.ShortcutManager; +import android.content.res.Configuration; +import android.content.res.Resources; import android.net.ConnectivityManager; +import android.net.Uri; import android.net.VpnService; -import android.os.Binder; import android.os.Build; +import android.os.Bundle; +import android.os.Handler; import android.os.Handler.Callback; import android.os.IBinder; import android.os.Message; import android.os.ParcelFileDescriptor; -import android.preference.PreferenceManager; +import android.os.RemoteException; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; import android.system.OsConstants; import android.text.TextUtils; +import android.util.Base64; import android.util.Log; +import android.widget.Toast; +import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.Inet6Address; import java.net.InetAddress; import java.net.UnknownHostException; +import java.nio.charset.Charset; import java.util.Collection; -import java.util.HashMap; import java.util.Locale; import java.util.Vector; -import ht.vpn.android.BuildConfig; +import ht.vpn.android.LaunchVPN; import ht.vpn.android.R; -import ht.vpn.android.VpnProfile; +import de.blinkt.openvpn.VpnProfile; +import ht.vpn.android.activities.DisconnectVPN; +import ht.vpn.android.api.ExternalAppDatabase; import de.blinkt.openvpn.core.VpnStatus.ByteCountListener; -import de.blinkt.openvpn.core.VpnStatus.ConnectionStatus; import de.blinkt.openvpn.core.VpnStatus.StateListener; -import ht.vpn.android.activities.DisconnectVPN; -import ht.vpn.android.activities.MainActivity; +import de.blinkt.openvpn.core.IOpenVPNServiceInternal; -import static de.blinkt.openvpn.core.NetworkSpace.ipAddress; -import static de.blinkt.openvpn.core.VpnStatus.ConnectionStatus.LEVEL_CONNECTED; -import static de.blinkt.openvpn.core.VpnStatus.ConnectionStatus.LEVEL_CONNECTING_NO_SERVER_REPLY_YET; -import static de.blinkt.openvpn.core.VpnStatus.ConnectionStatus.LEVEL_WAITING_FOR_USER_INPUT; +import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_CONNECTED; +import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_WAITING_FOR_USER_INPUT; +import static de.blinkt.openvpn.core.NetworkSpace.IpAddress; -public class OpenVPNService extends VpnService implements StateListener, Callback, ByteCountListener { +public class OpenVPNService extends VpnService implements StateListener, Callback, ByteCountListener, IOpenVPNServiceInternal { public static final String START_SERVICE = "de.blinkt.openvpn.START_SERVICE"; public static final String START_SERVICE_STICKY = "de.blinkt.openvpn.START_SERVICE_STICKY"; public static final String ALWAYS_SHOW_NOTIFICATION = "de.blinkt.openvpn.NOTIFICATION_ALWAYS_VISIBLE"; public static final String DISCONNECT_VPN = "de.blinkt.openvpn.DISCONNECT_VPN"; + public static final String NOTIFICATION_CHANNEL_BG_ID = "openvpn_bg"; + public static final String NOTIFICATION_CHANNEL_NEWSTATUS_ID = "openvpn_newstat"; + public static final String NOTIFICATION_CHANNEL_USERREQ_ID = "openvpn_userreq"; + + public static final String VPNSERVICE_TUN = "vpnservice-tun"; + public final static String ORBOT_PACKAGE_NAME = "org.torproject.android"; private static final String PAUSE_VPN = "de.blinkt.openvpn.PAUSE_VPN"; - private static final String RESUME_VPN = "de.blinkt.openvpn.RESUME_VPN"; - private static final int OPENVPN_STATUS = 1; + private static final String RESUME_VPN = "ht.vpn.android.RESUME_VPN"; + + public static final String EXTRA_CHALLENGE_TXT = "de.blinkt.openvpn.core.CR_TEXT_CHALLENGE"; + + private static final int PRIORITY_MIN = -2; + private static final int PRIORITY_DEFAULT = 0; + private static final int PRIORITY_MAX = 2; private static boolean mNotificationAlwaysVisible = false; + private static Class mNotificationActivityClass; private final Vector mDnslist = new Vector<>(); private final NetworkSpace mRoutes = new NetworkSpace(); private final NetworkSpace mRoutesv6 = new NetworkSpace(); - private final IBinder mBinder = new LocalBinder(); + private final Object mProcessLock = new Object(); + private String lastChannel; private Thread mProcessThread = null; private VpnProfile mProfile; private String mDomain = null; @@ -75,28 +99,114 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac private boolean mDisplayBytecount = false; private boolean mStarting = false; private long mConnecttime; - private boolean mOvpn3 = false; private OpenVPNManagement mManagement; + private final IBinder mBinder = new IOpenVPNServiceInternal.Stub() { + + @Override + public boolean protect(int fd) throws RemoteException { + return OpenVPNService.this.protect(fd); + } + + @Override + public void userPause(boolean shouldbePaused) throws RemoteException { + OpenVPNService.this.userPause(shouldbePaused); + } + + @Override + public boolean stopVPN(boolean replaceConnection) throws RemoteException { + return OpenVPNService.this.stopVPN(replaceConnection); + } + + @Override + public void addAllowedExternalApp(String packagename) throws RemoteException { + OpenVPNService.this.addAllowedExternalApp(packagename); + } + + @Override + public boolean isAllowedExternalApp(String packagename) throws RemoteException { + return OpenVPNService.this.isAllowedExternalApp(packagename); + } + + @Override + public void challengeResponse(String repsonse) throws RemoteException { + OpenVPNService.this.challengeResponse(repsonse); + } + + + }; private String mLastTunCfg; private String mRemoteGW; - private final Object mProcessLock = new Object(); + private Handler guiHandler; + private Toast mlastToast; + private Runnable mOpenVPNThread; // From: http://stackoverflow.com/questions/3758606/how-to-convert-byte-size-into-human-readable-format-in-java - public static String humanReadableByteCount(long bytes, boolean mbit) { - if (mbit) + public static String humanReadableByteCount(long bytes, boolean speed, Resources res) { + if (speed) bytes = bytes * 8; - int unit = mbit ? 1000 : 1024; - if (bytes < unit) - return bytes + (mbit ? " bit" : " B"); - - int exp = (int) (Math.log(bytes) / Math.log(unit)); - String pre = (mbit ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (mbit ? "" : ""); - if (mbit) - return String.format(Locale.getDefault(), "%.1f %sbit", bytes / Math.pow(unit, exp), pre); + int unit = speed ? 1000 : 1024; + + + int exp = Math.max(0, Math.min((int) (Math.log(bytes) / Math.log(unit)), 3)); + + float bytesUnit = (float) (bytes / Math.pow(unit, exp)); + + if (speed) + switch (exp) { + case 0: + return res.getString(R.string.bits_per_second, bytesUnit); + case 1: + return res.getString(R.string.kbits_per_second, bytesUnit); + case 2: + return res.getString(R.string.mbits_per_second, bytesUnit); + default: + return res.getString(R.string.gbits_per_second, bytesUnit); + } else - return String.format(Locale.getDefault(), "%.1f %sB", bytes / Math.pow(unit, exp), pre); + switch (exp) { + case 0: + return res.getString(R.string.volume_byte, bytesUnit); + case 1: + return res.getString(R.string.volume_kbyte, bytesUnit); + case 2: + return res.getString(R.string.volume_mbyte, bytesUnit); + default: + return res.getString(R.string.volume_gbyte, bytesUnit); + + } + + + } + + /** + * Sets the activity which should be opened when tapped on the permanent notification tile. + * + * @param activityClass The activity class to open + */ + public static void setNotificationActivityClass(Class activityClass) { + mNotificationActivityClass = activityClass; + } + + @Override + public void addAllowedExternalApp(String packagename) throws RemoteException { + ExternalAppDatabase extapps = new ExternalAppDatabase(OpenVPNService.this); + extapps.addApp(packagename); + } + + @Override + public boolean isAllowedExternalApp(String packagename) throws RemoteException { + return false; + } + + @Override + public void challengeResponse(String response) throws RemoteException { + if(mManagement != null) { + String b64response = Base64.encodeToString(response.getBytes(Charset.forName("UTF-8")), Base64.DEFAULT); + mManagement.sendCRResponse(b64response); + } } + @Override public IBinder onBind(Intent intent) { String action = intent.getAction(); @@ -108,12 +218,13 @@ public IBinder onBind(Intent intent) { @Override public void onRevoke() { - mManagement.stopVPN(); + VpnStatus.logError(R.string.permission_revoked); + mManagement.stopVPN(false); endVpnService(); } // Similar to revoke but do not try to stop process - public void processDied() { + public void openvpnStopped() { endVpnService(); } @@ -124,6 +235,7 @@ private void endVpnService() { VpnStatus.removeByteCountListener(this); unregisterDeviceStateReceiver(); ProfileManager.setConntectedVpnProfileDisconnected(this); + mOpenVPNThread = null; if (!mStarting) { stopForeground(!mNotificationAlwaysVisible); @@ -134,14 +246,20 @@ private void endVpnService() { } } - private void showNotification(String msg, String tickerText, boolean lowpriority, long when, ConnectionStatus status) { - String ns = Context.NOTIFICATION_SERVICE; - NotificationManager mNotificationManager = (NotificationManager) getSystemService(ns); - - + private void showNotification(final String msg, String tickerText, @NonNull String channel, + long when, ConnectionStatus status, Intent intent) { + NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); int icon = getIconByConnectionStatus(status); - Notification.Builder nbuilder = new Notification.Builder(this); + android.app.Notification.Builder nbuilder = new Notification.Builder(this); + + int priority; + if (channel.equals(NOTIFICATION_CHANNEL_BG_ID)) + priority = PRIORITY_MIN; + else if (channel.equals(NOTIFICATION_CHANNEL_USERREQ_ID)) + priority = PRIORITY_MAX; + else + priority = PRIORITY_DEFAULT; if (mProfile != null) nbuilder.setContentTitle(getString(R.string.notifcation_title, mProfile.mName)); @@ -151,17 +269,36 @@ private void showNotification(String msg, String tickerText, boolean lowpriority nbuilder.setContentText(msg); nbuilder.setOnlyAlertOnce(true); nbuilder.setOngoing(true); - nbuilder.setContentIntent(getLogPendingIntent()); - nbuilder.setSmallIcon(icon); + nbuilder.setSmallIcon(icon); + if (status == LEVEL_WAITING_FOR_USER_INPUT) { + PendingIntent pIntent = PendingIntent.getActivity(this, 0, intent, 0); + nbuilder.setContentIntent(pIntent); + } else { + nbuilder.setContentIntent(getGraphPendingIntent()); + } if (when != 0) nbuilder.setWhen(when); // Try to set the priority available since API 16 (Jellybean) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) - jbNotificationExtras(lowpriority, nbuilder); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + jbNotificationExtras(priority, nbuilder); + addVpnActionsToNotification(nbuilder); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + lpNotificationExtras(nbuilder, Notification.CATEGORY_SERVICE); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + //noinspection NewApi + nbuilder.setChannelId(channel); + if (mProfile != null) + //noinspection NewApi + nbuilder.setShortcutId(mProfile.getUUIDString()); + + } if (tickerText != null && !tickerText.equals("")) nbuilder.setTicker(tickerText); @@ -169,22 +306,52 @@ private void showNotification(String msg, String tickerText, boolean lowpriority @SuppressWarnings("deprecation") Notification notification = nbuilder.getNotification(); + int notificationId = channel.hashCode(); + + mNotificationManager.notify(notificationId, notification); + + startForeground(notificationId, notification); - mNotificationManager.notify(OPENVPN_STATUS, notification); - startForeground(OPENVPN_STATUS, notification); + if (lastChannel != null && !channel.equals(lastChannel)) { + // Cancel old notification + mNotificationManager.cancel(lastChannel.hashCode()); + } + + // Check if running on a TV + if (runningOnAndroidTV() && !(priority < 0)) + guiHandler.post(new Runnable() { + + @Override + public void run() { + + if (mlastToast != null) + mlastToast.cancel(); + String toastText = String.format(Locale.getDefault(), "%s - %s", mProfile.mName, msg); + mlastToast = Toast.makeText(getBaseContext(), toastText, Toast.LENGTH_SHORT); + mlastToast.show(); + } + }); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void lpNotificationExtras(Notification.Builder nbuilder, String category) { + nbuilder.setCategory(category); + nbuilder.setLocalOnly(true); + + } + + private boolean runningOnAndroidTV() { + UiModeManager uiModeManager = (UiModeManager) getSystemService(UI_MODE_SERVICE); + return uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION; } private int getIconByConnectionStatus(ConnectionStatus level) { switch (level) { - case LEVEL_CONNECTED: - return R.drawable.ic_stat_vpn; case LEVEL_AUTH_FAILED: case LEVEL_NONETWORK: case LEVEL_NOTCONNECTED: - return R.drawable.ic_stat_vpn_outline; case LEVEL_CONNECTING_NO_SERVER_REPLY_YET: case LEVEL_WAITING_FOR_USER_INPUT: - return R.drawable.ic_stat_vpn_outline; case LEVEL_CONNECTING_SERVER_REPLIED: return R.drawable.ic_stat_vpn_outline; case LEVEL_VPNPAUSED: @@ -197,52 +364,68 @@ private int getIconByConnectionStatus(ConnectionStatus level) { } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - private void jbNotificationExtras(boolean lowpriority, - Notification.Builder nbuilder) { + private void jbNotificationExtras(int priority, + android.app.Notification.Builder nbuilder) { try { - if (lowpriority) { + if (priority != 0) { Method setpriority = nbuilder.getClass().getMethod("setPriority", int.class); - // PRIORITY_MIN == -2 - setpriority.invoke(nbuilder, -2); + setpriority.invoke(nbuilder, priority); Method setUsesChronometer = nbuilder.getClass().getMethod("setUsesChronometer", boolean.class); setUsesChronometer.invoke(nbuilder, true); } - Intent disconnectVPN = new Intent(this, DisconnectVPN.class); - disconnectVPN.setAction(DISCONNECT_VPN); - PendingIntent disconnectPendingIntent = PendingIntent.getActivity(this, 0, disconnectVPN, 0); + //ignore exception + } catch (NoSuchMethodException | IllegalArgumentException | + InvocationTargetException | IllegalAccessException e) { + VpnStatus.logException(e); + } - nbuilder.addAction(android.R.drawable.ic_menu_close_clear_cancel, - getString(R.string.cancel_connection), disconnectPendingIntent); + } - Intent pauseVPN = new Intent(this, OpenVPNService.class); - if (mDeviceStateReceiver == null || !mDeviceStateReceiver.isUserPaused()) { - pauseVPN.setAction(PAUSE_VPN); - PendingIntent pauseVPNPending = PendingIntent.getService(this, 0, pauseVPN, 0); - nbuilder.addAction(android.R.drawable.ic_media_pause, - getString(R.string.pauseVPN), pauseVPNPending); + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) + private void addVpnActionsToNotification(Notification.Builder nbuilder) { + Intent disconnectVPN = new Intent(this, DisconnectVPN.class); + disconnectVPN.setAction(DISCONNECT_VPN); + PendingIntent disconnectPendingIntent = PendingIntent.getActivity(this, 0, disconnectVPN, 0); - } else { - pauseVPN.setAction(RESUME_VPN); - PendingIntent resumeVPNPending = PendingIntent.getService(this, 0, pauseVPN, 0); - nbuilder.addAction(android.R.drawable.ic_media_play, - getString(R.string.resumevpn), resumeVPNPending); - } + nbuilder.addAction(android.R.drawable.ic_menu_close_clear_cancel, + getString(R.string.cancel_connection), disconnectPendingIntent); + Intent pauseVPN = new Intent(this, OpenVPNService.class); + if (mDeviceStateReceiver == null || !mDeviceStateReceiver.isUserPaused()) { + pauseVPN.setAction(PAUSE_VPN); + PendingIntent pauseVPNPending = PendingIntent.getService(this, 0, pauseVPN, 0); + nbuilder.addAction(android.R.drawable.ic_media_pause, + getString(R.string.pauseVPN), pauseVPNPending); - //ignore exception - } catch (NoSuchMethodException | IllegalArgumentException | - InvocationTargetException | IllegalAccessException e) { - VpnStatus.logException(e); + } else { + pauseVPN.setAction(RESUME_VPN); + PendingIntent resumeVPNPending = PendingIntent.getService(this, 0, pauseVPN, 0); + nbuilder.addAction(android.R.drawable.ic_media_play, + getString(R.string.resumevpn), resumeVPNPending); } + } + PendingIntent getUserInputIntent(String needed) { + Intent intent = new Intent(getApplicationContext(), LaunchVPN.class); + intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + intent.putExtra("need", needed); + Bundle b = new Bundle(); + b.putString("need", needed); + PendingIntent pIntent = PendingIntent.getActivity(this, 12, intent, 0); + return pIntent; } - PendingIntent getLogPendingIntent() { + PendingIntent getGraphPendingIntent() { // Let the configure Button show the Log - Intent intent = new Intent(getBaseContext(), MainActivity.class); + + + Intent intent = new Intent(); + intent.setComponent(new ComponentName(this, getPackageName() + ".activities.MainActivity")); + + intent.putExtra("PAGE", "graph"); intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); PendingIntent startLW = PendingIntent.getActivity(this, 0, intent, 0); intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); @@ -257,6 +440,10 @@ synchronized void registerDeviceStateReceiver(OpenVPNManagement magnagement) { filter.addAction(Intent.ACTION_SCREEN_OFF); filter.addAction(Intent.ACTION_SCREEN_ON); mDeviceStateReceiver = new DeviceStateReceiver(magnagement); + + // Fetch initial network state + mDeviceStateReceiver.networkStateChange(this); + registerReceiver(mDeviceStateReceiver, filter); VpnStatus.addByteCountListener(mDeviceStateReceiver); @@ -287,6 +474,14 @@ public void userPause(boolean shouldBePaused) { mDeviceStateReceiver.userPause(shouldBePaused); } + @Override + public boolean stopVPN(boolean replaceConnection) throws RemoteException { + if (getManagement() != null) + return getManagement().stopVPN(replaceConnection); + else + return false; + } + @Override public int onStartCommand(Intent intent, int flags, int startId) { @@ -296,6 +491,9 @@ public int onStartCommand(Intent intent, int flags, int startId) { VpnStatus.addStateListener(this); VpnStatus.addByteCountListener(this); + guiHandler = new Handler(getMainLooper()); + + if (intent != null && PAUSE_VPN.equals(intent.getAction())) { if (mDeviceStateReceiver != null) mDeviceStateReceiver.userPause(true); @@ -315,124 +513,168 @@ public int onStartCommand(Intent intent, int flags, int startId) { return START_REDELIVER_INTENT; } - /* The intent is null when the service has been restarted */ - if (intent == null) { - mProfile = ProfileManager.getLastConnectedProfile(this, false); + // Always show notification here to avoid problem with startForeground timeout + VpnStatus.logInfo(R.string.building_configration); + VpnStatus.updateStateString("VPN_GENERATE_CONFIG", "", R.string.building_configration, ConnectionStatus.LEVEL_START); + showNotification(VpnStatus.getLastCleanLogMessage(this), + VpnStatus.getLastCleanLogMessage(this), NOTIFICATION_CHANNEL_NEWSTATUS_ID, 0, ConnectionStatus.LEVEL_START, null); + + if (intent != null && intent.hasExtra(getPackageName() + ".profileUUID")) { + String profileUUID = intent.getStringExtra(getPackageName() + ".profileUUID"); + int profileVersion = intent.getIntExtra(getPackageName() + ".profileVersion", 0); + // Try for 10s to get current version of the profile + mProfile = ProfileManager.get(this, profileUUID, profileVersion, 100); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + updateShortCutUsage(mProfile); + } + + } else { + /* The intent is null when we are set as always-on or the service has been restarted. */ + mProfile = ProfileManager.getLastConnectedProfile(this); VpnStatus.logInfo(R.string.service_restarted); /* Got no profile, just stop */ if (mProfile == null) { - Log.d("OpenVPN", "Got no last connected profile on null intent. Stopping"); - stopSelf(startId); - return START_NOT_STICKY; + Log.d("OpenVPN", "Got no last connected profile on null intent. Assuming always on."); + mProfile = ProfileManager.getAlwaysOnVPN(this); + + if (mProfile == null) { + stopSelf(startId); + return START_NOT_STICKY; + } } /* Do the asynchronous keychain certificate stuff */ mProfile.checkForRestart(this); - - /* Recreate the intent */ - intent = mProfile.getStartServiceIntent(this); - - } else { - String profileUUID = intent.getStringExtra(getPackageName() + ".profileUUID"); - mProfile = ProfileManager.get(this, profileUUID); } if (mProfile == null) { - Log.d("OpenVPN", "Got no last connected profile on null intent. Stopping"); stopSelf(startId); return START_NOT_STICKY; } - // Extract information from the intent. - String prefix = getPackageName(); - String[] argv = intent.getStringArrayExtra(prefix + ".ARGV"); - String nativeLibraryDirectory = intent.getStringExtra(prefix + ".nativelib"); + /* start the OpenVPN process itself in a background thread */ + new Thread(new Runnable() { + @Override + public void run() { + startOpenVPN(); + } + }).start(); + + + ProfileManager.setConnectedVpnProfile(this, mProfile); + VpnStatus.setConnectedVPNProfile(mProfile.getUUIDString()); + + return START_STICKY; + } + + @RequiresApi(Build.VERSION_CODES.N_MR1) + private void updateShortCutUsage(VpnProfile profile) { + if (profile == null) + return; + ShortcutManager shortcutManager = getSystemService(ShortcutManager.class); + shortcutManager.reportShortcutUsed(profile.getUUIDString()); + } + + private void startOpenVPN() { + try { + mProfile.writeConfigFile(this); + } catch (IOException e) { + VpnStatus.logException("Error writing config file", e); + endVpnService(); + return; + } + String nativeLibraryDirectory = getApplicationInfo().nativeLibraryDir; + + // Write OpenVPN binary + String[] argv = VPNLaunchHelper.buildOpenvpnArgv(this); - String startTitle = getString(R.string.start_vpn_title, mProfile.mName); - String startTicker = getString(R.string.start_vpn_ticker, mProfile.mName); - showNotification(startTitle, startTicker, - false, 0, LEVEL_CONNECTING_NO_SERVER_REPLY_YET); // Set a flag that we are starting a new VPN mStarting = true; // Stop the previous session by interrupting the thread. - if (mManagement != null && mManagement.stopVPN()) - // an old was asked to exit, wait 1s - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - //ignore - } - synchronized (mProcessLock) { - if (mProcessThread != null) { - mProcessThread.interrupt(); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - //ignore - } - } - } + stopOldOpenVPNProcess(); // An old running VPN should now be exited mStarting = false; // Start a new session by creating a new thread. - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - - mOvpn3 = prefs.getBoolean("ovpn3", false); - if (!"ovpn3".equals(BuildConfig.FLAVOR)) - mOvpn3 = false; - + boolean useOpenVPN3 = VpnProfile.doUseOpenVPN3(this); // Open the Management Interface - if (!mOvpn3) { - + if (!useOpenVPN3) { // start a Thread that handles incoming messages of the managment socket OpenVpnManagementThread ovpnManagementThread = new OpenVpnManagementThread(mProfile, this); if (ovpnManagementThread.openManagementInterface(this)) { - Thread mSocketManagerThread = new Thread(ovpnManagementThread, "OpenVPNManagementThread"); mSocketManagerThread.start(); mManagement = ovpnManagementThread; VpnStatus.logInfo("started Socket Thread"); } else { - return START_NOT_STICKY; + endVpnService(); + return; } } - Runnable processThread; - if (mOvpn3) { - + if (useOpenVPN3) { OpenVPNManagement mOpenVPN3 = instantiateOpenVPN3Core(); processThread = (Runnable) mOpenVPN3; mManagement = mOpenVPN3; - - } else { - HashMap env = new HashMap<>(); - processThread = new OpenVPNThread(this, argv, env, nativeLibraryDirectory); + processThread = new OpenVPNThread(this, argv, nativeLibraryDirectory); + mOpenVPNThread = processThread; } - synchronized (mProcessLock) { + synchronized (mProcessLock) + + { mProcessThread = new Thread(processThread, "OpenVPNProcessThread"); mProcessThread.start(); } - if (mDeviceStateReceiver != null) - unregisterDeviceStateReceiver(); - registerDeviceStateReceiver(mManagement); + new Handler(getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (mDeviceStateReceiver != null) + unregisterDeviceStateReceiver(); + registerDeviceStateReceiver(mManagement); + } + } - ProfileManager.setConnectedVpnProfile(this, mProfile); - /* TODO: At the moment we have no way to handle asynchronous PW input - * Fixing will also allow to handle challenge/response authentication */ - if (mProfile.needUserPWInput(true) != 0) - return START_NOT_STICKY; + ); + } - return START_STICKY; + + private void stopOldOpenVPNProcess() { + if (mManagement != null) { + if (mOpenVPNThread != null) + ((OpenVPNThread) mOpenVPNThread).setReplaceConnection(); + if (mManagement.stopVPN(true)) { + // an old was asked to exit, wait 1s + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + //ignore + } + } + } + + forceStopOpenVpnProcess(); + } + + public void forceStopOpenVpnProcess() { + synchronized (mProcessLock) { + if (mProcessThread != null) { + mProcessThread.interrupt(); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + //ignore + } + } + } } private OpenVPNManagement instantiateOpenVPN3Core() { @@ -440,17 +682,27 @@ private OpenVPNManagement instantiateOpenVPN3Core() { Class cl = Class.forName("de.blinkt.openvpn.core.OpenVPNThreadv3"); return (OpenVPNManagement) cl.getConstructor(OpenVPNService.class, VpnProfile.class).newInstance(this, mProfile); } catch (IllegalArgumentException | InstantiationException | InvocationTargetException | - NoSuchMethodException | ClassNotFoundException | IllegalAccessException e ) { + NoSuchMethodException | ClassNotFoundException | IllegalAccessException e) { e.printStackTrace(); } return null; } + @Override + public IBinder asBinder() { + return mBinder; + } + + @Override + public void onCreate() { + super.onCreate(); + } + @Override public void onDestroy() { synchronized (mProcessLock) { if (mProcessThread != null) { - mManagement.stopVPN(); + mManagement.stopVPN(true); } } @@ -459,7 +711,7 @@ public void onDestroy() { } // Just in case unregister for state VpnStatus.removeStateListener(this); - + VpnStatus.flushLog(); } private String getTunConfigString() { @@ -489,7 +741,8 @@ public ParcelFileDescriptor openTun() { VpnStatus.logInfo(R.string.last_openvpn_tun_config); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && mProfile.mAllowLocalLAN) { + boolean allowUnsetAF = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !mProfile.mBlockUnusedAddressFamilies; + if (allowUnsetAF) { allowAllAFFamilies(builder); } @@ -499,7 +752,9 @@ public ParcelFileDescriptor openTun() { } if (mLocalIP != null) { - addLocalNetworksToRoutes(); + // OpenVPN3 manages excluded local networks by callback + if (!VpnProfile.doUseOpenVPN3(this)) + addLocalNetworksToRoutes(); try { builder.addAddress(mLocalIP.mIp, mLocalIP.len); } catch (IllegalArgumentException iae) { @@ -538,12 +793,34 @@ public ParcelFileDescriptor openTun() { builder.setMtu(mMtu); } - Collection positiveIPv4Routes = mRoutes.getPositiveIPList(); - Collection positiveIPv6Routes = mRoutesv6.getPositiveIPList(); + Collection positiveIPv4Routes = mRoutes.getPositiveIPList(); + Collection positiveIPv6Routes = mRoutesv6.getPositiveIPList(); + + if ("samsung".equals(Build.BRAND) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && mDnslist.size() >= 1) { + // Check if the first DNS Server is in the VPN range + try { + IpAddress dnsServer = new IpAddress(new CIDRIP(mDnslist.get(0), 32), true); + boolean dnsIncluded = false; + for (IpAddress net : positiveIPv4Routes) { + if (net.containsNet(dnsServer)) { + dnsIncluded = true; + } + } + if (!dnsIncluded) { + String samsungwarning = String.format("Warning Samsung Android 5.0+ devices ignore DNS servers outside the VPN range. To enable DNS resolution a route to your DNS Server (%s) has been added.", mDnslist.get(0)); + VpnStatus.logWarning(samsungwarning); + positiveIPv4Routes.add(dnsServer); + } + } catch (Exception e) { + // If it looks like IPv6 ignore error + if (!mDnslist.get(0).contains(":")) + VpnStatus.logError("Error parsing DNS Server IP: " + mDnslist.get(0)); + } + } - ipAddress multicastRange = new ipAddress(new CIDRIP("224.0.0.0", 3), true); + IpAddress multicastRange = new IpAddress(new CIDRIP("224.0.0.0", 3), true); - for (ipAddress route : positiveIPv4Routes) { + for (IpAddress route : positiveIPv4Routes) { try { if (multicastRange.containsNet(route)) @@ -555,7 +832,7 @@ public ParcelFileDescriptor openTun() { } } - for (ipAddress route6 : positiveIPv6Routes) { + for (IpAddress route6 : positiveIPv6Routes) { try { builder.addRoute(route6.getIPv6Address(), route6.networkMask); } catch (IllegalArgumentException ia) { @@ -563,30 +840,34 @@ public ParcelFileDescriptor openTun() { } } - if ("samsung".equals(Build.BRAND) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && mDnslist.size() >= 1) { - // Check if the first DNS Server is in the VPN range - try { - ipAddress dnsServer = new ipAddress(new CIDRIP(mDnslist.get(0), 32), true); - boolean dnsIncluded=false; - for (ipAddress net : positiveIPv4Routes) { - if (net.containsNet(dnsServer)) { - dnsIncluded = true; - } - } - if (!dnsIncluded) { - String samsungwarning = String.format("Warning Samsung Android 5.0+ devices ignore DNS servers outside the VPN range. To enable DNS add a custom route to your DNS Server (%s) or change to a DNS inside your VPN range", mDnslist.get(0)); - VpnStatus.logWarning(samsungwarning); - } - } catch (Exception e) { - VpnStatus.logError("Error parsing DNS Server IP: " + mDnslist.get(0)); - } - } - if (mDomain != null) builder.addSearchDomain(mDomain); - VpnStatus.logInfo(R.string.local_ip_info, mLocalIP.mIp, mLocalIP.len, mLocalIPv6, mMtu); + String ipv4info; + String ipv6info; + if (allowUnsetAF) { + ipv4info = "(not set, allowed)"; + ipv6info = "(not set, allowed)"; + } else { + ipv4info = "(not set)"; + ipv6info = "(not set)"; + } + + int ipv4len; + if (mLocalIP!=null) { + ipv4len=mLocalIP.len; + ipv4info=mLocalIP.mIp; + } else { + ipv4len = -1; + } + + if (mLocalIPv6!=null) + { + ipv6info = mLocalIPv6; + } + + VpnStatus.logInfo(R.string.local_ip_info, ipv4info, ipv4len, ipv6info, mMtu); VpnStatus.logInfo(R.string.dns_server_info, TextUtils.join(", ", mDnslist), mDomain); VpnStatus.logInfo(R.string.routes_info_incl, TextUtils.join(", ", mRoutes.getNetworks(true)), TextUtils.join(", ", mRoutesv6.getNetworks(true))); VpnStatus.logInfo(R.string.routes_info_excl, TextUtils.join(", ", mRoutes.getNetworks(false)), TextUtils.join(", ", mRoutesv6.getNetworks(false))); @@ -594,6 +875,10 @@ public ParcelFileDescriptor openTun() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { setAllowedVpnPackages(builder); } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { + // VPN always uses the default network + builder.setUnderlyingNetworks(null); + } String session = mProfile.mName; @@ -601,6 +886,8 @@ public ParcelFileDescriptor openTun() { session = getString(R.string.session_ipv6string, session, mLocalIP, mLocalIPv6); else if (mLocalIP != null) session = getString(R.string.session_ipv4string, session, mLocalIP); + else + session = getString(R.string.session_ipv4string, session, mLocalIPv6); builder.setSession(session); @@ -618,7 +905,7 @@ else if (mLocalIP != null) mLocalIPv6 = null; mDomain = null; - builder.setConfigureIntent(getLogPendingIntent()); + builder.setConfigureIntent(getGraphPendingIntent()); try { //Debug.stopMethodTracing(); @@ -644,25 +931,11 @@ private void allowAllAFFamilies(Builder builder) { } private void addLocalNetworksToRoutes() { - - // Add local network interfaces - String[] localRoutes = NativeUtils.getIfconfig(); - - // The format of mLocalRoutes is kind of broken because I don't really like JNI - for (int i = 0; i < localRoutes.length; i += 3) { - String intf = localRoutes[i]; - String ipAddr = localRoutes[i + 1]; - String netMask = localRoutes[i + 2]; - - if (intf == null || intf.equals("lo") || - intf.startsWith("tun") || intf.startsWith("rmnet")) - continue; - - if (ipAddr == null || netMask == null) { - VpnStatus.logError("Local routes are broken?! (Report to author) " + TextUtils.join("|", localRoutes)); - continue; - } - + for (String net: NetworkUtils.getLocalNetworks(this, false)) + { + String[] netparts = net.split("/"); + String ipAddr = netparts[0]; + int netMask = Integer.parseInt(netparts[1]); if (ipAddr.equals(mLocalIP.mIp)) continue; @@ -672,17 +945,50 @@ private void addLocalNetworksToRoutes() { } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && mProfile.mAllowLocalLAN) mRoutes.addIP(new CIDRIP(ipAddr, netMask), false); } + + // IPv6 is Lollipop+ only so we can skip the lower than KITKAT case + if (mProfile.mAllowLocalLAN) { + for (String net : NetworkUtils.getLocalNetworks(this, true)) { + addRoutev6(net, false);; + } + } + + } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private void setAllowedVpnPackages(Builder builder) { + boolean profileUsesOrBot = false; + + for (Connection c : mProfile.mConnections) { + if (c.mProxyType == Connection.ProxyType.ORBOT) + profileUsesOrBot = true; + } + + if (profileUsesOrBot) + VpnStatus.logDebug("VPN Profile uses at least one server entry with Orbot. Setting up VPN so that OrBot is not redirected over VPN."); + + + boolean atLeastOneAllowedApp = false; + + if (mProfile.mAllowedAppsVpnAreDisallowed && profileUsesOrBot) { + try { + builder.addDisallowedApplication(ORBOT_PACKAGE_NAME); + } catch (PackageManager.NameNotFoundException e) { + VpnStatus.logDebug("Orbot not installed?"); + } + } + for (String pkg : mProfile.mAllowedAppsVpn) { try { if (mProfile.mAllowedAppsVpnAreDisallowed) { builder.addDisallowedApplication(pkg); } else { - builder.addAllowedApplication(pkg); + if (!(profileUsesOrBot && pkg.equals(ORBOT_PACKAGE_NAME))) { + builder.addAllowedApplication(pkg); + atLeastOneAllowedApp = true; + } } } catch (PackageManager.NameNotFoundException e) { mProfile.mAllowedAppsVpn.remove(pkg); @@ -690,11 +996,25 @@ private void setAllowedVpnPackages(Builder builder) { } } + if (!mProfile.mAllowedAppsVpnAreDisallowed && !atLeastOneAllowedApp) { + VpnStatus.logDebug(R.string.no_allowed_app, getPackageName()); + try { + builder.addAllowedApplication(getPackageName()); + } catch (PackageManager.NameNotFoundException e) { + VpnStatus.logError("This should not happen: " + e.getLocalizedMessage()); + } + } + if (mProfile.mAllowedAppsVpnAreDisallowed) { VpnStatus.logDebug(R.string.disallowed_vpn_apps_info, TextUtils.join(", ", mProfile.mAllowedAppsVpn)); } else { VpnStatus.logDebug(R.string.allowed_vpn_apps_info, TextUtils.join(", ", mProfile.mAllowedAppsVpn)); } + + if (mProfile.mAllowAppVpnBypass) { + builder.allowBypass(); + VpnStatus.logDebug("Apps may bypass VPN"); + } } public void addDNS(String dns) { @@ -710,21 +1030,21 @@ public void setDomain(String domain) { /** * Route that is always included, used by the v3 core */ - public void addRoute(CIDRIP route) { - mRoutes.addIP(route, true); + public void addRoute(CIDRIP route, boolean include) { + mRoutes.addIP(route, include); } public void addRoute(String dest, String mask, String gateway, String device) { CIDRIP route = new CIDRIP(dest, mask); boolean include = isAndroidTunDevice(device); - ipAddress gatewayIP = new ipAddress(new CIDRIP(gateway, 32), false); + IpAddress gatewayIP = new IpAddress(new CIDRIP(gateway, 32), false); if (mLocalIP == null) { - VpnStatus.logError("Local IP address unset but adding route?! This is broken! Please contact author with log"); + VpnStatus.logError("Local IP address unset and received. Neither pushed server config nor local config specifies an IP addresses. Opening tun device is most likely going to fail."); return; } - ipAddress localNet = new ipAddress(mLocalIP, true); + IpAddress localNet = new IpAddress(mLocalIP, true); if (localNet.containsNet(gatewayIP)) include = true; @@ -744,10 +1064,13 @@ public void addRoute(String dest, String mask, String gateway, String device) { } public void addRoutev6(String network, String device) { - String[] v6parts = network.split("/"); + // Tun is opened after ROUTE6, no device name may be present boolean included = isAndroidTunDevice(device); + addRoutev6(network, included); + } - // Tun is opened after ROUTE6, no device name may be present + public void addRoutev6(String network, boolean included) { + String[] v6parts = network.split("/"); try { Inet6Address ip = (Inet6Address) InetAddress.getAllByName(v6parts[0])[0]; @@ -763,7 +1086,7 @@ public void addRoutev6(String network, String device) { private boolean isAndroidTunDevice(String device) { return device != null && - (device.startsWith("tun") || "(null)".equals(device) || "vpnservice-tun".equals(device)); + (device.startsWith("tun") || "(null)".equals(device) || VPNSERVICE_TUN.equals(device)); } public void setMtu(int mtu) { @@ -812,7 +1135,7 @@ public void setLocalIP(String local, String netmask, int mtu, String mode) { if (mLocalIP.len <= 31 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { CIDRIP interfaceRoute = new CIDRIP(mLocalIP.mIp, mLocalIP.len); interfaceRoute.normalise(); - addRoute(interfaceRoute); + addRoute(interfaceRoute, true); } @@ -825,7 +1148,7 @@ public void setLocalIPv6(String ipv6addr) { } @Override - public void updateState(String state, String logmessage, int resid, ConnectionStatus level) { + public void updateState(String state, String logmessage, int resid, ConnectionStatus level, Intent intent) { // If the process is not running, ignore any state, // Notification should be invisible in this state @@ -833,18 +1156,15 @@ public void updateState(String state, String logmessage, int resid, ConnectionSt if (mProcessThread == null && !mNotificationAlwaysVisible) return; - boolean lowpriority = false; + String channel = NOTIFICATION_CHANNEL_NEWSTATUS_ID; // Display byte count only after being connected { - if (level == LEVEL_WAITING_FOR_USER_INPUT) { - // The user is presented a dialog of some kind, no need to inform the user - // with a notifcation - return; - } else if (level == LEVEL_CONNECTED) { + if (level == LEVEL_CONNECTED) { mDisplayBytecount = true; mConnecttime = System.currentTimeMillis(); - lowpriority = true; + if (!runningOnAndroidTV()) + channel = NOTIFICATION_CHANNEL_BG_ID; } else { mDisplayBytecount = false; } @@ -853,12 +1173,16 @@ public void updateState(String state, String logmessage, int resid, ConnectionSt // This also mean we are no longer connected, ignore bytecount messages until next // CONNECTED // Does not work :( - String msg = getString(resid); - showNotification(msg + " " + logmessage, msg, lowpriority, 0, level); + showNotification(VpnStatus.getLastCleanLogMessage(this), + VpnStatus.getLastCleanLogMessage(this), channel, 0, level, intent); } } + @Override + public void setConnectedVPN(String uuid) { + } + private void doSendBroadcast(String state, ConnectionStatus level) { Intent vpnstatus = new Intent(); vpnstatus.setAction("de.blinkt.openvpn.VPN_STATUS"); @@ -871,13 +1195,13 @@ private void doSendBroadcast(String state, ConnectionStatus level) { public void updateByteCount(long in, long out, long diffIn, long diffOut) { if (mDisplayBytecount) { String netstat = String.format(getString(R.string.statusline_bytecount), - humanReadableByteCount(in, false), - humanReadableByteCount(diffIn / OpenVPNManagement.mBytecountInterval, true), - humanReadableByteCount(out, false), - humanReadableByteCount(diffOut / OpenVPNManagement.mBytecountInterval, true)); + humanReadableByteCount(in, false, getResources()), + humanReadableByteCount(diffIn / OpenVPNManagement.mBytecountInterval, true, getResources()), + humanReadableByteCount(out, false, getResources()), + humanReadableByteCount(diffOut / OpenVPNManagement.mBytecountInterval, true, getResources())); - boolean lowpriority = !mNotificationAlwaysVisible; - showNotification(netstat, null, lowpriority, mConnecttime, LEVEL_CONNECTED); + + showNotification(netstat, null, NOTIFICATION_CHANNEL_BG_ID, mConnecttime, LEVEL_CONNECTED, null); } } @@ -912,10 +1236,80 @@ public String getTunReopenStatus() { } } - public class LocalBinder extends Binder { - public OpenVPNService getService() { - // Return this instance of LocalService so clients can call public methods - return OpenVPNService.this; + public void requestInputFromUser(int resid, String needed) { + VpnStatus.updateStateString("NEED", "need " + needed, resid, LEVEL_WAITING_FOR_USER_INPUT); + showNotification(getString(resid), getString(resid), NOTIFICATION_CHANNEL_NEWSTATUS_ID, 0, LEVEL_WAITING_FOR_USER_INPUT, null); + } + + + public void trigger_sso(String info) { + String channel = NOTIFICATION_CHANNEL_USERREQ_ID; + String method = info.split(":", 2)[0]; + + NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + + Notification.Builder nbuilder = new Notification.Builder(this); + nbuilder.setAutoCancel(true); + int icon = android.R.drawable.ic_dialog_info; + nbuilder.setSmallIcon(icon); + + Intent intent; + + int reason; + if (method.equals("OPEN_URL")) { + String url = info.split(":", 2)[1]; + reason = R.string.openurl_requested; + nbuilder.setContentTitle(getString(reason)); + + nbuilder.setContentText(url); + + + intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(url)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + + } else if (method.equals("CR_TEXT")) { + String challenge = info.split(":", 2)[1]; + reason = R.string.crtext_requested; + nbuilder.setContentTitle(getString(reason)); + nbuilder.setContentText(challenge); + + intent = new Intent(); + intent.setComponent(new ComponentName(this, getPackageName() + ".activities.CredentialsPopup")); + + intent.putExtra(EXTRA_CHALLENGE_TXT, challenge); + + } else { + VpnStatus.logError("Unknown SSO method found: " + method); + return; + } + + // updateStateString trigger the notification of the VPN to be refreshed, save this intent + // to have that notification also this intent to be set + PendingIntent pIntent = PendingIntent.getActivity(this, 0, intent, 0); + VpnStatus.updateStateString("USER_INPUT", "waiting for user input", reason, LEVEL_WAITING_FOR_USER_INPUT, intent); + nbuilder.setContentIntent(pIntent); + + + // Try to set the priority available since API 16 (Jellybean) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + jbNotificationExtras(PRIORITY_MAX, nbuilder); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + lpNotificationExtras(nbuilder, Notification.CATEGORY_STATUS); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + //noinspection NewApi + nbuilder.setChannelId(channel); } + + @SuppressWarnings("deprecation") + Notification notification = nbuilder.getNotification(); + + + int notificationId = channel.hashCode(); + + mNotificationManager.notify(notificationId, notification); } } diff --git a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNStatusService.java b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNStatusService.java new file mode 100644 index 0000000..faae932 --- /dev/null +++ b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNStatusService.java @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.core; + +import android.app.PendingIntent; +import android.app.Service; +import android.content.Intent; +import android.os.Build; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import androidx.annotation.Nullable; +import android.util.Pair; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.lang.ref.WeakReference; + +/** + * Created by arne on 08.11.16. + */ + +public class OpenVPNStatusService extends Service implements VpnStatus.LogListener, VpnStatus.ByteCountListener, VpnStatus.StateListener { + @Nullable + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + + static final RemoteCallbackList mCallbacks = + new RemoteCallbackList<>(); + + @Override + public void onCreate() { + super.onCreate(); + VpnStatus.addLogListener(this); + VpnStatus.addByteCountListener(this); + VpnStatus.addStateListener(this); + mHandler.setService(this); + + } + + @Override + public void onDestroy() { + super.onDestroy(); + + VpnStatus.removeLogListener(this); + VpnStatus.removeByteCountListener(this); + VpnStatus.removeStateListener(this); + mCallbacks.kill(); + + } + + private static final IServiceStatus.Stub mBinder = new IServiceStatus.Stub() { + + @Override + public ParcelFileDescriptor registerStatusCallback(IStatusCallbacks cb) throws RemoteException { + final LogItem[] logbuffer = VpnStatus.getlogbuffer(); + if (mLastUpdateMessage != null) + sendUpdate(cb, mLastUpdateMessage); + + mCallbacks.register(cb); + try { + final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); + new Thread("pushLogs") { + @Override + public void run() { + DataOutputStream fd = new DataOutputStream(new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1])); + try { + synchronized (VpnStatus.readFileLock) { + if (!VpnStatus.readFileLog) { + VpnStatus.readFileLock.wait(); + } + } + } catch (InterruptedException e) { + VpnStatus.logException(e); + } + try { + + for (LogItem logItem : logbuffer) { + byte[] bytes = logItem.getMarschaledBytes(); + fd.writeShort(bytes.length); + fd.write(bytes); + } + // Mark end + fd.writeShort(0x7fff); + fd.close(); + } catch (IOException e) { + e.printStackTrace(); + } + + } + }.start(); + return pipe[0]; + } catch (IOException e) { + e.printStackTrace(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { + throw new RemoteException(e.getMessage()); + } + return null; + } + } + + @Override + public void unregisterStatusCallback(IStatusCallbacks cb) throws RemoteException { + mCallbacks.unregister(cb); + } + + @Override + public String getLastConnectedVPN() throws RemoteException { + return VpnStatus.getLastConnectedVPNProfile(); + } + + @Override + public void setCachedPassword(String uuid, int type, String password) { + PasswordCache.setCachedPassword(uuid, type, password); + } + + @Override + public TrafficHistory getTrafficHistory() throws RemoteException { + return VpnStatus.trafficHistory; + } + + }; + + @Override + public void newLog(LogItem logItem) { + Message msg = mHandler.obtainMessage(SEND_NEW_LOGITEM, logItem); + msg.sendToTarget(); + } + + @Override + public void updateByteCount(long in, long out, long diffIn, long diffOut) { + Message msg = mHandler.obtainMessage(SEND_NEW_BYTECOUNT, Pair.create(in, out)); + msg.sendToTarget(); + } + + static UpdateMessage mLastUpdateMessage; + + static class UpdateMessage { + public String state; + public String logmessage; + public ConnectionStatus level; + public Intent intent; + int resId; + + UpdateMessage(String state, String logmessage, int resId, ConnectionStatus level, Intent intent) { + this.state = state; + this.resId = resId; + this.logmessage = logmessage; + this.level = level; + this.intent = intent; + } + } + + + @Override + public void updateState(String state, String logmessage, int localizedResId, ConnectionStatus level, Intent intent) { + + mLastUpdateMessage = new UpdateMessage(state, logmessage, localizedResId, level, intent); + Message msg = mHandler.obtainMessage(SEND_NEW_STATE, mLastUpdateMessage); + msg.sendToTarget(); + } + + @Override + public void setConnectedVPN(String uuid) { + Message msg = mHandler.obtainMessage(SEND_NEW_CONNECTED_VPN, uuid); + msg.sendToTarget(); + } + + private static final OpenVPNStatusHandler mHandler = new OpenVPNStatusHandler(); + + private static final int SEND_NEW_LOGITEM = 100; + private static final int SEND_NEW_STATE = 101; + private static final int SEND_NEW_BYTECOUNT = 102; + private static final int SEND_NEW_CONNECTED_VPN = 103; + + private static class OpenVPNStatusHandler extends Handler { + WeakReference service = null; + + private void setService(OpenVPNStatusService statusService) { + service = new WeakReference<>(statusService); + } + + @Override + public void handleMessage(Message msg) { + + RemoteCallbackList callbacks; + if (service == null || service.get() == null) + return; + callbacks = service.get().mCallbacks; + // Broadcast to all clients the new value. + final int N = callbacks.beginBroadcast(); + for (int i = 0; i < N; i++) { + + try { + IStatusCallbacks broadcastItem = callbacks.getBroadcastItem(i); + + switch (msg.what) { + case SEND_NEW_LOGITEM: + broadcastItem.newLogItem((LogItem) msg.obj); + break; + case SEND_NEW_BYTECOUNT: + Pair inout = (Pair) msg.obj; + broadcastItem.updateByteCount(inout.first, inout.second); + break; + case SEND_NEW_STATE: + sendUpdate(broadcastItem, (UpdateMessage) msg.obj); + break; + + case SEND_NEW_CONNECTED_VPN: + broadcastItem.connectedVPN((String) msg.obj); + break; + } + } catch (RemoteException e) { + // The RemoteCallbackList will take care of removing + // the dead object for us. + } + } + callbacks.finishBroadcast(); + } + } + + private static void sendUpdate(IStatusCallbacks broadcastItem, + UpdateMessage um) throws RemoteException { + broadcastItem.updateStateString(um.state, um.logmessage, um.resId, um.level, um.intent); + } +} \ No newline at end of file diff --git a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNThread.java b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNThread.java index 696ec19..121376d 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNThread.java +++ b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNThread.java @@ -1,210 +1,214 @@ -/* - * Copyright (c) 2012-2014 Arne Schwabe - * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt - */ - -package de.blinkt.openvpn.core; - -import android.annotation.SuppressLint; -import android.util.Log; - -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.text.SimpleDateFormat; -import java.util.Collections; -import java.util.Date; -import java.util.LinkedList; -import java.util.Locale; -import java.util.Map; -import java.util.Map.Entry; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import ht.vpn.android.R; -import ht.vpn.android.VpnProfile; -import de.blinkt.openvpn.core.VpnStatus.ConnectionStatus; -import de.blinkt.openvpn.core.VpnStatus.LogItem; - -public class OpenVPNThread implements Runnable { - private static final String DUMP_PATH_STRING = "Dump path: "; - @SuppressLint("SdCardPath") - private static final String BROKEN_PIE_SUPPORT = "/data/data/de.blinkt.openvpn/cache/pievpn"; - private final static String BROKEN_PIE_SUPPORT2 = "syntax error"; - private static final String TAG = "OpenVPN"; - public static final int M_FATAL = (1 << 4); - public static final int M_NONFATAL = (1 << 5); - public static final int M_WARN = (1 << 6); - public static final int M_DEBUG = (1 << 7); - private String[] mArgv; - private Process mProcess; - private String mNativeDir; - private OpenVPNService mService; - private String mDumpPath; - private Map mProcessEnv; - private boolean mBrokenPie=false; - - public OpenVPNThread(OpenVPNService service,String[] argv, Map processEnv, String nativelibdir) - { - mArgv = argv; - mNativeDir = nativelibdir; - mService = service; - mProcessEnv = processEnv; - } - - public void stopProcess() { - mProcess.destroy(); - } - - @Override - public void run() { - try { - Log.i(TAG, "Starting openvpn"); - startOpenVPNThreadArgs(mArgv, mProcessEnv); - Log.i(TAG, "Giving up"); - } catch (Exception e) { - VpnStatus.logException("Starting OpenVPN Thread" ,e); - Log.e(TAG, "OpenVPNThread Got " + e.toString()); - } finally { - int exitvalue = 0; - try { - if (mProcess!=null) - exitvalue = mProcess.waitFor(); - } catch ( IllegalThreadStateException ite) { - VpnStatus.logError("Illegal Thread state: " + ite.getLocalizedMessage()); - } catch (InterruptedException ie) { - VpnStatus.logError("InterruptedException: " + ie.getLocalizedMessage()); - } - if( exitvalue != 0) { - VpnStatus.logError("Process exited with exit value " + exitvalue); - if (mBrokenPie) { - /* This will probably fail since the NoPIE binary is probably not written */ - String[] noPieArgv = VPNLaunchHelper.replacePieWithNoPie(mArgv); - - // We are already noPIE, nothing to gain - if (!noPieArgv.equals(mArgv)) { - mArgv = noPieArgv; - VpnStatus.logInfo("PIE Version could not be executed. Trying no PIE version"); - run(); - return; - } - - } - - } - - VpnStatus.updateStateString("NOPROCESS", "No process running.", R.string.state_noprocess, ConnectionStatus.LEVEL_NOTCONNECTED); - if(mDumpPath!=null) { - try { - BufferedWriter logout = new BufferedWriter(new FileWriter(mDumpPath + ".log")); - SimpleDateFormat timeformat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss",Locale.GERMAN); - for(LogItem li : VpnStatus.getlogbuffer()){ - String time = timeformat.format(new Date(li.getLogtime())); - logout.write(time +" " + li.getString(mService) + "\n"); - } - logout.close(); - VpnStatus.logError(R.string.minidump_generated); - } catch (IOException e) { - VpnStatus.logError("Writing minidump log: " + e.getLocalizedMessage()); - } - } - - mService.processDied(); - Log.i(TAG, "Exiting"); - } - } - - private void startOpenVPNThreadArgs(String[] argv, Map env) { - LinkedList argvlist = new LinkedList(); - - Collections.addAll(argvlist, argv); - - ProcessBuilder pb = new ProcessBuilder(argvlist); - // Hack O rama - - String lbpath = genLibraryPath(argv, pb); - - pb.environment().put("LD_LIBRARY_PATH", lbpath); - - // Add extra variables - for(Entry e:env.entrySet()){ - pb.environment().put(e.getKey(), e.getValue()); - } - pb.redirectErrorStream(true); - try { - mProcess = pb.start(); - // Close the output, since we don't need it - mProcess.getOutputStream().close(); - InputStream in = mProcess.getInputStream(); - BufferedReader br = new BufferedReader(new InputStreamReader(in)); - - while(true) { - String logline = br.readLine(); - if(logline==null) - return; - - if (logline.startsWith(DUMP_PATH_STRING)) - mDumpPath = logline.substring(DUMP_PATH_STRING.length()); - - if (logline.startsWith(BROKEN_PIE_SUPPORT) || logline.contains(BROKEN_PIE_SUPPORT2)) - mBrokenPie = true; - - - // 1380308330.240114 18000002 Send to HTTP proxy: 'X-Online-Host: bla.blabla.com' - - Pattern p = Pattern.compile("(\\d+).(\\d+) ([0-9a-f])+ (.*)"); - Matcher m = p.matcher(logline); - if(m.matches()) { - int flags = Integer.parseInt(m.group(3),16); - String msg = m.group(4); - int logLevel = flags & 0x0F; - - VpnStatus.LogLevel logStatus = VpnStatus.LogLevel.INFO; - - if ((flags & M_FATAL) != 0) - logStatus = VpnStatus.LogLevel.ERROR; - else if ((flags & M_NONFATAL)!=0) - logStatus = VpnStatus.LogLevel.WARNING; - else if ((flags & M_WARN)!=0) - logStatus = VpnStatus.LogLevel.WARNING; - else if ((flags & M_DEBUG)!=0) - logStatus = VpnStatus.LogLevel.VERBOSE; - - if (msg.startsWith("MANAGEMENT: CMD")) - logLevel = Math.max(4, logLevel); - - - VpnStatus.logMessageOpenVPN(logStatus,logLevel,msg); - } else { - VpnStatus.logInfo("P:" + logline); - } - } - - - } catch (IOException e) { - VpnStatus.logException("Error reading from output of OpenVPN process" , e); - stopProcess(); - } - - - } - - private String genLibraryPath(String[] argv, ProcessBuilder pb) { - // Hack until I find a good way to get the real library path - String applibpath = argv[0].replaceFirst("/cache/.*$" , "/lib"); - - String lbpath = pb.environment().get("LD_LIBRARY_PATH"); - if(lbpath==null) - lbpath = applibpath; - else - lbpath = applibpath + ":" + lbpath; - - if (!applibpath.equals(mNativeDir)) { - lbpath = mNativeDir + ":" + lbpath; - } - return lbpath; - } -} +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.core; + +import android.annotation.SuppressLint; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedList; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import ht.vpn.android.R; + +public class OpenVPNThread implements Runnable { + private static final String DUMP_PATH_STRING = "Dump path: "; + @SuppressLint("SdCardPath") + private static final String BROKEN_PIE_SUPPORT = "/data/data/de.blinkt.openvpn/cache/pievpn"; + private final static String BROKEN_PIE_SUPPORT2 = "syntax error"; + private static final String TAG = "OpenVPN"; + public static final int M_FATAL = (1 << 4); + public static final int M_NONFATAL = (1 << 5); + public static final int M_WARN = (1 << 6); + public static final int M_DEBUG = (1 << 7); + private String[] mArgv; + private Process mProcess; + private String mNativeDir; + private OpenVPNService mService; + private String mDumpPath; + private boolean mBrokenPie = false; + private boolean mNoProcessExitStatus = false; + + public OpenVPNThread(OpenVPNService service, String[] argv, String nativelibdir) { + mArgv = argv; + mNativeDir = nativelibdir; + mService = service; + } + + public void stopProcess() { + mProcess.destroy(); + } + + void setReplaceConnection() + { + mNoProcessExitStatus=true; + } + + @Override + public void run() { + try { + Log.i(TAG, "Starting openvpn"); + startOpenVPNThreadArgs(mArgv); + Log.i(TAG, "OpenVPN process exited"); + } catch (Exception e) { + VpnStatus.logException("Starting OpenVPN Thread", e); + Log.e(TAG, "OpenVPNThread Got " + e.toString()); + } finally { + int exitvalue = 0; + try { + if (mProcess != null) + exitvalue = mProcess.waitFor(); + } catch (IllegalThreadStateException ite) { + VpnStatus.logError("Illegal Thread state: " + ite.getLocalizedMessage()); + } catch (InterruptedException ie) { + VpnStatus.logError("InterruptedException: " + ie.getLocalizedMessage()); + } + if (exitvalue != 0) { + VpnStatus.logError("Process exited with exit value " + exitvalue); + if (mBrokenPie) { + /* This will probably fail since the NoPIE binary is probably not written */ + String[] noPieArgv = VPNLaunchHelper.replacePieWithNoPie(mArgv); + + // We are already noPIE, nothing to gain + if (!noPieArgv.equals(mArgv)) { + mArgv = noPieArgv; + VpnStatus.logInfo("PIE Version could not be executed. Trying no PIE version"); + run(); + } + + } + + } + + if (!mNoProcessExitStatus) + VpnStatus.updateStateString("NOPROCESS", "No process running.", R.string.state_noprocess, ConnectionStatus.LEVEL_NOTCONNECTED); + + if (mDumpPath != null) { + try { + BufferedWriter logout = new BufferedWriter(new FileWriter(mDumpPath + ".log")); + SimpleDateFormat timeformat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.GERMAN); + for (LogItem li : VpnStatus.getlogbuffer()) { + String time = timeformat.format(new Date(li.getLogtime())); + logout.write(time + " " + li.getString(mService) + "\n"); + } + logout.close(); + VpnStatus.logError(R.string.minidump_generated); + } catch (IOException e) { + VpnStatus.logError("Writing minidump log: " + e.getLocalizedMessage()); + } + } + + if (!mNoProcessExitStatus) + mService.openvpnStopped(); + Log.i(TAG, "Exiting"); + } + } + + private void startOpenVPNThreadArgs(String[] argv) { + LinkedList argvlist = new LinkedList(); + + Collections.addAll(argvlist, argv); + + ProcessBuilder pb = new ProcessBuilder(argvlist); + // Hack O rama + + String lbpath = genLibraryPath(argv, pb); + + pb.environment().put("LD_LIBRARY_PATH", lbpath); + + pb.redirectErrorStream(true); + try { + mProcess = pb.start(); + // Close the output, since we don't need it + mProcess.getOutputStream().close(); + InputStream in = mProcess.getInputStream(); + BufferedReader br = new BufferedReader(new InputStreamReader(in)); + + while (true) { + String logline = br.readLine(); + if (logline == null) + return; + + if (logline.startsWith(DUMP_PATH_STRING)) + mDumpPath = logline.substring(DUMP_PATH_STRING.length()); + + if (logline.startsWith(BROKEN_PIE_SUPPORT) || logline.contains(BROKEN_PIE_SUPPORT2)) + mBrokenPie = true; + + + // 1380308330.240114 18000002 Send to HTTP proxy: 'X-Online-Host: bla.blabla.com' + + Pattern p = Pattern.compile("(\\d+).(\\d+) ([0-9a-f])+ (.*)"); + Matcher m = p.matcher(logline); + int logerror = 0; + if (m.matches()) { + int flags = Integer.parseInt(m.group(3), 16); + String msg = m.group(4); + int logLevel = flags & 0x0F; + + VpnStatus.LogLevel logStatus = VpnStatus.LogLevel.INFO; + + if ((flags & M_FATAL) != 0) + logStatus = VpnStatus.LogLevel.ERROR; + else if ((flags & M_NONFATAL) != 0) + logStatus = VpnStatus.LogLevel.WARNING; + else if ((flags & M_WARN) != 0) + logStatus = VpnStatus.LogLevel.WARNING; + else if ((flags & M_DEBUG) != 0) + logStatus = VpnStatus.LogLevel.VERBOSE; + + if (msg.startsWith("MANAGEMENT: CMD")) + logLevel = Math.max(4, logLevel); + + if ((msg.endsWith("md too weak") && msg.startsWith("OpenSSL: error")) || msg.contains("error:140AB18E")) + logerror = 1; + + VpnStatus.logMessageOpenVPN(logStatus, logLevel, msg); + if (logerror==1) + VpnStatus.logError("OpenSSL reported a certificate with a weak hash, please the in app FAQ about weak hashes"); + + } else { + VpnStatus.logInfo("P:" + logline); + } + + if (Thread.interrupted()) { + throw new InterruptedException("OpenVpn process was killed form java code"); + } + } + } catch (InterruptedException | IOException e) { + VpnStatus.logException("Error reading from output of OpenVPN process", e); + stopProcess(); + } + + + } + + private String genLibraryPath(String[] argv, ProcessBuilder pb) { + // Hack until I find a good way to get the real library path + String applibpath = argv[0].replaceFirst("/cache/.*$", "/lib"); + + String lbpath = pb.environment().get("LD_LIBRARY_PATH"); + if (lbpath == null) + lbpath = applibpath; + else + lbpath = applibpath + ":" + lbpath; + + if (!applibpath.equals(mNativeDir)) { + lbpath = mNativeDir + ":" + lbpath; + } + return lbpath; + } +} diff --git a/app/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java b/app/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java index 3af3ec0..52ea478 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java +++ b/app/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java @@ -1,616 +1,803 @@ -/* - * Copyright (c) 2012-2014 Arne Schwabe - * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt - */ - -package de.blinkt.openvpn.core; - -import android.content.Context; -import android.content.SharedPreferences; -import android.net.LocalServerSocket; -import android.net.LocalSocket; -import android.net.LocalSocketAddress; -import android.os.ParcelFileDescriptor; -import android.preference.PreferenceManager; -import android.support.annotation.NonNull; -import android.util.Log; - -import junit.framework.Assert; - -import java.io.FileDescriptor; -import java.io.IOException; -import java.io.InputStream; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedList; -import java.util.Locale; -import java.util.Vector; - -import ht.vpn.android.BuildConfig; -import ht.vpn.android.R; -import ht.vpn.android.VpnProfile; -import de.blinkt.openvpn.core.VpnStatus.ConnectionStatus; - -public class OpenVpnManagementThread implements Runnable, OpenVPNManagement { - - private static final String TAG = "openvpn"; - private LocalSocket mSocket; - private VpnProfile mProfile; - private OpenVPNService mOpenVPNService; - private LinkedList mFDList = new LinkedList(); - private LocalServerSocket mServerSocket; - private boolean mReleaseHold = true; - private boolean mWaitingForRelease = false; - private long mLastHoldRelease = 0; - - private static final Vector active = new Vector(); - private LocalSocket mServerSocketLocal; - - private pauseReason lastPauseReason = pauseReason.noNetwork; - - public OpenVpnManagementThread(VpnProfile profile, OpenVPNService openVpnService) { - mProfile = profile; - mOpenVPNService = openVpnService; - - - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(openVpnService); - boolean managemeNetworkState = prefs.getBoolean("netchangereconnect", true); - if (managemeNetworkState) - mReleaseHold = false; - - } - - public boolean openManagementInterface(@NonNull Context c) { - // Could take a while to open connection - int tries = 8; - - String socketName = (c.getCacheDir().getAbsolutePath() + "/" + "mgmtsocket"); - // The mServerSocketLocal is transferred to the LocalServerSocket, ignore warning - - mServerSocketLocal = new LocalSocket(); - - while (tries > 0 && !mServerSocketLocal.isConnected()) { - try { - mServerSocketLocal.bind(new LocalSocketAddress(socketName, - LocalSocketAddress.Namespace.FILESYSTEM)); - } catch (IOException e) { - // wait 300 ms before retrying - try { - Thread.sleep(300); - } catch (InterruptedException e1) { - } - - } - tries--; - } - - try { - - mServerSocket = new LocalServerSocket(mServerSocketLocal.getFileDescriptor()); - return true; - } catch (IOException e) { - VpnStatus.logException(e); - } - return false; - - - } - - public void managmentCommand(String cmd) { - try { - if (mSocket != null && mSocket.getOutputStream() != null) { - mSocket.getOutputStream().write(cmd.getBytes()); - mSocket.getOutputStream().flush(); - } - } catch (IOException e) { - // Ignore socket stack traces - } - } - - - @Override - public void run() { - byte[] buffer = new byte[2048]; - // mSocket.setSoTimeout(5); // Setting a timeout cannot be that bad - - String pendingInput = ""; - synchronized (active) { - active.add(this); - } - - try { - // Wait for a client to connect - mSocket = mServerSocket.accept(); - InputStream instream = mSocket.getInputStream(); - // Close the management socket after client connected - - mServerSocket.close(); - // Closing one of the two sockets also closes the other - //mServerSocketLocal.close(); - - while (true) { - int numbytesread = instream.read(buffer); - if (numbytesread == -1) - return; - - FileDescriptor[] fds = null; - try { - fds = mSocket.getAncillaryFileDescriptors(); - } catch (IOException e) { - VpnStatus.logException("Error reading fds from socket", e); - } - if (fds != null) { - Collections.addAll(mFDList, fds); - } - - String input = new String(buffer, 0, numbytesread, "UTF-8"); - - pendingInput += input; - - pendingInput = processInput(pendingInput); - - - } - } catch (IOException e) { - if (!e.getMessage().equals("socket closed") && !e.getMessage().equals("Connection reset by peer")) - VpnStatus.logException(e); - } - synchronized (active) { - active.remove(this); - } - } - - //! Hack O Rama 2000! - private void protectFileDescriptor(FileDescriptor fd) { - Exception exp; - try { - Method getInt = FileDescriptor.class.getDeclaredMethod("getInt$"); - int fdint = (Integer) getInt.invoke(fd); - - // You can even get more evil by parsing toString() and extract the int from that :) - - boolean result = mOpenVPNService.protect(fdint); - if (!result) - VpnStatus.logWarning("Could not protect VPN socket"); - - - //ParcelFileDescriptor pfd = ParcelFileDescriptor.fromFd(fdint); - //pfd.close(); - NativeUtils.jniclose(fdint); - return; - } catch (NoSuchMethodException e) { - exp = e; - } catch (IllegalArgumentException e) { - exp = e; - } catch (IllegalAccessException e) { - exp = e; - } catch (InvocationTargetException e) { - exp = e; - } catch (NullPointerException e) { - exp = e; - } - - Log.d("Openvpn", "Failed to retrieve fd from socket: " + fd); - VpnStatus.logException("Failed to retrieve fd from socket (" + fd + ")", exp); - } - - private String processInput(String pendingInput) { - - - while (pendingInput.contains("\n")) { - String[] tokens = pendingInput.split("\\r?\\n", 2); - processCommand(tokens[0]); - if (tokens.length == 1) - // No second part, newline was at the end - pendingInput = ""; - else - pendingInput = tokens[1]; - } - return pendingInput; - } - - - private void processCommand(String command) { - //Log.i(TAG, "Line from managment" + command); - - - if (command.startsWith(">") && command.contains(":")) { - String[] parts = command.split(":", 2); - String cmd = parts[0].substring(1); - String argument = parts[1]; - - - if (cmd.equals("INFO")) { - /* Ignore greeting from management */ - return; - } else if (cmd.equals("PASSWORD")) { - processPWCommand(argument); - } else if (cmd.equals("HOLD")) { - handleHold(); - } else if (cmd.equals("NEED-OK")) { - processNeedCommand(argument); - } else if (cmd.equals("BYTECOUNT")) { - processByteCount(argument); - } else if (cmd.equals("STATE")) { - processState(argument); - } else if (cmd.equals("PROXY")) { - processProxyCMD(argument); - } else if (cmd.equals("LOG")) { - processLogMessage(argument); - } else if (cmd.equals("RSA_SIGN")) { - processSignCommand(argument); - } else { - VpnStatus.logWarning("MGMT: Got unrecognized command" + command); - Log.i(TAG, "Got unrecognized command" + command); - } - } else if (command.startsWith("SUCCESS:")) { - /* Ignore this kind of message too */ - return; - } else if (command.startsWith("PROTECTFD: ")) { - FileDescriptor fdtoprotect = mFDList.pollFirst(); - if (fdtoprotect != null) - protectFileDescriptor(fdtoprotect); - } else { - Log.i(TAG, "Got unrecognized line from managment" + command); - VpnStatus.logWarning("MGMT: Got unrecognized line from management:" + command); - } - } - - private void processLogMessage(String argument) { - String[] args = argument.split(",", 4); - // 0 unix time stamp - // 1 log level N,I,E etc. - /* - (b) zero or more message flags in a single string: - I -- informational - F -- fatal error - N -- non-fatal error - W -- warning - D -- debug, and - */ - // 2 log message - - Log.d("OpenVPN", argument); - - VpnStatus.LogLevel level; - if (args[1].equals("I")) { - level = VpnStatus.LogLevel.INFO; - } else if (args[1].equals("W")) { - level = VpnStatus.LogLevel.WARNING; - } else if (args[1].equals("D")) { - level = VpnStatus.LogLevel.VERBOSE; - } else if (args[1].equals("F")) { - level = VpnStatus.LogLevel.ERROR; - } else { - level = VpnStatus.LogLevel.INFO; - } - - int ovpnlevel = Integer.parseInt(args[2]) & 0x0F; - String msg = args[3]; - - if (msg.startsWith("MANAGEMENT: CMD")) - ovpnlevel = Math.max(4, ovpnlevel); - - VpnStatus.logMessageOpenVPN(level, ovpnlevel, msg); - } - - private void handleHold() { - if (mReleaseHold) { - releaseHoldCmd(); - } else { - mWaitingForRelease = true; - - VpnStatus.updateStatePause(lastPauseReason); - - - } - } - - private void releaseHoldCmd() { - if ((System.currentTimeMillis() - mLastHoldRelease) < 5000) { - try { - Thread.sleep(3000); - } catch (InterruptedException ignored) { - } - - } - mWaitingForRelease = false; - mLastHoldRelease = System.currentTimeMillis(); - managmentCommand("hold release\n"); - managmentCommand("bytecount " + mBytecountInterval + "\n"); - managmentCommand("state on\n"); - //managmentCommand("log on all\n"); - } - - public void releaseHold() { - mReleaseHold = true; - if (mWaitingForRelease) - releaseHoldCmd(); - - } - - private void processProxyCMD(String argument) { - String[] args = argument.split(",", 3); - SocketAddress proxyaddr = ProxyDetection.detectProxy(mProfile); - - - if (args.length >= 2) { - String proto = args[1]; - if (proto.equals("UDP")) { - proxyaddr = null; - } - } - - if (proxyaddr instanceof InetSocketAddress) { - InetSocketAddress isa = (InetSocketAddress) proxyaddr; - - VpnStatus.logInfo(R.string.using_proxy, isa.getHostName(), isa.getPort()); - - String proxycmd = String.format(Locale.ENGLISH, "proxy HTTP %s %d\n", isa.getHostName(), isa.getPort()); - managmentCommand(proxycmd); - } else { - managmentCommand("proxy NONE\n"); - } - - } - - private void processState(String argument) { - String[] args = argument.split(",", 3); - String currentstate = args[1]; - - if (args[2].equals(",,")) - VpnStatus.updateStateString(currentstate, ""); - else - VpnStatus.updateStateString(currentstate, args[2]); - } - - - private void processByteCount(String argument) { - // >BYTECOUNT:{BYTES_IN},{BYTES_OUT} - int comma = argument.indexOf(','); - long in = Long.parseLong(argument.substring(0, comma)); - long out = Long.parseLong(argument.substring(comma + 1)); - - VpnStatus.updateByteCount(in, out); - - } - - - private void processNeedCommand(String argument) { - int p1 = argument.indexOf('\''); - int p2 = argument.indexOf('\'', p1 + 1); - - String needed = argument.substring(p1 + 1, p2); - String extra = argument.split(":", 2)[1]; - - String status = "ok"; - - - if (needed.equals("PROTECTFD")) { - FileDescriptor fdtoprotect = mFDList.pollFirst(); - protectFileDescriptor(fdtoprotect); - } else if (needed.equals("DNSSERVER")) { - mOpenVPNService.addDNS(extra); - } else if (needed.equals("DNSDOMAIN")) { - mOpenVPNService.setDomain(extra); - } else if (needed.equals("ROUTE")) { - String[] routeparts = extra.split(" "); - - /* - buf_printf (&out, "%s %s %s dev %s", network, netmask, gateway, rgi->iface); - else - buf_printf (&out, "%s %s %s", network, netmask, gateway); - */ - - if (routeparts.length == 5) { - if (BuildConfig.DEBUG) Assert.assertEquals("dev", routeparts[3]); - mOpenVPNService.addRoute(routeparts[0], routeparts[1], routeparts[2], routeparts[4]); - } else if (routeparts.length >= 3) { - mOpenVPNService.addRoute(routeparts[0], routeparts[1], routeparts[2], null); - } else { - VpnStatus.logError("Unrecognized ROUTE cmd:" + Arrays.toString(routeparts) + " | " + argument); - } - - } else if (needed.equals("ROUTE6")) { - String[] routeparts = extra.split(" "); - mOpenVPNService.addRoutev6(routeparts[0], routeparts[1]); - } else if (needed.equals("IFCONFIG")) { - String[] ifconfigparts = extra.split(" "); - int mtu = Integer.parseInt(ifconfigparts[2]); - mOpenVPNService.setLocalIP(ifconfigparts[0], ifconfigparts[1], mtu, ifconfigparts[3]); - } else if (needed.equals("IFCONFIG6")) { - mOpenVPNService.setLocalIPv6(extra); - - } else if (needed.equals("PERSIST_TUN_ACTION")) { - // check if tun cfg stayed the same - status = mOpenVPNService.getTunReopenStatus(); - } else if (needed.equals("OPENTUN")) { - if (sendTunFD(needed, extra)) - return; - else - status = "cancel"; - // This not nice or anything but setFileDescriptors accepts only FilDescriptor class :( - - } else { - Log.e(TAG, "Unkown needok command " + argument); - return; - } - - String cmd = String.format("needok '%s' %s\n", needed, status); - managmentCommand(cmd); - } - - private boolean sendTunFD(String needed, String extra) { - Exception exp; - if (!extra.equals("tun")) { - // We only support tun - VpnStatus.logError(String.format("Device type %s requested, but only tun is possible with the Android API, sorry!", extra)); - - return false; - } - ParcelFileDescriptor pfd = mOpenVPNService.openTun(); - if (pfd == null) - return false; - - Method setInt; - int fdint = pfd.getFd(); - try { - setInt = FileDescriptor.class.getDeclaredMethod("setInt$", int.class); - FileDescriptor fdtosend = new FileDescriptor(); - - setInt.invoke(fdtosend, fdint); - - FileDescriptor[] fds = {fdtosend}; - mSocket.setFileDescriptorsForSend(fds); - - // Trigger a send so we can close the fd on our side of the channel - // The API documentation fails to mention that it will not reset the file descriptor to - // be send and will happily send the file descriptor on every write ... - String cmd = String.format("needok '%s' %s\n", needed, "ok"); - managmentCommand(cmd); - - // Set the FileDescriptor to null to stop this mad behavior - mSocket.setFileDescriptorsForSend(null); - - pfd.close(); - - return true; - } catch (NoSuchMethodException e) { - exp = e; - } catch (IllegalArgumentException e) { - exp = e; - } catch (IllegalAccessException e) { - exp = e; - } catch (InvocationTargetException e) { - exp = e; - } catch (IOException e) { - exp = e; - } - VpnStatus.logException("Could not send fd over socket", exp); - - return false; - } - - private void processPWCommand(String argument) { - //argument has the form Need 'Private Key' password - // or ">PASSWORD:Verification Failed: '%s' ['%s']" - String needed; - - - try { - - int p1 = argument.indexOf('\''); - int p2 = argument.indexOf('\'', p1 + 1); - needed = argument.substring(p1 + 1, p2); - if (argument.startsWith("Verification Failed")) { - proccessPWFailed(needed, argument.substring(p2 + 1)); - return; - } - } catch (StringIndexOutOfBoundsException sioob) { - VpnStatus.logError("Could not parse management Password command: " + argument); - return; - } - - String pw = null; - - if (needed.equals("Private Key")) { - pw = mProfile.getPasswordPrivateKey(); - } else if (needed.equals("Auth")) { - String usercmd = String.format("username '%s' %s\n", - needed, VpnProfile.openVpnEscape(mProfile.mUsername)); - managmentCommand(usercmd); - pw = mProfile.getPasswordAuth(); - } - if (pw != null) { - String cmd = String.format("password '%s' %s\n", needed, VpnProfile.openVpnEscape(pw)); - managmentCommand(cmd); - } else { - VpnStatus.logError(String.format("Openvpn requires Authentication type '%s' but no password/key information available", needed)); - } - - } - - - private void proccessPWFailed(String needed, String args) { - VpnStatus.updateStateString("AUTH_FAILED", needed + args, R.string.state_auth_failed, ConnectionStatus.LEVEL_AUTH_FAILED); - } - - - private static boolean stopOpenVPN() { - synchronized (active) { - boolean sendCMD = false; - for (OpenVpnManagementThread mt : active) { - mt.managmentCommand("signal SIGINT\n"); - sendCMD = true; - try { - if (mt.mSocket != null) - mt.mSocket.close(); - } catch (IOException e) { - // Ignore close error on already closed socket - } - } - return sendCMD; - } - } - - @Override - public void networkChange(boolean samenetwork) { - if (!mWaitingForRelease) - if (samenetwork) - managmentCommand("network-change samenetwork\n"); - else - managmentCommand("network-change\n"); - } - - public void signalusr1() { - mReleaseHold = false; - - if (!mWaitingForRelease) - managmentCommand("signal SIGUSR1\n"); - else - // If signalusr1 is called update the state string - // if there is another for stopping - VpnStatus.updateStatePause(lastPauseReason); - } - - public void reconnect() { - signalusr1(); - releaseHold(); - } - - private void processSignCommand(String b64data) { - - String signed_string = mProfile.getSignedData(b64data); - if (signed_string == null) { - managmentCommand("rsa-sig\n"); - managmentCommand("\nEND\n"); - stopOpenVPN(); - return; - } - managmentCommand("rsa-sig\n"); - managmentCommand(signed_string); - managmentCommand("\nEND\n"); - } - - @Override - public void pause(pauseReason reason) { - lastPauseReason = reason; - signalusr1(); - } - - @Override - public void resume() { - releaseHold(); - /* Reset the reason why we are disconnected */ - lastPauseReason = pauseReason.noNetwork; - } - - @Override - public boolean stopVPN() { - return stopOpenVPN(); - } -} +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.core; + +import android.content.Context; +import android.content.Intent; +import android.net.LocalServerSocket; +import android.net.LocalSocket; +import android.net.LocalSocketAddress; +import android.os.Build; +import android.os.Handler; +import android.os.ParcelFileDescriptor; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import android.system.Os; +import android.util.Log; +import ht.vpn.android.R; +import de.blinkt.openvpn.VpnProfile; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.*; + +public class OpenVpnManagementThread implements Runnable, OpenVPNManagement { + + public static final int ORBOT_TIMEOUT_MS = 20 * 1000; + private static final String TAG = "openvpn"; + private static final Vector active = new Vector<>(); + private final Handler mResumeHandler; + private LocalSocket mSocket; + private VpnProfile mProfile; + private OpenVPNService mOpenVPNService; + private LinkedList mFDList = new LinkedList<>(); + private LocalServerSocket mServerSocket; + private boolean mWaitingForRelease = false; + private long mLastHoldRelease = 0; + private LocalSocket mServerSocketLocal; + + private pauseReason lastPauseReason = pauseReason.noNetwork; + private PausedStateCallback mPauseCallback; + private boolean mShuttingDown; + private Runnable mResumeHoldRunnable = new Runnable() { + @Override + public void run() { + if (OpenVpnManagementThread.this.shouldBeRunning()) { + OpenVpnManagementThread.this.releaseHoldCmd(); + } + } + }; + private Runnable orbotStatusTimeOutRunnable = new Runnable() { + @Override + public void run() { + sendProxyCMD(Connection.ProxyType.SOCKS5, "127.0.0.1", Integer.toString(OrbotHelper.SOCKS_PROXY_PORT_DEFAULT), false); + OrbotHelper.get(mOpenVPNService).removeStatusCallback(statusCallback); + + } + }; + private OrbotHelper.StatusCallback statusCallback = new OrbotHelper.StatusCallback() { + + @Override + public void onStatus(Intent statusIntent) { + StringBuilder extras = new StringBuilder(); + for (String key : statusIntent.getExtras().keySet()) { + Object val = statusIntent.getExtras().get(key); + + extras.append(String.format(Locale.ENGLISH, "%s - '%s'", key, val == null ? "null" : val.toString())); + } + VpnStatus.logDebug("Got Orbot status: " + extras); + } + + @Override + public void onNotYetInstalled() { + VpnStatus.logDebug("Orbot not yet installed"); + } + + @Override + public void onOrbotReady(Intent intent, String socksHost, int socksPort) { + mResumeHandler.removeCallbacks(orbotStatusTimeOutRunnable); + sendProxyCMD(Connection.ProxyType.SOCKS5, socksHost, Integer.toString(socksPort), false); + OrbotHelper.get(mOpenVPNService).removeStatusCallback(this); + } + + @Override + public void onDisabled(Intent intent) { + VpnStatus.logWarning("Orbot integration for external applications is disabled. Waiting %ds before connecting to the default port. Enable external app integration in Orbot or use Socks v5 config instead of Orbot to avoid this delay."); + } + }; + private transient Connection mCurrentProxyConnection; + + public OpenVpnManagementThread(VpnProfile profile, OpenVPNService openVpnService) { + mProfile = profile; + mOpenVPNService = openVpnService; + mResumeHandler = new Handler(openVpnService.getMainLooper()); + + } + + private static boolean stopOpenVPN() { + synchronized (active) { + boolean sendCMD = false; + for (OpenVpnManagementThread mt : active) { + sendCMD = mt.managmentCommand("signal SIGINT\n"); + try { + if (mt.mSocket != null) + mt.mSocket.close(); + } catch (IOException e) { + // Ignore close error on already closed socket + } + } + return sendCMD; + } + } + + public boolean openManagementInterface(@NonNull Context c) { + // Could take a while to open connection + int tries = 8; + + String socketName = (c.getCacheDir().getAbsolutePath() + "/" + "mgmtsocket"); + // The mServerSocketLocal is transferred to the LocalServerSocket, ignore warning + + mServerSocketLocal = new LocalSocket(); + + while (tries > 0 && !mServerSocketLocal.isBound()) { + try { + mServerSocketLocal.bind(new LocalSocketAddress(socketName, + LocalSocketAddress.Namespace.FILESYSTEM)); + } catch (IOException e) { + // wait 300 ms before retrying + try { + Thread.sleep(300); + } catch (InterruptedException ignored) { + } + + } + tries--; + } + + try { + + mServerSocket = new LocalServerSocket(mServerSocketLocal.getFileDescriptor()); + return true; + } catch (IOException e) { + VpnStatus.logException(e); + } + return false; + + + } + + /** + * @param cmd command to write to management socket + * @return true if command have been sent + */ + public boolean managmentCommand(String cmd) { + try { + if (mSocket != null && mSocket.getOutputStream() != null) { + mSocket.getOutputStream().write(cmd.getBytes()); + mSocket.getOutputStream().flush(); + return true; + } + } catch (IOException e) { + // Ignore socket stack traces + } + return false; + } + + @Override + public void run() { + byte[] buffer = new byte[2048]; + // mSocket.setSoTimeout(5); // Setting a timeout cannot be that bad + + String pendingInput = ""; + synchronized (active) { + active.add(this); + } + + try { + // Wait for a client to connect + mSocket = mServerSocket.accept(); + InputStream instream = mSocket.getInputStream(); + + + // Close the management socket after client connected + try { + mServerSocket.close(); + } catch (IOException e) { + VpnStatus.logException(e); + } + + // Closing one of the two sockets also closes the other + //mServerSocketLocal.close(); + managmentCommand("version 3\n"); + + while (true) { + + int numbytesread = instream.read(buffer); + if (numbytesread == -1) + return; + + FileDescriptor[] fds = null; + try { + fds = mSocket.getAncillaryFileDescriptors(); + } catch (IOException e) { + VpnStatus.logException("Error reading fds from socket", e); + } + if (fds != null) { + Collections.addAll(mFDList, fds); + } + + String input = new String(buffer, 0, numbytesread, "UTF-8"); + + pendingInput += input; + + pendingInput = processInput(pendingInput); + + + } + } catch (IOException e) { + if (!e.getMessage().equals("socket closed") && !e.getMessage().equals("Connection reset by peer")) + VpnStatus.logException(e); + } + synchronized (active) { + active.remove(this); + } + } + + //! Hack O Rama 2000! + private void protectFileDescriptor(FileDescriptor fd) { + try { + Method getInt = FileDescriptor.class.getDeclaredMethod("getInt$"); + int fdint = (Integer) getInt.invoke(fd); + + // You can even get more evil by parsing toString() and extract the int from that :) + + boolean result = mOpenVPNService.protect(fdint); + if (!result) + VpnStatus.logWarning("Could not protect VPN socket"); + + + //ParcelFileDescriptor pfd = ParcelFileDescriptor.fromFd(fdint); + //pfd.close(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + fdCloseLollipop(fd); + } else { + NativeUtils.jniclose(fdint); + } + return; + } catch ( NoSuchMethodException | IllegalArgumentException | InvocationTargetException | IllegalAccessException | NullPointerException e) { + VpnStatus.logException("Failed to retrieve fd from socket (" + fd + ")", e); + } + + Log.d("Openvpn", "Failed to retrieve fd from socket: " + fd); + + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + private void fdCloseLollipop(FileDescriptor fd) { + try { + Os.close(fd); + } catch (Exception e) { + VpnStatus.logException("Failed to close fd (" + fd + ")", e); + } + } + + private String processInput(String pendingInput) { + + + while (pendingInput.contains("\n")) { + String[] tokens = pendingInput.split("\\r?\\n", 2); + processCommand(tokens[0]); + if (tokens.length == 1) + // No second part, newline was at the end + pendingInput = ""; + else + pendingInput = tokens[1]; + } + return pendingInput; + } + + private void processCommand(String command) { + //Log.i(TAG, "Line from managment" + command); + + if (command.startsWith(">") && command.contains(":")) { + String[] parts = command.split(":", 2); + String cmd = parts[0].substring(1); + String argument = parts[1]; + + switch (cmd) { + case "INFO": + /* Ignore greeting from management */ + return; + case "PASSWORD": + processPWCommand(argument); + break; + case "HOLD": + handleHold(argument); + break; + case "NEED-OK": + processNeedCommand(argument); + break; + case "BYTECOUNT": + processByteCount(argument); + break; + case "STATE": + if (!mShuttingDown) + processState(argument); + break; + case "PROXY": + processProxyCMD(argument); + break; + case "LOG": + processLogMessage(argument); + break; + case "PK_SIGN": + processSignCommand(argument); + break; + case "INFOMSG": + processInfoMessage(argument); + break; + default: + VpnStatus.logWarning("MGMT: Got unrecognized command" + command); + Log.i(TAG, "Got unrecognized command" + command); + break; + } + } else if (command.startsWith("SUCCESS:")) { + /* Ignore this kind of message too */ + return; + } else if (command.startsWith("PROTECTFD: ")) { + FileDescriptor fdtoprotect = mFDList.pollFirst(); + if (fdtoprotect != null) + protectFileDescriptor(fdtoprotect); + } else { + Log.i(TAG, "Got unrecognized line from managment" + command); + VpnStatus.logWarning("MGMT: Got unrecognized line from management:" + command); + } + } + + private void processInfoMessage(String info) + { + if (info.startsWith("OPEN_URL:") || info.startsWith("CR_TEXT:")) + { + mOpenVPNService.trigger_sso(info); + } + else + { + VpnStatus.logDebug("Info message from server:" + info); + } + } + + private void processLogMessage(String argument) { + String[] args = argument.split(",", 4); + // 0 unix time stamp + // 1 log level N,I,E etc. + /* + (b) zero or more message flags in a single string: + I -- informational + F -- fatal error + N -- non-fatal error + W -- warning + D -- debug, and + */ + // 2 log message + + Log.d("OpenVPN", argument); + + VpnStatus.LogLevel level; + switch (args[1]) { + case "I": + level = VpnStatus.LogLevel.INFO; + break; + case "W": + level = VpnStatus.LogLevel.WARNING; + break; + case "D": + level = VpnStatus.LogLevel.VERBOSE; + break; + case "F": + level = VpnStatus.LogLevel.ERROR; + break; + default: + level = VpnStatus.LogLevel.INFO; + break; + } + + int ovpnlevel = Integer.parseInt(args[2]) & 0x0F; + String msg = args[3]; + + if (msg.startsWith("MANAGEMENT: CMD")) + ovpnlevel = Math.max(4, ovpnlevel); + + VpnStatus.logMessageOpenVPN(level, ovpnlevel, msg); + } + + boolean shouldBeRunning() { + if (mPauseCallback == null) + return false; + else + return mPauseCallback.shouldBeRunning(); + } + + private void handleHold(String argument) { + mWaitingForRelease = true; + int waittime = Integer.parseInt(argument.split(":")[1]); + if (shouldBeRunning()) { + if (waittime > 1) + VpnStatus.updateStateString("CONNECTRETRY", String.valueOf(waittime), + R.string.state_waitconnectretry, ConnectionStatus.LEVEL_CONNECTING_NO_SERVER_REPLY_YET); + mResumeHandler.postDelayed(mResumeHoldRunnable, waittime * 1000); + if (waittime > 5) + VpnStatus.logInfo(R.string.state_waitconnectretry, String.valueOf(waittime)); + else + VpnStatus.logDebug(R.string.state_waitconnectretry, String.valueOf(waittime)); + + } else { + VpnStatus.updateStatePause(lastPauseReason); + } + } + + private void releaseHoldCmd() { + mResumeHandler.removeCallbacks(mResumeHoldRunnable); + if ((System.currentTimeMillis() - mLastHoldRelease) < 5000) { + try { + Thread.sleep(3000); + } catch (InterruptedException ignored) { + } + + } + mWaitingForRelease = false; + mLastHoldRelease = System.currentTimeMillis(); + managmentCommand("hold release\n"); + managmentCommand("bytecount " + mBytecountInterval + "\n"); + managmentCommand("state on\n"); + //managmentCommand("log on all\n"); + } + + public void releaseHold() { + if (mWaitingForRelease) + releaseHoldCmd(); + } + + private void processProxyCMD(String argument) { + String[] args = argument.split(",", 3); + + Connection.ProxyType proxyType = Connection.ProxyType.NONE; + + int connectionEntryNumber = Integer.parseInt(args[0]) - 1; + String proxyport = null; + String proxyname = null; + boolean proxyUseAuth = false; + + if (mProfile.mConnections.length > connectionEntryNumber) { + Connection connection = mProfile.mConnections[connectionEntryNumber]; + proxyType = connection.mProxyType; + proxyname = connection.mProxyName; + proxyport = connection.mProxyPort; + proxyUseAuth = connection.mUseProxyAuth; + + // Use transient variable to remember http user/password + mCurrentProxyConnection = connection; + + } else { + VpnStatus.logError(String.format(Locale.ENGLISH, "OpenVPN is asking for a proxy of an unknown connection entry (%d)", connectionEntryNumber)); + } + + // atuo detection of proxy + if (proxyType == Connection.ProxyType.NONE) { + SocketAddress proxyaddr = ProxyDetection.detectProxy(mProfile); + if (proxyaddr instanceof InetSocketAddress) { + InetSocketAddress isa = (InetSocketAddress) proxyaddr; + proxyType = Connection.ProxyType.HTTP; + proxyname = isa.getHostName(); + proxyport = String.valueOf(isa.getPort()); + proxyUseAuth = false; + + } + } + + + if (args.length >= 2 && proxyType == Connection.ProxyType.HTTP) { + String proto = args[1]; + if (proto.equals("UDP")) { + proxyname = null; + VpnStatus.logInfo("Not using an HTTP proxy since the connection uses UDP"); + } + } + + + if (proxyType == Connection.ProxyType.ORBOT) { + VpnStatus.updateStateString("WAIT_ORBOT", "Waiting for Orbot to start", R.string.state_waitorbot, ConnectionStatus.LEVEL_CONNECTING_NO_SERVER_REPLY_YET); + OrbotHelper orbotHelper = OrbotHelper.get(mOpenVPNService); + if (!orbotHelper.checkTorReceier(mOpenVPNService)) + VpnStatus.logError("Orbot does not seem to be installed!"); + + mResumeHandler.postDelayed(orbotStatusTimeOutRunnable, ORBOT_TIMEOUT_MS); + orbotHelper.addStatusCallback(mOpenVPNService, statusCallback); + + orbotHelper.sendOrbotStartAndStatusBroadcast(); + + } else { + sendProxyCMD(proxyType, proxyname, proxyport, proxyUseAuth); + } + } + + private void sendProxyCMD(Connection.ProxyType proxyType, String proxyname, String proxyport, boolean usePwAuth) { + if (proxyType != Connection.ProxyType.NONE && proxyname != null) { + + VpnStatus.logInfo(R.string.using_proxy, proxyname, proxyname); + + String pwstr = usePwAuth ? " auto" : ""; + + String proxycmd = String.format(Locale.ENGLISH, "proxy %s %s %s%s\n", + proxyType == Connection.ProxyType.HTTP ? "HTTP" : "SOCKS", + proxyname, proxyport, pwstr); + managmentCommand(proxycmd); + } else { + managmentCommand("proxy NONE\n"); + } + } + + private void processState(String argument) { + String[] args = argument.split(",", 3); + String currentstate = args[1]; + + if (args[2].equals(",,")) + VpnStatus.updateStateString(currentstate, ""); + else + VpnStatus.updateStateString(currentstate, args[2]); + } + + private void processByteCount(String argument) { + // >BYTECOUNT:{BYTES_IN},{BYTES_OUT} + int comma = argument.indexOf(','); + long in = Long.parseLong(argument.substring(0, comma)); + long out = Long.parseLong(argument.substring(comma + 1)); + + VpnStatus.updateByteCount(in, out); + + } + + private void processNeedCommand(String argument) { + int p1 = argument.indexOf('\''); + int p2 = argument.indexOf('\'', p1 + 1); + + String needed = argument.substring(p1 + 1, p2); + String extra = argument.split(":", 2)[1]; + + String status = "ok"; + + + switch (needed) { + case "PROTECTFD": + FileDescriptor fdtoprotect = mFDList.pollFirst(); + protectFileDescriptor(fdtoprotect); + break; + case "DNSSERVER": + case "DNS6SERVER": + mOpenVPNService.addDNS(extra); + break; + case "DNSDOMAIN": + mOpenVPNService.setDomain(extra); + break; + case "ROUTE": { + String[] routeparts = extra.split(" "); + + /* + buf_printf (&out, "%s %s %s dev %s", network, netmask, gateway, rgi->iface); + else + buf_printf (&out, "%s %s %s", network, netmask, gateway); + */ + + if (routeparts.length == 5) { + //if (BuildConfig.DEBUG) + // assertEquals("dev", routeparts[3]); + mOpenVPNService.addRoute(routeparts[0], routeparts[1], routeparts[2], routeparts[4]); + } else if (routeparts.length >= 3) { + mOpenVPNService.addRoute(routeparts[0], routeparts[1], routeparts[2], null); + } else { + VpnStatus.logError("Unrecognized ROUTE cmd:" + Arrays.toString(routeparts) + " | " + argument); + } + + break; + } + case "ROUTE6": { + String[] routeparts = extra.split(" "); + mOpenVPNService.addRoutev6(routeparts[0], routeparts[1]); + break; + } + case "IFCONFIG": + String[] ifconfigparts = extra.split(" "); + int mtu = Integer.parseInt(ifconfigparts[2]); + mOpenVPNService.setLocalIP(ifconfigparts[0], ifconfigparts[1], mtu, ifconfigparts[3]); + break; + case "IFCONFIG6": + String[] ifconfig6parts = extra.split(" "); + mtu = Integer.parseInt(ifconfig6parts[1]); + mOpenVPNService.setMtu(mtu); + mOpenVPNService.setLocalIPv6(ifconfig6parts[0]); + break; + case "PERSIST_TUN_ACTION": + // check if tun cfg stayed the same + status = mOpenVPNService.getTunReopenStatus(); + break; + case "OPENTUN": + if (sendTunFD(needed, extra)) + return; + else + status = "cancel"; + // This not nice or anything but setFileDescriptors accepts only FilDescriptor class :( + + break; + default: + Log.e(TAG, "Unknown needok command " + argument); + return; + } + + String cmd = String.format("needok '%s' %s\n", needed, status); + managmentCommand(cmd); + } + + private boolean sendTunFD(String needed, String extra) { + if (!extra.equals("tun")) { + // We only support tun + VpnStatus.logError(String.format("Device type %s requested, but only tun is possible with the Android API, sorry!", extra)); + + return false; + } + ParcelFileDescriptor pfd = mOpenVPNService.openTun(); + if (pfd == null) + return false; + + Method setInt; + int fdint = pfd.getFd(); + try { + setInt = FileDescriptor.class.getDeclaredMethod("setInt$", int.class); + FileDescriptor fdtosend = new FileDescriptor(); + + setInt.invoke(fdtosend, fdint); + + FileDescriptor[] fds = {fdtosend}; + mSocket.setFileDescriptorsForSend(fds); + + // Trigger a send so we can close the fd on our side of the channel + // The API documentation fails to mention that it will not reset the file descriptor to + // be send and will happily send the file descriptor on every write ... + String cmd = String.format("needok '%s' %s\n", needed, "ok"); + managmentCommand(cmd); + + // Set the FileDescriptor to null to stop this mad behavior + mSocket.setFileDescriptorsForSend(null); + + pfd.close(); + + return true; + } catch (NoSuchMethodException | IllegalArgumentException | InvocationTargetException | + IOException | IllegalAccessException exp) { + VpnStatus.logException("Could not send fd over socket", exp); + } + + return false; + } + + private void processPWCommand(String argument) { + //argument has the form Need 'Private Key' password + // or ">PASSWORD:Verification Failed: '%s' ['%s']" + String needed; + + + try { + // Ignore Auth token message, already managed by openvpn itself + if (argument.startsWith("Auth-Token:")) { + return; + } + + int p1 = argument.indexOf('\''); + int p2 = argument.indexOf('\'', p1 + 1); + needed = argument.substring(p1 + 1, p2); + if (argument.startsWith("Verification Failed")) { + proccessPWFailed(needed, argument.substring(p2 + 1)); + return; + } + } catch (StringIndexOutOfBoundsException sioob) { + VpnStatus.logError("Could not parse management Password command: " + argument); + return; + } + + String pw = null; + String username = null; + + if (needed.equals("Private Key")) { + pw = mProfile.getPasswordPrivateKey(); + } else if (needed.equals("Auth")) { + pw = mProfile.getPasswordAuth(); + username = mProfile.mUsername; + + } else if (needed.equals("HTTP Proxy")) { + if( mCurrentProxyConnection != null) { + pw = mCurrentProxyConnection.mProxyAuthPassword; + username = mCurrentProxyConnection.mProxyAuthUser; + } + } + if (pw != null) { + if (username !=null) { + String usercmd = String.format("username '%s' %s\n", + needed, VpnProfile.openVpnEscape(username)); + managmentCommand(usercmd); + } + String cmd = String.format("password '%s' %s\n", needed, VpnProfile.openVpnEscape(pw)); + managmentCommand(cmd); + } else { + mOpenVPNService.requestInputFromUser(R.string.password, needed); + VpnStatus.logError(String.format("Openvpn requires Authentication type '%s' but no password/key information available", needed)); + } + + } + + private void proccessPWFailed(String needed, String args) { + VpnStatus.updateStateString("AUTH_FAILED", needed + args, R.string.state_auth_failed, ConnectionStatus.LEVEL_AUTH_FAILED); + } + + @Override + public void networkChange(boolean samenetwork) { + if (mWaitingForRelease) + releaseHold(); + else if (samenetwork) + managmentCommand("network-change samenetwork\n"); + else + managmentCommand("network-change\n"); + } + + @Override + public void setPauseCallback(PausedStateCallback callback) { + mPauseCallback = callback; + } + + @Override + public void sendCRResponse(String response) { + managmentCommand("cr-response " + response + "\n"); + } + + public void signalusr1() { + mResumeHandler.removeCallbacks(mResumeHoldRunnable); + if (!mWaitingForRelease) + managmentCommand("signal SIGUSR1\n"); + else + // If signalusr1 is called update the state string + // if there is another for stopping + VpnStatus.updateStatePause(lastPauseReason); + } + + public void reconnect() { + signalusr1(); + releaseHold(); + } + + private void processSignCommand(String argument) { + + String[] arguments = argument.split(","); + + boolean pkcs1padding = arguments[1].equals("RSA_PKCS1_PADDING"); + String signed_string = mProfile.getSignedData(mOpenVPNService, arguments[0], pkcs1padding); + + if (signed_string == null) { + managmentCommand("pk-sig\n"); + managmentCommand("\nEND\n"); + stopOpenVPN(); + return; + } + managmentCommand("pk-sig\n"); + managmentCommand(signed_string); + managmentCommand("\nEND\n"); + } + + @Override + public void pause(pauseReason reason) { + lastPauseReason = reason; + signalusr1(); + } + + @Override + public void resume() { + releaseHold(); + /* Reset the reason why we are disconnected */ + lastPauseReason = pauseReason.noNetwork; + } + + @Override + public boolean stopVPN(boolean replaceConnection) { + boolean stopSucceed = stopOpenVPN(); + if (stopSucceed) { + mShuttingDown = true; + + } + return stopSucceed; + } + +} diff --git a/app/src/main/java/de/blinkt/openvpn/core/OrbotHelper.java b/app/src/main/java/de/blinkt/openvpn/core/OrbotHelper.java new file mode 100644 index 0000000..68f5835 --- /dev/null +++ b/app/src/main/java/de/blinkt/openvpn/core/OrbotHelper.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2012-2018 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +/* + * Portions Copyright 2014-2016 Hans-Christoph Steiner + * Portions Copyright 2012-2016 Nathan Freitas + * Portions Copyright (c) 2016 CommonsWare, LLC + * + * 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 de.blinkt.openvpn.core; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.text.TextUtils; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static de.blinkt.openvpn.core.OpenVPNService.ORBOT_PACKAGE_NAME; + +public class OrbotHelper { + //! Based on the class from NetCipher but stripped down and modified for icsopenvpn + + /** + * {@link Intent} send by Orbot with {@code ON/OFF/STARTING/STOPPING} status + * included as an {@link #EXTRA_STATUS} {@code String}. Your app should + * always receive {@code ACTION_STATUS Intent}s since any other app could + * start Orbot. Also, user-triggered starts and stops will also cause + * {@code ACTION_STATUS Intent}s to be broadcast. + */ + public final static String ACTION_STATUS = "org.torproject.android.intent.action.STATUS"; + public final static String STATUS_ON = "ON"; + public final static String STATUS_STARTS_DISABLED = "STARTS_DISABLED"; + + public final static String STATUS_STARTING = "STARTING"; + public final static String STATUS_STOPPING = "STOPPING"; + public final static String EXTRA_STATUS = "org.torproject.android.intent.extra.STATUS"; + /** + * A request to Orbot to transparently start Tor services + */ + public final static String ACTION_START = "org.torproject.android.intent.action.START"; + public final static String EXTRA_PACKAGE_NAME = "org.torproject.android.intent.extra.PACKAGE_NAME"; + public static final int SOCKS_PROXY_PORT_DEFAULT = 9050; + private static OrbotHelper mInstance; + + String EXTRA_SOCKS_PROXY_HOST = "org.torproject.android.intent.extra.SOCKS_PROXY_HOST"; + String EXTRA_SOCKS_PROXY_PORT = "org.torproject.android.intent.extra.SOCKS_PROXY_PORT"; + private Context mContext; + private Set statusCallbacks = new HashSet<>(); + private BroadcastReceiver orbotStatusReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context c, Intent intent) { + if (TextUtils.equals(intent.getAction(), + OrbotHelper.ACTION_STATUS)) { + for (StatusCallback cb : statusCallbacks) { + cb.onStatus(intent); + } + + String status = intent.getStringExtra(EXTRA_STATUS); + if (TextUtils.equals(status, STATUS_ON)) { + int socksPort = intent.getIntExtra(EXTRA_SOCKS_PROXY_PORT, SOCKS_PROXY_PORT_DEFAULT); + String socksHost = intent.getStringExtra(EXTRA_SOCKS_PROXY_HOST); + if (TextUtils.isEmpty(socksHost)) + socksHost = "127.0.0.1"; + for (StatusCallback cb : statusCallbacks) { + cb.onOrbotReady(intent, socksHost, socksPort); + } + } else if (TextUtils.equals(status, STATUS_STARTS_DISABLED)) { + for (StatusCallback cb : statusCallbacks) + cb.onDisabled(intent); + } + + } + } + }; + + private OrbotHelper() { + + } + + public static OrbotHelper get(OpenVPNService mOpenVPNService) { + if (mInstance == null) + mInstance = new OrbotHelper(); + return mInstance; + } + + /** + * Gets an {@link Intent} for starting Orbot. Orbot will reply with the + * current status to the {@code packageName} of the app in the provided + * {@link Context} (i.e. {@link Context#getPackageName()}. + */ + public static Intent getOrbotStartIntent(Context context) { + Intent intent = new Intent(ACTION_START); + intent.setPackage(ORBOT_PACKAGE_NAME); + intent.putExtra(EXTRA_PACKAGE_NAME, context.getPackageName()); + return intent; + } + + public static boolean checkTorReceier(Context c) { + Intent startOrbot = getOrbotStartIntent(c); + PackageManager pm = c.getPackageManager(); + Intent result = null; + List receivers = + pm.queryBroadcastReceivers(startOrbot, 0); + + return receivers != null && receivers.size() > 0; + } + + /** + * Adds a StatusCallback to be called when we find out that + * Orbot is ready. If Orbot is ready for use, your callback + * will be called with onEnabled() immediately, before this + * method returns. + * + * @param cb a callback + * @return the singleton, for chaining + */ + public synchronized OrbotHelper addStatusCallback(Context c, StatusCallback cb) { + if (statusCallbacks.size() == 0) { + c.getApplicationContext().registerReceiver(orbotStatusReceiver, + new IntentFilter(OrbotHelper.ACTION_STATUS)); + mContext = c.getApplicationContext(); + } + if (!checkTorReceier(c)) + cb.onNotYetInstalled(); + statusCallbacks.add(cb); + return (this); + } + + /** + * Removes an existing registered StatusCallback. + * + * @param cb the callback to remove + * @return the singleton, for chaining + */ + public synchronized void removeStatusCallback(StatusCallback cb) { + statusCallbacks.remove(cb); + if (statusCallbacks.size() == 0) + mContext.unregisterReceiver(orbotStatusReceiver); + } + + public void sendOrbotStartAndStatusBroadcast() { + mContext.sendBroadcast(getOrbotStartIntent(mContext)); + } + + private void startOrbotService(String action) { + Intent clearVPNMode = new Intent(); + clearVPNMode.setComponent(new ComponentName(ORBOT_PACKAGE_NAME, ".service.TorService")); + clearVPNMode.setAction(action); + mContext.startService(clearVPNMode); + } + + public interface StatusCallback { + /** + * Called when Orbot is operational + * + * @param statusIntent an Intent containing information about + * Orbot, including proxy ports + */ + void onStatus(Intent statusIntent); + + + /** + * Called if Orbot is not yet installed. Usually, you handle + * this by checking the return value from init() on OrbotInitializer + * or calling isInstalled() on OrbotInitializer. However, if + * you have need for it, if a callback is registered before + * an init() call determines that Orbot is not installed, your + * callback will be called with onNotYetInstalled(). + */ + void onNotYetInstalled(); + + void onOrbotReady(Intent intent, String socksHost, int socksPort); + + /** + * Called if Orbot background control is disabled. + * @param intent the intent delivered + */ + void onDisabled(Intent intent); + } +} diff --git a/app/src/main/java/de/blinkt/openvpn/core/PRNGFixes.java b/app/src/main/java/de/blinkt/openvpn/core/PRNGFixes.java index a788426..49a7eaa 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/PRNGFixes.java +++ b/app/src/main/java/de/blinkt/openvpn/core/PRNGFixes.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2014 Arne Schwabe + * Copyright (c) 2012-2016 Arne Schwabe * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt */ diff --git a/app/src/main/java/de/blinkt/openvpn/core/PasswordCache.java b/app/src/main/java/de/blinkt/openvpn/core/PasswordCache.java new file mode 100644 index 0000000..179a8a7 --- /dev/null +++ b/app/src/main/java/de/blinkt/openvpn/core/PasswordCache.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.core; + +import java.util.UUID; + +/** + * Created by arne on 15.12.16. + */ + +public class PasswordCache { + public static final int PCKS12ORCERTPASSWORD = 2; + public static final int AUTHPASSWORD = 3; + private static PasswordCache mInstance; + final private UUID mUuid; + private String mKeyOrPkcs12Password; + private String mAuthPassword; + + private PasswordCache(UUID uuid) { + mUuid = uuid; + } + + public static PasswordCache getInstance(UUID uuid) { + if (mInstance == null || !mInstance.mUuid.equals(uuid)) { + mInstance = new PasswordCache(uuid); + } + return mInstance; + } + + public static String getPKCS12orCertificatePassword(UUID uuid, boolean resetPw) { + String pwcopy = getInstance(uuid).mKeyOrPkcs12Password; + if (resetPw) + getInstance(uuid).mKeyOrPkcs12Password = null; + return pwcopy; + } + + + public static String getAuthPassword(UUID uuid, boolean resetPW) { + String pwcopy = getInstance(uuid).mAuthPassword; + if (resetPW) + getInstance(uuid).mAuthPassword = null; + return pwcopy; + } + + public static void setCachedPassword(String uuid, int type, String password) { + PasswordCache instance = getInstance(UUID.fromString(uuid)); + switch (type) { + case PCKS12ORCERTPASSWORD: + instance.mKeyOrPkcs12Password = password; + break; + case AUTHPASSWORD: + instance.mAuthPassword = password; + break; + } + } + + +} diff --git a/app/src/main/java/de/blinkt/openvpn/core/Preferences.java b/app/src/main/java/de/blinkt/openvpn/core/Preferences.java new file mode 100644 index 0000000..76a064f --- /dev/null +++ b/app/src/main/java/de/blinkt/openvpn/core/Preferences.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.core; + +import android.content.Context; +import android.content.SharedPreferences; + +/** + * Created by arne on 08.01.17. + */ + +// Until I find a good solution + +public class Preferences { + + static SharedPreferences getSharedPreferencesMulti(String name, Context c) { + return c.getSharedPreferences(name, Context.MODE_MULTI_PROCESS | Context.MODE_PRIVATE); + + } + + + public static SharedPreferences getDefaultSharedPreferences(Context c) { + return c.getSharedPreferences(c.getPackageName() + "_preferences", Context.MODE_MULTI_PROCESS | Context.MODE_PRIVATE); + + } + + +} diff --git a/app/src/main/java/de/blinkt/openvpn/core/ProfileManager.java b/app/src/main/java/de/blinkt/openvpn/core/ProfileManager.java index 2490daf..b9edc4b 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/ProfileManager.java +++ b/app/src/main/java/de/blinkt/openvpn/core/ProfileManager.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2014 Arne Schwabe + * Copyright (c) 2012-2016 Arne Schwabe * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt */ @@ -9,7 +9,6 @@ import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; -import android.preference.PreferenceManager; import java.io.IOException; import java.io.ObjectInputStream; @@ -17,14 +16,17 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.Locale; import java.util.Set; +import java.util.UUID; -import ht.vpn.android.VpnProfile; +import de.blinkt.openvpn.VpnProfile; public class ProfileManager { private static final String PREFS_NAME = "VPNList"; private static final String LAST_CONNECTED_PROFILE = "lastConnectedProfile"; + private static final String TEMPORARY_PROFILE_FILENAME = "temporary-vpn-profile"; private static ProfileManager instance; private static VpnProfile mLastConnectedVpn = null; @@ -59,30 +61,31 @@ synchronized public static ProfileManager getInstance(Context context) { } public static void setConntectedVpnProfileDisconnected(Context c) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(c); + SharedPreferences prefs = Preferences.getDefaultSharedPreferences(c); Editor prefsedit = prefs.edit(); prefsedit.putString(LAST_CONNECTED_PROFILE, null); prefsedit.apply(); } - public static void setConnectedVpnProfile(Context c, VpnProfile connectedrofile) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(c); + /** + * Sets the profile that is connected (to connect if the service restarts) + */ + public static void setConnectedVpnProfile(Context c, VpnProfile connectedProfile) { + SharedPreferences prefs = Preferences.getDefaultSharedPreferences(c); Editor prefsedit = prefs.edit(); - prefsedit.putString(LAST_CONNECTED_PROFILE, connectedrofile.getUUIDString()); + prefsedit.putString(LAST_CONNECTED_PROFILE, connectedProfile.getUUIDString()); prefsedit.apply(); - mLastConnectedVpn = connectedrofile; + mLastConnectedVpn = connectedProfile; } - public static VpnProfile getLastConnectedProfile(Context c, boolean onBoot) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(c); - - boolean useStartOnBoot = prefs.getBoolean("restartvpnonboot", false); - - if (onBoot && !useStartOnBoot) - return null; + /** + * Returns the profile that was last connected (to connect if the service restarts) + */ + public static VpnProfile getLastConnectedProfile(Context c) { + SharedPreferences prefs = Preferences.getDefaultSharedPreferences(c); String lastConnectedProfile = prefs.getString(LAST_CONNECTED_PROFILE, null); if (lastConnectedProfile != null) @@ -106,7 +109,7 @@ public VpnProfile getProfileByName(String name) { } public void saveProfileList(Context context) { - SharedPreferences sharedprefs = context.getSharedPreferences(PREFS_NAME, Activity.MODE_PRIVATE); + SharedPreferences sharedprefs = Preferences.getSharedPreferencesMulti(PREFS_NAME, context); Editor editor = sharedprefs.edit(); editor.putStringSet("vpnlist", profiles.keySet()); @@ -124,19 +127,36 @@ public void addProfile(VpnProfile profile) { } - public static void setTemporaryProfile(VpnProfile tmp) { + public static void setTemporaryProfile(Context c, VpnProfile tmp) { + tmp.mTemporaryProfile = true; ProfileManager.tmpprofile = tmp; + saveProfile(c, tmp, true, true); } + public static boolean isTempProfile() { + return mLastConnectedVpn != null && mLastConnectedVpn == tmpprofile; + } public void saveProfile(Context context, VpnProfile profile) { - ObjectOutputStream vpnfile; + saveProfile(context, profile, true, false); + } + + private static void saveProfile(Context context, VpnProfile profile, boolean updateVersion, boolean isTemporary) { + + if (updateVersion) + profile.mVersion += 1; + ObjectOutputStream vpnFile; + + String filename = profile.getUUID().toString() + ".vp"; + if (isTemporary) + filename = TEMPORARY_PROFILE_FILENAME + ".vp"; + try { - vpnfile = new ObjectOutputStream(context.openFileOutput((profile.getUUID().toString() + ".vp"), Activity.MODE_PRIVATE)); + vpnFile = new ObjectOutputStream(context.openFileOutput(filename, Activity.MODE_PRIVATE)); - vpnfile.writeObject(profile); - vpnfile.flush(); - vpnfile.close(); + vpnFile.writeObject(profile); + vpnFile.flush(); + vpnFile.close(); } catch (IOException e) { VpnStatus.logException("saving VPN profile", e); throw new RuntimeException(e); @@ -146,15 +166,18 @@ public void saveProfile(Context context, VpnProfile profile) { private void loadVPNList(Context context) { profiles = new HashMap<>(); - SharedPreferences listpref = context.getSharedPreferences(PREFS_NAME, Activity.MODE_PRIVATE); + SharedPreferences listpref = Preferences.getSharedPreferencesMulti(PREFS_NAME, context); Set vlist = listpref.getStringSet("vpnlist", null); if (vlist == null) { vlist = new HashSet<>(); } + // Always try to load the temporary profile + vlist.add(TEMPORARY_PROFILE_FILENAME); for (String vpnentry : vlist) { + ObjectInputStream vpnfile=null; try { - ObjectInputStream vpnfile = new ObjectInputStream(context.openFileInput(vpnentry + ".vp")); + vpnfile = new ObjectInputStream(context.openFileInput(vpnentry + ".vp")); VpnProfile vp = ((VpnProfile) vpnfile.readObject()); // Sanity check @@ -162,10 +185,24 @@ private void loadVPNList(Context context) { continue; vp.upgradeProfile(); - profiles.put(vp.getUUID().toString(), vp); + if (vpnentry.equals(TEMPORARY_PROFILE_FILENAME)) { + tmpprofile = vp; + } else { + profiles.put(vp.getUUID().toString(), vp); + } + } catch (IOException | ClassNotFoundException e) { - VpnStatus.logException("Loading VPN List", e); + if (!vpnentry.equals(TEMPORARY_PROFILE_FILENAME)) + VpnStatus.logException("Loading VPN List", e); + } finally { + if (vpnfile!=null) { + try { + vpnfile.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } } } } @@ -182,12 +219,49 @@ public void removeProfile(Context context, VpnProfile profile) { } public static VpnProfile get(Context context, String profileUUID) { + return get(context, profileUUID, 0, 10); + } + + public static VpnProfile get(Context context, String profileUUID, int version, int tries) { checkInstance(context); - return get(profileUUID); + VpnProfile profile = get(profileUUID); + int tried = 0; + while ((profile == null || profile.mVersion < version) && (tried++ < tries)) { + try { + Thread.sleep(100); + } catch (InterruptedException ignored) { + } + instance.loadVPNList(context); + profile = get(profileUUID); + int ver = profile == null ? -1 : profile.mVersion; + } + + if (tried > 5) + + { + int ver = profile == null ? -1 : profile.mVersion; + VpnStatus.logError(String.format(Locale.US, "Used x %d tries to get current version (%d/%d) of the profile", tried, ver, version)); + } + return profile; } public static VpnProfile getLastConnectedVpn() { return mLastConnectedVpn; } + public static VpnProfile getAlwaysOnVPN(Context context) { + checkInstance(context); + SharedPreferences prefs = Preferences.getDefaultSharedPreferences(context); + + String uuid = prefs.getString("alwaysOnVpn", null); + return get(uuid); + + } + + public static void updateLRU(Context c, VpnProfile profile) { + profile.mLastUsed = System.currentTimeMillis(); + // LRU does not change the profile, no need for the service to refresh + if (profile!=tmpprofile) + saveProfile(c, profile, false, false); + } } diff --git a/app/src/main/java/de/blinkt/openvpn/core/ProxyDetection.java b/app/src/main/java/de/blinkt/openvpn/core/ProxyDetection.java index f7ab6ab..53bf0e8 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/ProxyDetection.java +++ b/app/src/main/java/de/blinkt/openvpn/core/ProxyDetection.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2014 Arne Schwabe + * Copyright (c) 2012-2016 Arne Schwabe * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt */ @@ -15,7 +15,7 @@ import java.util.List; import ht.vpn.android.R; -import ht.vpn.android.VpnProfile; +import de.blinkt.openvpn.VpnProfile; public class ProxyDetection { static SocketAddress detectProxy(VpnProfile vp) { diff --git a/app/src/main/java/de/blinkt/openvpn/core/StatusListener.java b/app/src/main/java/de/blinkt/openvpn/core/StatusListener.java new file mode 100644 index 0000000..b37a100 --- /dev/null +++ b/app/src/main/java/de/blinkt/openvpn/core/StatusListener.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.core; + +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.util.Log; +import ht.vpn.android.BuildConfig; +import de.blinkt.openvpn.core.VpnStatus.LogLevel; + +import java.io.DataInputStream; +import java.io.File; +import java.io.IOException; + +/** + * Created by arne on 09.11.16. + */ + +public class StatusListener implements VpnStatus.LogListener { + private File mCacheDir; + private Context mContext; + private IStatusCallbacks mCallback = new IStatusCallbacks.Stub() { + @Override + public void newLogItem(LogItem item) throws RemoteException { + VpnStatus.newLogItem(item); + } + + @Override + public void updateStateString(String state, String msg, int resid, ConnectionStatus + level, Intent intent) throws RemoteException { + VpnStatus.updateStateString(state, msg, resid, level, intent); + } + + @Override + public void updateByteCount(long inBytes, long outBytes) throws RemoteException { + VpnStatus.updateByteCount(inBytes, outBytes); + } + + @Override + public void connectedVPN(String uuid) throws RemoteException { + VpnStatus.setConnectedVPNProfile(uuid); + } + }; + private ServiceConnection mConnection = new ServiceConnection() { + + + @Override + public void onServiceConnected(ComponentName className, + IBinder service) { + // We've bound to LocalService, cast the IBinder and get LocalService instance + IServiceStatus serviceStatus = IServiceStatus.Stub.asInterface(service); + try { + /* Check if this a local service ... */ + if (service.queryLocalInterface("de.blinkt.openvpn.core.IServiceStatus") == null) { + // Not a local service + VpnStatus.setConnectedVPNProfile(serviceStatus.getLastConnectedVPN()); + VpnStatus.setTrafficHistory(serviceStatus.getTrafficHistory()); + ParcelFileDescriptor pfd = serviceStatus.registerStatusCallback(mCallback); + DataInputStream fd = new DataInputStream(new ParcelFileDescriptor.AutoCloseInputStream(pfd)); + + short len = fd.readShort(); + byte[] buf = new byte[65336]; + while (len != 0x7fff) { + fd.readFully(buf, 0, len); + LogItem logitem = new LogItem(buf, len); + VpnStatus.newLogItem(logitem, false); + len = fd.readShort(); + } + fd.close(); + + + } else { + VpnStatus.initLogCache(mCacheDir); + /* Set up logging to Logcat with a context) */ + + if (BuildConfig.DEBUG || BuildConfig.FLAVOR.equals("skeleton")) { + VpnStatus.addLogListener(StatusListener.this); + } + + + } + + } catch (RemoteException | IOException e) { + e.printStackTrace(); + VpnStatus.logException(e); + } + } + + @Override + public void onServiceDisconnected(ComponentName arg0) { + VpnStatus.removeLogListener(StatusListener.this); + } + + }; + + public void init(Context c) { + + Intent intent = new Intent(c, OpenVPNStatusService.class); + intent.setAction(OpenVPNService.START_SERVICE); + mCacheDir = c.getCacheDir(); + + c.bindService(intent, mConnection, Context.BIND_AUTO_CREATE); + this.mContext = c; + + } + + @Override + public void newLog(LogItem logItem) { + switch (logItem.getLogLevel()) { + case INFO: + Log.i("OpenVPN", logItem.getString(mContext)); + break; + case DEBUG: + Log.d("OpenVPN", logItem.getString(mContext)); + break; + case ERROR: + Log.e("OpenVPN", logItem.getString(mContext)); + break; + case VERBOSE: + Log.v("OpenVPN", logItem.getString(mContext)); + break; + case WARNING: + default: + Log.w("OpenVPN", logItem.getString(mContext)); + break; + } + + } +} diff --git a/app/src/main/java/de/blinkt/openvpn/core/TrafficHistory.java b/app/src/main/java/de/blinkt/openvpn/core/TrafficHistory.java new file mode 100644 index 0000000..6ba3506 --- /dev/null +++ b/app/src/main/java/de/blinkt/openvpn/core/TrafficHistory.java @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2012-2017 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.core; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Vector; + +import static java.lang.Math.max; + +/** + * Created by arne on 23.05.17. + */ + +public class TrafficHistory implements Parcelable { + + public static final long PERIODS_TO_KEEP = 5; + public static final int TIME_PERIOD_MINTUES = 60 * 1000; + public static final int TIME_PERIOD_HOURS = 3600 * 1000; + private LinkedList trafficHistorySeconds = new LinkedList<>(); + private LinkedList trafficHistoryMinutes = new LinkedList<>(); + private LinkedList trafficHistoryHours = new LinkedList<>(); + + private TrafficDatapoint lastSecondUsedForMinute; + private TrafficDatapoint lastMinuteUsedForHours; + + public TrafficHistory() { + + } + + protected TrafficHistory(Parcel in) { + in.readList(trafficHistorySeconds, getClass().getClassLoader()); + in.readList(trafficHistoryMinutes, getClass().getClassLoader()); + in.readList(trafficHistoryHours, getClass().getClassLoader()); + lastSecondUsedForMinute = in.readParcelable(getClass().getClassLoader()); + lastMinuteUsedForHours = in.readParcelable(getClass().getClassLoader()); + } + + public static final Creator CREATOR = new Creator() { + @Override + public TrafficHistory createFromParcel(Parcel in) { + return new TrafficHistory(in); + } + + @Override + public TrafficHistory[] newArray(int size) { + return new TrafficHistory[size]; + } + }; + + public LastDiff getLastDiff(TrafficDatapoint tdp) { + + TrafficDatapoint lasttdp; + + + if (trafficHistorySeconds.size() == 0) + lasttdp = new TrafficDatapoint(0, 0, System.currentTimeMillis()); + + else + lasttdp = trafficHistorySeconds.getLast(); + + if (tdp == null) { + tdp = lasttdp; + if (trafficHistorySeconds.size() < 2) + lasttdp = tdp; + else { + trafficHistorySeconds.descendingIterator().next(); + tdp = trafficHistorySeconds.descendingIterator().next(); + } + } + + return new LastDiff(lasttdp, tdp); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeList(trafficHistorySeconds); + dest.writeList(trafficHistoryMinutes); + dest.writeList(trafficHistoryHours); + dest.writeParcelable(lastSecondUsedForMinute, 0); + dest.writeParcelable(lastMinuteUsedForHours, 0); + + } + + public LinkedList getHours() { + return trafficHistoryHours; + } + + public LinkedList getMinutes() { + return trafficHistoryMinutes; + } + + public LinkedList getSeconds() { + return trafficHistorySeconds; + } + + public static LinkedList getDummyList() { + LinkedList list = new LinkedList<>(); + list.add(new TrafficDatapoint(0, 0, System.currentTimeMillis())); + return list; + } + + + public static class TrafficDatapoint implements Parcelable { + private TrafficDatapoint(long inBytes, long outBytes, long timestamp) { + this.in = inBytes; + this.out = outBytes; + this.timestamp = timestamp; + } + + public final long timestamp; + public final long in; + public final long out; + + private TrafficDatapoint(Parcel in) { + timestamp = in.readLong(); + this.in = in.readLong(); + out = in.readLong(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public TrafficDatapoint createFromParcel(Parcel in) { + return new TrafficDatapoint(in); + } + + @Override + public TrafficDatapoint[] newArray(int size) { + return new TrafficDatapoint[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(timestamp); + dest.writeLong(in); + dest.writeLong(out); + } + } + + LastDiff add(long in, long out) { + TrafficDatapoint tdp = new TrafficDatapoint(in, out, System.currentTimeMillis()); + + LastDiff diff = getLastDiff(tdp); + addDataPoint(tdp); + return diff; + } + + private void addDataPoint(TrafficDatapoint tdp) { + trafficHistorySeconds.add(tdp); + + if (lastSecondUsedForMinute == null) { + lastSecondUsedForMinute = new TrafficDatapoint(0, 0, 0); + lastMinuteUsedForHours = new TrafficDatapoint(0, 0, 0); + } + + removeAndAverage(tdp, true); + } + + private void removeAndAverage(TrafficDatapoint newTdp, boolean seconds) { + HashSet toRemove = new HashSet<>(); + Vector toAverage = new Vector<>(); + + long timePeriod; + LinkedList tpList, nextList; + TrafficDatapoint lastTsPeriod; + + if (seconds) { + timePeriod = TIME_PERIOD_MINTUES; + tpList = trafficHistorySeconds; + nextList = trafficHistoryMinutes; + lastTsPeriod = lastSecondUsedForMinute; + } else { + timePeriod = TIME_PERIOD_HOURS; + tpList = trafficHistoryMinutes; + nextList = trafficHistoryHours; + lastTsPeriod = lastMinuteUsedForHours; + } + + if (newTdp.timestamp / timePeriod > (lastTsPeriod.timestamp / timePeriod)) { + nextList.add(newTdp); + + if (seconds) { + lastSecondUsedForMinute = newTdp; + removeAndAverage(newTdp, false); + } else + lastMinuteUsedForHours = newTdp; + + for (TrafficDatapoint tph : tpList) { + // List is iteratered from oldest to newest, remembert first one that we did not + if ((newTdp.timestamp - tph.timestamp) / timePeriod >= PERIODS_TO_KEEP) + toRemove.add(tph); + } + tpList.removeAll(toRemove); + } + } + + static class LastDiff { + + final private TrafficDatapoint tdp; + final private TrafficDatapoint lasttdp; + + private LastDiff(TrafficDatapoint lasttdp, TrafficDatapoint tdp) { + this.lasttdp = lasttdp; + this.tdp = tdp; + } + + public long getDiffOut() { + return max(0, tdp.out - lasttdp.out); + } + + public long getDiffIn() { + return max(0, tdp.in - lasttdp.in); + } + + public long getIn() { + return tdp.in; + } + + public long getOut() { + return tdp.out; + } + + } + + +} \ No newline at end of file diff --git a/app/src/main/java/de/blinkt/openvpn/core/VPNLaunchHelper.java b/app/src/main/java/de/blinkt/openvpn/core/VPNLaunchHelper.java index 6bee33c..fdf3b6c 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/VPNLaunchHelper.java +++ b/app/src/main/java/de/blinkt/openvpn/core/VPNLaunchHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2014 Arne Schwabe + * Copyright (c) 2012-2016 Arne Schwabe * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt */ @@ -14,10 +14,11 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; import java.util.Vector; +import de.blinkt.openvpn.VpnProfile; import ht.vpn.android.R; -import ht.vpn.android.VpnProfile; public class VPNLaunchHelper { private static final String MININONPIEVPN = "nopie_openvpn"; @@ -25,52 +26,58 @@ public class VPNLaunchHelper { private static final String OVPNCONFIGFILE = "android.conf"; - - static private String writeMiniVPN(Context context) { + private static String writeMiniVPN(Context context) { + String nativeAPI = NativeUtils.getNativeAPI(); + /* Q does not allow executing binaries written in temp directory anymore */ + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { + return new File(context.getApplicationInfo().nativeLibraryDir, "libovpnexec.so").getPath(); + } String[] abis; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) - abis = getSupportedAbisLollipop(); - else - abis = new String[]{Build.CPU_ABI, Build.CPU_ABI2}; + abis = getSupportedABIsLollipop(); - for (String abi: abis) { + if (!nativeAPI.equals(abis[0])) { + VpnStatus.logWarning(R.string.abi_mismatch, Arrays.toString(abis), nativeAPI); + abis = new String[]{nativeAPI}; + } - File mvpnout = new File(context.getCacheDir(), getMiniVPNExecutableName() + "." + abi); - if ((mvpnout.exists() && mvpnout.canExecute()) || writeMiniVPNBinary(context, abi, mvpnout)) { - return mvpnout.getPath(); + for (String abi : abis) { + File vpnExecutable = new File(context.getCacheDir(), "c_" + getMiniVPNExecutableName() + "." + abi); + if ((vpnExecutable.exists() && vpnExecutable.canExecute()) || writeMiniVPNBinary(context, abi, vpnExecutable)) { + return vpnExecutable.getPath(); } } - return null; - } + throw new RuntimeException("Cannot find any execulte for this device's ABIs " + abis.toString()); + } @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private static String[] getSupportedAbisLollipop() { + private static String[] getSupportedABIsLollipop() { return Build.SUPPORTED_ABIS; } - private static String getMiniVPNExecutableName() - { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) - return MINIPIEVPN; - else - return MININONPIEVPN; + private static String getMiniVPNExecutableName() { + return MINIPIEVPN; } - public static String[] replacePieWithNoPie(String[] mArgv) - { + public static String[] replacePieWithNoPie(String[] mArgv) { mArgv[0] = mArgv[0].replace(MINIPIEVPN, MININONPIEVPN); return mArgv; } - public static String[] buildOpenvpnArgv(Context c) { + static String[] buildOpenvpnArgv(Context c) { Vector args = new Vector<>(); + String binaryName = writeMiniVPN(c); // Add fixed paramenters //args.add("/data/data/de.blinkt.openvpn/lib/openvpn"); - args.add(writeMiniVPN(c)); + if (binaryName == null) { + VpnStatus.logError("Error writing minivpn binary"); + return null; + } + + args.add(binaryName); args.add("--config"); args.add(getConfigFilePath(c)); @@ -84,8 +91,7 @@ private static boolean writeMiniVPNBinary(Context context, String abi, File mvpn try { mvpn = context.getAssets().open(getMiniVPNExecutableName() + "." + abi); - } - catch (IOException errabi) { + } catch (IOException errabi) { VpnStatus.logInfo("Failed getting assets for archicture " + abi); return false; } @@ -93,16 +99,16 @@ private static boolean writeMiniVPNBinary(Context context, String abi, File mvpn FileOutputStream fout = new FileOutputStream(mvpnout); - byte buf[]= new byte[4096]; + byte buf[] = new byte[4096]; int lenread = mvpn.read(buf); - while(lenread> 0) { + while (lenread > 0) { fout.write(buf, 0, lenread); lenread = mvpn.read(buf); } fout.close(); - if(!mvpnout.setExecutable(true)) { + if (!mvpnout.setExecutable(true)) { VpnStatus.logError("Failed to make OpenVPN executable"); return false; } @@ -115,21 +121,20 @@ private static boolean writeMiniVPNBinary(Context context, String abi, File mvpn } } - - public static void startOpenVpn(VpnProfile startprofile, Context context) { - if(writeMiniVPN(context)==null) { - VpnStatus.logError("Error writing minivpn binary"); - return; - } - VpnStatus.logInfo(R.string.building_configration); + public static void startOpenVpn(VpnProfile startprofile, Context context) { + Intent startVPN = startprofile.prepareStartService(context); + if (startVPN != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + //noinspection NewApi + context.startForegroundService(startVPN); + else + context.startService(startVPN); - Intent startVPN = startprofile.prepareStartService(context); - if(startVPN!=null) - context.startService(startVPN); + } + } - } public static String getConfigFilePath(Context context) { return context.getCacheDir().getAbsolutePath() + "/" + OVPNCONFIGFILE; diff --git a/app/src/main/java/de/blinkt/openvpn/core/VpnStatus.java b/app/src/main/java/de/blinkt/openvpn/core/VpnStatus.java index 3c6f18c..16aa855 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/VpnStatus.java +++ b/app/src/main/java/de/blinkt/openvpn/core/VpnStatus.java @@ -1,36 +1,22 @@ /* - * Copyright (c) 2012-2014 Arne Schwabe + * Copyright (c) 2012-2016 Arne Schwabe * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt */ package de.blinkt.openvpn.core; -import android.annotation.SuppressLint; -import android.app.Activity; +import android.app.PendingIntent; import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.Signature; +import android.content.Intent; import android.os.Build; -import android.os.Parcel; -import android.os.Parcelable; -import android.text.TextUtils; +import android.os.HandlerThread; +import android.os.Message; -import java.io.ByteArrayInputStream; import java.io.File; import java.io.PrintWriter; import java.io.StringWriter; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.FormatFlagsConversionMismatchException; import java.util.LinkedList; import java.util.Locale; -import java.util.UnknownFormatConversionException; import java.util.Vector; import ht.vpn.android.R; @@ -38,7 +24,7 @@ public class VpnStatus { - public static LinkedList logbuffer; + private static final LinkedList logbuffer; private static Vector logListener; private static Vector stateListener; @@ -50,7 +36,17 @@ public class VpnStatus { private static int mLastStateresid = R.string.state_noprocess; - private static long mlastByteCount[] = {0, 0, 0, 0}; + private static Intent mLastIntent = null; + + private static HandlerThread mHandlerThread; + + private static String mLastConnectedVPNUUID; + static boolean readFileLog =false; + final static java.lang.Object readFileLock = new Object(); + + + public static TrafficHistory trafficHistory; + public static void logException(LogLevel ll, String context, Exception e) { StringWriter sw = new StringWriter(); @@ -72,20 +68,90 @@ public static void logException(String context, Exception e) { logException(LogLevel.ERROR, context, e); } - private static final int MAXLOGENTRIES = 1000; + static final int MAXLOGENTRIES = 1000; + + public static boolean isVPNActive() { + return mLastLevel != ConnectionStatus.LEVEL_AUTH_FAILED && !(mLastLevel == ConnectionStatus.LEVEL_NOTCONNECTED); + } + + public static String getLastCleanLogMessage(Context c) { + String message = mLaststatemsg; + switch (mLastLevel) { + case LEVEL_CONNECTED: + String[] parts = mLaststatemsg.split(","); + /* + (a) the integer unix date/time, + (b) the state name, + 0 (c) optional descriptive string (used mostly on RECONNECTING + and EXITING to show the reason for the disconnect), + + 1 (d) optional TUN/TAP local IPv4 address + 2 (e) optional address of remote server, + 3 (f) optional port of remote server, + 4 (g) optional local address, + 5 (h) optional local port, and + 6 (i) optional TUN/TAP local IPv6 address. +*/ + // Return only the assigned IP addresses in the UI + if (parts.length >= 7) + message = String.format(Locale.US, "%s %s", parts[1], parts[6]); + break; + } + + while (message.endsWith(",")) + message = message.substring(0, message.length() - 1); + + String status = mLaststate; + if (status.equals("NOPROCESS")) + return message; + + if (mLastStateresid == R.string.state_waitconnectretry) { + return c.getString(R.string.state_waitconnectretry, mLaststatemsg); + } + + String prefix = c.getString(mLastStateresid); + if (mLastStateresid == R.string.unknown_state) + message = status + message; + if (message.length() > 0) + prefix += ": "; + + return prefix + message; + + } + + public static void initLogCache(File cacheDir) { + mHandlerThread = new HandlerThread("LogFileWriter", Thread.MIN_PRIORITY); + mHandlerThread.start(); + mLogFileHandler = new LogFileHandler(mHandlerThread.getLooper()); + + + Message m = mLogFileHandler.obtainMessage(LogFileHandler.LOG_INIT, cacheDir); + mLogFileHandler.sendMessage(m); + + } + + public static void flushLog() { + if (mLogFileHandler!=null) + mLogFileHandler.sendEmptyMessage(LogFileHandler.FLUSH_TO_DISK); + } + + public static void setConnectedVPNProfile(String uuid) { + mLastConnectedVPNUUID = uuid; + for (StateListener sl: stateListener) + sl.setConnectedVPN(uuid); + } + + + public static String getLastConnectedVPNProfile() + { + return mLastConnectedVPNUUID; + } - public enum ConnectionStatus { - LEVEL_CONNECTED, - LEVEL_VPNPAUSED, - LEVEL_CONNECTING_SERVER_REPLIED, - LEVEL_CONNECTING_NO_SERVER_REPLY_YET, - LEVEL_NONETWORK, - LEVEL_NOTCONNECTED, - LEVEL_AUTH_FAILED, - LEVEL_WAITING_FOR_USER_INPUT, - UNKNOWN_LEVEL + public static void setTrafficHistory(TrafficHistory trafficHistory) { + VpnStatus.trafficHistory = trafficHistory; } + public enum LogLevel { INFO(2), ERROR(-2), @@ -105,14 +171,17 @@ public int getInt() { public static LogLevel getEnumByValue(int value) { switch (value) { - case 1: - return INFO; case 2: + return INFO; + case -2: return ERROR; - case 3: + case 1: return WARNING; + case 3: + return VERBOSE; case 4: return DEBUG; + default: return null; } @@ -120,208 +189,36 @@ public static LogLevel getEnumByValue(int value) { } // keytool -printcert -jarfile de.blinkt.openvpn_85.apk - public static final byte[] officalkey = {-58, -42, -44, -106, 90, -88, -87, -88, -52, -124, 84, 117, 66, 79, -112, -111, -46, 86, -37, 109}; - public static final byte[] officaldebugkey = {-99, -69, 45, 71, 114, -116, 82, 66, -99, -122, 50, -70, -56, -111, 98, -35, -65, 105, 82, 43}; - public static final byte[] amazonkey = {-116, -115, -118, -89, -116, -112, 120, 55, 79, -8, -119, -23, 106, -114, -85, -56, -4, 105, 26, -57}; - public static final byte[] fdroidkey = {-92, 111, -42, -46, 123, -96, -60, 79, -27, -31, 49, 103, 11, -54, -68, -27, 17, 2, 121, 104}; + static final byte[] officalkey = {-58, -42, -44, -106, 90, -88, -87, -88, -52, -124, 84, 117, 66, 79, -112, -111, -46, 86, -37, 109}; + static final byte[] officaldebugkey = {-99, -69, 45, 71, 114, -116, 82, 66, -99, -122, 50, -70, -56, -111, 98, -35, -65, 105, 82, 43}; + static final byte[] amazonkey = {-116, -115, -118, -89, -116, -112, 120, 55, 79, -8, -119, -23, 106, -114, -85, -56, -4, 105, 26, -57}; + static final byte[] fdroidkey = {-92, 111, -42, -46, 123, -96, -60, 79, -27, -31, 49, 103, 11, -54, -68, -27, 17, 2, 121, 104}; private static ConnectionStatus mLastLevel = ConnectionStatus.LEVEL_NOTCONNECTED; + private static LogFileHandler mLogFileHandler; + static { logbuffer = new LinkedList<>(); logListener = new Vector<>(); stateListener = new Vector<>(); byteCountListener = new Vector<>(); - logInformation(); - } - - - public static class LogItem implements Parcelable { - - - private Object[] mArgs = null; - private String mMessage = null; - private int mRessourceId; - // Default log priority - LogLevel mLevel = LogLevel.INFO; - private long logtime = System.currentTimeMillis(); - private int mVerbosityLevel = -1; - - private LogItem(int ressourceId, Object[] args) { - mRessourceId = ressourceId; - mArgs = args; - } - - public LogItem(LogLevel level, int verblevel, String message) { - mMessage = message; - mLevel = level; - mVerbosityLevel = verblevel; - } - - @Override - public int describeContents() { - return 0; - } - - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeArray(mArgs); - dest.writeString(mMessage); - dest.writeInt(mRessourceId); - dest.writeInt(mLevel.getInt()); - dest.writeInt(mVerbosityLevel); - - dest.writeLong(logtime); - } - - public LogItem(Parcel in) { - mArgs = in.readArray(Object.class.getClassLoader()); - mMessage = in.readString(); - mRessourceId = in.readInt(); - mLevel = LogLevel.getEnumByValue(in.readInt()); - mVerbosityLevel = in.readInt(); - logtime = in.readLong(); - } - - public static final Creator CREATOR - = new Creator() { - public LogItem createFromParcel(Parcel in) { - return new LogItem(in); - } - - public LogItem[] newArray(int size) { - return new LogItem[size]; - } - }; - - public LogItem(LogLevel loglevel, int ressourceId, Object... args) { - mRessourceId = ressourceId; - mArgs = args; - mLevel = loglevel; - } - - - public LogItem(LogLevel loglevel, String msg) { - mLevel = loglevel; - mMessage = msg; - } - - - public LogItem(LogLevel loglevel, int ressourceId) { - mRessourceId = ressourceId; - mLevel = loglevel; - } - - public String getString(Context c) { - try { - if (mMessage != null) { - return mMessage; - } else { - if (c != null) { - if (mRessourceId == R.string.mobile_info) - return getMobileInfoString(c); - if (mArgs == null) - return c.getString(mRessourceId); - else - return c.getString(mRessourceId, mArgs); - } else { - String str = String.format(Locale.ENGLISH, "Log (no context) resid %d", mRessourceId); - if (mArgs != null) - str += TextUtils.join("|", mArgs); - - - return str; - } - } - } catch (UnknownFormatConversionException e) { - if (c != null) - throw new UnknownFormatConversionException(e.getLocalizedMessage() + getString(null)); - else - throw e; - } catch (FormatFlagsConversionMismatchException e) { - if (c != null) - throw new FormatFlagsConversionMismatchException(e.getLocalizedMessage() + getString(null), e.getConversion()); - else - throw e; - } - - } - - public LogLevel getLogLevel() { - return mLevel; - } - - // The lint is wrong here - @SuppressLint("StringFormatMatches") - private String getMobileInfoString(Context c) { - c.getPackageManager(); - String apksign = "error getting package signature"; - - String version = "error getting version"; - try { - @SuppressLint("PackageManagerGetSignatures") - Signature raw = c.getPackageManager().getPackageInfo(c.getPackageName(), PackageManager.GET_SIGNATURES).signatures[0]; - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - X509Certificate cert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(raw.toByteArray())); - MessageDigest md = MessageDigest.getInstance("SHA-1"); - byte[] der = cert.getEncoded(); - md.update(der); - byte[] digest = md.digest(); - - if (Arrays.equals(digest, officalkey)) - apksign = c.getString(R.string.official_build); - else if (Arrays.equals(digest, officaldebugkey)) - apksign = c.getString(R.string.debug_build); - else if (Arrays.equals(digest, amazonkey)) - apksign = "amazon version"; - else if (Arrays.equals(digest, fdroidkey)) - apksign = "F-Droid built and signed version"; - else - apksign = c.getString(R.string.built_by, cert.getSubjectX500Principal().getName()); - - PackageInfo packageinfo = c.getPackageManager().getPackageInfo(c.getPackageName(), 0); - version = packageinfo.versionName; - - } catch (NameNotFoundException | CertificateException | - NoSuchAlgorithmException ignored) { - } - - Object[] argsext = Arrays.copyOf(mArgs, mArgs.length + 2); - argsext[argsext.length - 1] = apksign; - argsext[argsext.length - 2] = version; - - return c.getString(R.string.mobile_info_extended, argsext); - - } - - public long getLogtime() { - return logtime; - } + trafficHistory = new TrafficHistory(); + logInformation(); - public int getVerbosityLevel() { - if (mVerbosityLevel == -1) { - // Hack: - // For message not from OpenVPN, report the status level as log level - return mLevel.getInt(); - } - return mVerbosityLevel; - } } - public void saveLogToDisk(Context c) { - - File logOut = new File(c.getCacheDir(), "log.xml"); - } public interface LogListener { void newLog(LogItem logItem); } public interface StateListener { - void updateState(String state, String logmessage, int localizedResId, ConnectionStatus level); + void updateState(String state, String logmessage, int localizedResId, ConnectionStatus level, Intent Intent); + + void setConnectedVPN(String uuid); } public interface ByteCountListener { @@ -336,10 +233,20 @@ public synchronized static void logMessage(LogLevel level, String prefix, String public synchronized static void clearLog() { logbuffer.clear(); logInformation(); + if (mLogFileHandler != null) + mLogFileHandler.sendEmptyMessage(LogFileHandler.TRIM_LOG_FILE); } private static void logInformation() { - logInfo(R.string.mobile_info, Build.MODEL, Build.BOARD, Build.BRAND, Build.VERSION.SDK_INT); + String nativeAPI; + try { + nativeAPI = NativeUtils.getNativeAPI(); + } catch (UnsatisfiedLinkError ignore) { + nativeAPI = "error"; + } + + logInfo(R.string.mobile_info, Build.MODEL, Build.BOARD, Build.BRAND, Build.VERSION.SDK_INT, + nativeAPI, Build.VERSION.RELEASE, Build.ID, Build.FINGERPRINT, "", ""); } public synchronized static void addLogListener(LogListener ll) { @@ -351,7 +258,8 @@ public synchronized static void removeLogListener(LogListener ll) { } public synchronized static void addByteCountListener(ByteCountListener bcl) { - bcl.updateByteCount(mlastByteCount[0], mlastByteCount[1], mlastByteCount[2], mlastByteCount[3]); + TrafficHistory.LastDiff diff = trafficHistory.getLastDiff(null); + bcl.updateByteCount(diff.getIn(), diff.getOut(), diff.getDiffIn(),diff.getDiffOut()); byteCountListener.add(bcl); } @@ -364,37 +272,41 @@ public synchronized static void addStateListener(StateListener sl) { if (!stateListener.contains(sl)) { stateListener.add(sl); if (mLaststate != null) - sl.updateState(mLaststate, mLaststatemsg, mLastStateresid, mLastLevel); + sl.updateState(mLaststate, mLaststatemsg, mLastStateresid, mLastLevel, mLastIntent); } } private static int getLocalizedState(String state) { - if (state.equals("CONNECTING")) - return R.string.state_connecting; - else if (state.equals("WAIT")) - return R.string.state_wait; - else if (state.equals("AUTH")) - return R.string.state_auth; - else if (state.equals("GET_CONFIG")) - return R.string.state_get_config; - else if (state.equals("ASSIGN_IP")) - return R.string.state_assign_ip; - else if (state.equals("ADD_ROUTES")) - return R.string.state_add_routes; - else if (state.equals("CONNECTED")) - return R.string.state_connected; - else if (state.equals("DISCONNECTED")) - return R.string.state_disconnected; - else if (state.equals("RECONNECTING")) - return R.string.state_reconnecting; - else if (state.equals("EXITING")) - return R.string.state_exiting; - else if (state.equals("RESOLVE")) - return R.string.state_resolve; - else if (state.equals("TCP_CONNECT")) - return R.string.state_tcp_connect; - else - return R.string.unknown_state; + switch (state) { + case "CONNECTING": + return R.string.state_connecting; + case "WAIT": + return R.string.state_wait; + case "AUTH": + return R.string.state_auth; + case "GET_CONFIG": + return R.string.state_get_config; + case "ASSIGN_IP": + return R.string.state_assign_ip; + case "ADD_ROUTES": + return R.string.state_add_routes; + case "CONNECTED": + return R.string.state_connected; + case "DISCONNECTED": + return R.string.state_disconnected; + case "RECONNECTING": + return R.string.state_reconnecting; + case "EXITING": + return R.string.state_exiting; + case "RESOLVE": + return R.string.state_resolve; + case "TCP_CONNECT": + return R.string.state_tcp_connect; + case "AUTH_PENDING": + return R.string.state_auth_pending; + default: + return R.string.unknown_state; + } } @@ -415,7 +327,7 @@ public static void updateStatePause(OpenVPNManagement.pauseReason pauseReason) { private static ConnectionStatus getLevel(String state) { String[] noreplyet = {"CONNECTING", "WAIT", "RECONNECTING", "RESOLVE", "TCP_CONNECT"}; - String[] reply = {"AUTH", "GET_CONFIG", "ASSIGN_IP", "ADD_ROUTES"}; + String[] reply = {"AUTH", "GET_CONFIG", "ASSIGN_IP", "ADD_ROUTES", "AUTH_PENDING"}; String[] connected = {"CONNECTED"}; String[] notconnected = {"DISCONNECTED", "EXITING"}; @@ -453,13 +365,22 @@ synchronized public static LogItem[] getlogbuffer() { } - public static void updateStateString(String state, String msg) { + static void updateStateString(String state, String msg) { + // We want to skip announcing that we are trying to get the configuration since + // this is just polling until the user input has finished.be + if (mLastLevel == ConnectionStatus.LEVEL_WAITING_FOR_USER_INPUT && state.equals("GET_CONFIG")) + return; int rid = getLocalizedState(state); ConnectionStatus level = getLevel(state); updateStateString(state, msg, rid, level); } - public synchronized static void updateStateString(String state, String msg, int resid, ConnectionStatus level) { + public synchronized static void updateStateString(String state, String msg, int resid, ConnectionStatus level) + { + updateStateString(state, msg, resid, level, null); + } + + public synchronized static void updateStateString(String state, String msg, int resid, ConnectionStatus level, Intent intent) { // Workound for OpenVPN doing AUTH and wait and being connected // Simply ignore these state if (mLastLevel == ConnectionStatus.LEVEL_CONNECTED && @@ -472,10 +393,11 @@ public synchronized static void updateStateString(String state, String msg, int mLaststatemsg = msg; mLastStateresid = resid; mLastLevel = level; + mLastIntent = intent; for (StateListener sl : stateListener) { - sl.updateState(state, msg, resid, level); + sl.updateState(state, msg, resid, level, intent); } //newLogItem(new LogItem((LogLevel.DEBUG), String.format("New OpenVPN Status (%s->%s): %s",state,level.toString(),msg))); } @@ -496,17 +418,35 @@ public static void logDebug(int resourceId, Object... args) { newLogItem(new LogItem(LogLevel.DEBUG, resourceId, args)); } + static void newLogItem(LogItem logItem) { + newLogItem(logItem, false); + } + + + synchronized static void newLogItem(LogItem logItem, boolean cachedLine) { + if (cachedLine) { + logbuffer.addFirst(logItem); + } else { + logbuffer.addLast(logItem); + if (mLogFileHandler != null) { + Message m = mLogFileHandler.obtainMessage(LogFileHandler.LOG_MESSAGE, logItem); + mLogFileHandler.sendMessage(m); + } + } - private synchronized static void newLogItem(LogItem logItem) { - logbuffer.addLast(logItem); - if (logbuffer.size() > MAXLOGENTRIES) - logbuffer.removeFirst(); + if (logbuffer.size() > MAXLOGENTRIES + MAXLOGENTRIES / 2) { + while (logbuffer.size() > MAXLOGENTRIES) + logbuffer.removeFirst(); + if (mLogFileHandler != null) + mLogFileHandler.sendMessage(mLogFileHandler.obtainMessage(LogFileHandler.TRIM_LOG_FILE)); + } for (LogListener ll : logListener) { ll.newLog(logItem); } } + public static void logError(String msg) { newLogItem(new LogItem(LogLevel.ERROR, msg)); @@ -536,17 +476,10 @@ public static void logMessageOpenVPN(LogLevel level, int ovpnlevel, String messa public static synchronized void updateByteCount(long in, long out) { - long lastIn = mlastByteCount[0]; - long lastOut = mlastByteCount[1]; - long diffIn = mlastByteCount[2] = in - lastIn; - long diffOut = mlastByteCount[3] = out - lastOut; + TrafficHistory.LastDiff diff = trafficHistory.add(in, out); - - mlastByteCount = new long[]{in, out, diffIn, diffOut}; for (ByteCountListener bcl : byteCountListener) { - bcl.updateByteCount(in, out, diffIn, diffOut); + bcl.updateByteCount(in, out, diff.getDiffIn(), diff.getDiffOut()); } } - - } diff --git a/app/src/main/java/de/blinkt/openvpn/core/X509Utils.java b/app/src/main/java/de/blinkt/openvpn/core/X509Utils.java index 21e368e..7eca388 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/X509Utils.java +++ b/app/src/main/java/de/blinkt/openvpn/core/X509Utils.java @@ -1,15 +1,17 @@ /* - * Copyright (c) 2012-2014 Arne Schwabe + * Copyright (c) 2012-2016 Arne Schwabe * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt */ package de.blinkt.openvpn.core; +import android.annotation.SuppressLint; import android.content.Context; +import android.content.res.Resources; import android.text.TextUtils; import ht.vpn.android.R; -import ht.vpn.android.VpnProfile; +import de.blinkt.openvpn.VpnProfile; import org.spongycastle.util.io.pem.PemObject; import org.spongycastle.util.io.pem.PemReader; @@ -20,30 +22,39 @@ import java.lang.reflect.Method; import java.security.cert.Certificate; import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateFactory; +import java.security.cert.CertificateNotYetValidException; import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Date; import java.util.Hashtable; +import java.util.Vector; public class X509Utils { - public static Certificate getCertificateFromFile(String certfilename) throws FileNotFoundException, CertificateException { + public static Certificate[] getCertificatesFromFile(String certfilename) throws FileNotFoundException, CertificateException { CertificateFactory certFact = CertificateFactory.getInstance("X.509"); - InputStream inStream; - + Vector certificates = new Vector<>(); if(VpnProfile.isEmbedded(certfilename)) { - // The java certifcate reader is ... kind of stupid - // It does NOT ignore chars before the --BEGIN ... int subIndex = certfilename.indexOf("-----BEGIN CERTIFICATE-----"); - subIndex = Math.max(0,subIndex); - inStream = new ByteArrayInputStream(certfilename.substring(subIndex).getBytes()); + do { + // The java certifcate reader is ... kind of stupid + // It does NOT ignore chars before the --BEGIN ... + subIndex = Math.max(0, subIndex); + InputStream inStream = new ByteArrayInputStream(certfilename.substring(subIndex).getBytes()); + certificates.add(certFact.generateCertificate(inStream)); + subIndex = certfilename.indexOf("-----BEGIN CERTIFICATE-----", subIndex+1); + } while (subIndex > 0); + return certificates.toArray(new Certificate[certificates.size()]); } else { - inStream = new FileInputStream(certfilename); + InputStream inStream = new FileInputStream(certfilename); + return new Certificate[] {certFact.generateCertificate(inStream)}; } - return certFact.generateCertificate(inStream); } public static PemObject readPemObjectFromFile (String keyfilename) throws IOException { @@ -67,9 +78,10 @@ public static PemObject readPemObjectFromFile (String keyfilename) throws IOExce public static String getCertificateFriendlyName (Context c, String filename) { if(!TextUtils.isEmpty(filename)) { try { - X509Certificate cert = (X509Certificate) getCertificateFromFile(filename); - - return getCertificateFriendlyName(cert); + X509Certificate cert = (X509Certificate) getCertificatesFromFile(filename)[0]; + String friendlycn = getCertificateFriendlyName(cert); + friendlycn = getCertificateValidityString(cert, c.getResources()) + friendlycn; + return friendlycn; } catch (Exception e) { VpnStatus.logError("Could not read certificate" + e.getLocalizedMessage()); @@ -78,6 +90,40 @@ public static String getCertificateFriendlyName (Context c, String filename) { return c.getString(R.string.cannotparsecert); } + public static String getCertificateValidityString(X509Certificate cert, Resources res) { + try { + cert.checkValidity(); + } catch (CertificateExpiredException ce) { + return "EXPIRED: "; + } catch (CertificateNotYetValidException cny) { + return "NOT YET VALID: "; + } + + Date certNotAfter = cert.getNotAfter(); + Date now = new Date(); + long timeLeft = certNotAfter.getTime() - now.getTime(); // Time left in ms + + // More than 72h left, display days + // More than 3 months display months + if (timeLeft > 90l* 24 * 3600 * 1000) { + long months = getMonthsDifference(now, certNotAfter); + return res.getQuantityString(R.plurals.months_left, (int) months, months); + } else if (timeLeft > 72 * 3600 * 1000) { + long days = timeLeft / (24 * 3600 * 1000); + return res.getQuantityString(R.plurals.days_left, (int) days, days); + } else { + long hours = timeLeft / (3600 * 1000); + + return res.getQuantityString(R.plurals.hours_left, (int)hours, hours); + } + } + + public static int getMonthsDifference(Date date1, Date date2) { + int m1 = date1.getYear() * 12 + date1.getMonth(); + int m2 = date2.getYear() * 12 + date2.getMonth(); + return m2 - m1 + 1; + } + public static String getCertificateFriendlyName(X509Certificate cert) { X500Principal principal = cert.getSubjectX500Principal(); byte[] encodedSubject = principal.getEncoded(); @@ -86,7 +132,7 @@ public static String getCertificateFriendlyName(X509Certificate cert) { /* Hack so we do not have to ship a whole Spongy/bouncycastle */ Exception exp=null; try { - Class X509NameClass = Class.forName("com.android.org.bouncycastle.asn1.x509.X509Name"); + @SuppressLint("PrivateApi") Class X509NameClass = Class.forName("com.android.org.bouncycastle.asn1.x509.X509Name"); Method getInstance = X509NameClass.getMethod("getInstance",Object.class); Hashtable defaultSymbols = (Hashtable) X509NameClass.getField("DefaultSymbols").get(X509NameClass); diff --git a/app/src/main/java/ht/vpn/android/LaunchVPN.java b/app/src/main/java/ht/vpn/android/LaunchVPN.java index cc507ab..f110f3b 100644 --- a/app/src/main/java/ht/vpn/android/LaunchVPN.java +++ b/app/src/main/java/ht/vpn/android/LaunchVPN.java @@ -1,29 +1,50 @@ /* - * Copyright (c) 2012-2014 Arne Schwabe + * Copyright (c) 2012-2016 Arne Schwabe * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt */ package ht.vpn.android; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.app.Activity; import android.app.AlertDialog; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; +import android.content.ServiceConnection; import android.content.SharedPreferences; import android.net.VpnService; +import android.os.Build; import android.os.Bundle; -import android.preference.PreferenceManager; +import android.os.IBinder; +import android.os.RemoteException; +import android.text.InputType; +import android.text.TextUtils; +import android.text.method.PasswordTransformationMethod; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.EditText; + +import androidx.appcompat.view.ContextThemeWrapper; import java.io.IOException; +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.core.ConnectionStatus; +import de.blinkt.openvpn.core.IServiceStatus; +import de.blinkt.openvpn.core.OpenVPNStatusService; +import de.blinkt.openvpn.core.PasswordCache; +import de.blinkt.openvpn.core.Preferences; import de.blinkt.openvpn.core.ProfileManager; import de.blinkt.openvpn.core.VPNLaunchHelper; import de.blinkt.openvpn.core.VpnStatus; -import de.blinkt.openvpn.core.VpnStatus.ConnectionStatus; -import ht.vpn.android.activities.GrantPermissionsActivity; -import ht.vpn.android.activities.LoginActivity; -import ht.vpn.android.utils.PrefUtils; +import ht.vpn.android.api.ExternalAppDatabase; /** * This Activity actually handles two stages of a launcher shortcut's life cycle. @@ -55,27 +76,52 @@ public class LaunchVPN extends Activity { public static final String EXTRA_KEY = "ht.vpn.android.shortcutProfileUUID"; public static final String EXTRA_NAME = "ht.vpn.android.shortcutProfileName"; public static final String EXTRA_HIDELOG = "ht.vpn.android.showNoLogWindow"; + public static final String CLEARLOG = "clearlogconnect"; - private static final int START_VPN_PROFILE = 70, LOGIN_ACTIVITY = 100; + + private static final int START_VPN_PROFILE = 70; - private ProfileManager mPM; private VpnProfile mSelectedProfile; private boolean mhideLog = false; private boolean mCmfixed = false; + private String mTransientAuthPW; + private String mTransientCertOrPCKS12PW; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); + startVpnFromIntent(); + } - mPM = ProfileManager.getInstance(this); + private ServiceConnection mConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName componentName, IBinder binder) { + IServiceStatus service = IServiceStatus.Stub.asInterface(binder); + try { + if (mTransientAuthPW != null) - } + service.setCachedPassword(mSelectedProfile.getUUIDString(), PasswordCache.AUTHPASSWORD, mTransientAuthPW); + if (mTransientCertOrPCKS12PW != null) + service.setCachedPassword(mSelectedProfile.getUUIDString(), PasswordCache.PCKS12ORCERTPASSWORD, mTransientCertOrPCKS12PW); - @Override - protected void onStart() { - super.onStart(); + onActivityResult(START_VPN_PROFILE, Activity.RESULT_OK, null); + + } catch (RemoteException e) { + e.printStackTrace(); + } + + unbindService(this); + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + + } + }; + + protected void startVpnFromIntent() { // Resolve the intent final Intent intent = getIntent(); @@ -85,6 +131,10 @@ protected void onStart() { if (Intent.ACTION_MAIN.equals(action)) { + // Check if we need to clear the log + if (Preferences.getDefaultSharedPreferences(this).getBoolean(CLEARLOG, true)) + VpnStatus.clearLog(); + // we got called to be the starting point, most likely a shortcut String shortcutUUID = intent.getStringExtra(EXTRA_KEY); String shortcutName = intent.getStringExtra(EXTRA_NAME); @@ -94,29 +144,80 @@ protected void onStart() { if (shortcutName != null && profileToConnect == null) profileToConnect = ProfileManager.getInstance(this).getProfileByName(shortcutName); + if (profileToConnect == null) { VpnStatus.logError(R.string.shortcut_profile_notfound); // show Log window to display error + showLogWindow(); finish(); - return; + } else { + mSelectedProfile = profileToConnect; + launchVPN(); } - - mSelectedProfile = profileToConnect; - launchVPN(); - } } private void askForPW(final int type) { - if(!PrefUtils.contains(this, Preferences.USERNAME) || !PrefUtils.contains(this, Preferences.PASSWORD)) { - if (type == R.string.password) { - Intent intent = new Intent(this, LoginActivity.class); - intent.putExtra(LoginActivity.PROCEED_TO_MAIN, false); - startActivityForResult(intent, LOGIN_ACTIVITY); - } + + final EditText entry = new EditText(this); + + entry.setSingleLine(); + entry.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + entry.setTransformationMethod(new PasswordTransformationMethod()); + + AlertDialog.Builder dialog = new AlertDialog.Builder(this); + dialog.setTitle(getString(R.string.pw_request_dialog_title, getString(type))); + dialog.setMessage(getString(R.string.pw_request_dialog_prompt, mSelectedProfile.mName)); + + LayoutInflater inflater = LayoutInflater.from(new ContextThemeWrapper(getApplicationContext(), R.style.Theme_VPNht)); + @SuppressLint("InflateParams") final View userpwlayout = inflater.inflate(R.layout.userpass, null, false); + + if (type == R.string.password) { + ((EditText) userpwlayout.findViewById(R.id.username)).setText(mSelectedProfile.mUsername); + ((EditText) userpwlayout.findViewById(R.id.password)).setText(mSelectedProfile.mPassword); + ((CheckBox) userpwlayout.findViewById(R.id.savePassword)).setChecked(!TextUtils.isEmpty(mSelectedProfile.mPassword)); + ((CheckBox) userpwlayout.findViewById(R.id.showPassword)).setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) + ((EditText) userpwlayout.findViewById(R.id.password)).setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); + else + ((EditText) userpwlayout.findViewById(R.id.password)).setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + } + }); + + dialog.setView(userpwlayout); } else { - onActivityResult(LOGIN_ACTIVITY, RESULT_OK, null); + dialog.setView(entry); } + + dialog.setPositiveButton(android.R.string.ok, + (callingDialog, which) -> { + if (type == R.string.password) { + mSelectedProfile.mUsername = ((EditText) userpwlayout.findViewById(R.id.username)).getText().toString(); + + String pw = ((EditText) userpwlayout.findViewById(R.id.password)).getText().toString(); + if (((CheckBox) userpwlayout.findViewById(R.id.savePassword)).isChecked()) { + mSelectedProfile.mPassword = pw; + } else { + mSelectedProfile.mPassword = null; + mTransientAuthPW = pw; + } + } else { + mTransientCertOrPCKS12PW = entry.getText().toString(); + } + Intent intent = new Intent(LaunchVPN.this, OpenVPNStatusService.class); + bindService(intent, mConnection, Context.BIND_AUTO_CREATE); + }); + dialog.setNegativeButton(android.R.string.cancel, + (callingDialog, which) -> { + VpnStatus.updateStateString("USER_VPN_PASSWORD_CANCELLED", "", R.string.state_user_vpn_password_cancelled, + ConnectionStatus.LEVEL_NOTCONNECTED); + finish(); + }); + + dialog.create().show(); + } @Override @@ -125,31 +226,43 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == START_VPN_PROFILE) { if (resultCode == Activity.RESULT_OK) { - int needpw = mSelectedProfile.needUserPWInput(false); + int needpw = mSelectedProfile.needUserPWInput(mTransientCertOrPCKS12PW, mTransientAuthPW); if (needpw != 0) { VpnStatus.updateStateString("USER_VPN_PASSWORD", "", R.string.state_user_vpn_password, ConnectionStatus.LEVEL_WAITING_FOR_USER_INPUT); askForPW(needpw); } else { - new Thread(startOpenVpnRunnable).start(); + SharedPreferences prefs = Preferences.getDefaultSharedPreferences(this); + boolean showLogWindow = prefs.getBoolean("showlogwindow", true); + + if (!mhideLog && showLogWindow) + showLogWindow(); + ProfileManager.updateLRU(this, mSelectedProfile); + VPNLaunchHelper.startOpenVpn(mSelectedProfile, getBaseContext()); + finish(); } } else if (resultCode == Activity.RESULT_CANCELED) { // User does not want us to start, so we just vanish - VpnStatus.updateStateString("USER_VPN_PERMISSION_CANCELLED", "", R.string.state_user_vpn_permission_cancelled, ConnectionStatus.LEVEL_NOTCONNECTED); - finish(); - } - } else if(requestCode == LOGIN_ACTIVITY) { - if (resultCode == RESULT_OK) { - mSelectedProfile.mUsername = PrefUtils.get(this, Preferences.USERNAME, ""); - mSelectedProfile.mPassword = PrefUtils.get(this, Preferences.PASSWORD, ""); - onActivityResult(START_VPN_PROFILE, Activity.RESULT_OK, null); - } else { - VpnStatus.updateStateString("USER_VPN_PASSWORD_CANCELLED", "", R.string.state_user_vpn_password_cancelled, ConnectionStatus.LEVEL_NOTCONNECTED); + VpnStatus.updateStateString("USER_VPN_PERMISSION_CANCELLED", "", R.string.state_user_vpn_permission_cancelled, + ConnectionStatus.LEVEL_NOTCONNECTED); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + VpnStatus.logError(R.string.nought_alwayson_warning); + finish(); } } } + void showLogWindow() { + + Intent startLW = new Intent(); + startLW.setComponent(new ComponentName(this, getPackageName() + ".activities.LogWindow")); + startLW.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + startActivity(startLW); + + } + void showConfigErrorDialog(int vpnok) { AlertDialog.Builder d = new AlertDialog.Builder(this); d.setTitle(R.string.config_error_found); @@ -162,9 +275,27 @@ public void onClick(DialogInterface dialog, int which) { } }); + d.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + finish(); + } + }); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) + setOnDismissListener(d); d.show(); } + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + private void setOnDismissListener(AlertDialog.Builder d) { + d.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + finish(); + } + }); + } + void launchVPN() { int vpnok = mSelectedProfile.checkProfile(this); if (vpnok != R.string.no_error_found) { @@ -174,7 +305,7 @@ void launchVPN() { Intent intent = VpnService.prepare(this); // Check if we want to fix /dev/tun - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + SharedPreferences prefs = Preferences.getDefaultSharedPreferences(this); boolean usecm9fix = prefs.getBoolean("useCM9Fix", false); boolean loadTunModule = prefs.getBoolean("loadTunModule", false); @@ -185,11 +316,18 @@ void launchVPN() { execeuteSUcmd("chown system /dev/tun"); } - if (intent != null) { - VpnStatus.updateStateString("USER_VPN_PERMISSION", "", R.string.state_user_vpn_permission, ConnectionStatus.LEVEL_WAITING_FOR_USER_INPUT); + VpnStatus.updateStateString("USER_VPN_PERMISSION", "", R.string.state_user_vpn_permission, + ConnectionStatus.LEVEL_WAITING_FOR_USER_INPUT); // Start the query - startActivityForResult(new Intent(this, GrantPermissionsActivity.class), START_VPN_PROFILE); + try { + startActivityForResult(intent, START_VPN_PROFILE); + } catch (ActivityNotFoundException ane) { + // Shame on you Sony! At least one user reported that + // an official Sony Xperia Arc S image triggers this exception + VpnStatus.logError(R.string.no_vpn_support_image); + showLogWindow(); + } } else { onActivityResult(START_VPN_PROFILE, Activity.RESULT_OK, null); } @@ -197,26 +335,14 @@ void launchVPN() { } private void execeuteSUcmd(String command) { - ProcessBuilder pb = new ProcessBuilder("su", "-c", command); try { + ProcessBuilder pb = new ProcessBuilder("su", "-c", command); Process p = pb.start(); int ret = p.waitFor(); if (ret == 0) mCmfixed = true; - } catch (InterruptedException e) { - VpnStatus.logException("SU command", e); - - } catch (IOException e) { + } catch (InterruptedException | IOException e) { VpnStatus.logException("SU command", e); } } - - private Runnable startOpenVpnRunnable = new Runnable() { - @Override - public void run() { - VPNLaunchHelper.startOpenVpn(mSelectedProfile, getBaseContext()); - finish(); - } - }; - } diff --git a/app/src/main/java/ht/vpn/android/OnBootReceiver.java b/app/src/main/java/ht/vpn/android/OnBootReceiver.java index adf31b5..c96dcf4 100755 --- a/app/src/main/java/ht/vpn/android/OnBootReceiver.java +++ b/app/src/main/java/ht/vpn/android/OnBootReceiver.java @@ -14,6 +14,7 @@ import java.io.IOException; import java.io.StringReader; +import de.blinkt.openvpn.VpnProfile; import de.blinkt.openvpn.core.ConfigParser; import de.blinkt.openvpn.core.ProfileManager; import de.blinkt.openvpn.core.VPNLaunchHelper; @@ -41,12 +42,12 @@ void launchVPN(Context context) { ConfigParser configParser = new ConfigParser(); try { Server server = new Server(); - server.hostname = PrefUtils.get(context, Preferences.LAST_CONNECTED_HOSTNAME, "hub.vpn.ht"); + server.hostname = PrefUtils.get(context, Preferences.LAST_CONNECTED_HOSTNAME, "hub.digital.ht"); server.country = PrefUtils.get(context, Preferences.LAST_CONNECTED_COUNTRY, "Nearest"); configParser.parseConfig(new StringReader(VPNHTConfig.generate(PrefUtils.getPrefs(context), server, PrefUtils.get(context, Preferences.LAST_CONNECTED_FIREWALL, false)))); VpnProfile profile = configParser.convertProfile(); profile.mName = server.country; - ProfileManager.setTemporaryProfile(profile); + ProfileManager.setTemporaryProfile(VPNhtApplication.getAppContext(), profile); Intent intent = new Intent(context, LaunchVPN.class); intent.setAction(Intent.ACTION_MAIN); diff --git a/app/src/main/java/ht/vpn/android/Preferences.java b/app/src/main/java/ht/vpn/android/Preferences.java index 2004bd3..4776adb 100644 --- a/app/src/main/java/ht/vpn/android/Preferences.java +++ b/app/src/main/java/ht/vpn/android/Preferences.java @@ -5,9 +5,7 @@ public class Preferences { public final static String PASSWORD = "password"; public final static String ENC_TYPE = "encryption"; public final static String KILLSWITCH = "killswitch"; - public final static String RECONNECT_NETCHANGE = "netchangereconnect"; public final static String RECONNECT_REBOOT = "rebootreconnect"; - public final static String SMARTDNS = "smartdns"; public final static String LOG_VERBOSITY = "log_verbosity"; public final static String LAST_CONNECTED_HOSTNAME = "hostname_last"; public final static String LAST_CONNECTED_COUNTRY = "country_latest"; diff --git a/app/src/main/java/ht/vpn/android/VPNhtApplication.java b/app/src/main/java/ht/vpn/android/VPNhtApplication.java index 7fd6683..c989a70 100644 --- a/app/src/main/java/ht/vpn/android/VPNhtApplication.java +++ b/app/src/main/java/ht/vpn/android/VPNhtApplication.java @@ -1,16 +1,14 @@ package ht.vpn.android; -import android.app.Application; +import android.content.Intent; -import com.crashlytics.android.Crashlytics; - -import de.blinkt.openvpn.core.PRNGFixes; +import de.blinkt.openvpn.core.ConnectionStatus; +import de.blinkt.openvpn.core.ICSOpenVPNApplication; import de.blinkt.openvpn.core.VpnStatus; import ht.vpn.android.utils.PrefUtils; -import io.fabric.sdk.android.Fabric; import timber.log.Timber; -public class VPNhtApplication extends Application implements VpnStatus.StateListener { +public class VPNhtApplication extends ICSOpenVPNApplication implements VpnStatus.StateListener { private static VPNhtApplication sThis; private boolean mCreated = false; @@ -18,8 +16,6 @@ public class VPNhtApplication extends Application implements VpnStatus.StateList @Override public void onCreate() { super.onCreate(); - PRNGFixes.apply(); - Fabric.with(this, new Crashlytics()); sThis = this; if (BuildConfig.DEBUG) { @@ -34,16 +30,20 @@ public static VPNhtApplication getAppContext() { } @Override - public void updateState(String state, String logmessage, int localizedResId, VpnStatus.ConnectionStatus level) { + public void updateState(String state, String logmessage, int localizedResId, ConnectionStatus level, Intent intent) { if(!mCreated) { mCreated = true; return; } - if(level.equals(VpnStatus.ConnectionStatus.LEVEL_NOTCONNECTED)) { + if(level.equals(ConnectionStatus.LEVEL_NOTCONNECTED)) { PrefUtils.remove(this, Preferences.LAST_CONNECTED_HOSTNAME); PrefUtils.remove(this, Preferences.LAST_CONNECTED_FIREWALL); PrefUtils.remove(this, Preferences.LAST_CONNECTED_COUNTRY); } } + + @Override + public void setConnectedVPN(String uuid) { + } } diff --git a/app/src/main/java/ht/vpn/android/activities/BaseActivity.java b/app/src/main/java/ht/vpn/android/activities/BaseActivity.java index 667361a..00435ef 100644 --- a/app/src/main/java/ht/vpn/android/activities/BaseActivity.java +++ b/app/src/main/java/ht/vpn/android/activities/BaseActivity.java @@ -1,19 +1,19 @@ package ht.vpn.android.activities; import android.os.Bundle; -import android.os.PersistableBundle; -import android.support.annotation.Nullable; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; import butterknife.ButterKnife; -import butterknife.Bind; +import butterknife.BindView; import ht.vpn.android.R; -public class BaseActivity extends AppCompatActivity { +public abstract class BaseActivity extends AppCompatActivity { @Nullable - @Bind(R.id.toolbar) + @BindView(R.id.toolbar) Toolbar mToolbar; protected void onCreate(Bundle savedInstanceState, int layoutRes) { diff --git a/app/src/main/java/ht/vpn/android/activities/DisconnectVPN.java b/app/src/main/java/ht/vpn/android/activities/DisconnectVPN.java index 98b39e0..cb81e35 100755 --- a/app/src/main/java/ht/vpn/android/activities/DisconnectVPN.java +++ b/app/src/main/java/ht/vpn/android/activities/DisconnectVPN.java @@ -13,13 +13,16 @@ import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; +import android.os.RemoteException; +import de.blinkt.openvpn.core.IOpenVPNServiceInternal; +import de.blinkt.openvpn.core.VpnStatus; import ht.vpn.android.R; import de.blinkt.openvpn.core.OpenVPNService; import de.blinkt.openvpn.core.ProfileManager; public class DisconnectVPN extends Activity implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener { - protected OpenVPNService mService; + protected IOpenVPNServiceInternal mService; private ServiceConnection mConnection = new ServiceConnection() { @@ -27,9 +30,7 @@ public class DisconnectVPN extends Activity implements DialogInterface.OnClickLi @Override public void onServiceConnected(ComponentName className, IBinder service) { - // We've bound to LocalService, cast the IBinder and get LocalService instance - OpenVPNService.LocalBinder binder = (OpenVPNService.LocalBinder) service; - mService = binder.getService(); + mService = IOpenVPNServiceInternal.Stub.asInterface(service); } @Override @@ -81,8 +82,13 @@ private void showDisconnectDialog() { public void onClick(DialogInterface dialog, int which) { if (which == DialogInterface.BUTTON_POSITIVE) { ProfileManager.setConntectedVpnProfileDisconnected(this); - if (mService != null && mService.getManagement() != null) - mService.getManagement().stopVPN(); + if (mService != null) { + try { + mService.stopVPN(false); + } catch (RemoteException e) { + VpnStatus.logException(e); + } + } } finish(); } diff --git a/app/src/main/java/ht/vpn/android/activities/LogActivity.java b/app/src/main/java/ht/vpn/android/activities/LogActivity.java index 646dc52..6ed1666 100755 --- a/app/src/main/java/ht/vpn/android/activities/LogActivity.java +++ b/app/src/main/java/ht/vpn/android/activities/LogActivity.java @@ -1,7 +1,8 @@ package ht.vpn.android.activities; import android.os.Bundle; -import android.support.v7.app.ActionBar; + +import androidx.appcompat.app.ActionBar; import ht.vpn.android.R; import ht.vpn.android.fragment.LogFragment; diff --git a/app/src/main/java/ht/vpn/android/activities/LoginActivity.java b/app/src/main/java/ht/vpn/android/activities/LoginActivity.java index 809d859..3fa5f6c 100644 --- a/app/src/main/java/ht/vpn/android/activities/LoginActivity.java +++ b/app/src/main/java/ht/vpn/android/activities/LoginActivity.java @@ -1,20 +1,22 @@ package ht.vpn.android.activities; +import android.annotation.SuppressLint; import android.content.Intent; import android.net.Uri; import android.os.Bundle; -import android.support.annotation.NonNull; import android.view.View; import android.widget.EditText; import android.widget.Toast; +import androidx.annotation.NonNull; + import com.mobsandgeeks.saripaar.ValidationError; import com.mobsandgeeks.saripaar.Validator; import com.mobsandgeeks.saripaar.annotation.NotEmpty; import java.util.List; -import butterknife.Bind; +import butterknife.BindView; import butterknife.OnClick; import ht.vpn.android.Preferences; import ht.vpn.android.R; @@ -34,12 +36,13 @@ public class LoginActivity extends BaseActivity implements Validator.ValidationL private boolean mDoNotSave = false; @NotEmpty - @Bind(R.id.usernameEdit) + @BindView(R.id.usernameEdit) EditText mUsername; @NotEmpty - @Bind(R.id.passwordEdit) + @BindView(R.id.passwordEdit) EditText mPassword; + @SuppressLint("MissingSuperCall") @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState, R.layout.activity_login); diff --git a/app/src/main/java/ht/vpn/android/activities/MainActivity.java b/app/src/main/java/ht/vpn/android/activities/MainActivity.java index b6019da..d335eac 100644 --- a/app/src/main/java/ht/vpn/android/activities/MainActivity.java +++ b/app/src/main/java/ht/vpn/android/activities/MainActivity.java @@ -1,34 +1,31 @@ package ht.vpn.android.activities; -import android.app.Activity; -import android.content.ActivityNotFoundException; +import android.annotation.SuppressLint; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; -import android.content.SharedPreferences; -import android.net.VpnService; import android.os.Bundle; import android.os.IBinder; -import android.preference.PreferenceManager; import android.view.Menu; -import android.view.MenuInflater; import android.view.MenuItem; import android.widget.Toast; +import de.blinkt.openvpn.core.ConnectionStatus; +import de.blinkt.openvpn.core.IOpenVPNServiceInternal; import de.blinkt.openvpn.core.OpenVPNService; import de.blinkt.openvpn.core.VpnStatus; import ht.vpn.android.Preferences; import ht.vpn.android.R; -import ht.vpn.android.VPNhtApplication; import ht.vpn.android.utils.NetworkUtils; import ht.vpn.android.utils.PrefUtils; public class MainActivity extends BaseActivity implements VpnStatus.StateListener { - private OpenVPNService mService; + private IOpenVPNServiceInternal mService; private Boolean mConnected = false; + @SuppressLint("MissingSuperCall") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState, R.layout.activity_main); @@ -95,9 +92,7 @@ public void startLoginActivity() { private ServiceConnection mConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName className, IBinder service) { - // We've bound to LocalService, cast the IBinder and get LocalService instance - OpenVPNService.LocalBinder binder = (OpenVPNService.LocalBinder) service; - mService = binder.getService(); + mService = IOpenVPNServiceInternal.Stub.asInterface(service); } @Override @@ -106,13 +101,17 @@ public void onServiceDisconnected(ComponentName arg0) { } }; - public OpenVPNService getService() { + public IOpenVPNServiceInternal getService() { return mService; } @Override - public void updateState(String state, String logmessage, int localizedResId, VpnStatus.ConnectionStatus level) { - mConnected = level.equals(VpnStatus.ConnectionStatus.LEVEL_CONNECTED); + public void updateState(String state, String logmessage, int localizedResId, ConnectionStatus level, Intent Intent) { + mConnected = level.equals(ConnectionStatus.LEVEL_CONNECTED); supportInvalidateOptionsMenu(); } + + @Override + public void setConnectedVPN(String uuid) { + } } diff --git a/app/src/main/java/ht/vpn/android/activities/PreferencesActivity.java b/app/src/main/java/ht/vpn/android/activities/PreferencesActivity.java index ffc3d60..b620770 100644 --- a/app/src/main/java/ht/vpn/android/activities/PreferencesActivity.java +++ b/app/src/main/java/ht/vpn/android/activities/PreferencesActivity.java @@ -3,22 +3,21 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; -import android.graphics.Color; import android.os.Bundle; -import android.support.v7.app.ActionBar; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; import android.view.MenuItem; import android.view.View; +import androidx.appcompat.app.ActionBar; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import butterknife.Bind; +import butterknife.BindView; import ht.vpn.android.Preferences; import ht.vpn.android.R; -import ht.vpn.android.VpnProfile; import ht.vpn.android.content.PrefItem; import ht.vpn.android.content.adapters.PreferencesListAdapter; import ht.vpn.android.fragment.dialog.StringArraySelectorDialogFragment; @@ -29,7 +28,7 @@ public class PreferencesActivity extends BaseActivity { private List mPrefItems = new ArrayList<>(); private LinearLayoutManager mLayoutManager; - @Bind(R.id.recyclerView) + @BindView(R.id.recyclerView) RecyclerView mRecyclerView; @Override @@ -94,7 +93,7 @@ public String get(PrefItem item) { mPrefItems.add(getString(R.string.connection)); - final List options = Arrays.asList("BF-CBC", "AES-128-CBC", "AES-256-CBC"); + final List options = Arrays.asList("AES-128-CBC", "AES-256-CBC"); mPrefItems.add(new PrefItem(this, R.drawable.ic_prefs_encryption, R.string.cipher, Preferences.ENC_TYPE, "AES-128-CBC", new PrefItem.OnClickListener() { @Override @@ -115,22 +114,6 @@ public String get(PrefItem item) { ) ); - mPrefItems.add(new PrefItem(this, R.drawable.ic_prefs_smartdns, R.string.smartdns, Preferences.SMARTDNS, true, - new PrefItem.OnClickListener() { - @Override - public void onClick(PrefItem item) { - item.saveValue(!(Boolean) item.getValue()); - } - }, - new PrefItem.SubTitleGenerator() { - @Override - public String get(PrefItem item) { - return getString(R.string.smartdns_summary); - } - } - ) - ); - mPrefItems.add(getResources().getString(R.string.account)); mPrefItems.add(new PrefItem(this, R.drawable.ic_prefs_logout, R.string.logout, Preferences.USERNAME, 0, new PrefItem.OnClickListener() { diff --git a/app/src/main/java/ht/vpn/android/api/ExternalAppDatabase.java b/app/src/main/java/ht/vpn/android/api/ExternalAppDatabase.java index 9b0eef2..d448840 100755 --- a/app/src/main/java/ht/vpn/android/api/ExternalAppDatabase.java +++ b/app/src/main/java/ht/vpn/android/api/ExternalAppDatabase.java @@ -15,7 +15,7 @@ public class ExternalAppDatabase { - Context mContext; + private Context mContext; public ExternalAppDatabase(Context c) { mContext = c; @@ -29,7 +29,7 @@ boolean isAllowed(String packagename) { } - public Set getExtAppList() { + Set getExtAppList() { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); Set apps = prefs.getStringSet(PREFERENCES_KEY, new HashSet()); apps.add("pct.droid"); @@ -54,10 +54,9 @@ public void clearAllApiApps() { saveExtAppList(new HashSet()); } - public void removeApp(String packagename) { + void removeApp(String packagename) { Set allowedapps = getExtAppList(); allowedapps.remove(packagename); saveExtAppList(allowedapps); } - } diff --git a/app/src/main/java/ht/vpn/android/api/ExternalOpenVPNService.java b/app/src/main/java/ht/vpn/android/api/ExternalOpenVPNService.java index 11ad424..2b1954f 100755 --- a/app/src/main/java/ht/vpn/android/api/ExternalOpenVPNService.java +++ b/app/src/main/java/ht/vpn/android/api/ExternalOpenVPNService.java @@ -32,19 +32,24 @@ import java.util.List; import java.util.Set; +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.core.ConnectionStatus; +import de.blinkt.openvpn.core.IOpenVPNServiceInternal; +import ht.vpn.android.Preferences; import ht.vpn.android.R; -import ht.vpn.android.VpnProfile; import de.blinkt.openvpn.core.ConfigParser; import de.blinkt.openvpn.core.ConfigParser.ConfigParseError; import de.blinkt.openvpn.core.OpenVPNService; -import de.blinkt.openvpn.core.OpenVPNService.LocalBinder; import de.blinkt.openvpn.core.ProfileManager; import de.blinkt.openvpn.core.VPNLaunchHelper; import de.blinkt.openvpn.core.VpnStatus; -import de.blinkt.openvpn.core.VpnStatus.ConnectionStatus; import de.blinkt.openvpn.core.VpnStatus.StateListener; +import ht.vpn.android.VPNhtApplication; import ht.vpn.android.activities.ConfirmAPIActivity; import ht.vpn.android.activities.GrantPermissionsActivity; +import ht.vpn.android.api.IOpenVPNAPIService; +import ht.vpn.android.api.IOpenVPNStatusCallback; +import ht.vpn.android.utils.PrefUtils; @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) public class ExternalOpenVPNService extends Service implements StateListener { @@ -54,7 +59,7 @@ public class ExternalOpenVPNService extends Service implements StateListener { final RemoteCallbackList mCallbacks = new RemoteCallbackList<>(); - private OpenVPNService mService; + private IOpenVPNServiceInternal mService; private ExternalAppDatabase mExtAppDb; @@ -64,9 +69,7 @@ public class ExternalOpenVPNService extends Service implements StateListener { @Override public void onServiceConnected(ComponentName className, IBinder service) { - // We've bound to LocalService, cast the IBinder and get LocalService instance - LocalBinder binder = (LocalBinder) service; - mService = binder.getService(); + mService = IOpenVPNServiceInternal.Stub.asInterface(service); } @Override @@ -136,7 +139,7 @@ private void startProfile(VpnProfile vp) { /* Check if we need to show the confirmation dialog, * Check if we need to ask for username/password */ - int needpw = vp.needUserPWInput(false); + int needpw = vp.needUserPWInput(null, null); if (vpnPermissionIntent != null || needpw != 0) { Intent shortVPNIntent = new Intent(Intent.ACTION_MAIN); @@ -178,7 +181,10 @@ public void startVPN(String name, String inlineConfig) throws RemoteException { throw new RemoteException("The inline file would require user input: " + getString(needpw)); */ - ProfileManager.setTemporaryProfile(vp); + vp.mUsername = PrefUtils.get(ExternalOpenVPNService.this, Preferences.USERNAME, ""); + vp.mPassword = PrefUtils.get(ExternalOpenVPNService.this, Preferences.PASSWORD, ""); + + ProfileManager.setTemporaryProfile(ExternalOpenVPNService.this, vp); startProfile(vp); } catch (IOException | ConfigParseError e) { throw new RemoteException(e.getMessage()); @@ -285,8 +291,8 @@ public void unregisterStatusCallback(IOpenVPNStatusCallback cb) @Override public void disconnect() throws RemoteException { checkOpenVPNPermission(); - if (mService != null && mService.getManagement() != null) - mService.getManagement().stopVPN(); + if (mService != null) + mService.stopVPN(false); } @Override @@ -321,6 +327,11 @@ public void onDestroy() { VpnStatus.removeStateListener(this); } + @Override + public void setConnectedVPN(String uuid) { + + } + class UpdateMessage { public String state; public String logmessage; @@ -335,7 +346,7 @@ public UpdateMessage(String state, String logmessage, ConnectionStatus level) { } @Override - public void updateState(String state, String logmessage, int resid, ConnectionStatus level) { + public void updateState(String state, String logmessage, int resid, ConnectionStatus level, Intent intent) { mMostRecentState = new UpdateMessage(state, logmessage, level); if (ProfileManager.getLastConnectedVpn() != null) mMostRecentState.vpnUUID = ProfileManager.getLastConnectedVpn().getUUIDString(); diff --git a/app/src/main/java/ht/vpn/android/content/VPNHTConfig.java b/app/src/main/java/ht/vpn/android/content/VPNHTConfig.java index a3e4094..912f8ea 100644 --- a/app/src/main/java/ht/vpn/android/content/VPNHTConfig.java +++ b/app/src/main/java/ht/vpn/android/content/VPNHTConfig.java @@ -4,12 +4,16 @@ import java.util.ArrayList; +import de.blinkt.openvpn.core.VpnStatus; import ht.vpn.android.Preferences; import ht.vpn.android.network.responses.Server; public class VPNHTConfig { public static String generate(SharedPreferences preferences, Server server, Boolean firewall) { + VpnStatus.logInfo("Connecting to server: " + server.hostname + " " + server.countryCode); + VpnStatus.logInfo("Firewall option enabled: " + firewall); + ArrayList stringList = new ArrayList<>(); stringList.add("client"); @@ -37,7 +41,7 @@ public static String generate(SharedPreferences preferences, Server server, Bool stringList.add("ping 15"); stringList.add("ping-restart 45"); stringList.add("ping-timer-rem"); - stringList.add("ns-cert-type server"); + stringList.add("remote-cert-tls server"); stringList.add("mute 10"); stringList.add("comp-lzo"); stringList.add("verb 3"); @@ -45,12 +49,6 @@ public static String generate(SharedPreferences preferences, Server server, Bool stringList.add("fast-io"); stringList.add("auth-nocache"); - if(!preferences.getBoolean(Preferences.SMARTDNS, true)) { - stringList.add("route-nopull"); - stringList.add("redirect-gateway def1"); - stringList.add("dhcp-option DNS 10.11.0.1"); - } - if(firewall) { stringList.add(String.format("cipher %s", "AES-128-CBC")); } else { @@ -148,11 +146,6 @@ private static String[] getPortsByProtocol(String protocol) { stringList.add(Integer.toString(i)); } break; - case "BF-CBC": - for(int i = 1202; i < 1206; i++) { - stringList.add(Integer.toString(i)); - } - break; } return stringList.toArray(new String[stringList.size()]); } diff --git a/app/src/main/java/ht/vpn/android/content/adapters/LogListAdapter.java b/app/src/main/java/ht/vpn/android/content/adapters/LogListAdapter.java index 0eb4e39..ce9b733 100644 --- a/app/src/main/java/ht/vpn/android/content/adapters/LogListAdapter.java +++ b/app/src/main/java/ht/vpn/android/content/adapters/LogListAdapter.java @@ -2,20 +2,22 @@ import android.content.Context; import android.graphics.Color; -import android.support.v7.widget.RecyclerView; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import androidx.recyclerview.widget.RecyclerView; + import java.text.SimpleDateFormat; import java.util.Collections; import java.util.Date; import java.util.Locale; import java.util.Vector; +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.core.LogItem; import de.blinkt.openvpn.core.VpnStatus; import ht.vpn.android.Preferences; -import ht.vpn.android.VpnProfile; import ht.vpn.android.utils.PixelUtils; import ht.vpn.android.utils.PrefUtils; import ht.vpn.android.utils.ThreadUtils; @@ -28,8 +30,8 @@ public class LogListAdapter extends RecyclerView.Adapter mLogEntries = new Vector(); - private Vector mLevelLogEntries = new Vector(); + private Vector mLogEntries = new Vector(); + private Vector mLevelLogEntries = new Vector(); public LogListAdapter(Context context, RecyclerView recyclerView) { initLogBuffer(); @@ -52,7 +54,7 @@ public LogListAdapter.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) @Override public void onBindViewHolder(LogListAdapter.ViewHolder viewHolder, int position) { - VpnStatus.LogItem item = mLevelLogEntries.get(position); + LogItem item = mLevelLogEntries.get(position); String text = getTime(item) + " " + item.getString(mContext); viewHolder.itemView.setText(text); } @@ -68,7 +70,7 @@ public int getItemCount() { } @Override - public void newLog(VpnStatus.LogItem logItem) { + public void newLog(LogItem logItem) { if(mLogEntries.size() == MAX_LOG_ENTRIES) { mLevelLogEntries.remove(mLogEntries.get(0)); mLogEntries.removeElementAt(0); @@ -94,13 +96,13 @@ private void initLogBuffer() { private void initLevelEntries() { mLevelLogEntries.clear(); - for(VpnStatus.LogItem li: mLogEntries) { + for(LogItem li: mLogEntries) { if (li.getVerbosityLevel() <= mLogLevel || mLogLevel == VpnProfile.MAXLOGLEVEL) mLevelLogEntries.add(li); } } - private String getTime(VpnStatus.LogItem le) { + private String getTime(LogItem le) { Date d = new Date(le.getLogtime()); SimpleDateFormat timeformat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US); return timeformat.format(d); @@ -108,7 +110,7 @@ private String getTime(VpnStatus.LogItem le) { public String getLogAsString() { StringBuilder stringBuilder = new StringBuilder(); - for (VpnStatus.LogItem entry : mLogEntries) { + for (LogItem entry : mLogEntries) { stringBuilder.append(getTime(entry)); stringBuilder.append(' '); stringBuilder.append(entry.getString(mContext)); @@ -117,10 +119,10 @@ public String getLogAsString() { return stringBuilder.toString(); } - public class ViewHolder extends RecyclerView.ViewHolder { + static class ViewHolder extends RecyclerView.ViewHolder { TextView itemView; - public ViewHolder(View itemView) { + ViewHolder(View itemView) { super(itemView); this.itemView = (TextView) itemView; } diff --git a/app/src/main/java/ht/vpn/android/content/adapters/PreferencesListAdapter.java b/app/src/main/java/ht/vpn/android/content/adapters/PreferencesListAdapter.java index 37e0a09..4f2b091 100644 --- a/app/src/main/java/ht/vpn/android/content/adapters/PreferencesListAdapter.java +++ b/app/src/main/java/ht/vpn/android/content/adapters/PreferencesListAdapter.java @@ -1,7 +1,6 @@ package ht.vpn.android.content.adapters; import android.graphics.PorterDuff; -import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -9,10 +8,12 @@ import android.widget.ImageView; import android.widget.TextView; +import androidx.recyclerview.widget.RecyclerView; + import java.util.List; +import butterknife.BindView; import butterknife.ButterKnife; -import butterknife.Bind; import ht.vpn.android.R; import ht.vpn.android.VPNhtApplication; import ht.vpn.android.content.PrefItem; @@ -79,13 +80,13 @@ public int getItemViewType(int position) { public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { View itemView; - @Bind(android.R.id.icon) + @BindView(android.R.id.icon) ImageView icon; - @Bind(android.R.id.text1) + @BindView(android.R.id.text1) TextView text1; - @Bind(android.R.id.text2) + @BindView(android.R.id.text2) TextView text2; - @Bind(android.R.id.checkbox) + @BindView(android.R.id.checkbox) CheckBox checkBox; public ViewHolder(View itemView) { diff --git a/app/src/main/java/ht/vpn/android/fragment/BaseFragment.java b/app/src/main/java/ht/vpn/android/fragment/BaseFragment.java index 32e7842..c1e1c25 100644 --- a/app/src/main/java/ht/vpn/android/fragment/BaseFragment.java +++ b/app/src/main/java/ht/vpn/android/fragment/BaseFragment.java @@ -1,9 +1,10 @@ package ht.vpn.android.fragment; import android.os.Bundle; -import android.support.v4.app.Fragment; import android.view.View; +import androidx.fragment.app.Fragment; + import butterknife.ButterKnife; public abstract class BaseFragment extends Fragment { diff --git a/app/src/main/java/ht/vpn/android/fragment/LogFragment.java b/app/src/main/java/ht/vpn/android/fragment/LogFragment.java index 0cc51cf..3665469 100644 --- a/app/src/main/java/ht/vpn/android/fragment/LogFragment.java +++ b/app/src/main/java/ht/vpn/android/fragment/LogFragment.java @@ -2,9 +2,6 @@ import android.content.Intent; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -12,7 +9,11 @@ import android.view.View; import android.view.ViewGroup; -import butterknife.Bind; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import butterknife.BindView; import ht.vpn.android.R; import ht.vpn.android.content.adapters.LogListAdapter; @@ -20,7 +21,7 @@ public class LogFragment extends BaseFragment { private LogListAdapter mAdapter; - @Bind(R.id.recyclerView) + @BindView(R.id.recyclerView) RecyclerView mRecyclerView; public LogFragment() { diff --git a/app/src/main/java/ht/vpn/android/fragment/VPNFragment.java b/app/src/main/java/ht/vpn/android/fragment/VPNFragment.java index fab3a00..b44623e 100644 --- a/app/src/main/java/ht/vpn/android/fragment/VPNFragment.java +++ b/app/src/main/java/ht/vpn/android/fragment/VPNFragment.java @@ -8,9 +8,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.support.annotation.Nullable; -import android.support.design.widget.Snackbar; -import android.support.v7.widget.CardView; +import android.os.RemoteException; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -21,6 +19,9 @@ import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.Nullable; +import androidx.cardview.widget.CardView; + import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.OnMapReadyCallback; @@ -29,18 +30,23 @@ import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.Marker; import com.google.android.gms.maps.model.MarkerOptions; +import com.google.android.material.snackbar.Snackbar; import java.io.IOException; import java.io.StringReader; import java.net.SocketTimeoutException; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; -import butterknife.Bind; +import butterknife.BindView; import butterknife.OnClick; +import de.blinkt.openvpn.VpnProfile; import de.blinkt.openvpn.core.ConfigParser; +import de.blinkt.openvpn.core.ConnectionStatus; +import de.blinkt.openvpn.core.LogItem; import de.blinkt.openvpn.core.ProfileManager; import de.blinkt.openvpn.core.VPNLaunchHelper; import de.blinkt.openvpn.core.VpnStatus; @@ -48,7 +54,6 @@ import ht.vpn.android.Preferences; import ht.vpn.android.R; import ht.vpn.android.VPNhtApplication; -import ht.vpn.android.VpnProfile; import ht.vpn.android.activities.MainActivity; import ht.vpn.android.content.VPNHTConfig; import ht.vpn.android.fragment.dialog.ConnectingDialogFragment; @@ -66,6 +71,13 @@ import retrofit.client.Response; import timber.log.Timber; +import static de.blinkt.openvpn.core.VpnStatus.addLogListener; +import static de.blinkt.openvpn.core.VpnStatus.addStateListener; +import static de.blinkt.openvpn.core.VpnStatus.clearLog; +import static de.blinkt.openvpn.core.VpnStatus.getlogbuffer; +import static de.blinkt.openvpn.core.VpnStatus.removeLogListener; +import static de.blinkt.openvpn.core.VpnStatus.removeStateListener; + public class VPNFragment extends BaseFragment implements VpnStatus.LogListener, VpnStatus.StateListener { private GoogleMap mMap; @@ -77,26 +89,26 @@ public class VPNFragment extends BaseFragment implements VpnStatus.LogListener, private ArrayList mServers = new ArrayList<>(); private HashMap mMarkers = new HashMap<>(); private MainActivity mActivity; - private VpnStatus.ConnectionStatus mCurrentVPNState = VpnStatus.ConnectionStatus.LEVEL_NOTCONNECTED; + private ConnectionStatus mCurrentVPNState = ConnectionStatus.LEVEL_NOTCONNECTED; private Marker mCurrentPosMarker; - @Bind(R.id.scrollView) + @BindView(R.id.scrollView) TouchableScrollView mScrollView; - @Bind(R.id.connectedCard) + @BindView(R.id.connectedCard) CardView mConnectedCard; - @Bind(R.id.connectCard) + @BindView(R.id.connectCard) CardView mConnectCard; - @Bind(R.id.connectButton) + @BindView(R.id.connectButton) Button mConnectButton; - @Bind(R.id.disconnectButton) + @BindView(R.id.disconnectButton) Button mDisconnectButton; - @Bind(R.id.locationSpinner) + @BindView(R.id.locationSpinner) Spinner mLocationSpinner; - @Bind(R.id.firewallSwitch) + @BindView(R.id.firewallSwitch) CheckBox mFirewallSwitch; - @Bind(R.id.ipText) + @BindView(R.id.ipText) TextView mIPText; - @Bind(R.id.locationText) + @BindView(R.id.locationText) TextView mLocationText; @Nullable @@ -115,17 +127,17 @@ public void onActivityCreated(Bundle savedInstanceState) { mScrollView.addInterceptScrollView(mMapFragment.getView()); - VpnStatus.addLogListener(this); - VpnStatus.addStateListener(this); + addLogListener(this); + addStateListener(this); updateIPData(); } @Override public void onDestroy() { - super.onPause(); - VpnStatus.removeStateListener(this); - VpnStatus.removeLogListener(this); + super.onDestroy(); + removeStateListener(this); + removeLogListener(this); } @OnClick(R.id.connectButton) @@ -145,7 +157,7 @@ protected Void doInBackground(Void... voids) { return null; } - VpnStatus.clearLog(); + clearLog(); Server server = mServers.get(spinnerPosition); PrefUtils.save(mActivity, Preferences.LAST_CONNECTED_HOSTNAME, server.hostname); PrefUtils.save(mActivity, Preferences.LAST_CONNECTED_COUNTRY, server.country); @@ -158,7 +170,7 @@ protected Void doInBackground(Void... voids) { profile.mUsername = PrefUtils.get(mActivity, Preferences.USERNAME, ""); profile.mPassword = PrefUtils.get(mActivity, Preferences.PASSWORD, ""); profile.mAuthenticationType = VpnProfile.TYPE_USERPASS; - ProfileManager.setTemporaryProfile(profile); + ProfileManager.setTemporaryProfile(VPNhtApplication.getAppContext(), profile); Intent vpnPermissionIntent = VpnService.prepare(mActivity); @@ -184,9 +196,13 @@ protected Void doInBackground(Void... voids) { } @OnClick(R.id.disconnectButton) - public void diconnectClick(View v) { + void diconnectClick(View v) { DisconnectingDialogFragment.show(getChildFragmentManager()); - mActivity.getService().getManagement().stopVPN(); + try { + mActivity.getService().stopVPN(false); + } catch (RemoteException | NullPointerException e) { + VpnStatus.logException(e); + } } private void updateMapLocation() { @@ -216,6 +232,7 @@ public void run() { mCurrentPosMarker.setVisible(false); } }); + IPService.get().status(mIPCallback); } @@ -254,13 +271,17 @@ public void onMapReady(GoogleMap googleMap) { private Callback mIPCallback = new Callback() { @Override public void success(final IPService.Data data, Response response) { - if(mActivity == null) return; + Timber.d("Updated IP data"); + if(mActivity == null) { + Timber.d("Returning because mActivity is null"); + return; + } if(response != null && response.getStatus() == 200) { mShowsConnected = data.connected; mDetectedCountry = data.country; - if(!mShowsConnected && mCurrentVPNState.equals(VpnStatus.ConnectionStatus.LEVEL_CONNECTED)) { + if(!mShowsConnected && mCurrentVPNState.equals(ConnectionStatus.LEVEL_CONNECTED)) { updateIPData(); return; } @@ -316,8 +337,13 @@ public void run() { @Override public void failure(RetrofitError error) { + Timber.e("Error getting IP data %s %s", error.getMessage(), error.getCause()); if (error.getResponse() != null && error.getResponse().getStatus() == 401) { mActivity.startLoginActivity(); + } else if (error.getCause() instanceof UnknownHostException) { + if (!error.getMessage().contains("check")) { + IPService.getFallback().status(mIPCallback); + } } else if (error.getCause() instanceof SocketTimeoutException){ updateIPData(); } @@ -345,7 +371,11 @@ public void success(ServersResponse serversResponse, Response response) { } mMarkers.put(marker, spinnerList.size()); - spinnerList.add(server.country); + if (server.countryCode != null && server.location != null) { + spinnerList.add(String.format("%s, %s", server.location, server.country)); + } else { + spinnerList.add(server.country); + } } final ArrayAdapter arrayAdapter = new ArrayAdapter<>(mActivity, R.layout.simple_spinner_item, spinnerList); @@ -365,7 +395,6 @@ public void run() { @Override public void failure(RetrofitError error) { - Timber.e(error, error.getMessage()); Toast.makeText(mActivity, R.string.unknown_error, Toast.LENGTH_SHORT).show(); if(error.getResponse() != null && error.getResponse().getStatus() == 401) @@ -385,19 +414,19 @@ public boolean onMarkerClick(Marker marker) { }; @Override - public void newLog(VpnStatus.LogItem logItem) { + public void newLog(LogItem logItem) { Timber.i("%s: %s", logItem.getLogLevel(), logItem.getString(mActivity)); } @Override - public void updateState(final String state, String logmessage, int localizedResId, final VpnStatus.ConnectionStatus level) { + public void updateState(String state, String logmessage, int localizedResId, final ConnectionStatus level, Intent Intent) { Timber.d("State: %s", state); if(mCurrentVPNState.equals(level)) return; mCurrentVPNState = level; - if(level.equals(VpnStatus.ConnectionStatus.LEVEL_CONNECTED) || level.equals(VpnStatus.ConnectionStatus.LEVEL_NOTCONNECTED)) { + if(level.equals(ConnectionStatus.LEVEL_CONNECTED) || level.equals(ConnectionStatus.LEVEL_NOTCONNECTED)) { new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { @Override public void run() { @@ -411,20 +440,20 @@ public void run() { ThreadUtils.runOnUiThread(new Runnable() { @Override public void run() { - if(level.equals(VpnStatus.ConnectionStatus.LEVEL_CONNECTED)) { + if(level.equals(ConnectionStatus.LEVEL_CONNECTED)) { mConnectButton.setVisibility(View.GONE); mConnectedCard.setVisibility(View.VISIBLE); mConnectCard.setVisibility(View.GONE); mDisconnectButton.setVisibility(View.VISIBLE); - } else if(level.equals(VpnStatus.ConnectionStatus.LEVEL_NOTCONNECTED)) { + } else if(level.equals(ConnectionStatus.LEVEL_NOTCONNECTED)) { mConnectedCard.setVisibility(View.GONE); mDisconnectButton.setVisibility(View.GONE); mConnectButton.setVisibility(View.VISIBLE); mConnectCard.setVisibility(View.VISIBLE); - if(VpnStatus.getlogbuffer().length > 10) { - for(int i = VpnStatus.getlogbuffer().length - 10; i < VpnStatus.getlogbuffer().length; i++) { - VpnStatus.LogItem log = VpnStatus.getlogbuffer()[i]; + if(getlogbuffer().length > 10) { + for(int i = getlogbuffer().length - 10; i < getlogbuffer().length; i++) { + LogItem log = getlogbuffer()[i]; String logString = log.getString(mActivity); if (logString.contains("Cannot open TUN")) { Timber.d("NEEDS REBOOT"); @@ -439,4 +468,9 @@ public void run() { } }); } + + @Override + public void setConnectedVPN(String uuid) { + + } } diff --git a/app/src/main/java/ht/vpn/android/fragment/dialog/ConnectingDialogFragment.java b/app/src/main/java/ht/vpn/android/fragment/dialog/ConnectingDialogFragment.java index 2fc924f..5aa1278 100644 --- a/app/src/main/java/ht/vpn/android/fragment/dialog/ConnectingDialogFragment.java +++ b/app/src/main/java/ht/vpn/android/fragment/dialog/ConnectingDialogFragment.java @@ -4,12 +4,15 @@ import android.app.Activity; import android.app.Dialog; import android.app.ProgressDialog; +import android.content.Intent; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.DialogFragment; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + +import de.blinkt.openvpn.core.ConnectionStatus; import de.blinkt.openvpn.core.VpnStatus; import ht.vpn.android.R; import ht.vpn.android.utils.ThreadUtils; @@ -73,7 +76,7 @@ public void onDetach() { } @Override - public void updateState(final String state, String logmessage, final int localizedResId, VpnStatus.ConnectionStatus level) { + public void updateState(final String state, String logmessage, final int localizedResId, ConnectionStatus level, Intent intent) { ThreadUtils.runOnUiThread(new Runnable() { @Override public void run() { @@ -84,4 +87,8 @@ public void run() { }); } + @Override + public void setConnectedVPN(String uuid) { + } + } \ No newline at end of file diff --git a/app/src/main/java/ht/vpn/android/fragment/dialog/DisconnectingDialogFragment.java b/app/src/main/java/ht/vpn/android/fragment/dialog/DisconnectingDialogFragment.java index 45ed80f..63ef6ce 100644 --- a/app/src/main/java/ht/vpn/android/fragment/dialog/DisconnectingDialogFragment.java +++ b/app/src/main/java/ht/vpn/android/fragment/dialog/DisconnectingDialogFragment.java @@ -4,10 +4,11 @@ import android.app.Dialog; import android.app.ProgressDialog; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.DialogFragment; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; import ht.vpn.android.R; diff --git a/app/src/main/java/ht/vpn/android/fragment/dialog/LoadingDialogFragment.java b/app/src/main/java/ht/vpn/android/fragment/dialog/LoadingDialogFragment.java index 630e4e1..a9ee9d4 100644 --- a/app/src/main/java/ht/vpn/android/fragment/dialog/LoadingDialogFragment.java +++ b/app/src/main/java/ht/vpn/android/fragment/dialog/LoadingDialogFragment.java @@ -4,10 +4,11 @@ import android.app.Dialog; import android.app.ProgressDialog; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.DialogFragment; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; import ht.vpn.android.R; diff --git a/app/src/main/java/ht/vpn/android/fragment/dialog/RebootDialogFragment.java b/app/src/main/java/ht/vpn/android/fragment/dialog/RebootDialogFragment.java index f846bb6..4133205 100644 --- a/app/src/main/java/ht/vpn/android/fragment/dialog/RebootDialogFragment.java +++ b/app/src/main/java/ht/vpn/android/fragment/dialog/RebootDialogFragment.java @@ -4,10 +4,11 @@ import android.app.AlertDialog; import android.app.Dialog; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.DialogFragment; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; import ht.vpn.android.R; diff --git a/app/src/main/java/ht/vpn/android/fragment/dialog/StringArraySelectorDialogFragment.java b/app/src/main/java/ht/vpn/android/fragment/dialog/StringArraySelectorDialogFragment.java index dde6718..3835f29 100644 --- a/app/src/main/java/ht/vpn/android/fragment/dialog/StringArraySelectorDialogFragment.java +++ b/app/src/main/java/ht/vpn/android/fragment/dialog/StringArraySelectorDialogFragment.java @@ -5,8 +5,9 @@ import android.app.Dialog; import android.content.DialogInterface; import android.os.Bundle; -import android.support.v4.app.DialogFragment; -import android.support.v4.app.FragmentManager; + +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentManager; import java.util.List; diff --git a/app/src/main/java/ht/vpn/android/network/IPService.java b/app/src/main/java/ht/vpn/android/network/IPService.java index d1333c0..72dd234 100644 --- a/app/src/main/java/ht/vpn/android/network/IPService.java +++ b/app/src/main/java/ht/vpn/android/network/IPService.java @@ -5,16 +5,21 @@ import retrofit.http.GET; public class IPService { - private static final String API_URL = "http://myip.ht/"; + private static final String API_URL = "https://myip.ht/"; + private static final String API_FALLBACK_URL = "https://check.myip.ht:8080/"; private Client mClient; - private IPService() { - RestAdapter restAdapter = new RestAdapter.Builder().setEndpoint(API_URL).setRequestInterceptor(new UserAgentInterceptor()).build(); + private IPService(String url) { + RestAdapter restAdapter = new RestAdapter.Builder().setEndpoint(url).setRequestInterceptor(new UserAgentInterceptor()).build(); mClient = restAdapter.create(Client.class); } public static IPService.Client get() { - return new IPService().mClient; + return new IPService(API_URL).mClient; + } + + public static IPService.Client getFallback() { + return new IPService(API_FALLBACK_URL).mClient; } public interface Client { diff --git a/app/src/main/java/ht/vpn/android/network/VPNService.java b/app/src/main/java/ht/vpn/android/network/VPNService.java index e22f5b5..95f7537 100644 --- a/app/src/main/java/ht/vpn/android/network/VPNService.java +++ b/app/src/main/java/ht/vpn/android/network/VPNService.java @@ -3,14 +3,13 @@ import android.util.Base64; import ht.vpn.android.network.responses.ServersResponse; -import ht.vpn.android.network.responses.SmartDNSResponse; import retrofit.Callback; import retrofit.RequestInterceptor; import retrofit.RestAdapter; import retrofit.http.GET; public class VPNService { - private static final String API_URL = "https://api.vpn.ht/"; + private static final String API_URL = "https://api.digital.ht/"; private Client mClient; private VPNService(String username, String password) { @@ -43,7 +42,5 @@ public static VPNService.Client get(String username, String password) { public interface Client { @GET("/servers") void servers(Callback callback); - @GET("/smartdns") - void smartdns(Callback callback); } } diff --git a/app/src/main/java/ht/vpn/android/network/responses/SmartDNSResponse.java b/app/src/main/java/ht/vpn/android/network/responses/SmartDNSResponse.java deleted file mode 100644 index 8479801..0000000 --- a/app/src/main/java/ht/vpn/android/network/responses/SmartDNSResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package ht.vpn.android.network.responses; - -public class SmartDNSResponse extends BaseResponse { - - public String[] dns; - -} diff --git a/app/src/main/jniLibs/arm64-v8a/libjbcrypto.so b/app/src/main/jniLibs/arm64-v8a/libjbcrypto.so deleted file mode 100755 index 751bb8e..0000000 Binary files a/app/src/main/jniLibs/arm64-v8a/libjbcrypto.so and /dev/null differ diff --git a/app/src/main/jniLibs/arm64-v8a/libopenvpn.so b/app/src/main/jniLibs/arm64-v8a/libopenvpn.so old mode 100755 new mode 100644 index 22b33a0..347be9f Binary files a/app/src/main/jniLibs/arm64-v8a/libopenvpn.so and b/app/src/main/jniLibs/arm64-v8a/libopenvpn.so differ diff --git a/app/src/main/jniLibs/arm64-v8a/libopvpnutil.so b/app/src/main/jniLibs/arm64-v8a/libopvpnutil.so old mode 100755 new mode 100644 index a61ec24..7a4c856 Binary files a/app/src/main/jniLibs/arm64-v8a/libopvpnutil.so and b/app/src/main/jniLibs/arm64-v8a/libopvpnutil.so differ diff --git a/app/src/main/jniLibs/arm64-v8a/libovpn3.so b/app/src/main/jniLibs/arm64-v8a/libovpn3.so new file mode 100644 index 0000000..121c9a3 Binary files /dev/null and b/app/src/main/jniLibs/arm64-v8a/libovpn3.so differ diff --git a/app/src/main/jniLibs/arm64-v8a/libovpnexec.so b/app/src/main/jniLibs/arm64-v8a/libovpnexec.so new file mode 100644 index 0000000..fe72023 Binary files /dev/null and b/app/src/main/jniLibs/arm64-v8a/libovpnexec.so differ diff --git a/app/src/main/jniLibs/armeabi-v7a/libjbcrypto.so b/app/src/main/jniLibs/armeabi-v7a/libjbcrypto.so deleted file mode 100755 index 41bcec4..0000000 Binary files a/app/src/main/jniLibs/armeabi-v7a/libjbcrypto.so and /dev/null differ diff --git a/app/src/main/jniLibs/armeabi-v7a/libopenvpn.so b/app/src/main/jniLibs/armeabi-v7a/libopenvpn.so old mode 100755 new mode 100644 index e1604c5..c1a6015 Binary files a/app/src/main/jniLibs/armeabi-v7a/libopenvpn.so and b/app/src/main/jniLibs/armeabi-v7a/libopenvpn.so differ diff --git a/app/src/main/jniLibs/armeabi-v7a/libopvpnutil.so b/app/src/main/jniLibs/armeabi-v7a/libopvpnutil.so old mode 100755 new mode 100644 index c4f0a9d..2680aac Binary files a/app/src/main/jniLibs/armeabi-v7a/libopvpnutil.so and b/app/src/main/jniLibs/armeabi-v7a/libopvpnutil.so differ diff --git a/app/src/main/jniLibs/armeabi-v7a/libovpn3.so b/app/src/main/jniLibs/armeabi-v7a/libovpn3.so new file mode 100644 index 0000000..d7f8d9b Binary files /dev/null and b/app/src/main/jniLibs/armeabi-v7a/libovpn3.so differ diff --git a/app/src/main/jniLibs/armeabi-v7a/libovpnexec.so b/app/src/main/jniLibs/armeabi-v7a/libovpnexec.so new file mode 100644 index 0000000..67a881c Binary files /dev/null and b/app/src/main/jniLibs/armeabi-v7a/libovpnexec.so differ diff --git a/app/src/main/jniLibs/armeabi/libjbcrypto.so b/app/src/main/jniLibs/armeabi/libjbcrypto.so deleted file mode 100755 index 063aa5f..0000000 Binary files a/app/src/main/jniLibs/armeabi/libjbcrypto.so and /dev/null differ diff --git a/app/src/main/jniLibs/armeabi/libopenvpn.so b/app/src/main/jniLibs/armeabi/libopenvpn.so deleted file mode 100755 index 1f6bc9a..0000000 Binary files a/app/src/main/jniLibs/armeabi/libopenvpn.so and /dev/null differ diff --git a/app/src/main/jniLibs/armeabi/libopvpnutil.so b/app/src/main/jniLibs/armeabi/libopvpnutil.so deleted file mode 100755 index 42b138f..0000000 Binary files a/app/src/main/jniLibs/armeabi/libopvpnutil.so and /dev/null differ diff --git a/app/src/main/jniLibs/mips/libjbcrypto.so b/app/src/main/jniLibs/mips/libjbcrypto.so deleted file mode 100755 index 39baace..0000000 Binary files a/app/src/main/jniLibs/mips/libjbcrypto.so and /dev/null differ diff --git a/app/src/main/jniLibs/mips/libopenvpn.so b/app/src/main/jniLibs/mips/libopenvpn.so deleted file mode 100755 index 7323e5a..0000000 Binary files a/app/src/main/jniLibs/mips/libopenvpn.so and /dev/null differ diff --git a/app/src/main/jniLibs/mips/libopvpnutil.so b/app/src/main/jniLibs/mips/libopvpnutil.so deleted file mode 100755 index 816e262..0000000 Binary files a/app/src/main/jniLibs/mips/libopvpnutil.so and /dev/null differ diff --git a/app/src/main/jniLibs/x86/libjbcrypto.so b/app/src/main/jniLibs/x86/libjbcrypto.so deleted file mode 100755 index f7c2c97..0000000 Binary files a/app/src/main/jniLibs/x86/libjbcrypto.so and /dev/null differ diff --git a/app/src/main/jniLibs/x86/libopenvpn.so b/app/src/main/jniLibs/x86/libopenvpn.so old mode 100755 new mode 100644 index 61855dc..31794ba Binary files a/app/src/main/jniLibs/x86/libopenvpn.so and b/app/src/main/jniLibs/x86/libopenvpn.so differ diff --git a/app/src/main/jniLibs/x86/libopvpnutil.so b/app/src/main/jniLibs/x86/libopvpnutil.so old mode 100755 new mode 100644 index 118ad9c..4732e31 Binary files a/app/src/main/jniLibs/x86/libopvpnutil.so and b/app/src/main/jniLibs/x86/libopvpnutil.so differ diff --git a/app/src/main/jniLibs/x86/libovpn3.so b/app/src/main/jniLibs/x86/libovpn3.so new file mode 100644 index 0000000..6a6e628 Binary files /dev/null and b/app/src/main/jniLibs/x86/libovpn3.so differ diff --git a/app/src/main/jniLibs/x86/libovpnexec.so b/app/src/main/jniLibs/x86/libovpnexec.so new file mode 100644 index 0000000..f2a5eca Binary files /dev/null and b/app/src/main/jniLibs/x86/libovpnexec.so differ diff --git a/app/src/main/jniLibs/x86_64/libjbcrypto.so b/app/src/main/jniLibs/x86_64/libjbcrypto.so deleted file mode 100755 index f4e7a38..0000000 Binary files a/app/src/main/jniLibs/x86_64/libjbcrypto.so and /dev/null differ diff --git a/app/src/main/jniLibs/x86_64/libopenvpn.so b/app/src/main/jniLibs/x86_64/libopenvpn.so old mode 100755 new mode 100644 index 56cddf8..c413bb5 Binary files a/app/src/main/jniLibs/x86_64/libopenvpn.so and b/app/src/main/jniLibs/x86_64/libopenvpn.so differ diff --git a/app/src/main/jniLibs/x86_64/libopvpnutil.so b/app/src/main/jniLibs/x86_64/libopvpnutil.so old mode 100755 new mode 100644 index 5bf96a7..1c33307 Binary files a/app/src/main/jniLibs/x86_64/libopvpnutil.so and b/app/src/main/jniLibs/x86_64/libopvpnutil.so differ diff --git a/app/src/main/jniLibs/x86_64/libovpn3.so b/app/src/main/jniLibs/x86_64/libovpn3.so new file mode 100644 index 0000000..9d2c94f Binary files /dev/null and b/app/src/main/jniLibs/x86_64/libovpn3.so differ diff --git a/app/src/main/jniLibs/x86_64/libovpnexec.so b/app/src/main/jniLibs/x86_64/libovpnexec.so new file mode 100644 index 0000000..e261dd8 Binary files /dev/null and b/app/src/main/jniLibs/x86_64/libovpnexec.so differ diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index ae4f93c..63e09d1 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -1,5 +1,6 @@ @@ -17,7 +18,7 @@ android:contentDescription="@string/app_name" android:scaleType="fitCenter" /> - - - + - - - + - + android:textColor="@android:color/white" + app:backgroundTint="@color/primary" /> - + app:backgroundTint="@color/background" /> - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_vpn.xml b/app/src/main/res/layout/fragment_vpn.xml index cf961ac..3821f5c 100644 --- a/app/src/main/res/layout/fragment_vpn.xml +++ b/app/src/main/res/layout/fragment_vpn.xml @@ -12,7 +12,7 @@ android:layout_marginTop="@dimen/activity_vertical_margin" android:orientation="vertical"> - - - + - - + - - + - - + - + android:textColor="@android:color/white" + android:backgroundTint="@color/green" /> - + android:textColor="@android:color/white" + android:backgroundTint="@color/red" /> \ No newline at end of file diff --git a/app/src/main/res/layout/list_icon_twoline_item.xml b/app/src/main/res/layout/list_icon_twoline_item.xml index fd362fb..acedf33 100644 --- a/app/src/main/res/layout/list_icon_twoline_item.xml +++ b/app/src/main/res/layout/list_icon_twoline_item.xml @@ -28,7 +28,7 @@ android:paddingTop="@dimen/iconlist_text_twolines_padding" android:paddingBottom="@dimen/iconlist_text_twolines_padding"> - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/userpass.xml b/app/src/main/res/layout/userpass.xml index 626bdb8..fdca464 100644 --- a/app/src/main/res/layout/userpass.xml +++ b/app/src/main/res/layout/userpass.xml @@ -6,35 +6,35 @@ android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin"> - - - + - - - + diff --git a/app/src/main/res/values-fr/.DS_Store b/app/src/main/res/values-fr/.DS_Store deleted file mode 100644 index 5008ddf..0000000 Binary files a/app/src/main/res/values-fr/.DS_Store and /dev/null differ diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 8e530fc..1e9d791 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -9,7 +9,7 @@ Mot de passe Connexion Enregistrement - L'identifiant ou le mot de passe est erroné + L\'identifiant ou le mot de passe est erroné Préférences diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 07cb50a..2b2ba34 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,7 +1,7 @@ - - - - - - - - - - diff --git a/app/src/main/res/values/vpn_plurals.xml b/app/src/main/res/values/vpn_plurals.xml new file mode 100644 index 0000000..1c5a03b --- /dev/null +++ b/app/src/main/res/values/vpn_plurals.xml @@ -0,0 +1,19 @@ + + + + One month left + %d months left + + + One day left + %d days left + + + One hour left + %d hours left + + + One minute left + %d minutes left + + \ No newline at end of file diff --git a/app/src/main/res/values/vpn_strings.xml b/app/src/main/res/values/vpn_strings.xml old mode 100755 new mode 100644 index e9b6f82..5180b7f --- a/app/src/main/res/values/vpn_strings.xml +++ b/app/src/main/res/values/vpn_strings.xml @@ -1,5 +1,5 @@ @@ -17,7 +17,7 @@ PKCS12 File CA Certificate You must select a certificate - Source code and issue tracker available at http://code.google.com/p/ics-openvpn/ + Source code and issue tracker available at https://github.com/schwabe/ics-openvpn/ This program uses the following components; see the source code for full details on the licenses About Profiles @@ -41,13 +41,14 @@ Please enter a unique Profile Name Profile Name You must select a User certificate + You must select a CA certificate No error found Error in Configuration Error parsing the IPv4 address Error parsing the custom routes (leave empty to query on demand) OpenVPN Shortcut - Connect to VPN + Connecting to VPN… Profile specified in shortcut not found Random Host Prefix Adds 6 random chars in front of hostname @@ -80,7 +81,7 @@ Secondary DNS Server used if the normal DNS Server cannot be reached. Backup DNS Server Ignore pushed routes - Ignore routed pushed by the server. + Ignore routes pushed by the server. Redirects all Traffic over the VPN Use default Route Enter custom routes. Only enter destination in CIDR format. \"10.0.0.0/8 2002::/16\" would direct the networks 10.0.0.0/8 and 2002::/16 over the VPN. @@ -110,12 +111,12 @@ %1$s %2$s Send log file Send - VPN.ht log file + ICS OpenVPN log file Copied log entry to clip board Tap Mode Tap Mode is not possible with the non root VPN API. Therefore this application cannot provide tap support Again? Are you kidding? No, tap mode is really not supported and sending more mail asking if it will be supported will not help. - A third time? Actually, one could write a a tap emulator based on tun that would add layer2 information on send and strip layer2 information on receive. But this tap emulator would also have to implement ARP and possibly a DHCP client. I am not aware of anybody doing any work in this direction. Contact me if you want to start coding on this. + A third time? Actually, one could write a tap emulator based on tun that would add layer2 information on send and strip layer2 information on receive. But this tap emulator would also have to implement ARP and possibly a DHCP client. I am not aware of anybody doing any work in this direction. Contact me if you want to start coding on this. FAQ Copying log entries To copy a single log entry press and hold on the log entry. To copy/send the whole log use the Send Log option. Use the hardware menu button, if the button is not visible in the GUI. @@ -135,7 +136,7 @@ Refusing to open tun device without IP information Import Profile from ovpn file Import - Could not read Profile to import + Could not read profile to import Error reading config file add Profile Could not find file %1$s mentioned in the imported config file @@ -156,11 +157,11 @@ imported profile imported profile %d Broken Images - <p>Official HTC images are known to have a strange routing problem causing traffic not to flow through the tunnel (See also <a href="http://code.google.com/p/ics-openvpn/issues/detail?id=18">Issue 18</a> in the bug tracker.)</p><p>Older official SONY images from Xperia Arc S and Xperia Ray have been reported to be missing the VPNService completely from the image. (See also <a href="http://code.google.com/p/ics-openvpn/issues/detail?id=29">Issue 29</a> in the bug tracker.)</p><p>On custom build images the tun module might be missing or the rights of /dev/tun might be wrong. Some CM9 images need the "Fix ownership" option under "Device specific hacks" enabled.</p><p>Most importantly: If your device has a broken Android image, report it to your vendor. The more people who report an issue to the vendor, the more likely they are to fix it.</p> + <p>Official HTC images are known to have a strange routing problem causing traffic not to flow through the tunnel (See also <a href="https://github.com/schwabe/ics-openvpn/issues/18">Issue 18</a> in the bug tracker.)</p><p>Older official SONY images from Xperia Arc S and Xperia Ray have been reported to be missing the VPNService completely from the image. (See also <a href="https://github.com/schwabe/ics-openvpn/issues/29">Issue 29</a> in the bug tracker.)</p><p>On custom build images the tun module might be missing or the rights of /dev/tun might be wrong. Some CM9 images need the "Fix ownership" option under "Device specific hacks" enabled.</p><p>Most importantly: If your device has a broken Android image, report it to your vendor. The more people who report an issue to the vendor, the more likely they are to fix it.</p> PKCS12 File Encryption Key Private Key Password file icon - TLS Authentication + TLS Authentication/Encryption Generated Config Settings Tries to set the owner of /dev/tun to system. Some CM9 images need this to make the VPNService API work. Requires root. @@ -176,8 +177,9 @@ No CA Certificate returned while reading from Android keystore. Authentication will probably fail. Shows the log window on connect. The log window can always be accessed from the notification status. Show log window - Running on %1$s (%2$s) %3$s, Android API %4$d + %10$s %9$s running on %3$s %1$s (%2$s), Android %6$s (%7$s) API %4$d, ABI %5$s, (%8$s) Error signing with Android keystore key %1$s: %2$s + Error signing with external authenticator app (%3$s): %1$s: %2$s The VPN connection warning telling you that this app can intercept all traffic is imposed by the system to prevent abuse of the VPNService API.\nThe VPN connection notification (The key symbol) is also imposed by the Android system to signal an ongoing VPN connection. On some images this notification plays a sound.\nAndroid introduced these system dialogs for your own safety and made sure that they cannot be circumvented. (On some images this unfortunately includes a notification sound) Connection warning and notification sound English translation by Arne Schwabe<arne@rfc2549.org> @@ -186,22 +188,21 @@ Routing Obscure OpenVPN Settings. Normally not needed. Advanced - VPN.ht Config + ICS Openvpn Config No DNS servers being used. Name resolution may not work. Consider setting custom DNS Servers. Please also note that Android will keep using your proxy settings specified for your mobile/Wi-Fi connection when no DNS servers are set. Could not add DNS Server \"%1$s\", rejected by the system: %2$s Could not configure IP Address \"%1$s\", rejected by the system: %2$s - <p>Get a working config (tested on your computer or download from your provider/organisation)</p><p>If it is a single file with no extra pem/pks12 files you can email the file yourself and open the attachment. If you have multiple files put them on your sd card.</p><p>Click on the email attachment/Use the folder icon in the vpn list to import the config file</p><p>If there are errors about missing files put the missing files on your sd card.</p><p>Click on the save symbol to add the imported VPN to your VPN list</p><p>Connect the VPN by clicking on the name of the VPN</p><p>If there are error or warnings in the log try to understand the warnings/error and try to fix them</p> + <p>Get a working config (tested on your computer or download from your provider/organisation)</p><p>If it is a single file with no extra pem/pkcs12 files you can email the file yourself and open the attachment. If you have multiple files put them on your sd card.</p><p>Click on the email attachment/Use the folder icon in the vpn list to import the config file</p><p>If there are errors about missing files put the missing files on your sd card.</p><p>Click on the save symbol to add the imported VPN to your VPN list</p><p>Connect the VPN by clicking on the name of the VPN</p><p>If there are error or warnings in the log try to understand the warnings/error and try to fix them</p> Quick Start Try to load the tun.ko kernel module before trying to connect. Needs rooted devices. Load tun module Import PKCS12 from configuration into Android Keystore Error getting proxy settings: %s - Using proxy %1$s %2$d + Using proxy %1$s %2$s Use system proxy Use the system wide configuration for HTTP/HTTPS proxies to connect. - You can <a href=\"https://www.paypal.com/cgi-bin/webscr?hosted_button_id=R2M6ZP9AF25LS&amp;cmd=_s-xclick\">donate with PayPal</a> - OpenVPN will reconnect a VPN if it was active on system reboot/shutdown. Please read the Connection warning FAQ before using this option. - Reconnect on reboot + OpenVPN will connect the specified VPN if it was active on system boot. Please read the connection warning FAQ before using this option on Android < 5.0. + Connect on boot Ignore Restart Configuration changes are applied after restarting the VPN. (Re)start the VPN now? @@ -220,17 +221,17 @@ OpenVPN Log Import OpenVPN configuration Battery consumption - In my personal tests the main reason for high battery consumption of OpenVPN are the keepalive packets. Most OpenVPN servers have a configuration directive like \'keepalive 10 60\' which causes the client and server to exchange keepalive packets every ten seconds. <p> While these packets are small and do not use much traffic, they keep the mobile radio network busy and increase the energy consumption. (See also <a href="http://developer.android.com/training/efficient-downloads/efficient-network-access.html#RadioStateMachine">The Radio State Machine | Android Developers</a>) <p> This keepalive setting cannot be changed on the client. Only the system administrator of the OpenVPN can change the setting. <p> Unfortunately using a keepalive larger than 60 seconds with UDP can cause some NAT gateways to drop the connection due to an inactivity timeout. Using TCP with a long keep alive timeout works, but tunneling TCP over TCP performs extremely poorly on connections with high packet loss. (See <a href="http://sites.inka.de/bigred/devel/tcp-tcp.html">Why TCP Over TCP Is A Bad Idea</a>) - The Android Tethering feature (over WiFi, USB or Bluetooth) and the VPNService API (used by this program) do not work together. For more details see the <a href=\"http://code.google.com/p/ics-openvpn/issues/detail?id=34\">issue #34</a> + In my personal tests the main reason for high battery consumption of OpenVPN are the keepalive packets. Most OpenVPN servers have a configuration directive like \'keepalive 10 60\' which causes the client and server to exchange keepalive packets every ten seconds. <p> While these packets are small and do not use much traffic, they keep the mobile radio network busy and increase the energy consumption. (See also <a href="https://developer.android.com/training/efficient-downloads/efficient-network-access.html#RadioStateMachine">The Radio State Machine | Android Developers</a>) <p> This keepalive setting cannot be changed on the client. Only the system administrator of the OpenVPN can change the setting. <p> Unfortunately using a keepalive larger than 60 seconds with UDP can cause some NAT gateways to drop the connection due to an inactivity timeout. Using TCP with a long keep alive timeout works, but tunneling TCP over TCP performs extremely poorly on connections with high packet loss. (See <a href="http://sites.inka.de/bigred/devel/tcp-tcp.html">Why TCP Over TCP Is A Bad Idea</a>) + The Android Tethering feature (over WiFi, USB or Bluetooth) and the VPNService API (used by this program) do not work together. For more details see the <a href=\"https://github.com/schwabe/ics-openvpn/issues/34\">issue #34</a> VPN and Tethering Connection retries Reconnection settings Number of seconds to wait between connection attempts. Seconds between connections - VPN.ht crashed unexpectedly. Please consider using the send Minidump option in the main menu + OpenVPN crashed unexpectedly. Please consider using the send Minidump option in the main menu Send Minidump to developer Sends debugging information about last crash to developer - VPN.ht - %s + OpenVPN - %s %1$s - %2$s %1$s - %3$s, %2$s Connecting @@ -248,7 +249,8 @@ Connecting (TCP) Authentication failed Waiting for usable network - ↓%2$s/s %1$s - ↑%4$s/s %3$s + Waiting for Orbot to start + ↓%2$s %1$s - ↑%4$s %3$s Not connected Connecting to VPN %s Connecting to VPN %s @@ -256,7 +258,6 @@ Encryption cipher Packet authentication Enter packet authentication method - Running on %1$s (%2$s) %3$s, Android API %4$d, version %5$s, %6$s built by %s debug build official build @@ -269,18 +270,18 @@ RDN (common name) RDN prefix tls-remote (DEPRECATED) - You can help translating by visiting http://crowdin.net/project/ics-openvpn/invite + You can help translating by visiting https://crowdin.net/project/ics-openvpn/invite %1$s attempts to control %2$s By proceeding, you are giving the application permission to completely control OpenVPN for Android and to intercept all network traffic.Do NOT accept unless you trust the application. Otherwise, you run the risk of having your data compromised by malicious software." I trust this application. No app allowed to use external API Allowed apps: %s Clear list of allowed external apps?\nCurrent list of allowed apps:\n\n%s - "Pause VPN when screen is off and less than 64 kB transferred data in 60s. When the \"Persistent Tun\" option is enabled pausing the VPN will leave your device with NO network connectivity. Without the \"Persistent Tun\" option the device will have no VPN connection/protection. + Pause VPN when screen is off and less than 64 kB transferred data in 60s. When the \"Persistent Tun\" option is enabled pausing the VPN will leave your device with NO network connectivity. Without the \"Persistent Tun\" option the device will have no VPN connection/protection. Pause VPN connection after screen off Pausing connection in screen off state: less than %1$s in %2$ss Warning: Persistent tun not enabled for this VPN. Traffic will use the normal Internet connection when the screen is off. - Save credentials + Save Password Pause VPN Resume VPN VPN pause requested by user @@ -292,7 +293,7 @@ Allow changes to VPN Profiles Hardware Keystore: Icon of app trying to use OpenVPN for Android - "Starting with Android 4.3 the VPN confirmation is guarded against \"overlaying apps\". This results in the dialog not reacting to touch input. If you have an app that uses overlays it may cause this behaviour. If you find an offending app contact the author of the app. This problem affect all VPN applications on Android 4.3 and later. See also <a href=\"http://code.google.com/p/ics-openvpn/issues/detail?id=185\">Issue 185<a> for additional details" + "Starting with Android 4.3 the VPN confirmation is guarded against \"overlaying apps\". This results in the dialog not reacting to touch input. If you have an app that uses overlays it may cause this behaviour. If you find an offending app contact the author of the app. This problem affect all VPN applications on Android 4.3 and later. See also <a href=\"https://github.com/schwabe/ics-openvpn/issues/185\">Issue 185<a> for additional details" Vpn Confirmation Dialog Alternatively you can send me a donation with the Play Store: Thanks for donating %s! @@ -309,17 +310,18 @@ View options Unhandled exception: %1$s\n\n%2$s %3$s: %1$s\n\n%2$s - If you have rooted your Android device you can install the <a href=\"http://xposed.info/\">Xposed framework</a> and a the <a href=\"http://repo.xposed.info/module/de.blinkt.vpndialogxposed\">VPN Dialog confirm module</a> at your own risk" + If you have rooted your Android device you can install the <a href=\"http://xposed.info/\">Xposed framework</a> and the <a href=\"http://repo.xposed.info/module/de.blinkt.vpndialogxposed\">VPN Dialog confirm module</a> at your own risk" Full licenses - Networks directly connected to the local interfaces will not be routed over the VPN. Deselecting this option will redirect all traffic indented for local networks to the VPN. + Networks directly connected to the local interfaces will not be routed over the VPN. Deselecting this option will redirect all traffic intented for local networks to the VPN. Bypass VPN for local networks Username/Password file [Imported from: %s] Some files could not be found. Please select the files to import the profile: - To use this app you need a VPN provider/VPN gateway supporting OpenVPN (often provided by your employer). Check out http://community.openvpn.net/ for more information on OpenVPN and how to setup your own OpenVPN server. + To use this app you need a VPN provider/VPN gateway supporting OpenVPN (often provided by your employer). Check out https://community.openvpn.net/ for more information on OpenVPN and how to setup your own OpenVPN server. Import log: Vpn topology \"%3$s\" specified but ifconfig %1$s %2$s looks more like an IP address with a network mask. Assuming \"subnet\" topology. The MSS override value has to be a integer between 0 and 9000 + The MTU override value has to be a integer between 64 and 9000 Announce to TCP sessions running over the tunnel that they should limit their send packet sizes such that after OpenVPN has encapsulated them, the resulting UDP packet size that OpenVPN sends to its peer will not exceed this number of bytes. (default is 1450) Override MSS value of TCP payload Set MSS of TCP payload @@ -331,6 +333,7 @@ Package %s is no longer installed, removing it from app allow/disallow list VPN is used for all apps but exclude selected VPN is used for only for selected apps + Allow apps to bypass the VPN Remove remote server entry? Keep Delete @@ -346,13 +349,13 @@ Duplicate VPN profile Duplicating profile: %s Show log - Multiple OpenVPN clients for Android exist. The most common ones are OpenVPN for Android (this client), OpenVPN Connect and OpenVPN Settings.<p>The clients can be grouped into two groups: OpenVPN for Android and OpenVPN Connect use the official VPNService API (Android 4.0+) and require no root and OpenVPN Settings which uses root.<p>OpenVPN for Android is an open source client and developed by Arne Schwabe. It is targeted at more advanced users and offers many settings and the ability to import profiles from files and to configure/change profiles inside the app. The client is based on the community version of OpenVPN. It is based on the OpenVPN 2.x source code. This client can be seen as the semi officially client of the community. <p>OpenVPN Connect is non open source client that is developed by OpenVPN Technologies, Inc. The client is indented to be general use client and moree targeted at the average user and allows the import of OpenVPN profiles. This client is based on the OpenVPN C++ reimplementation of the OpenVPN protocol (This was required to allow OpenVPN Technologies, Inc to publish an iOS OpenVPN app). This client is the official client of the OpenVPN technologies <p> OpenVPN Settings is the oldest of the clients and also a UI for the open source OpenVPN. In contrast to OpenVPN for Android it requires root and does not use the VPNService API. It does not depend on Android 4.0+ + Multiple OpenVPN clients for Android exist. The most common ones are OpenVPN for Android (this client), OpenVPN Connect and OpenVPN Settings.<p>The clients can be grouped into two groups: OpenVPN for Android and OpenVPN Connect use the official VPNService API (Android 4.0+) and require no root and OpenVPN Settings which uses root.<p>OpenVPN for Android is an open source client and developed by Arne Schwabe. It is targeted at more advanced users and offers many settings and the ability to import profiles from files and to configure/change profiles inside the app. The client is based on the community version of OpenVPN. It is based on the OpenVPN 2.x source code. This client can be seen as the semi officially client of the community. <p>OpenVPN Connect is non open source client that is developed by OpenVPN Technologies, Inc. The client is indented to be general use client and more targeted at the average user and allows the import of OpenVPN profiles. This client is based on the OpenVPN C++ reimplementation of the OpenVPN protocol (This was required to allow OpenVPN Technologies, Inc to publish an iOS OpenVPN app). This client is the official client of the OpenVPN technologies <p> OpenVPN Settings is the oldest of the clients and also a UI for the open source OpenVPN. In contrast to OpenVPN for Android it requires root and does not use the VPNService API. It does not depend on Android 4.0+ Differences between the OpenVPN Android clients Ignoring multicast route: %s Android supports only CIDR routes to the VPN. Since non-CIDR routes are almost never used, OpenVPN for Android will use a /32 for routes that are not CIDR and issue a warning. Tethering works while the VPN is active. The tethered connection will NOT use the VPN. Early KitKat version set the wrong MSS value on TCP connections (#61948). Try to enable the mssfix option to workaround this bug. - Android will keep using your proxy settings specified for the mobile/Wi-Fi connection when no DNS servers are set. OpenVPN for Android will warn you about this in the log.

When a VPN sets a DNS server Android will not a proxy. There is no API to set a proxy for a VPN connection.

+ Android will keep using your proxy settings specified for the mobile/Wi-Fi connection when no DNS servers are set. OpenVPN for Android will warn you about this in the log.

When a VPN sets a DNS server Android will not use a proxy. There is no API to set a proxy for a VPN connection.

VPN apps may stop working when uninstalled and reinstalled again. For details see #80074 The configured client IP and the IPs in its network mask are not routed to the VPN. OpenVPN works around this bug by explicitly adding a route that corrosponds to the client IP and its netmask Opening a tun device while another tun device is active, which is used for persist-tun support, crashes the VPNServices on the device. A reboot is required to make VPN work again. OpenVPN for Android tries to avoid reopening the tun device and if really needed first closes the current TUN before opening the new TUN device to avoid to crash. This may lead to a short window where packets are sent over the non-VPN connection. Even with this workaround the VPNServices sometimes crashes and requires a reboot of the device. @@ -375,9 +378,121 @@ Persist tun mode %s and later Connections fails with SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure - Newer OpenVPN for Android versions (0.6.29/March 2015) use a more secure default for the allowed cipher suites (tls-cipher \"DEFAULT:!EXP:!PSK:!SRP:!kRSA\"). Unfortunately, omitting the less secure cipher suites and export cipher suites, especially the omission of cipher suites that do not support Perfect Forward Secrecy (Diffie-Hellman) causes some problems. This usually caused by an well-intentioned but poorly executed attempts to strengthen TLS security by setting tls-cipher on the server.\nTo solve this problem the problem, set the tls-cipher settings on the server to reasonable default like tls-cipher \"DEFAULT:!EXP:!PSK:!SRP:!kRSA\". To work around the problem on the client add the custom option tls-cipher DEFAULT on the Android client. + Newer OpenVPN for Android versions (0.6.29/March 2015) use a more secure default for the allowed cipher suites (tls-cipher \"DEFAULT:!EXP:!PSK:!SRP:!kRSA\"). Unfortunately, omitting the less secure cipher suites and export cipher suites, especially the omission of cipher suites that do not support Perfect Forward Secrecy (Diffie-Hellman) causes some problems. This usually caused by an well-intentioned but poorly executed attempt to strengthen TLS security by setting tls-cipher on the server or some embedded OSes with stripped down SSL (e.g. MikroTik).\nTo solve this problem the problem, set the tls-cipher settings on the server to reasonable default like tls-cipher \"DEFAULT:!EXP:!PSK:!SRP:!kRSA\". To work around the problem on the client add the custom option tls-cipher DEFAULT on the Android client. This profile has been added from an external app (%s) and has been marked as not user editable. Certificate Revocation List - Service restarted + Restarting OpenVPN Service (App crashed probably crashed or killed for memory pressure) + Importing the config yielded an error, cannot save it + Search + (Last dump is %1$d:%2$dh old (%3$s)) + Clear log on new connection + Connect Timeout + No allowed app added. Adding ourselves (%s) to have at least one app in the allowed app list to not allow all apps + OpenVPN for Android can try to discover the missing file(s) on the sdcard automatically. Tap this message start the permission request. + Protocol + Enabled + Preferred native ABI precedence of this device (%1$s) and ABI reported by native libraries (%2$s) mismatch + VPN permission revoked by OS (e.g. other VPN program started), stopping VPN + Push Peer info + Send extra information to the server, e.g. SSL version and Android version + Need %1$s + Please enter the password for profile %1$s + Use inline data + Export configuration file + tls-auth file is missing + Missing user certificate or user certifcate key file + Missing CA certificate + Certifcate Revoke List (optional) + Reread (%d) log items from log cache file + Even though Samsung phones are among the most selling Android phones, Samsung\'s firmware are also among the most buggy Android firmwares. The bugs are not limited to the VPN operation on these devices but many of them can be workarounded. In the following some of these bugs are described.\n\nDNS does not work unless the DNS server in the VPN range.\n\nOn many Samsung 5.x devices the allowed/disallowed apps feature does not work.\nOn Samsung 6.x VPN is reported not to work unless the VPN app is exempted from Powersave features. + Samsung phones + No VPN selected. + Default VPN + VPN used in places where a default VPN needed. These are currently on boot, for Always-On and the Quick Settings Tile. + Currently selected VPN: \'%s\' + Reconnect + Toggle VPN + Connect to %s + Disconnect %s + Enter the maximum time between connection attempts. OpenVPN will slowly raise its waiting time after an unsuccessful connection attempt up to this value. Defaults to 300s. + Maximum time between connection attempts + Waiting %ss seconds between connection attempt + Networks more .. -> VPNS]]> + Connection to OpenVPN closed (%s) + Change sorting + Sort + Profiles sorted by last recently used + Profiles sorted by name + Config uses option tls-remote that was deprecated in 2.3 and finally removed in 2.4 + Behaviour on AUTH_FAILED + Graph + Use logarithmic scale + Not enough data + Average per hour + Average per minute + Last 5 minutes + In + Out + %.0f bit/s + %.1f kbit/s + %.1f Mbit/s + %.1f Gbit/s + <p>Starting with OpenSSL version 1.1, OpenSSL rejects weak signatures in certificates like + MD5.</p><p><b>MD5 signatures are completely insecure and should not be used anymore.</b> MD5 + collisions can be created in <a + href="https://natmchugh.blogspot.de/2015/02/create-your-own-md5-collisions.html">few hours at a minimal cost.</a>. + You should update the VPN certificates as soon as possible.</p><p>Unfortunately, older easy-rsa + distributions included the config option "default_md md5". If you are using an old easy-rsa version, update to + the <a href="https://github.com/OpenVPN/easy-rsa/releases">latest version</a>) or change md5 to sha256 and + regenerate your certificates.</p><p>If you really want to use old and broken certificates use the custom + configuration option tls-cipher "DEFAULT:@SECLEVEL=0" under advanced configuration or as additional line in your + imported configuration</p> + + %.0f B + %.1f kB + %.1f MB + %.1f GB + Connection statistics + Ongoing statistics of the established OpenVPN connection + Connection status change + Status changes of the OpenVPN connection (Connecting, authenticating,…) + Weak (MD5) hashes in certificate signature (SSL_CTX_use_certificate md too weak) + OpenSSL Speed Test + OpenSSL cipher names + OpenSSL Crypto Speed test + OpenSSL returned an error + Running test… + Test selected algorithms + An external app tries to control %s. The app requesting access cannot be determined. Allowing this app grants ALL apps access. + The OpenVPN 3 C++ implementation does not support static keys. Please change to OpenVPN 2.x under general settings. + Using PKCS12 files directly with OpenVPN 3 C++ implementation is not supported. Please import the pkcs12 files into the Android keystore or change to OpenVPN 2.x under general settings. + Proxy + None + Tor (Orbot) + OpenVPN 3 C++ implementation does not support connecting via Socks proxy + Orbot application cannot be found. Please install Orbot or use manual Socks v5 integration. + Remote API + OpenVPN for Android supports two remote APIs, a sophisticated API using AIDL (remoteEXample in the git repository) and a simple one using Intents. <p>Examples using adb shell and the intents. Replace profilname with your profile name<p><p> adb shell am start-activity -a android.intent.action.MAIN de.blinkt.openvpn/.api.DisconnectVPN<p> adb shell am start-activity -a android.intent.action.MAIN -e de.blinkt.openvpn.api.profileName Blinkt de.blinkt.openvpn/.api.ConnectVPN + Enable Proxy Authentication + Cannot use extra http-proxy-option statement and Orbot integration at the same time + Info from server: \'%s\' + User interaction required + OpenVPN connection requires a user input, e.g. two factor + authentification + + Open URL to continue VPN authentication + Answer challenge to continue VPN authentication + Authentication pending + External Authenticator + Configure + External Authneticator not configured + Block non VPN connection (\"Killswitch\") + It is often desired to block connections without VPN. Other apps often use markting terms like \"Killswitch\" or \"Seamless tunnel\" for this feature. OpenVPN and this app offer persist-tun, a feature to implement this functionality.<p>The problem with all these methods offered by apps is that they can only provide best effort and are no complete solutions. On boot, app crashing and other corner cases the app cannot ensure that this block of non VPN connection works. Thus giving the user a false sense of security.<p>The <b>only</b> reliable way to ensure non VPN connections are blocked is to use Android 8.0 or later and use the \"block connections without VPN\" setting that can be found under Settings > Network & Internet > Advanced/VPN > OpenVPN for Android > Enable Always ON VPN, Enable Block Connections without VPN + This option instructs Android to not allow protocols (IPv4/IPv6) if the VPN does not set any IPv4 or IPv6 addresses. + Block IPv6 (or IPv4) if not used by the VPN + Install new certificate + AS servername + Request autologin profile + Import Profile from Access Server
diff --git a/app/src/main/res/values/vpn_untranslatable.xml b/app/src/main/res/values/vpn_untranslatable.xml old mode 100755 new mode 100644 index 629514a..2823dd0 --- a/app/src/main/res/values/vpn_untranslatable.xml +++ b/app/src/main/res/values/vpn_untranslatable.xml @@ -1,23 +1,31 @@ - - - Copyright 2012–2014 Arne Schwabe <arne@rfc2549.org> + Copyright 2012–2018 Arne Schwabe <arne@rfc2549.org> + + App Logo design by Helen Beierling + <helbeierling@t-online.de> + + Copyright © 2002–2010 OpenVPN Technologies, Inc. <sales@openvpn.net>\n - "OpenVPN" is a trademark of OpenVPN Technologies, Inc.\n + "OpenVPN" is a trademark of OpenVPN Technologies, Inc.\n +
openvpn.uni-paderborn.de 1194 File Dialog based on work by Alexander Ponomarev - Copyright © 1996 – 2011 Markus Franz Xaver Johannes Oberhumer - This product includes software developed by the OpenSSL Project for use in the OpenSSL Toolkit\n + Copyright © 1996 – 2011 Markus Franz Xaver Johannes Oberhumer + + This product includes software developed by the OpenSSL + Project for use in the OpenSSL Toolkit\n Copyright © 1998-2008 The OpenSSL Project. All rights reserved.\n\n This product includes cryptographic software written by Eric Young (eay@cryptsoft.com)\n - Copyright © 1995-1998 Eric Young (eay@cryptsoft.com) All rights reserved. + Copyright © 1995-1998 Eric Young (eay@cryptsoft.com) All rights reserved. + OpenVPN File Dialog LZO @@ -25,12 +33,16 @@ Unknown state Allows another app to control OpenVPN Bouncy Castle Crypto APIs - Copyright © 2000–2012 The Legion Of The Bouncy Castle (http://www.bouncycastle.org) + Copyright © 2000–2012 The Legion Of The Bouncy Castle + (http://www.bouncycastle.org) + 0 1 + tls-crypt + tls-crypt-v2 1 @@ -40,11 +52,25 @@ -1 OpenVPN for Android crashed, crash reported - + Waiting for user permission to use VPN API Waiting for user VPN password VPN password input dialog cancelled VPN API permission dialog cancelled + aes-256-gcm bf-cbc sha1 + + + Unique UUID that identifies the profile (example: + 0E910C15–9A85-4DD9-AE0D-E6862392E638). Generate using uuidgen or similar tools + + UUID + Content of the OpenVPN configuration file. These files are usually have the extension .ovpn (sometimes also .conf) and are plain text multi line configuration files. If your MDM does not support multiline configuration entries, you can also use a base64 encoded string here. A text file can be converted to base64 using openssl base64 -A -in + Config + Name of the VPN profile + Name + List of VPN configurations + VPN configuration + Version of the managed configuration schema diff --git a/build.gradle b/build.gradle index 281e2b9..efc6246 100644 --- a/build.gradle +++ b/build.gradle @@ -3,9 +3,10 @@ buildscript { repositories { jcenter() + google() } dependencies { - classpath 'com.android.tools.build:gradle:2.1.2' + classpath 'com.android.tools.build:gradle:3.6.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -14,6 +15,7 @@ buildscript { allprojects { repositories { + google() jcenter() maven { url "https://oss.sonatype.org/content/repositories/snapshots/" diff --git a/gradle.properties b/gradle.properties index 1d3591c..b3d4a30 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,18 +1,16 @@ -# Project-wide Gradle settings. - -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. - -# For more details on how to configure your build environment visit +## For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html - +# # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -# Default value: -Xmx10248m -XX:MaxPermSize=256m +# Default value: -Xmx1024m -XX:MaxPermSize=256m # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 - +# # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true \ No newline at end of file +# org.gradle.parallel=true +#Sun Apr 05 14:25:01 CEST 2020 +org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" +android.useAndroidX=true +android.enableJetifier=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cad7943..36a43dd 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Jul 18 22:43:28 CEST 2016 +#Sun Apr 05 14:24:43 CEST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip