From d06a9f0d865f9b7c40897d644adb33dce53efce9 Mon Sep 17 00:00:00 2001 From: Dominik Chlup Date: Fri, 19 Jun 2026 21:41:49 +0200 Subject: [PATCH 1/2] Internal custom URL no longer requires network --- .../com/mendhak/gpslogger/common/Systems.java | 33 +++++++++++-------- .../gpslogger/common/network/Networks.java | 16 +++++++++ .../senders/customurl/CustomUrlManager.java | 5 +-- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/gpslogger/src/main/java/com/mendhak/gpslogger/common/Systems.java b/gpslogger/src/main/java/com/mendhak/gpslogger/common/Systems.java index 3113d19a1..f789ab431 100644 --- a/gpslogger/src/main/java/com/mendhak/gpslogger/common/Systems.java +++ b/gpslogger/src/main/java/com/mendhak/gpslogger/common/Systems.java @@ -326,28 +326,33 @@ else if(appThemeSetting.equalsIgnoreCase("light")){ * @return */ public static void startWorkManagerRequest(Class workerClass, HashMap dataMap, String tag) { + startWorkManagerRequest(workerClass, dataMap, tag, false); + } + public static void startWorkManagerRequest(Class workerClass, HashMap dataMap, String tag, boolean requiresNetwork) { androidx.work.Data data = new Data.Builder().putAll(dataMap).build(); - NetworkRequest.Builder builder = new NetworkRequest.Builder(); - builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){ - builder.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); - } - if(PreferenceHelper.getInstance().shouldAutoSendOnWifiOnly()){ - builder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI); - } - NetworkRequest networkRequest = builder.build(); + Constraints.Builder constraintsBuilder = new Constraints.Builder(); + if (requiresNetwork) { + NetworkRequest.Builder builder = new NetworkRequest.Builder(); + builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + builder.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); + } + if (PreferenceHelper.getInstance().shouldAutoSendOnWifiOnly()) { + builder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI); + } + NetworkRequest networkRequest = builder.build(); - Constraints constraints = new Constraints.Builder() - .setRequiredNetworkRequest(networkRequest, PreferenceHelper.getInstance().shouldAutoSendOnWifiOnly() ? NetworkType.UNMETERED: NetworkType.CONNECTED) - .setRequiredNetworkType(PreferenceHelper.getInstance().shouldAutoSendOnWifiOnly() ? NetworkType.UNMETERED: NetworkType.CONNECTED) - .build(); + constraintsBuilder + .setRequiredNetworkRequest(networkRequest, PreferenceHelper.getInstance().shouldAutoSendOnWifiOnly() ? NetworkType.UNMETERED : NetworkType.CONNECTED) + .setRequiredNetworkType(PreferenceHelper.getInstance().shouldAutoSendOnWifiOnly() ? NetworkType.UNMETERED : NetworkType.CONNECTED); + } OneTimeWorkRequest workRequest = new OneTimeWorkRequest .Builder(workerClass) - .setConstraints(constraints) + .setConstraints(constraintsBuilder.build()) .setInitialDelay(1, java.util.concurrent.TimeUnit.SECONDS) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, java.util.concurrent.TimeUnit.SECONDS) .setInputData(data) diff --git a/gpslogger/src/main/java/com/mendhak/gpslogger/common/network/Networks.java b/gpslogger/src/main/java/com/mendhak/gpslogger/common/network/Networks.java index 606877d50..379d739ea 100644 --- a/gpslogger/src/main/java/com/mendhak/gpslogger/common/network/Networks.java +++ b/gpslogger/src/main/java/com/mendhak/gpslogger/common/network/Networks.java @@ -154,4 +154,20 @@ public static TrustManager getTrustManager(Context context) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, CertStoreException { return new LocalX509TrustManager(getKnownServersStore(context)); } + + public static boolean isInternalAddress(String url) { + if (url == null || url.isEmpty()) return false; + + try { + String host = android.net.Uri.parse(url).getHost(); + if (host == null) return false; + + return host.equalsIgnoreCase("localhost") + || host.equals("127.0.0.1") + || host.equals("::1") + || host.equals("[::1]"); + } catch (Exception e) { + return false; + } + } } diff --git a/gpslogger/src/main/java/com/mendhak/gpslogger/senders/customurl/CustomUrlManager.java b/gpslogger/src/main/java/com/mendhak/gpslogger/senders/customurl/CustomUrlManager.java index bfd9ccee6..36066621c 100644 --- a/gpslogger/src/main/java/com/mendhak/gpslogger/senders/customurl/CustomUrlManager.java +++ b/gpslogger/src/main/java/com/mendhak/gpslogger/senders/customurl/CustomUrlManager.java @@ -8,6 +8,7 @@ import com.mendhak.gpslogger.common.SerializableLocation; import com.mendhak.gpslogger.common.Strings; import com.mendhak.gpslogger.common.Systems; +import com.mendhak.gpslogger.common.network.Networks; import com.mendhak.gpslogger.common.slf4j.Logs; import com.mendhak.gpslogger.loggers.csv.CSVFileLogger; import com.mendhak.gpslogger.loggers.customurl.CustomUrlRequest; @@ -163,8 +164,8 @@ public void sendByHttp(String url, String method, String body, String headers, S put("callbackType", "customurl"); }}; - Systems.startWorkManagerRequest(CustomUrlWorker.class, dataMap, tag); - + boolean requiresNetwork = !Networks.isInternalAddress(url); + Systems.startWorkManagerRequest(CustomUrlWorker.class, dataMap, tag, requiresNetwork); } private String getFormattedTextblock(String textToFormat, SerializableLocation loc) throws Exception { From 9fb0e0f00298f1c0f0ac69aa2ab5ed08425faa33 Mon Sep 17 00:00:00 2001 From: Dominik Chlup Date: Tue, 30 Jun 2026 11:26:29 +0200 Subject: [PATCH 2/2] Extend detection to RFC 1918 private ranges --- .../common/network/InternalAddresses.java | 96 +++++++++++++++++++ .../gpslogger/common/network/Networks.java | 16 ---- .../senders/customurl/CustomUrlManager.java | 4 +- .../common/network/InternalAddressesTest.java | 66 +++++++++++++ 4 files changed, 164 insertions(+), 18 deletions(-) create mode 100644 gpslogger/src/main/java/com/mendhak/gpslogger/common/network/InternalAddresses.java create mode 100644 gpslogger/src/test/java/com/mendhak/gpslogger/common/network/InternalAddressesTest.java diff --git a/gpslogger/src/main/java/com/mendhak/gpslogger/common/network/InternalAddresses.java b/gpslogger/src/main/java/com/mendhak/gpslogger/common/network/InternalAddresses.java new file mode 100644 index 000000000..a2e970a5d --- /dev/null +++ b/gpslogger/src/main/java/com/mendhak/gpslogger/common/network/InternalAddresses.java @@ -0,0 +1,96 @@ +package com.mendhak.gpslogger.common.network; + +import java.net.URI; + +/** + * Checks whether a URL points at the local machine or a private network, based + * only on the literal host text. Hostnames such as example.com are not resolved + * via DNS and therefore return false. + * + * Used by CustomUrlManager to decide whether WorkManager needs network + * connectivity (internal URLs can run offline). + * + * Internal means localhost, IPv4 loopback (127.0.0.0/8, not part of RFC 1918) + * or the three private ranges from RFC 1918: + * https://datatracker.ietf.org/doc/html/rfc1918 + * (10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16). + */ +public final class InternalAddresses { + + private static final long NOT_IPV4 = -1L; + + private InternalAddresses() { + } + + /** + * Returns true if the host component of the URL is an internal address. + */ + public static boolean isInternalUrl(String url) { + if (url == null || url.isEmpty()) { + return false; + } + + String host; + try { + host = new URI(url).getHost(); + } catch (Exception e) { + return false; + } + + if (host == null || host.isEmpty()) { + return false; + } + if (host.equalsIgnoreCase("localhost")) { + return true; + } + return isPrivateIpv4(host); + } + + static boolean isPrivateIpv4(String host) { + long ip = parseIpv4(host); + if (ip == NOT_IPV4) { + return false; + } + int first = (int) ((ip >>> 24) & 0xFF); + int second = (int) ((ip >>> 16) & 0xFF); + + return first == 127 // loopback + || first == 10 // RFC 1918: 10.0.0.0/8 + || (first == 172 && second >= 16 && second <= 31) // RFC 1918: 172.16.0.0/12 + || (first == 192 && second == 168); // RFC 1918: 192.168.0.0/16 + } + + private static long parseIpv4(String host) { + long packed = 0; + int octet = 0; + int digits = 0; + int dots = 0; + + for (int i = 0, length = host.length(); i < length; i++) { + char c = host.charAt(i); + if (c == '.') { + if (digits == 0 || ++dots > 3) { + return NOT_IPV4; // reject empty octets such as "10..0.1" or trailing dots + } + packed = (packed << 8) | octet; + octet = 0; + digits = 0; + } else if (c >= '0' && c <= '9') { + if (++digits > 3) { + return NOT_IPV4; // octets have at most 3 digits, e.g. reject "0010.0.0.1" + } + octet = octet * 10 + (c - '0'); + if (octet > 255) { + return NOT_IPV4; // octet out of range, e.g. reject "256.0.0.1" + } + } else { + return NOT_IPV4; + } + } + + if (dots != 3 || digits == 0) { + return NOT_IPV4; // need exactly four non-empty octets + } + return (packed << 8) | octet; + } +} diff --git a/gpslogger/src/main/java/com/mendhak/gpslogger/common/network/Networks.java b/gpslogger/src/main/java/com/mendhak/gpslogger/common/network/Networks.java index 379d739ea..606877d50 100644 --- a/gpslogger/src/main/java/com/mendhak/gpslogger/common/network/Networks.java +++ b/gpslogger/src/main/java/com/mendhak/gpslogger/common/network/Networks.java @@ -154,20 +154,4 @@ public static TrustManager getTrustManager(Context context) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, CertStoreException { return new LocalX509TrustManager(getKnownServersStore(context)); } - - public static boolean isInternalAddress(String url) { - if (url == null || url.isEmpty()) return false; - - try { - String host = android.net.Uri.parse(url).getHost(); - if (host == null) return false; - - return host.equalsIgnoreCase("localhost") - || host.equals("127.0.0.1") - || host.equals("::1") - || host.equals("[::1]"); - } catch (Exception e) { - return false; - } - } } diff --git a/gpslogger/src/main/java/com/mendhak/gpslogger/senders/customurl/CustomUrlManager.java b/gpslogger/src/main/java/com/mendhak/gpslogger/senders/customurl/CustomUrlManager.java index 36066621c..60a9d5387 100644 --- a/gpslogger/src/main/java/com/mendhak/gpslogger/senders/customurl/CustomUrlManager.java +++ b/gpslogger/src/main/java/com/mendhak/gpslogger/senders/customurl/CustomUrlManager.java @@ -8,7 +8,7 @@ import com.mendhak.gpslogger.common.SerializableLocation; import com.mendhak.gpslogger.common.Strings; import com.mendhak.gpslogger.common.Systems; -import com.mendhak.gpslogger.common.network.Networks; +import com.mendhak.gpslogger.common.network.InternalAddresses; import com.mendhak.gpslogger.common.slf4j.Logs; import com.mendhak.gpslogger.loggers.csv.CSVFileLogger; import com.mendhak.gpslogger.loggers.customurl.CustomUrlRequest; @@ -164,7 +164,7 @@ public void sendByHttp(String url, String method, String body, String headers, S put("callbackType", "customurl"); }}; - boolean requiresNetwork = !Networks.isInternalAddress(url); + boolean requiresNetwork = !InternalAddresses.isInternalUrl(url); Systems.startWorkManagerRequest(CustomUrlWorker.class, dataMap, tag, requiresNetwork); } diff --git a/gpslogger/src/test/java/com/mendhak/gpslogger/common/network/InternalAddressesTest.java b/gpslogger/src/test/java/com/mendhak/gpslogger/common/network/InternalAddressesTest.java new file mode 100644 index 000000000..ac0bad43d --- /dev/null +++ b/gpslogger/src/test/java/com/mendhak/gpslogger/common/network/InternalAddressesTest.java @@ -0,0 +1,66 @@ +package com.mendhak.gpslogger.common.network; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@SmallTest +@RunWith(MockitoJUnitRunner.class) +public class InternalAddressesTest { + + @Test + public void isInternalUrl_WhenNullOrEmpty_ReturnsFalse() { + assertThat("Null URL is not internal", InternalAddresses.isInternalUrl(null), is(false)); + assertThat("Empty URL is not internal", InternalAddresses.isInternalUrl(""), is(false)); + } + + @Test + public void isInternalUrl_WhenInternalAddress_ReturnsTrue() { + assertThat("localhost is internal", InternalAddresses.isInternalUrl("http://localhost/test"), is(true)); + assertThat("127.0.0.1 is internal", InternalAddresses.isInternalUrl("http://127.0.0.1/test"), is(true)); + assertThat("10.0.0.0/8 is internal", InternalAddresses.isInternalUrl("http://10.0.0.1/test"), is(true)); + assertThat("172.16.5.2 is internal", InternalAddresses.isInternalUrl("http://172.16.5.2/test"), is(true)); + assertThat("172.16.0.0/12 is internal", InternalAddresses.isInternalUrl("http://172.31.255.255/test"), is(true)); + assertThat("192.168.0.0/16 is internal", InternalAddresses.isInternalUrl("http://192.168.1.65:8000/test"), is(true)); + } + + @Test + public void isInternalUrl_WhenPublicAddressOrHostname_ReturnsFalse() { + assertThat("Public address is not internal", InternalAddresses.isInternalUrl("http://8.8.8.8/test"), is(false)); + assertThat("Outside 172.16.0.0/12 is not internal", InternalAddresses.isInternalUrl("http://172.32.0.1/test"), is(false)); + assertThat("Hostname is not resolved, so not internal", InternalAddresses.isInternalUrl("http://example.com/test"), is(false)); + } + + @Test + public void isPrivateIpv4_WhenValidPrivateAddress_ReturnsTrue() { + assertThat("Loopback is private", InternalAddresses.isPrivateIpv4("127.0.0.1"), is(true)); + assertThat("10.0.0.0/8 is private", InternalAddresses.isPrivateIpv4("10.0.0.1"), is(true)); + assertThat("172.16.0.0/12 is private", InternalAddresses.isPrivateIpv4("172.16.5.2"), is(true)); + assertThat("192.168.0.0/16 is private", InternalAddresses.isPrivateIpv4("192.168.1.1"), is(true)); + } + + @Test + public void isPrivateIpv4_WhenMalformedLiteral_ReturnsFalse() { + assertThat("Empty octet is rejected", InternalAddresses.isPrivateIpv4("10..0.1"), is(false)); + assertThat("Trailing dot is rejected", InternalAddresses.isPrivateIpv4("10.0.0."), is(false)); + assertThat("Leading dot is rejected", InternalAddresses.isPrivateIpv4(".10.0.0"), is(false)); + assertThat("Only dots is rejected", InternalAddresses.isPrivateIpv4("10..."), is(false)); + assertThat("Octet out of range is rejected", InternalAddresses.isPrivateIpv4("10.0.0.256"), is(false)); + assertThat("Too few octets is rejected", InternalAddresses.isPrivateIpv4("10.0.0"), is(false)); + assertThat("Too many octets is rejected", InternalAddresses.isPrivateIpv4("10.0.0.1.5"), is(false)); + assertThat("Empty string is rejected", InternalAddresses.isPrivateIpv4(""), is(false)); + } + + @Test + public void isPrivateIpv4_WhenOctetHasTooManyDigits_ReturnsFalse() { + assertThat("Zero-padded first octet is rejected", InternalAddresses.isPrivateIpv4("0010.0.0.1"), is(false)); + assertThat("Zero-padded middle octet is rejected", InternalAddresses.isPrivateIpv4("172.0016.0.1"), is(false)); + assertThat("Zero-padded last octet is rejected", InternalAddresses.isPrivateIpv4("192.168.1.0001"), is(false)); + assertThat("Four-digit octet is rejected", InternalAddresses.isPrivateIpv4("1270.0.0.1"), is(false)); + } +}