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