OSDN Git Service

Merge pull request #4 from Bytom/retireTx
[bytom/bytom-java-sdk.git] / src / main / java / io / bytom / http / Client.java
1 package io.bytom.http;
2
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;
13
14 import javax.net.ssl.*;
15 import java.io.*;
16 import java.lang.reflect.Type;
17 import java.net.*;
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;
28 import java.util.*;
29 import java.util.concurrent.TimeUnit;
30 import java.util.concurrent.atomic.AtomicInteger;
31
32 /**
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
37  * to an HSM server.
38  */
39 public class Client {
40     private String url;
41     private String accessToken;
42     private OkHttpClient httpClient;
43
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
48
49     public static Logger logger = Logger.getLogger(Client.class);
50
51     private static class BuildProperties {
52         public String version;
53     }
54
55     static {
56         InputStream in = Client.class.getClassLoader().getResourceAsStream("properties.json");
57         if (in != null) {
58             InputStreamReader inr = new InputStreamReader(in);
59             version = Utils.serializer.fromJson(inr, BuildProperties.class).version;
60         }
61     }
62
63     public static Client generateClient() throws BytomException {
64
65         String coreURL = Configuration.getValue("bytom.api.url");
66         String accessToken = Configuration.getValue("client.access.token");
67
68         if (coreURL == null || coreURL.isEmpty()) {
69             coreURL = "http://127.0.0.1:9888";
70         }
71
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.");
76         }
77
78         return new Client(coreURL, accessToken);
79     }
80
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);
85         }
86         this.url = builder.url;
87         this.accessToken = builder.accessToken;
88         this.httpClient = buildHttpClient(builder);
89     }
90
91     /**
92      * Create a new http Client object using the default development host URL.
93      */
94     public Client() throws BytomException {
95         this(new Builder());
96     }
97
98     /**
99      * Create a new http Client object
100      *
101      * @param url the URL of the Chain Core or HSM
102      */
103     public Client(String url) throws BytomException {
104         this(new Builder().setUrl(url));
105     }
106
107     /**
108      * Create a new http Client object
109      *
110      * @param url         the URL of the Chain Core or HSM
111      * @param accessToken a Client API access token
112      */
113     public Client(String url, String accessToken) throws BytomException {
114         this(new Builder().setUrl(url).setAccessToken(accessToken));
115     }
116
117     /**
118      * Perform a single HTTP POST request against the API for a specific action.
119      *
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
125      */
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);
137                         } else {
138                             return deserializer.fromJson(response.body().charStream(), tClass);
139                         }
140                     }
141                 };
142         return post(action, body, rc);
143     }
144
145     /**
146      * Perform a single HTTP POST request against the API for a specific action,
147      * ignoring the body of the response.
148      *
149      * @param action The requested API action
150      * @param body   Body payload sent to the API as JSON
151      * @throws BytomException
152      */
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());
162                         }
163                         return null;
164                     }
165                 };
166         post(action, body, rc);
167     }
168
169     /**
170      * return the value of named as key from json
171      *
172      * @param action
173      * @param body
174      * @param key
175      * @param tClass
176      * @param <T>
177      * @return
178      * @throws BytomException
179      */
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,
184                     BytomException {
185                 JsonElement root = new JsonParser().parse(response.body().charStream());
186                 JsonElement status = root.getAsJsonObject().get("status");
187                 JsonElement data = root.getAsJsonObject().get("data");
188
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);
193                 else
194                     return deserializer.fromJson(response.body().charStream(), tClass);
195             }
196         };
197         return post(action, body, rc);
198     }
199
200     /**
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
204      * each input object.
205      *
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
212      */
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);
220                     }
221                 };
222         return post(action, body, rc);
223     }
224
225     /**
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
229      * batch call.
230      * <p>
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.
234      *
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
240      */
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);
247
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.
252                             throw errors.get(0);
253                         }
254
255                         List<T> successes = batch.successes();
256                         if (successes.size() == 1) {
257                             return successes.get(0);
258                         }
259
260                         // We should never get here, unless there is a bug in either the SDK or
261                         // API code, causing a non-singleton response.
262                         /*
263                         throw new BytomException(
264                                 "Invalid singleton response, request ID "
265                                         + batch.response().headers().get("Bytom-Request-ID"));
266                         */
267                         throw new BytomException("Invalid singleton response.");
268                     }
269                 };
270         return post(action, body, rc);
271     }
272
273     /**
274      * Returns true if a client access token stored in the client.
275      *
276      * @return a boolean
277      */
278     public boolean hasAccessToken() {
279         return this.accessToken != null && !this.accessToken.isEmpty();
280     }
281
282     /**
283      * Returns the client access token (possibly null).
284      *
285      * @return the client access token
286      */
287     public String accessToken() {
288         return accessToken;
289     }
290
291     public String getUrl() {
292         return url;
293     }
294
295     /**
296      * Pins a public key to the HTTP client.
297      *
298      * @param provider           certificate provider
299      * @param subjPubKeyInfoHash public key hash
300      */
301     public void pinCertificate(String provider, String subjPubKeyInfoHash) {
302         CertificatePinner cp =
303                 new CertificatePinner.Builder().add(provider, subjPubKeyInfoHash).build();
304         this.httpClient.setCertificatePinner(cp);
305     }
306
307     /**
308      * Sets the default connect timeout for new connections. A value of 0 means no timeout.
309      *
310      * @param timeout the number of time units for the default timeout
311      * @param unit    the unit of time
312      */
313     public void setConnectTimeout(long timeout, TimeUnit unit) {
314         this.httpClient.setConnectTimeout(timeout, unit);
315     }
316
317     /**
318      * Sets the default read timeout for new connections. A value of 0 means no timeout.
319      *
320      * @param timeout the number of time units for the default timeout
321      * @param unit    the unit of time
322      */
323     public void setReadTimeout(long timeout, TimeUnit unit) {
324         this.httpClient.setReadTimeout(timeout, unit);
325     }
326
327     /**
328      * Sets the default write timeout for new connections. A value of 0 means no timeout.
329      *
330      * @param timeout the number of time units for the default timeout
331      * @param unit    the unit of time
332      */
333     public void setWriteTimeout(long timeout, TimeUnit unit) {
334         this.httpClient.setWriteTimeout(timeout, unit);
335     }
336
337     /**
338      * Sets the proxy information for the HTTP client.
339      *
340      * @param proxy proxy object
341      */
342     public void setProxy(Proxy proxy) {
343         this.httpClient.setProxy(proxy);
344     }
345
346     /**
347      * Defines an interface for deserializing HTTP responses into objects.
348      *
349      * @param <T> the type of object to return
350      */
351     public interface ResponseCreator<T> {
352         /**
353          * Deserializes an HTTP response into a Java object of type T.
354          *
355          * @param response     HTTP response object
356          * @param deserializer json deserializer
357          * @return an object of type T
358          * @throws BytomException
359          * @throws IOException
360          */
361         T create(Response response, Gson deserializer) throws BytomException, IOException;
362     }
363
364     /**
365      * Builds and executes an HTTP Post request.
366      *
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
372      */
373     private <T> T post(String path, Object body, ResponseCreator<T> respCreator)
374             throws BytomException {
375
376         RequestBody requestBody = RequestBody.create(this.JSON, Utils.serializer.toJson(body));
377         Request req;
378
379         BytomException exception = null;
380         URL endpointURL = null;
381
382         try {
383             endpointURL = new URL(url + "/" + path);
384         } catch (MalformedURLException e) {
385             e.printStackTrace();
386         }
387
388         Request.Builder builder =
389                 new Request.Builder()
390                         .header("User-Agent", "bytom-sdk-java/" + version)
391                         .url(endpointURL)
392                         .method("POST", requestBody);
393         if (hasAccessToken()) {
394             builder = builder.header("Authorization", buildCredentials());
395         }
396         req = builder.build();
397
398         Response resp = null;
399
400         T object = null;
401
402         try {
403             resp = this.checkError(this.httpClient.newCall(req).execute());
404             object = respCreator.create(resp, Utils.serializer);
405         } catch (IOException e) {
406             e.printStackTrace();
407         }
408
409         return object;
410     }
411
412     private OkHttpClient buildHttpClient(Builder builder) throws ConfigurationException {
413         OkHttpClient httpClient = builder.baseHttpClient.clone();
414
415         try {
416             if (builder.trustManagers != null) {
417                 SSLContext sslContext = SSLContext.getInstance("TLS");
418                 sslContext.init(builder.keyManagers, builder.trustManagers, null);
419                 httpClient.setSslSocketFactory(sslContext.getSocketFactory());
420             }
421         } catch (GeneralSecurityException ex) {
422             throw new ConfigurationException("Unable to configure TLS", ex);
423         }
424         if (builder.readTimeoutUnit != null) {
425             httpClient.setReadTimeout(builder.readTimeout, builder.readTimeoutUnit);
426         }
427         if (builder.writeTimeoutUnit != null) {
428             httpClient.setWriteTimeout(builder.writeTimeout, builder.writeTimeoutUnit);
429         }
430         if (builder.connectTimeoutUnit != null) {
431             httpClient.setConnectTimeout(builder.connectTimeout, builder.connectTimeoutUnit);
432         }
433         if (builder.pool != null) {
434             httpClient.setConnectionPool(builder.pool);
435         }
436         if (builder.proxy != null) {
437             httpClient.setProxy(builder.proxy);
438         }
439         if (builder.cp != null) {
440             httpClient.setCertificatePinner(builder.cp);
441         }
442
443         return httpClient;
444     }
445
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;
449
450     // the max amount of time cored leader election could take
451     private static final int RETRY_MAX_DELAY_MILLIS = 15000;
452
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);
457
458         // To incorporate jitter, use a pseudo random delay between [max/2, max] millis.
459         return randomGenerator.nextInt(max / 2) + max / 2 + 1;
460     }
461
462     private static final int[] RETRIABLE_STATUS_CODES = {
463             408, // Request Timeout
464             429, // Too Many Requests
465             500, // Internal Server Error
466             502, // Bad Gateway
467             503, // Service Unavailable
468             504, // Gateway Timeout
469             509, // Bandwidth Limit Exceeded
470     };
471
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) {
475                 return true;
476             }
477         }
478         return false;
479     }
480
481     private Response checkError(Response response) throws BytomException {
482         /*
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);
490         } */
491
492         if ((response.code() / 100) != 2) {
493             try {
494                 APIException err =
495                         Utils.serializer.fromJson(response.body().charStream(), APIException.class);
496                 if (err.code != null) {
497                     //err.requestId = rid;
498                     err.statusCode = response.code();
499                     throw err;
500                 }
501             } catch (IOException ex) {
502                 //throw new JSONException("Unable to read body. " + ex.getMessage(), rid);
503                 throw new JSONException("Unable to read body. ");
504             }
505         }
506         return response;
507     }
508
509     private String buildCredentials() {
510         String user = "";
511         String pass = "";
512         if (hasAccessToken()) {
513             String[] parts = accessToken.split(":");
514             if (parts.length >= 1) {
515                 user = parts[0];
516             }
517             if (parts.length >= 2) {
518                 pass = parts[1];
519             }
520         }
521         return Credentials.basic(user, pass);
522     }
523
524     /**
525      * Overrides {@link Object#hashCode()}
526      *
527      * @return the hash code
528      */
529     @Override
530     public int hashCode() {
531         int code = this.url.hashCode();
532         if (this.hasAccessToken()) {
533             code = code * 31 + this.accessToken.hashCode();
534         }
535         return code;
536     }
537
538     /**
539      * Overrides {@link Object#equals(Object)}
540      *
541      * @param o the object to compare
542      * @return a boolean specifying equality
543      */
544     @Override
545     public boolean equals(Object o) {
546         if (o == null) return false;
547         if (!(o instanceof Client)) return false;
548
549         Client other = (Client) o;
550         if (!this.url.equalsIgnoreCase(other.url)) {
551             return false;
552         }
553         return Objects.equals(this.accessToken, other.accessToken);
554     }
555
556     /**
557      * A builder class for creating client objects
558      */
559     public static class Builder {
560
561         private String url;
562
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;
574         private Proxy proxy;
575         private ConnectionPool pool;
576
577         public Builder() {
578             this.baseHttpClient = new OkHttpClient();
579             this.baseHttpClient.setFollowRedirects(false);
580             this.setDefaults();
581         }
582
583         public Builder(Client client) {
584             this.baseHttpClient = client.httpClient.clone();
585             this.url = client.url;
586             this.accessToken = client.accessToken;
587         }
588
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);
594         }
595
596         public Builder setUrl(String url) {
597             this.url = url;
598             return this;
599         }
600
601         /**
602          * Sets the access token for the client
603          *
604          * @param accessToken The access token for the Chain Core or HSM
605          */
606         public Builder setAccessToken(String accessToken) {
607             this.accessToken = accessToken;
608             return this;
609         }
610
611         /**
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.
614          *
615          * @param certStream input stream of PEM-encoded X.509 certificate
616          * @param keyStream  input stream of PEM-encoded private key
617          */
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);
624
625                 // Parse the private key from PEM-encoded input.
626                 Object obj = parser.readObject();
627                 PrivateKeyInfo info;
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;
635                 } else {
636                     throw new ConfigurationException("Unsupported private key provided.");
637                 }
638
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(
646                         "key",
647                         kf.generatePrivate(spec),
648                         DEFAULT_KEYSTORE_PASSWORD,
649                         new X509Certificate[]{certificate});
650
651                 // Use key store to build a key manager.
652                 KeyManagerFactory keyManagerFactory =
653                         KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
654                 keyManagerFactory.init(keyStore, DEFAULT_KEYSTORE_PASSWORD);
655
656                 this.keyManagers = keyManagerFactory.getKeyManagers();
657                 return this;
658             } catch (GeneralSecurityException | IOException ex) {
659                 throw new ConfigurationException("Unable to store X.509 cert/key pair", ex);
660             }
661         }
662
663         /**
664          * Sets the client's certificate and key for TLS client authentication.
665          *
666          * @param certPath file path to PEM-encoded X.509 certificate
667          * @param keyPath  file path to PEM-encoded private key
668          */
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);
677             }
678         }
679
680         /**
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.
683          *
684          * @param is input stream of the certificates to trust, in PEM format.
685          */
686         public Builder setTrustedCerts(InputStream is) throws ConfigurationException {
687             try {
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");
694                 }
695
696                 // Create a new key store and input the cert.
697                 KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
698                 keyStore.load(null, DEFAULT_KEYSTORE_PASSWORD);
699                 int index = 0;
700                 for (Certificate certificate : certificates) {
701                     String certificateAlias = Integer.toString(index++);
702                     keyStore.setCertificateEntry(certificateAlias, certificate);
703                 }
704
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));
716                 }
717
718                 this.trustManagers = trustManagers;
719                 return this;
720             } catch (GeneralSecurityException | IOException ex) {
721                 throw new ConfigurationException("Unable to configure trusted CA certs", ex);
722             }
723         }
724
725         /**
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.
728          *
729          * @param path The path of a file containing certificates to trust, in PEM format.
730          */
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);
736             }
737         }
738
739         /**
740          * Sets the certificate pinner for the client
741          *
742          * @param provider           certificate provider
743          * @param subjPubKeyInfoHash public key hash
744          */
745         public Builder pinCertificate(String provider, String subjPubKeyInfoHash) {
746             this.cp = new CertificatePinner.Builder().add(provider, subjPubKeyInfoHash).build();
747             return this;
748         }
749
750         /**
751          * Sets the connect timeout for the client
752          *
753          * @param timeout the number of time units for the default timeout
754          * @param unit    the unit of time
755          */
756         public Builder setConnectTimeout(long timeout, TimeUnit unit) {
757             this.connectTimeout = timeout;
758             this.connectTimeoutUnit = unit;
759             return this;
760         }
761
762         /**
763          * Sets the read timeout for the client
764          *
765          * @param timeout the number of time units for the default timeout
766          * @param unit    the unit of time
767          */
768         public Builder setReadTimeout(long timeout, TimeUnit unit) {
769             this.readTimeout = timeout;
770             this.readTimeoutUnit = unit;
771             return this;
772         }
773
774         /**
775          * Sets the write timeout for the client
776          *
777          * @param timeout the number of time units for the default timeout
778          * @param unit    the unit of time
779          */
780         public Builder setWriteTimeout(long timeout, TimeUnit unit) {
781             this.writeTimeout = timeout;
782             this.writeTimeoutUnit = unit;
783             return this;
784         }
785
786         /**
787          * Sets the proxy for the client
788          *
789          * @param proxy
790          */
791         public Builder setProxy(Proxy proxy) {
792             this.proxy = proxy;
793             return this;
794         }
795
796         /**
797          * Sets the connection pool for the client
798          *
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
802          */
803         public Builder setConnectionPool(int maxIdle, long timeout, TimeUnit unit) {
804             this.pool = new ConnectionPool(maxIdle, unit.toMillis(timeout));
805             return this;
806         }
807
808         /**
809          * Builds a client with all of the provided parameters.
810          */
811         public Client build() throws ConfigurationException {
812             return new Client(this);
813         }
814     }
815 }