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.apache.log4j.Logger;
10 import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
11 import org.bouncycastle.openssl.PEMKeyPair;
12 import org.bouncycastle.openssl.PEMParser;
14 import javax.net.ssl.*;
16 import java.lang.reflect.Type;
18 import java.nio.file.Files;
19 import java.nio.file.Paths;
20 import java.security.GeneralSecurityException;
21 import java.security.KeyFactory;
22 import java.security.KeyStore;
23 import java.security.cert.Certificate;
24 import java.security.cert.CertificateFactory;
25 import java.security.cert.X509Certificate;
26 import java.security.spec.KeySpec;
27 import java.security.spec.PKCS8EncodedKeySpec;
29 import java.util.concurrent.TimeUnit;
30 import java.util.concurrent.atomic.AtomicInteger;
33 * The Client object contains all information necessary to
34 * perform an HTTP request against a remote API. Typically,
35 * an application will have a client that makes requests to
36 * a Chain Core, and a separate Client that makes requests
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 = "123456".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 public static Logger logger = Logger.getLogger(Client.class);
51 private static class BuildProperties {
52 public String version;
56 InputStream in = Client.class.getClassLoader().getResourceAsStream("properties.json");
58 InputStreamReader inr = new InputStreamReader(in);
59 version = Utils.serializer.fromJson(inr, BuildProperties.class).version;
63 public static Client generateClient() throws BytomException {
65 String coreURL = Configuration.getValue("bytom.api.url");
66 String accessToken = Configuration.getValue("client.access.token");
68 if (coreURL == null || coreURL.isEmpty()) {
69 coreURL = "http://127.0.0.1:9888";
72 if (coreURL.endsWith("/")) {
73 //split the last char "/"
74 coreURL = coreURL.substring(0, coreURL.length()-1);
75 logger.info("check the coreURL is right.");
78 return new Client(coreURL, accessToken);
81 public Client(Builder builder) throws ConfigurationException {
82 if (builder.url.endsWith("/")) {
83 //split the last char "/"
84 builder.url = builder.url.substring(0, builder.url.length()-1);
86 this.url = builder.url;
87 this.accessToken = builder.accessToken;
88 this.httpClient = buildHttpClient(builder);
92 * Create a new http Client object using the default development host URL.
94 public Client() throws BytomException {
99 * Create a new http Client object
101 * @param url the URL of the Chain Core or HSM
103 public Client(String url) throws BytomException {
104 this(new Builder().setUrl(url));
108 * Create a new http Client object
110 * @param url the URL of the Chain Core or HSM
111 * @param accessToken a Client API access token
113 public Client(String url, String accessToken) throws BytomException {
114 this(new Builder().setUrl(url).setAccessToken(accessToken));
118 * Perform a single HTTP POST request against the API for a specific action.
120 * @param action The requested API action
121 * @param body Body payload sent to the API as JSON
122 * @param tClass Type of object to be deserialized from the response JSON
123 * @return the result of the post request
124 * @throws BytomException
126 public <T> T request(String action, Object body, final Type tClass) throws BytomException {
127 ResponseCreator<T> rc =
128 new ResponseCreator<T>() {
129 public T create(Response response, Gson deserializer) throws IOException, BytomException {
130 JsonElement root = new JsonParser().parse(response.body().charStream());
131 JsonElement status = root.getAsJsonObject().get("status");
132 JsonElement data = root.getAsJsonObject().get("data");
133 if (status != null && status.toString().contains("fail")) {
134 throw new BytomException(root.getAsJsonObject().get("msg").toString());
135 } else if (data != null) {
136 return deserializer.fromJson(data, tClass);
138 return deserializer.fromJson(response.body().charStream(), tClass);
142 return post(action, body, rc);
146 * Perform a single HTTP POST request against the API for a specific action,
147 * ignoring the body of the response.
149 * @param action The requested API action
150 * @param body Body payload sent to the API as JSON
151 * @throws BytomException
153 public void request(String action, Object body) throws BytomException {
154 ResponseCreator<Object> rc =
155 new ResponseCreator<Object>() {
156 public Object create(Response response, Gson deserializer) throws IOException, BytomException {
157 JsonElement root = new JsonParser().parse(response.body().charStream());
158 JsonElement status = root.getAsJsonObject().get("status");
159 JsonElement data = root.getAsJsonObject().get("data");
160 if (status != null && status.toString().contains("fail")) {
161 throw new BytomException(root.getAsJsonObject().get("msg").toString());
166 post(action, body, rc);
170 * return the value of named as key from json
178 * @throws BytomException
180 public <T> T requestGet(String action, Object body, final String key, final Type tClass)
181 throws BytomException {
182 ResponseCreator<T> rc = new ResponseCreator<T>() {
183 public T create(Response response, Gson deserializer) throws IOException,
185 JsonElement root = new JsonParser().parse(response.body().charStream());
186 JsonElement status = root.getAsJsonObject().get("status");
187 JsonElement data = root.getAsJsonObject().get("data");
189 if (status != null && status.toString().contains("fail"))
190 throw new BytomException(root.getAsJsonObject().get("msg").toString());
191 else if (data != null)
192 return deserializer.fromJson(data.getAsJsonObject().get(key), tClass);
194 return deserializer.fromJson(response.body().charStream(), tClass);
197 return post(action, body, rc);
201 * Perform a single HTTP POST request against the API for a specific action.
202 * Use this method if you want batch semantics, i.e., the endpoint response
203 * is an array of valid objects interleaved with arrays, once corresponding to
206 * @param action The requested API action
207 * @param body Body payload sent to the API as JSON
208 * @param tClass Type of object to be deserialized from the response JSON
209 * @param eClass Type of error object to be deserialized from the response JSON
210 * @return the result of the post request
211 * @throws BytomException
213 public <T> BatchResponse<T> batchRequest(
214 String action, Object body, final Type tClass, final Type eClass) throws BytomException {
215 ResponseCreator<BatchResponse<T>> rc =
216 new ResponseCreator<BatchResponse<T>>() {
217 public BatchResponse<T> create(Response response, Gson deserializer)
218 throws BytomException, IOException {
219 return new BatchResponse<T>(response, deserializer, tClass, eClass);
222 return post(action, body, rc);
226 * Perform a single HTTP POST request against the API for a specific action.
227 * Use this method if you want single-item semantics (creating single assets,
228 * building single transactions) but the API endpoint is implemented as a
231 * Because request bodies for batch calls do not share a consistent format,
232 * this method does not perform any automatic arrayification of outgoing
233 * parameters. Remember to arrayify your request objects where appropriate.
235 * @param action The requested API action
236 * @param body Body payload sent to the API as JSON
237 * @param tClass Type of object to be deserialized from the response JSON
238 * @return the result of the post request
239 * @throws BytomException
241 public <T> T singletonBatchRequest(
242 String action, Object body, final Type tClass, final Type eClass) throws BytomException {
243 ResponseCreator<T> rc =
244 new ResponseCreator<T>() {
245 public T create(Response response, Gson deserializer) throws BytomException, IOException {
246 BatchResponse<T> batch = new BatchResponse<>(response, deserializer, tClass, eClass);
248 List<APIException> errors = batch.errors();
249 if (errors.size() == 1) {
250 // This throw must occur within this lambda in order for APIClient's
251 // retry logic to take effect.
255 List<T> successes = batch.successes();
256 if (successes.size() == 1) {
257 return successes.get(0);
260 // We should never get here, unless there is a bug in either the SDK or
261 // API code, causing a non-singleton response.
263 throw new BytomException(
264 "Invalid singleton response, request ID "
265 + batch.response().headers().get("Bytom-Request-ID"));
267 throw new BytomException("Invalid singleton response.");
270 return post(action, body, rc);
274 * Returns true if a client access token stored in the client.
278 public boolean hasAccessToken() {
279 return this.accessToken != null && !this.accessToken.isEmpty();
283 * Returns the client access token (possibly null).
285 * @return the client access token
287 public String accessToken() {
291 public String getUrl() {
296 * Pins a public key to the HTTP client.
298 * @param provider certificate provider
299 * @param subjPubKeyInfoHash public key hash
301 public void pinCertificate(String provider, String subjPubKeyInfoHash) {
302 CertificatePinner cp =
303 new CertificatePinner.Builder().add(provider, subjPubKeyInfoHash).build();
304 this.httpClient.setCertificatePinner(cp);
308 * Sets the default connect timeout for new connections. A value of 0 means no timeout.
310 * @param timeout the number of time units for the default timeout
311 * @param unit the unit of time
313 public void setConnectTimeout(long timeout, TimeUnit unit) {
314 this.httpClient.setConnectTimeout(timeout, unit);
318 * Sets the default read timeout for new connections. A value of 0 means no timeout.
320 * @param timeout the number of time units for the default timeout
321 * @param unit the unit of time
323 public void setReadTimeout(long timeout, TimeUnit unit) {
324 this.httpClient.setReadTimeout(timeout, unit);
328 * Sets the default write timeout for new connections. A value of 0 means no timeout.
330 * @param timeout the number of time units for the default timeout
331 * @param unit the unit of time
333 public void setWriteTimeout(long timeout, TimeUnit unit) {
334 this.httpClient.setWriteTimeout(timeout, unit);
338 * Sets the proxy information for the HTTP client.
340 * @param proxy proxy object
342 public void setProxy(Proxy proxy) {
343 this.httpClient.setProxy(proxy);
347 * Defines an interface for deserializing HTTP responses into objects.
349 * @param <T> the type of object to return
351 public interface ResponseCreator<T> {
353 * Deserializes an HTTP response into a Java object of type T.
355 * @param response HTTP response object
356 * @param deserializer json deserializer
357 * @return an object of type T
358 * @throws BytomException
359 * @throws IOException
361 T create(Response response, Gson deserializer) throws BytomException, IOException;
365 * Builds and executes an HTTP Post request.
367 * @param path the path to the endpoint
368 * @param body the request body
369 * @param respCreator object specifying the response structure
370 * @return a response deserialized into type T
371 * @throws BytomException
373 private <T> T post(String path, Object body, ResponseCreator<T> respCreator)
374 throws BytomException {
376 RequestBody requestBody = RequestBody.create(this.JSON, Utils.serializer.toJson(body));
379 BytomException exception = null;
380 URL endpointURL = null;
383 endpointURL = new URL(url + "/" + path);
384 } catch (MalformedURLException e) {
388 Request.Builder builder =
389 new Request.Builder()
390 .header("User-Agent", "bytom-sdk-java/" + version)
392 .method("POST", requestBody);
393 if (hasAccessToken()) {
394 builder = builder.header("Authorization", buildCredentials());
396 req = builder.build();
398 Response resp = null;
403 resp = this.checkError(this.httpClient.newCall(req).execute());
404 object = respCreator.create(resp, Utils.serializer);
405 } catch (IOException e) {
412 private OkHttpClient buildHttpClient(Builder builder) throws ConfigurationException {
413 OkHttpClient httpClient = builder.baseHttpClient.clone();
416 if (builder.trustManagers != null) {
417 SSLContext sslContext = SSLContext.getInstance("TLS");
418 sslContext.init(builder.keyManagers, builder.trustManagers, null);
419 httpClient.setSslSocketFactory(sslContext.getSocketFactory());
421 } catch (GeneralSecurityException ex) {
422 throw new ConfigurationException("Unable to configure TLS", ex);
424 if (builder.readTimeoutUnit != null) {
425 httpClient.setReadTimeout(builder.readTimeout, builder.readTimeoutUnit);
427 if (builder.writeTimeoutUnit != null) {
428 httpClient.setWriteTimeout(builder.writeTimeout, builder.writeTimeoutUnit);
430 if (builder.connectTimeoutUnit != null) {
431 httpClient.setConnectTimeout(builder.connectTimeout, builder.connectTimeoutUnit);
433 if (builder.pool != null) {
434 httpClient.setConnectionPool(builder.pool);
436 if (builder.proxy != null) {
437 httpClient.setProxy(builder.proxy);
439 if (builder.cp != null) {
440 httpClient.setCertificatePinner(builder.cp);
446 private static final Random randomGenerator = new Random();
447 private static final int MAX_RETRIES = 10;
448 private static final int RETRY_BASE_DELAY_MILLIS = 40;
450 // the max amount of time cored leader election could take
451 private static final int RETRY_MAX_DELAY_MILLIS = 15000;
453 private static int retryDelayMillis(int retryAttempt) {
454 // Calculate the max delay as base * 2 ^ (retryAttempt - 1).
455 int max = RETRY_BASE_DELAY_MILLIS * (1 << (retryAttempt - 1));
456 max = Math.min(max, RETRY_MAX_DELAY_MILLIS);
458 // To incorporate jitter, use a pseudo random delay between [max/2, max] millis.
459 return randomGenerator.nextInt(max / 2) + max / 2 + 1;
462 private static final int[] RETRIABLE_STATUS_CODES = {
463 408, // Request Timeout
464 429, // Too Many Requests
465 500, // Internal Server Error
467 503, // Service Unavailable
468 504, // Gateway Timeout
469 509, // Bandwidth Limit Exceeded
472 private static boolean isRetriableStatusCode(int statusCode) {
473 for (int i = 0; i < RETRIABLE_STATUS_CODES.length; i++) {
474 if (RETRIABLE_STATUS_CODES[i] == statusCode) {
481 private Response checkError(Response response) throws BytomException {
483 String rid = response.headers().get("Bytom-Request-ID");
484 if (rid == null || rid.length() == 0) {
485 // Header field Bytom-Request-ID is set by the backend
486 // API server. If this field is set, then we can expect
487 // the body to be well-formed JSON. If it's not set,
488 // then we are probably talking to a gateway or proxy.
489 throw new ConnectivityException(response);
492 if ((response.code() / 100) != 2) {
495 Utils.serializer.fromJson(response.body().charStream(), APIException.class);
496 if (err.code != null) {
497 //err.requestId = rid;
498 err.statusCode = response.code();
501 } catch (IOException ex) {
502 //throw new JSONException("Unable to read body. " + ex.getMessage(), rid);
503 throw new JSONException("Unable to read body. ");
509 private String buildCredentials() {
512 if (hasAccessToken()) {
513 String[] parts = accessToken.split(":");
514 if (parts.length >= 1) {
517 if (parts.length >= 2) {
521 return Credentials.basic(user, pass);
525 * Overrides {@link Object#hashCode()}
527 * @return the hash code
530 public int hashCode() {
531 int code = this.url.hashCode();
532 if (this.hasAccessToken()) {
533 code = code * 31 + this.accessToken.hashCode();
539 * Overrides {@link Object#equals(Object)}
541 * @param o the object to compare
542 * @return a boolean specifying equality
545 public boolean equals(Object o) {
546 if (o == null) return false;
547 if (!(o instanceof Client)) return false;
549 Client other = (Client) o;
550 if (!this.url.equalsIgnoreCase(other.url)) {
553 return Objects.equals(this.accessToken, other.accessToken);
557 * A builder class for creating client objects
559 public static class Builder {
563 private OkHttpClient baseHttpClient;
564 private String accessToken;
565 private CertificatePinner cp;
566 private KeyManager[] keyManagers;
567 private TrustManager[] trustManagers;
568 private long connectTimeout;
569 private TimeUnit connectTimeoutUnit;
570 private long readTimeout;
571 private TimeUnit readTimeoutUnit;
572 private long writeTimeout;
573 private TimeUnit writeTimeoutUnit;
575 private ConnectionPool pool;
578 this.baseHttpClient = new OkHttpClient();
579 this.baseHttpClient.setFollowRedirects(false);
583 public Builder(Client client) {
584 this.baseHttpClient = client.httpClient.clone();
585 this.url = client.url;
586 this.accessToken = client.accessToken;
589 private void setDefaults() {
590 this.setReadTimeout(30, TimeUnit.SECONDS);
591 this.setWriteTimeout(30, TimeUnit.SECONDS);
592 this.setConnectTimeout(30, TimeUnit.SECONDS);
593 this.setConnectionPool(50, 2, TimeUnit.MINUTES);
596 public Builder setUrl(String url) {
602 * Sets the access token for the client
604 * @param accessToken The access token for the Chain Core or HSM
606 public Builder setAccessToken(String accessToken) {
607 this.accessToken = accessToken;
612 * Sets the client's certificate and key for TLS client authentication.
613 * PEM-encoded, RSA private keys adhering to PKCS#1 or PKCS#8 are supported.
615 * @param certStream input stream of PEM-encoded X.509 certificate
616 * @param keyStream input stream of PEM-encoded private key
618 public Builder setX509KeyPair(InputStream certStream, InputStream keyStream)
619 throws ConfigurationException {
620 try (PEMParser parser = new PEMParser(new InputStreamReader(keyStream))) {
621 // Extract certs from PEM-encoded input.
622 CertificateFactory factory = CertificateFactory.getInstance("X.509");
623 X509Certificate certificate = (X509Certificate) factory.generateCertificate(certStream);
625 // Parse the private key from PEM-encoded input.
626 Object obj = parser.readObject();
628 if (obj instanceof PEMKeyPair) {
629 // PKCS#1 Private Key found.
630 PEMKeyPair kp = (PEMKeyPair) obj;
631 info = kp.getPrivateKeyInfo();
632 } else if (obj instanceof PrivateKeyInfo) {
633 // PKCS#8 Private Key found.
634 info = (PrivateKeyInfo) obj;
636 throw new ConfigurationException("Unsupported private key provided.");
639 // Create a new key store and input the pair.
640 KeySpec spec = new PKCS8EncodedKeySpec(info.getEncoded());
641 KeyFactory kf = KeyFactory.getInstance("RSA");
642 KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
643 keyStore.load(null, DEFAULT_KEYSTORE_PASSWORD);
644 keyStore.setCertificateEntry("cert", certificate);
645 keyStore.setKeyEntry(
647 kf.generatePrivate(spec),
648 DEFAULT_KEYSTORE_PASSWORD,
649 new X509Certificate[]{certificate});
651 // Use key store to build a key manager.
652 KeyManagerFactory keyManagerFactory =
653 KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
654 keyManagerFactory.init(keyStore, DEFAULT_KEYSTORE_PASSWORD);
656 this.keyManagers = keyManagerFactory.getKeyManagers();
658 } catch (GeneralSecurityException | IOException ex) {
659 throw new ConfigurationException("Unable to store X.509 cert/key pair", ex);
664 * Sets the client's certificate and key for TLS client authentication.
666 * @param certPath file path to PEM-encoded X.509 certificate
667 * @param keyPath file path to PEM-encoded private key
669 public Builder setX509KeyPair(String certPath, String keyPath) throws ConfigurationException {
670 try (InputStream certStream =
671 new ByteArrayInputStream(Files.readAllBytes(Paths.get(certPath)));
672 InputStream keyStream =
673 new ByteArrayInputStream(Files.readAllBytes(Paths.get(keyPath)))) {
674 return setX509KeyPair(certStream, keyStream);
675 } catch (IOException ex) {
676 throw new ConfigurationException("Unable to store X509 cert/key pair", ex);
681 * Trusts the given CA certs, and no others. Use this if you are running
682 * your own CA, or are using a self-signed server certificate.
684 * @param is input stream of the certificates to trust, in PEM format.
686 public Builder setTrustedCerts(InputStream is) throws ConfigurationException {
688 // Extract certs from PEM-encoded input.
689 CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
690 Collection<? extends Certificate> certificates =
691 certificateFactory.generateCertificates(is);
692 if (certificates.isEmpty()) {
693 throw new IllegalArgumentException("expected non-empty set of trusted certificates");
696 // Create a new key store and input the cert.
697 KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
698 keyStore.load(null, DEFAULT_KEYSTORE_PASSWORD);
700 for (Certificate certificate : certificates) {
701 String certificateAlias = Integer.toString(index++);
702 keyStore.setCertificateEntry(certificateAlias, certificate);
705 // Use key store to build an X509 trust manager.
706 KeyManagerFactory keyManagerFactory =
707 KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
708 keyManagerFactory.init(keyStore, DEFAULT_KEYSTORE_PASSWORD);
709 TrustManagerFactory trustManagerFactory =
710 TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
711 trustManagerFactory.init(keyStore);
712 TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
713 if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
714 throw new IllegalStateException(
715 "Unexpected default trust managers:" + Arrays.toString(trustManagers));
718 this.trustManagers = trustManagers;
720 } catch (GeneralSecurityException | IOException ex) {
721 throw new ConfigurationException("Unable to configure trusted CA certs", ex);
726 * Trusts the given CA certs, and no others. Use this if you are running
727 * your own CA, or are using a self-signed server certificate.
729 * @param path The path of a file containing certificates to trust, in PEM format.
731 public Builder setTrustedCerts(String path) throws ConfigurationException {
732 try (InputStream is = new FileInputStream(path)) {
733 return setTrustedCerts(is);
734 } catch (IOException ex) {
735 throw new ConfigurationException("Unable to configure trusted CA certs", ex);
740 * Sets the certificate pinner for the client
742 * @param provider certificate provider
743 * @param subjPubKeyInfoHash public key hash
745 public Builder pinCertificate(String provider, String subjPubKeyInfoHash) {
746 this.cp = new CertificatePinner.Builder().add(provider, subjPubKeyInfoHash).build();
751 * Sets the connect timeout for the client
753 * @param timeout the number of time units for the default timeout
754 * @param unit the unit of time
756 public Builder setConnectTimeout(long timeout, TimeUnit unit) {
757 this.connectTimeout = timeout;
758 this.connectTimeoutUnit = unit;
763 * Sets the read timeout for the client
765 * @param timeout the number of time units for the default timeout
766 * @param unit the unit of time
768 public Builder setReadTimeout(long timeout, TimeUnit unit) {
769 this.readTimeout = timeout;
770 this.readTimeoutUnit = unit;
775 * Sets the write timeout for the client
777 * @param timeout the number of time units for the default timeout
778 * @param unit the unit of time
780 public Builder setWriteTimeout(long timeout, TimeUnit unit) {
781 this.writeTimeout = timeout;
782 this.writeTimeoutUnit = unit;
787 * Sets the proxy for the client
791 public Builder setProxy(Proxy proxy) {
797 * Sets the connection pool for the client
799 * @param maxIdle the maximum number of idle http connections in the pool
800 * @param timeout the number of time units until an idle http connection in the pool is closed
801 * @param unit the unit of time
803 public Builder setConnectionPool(int maxIdle, long timeout, TimeUnit unit) {
804 this.pool = new ConnectionPool(maxIdle, unit.toMillis(timeout));
809 * Builds a client with all of the provided parameters.
811 public Client build() throws ConfigurationException {
812 return new Client(this);