Code review fixes: app 2.9.0, server 2.2.0
Telemetry no longer loses queued batches. The server drops a bad value, or a sample without a plausible time, instead of rejecting the batch, and answers 200 with what it dropped. The app sends only values inside the server's ranges (speed as a magnitude, so reversing is fine) and no samples before the head unit's clock is set. Server: - Pairing hands out and accepts a token only after the device is saved; a failed save leaves the request pending. - A paired token serves only the installation id it was paired with (403 otherwise); the shared INGEST_TOKEN still serves any. - Wrong-password lockouts and open pairing requests are limited per client address (last X-Forwarded-For entry), so a stranger cannot lock the owner out; a car starting again replaces its own request. - ABRP forwarding skips samples stamped ahead of the server's clock, which used to stop it until a restart. App: - Settings written to plaintext during a keystore failure move back into encrypted storage; the stored installation id wins. - The installation id is derived from ANDROID_ID on first start, so a reinstall finds its history; a stored or imported id still wins. - The scanned config file is deleted after import (it holds the token). - GPS stops while live telemetry is off or the server unconfigured, and pauses while parked and idle, keeping the fix the car parked with. - Range estimates use the monotonic clock. - One HTTP helper (ServerHttp) for telemetry, sync and pairing. - Last Turkish strings translated; mise logs shows the NilsMG4 tags. Grafana reads InfluxDB with its own read-only token (new grafana-influx Secret, created by hand as described in k8s/README.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* One JSON request to the car server, shared by telemetry, consumption sync and pairing.
|
||||
* Blocking, with short timeouts — call it off the main thread.
|
||||
*
|
||||
* The token goes in the Authorization header, never the URL, and is never logged. The answer
|
||||
* body is read for every status (error bodies name the problem) and capped, so a wrong server
|
||||
* cannot fill memory.
|
||||
*/
|
||||
final class ServerHttp {
|
||||
|
||||
static final int TIMEOUT_MS = 8_000;
|
||||
private static final int MAX_BODY_BYTES = 64 * 1024;
|
||||
|
||||
/** Status and body of an answer. */
|
||||
static final class Response {
|
||||
final int status;
|
||||
final String body;
|
||||
|
||||
Response(int status, String body) {
|
||||
this.status = status;
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
boolean ok() { return status >= 200 && status < 300; }
|
||||
|
||||
/** The body for a log line: short, on one line. */
|
||||
String excerpt() {
|
||||
String text = body.replace('\n', ' ').trim();
|
||||
return text.length() > 300 ? text.substring(0, 300) + "…" : text;
|
||||
}
|
||||
}
|
||||
|
||||
private ServerHttp() { }
|
||||
|
||||
/**
|
||||
* Sends {@code json} (or nothing, when null) with {@code method} to {@code url}.
|
||||
* {@code token} may be null for the unauthenticated pairing calls.
|
||||
*/
|
||||
static Response request(String method, String url, String token, JSONObject json) throws Exception {
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
try {
|
||||
connection.setRequestMethod(method);
|
||||
connection.setConnectTimeout(TIMEOUT_MS);
|
||||
connection.setReadTimeout(TIMEOUT_MS);
|
||||
if (token != null) connection.setRequestProperty("Authorization", "Bearer " + token);
|
||||
if (json != null) {
|
||||
byte[] body = json.toString().getBytes(StandardCharsets.UTF_8);
|
||||
connection.setDoOutput(true);
|
||||
connection.setFixedLengthStreamingMode(body.length);
|
||||
connection.setRequestProperty("Content-Type", "application/json");
|
||||
try (OutputStream out = connection.getOutputStream()) { out.write(body); }
|
||||
}
|
||||
int status = connection.getResponseCode();
|
||||
return new Response(status, readBody(connection, status));
|
||||
} finally {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private static String readBody(HttpURLConnection connection, int status) {
|
||||
try {
|
||||
InputStream in = status < 400 ? connection.getInputStream() : connection.getErrorStream();
|
||||
if (in == null) return "";
|
||||
try (InputStream stream = in) {
|
||||
ByteArrayOutputStream buf = new ByteArrayOutputStream();
|
||||
byte[] chunk = new byte[4096];
|
||||
int n;
|
||||
while ((n = stream.read(chunk)) != -1 && buf.size() < MAX_BODY_BYTES) buf.write(chunk, 0, n);
|
||||
return buf.toString(StandardCharsets.UTF_8.name());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// The status is what matters; an unreadable body is just empty.
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user