diff options
author | cyBerta <cyberta@riseup.net> | 2019-05-24 18:01:03 +0200 |
---|---|---|
committer | cyBerta <cyberta@riseup.net> | 2019-08-02 01:49:37 +0200 |
commit | db1e1a2045a2e6456d54765be3cf95186ce987f7 (patch) | |
tree | 0fc04949eba47e99d7fe7f711fb00bf1c16e3e0a /app/src/main/java/se/leap | |
parent | 8ffbb96d908fdc5a17255ec3fbdc807f663ade38 (diff) |
squashed commit for Pluggable Transports
* implement handling of different provider API version (v1 and v2)
* detect provider's obfs support
* shapeshifter-dispatcher installation
* necessary changes to control shapeshifter-dispatcher from Bitmask
* route openvpn traffic over shapeshifter-dispatcher
Diffstat (limited to 'app/src/main/java/se/leap')
8 files changed, 754 insertions, 75 deletions
diff --git a/app/src/main/java/se/leap/bitmaskclient/Constants.java b/app/src/main/java/se/leap/bitmaskclient/Constants.java index 42df6d1d..7503d29f 100644 --- a/app/src/main/java/se/leap/bitmaskclient/Constants.java +++ b/app/src/main/java/se/leap/bitmaskclient/Constants.java @@ -113,4 +113,22 @@ public interface Constants { String FIRST_TIME_USER_DATE = "first_time_user_date"; + ////////////////////////////////////////////// + // JSON KEYS + ///////////////////////////////////////////// + String IP_ADDRESS = "ip_address"; + String REMOTE = "remote"; + String PORTS = "ports"; + String PROTOCOLS = "protocols"; + String CAPABILITIES = "capabilities"; + String TRANSPORT = "transport"; + String TYPE = "type"; + String OPTIONS = "options"; + String VERSION = "version"; + String NAME = "name"; + String TIMEZONE = "timezone"; + String LOCATIONS = "locations"; + String LOCATION = "location"; + String OPENVPN_CONFIGURATION = "openvpn_configuration"; + String GATEWAYS = "gateways"; } diff --git a/app/src/main/java/se/leap/bitmaskclient/StartActivity.java b/app/src/main/java/se/leap/bitmaskclient/StartActivity.java index d8aca351..945429fd 100644 --- a/app/src/main/java/se/leap/bitmaskclient/StartActivity.java +++ b/app/src/main/java/se/leap/bitmaskclient/StartActivity.java @@ -25,20 +25,36 @@ import android.support.annotation.IntDef; import android.support.annotation.Nullable; import android.util.Log; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import de.blinkt.openvpn.core.Preferences; import de.blinkt.openvpn.core.VpnStatus; import se.leap.bitmaskclient.eip.EipCommand; +import se.leap.bitmaskclient.utils.PreferenceHelper; +import static se.leap.bitmaskclient.BuildConfig.useDemoConfig; import static se.leap.bitmaskclient.Constants.APP_ACTION_CONFIGURE_ALWAYS_ON_PROFILE; import static se.leap.bitmaskclient.Constants.EIP_RESTART_ON_BOOT; import static se.leap.bitmaskclient.Constants.PREFERENCES_APP_VERSION; +import static se.leap.bitmaskclient.Constants.PROVIDER_CONFIGURED; import static se.leap.bitmaskclient.Constants.PROVIDER_EIP_DEFINITION; import static se.leap.bitmaskclient.Constants.PROVIDER_KEY; +import static se.leap.bitmaskclient.Constants.PROVIDER_PRIVATE_KEY; +import static se.leap.bitmaskclient.Constants.PROVIDER_VPN_CERTIFICATE; import static se.leap.bitmaskclient.Constants.REQUEST_CODE_CONFIGURE_LEAP; import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES; import static se.leap.bitmaskclient.MainActivity.ACTION_SHOW_VPN_FRAGMENT; +import static se.leap.bitmaskclient.Provider.CA_CERT; +import static se.leap.bitmaskclient.Provider.MAIN_URL; import static se.leap.bitmaskclient.utils.ConfigHelper.isDefaultBitmask; import static se.leap.bitmaskclient.utils.PreferenceHelper.getSavedProviderFromSharedPreferences; import static se.leap.bitmaskclient.utils.PreferenceHelper.providerInSharedPreferences; @@ -90,6 +106,10 @@ public class StartActivity extends Activity{ // initialize app necessities VpnStatus.initLogCache(getApplicationContext().getCacheDir()); + if (useDemoConfig) { + demoSetup(); + } + prepareEIP(); } @@ -162,8 +182,8 @@ public class StartActivity extends Activity{ } private void prepareEIP() { - boolean provider_exists = providerInSharedPreferences(preferences); - if (provider_exists) { + boolean providerExists = providerInSharedPreferences(preferences); + if (providerExists) { Provider provider = getSavedProviderFromSharedPreferences(preferences); if(!provider.isConfigured()) { configureLeapProvider(); @@ -216,4 +236,65 @@ public class StartActivity extends Activity{ finish(); } + private String getInputAsString(InputStream fileAsInputStream) throws IOException { + BufferedReader br = new BufferedReader(new InputStreamReader(fileAsInputStream)); + StringBuilder sb = new StringBuilder(); + String line = br.readLine(); + while (line != null) { + sb.append(line); + line = br.readLine(); + } + + return sb.toString(); + } + + private void demoSetup() { + try { + //set demo data + String demoEipServiceJson = getInputAsString(getAssets().open("ptdemo.bitmask.eip-service.json")); + String secrets = getInputAsString(getAssets().open("ptdemo.bitmask.secrets.json")); + String provider = getInputAsString(getAssets().open("ptdemo.bitmask.net.json")); + + Log.d(TAG, "setup provider: " + provider); + Log.d(TAG, "setup eip json: " + demoEipServiceJson); + JSONObject secretsJson = new JSONObject(secrets); + + preferences.edit().putString(PROVIDER_EIP_DEFINITION+".demo.bitmask.net", demoEipServiceJson). + putString(PROVIDER_EIP_DEFINITION, demoEipServiceJson). + putString(CA_CERT, secretsJson.getString(CA_CERT)). + putString(PROVIDER_PRIVATE_KEY, secretsJson.getString(PROVIDER_PRIVATE_KEY)). + putString(PROVIDER_VPN_CERTIFICATE, secretsJson.getString(PROVIDER_VPN_CERTIFICATE)). + putString(Provider.KEY, provider). + putString(MAIN_URL, "https://demo.bitmask.net"). + putBoolean(PROVIDER_CONFIGURED, true).commit(); + + PreferenceHelper.getSavedProviderFromSharedPreferences(preferences); + ProviderObservable.getInstance().updateProvider(PreferenceHelper.getSavedProviderFromSharedPreferences(preferences)); + + // remove last used profiles + SharedPreferences prefs = Preferences.getDefaultSharedPreferences(this); + SharedPreferences.Editor prefsedit = prefs.edit(); + prefsedit.remove("lastConnectedProfile").commit(); + File f = new File(this.getCacheDir().getAbsolutePath() + "/android.conf"); + if (f.exists()) { + Log.d(TAG, "android.conf exists -> delete:" + f.delete()); + } + + File filesDirectory = new File(this.getFilesDir().getAbsolutePath()); + if (filesDirectory.exists() && filesDirectory.isDirectory()) { + File[] filesInDirectory = filesDirectory.listFiles(); + for (File file : filesInDirectory) { + Log.d(TAG, "delete profile: " + file.getName() + ": "+ file.delete()); + + } + } else Log.d(TAG, "file folder doesn't exist"); + + } catch (IOException e) { + e.printStackTrace(); + } catch (JSONException e) { + e.printStackTrace(); + } + + } + } diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java b/app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java index 55ade1ae..b1554af0 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java @@ -22,11 +22,17 @@ import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; -import java.io.StringReader; import de.blinkt.openvpn.VpnProfile; import de.blinkt.openvpn.core.ConfigParser; +import static se.leap.bitmaskclient.Constants.LOCATION; +import static se.leap.bitmaskclient.Constants.LOCATIONS; +import static se.leap.bitmaskclient.Constants.NAME; +import static se.leap.bitmaskclient.Constants.OPENVPN_CONFIGURATION; +import static se.leap.bitmaskclient.Constants.TIMEZONE; +import static se.leap.bitmaskclient.Constants.VERSION; + /** * Gateway provides objects defining gateways and their metadata. * Each instance contains a VpnProfile for OpenVPN specific data and member @@ -34,6 +40,7 @@ import de.blinkt.openvpn.core.ConfigParser; * * @author Sean Leonard <meanderingcode@aetherislands.net> * @author Parménides GV <parmegv@sdf.org> + * @author cyberta */ public class Gateway { @@ -44,50 +51,57 @@ public class Gateway { private JSONObject secrets; private JSONObject gateway; - private String mName; + private String name; private int timezone; - private VpnProfile mVpnProfile; + private int apiVersion; + private VpnProfile vpnProfile; /** * Build a gateway object from a JSON OpenVPN gateway definition in eip-service.json * and create a VpnProfile belonging to it. */ - public Gateway(JSONObject eip_definition, JSONObject secrets, JSONObject gateway) { + public Gateway(JSONObject eipDefinition, JSONObject secrets, JSONObject gateway) { this.gateway = gateway; this.secrets = secrets; - generalConfiguration = getGeneralConfiguration(eip_definition); - timezone = getTimezone(eip_definition); - mName = locationAsName(eip_definition); - - mVpnProfile = createVPNProfile(); - mVpnProfile.mName = mName; + generalConfiguration = getGeneralConfiguration(eipDefinition); + timezone = getTimezone(eipDefinition); + name = locationAsName(eipDefinition); + apiVersion = getApiVersion(eipDefinition); + vpnProfile = createVPNProfile(); + if (vpnProfile != null) { + vpnProfile.mName = name; + } } - private JSONObject getGeneralConfiguration(JSONObject eip_definition) { + private JSONObject getGeneralConfiguration(JSONObject eipDefinition) { try { - return eip_definition.getJSONObject("openvpn_configuration"); + return eipDefinition.getJSONObject(OPENVPN_CONFIGURATION); } catch (JSONException e) { return new JSONObject(); } } - private int getTimezone(JSONObject eip_definition) { - JSONObject location = getLocationInfo(eip_definition); - return location.optInt("timezone"); + private int getTimezone(JSONObject eipDefinition) { + JSONObject location = getLocationInfo(eipDefinition); + return location.optInt(TIMEZONE); + } + + private int getApiVersion(JSONObject eipDefinition) { + return eipDefinition.optInt(VERSION); } - private String locationAsName(JSONObject eip_definition) { - JSONObject location = getLocationInfo(eip_definition); - return location.optString("name"); + private String locationAsName(JSONObject eipDefinition) { + JSONObject location = getLocationInfo(eipDefinition); + return location.optString(NAME); } private JSONObject getLocationInfo(JSONObject eipDefinition) { try { - JSONObject locations = eipDefinition.getJSONObject("locations"); + JSONObject locations = eipDefinition.getJSONObject(LOCATIONS); - return locations.getJSONObject(gateway.getString("location")); + return locations.getJSONObject(gateway.getString(LOCATION)); } catch (JSONException e) { return new JSONObject(); } @@ -98,18 +112,9 @@ public class Gateway { */ private VpnProfile createVPNProfile() { try { - ConfigParser cp = new ConfigParser(); - - VpnConfigGenerator vpnConfigurationGenerator = new VpnConfigGenerator(generalConfiguration, secrets, gateway); - String configuration = vpnConfigurationGenerator.generate(); - - cp.parseConfig(new StringReader(configuration)); - return cp.convertProfile(); - } catch (ConfigParser.ConfigParseError e) { - // FIXME We didn't get a VpnProfile! Error handling! and log level - e.printStackTrace(); - return null; - } catch (IOException e) { + VpnConfigGenerator vpnConfigurationGenerator = new VpnConfigGenerator(generalConfiguration, secrets, gateway, apiVersion); + return vpnConfigurationGenerator.generateVpnProfile(); + } catch (ConfigParser.ConfigParseError | IOException | CloneNotSupportedException | JSONException e) { // FIXME We didn't get a VpnProfile! Error handling! and log level e.printStackTrace(); return null; @@ -117,11 +122,11 @@ public class Gateway { } public String getName() { - return mName; + return name; } public VpnProfile getProfile() { - return mVpnProfile; + return vpnProfile; } public int getTimezone() { diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaySelector.java b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaySelector.java index 2bd666bf..0ba0f207 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaySelector.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaySelector.java @@ -36,7 +36,7 @@ public class GatewaySelector { } } - Log.e(TAG, "There are less than " + nClosest + " Gateways available."); + Log.e(TAG, "There are less than " + (nClosest + 1) + " Gateways available."); return null; } diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java index 060843fd..c650938c 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java @@ -30,11 +30,18 @@ import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.core.connection.Connection; import se.leap.bitmaskclient.Provider; import se.leap.bitmaskclient.utils.PreferenceHelper; +import static se.leap.bitmaskclient.Constants.CAPABILITIES; +import static se.leap.bitmaskclient.Constants.GATEWAYS; import static se.leap.bitmaskclient.Constants.PROVIDER_PRIVATE_KEY; import static se.leap.bitmaskclient.Constants.PROVIDER_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.Constants.TRANSPORT; +import static se.leap.bitmaskclient.Constants.TYPE; +import static se.leap.bitmaskclient.Constants.VERSION; /** * @author parmegv @@ -88,10 +95,11 @@ public class GatewaysManager { */ void fromEipServiceJson(JSONObject eipDefinition) { try { - JSONArray gatewaysDefined = eipDefinition.getJSONArray("gateways"); + JSONArray gatewaysDefined = eipDefinition.getJSONArray(GATEWAYS); + int apiVersion = eipDefinition.getInt(VERSION); for (int i = 0; i < gatewaysDefined.length(); i++) { JSONObject gw = gatewaysDefined.getJSONObject(i); - if (isOpenVpnGateway(gw)) { + if (isOpenVpnGateway(gw, apiVersion)) { JSONObject secrets = secretsConfiguration(); Gateway aux = new Gateway(eipDefinition, secrets, gw); if (!gateways.contains(aux)) { @@ -110,12 +118,29 @@ public class GatewaysManager { * @param gateway to check * @return true if gateway is an OpenVpn gateway otherwise false */ - private boolean isOpenVpnGateway(JSONObject gateway) { - try { - String transport = gateway.getJSONObject("capabilities").getJSONArray("transport").toString(); - return transport.contains("openvpn"); - } catch (JSONException e) { - return false; + private boolean isOpenVpnGateway(JSONObject gateway, int apiVersion) { + switch (apiVersion) { + default: + case 1: + try { + String transport = gateway.getJSONObject(CAPABILITIES).getJSONArray(TRANSPORT).toString(); + return transport.contains("openvpn"); + } catch (JSONException e) { + return false; + } + case 2: + try { + JSONArray transports = gateway.getJSONObject(CAPABILITIES).getJSONArray(TRANSPORT); + for (int i = 0; i < transports.length(); i++) { + JSONObject transport = transports.getJSONObject(i); + if (transport.optString(TYPE).equals("openvpn")) { + return true; + } + } + return false; + } catch (JSONException e) { + return false; + } } } diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java b/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java index 6f0ccf18..7f09d21e 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java @@ -20,48 +20,133 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import java.io.IOException; +import java.io.StringReader; import java.util.Iterator; +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.core.ConfigParser; +import de.blinkt.openvpn.core.connection.Connection; +import de.blinkt.openvpn.core.connection.Obfs4Connection; import se.leap.bitmaskclient.Provider; +import static se.leap.bitmaskclient.Constants.CAPABILITIES; +import static se.leap.bitmaskclient.Constants.IP_ADDRESS; +import static se.leap.bitmaskclient.Constants.OPTIONS; +import static se.leap.bitmaskclient.Constants.PORTS; +import static se.leap.bitmaskclient.Constants.PROTOCOLS; import static se.leap.bitmaskclient.Constants.PROVIDER_PRIVATE_KEY; import static se.leap.bitmaskclient.Constants.PROVIDER_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.Constants.REMOTE; +import static se.leap.bitmaskclient.Constants.TRANSPORT; +import static se.leap.bitmaskclient.Constants.TYPE; public class VpnConfigGenerator { - private JSONObject general_configuration; + private JSONObject generalConfiguration; private JSONObject gateway; private JSONObject secrets; + private JSONObject obfs4Transport; + private int apiVersion; + + private ConfigParser icsOpenvpnConfigParser = new ConfigParser(); + public final static String TAG = VpnConfigGenerator.class.getSimpleName(); private final String newLine = System.getProperty("line.separator"); // Platform new line - public VpnConfigGenerator(JSONObject general_configuration, JSONObject secrets, JSONObject gateway) { - this.general_configuration = general_configuration; + public VpnConfigGenerator(JSONObject generalConfiguration, JSONObject secrets, JSONObject gateway, int apiVersion) { + this.generalConfiguration = generalConfiguration; this.gateway = gateway; this.secrets = secrets; + this.apiVersion = apiVersion; + checkCapabilities(); } - public String generate() { - return - generalConfiguration() + public void checkCapabilities() { + + try { + switch (apiVersion) { + case 2: + JSONArray supportedTransports = gateway.getJSONObject(CAPABILITIES).getJSONArray(TRANSPORT); + for (int i = 0; i < supportedTransports.length(); i++) { + JSONObject transport = supportedTransports.getJSONObject(i); + if (transport.getString(TYPE).equals("obfs4")) { + obfs4Transport = transport; + } + } + break; + default: + break; + } + + } catch (JSONException e) { + e.printStackTrace(); + } + } + + public VpnProfile generateVpnProfile() throws IllegalStateException, + IOException, + ConfigParser.ConfigParseError, + CloneNotSupportedException, + JSONException, + NumberFormatException { + + VpnProfile profile = createOvpnProfile(); + if (supportsObfs4()) { + addPluggableTransportConnections(profile); + } + return profile; + } + + private boolean supportsObfs4(){ + return obfs4Transport != null; + } + + private void addPluggableTransportConnections(VpnProfile profile) throws JSONException, CloneNotSupportedException { + JSONArray ports = obfs4Transport.getJSONArray(PORTS); + Connection[] updatedConnections = new Connection[profile.mConnections.length + ports.length()]; + + for (int i = 0; i < ports.length(); i++) { + String port = ports.getString(i); + Obfs4Connection obfs4Connection = new Obfs4Connection(); + obfs4Connection.setObfs4RemoteProxyName(gateway.getString(IP_ADDRESS)); + obfs4Connection.setObfs4RemoteProxyPort(port); + obfs4Connection.setTransportOptions(obfs4Transport.optJSONObject(OPTIONS)); + updatedConnections[i] = obfs4Connection; + } + int k = 0; + for (int i = ports.length(); i < updatedConnections.length; i++, k++) { + updatedConnections[i] = profile.mConnections[k].clone(); + } + profile.mConnections = updatedConnections; + } + + private String getConfigurationString() { + return generalConfiguration() + newLine - + gatewayConfiguration() + + ovpnGatewayConfiguration() + newLine + secretsConfiguration() + newLine + androidCustomizations(); } + private VpnProfile createOvpnProfile() throws IOException, ConfigParser.ConfigParseError { + String configuration = getConfigurationString(); + icsOpenvpnConfigParser.parseConfig(new StringReader(configuration)); + return icsOpenvpnConfigParser.convertProfile(); + } + private String generalConfiguration() { String commonOptions = ""; try { - Iterator keys = general_configuration.keys(); + Iterator keys = generalConfiguration.keys(); while (keys.hasNext()) { String key = keys.next().toString(); commonOptions += key + " "; - for (String word : String.valueOf(general_configuration.get(key)).split(" ")) + for (String word : String.valueOf(generalConfiguration.get(key)).split(" ")) commonOptions += word + " "; commonOptions += newLine; @@ -76,41 +161,73 @@ public class VpnConfigGenerator { return commonOptions; } - private String gatewayConfiguration() { + private String ovpnGatewayConfiguration() { String remotes = ""; - String ipAddressKeyword = "ip_address"; - String remoteKeyword = "remote"; - String portsKeyword = "ports"; - String protocolKeyword = "protocols"; - String capabilitiesKeyword = "capabilities"; - + StringBuilder stringBuilder = new StringBuilder(); try { - String ip_address = gateway.getString(ipAddressKeyword); - JSONObject capabilities = gateway.getJSONObject(capabilitiesKeyword); - JSONArray ports = capabilities.getJSONArray(portsKeyword); - for (int i = 0; i < ports.length(); i++) { - String port_specific_remotes = ""; - int port = ports.getInt(i); - JSONArray protocols = capabilities.getJSONArray(protocolKeyword); - for (int j = 0; j < protocols.length(); j++) { - String protocol = protocols.optString(j); - String new_remote = remoteKeyword + " " + ip_address + " " + port + " " + protocol + newLine; - - port_specific_remotes += new_remote; - } - remotes += port_specific_remotes; + String ipAddress = gateway.getString(IP_ADDRESS); + JSONObject capabilities = gateway.getJSONObject(CAPABILITIES); + JSONArray transports = capabilities.getJSONArray(TRANSPORT); + switch (apiVersion) { + default: + case 1: + ovpnGatewayConfigApiv1(stringBuilder, ipAddress, capabilities); + break; + case 2: + ovpnGatewayConfigApiv2(stringBuilder, ipAddress, transports); + break; } } catch (JSONException e) { // TODO Auto-generated catch block e.printStackTrace(); } + + remotes = stringBuilder.toString(); if (remotes.endsWith(newLine)) { remotes = remotes.substring(0, remotes.lastIndexOf(newLine)); } return remotes; } + private void ovpnGatewayConfigApiv1(StringBuilder stringBuilder, String ipAddress, JSONObject capabilities) throws JSONException { + int port; + String protocol; + + JSONArray ports = capabilities.getJSONArray(PORTS); + for (int i = 0; i < ports.length(); i++) { + port = ports.getInt(i); + JSONArray protocols = capabilities.getJSONArray(PROTOCOLS); + for (int j = 0; j < protocols.length(); j++) { + protocol = protocols.optString(j); + String newRemote = REMOTE + " " + ipAddress + " " + port + " " + protocol + newLine; + stringBuilder.append(newRemote); + } + } + } + + private void ovpnGatewayConfigApiv2(StringBuilder stringBuilder, String ipAddress, JSONArray transports) throws JSONException { + String port; + String protocol; + for (int i = 0; i < transports.length(); i++) { + JSONObject transport = transports.getJSONObject(i); + if (!transport.getString(TYPE).equals("openvpn")) { + continue; + } + JSONArray ports = transport.getJSONArray(PORTS); + for (int j = 0; j < ports.length(); j++) { + port = ports.getString(j); + JSONArray protocols = transport.getJSONArray(PROTOCOLS); + for (int k = 0; k < protocols.length(); k++) { + protocol = protocols.optString(k); + String newRemote = REMOTE + " " + ipAddress + " " + port + " " + protocol + newLine; + stringBuilder.append(newRemote); + } + } + } + } + + private String secretsConfiguration() { try { String ca = diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/BinaryInstaller.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/BinaryInstaller.java new file mode 100644 index 00000000..0d6aa61e --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/BinaryInstaller.java @@ -0,0 +1,204 @@ +/* Copyright (c) 2009, Nathan Freitas, Orbot / The Guardian Project - http://openideals.com/guardian */ +/* See LICENSE for licensing information */ + +package se.leap.bitmaskclient.pluggableTransports; + +import android.content.Context; +import android.util.Log; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.TimeoutException; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public class BinaryInstaller { + + File installFolder; + Context context; + + public BinaryInstaller(Context context, File installFolder) + { + this.installFolder = installFolder; + + this.context = context; + } + + public void deleteDirectory(File file) { + if( file.exists() ) { + if (file.isDirectory()) { + File[] files = file.listFiles(); + for(int i=0; i<files.length; i++) { + if(files[i].isDirectory()) { + deleteDirectory(files[i]); + } + else { + files[i].delete(); + } + } + } + + file.delete(); + } + } + + private final static String COMMAND_RM_FORCE = "rm -f "; + private final static String MP3_EXT = ".mp3"; + // + /* + * Extract the resources from the APK file using ZIP + */ + public File installResource (String basePath, String assetKey, boolean overwrite) throws IOException, FileNotFoundException, TimeoutException + { + + InputStream is; + File outFile; + + outFile = new File(installFolder, assetKey); + + if (outFile.exists() && (!overwrite)) { + Log.d("BINARY_INSTALLER", "Binary already exists! Using " + outFile.getCanonicalPath()); + return outFile; + } + + deleteDirectory(installFolder); + installFolder.mkdirs(); + + Log.d("BINARY_INSTALLER", "Search asset in " + basePath + "/" + assetKey); + + is = context.getAssets().open(basePath + '/' + assetKey); + streamToFile(is,outFile, false, false); + setExecutable(outFile); + + Log.d("BINARY_INSTALLER", "Asset copied from " + basePath + "/" + assetKey + " to: " + outFile.getCanonicalPath()); + + return outFile; + } + + + private final static int FILE_WRITE_BUFFER_SIZE = 1024*8; + /* + * Write the inputstream contents to the file + */ + public static boolean streamToFile(InputStream stm, File outFile, boolean append, boolean zip) throws IOException + + { + byte[] buffer = new byte[FILE_WRITE_BUFFER_SIZE]; + + int bytecount; + + OutputStream stmOut = new FileOutputStream(outFile.getAbsolutePath(), append); + ZipInputStream zis = null; + + if (zip) + { + zis = new ZipInputStream(stm); + ZipEntry ze = zis.getNextEntry(); + stm = zis; + + } + + while ((bytecount = stm.read(buffer)) > 0) + { + + stmOut.write(buffer, 0, bytecount); + + } + + stmOut.close(); + stm.close(); + + if (zis != null) + zis.close(); + + + return true; + + } + + //copy the file from inputstream to File output - alternative impl + public static boolean copyFile (InputStream is, File outputFile) + { + + try { + if (outputFile.exists()) + outputFile.delete(); + + boolean newFile = outputFile.createNewFile(); + DataOutputStream out = new DataOutputStream(new FileOutputStream(outputFile)); + DataInputStream in = new DataInputStream(is); + + int b = -1; + byte[] data = new byte[1024]; + + while ((b = in.read(data)) != -1) { + out.write(data); + } + + if (b == -1); //rejoice + + // + out.flush(); + out.close(); + in.close(); + // chmod? + + return newFile; + + + } catch (IOException ex) { + Log.e("Binaryinstaller", "error copying binary", ex); + return false; + } + + } + + /** + * Copies a raw resource file, given its ID to the given location + * @param ctx context + * @param resid resource id + * @param file destination file + * @param mode file permissions (E.g.: "755") + * @throws IOException on error + * @throws InterruptedException when interrupted + */ + public static void copyRawFile(Context ctx, int resid, File file, String mode, boolean isZipd) throws IOException, InterruptedException + { + final String abspath = file.getAbsolutePath(); + // Write the iptables binary + final FileOutputStream out = new FileOutputStream(file); + InputStream is = ctx.getResources().openRawResource(resid); + + if (isZipd) + { + ZipInputStream zis = new ZipInputStream(is); + ZipEntry ze = zis.getNextEntry(); + is = zis; + } + + byte buf[] = new byte[1024]; + int len; + while ((len = is.read(buf)) > 0) { + out.write(buf, 0, len); + } + out.close(); + is.close(); + // Change the permissions + Runtime.getRuntime().exec("chmod "+mode+" "+abspath).waitFor(); + } + + + private void setExecutable(File fileBin) { + fileBin.setReadable(true); + fileBin.setExecutable(true); + fileBin.setWritable(false); + fileBin.setWritable(true, true); + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Dispatcher.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Dispatcher.java new file mode 100644 index 00000000..ac846fd9 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Dispatcher.java @@ -0,0 +1,229 @@ +/** + * Copyright (c) 2019 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 <http://www.gnu.org/licenses/>. + */ +package se.leap.bitmaskclient.pluggableTransports; + +import android.content.Context; +import android.support.annotation.WorkerThread; +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.util.StringTokenizer; + + +/** + * Created by cyberta on 22.02.19. + */ + +public class Dispatcher { + private static final String ASSET_KEY = "piedispatcher"; + private static final String TAG = Dispatcher.class.getName(); + private final String remoteIP; + private final String remotePort; + private final String certificate; + private final String iatMode; + private File fileDispatcher; + private Context context; + private String port = ""; + private Thread dispatcherThread = null; + private int dipatcherPid = -1; + + public Dispatcher(Context context, String remoteIP, String remotePort, String certificate, String iatMode) { + this.context = context.getApplicationContext(); + this.remoteIP = remoteIP; + this.remotePort = remotePort; + this.certificate = certificate; + this.iatMode = iatMode; + } + + @WorkerThread + public void initSync() { + try { + fileDispatcher = installDispatcher(); + + // start dispatcher + dispatcherThread = new Thread(() -> { + try { + StringBuilder dispatcherLog = new StringBuilder(); + String dispatcherCommand = fileDispatcher.getCanonicalPath() + + " -transparent" + + " -client" + + " -state " + context.getFilesDir().getCanonicalPath() + "/state" + + " -target " + remoteIP + ":" + remotePort + + " -transports obfs4" + + " -options \"" + String.format("{\\\"cert\\\": \\\"%s\\\", \\\"iatMode\\\": \\\"%s\\\"}\"", certificate, iatMode) + + " -logLevel DEBUG -enableLogging"; + + runBlockingCmd(new String[]{dispatcherCommand}, dispatcherLog); + } catch (IOException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } + }); + dispatcherThread.start(); + + // get pid of dispatcher + StringBuilder log = new StringBuilder(); + String pidCommand = "ps | grep " + fileDispatcher.getCanonicalPath(); + runBlockingCmd(new String[]{pidCommand}, log); + String output = log.toString(); + StringTokenizer st = new StringTokenizer(output, " "); + st.nextToken(); // proc owner + dipatcherPid = Integer.parseInt(st.nextToken().trim()); + + // get open port of dispatcher + String getPortCommand = "cat " + context.getFilesDir().getCanonicalPath() + "/state/dispatcher.log | grep \"obfs4 - registered listener\""; + long timeout = System.currentTimeMillis() + 5000; + int i = 1; + while (this.port.length() == 0 && System.currentTimeMillis() < timeout) { + log = new StringBuilder(); + Log.d(TAG, i + ". try to get port"); + runBlockingCmd(new String[]{getPortCommand}, log); + output = log.toString(); + if (output.length() > 0) { + Log.d(TAG, "dispatcher log: \n =================\n" + output); + } + + String dispatcherLog[] = output.split(" "); + if (dispatcherLog.length > 0) { + String localAddressAndPort = dispatcherLog[dispatcherLog.length - 1]; + if (localAddressAndPort.contains(":")) { + this.port = localAddressAndPort.split(":")[1].replace(System.getProperty("line.separator"), ""); + Log.d(TAG, "local port is: " + this.port); + } + } + i += 1; + } + + } catch(Exception e){ + if (dispatcherThread.isAlive()) { + Log.e(TAG, e.getMessage() + ". Shutting down Dispatcher thread."); + stop(); + } + } + } + + public String getPort() { + return port; + } + + public void stop() { + Log.d(TAG, "Shutting down Dispatcher thread."); + if (dispatcherThread != null && dispatcherThread.isAlive()) { + try { + killProcess(dipatcherPid); + } catch (Exception e) { + e.printStackTrace(); + } + dispatcherThread.interrupt(); + } + } + + private void killProcess(int pid) throws Exception { + String killPid = "kill -9 " + pid; + runCmd(new String[]{killPid}, null, false); + } + + public boolean isRunning() { + return dispatcherThread != null && dispatcherThread.isAlive(); + } + + private File installDispatcher(){ + File fileDispatcher = null; + BinaryInstaller bi = new BinaryInstaller(context,context.getFilesDir()); + + String arch = System.getProperty("os.arch"); + if (arch.contains("arm")) + arch = "arm"; + else + arch = "x86"; + + try { + fileDispatcher = bi.installResource(arch, ASSET_KEY, false); + } catch (Exception ioe) { + Log.d(TAG,"Couldn't install dispatcher: " + ioe); + } + + return fileDispatcher; + } + + @WorkerThread + private void runBlockingCmd(String[] cmds, StringBuilder log) throws Exception { + runCmd(cmds, log, true); + } + + @WorkerThread + private int runCmd(String[] cmds, StringBuilder log, + boolean waitFor) throws Exception { + + int exitCode = -1; + Process proc = Runtime.getRuntime().exec("sh"); + OutputStreamWriter out = new OutputStreamWriter(proc.getOutputStream()); + + try { + for (String cmd : cmds) { + Log.d(TAG, "executing CMD: " + cmd); + out.write(cmd); + out.write("\n"); + } + + out.flush(); + out.write("exit\n"); + out.flush(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + out.close(); + } + + if (waitFor) { + // Consume the "stdout" + InputStreamReader reader = new InputStreamReader(proc.getInputStream()); + readToLogString(reader, log); + + // Consume the "stderr" + reader = new InputStreamReader(proc.getErrorStream()); + readToLogString(reader, log); + + try { + exitCode = proc.waitFor(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + return exitCode; + } + + private void readToLogString(InputStreamReader reader, StringBuilder log) throws IOException { + final char buf[] = new char[10]; + int read = 0; + try { + while ((read = reader.read(buf)) != -1) { + if (log != null) + log.append(buf, 0, read); + } + } catch (IOException e) { + reader.close(); + throw new IOException(e); + } + reader.close(); + } +} |