OSDN Git Service

a little modify
[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 String accessToken;
41     private OkHttpClient httpClient;
42
43     // Used to create empty, in-memory key stores.
44     private static final char[] DEFAULT_KEYSTORE_PASSWORD = "123456".toCharArray();
45     private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
46     private static String version = "dev"; // updated in the static initializer
47
48     private static class BuildProperties {
49         public String version;
50     }
51
52     static {
53         InputStream in = Client.class.getClassLoader().getResourceAsStream("properties.json");
54         if (in != null) {
55             InputStreamReader inr = new InputStreamReader(in);
56             version = Utils.serializer.fromJson(inr, BuildProperties.class).version;
57         }
58     }
59
60     public static Client generateClient() throws BytomException {
61
62         String coreURL = Configuration.getValue("bytom.api.url");
63         String accessToken = Configuration.getValue("client.access.token");
64
65         if (coreURL == null || coreURL.isEmpty()) {
66             coreURL = "http://127.0.0.1:9888";
67         }
68
69         return new Client(coreURL, accessToken);
70     }
71
72     public Client(Builder builder) throws ConfigurationException {
73         this.url = builder.url;
74         this.accessToken = builder.accessToken;
75         this.httpClient = buildHttpClient(builder);
76     }
77
78     /**
79      * Create a new http Client object using the default development host URL.
80      */
81     public Client() throws BytomException {
82         this(new Builder());
83     }
84
85     /**
86      * Create a new http Client object
87      *
88      * @param url the URL of the Chain Core or HSM
89      */
90     public Client(String url) throws BytomException {
91         this(new Builder().setUrl(url));
92     }
93
94     /**
95      * Create a new http Client object
96      *
97      * @param url         the URL of the Chain Core or HSM
98      * @param accessToken a Client API access token
99      */
100     public Client(String url, String accessToken) throws BytomException {
101         this(new Builder().setUrl(url).setAccessToken(accessToken));
102     }
103
104     /**
105      * Perform a single HTTP POST request against the API for a specific action.
106      *
107      * @param action The requested API action
108      * @param body   Body payload sent to the API as JSON
109      * @param tClass Type of object to be deserialized from the response JSON
110      * @return the result of the post request
111      * @throws BytomException
112      */
113     public <T> T request(String action, Object body, final Type tClass) throws BytomException {
114         ResponseCreator<T> rc =
115                 new ResponseCreator<T>() {
116                     public T create(Response response, Gson deserializer) throws IOException, BytomException {
117                         JsonElement root = new JsonParser().parse(response.body().charStream());
118                         JsonElement status = root.getAsJsonObject().get("status");
119                         JsonElement data = root.getAsJsonObject().get("data");
120                         if (status != null && status.toString().contains("fail")) {
121                             throw new BytomException(root.getAsJsonObject().get("msg").toString());
122                         } else if (data != null) {
123                             return deserializer.fromJson(data, tClass);
124                         } else {
125                             return deserializer.fromJson(response.body().charStream(), tClass);
126                         }
127                     }
128                 };
129         return post(action, body, rc);
130     }
131
132     /**
133      * Perform a single HTTP POST request against the API for a specific action,
134      * ignoring the body of the response.
135      *
136      * @param action The requested API action
137      * @param body   Body payload sent to the API as JSON
138      * @throws BytomException
139      */
140     public void request(String action, Object body) throws BytomException {
141         ResponseCreator<Object> rc =
142                 new ResponseCreator<Object>() {
143                     public Object create(Response response, Gson deserializer) throws IOException, BytomException {
144                         JsonElement root = new JsonParser().parse(response.body().charStream());
145                         JsonElement status = root.getAsJsonObject().get("status");
146                         JsonElement data = root.getAsJsonObject().get("data");
147                         if (status != null && status.toString().contains("fail")) {
148                             throw new BytomException(root.getAsJsonObject().get("msg").toString());
149                         }
150                         return null;
151                     }
152                 };
153         post(action, body, rc);
154     }
155
156     /**
157      * return the value of named as key from json
158      *
159      * @param action
160      * @param body
161      * @param key
162      * @param tClass
163      * @param <T>
164      * @return
165      * @throws BytomException
166      */
167     public <T> T requestGet(String action, Object body, final String key, final Type tClass)
168             throws BytomException {
169         ResponseCreator<T> rc = new ResponseCreator<T>() {
170             public T create(Response response, Gson deserializer) throws IOException,
171                     BytomException {
172                 JsonElement root = new JsonParser().parse(response.body().charStream());
173                 JsonElement status = root.getAsJsonObject().get("status");
174                 JsonElement data = root.getAsJsonObject().get("data");
175
176                 if (status != null && status.toString().contains("fail"))
177                     throw new BytomException(root.getAsJsonObject().get("msg").toString());
178                 else if (data != null)
179                     return deserializer.fromJson(data.getAsJsonObject().get(key), tClass);
180                 else
181                     return deserializer.fromJson(response.body().charStream(), tClass);
182             }
183         };
184         return post(action, body, rc);
185     }
186
187     /**
188      * Perform a single HTTP POST request against the API for a specific action.
189      * Use this method if you want batch semantics, i.e., the endpoint response
190      * is an array of valid objects interleaved with arrays, once corresponding to
191      * each input object.
192      *
193      * @param action The requested API action
194      * @param body   Body payload sent to the API as JSON
195      * @param tClass Type of object to be deserialized from the response JSON
196      * @param eClass Type of error object to be deserialized from the response JSON
197      * @return the result of the post request
198      * @throws BytomException
199      */
200     public <T> BatchResponse<T> batchRequest(
201             String action, Object body, final Type tClass, final Type eClass) throws BytomException {
202         ResponseCreator<BatchResponse<T>> rc =
203                 new ResponseCreator<BatchResponse<T>>() {
204                     public BatchResponse<T> create(Response response, Gson deserializer)
205                             throws BytomException, IOException {
206                         return new BatchResponse<>(response, deserializer, tClass, eClass);
207                     }
208                 };
209         return post(action, body, rc);
210     }
211
212     /**
213      * Perform a single HTTP POST request against the API for a specific action.
214      * Use this method if you want single-item semantics (creating single assets,
215      * building single transactions) but the API endpoint is implemented as a
216      * batch call.
217      * <p>
218      * Because request bodies for batch calls do not share a consistent format,
219      * this method does not perform any automatic arrayification of outgoing
220      * parameters. Remember to arrayify your request objects where appropriate.
221      *
222      * @param action The requested API action
223      * @param body   Body payload sent to the API as JSON
224      * @param tClass Type of object to be deserialized from the response JSON
225      * @return the result of the post request
226      * @throws BytomException
227      */
228     public <T> T singletonBatchRequest(
229             String action, Object body, final Type tClass, final Type eClass) throws BytomException {
230         ResponseCreator<T> rc =
231                 new ResponseCreator<T>() {
232                     public T create(Response response, Gson deserializer) throws BytomException, IOException {
233                         BatchResponse<T> batch = new BatchResponse<>(response, deserializer, tClass, eClass);
234
235                         List<APIException> errors = batch.errors();
236                         if (errors.size() == 1) {
237                             // This throw must occur within this lambda in order for APIClient's
238                             // retry logic to take effect.
239                             throw errors.get(0);
240                         }
241
242                         List<T> successes = batch.successes();
243                         if (successes.size() == 1) {
244                             return successes.get(0);
245                         }
246
247                         // We should never get here, unless there is a bug in either the SDK or
248                         // API code, causing a non-singleton response.
249                         /*
250                         throw new BytomException(
251                                 "Invalid singleton response, request ID "
252                                         + batch.response().headers().get("Bytom-Request-ID"));
253                         */
254                         throw new BytomException("Invalid singleton response.");
255                     }
256                 };
257         return post(action, body, rc);
258     }
259
260     /**
261      * Returns true if a client access token stored in the client.
262      *
263      * @return a boolean
264      */
265     public boolean hasAccessToken() {
266         return this.accessToken != null && !this.accessToken.isEmpty();
267     }
268
269     /**
270      * Returns the client access token (possibly null).
271      *
272      * @return the client access token
273      */
274     public String accessToken() {
275         return accessToken;
276     }
277
278     public String getUrl() {
279         return url;
280     }
281
282     /**
283      * Pins a public key to the HTTP client.
284      *
285      * @param provider           certificate provider
286      * @param subjPubKeyInfoHash public key hash
287      */
288     public void pinCertificate(String provider, String subjPubKeyInfoHash) {
289         CertificatePinner cp =
290                 new CertificatePinner.Builder().add(provider, subjPubKeyInfoHash).build();
291         this.httpClient.setCertificatePinner(cp);
292     }
293
294     /**
295      * Sets the default connect timeout for new connections. A value of 0 means no timeout.
296      *
297      * @param timeout the number of time units for the default timeout
298      * @param unit    the unit of time
299      */
300     public void setConnectTimeout(long timeout, TimeUnit unit) {
301         this.httpClient.setConnectTimeout(timeout, unit);
302     }
303
304     /**
305      * Sets the default read timeout for new connections. A value of 0 means no timeout.
306      *
307      * @param timeout the number of time units for the default timeout
308      * @param unit    the unit of time
309      */
310     public void setReadTimeout(long timeout, TimeUnit unit) {
311         this.httpClient.setReadTimeout(timeout, unit);
312     }
313
314     /**
315      * Sets the default write timeout for new connections. A value of 0 means no timeout.
316      *
317      * @param timeout the number of time units for the default timeout
318      * @param unit    the unit of time
319      */
320     public void setWriteTimeout(long timeout, TimeUnit unit) {
321         this.httpClient.setWriteTimeout(timeout, unit);
322     }
323
324     /**
325      * Sets the proxy information for the HTTP client.
326      *
327      * @param proxy proxy object
328      */
329     public void setProxy(Proxy proxy) {
330         this.httpClient.setProxy(proxy);
331     }
332
333     /**
334      * Defines an interface for deserializing HTTP responses into objects.
335      *
336      * @param <T> the type of object to return
337      */
338     public interface ResponseCreator<T> {
339         /**
340          * Deserializes an HTTP response into a Java object of type T.
341          *
342          * @param response     HTTP response object
343          * @param deserializer json deserializer
344          * @return an object of type T
345          * @throws BytomException
346          * @throws IOException
347          */
348         T create(Response response, Gson deserializer) throws BytomException, IOException;
349     }
350
351     /**
352      * Builds and executes an HTTP Post request.
353      *
354      * @param path        the path to the endpoint
355      * @param body        the request body
356      * @param respCreator object specifying the response structure
357      * @return a response deserialized into type T
358      * @throws BytomException
359      */
360     private <T> T post(String path, Object body, ResponseCreator<T> respCreator)
361             throws BytomException {
362
363         RequestBody requestBody = RequestBody.create(this.JSON, Utils.serializer.toJson(body));
364         Request req;
365
366         BytomException exception = null;
367         URL endpointURL = null;
368
369         try {
370             endpointURL = new URL(url + "/" + path);
371         } catch (MalformedURLException e) {
372             e.printStackTrace();
373         }
374
375         Request.Builder builder =
376                 new Request.Builder()
377                         .header("User-Agent", "bytom-sdk-java/" + version)
378                         .url(endpointURL)
379                         .method("POST", requestBody);
380         if (hasAccessToken()) {
381             builder = builder.header("Authorization", buildCredentials());
382         }
383         req = builder.build();
384
385         Response resp = null;
386
387         T object = null;
388
389         try {
390             resp = this.checkError(this.httpClient.newCall(req).execute());
391             object = respCreator.create(resp, Utils.serializer);
392         } catch (IOException e) {
393             e.printStackTrace();
394         }
395
396         return object;
397     }
398
399     private OkHttpClient buildHttpClient(Builder builder) throws ConfigurationException {
400         OkHttpClient httpClient = builder.baseHttpClient.clone();
401
402         try {
403             if (builder.trustManagers != null) {
404                 SSLContext sslContext = SSLContext.getInstance("TLS");
405                 sslContext.init(builder.keyManagers, builder.trustManagers, null);
406                 httpClient.setSslSocketFactory(sslContext.getSocketFactory());
407             }
408         } catch (GeneralSecurityException ex) {
409             throw new ConfigurationException("Unable to configure TLS", ex);
410         }
411         if (builder.readTimeoutUnit != null) {
412             httpClient.setReadTimeout(builder.readTimeout, builder.readTimeoutUnit);
413         }
414         if (builder.writeTimeoutUnit != null) {
415             httpClient.setWriteTimeout(builder.writeTimeout, builder.writeTimeoutUnit);
416         }
417         if (builder.connectTimeoutUnit != null) {
418             httpClient.setConnectTimeout(builder.connectTimeout, builder.connectTimeoutUnit);
419         }
420         if (builder.pool != null) {
421             httpClient.setConnectionPool(builder.pool);
422         }
423         if (builder.proxy != null) {
424             httpClient.setProxy(builder.proxy);
425         }
426         if (builder.cp != null) {
427             httpClient.setCertificatePinner(builder.cp);
428         }
429
430         return httpClient;
431     }
432
433     private static final Random randomGenerator = new Random();
434     private static final int MAX_RETRIES = 10;
435     private static final int RETRY_BASE_DELAY_MILLIS = 40;
436
437     // the max amount of time cored leader election could take
438     private static final int RETRY_MAX_DELAY_MILLIS = 15000;
439
440     private static int retryDelayMillis(int retryAttempt) {
441         // Calculate the max delay as base * 2 ^ (retryAttempt - 1).
442         int max = RETRY_BASE_DELAY_MILLIS * (1 << (retryAttempt - 1));
443         max = Math.min(max, RETRY_MAX_DELAY_MILLIS);
444
445         // To incorporate jitter, use a pseudo random delay between [max/2, max] millis.
446         return randomGenerator.nextInt(max / 2) + max / 2 + 1;
447     }
448
449     private static final int[] RETRIABLE_STATUS_CODES = {
450             408, // Request Timeout
451             429, // Too Many Requests
452             500, // Internal Server Error
453             502, // Bad Gateway
454             503, // Service Unavailable
455             504, // Gateway Timeout
456             509, // Bandwidth Limit Exceeded
457     };
458
459     private static boolean isRetriableStatusCode(int statusCode) {
460         for (int i = 0; i < RETRIABLE_STATUS_CODES.length; i++) {
461             if (RETRIABLE_STATUS_CODES[i] == statusCode) {
462                 return true;
463             }
464         }
465         return false;
466     }
467
468     private Response checkError(Response response) throws BytomException {
469         /*
470         String rid = response.headers().get("Bytom-Request-ID");
471         if (rid == null || rid.length() == 0) {
472             // Header field Bytom-Request-ID is set by the backend
473             // API server. If this field is set, then we can expect
474             // the body to be well-formed JSON. If it's not set,
475             // then we are probably talking to a gateway or proxy.
476             throw new ConnectivityException(response);
477         } */
478
479         if ((response.code() / 100) != 2) {
480             try {
481                 APIException err =
482                         Utils.serializer.fromJson(response.body().charStream(), APIException.class);
483                 if (err.code != null) {
484                     //err.requestId = rid;
485                     err.statusCode = response.code();
486                     throw err;
487                 }
488             } catch (IOException ex) {
489                 //throw new JSONException("Unable to read body. " + ex.getMessage(), rid);
490                 throw new JSONException("Unable to read body. ");
491             }
492         }
493         return response;
494     }
495
496     private String buildCredentials() {
497         String user = "";
498         String pass = "";
499         if (hasAccessToken()) {
500             String[] parts = accessToken.split(":");
501             if (parts.length >= 1) {
502                 user = parts[0];
503             }
504             if (parts.length >= 2) {
505                 pass = parts[1];
506             }
507         }
508         return Credentials.basic(user, pass);
509     }
510
511     /**
512      * Overrides {@link Object#hashCode()}
513      *
514      * @return the hash code
515      */
516     @Override
517     public int hashCode() {
518         int code = this.url.hashCode();
519         if (this.hasAccessToken()) {
520             code = code * 31 + this.accessToken.hashCode();
521         }
522         return code;
523     }
524
525     /**
526      * Overrides {@link Object#equals(Object)}
527      *
528      * @param o the object to compare
529      * @return a boolean specifying equality
530      */
531     @Override
532     public boolean equals(Object o) {
533         if (o == null) return false;
534         if (!(o instanceof Client)) return false;
535
536         Client other = (Client) o;
537         if (!this.url.equalsIgnoreCase(other.url)) {
538             return false;
539         }
540         return Objects.equals(this.accessToken, other.accessToken);
541     }
542
543     /**
544      * A builder class for creating client objects
545      */
546     public static class Builder {
547
548         private String url;
549
550         private OkHttpClient baseHttpClient;
551         private String accessToken;
552         private CertificatePinner cp;
553         private KeyManager[] keyManagers;
554         private TrustManager[] trustManagers;
555         private long connectTimeout;
556         private TimeUnit connectTimeoutUnit;
557         private long readTimeout;
558         private TimeUnit readTimeoutUnit;
559         private long writeTimeout;
560         private TimeUnit writeTimeoutUnit;
561         private Proxy proxy;
562         private ConnectionPool pool;
563
564         public Builder() {
565             this.baseHttpClient = new OkHttpClient();
566             this.baseHttpClient.setFollowRedirects(false);
567             this.setDefaults();
568         }
569
570         public Builder(Client client) {
571             this.baseHttpClient = client.httpClient.clone();
572             this.url = client.url;
573             this.accessToken = client.accessToken;
574         }
575
576         private void setDefaults() {
577             this.setReadTimeout(30, TimeUnit.SECONDS);
578             this.setWriteTimeout(30, TimeUnit.SECONDS);
579             this.setConnectTimeout(30, TimeUnit.SECONDS);
580             this.setConnectionPool(50, 2, TimeUnit.MINUTES);
581         }
582
583         public Builder setUrl(String url) {
584             this.url = url;
585             return this;
586         }
587
588         /**
589          * Sets the access token for the client
590          *
591          * @param accessToken The access token for the Chain Core or HSM
592          */
593         public Builder setAccessToken(String accessToken) {
594             this.accessToken = accessToken;
595             return this;
596         }
597
598         /**
599          * Sets the client's certificate and key for TLS client authentication.
600          * PEM-encoded, RSA private keys adhering to PKCS#1 or PKCS#8 are supported.
601          *
602          * @param certStream input stream of PEM-encoded X.509 certificate
603          * @param keyStream  input stream of PEM-encoded private key
604          */
605         public Builder setX509KeyPair(InputStream certStream, InputStream keyStream)
606                 throws ConfigurationException {
607             try (PEMParser parser = new PEMParser(new InputStreamReader(keyStream))) {
608                 // Extract certs from PEM-encoded input.
609                 CertificateFactory factory = CertificateFactory.getInstance("X.509");
610                 X509Certificate certificate = (X509Certificate) factory.generateCertificate(certStream);
611
612                 // Parse the private key from PEM-encoded input.
613                 Object obj = parser.readObject();
614                 PrivateKeyInfo info;
615                 if (obj instanceof PEMKeyPair) {
616                     // PKCS#1 Private Key found.
617                     PEMKeyPair kp = (PEMKeyPair) obj;
618                     info = kp.getPrivateKeyInfo();
619                 } else if (obj instanceof PrivateKeyInfo) {
620                     // PKCS#8 Private Key found.
621                     info = (PrivateKeyInfo) obj;
622                 } else {
623                     throw new ConfigurationException("Unsupported private key provided.");
624                 }
625
626                 // Create a new key store and input the pair.
627                 KeySpec spec = new PKCS8EncodedKeySpec(info.getEncoded());
628                 KeyFactory kf = KeyFactory.getInstance("RSA");
629                 KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
630                 keyStore.load(null, DEFAULT_KEYSTORE_PASSWORD);
631                 keyStore.setCertificateEntry("cert", certificate);
632                 keyStore.setKeyEntry(
633                         "key",
634                         kf.generatePrivate(spec),
635                         DEFAULT_KEYSTORE_PASSWORD,
636                         new X509Certificate[]{certificate});
637
638                 // Use key store to build a key manager.
639                 KeyManagerFactory keyManagerFactory =
640                         KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
641                 keyManagerFactory.init(keyStore, DEFAULT_KEYSTORE_PASSWORD);
642
643                 this.keyManagers = keyManagerFactory.getKeyManagers();
644                 return this;
645             } catch (GeneralSecurityException | IOException ex) {
646                 throw new ConfigurationException("Unable to store X.509 cert/key pair", ex);
647             }
648         }
649
650         /**
651          * Sets the client's certificate and key for TLS client authentication.
652          *
653          * @param certPath file path to PEM-encoded X.509 certificate
654          * @param keyPath  file path to PEM-encoded private key
655          */
656         public Builder setX509KeyPair(String certPath, String keyPath) throws ConfigurationException {
657             try (InputStream certStream =
658                          new ByteArrayInputStream(Files.readAllBytes(Paths.get(certPath)));
659                  InputStream keyStream =
660                          new ByteArrayInputStream(Files.readAllBytes(Paths.get(keyPath)))) {
661                 return setX509KeyPair(certStream, keyStream);
662             } catch (IOException ex) {
663                 throw new ConfigurationException("Unable to store X509 cert/key pair", ex);
664             }
665         }
666
667         /**
668          * Trusts the given CA certs, and no others. Use this if you are running
669          * your own CA, or are using a self-signed server certificate.
670          *
671          * @param is input stream of the certificates to trust, in PEM format.
672          */
673         public Builder setTrustedCerts(InputStream is) throws ConfigurationException {
674             try {
675                 // Extract certs from PEM-encoded input.
676                 CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
677                 Collection<? extends Certificate> certificates =
678                         certificateFactory.generateCertificates(is);
679                 if (certificates.isEmpty()) {
680                     throw new IllegalArgumentException("expected non-empty set of trusted certificates");
681                 }
682
683                 // Create a new key store and input the cert.
684                 KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
685                 keyStore.load(null, DEFAULT_KEYSTORE_PASSWORD);
686                 int index = 0;
687                 for (Certificate certificate : certificates) {
688                     String certificateAlias = Integer.toString(index++);
689                     keyStore.setCertificateEntry(certificateAlias, certificate);
690                 }
691
692                 // Use key store to build an X509 trust manager.
693                 KeyManagerFactory keyManagerFactory =
694                         KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
695                 keyManagerFactory.init(keyStore, DEFAULT_KEYSTORE_PASSWORD);
696                 TrustManagerFactory trustManagerFactory =
697                         TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
698                 trustManagerFactory.init(keyStore);
699                 TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
700                 if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
701                     throw new IllegalStateException(
702                             "Unexpected default trust managers:" + Arrays.toString(trustManagers));
703                 }
704
705                 this.trustManagers = trustManagers;
706                 return this;
707             } catch (GeneralSecurityException | IOException ex) {
708                 throw new ConfigurationException("Unable to configure trusted CA certs", ex);
709             }
710         }
711
712         /**
713          * Trusts the given CA certs, and no others. Use this if you are running
714          * your own CA, or are using a self-signed server certificate.
715          *
716          * @param path The path of a file containing certificates to trust, in PEM format.
717          */
718         public Builder setTrustedCerts(String path) throws ConfigurationException {
719             try (InputStream is = new FileInputStream(path)) {
720                 return setTrustedCerts(is);
721             } catch (IOException ex) {
722                 throw new ConfigurationException("Unable to configure trusted CA certs", ex);
723             }
724         }
725
726         /**
727          * Sets the certificate pinner for the client
728          *
729          * @param provider           certificate provider
730          * @param subjPubKeyInfoHash public key hash
731          */
732         public Builder pinCertificate(String provider, String subjPubKeyInfoHash) {
733             this.cp = new CertificatePinner.Builder().add(provider, subjPubKeyInfoHash).build();
734             return this;
735         }
736
737         /**
738          * Sets the connect timeout for the client
739          *
740          * @param timeout the number of time units for the default timeout
741          * @param unit    the unit of time
742          */
743         public Builder setConnectTimeout(long timeout, TimeUnit unit) {
744             this.connectTimeout = timeout;
745             this.connectTimeoutUnit = unit;
746             return this;
747         }
748
749         /**
750          * Sets the read timeout for the client
751          *
752          * @param timeout the number of time units for the default timeout
753          * @param unit    the unit of time
754          */
755         public Builder setReadTimeout(long timeout, TimeUnit unit) {
756             this.readTimeout = timeout;
757             this.readTimeoutUnit = unit;
758             return this;
759         }
760
761         /**
762          * Sets the write timeout for the client
763          *
764          * @param timeout the number of time units for the default timeout
765          * @param unit    the unit of time
766          */
767         public Builder setWriteTimeout(long timeout, TimeUnit unit) {
768             this.writeTimeout = timeout;
769             this.writeTimeoutUnit = unit;
770             return this;
771         }
772
773         /**
774          * Sets the proxy for the client
775          *
776          * @param proxy
777          */
778         public Builder setProxy(Proxy proxy) {
779             this.proxy = proxy;
780             return this;
781         }
782
783         /**
784          * Sets the connection pool for the client
785          *
786          * @param maxIdle the maximum number of idle http connections in the pool
787          * @param timeout the number of time units until an idle http connection in the pool is closed
788          * @param unit    the unit of time
789          */
790         public Builder setConnectionPool(int maxIdle, long timeout, TimeUnit unit) {
791             this.pool = new ConnectionPool(maxIdle, unit.toMillis(timeout));
792             return this;
793         }
794
795         /**
796          * Builds a client with all of the provided parameters.
797          */
798         public Client build() throws ConfigurationException {
799             return new Client(this);
800         }
801     }
802 }