3 import com.google.gson.JsonElement;
4 import com.google.gson.JsonParser;
5 import io.bytom.common.*;
6 import io.bytom.exception.*;
7 import com.google.gson.Gson;
8 import com.squareup.okhttp.*;
9 import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
10 import org.bouncycastle.openssl.PEMKeyPair;
11 import org.bouncycastle.openssl.PEMParser;
13 import javax.net.ssl.*;
15 import java.lang.reflect.Type;
17 import java.nio.file.Files;
18 import java.nio.file.Paths;
19 import java.security.GeneralSecurityException;
20 import java.security.KeyFactory;
21 import java.security.KeyStore;
22 import java.security.cert.Certificate;
23 import java.security.cert.CertificateFactory;
24 import java.security.cert.X509Certificate;
25 import java.security.spec.KeySpec;
26 import java.security.spec.PKCS8EncodedKeySpec;
28 import java.util.concurrent.TimeUnit;
29 import java.util.concurrent.atomic.AtomicInteger;
32 * The Client object contains all information necessary to
33 * perform an HTTP request against a remote API. Typically,
34 * an application will have a client that makes requests to
35 * a Chain Core, and a separate Client that makes requests
40 private AtomicInteger urlIndex;
41 private String accessToken;
42 private OkHttpClient httpClient;
44 // Used to create empty, in-memory key stores.
45 private static final char[] DEFAULT_KEYSTORE_PASSWORD = "password".toCharArray();
46 private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
47 private static String version = "dev"; // updated in the static initializer
49 private static class BuildProperties {
50 public String version;
54 InputStream in = Client.class.getClassLoader().getResourceAsStream("properties.json");
56 InputStreamReader inr = new InputStreamReader(in);
57 version = Utils.serializer.fromJson(inr, BuildProperties.class).version;
61 public Client(Builder builder) throws ConfigurationException {
62 this.urlIndex = new AtomicInteger(0);
63 this.url = builder.url;
64 this.accessToken = builder.accessToken;
65 this.httpClient = buildHttpClient(builder);
69 * Create a new http Client object using the default development host URL.
71 public Client() throws BytomException {
76 * Create a new http Client object
78 * @param url the URL of the Chain Core or HSM
80 public Client(String url) throws BytomException {
81 this(new Builder().setUrl(url));
85 * Create a new http Client object
87 * @param url the URL of the Chain Core or HSM
88 * @param accessToken a Client API access token
90 public Client(String url, String accessToken) throws BytomException {
91 this(new Builder().setUrl(url).setAccessToken(accessToken));
95 * Perform a single HTTP POST request against the API for a specific action.
97 * @param action The requested API action
98 * @param body Body payload sent to the API as JSON
99 * @param tClass Type of object to be deserialized from the response JSON
100 * @return the result of the post request
101 * @throws BytomException
103 public <T> T request(String action, Object body, final Type tClass) throws BytomException {
104 ResponseCreator<T> rc =
105 new ResponseCreator<T>() {
106 public T create(Response response, Gson deserializer) throws IOException, BytomException {
107 JsonElement root = new JsonParser().parse(response.body().charStream());
108 JsonElement status = root.getAsJsonObject().get("status");
109 JsonElement data = root.getAsJsonObject().get("data");
110 if (status != null && status.toString().contains("fail")) {
111 throw new BytomException(root.getAsJsonObject().get("msg").toString());
112 } else if (data != null) {
113 return deserializer.fromJson(data, tClass);
115 return deserializer.fromJson(response.body().charStream(), tClass);
119 return post(action, body, rc);
123 * Perform a single HTTP POST request against the API for a specific action,
124 * ignoring the body of the response.
126 * @param action The requested API action
127 * @param body Body payload sent to the API as JSON
128 * @throws BytomException
130 public void request(String action, Object body) throws BytomException {
131 ResponseCreator<Object> rc =
132 new ResponseCreator<Object>() {
133 public Object create(Response response, Gson deserializer) throws IOException, BytomException {
134 JsonElement root = new JsonParser().parse(response.body().charStream());
135 JsonElement status = root.getAsJsonObject().get("status");
136 JsonElement data = root.getAsJsonObject().get("data");
137 if (status != null && status.toString().contains("fail")) {
138 throw new BytomException(root.getAsJsonObject().get("msg").toString());
143 post(action, body, rc);
147 * return the value of named as key from json
155 * @throws BytomException
157 public <T> T requestGet(String action, Object body, final String key, final Type tClass)
158 throws BytomException {
159 ResponseCreator<T> rc = new ResponseCreator<T>() {
160 public T create(Response response, Gson deserializer) throws IOException,
162 JsonElement root = new JsonParser().parse(response.body().charStream());
163 JsonElement status = root.getAsJsonObject().get("status");
164 JsonElement data = root.getAsJsonObject().get("data");
166 if (status != null && status.toString().contains("fail"))
167 throw new BytomException(root.getAsJsonObject().get("msg").toString());
168 else if (data != null)
169 return deserializer.fromJson(data.getAsJsonObject().get(key), tClass);
171 return deserializer.fromJson(response.body().charStream(), tClass);
174 return post(action, body, rc);
178 * Perform a single HTTP POST request against the API for a specific action.
179 * Use this method if you want batch semantics, i.e., the endpoint response
180 * is an array of valid objects interleaved with arrays, once corresponding to
183 * @param action The requested API action
184 * @param body Body payload sent to the API as JSON
185 * @param tClass Type of object to be deserialized from the response JSON
186 * @param eClass Type of error object to be deserialized from the response JSON
187 * @return the result of the post request
188 * @throws BytomException
190 public <T> BatchResponse<T> batchRequest(
191 String action, Object body, final Type tClass, final Type eClass) throws BytomException {
192 ResponseCreator<BatchResponse<T>> rc =
193 new ResponseCreator<BatchResponse<T>>() {
194 public BatchResponse<T> create(Response response, Gson deserializer)
195 throws BytomException, IOException {
196 return new BatchResponse<>(response, deserializer, tClass, eClass);
199 return post(action, body, rc);
203 * Perform a single HTTP POST request against the API for a specific action.
204 * Use this method if you want single-item semantics (creating single assets,
205 * building single transactions) but the API endpoint is implemented as a
208 * Because request bodies for batch calls do not share a consistent format,
209 * this method does not perform any automatic arrayification of outgoing
210 * parameters. Remember to arrayify your request objects where appropriate.
212 * @param action The requested API action
213 * @param body Body payload sent to the API as JSON
214 * @param tClass Type of object to be deserialized from the response JSON
215 * @return the result of the post request
216 * @throws BytomException
218 public <T> T singletonBatchRequest(
219 String action, Object body, final Type tClass, final Type eClass) throws BytomException {
220 ResponseCreator<T> rc =
221 new ResponseCreator<T>() {
222 public T create(Response response, Gson deserializer) throws BytomException, IOException {
223 BatchResponse<T> batch = new BatchResponse<>(response, deserializer, tClass, eClass);
225 List<APIException> errors = batch.errors();
226 if (errors.size() == 1) {
227 // This throw must occur within this lambda in order for APIClient's
228 // retry logic to take effect.
232 List<T> successes = batch.successes();
233 if (successes.size() == 1) {
234 return successes.get(0);
237 // We should never get here, unless there is a bug in either the SDK or
238 // API code, causing a non-singleton response.
240 throw new BytomException(
241 "Invalid singleton response, request ID "
242 + batch.response().headers().get("Bytom-Request-ID"));
244 throw new BytomException("Invalid singleton response.");
247 return post(action, body, rc);
251 * Returns true if a client access token stored in the client.
255 public boolean hasAccessToken() {
256 return this.accessToken != null && !this.accessToken.isEmpty();
260 * Returns the client access token (possibly null).
262 * @return the client access token
264 public String accessToken() {
269 * Pins a public key to the HTTP client.
271 * @param provider certificate provider
272 * @param subjPubKeyInfoHash public key hash
274 public void pinCertificate(String provider, String subjPubKeyInfoHash) {
275 CertificatePinner cp =
276 new CertificatePinner.Builder().add(provider, subjPubKeyInfoHash).build();
277 this.httpClient.setCertificatePinner(cp);
281 * Sets the default connect timeout for new connections. A value of 0 means no timeout.
283 * @param timeout the number of time units for the default timeout
284 * @param unit the unit of time
286 public void setConnectTimeout(long timeout, TimeUnit unit) {
287 this.httpClient.setConnectTimeout(timeout, unit);
291 * Sets the default read timeout for new connections. A value of 0 means no timeout.
293 * @param timeout the number of time units for the default timeout
294 * @param unit the unit of time
296 public void setReadTimeout(long timeout, TimeUnit unit) {
297 this.httpClient.setReadTimeout(timeout, unit);
301 * Sets the default write timeout for new connections. A value of 0 means no timeout.
303 * @param timeout the number of time units for the default timeout
304 * @param unit the unit of time
306 public void setWriteTimeout(long timeout, TimeUnit unit) {
307 this.httpClient.setWriteTimeout(timeout, unit);
311 * Sets the proxy information for the HTTP client.
313 * @param proxy proxy object
315 public void setProxy(Proxy proxy) {
316 this.httpClient.setProxy(proxy);
320 * Defines an interface for deserializing HTTP responses into objects.
322 * @param <T> the type of object to return
324 public interface ResponseCreator<T> {
326 * Deserializes an HTTP response into a Java object of type T.
328 * @param response HTTP response object
329 * @param deserializer json deserializer
330 * @return an object of type T
331 * @throws BytomException
332 * @throws IOException
334 T create(Response response, Gson deserializer) throws BytomException, IOException;
338 * Builds and executes an HTTP Post request.
340 * @param path the path to the endpoint
341 * @param body the request body
342 * @param respCreator object specifying the response structure
343 * @return a response deserialized into type T
344 * @throws BytomException
346 private <T> T post(String path, Object body, ResponseCreator<T> respCreator)
347 throws BytomException {
349 RequestBody requestBody = RequestBody.create(this.JSON, Utils.serializer.toJson(body));
352 BytomException exception = null;
353 URL endpointURL = null;
356 endpointURL = new URL(url + "/" + path);
357 } catch (MalformedURLException e) {
361 Request.Builder builder =
362 new Request.Builder()
363 .header("User-Agent", "bytom-sdk-java/" + version)
365 .method("POST", requestBody);
366 if (hasAccessToken()) {
367 builder = builder.header("Authorization", buildCredentials());
369 req = builder.build();
371 Response resp = null;
376 resp = this.checkError(this.httpClient.newCall(req).execute());
377 object = respCreator.create(resp, Utils.serializer);
378 } catch (IOException e) {
385 private OkHttpClient buildHttpClient(Builder builder) throws ConfigurationException {
386 OkHttpClient httpClient = builder.baseHttpClient.clone();
389 if (builder.trustManagers != null) {
390 SSLContext sslContext = SSLContext.getInstance("TLS");
391 sslContext.init(builder.keyManagers, builder.trustManagers, null);
392 httpClient.setSslSocketFactory(sslContext.getSocketFactory());
394 } catch (GeneralSecurityException ex) {
395 throw new ConfigurationException("Unable to configure TLS", ex);
397 if (builder.readTimeoutUnit != null) {
398 httpClient.setReadTimeout(builder.readTimeout, builder.readTimeoutUnit);
400 if (builder.writeTimeoutUnit != null) {
401 httpClient.setWriteTimeout(builder.writeTimeout, builder.writeTimeoutUnit);
403 if (builder.connectTimeoutUnit != null) {
404 httpClient.setConnectTimeout(builder.connectTimeout, builder.connectTimeoutUnit);
406 if (builder.pool != null) {
407 httpClient.setConnectionPool(builder.pool);
409 if (builder.proxy != null) {
410 httpClient.setProxy(builder.proxy);
412 if (builder.cp != null) {
413 httpClient.setCertificatePinner(builder.cp);
419 private static final Random randomGenerator = new Random();
420 private static final int MAX_RETRIES = 10;
421 private static final int RETRY_BASE_DELAY_MILLIS = 40;
423 // the max amount of time cored leader election could take
424 private static final int RETRY_MAX_DELAY_MILLIS = 15000;
426 private static int retryDelayMillis(int retryAttempt) {
427 // Calculate the max delay as base * 2 ^ (retryAttempt - 1).
428 int max = RETRY_BASE_DELAY_MILLIS * (1 << (retryAttempt - 1));
429 max = Math.min(max, RETRY_MAX_DELAY_MILLIS);
431 // To incorporate jitter, use a pseudo random delay between [max/2, max] millis.
432 return randomGenerator.nextInt(max / 2) + max / 2 + 1;
435 private static final int[] RETRIABLE_STATUS_CODES = {
436 408, // Request Timeout
437 429, // Too Many Requests
438 500, // Internal Server Error
440 503, // Service Unavailable
441 504, // Gateway Timeout
442 509, // Bandwidth Limit Exceeded
445 private static boolean isRetriableStatusCode(int statusCode) {
446 for (int i = 0; i < RETRIABLE_STATUS_CODES.length; i++) {
447 if (RETRIABLE_STATUS_CODES[i] == statusCode) {
454 private Response checkError(Response response) throws BytomException {
456 String rid = response.headers().get("Bytom-Request-ID");
457 if (rid == null || rid.length() == 0) {
458 // Header field Bytom-Request-ID is set by the backend
459 // API server. If this field is set, then we can expect
460 // the body to be well-formed JSON. If it's not set,
461 // then we are probably talking to a gateway or proxy.
462 throw new ConnectivityException(response);
465 if ((response.code() / 100) != 2) {
468 Utils.serializer.fromJson(response.body().charStream(), APIException.class);
469 if (err.code != null) {
470 //err.requestId = rid;
471 err.statusCode = response.code();
474 } catch (IOException ex) {
475 //throw new JSONException("Unable to read body. " + ex.getMessage(), rid);
476 throw new JSONException("Unable to read body. ");
482 private String buildCredentials() {
485 if (hasAccessToken()) {
486 String[] parts = accessToken.split(":");
487 if (parts.length >= 1) {
490 if (parts.length >= 2) {
494 return Credentials.basic(user, pass);
498 * Overrides {@link Object#hashCode()}
500 * @return the hash code
503 public int hashCode() {
504 int code = this.url.hashCode();
505 if (this.hasAccessToken()) {
506 code = code * 31 + this.accessToken.hashCode();
512 * Overrides {@link Object#equals(Object)}
514 * @param o the object to compare
515 * @return a boolean specifying equality
518 public boolean equals(Object o) {
519 if (o == null) return false;
520 if (!(o instanceof Client)) return false;
522 Client other = (Client) o;
523 if (!this.url.equalsIgnoreCase(other.url)) {
526 return Objects.equals(this.accessToken, other.accessToken);
530 * A builder class for creating client objects
532 public static class Builder {
536 private OkHttpClient baseHttpClient;
537 private String accessToken;
538 private CertificatePinner cp;
539 private KeyManager[] keyManagers;
540 private TrustManager[] trustManagers;
541 private long connectTimeout;
542 private TimeUnit connectTimeoutUnit;
543 private long readTimeout;
544 private TimeUnit readTimeoutUnit;
545 private long writeTimeout;
546 private TimeUnit writeTimeoutUnit;
548 private ConnectionPool pool;
551 this.baseHttpClient = new OkHttpClient();
552 this.baseHttpClient.setFollowRedirects(false);
556 public Builder(Client client) {
557 this.baseHttpClient = client.httpClient.clone();
558 this.url = client.url;
559 this.accessToken = client.accessToken;
562 private void setDefaults() {
563 this.setReadTimeout(30, TimeUnit.SECONDS);
564 this.setWriteTimeout(30, TimeUnit.SECONDS);
565 this.setConnectTimeout(30, TimeUnit.SECONDS);
566 this.setConnectionPool(50, 2, TimeUnit.MINUTES);
569 public Builder setUrl(String url) {
575 * Sets the access token for the client
577 * @param accessToken The access token for the Chain Core or HSM
579 public Builder setAccessToken(String accessToken) {
580 this.accessToken = accessToken;
585 * Sets the client's certificate and key for TLS client authentication.
586 * PEM-encoded, RSA private keys adhering to PKCS#1 or PKCS#8 are supported.
588 * @param certStream input stream of PEM-encoded X.509 certificate
589 * @param keyStream input stream of PEM-encoded private key
591 public Builder setX509KeyPair(InputStream certStream, InputStream keyStream)
592 throws ConfigurationException {
593 try (PEMParser parser = new PEMParser(new InputStreamReader(keyStream))) {
594 // Extract certs from PEM-encoded input.
595 CertificateFactory factory = CertificateFactory.getInstance("X.509");
596 X509Certificate certificate = (X509Certificate) factory.generateCertificate(certStream);
598 // Parse the private key from PEM-encoded input.
599 Object obj = parser.readObject();
601 if (obj instanceof PEMKeyPair) {
602 // PKCS#1 Private Key found.
603 PEMKeyPair kp = (PEMKeyPair) obj;
604 info = kp.getPrivateKeyInfo();
605 } else if (obj instanceof PrivateKeyInfo) {
606 // PKCS#8 Private Key found.
607 info = (PrivateKeyInfo) obj;
609 throw new ConfigurationException("Unsupported private key provided.");
612 // Create a new key store and input the pair.
613 KeySpec spec = new PKCS8EncodedKeySpec(info.getEncoded());
614 KeyFactory kf = KeyFactory.getInstance("RSA");
615 KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
616 keyStore.load(null, DEFAULT_KEYSTORE_PASSWORD);
617 keyStore.setCertificateEntry("cert", certificate);
618 keyStore.setKeyEntry(
620 kf.generatePrivate(spec),
621 DEFAULT_KEYSTORE_PASSWORD,
622 new X509Certificate[]{certificate});
624 // Use key store to build a key manager.
625 KeyManagerFactory keyManagerFactory =
626 KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
627 keyManagerFactory.init(keyStore, DEFAULT_KEYSTORE_PASSWORD);
629 this.keyManagers = keyManagerFactory.getKeyManagers();
631 } catch (GeneralSecurityException | IOException ex) {
632 throw new ConfigurationException("Unable to store X.509 cert/key pair", ex);
637 * Sets the client's certificate and key for TLS client authentication.
639 * @param certPath file path to PEM-encoded X.509 certificate
640 * @param keyPath file path to PEM-encoded private key
642 public Builder setX509KeyPair(String certPath, String keyPath) throws ConfigurationException {
643 try (InputStream certStream =
644 new ByteArrayInputStream(Files.readAllBytes(Paths.get(certPath)));
645 InputStream keyStream =
646 new ByteArrayInputStream(Files.readAllBytes(Paths.get(keyPath)))) {
647 return setX509KeyPair(certStream, keyStream);
648 } catch (IOException ex) {
649 throw new ConfigurationException("Unable to store X509 cert/key pair", ex);
654 * Trusts the given CA certs, and no others. Use this if you are running
655 * your own CA, or are using a self-signed server certificate.
657 * @param is input stream of the certificates to trust, in PEM format.
659 public Builder setTrustedCerts(InputStream is) throws ConfigurationException {
661 // Extract certs from PEM-encoded input.
662 CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
663 Collection<? extends Certificate> certificates =
664 certificateFactory.generateCertificates(is);
665 if (certificates.isEmpty()) {
666 throw new IllegalArgumentException("expected non-empty set of trusted certificates");
669 // Create a new key store and input the cert.
670 KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
671 keyStore.load(null, DEFAULT_KEYSTORE_PASSWORD);
673 for (Certificate certificate : certificates) {
674 String certificateAlias = Integer.toString(index++);
675 keyStore.setCertificateEntry(certificateAlias, certificate);
678 // Use key store to build an X509 trust manager.
679 KeyManagerFactory keyManagerFactory =
680 KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
681 keyManagerFactory.init(keyStore, DEFAULT_KEYSTORE_PASSWORD);
682 TrustManagerFactory trustManagerFactory =
683 TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
684 trustManagerFactory.init(keyStore);
685 TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
686 if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
687 throw new IllegalStateException(
688 "Unexpected default trust managers:" + Arrays.toString(trustManagers));
691 this.trustManagers = trustManagers;
693 } catch (GeneralSecurityException | IOException ex) {
694 throw new ConfigurationException("Unable to configure trusted CA certs", ex);
699 * Trusts the given CA certs, and no others. Use this if you are running
700 * your own CA, or are using a self-signed server certificate.
702 * @param path The path of a file containing certificates to trust, in PEM format.
704 public Builder setTrustedCerts(String path) throws ConfigurationException {
705 try (InputStream is = new FileInputStream(path)) {
706 return setTrustedCerts(is);
707 } catch (IOException ex) {
708 throw new ConfigurationException("Unable to configure trusted CA certs", ex);
713 * Sets the certificate pinner for the client
715 * @param provider certificate provider
716 * @param subjPubKeyInfoHash public key hash
718 public Builder pinCertificate(String provider, String subjPubKeyInfoHash) {
719 this.cp = new CertificatePinner.Builder().add(provider, subjPubKeyInfoHash).build();
724 * Sets the connect timeout for the client
726 * @param timeout the number of time units for the default timeout
727 * @param unit the unit of time
729 public Builder setConnectTimeout(long timeout, TimeUnit unit) {
730 this.connectTimeout = timeout;
731 this.connectTimeoutUnit = unit;
736 * Sets the read timeout for the client
738 * @param timeout the number of time units for the default timeout
739 * @param unit the unit of time
741 public Builder setReadTimeout(long timeout, TimeUnit unit) {
742 this.readTimeout = timeout;
743 this.readTimeoutUnit = unit;
748 * Sets the write timeout for the client
750 * @param timeout the number of time units for the default timeout
751 * @param unit the unit of time
753 public Builder setWriteTimeout(long timeout, TimeUnit unit) {
754 this.writeTimeout = timeout;
755 this.writeTimeoutUnit = unit;
760 * Sets the proxy for the client
764 public Builder setProxy(Proxy proxy) {
770 * Sets the connection pool for the client
772 * @param maxIdle the maximum number of idle http connections in the pool
773 * @param timeout the number of time units until an idle http connection in the pool is closed
774 * @param unit the unit of time
776 public Builder setConnectionPool(int maxIdle, long timeout, TimeUnit unit) {
777 this.pool = new ConnectionPool(maxIdle, unit.toMillis(timeout));
782 * Builds a client with all of the provided parameters.
784 public Client build() throws ConfigurationException {
785 return new Client(this);