From 340bda7154194d64a719fb5c86a702a4e5773be0 Mon Sep 17 00:00:00 2001 From: Xia Wang Date: Fri, 28 Mar 2014 17:20:53 -0700 Subject: Add legacy VPN test framework and test cases - VpnProfileParser can parse VPN profiles from an xml file - CertInstallerHelper installs keys and certificates to keystore - VpnTests includes all test cases Change-Id: Ib5ce33e770ee4f82bea153f9b5c5cf3802f95b0c --- tests/Android.mk | 2 +- tests/AndroidManifest.xml | 3 + .../android/settings/vpn2/CertInstallerHelper.java | 223 +++++++++++ tests/src/com/android/settings/vpn2/VpnInfo.java | 65 ++++ .../android/settings/vpn2/VpnProfileParser.java | 246 ++++++++++++ tests/src/com/android/settings/vpn2/VpnTests.java | 420 +++++++++++++++++++++ 6 files changed, 958 insertions(+), 1 deletion(-) create mode 100644 tests/src/com/android/settings/vpn2/CertInstallerHelper.java create mode 100644 tests/src/com/android/settings/vpn2/VpnInfo.java create mode 100644 tests/src/com/android/settings/vpn2/VpnProfileParser.java create mode 100644 tests/src/com/android/settings/vpn2/VpnTests.java (limited to 'tests') diff --git a/tests/Android.mk b/tests/Android.mk index f54aeee..bb31539 100644 --- a/tests/Android.mk +++ b/tests/Android.mk @@ -5,7 +5,7 @@ include $(CLEAR_VARS) LOCAL_MODULE_TAGS := tests LOCAL_CERTIFICATE := platform -LOCAL_JAVA_LIBRARIES := android.test.runner +LOCAL_JAVA_LIBRARIES := android.test.runner bouncycastle # Include all test java files. LOCAL_SRC_FILES := $(call all-java-files-under, src) diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml index 53bf40f..aa7f947 100644 --- a/tests/AndroidManifest.xml +++ b/tests/AndroidManifest.xml @@ -19,6 +19,9 @@ + + + diff --git a/tests/src/com/android/settings/vpn2/CertInstallerHelper.java b/tests/src/com/android/settings/vpn2/CertInstallerHelper.java new file mode 100644 index 0000000..f95893f --- /dev/null +++ b/tests/src/com/android/settings/vpn2/CertInstallerHelper.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.vpn2; + +import android.os.Environment; +import android.security.Credentials; +import android.security.KeyStore; +import android.util.Log; + +import com.android.internal.net.VpnProfile; +import com.android.org.bouncycastle.asn1.ASN1InputStream; +import com.android.org.bouncycastle.asn1.ASN1Sequence; +import com.android.org.bouncycastle.asn1.DEROctetString; +import com.android.org.bouncycastle.asn1.x509.BasicConstraints; + +import junit.framework.Assert; + +import libcore.io.Streams; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.KeyStore.PasswordProtection; +import java.security.KeyStore.PrivateKeyEntry; +import java.security.PrivateKey; +import java.security.UnrecoverableEntryException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; + +/** + * Certificate installer helper to extract information from a provided file + * and install certificates to keystore. + */ +public class CertInstallerHelper { + private static final String TAG = "CertInstallerHelper"; + /* Define a password to unlock keystore after it is reset */ + private static final String CERT_STORE_PASSWORD = "password"; + private final int mUid = KeyStore.UID_SELF; + private PrivateKey mUserKey; // private key + private X509Certificate mUserCert; // user certificate + private List mCaCerts = new ArrayList(); + private KeyStore mKeyStore = KeyStore.getInstance(); + + /** + * Unlock keystore and set password + */ + public CertInstallerHelper() { + mKeyStore.reset(); + mKeyStore.password(CERT_STORE_PASSWORD); + } + + private void extractCertificate(String certFile, String password) { + InputStream in = null; + final byte[] raw; + java.security.KeyStore keystore = null; + try { + // Read .p12 file from SDCARD and extract with password + in = new FileInputStream(new File( + Environment.getExternalStorageDirectory(), certFile)); + raw = Streams.readFully(in); + + keystore = java.security.KeyStore.getInstance("PKCS12"); + PasswordProtection passwordProtection = new PasswordProtection(password.toCharArray()); + keystore.load(new ByteArrayInputStream(raw), passwordProtection.getPassword()); + + // Install certificates and private keys + Enumeration aliases = keystore.aliases(); + if (!aliases.hasMoreElements()) { + Assert.fail("key store failed to put in keychain"); + } + ArrayList aliasesList = Collections.list(aliases); + // The keystore is initialized for each test case, there will + // be only one alias in the keystore + Assert.assertEquals(1, aliasesList.size()); + String alias = aliasesList.get(0); + java.security.KeyStore.Entry entry = keystore.getEntry(alias, passwordProtection); + Log.d(TAG, "extracted alias = " + alias + ", entry=" + entry.getClass()); + + if (entry instanceof PrivateKeyEntry) { + Assert.assertTrue(installFrom((PrivateKeyEntry) entry)); + } + } catch (IOException e) { + Assert.fail("Failed to read certficate: " + e); + } catch (KeyStoreException e) { + Log.e(TAG, "failed to extract certificate" + e); + } catch (NoSuchAlgorithmException e) { + Log.e(TAG, "failed to extract certificate" + e); + } catch (CertificateException e) { + Log.e(TAG, "failed to extract certificate" + e); + } catch (UnrecoverableEntryException e) { + Log.e(TAG, "failed to extract certificate" + e); + } + finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + Log.e(TAG, "close FileInputStream error: " + e); + } + } + } + } + + /** + * Extract private keys, user certificates and ca certificates + */ + private synchronized boolean installFrom(PrivateKeyEntry entry) { + mUserKey = entry.getPrivateKey(); + mUserCert = (X509Certificate) entry.getCertificate(); + + Certificate[] certs = entry.getCertificateChain(); + Log.d(TAG, "# certs extracted = " + certs.length); + mCaCerts = new ArrayList(certs.length); + for (Certificate c : certs) { + X509Certificate cert = (X509Certificate) c; + if (isCa(cert)) { + mCaCerts.add(cert); + } + } + Log.d(TAG, "# ca certs extracted = " + mCaCerts.size()); + return true; + } + + private boolean isCa(X509Certificate cert) { + try { + byte[] asn1EncodedBytes = cert.getExtensionValue("2.5.29.19"); + if (asn1EncodedBytes == null) { + return false; + } + DEROctetString derOctetString = (DEROctetString) + new ASN1InputStream(asn1EncodedBytes).readObject(); + byte[] octets = derOctetString.getOctets(); + ASN1Sequence sequence = (ASN1Sequence) + new ASN1InputStream(octets).readObject(); + return BasicConstraints.getInstance(sequence).isCA(); + } catch (IOException e) { + return false; + } + } + + /** + * Extract certificate from the given file, and install it to keystore + * @param name certificate name + * @param certFile .p12 file which includes certificates + * @param password password to extract the .p12 file + */ + public void installCertificate(VpnProfile profile, String certFile, String password) { + // extract private keys, certificates from the provided file + extractCertificate(certFile, password); + // install certificate to the keystore + int flags = KeyStore.FLAG_ENCRYPTED; + try { + if (mUserKey != null) { + Log.v(TAG, "has private key"); + String key = Credentials.USER_PRIVATE_KEY + profile.ipsecUserCert; + byte[] value = mUserKey.getEncoded(); + + if (!mKeyStore.importKey(key, value, mUid, flags)) { + Log.e(TAG, "Failed to install " + key + " as user " + mUid); + return; + } + Log.v(TAG, "install " + key + " as user " + mUid + " is successful"); + } + + if (mUserCert != null) { + String certName = Credentials.USER_CERTIFICATE + profile.ipsecUserCert; + byte[] certData = Credentials.convertToPem(mUserCert); + + if (!mKeyStore.put(certName, certData, mUid, flags)) { + Log.e(TAG, "Failed to install " + certName + " as user " + mUid); + return; + } + Log.v(TAG, "install " + certName + " as user" + mUid + " is successful."); + } + + if (!mCaCerts.isEmpty()) { + String caListName = Credentials.CA_CERTIFICATE + profile.ipsecCaCert; + X509Certificate[] caCerts = mCaCerts.toArray(new X509Certificate[mCaCerts.size()]); + byte[] caListData = Credentials.convertToPem(caCerts); + + if (!mKeyStore.put(caListName, caListData, mUid, flags)) { + Log.e(TAG, "Failed to install " + caListName + " as user " + mUid); + return; + } + Log.v(TAG, " install " + caListName + " as user " + mUid + " is successful"); + } + } catch (CertificateEncodingException e) { + Log.e(TAG, "Exception while convert certificates to pem " + e); + throw new AssertionError(e); + } catch (IOException e) { + Log.e(TAG, "IOException while convert to pem: " + e); + } + } + + public int getUid() { + return mUid; + } +} diff --git a/tests/src/com/android/settings/vpn2/VpnInfo.java b/tests/src/com/android/settings/vpn2/VpnInfo.java new file mode 100644 index 0000000..ab7fb0f --- /dev/null +++ b/tests/src/com/android/settings/vpn2/VpnInfo.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.vpn2; + +import com.android.internal.net.VpnProfile; + +/** + * Wrapper for VPN Profile and associated certificate files + */ +public class VpnInfo { + // VPN Profile + private VpnProfile mVpnProfile; + // Certificate file in PC12 format for user certificates and private keys + private String mCertificateFile = null; + // Password to extract certificates from the file + private String mPassword = null; + + public VpnInfo(VpnProfile vpnProfile, String certFile, String password) { + mVpnProfile = vpnProfile; + mCertificateFile = certFile; + mPassword = password; + } + + public VpnInfo(VpnProfile vpnProfile) { + mVpnProfile = vpnProfile; + } + + public void setVpnProfile(VpnProfile vpnProfile) { + mVpnProfile = vpnProfile; + } + + public void setCertificateFile(String certFile) { + mCertificateFile = certFile; + } + + public void setPassword(String password) { + mPassword = password; + } + + public VpnProfile getVpnProfile() { + return mVpnProfile; + } + + public String getCertificateFile() { + return mCertificateFile; + } + + public String getPassword() { + return mPassword; + } +} diff --git a/tests/src/com/android/settings/vpn2/VpnProfileParser.java b/tests/src/com/android/settings/vpn2/VpnProfileParser.java new file mode 100644 index 0000000..51c2550 --- /dev/null +++ b/tests/src/com/android/settings/vpn2/VpnProfileParser.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.vpn2; + +import android.util.Log; + +import com.android.internal.net.VpnProfile; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +/** + * Parse VPN profiles from an XML file + */ +public class VpnProfileParser { + private final static String TAG = "VpnProfileParser"; + private static Map mVpnPool = new HashMap(); + + static DefaultHandler mHandler = new DefaultHandler() { + boolean name; + boolean type; + boolean server; + boolean username; + boolean password; + boolean dnsServers; + boolean searchDomains; + boolean routes; + boolean mppe; + boolean l2tpSecret; + boolean ipsecIdentifier; + boolean ipsecSecret; + boolean ipsecUserCert; + boolean ipsecCaCert; + boolean ipsecServerCert; + boolean certFile; + boolean certFilePassword; + VpnProfile profile = null; + VpnInfo vpnInfo = null; + + + @Override + public void startElement(String uri, String localName, String tagName, + Attributes attributes) throws SAXException { + if (tagName.equalsIgnoreCase("vpn")) { + //create a new VPN profile + profile = new VpnProfile(Long.toHexString(System.currentTimeMillis())); + vpnInfo = new VpnInfo(profile); + } + if (tagName.equalsIgnoreCase("name")) { + name = true; + } + if (tagName.equalsIgnoreCase("type")) { + type = true; + } + if (tagName.equalsIgnoreCase("server")) { + server = true; + } + if (tagName.equalsIgnoreCase("username")) { + username = true; + } + if (tagName.equalsIgnoreCase("password")) { + password = true; + } + if (tagName.equalsIgnoreCase("dnsServers")) { + dnsServers = true; + } + if (tagName.equalsIgnoreCase("searchDomains")) { + searchDomains = true; + } + if (tagName.equalsIgnoreCase("mppe")) { + mppe = true; + } + if (tagName.equalsIgnoreCase("l2tpSecret")) { + l2tpSecret = true; + } + if (tagName.equalsIgnoreCase("ipsecIdentifier")) { + ipsecIdentifier = true; + } + if (tagName.equalsIgnoreCase("ipsecSecret")) { + ipsecSecret = true; + } + if (tagName.equalsIgnoreCase("ipsecUserCert")) { + ipsecUserCert = true; + } + if (tagName.equalsIgnoreCase("ipsecCaCert")) { + ipsecCaCert = true; + } + if (tagName.equalsIgnoreCase("ipsecServerCert")) { + ipsecServerCert = true; + } + if (tagName.equalsIgnoreCase("routes")) { + routes = true; + } + if (tagName.equalsIgnoreCase("cert-file")) { + certFile = true; + } + if (tagName.equalsIgnoreCase("cert-file-password")) { + certFilePassword = true; + } + } + + @Override + public void endElement(String uri, String localName, String tagName) throws SAXException { + if (tagName.equalsIgnoreCase("vpn")) { + mVpnPool.put(profile.type, vpnInfo); + } + } + + @Override + public void characters(char ch[], int start, int length) throws SAXException { + String strValue = new String(ch, start, length); + if (name) { + profile.name = strValue; + name = false; + } + if (type) { + int t = getVpnProfileType(strValue); + if (t < 0) { + throw new SAXException("not a valid VPN type"); + } else { + profile.type = t; + } + type = false; + } + if (server) { + profile.server = strValue; + server = false; + } + if (username) { + profile.username = strValue; + username = false; + } + if (password) { + profile.password = strValue; + password = false; + } + if (dnsServers) { + profile.dnsServers = strValue; + dnsServers = false; + } + if (searchDomains) { + profile.searchDomains = strValue; + searchDomains = false; + } + if (mppe) { + profile.mppe = Boolean.valueOf(strValue); + mppe = false; + } + if (l2tpSecret) { + profile.l2tpSecret = strValue; + l2tpSecret = false; + } + if (ipsecIdentifier) { + profile.ipsecIdentifier = strValue; + ipsecIdentifier = false; + } + if (ipsecSecret) { + profile.ipsecSecret = strValue; + ipsecSecret = false; + } + if (ipsecUserCert) { + profile.ipsecUserCert = strValue; + ipsecUserCert = false; + } + if (ipsecCaCert) { + profile.ipsecCaCert = strValue; + ipsecCaCert = false; + } + if (ipsecServerCert) { + profile.ipsecServerCert = strValue; + ipsecServerCert = false; + } + if (routes) { + profile.routes = strValue; + routes = false; + } + if (certFile) { + vpnInfo.setCertificateFile(strValue); + certFile = false; + } + if (certFilePassword) { + vpnInfo.setPassword(strValue); + certFilePassword = false; + } + } + + private int getVpnProfileType(String type) { + if (type.equalsIgnoreCase("TYPE_PPTP")) { + return VpnProfile.TYPE_PPTP; + } else if (type.equalsIgnoreCase("TYPE_L2TP_IPSEC_PSK")) { + return VpnProfile.TYPE_L2TP_IPSEC_PSK; + } else if (type.equalsIgnoreCase("TYPE_L2TP_IPSEC_RSA")) { + return VpnProfile.TYPE_L2TP_IPSEC_RSA; + } else if (type.equalsIgnoreCase("TYPE_IPSEC_XAUTH_PSK")) { + return VpnProfile.TYPE_IPSEC_XAUTH_PSK; + } else if (type.equalsIgnoreCase("TYPE_IPSEC_XAUTH_RSA")) { + return VpnProfile.TYPE_IPSEC_XAUTH_RSA; + } else if (type.equalsIgnoreCase("TYPE_IPSEC_HYBRID_RSA")) { + return VpnProfile.TYPE_IPSEC_HYBRID_RSA; + } else { + Log.v(TAG, "Invalid VPN type: " + type); + return -1; + } + } + }; + + public static Map parse(InputStream in) { + try { + SAXParserFactory factory = SAXParserFactory.newInstance(); + SAXParser saxParser = factory.newSAXParser(); + saxParser.parse(in, mHandler); + } catch (SAXException e) { + Log.e(TAG, "Parse vpn profile exception: " + e.toString()); + } catch (IOException e) { + Log.e(TAG, "Parse vpn profile exception: " + e.toString()); + } catch (ParserConfigurationException e) { + Log.e(TAG, "Parse vpn profile exception: " + e.toString()); + } finally { + return mVpnPool; + } + } +} diff --git a/tests/src/com/android/settings/vpn2/VpnTests.java b/tests/src/com/android/settings/vpn2/VpnTests.java new file mode 100644 index 0000000..2c6963a --- /dev/null +++ b/tests/src/com/android/settings/vpn2/VpnTests.java @@ -0,0 +1,420 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.vpn2; + +import android.content.Context; +import android.net.IConnectivityManager; +import android.net.LinkAddress; +import android.net.RouteInfo; +import android.os.Bundle; +import android.os.Environment; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.security.Credentials; +import android.security.KeyStore; +import android.test.InstrumentationTestCase; +import android.test.InstrumentationTestRunner; +import android.test.suitebuilder.annotation.LargeTest; +import android.util.Log; + +import com.android.internal.net.LegacyVpnInfo; +import com.android.internal.net.VpnConfig; +import com.android.internal.net.VpnProfile; + +import junit.framework.Assert; + +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.util.EntityUtils; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +/** + * Legacy VPN connection tests + * + * To run the test, use command: + * adb shell am instrument -e class com.android.settings.vpn2.VpnTests -e profile foo.xml + * -w com.android.settings.tests/android.test.InstrumentationTestRunner + * + * VPN profiles are saved in an xml file and will be loaded through {@link VpnProfileParser}. + * Push the profile (foo.xml) to the external storage, e.g adb push foo.xml /sdcard/ before running + * the above command. + * + * A typical profile looks like the following: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * VPN types include: TYPE_PPTP, TYPE_L2TP_IPSEC_PSK, TYPE_L2TP_IPSEC_RSA, + * TYPE_IPSEC_XAUTH_PSK, TYPE_IPSEC_XAUTH_RSA, TYPE_IPSEC_HYBRID_RSA + */ +public class VpnTests extends InstrumentationTestCase { + private static final String TAG = "VpnTests"; + /* Maximum time to wait for VPN connection */ + private static final long MAX_CONNECTION_TIME = 5 * 60 * 1000; + private static final int MAX_DISCONNECTION_TRIES = 3; + private static final String EXTERNAL_SERVER = + "http://ip2country.sourceforge.net/ip2c.php?format=JSON"; + private static final String VPN_INTERFACE = "ppp0"; + private final IConnectivityManager mService = IConnectivityManager.Stub + .asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE)); + private Map mVpnInfoPool = null; + private Context mContext; + private CertInstallerHelper mCertHelper = null; + private KeyStore mKeyStore = KeyStore.getInstance(); + private String mPreviousIpAddress = null; + private boolean DEBUG = false; + + @Override + protected void setUp() throws Exception { + super.setUp(); + InputStream in = null; + InstrumentationTestRunner mRunner = (InstrumentationTestRunner)getInstrumentation(); + mContext = mRunner.getContext(); + Bundle arguments = mRunner.getArguments(); + String PROFILE_NAME = arguments.getString("profile"); + Assert.assertNotNull("Push profile to external storage and load with" + + "'-e profile '", PROFILE_NAME); + File profileFile = new File(Environment.getExternalStorageDirectory(), PROFILE_NAME); + in = new FileInputStream(profileFile); + mVpnInfoPool = VpnProfileParser.parse(in); + Assert.assertNotNull("no VPN profiles are parsed", mVpnInfoPool); + if (DEBUG) { + Log.v(TAG, "print out the vpn profiles"); + for (Map.Entry profileEntrySet: mVpnInfoPool.entrySet()) { + VpnInfo vpnInfo = profileEntrySet.getValue(); + printVpnProfile(vpnInfo.getVpnProfile()); + if (vpnInfo.getCertificateFile() != null) { + Log.d(TAG, "certificate file for this vpn is " + vpnInfo.getCertificateFile()); + } + if (vpnInfo.getPassword() != null) { + Log.d(TAG, "password for the certificate file is: " + vpnInfo.getPassword()); + } + } + } + // disconnect existing vpn if there is any + LegacyVpnInfo oldVpn = mService.getLegacyVpnInfo(); + if (oldVpn != null) { + Log.v(TAG, "disconnect legacy VPN"); + disconnect(); + // wait till the legacy VPN is disconnected. + int tries = 0; + while (tries < MAX_DISCONNECTION_TRIES && mService.getLegacyVpnInfo() != null) { + tries++; + Thread.sleep(10 * 1000); + Log.v(TAG, "Wait for legacy VPN to be disconnected."); + } + Assert.assertNull("Failed to disconect VPN", mService.getLegacyVpnInfo()); + // wait for 30 seconds after the previous VPN is disconnected. + sleep(30 * 1000); + } + // Create CertInstallerHelper to initialize the keystore + mCertHelper = new CertInstallerHelper(); + } + + private void printVpnProfile(VpnProfile profile) { + Log.v(TAG, "profile: "); + Log.v(TAG, "key: " + profile.key); + Log.v(TAG, "name: " + profile.name); + Log.v(TAG, "type: " + profile.type); + Log.v(TAG, "server: " + profile.server); + Log.v(TAG, "username: " + profile.username); + Log.v(TAG, "password: " + profile.password); + Log.v(TAG, "dnsServers: " + profile.dnsServers); + Log.v(TAG, "searchDomains: " + profile.searchDomains); + Log.v(TAG, "routes: " + profile.routes); + Log.v(TAG, "mppe: " + profile.mppe); + Log.v(TAG, "l2tpSecret: " + profile.l2tpSecret); + Log.v(TAG, "ipsecIdentifier: " + profile.ipsecIdentifier); + Log.v(TAG, "ipsecSecret: " + profile.ipsecSecret); + Log.v(TAG, "ipsecUserCert: " + profile.ipsecUserCert); + Log.v(TAG, "ipsecCaCert: " + profile.ipsecCaCert); + Log.v(TAG, "ipsecServerCert: " + profile.ipsecServerCert); + } + + private void printKeyStore(VpnProfile profile) { + // print out the information from keystore + String privateKey = ""; + String userCert = ""; + String caCert = ""; + String serverCert = ""; + if (!profile.ipsecUserCert.isEmpty()) { + privateKey = Credentials.USER_PRIVATE_KEY + profile.ipsecUserCert; + byte[] value = mKeyStore.get(Credentials.USER_CERTIFICATE + profile.ipsecUserCert); + userCert = (value == null) ? null : new String(value, StandardCharsets.UTF_8); + } + if (!profile.ipsecCaCert.isEmpty()) { + byte[] value = mKeyStore.get(Credentials.CA_CERTIFICATE + profile.ipsecCaCert); + caCert = (value == null) ? null : new String(value, StandardCharsets.UTF_8); + } + if (!profile.ipsecServerCert.isEmpty()) { + byte[] value = mKeyStore.get(Credentials.USER_CERTIFICATE + profile.ipsecServerCert); + serverCert = (value == null) ? null : new String(value, StandardCharsets.UTF_8); + } + Log.v(TAG, "privateKey: \n" + ((privateKey == null) ? "" : privateKey)); + Log.v(TAG, "userCert: \n" + ((userCert == null) ? "" : userCert)); + Log.v(TAG, "caCert: \n" + ((caCert == null) ? "" : caCert)); + Log.v(TAG, "serverCert: \n" + ((serverCert == null) ? "" : serverCert)); + } + + /** + * Connect legacy VPN + */ + private void connect(VpnProfile profile) throws Exception { + try { + mService.startLegacyVpn(profile); + } catch (IllegalStateException e) { + fail(String.format("start legacy vpn: %s failed: %s", profile.name, e.toString())); + } + } + + /** + * Disconnect legacy VPN + */ + private void disconnect() throws Exception { + try { + mService.prepareVpn(VpnConfig.LEGACY_VPN, VpnConfig.LEGACY_VPN); + } catch (RemoteException e) { + Log.e(TAG, String.format("disconnect VPN exception: %s", e.toString())); + } + } + + /** + * Get external IP address + */ + private String getIpAddress() { + String ip = null; + try { + HttpClient httpClient = new DefaultHttpClient(); + HttpGet httpGet = new HttpGet(EXTERNAL_SERVER); + HttpResponse httpResponse = httpClient.execute(httpGet); + Log.i(TAG, "Response from httpget: " + httpResponse.getStatusLine().toString()); + + String entityStr = EntityUtils.toString(httpResponse.getEntity()); + JSONObject json_data = new JSONObject(entityStr); + ip = json_data.getString("ip"); + Log.v(TAG, "json_data: " + ip); + } catch (IllegalArgumentException e) { + Log.e(TAG, "exception while getting external IP: " + e.toString()); + } catch (IOException e) { + Log.e(TAG, "IOException while getting IP: " + e.toString()); + } catch (JSONException e) { + Log.e(TAG, "exception while creating JSONObject: " + e.toString()); + } + return ip; + } + + /** + * Verify the vpn connection by checking the VPN state and external IP + */ + private void validateVpnConnection(VpnProfile profile) throws Exception { + validateVpnConnection(profile, false); + } + + /** + * Verify the vpn connection by checking the VPN state, external IP or ping test + */ + private void validateVpnConnection(VpnProfile profile, boolean pingTestFlag) throws Exception { + LegacyVpnInfo legacyVpnInfo = mService.getLegacyVpnInfo(); + Assert.assertTrue(legacyVpnInfo != null); + + long start = System.currentTimeMillis(); + while (((System.currentTimeMillis() - start) < MAX_CONNECTION_TIME) && + (legacyVpnInfo.state != LegacyVpnInfo.STATE_CONNECTED)) { + Log.v(TAG, "vpn state: " + legacyVpnInfo.state); + sleep(10 * 1000); + legacyVpnInfo = mService.getLegacyVpnInfo(); + } + + // the vpn state should be CONNECTED + Assert.assertTrue(legacyVpnInfo.state == LegacyVpnInfo.STATE_CONNECTED); + if (pingTestFlag) { + Assert.assertTrue(pingTest(profile.server)); + } else { + String curIpAddress = getIpAddress(); + // the outgoing IP address should be the same as the VPN server address + Assert.assertEquals(profile.server, curIpAddress); + } + } + + private boolean pingTest(String server) { + final long PING_TIMER = 3 * 60 * 1000; // 3 minutes + if (server == null || server.isEmpty()) { + return false; + } + long startTime = System.currentTimeMillis(); + while ((System.currentTimeMillis() - startTime) < PING_TIMER) { + try { + Log.v(TAG, "Start ping test, ping " + server); + Process p = Runtime.getRuntime().exec("ping -c 10 -w 100 " + server); + int status = p.waitFor(); + if (status == 0) { + // if any of the ping test is successful, return true + return true; + } + } catch (UnknownHostException e) { + Log.e(TAG, "Ping test Fail: Unknown Host"); + } catch (IOException e) { + Log.e(TAG, "Ping test Fail: IOException"); + } catch (InterruptedException e) { + Log.e(TAG, "Ping test Fail: InterruptedException"); + } + } + // ping test timeout + return false; + } + + /** + * Install certificates from a file loaded in external stroage on the device + * @param profile vpn profile + * @param fileName certificate file name + * @param password password to extract certificate file + */ + private void installCertificatesFromFile(VpnProfile profile, String fileName, String password) + throws Exception { + if (profile == null || fileName == null || password == null) { + throw new Exception ("vpn profile, certificate file name and password can not be null"); + } + + int curUid = mContext.getUserId(); + mCertHelper.installCertificate(profile, fileName, password); + + if (DEBUG) { + printKeyStore(profile); + } + } + + private void sleep(long time) { + try { + Thread.sleep(time); + } catch (InterruptedException e) { + Log.e(TAG, "interrupted: " + e.toString()); + } + } + + /** + * Test PPTP VPN connection + */ + @LargeTest + public void testPPTPConnection() throws Exception { + mPreviousIpAddress = getIpAddress(); + VpnInfo curVpnInfo = mVpnInfoPool.get(VpnProfile.TYPE_PPTP); + VpnProfile vpnProfile = curVpnInfo.getVpnProfile(); + connect(vpnProfile); + validateVpnConnection(vpnProfile); + } + + /** + * Test L2TP/IPSec PSK VPN connection + */ + @LargeTest + public void testL2tpIpsecPskConnection() throws Exception { + mPreviousIpAddress = getIpAddress(); + VpnInfo curVpnInfo = mVpnInfoPool.get(VpnProfile.TYPE_L2TP_IPSEC_PSK); + VpnProfile vpnProfile = curVpnInfo.getVpnProfile(); + connect(vpnProfile); + validateVpnConnection(vpnProfile); + } + + /** + * Test L2TP/IPSec RSA VPN connection + */ + @LargeTest + public void testL2tpIpsecRsaConnection() throws Exception { + mPreviousIpAddress = getIpAddress(); + VpnInfo curVpnInfo = mVpnInfoPool.get(VpnProfile.TYPE_L2TP_IPSEC_RSA); + VpnProfile vpnProfile = curVpnInfo.getVpnProfile(); + if (DEBUG) { + printVpnProfile(vpnProfile); + } + String certFile = curVpnInfo.getCertificateFile(); + String password = curVpnInfo.getPassword(); + installCertificatesFromFile(vpnProfile, certFile, password); + connect(vpnProfile); + validateVpnConnection(vpnProfile); + } + + /** + * Test IPSec Xauth RSA VPN connection + */ + @LargeTest + public void testIpsecXauthRsaConnection() throws Exception { + mPreviousIpAddress = getIpAddress(); + VpnInfo curVpnInfo = mVpnInfoPool.get(VpnProfile.TYPE_IPSEC_XAUTH_RSA); + VpnProfile vpnProfile = curVpnInfo.getVpnProfile(); + if (DEBUG) { + printVpnProfile(vpnProfile); + } + String certFile = curVpnInfo.getCertificateFile(); + String password = curVpnInfo.getPassword(); + installCertificatesFromFile(vpnProfile, certFile, password); + connect(vpnProfile); + validateVpnConnection(vpnProfile); + } + + /** + * Test IPSec Xauth PSK VPN connection + */ + @LargeTest + public void testIpsecXauthPskConnection() throws Exception { + VpnInfo curVpnInfo = mVpnInfoPool.get(VpnProfile.TYPE_IPSEC_XAUTH_PSK); + VpnProfile vpnProfile = curVpnInfo.getVpnProfile(); + if (DEBUG) { + printVpnProfile(vpnProfile); + } + connect(vpnProfile); + validateVpnConnection(vpnProfile, true); + } + + /** + * Test IPSec Hybrid RSA VPN connection + */ + @LargeTest + public void testIpsecHybridRsaConnection() throws Exception { + mPreviousIpAddress = getIpAddress(); + VpnInfo curVpnInfo = mVpnInfoPool.get(VpnProfile.TYPE_IPSEC_HYBRID_RSA); + VpnProfile vpnProfile = curVpnInfo.getVpnProfile(); + if (DEBUG) { + printVpnProfile(vpnProfile); + } + connect(vpnProfile); + validateVpnConnection(vpnProfile); + } +} -- cgit v1.1