OSDN Git Service

commit java sdk doc and source code
[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.bouncycastle.asn1.pkcs.PrivateKeyInfo;
10 import org.bouncycastle.openssl.PEMKeyPair;
11 import org.bouncycastle.openssl.PEMParser;
12
13 import javax.net.ssl.*;
14 import java.io.*;
15 import java.lang.reflect.Type;
16 import java.net.*;
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;
27 import java.util.*;
28 import java.util.concurrent.TimeUnit;
29 import java.util.concurrent.atomic.AtomicInteger;
30
31 /**
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
36  * to an HSM server.
37  */
38 public class Client {
39     private String url;
40     private AtomicInteger urlIndex;
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 = "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
48
49     private static class BuildProperties {
50         public String version;
51     }
52
53     static {
54         InputStream in = Client.class.getClassLoader().getResourceAsStream("properties.json");
55         if (in != null) {
56             InputStreamReader inr = new InputStreamReader(in);
57             version = Utils.serializer.fromJson(inr, BuildProperties.class).version;
58         }
59     }
60
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);
66     }
67
68     /**
69      * Create a new http Client object using the default development host URL.
70      */
71     public Client() throws BytomException {
72         this(new Builder());
73     }
74
75     /**
76      * Create a new http Client object
77      *
78      * @param url the URL of the Chain Core or HSM
79      */
80     public Client(String url) throws BytomException {
81         this(new Builder().setUrl(url));
82     }
83
84     /**
85      * Create a new http Client object
86      *
87      * @param url         the URL of the Chain Core or HSM
88      * @param accessToken a Client API access token
89      */
90     public Client(String url, String accessToken) throws BytomException {
91         this(new Builder().setUrl(url).setAccessToken(accessToken));
92     }
93
94     /**
95      * Perform a single HTTP POST request against the API for a specific action.
96      *
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
102      */
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);
114                         } else {
115                             return deserializer.fromJson(response.body().charStream(), tClass);
116                         }
117                     }
118                 };
119         return post(action, body, rc);
120     }
121
122     /**
123      * Perform a single HTTP POST request against the API for a specific action,
124      * ignoring the body of the response.
125      *
126      * @param action The requested API action
127      * @param body   Body payload sent to the API as JSON
128      * @throws BytomException
129      */
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());
139                         }
140                         return null;
141                     }
142                 };
143         post(action, body, rc);
144     }
145
146     /**
147      * return the value of named as key from json
148      *
149      * @param action
150      * @param body
151      * @param key
152      * @param tClass
153      * @param <T>
154      * @return
155      * @throws BytomException
156      */
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,
161                     BytomException {
162                 JsonElement root = new JsonParser().parse(response.body().charStream());
163                 JsonElement status = root.getAsJsonObject().get("status");
164                 JsonElement data = root.getAsJsonObject().get("data");
165
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);
170                 else
171                     return deserializer.fromJson(response.body().charStream(), tClass);
172             }
173         };
174         return post(action, body, rc);
175     }
176
177     /**
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
181      * each input object.
182      *
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
189      */
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);
197                     }
198                 };
199         return post(action, body, rc);
200     }
201
202     /**
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
206      * batch call.
207      * <p>
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.
211      *
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
217      */
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);
224
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.
229                             throw errors.get(0);
230                         }
231
232                         List<T> successes = batch.successes();
233                         if (successes.size() == 1) {
234                             return successes.get(0);
235                         }
236
237                         // We should never get here, unless there is a bug in either the SDK or
238                         // API code, causing a non-singleton response.
239                         /*
240                         throw new BytomException(
241                                 "Invalid singleton response, request ID "
242                                         + batch.response().headers().get("Bytom-Request-ID"));
243                         */
244                         throw new BytomException("Invalid singleton response.");
245                     }
246                 };
247         return post(action, body, rc);
248     }
249
250     /**
251      * Returns true if a client access token stored in the client.
252      *
253      * @return a boolean
254      */
255     public boolean hasAccessToken() {
256         return this.accessToken != null && !this.accessToken.isEmpty();
257     }
258
259     /**
260      * Returns the client access token (possibly null).
261      *
262      * @return the client access token
263      */
264     public String accessToken() {
265         return accessToken;
266     }
267
268     /**
269      * Pins a public key to the HTTP client.
270      *
271      * @param provider           certificate provider
272      * @param subjPubKeyInfoHash public key hash
273      */
274     public void pinCertificate(String provider, String subjPubKeyInfoHash) {
275         CertificatePinner cp =
276                 new CertificatePinner.Builder().add(provider, subjPubKeyInfoHash).build();
277         this.httpClient.setCertificatePinner(cp);
278     }
279
280     /**
281      * Sets the default connect timeout for new connections. A value of 0 means no timeout.
282      *
283      * @param timeout the number of time units for the default timeout
284      * @param unit    the unit of time
285      */
286     public void setConnectTimeout(long timeout, TimeUnit unit) {
287         this.httpClient.setConnectTimeout(timeout, unit);
288     }
289
290     /**
291      * Sets the default read timeout for new connections. A value of 0 means no timeout.
292      *
293      * @param timeout the number of time units for the default timeout
294      * @param unit    the unit of time
295      */
296     public void setReadTimeout(long timeout, TimeUnit unit) {
297         this.httpClient.setReadTimeout(timeout, unit);
298     }
299
300     /**
301      * Sets the default write timeout for new connections. A value of 0 means no timeout.
302      *
303      * @param timeout the number of time units for the default timeout
304      * @param unit    the unit of time
305      */
306     public void setWriteTimeout(long timeout, TimeUnit unit) {
307         this.httpClient.setWriteTimeout(timeout, unit);
308     }
309
310     /**
311      * Sets the proxy information for the HTTP client.
312      *
313      * @param proxy proxy object
314      */
315     public void setProxy(Proxy proxy) {
316         this.httpClient.setProxy(proxy);
317     }
318
319     /**
320      * Defines an interface for deserializing HTTP responses into objects.
321      *
322      * @param <T> the type of object to return
323      */
324     public interface ResponseCreator<T> {
325         /**
326          * Deserializes an HTTP response into a Java object of type T.
327          *
328          * @param response     HTTP response object
329          * @param deserializer json deserializer
330          * @return an object of type T
331          * @throws BytomException
332          * @throws IOException
333          */
334         T create(Response response, Gson deserializer) throws BytomException, IOException;
335     }
336
337     /**
338      * Builds and executes an HTTP Post request.
339      *
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
345      */
346     private <T> T post(String path, Object body, ResponseCreator<T> respCreator)
347             throws BytomException {
348
349         RequestBody requestBody = RequestBody.create(this.JSON, Utils.serializer.toJson(body));
350         Request req;
351
352         BytomException exception = null;
353         URL endpointURL = null;
354
355         try {
356             endpointURL = new URL(url + "/" + path);
357         } catch (MalformedURLException e) {
358             e.printStackTrace();
359         }
360
361         Request.Builder builder =
362                 new Request.Builder()
363                         .header("User-Agent", "bytom-sdk-java/" + version)
364                         .url(endpointURL)
365                         .method("POST", requestBody);
366         if (hasAccessToken()) {
367             builder = builder.header("Authorization", buildCredentials());
368         }
369         req = builder.build();
370
371         Response resp = null;
372
373         T object = null;
374
375         try {
376             resp = this.checkError(this.httpClient.newCall(req).execute());
377             object = respCreator.create(resp, Utils.serializer);
378         } catch (IOException e) {
379             e.printStackTrace();
380         }
381
382         return object;
383     }
384
385     private OkHttpClient buildHttpClient(Builder builder) throws ConfigurationException {
386         OkHttpClient httpClient = builder.baseHttpClient.clone();
387
388         try {
389             if (builder.trustManagers != null) {
390                 SSLContext sslContext = SSLContext.getInstance("TLS");
391                 sslContext.init(builder.keyManagers, builder.trustManagers, null);
392                 httpClient.setSslSocketFactory(sslContext.getSocketFactory());
393             }
394         } catch (GeneralSecurityException ex) {
395             throw new ConfigurationException("Unable to configure TLS", ex);
396         }
397         if (builder.readTimeoutUnit != null) {
398             httpClient.setReadTimeout(builder.readTimeout, builder.readTimeoutUnit);
399         }
400         if (builder.writeTimeoutUnit != null) {
401             httpClient.setWriteTimeout(builder.writeTimeout, builder.writeTimeoutUnit);
402         }
403         if (builder.connectTimeoutUnit != null) {
404             httpClient.setConnectTimeout(builder.connectTimeout, builder.connectTimeoutUnit);
405         }
406         if (builder.pool != null) {
407             httpClient.setConnectionPool(builder.pool);
408         }
409         if (builder.proxy != null) {
410             httpClient.setProxy(builder.proxy);
411         }
412         if (builder.cp != null) {
413             httpClient.setCertificatePinner(builder.cp);
414         }
415
416         return httpClient;
417     }
418
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;
422
423     // the max amount of time cored leader election could take
424     private static final int RETRY_MAX_DELAY_MILLIS = 15000;
425
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);
430
431         // To incorporate jitter, use a pseudo random delay between [max/2, max] millis.
432         return randomGenerator.nextInt(max / 2) + max / 2 + 1;
433     }
434
435     private static final int[] RETRIABLE_STATUS_CODES = {
436             408, // Request Timeout
437             429, // Too Many Requests
438             500, // Internal Server Error
439             502, // Bad Gateway
440             503, // Service Unavailable
441             504, // Gateway Timeout
442             509, // Bandwidth Limit Exceeded
443     };
444
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) {
448                 return true;
449             }
450         }
451         return false;
452     }
453
454     private Response checkError(Response response) throws BytomException {
455         /*
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);
463         } */
464
465         if ((response.code() / 100) != 2) {
466             try {
467                 APIException err =
468                         Utils.serializer.fromJson(response.body().charStream(), APIException.class);
469                 if (err.code != null) {
470                     //err.requestId = rid;
471                     err.statusCode = response.code();
472                     throw err;
473                 }
474             } catch (IOException ex) {
475                 //throw new JSONException("Unable to read body. " + ex.getMessage(), rid);
476                 throw new JSONException("Unable to read body. ");
477             }
478         }
479         return response;
480     }
481
482     private String buildCredentials() {
483         String user = "";
484         String pass = "";
485         if (hasAccessToken()) {
486             String[] parts = accessToken.split(":");
487             if (parts.length >= 1) {
488                 user = parts[0];
489             }
490             if (parts.length >= 2) {
491                 pass = parts[1];
492             }
493         }
494         return Credentials.basic(user, pass);
495     }
496
497     /**
498      * Overrides {@link Object#hashCode()}
499      *
500      * @return the hash code
501      */
502     @Override
503     public int hashCode() {
504         int code = this.url.hashCode();
505         if (this.hasAccessToken()) {
506             code = code * 31 + this.accessToken.hashCode();
507         }
508         return code;
509     }
510
511     /**
512      * Overrides {@link Object#equals(Object)}
513      *
514      * @param o the object to compare
515      * @return a boolean specifying equality
516      */
517     @Override
518     public boolean equals(Object o) {
519         if (o == null) return false;
520         if (!(o instanceof Client)) return false;
521
522         Client other = (Client) o;
523         if (!this.url.equalsIgnoreCase(other.url)) {
524             return false;
525         }
526         return Objects.equals(this.accessToken, other.accessToken);
527     }
528
529     /**
530      * A builder class for creating client objects
531      */
532     public static class Builder {
533
534         private String url;
535
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;
547         private Proxy proxy;
548         private ConnectionPool pool;
549
550         public Builder() {
551             this.baseHttpClient = new OkHttpClient();
552             this.baseHttpClient.setFollowRedirects(false);
553             this.setDefaults();
554         }
555
556         public Builder(Client client) {
557             this.baseHttpClient = client.httpClient.clone();
558             this.url = client.url;
559             this.accessToken = client.accessToken;
560         }
561
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);
567         }
568
569         public Builder setUrl(String url) {
570             this.url = url;
571             return this;
572         }
573
574         /**
575          * Sets the access token for the client
576          *
577          * @param accessToken The access token for the Chain Core or HSM
578          */
579         public Builder setAccessToken(String accessToken) {
580             this.accessToken = accessToken;
581             return this;
582         }
583
584         /**
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.
587          *
588          * @param certStream input stream of PEM-encoded X.509 certificate
589          * @param keyStream  input stream of PEM-encoded private key
590          */
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);
597
598                 // Parse the private key from PEM-encoded input.
599                 Object obj = parser.readObject();
600                 PrivateKeyInfo info;
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;
608                 } else {
609                     throw new ConfigurationException("Unsupported private key provided.");
610                 }
611
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(
619                         "key",
620                         kf.generatePrivate(spec),
621                         DEFAULT_KEYSTORE_PASSWORD,
622                         new X509Certificate[]{certificate});
623
624                 // Use key store to build a key manager.
625                 KeyManagerFactory keyManagerFactory =
626                         KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
627                 keyManagerFactory.init(keyStore, DEFAULT_KEYSTORE_PASSWORD);
628
629                 this.keyManagers = keyManagerFactory.getKeyManagers();
630                 return this;
631             } catch (GeneralSecurityException | IOException ex) {
632                 throw new ConfigurationException("Unable to store X.509 cert/key pair", ex);
633             }
634         }
635
636         /**
637          * Sets the client's certificate and key for TLS client authentication.
638          *
639          * @param certPath file path to PEM-encoded X.509 certificate
640          * @param keyPath  file path to PEM-encoded private key
641          */
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);
650             }
651         }
652
653         /**
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.
656          *
657          * @param is input stream of the certificates to trust, in PEM format.
658          */
659         public Builder setTrustedCerts(InputStream is) throws ConfigurationException {
660             try {
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");
667                 }
668
669                 // Create a new key store and input the cert.
670                 KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
671                 keyStore.load(null, DEFAULT_KEYSTORE_PASSWORD);
672                 int index = 0;
673                 for (Certificate certificate : certificates) {
674                     String certificateAlias = Integer.toString(index++);
675                     keyStore.setCertificateEntry(certificateAlias, certificate);
676                 }
677
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));
689                 }
690
691                 this.trustManagers = trustManagers;
692                 return this;
693             } catch (GeneralSecurityException | IOException ex) {
694                 throw new ConfigurationException("Unable to configure trusted CA certs", ex);
695             }
696         }
697
698         /**
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.
701          *
702          * @param path The path of a file containing certificates to trust, in PEM format.
703          */
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);
709             }
710         }
711
712         /**
713          * Sets the certificate pinner for the client
714          *
715          * @param provider           certificate provider
716          * @param subjPubKeyInfoHash public key hash
717          */
718         public Builder pinCertificate(String provider, String subjPubKeyInfoHash) {
719             this.cp = new CertificatePinner.Builder().add(provider, subjPubKeyInfoHash).build();
720             return this;
721         }
722
723         /**
724          * Sets the connect timeout for the client
725          *
726          * @param timeout the number of time units for the default timeout
727          * @param unit    the unit of time
728          */
729         public Builder setConnectTimeout(long timeout, TimeUnit unit) {
730             this.connectTimeout = timeout;
731             this.connectTimeoutUnit = unit;
732             return this;
733         }
734
735         /**
736          * Sets the read timeout for the client
737          *
738          * @param timeout the number of time units for the default timeout
739          * @param unit    the unit of time
740          */
741         public Builder setReadTimeout(long timeout, TimeUnit unit) {
742             this.readTimeout = timeout;
743             this.readTimeoutUnit = unit;
744             return this;
745         }
746
747         /**
748          * Sets the write timeout for the client
749          *
750          * @param timeout the number of time units for the default timeout
751          * @param unit    the unit of time
752          */
753         public Builder setWriteTimeout(long timeout, TimeUnit unit) {
754             this.writeTimeout = timeout;
755             this.writeTimeoutUnit = unit;
756             return this;
757         }
758
759         /**
760          * Sets the proxy for the client
761          *
762          * @param proxy
763          */
764         public Builder setProxy(Proxy proxy) {
765             this.proxy = proxy;
766             return this;
767         }
768
769         /**
770          * Sets the connection pool for the client
771          *
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
775          */
776         public Builder setConnectionPool(int maxIdle, long timeout, TimeUnit unit) {
777             this.pool = new ConnectionPool(maxIdle, unit.toMillis(timeout));
778             return this;
779         }
780
781         /**
782          * Builds a client with all of the provided parameters.
783          */
784         public Client build() throws ConfigurationException {
785             return new Client(this);
786         }
787     }
788 }