taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

DonauNetworkClient.java (8159B)


      1 package net.taler.donauverificator.network;
      2 
      3 import android.net.Uri;
      4 import android.util.Log;
      5 
      6 import org.json.JSONArray;
      7 import org.json.JSONException;
      8 import org.json.JSONObject;
      9 
     10 import java.io.BufferedReader;
     11 import java.io.IOException;
     12 import java.io.InputStream;
     13 import java.io.InputStreamReader;
     14 import java.net.HttpURLConnection;
     15 import java.net.MalformedURLException;
     16 import java.net.URL;
     17 import java.nio.charset.StandardCharsets;
     18 import java.util.ArrayList;
     19 import java.util.Collections;
     20 import java.util.List;
     21 import java.util.zip.GZIPInputStream;
     22 import java.util.zip.InflaterInputStream;
     23 
     24 public final class DonauNetworkClient {
     25 
     26     private static final String TAG = "DonauNetworkClient";
     27     private static final int TIMEOUT_MS = 5000;
     28 
     29     private final boolean insecureScheme;
     30     private final String host;
     31     private final int port;
     32     private final List<String> authorityPathSegments;
     33 
     34     public DonauNetworkClient(boolean insecureScheme, String host, int port, List<String> authorityPathSegments) {
     35         this.insecureScheme = insecureScheme;
     36         this.host = host;
     37         this.port = port;
     38         if (authorityPathSegments == null || authorityPathSegments.isEmpty()) {
     39             this.authorityPathSegments = Collections.emptyList();
     40         } else {
     41             this.authorityPathSegments = Collections.unmodifiableList(new ArrayList<>(authorityPathSegments));
     42         }
     43     }
     44 
     45     public List<String> fetchSigningKeys() throws IOException, JSONException {
     46         URL url = buildUrl("keys");
     47         HttpURLConnection connection = openGetConnection(url);
     48         connection.setRequestProperty("Accept", "application/json");
     49         try {
     50             int status = connection.getResponseCode();
     51             if (status != HttpURLConnection.HTTP_OK) {
     52                 throw new HttpStatusException(status, "Unexpected status for /keys: " + status);
     53             }
     54             String body = readStream(connection);
     55             JSONObject json = new JSONObject(body);
     56             JSONArray signkeys = json.optJSONArray("signkeys");
     57             if (signkeys == null) {
     58                 return Collections.emptyList();
     59             }
     60             List<String> result = new ArrayList<>();
     61             for (int i = 0; i < signkeys.length(); i++) {
     62                 Object entry = signkeys.get(i);
     63                 String keyCandidate = extractSigningKey(entry);
     64                 if (keyCandidate != null) {
     65                     result.add(keyCandidate);
     66                 }
     67             }
     68             return result;
     69         } finally {
     70             connection.disconnect();
     71         }
     72     }
     73 
     74     public DonationStatement fetchDonationStatement(int year, String hashedDonorId) throws IOException, JSONException {
     75         URL url = buildUrl("donation-statement", String.valueOf(year), hashedDonorId);
     76         HttpURLConnection connection = openGetConnection(url);
     77         connection.setRequestProperty("Accept", "application/json");
     78         try {
     79             int status = connection.getResponseCode();
     80             if (status != HttpURLConnection.HTTP_OK) {
     81                 throw new HttpStatusException(status, "Unexpected status for /donation-statement: " + status);
     82             }
     83             String body = readStream(connection);
     84             JSONObject json = new JSONObject(body);
     85             String total = trimToNull(json.optString("total", null));
     86             String signature = trimToNull(json.optString("donation_statement_sig", null));
     87             String pub = trimToNull(json.optString("donau_pub", null));
     88             if (total == null || signature == null || pub == null) {
     89                 throw new JSONException("Incomplete donation statement response");
     90             }
     91             return new DonationStatement(total, signature, pub);
     92         } finally {
     93             connection.disconnect();
     94         }
     95     }
     96 
     97     private HttpURLConnection openGetConnection(URL url) throws IOException {
     98         HttpURLConnection connection = (HttpURLConnection) url.openConnection();
     99         connection.setRequestMethod("GET");
    100         connection.setConnectTimeout(TIMEOUT_MS);
    101         connection.setReadTimeout(TIMEOUT_MS);
    102         connection.setInstanceFollowRedirects(false);
    103         connection.setRequestProperty("Accept-Encoding", "gzip, deflate");
    104         return connection;
    105     }
    106 
    107     private URL buildUrl(String... extraSegments) throws MalformedURLException {
    108         if (host == null || host.isEmpty()) {
    109             throw new MalformedURLException("Missing host");
    110         }
    111         Uri.Builder builder = new Uri.Builder()
    112                 .scheme(insecureScheme ? "http" : "https")
    113                 .encodedAuthority(port != -1 ? host + ":" + port : host);
    114         for (String segment : authorityPathSegments) {
    115             if (segment != null && !segment.trim().isEmpty()) {
    116                 builder.appendPath(segment.trim());
    117             }
    118         }
    119         for (String segment : extraSegments) {
    120             builder.appendPath(segment);
    121         }
    122         return new URL(builder.build().toString());
    123     }
    124 
    125     private String readStream(HttpURLConnection connection) throws IOException {
    126         InputStream input = null;
    127         try {
    128             input = maybeWrap(connection.getInputStream(), connection.getHeaderField("Content-Encoding"));
    129             BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
    130             StringBuilder builder = new StringBuilder();
    131             String line;
    132             while ((line = reader.readLine()) != null) {
    133                 builder.append(line);
    134             }
    135             return builder.toString();
    136         } finally {
    137             if (input != null) {
    138                 try {
    139                     input.close();
    140                 } catch (IOException e) {
    141                     Log.w(TAG, "Failed to close input stream", e);
    142                 }
    143             }
    144         }
    145     }
    146 
    147     private InputStream maybeWrap(InputStream input, String encoding) throws IOException {
    148         if (encoding == null) {
    149             return input;
    150         }
    151         if ("gzip".equalsIgnoreCase(encoding)) {
    152             return new GZIPInputStream(input);
    153         }
    154         if ("deflate".equalsIgnoreCase(encoding)) {
    155             return new InflaterInputStream(input);
    156         }
    157         return input;
    158     }
    159 
    160     private static String extractSigningKey(Object entry) {
    161         if (entry instanceof JSONObject) {
    162             JSONObject object = (JSONObject) entry;
    163             String direct = trimToNull(object.optString("key", null));
    164             if (direct != null) {
    165                 return direct;
    166             }
    167             JSONObject keyObject = object.optJSONObject("key");
    168             if (keyObject != null) {
    169                 String nested = trimToNull(keyObject.optString("eddsa_pub", null));
    170                 if (nested != null) {
    171                     return nested;
    172                 }
    173             }
    174             return trimToNull(object.optString("eddsa_pub", null));
    175         }
    176         if (entry instanceof String) {
    177             return trimToNull((String) entry);
    178         }
    179         return null;
    180     }
    181 
    182     private static int extractYear(Object entry) {
    183         if (entry instanceof JSONObject) {
    184             JSONObject object = (JSONObject) entry;
    185             if (object.has("year")) {
    186                 try {
    187                     return object.getInt("year");
    188                 } catch (JSONException e) {
    189                     return Integer.MIN_VALUE;
    190                 }
    191             }
    192         }
    193         return Integer.MIN_VALUE;
    194     }
    195 
    196     private static String trimToNull(String value) {
    197         if (value == null) {
    198             return null;
    199         }
    200         String trimmed = value.trim();
    201         return trimmed.isEmpty() ? null : trimmed;
    202     }
    203 
    204     public record DonationStatement(String total, String signature, String publicKey) {
    205     }
    206 
    207     public static final class HttpStatusException extends IOException {
    208         private final int statusCode;
    209 
    210         public HttpStatusException(int statusCode, String message) {
    211             super(message);
    212             this.statusCode = statusCode;
    213         }
    214 
    215         public int getStatusCode() {
    216             return statusCode;
    217         }
    218     }
    219 }