1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2025-01-26 09:37:33 +01:00

Garmin: Implement etag for agps requests

This commit is contained in:
José Rebelo 2024-05-17 17:24:50 +01:00 committed by Daniele Gobbetti
parent ea1c1c808c
commit 88a9c81dcc
5 changed files with 225 additions and 98 deletions

View File

@ -11,14 +11,18 @@ import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http.GarminHttpRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http.GarminHttpResponse;
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class AgpsHandler { public class AgpsHandler {
@ -30,13 +34,13 @@ public class AgpsHandler {
this.deviceSupport = deviceSupport; this.deviceSupport = deviceSupport;
} }
public byte[] handleAgpsRequest(final String url, final String path, final Map<String, String> query) { public GarminHttpResponse handleAgpsRequest(final GarminHttpRequest request) {
saveKnownUrl(url); saveKnownUrl(request.getUrl());
try { try {
final DocumentFile agpsFile = deviceSupport.getAgpsFile(url); final DocumentFile agpsFile = deviceSupport.getAgpsFile(request.getUrl());
if (agpsFile == null) { if (agpsFile == null) {
LOG.warn("File with AGPS data for {} does not exist.", url); LOG.warn("File with AGPS data for {} does not exist.", request.getUrl());
return null; return null;
} }
try (InputStream agpsIn = deviceSupport.getContext().getContentResolver().openInputStream(agpsFile.getUri())) { try (InputStream agpsIn = deviceSupport.getContext().getContentResolver().openInputStream(agpsFile.getUri())) {
@ -45,18 +49,35 @@ public class AgpsHandler {
return null; return null;
} }
// Run some sanity checks on known agps file formats final GarminHttpResponse response = new GarminHttpResponse();
final byte[] rawBytes = FileUtils.readAll(agpsIn, 1024 * 1024); // 1MB, they're usually ~60KB final byte[] rawBytes = FileUtils.readAll(agpsIn, 1024 * 1024); // 1MB, they're usually ~60KB
final String fileHash = GB.hexdump(CheckSums.md5(rawBytes)).toLowerCase(Locale.ROOT);
final String etag = "\"" + fileHash + "\"";
response.getHeaders().put("etag", etag);
if (request.getHeaders().containsKey("if-none-match")) {
// Check checksum
final String ifNoneMatch = request.getHeaders().get("if-none-match");
LOG.debug("agps request hash = {}, file hash = {}", ifNoneMatch, etag);
if (etag.equals(ifNoneMatch)) {
response.setStatus(304);
return response;
}
}
// Run some sanity checks on known agps file formats
final GarminAgpsFile garminAgpsFile = new GarminAgpsFile(rawBytes); final GarminAgpsFile garminAgpsFile = new GarminAgpsFile(rawBytes);
if (query.containsKey(QUERY_CONSTELLATIONS)) { if (request.getQuery().containsKey(QUERY_CONSTELLATIONS)) {
final String[] requestedConstellations = Objects.requireNonNull(query.get(QUERY_CONSTELLATIONS)).split(","); final String[] requestedConstellations = Objects.requireNonNull(request.getQuery().get(QUERY_CONSTELLATIONS)).split(",");
if (!garminAgpsFile.isValidTar(requestedConstellations)) { if (!garminAgpsFile.isValidTar(requestedConstellations)) {
reportError(url); reportError(request.getUrl());
return null; return null;
} }
} else if (path.contains(("/rxnetworks/"))) { } else if (request.getPath().contains(("/rxnetworks/"))) {
if (!garminAgpsFile.isValidRxNetworks()) { if (!garminAgpsFile.isValidRxNetworks()) {
reportError(url); reportError(request.getUrl());
return null; return null;
} }
} else { } else {
@ -64,12 +85,23 @@ public class AgpsHandler {
return null; return null;
} }
LOG.info("Sending new AGPS data to the device from {}", agpsFile.getUri()); LOG.info("Sending new AGPS data (length: {}) to the device from {}", rawBytes.length, agpsFile.getUri());
return rawBytes;
if (request.getHeaders().containsKey("accept")) {
response.getHeaders().put("Content-Type", request.getHeaders().get("accept"));
} else {
response.getHeaders().put("Content-Type", "application/octet-stream");
}
response.setStatus(200);
response.setBody(rawBytes);
response.setOnDataSuccessfullySentListener(getOnDataSuccessfullySentListener(request.getUrl()));
return response;
} }
} catch (final IOException e) { } catch (final IOException e) {
LOG.error("Unable to obtain AGPS data", e); LOG.error("Unable to obtain AGPS data", e);
reportError(url); reportError(request.getUrl());
return null; return null;
} }
} }

View File

@ -0,0 +1,64 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiHttpService;
import nodomain.freeyourgadget.gadgetbridge.util.HttpUtils;
public class GarminHttpRequest {
private final GdiHttpService.HttpService.RawRequest rawRequest;
private final String url;
private final String path;
private final Map<String, String> query;
private final Map<String, String> headers;
public GarminHttpRequest(final GdiHttpService.HttpService.RawRequest rawRequest) {
this.rawRequest = rawRequest;
final URL netUrl;
try {
netUrl = new URL(rawRequest.getUrl());
} catch (final MalformedURLException e) {
throw new IllegalArgumentException("Failed to parse url", e);
}
this.url = rawRequest.getUrl();
this.path = netUrl.getPath();
this.query = HttpUtils.urlQueryParameters(netUrl);
this.headers = headersToMap(rawRequest.getHeaderList());
}
public GdiHttpService.HttpService.RawRequest getRawRequest() {
return rawRequest;
}
public String getUrl() {
return url;
}
public String getPath() {
return path;
}
public Map<String, String> getQuery() {
return query;
}
public Map<String, String> getHeaders() {
return headers;
}
private static Map<String, String> headersToMap(final List<GdiHttpService.HttpService.Header> headers) {
final Map<String, String> ret = new HashMap<>();
for (final GdiHttpService.HttpService.Header header : headers) {
ret.put(header.getKey().toLowerCase(Locale.ROOT), header.getValue());
}
return ret;
}
}

View File

@ -0,0 +1,41 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Callable;
public class GarminHttpResponse {
private int status = 200;
private final Map<String, String> headers = new LinkedHashMap<>();
private byte[] body = new byte[0];
private Callable<Void> onDataSuccessfullySentListener;
public int getStatus() {
return status;
}
public void setStatus(final int status) {
this.status = status;
}
public Map<String, String> getHeaders() {
return headers;
}
public byte[] getBody() {
return body;
}
public void setBody(final byte[] body) {
this.body = body;
}
public Callable<Void> getOnDataSuccessfullySentListener() {
return onDataSuccessfullySentListener;
}
public void setOnDataSuccessfullySentListener(final Callable<Void> onDataSuccessfullySentListener) {
this.onDataSuccessfullySentListener = onDataSuccessfullySentListener;
}
}

View File

@ -1,36 +1,23 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http; package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Callable;
import java.util.zip.GZIPOutputStream; import java.util.zip.GZIPOutputStream;
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiHttpService; import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiHttpService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.agps.AgpsHandler; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.agps.AgpsHandler;
import nodomain.freeyourgadget.gadgetbridge.util.HttpUtils;
public class HttpHandler { public class HttpHandler {
private static final Logger LOG = LoggerFactory.getLogger(HttpHandler.class); private static final Logger LOG = LoggerFactory.getLogger(HttpHandler.class);
private static final Gson GSON = new GsonBuilder()
//.serializeNulls()
.create();
private final AgpsHandler agpsHandler; private final AgpsHandler agpsHandler;
public HttpHandler(GarminSupport deviceSupport) { public HttpHandler(GarminSupport deviceSupport) {
@ -54,74 +41,67 @@ public class HttpHandler {
} }
public GdiHttpService.HttpService.RawResponse handleRawRequest(final GdiHttpService.HttpService.RawRequest rawRequest) { public GdiHttpService.HttpService.RawResponse handleRawRequest(final GdiHttpService.HttpService.RawRequest rawRequest) {
// TODO Return status code 304 (Not Modified) when we don't have newer data and "if-none-match" is set. LOG.debug("Got rawRequest: {} - {}", rawRequest.getMethod(), rawRequest.getUrl());
final String urlString = rawRequest.getUrl();
LOG.debug("Got rawRequest: {} - {}", rawRequest.getMethod(), urlString);
final URL url; final GarminHttpRequest request = new GarminHttpRequest(rawRequest);
try {
url = new URL(urlString);
} catch (final MalformedURLException e) {
LOG.error("Failed to parse url", e);
return null;
}
final String path = url.getPath(); final GarminHttpResponse response;
final Map<String, String> query = HttpUtils.urlQueryParameters(url); if (request.getPath().startsWith("/weather/")) {
final Map<String, String> requestHeaders = headersToMap(rawRequest.getHeaderList()); LOG.info("Got weather request for {}", request.getPath());
response = WeatherHandler.handleWeatherRequest(request);
if (path.startsWith("/weather/")) { } else if (request.getPath().startsWith("/ephemeris/")) {
LOG.info("Got weather request for {}", path); LOG.info("Got AGPS request for {}", request.getPath());
final Object weatherData = WeatherHandler.handleWeatherRequest(path, query); response = agpsHandler.handleAgpsRequest(request);
if (weatherData == null) {
return null;
}
final String json = GSON.toJson(weatherData);
LOG.debug("Weather response: {}", json);
return createRawResponse(rawRequest, json.getBytes(StandardCharsets.UTF_8), "application/json", null);
} else if (path.startsWith("/ephemeris/")) {
LOG.info("Got AGPS request for {}", path);
final byte[] agpsData = agpsHandler.handleAgpsRequest(urlString, path, query);
if (agpsData == null) {
return null;
}
LOG.debug("Successfully obtained AGPS data (length: {})", agpsData.length);
final String contentType = requestHeaders.containsKey("accept") ? requestHeaders.get("accept") : "application/octet-stream";
return createRawResponse(rawRequest, agpsData, contentType, agpsHandler.getOnDataSuccessfullySentListener(urlString));
} else { } else {
LOG.warn("Unhandled path {}", urlString); LOG.warn("Unhandled path {}", request.getPath());
response = null;
}
if (response == null) {
return null; return null;
} }
LOG.debug("Http response status={}", response.getStatus());
return createRawResponse(request, response);
} }
private static GdiHttpService.HttpService.RawResponse createRawResponse( private static GdiHttpService.HttpService.RawResponse createRawResponse(
final GdiHttpService.HttpService.RawRequest rawRequest, final GarminHttpRequest request,
final byte[] data, final GarminHttpResponse response
final String contentType, ) {
final Callable<Void> onDataSuccessfullySentListener final List<GdiHttpService.HttpService.Header> responseHeaders = new ArrayList<>();
) { for (final Map.Entry<String, String> h : response.getHeaders().entrySet()) {
if (rawRequest.hasUseDataXfer() && rawRequest.getUseDataXfer()) { responseHeaders.add(
GdiHttpService.HttpService.Header.newBuilder()
.setKey(h.getKey())
.setValue(h.getValue())
.build()
);
}
if (response.getStatus() == 200 && request.getRawRequest().hasUseDataXfer() && request.getRawRequest().getUseDataXfer()) {
LOG.debug("Data will be returned using data_xfer"); LOG.debug("Data will be returned using data_xfer");
int id = DataTransferHandler.registerData(data); int id = DataTransferHandler.registerData(response.getBody());
if (onDataSuccessfullySentListener != null) { if (response.getOnDataSuccessfullySentListener() != null) {
DataTransferHandler.addOnDataSuccessfullySentListener(id, onDataSuccessfullySentListener); DataTransferHandler.addOnDataSuccessfullySentListener(id, response.getOnDataSuccessfullySentListener());
} }
return GdiHttpService.HttpService.RawResponse.newBuilder() return GdiHttpService.HttpService.RawResponse.newBuilder()
.setStatus(GdiHttpService.HttpService.Status.OK) .setStatus(GdiHttpService.HttpService.Status.OK)
.setHttpStatus(200) .setHttpStatus(response.getStatus())
.addAllHeader(responseHeaders)
.setXferData( .setXferData(
GdiHttpService.HttpService.DataTransferItem.newBuilder() GdiHttpService.HttpService.DataTransferItem.newBuilder()
.setId(id) .setId(id)
.setSize(data.length) .setSize(response.getBody().length)
.build() .build()
) )
.build(); .build();
} }
final Map<String, String> requestHeaders = headersToMap(rawRequest.getHeaderList());
final List<GdiHttpService.HttpService.Header> responseHeaders = new ArrayList<>();
final byte[] responseBody; final byte[] responseBody;
if ("gzip".equals(requestHeaders.get("accept-encoding"))) { if ("gzip".equals(request.getHeaders().get("accept-encoding"))) {
LOG.debug("Compressing response");
responseHeaders.add( responseHeaders.add(
GdiHttpService.HttpService.Header.newBuilder() GdiHttpService.HttpService.Header.newBuilder()
.setKey("Content-Encoding") .setKey("Content-Encoding")
@ -131,7 +111,7 @@ public class HttpHandler {
final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (GZIPOutputStream gzos = new GZIPOutputStream(baos)) { try (GZIPOutputStream gzos = new GZIPOutputStream(baos)) {
gzos.write(data); gzos.write(response.getBody());
gzos.finish(); gzos.finish();
gzos.flush(); gzos.flush();
responseBody = baos.toByteArray(); responseBody = baos.toByteArray();
@ -140,29 +120,14 @@ public class HttpHandler {
return null; return null;
} }
} else { } else {
responseBody = data; responseBody = response.getBody();
} }
responseHeaders.add(
GdiHttpService.HttpService.Header.newBuilder()
.setKey("Content-Type")
.setValue(contentType)
.build()
);
return GdiHttpService.HttpService.RawResponse.newBuilder() return GdiHttpService.HttpService.RawResponse.newBuilder()
.setStatus(GdiHttpService.HttpService.Status.OK) .setStatus(GdiHttpService.HttpService.Status.OK)
.setHttpStatus(200) .setHttpStatus(response.getStatus())
.setBody(ByteString.copyFrom(responseBody)) .setBody(ByteString.copyFrom(responseBody))
.addAllHeader(responseHeaders) .addAllHeader(responseHeaders)
.build(); .build();
} }
private static Map<String, String> headersToMap(final List<GdiHttpService.HttpService.Header> headers) {
final Map<String, String> ret = new HashMap<>();
for (final GdiHttpService.HttpService.Header header : headers) {
ret.put(header.getKey().toLowerCase(Locale.ROOT), header.getValue());
}
return ret;
}
} }

View File

@ -2,12 +2,16 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http;
import android.location.Location; import android.location.Location;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import net.e175.klaus.solarpositioning.DeltaT; import net.e175.klaus.solarpositioning.DeltaT;
import net.e175.klaus.solarpositioning.SPA; import net.e175.klaus.solarpositioning.SPA;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
@ -23,8 +27,15 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.Curre
public class WeatherHandler { public class WeatherHandler {
private static final Logger LOG = LoggerFactory.getLogger(WeatherHandler.class); private static final Logger LOG = LoggerFactory.getLogger(WeatherHandler.class);
private static final Gson GSON = new GsonBuilder()
//.serializeNulls()
.create();
// These get requested on connection at most every 5 minutes // These get requested on connection at most every 5 minutes
public static Object handleWeatherRequest(final String path, final Map<String, String> query) { public static GarminHttpResponse handleWeatherRequest(final GarminHttpRequest request) {
final String path = request.getPath();
final Map<String, String> query = request.getQuery();
final WeatherSpec weatherSpec = Weather.getInstance().getWeatherSpec(); final WeatherSpec weatherSpec = Weather.getInstance().getWeatherSpec();
if (weatherSpec == null) { if (weatherSpec == null) {
@ -32,6 +43,7 @@ public class WeatherHandler {
return null; return null;
} }
final Object weatherData;
switch (path) { switch (path) {
case "/weather/v2/forecast/day": { case "/weather/v2/forecast/day": {
final int lat = getQueryNum(query, "lat", 0); final int lat = getQueryNum(query, "lat", 0);
@ -46,7 +58,8 @@ public class WeatherHandler {
date.add(Calendar.DAY_OF_MONTH, 1); date.add(Calendar.DAY_OF_MONTH, 1);
ret.add(new WeatherForecastDay(date, weatherSpec.forecasts.get(i))); ret.add(new WeatherForecastDay(date, weatherSpec.forecasts.get(i)));
} }
return ret; weatherData = ret;
break;
} }
case "/weather/v2/forecast/hour": { case "/weather/v2/forecast/hour": {
final int lat = getQueryNum(query, "lat", 0); final int lat = getQueryNum(query, "lat", 0);
@ -60,7 +73,8 @@ public class WeatherHandler {
for (int i = 0; i < Math.min(duration, weatherSpec.hourly.size()); i++) { for (int i = 0; i < Math.min(duration, weatherSpec.hourly.size()); i++) {
ret.add(new WeatherForecastHour(weatherSpec.hourly.get(i))); ret.add(new WeatherForecastHour(weatherSpec.hourly.get(i)));
} }
return ret; weatherData = ret;
break;
} }
case "/weather/v2/current": { case "/weather/v2/current": {
final int lat = getQueryNum(query, "lat", 0); final int lat = getQueryNum(query, "lat", 0);
@ -68,13 +82,24 @@ public class WeatherHandler {
final String tempUnit = getQueryString(query, "tempUnit", "CELSIUS"); final String tempUnit = getQueryString(query, "tempUnit", "CELSIUS");
final String speedUnit = getQueryString(query, "speedUnit", "METERS_PER_SECOND"); final String speedUnit = getQueryString(query, "speedUnit", "METERS_PER_SECOND");
final String provider = getQueryString(query, "provider", "dci"); final String provider = getQueryString(query, "provider", "dci");
return new WeatherForecastCurrent(weatherSpec); weatherData = new WeatherForecastCurrent(weatherSpec);
break;
} }
default:
LOG.warn("Unknown weather path {}", path);
final GarminHttpResponse response = new GarminHttpResponse();
response.setStatus(404);
return response;
} }
LOG.warn("Unknown weather path {}", path); final String json = GSON.toJson(weatherData);
LOG.debug("Weather response: {}", json);
return null; final GarminHttpResponse response = new GarminHttpResponse();
response.setStatus(200);
response.setBody(json.getBytes(StandardCharsets.UTF_8));
response.getHeaders().put("Content-Type", "application/json");
return response;
} }
private static int getQueryNum(final Map<String, String> query, final String key, final int defaultValue) { private static int getQueryNum(final Map<String, String> query, final String key, final int defaultValue) {