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 (and, * through {@link #send}, the direct ABRP upload). * 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 { byte[] body = json == null ? null : json.toString().getBytes(StandardCharsets.UTF_8); return send(method, url, token == null ? null : "Bearer " + token, "application/json", body); } /** * The request underneath, also used for ABRP ({@link AbrpApi}): {@code authorization} is the * whole header value or null, {@code body} null for none. */ static Response send(String method, String url, String authorization, String contentType, byte[] body) throws Exception { return send(method, url, authorization, contentType, null, body); } /** As above, with a {@code Content-Encoding} ({@code "gzip"}) for the body, or null. */ static Response send(String method, String url, String authorization, String contentType, String contentEncoding, byte[] body) throws Exception { HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); try { connection.setRequestMethod(method); connection.setConnectTimeout(TIMEOUT_MS); connection.setReadTimeout(TIMEOUT_MS); if (authorization != null) connection.setRequestProperty("Authorization", authorization); if (body != null) { connection.setDoOutput(true); connection.setFixedLengthStreamingMode(body.length); connection.setRequestProperty("Content-Type", contentType); if (contentEncoding != null) connection.setRequestProperty("Content-Encoding", contentEncoding); 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 ""; } } }