/**
* 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) arg.clone());
// options.put("location", (Vector>) 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();
}
}
}
}