/** * Copyright 2016 Yahoo Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.yahoo.pulsar.client.impl; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.Map; import java.util.Properties; import java.util.concurrent.CompletableFuture; import io.netty.channel.EventLoopGroup; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.ssl.SslContext; import org.asynchttpclient.*; import org.asynchttpclient.channel.DefaultKeepAliveStrategy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.util.concurrent.MoreExecutors; import com.yahoo.pulsar.client.api.Authentication; import com.yahoo.pulsar.client.api.AuthenticationDataProvider; import com.yahoo.pulsar.client.api.PulsarClientException; import com.yahoo.pulsar.common.util.ObjectMapperFactory; import com.yahoo.pulsar.common.util.SecurityUtility; public class HttpClient implements Closeable { protected final static int DEFAULT_CONNECT_TIMEOUT_IN_SECONDS = 10; protected final static int DEFAULT_READ_TIMEOUT_IN_SECONDS = 30; protected final AsyncHttpClient httpClient; protected final URL url; protected final Authentication authentication; protected HttpClient(String serviceUrl, Authentication authentication, EventLoopGroup eventLoopGroup, boolean tlsAllowInsecureConnection, String tlsTrustCertsFilePath) throws PulsarClientException { this(serviceUrl, authentication, eventLoopGroup, tlsAllowInsecureConnection, tlsTrustCertsFilePath, DEFAULT_CONNECT_TIMEOUT_IN_SECONDS, DEFAULT_READ_TIMEOUT_IN_SECONDS); } protected HttpClient(String serviceUrl, Authentication authentication, EventLoopGroup eventLoopGroup, boolean tlsAllowInsecureConnection, String tlsTrustCertsFilePath, int connectTimeoutInSeconds, int readTimeoutInSeconds) throws PulsarClientException { this.authentication = authentication; try { // Ensure trailing "/" on url url = new URL(serviceUrl); } catch (MalformedURLException e) { throw new PulsarClientException.InvalidServiceURL(e); } DefaultAsyncHttpClientConfig.Builder confBuilder = new DefaultAsyncHttpClientConfig.Builder(); confBuilder.setFollowRedirect(true); confBuilder.setConnectTimeout(connectTimeoutInSeconds * 1000); confBuilder.setReadTimeout(readTimeoutInSeconds * 1000); confBuilder.setUserAgent(String.format("Pulsar-Java-v%s", getPulsarClientVersion())); confBuilder.setKeepAliveStrategy(new DefaultKeepAliveStrategy() { @Override public boolean keepAlive(Request ahcRequest, HttpRequest request, HttpResponse response) { // Close connection upon a server error or per HTTP spec return (response.getStatus().code() / 100 != 5) && super.keepAlive(ahcRequest, request, response); } }); if ("https".equals(url.getProtocol())) { try { SslContext sslCtx = null; // Set client key and certificate if available AuthenticationDataProvider authData = authentication.getAuthData(); if (authData.hasDataForTls()) { sslCtx = SecurityUtility.createNettySslContext(tlsAllowInsecureConnection, tlsTrustCertsFilePath, authData.getTlsCertificates(), authData.getTlsPrivateKey()); } else { sslCtx = SecurityUtility.createNettySslContext(tlsAllowInsecureConnection, tlsTrustCertsFilePath); } confBuilder.setSslContext(sslCtx); confBuilder.setAcceptAnyCertificate(tlsAllowInsecureConnection); } catch (Exception e) { throw new PulsarClientException.InvalidConfigurationException(e); } } confBuilder.setEventLoopGroup(eventLoopGroup); AsyncHttpClientConfig config = confBuilder.build(); httpClient = new DefaultAsyncHttpClient(config); log.debug("Using HTTP url: {}", this.url); } @Override public void close() throws IOException { httpClient.close(); } public <T> CompletableFuture<T> get(String path, Class<T> clazz) { final CompletableFuture<T> future = new CompletableFuture<>(); try { String requestUrl = new URL(url, path).toString(); AuthenticationDataProvider authData = authentication.getAuthData(); BoundRequestBuilder builder = httpClient.prepareGet(requestUrl); // Add headers for authentication if any if (authData.hasDataForHttp()) { for (Map.Entry<String, String> header : authData.getHttpHeaders()) { builder.setHeader(header.getKey(), header.getValue()); } } final ListenableFuture<Response> responseFuture = builder.setHeader("Accept", "application/json") .execute(new AsyncCompletionHandler<Response>() { @Override public Response onCompleted(Response response) throws Exception { return response; } @Override public void onThrowable(Throwable t) { log.warn("[{}] Failed to perform http request: {}", requestUrl, t.getMessage()); future.completeExceptionally(new PulsarClientException(t)); } }); responseFuture.addListener(() -> { try { Response response = responseFuture.get(); if (response.getStatusCode() != HttpURLConnection.HTTP_OK) { log.warn("[{}] HTTP get request failed: {}", requestUrl, response.getStatusText()); future.completeExceptionally( new PulsarClientException("HTTP get request failed: " + response.getStatusText())); return; } T data = ObjectMapperFactory.getThreadLocal().readValue(response.getResponseBodyAsBytes(), clazz); future.complete(data); } catch (Exception e) { log.warn("[{}] Error during HTTP get request: {}", requestUrl, e.getMessage()); future.completeExceptionally(new PulsarClientException(e)); } }, MoreExecutors.sameThreadExecutor()); } catch (Exception e) { log.warn("[{}] Failed to get authentication data for lookup: {}", path, e.getMessage()); if (e instanceof PulsarClientException) { future.completeExceptionally(e); } else { future.completeExceptionally(new PulsarClientException(e)); } } return future; } /** * Looks for a file called pulsar-client-version.properties and returns the client version * * @return client version or unknown version depending on whether the file is found or not. */ public static String getPulsarClientVersion() { String path = "/pulsar-client-version.properties"; String unknownClientIdentifier = "UnknownClient"; try { InputStream stream = HttpClient.class.getResourceAsStream(path); if (stream == null) { return unknownClientIdentifier; } Properties props = new Properties(); try { props.load(stream); String version = (String) props.get("pulsar-client-version"); return version; } catch (IOException e) { return unknownClientIdentifier; } finally { stream.close(); } } catch (Throwable t) { return unknownClientIdentifier; } } private static final Logger log = LoggerFactory.getLogger(HttpClient.class); }