/**
* 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.eip;
import android.app.IntentService;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.lang.ref.WeakReference;
import de.blinkt.openvpn.LaunchVPN;
import se.leap.bitmaskclient.OnBootReceiver;
import static android.app.Activity.RESULT_CANCELED;
import static android.app.Activity.RESULT_OK;
import static android.content.Intent.CATEGORY_DEFAULT;
import static se.leap.bitmaskclient.Constants.BROADCAST_EIP_EVENT;
import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_CODE;
import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_KEY;
import static se.leap.bitmaskclient.Constants.EIP_ACTION_CHECK_CERT_VALIDITY;
import static se.leap.bitmaskclient.Constants.EIP_ACTION_IS_RUNNING;
import static se.leap.bitmaskclient.Constants.EIP_ACTION_START;
import static se.leap.bitmaskclient.Constants.EIP_ACTION_START_ALWAYS_ON_VPN;
import static se.leap.bitmaskclient.Constants.EIP_ACTION_STOP;
import static se.leap.bitmaskclient.Constants.EIP_EARLY_ROUTES;
import static se.leap.bitmaskclient.Constants.EIP_RECEIVER;
import static se.leap.bitmaskclient.Constants.EIP_REQUEST;
import static se.leap.bitmaskclient.Constants.EIP_RESTART_ON_BOOT;
import static se.leap.bitmaskclient.Constants.PROVIDER_EIP_DEFINITION;
import static se.leap.bitmaskclient.Constants.PROVIDER_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES;
import static se.leap.bitmaskclient.MainActivityErrorDialog.DOWNLOAD_ERRORS.ERROR_INVALID_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.R.string.vpn_certificate_is_invalid;
/**
* 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 TAG = EIP.class.getSimpleName(),
SERVICE_API_PATH = "config/eip-service.json",
ERRORS = "errors",
ERROR_ID = "errorID";
private WeakReference mReceiverRef = new WeakReference<>(null);
private SharedPreferences preferences;
public EIP() {
super(TAG);
}
@Override
public void onCreate() {
super.onCreate();
preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
}
@Override
protected void onHandleIntent(Intent intent) {
String action = intent.getAction();
if (intent.getParcelableExtra(EIP_RECEIVER) != null) {
mReceiverRef = new WeakReference<>((ResultReceiver) intent.getParcelableExtra(EIP_RECEIVER));
}
if (action == null) {
return;
}
switch (action) {
case EIP_ACTION_START:
boolean earlyRoutes = intent.getBooleanExtra(EIP_EARLY_ROUTES, true);
startEIP(earlyRoutes);
break;
case EIP_ACTION_START_ALWAYS_ON_VPN:
startEIPAlwaysOnVpn();
break;
case EIP_ACTION_STOP:
stopEIP();
break;
case EIP_ACTION_IS_RUNNING:
isRunning();
break;
case EIP_ACTION_CHECK_CERT_VALIDITY:
checkVPNCertificateValidity();
break;
}
}
/**
* Initiates an EIP connection by selecting a gateway and preparing and sending an
* Intent to {@link de.blinkt.openvpn.LaunchVPN}.
* It also sets up early routes.
*/
private void startEIP(boolean earlyRoutes) {
if (!EipStatus.getInstance().isBlockingVpnEstablished() && earlyRoutes) {
earlyRoutes();
}
Bundle result = new Bundle();
if (!preferences.getBoolean(EIP_RESTART_ON_BOOT, false)){
preferences.edit().putBoolean(EIP_RESTART_ON_BOOT, true).commit();
}
GatewaysManager gatewaysManager = gatewaysFromPreferences();
if (!isVPNCertificateValid()){
setErrorResult(result, vpn_certificate_is_invalid, ERROR_INVALID_VPN_CERTIFICATE.toString());
tellToReceiverOrBroadcast(EIP_ACTION_START, RESULT_CANCELED, result);
return;
}
Gateway gateway = gatewaysManager.select();
if (gateway != null && gateway.getProfile() != null) {
launchActiveGateway(gateway);
tellToReceiverOrBroadcast(EIP_ACTION_START, RESULT_OK);
} else
tellToReceiverOrBroadcast(EIP_ACTION_START, RESULT_CANCELED);
}
/**
* Tries to start the last used vpn profile when the OS was rebooted and always-on-VPN is enabled.
* The {@link OnBootReceiver} will care if there is no profile.
*/
private void startEIPAlwaysOnVpn() {
Log.d(TAG, "startEIPAlwaysOnVpn vpn");
GatewaysManager gatewaysManager = gatewaysFromPreferences();
Gateway gateway = gatewaysManager.select();
if (gateway != null && gateway.getProfile() != null) {
Log.d(TAG, "startEIPAlwaysOnVpn eip launch avtive gateway vpn");
launchActiveGateway(gateway);
} else {
Log.d(TAG, "startEIPAlwaysOnVpn no active profile available!");
}
}
/**
* Early routes are routes that block traffic until a new
* VpnService is started properly.
*/
private void earlyRoutes() {
Intent voidVpnLauncher = new Intent(getApplicationContext(), VoidVpnLauncher.class);
voidVpnLauncher.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(voidVpnLauncher);
}
private void launchActiveGateway(Gateway gateway) {
Intent intent = new Intent(this, LaunchVPN.class);
intent.setAction(Intent.ACTION_MAIN);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(LaunchVPN.EXTRA_HIDELOG, true);
intent.putExtra(LaunchVPN.EXTRA_TEMP_VPN_PROFILE, gateway.getProfile());
startActivity(intent);
}
private void stopEIP() {
// TODO stop eip from here if possible...
// TODO then refactor EipFragment.handleSwitchOff
EipStatus eipStatus = EipStatus.getInstance();
int resultCode = RESULT_CANCELED;
if (eipStatus.isConnected() || eipStatus.isConnecting())
resultCode = RESULT_OK;
tellToReceiverOrBroadcast(EIP_ACTION_STOP, resultCode);
}
/**
* Checks the last stored status notified by ics-openvpn
* Sends Activity.RESULT_CANCELED
to the ResultReceiver that made the
* request if it's not connected, Activity.RESULT_OK
otherwise.
*/
private void isRunning() {
EipStatus eipStatus = EipStatus.getInstance();
int resultCode = (eipStatus.isConnected()) ?
RESULT_OK :
RESULT_CANCELED;
tellToReceiverOrBroadcast(EIP_ACTION_IS_RUNNING, resultCode);
}
private JSONObject eipDefinitionFromPreferences() {
JSONObject result = new JSONObject();
try {
String eipDefinitionString = preferences.getString(PROVIDER_EIP_DEFINITION, "");
if (!eipDefinitionString.isEmpty()) {
result = new JSONObject(eipDefinitionString);
}
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return result;
}
private GatewaysManager gatewaysFromPreferences() {
GatewaysManager gatewaysManager = new GatewaysManager(this, preferences);
//TODO: THIS IS A QUICK FIX - it deletes all profiles in ProfileManager, thus it's possible
// to add all gateways from prefs without duplicates, but this should be refactored.
gatewaysManager.clearGatewaysAndProfiles();
gatewaysManager.fromEipServiceJson(eipDefinitionFromPreferences());
return gatewaysManager;
}
private void checkVPNCertificateValidity() {
int resultCode = isVPNCertificateValid() ?
RESULT_OK :
RESULT_CANCELED;
tellToReceiverOrBroadcast(EIP_ACTION_CHECK_CERT_VALIDITY, resultCode);
}
private boolean isVPNCertificateValid() {
VpnCertificateValidator validator = new VpnCertificateValidator(preferences.getString(PROVIDER_VPN_CERTIFICATE, ""));
return validator.isValid();
}
private void tellToReceiverOrBroadcast(String action, int resultCode, Bundle resultData) {
resultData.putString(EIP_REQUEST, action);
if (mReceiverRef.get() != null) {
mReceiverRef.get().send(resultCode, resultData);
} else {
broadcastEvent(resultCode, resultData);
}
}
private void tellToReceiverOrBroadcast(String action, int resultCode) {
tellToReceiverOrBroadcast(action, resultCode, new Bundle());
}
private void broadcastEvent(int resultCode , Bundle resultData) {
Intent intentUpdate = new Intent(BROADCAST_EIP_EVENT);
intentUpdate.addCategory(CATEGORY_DEFAULT);
intentUpdate.putExtra(BROADCAST_RESULT_CODE, resultCode);
intentUpdate.putExtra(BROADCAST_RESULT_KEY, resultData);
Log.d(TAG, "sending broadcast");
LocalBroadcastManager.getInstance(this).sendBroadcast(intentUpdate);
}
Bundle setErrorResult(Bundle result, int errorMessageId, String errorId) {
JSONObject errorJson = new JSONObject();
addErrorMessageToJson(errorJson, getResources().getString(errorMessageId), errorId);
result.putString(ERRORS, errorJson.toString());
result.putBoolean(BROADCAST_RESULT_KEY, false);
return result;
}
private void addErrorMessageToJson(JSONObject jsonObject, String errorMessage, String errorId) {
try {
jsonObject.put(ERRORS, errorMessage);
jsonObject.put(ERROR_ID, errorId);
} catch (JSONException e) {
e.printStackTrace();
}
}
}