io.crums.client.Client Maven / Gradle / Ivy
/*
* Copyright 2020 Babak Farhang
*/
package io.crums.client;
import java.io.IOException;
import java.io.PrintStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.ByteBuffer;
import java.net.http.HttpClient.Version;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;
import io.crums.util.json.simple.JSONObject;
import io.crums.util.json.simple.parser.JSONParser;
import io.crums.util.json.simple.parser.ParseException;
import io.crums.util.IntegralStrings;
import io.crums.util.Lists;
import io.crums.util.main.Args;
import io.crums.util.main.TablePrint;
import io.crums.model.Constants;
import io.crums.model.CrumRecord;
import io.crums.model.TreeRef;
import io.crums.model.json.CrumTrailParser;
import io.crums.model.json.TreeRefParser;
/**
* The crums REST client.
*
* Usage
*
* This following does not require setup:
*
*
* - {@linkplain #getBeacon()}
*
*
* More typically there are hashes to be witnessed, their crumtrails to be retrieved.
* This a 2 step process. First the hashes are gathered. To set a single hash invoke
* one of the following:
*
*
* - {@linkplain #setHash(byte[])}
* - {@linkplain #setHash(ByteBuffer)}
* - {@linkplain #setHash(String)}
*
*
* To set multiple hashes invoke any of
*
*
* - {@linkplain #addHash(byte[])}
* - {@linkplain #addHash(ByteBuffer)}
* - {@linkplain #addHash(String)}
*
*
* for each hash. There are 2 choices for retrieving {@linkplain CrumRecord}s for the hashes:
*
*
* - {@linkplain #getCrumRecordsAsJson()}
* - {@linkplain #getCrumRecords()}
*
*
* Note the above 2 methods do not clear the instance's hashes. To do that
*
*
* - {@linkplain #clearHashes()}
*
*
* Also note that methods like this that change instance state (i.e. methods with side effects)
* return the instance itself for invocation chaining.
*
* Not Thread-safe
* Instances are not thread-safe. To access the server from multiple threads, use one instance per thread.
*
* TODO: add list tree-refs
*
*/
public class Client {
public final static String REST_ROOT_URL = "https://crums.io";
private HttpClient httpClient;
private final ArrayList hashes = new ArrayList<>();
/**
* @return the hash
*/
public String getHash() {
if (hashes.size() > 1)
throw new IllegalStateException(
hashes.isEmpty() ? "hash not set" : "ambiguous: " + hashes);
return hashes.isEmpty() ? null : hashes.get(0);
}
/**
* Sets the query hash to the given single value.
*
* @param hash 64-char hex
*/
public Client setHash(String hash) {
hash = hash.trim().toLowerCase();
checkHash(hash);
hashes.clear();
hashes.add(hash);
return this;
}
/**
* Sets the query hash to the given single value.
*
* @param hash 32 bytes
*/
public Client setHash(byte[] hash) {
checkHashWidth(hash.length);
hashes.clear();
hashes.add(IntegralStrings.toHex(hash));
return this;
}
/**
* Sets the query hash to the given single value.
*
* @param hash 32 bytes
*/
public Client setHash(ByteBuffer hash) {
checkHashWidth(hash.remaining());
hashes.clear();
hashes.add(IntegralStrings.toHex(hash));
return this;
}
/**
* Adds the given hash to the query.
*
* @param hash 64-char hex
*/
public Client addHash(String hash) {
hash = hash.trim().toLowerCase();
checkHash(hash);
hashes.add(hash);
return this;
}
/**
* Adds the given hash to the query.
*
* @param hash 32 bytes
*/
public Client addHash(byte[] hash) {
checkHashWidth(hash.length);
hashes.add(IntegralStrings.toHex(hash));
return this;
}
/**
* Adds the given hash to the query.
*
* @param hash 32 bytes
*/
public Client addHash(ByteBuffer hash) {
checkHashWidth(hash.remaining());
hashes.add(IntegralStrings.toHex(hash));
return this;
}
/**
* Clears the hashes set or added.
*/
public Client clearHashes() {
hashes.clear();
return this;
}
/**
* Returns the added or set hashes.
*
* @return an immutable snapshot of the added hashes
*/
public List getHashes() {
return Lists.readOnlyCopy(hashes);
}
private void checkHash(String hash) {
checkHashWidth(IntegralStrings.hexToBytes(hash).length);
}
private void checkHashWidth(int len) {
if (len != Constants.HASH_WIDTH)
throw new IllegalArgumentException("illegal hash width: " + len);
}
/**
* Returns the {@linkplain CrumRecord}s for the added {@linkplain #getHashes() hashes}.
*
* @return non-empty, immutable list of records
*/
public List getCrumRecords() throws ClientException {
return CrumTrailParser.INSTANCE.toCrumRecords( getCrumRecordsAsJson() );
}
private final static String STAMP_URL_PREFIX =
REST_ROOT_URL + Constants.STAMP_PATH + "?" + Constants.QS_HASH_NAME + "=";
/**
* Returns the server response as a json object.
*
* @return either a JSONObject
or JSONArray
*/
public Object getCrumRecordsAsJson() throws ClientException {
if (hashes.isEmpty())
throw new IllegalStateException("no hashes set");
String url;
{
Iterator ih = hashes.iterator();
StringBuilder str = new StringBuilder(STAMP_URL_PREFIX.length() + 65 * hashes.size() - 1);
str.append(STAMP_URL_PREFIX).append(ih.next());
while (ih.hasNext())
str.append(',').append(ih.next());
url = str.toString();
}
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(Duration.ofMinutes(1)).GET().build();
HttpResponse response;
try {
response = httpClient().send(request, BodyHandlers.ofString());
if (response.statusCode() == 200 || response.statusCode() == 202)
return new JSONParser().parse(response.body());
throw new ClientException(
response.statusCode() + " HTTP status from " + url + "\n" + response.body());
} catch (IOException iox) {
throw new ClientException("network error on attempting " + url, iox);
} catch (InterruptedException ix) {
throw new ClientException("interrupted on " + url, ix);
} catch (ParseException px) {
throw new ClientException("failed to parse response from " + url, px);
}
}
/**
* Returns a hash reference to the latest published tree at the server. Because its hash
* value cannot be computed in advance, its value acts as a beacon.
*/
public TreeRef getBeacon() throws ClientException {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BEACON_URL))
.timeout(Duration.ofMinutes(1)).GET().build();
try {
HttpResponse response = httpClient().send(request, BodyHandlers.ofString());
if (response.statusCode() != 200)
throw new ClientException(
response.statusCode() + " HTTP status from " + BEACON_URL + "\n" + response.body());
JSONObject jtree = (JSONObject) new JSONParser().parse(response.body());
return TreeRefParser.INSTANCE.toTreeRef(jtree);
} catch (IOException iox) {
throw new ClientException("network error on attempting " + BEACON_URL, iox);
} catch (InterruptedException ix) {
throw new ClientException("interrupted", ix);
} catch (ParseException px) {
throw new ClientException("failed to parse response", px);
}
}
private final static String BEACON_URL =
REST_ROOT_URL + Constants.API_PATH + Constants.BEACON_VERB;
private HttpClient httpClient() {
if (httpClient == null)
httpClient = HttpClient.newBuilder().version(Version.HTTP_1_1).build();
return httpClient;
}
public final static String STAMP = "stamp";
// wip
public static void main(String[] args) throws Exception {
if (Args.help(args)) {
printHelp();
return;
}
Client client = new Client();
String hash = Args.getValue(args, STAMP);
if (hash == null) {
System.err.println("No commands given.");
System.err.println();
printUsage(System.err);
return;
}
for (
StringTokenizer tokenizer = new StringTokenizer(hash, ",");
tokenizer.hasMoreTokens(); ) {
client.addHash(tokenizer.nextToken());
}
System.out.println();
System.out.println(client.getCrumRecordsAsJson());
System.out.println();
}
private static void printHelp() {
System.out.println();
System.out.println("Description:");
System.out.println();
System.out.println("HTTP REST client for crums.io");
System.out.println();
printUsage(System.out);
}
private static void printUsage(PrintStream out) {
out.println("Usage:");
out.println();
out.println("Arguments are specified as 'name=value' pairs.");
out.println();
TablePrint table = new TablePrint(out, 15, 60, 5);
table.setIndentation(1);
table.printRow(STAMP + "=*", "1 or more (comma-separated) SHA-256 hashes in hex", "R");
out.println();
}
}