/**
* 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.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.openvpn.ConfigParser;
import se.leap.openvpn.ConfigParser.ConfigParseError;
import se.leap.openvpn.LaunchVPN;
import se.leap.openvpn.OpenVpnManagementThread;
import se.leap.openvpn.OpenVpnService;
import se.leap.openvpn.OpenVpnService.LocalBinder;
import se.leap.openvpn.ProfileManager;
import se.leap.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.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 .openvpn.OpenVpnService} connections.
*
* @author Sean Leonard
*/
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 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.RETRIEVE_SERVICE);
return bindService(bindIntent, mVpnServiceConn, BIND_AUTO_CREATE);
}
private static 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 = mVpnService.isRunning();
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;
if (mBound) {
resultCode = (mVpnService.isRunning()) ? 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 = false;
try {
running = mVpnService.isRunning();
} catch (NullPointerException e){
e.printStackTrace();
}
if (retrieved_vpn_service && running && mReceiver != null){
mReceiver.send(Activity.RESULT_OK, resultData);
}
else{
mReceiver.send(Activity.RESULT_CANCELED, resultData);
}
}
}
/**
* 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();
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(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
OpenVpnManagementThread.stopOpenVPN();
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);
Collection profiles = vpl.getProfiles();
for (VpnProfile profile : profiles){
vpl.removeProfile(context, 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.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) ){
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.parseOptions();
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();
}
}
/**
* FIXME This method is really the outline of the refactoring needed in se.leap.openvpn.ConfigParser
*/
private void parseOptions(){
// FIXME move these to a common API (& version) definition place, like ProviderAPI or ConfigHelper
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 def = (JSONObject) eipDefinition.get(common_options);
Iterator keys = def.keys();
Vector> value = new Vector>();
while ( keys.hasNext() ){
String key = keys.next().toString();
arg.add(key);
for ( String word : def.getString(key).split(" ") )
arg.add(word);
value.add( (Vector) arg.clone() );
options.put(key, (Vector>) value.clone());
value.clear();
arg.clear();
}
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
try {
arg.add(remote);
arg.add(mGateway.getString(remote));
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
args.add((Vector) arg.clone());
options.put("remote", (Vector>) args.clone() );
arg.clear();
args.clear();
try {
arg.add(location_key);
String locationText = "";
locationText = eipDefinition.getJSONObject(locations).getJSONObject(mGateway.getString(location_key)).getString("name");
arg.add(locationText);
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
args.add((Vector) arg.clone());
options.put("location", (Vector>) args.clone() );
arg.clear();
args.clear();
JSONArray protocolsJSON = null;
arg.add("proto");
try {
protocolsJSON = mGateway.getJSONObject(capabilities).getJSONArray(protos);
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Vector protocols = new Vector();
for ( int i=0; i) arg.clone());
options.put("proto", (Vector>) args.clone());
arg.clear();
args.clear();
String port = null;
arg.add("port");
try {
port = mGateway.getJSONObject(capabilities).getJSONArray(ports).optString(0);
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
arg.add(port);
args.add((Vector) arg.clone());
options.put("port", (Vector>) args.clone());
args.clear();
arg.clear();
}
/**
* Create and attach the VpnProfile to our gateway object
*/
protected void createVPNProfile(){
try {
ConfigParser cp = new ConfigParser();
cp.setDefinition(options);
VpnProfile vp = cp.convertProfile();
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 createing VPNProfile");
e.printStackTrace();
}
}
}
}