Testing HTTPS Connections with Apache HttpClient 4.2

By | October 8, 2014

In this article I am going to show how to unit test a HTTPS endpoint using Apache HttpClient. In addition I will show how to set up a HTTPS endpoint using the Mule ESB as a mock HTTPS server.
The example in this article is limited to showing how the client will assert the identity of the server using a truststore. The server will not assert the identity of the client (mutual authentication).

Create the Example Project

I am going to develop the example project in Spring Tool Suite with the Anypoint Studio plug-in installed. The main reason for this is to be able to create a project with the appropriate Mule dependencies conveniently and quickly. If you are able to gather the necessary dependencies in some other way, feel free to use the IDE of your choice.
If you use the Anypoint Studio plug-in, you need at least one Mule runtime installed. I have used the 3.5.0 community edition runtime. Version 3.5.0 of Mule CE contains version 4.2 of the Apache HttpClient library, this is the version of HttpClient I chose.

Let’s get started by creating the example project:

  • In the File menu, select New and then Other.
    In the dialog that appears, select the Mule Project wizard.
  • In the Project Settings dialog, enter the name of the project and select the Mule runtime that the project will use. I will call my project “HTTPSUnitTestWithHTTPClient” and, as before, use the Mule 3.5.0 CE runtime.

We have now created the example project. Before we implement anything in this project, we are going to create the server keystore and the client truststore.

Create the Server Keystore

The server keystore is a file which contains the server’s private key. This key is used when clients negotiate a connection with the server to make it possible for clients to verify that the server it is connecting to is indeed the server that the client wants to connect to and not an imposter. In addition, HTTPS negotiation results in an encrypted connection enabling the exchange of information between client and server that is protected against eavesdropping.

To create the server keystore, I used the following commands from the terminal or command window:

keytool -keypass secret -storepass secret -genkey -alias httpskey -keyalg RSA -keystore server_keystore.jks

After having issued the command, you will be asked a number of questions. Answer them as you desire. Below is how I answered the questions when creating my server keystore.

keytool -keypass secret -storepass secret -genkey -alias httpskey -keyalg RSA -keystore server_keystore.jks
What is your first and last name?
  [Unknown]:  Ivan Krizsan
What is the name of your organizational unit?
  [Unknown]:  Home Unit
What is the name of your organization?
  [Unknown]:  Home Organization
What is the name of your City or Locality?
  [Unknown]:  Home City
What is the name of your State or Province?
  [Unknown]:  Home State
What is the two-letter country code for this unit?
  [Unknown]:  SE
Is CN=Ivan Krizsan, OU=Home Unit, O=Home Organization, L=Home City, ST=Home State, C=SE correct?
  [no]:  yes

The result should be a file named “server_keystore.jks”.

Create the Client Truststore

The client truststore contain one or more entries for certificates that the client is expected to trust. When the client tries to connect to a server, the identity of the server is verified against the entries in the truststore.
To create the client truststore, I used the following two commands:

keytool -export -alias httpskey -keystore server_keystore.jks -storepass secret -file server.cert

keytool -import -v -trustcacerts -alias httpskey -keystore client_truststore.jks -storepass secret -file server.cert

After having issued the second command, you will be asked whether to trust the certificate in the server.cert file. Answer yes to this question.
Now you should have a total of three files: “server_keystore.jks”, “client_truststore.jks” and “server.cert”. Copy the first two files to the “src/main/resources” of the example project we created earlier.

Create the Mock Server Configuration

In the example project there should be a file named “httpsunittestwithhttpclient.xml” in the “src/main/app” directory. Paste the following Mule configuration into this file:

