From 9f54528ddd285dc99797483c026fbc7345febb39 Mon Sep 17 00:00:00 2001 From: kwadronaut Date: Thu, 13 Oct 2016 09:30:51 +0200 Subject: Handle RequestPermisson sdk>23 closes #8536 --- app/src/main/AndroidManifest.xml | 8 +- .../main/java/de/blinkt/openvpn/core/LogItem.java | 377 +++++++++++++++++++++ .../de/blinkt/openvpn/core/TestLogFileHandler.java | 119 +++++++ 3 files changed, 500 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/de/blinkt/openvpn/core/LogItem.java create mode 100644 app/src/test/java/de/blinkt/openvpn/core/TestLogFileHandler.java (limited to 'app/src') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 73808ffb..7e0c9c0b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,8 +17,8 @@ + android:versionCode="128" + android:versionName="0.9.6Beta" > @@ -28,7 +28,7 @@ + android:targetSdkVersion="24"/> + android:theme="@android:style/Theme.Translucent.NoTitleBar" /> 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 00000000..6aefbb2e --- /dev/null +++ b/app/src/main/java/de/blinkt/openvpn/core/LogItem.java @@ -0,0 +1,377 @@ +/* + * 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 android.text.TextUtils; +import android.util.Log; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; +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 se.leap.bitmaskclient.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 { + 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/test/java/de/blinkt/openvpn/core/TestLogFileHandler.java b/app/src/test/java/de/blinkt/openvpn/core/TestLogFileHandler.java new file mode 100644 index 00000000..c35df598 --- /dev/null +++ b/app/src/test/java/de/blinkt/openvpn/core/TestLogFileHandler.java @@ -0,0 +1,119 @@ +package de.blinkt.openvpn.core; + +import android.annotation.SuppressLint; + +import junit.framework.Assert; + +import org.junit.Before; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.util.Arrays; + +public class TestLogFileHandler { + + byte[] testUnescaped = new byte[]{0x00, 0x55, -27, 0x00, 0x56, 0x10, -128, 0x55, 0x54}; + byte[] expectedEscaped = new byte[]{0x55, 0x00, 0x00, 0x00, 0x09, 0x00, 0x56, 0x00, -27, 0x00, 0x56, 0x01, 0x10, -128, 0x56, 0x00, 0x54}; + private TestingLogFileHandler lfh; + + + @Before + public void setup() { + lfh = new TestingLogFileHandler(); + } + + @Test + public void testWriteByteArray() throws IOException { + + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + + lfh.setLogFile(byteArrayOutputStream); + + lfh.writeEscapedBytes(testUnescaped); + + byte[] result = byteArrayOutputStream.toByteArray(); + Assert.assertTrue(Arrays.equals(expectedEscaped, result)); + } + + @Test + public void readByteArray() throws IOException { + + ByteArrayInputStream in = new ByteArrayInputStream(expectedEscaped); + + lfh.readCacheContents(in); + + Assert.assertTrue(Arrays.equals(testUnescaped, lfh.mRestoredByteArray)); + + } + + @Test + public void testMarschal() throws UnsupportedEncodingException { + LogItem li = new LogItem(VpnStatus.LogLevel.DEBUG, 72, "foobar"); + LogItem li2 = marschalAndBack(li); + testEquals(li, li2); + Assert.assertEquals(li, li2); + } + + @Test + public void testMarschalArgs() throws UnsupportedEncodingException { + LogItem li = new LogItem(VpnStatus.LogLevel.DEBUG, 72, 772, "sinnloser Text", 7723, 723.2f, 7.2); + LogItem li2 = marschalAndBack(li); + testEquals(li, li2); + Assert.assertEquals(li, li2); + } + + @Test + public void testMarschalString() throws UnsupportedEncodingException { + LogItem li = new LogItem(VpnStatus.LogLevel.DEBUG, "Nutzlose Nachricht"); + LogItem li2 = marschalAndBack(li); + testEquals(li, li2); + Assert.assertEquals(li, li2); + } + + + private void testEquals(LogItem li, LogItem li2) { + Assert.assertEquals(li.getLogLevel(), li2.getLogLevel()); + Assert.assertEquals(li.getLogtime(), li2.getLogtime()); + Assert.assertEquals(li.getVerbosityLevel(), li2.getVerbosityLevel()); + Assert.assertEquals(li.toString(), li2.toString()); + + } + + private LogItem marschalAndBack(LogItem li) throws UnsupportedEncodingException { + byte[] bytes = li.getMarschaledBytes(); + + return new LogItem(bytes, bytes.length); + } + + + @SuppressLint("HandlerLeak") + static class TestingLogFileHandler extends LogFileHandler { + + public byte[] mRestoredByteArray; + + public TestingLogFileHandler() { + super(null); + } + + public void setLogFile(OutputStream out) { + mLogFile = out; + } + + @Override + public void readCacheContents(InputStream in) throws IOException { + super.readCacheContents(in); + } + + @Override + protected void restoreLogItem(byte[] buf, int len) { + mRestoredByteArray = Arrays.copyOf(buf, len); + } + } + + +} \ No newline at end of file -- cgit v1.2.3