1 /*
2  * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License").
5  * You may not use this file except in compliance with the License.
6  * A copy of the License is located at
7  *
8  *  http://aws.amazon.com/apache2.0
9  *
10  * or in the "license" file accompanying this file. This file is distributed
11  * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12  * express or implied. See the License for the specific language governing
13  * permissions and limitations under the License.
14  */
15 
16 package software.amazon.awssdk.http.urlconnection;
17 
18 import static software.amazon.awssdk.http.Header.ACCEPT;
19 import static software.amazon.awssdk.http.Header.CONTENT_LENGTH;
20 import static software.amazon.awssdk.http.HttpStatusFamily.CLIENT_ERROR;
21 import static software.amazon.awssdk.http.HttpStatusFamily.SERVER_ERROR;
22 import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely;
23 import static software.amazon.awssdk.utils.NumericUtils.saturatedCast;
24 
25 import java.io.IOException;
26 import java.io.InputStream;
27 import java.io.OutputStream;
28 import java.io.UncheckedIOException;
29 import java.net.HttpURLConnection;
30 import java.net.InetSocketAddress;
31 import java.net.Proxy;
32 import java.net.URI;
33 import java.nio.charset.StandardCharsets;
34 import java.security.KeyManagementException;
35 import java.security.NoSuchAlgorithmException;
36 import java.security.cert.X509Certificate;
37 import java.time.Duration;
38 import java.util.Base64;
39 import java.util.List;
40 import java.util.Locale;
41 import java.util.Map;
42 import java.util.Objects;
43 import java.util.Optional;
44 import java.util.function.Consumer;
45 import java.util.function.Supplier;
46 import java.util.stream.Collectors;
47 import javax.net.ssl.HostnameVerifier;
48 import javax.net.ssl.HttpsURLConnection;
49 import javax.net.ssl.KeyManager;
50 import javax.net.ssl.SSLContext;
51 import javax.net.ssl.SSLSession;
52 import javax.net.ssl.SSLSocketFactory;
53 import javax.net.ssl.TrustManager;
54 import javax.net.ssl.X509TrustManager;
55 import software.amazon.awssdk.annotations.SdkPublicApi;
56 import software.amazon.awssdk.http.AbortableInputStream;
57 import software.amazon.awssdk.http.ContentStreamProvider;
58 import software.amazon.awssdk.http.ExecutableHttpRequest;
59 import software.amazon.awssdk.http.HttpExecuteRequest;
60 import software.amazon.awssdk.http.HttpExecuteResponse;
61 import software.amazon.awssdk.http.HttpStatusFamily;
62 import software.amazon.awssdk.http.SdkHttpClient;
63 import software.amazon.awssdk.http.SdkHttpConfigurationOption;
64 import software.amazon.awssdk.http.SdkHttpRequest;
65 import software.amazon.awssdk.http.SdkHttpResponse;
66 import software.amazon.awssdk.http.TlsKeyManagersProvider;
67 import software.amazon.awssdk.http.TlsTrustManagersProvider;
68 import software.amazon.awssdk.utils.AttributeMap;
69 import software.amazon.awssdk.utils.IoUtils;
70 import software.amazon.awssdk.utils.Logger;
71 import software.amazon.awssdk.utils.StringUtils;
72 import software.amazon.awssdk.utils.Validate;
73 
74 /**
75  * An implementation of {@link SdkHttpClient} that uses {@link HttpURLConnection} to communicate with the service. This is the
76  * leanest synchronous client that optimizes for minimum dependencies and startup latency in exchange for having less
77  * functionality than other implementations.
78  *
79  * <p>See software.amazon.awssdk.http.apache.ApacheHttpClient for an alternative implementation.</p>
80  *
81  * <p>This can be created via {@link #builder()}</p>
82  */
83 @SdkPublicApi
84 public final class UrlConnectionHttpClient implements SdkHttpClient {
85 
86     private static final Logger log = Logger.loggerFor(UrlConnectionHttpClient.class);
87     private static final String CLIENT_NAME = "UrlConnection";
88 
89     private final AttributeMap options;
90     private final UrlConnectionFactory connectionFactory;
91     private final ProxyConfiguration proxyConfiguration;
92 
UrlConnectionHttpClient(AttributeMap options, UrlConnectionFactory connectionFactory, DefaultBuilder builder)93     private UrlConnectionHttpClient(AttributeMap options, UrlConnectionFactory connectionFactory, DefaultBuilder builder) {
94         this.options = options;
95         this.proxyConfiguration = builder != null ? builder.proxyConfiguration : null;
96 
97         if (connectionFactory != null) {
98             this.connectionFactory = connectionFactory;
99         } else {
100             // Note: This socket factory MUST be reused between requests because the connection pool in the JVM is keyed by both
101             // URL and SSLSocketFactory. If the socket factory is not reused, connections will not be reused between requests.
102             SSLSocketFactory socketFactory = getSslContext(options).getSocketFactory();
103 
104             this.connectionFactory = url -> createDefaultConnection(url, socketFactory);
105         }
106     }
107 
UrlConnectionHttpClient(AttributeMap options, UrlConnectionFactory connectionFactory)108     private UrlConnectionHttpClient(AttributeMap options, UrlConnectionFactory connectionFactory) {
109         this(options, connectionFactory, null);
110     }
111 
builder()112     public static Builder builder() {
113         return new DefaultBuilder();
114     }
115 
116     /**
117      * Create a {@link HttpURLConnection} client with the default properties
118      *
119      * @return an {@link UrlConnectionHttpClient}
120      */
create()121     public static SdkHttpClient create() {
122         return new DefaultBuilder().build();
123     }
124 
125     /**
126      * Use this method if you want to control the way a {@link HttpURLConnection} is created.
127      * This will ignore SDK defaults like {@link SdkHttpConfigurationOption#CONNECTION_TIMEOUT}
128      * and {@link SdkHttpConfigurationOption#READ_TIMEOUT}
129      * @param connectionFactory a function that, given a {@link URI} will create an {@link HttpURLConnection}
130      * @return an {@link UrlConnectionHttpClient}
131      */
create(UrlConnectionFactory connectionFactory)132     public static SdkHttpClient create(UrlConnectionFactory connectionFactory) {
133         return new UrlConnectionHttpClient(AttributeMap.empty(), connectionFactory);
134     }
135 
136     @Override
prepareRequest(HttpExecuteRequest request)137     public ExecutableHttpRequest prepareRequest(HttpExecuteRequest request) {
138         HttpURLConnection connection = createAndConfigureConnection(request);
139         return new RequestCallable(connection, request);
140     }
141 
142     @Override
close()143     public void close() {
144         // Nothing to close. The connections will be closed by closing the InputStreams.
145     }
146 
147     @Override
clientName()148     public String clientName() {
149         return CLIENT_NAME;
150     }
151 
createAndConfigureConnection(HttpExecuteRequest request)152     private HttpURLConnection createAndConfigureConnection(HttpExecuteRequest request) {
153         SdkHttpRequest sdkHttpRequest = request.httpRequest();
154         HttpURLConnection connection = connectionFactory.createConnection(sdkHttpRequest.getUri());
155         setHeaders(connection, sdkHttpRequest);
156 
157         // connection.setRequestProperty("Transfer-Encoding", "chunked") does not work, i.e., property does not get set
158         if (sdkHttpRequest.matchingHeaders("Transfer-Encoding").contains("chunked")) {
159             connection.setChunkedStreamingMode(-1);
160         }
161 
162         if (!sdkHttpRequest.firstMatchingHeader(ACCEPT).isPresent()) {
163             // Override Accept header because the default one in JDK does not comply with RFC 7231
164             // See: https://bugs.openjdk.org/browse/JDK-8163921
165             connection.setRequestProperty(ACCEPT, "*/*");
166         }
167 
168         invokeSafely(() -> connection.setRequestMethod(sdkHttpRequest.method().name()));
169         if (request.contentStreamProvider().isPresent()) {
170             connection.setDoOutput(true);
171         }
172 
173         // Disable following redirects since it breaks SDK error handling and matches Apache.
174         // See: https://github.com/aws/aws-sdk-java-v2/issues/975
175         connection.setInstanceFollowRedirects(false);
176 
177         sdkHttpRequest.firstMatchingHeader(CONTENT_LENGTH).map(Long::parseLong)
178                       .ifPresent(connection::setFixedLengthStreamingMode);
179 
180         return connection;
181     }
182 
setHeaders(HttpURLConnection connection, SdkHttpRequest request)183     private void setHeaders(HttpURLConnection connection, SdkHttpRequest request) {
184         request.forEachHeader((name, values) -> {
185             String commaSeparated = String.join(",", values);
186             connection.addRequestProperty(name, commaSeparated);
187         });
188     }
189 
createDefaultConnection(URI uri, SSLSocketFactory socketFactory)190     private HttpURLConnection createDefaultConnection(URI uri, SSLSocketFactory socketFactory) {
191 
192         Optional<Proxy> proxy = determineProxy(uri);
193         HttpURLConnection connection = !proxy.isPresent() ?
194                                        invokeSafely(() -> (HttpURLConnection) uri.toURL().openConnection())
195                                                           :
196                                        invokeSafely(() -> (HttpURLConnection) uri.toURL().openConnection(proxy.get()));
197 
198         if (connection instanceof HttpsURLConnection) {
199             HttpsURLConnection httpsConnection = (HttpsURLConnection) connection;
200 
201             if (options.get(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES)) {
202                 httpsConnection.setHostnameVerifier(NoOpHostNameVerifier.INSTANCE);
203             }
204             httpsConnection.setSSLSocketFactory(socketFactory);
205         }
206 
207         if (proxy.isPresent() && shouldProxyAuthorize()) {
208             connection.addRequestProperty("proxy-authorization", String.format("Basic %s", encodedAuthToken(proxyConfiguration)));
209         }
210 
211         connection.setConnectTimeout(saturatedCast(options.get(SdkHttpConfigurationOption.CONNECTION_TIMEOUT).toMillis()));
212         connection.setReadTimeout(saturatedCast(options.get(SdkHttpConfigurationOption.READ_TIMEOUT).toMillis()));
213 
214         return connection;
215     }
216 
217     /**
218      * If a proxy is configured with username+password, then set the proxy-authorization header to authorize ourselves with the
219      * proxy
220      */
encodedAuthToken(ProxyConfiguration proxyConfiguration)221     private static String encodedAuthToken(ProxyConfiguration proxyConfiguration) {
222 
223         String authToken = String.format("%s:%s", proxyConfiguration.username(), proxyConfiguration.password());
224         return Base64.getEncoder().encodeToString(authToken.getBytes(StandardCharsets.UTF_8));
225     }
226 
shouldProxyAuthorize()227     private boolean shouldProxyAuthorize() {
228         return this.proxyConfiguration != null
229                && ! StringUtils.isEmpty(this.proxyConfiguration.username())
230                && ! StringUtils.isEmpty(this.proxyConfiguration.password());
231     }
232 
determineProxy(URI uri)233     private Optional<Proxy> determineProxy(URI uri) {
234         if (isProxyEnabled() && isProxyHostIncluded(uri)) {
235             return Optional.of(
236                 new Proxy(Proxy.Type.HTTP,
237                           InetSocketAddress.createUnresolved(this.proxyConfiguration.host(), this.proxyConfiguration.port())));
238         }
239         return Optional.empty();
240     }
241 
isProxyHostIncluded(URI uri)242     private boolean isProxyHostIncluded(URI uri) {
243         return this.proxyConfiguration.nonProxyHosts()
244                                       .stream()
245                                       .noneMatch(uri.getHost().toLowerCase(Locale.getDefault())::matches);
246     }
247 
isProxyEnabled()248     private boolean isProxyEnabled() {
249         return this.proxyConfiguration != null && this.proxyConfiguration.host() != null;
250     }
251 
getSslContext(AttributeMap options)252     private SSLContext getSslContext(AttributeMap options) {
253         Validate.isTrue(options.get(SdkHttpConfigurationOption.TLS_TRUST_MANAGERS_PROVIDER) == null ||
254                         !options.get(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES),
255                         "A TlsTrustManagerProvider can't be provided if TrustAllCertificates is also set");
256 
257         TrustManager[] trustManagers = null;
258         if (options.get(SdkHttpConfigurationOption.TLS_TRUST_MANAGERS_PROVIDER) != null) {
259             trustManagers = options.get(SdkHttpConfigurationOption.TLS_TRUST_MANAGERS_PROVIDER).trustManagers();
260         }
261 
262         if (options.get(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES)) {
263             log.warn(() -> "SSL Certificate verification is disabled. This is not a safe setting and should only be "
264                            + "used for testing.");
265             trustManagers = new TrustManager[] { TrustAllManager.INSTANCE };
266         }
267 
268         TlsKeyManagersProvider provider = this.options.get(SdkHttpConfigurationOption.TLS_KEY_MANAGERS_PROVIDER);
269         KeyManager[] keyManagers = provider.keyManagers();
270 
271         SSLContext context;
272         try {
273             context = SSLContext.getInstance("TLS");
274             context.init(keyManagers, trustManagers, null);
275             return context;
276         } catch (NoSuchAlgorithmException | KeyManagementException ex) {
277             throw new RuntimeException(ex.getMessage(), ex);
278         }
279     }
280 
281     private static class RequestCallable implements ExecutableHttpRequest {
282         private final HttpURLConnection connection;
283         private final HttpExecuteRequest request;
284 
285         /**
286          * Whether we encountered the 'bug' in the way the HttpURLConnection handles 'Expect: 100-continue' cases. See
287          * {@link #getAndHandle100Bug} for more information.
288          */
289         private boolean expect100BugEncountered = false;
290 
291         /**
292          * Result cache for {@link #responseHasNoContent()}.
293          */
294         private Boolean responseHasNoContent;
295 
RequestCallable(HttpURLConnection connection, HttpExecuteRequest request)296         private RequestCallable(HttpURLConnection connection, HttpExecuteRequest request) {
297             this.connection = connection;
298             this.request = request;
299         }
300 
301         @Override
call()302         public HttpExecuteResponse call() throws IOException {
303             connection.connect();
304 
305             Optional<ContentStreamProvider> requestContent = request.contentStreamProvider();
306 
307             if (requestContent.isPresent()) {
308                 Optional<OutputStream> outputStream = tryGetOutputStream();
309                 if (outputStream.isPresent()) {
310                     IoUtils.copy(requestContent.get().newStream(), outputStream.get());
311                 }
312             }
313 
314             int responseCode = getResponseCodeSafely(connection);
315             boolean isErrorResponse = HttpStatusFamily.of(responseCode).isOneOf(CLIENT_ERROR, SERVER_ERROR);
316             Optional<InputStream> responseContent = isErrorResponse ? tryGetErrorStream() : tryGetInputStream();
317             AbortableInputStream responseBody = responseContent.map(AbortableInputStream::create).orElse(null);
318 
319             return HttpExecuteResponse.builder()
320                                       .response(SdkHttpResponse.builder()
321                                                            .statusCode(responseCode)
322                                                            .statusText(connection.getResponseMessage())
323                                                            // TODO: Don't ignore abort?
324                                                            .headers(extractHeaders(connection))
325                                                            .build())
326                                       .responseBody(responseBody)
327                                       .build();
328         }
329 
tryGetOutputStream()330         private Optional<OutputStream> tryGetOutputStream() {
331             return getAndHandle100Bug(() -> invokeSafely(connection::getOutputStream), false);
332         }
333 
tryGetInputStream()334         private Optional<InputStream> tryGetInputStream() {
335             return responseHasNoContent()
336                    ? Optional.empty()
337                    : getAndHandle100Bug(() -> invokeSafely(connection::getInputStream), true);
338         }
339 
tryGetErrorStream()340         private Optional<InputStream> tryGetErrorStream() {
341             InputStream result = invokeSafely(connection::getErrorStream);
342             if (result == null && expect100BugEncountered) {
343                 log.debug(() -> "The response payload has been dropped because of a limitation of the JDK's URL Connection "
344                                 + "HTTP client, resulting in a less descriptive SDK exception error message. Using "
345                                 + "the Apache HTTP client removes this limitation.");
346             }
347             return Optional.ofNullable(result);
348         }
349 
350         /**
351          * This handles a bug in {@link HttpURLConnection#getOutputStream()} and {@link HttpURLConnection#getInputStream()}
352          * where these methods will throw a ProtocolException if we sent an "Expect: 100-continue" header, and the
353          * service responds with something other than a 100.
354          *
355          * HttpUrlConnection still gives us access to the response code and headers when this bug is encountered, so our
356          * handling of the bug is:
357          * <ol>
358          *     <li>If the service returned a response status or content length that indicates there was no response payload,
359          *     we ignore that we couldn't read the response payload, and just return the response with what we have.</li>
360          *     <li>If the service returned a payload and we can't read it because of the bug, we throw an exception for
361          *     non-failure cases (2xx, 3xx) or log and return the response without the payload for failure cases (4xx or 5xx)
362          *     .</li>
363          * </ol>
364          */
getAndHandle100Bug(Supplier<T> supplier, boolean failOn100Bug)365         private <T> Optional<T> getAndHandle100Bug(Supplier<T> supplier, boolean failOn100Bug) {
366             try {
367                 return Optional.ofNullable(supplier.get());
368             } catch (RuntimeException e) {
369                 if (!exceptionCausedBy100HandlingBug(e)) {
370                     throw e;
371                 }
372 
373                 if (responseHasNoContent()) {
374                     return Optional.empty();
375                 }
376 
377                 expect100BugEncountered = true;
378 
379                 if (!failOn100Bug) {
380                     return Optional.empty();
381                 }
382 
383                 int responseCode = invokeSafely(connection::getResponseCode);
384                 String message = "Unable to read response payload, because service returned response code "
385                                  + responseCode + " to an Expect: 100-continue request. Using another HTTP client "
386                                  + "implementation (e.g. Apache) removes this limitation.";
387                 throw new UncheckedIOException(new IOException(message, e));
388             }
389         }
390 
exceptionCausedBy100HandlingBug(RuntimeException e)391         private boolean exceptionCausedBy100HandlingBug(RuntimeException e) {
392             return requestWasExpect100Continue() &&
393                    e.getMessage() != null &&
394                    e.getMessage().startsWith("java.net.ProtocolException: Server rejected operation");
395         }
396 
requestWasExpect100Continue()397         private Boolean requestWasExpect100Continue() {
398             return request.httpRequest()
399                           .firstMatchingHeader("Expect")
400                           .map(expect -> expect.equalsIgnoreCase("100-continue"))
401                           .orElse(false);
402         }
403 
responseHasNoContent()404         private boolean responseHasNoContent() {
405             // We cannot account for chunked encoded responses, because we only have access to headers and response code here,
406             // so we assume chunked encoded responses DO have content.
407             if (responseHasNoContent == null) {
408                 responseHasNoContent = responseNeverHasPayload(invokeSafely(connection::getResponseCode)) ||
409                                        Objects.equals(connection.getHeaderField("Content-Length"), "0") ||
410                                        Objects.equals(connection.getRequestMethod(), "HEAD");
411             }
412             return responseHasNoContent;
413         }
414 
responseNeverHasPayload(int responseCode)415         private boolean responseNeverHasPayload(int responseCode) {
416             return responseCode == 204 || responseCode == 304 || (responseCode >= 100 && responseCode < 200);
417         }
418 
419         /**
420          * {@link sun.net.www.protocol.http.HttpURLConnection#getInputStream0()} has been observed to intermittently throw
421          * {@link NullPointerException}s for reasons that still require further investigation, but are assumed to be due to a
422          * bug in the JDK. Propagating such NPEs is confusing for users and are not subject to being retried on by the default
423          * retry policy configuration, so instead we bias towards propagating these as {@link IOException}s.
424          * <p>
425          * TODO: Determine precise root cause of intermittent NPEs, submit JDK bug report if applicable, and consider applying
426          * this behavior only on unpatched JVM runtime versions.
427          */
getResponseCodeSafely(HttpURLConnection connection)428         private static int getResponseCodeSafely(HttpURLConnection connection) throws IOException {
429             Validate.paramNotNull(connection, "connection");
430             try {
431                 return connection.getResponseCode();
432             } catch (NullPointerException e) {
433                 throw new IOException("Unexpected NullPointerException when trying to read response from HttpURLConnection", e);
434             }
435         }
436 
extractHeaders(HttpURLConnection response)437         private Map<String, List<String>> extractHeaders(HttpURLConnection response) {
438             return response.getHeaderFields().entrySet().stream()
439                            .filter(e -> e.getKey() != null)
440                            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
441         }
442 
443         @Override
abort()444         public void abort() {
445             connection.disconnect();
446         }
447     }
448 
449     /**
450      * A builder for an instance of {@link SdkHttpClient} that uses JDKs build-in {@link java.net.URLConnection} HTTP
451      * implementation. A builder can be created via {@link #builder()}.
452      *
453      * <pre class="brush: java">
454      * SdkHttpClient httpClient = UrlConnectionHttpClient.builder()
455      * .socketTimeout(Duration.ofSeconds(10))
456      * .connectionTimeout(Duration.ofSeconds(1))
457      * .build();
458      * </pre>
459      */
460     public interface Builder extends SdkHttpClient.Builder<UrlConnectionHttpClient.Builder> {
461 
462         /**
463          * The amount of time to wait for data to be transferred over an established, open connection before the connection is
464          * timed out. A duration of 0 means infinity, and is not recommended.
465          */
socketTimeout(Duration socketTimeout)466         Builder socketTimeout(Duration socketTimeout);
467 
468         /**
469          * The amount of time to wait when initially establishing a connection before giving up and timing out. A duration of 0
470          * means infinity, and is not recommended.
471          */
connectionTimeout(Duration connectionTimeout)472         Builder connectionTimeout(Duration connectionTimeout);
473 
474         /**
475          * Configure the {@link TlsKeyManagersProvider} that will provide the {@link javax.net.ssl.KeyManager}s to use
476          * when constructing the SSL context.
477          */
tlsKeyManagersProvider(TlsKeyManagersProvider tlsKeyManagersProvider)478         Builder tlsKeyManagersProvider(TlsKeyManagersProvider tlsKeyManagersProvider);
479 
480         /**
481          * Configure the {@link TlsTrustManagersProvider} that will provide the {@link javax.net.ssl.TrustManager}s to use
482          * when constructing the SSL context.
483          */
tlsTrustManagersProvider(TlsTrustManagersProvider tlsTrustManagersProvider)484         Builder tlsTrustManagersProvider(TlsTrustManagersProvider tlsTrustManagersProvider);
485 
486         /**
487          * Configuration that defines how to communicate via an HTTP proxy.
488          * @param proxyConfiguration proxy configuration builder object.
489          * @return the builder for method chaining.
490          */
proxyConfiguration(ProxyConfiguration proxyConfiguration)491         Builder proxyConfiguration(ProxyConfiguration proxyConfiguration);
492 
493         /**
494          * Sets the http proxy configuration to use for this client.
495          *
496          * @param proxyConfigurationBuilderConsumer The consumer of the proxy configuration builder object.
497          * @return the builder for method chaining.
498          */
proxyConfiguration(Consumer<ProxyConfiguration.Builder> proxyConfigurationBuilderConsumer)499         Builder proxyConfiguration(Consumer<ProxyConfiguration.Builder> proxyConfigurationBuilderConsumer);
500 
501 
502     }
503 
504     private static final class DefaultBuilder implements Builder {
505         private final AttributeMap.Builder standardOptions = AttributeMap.builder();
506         private ProxyConfiguration proxyConfiguration;
507 
DefaultBuilder()508         private DefaultBuilder() {
509         }
510 
511         /**
512          * Sets the read timeout to a specified timeout. A timeout of zero is interpreted as an infinite timeout.
513          *
514          * @param socketTimeout the timeout as a {@link Duration}
515          * @return this object for method chaining
516          */
517         @Override
socketTimeout(Duration socketTimeout)518         public Builder socketTimeout(Duration socketTimeout) {
519             standardOptions.put(SdkHttpConfigurationOption.READ_TIMEOUT, socketTimeout);
520             return this;
521         }
522 
setSocketTimeout(Duration socketTimeout)523         public void setSocketTimeout(Duration socketTimeout) {
524             socketTimeout(socketTimeout);
525         }
526 
527         /**
528          * Sets the connect timeout to a specified timeout. A timeout of zero is interpreted as an infinite timeout.
529          *
530          * @param connectionTimeout the timeout as a {@link Duration}
531          * @return this object for method chaining
532          */
533         @Override
connectionTimeout(Duration connectionTimeout)534         public Builder connectionTimeout(Duration connectionTimeout) {
535             standardOptions.put(SdkHttpConfigurationOption.CONNECTION_TIMEOUT, connectionTimeout);
536             return this;
537         }
538 
setConnectionTimeout(Duration connectionTimeout)539         public void setConnectionTimeout(Duration connectionTimeout) {
540             connectionTimeout(connectionTimeout);
541         }
542 
543         @Override
tlsKeyManagersProvider(TlsKeyManagersProvider tlsKeyManagersProvider)544         public Builder tlsKeyManagersProvider(TlsKeyManagersProvider tlsKeyManagersProvider) {
545             standardOptions.put(SdkHttpConfigurationOption.TLS_KEY_MANAGERS_PROVIDER, tlsKeyManagersProvider);
546             return this;
547         }
548 
setTlsKeyManagersProvider(TlsKeyManagersProvider tlsKeyManagersProvider)549         public void setTlsKeyManagersProvider(TlsKeyManagersProvider tlsKeyManagersProvider) {
550             tlsKeyManagersProvider(tlsKeyManagersProvider);
551         }
552 
553         @Override
tlsTrustManagersProvider(TlsTrustManagersProvider tlsTrustManagersProvider)554         public Builder tlsTrustManagersProvider(TlsTrustManagersProvider tlsTrustManagersProvider) {
555             standardOptions.put(SdkHttpConfigurationOption.TLS_TRUST_MANAGERS_PROVIDER, tlsTrustManagersProvider);
556             return this;
557         }
558 
setTlsTrustManagersProvider(TlsTrustManagersProvider tlsTrustManagersProvider)559         public void setTlsTrustManagersProvider(TlsTrustManagersProvider tlsTrustManagersProvider) {
560             tlsTrustManagersProvider(tlsTrustManagersProvider);
561         }
562 
563         @Override
proxyConfiguration(ProxyConfiguration proxyConfiguration)564         public Builder proxyConfiguration(ProxyConfiguration proxyConfiguration) {
565             this.proxyConfiguration = proxyConfiguration;
566             return this;
567         }
568 
569         @Override
proxyConfiguration(Consumer<ProxyConfiguration.Builder> proxyConfigurationBuilderConsumer)570         public Builder proxyConfiguration(Consumer<ProxyConfiguration.Builder> proxyConfigurationBuilderConsumer) {
571             ProxyConfiguration.Builder builder = ProxyConfiguration.builder();
572             proxyConfigurationBuilderConsumer.accept(builder);
573             return proxyConfiguration(builder.build());
574         }
575 
setProxyConfiguration(ProxyConfiguration proxyConfiguration)576         public void setProxyConfiguration(ProxyConfiguration proxyConfiguration) {
577             proxyConfiguration(proxyConfiguration);
578         }
579 
580 
581         /**
582          * Used by the SDK to create a {@link SdkHttpClient} with service-default values if no other values have been configured
583          *
584          * @param serviceDefaults Service specific defaults. Keys will be one of the constants defined in
585          * {@link SdkHttpConfigurationOption}.
586          * @return an instance of {@link SdkHttpClient}
587          */
588         @Override
buildWithDefaults(AttributeMap serviceDefaults)589         public SdkHttpClient buildWithDefaults(AttributeMap serviceDefaults) {
590             return new UrlConnectionHttpClient(standardOptions.build()
591                                                               .merge(serviceDefaults)
592                                                               .merge(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS),
593                                                null, this);
594         }
595     }
596 
597     private static class NoOpHostNameVerifier implements HostnameVerifier {
598 
599         static final NoOpHostNameVerifier INSTANCE = new NoOpHostNameVerifier();
600 
601         @Override
verify(String s, SSLSession sslSession)602         public boolean verify(String s, SSLSession sslSession) {
603             return true;
604         }
605     }
606 
607     /**
608      * Insecure trust manager to trust all certs. Should only be used for testing.
609      */
610     private static class TrustAllManager implements X509TrustManager {
611 
612         private static final TrustAllManager INSTANCE = new TrustAllManager();
613 
614         @Override
checkClientTrusted(X509Certificate[] x509Certificates, String s)615         public void checkClientTrusted(X509Certificate[] x509Certificates, String s) {
616             log.debug(() -> "Accepting a client certificate: " + x509Certificates[0].getSubjectDN());
617         }
618 
619         @Override
checkServerTrusted(X509Certificate[] x509Certificates, String s)620         public void checkServerTrusted(X509Certificate[] x509Certificates, String s) {
621             log.debug(() -> "Accepting a server certificate: " + x509Certificates[0].getSubjectDN());
622         }
623 
624         @Override
getAcceptedIssuers()625         public X509Certificate[] getAcceptedIssuers() {
626             return new X509Certificate[0];
627         }
628     }
629 }
630