Статьи

Тестирование HTTPS-соединений с Apache HttpClient 4.2

 

В этой статье я собираюсь показать, как выполнить модульное тестирование конечной точки HTTPS с помощью Apache HttpClient . Кроме того, я покажу, как настроить конечную точку HTTPS, используя Mule ESB в качестве фиктивного HTTPS-сервера.
Пример в этой статье ограничен показом того, как клиент будет утверждать идентичность сервера с помощью склада доверенных сертификатов. Сервер не будет утверждать личность клиента (взаимная аутентификация).

Создать пример проекта

Я собираюсь разработать пример проекта в Spring Tool Suite с установленным плагином Anypoint Studio . Основная причина этого заключается в том, чтобы иметь возможность удобно и быстро создать проект с соответствующими зависимостями Mule. Если вы можете собрать необходимые зависимости каким-либо другим способом, не стесняйтесь использовать IDE по вашему выбору.
Если вы используете плагин Anypoint Studio, вам нужно установить хотя бы одну среду исполнения Mule. Я использовал среду выполнения 3.5.0. Версия 3.5.0 Mule CE содержит версию 4.2 библиотеки Apache HttpClient, это версия HttpClient, которую я выбрал.

Давайте начнем с создания примера проекта:

  • В меню File выберите New и затем Other.
    В появившемся диалоговом окне выберите мастер проекта Mule.
  • В диалоговом окне «Параметры проекта» введите имя проекта и выберите среду выполнения Mule, которую будет использовать проект. Я назову свой проект «HTTPSUnitTestWithHTTPClient» и, как и раньше, использую среду выполнения Mule 3.5.0 CE.

Теперь мы создали пример проекта. Прежде чем мы реализуем что-либо в этом проекте, мы собираемся создать хранилище ключей сервера и хранилище доверенных сертификатов клиента.

Создайте хранилище ключей сервера

Хранилище ключей сервера — это файл, который содержит закрытый ключ сервера. Этот ключ используется, когда клиенты договариваются о соединении с сервером, чтобы клиенты могли проверить, что сервер, к которому он подключается, действительно является сервером, к которому клиент хочет подключиться, а не обманщиком. Кроме того, согласование HTTPS приводит к зашифрованному соединению, обеспечивающему обмен информацией между клиентом и сервером, который защищен от перехвата.

Чтобы создать хранилище ключей сервера, я использовал следующие команды из терминала или командного окна:

keytool-keypass secret-storepass secret-genkey-aliashttpskey-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

Результатом должен быть файл с именем «server_keystore.jks».

Создайте клиентское хранилище доверенных сертификатов

Склад доверенных сертификатов клиента содержит одну или несколько записей для сертификатов, которым клиент должен доверять. Когда клиент пытается подключиться к серверу, удостоверение сервера проверяется по записям в хранилище доверенных сертификатов.

Чтобы создать клиентское хранилище доверенных сертификатов, я использовал следующие две команды:

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

После ввода второй команды вас спросят, доверять ли сертификату в файле server.cert. Ответьте да на этот вопрос.

Теперь у вас должно быть всего три файла: «server_keystore.jks», «client_truststore.jks» и «server.cert». Скопируйте первые два файла в «src / main / resources» примера проекта, который мы создали ранее.

Создать макет конфигурации сервера

В примере проекта должен быть файл с именем «httpsunittestwithhttpclient.xml» в каталоге «src / main / app». Вставьте следующую конфигурацию Mule в этот файл:

<?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"/>

Если вы знакомы с файлами конфигурации Mule, не стесняйтесь пропустить следующее объяснение.

Приведенный выше файл конфигурации Mule содержит:

  • Разъем HTTPS с именем «HTTPSNoClientCert».
    Разъем Mule содержит конфигурацию для средства связи. В этом случае для связи используется базовый протокол HTTPS с некоторыми параметрами времени ожидания и хранилище ключей.
    Кроме того, на соединителе настроен нулевой обработчик сеанса, который предотвратит отправку Mule информации сеанса Mule клиентам.
  • Поток с именем «https-noclientcert-flow».
    Поток Мула описывает потенциальный путь, по которому может пройти сообщение.
  • Поток в свою очередь содержит входящую конечную точку.
    Входящая конечная точка является получателем сообщений, отправляемых потоку.
    Как можно видеть, поток ссылается на упомянутый ранее HTTPS-коннектор с использованием атрибута connector-ref. Атрибуты хоста и порта указывают адрес и порт, на котором входящая конечная точка будет прослушивать входящие сообщения.
  • После входящей конечной точки поток содержит тестовый компонент.
    Его цель — записывать полученные сообщения на консоль.
  • Наконец, поток содержит процессор, который устанавливает полезную нагрузку (исходящего) сообщения.

Мы можем протестировать фиктивный HTTPS-сервер, щелкнув правой кнопкой мыши на файле конфигурации Mule, который мы только что создали, и выбрав Run As → Mule Application. Затем откройте браузер, например Firefox, и введите URL-адрес «https: // localhost: 8082 /».

Может быть предупреждение о ненадежном сертификате, но просто подтвердите, что вы доверяете сертификату. Сделав это, вы должны увидеть такую ​​строку в окне браузера, если все работает как положено:

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

Добавьте файл конфигурации Log4J

Чтобы увидеть, что регистрирует Mule, добавьте следующий файл с именем «log4j.xml» в каталог «src / test / resources» в примере проекта:

<!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>

Выполнить тест

Пока что мы только готовились к реальной цели этой статьи, а именно к тесту, показывающему, как подключиться к серверу по HTTPS. Причина, по которой я предпочитаю подключение Apache HttpClient к серверу, например, через клиентский API-интерфейс Mule, заключается в том, что он дает мне больший контроль над запросом, отправляемым на сервер, и я также знаю, что ответ, полученный в моем тестовом коде, имеет не были изменены в любом случае.

Тест, который подключается к серверу-макету, выглядит следующим образом:

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 =

Код довольно хорошо прокомментирован, но я, несмотря на это, приведу краткий анализ:

  • Тест-класс наследуется от класса FunctionalTestCase .
    FunctionalTestCase — это базовый класс для тестов, который хочет запустить экземпляр Mule как часть тестов.
  • Метод getConfigFiles возвращает файлы конфигурации Mule, которые должны быть загружены в экземпляр Mule, запущенный как часть тестов.
    В данном случае это файл конфигурации Mule, содержащий фиктивный сервер.
  • Метод setUp создает и настраивает экземпляр Apache HttpClient, который использует определенное хранилище доверенных сертификатов для проверки подлинности серверов, с которыми он связывается.
  • Тест-метод testSuccessfulConnection проверяет, что ожидается успешной попыткой подключения к серверу.

Выполнение теста

Если мы теперь щелкнем правой кнопкой мыши по классу test в IDE и выберем Run As → JUnit Test, тест должен пройти, и вы увидите, среди прочего, вывод, похожий на этот в консоли:

********************************************************************************
* 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

Из вышесказанного видно, что запрос с HTTP-заголовком «test-header-name» со значением «test-header-value» был получен фиктивным сервером. Мы также можем видеть заголовки ответа и полезную нагрузку ответа, которая содержит строку приветствия того же типа, которую мы видели в браузере при тестировании фиктивного сервера.

Обратите внимание, что мы также видим, что библиотека Apache HttpClient использовалась для отправки запроса на сервер путем поиска HTTP-заголовка «User-Agent» со значением «Apache-HttpClient / 4.2 (java 1.5)».