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:
parent
ea1c1c808c
commit
88a9c81dcc
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
final GarminHttpRequest request,
|
||||
final GarminHttpResponse response
|
||||
) {
|
||||
if (rawRequest.hasUseDataXfer() && rawRequest.getUseDataXfer()) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
return null;
|
||||
final String json = GSON.toJson(weatherData);
|
||||
LOG.debug("Weather response: {}", json);
|
||||
|
||||
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) {
|
||||
|
Loading…
Reference in New Issue
Block a user