<?xml version="1.0" encoding="UTF-8"?>
<mule
    xmlns:https="http://www.mulesoft.org/schema/mule/https"
    xmlns="http://www.mulesoft.org/schema/mule/core"
    xmlns:doc="http://www.mulesoft.org/schema/mule/documentation"
    xmlns:spring="http://www.springframework.org/schema/beans"
    xmlns:test="http://www.mulesoft.org/schema/mule/test"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-current.xsd
        
        http://www.mulesoft.org/schema/mule/core
        http://www.mulesoft.org/schema/mule/core/current/mule.xsd
        
        http://www.mulesoft.org/schema/mule/https
        http://www.mulesoft.org/schema/mule/https/current/mule-https.xsd
        
        http://www.mulesoft.org/schema/mule/test
        http://www.mulesoft.org/schema/mule/test/current/mule-test.xsd">
        
    <https:connector
        name="HTTPSNoClientCert"
        clientSoTimeout="10000"
        serverSoTimeout="10000"
        socketSoLinger="0">
        <!-- Removes the Mule session information from messages sent over this connector. -->
        <service-overrides sessionHandler="org.mule.session.NullSessionHandler"/>
        <!-- Sets the keystore and passwords used by this connector. -->
        <https:tls-key-store path="server_keystore.jks"
            keyPassword="secret" storePassword="secret" />
    </https:connector>
    
    <flow name="https-noclientcert-flow">
        <https:inbound-endpoint
            exchange-pattern="request-response"
            host="localhost" port="8082"
            connector-ref="HTTPSNoClientCert"/>
        
        <test:component logMessageDetails="true"/>
        
        <set-payload value="#[string:Hello, the time is now #[server.dateTime]]"/>
    </flow>
</mule>

If you are familiar with Mule configuration files, please feel free to skip the following explanation.
The above Mule configuration file contains:

  • A HTTPS connector named “HTTPSNoClientCert”.
    A Mule connector contains the configuration for a means of communication. In this case the communication use the basic protocol HTTPS with some timeout parameters and a keystore.
    In addition the connector has a null session handler configured on it which will prevent Mule from sending out Mule session information to clients.
  • A flow named “https-noclientcert-flow”.
    A Mule flow describes a potential path a message may travel.
  • The flow in turn contains an inbound endpoint.
    The inbound endpoint is a receiver for messages sent to the flow.
    As can be seen, the flow references the HTTPS connector mentioned earlier using the connector-ref attribute. The host and port attributes specifies the address and port at which the inbound endpoint will be listening for incoming messages.
  • After the inbound endpoint, the flow contains a test-component.
    Its purpose is to log received messages to the console.
  • Finally the flow contains a processor that sets the payload of the (outgoing) message.

We can test the mock HTTPS server by right-clicking on the Mule configuration file we just created and selecting Run As → Mule Application. Then open a browser, like Firefox, and enter the URL “https://localhost:8082/”.
There may be a warning about an untrusted certificate, but just confirm that you trust the certificate. Having done that, you should see a string like this in the browser window if everything works as expected:

Hello, the time is now 2014-10-05T21:28:19.948+02:00

Add a Log4J Configuration File

In order to be able to see what Mule logs, add the following file named “log4j.xml” in the “src/test/resources” directory in the example project:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration>
    <appender name="stdout" class="org.apache.log4j.ConsoleAppender">
        <param name="Target" value="System.out" />
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d %-5p %-30C - %m%n" />
        </layout>
    </appender>
    
    <logger name="org.mule">
        <level value="INFO"/>
    </logger>
    
    <logger name="org.mule.transport">
        <level value="WARN"/>
    </logger>
    
    <logger name="org.mule.tck.functional">
        <level value="DEBUG"/>
    </logger>
    
    <root>
        <priority value="WARN" />
        <appender-ref ref="stdout" />
    </root>
</log4j:configuration>

Implement the Test

So far, we have only been preparing for the actual purpose of this article, namely the test showing how to connect to a server over HTTPS. The reason I prefer the Apache HttpClient to connect to the server over, for instance the Mule client API, is that it gives me more control over the request sent to the server and I also know that the response I receive in my test-code has not been altered in any way.
The test that connects to the mock server looks like this:

