diff options
Diffstat (limited to 'app/src/main/java/se/leap/bitmaskclient/base/models')
5 files changed, 693 insertions, 151 deletions
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java index 754491f8..b8849c4d 100644 --- a/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java +++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java @@ -42,6 +42,8 @@ public interface Constants { String RESTART_ON_UPDATE = "restart_on_update"; String LAST_UPDATE_CHECK = "last_update_check"; String PREFERRED_CITY = "preferred_city"; + // ATTENTION: this key is also used in bitmask-core for persistence + String COUNTRYCODE = "COUNTRYCODE"; String USE_SNOWFLAKE = "use_snowflake"; String PREFER_UDP = "prefer_UDP"; String GATEWAY_PINNING = "gateway_pinning"; @@ -51,9 +53,12 @@ public interface Constants { String OBFUSCATION_PINNING_PORT = "obfuscation_pinning_port"; String OBFUSCATION_PINNING_CERT = "obfuscation_pinning_cert"; String OBFUSCATION_PINNING_KCP = "obfuscation_pinning_udp"; + String OBFUSCATION_PINNING_PROTOCOL = "obfuscation_pinning_protocol"; String OBFUSCATION_PINNING_LOCATION = "obfuscation_pinning_location"; String USE_SYSTEM_PROXY = "usesystemproxy"; String CUSTOM_PROVIDER_DOMAINS = "custom_provider_domains"; + String USE_PORT_HOPPING = "use_port_hopping"; + String USE_TUNNEL = "tunnel"; ////////////////////////////////////////////// @@ -122,6 +127,10 @@ public interface Constants { String PROVIDER_MOTD_HASHES = "Constants.PROVIDER_MOTD_HASHES"; String PROVIDER_MOTD_LAST_SEEN = "Constants.PROVIDER_MOTD_LAST_SEEN"; String PROVIDER_MOTD_LAST_UPDATED = "Constants.PROVIDER_MOTD_LAST_UPDATED"; + String PROVIDER_MODELS_PROVIDER = "Constants.PROVIDER_MODELS_PROVIDER"; + String PROVIDER_MODELS_EIPSERVICE = "Constants.PROVIDER_MDOELS_EIPSERVICE"; + String PROVIDER_MODELS_GATEWAYS = "Constants.PROVIDER_MODELS_GATEWAYS"; + String PROVIDER_MODELS_BRIDGES = "Constants.PROVIDER_MODELS_BRIDGES"; //////////////////////////////////////////////// // PRESHIPPED PROVIDER CONFIG @@ -184,6 +193,7 @@ public interface Constants { String UDP = "udp"; String TCP = "tcp"; String KCP = "kcp"; + String QUIC = "quic"; String CAPABILITIES = "capabilities"; String TRANSPORT = "transport"; String TYPE = "type"; @@ -194,6 +204,10 @@ public interface Constants { String ENDPOINTS = "endpoints"; String PORT_SEED = "port_seed"; String PORT_COUNT = "port_count"; + String HOP_JITTER = "hop_jitter"; + String MIN_HOP_PORT = "min_hop_port"; + String MAX_HOP_PORT = "max_hop_port"; + String MIN_HOP_SECONDS = "min_hop_seconds"; String EXPERIMENTAL = "experimental"; String VERSION = "version"; String NAME = "name"; diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/FeatureVersionCode.java b/app/src/main/java/se/leap/bitmaskclient/base/models/FeatureVersionCode.java index 8b78c966..1771f0b0 100644 --- a/app/src/main/java/se/leap/bitmaskclient/base/models/FeatureVersionCode.java +++ b/app/src/main/java/se/leap/bitmaskclient/base/models/FeatureVersionCode.java @@ -5,6 +5,7 @@ public interface FeatureVersionCode { int GEOIP_SERVICE = 148; int CALYX_PROVIDER_LILYPAD_UPDATE = 165000; int RISEUP_PROVIDER_LILYPAD_UPDATE = 165000; + int RISEUP_PROVIDER_LILYPAD_UPDATE_v2 = 172000; int ENCRYPTED_SHARED_PREFS = 170000; int NOTIFICATION_PREMISSION_API_UPDATE = 170000; diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/Introducer.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Introducer.java new file mode 100644 index 00000000..e3175010 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Introducer.java @@ -0,0 +1,132 @@ +package se.leap.bitmaskclient.base.models; + +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +import java.io.UnsupportedEncodingException; +import java.net.IDN; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.util.Locale; + +public class Introducer implements Parcelable { + private final String type; + private final String address; + private final String certificate; + private final String fullyQualifiedDomainName; + private final boolean kcpEnabled; + private final String auth; + + public Introducer(String type, String address, String certificate, String fullyQualifiedDomainName, boolean kcpEnabled, String auth) { + this.type = type; + this.address = address; + this.certificate = certificate; + this.fullyQualifiedDomainName = fullyQualifiedDomainName; + this.kcpEnabled = kcpEnabled; + this.auth = auth; + } + + protected Introducer(Parcel in) { + type = in.readString(); + address = in.readString(); + certificate = in.readString(); + fullyQualifiedDomainName = in.readString(); + kcpEnabled = in.readByte() != 0; + auth = in.readString(); + } + + public String getFullyQualifiedDomainName() { + return fullyQualifiedDomainName; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(type); + dest.writeString(address); + dest.writeString(certificate); + dest.writeString(fullyQualifiedDomainName); + dest.writeByte((byte) (kcpEnabled ? 1 : 0)); + dest.writeString(auth); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<Introducer> CREATOR = new Creator<>() { + @Override + public Introducer createFromParcel(Parcel in) { + return new Introducer(in); + } + + @Override + public Introducer[] newArray(int size) { + return new Introducer[size]; + } + }; + + public boolean validate() { + if (!"obfsvpnintro".equals(type)) { + throw new IllegalArgumentException("Unknown type: " + type); + } + if (!address.contains(":") || address.split(":").length != 2) { + throw new IllegalArgumentException("Expected address in format ipaddr:port"); + } + if (certificate.length() != 70) { + throw new IllegalArgumentException("Wrong certificate length: " + certificate.length()); + } + if (!"localhost".equals(fullyQualifiedDomainName) && fullyQualifiedDomainName.split("\\.").length < 2) { + throw new IllegalArgumentException("Expected a FQDN, got: " + fullyQualifiedDomainName); + } + + if (auth == null || auth.isEmpty()) { + throw new IllegalArgumentException("Auth token is missing"); + } + return true; + } + + public static Introducer fromUrl(String introducerUrl) throws URISyntaxException, IllegalArgumentException { + Uri uri = Uri.parse(introducerUrl); + String fqdn = uri.getQueryParameter("fqdn"); + if (fqdn == null || fqdn.isEmpty()) { + throw new IllegalArgumentException("FQDN not found in the introducer URL"); + } + + if (!isAscii(fqdn)) { + throw new IllegalArgumentException("FQDN is not ASCII: " + fqdn); + } + + boolean kcp = "1".equals(uri.getQueryParameter( "kcp")); + + String cert = uri.getQueryParameter( "cert"); + if (cert == null || cert.isEmpty()) { + throw new IllegalArgumentException("Cert not found in the introducer URL"); + } + + String auth = uri.getQueryParameter( "auth"); + if (auth == null || auth.isEmpty()) { + throw new IllegalArgumentException("Authentication token not found in the introducer URL"); + } + return new Introducer(uri.getScheme(), uri.getAuthority(), cert, fqdn, kcp, auth); + } + + public String getAuthToken() { + return auth; + } + + private static boolean isAscii(String fqdn) { + try { + String asciiFQDN = IDN.toASCII(fqdn, IDN.USE_STD3_ASCII_RULES); + return fqdn.equals(asciiFQDN); + } catch (IllegalArgumentException e) { + return false; + } + } + + public String toUrl() throws UnsupportedEncodingException { + return String.format(Locale.US, "%s://%s?fqdn=%s&kcp=%d&cert=%s&auth=%s", type, address, URLEncoder.encode(fullyQualifiedDomainName, "UTF-8"), kcpEnabled ? 1 : 0, URLEncoder.encode(certificate, "UTF-8"), URLEncoder.encode(auth, "UTF-8")); + } + +}
\ No newline at end of file diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java index cb9bd520..b4ec23e6 100644 --- a/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java +++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java @@ -17,6 +17,7 @@ package se.leap.bitmaskclient.base.models; import static de.blinkt.openvpn.core.connection.Connection.TransportProtocol.KCP; +import static de.blinkt.openvpn.core.connection.Connection.TransportProtocol.QUIC; import static de.blinkt.openvpn.core.connection.Connection.TransportProtocol.TCP; import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4; import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4_HOP; @@ -28,8 +29,9 @@ import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_ALLOWED_REGIS import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_ALLOW_ANONYMOUS; import static se.leap.bitmaskclient.base.models.Constants.TRANSPORT; import static se.leap.bitmaskclient.base.models.Constants.TYPE; -import static se.leap.bitmaskclient.base.utils.BuildConfigHelper.useObfsVpn; -import static se.leap.bitmaskclient.base.utils.RSAHelper.parseRsaKeyFromString; +import static se.leap.bitmaskclient.base.utils.ConfigHelper.isDomainName; +import static se.leap.bitmaskclient.base.utils.ConfigHelper.isNetworkUrl; +import static se.leap.bitmaskclient.base.utils.PrivateKeyHelper.parsePrivateKeyFromString; import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS; import android.os.Parcel; @@ -38,23 +40,33 @@ import android.os.Parcelable; import androidx.annotation.NonNull; import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.net.MalformedURLException; +import java.net.URISyntaxException; import java.net.URL; -import java.security.interfaces.RSAPrivateKey; +import java.security.PrivateKey; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.Locale; +import java.util.Objects; import java.util.Set; import de.blinkt.openvpn.core.connection.Connection.TransportProtocol; import de.blinkt.openvpn.core.connection.Connection.TransportType; +import io.swagger.client.JSON; +import io.swagger.client.model.ModelsBridge; +import io.swagger.client.model.ModelsEIPService; +import io.swagger.client.model.ModelsGateway; +import io.swagger.client.model.ModelsProvider; import motd.IStringCollection; import motd.Motd; +import se.leap.bitmaskclient.BuildConfig; /** * @author Sean Leonard <meanderingcode@aetherislands.net> @@ -69,25 +81,30 @@ public final class Provider implements Parcelable { private JSONObject eipServiceJson = new JSONObject(); private JSONObject geoIpJson = new JSONObject(); private JSONObject motdJson = new JSONObject(); - private DefaultedURL mainUrl = new DefaultedURL(); - private DefaultedURL apiUrl = new DefaultedURL(); - private DefaultedURL geoipUrl = new DefaultedURL(); - private DefaultedURL motdUrl = new DefaultedURL(); + private String mainUrl = ""; + private String apiUrl = ""; + private String geoipUrl = ""; + private String motdUrl = ""; + private ModelsEIPService modelsEIPService = null; + private ModelsProvider modelsProvider = null; + private ModelsGateway[] modelsGateways = null; + private ModelsBridge[] modelsBridges = null; private String domain = ""; private String providerIp = ""; // ip of the provider main url private String providerApiIp = ""; // ip of the provider api url private String certificatePin = ""; private String certificatePinEncoding = ""; private String caCert = ""; - private String apiVersion = ""; - private String privateKey = ""; - - private transient RSAPrivateKey rsaPrivateKey = null; + private int apiVersion = 3; + private int[] apiVersions = new int[0]; + private String privateKeyString = ""; + private transient PrivateKey privateKey = null; private String vpnCertificate = ""; private long lastEipServiceUpdate = 0L; private long lastGeoIpUpdate = 0L; private long lastMotdUpdate = 0L; private long lastMotdSeen = 0L; + private Introducer introducer = null; private Set<String> lastMotdSeenHashes = new HashSet<>(); private boolean shouldUpdateVpnCertificate; @@ -97,6 +114,7 @@ public final class Provider implements Parcelable { final public static String API_URL = "api_uri", API_VERSION = "api_version", + API_VERSIONS = "api_versions", ALLOW_REGISTRATION = "allow_registration", API_RETURN_SERIAL = "serial", SERVICE = "service", @@ -117,37 +135,36 @@ public final class Provider implements Parcelable { public Provider() { } + public Provider(Introducer introducer) { + this("https://" + introducer.getFullyQualifiedDomainName()); + this.introducer = introducer; + } + public Provider(String mainUrl) { - this(mainUrl, null); + this(mainUrl, null); + domain = getHostFromUrl(mainUrl); } public Provider(String mainUrl, String geoipUrl) { - try { - this.mainUrl.setUrl(new URL(mainUrl)); - } catch (MalformedURLException e) { - this.mainUrl = new DefaultedURL(); - } + setMainUrl(mainUrl); setGeoipUrl(geoipUrl); + domain = getHostFromUrl(mainUrl); } - public static Provider createCustomProvider(String mainUrl, String domain) { + public static Provider createCustomProvider(String mainUrl, String domain, Introducer introducer) { Provider p = new Provider(mainUrl); p.domain = domain; + p.introducer = introducer; return p; } public Provider(String mainUrl, String geoipUrl, String motdUrl, String providerIp, String providerApiIp) { - try { - this.mainUrl.setUrl(new URL(mainUrl)); - if (providerIp != null) { - this.providerIp = providerIp; - } - if (providerApiIp != null) { - this.providerApiIp = providerApiIp; - } - } catch (MalformedURLException e) { - e.printStackTrace(); - return; + setMainUrl(mainUrl); + if (providerIp != null) { + this.providerIp = providerIp; + } + if (providerApiIp != null) { + this.providerApiIp = providerApiIp; } setGeoipUrl(geoipUrl); setMotdUrl(motdUrl); @@ -179,64 +196,195 @@ public final class Provider implements Parcelable { } }; + public void setBridges(String bridgesJson) { + if (bridgesJson == null) { + this.modelsBridges = null; + return; + } + try { + this.modelsBridges = JSON.createGson().create().fromJson(bridgesJson, ModelsBridge[].class); + } catch (JsonSyntaxException e) { + e.printStackTrace(); + } + } + + public ModelsBridge[] getBridges() { + return this.modelsBridges; + } + + public String getBridgesJson() { + return getJsonString(modelsBridges); + } + + public void setGateways(String gatewaysJson) { + if (gatewaysJson == null) { + this.modelsGateways = null; + return; + } + try { + this.modelsGateways = JSON.createGson().create().fromJson(gatewaysJson, ModelsGateway[].class); + } catch (JsonSyntaxException e) { + e.printStackTrace(); + } + } + + public ModelsGateway[] getGateways() { + return modelsGateways; + } + + public String getGatewaysJson() { + return getJsonString(modelsGateways); + } + + public void setService(String serviceJson) { + if (serviceJson == null) { + this.modelsEIPService = null; + return; + } + try { + this.modelsEIPService = JSON.createGson().create().fromJson(serviceJson, ModelsEIPService.class); + } catch (JsonSyntaxException e) { + e.printStackTrace(); + } + } + public ModelsEIPService getService() { + return this.modelsEIPService; + } + + public String getServiceJson() { + return getJsonString(modelsEIPService); + } + + public void setModelsProvider(String json) { + if (json == null) { + this.modelsProvider = null; + return; + } + try { + this.modelsProvider = JSON.createGson().create().fromJson(json, ModelsProvider.class); + } catch (JsonSyntaxException e) { + e.printStackTrace(); + } + } + + public String getModelsProviderJson() { + return getJsonString(modelsProvider); + } + + private String getJsonString(Object model) { + if (model == null) { + return null; + } + try { + return JSON.createGson().create().toJson(model); + } catch (JsonSyntaxException e) { + e.printStackTrace(); + return null; + } + } + public boolean isConfigured() { - return !mainUrl.isDefault() && - !apiUrl.isDefault() && - hasCaCert() && - hasDefinition() && - hasVpnCertificate() && - hasEIP() && - hasPrivateKey(); + if (apiVersion < 5) { + return !mainUrl.isEmpty() && + !apiUrl.isEmpty() && + hasCaCert() && + hasDefinition() && + hasVpnCertificate() && + hasEIP() && + hasPrivateKey(); + } else { + return !mainUrl.isEmpty() && + modelsProvider != null && + modelsEIPService != null && + modelsGateways != null && + hasVpnCertificate() && + hasPrivateKey(); + } } public boolean supportsPluggableTransports() { - if (useObfsVpn()) { - return supportsTransports(new Pair[]{new Pair<>(OBFS4, TCP), new Pair<>(OBFS4, KCP), new Pair<>(OBFS4_HOP, TCP), new Pair<>(OBFS4_HOP, KCP)}); - } - return supportsTransports(new Pair[]{new Pair<>(OBFS4, TCP)}); + return supportsTransports(new Pair[]{new Pair<>(OBFS4, TCP), new Pair<>(OBFS4, KCP), new Pair<>(OBFS4, QUIC), new Pair<>(OBFS4_HOP, TCP), new Pair<>(OBFS4_HOP, KCP), new Pair<>(OBFS4_HOP, QUIC)}); } public boolean supportsExperimentalPluggableTransports() { - return supportsTransports(new Pair[]{new Pair<>(OBFS4, KCP), new Pair<>(OBFS4_HOP, TCP), new Pair<>(OBFS4_HOP, KCP)}); + return supportsTransports(new Pair[]{new Pair<>(OBFS4, KCP), new Pair<>(OBFS4_HOP, TCP), new Pair<>(OBFS4_HOP, KCP), new Pair<>(OBFS4, QUIC), new Pair<>(OBFS4_HOP, QUIC)}); + } + + + public boolean supportsObfs4() { + return supportsTransports(new Pair[]{new Pair<>(OBFS4, TCP)}); + } + + public boolean supportsObfs4Kcp() { + return supportsTransports(new Pair[]{new Pair<>(OBFS4, KCP)}); + } + + public boolean supportsObfs4Quic() { + return supportsTransports(new Pair[]{new Pair<>(OBFS4, QUIC)}); + } + + public boolean supportsObfs4Hop() { + return supportsTransports(new Pair[]{new Pair<>(OBFS4_HOP, KCP), new Pair<>(OBFS4_HOP, QUIC), new Pair<>(OBFS4_HOP, TCP)}); } private boolean supportsTransports(Pair<TransportType, TransportProtocol>[] transportTypes) { - try { - JSONArray gatewayJsons = eipServiceJson.getJSONArray(GATEWAYS); - for (int i = 0; i < gatewayJsons.length(); i++) { - JSONArray transports = gatewayJsons.getJSONObject(i). - getJSONObject(CAPABILITIES). - getJSONArray(TRANSPORT); - for (int j = 0; j < transports.length(); j++) { - String supportedTransportType = transports.getJSONObject(j).getString(TYPE); - JSONArray transportProtocols = transports.getJSONObject(j).getJSONArray(PROTOCOLS); - for (Pair<TransportType, TransportProtocol> transportPair : transportTypes) { - for (int k = 0; k < transportProtocols.length(); k++) { - if (transportPair.first.toString().equals(supportedTransportType) && - transportPair.second.toString().equals(transportProtocols.getString(k))) { - return true; + if (apiVersion < 5) { + try { + JSONArray gatewayJsons = eipServiceJson.getJSONArray(GATEWAYS); + for (int i = 0; i < gatewayJsons.length(); i++) { + JSONArray transports = gatewayJsons.getJSONObject(i). + getJSONObject(CAPABILITIES). + getJSONArray(TRANSPORT); + for (int j = 0; j < transports.length(); j++) { + String supportedTransportType = transports.getJSONObject(j).getString(TYPE); + JSONArray transportProtocols = transports.getJSONObject(j).getJSONArray(PROTOCOLS); + for (Pair<TransportType, TransportProtocol> transportPair : transportTypes) { + for (int k = 0; k < transportProtocols.length(); k++) { + if (transportPair.first.toString().equals(supportedTransportType) && + transportPair.second.toString().equals(transportProtocols.getString(k))) { + return true; + } } } } } + } catch (Exception e) { + // ignore + } + } else { + if (modelsBridges == null) return false; + for (ModelsBridge bridge : modelsBridges) { + for (Pair<TransportType, TransportProtocol> transportPair : transportTypes) { + if (transportPair.first.toString().equals(bridge.getType()) && + transportPair.second.toString().equals(bridge.getTransport())) { + return true; + } + } } - } catch (Exception e) { - // ignore } + return false; } public String getIpForHostname(String host) { if (host != null) { - if (host.equals(mainUrl.getUrl().getHost())) { + if (host.equals(getHostFromUrl(mainUrl))) { return providerIp; - } else if (host.equals(apiUrl.getUrl().getHost())) { + } else if (host.equals(getHostFromUrl(apiUrl))) { return providerApiIp; } } return ""; } + private String getHostFromUrl(String url) { + try { + return new URL(url).getHost(); + } catch (MalformedURLException e) { + return ""; + } + } + public String getProviderApiIp() { return this.providerApiIp; } @@ -256,14 +404,21 @@ public final class Provider implements Parcelable { } public void setMainUrl(URL url) { - mainUrl.setUrl(url); + mainUrl = url.toString(); } public void setMainUrl(String url) { try { - mainUrl.setUrl(new URL(url)); + if (isNetworkUrl(url)) { + this.mainUrl = new URL(url).toString(); + } else if (isDomainName(url)){ + this.mainUrl = new URL("https://" + url).toString(); + } else { + this.mainUrl = ""; + } } catch (MalformedURLException e) { e.printStackTrace(); + this.mainUrl = ""; } } @@ -281,55 +436,54 @@ public final class Provider implements Parcelable { } public String getDomain() { - return domain; - } - - public String getMainUrlString() { - return getMainUrl().toString(); + if ((apiVersion < 5 && (domain == null || domain.isEmpty())) || + (modelsProvider == null)) { + return getHostFromUrl(mainUrl); + } + if (apiVersion < 5) { + return domain; + } + return modelsProvider.getDomain(); } - public DefaultedURL getMainUrl() { + public String getMainUrl() { return mainUrl; } - protected DefaultedURL getApiUrl() { - return apiUrl; - } - - public DefaultedURL getGeoipUrl() { + public String getGeoipUrl() { return geoipUrl; } public void setGeoipUrl(String url) { try { - this.geoipUrl.setUrl(new URL(url)); + this.geoipUrl = new URL(url).toString(); } catch (MalformedURLException e) { - this.geoipUrl = new DefaultedURL(); + this.geoipUrl = ""; } } - public DefaultedURL getMotdUrl() { + public String getMotdUrl() { return this.motdUrl; } public void setMotdUrl(String url) { try { - this.motdUrl.setUrl(new URL(url)); + this.motdUrl = new URL(url).toString(); } catch (MalformedURLException e) { - this.motdUrl = new DefaultedURL(); + this.motdUrl = ""; } } public String getApiUrlWithVersion() { - return getApiUrlString() + "/" + getApiVersion(); + return getApiUrl() + "/" + getApiVersion(); } - public String getApiUrlString() { - return getApiUrl().toString(); + public String getApiUrl() { + return apiUrl; } - public String getApiVersion() { + public int getApiVersion() { return apiVersion; } @@ -338,7 +492,7 @@ public final class Provider implements Parcelable { } public boolean hasDefinition() { - return definition != null && definition.length() > 0; + return (definition != null && definition.length() > 0) || (modelsProvider != null); } public boolean hasGeoIpJson() { @@ -363,7 +517,7 @@ public final class Provider implements Parcelable { name = definition.getJSONObject(API_TERM_NAME).getString("en"); } catch (JSONException e2) { if (mainUrl != null) { - String host = mainUrl.getDomain(); + String host = getHostFromUrl(mainUrl); name = host.substring(0, host.indexOf(".")); } } @@ -394,12 +548,25 @@ public final class Provider implements Parcelable { && !getEipServiceJson().has(ERRORS); } + public boolean hasServiceInfo() { + return modelsEIPService != null; + } + public boolean hasGatewaysInDifferentLocations() { - try { - return getEipServiceJson().getJSONObject(LOCATIONS).length() > 1; - } catch (NullPointerException | JSONException e) { - return false; + if (apiVersion >= 5) { + try { + return getService().getLocations().size() > 1; + } catch (NullPointerException e) { + return false; + } + } else { + try { + return getEipServiceJson().getJSONObject(LOCATIONS).length() > 1; + } catch (NullPointerException | JSONException e) { + return false; + } } + } @Override @@ -410,17 +577,17 @@ public final class Provider implements Parcelable { @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeString(getDomain()); - parcel.writeString(getMainUrlString()); + parcel.writeString(getMainUrl()); parcel.writeString(getProviderIp()); parcel.writeString(getProviderApiIp()); - parcel.writeString(getGeoipUrl().toString()); - parcel.writeString(getMotdUrl().toString()); + parcel.writeString(getGeoipUrl()); + parcel.writeString(getMotdUrl()); parcel.writeString(getDefinitionString()); parcel.writeString(getCaCert()); parcel.writeString(getEipServiceJsonString()); parcel.writeString(getGeoIpJsonString()); parcel.writeString(getMotdJsonString()); - parcel.writeString(getPrivateKey()); + parcel.writeString(getPrivateKeyString()); parcel.writeString(getVpnCertificate()); parcel.writeLong(lastEipServiceUpdate); parcel.writeLong(lastGeoIpUpdate); @@ -428,6 +595,14 @@ public final class Provider implements Parcelable { parcel.writeLong(lastMotdSeen); parcel.writeStringList(new ArrayList<>(lastMotdSeenHashes)); parcel.writeInt(shouldUpdateVpnCertificate ? 0 : 1); + parcel.writeParcelable(introducer, 0); + if (apiVersion == 5) { + Gson gson = JSON.createGson().create(); + parcel.writeString(modelsProvider != null ? gson.toJson(modelsProvider) : ""); + parcel.writeString(modelsEIPService != null ? gson.toJson(modelsEIPService) : ""); + parcel.writeString(modelsBridges != null ? gson.toJson(modelsBridges) : ""); + parcel.writeString(modelsGateways != null ? gson.toJson(modelsGateways) : ""); + } } @@ -435,7 +610,7 @@ public final class Provider implements Parcelable { private Provider(Parcel in) { try { domain = in.readString(); - mainUrl.setUrl(new URL(in.readString())); + setMainUrl(in.readString()); String tmpString = in.readString(); if (!tmpString.isEmpty()) { providerIp = tmpString; @@ -446,11 +621,11 @@ public final class Provider implements Parcelable { } tmpString = in.readString(); if (!tmpString.isEmpty()) { - geoipUrl.setUrl(new URL(tmpString)); + geoipUrl = new URL(tmpString).toString(); } tmpString = in.readString(); if (!tmpString.isEmpty()) { - motdUrl.setUrl(new URL(tmpString)); + motdUrl = new URL(tmpString).toString(); } tmpString = in.readString(); if (!tmpString.isEmpty()) { @@ -475,7 +650,7 @@ public final class Provider implements Parcelable { } tmpString = in.readString(); if (!tmpString.isEmpty()) { - this.setPrivateKey(tmpString); + this.setPrivateKeyString(tmpString); } tmpString = in.readString(); if (!tmpString.isEmpty()) { @@ -489,6 +664,13 @@ public final class Provider implements Parcelable { in.readStringList(lastMotdSeenHashes); this.lastMotdSeenHashes = new HashSet<>(lastMotdSeenHashes); this.shouldUpdateVpnCertificate = in.readInt() == 0; + this.introducer = in.readParcelable(Introducer.class.getClassLoader()); + if (this.apiVersion == 5) { + this.setModelsProvider(in.readString()); + this.setService(in.readString()); + this.setBridges(in.readString()); + this.setGateways(in.readString()); + } } catch (MalformedURLException | JSONException e) { e.printStackTrace(); } @@ -500,24 +682,28 @@ public final class Provider implements Parcelable { if (o instanceof Provider) { Provider p = (Provider) o; return getDomain().equals(p.getDomain()) && - mainUrl.getDomain().equals(p.mainUrl.getDomain()) && - definition.toString().equals(p.getDefinition().toString()) && - eipServiceJson.toString().equals(p.getEipServiceJsonString()) && - geoIpJson.toString().equals(p.getGeoIpJsonString()) && - motdJson.toString().equals(p.getMotdJsonString()) && - providerIp.equals(p.getProviderIp()) && - providerApiIp.equals(p.getProviderApiIp()) && - apiUrl.equals(p.getApiUrl()) && - geoipUrl.equals(p.getGeoipUrl()) && - motdUrl.equals(p.getMotdUrl()) && - certificatePin.equals(p.getCertificatePin()) && - certificatePinEncoding.equals(p.getCertificatePinEncoding()) && - caCert.equals(p.getCaCert()) && - apiVersion.equals(p.getApiVersion()) && - privateKey.equals(p.getPrivateKey()) && - vpnCertificate.equals(p.getVpnCertificate()) && - allowAnonymous == p.allowsAnonymous() && - allowRegistered == p.allowsRegistered(); + getHostFromUrl(mainUrl).equals(getHostFromUrl(p.getMainUrl())) && + definition.toString().equals(p.getDefinition().toString()) && + eipServiceJson.toString().equals(p.getEipServiceJsonString()) && + geoIpJson.toString().equals(p.getGeoIpJsonString()) && + motdJson.toString().equals(p.getMotdJsonString()) && + providerIp.equals(p.getProviderIp()) && + providerApiIp.equals(p.getProviderApiIp()) && + apiUrl.equals(p.getApiUrl()) && + geoipUrl.equals(p.getGeoipUrl()) && + motdUrl.equals(p.getMotdUrl()) && + certificatePin.equals(p.getCertificatePin()) && + certificatePinEncoding.equals(p.getCertificatePinEncoding()) && + caCert.equals(p.getCaCert()) && + apiVersion == p.getApiVersion() && + privateKeyString.equals(p.getPrivateKeyString()) && + vpnCertificate.equals(p.getVpnCertificate()) && + allowAnonymous == p.allowsAnonymous() && + allowRegistered == p.allowsRegistered() && + Objects.equals(modelsProvider, p.modelsProvider) && + Objects.equals(modelsEIPService, p.modelsEIPService) && + Arrays.equals(modelsBridges, p.modelsBridges) && + Arrays.equals(modelsGateways, p.modelsGateways); } else return false; } @@ -535,7 +721,7 @@ public final class Provider implements Parcelable { @Override public int hashCode() { - return getMainUrlString().hashCode(); + return getMainUrl().hashCode(); } @Override @@ -545,20 +731,65 @@ public final class Provider implements Parcelable { private boolean parseDefinition(JSONObject definition) { try { + this.apiVersions = parseApiVersionsArray(); + this.apiVersion = selectPreferredApiVersion(); + this.domain = getDefinition().getString(Provider.DOMAIN); String pin = definition.getString(CA_CERT_FINGERPRINT); this.certificatePin = pin.split(":")[1].trim(); this.certificatePinEncoding = pin.split(":")[0].trim(); - this.apiUrl.setUrl(new URL(definition.getString(API_URL))); - this.allowAnonymous = definition.getJSONObject(Provider.SERVICE).getBoolean(PROVIDER_ALLOW_ANONYMOUS); - this.allowRegistered = definition.getJSONObject(Provider.SERVICE).getBoolean(PROVIDER_ALLOWED_REGISTERED); - this.apiVersion = getDefinition().getString(Provider.API_VERSION); - this.domain = getDefinition().getString(Provider.DOMAIN); + this.apiUrl = new URL(definition.getString(API_URL)).toString(); + JSONObject serviceJSONObject = definition.getJSONObject(Provider.SERVICE); + if (serviceJSONObject.has(PROVIDER_ALLOW_ANONYMOUS)) { + this.allowAnonymous = serviceJSONObject.getBoolean(PROVIDER_ALLOW_ANONYMOUS); + } + if (serviceJSONObject.has(PROVIDER_ALLOWED_REGISTERED)) { + this.allowRegistered = serviceJSONObject.getBoolean(PROVIDER_ALLOWED_REGISTERED); + } return true; - } catch (JSONException | ArrayIndexOutOfBoundsException | MalformedURLException e) { + } catch (JSONException | ArrayIndexOutOfBoundsException | MalformedURLException | NullPointerException | NumberFormatException e) { return false; } } + /** + @returns latest api version supported by client and server or the version set in 'api_version' + in case there's not a common supported version + */ + private int selectPreferredApiVersion() throws JSONException, NumberFormatException { + if (apiVersions.length == 0) { + return Integer.parseInt(getDefinition().getString(Provider.API_VERSION)); + } + + // apiVersion is a sorted Array + for (int i = apiVersions.length -1; i >= 0; i--) { + if (apiVersions[i] == BuildConfig.preferred_client_api_version || + apiVersions[i] < BuildConfig.preferred_client_api_version) { + return apiVersions[i]; + } + } + + return Integer.parseInt(getDefinition().getString(Provider.API_VERSION)); + } + + private int[] parseApiVersionsArray() { + int[] versionArray = new int[0]; + try { + JSONArray versions = getDefinition().getJSONArray(Provider.API_VERSIONS); + versionArray = new int[versions.length()]; + for (int i = 0; i < versions.length(); i++) { + try { + versionArray[i] = Integer.parseInt(versions.getString(i)); + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } + } catch (JSONException ignore) { + // this backend doesn't support api_versions yet + } + Arrays.sort(versionArray); + return versionArray; + } + public void setCaCert(String cert) { this.caCert = cert; } @@ -600,7 +831,7 @@ public final class Provider implements Parcelable { * @return true if last message of the day was shown more than 24h ago */ public boolean shouldShowMotdSeen() { - return !motdUrl.isDefault() && System.currentTimeMillis() - lastMotdSeen >= MOTD_TIMEOUT; + return !motdUrl.isEmpty() && System.currentTimeMillis() - lastMotdSeen >= MOTD_TIMEOUT; } /** @@ -636,7 +867,7 @@ public final class Provider implements Parcelable { } public boolean shouldUpdateMotdJson() { - return !motdUrl.isDefault() && System.currentTimeMillis() - lastMotdUpdate >= MOTD_TIMEOUT; + return !motdUrl.isEmpty() && System.currentTimeMillis() - lastMotdUpdate >= MOTD_TIMEOUT; } public void setMotdJson(@NonNull JSONObject motdJson) { @@ -693,31 +924,31 @@ public final class Provider implements Parcelable { } public boolean isDefault() { - return getMainUrl().isDefault() && - getApiUrl().isDefault() && - getGeoipUrl().isDefault() && + return getMainUrl().isEmpty() && + getApiUrl().isEmpty() && + getGeoipUrl().isEmpty() && certificatePin.isEmpty() && certificatePinEncoding.isEmpty() && caCert.isEmpty(); } - public String getPrivateKey() { - return privateKey; + public String getPrivateKeyString() { + return privateKeyString; } - public RSAPrivateKey getRSAPrivateKey() { - if (rsaPrivateKey == null) { - rsaPrivateKey = parseRsaKeyFromString(privateKey); + public PrivateKey getPrivateKey() { + if (privateKey == null) { + privateKey = parsePrivateKeyFromString(privateKeyString); } - return rsaPrivateKey; + return privateKey; } - public void setPrivateKey(String privateKey) { - this.privateKey = privateKey; + public void setPrivateKeyString(String privateKeyString) { + this.privateKeyString = privateKeyString; } public boolean hasPrivateKey() { - return privateKey != null && privateKey.length() > 0; + return privateKeyString != null && privateKeyString.length() > 0; } public String getVpnCertificate() { @@ -744,6 +975,18 @@ public final class Provider implements Parcelable { return getCertificatePinEncoding() + ":" + getCertificatePin(); } + public boolean hasIntroducer() { + return introducer != null; + } + + public Introducer getIntroducer() { + return introducer; + } + + public void setIntroducer(String introducerUrl) throws URISyntaxException, IllegalArgumentException { + this.introducer = Introducer.fromUrl(introducerUrl); + } + /** * resets everything except the main url, the providerIp and the geoip * service url (currently preseeded) @@ -753,16 +996,21 @@ public final class Provider implements Parcelable { eipServiceJson = new JSONObject(); geoIpJson = new JSONObject(); motdJson = new JSONObject(); - apiUrl = new DefaultedURL(); + apiUrl = ""; certificatePin = ""; certificatePinEncoding = ""; caCert = ""; - apiVersion = ""; - privateKey = ""; + apiVersion = BuildConfig.preferred_client_api_version; + privateKeyString = ""; vpnCertificate = ""; allowRegistered = false; allowAnonymous = false; lastGeoIpUpdate = 0L; lastEipServiceUpdate = 0L; + modelsProvider = null; + modelsGateways = null; + modelsBridges = null; + modelsEIPService = null; } + } diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/Transport.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Transport.java index 33fbbf7a..d0149a3f 100644 --- a/app/src/main/java/se/leap/bitmaskclient/base/models/Transport.java +++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Transport.java @@ -1,5 +1,21 @@ package se.leap.bitmaskclient.base.models; +import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4_HOP; +import static de.blinkt.openvpn.core.connection.Connection.TransportType.OPENVPN; +import static se.leap.bitmaskclient.base.models.Constants.CAPABILITIES; +import static se.leap.bitmaskclient.base.models.Constants.CERT; +import static se.leap.bitmaskclient.base.models.Constants.HOP_JITTER; +import static se.leap.bitmaskclient.base.models.Constants.IAT_MODE; +import static se.leap.bitmaskclient.base.models.Constants.MAX_HOP_PORT; +import static se.leap.bitmaskclient.base.models.Constants.MIN_HOP_PORT; +import static se.leap.bitmaskclient.base.models.Constants.MIN_HOP_SECONDS; +import static se.leap.bitmaskclient.base.models.Constants.PORTS; +import static se.leap.bitmaskclient.base.models.Constants.PORT_COUNT; +import static se.leap.bitmaskclient.base.models.Constants.PORT_SEED; +import static se.leap.bitmaskclient.base.models.Constants.PROTOCOLS; +import static se.leap.bitmaskclient.base.models.Constants.TRANSPORT; + +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.gson.FieldNamingPolicy; @@ -7,31 +23,44 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.annotations.SerializedName; +import org.json.JSONArray; import org.json.JSONObject; import java.io.Serializable; +import java.util.Map; +import java.util.Objects; +import java.util.Vector; import de.blinkt.openvpn.core.connection.Connection; +import io.swagger.client.model.ModelsBridge; +import io.swagger.client.model.ModelsGateway; public class Transport implements Serializable { - private String type; - private String[] protocols; + private final String type; + private final String[] protocols; @Nullable - private String[] ports; + private final String[] ports; @Nullable - private Options options; + private final Options options; public Transport(String type, String[] protocols, String[] ports, String cert) { this(type, protocols, ports, new Options(cert, "0")); } - public Transport(String type, String[] protocols, String[] ports, Options options) { + public Transport(String type, String[] protocols, @Nullable String[] ports, @Nullable Options options) { this.type = type; this.protocols = protocols; this.ports = ports; this.options = options; } + public Transport(String type, String[] protocols, @Nullable String[] ports) { + this.type = type; + this.protocols = protocols; + this.ports = ports; + this.options = null; + } + public String getType() { return type; } @@ -67,12 +96,107 @@ public class Transport implements Serializable { fromJson(json.toString(), Transport.class); } + public static Transport createTransportFrom(ModelsBridge modelsBridge) { + if (modelsBridge == null) { + return null; + } + Map<String, Object> options = modelsBridge.getOptions(); + Transport.Options transportOptions = new Transport.Options((String) options.get(CERT), (String) options.get(IAT_MODE)); + if (OBFS4_HOP.toString().equals(modelsBridge.getType())) { + transportOptions.minHopSeconds = getIntOption(options, MIN_HOP_SECONDS, 5); + transportOptions.minHopPort = getIntOption(options, MIN_HOP_PORT, 49152); + transportOptions.maxHopPort = getIntOption(options, MAX_HOP_PORT, 65535); + transportOptions.hopJitter = getIntOption(options, HOP_JITTER, 10); + transportOptions.portCount = getIntOption(options, PORT_COUNT, 100); + transportOptions.portSeed = getIntOption(options, PORT_SEED, 1); + } + Transport transport = new Transport( + modelsBridge.getType(), + new String[]{modelsBridge.getTransport()}, + new String[]{String.valueOf(modelsBridge.getPort())}, + transportOptions + ); + return transport; + } + + private static int getIntOption(Map<String, Object> options, String key, int defaultValue) { + try { + Object o = options.get(key); + if (o == null) { + return defaultValue; + } + if (o instanceof String) { + return Integer.parseInt((String) o); + } + return (int) o; + } catch (NullPointerException | ClassCastException | NumberFormatException e){ + e.printStackTrace(); + return defaultValue; + } + } + + public static Transport createTransportFrom(ModelsGateway modelsGateway) { + if (modelsGateway == null) { + return null; + } + Transport transport = new Transport( + modelsGateway.getType(), + new String[]{modelsGateway.getTransport()}, + new String[]{String.valueOf(modelsGateway.getPort())} + ); + return transport; + } + + + @NonNull + public static Vector<Transport> createTransportsFrom(JSONObject gateway, int apiVersion) throws IllegalArgumentException { + Vector<Transport> transports = new Vector<>(); + try { + if (apiVersion >= 3) { + JSONArray supportedTransports = gateway.getJSONObject(CAPABILITIES).getJSONArray(TRANSPORT); + for (int i = 0; i < supportedTransports.length(); i++) { + Transport transport = Transport.fromJson(supportedTransports.getJSONObject(i)); + transports.add(transport); + } + } else { + JSONObject capabilities = gateway.getJSONObject(CAPABILITIES); + JSONArray ports = capabilities.getJSONArray(PORTS); + JSONArray protocols = capabilities.getJSONArray(PROTOCOLS); + String[] portArray = new String[ports.length()]; + String[] protocolArray = new String[protocols.length()]; + for (int i = 0; i < ports.length(); i++) { + portArray[i] = String.valueOf(ports.get(i)); + } + for (int i = 0; i < protocols.length(); i++) { + protocolArray[i] = protocols.optString(i); + } + Transport transport = new Transport(OPENVPN.toString(), protocolArray, portArray); + transports.add(transport); + } + } catch (Exception e) { + throw new IllegalArgumentException(); + //throw new ConfigParser.ConfigParseError("Api version ("+ apiVersion +") did not match required JSON fields"); + } + return transports; + } + + public static Vector<Transport> createTransportsFrom(ModelsBridge modelsBridge) { + Vector<Transport> transports = new Vector<>(); + transports.add(Transport.createTransportFrom(modelsBridge)); + return transports; + } + + public static Vector<Transport> createTransportsFrom(ModelsGateway modelsGateway) { + Vector<Transport> transports = new Vector<>(); + transports.add(Transport.createTransportFrom(modelsGateway)); + return transports; + } + public static class Options implements Serializable { @Nullable - private String cert; + private final String cert; @SerializedName("iatMode") - private String iatMode; - + private final String iatMode; @Nullable private Endpoint[] endpoints; @@ -81,23 +205,30 @@ public class Transport implements Serializable { private int portSeed; private int portCount; - + private int minHopPort; + private int maxHopPort; + private int minHopSeconds; + private int hopJitter; public Options(String cert, String iatMode) { this.cert = cert; this.iatMode = iatMode; } - public Options(String iatMode, Endpoint[] endpoints, int portSeed, int portCount, boolean experimental) { - this(iatMode, endpoints, null, portSeed, portCount, experimental); + public Options(String iatMode, Endpoint[] endpoints, int portSeed, int portCount, int minHopPort, int maxHopPort, int minHopSeconds, int hopJitter, boolean experimental) { + this(iatMode, endpoints, null, portSeed, portCount, minHopPort, maxHopPort, minHopSeconds, hopJitter, experimental); } - public Options(String iatMode, Endpoint[] endpoints, String cert, int portSeed, int portCount, boolean experimental) { + public Options(String iatMode, Endpoint[] endpoints, String cert, int portSeed, int portCount, int minHopPort, int maxHopPort, int minHopSeconds, int hopJitter, boolean experimental) { this.iatMode = iatMode; this.endpoints = endpoints; this.portSeed = portSeed; this.portCount = portCount; this.experimental = experimental; + this.minHopPort = minHopPort; + this.maxHopPort = maxHopPort; + this.minHopSeconds = minHopSeconds; + this.hopJitter = hopJitter; this.cert = cert; } @@ -128,6 +259,22 @@ public class Transport implements Serializable { return portCount; } + public int getMinHopPort() { + return minHopPort; + } + + public int getMaxHopPort() { + return maxHopPort; + } + + public int getMinHopSeconds() { + return minHopSeconds; + } + + public int getHopJitter() { + return hopJitter; + } + @Override public String toString() { return new Gson().toJson(this); @@ -136,8 +283,8 @@ public class Transport implements Serializable { public static class Endpoint implements Serializable { - private String ip; - private String cert; + private final String ip; + private final String cert; public Endpoint(String ip, String cert) { this.ip = ip; |