/*
 * 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.os.Bundle;
import android.os.Environment;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.security.Credentials;
import android.security.KeyStore;
import android.security.NetworkSecurityPolicy;
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 java.net.HttpURLConnection;
import java.net.URL;
import junit.framework.Assert;

import libcore.io.Streams;
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>
 *   <name></name>
 *   <type></type>
 *   <server></server>
 *   <username></username>
 *   <password></password>
 *   <dnsServers></dnsServers>
 *   <searchDomains></searchDomains>
 *   <routes></routes>
 *   <l2tpSecret></l2tpSecret>
 *   <ipsecIdentifier></ipsecIdentifier>
 *   <ipsecSecret></ipsecSecret>
 *   <ipsecUserCert></ipsecUserCert>
 *   <ipsecCaCert></ipsecCaCert>
 *   <ipsecServerCert></ipsecServerCert>
 * </vpn>
 * 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 long VPN_STAY_TIME = 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<Integer, VpnInfo> 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 <filename>'", 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<Integer, VpnInfo> 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(UserHandle.myUserId());
        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(UserHandle.myUserId()) != null) {
                tries++;
                Thread.sleep(10 * 1000);
                Log.v(TAG, "Wait for legacy VPN to be disconnected.");
            }
            Assert.assertNull("Failed to disconect VPN",
                    mService.getLegacyVpnInfo(UserHandle.myUserId()));
            // wait for 30 seconds after the previous VPN is disconnected.
            sleep(30 * 1000);
        }
        // Create CertInstallerHelper to initialize the keystore
        mCertHelper = new CertInstallerHelper();
    }

    @Override
    protected void tearDown() throws Exception {
        sleep(VPN_STAY_TIME);
        super.tearDown();
    }

    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, UserHandle.myUserId());
        } catch (RemoteException e) {
            Log.e(TAG, String.format("disconnect VPN exception: %s", e.toString()));
        }
    }

    /**
     * Get external IP address
     */
    private String getIpAddress() {
        String ip = null;
        HttpURLConnection urlConnection = null;
        // TODO: Rewrite this test to use an HTTPS URL.
        // Because this test uses cleartext HTTP, the network security policy of this app needs to
        // be temporarily relaxed to permit such traffic.
        NetworkSecurityPolicy networkSecurityPolicy = NetworkSecurityPolicy.getInstance();
        boolean cleartextTrafficPermittedBeforeTest =
                networkSecurityPolicy.isCleartextTrafficPermitted();
        networkSecurityPolicy.setCleartextTrafficPermitted(true);
        try {
            URL url = new URL(EXTERNAL_SERVER);
            urlConnection = (HttpURLConnection) url.openConnection();
            Log.i(TAG, "Response from httpget: " + urlConnection.getResponseCode());

            InputStream is = urlConnection.getInputStream();
            String response;
            try {
                response = new String(Streams.readFully(is), StandardCharsets.UTF_8);
            } finally {
                is.close();
            }

            JSONObject json_data = new JSONObject(response);
            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());
        } finally {
            networkSecurityPolicy.setCleartextTrafficPermitted(cleartextTrafficPermittedBeforeTest);
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
        }
        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(UserHandle.myUserId());
        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(UserHandle.myUserId());
        }

        // 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);
    }
}
