/** * Copyright (c) 2013 LEAP Encryption Access Project and contributers * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package se.leap.bitmaskclient; import java.io.StringReader; import java.io.IOException; import java.util.Calendar; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.Set; import java.util.TreeMap; import java.util.Vector; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import se.leap.bitmaskclient.R; import se.leap.bitmaskclient.Dashboard; import se.leap.bitmaskclient.Provider; import de.blinkt.openvpn.activities.DisconnectVPN; import de.blinkt.openvpn.core.ConfigParser; import de.blinkt.openvpn.core.ConfigParser.ConfigParseError; import de.blinkt.openvpn.LaunchVPN; import de.blinkt.openvpn.core.OpenVpnManagementThread; import de.blinkt.openvpn.core.OpenVpnService; import de.blinkt.openvpn.core.OpenVpnService.LocalBinder; import de.blinkt.openvpn.core.ProfileManager; import de.blinkt.openvpn.VpnProfile; import android.app.Activity; import android.app.IntentService; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.SharedPreferences; import android.drm.DrmStore.Action; import android.os.Bundle; import android.os.IBinder; import android.os.ResultReceiver; import android.util.Log; /** * EIP is the abstract base class for interacting with and managing the Encrypted * Internet Proxy connection. Connections are started, stopped, and queried through * this IntentService. * Contains logic for parsing eip-service.json from the provider, configuring and selecting * gateways, and controlling {@link de.blinkt.openvpn.core.OpenVpnService} connections. * * @author Sean Leonard * @author Parménides GV */ public final class EIP extends IntentService { public final static String AUTHED_EIP = "authed eip"; public final static String ACTION_START_EIP = "se.leap.bitmaskclient.START_EIP"; public final static String ACTION_STOP_EIP = "se.leap.bitmaskclient.STOP_EIP"; public final static String ACTION_UPDATE_EIP_SERVICE = "se.leap.bitmaskclient.UPDATE_EIP_SERVICE"; public final static String ACTION_IS_EIP_RUNNING = "se.leap.bitmaskclient.IS_RUNNING"; public final static String EIP_NOTIFICATION = "EIP_NOTIFICATION"; public final static String STATUS = "eip status"; public final static String ALLOWED_ANON = "allow_anonymous"; public final static String CERTIFICATE = "cert"; public final static String PRIVATE_KEY = "private_key"; public final static String KEY = "eip"; public final static String PARSED_SERIAL = "eip_parsed_serial"; public final static String SERVICE_API_PATH = "config/eip-service.json"; public final static String RECEIVER_TAG = "receiverTag"; public final static String REQUEST_TAG = "requestTag"; public final static String TAG = "se.leap.bitmaskclient.EIP"; private static Context context; private static ResultReceiver mReceiver; private static OpenVpnService mVpnService; private static boolean mBound = false; // Used to store actions to "resume" onServiceConnection private static String mPending = null; private static int parsedEipSerial; private static JSONObject eipDefinition = null; private static OVPNGateway activeGateway = null; public EIP(){ super("LEAPEIP"); } @Override public void onCreate() { super.onCreate(); context = getApplicationContext(); updateEIPService(); this.retreiveVpnService(); } @Override public void onDestroy() { unbindService(mVpnServiceConn); mBound = false; super.onDestroy(); } @Override protected void onHandleIntent(Intent intent) { String action = intent.getAction(); mReceiver = intent.getParcelableExtra(RECEIVER_TAG); if ( action == ACTION_IS_EIP_RUNNING ) this.isRunning(); if ( action == ACTION_UPDATE_EIP_SERVICE ) this.updateEIPService(); else if ( action == ACTION_START_EIP ) this.startEIP(); else if ( action == ACTION_STOP_EIP ) this.stopEIP(); } /** * Sends an Intent to bind OpenVpnService. * Used when OpenVpnService isn't bound but might be running. */ private boolean retreiveVpnService() { Intent bindIntent = new Intent(this,OpenVpnService.class); bindIntent.setAction(OpenVpnService.START_SERVICE); return bindService(bindIntent, mVpnServiceConn, BIND_AUTO_CREATE); } private ServiceConnection mVpnServiceConn = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { LocalBinder binder = (LocalBinder) service; mVpnService = binder.getService(); mBound = true; if (mReceiver != null && mPending != null) { boolean running = isConnected(); int resultCode = Activity.RESULT_CANCELED; if (mPending.equals(ACTION_IS_EIP_RUNNING)){ resultCode = (running) ? Activity.RESULT_OK : Activity.RESULT_CANCELED; } else if (mPending.equals(ACTION_START_EIP)){ resultCode = (running) ? Activity.RESULT_OK : Activity.RESULT_CANCELED; } else if (mPending.equals(ACTION_STOP_EIP)){ resultCode = (running) ? Activity.RESULT_CANCELED : Activity.RESULT_OK; } Bundle resultData = new Bundle(); resultData.putString(REQUEST_TAG, ACTION_IS_EIP_RUNNING); mReceiver.send(resultCode, resultData); mPending = null; } } @Override public void onServiceDisconnected(ComponentName name) { mBound = false; if (mReceiver != null){ Bundle resultData = new Bundle(); resultData.putString(REQUEST_TAG, EIP_NOTIFICATION); mReceiver.send(Activity.RESULT_CANCELED, resultData); } } }; /** * Attempts to determine if OpenVpnService has an established VPN connection * through the bound ServiceConnection. If there is no bound service, this * method will attempt to bind a running OpenVpnService and send * Activity.RESULT_CANCELED to the ResultReceiver that made the * request. * Note: If the request to bind OpenVpnService is successful, the ResultReceiver * will be notified in {@link onServiceConnected()} */ private void isRunning() { Bundle resultData = new Bundle(); resultData.putString(REQUEST_TAG, ACTION_IS_EIP_RUNNING); int resultCode = Activity.RESULT_CANCELED; boolean is_connected = isConnected(); if (mBound) { resultCode = (is_connected) ? Activity.RESULT_OK : Activity.RESULT_CANCELED; if (mReceiver != null){ mReceiver.send(resultCode, resultData); } } else { mPending = ACTION_IS_EIP_RUNNING; boolean retrieved_vpn_service = retreiveVpnService(); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } boolean running = is_connected; if (retrieved_vpn_service && running && mReceiver != null){ mReceiver.send(Activity.RESULT_OK, resultData); } else{ mReceiver.send(Activity.RESULT_CANCELED, resultData); } } } private boolean isConnected() { return getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).getString(STATUS, "").equalsIgnoreCase("LEVEL_CONNECTED"); } /** * Initiates an EIP connection by selecting a gateway and preparing and sending an * Intent to {@link se.leap.openvpn.LaunchVPN} */ private void startEIP() { activeGateway = selectGateway(); if(activeGateway != null && activeGateway.mVpnProfile != null) { Intent intent = new Intent(this,LaunchVPN.class); intent.setAction(Intent.ACTION_MAIN); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra(LaunchVPN.EXTRA_KEY, activeGateway.mVpnProfile.getUUID().toString() ); intent.putExtra(LaunchVPN.EXTRA_NAME, activeGateway.mVpnProfile.getName() ); intent.putExtra(LaunchVPN.EXTRA_HIDELOG, true); intent.putExtra(RECEIVER_TAG, mReceiver); startActivity(intent); mPending = ACTION_START_EIP; } } /** * Disconnects the EIP connection gracefully through the bound service or forcefully * if there is no bound service. Sends a message to the requesting ResultReceiver. */ private void stopEIP() { if (mBound) mVpnService.onRevoke(); else if(getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).getString(STATUS, "").startsWith("LEVEL_CONNECT")){ Intent disconnect_vpn = new Intent(this, DisconnectVPN.class); disconnect_vpn.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(disconnect_vpn); } if (mReceiver != null){ Bundle resultData = new Bundle(); resultData.putString(REQUEST_TAG, ACTION_STOP_EIP); mReceiver.send(Activity.RESULT_OK, resultData); } } /** * Loads eip-service.json from SharedPreferences and calls {@link updateGateways()} * to parse gateway definitions. * TODO Implement API call to refresh eip-service.json from the provider */ private void updateEIPService() { try { eipDefinition = new JSONObject(getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).getString(KEY, "")); parsedEipSerial = getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).getInt(PARSED_SERIAL, 0); } catch (JSONException e) { // TODO Auto-generated catch block e.printStackTrace(); } if(parsedEipSerial == 0) { // Delete all vpn profiles ProfileManager vpl = ProfileManager.getInstance(context); VpnProfile[] profiles = (VpnProfile[]) vpl.getProfiles().toArray(new VpnProfile[vpl.getProfiles().size()]); for (int current_profile = 0; current_profile < profiles.length; current_profile++){ vpl.removeProfile(context, profiles[current_profile]); } } if (eipDefinition.optInt("serial") > parsedEipSerial) updateGateways(); } /** * Choose a gateway to connect to based on timezone from system locale data * * @return The gateway to connect to */ private OVPNGateway selectGateway() { // TODO Remove String arg constructor in favor of findGatewayByName(String) Calendar cal = Calendar.getInstance(); int localOffset = cal.get(Calendar.ZONE_OFFSET) / 3600000; TreeMap> offsets = new TreeMap>(); JSONObject locationsObjects = null; Iterator locations = null; try { locationsObjects = eipDefinition.getJSONObject("locations"); locations = locationsObjects.keys(); } catch (JSONException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } while (locations.hasNext()) { String locationName = locations.next(); JSONObject location = null; try { location = locationsObjects.getJSONObject(locationName); // Distance along the numberline of Prime Meridian centric, assumes UTC-11 through UTC+12 int dist = Math.abs(localOffset - location.optInt("timezone")); // Farther than 12 timezones and it's shorter around the "back" if (dist > 12) dist = 12 - (dist -12); // Well i'll be. Absolute values make equations do funny things. Set set = offsets.get(dist); if (set == null) set = new HashSet(); set.add(locationName); offsets.put(dist, set); } catch (JSONException e) { // TODO Auto-generated catch block e.printStackTrace(); } } String closestLocation = offsets.isEmpty() ? "" : offsets.firstEntry().getValue().iterator().next(); JSONArray gateways = null; String chosenHost = null; try { gateways = eipDefinition.getJSONArray("gateways"); for (int i = 0; i < gateways.length(); i++) { JSONObject gw = gateways.getJSONObject(i); if ( gw.getString("location").equalsIgnoreCase(closestLocation) || closestLocation.isEmpty()){ chosenHost = gw.getString("host"); break; } } } catch (JSONException e) { // TODO Auto-generated catch block e.printStackTrace(); } return new OVPNGateway(chosenHost); } /** * Walk the list of gateways defined in eip-service.json and parse them into * OVPNGateway objects. * TODO Store the OVPNGateways (as Serializable) in SharedPreferences */ private void updateGateways(){ JSONArray gatewaysDefined = null; try { gatewaysDefined = eipDefinition.getJSONArray("gateways"); } catch (JSONException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } for ( int i=0 ; i < gatewaysDefined.length(); i++ ){ JSONObject gw = null; try { gw = gatewaysDefined.getJSONObject(i); } catch (JSONException e) { // TODO Auto-generated catch block e.printStackTrace(); } try { if ( gw.getJSONObject("capabilities").getJSONArray("transport").toString().contains("openvpn") ){ new OVPNGateway(gw); } } catch (JSONException e) { // TODO Auto-generated catch block e.printStackTrace(); } } getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putInt(PARSED_SERIAL, eipDefinition.optInt(Provider.API_RETURN_SERIAL)).commit(); } /** * OVPNGateway provides objects defining gateways and their options and metadata. * Each instance contains a VpnProfile for OpenVPN specific data and member * variables describing capabilities and location * * @author Sean Leonard */ private class OVPNGateway { private String TAG = "OVPNGateway"; private String mName; private VpnProfile mVpnProfile; private JSONObject mGateway; private HashMap>> options = new HashMap>>(); /** * Attempts to retrieve a VpnProfile by name and build an OVPNGateway around it. * FIXME This needs to become a findGatewayByName() method * * @param name The hostname of the gateway to inflate */ private OVPNGateway(String name){ mName = name; this.loadVpnProfile(); } private void loadVpnProfile() { ProfileManager vpl = ProfileManager.getInstance(context); try { if ( mName == null ) mVpnProfile = vpl.getProfiles().iterator().next(); else mVpnProfile = vpl.getProfileByName(mName); } catch (NoSuchElementException e) { updateEIPService(); this.loadVpnProfile(); // FIXME catch infinite loops } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } /** * Build a gateway object from a JSON OpenVPN gateway definition in eip-service.json * and create a VpnProfile belonging to it. * * @param gateway The JSON OpenVPN gateway definition to parse */ protected OVPNGateway(JSONObject gateway){ mGateway = gateway; // Currently deletes VpnProfile for host, if there already is one, and builds new ProfileManager vpl = ProfileManager.getInstance(context); Collection profiles = vpl.getProfiles(); for (Iterator it = profiles.iterator(); it.hasNext(); ){ VpnProfile p = it.next(); try { if ( p.mName.equalsIgnoreCase( gateway.getString("host") ) ){ it.remove(); vpl.removeProfile(context, p); } } catch (JSONException e) { // TODO Auto-generated catch block e.printStackTrace(); } } this.createVPNProfile(); setUniqueProfileName(vpl); vpl.addProfile(mVpnProfile); vpl.saveProfile(context, mVpnProfile); vpl.saveProfileList(context); } /** * Attempts to create a unique profile name from the hostname of the gateway * * @param profileManager */ private void setUniqueProfileName(ProfileManager profileManager) { int i=0; String newname; try { newname = mGateway.getString("host"); while(profileManager.getProfileByName(newname)!=null) { i++; if(i==1) newname = getString(R.string.converted_profile); else newname = getString(R.string.converted_profile_i,i); } mVpnProfile.mName=newname; } catch (JSONException e) { // TODO Auto-generated catch block Log.v(TAG,"Couldn't read gateway name for profile creation!"); e.printStackTrace(); } } /** * Parses data from eip-service.json to a section of the openvpn config file */ private String configFromEipServiceDotJson() { String parsed_configuration = ""; String common_options = "openvpn_configuration"; String remote = "ip_address"; String ports = "ports"; String protos = "protocols"; String capabilities = "capabilities"; String location_key = "location"; String locations = "locations"; Vector arg = new Vector(); Vector> args = new Vector>(); try { JSONObject openvpn_configuration = eipDefinition.getJSONObject(common_options); Iterator keys = openvpn_configuration.keys(); Vector> value = new Vector>(); while ( keys.hasNext() ){ String key = keys.next().toString(); parsed_configuration += key + " "; for ( String word : openvpn_configuration.getString(key).split(" ") ) parsed_configuration += word + " "; parsed_configuration += System.getProperty("line.separator"); } } catch (JSONException e) { // TODO Auto-generated catch block e.printStackTrace(); } parsed_configuration += "client" + System.getProperty("line.separator"); try { JSONArray protocolsJSON = mGateway.getJSONObject(capabilities).getJSONArray(protos); String remote_line = "remote"; for ( int i=0; i>) args.clone() ); // arg.clear(); // args.clear(); return parsed_configuration; } private String caSecretFromSharedPreferences() { String secret_lines = ""; SharedPreferences preferences = context.getSharedPreferences(Dashboard.SHARED_PREFERENCES, context.MODE_PRIVATE); System.getProperty("line.separator"); secret_lines += ""; secret_lines += System.getProperty("line.separator"); secret_lines += preferences.getString(Provider.CA_CERT, ""); secret_lines += System.getProperty("line.separator"); secret_lines += ""; return secret_lines; } private String keySecretFromSharedPreferences() { String secret_lines = ""; SharedPreferences preferences = context.getSharedPreferences(Dashboard.SHARED_PREFERENCES, context.MODE_PRIVATE); secret_lines += System.getProperty("line.separator"); secret_lines +=""; secret_lines += System.getProperty("line.separator"); secret_lines += preferences.getString(EIP.PRIVATE_KEY, ""); secret_lines += System.getProperty("line.separator"); secret_lines += ""; secret_lines += System.getProperty("line.separator"); return secret_lines; } private String certSecretFromSharedPreferences() { String secret_lines = ""; SharedPreferences preferences = context.getSharedPreferences(Dashboard.SHARED_PREFERENCES, context.MODE_PRIVATE); secret_lines += System.getProperty("line.separator"); secret_lines +=""; secret_lines += System.getProperty("line.separator"); secret_lines += preferences.getString(EIP.CERTIFICATE, ""); secret_lines += System.getProperty("line.separator"); secret_lines += ""; secret_lines += System.getProperty("line.separator"); return secret_lines; } /** * Create and attach the VpnProfile to our gateway object */ protected void createVPNProfile(){ try { ConfigParser cp = new ConfigParser(); Log.d(TAG, configFromEipServiceDotJson()); Log.d(TAG, caSecretFromSharedPreferences()); Log.d(TAG, keySecretFromSharedPreferences()); Log.d(TAG, certSecretFromSharedPreferences()); cp.parseConfig(new StringReader(configFromEipServiceDotJson())); cp.parseConfig(new StringReader(caSecretFromSharedPreferences())); cp.parseConfig(new StringReader(keySecretFromSharedPreferences())); cp.parseConfig(new StringReader(certSecretFromSharedPreferences())); VpnProfile vp = cp.convertProfile(); //vp.mAuthenticationType=VpnProfile.TYPE_STATICKEYS; mVpnProfile = vp; Log.v(TAG,"Created VPNProfile"); } catch (ConfigParseError e) { // FIXME We didn't get a VpnProfile! Error handling! and log level Log.v(TAG,"Error creating VPNProfile"); e.printStackTrace(); } catch (IOException e) { // FIXME We didn't get a VpnProfile! Error handling! and log level Log.v(TAG,"Error creating VPNProfile"); e.printStackTrace(); } } } }