package se.ivankrizsan.mule.https;

import java.security.KeyStore;
import java.util.HashMap;
import java.util.Map;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import org.apache.commons.io.IOUtils;
import org.apache.http.Header;
import org.apache.http.HeaderIterator;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mule.tck.junit4.FunctionalTestCase;

/**
 * Tests connecting to a HTTPS server that does not require a client certificate.
 *
 * @author Ivan Krizsan
 */
public class HttpsNoClientCertificateTest extends FunctionalTestCase {
    /* Constant(s): */
    /** Mule configuration file(s) loaded by the test-case. */
    private final static String[] TEST_CONFIGURATION_FILES =
    { "httpsunittestwithhttpclient.xml" };
    /** URL which to send test-request to. */
    private final static String TEST_ENDPOINT_URL = "https://localhost:8082/";
    /** Client truststore. */
    private static final String CLIENT_TRUSTSTORE = "client_truststore.jks";
    /** Client truststore password. */
    private static final String CLIENT_TRUSTSTORE_PASSWORD = "secret";

    /* Instance variable(s): */
    private DefaultHttpClient mHttpClient;

    @Override
    protected String[] getConfigFiles() {
        return TEST_CONFIGURATION_FILES;
    }

    /**
     * Prepares for tests by performing creating and initializing the HTTP
     * client used to send test-request.
     *
     * @throws Exception If error occurred.
     */
    @Before
    public void setUp() throws Exception {
        mHttpClient = new DefaultHttpClient();

        /* Load client truststore. */
        final KeyStore theClientTruststore = KeyStore.getInstance("JKS");
        theClientTruststore.load(loadResource(CLIENT_TRUSTSTORE),
            CLIENT_TRUSTSTORE_PASSWORD.toCharArray());

        /* Create a trust manager factory using the client truststore. */
        final TrustManagerFactory theTrustManagerFactory =
            TrustManagerFactory.getInstance(TrustManagerFactory
                .getDefaultAlgorithm());
        theTrustManagerFactory.init(theClientTruststore);

        /*
         * Create a SSL context with a trust manager that uses the
         * client truststore.
         */
        final SSLContext theSslContext = SSLContext.getInstance("TLS");
        theSslContext.init(null, theTrustManagerFactory.getTrustManagers(),
            null);

        /*
         * Create a SSL socket factory that uses the client truststore SSL
         * context and that does not perform any kind of hostname verification.
         * IMPORTANT: Hostname verification should be performed in a
         * production environment!
         * To turn on hostname verification, change the
         * ALLOW_ALL_HOSTNAME_VERIFIER below to STRICT_HOSTNAME_VERIFIER.
         */
        final SSLSocketFactory theSslSocketFactory =
            new SSLSocketFactory(theSslContext,
                SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);

        /*
         * Register the SSL socket factory to be used with HTTPS connections
         * with the HTTP client.
         * A {@code Scheme} object is used to associate the protocol scheme,
         * such as HTTPS in this case, and a socket factory.
         */
        final Scheme theHttpsScheme =
            new Scheme("https", 443, theSslSocketFactory);
        mHttpClient.getConnectionManager().getSchemeRegistry().register(
            theHttpsScheme);
    }

