1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-04 09:17:29 +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.Collections;
import java.util.List;
import java.util.Map;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.Callable;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences;
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.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class AgpsHandler {
@ -30,13 +34,13 @@ public class AgpsHandler {
this.deviceSupport = deviceSupport;
}
public byte[] handleAgpsRequest(final String url, final String path, final Map<String, String> query) {
saveKnownUrl(url);
public GarminHttpResponse handleAgpsRequest(final GarminHttpRequest request) {
saveKnownUrl(request.getUrl());
try {
final DocumentFile agpsFile = deviceSupport.getAgpsFile(url);
final DocumentFile agpsFile = deviceSupport.getAgpsFile(request.getUrl());
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;
}
try (InputStream agpsIn = deviceSupport.getContext().getContentResolver().openInputStream(agpsFile.getUri())) {
@ -45,18 +49,35 @@ public class AgpsHandler {
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 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);
if (query.containsKey(QUERY_CONSTELLATIONS)) {
final String[] requestedConstellations = Objects.requireNonNull(query.get(QUERY_CONSTELLATIONS)).split(",");
if (request.getQuery().containsKey(QUERY_CONSTELLATIONS)) {
final String[] requestedConstellations = Objects.requireNonNull(request.getQuery().get(QUERY_CONSTELLATIONS)).split(",");
if (!garminAgpsFile.isValidTar(requestedConstellations)) {
reportError(url);
reportError(request.getUrl());
return null;
}
} else if (path.contains(("/rxnetworks/"))) {
} else if (request.getPath().contains(("/rxnetworks/"))) {
if (!garminAgpsFile.isValidRxNetworks()) {
reportError(url);
reportError(request.getUrl());
return null;
}
} else {
@ -64,12 +85,23 @@ public class AgpsHandler {
return null;
}
LOG.info("Sending new AGPS data to the device from {}", agpsFile.getUri());
return rawBytes;
LOG.info("Sending new AGPS data (length: {}) to the device from {}", rawBytes.length, agpsFile.getUri());
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) {
LOG.error("Unable to obtain AGPS data", e);
reportError(url);
reportError(request.getUrl());
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;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.protobuf.ByteString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.zip.GZIPOutputStream;
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiHttpService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.agps.AgpsHandler;
import nodomain.freeyourgadget.gadgetbridge.util.HttpUtils;
public class HttpHandler {
private static final Logger LOG = LoggerFactory.getLogger(HttpHandler.class);
private static final Gson GSON = new GsonBuilder()
//.serializeNulls()
.create();
private final AgpsHandler agpsHandler;
public HttpHandler(GarminSupport deviceSupport) {
@ -54,74 +41,67 @@ public class HttpHandler {
}
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.
final String urlString = rawRequest.getUrl();
LOG.debug("Got rawRequest: {} - {}", rawRequest.getMethod(), urlString);
LOG.debug("Got rawRequest: {} - {}", rawRequest.getMethod(), rawRequest.getUrl());
final URL url;
try {
url = new URL(urlString);
} catch (final MalformedURLException e) {
LOG.error("Failed to parse url", e);
return null;
}
final GarminHttpRequest request = new GarminHttpRequest(rawRequest);
final String path = url.getPath();
final Map<String, String> query = HttpUtils.urlQueryParameters(url);
final Map<String, String> requestHeaders = headersToMap(rawRequest.getHeaderList());
if (path.startsWith("/weather/")) {
LOG.info("Got weather request for {}", path);
final Object weatherData = WeatherHandler.handleWeatherRequest(path, query);
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));
final GarminHttpResponse response;
if (request.getPath().startsWith("/weather/")) {
LOG.info("Got weather request for {}", request.getPath());
response = WeatherHandler.handleWeatherRequest(request);
} else if (request.getPath().startsWith("/ephemeris/")) {
LOG.info("Got AGPS request for {}", request.getPath());
response = agpsHandler.handleAgpsRequest(request);
} else {
LOG.warn("Unhandled path {}", urlString);
LOG.warn("Unhandled path {}", request.getPath());
response = null;
}
if (response == null) {
return null;
}
LOG.debug("Http response status={}", response.getStatus());
return createRawResponse(request, response);
}
private static GdiHttpService.HttpService.RawResponse createRawResponse(
final GdiHttpService.HttpService.RawRequest rawRequest,
final byte[] data,
final String contentType,
final Callable<Void> onDataSuccessfullySentListener
) {
if (rawRequest.hasUseDataXfer() && rawRequest.getUseDataXfer()) {
final GarminHttpRequest request,
final GarminHttpResponse response
) {
final List<GdiHttpService.HttpService.Header> responseHeaders = new ArrayList<>();
for (final Map.Entry<String, String> h : response.getHeaders().entrySet()) {
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");
int id = DataTransferHandler.registerData(data);
if (onDataSuccessfullySentListener != null) {
DataTransferHandler.addOnDataSuccessfullySentListener(id, onDataSuccessfullySentListener);
int id = DataTransferHandler.registerData(response.getBody());
if (response.getOnDataSuccessfullySentListener() != null) {
DataTransferHandler.addOnDataSuccessfullySentListener(id, response.getOnDataSuccessfullySentListener());
}
return GdiHttpService.HttpService.RawResponse.newBuilder()
.setStatus(GdiHttpService.HttpService.Status.OK)
.setHttpStatus(200)
.setHttpStatus(response.getStatus())
.addAllHeader(responseHeaders)
.setXferData(
GdiHttpService.HttpService.DataTransferItem.newBuilder()
.setId(id)
.setSize(data.length)
.setSize(response.getBody().length)
.build()
)
.build();
}
final Map<String, String> requestHeaders = headersToMap(rawRequest.getHeaderList());
final List<GdiHttpService.HttpService.Header> responseHeaders = new ArrayList<>();
final byte[] responseBody;
if ("gzip".equals(requestHeaders.get("accept-encoding"))) {
if ("gzip".equals(request.getHeaders().get("accept-encoding"))) {
LOG.debug("Compressing response");
responseHeaders.add(
GdiHttpService.HttpService.Header.newBuilder()
.setKey("Content-Encoding")
@ -131,7 +111,7 @@ public class HttpHandler {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (GZIPOutputStream gzos = new GZIPOutputStream(baos)) {
gzos.write(data);
gzos.write(response.getBody());
gzos.finish();
gzos.flush();
responseBody = baos.toByteArray();
@ -140,29 +120,14 @@ public class HttpHandler {
return null;
}
} else {
responseBody = data;
responseBody = response.getBody();
}
responseHeaders.add(
GdiHttpService.HttpService.Header.newBuilder()
.setKey("Content-Type")
.setValue(contentType)
.build()
);
return GdiHttpService.HttpService.RawResponse.newBuilder()
.setStatus(GdiHttpService.HttpService.Status.OK)
.setHttpStatus(200)
.setHttpStatus(response.getStatus())
.setBody(ByteString.copyFrom(responseBody))
.addAllHeader(responseHeaders)
.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 com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import net.e175.klaus.solarpositioning.DeltaT;
import net.e175.klaus.solarpositioning.SPA;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
@ -23,8 +27,15 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.Curre
public class WeatherHandler {
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
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();
if (weatherSpec == null) {
@ -32,6 +43,7 @@ public class WeatherHandler {
return null;
}
final Object weatherData;
switch (path) {
case "/weather/v2/forecast/day": {
final int lat = getQueryNum(query, "lat", 0);
@ -46,7 +58,8 @@ public class WeatherHandler {
date.add(Calendar.DAY_OF_MONTH, 1);
ret.add(new WeatherForecastDay(date, weatherSpec.forecasts.get(i)));
}
return ret;
weatherData = ret;
break;
}
case "/weather/v2/forecast/hour": {
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++) {
ret.add(new WeatherForecastHour(weatherSpec.hourly.get(i)));
}
return ret;
weatherData = ret;
break;
}
case "/weather/v2/current": {
final int lat = getQueryNum(query, "lat", 0);
@ -68,13 +82,24 @@ public class WeatherHandler {
final String tempUnit = getQueryString(query, "tempUnit", "CELSIUS");
final String speedUnit = getQueryString(query, "speedUnit", "METERS_PER_SECOND");
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) {