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 String accessToken;
41 private OkHttpClient httpClient;
43 // Used to create empty, in-memory key stores.
44 private static final char[] DEFAULT_KEYSTORE_PASSWORD = "123456".toCharArray();
45 private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
46 private static String version = "dev"; // updated in the static initializer
48 private static class BuildProperties {
49 public String version;
53 InputStream in = Client.class.getClassLoader().getResourceAsStream("properties.json");
55 InputStreamReader inr = new InputStreamReader(in);
56 version = Utils.serializer.fromJson(inr, BuildProperties.class).version;
60 public static Client generateClient() throws BytomException {
62 String coreURL = Configuration.getValue("bytom.api.url");
63 String accessToken = Configuration.getValue("client.access.token");
65 if (coreURL == null || coreURL.isEmpty()) {
66 coreURL = "http://127.0.0.1:9888";
69 return new Client(coreURL, accessToken);
72 public Client(Builder builder) throws ConfigurationException {
73 this.url = builder.url;
74 this.accessToken = builder.accessToken;
75 this.httpClient = buildHttpClient(builder);
79 * Create a new http Client object using the default development host URL.
81 public Client() throws BytomException {
86 * Create a new http Client object
88 * @param url the URL of the Chain Core or HSM
90 public Client(String url) throws BytomException {
91 this(new Builder().setUrl(url));
95 * Create a new http Client object
97 * @param url the URL of the Chain Core or HSM
98 * @param accessToken a Client API access token
100 public Client(String url, String accessToken) throws BytomException {
101 this(new Builder().setUrl(url).setAccessToken(accessToken));
105 * Perform a single HTTP POST request against the API for a specific action.
107 * @param action The requested API action
108 * @param body Body payload sent to the API as JSON
109 * @param tClass Type of object to be deserialized from the response JSON
110 * @return the result of the post request
111 * @throws BytomException
113 public <T> T request(String action, Object body, final Type tClass) throws BytomException {
114 ResponseCreator<T> rc =
115 new ResponseCreator<T>() {
116 public T create(Response response, Gson deserializer) throws IOException, BytomException {
117 JsonElement root = new JsonParser().parse(response.body().charStream());
118 JsonElement status = root.getAsJsonObject().get("status");
119 JsonElement data = root.getAsJsonObject().get("data");
120 if (status != null && status.toString().contains("fail")) {
121 throw new BytomException(root.getAsJsonObject().get("msg").toString());
122 } else if (data != null) {
123 return deserializer.fromJson(data, tClass);
125 return deserializer.fromJson(response.body().charStream(), tClass);
129 return post(action, body, rc);
133 * Perform a single HTTP POST request against the API for a specific action,
134 * ignoring the body of the response.
136 * @param action The requested API action
137 * @param body Body payload sent to the API as JSON
138 * @throws BytomException
140 public void request(String action, Object body) throws BytomException {
141 ResponseCreator<Object> rc =
142 new ResponseCreator<Object>() {
143 public Object create(Response response, Gson deserializer) throws IOException, BytomException {
144 JsonElement root = new JsonParser().parse(response.body().charStream());
145 JsonElement status = root.getAsJsonObject().get("status");
146 JsonElement data = root.getAsJsonObject().get("data");
147 if (status != null && status.toString().contains("fail")) {
148 throw new BytomException(root.getAsJsonObject().get("msg").toString());
153 post(action, body, rc);
157 * return the value of named as key from json
165 * @throws BytomException
167 public <T> T requestGet(String action, Object body, final String key, final Type tClass)
168 throws BytomException {
169 ResponseCreator<T> rc = new ResponseCreator<T>() {
170 public T create(Response response, Gson deserializer) throws IOException,
172 JsonElement root = new JsonParser().parse(response.body().charStream());
173 JsonElement status = root.getAsJsonObject().get("status");
174 JsonElement data = root.getAsJsonObject().get("data");
176 if (status != null && status.toString().contains("fail"))
177 throw new BytomException(root.getAsJsonObject().get("msg").toString());
178 else if (data != null)
179 return deserializer.fromJson(data.getAsJsonObject().get(key), tClass);
181 return deserializer.fromJson(response.body().charStream(), tClass);
184 return post(action, body, rc);
188 * Perform a single HTTP POST request against the API for a specific action.
189 * Use this method if you want batch semantics, i.e., the endpoint response
190 * is an array of valid objects interleaved with arrays, once corresponding to
193 * @param action The requested API action
194 * @param body Body payload sent to the API as JSON
195 * @param tClass Type of object to be deserialized from the response JSON
196 * @param eClass Type of error object to be deserialized from the response JSON
197 * @return the result of the post request
198 * @throws BytomException
200 public <T> BatchResponse<T> batchRequest(
201 String action, Object body, final Type tClass, final Type eClass) throws BytomException {
202 ResponseCreator<BatchResponse<T>> rc =
203 new ResponseCreator<BatchResponse<T>>() {
204 public BatchResponse<T> create(Response response, Gson deserializer)
205 throws BytomException, IOException {
206 return new BatchResponse<>(response, deserializer, tClass, eClass);
209 return post(action, body, rc);
213 * Perform a single HTTP POST request against the API for a specific action.
214 * Use this method if you want single-item semantics (creating single assets,
215 * building single transactions) but the API endpoint is implemented as a
218 * Because request bodies for batch calls do not share a consistent format,
219 * this method does not perform any automatic arrayification of outgoing
220 * parameters. Remember to arrayify your request objects where appropriate.
222 * @param action The requested API action
223 * @param body Body payload sent to the API as JSON
224 * @param tClass Type of object to be deserialized from the response JSON
225 * @return the result of the post request
226 * @throws BytomException
228 public <T> T singletonBatchRequest(
229 String action, Object body, final Type tClass, final Type eClass) throws BytomException {
230 ResponseCreator<T> rc =
231 new ResponseCreator<T>() {
232 public T create(Response response, Gson deserializer) throws BytomException, IOException {
233 BatchResponse<T> batch = new BatchResponse<>(response, deserializer, tClass, eClass);
235 List<APIException> errors = batch.errors();
236 if (errors.size() == 1) {
237 // This throw must occur within this lambda in order for APIClient's
238 // retry logic to take effect.
242 List<T> successes = batch.successes();
243 if (successes.size() == 1) {
244 return successes.get(0);
247 // We should never get here, unless there is a bug in either the SDK or
248 // API code, causing a non-singleton response.
250 throw new BytomException(
251 "Invalid singleton response, request ID "
252 + batch.response().headers().get("Bytom-Request-ID"));
254 throw new BytomException("Invalid singleton response.");
257 return post(action, body, rc);
261 * Returns true if a client access token stored in the client.
265 public boolean hasAccessToken() {
266 return this.accessToken != null && !this.accessToken.isEmpty();
270 * Returns the client access token (possibly null).
272 * @return the client access token
274 public String accessToken() {
278 public String getUrl() {
283 * Pins a public key to the HTTP client.
285 * @param provider certificate provider
286 * @param subjPubKeyInfoHash public key hash
288 public void pinCertificate(String provider, String subjPubKeyInfoHash) {
289 CertificatePinner cp =
290 new CertificatePinner.Builder().add(provider, subjPubKeyInfoHash).build();
291 this.httpClient.setCertificatePinner(cp);
295 * Sets the default connect timeout for new connections. A value of 0 means no timeout.
297 * @param timeout the number of time units for the default timeout
298 * @param unit the unit of time
300 public void setConnectTimeout(long timeout, TimeUnit unit) {
301 this.httpClient.setConnectTimeout(timeout, unit);
305 * Sets the default read timeout for new connections. A value of 0 means no timeout.
307 * @param timeout the number of time units for the default timeout
308 * @param unit the unit of time
310 public void setReadTimeout(long timeout, TimeUnit unit) {
311 this.httpClient.setReadTimeout(timeout, unit);
315 * Sets the default write timeout for new connections. A value of 0 means no timeout.
317 * @param timeout the number of time units for the default timeout
318 * @param unit the unit of time
320 public void setWriteTimeout(long timeout, TimeUnit unit) {
321 this.httpClient.setWriteTimeout(timeout, unit);
325 * Sets the proxy information for the HTTP client.
327 * @param proxy proxy object
329 public void setProxy(Proxy proxy) {
330 this.httpClient.setProxy(proxy);
334 * Defines an interface for deserializing HTTP responses into objects.
336 * @param <T> the type of object to return
338 public interface ResponseCreator<T> {
340 * Deserializes an HTTP response into a Java object of type T.
342 * @param response HTTP response object
343 * @param deserializer json deserializer
344 * @return an object of type T
345 * @throws BytomException
346 * @throws IOException
348 T create(Response response, Gson deserializer) throws BytomException, IOException;
352 * Builds and executes an HTTP Post request.
354 * @param path the path to the endpoint
355 * @param body the request body
356 * @param respCreator object specifying the response structure
357 * @return a response deserialized into type T
358 * @throws BytomException
360 private <T> T post(String path, Object body, ResponseCreator<T> respCreator)
361 throws BytomException {
363 RequestBody requestBody = RequestBody.create(this.JSON, Utils.serializer.toJson(body));
366 BytomException exception = null;
367 URL endpointURL = null;
370 endpointURL = new URL(url + "/" + path);
371 } catch (MalformedURLException e) {
375 Request.Builder builder =
376 new Request.Builder()
377 .header("User-Agent", "bytom-sdk-java/" + version)
379 .method("POST", requestBody);
380 if (hasAccessToken()) {
381 builder = builder.header("Authorization", buildCredentials());
383 req = builder.build();
385 Response resp = null;
390 resp = this.checkError(this.httpClient.newCall(req).execute());
391 object = respCreator.create(resp, Utils.serializer);
392 } catch (IOException e) {
399 private OkHttpClient buildHttpClient(Builder builder) throws ConfigurationException {
400 OkHttpClient httpClient = builder.baseHttpClient.clone();
403 if (builder.trustManagers != null) {
404 SSLContext sslContext = SSLContext.getInstance("TLS");
405 sslContext.init(builder.keyManagers, builder.trustManagers, null);
406 httpClient.setSslSocketFactory(sslContext.getSocketFactory());
408 } catch (GeneralSecurityException ex) {
409 throw new ConfigurationException("Unable to configure TLS", ex);
411 if (builder.readTimeoutUnit != null) {
412 httpClient.setReadTimeout(builder.readTimeout, builder.readTimeoutUnit);
414 if (builder.writeTimeoutUnit != null) {
415 httpClient.setWriteTimeout(builder.writeTimeout, builder.writeTimeoutUnit);
417 if (builder.connectTimeoutUnit != null) {
418 httpClient.setConnectTimeout(builder.connectTimeout, builder.connectTimeoutUnit);
420 if (builder.pool != null) {
421 httpClient.setConnectionPool(builder.pool);
423 if (builder.proxy != null) {
424 httpClient.setProxy(builder.proxy);
426 if (builder.cp != null) {
427 httpClient.setCertificatePinner(builder.cp);
433 private static final Random randomGenerator = new Random();
434 private static final int MAX_RETRIES = 10;
435 private static final int RETRY_BASE_DELAY_MILLIS = 40;
437 // the max amount of time cored leader election could take
438 private static final int RETRY_MAX_DELAY_MILLIS = 15000;
440 private static int retryDelayMillis(int retryAttempt) {
441 // Calculate the max delay as base * 2 ^ (retryAttempt - 1).
442 int max = RETRY_BASE_DELAY_MILLIS * (1 << (retryAttempt - 1));
443 max = Math.min(max, RETRY_MAX_DELAY_MILLIS);
445 // To incorporate jitter, use a pseudo random delay between [max/2, max] millis.
446 return randomGenerator.nextInt(max / 2) + max / 2 + 1;
449 private static final int[] RETRIABLE_STATUS_CODES = {
450 408, // Request Timeout
451 429, // Too Many Requests
452 500, // Internal Server Error
454 503, // Service Unavailable
455 504, // Gateway Timeout
456 509, // Bandwidth Limit Exceeded
459 private static boolean isRetriableStatusCode(int statusCode) {
460 for (int i = 0; i < RETRIABLE_STATUS_CODES.length; i++) {
461 if (RETRIABLE_STATUS_CODES[i] == statusCode) {
468 private Response checkError(Response response) throws BytomException {
470 String rid = response.headers().get("Bytom-Request-ID");
471 if (rid == null || rid.length() == 0) {
472 // Header field Bytom-Request-ID is set by the backend
473 // API server. If this field is set, then we can expect
474 // the body to be well-formed JSON. If it's not set,
475 // then we are probably talking to a gateway or proxy.
476 throw new ConnectivityException(response);
479 if ((response.code() / 100) != 2) {
482 Utils.serializer.fromJson(response.body().charStream(), APIException.class);
483 if (err.code != null) {
484 //err.requestId = rid;
485 err.statusCode = response.code();
488 } catch (IOException ex) {
489 //throw new JSONException("Unable to read body. " + ex.getMessage(), rid);
490 throw new JSONException("Unable to read body. ");
496 private String buildCredentials() {
499 if (hasAccessToken()) {
500 String[] parts = accessToken.split(":");
501 if (parts.length >= 1) {
504 if (parts.length >= 2) {
508 return Credentials.basic(user, pass);
512 * Overrides {@link Object#hashCode()}
514 * @return the hash code
517 public int hashCode() {
518 int code = this.url.hashCode();
519 if (this.hasAccessToken()) {
520 code = code * 31 + this.accessToken.hashCode();
526 * Overrides {@link Object#equals(Object)}
528 * @param o the object to compare
529 * @return a boolean specifying equality
532 public boolean equals(Object o) {
533 if (o == null) return false;
534 if (!(o instanceof Client)) return false;
536 Client other = (Client) o;
537 if (!this.url.equalsIgnoreCase(other.url)) {
540 return Objects.equals(this.accessToken, other.accessToken);
544 * A builder class for creating client objects
546 public static class Builder {
550 private OkHttpClient baseHttpClient;
551 private String accessToken;
552 private CertificatePinner cp;
553 private KeyManager[] keyManagers;
554 private TrustManager[] trustManagers;
555 private long connectTimeout;
556 private TimeUnit connectTimeoutUnit;
557 private long readTimeout;
558 private TimeUnit readTimeoutUnit;
559 private long writeTimeout;
560 private TimeUnit writeTimeoutUnit;
562 private ConnectionPool pool;
565 this.baseHttpClient = new OkHttpClient();
566 this.baseHttpClient.setFollowRedirects(false);
570 public Builder(Client client) {
571 this.baseHttpClient = client.httpClient.clone();
572 this.url = client.url;
573 this.accessToken = client.accessToken;
576 private void setDefaults() {
577 this.setReadTimeout(30, TimeUnit.SECONDS);
578 this.setWriteTimeout(30, TimeUnit.SECONDS);
579 this.setConnectTimeout(30, TimeUnit.SECONDS);
580 this.setConnectionPool(50, 2, TimeUnit.MINUTES);
583 public Builder setUrl(String url) {
589 * Sets the access token for the client
591 * @param accessToken The access token for the Chain Core or HSM
593 public Builder setAccessToken(String accessToken) {
594 this.accessToken = accessToken;
599 * Sets the client's certificate and key for TLS client authentication.
600 * PEM-encoded, RSA private keys adhering to PKCS#1 or PKCS#8 are supported.
602 * @param certStream input stream of PEM-encoded X.509 certificate
603 * @param keyStream input stream of PEM-encoded private key
605 public Builder setX509KeyPair(InputStream certStream, InputStream keyStream)
606 throws ConfigurationException {
607 try (PEMParser parser = new PEMParser(new InputStreamReader(keyStream))) {
608 // Extract certs from PEM-encoded input.
609 CertificateFactory factory = CertificateFactory.getInstance("X.509");
610 X509Certificate certificate = (X509Certificate) factory.generateCertificate(certStream);
612 // Parse the private key from PEM-encoded input.
613 Object obj = parser.readObject();
615 if (obj instanceof PEMKeyPair) {
616 // PKCS#1 Private Key found.
617 PEMKeyPair kp = (PEMKeyPair) obj;
618 info = kp.getPrivateKeyInfo();
619 } else if (obj instanceof PrivateKeyInfo) {
620 // PKCS#8 Private Key found.
621 info = (PrivateKeyInfo) obj;
623 throw new ConfigurationException("Unsupported private key provided.");
626 // Create a new key store and input the pair.
627 KeySpec spec = new PKCS8EncodedKeySpec(info.getEncoded());
628 KeyFactory kf = KeyFactory.getInstance("RSA");
629 KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
630 keyStore.load(null, DEFAULT_KEYSTORE_PASSWORD);
631 keyStore.setCertificateEntry("cert", certificate);
632 keyStore.setKeyEntry(
634 kf.generatePrivate(spec),
635 DEFAULT_KEYSTORE_PASSWORD,
636 new X509Certificate[]{certificate});
638 // Use key store to build a key manager.
639 KeyManagerFactory keyManagerFactory =
640 KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
641 keyManagerFactory.init(keyStore, DEFAULT_KEYSTORE_PASSWORD);
643 this.keyManagers = keyManagerFactory.getKeyManagers();
645 } catch (GeneralSecurityException | IOException ex) {
646 throw new ConfigurationException("Unable to store X.509 cert/key pair", ex);
651 * Sets the client's certificate and key for TLS client authentication.
653 * @param certPath file path to PEM-encoded X.509 certificate
654 * @param keyPath file path to PEM-encoded private key
656 public Builder setX509KeyPair(String certPath, String keyPath) throws ConfigurationException {
657 try (InputStream certStream =
658 new ByteArrayInputStream(Files.readAllBytes(Paths.get(certPath)));
659 InputStream keyStream =
660 new ByteArrayInputStream(Files.readAllBytes(Paths.get(keyPath)))) {
661 return setX509KeyPair(certStream, keyStream);
662 } catch (IOException ex) {
663 throw new ConfigurationException("Unable to store X509 cert/key pair", ex);
668 * Trusts the given CA certs, and no others. Use this if you are running
669 * your own CA, or are using a self-signed server certificate.
671 * @param is input stream of the certificates to trust, in PEM format.
673 public Builder setTrustedCerts(InputStream is) throws ConfigurationException {
675 // Extract certs from PEM-encoded input.
676 CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
677 Collection<? extends Certificate> certificates =
678 certificateFactory.generateCertificates(is);
679 if (certificates.isEmpty()) {
680 throw new IllegalArgumentException("expected non-empty set of trusted certificates");
683 // Create a new key store and input the cert.
684 KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
685 keyStore.load(null, DEFAULT_KEYSTORE_PASSWORD);
687 for (Certificate certificate : certificates) {
688 String certificateAlias = Integer.toString(index++);
689 keyStore.setCertificateEntry(certificateAlias, certificate);
692 // Use key store to build an X509 trust manager.
693 KeyManagerFactory keyManagerFactory =
694 KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
695 keyManagerFactory.init(keyStore, DEFAULT_KEYSTORE_PASSWORD);
696 TrustManagerFactory trustManagerFactory =
697 TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
698 trustManagerFactory.init(keyStore);
699 TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
700 if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
701 throw new IllegalStateException(
702 "Unexpected default trust managers:" + Arrays.toString(trustManagers));
705 this.trustManagers = trustManagers;
707 } catch (GeneralSecurityException | IOException ex) {
708 throw new ConfigurationException("Unable to configure trusted CA certs", ex);
713 * Trusts the given CA certs, and no others. Use this if you are running
714 * your own CA, or are using a self-signed server certificate.
716 * @param path The path of a file containing certificates to trust, in PEM format.
718 public Builder setTrustedCerts(String path) throws ConfigurationException {
719 try (InputStream is = new FileInputStream(path)) {
720 return setTrustedCerts(is);
721 } catch (IOException ex) {
722 throw new ConfigurationException("Unable to configure trusted CA certs", ex);
727 * Sets the certificate pinner for the client
729 * @param provider certificate provider
730 * @param subjPubKeyInfoHash public key hash
732 public Builder pinCertificate(String provider, String subjPubKeyInfoHash) {
733 this.cp = new CertificatePinner.Builder().add(provider, subjPubKeyInfoHash).build();
738 * Sets the connect timeout for the client
740 * @param timeout the number of time units for the default timeout
741 * @param unit the unit of time
743 public Builder setConnectTimeout(long timeout, TimeUnit unit) {
744 this.connectTimeout = timeout;
745 this.connectTimeoutUnit = unit;
750 * Sets the read timeout for the client
752 * @param timeout the number of time units for the default timeout
753 * @param unit the unit of time
755 public Builder setReadTimeout(long timeout, TimeUnit unit) {
756 this.readTimeout = timeout;
757 this.readTimeoutUnit = unit;
762 * Sets the write timeout for the client
764 * @param timeout the number of time units for the default timeout
765 * @param unit the unit of time
767 public Builder setWriteTimeout(long timeout, TimeUnit unit) {
768 this.writeTimeout = timeout;
769 this.writeTimeoutUnit = unit;
774 * Sets the proxy for the client
778 public Builder setProxy(Proxy proxy) {
784 * Sets the connection pool for the client
786 * @param maxIdle the maximum number of idle http connections in the pool
787 * @param timeout the number of time units until an idle http connection in the pool is closed
788 * @param unit the unit of time
790 public Builder setConnectionPool(int maxIdle, long timeout, TimeUnit unit) {
791 this.pool = new ConnectionPool(maxIdle, unit.toMillis(timeout));
796 * Builds a client with all of the provided parameters.
798 public Client build() throws ConfigurationException {
799 return new Client(this);