    /**
     * Tests a successful HTTPS connection.
     *
     * @throws Exception If error occurs. Indicates test failure.
     */
    @Test
    public void testSuccessfulConnection() throws Exception {
        /*
         * Create a HTTP GET to the HTTPS endpoint and set a HTTP header
         * on the request.
         */
        final HttpGet theHttpGet = new HttpGet(TEST_ENDPOINT_URL);
        theHttpGet.addHeader("test-header-name", "test-header-value");

        /* Send the HTTPS request. */
        final HttpResponse theHttpResponse = mHttpClient.execute(theHttpGet);

        Assert.assertNotNull("A response message should have been received",
            theHttpResponse);

        /* Insert headers from the response in a map. */
        final Map<String, String> theResponseHeadersMap =
            new HashMap<String, String>();
        final HeaderIterator theHeaderIterator =
            theHttpResponse.headerIterator();
        while (theHeaderIterator.hasNext()) {
            final Header theHeader = theHeaderIterator.nextHeader();
            theResponseHeadersMap
            .put(theHeader.getName(), theHeader.getValue());
        }

        /* Retrieve the response body as a string. */
        final String theResponseBodyString =
            IOUtils.toString(theHttpResponse.getEntity().getContent(), "UTF-8");

        /* Print the response headers and body to have something to show. */
        System.out.println("Response headers:  " + theResponseHeadersMap);
        System.out.println("Response body:     " + theResponseBodyString);
    }
}

The code is fairly well commented, but I will despite this provide a brief analysis:

  • The test-class inherits from the class FunctionalTestCase.
    FunctionalTestCase is a base-class for tests that wants to start a Mule instance as part of the tests.
  • The method getConfigFiles returns the Mule configuration file(s) that are to be loaded into the Mule instance started as part of the tests.
    In this case it is the Mule configuration file containing the mock server.
  • The setUp method creates and configures an instance of the Apache HttpClient that uses a certain truststore to verify the identity of the server(s) it communicates with.
  • The test-method testSuccessfulConnection tests what is expected to be a successful connection attempt to the server.

Running the Test

If we now right-click the test-class in the IDE and selects Run As → JUnit Test, the test should pass and you should see, among other things, output similar to this in the console:

2014-10-05 22:00:11,844 INFO  org.mule.module.logging.DispatchingLogger - 
********************************************************************************
* Message Received in service: https-noclientcert-flow. Content is: /          *
********************************************************************************
2014-10-05 22:00:11,845 INFO  org.mule.module.logging.DispatchingLogger - Full Message payload: 
/

Message properties:
  INVOCATION scoped properties:
  INBOUND scoped properties:
    Connection=true
    Host=localhost:8082
    Keep-Alive=true
    MULE_ORIGINATING_ENDPOINT=endpoint.https.localhost.8082
    MULE_REMOTE_CLIENT_ADDRESS=/127.0.0.1:50474
    User-Agent=Apache-HttpClient/4.2 (java 1.5)
    http.context.path=/
    http.context.uri=https://localhost:8082
    http.headers={Host=localhost:8082, User-Agent=Apache-HttpClient/4.2 (java 1.5), Keep-Alive=true, test-header-name=test-header-value, Connection=true}
    http.method=GET
    http.query.params={}
    http.query.string=
    http.relative.path=
    http.request=/
    http.request.path=/
    http.version=HTTP/1.1
    test-header-name=test-header-value
  OUTBOUND scoped properties:
    LOCAL_CERTIFICATES=[Ljava.security.cert.X509Certificate;@76c5ccf
    MULE_ENCODING=UTF-8
  SESSION scoped properties:

Response headers:  {LOCAL_CERTIFICATES=[Ljava.security.cert.X509Certificate;@76c5ccf, X-MULE_ENCODING=UTF-8, Date=Sun, 05 Oct 2014 22:00:11 +0200, Content-Length=52, Connection=close, Content-Type=tex
t/plain, Server=Mule EE Core Extensions/3.5.0}
Response body:     Hello, the time is now 2014-10-05T22:00:11.859+02:00

From the above, we can see that the request, with the HTTP header “test-header-name” having the value “test-header-value”, was received by the mock server. We can also see the response headers and the response payload, the latter which contains the same type of greeting string that we saw in the browser when testing the mock server.

Note that we can also see that the Apache HttpClient library was used to send the request to the server by looking for the “User-Agent” HTTP header which has the value “Apache-HttpClient/4.2 (java 1.5)”.

 

Leave a Reply

Your email address will not be published. Required fields are marked *