Closable & secure http connection

Updated: Aug 1, 2021

Today I will discuss how to create a secure and closable http client and maintain several such clients for several destination hosts.

 

Content

  1. The maven package

  2. The Idea

  3. The Mechanism

3.1. HttpConnManager class

3.2. HttpConnection class

3.3. QueryResponse class

3.4. Executor class

 

The maven package

I am using the httpclient from apache. I have added the below maven dependency in my pom.

Dependency

<dependency>
  <groupId>org.apache.httpcomponents</groupId>
  <artifactId>httpclient</artifactId>
  <version>4.5.3</version>
</dependency>

The Idea

The idea is to create a pool of HTTP connections to connect to a remote host, the connection will be closable and secure via TLS. Closable means we can create a pool of connections per HTTP remote host that can be configured using maximum open connections in the pool, evict open connections after an interval if not used and few more configuration parameters. It will be configured with SSL. Each request will have request configuration such as timeouts will be configured for them. A cache will be maintained where for each host a pooled connection object will be used.


The Mechanism

The CloseableHttpClient class used for this purpose. To build and configure such an object we take help of the HttpClientBuilder class. The builder is configured for HTTP request configurations such as socket and HTTP timeout. In this way each request created by the closable client will have these request configurations by default. The builders also configured with SSL. For each unique host a HttpConnection object is created. Each such object is created with the help of the closable client so that each HttpConnection object has the properties of a closable client. A cache of HTTP connections is maintained where the connection is saved for each host. The HttpConnManager class performs these two tasks, it configures a connection to be secure and closable and maintains the cache. The connections map holds this cache. All these are encapsulated by the HttpConnManager. We provide a HttpClientBuilder object and few properties to the HttpConnManager. The properties are used to configure the HttpClientBuilder, if a value for a property is not provided the default value is used. When a connection is needed for a remote host we can call the getConnection() method of the HttpConnManager with the hostname. The corresponding HttPConnection object is either returned from the cache or created and saved in the cache then returned to the caller.


HttpConnManager class

import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;   
import java.util.Properties;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
 
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
 
import org.apache.http.HttpPost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.config.RequestConfig.Builder;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBulder;
import org.apache.http.ssl.SSLContexts;

public class HttpConnManager {
  public static final String[] secureProtocols = {"TLSv1","TLSv1.1", "TLSv1.2"};
  public static final String SOCKET_TIMEOUT_PROP = "http.client.timeout.read";
  public static final int SOCKET_TIMEOUT = (int) TimeUnit.MINUTES.toMillis(4);
  public static final int CONNECTION_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(60);
 
  public static final String MAX_CONN_PER_ROUTE_PROP =  
  "http.client.conn.max.route";
  public static final int MAX_CONN_PER_ROUTE = 128;
  public static final String MAX_CONN_TOTAL_PROP = "http.client.conn.max.total";
  public static final int MAX_CONN_TOTAL = 500 * MAX_CONN_PER_ROUTE ;
  public static final String EVICT_INTERVAL_PROP = 
   "http.client.conn.evict-idle.interval";
  public static final long EVICT_INTERVAL = TimeUnit.MINUTES.toMillis(1);
  public static final String CONN_TIME_TO_LIVE_PROP = "http.client.conn.ttl";
  public static final int CONN_TIME_TO_LIVE = (int) TimeUnit.MINUTES.toMillis(1);
 
