mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-07-30 09:06:35 -04:00
Merge branch 'swap_fix' into 'master'
improve security of swap feature See merge request fdroid/fdroidclient!1682
This commit is contained in:
@@ -3,6 +3,7 @@ package org.fdroid.fdroid;
|
||||
import android.content.Context;
|
||||
|
||||
import org.apache.commons.net.util.SubnetUtils;
|
||||
import org.apache.commons.net.util.SubnetUtils6;
|
||||
import org.fdroid.database.Repository;
|
||||
import org.fdroid.index.IndexFormatVersion;
|
||||
|
||||
@@ -23,6 +24,8 @@ public class FDroidApp {
|
||||
@Nullable
|
||||
public static volatile SubnetUtils.SubnetInfo subnetInfo;
|
||||
@Nullable
|
||||
public static volatile SubnetUtils6.SubnetInfo subnet6Info;
|
||||
@Nullable
|
||||
public static volatile String ssid;
|
||||
@Nullable
|
||||
public static volatile String bssid;
|
||||
@@ -32,14 +35,12 @@ public class FDroidApp {
|
||||
@SuppressWarnings("unused")
|
||||
public static volatile String queryString;
|
||||
|
||||
public static final SubnetUtils.SubnetInfo UNSET_SUBNET_INFO =
|
||||
new SubnetUtils("0.0.0.0/32").getInfo();
|
||||
|
||||
public static void initWifiSettings() {
|
||||
port = generateNewPort ? (int) (Math.random() * 10000 + 8080) : port == 0 ? 8888 : port;
|
||||
generateNewPort = false;
|
||||
ipAddressString = null;
|
||||
subnetInfo = UNSET_SUBNET_INFO;
|
||||
subnetInfo = null;
|
||||
subnet6Info = null;
|
||||
ssid = null;
|
||||
bssid = null;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import android.content.ServiceConnection;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.LightingColorFilter;
|
||||
import android.net.InetAddresses;
|
||||
import android.net.Uri;
|
||||
import android.net.wifi.WifiInfo;
|
||||
import android.net.wifi.WifiManager;
|
||||
@@ -45,6 +46,7 @@ import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.core.content.ContextCompat;
|
||||
@@ -71,6 +73,10 @@ import org.fdroid.fdroid.qr.CameraCharacteristicsChecker;
|
||||
import org.fdroid.settings.SettingsManager;
|
||||
import org.fdroid.ui.nearby.SwapSuccessViewModel;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
@@ -79,6 +85,7 @@ import java.util.Set;
|
||||
import java.util.Stack;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@@ -147,6 +154,8 @@ public class SwapWorkflowActivity extends AppCompatActivity {
|
||||
private int currentSwapViewLayoutRes = R.layout.swap_start_swap;
|
||||
private final Stack<Integer> backstack = new Stack<>();
|
||||
|
||||
private boolean waitingForSwap = false;
|
||||
|
||||
private final ActivityResultLauncher<String> requestPermissionLauncher =
|
||||
registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
|
||||
if (isGranted) sendFDroidBluetooth();
|
||||
@@ -463,18 +472,128 @@ public class SwapWorkflowActivity extends AppCompatActivity {
|
||||
Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
confirmSwapConfig = new NewRepoConfig(this, intent);
|
||||
checkIfNewRepoOnSameWifi(confirmSwapConfig);
|
||||
|
||||
// don't save the repo config unless it's on the same network as the device
|
||||
// if no config is saved, the confirmation dialog should not be displayed
|
||||
NewRepoConfig repoConfig = new NewRepoConfig(this, intent);
|
||||
if (!checkIfNewRepoOnSameWifi(repoConfig)) {
|
||||
// show a toast here so the user is aware of the issue
|
||||
String msg = getString(R.string.not_on_same_wifi, repoConfig.getSsid());
|
||||
Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
confirmSwapConfig = repoConfig;
|
||||
}
|
||||
|
||||
private static boolean isSwapUrl(Uri uri) {
|
||||
return isSwapUrl(uri.getHost(), uri.getPort());
|
||||
static boolean isSwapUrl(Uri uri) {
|
||||
// check for null argument to prevent unexpected null pointer exceptions
|
||||
return uri != null && isSwapUrl(uri.getHost(), uri.getPort());
|
||||
}
|
||||
|
||||
private static boolean isSwapUrl(String host, int port) {
|
||||
return port > 1023 // only root can use <= 1023, so never a swap repo
|
||||
&& host.matches("[0-9.]+") // host must be an IP address
|
||||
&& FDroidApp.subnetInfo.isInRange(host); // on the same subnet as we are
|
||||
@VisibleForTesting
|
||||
static boolean isSwapUrl(String host, int port) {
|
||||
Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Checking swap URL %s:%d", host, port));
|
||||
if (port < 1024) {
|
||||
// only root can use < 1024, so this can't be a swap repo
|
||||
Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d is invalid because the port is out of range ( < 1024)", host, port));
|
||||
return false;
|
||||
}
|
||||
// verify that host is a numeric ip address to prevent a possible dns loookup
|
||||
if (!isNumericAddress(host)) {
|
||||
Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d is invalid because it does not use a numeric ip address", host, port));
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if (isIpv4(host) && FDroidApp.subnetInfo != null) {
|
||||
if (FDroidApp.subnetInfo.isInRange(host)) {
|
||||
Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d is on the subnet (ipv4)", host, port));
|
||||
return true;
|
||||
} else {
|
||||
Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d is invalid because it is not on the subnet (ipv4)", host, port));
|
||||
return false;
|
||||
}
|
||||
} else if (isIpv6(host) && FDroidApp.subnet6Info != null) {
|
||||
if (FDroidApp.subnet6Info.isInRange(host)) {
|
||||
Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d is on the subnet (ipv6)", host, port));
|
||||
return true;
|
||||
} else {
|
||||
Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d is invalid because it is not on the subnet (ipv6)", host, port));
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// unable to verify subnet range
|
||||
Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d is invalid because it was in an unexpected format", host, port));
|
||||
return false;
|
||||
}
|
||||
} catch (NullPointerException e) {
|
||||
// FDroidApp.subnetInfo/subnet6Info may not have been setup
|
||||
Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d could not be validated because the subnet info was null", host, port));
|
||||
return false;
|
||||
} catch (IllegalArgumentException e) {
|
||||
// in testing, isNumericAddress may return true for non-numeric adresses and
|
||||
// subnetInfo/subnet6Info.isInRange fails to parse it and throws an exception
|
||||
Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d could not be validated because it could not be parsed", host, port));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
private static boolean isNumericAddress(String host) {
|
||||
if (host == null) {
|
||||
return false;
|
||||
}
|
||||
// use new isNumericAddress method if possible, otherwise attempt to use regex
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
return InetAddresses.isNumericAddress(host);
|
||||
} else {
|
||||
// regex found online, verified with unit testing
|
||||
String ipv4String = "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$";
|
||||
String ipv6String = "^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|" +
|
||||
"^(?:[0-9a-fA-F]{1,4}:){1,7}:$|" +
|
||||
"^(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}$|" +
|
||||
"^(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}$|" +
|
||||
"^(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}$|" +
|
||||
"^(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}$|" +
|
||||
"^(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}$|" +
|
||||
"^[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}$|" +
|
||||
"^:(?::[0-9a-fA-F]{1,4}){1,7}$|" +
|
||||
"^::$|" +
|
||||
"^(?:[0-9a-fA-F]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1?[0-9])?[0-9])\\.){3}(?:25[0-5]|(?:2[0-4]|1?[0-9])?[0-9])$";
|
||||
Pattern ipv4Pattern = Pattern.compile(ipv4String);
|
||||
Pattern ipv6Pattern = Pattern.compile(ipv6String);
|
||||
return ipv4Pattern.matcher(host).matches() || ipv6Pattern.matcher(host).matches();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isIpv4(String host) {
|
||||
// use new parseNumericAddress method if possible, otherwise attempt to check for format
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
try {
|
||||
InetAddress i = InetAddresses.parseNumericAddress(host);
|
||||
return (i instanceof Inet4Address);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// no IPv4 literal should contain ":"
|
||||
return !host.contains(":");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isIpv6(String host) {
|
||||
// use new parseNumericAddress method if possible, otherwise attempt to check for format
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
try {
|
||||
InetAddress i = InetAddresses.parseNumericAddress(host);
|
||||
return (i instanceof Inet6Address);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// every IPv6 literal should contain ":"
|
||||
return host.contains(":");
|
||||
}
|
||||
}
|
||||
|
||||
private void promptToSelectWifiNetwork() {
|
||||
@@ -742,6 +861,8 @@ public class SwapWorkflowActivity extends AppCompatActivity {
|
||||
private void startSwappingWithPeer() {
|
||||
getSwapService().connectToPeer();
|
||||
inflateSwapView(R.layout.swap_connecting);
|
||||
// initiate swap, set flag
|
||||
waitingForSwap = true;
|
||||
}
|
||||
|
||||
public void swapWith(Peer peer) {
|
||||
@@ -789,8 +910,7 @@ public class SwapWorkflowActivity extends AppCompatActivity {
|
||||
if (scanResult != null) {
|
||||
if (scanResult.getContents() != null) {
|
||||
NewRepoConfig repoConfig = new NewRepoConfig(this, scanResult.getContents());
|
||||
if (repoConfig.isValidRepo()) {
|
||||
checkIfNewRepoOnSameWifi(repoConfig);
|
||||
if (repoConfig.isValidRepo() && checkIfNewRepoOnSameWifi(repoConfig)) {
|
||||
confirmSwapConfig = repoConfig;
|
||||
showRelevantView();
|
||||
} else {
|
||||
@@ -824,25 +944,36 @@ public class SwapWorkflowActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private void checkIfNewRepoOnSameWifi(NewRepoConfig newRepo) {
|
||||
// if this is a local repo, check we're on the same wifi
|
||||
if (!TextUtils.isEmpty(newRepo.getBssid())) {
|
||||
WifiManager wifiManager = ContextCompat.getSystemService(getApplicationContext(),
|
||||
WifiManager.class);
|
||||
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
|
||||
String bssid = wifiInfo.getBSSID();
|
||||
if (TextUtils.isEmpty(bssid)) { /* not all devices have wifi */
|
||||
return;
|
||||
}
|
||||
bssid = bssid.toLowerCase(Locale.ENGLISH);
|
||||
String newRepoBssid = Uri.decode(newRepo.getBssid()).toLowerCase(Locale.ENGLISH);
|
||||
if (!bssid.equals(newRepoBssid)) {
|
||||
String msg = getString(R.string.not_on_same_wifi, newRepo.getSsid());
|
||||
Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
// TODO we should help the user to the right thing here,
|
||||
// instead of just showing a message!
|
||||
private boolean checkIfNewRepoOnSameWifi(NewRepoConfig newRepo) {
|
||||
// check subnet range first. sometimes returns true when bssid is empty
|
||||
if (isSwapUrl(newRepo.getRepoUri())) {
|
||||
return true;
|
||||
}
|
||||
// if this is a local repo, check whether we're on the same wifi
|
||||
if (TextUtils.isEmpty(newRepo.getBssid())) {
|
||||
// repo may not be connected to an access point
|
||||
Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap repo may not be connected to wifi"));
|
||||
return false;
|
||||
}
|
||||
WifiManager wifiManager = ContextCompat.getSystemService(getApplicationContext(),
|
||||
WifiManager.class);
|
||||
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
|
||||
String bssid = wifiInfo.getBSSID();
|
||||
if (TextUtils.isEmpty(bssid)) {
|
||||
// device may not be connected to an access point
|
||||
Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Device may not be connected to wifi"));
|
||||
return false;
|
||||
}
|
||||
bssid = bssid.toLowerCase(Locale.ENGLISH);
|
||||
String newRepoBssid = Uri.decode(newRepo.getBssid()).toLowerCase(Locale.ENGLISH);
|
||||
if (!bssid.equals(newRepoBssid)) {
|
||||
// repo config bssid doesn't match device bssid, check subnet
|
||||
Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap repo and device are not connected to the same wifi"));
|
||||
return false;
|
||||
}
|
||||
// repo config appears to be on same wifi as device
|
||||
Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap repo and device are connected to the same wifi"));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1454,14 +1585,21 @@ public class SwapWorkflowActivity extends AppCompatActivity {
|
||||
};
|
||||
|
||||
private void onRepoUpdateSuccess() {
|
||||
CircularProgressIndicator progressBar = container.findViewById(R.id.progress_bar);
|
||||
Button tryAgainButton = container.findViewById(R.id.try_again);
|
||||
if (progressBar != null && tryAgainButton != null) {
|
||||
progressBar.show();
|
||||
tryAgainButton.setVisibility(View.GONE);
|
||||
// there is a 5 second loop in the SwapService that updates the index,
|
||||
// and a local observer watching that index that calls this method.
|
||||
// if the flag is not cleared, the success screen continually pops up,
|
||||
// even if the user hits the back button to exit.
|
||||
if (waitingForSwap) {
|
||||
waitingForSwap = false;
|
||||
CircularProgressIndicator progressBar = container.findViewById(R.id.progress_bar);
|
||||
Button tryAgainButton = container.findViewById(R.id.try_again);
|
||||
if (progressBar != null && tryAgainButton != null) {
|
||||
progressBar.show();
|
||||
tryAgainButton.setVisibility(View.GONE);
|
||||
}
|
||||
getSwapService().addCurrentPeerToActive();
|
||||
inflateSwapView(R.layout.swap_success);
|
||||
}
|
||||
getSwapService().addCurrentPeerToActive();
|
||||
inflateSwapView(R.layout.swap_success);
|
||||
}
|
||||
|
||||
private void onRepoUpdateError(Exception e) {
|
||||
|
||||
@@ -25,6 +25,7 @@ import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
import org.apache.commons.net.util.SubnetUtils;
|
||||
import org.apache.commons.net.util.SubnetUtils6;
|
||||
import org.fdroid.database.Repository;
|
||||
import org.fdroid.BuildConfig;
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
@@ -33,6 +34,7 @@ import org.fdroid.fdroid.Preferences;
|
||||
import org.fdroid.R;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InterfaceAddress;
|
||||
@@ -165,11 +167,13 @@ public class WifiStateChangeService extends Worker {
|
||||
return;
|
||||
}
|
||||
if (wifiState == WifiManager.WIFI_STATE_ENABLED) {
|
||||
Utils.debugLog(TAG, "wifi enabled, get network info");
|
||||
wifiInfo = wifiManager.getConnectionInfo();
|
||||
FDroidApp.ipAddressString = formatIpAddress(wifiInfo.getIpAddress());
|
||||
setSsid(wifiInfo);
|
||||
DhcpInfo dhcpInfo = wifiManager.getDhcpInfo();
|
||||
if (dhcpInfo != null) {
|
||||
Utils.debugLog(TAG, "get address/subnet info from dhcp");
|
||||
String netmask = formatIpAddress(dhcpInfo.netmask);
|
||||
if (!TextUtils.isEmpty(FDroidApp.ipAddressString) && netmask != null) {
|
||||
try {
|
||||
@@ -180,13 +184,18 @@ public class WifiStateChangeService extends Worker {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (FDroidApp.ipAddressString == null
|
||||
|| FDroidApp.subnetInfo == FDroidApp.UNSET_SUBNET_INFO) {
|
||||
if (FDroidApp.ipAddressString == null || FDroidApp.subnetInfo == null) {
|
||||
Utils.debugLog(TAG, "could not get address/subnet info from dhcp, try network interface");
|
||||
setIpInfoFromNetworkInterface();
|
||||
}
|
||||
} else if (wifiState == WifiManager.WIFI_STATE_DISABLED
|
||||
|| wifiState == WifiManager.WIFI_STATE_DISABLING
|
||||
|| wifiState == WifiManager.WIFI_STATE_UNKNOWN) {
|
||||
Utils.debugLog(TAG, "wifi disabled/unknown, clear network info");
|
||||
// subnet info is only used for the swap feature on wifi
|
||||
// if wifi is disabled, subnet info should be cleared
|
||||
FDroidApp.subnetInfo = null;
|
||||
FDroidApp.subnet6Info = null;
|
||||
// try once to see if its a hotspot
|
||||
setIpInfoFromNetworkInterface();
|
||||
if (FDroidApp.ipAddressString == null) {
|
||||
@@ -313,9 +322,11 @@ public class WifiStateChangeService extends Worker {
|
||||
* @see <a href="https://issuetracker.google.com/issues/37015180">netmask of WifiManager.getDhcpInfo() is always zero on Android 5.0</a>
|
||||
*/
|
||||
private void setIpInfoFromNetworkInterface() {
|
||||
Utils.debugLog(TAG, "get address/subnet info from network interface");
|
||||
try {
|
||||
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
|
||||
if (networkInterfaces == null) {
|
||||
Utils.debugLog(TAG, "could not get address/subnet info, network interface was null");
|
||||
return;
|
||||
}
|
||||
while (networkInterfaces.hasMoreElements()) {
|
||||
@@ -323,7 +334,9 @@ public class WifiStateChangeService extends Worker {
|
||||
|
||||
for (Enumeration<InetAddress> inetAddresses = netIf.getInetAddresses(); inetAddresses.hasMoreElements(); ) {
|
||||
InetAddress inetAddress = inetAddresses.nextElement();
|
||||
if (inetAddress.isLoopbackAddress() || inetAddress instanceof Inet6Address) {
|
||||
Utils.debugLog(TAG, String.format(Locale.ENGLISH, "checking address %s", inetAddress.getHostAddress()));
|
||||
if (inetAddress.isLoopbackAddress()) {
|
||||
Utils.debugLog(TAG, "the address was the loopback address");
|
||||
continue;
|
||||
}
|
||||
if (netIf.getDisplayName().contains("wlan0")
|
||||
@@ -332,21 +345,32 @@ public class WifiStateChangeService extends Worker {
|
||||
FDroidApp.ipAddressString = inetAddress.getHostAddress();
|
||||
for (InterfaceAddress address : netIf.getInterfaceAddresses()) {
|
||||
short networkPrefixLength = address.getNetworkPrefixLength();
|
||||
if (networkPrefixLength > 32) {
|
||||
// something is giving a "/64" netmask, IPv6?
|
||||
// java.lang.IllegalArgumentException: Value [64] not in range [0,32]
|
||||
// assume that ipv6 prefix length > 32
|
||||
if ((inetAddress instanceof Inet4Address && networkPrefixLength > 32)
|
||||
|| (inetAddress instanceof Inet6Address && networkPrefixLength <= 32)) {
|
||||
Utils.debugLog(TAG, String.format(Locale.ENGLISH, "invalid prefix length: %d", networkPrefixLength));
|
||||
continue;
|
||||
}
|
||||
// include support for both ipv4 and ipv6
|
||||
try {
|
||||
String cidr = String.format(Locale.ENGLISH, "%s/%d",
|
||||
FDroidApp.ipAddressString, networkPrefixLength);
|
||||
FDroidApp.subnetInfo = new SubnetUtils(cidr).getInfo();
|
||||
break;
|
||||
if (inetAddress instanceof Inet4Address) {
|
||||
String cidr = String.format(Locale.ENGLISH, "%s/%d",
|
||||
FDroidApp.ipAddressString, networkPrefixLength);
|
||||
FDroidApp.subnetInfo = new SubnetUtils(cidr).getInfo();
|
||||
Utils.debugLog(TAG, String.format(Locale.ENGLISH, "set ipv4 subnet info: %s", cidr));
|
||||
break;
|
||||
} else if (inetAddress instanceof Inet6Address) {
|
||||
String cidr = String.format(Locale.ENGLISH, "%s/%d",
|
||||
FDroidApp.ipAddressString, networkPrefixLength);
|
||||
FDroidApp.subnet6Info = new SubnetUtils6(cidr).getInfo();
|
||||
Utils.debugLog(TAG, String.format(Locale.ENGLISH, "set ipv6 subnet info: %s", cidr));
|
||||
break;
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
e.printStackTrace();
|
||||
} else {
|
||||
Log.i(TAG, "Getting subnet failed: " + e.getLocalizedMessage());
|
||||
Utils.debugLog(TAG, String.format(Locale.ENGLISH, "exception thrown while getting subnet info: %s", e.getLocalizedMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -355,7 +379,7 @@ public class WifiStateChangeService extends Worker {
|
||||
}
|
||||
} catch (NullPointerException | SocketException e) {
|
||||
// NetworkInterface.getNetworkInterfaces() can throw a NullPointerException internally
|
||||
Log.e(TAG, "Could not get ip address", e);
|
||||
Utils.debugLog(TAG, String.format(Locale.ENGLISH, "exception thrown while getting network interface: %s", e.getLocalizedMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
package org.fdroid.fdroid.nearby;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
|
||||
import org.apache.commons.net.util.SubnetUtils;
|
||||
import org.apache.commons.net.util.SubnetUtils6;
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
/**
|
||||
* Tests for {@link SwapWorkflowActivity#isSwapUrl}, which is the security gate
|
||||
* that decides whether an incoming swap URL points at a repo on the local
|
||||
* subnet. The host must be a numeric IP literal on our subnet; it must never
|
||||
* be resolved via DNS (see merge request !1682 discussion). The two SDK
|
||||
* branches (API 29+ {@code InetAddresses.isNumericAddress} vs. the legacy
|
||||
* pattern fallback) are exercised separately.
|
||||
*/
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = Build.VERSION_CODES.P) // default: legacy pattern-match branch
|
||||
public class SwapWorkflowActivityTest {
|
||||
|
||||
private SubnetUtils.SubnetInfo savedSubnetInfo;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
savedSubnetInfo = FDroidApp.subnetInfo;
|
||||
// pretend the device sits on 192.168.0.0/24(ipv4) 2001:db8:abcd::/64(ipv6)
|
||||
FDroidApp.subnetInfo = new SubnetUtils("192.168.0.1/24").getInfo();
|
||||
FDroidApp.subnet6Info = new SubnetUtils6("2001:db8:abcd::/64").getInfo();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
FDroidApp.subnetInfo = savedSubnetInfo;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// legacy branch (API < 29, host.matches("[0-9.]+"))
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
public void testValidSwapUrlLegacy() {
|
||||
assertTrue(SwapWorkflowActivity.isSwapUrl("192.168.0.50", 8888));
|
||||
assertTrue(SwapWorkflowActivity.isSwapUrl("2001:db8:abcd::1234", 8888));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOutOfSubnetLegacy() {
|
||||
assertFalse(SwapWorkflowActivity.isSwapUrl("10.0.0.50", 8888));
|
||||
assertFalse(SwapWorkflowActivity.isSwapUrl("2010:db8:abcd::1234", 8888));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHostnameIsNeverResolvedLegacy() {
|
||||
// a hostname must be rejected outright, never resolved via DNS
|
||||
assertFalse(SwapWorkflowActivity.isSwapUrl("example.com", 8888));
|
||||
assertFalse(SwapWorkflowActivity.isSwapUrl("localhost", 8888));
|
||||
}
|
||||
|
||||
// removed testIpv6IsRejectedLegacy because ipv6 addresses are now supported
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// modern branch (API 29+, android.net.InetAddresses.isNumericAddress)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@Config(sdk = 30)
|
||||
public void testValidSwapUrlModern() {
|
||||
assertTrue(SwapWorkflowActivity.isSwapUrl("192.168.0.50", 8888));
|
||||
assertTrue(SwapWorkflowActivity.isSwapUrl("2001:db8:abcd::1234", 8888));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = 30)
|
||||
public void testOutOfSubnetModern() {
|
||||
assertFalse(SwapWorkflowActivity.isSwapUrl("10.0.0.50", 8888));
|
||||
assertFalse(SwapWorkflowActivity.isSwapUrl("2010:db8:abcd::1234", 8888));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = 30)
|
||||
public void testHostnameIsNeverResolvedModern() {
|
||||
// a hostname must be rejected outright, never resolved via DNS
|
||||
// several of these are considered numeric due to a Robolectric issue, but fail
|
||||
// anyway due to an IllegalArgumentException that happens further in the code
|
||||
assertFalse(SwapWorkflowActivity.isSwapUrl("foo", 8888));
|
||||
assertFalse(SwapWorkflowActivity.isSwapUrl("1", 8888));
|
||||
assertFalse(SwapWorkflowActivity.isSwapUrl("example.com", 8888));
|
||||
assertFalse(SwapWorkflowActivity.isSwapUrl("localhost", 8888));
|
||||
}
|
||||
|
||||
// removed testIpv6IsRejectedModern because ipv6 addresses are now supported
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// port and null-safety (independent of the numeric-address branch)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
public void testPrivilegedPortIsRejected() {
|
||||
assertFalse(SwapWorkflowActivity.isSwapUrl("192.168.0.50", 80));
|
||||
assertFalse(SwapWorkflowActivity.isSwapUrl("192.168.0.50", 1023));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullHostIsRejected() {
|
||||
assertFalse(SwapWorkflowActivity.isSwapUrl((String) null, 8888));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullUriIsRejected() {
|
||||
// regression: checkIfNewRepoOnSameWifi() may pass a null repo Uri
|
||||
assertFalse(SwapWorkflowActivity.isSwapUrl((Uri) null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUriWithoutHostIsRejected() {
|
||||
assertFalse(SwapWorkflowActivity.isSwapUrl(Uri.parse("mailto:swap@example.com")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidSwapUri() {
|
||||
assertTrue(SwapWorkflowActivity.isSwapUrl(
|
||||
Uri.parse("http://192.168.0.50:8888/fdroid/repo")));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user