Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 19 additions & 14 deletions gpslogger/src/main/java/com/mendhak/gpslogger/common/Systems.java
Original file line number Diff line number Diff line change
Expand Up @@ -326,28 +326,33 @@ else if(appThemeSetting.equalsIgnoreCase("light")){
* @return
*/
public static void startWorkManagerRequest(Class workerClass, HashMap<String, Object> dataMap, String tag) {
startWorkManagerRequest(workerClass, dataMap, tag, false);
}

public static void startWorkManagerRequest(Class workerClass, HashMap<String, Object> 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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.InternalAddresses;
import com.mendhak.gpslogger.common.slf4j.Logs;
import com.mendhak.gpslogger.loggers.csv.CSVFileLogger;
import com.mendhak.gpslogger.loggers.customurl.CustomUrlRequest;
Expand Down Expand Up @@ -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 = !InternalAddresses.isInternalUrl(url);
Systems.startWorkManagerRequest(CustomUrlWorker.class, dataMap, tag, requiresNetwork);
}

private String getFormattedTextblock(String textToFormat, SerializableLocation loc) throws Exception {
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
Loading