 private CloseableHttpClient CClient;
  private Lock lock = new ReentrantLock();
  private ConcurrentMap<HttpHost, HttpConnection> connections = 
    new ConcurrentHashMap<>();
public  HttpConnManager(HttpClientBuilder builder, Properties properties) {
   init(builder, properties);
  }
  private void init(HttpClientBuilder builder, Properties properties ) {
    Builder requestConfig = RequestConfig.custom();
    requestConfig.setConnectionTimeout(CONNECTION_TIMEOUT);
    int socketTimeout = getProperty(properties, SOCKET_TIMEOUT_PROP,  
          SOCKET_TIMEOUT);
    requestConfig.setSocketTimeout(socketTimeout);
 
    builder.setDefaultRequestConfig(requestConfig.build());
    builder.disableAutomaticRetries();
    builder.disableAuthCaching();
    builder.disableConnectionState();
    builder.disableCookiemanagement();
    builder.disableRedirectHandling();  
 
    long evictInterval = getProperty (properties, EVICT_INTERVAL_PROP,  
         EVICT_INTERVAL);
    builder.evictIdleConnections( evictInterval, TimeUnit.MILLISECONDS);
 
    int maxConnperRoute =  getProperty (properties, MAX_CONN_PER_ROUTE_PROP ,  
         MAX_CONN_PER_ROUTE);
    builder.setMaxConnPeroute(maxConnperRoute);
 
    int maxConnTotal = getProperty(properties,MAX_CONN_TOTAL_PROP,
         MAX_CONN_TOTAL);
    builder.setMaxConnTotal(maxConnTotal);
 
    int connTtl =  getProperty (properties, CONN_TIME_TO_LIVE_PROP,  
CONN_TIME_TO_LIVE);
    builder.setConnectionTimeToLive(connTtl);
 
    CClient = secureClient(builder); 
  }
  private CloseableHttpClient secureClient(HttpClientBuilder builder) {
    try {
      SSLContext sslcontext = SSLContexts.custom()                                                   .   .loadTrustMaterial(TrustSelfSignedStrategy.INSTANCE)
            .build();
      HostnameVerifier verifier = v   
    SSLConnectionSocketFactory.getDefaultHostnameVerifier();
      SSLConnectionSocketFactory socketFactory = 
    new SSLConnectionSocketFactory(sslcontext, secureProtocols, null, 
         verifier);
      builder.setSSLSocketFactory(socketFactory);
 
      return builder.build();
    }catch(NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {
      throw new RuntimeException(e);
    }
  }
  private int getProperty(Properties properties, String name, long value) {
    final String strValue =   properties.getProperty(name);
    if( strValue !== null) { value = Integer.parseInt(strValue);}
    return value;
  }
  private long getProperty(Properties properties, String name, long value) {
    final String strValue =   properties.getProperty(name);
    if( strValue !== null) { value = Long.parseLong(strValue);}
    return value;
  }
  public HttpConnection getConnection(HttpHost host) {
    HttpConnection conn = connections.get(host);
    if( conn == null) {
       conn = createConnection(host); 
    }
    return  conn;
  }
  private  HttpConnection createConnection(HttpHost host) {
     HttpConnection conn;
     lock.lock();
     try {
       conn = connections.get(host);
       if( conn == null) {
          conn = createConnectionImpl(host);
          connections.put(host, connection);
       }
       return conn;
     } finally {
       lock.unlock();
     }
  }
  private  HttpConnection createConnectionImpl(HttpHost host) {
    return new HttpConnection(CClient, host); 
  }  
} 

The HttpConnection class encapsulates individual REST endpoints and provides methods to interact with those endpoints. Here you might say why different remote hosts will have the same endpoint, but the point here is just to show how the actual HTTP request creation can be encapsulated behind a class method. In this class we take the headers from the incoming request and use them to create a HTTPGet or HTTPPost request object. We also take a few request parameters to finally create a request URL out of a URL template. The request objects are created based on the closable client. In this example we take the employee id and employee department from the incoming URL and create HTTP requests to a remote host. The remote host can be another micro service as well. The response body and response status are then encapsulated in a QueryResponse object.


HttpConnection class

import static org.apache.http.HttpStatus.SC_NO_CONTENT;
import static org.apache.http.cookie.SM.COOKIE;
 
import java.io.IOException;
import java.util.Objects;
 
import javax.servlet.http.HttpServletRequest;
 
import org.apache.http.*;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HttpConnection {
  //@formatter:off
  public static final String relativepath_Get =
    "/api/v1/rest/employee/details?eid=%1$s&dept=%2$s";
  //@formatter:on
 public static final String relativepath_Post =
   "/api/v1/rest/employee/details";
 
  private final Logger logger =LoggerFactory.getLogger(HttpConnection.class);  
  private CloseableHttpClient client;
  private HttpHost host;
 
  public HttpConnection(CloseableHttpClient client,  HttpHost host) {
    this.client = client;
    this.host = host;
  }
  public String executePost(HttpServletRequest incomingRequest, String query)  
            throws IOException {
     CloseableHttpResponse response = null;
     try {
       HttpPost request = new HttpPost(relativepath_Post);
       addHeaders(incomingRequest, request);
       request.setEntity(new StringEntity(query,ContentType.APPLICATION_JSON));
       response = execute(request);
       String postResponse = EntityUtils.toString(response.getEntity());
       return  postResponse;
     } finally {
      close(response);
     }
  }
  public QueryResponse executeGet(HttpServletRequest incomingRequest, String eid, 
        String dept) throws RuntimeException {
     CloseableHttpResponse response = null; 
     try { 
       String path = String.format(relativePath_Get, eid, dept);
       HttpGet request = new HttpGet(path);
       addHeaders(incomingRequest, request);
       response = execute(request);
       int status = response.getStatusLine().getStatusCode();
       if(status != 200) {
           String msg = String.format("received status code %1$d for request 
         %2$s %3$s", status, host, path);
           throw new RuntimeException(msg);
       }
       QueryResponse queryResponse = toQueryResponse(response);
       return  queryResponse;   
     } catch(RuntimeException e) { 
       throw e;
     } catch(Throwable t) {
      throw new RuntimeException(host.toString()+ ' '+ t.getMessage(), t);
     } finally {
      close(response);
     }
  }
  public ClosableHttpResponse execute(HttpRequest request) throws IOException {
     ClosableHttpResponse response = client.execute(host, request);
     return response;
  }
  private void close(ClosableHttpResponse response) {
     if(response == null) {
      return;
     }
     EntityUtils.consumeQuietly(response.getEntity());
     try {
       response.close();
     } catch(IOException e) {
       logger.warn(e,getMessage(), e);
     }
  }
  protected void addHeaders(HttpServletRequest request, HttpRequestbase req) {
     req.setHeader("Authorization", request.getHeader("Authorization"));
     req.setHeader(COOKIE, request.getHeader("cookie"));
     req.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");
  }
  private QueryResponse toQueryResponse(HttpResponse response) throws IOException {
    int status = response.getStatusLine().getStatusCode();
    String body = null;
    if(status != SC_NO_CONTENT) {
      body = EntityUtils.toString(response.getEntity());
    }
    QueryResponse result = new QueryResponse(status, body);
    Header[] headers = response.getAllHeaders();
    for(Header header : headers) {
     result.addHeader(header.getName(), header.getValue());
    }
    return result;
  }
}

After a response is received it is wrapped to a class, we name this class QueryResponse. The headers of the response are also copied in this object and a HTTP response to the caller can be created.


QueryResponse class

public class QueryResponse {
   private int status;
   private Map<String, List<String>> headers = new hashMap<>();
   private string body;
   private boolean isJson;
 
   public  QueryResponse(String body) {this(HttpStatus.OK, body);}
   public  QueryResponse(int status, String body) {
     this.status = status;
     this.body = body;
   }
   public int getStatus() {return status;}
 
   public String getBody() {return body;}
 
   public Map<String, List<String>> getHeaders() { return headers;}
 
   public void addHeader(String name, String value) {
     Objects.requireNotNull(name);
     Objects.requireNotNull(value);
     List<String> headerList = headers.get(name);
     if(headerList == null) {
       headerList = new ArrayList<>();
       headers.put(name,headerList);
     }
     if("Content-Type".equalsIgnoreCase(name)) && 
   value.startsWith("application/json")) { isJson = true;}
  headerList.add(value); 
  }
}

In the end I have created an Executor class that can use all the above classes and send HTTP requests.


Executor class

import org.apache.http.config.ConnectionConfig;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.HttpHost;
import java.util.Properties;
 
public class Executor {
  private static final int  bufferSize = 1024 * 32;
 
