Merge branch 'swap_fix' into 'master'

improve security of swap feature

See merge request fdroid/fdroidclient!1682
This commit is contained in:
Hans-Christoph Steiner
2026-07-08 19:15:30 +00:00
4 changed files with 348 additions and 51 deletions

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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()));
}
}

View File

@@ -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")));
}
}