From b92e1ff9475fbc442bebf9c005a8d94e504bb4d9 Mon Sep 17 00:00:00 2001 From: kuhy Date: Mon, 29 Apr 2024 16:21:27 +0200 Subject: [PATCH] Garmin protocol: add AGPS data checks --- .../garmin/file/GarminAgpsDataType.java | 25 ++++++ .../devices/garmin/file/GarminAgpsFile.java | 21 +++-- .../devices/garmin/http/EphemerisHandler.java | 22 ++++++ .../gadgetbridge/util/GBTarFile.java | 79 +++++++++++++++++++ 4 files changed, 135 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/file/GarminAgpsDataType.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBTarFile.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/file/GarminAgpsDataType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/file/GarminAgpsDataType.java new file mode 100644 index 000000000..7e8c7e23a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/file/GarminAgpsDataType.java @@ -0,0 +1,25 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.file; + +public enum GarminAgpsDataType { + GLONASS("CPE_GLO.BIN"), QZSS("CPE_QZSS.BIN"), GPS("CPE_GPS.BIN"), + GALILEO("CPE_GAL.BIN"); + + private final String fileName; + + GarminAgpsDataType(String fileName) { + this.fileName = fileName; + } + + public String getFileName() { + return fileName; + } + + public static boolean isValidAgpsDataFileName(String fileName) { + for (GarminAgpsDataType type: GarminAgpsDataType.values()) { + if (fileName.equals(type.fileName)) { + return true; + } + } + return false; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/file/GarminAgpsFile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/file/GarminAgpsFile.java index 273735e10..286523127 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/file/GarminAgpsFile.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/file/GarminAgpsFile.java @@ -3,15 +3,11 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.file; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils; +import nodomain.freeyourgadget.gadgetbridge.util.GBTarFile; public class GarminAgpsFile { private static final Logger LOG = LoggerFactory.getLogger(GarminAgpsFile.class); - public static final int TAR_MAGIC_BYTES_OFFSET = 257; - public static final byte[] TAR_MAGIC_BYTES = new byte[]{ - 'u', 's', 't', 'a', 'r', '\0' - }; private final byte[] tarBytes; public GarminAgpsFile(final byte[] tarBytes) { @@ -19,17 +15,18 @@ public class GarminAgpsFile { } public boolean isValid() { - if (!ArrayUtils.equals(tarBytes, TAR_MAGIC_BYTES, TAR_MAGIC_BYTES_OFFSET)) { + if (!GBTarFile.isTarFile(tarBytes)) { LOG.debug("Is not TAR file!"); return false; } - // TODO Add additional checks. - // Archive usually contains following files: - // CPE_GLO.BIN - // CPE_QZSS.BIN - // CPE_GPS.BIN - // CPE_GAL.BIN + final GBTarFile tarFile = new GBTarFile(tarBytes); + for (final String fileName: tarFile.listFileNames()) { + if (!GarminAgpsDataType.isValidAgpsDataFileName(fileName)) { + LOG.error("Unknown file in TAR archive: {}", fileName); + return false; + } + } return true; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/http/EphemerisHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/http/EphemerisHandler.java index 1b43e87a7..86a0eef9d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/http/EphemerisHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/http/EphemerisHandler.java @@ -8,13 +8,17 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Map; +import java.util.Objects; import java.util.concurrent.Callable; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.file.GarminAgpsDataType; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; +import nodomain.freeyourgadget.gadgetbridge.util.GBTarFile; public class EphemerisHandler { private static final Logger LOG = LoggerFactory.getLogger(EphemerisHandler.class); + private static final String QUERY_CONSTELLATIONS = "constellations"; private final GarminSupport deviceSupport; public EphemerisHandler(GarminSupport deviceSupport) { @@ -23,6 +27,10 @@ public class EphemerisHandler { public byte[] handleEphemerisRequest(final String path, final Map query) { try { + if (!query.containsKey(QUERY_CONSTELLATIONS)) { + LOG.debug("Query does not contain information about constellations; skipping request."); + return null; + } final File agpsFile = deviceSupport.getAgpsFile(); if (!agpsFile.exists() || !agpsFile.isFile()) { LOG.info("File with AGPS data does not exist."); @@ -30,6 +38,20 @@ public class EphemerisHandler { } try(InputStream agpsIn = new FileInputStream(agpsFile)) { final byte[] rawBytes = FileUtils.readAll(agpsIn, 1024 * 1024); // 1MB, they're usually ~60KB + final GBTarFile tarFile = new GBTarFile(rawBytes); + final String[] requestedConstellations = Objects.requireNonNull(query.get(QUERY_CONSTELLATIONS)).split(","); + for (final String constellation: requestedConstellations) { + try { + final GarminAgpsDataType garminAgpsDataType = GarminAgpsDataType.valueOf(constellation); + if (!tarFile.containsFile(garminAgpsDataType.getFileName())) { + LOG.error("AGPS archive is missing requested file: {}", garminAgpsDataType.getFileName()); + return null; + } + } catch (IllegalArgumentException e) { + LOG.error("Device requested unsupported AGPS data type: {}", constellation); + return null; + } + } LOG.info("Sending new AGPS data to the device."); return rawBytes; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBTarFile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBTarFile.java new file mode 100644 index 000000000..d2bd038a6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBTarFile.java @@ -0,0 +1,79 @@ +package nodomain.freeyourgadget.gadgetbridge.util; + +import org.bouncycastle.shaded.util.Arrays; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + + +public class GBTarFile { + private static final Logger LOG = LoggerFactory.getLogger(GBTarFile.class); + private final byte[] tarBytes; + public static final int TAR_MAGIC_BYTES_OFFSET = 257; + public static final byte[] TAR_MAGIC_BYTES = new byte[]{ + 'u', 's', 't', 'a', 'r', '\0' + }; + public static final int TAR_BLOCK_SIZE = 512; + public static final int TAR_HEADER_FILE_NAME_OFFSET = 0; + public static final int TAR_HEADER_FILE_NAME_LENGTH = 100; + public static final int TAR_HEADER_FILE_SIZE_OFFSET = 124; + public static final int TAR_HEADER_FILE_SIZE_LENGTH = 12; + + + public GBTarFile(byte[] tarBytes) { + this.tarBytes = tarBytes; + } + + public static boolean isTarFile(byte[] data) { + return ArrayUtils.equals(data, TAR_MAGIC_BYTES, TAR_MAGIC_BYTES_OFFSET); + } + + public List listFileNames() { + final List fileNames = new ArrayList<>(); + for (TarHeader header: listHeaders()) { + fileNames.add(header.fileName); + } + return fileNames; + } + + public boolean containsFile(String fileName) { + for (TarHeader header: listHeaders()) { + if (fileName.equals(header.fileName)) { + return true; + } + } + return false; + } + + private List listHeaders() { + final List headers = new ArrayList<>(); + int offset = 0; + while (ArrayUtils.equals(tarBytes, TAR_MAGIC_BYTES, offset + TAR_MAGIC_BYTES_OFFSET)) { + final TarHeader tarHeader = new TarHeader(Arrays.copyOfRange(tarBytes, offset, offset + TAR_BLOCK_SIZE)); + headers.add(tarHeader); + offset += (((tarHeader.fileSize + TAR_BLOCK_SIZE - 1) / TAR_BLOCK_SIZE) + 1) * TAR_BLOCK_SIZE; + } + return headers; + } + + private static class TarHeader { + final String fileName; + final int fileSize; + + public TarHeader(byte[] header) { + fileName = parseString(header, TAR_HEADER_FILE_NAME_OFFSET, TAR_HEADER_FILE_NAME_LENGTH); + fileSize = Integer.parseInt(parseString(header, TAR_HEADER_FILE_SIZE_OFFSET, TAR_HEADER_FILE_SIZE_LENGTH).trim(), 8); + } + + private static String parseString(final byte[] data, final int offset, final int maxLength) { + int length = 0; + while (length < maxLength && offset + length < data.length && data[offset + length] != 0) { + length++; + } + return new String(data, offset, length, StandardCharsets.US_ASCII); + } + } +}