  public static void main(String[] args) {
    String hostString1 = "https://sample.host1.com";
    String hostString2 = "https://sample.host2.com";
    int port = 443;
    String scheme = "https";
    HttpHost host1 = new HttpHost(hostString1, port, scheme);
    HttpHost host2 = new HttpHost(hostString2, port, scheme);
 
    HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
    final ConnectionConfig connectionConfig = 
   ConnectionConfig.custom().setBuffer(bufferSize).build();
    httpClientBuilder.setDefaultConnectionConfig(connectionConfig);
 
    Properties properties = new  Properties();
 //properties needed to create the closeable client
    properties.setProperty(<name>,<value>); 
 
    HttpConnManager connManager = new HttpConnManager(httpClientBuilder,
           properties);
    HttpConnection httpConnection = connManager.getConnection(host1);
 //incomingRequest is a HttpServletRequest, and query is a query payload
    httpConnection.executePost(incomingRequest, query); 
 //eid is employee id , dept is employee department, retrieved from get 
    //request URL parameter
    httpConnection.executeGet(incomingRequest, eid, dept);  
  }
}

Imagine there is a micro service between an upstream and one or more downstream service. This way the micro service can create a secure HTTP connection to downstream for each call it receives from its upstream. Though nowadays we prefer to use spring, this approach is useful if we have legacy code where spring is not used. I have found this link to cwiki on CloseableHttpClient interesting, you can refer to it for related code snippets.


25 views0 comments

Recent Posts

See All