Сам себе web-сервис овер SSH

… или как замокать сервис, на порту удаленной машины, подняв его где угодно

Соль

Наверняка вам приходилось тестировать серверные компоненты и их взаимодействием между собой. Уверен, вы даже знаете, что порт это не только место, куда приходят корабли, а ссш - это не только звук, издаваемый змеёй. Значит вы в курсе, что сервисы, располагаясь на одной или нескольких машинах активно между собой общаются. Чаще всего по протоколу HTTP. И от версии к версии это общение нужно контролировать.

Есть ряд давно известных способов узнать как и что передается. Один из наиболее популярных и не требующих серьезной подготовки - подключиться специальной программой к одному из источников передачи или приёма данных. Такие программы называются снифферами.

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

Мы рассмотрим оба.

Far far away…

В далеком царстве, предалеком государстве жили и тужили тестировщики. И был у них зверь страшный, с именем заморским. Tcpdumpом его кликали.

И приносили тестировщикам каждую неделю жертву новую. Сервис жертвой этой был. Да необычный сервис, а с коллбэком.

Принося сервис новый, клич разработчики пускали по полям гулять: “Вы же тестировщики!” - восклицали они, - “Спустите tcpdump свой, на колбэки эти треклятыя!” - просили они.

Забросил Старик невод первый раз… Запускали tcpdump тестировщики, да грепали в брюхе его необъятном крохи магические… Нагрепанное бережно собирали, да парсили бедные. С трудом превеликим. Но некуда было им деваться, жертву перемолоть требовалось…

Подходим ближе

Я думаю, понятно, что речь идет о первом способе - использовании сниффера. Tcpdump - это сниффер, который можно найти в любом unix-based дистрибутиве ОС. Программа отличная, с огромным количеством возможностей.

И казалось бы, не такая уж и грустная сказка. Пока не задумаешься о том, что нужно каждый релиз сервиса лезть в tcpdump и просеивать горы трафика! Ведь все что мы ловим - это просто текст. Текст, который нужно самостоятельно обрабатывать. Становится мучительно горько, скучно и тяжело.

Проблема

В двух словах - нам нужно каждый релиз повторять одно и то же действие - включить сниффер, получить его лог. Обработать этот лог. Убедиться что в логе есть все что нужно. И все это обычно по ssh. (Мы же тестируем сервис интеграционно, как он будет работать в боевом режиме).

Вы сказали ssh?

Повторяющиеся из релиза в релиз механические действия, отнимающие много времени и сил стоит автоматизировать. Как это делать в JAVA? Берем библиотечку для работы по SSH, подключаемся к серверу, запускаем tcpdump и ловим все что можем в файлик. После теста киляем процесс и грепаем в файлике что нужно. Делали так? Нет? И не нужно!

Почему не нужно?

  • Работа с парсингом больших строк - это всегда много специфического кода, который сложно поддерживать.
  • Парсинг HTTP сообщений уже давно сделан в сотнях библиотек. Зачем делать сто первую?

Начнем сначала

Так как речь идет о тестировании - скорее всего, у нас есть все возможности не просто поставить сеточку в виде tcpdump, но и подменить один из сервисов целиком. Для таких целей есть замечательная библиотека WireMock. Ее код можно посмотреть на GitHub-странице проекта. Суть ее в том, что поднимается web-сервис, с хорошим REST-api, который можно настроить почти произвольным образом.

Тут и произвольные статус коды, и произвольное содержимое, и проксирование запроса в реальные сервисы с возможностью сохранить ответы и отсылать их затем самостоятельно. Особо стоит отметить возможность воссоздать негативное поведение - таймауты, обрывы связи, невалидные ответы. Красота! Библиотека при этом может работать и как самостоятельный jar, и как WAR, который можно загрузить в Jetty, Tomcat… И самое главное: эту библиотеку можно использовать прямо в тестах как рулу!

Схема

Что проверяем? Сообщение в схеме

тестируемый_сервис -> {сообщение} -> фейковый_сервис.

Более точно, схема будет выглядеть так:

тестируемый_сервис -> фейковый_сервис :(его лог): {сообщение}.

Значит нам нужно сделать несколько вещей:

  • Поднять фейковый сервис и настроить его принимать определенные сообщения, отвечая ОК.
  • Обеспечить доставку сообщений до фейкового сервиса (в схеме это ->).
  • Провалидировать то что пришло.

Поднимаем фейк-сервис у себя

Итак, как и написано в гайде на странице библиотеки, создаем рулу:

@Rule
public WireMockRule wiremock = new WireMockRule(LOCAL_MOCKED_PORT);

И включаем эмуляцию сервиса:

@Test
public void shouldSend3Callbacks() throws Exception {
    // Пусть наш фейк-сервис принимает любые сообщения
    stubFor(any(urlMatching(".*")).willReturn(aResponse()
                .withStatus(HttpStatus.OK_200).withBody("OK")));
                
    // Здесь та магия, которая инициирует общение сервисов
    
    // Обращаясь к логу фейк-сервиса, убеждаемся в наличии нужных сообщений
    verify(3, postRequestedFor(urlMatching(".*callback.*"))
                 .withRequestBody(matching("^status=.*")));
}

Говорим тестируемому - “Теперь ходи к нам!”

Осталась одна сложность - заставить удаленный сервис ходить в наш фейковый. Хорошо, если у нас обе машины связаны друг с другом напрямую, любой порт доступен и открыт. В таком случае, все что ниже можно не читать.

Но обычно все сложнее. Открыты только определенные порты, машины в разных подсетях, а одна из машин вообще может не иметь постоянного IP адреса. Как быть?

Вот тут вспоминаем о такой штуке как ssh порт-форвардинг (или ssh-туннелинг). Нам потребуется REMOTE (который с ключом -R) и соответственно ssh-доступ к машинке. Это позволит тестовому сервису обращаться на свой локальный порт, а нам слушать свой. И все будет работать.

Подробнее о том, что такое порт-форвардинг, я расскажу чуть позже. Если в двух словах, то это прокидывание трубы через ssh соединение от порта на удаленной машине, до порта на локальной. Хороший мануал можно найти на www.debianadmin.com

Сейчас же, мы займемся самым верхним уровнем - интерфейсом рулы. Она позволит прокинуть связь хост_удаленной_машины:порт -> ssh -> хост_машины_где_фейк_сервис:его_порт.

Делаем рулу порт-форвардинга

Вспоминаем про библиотеку Ganymed SSH2. Подключаем ее, используя мавен:

<!--https://code.google.com/p/ganymed-ssh-2/-->
<dependency>
   <groupId>ch.ethz.ganymed</groupId>
   <artifactId>ganymed-ssh2</artifactId>
   <version>${last-ganymed-ssh-ver}</version>
</dependency>

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

Остаётся 3 параметра, которые требуется указывать:

@Rule
public SshRemotePortForwardingRule forward = onRemoteHost(props().serviceURI())
          .whenRemoteUsesPort(BIND_PORT_ON_REMOTE)
          .forwardToLocal().withForwardToPort(LOCAL_MOCKED_PORT);

Здесь, .forwardToLocal() это:

public SshRemotePortForwardingRule forwardToLocal() {
    try {
        hostToForward = getLocalHost().getHostAddress();
    } catch (UnknownHostException e) {
        throw new RuntimeException("Can't get local host", e);
    }
    return this;
}

Рулу удобно делать как наследника ExternalResource, переопределив before() для авторизации и поднятия порт-форвардинга, а after() для закрытия проброски порта и соединения.

Само соединение должно выглядеть примерно так:

logger.info(format("Try to create port forwarding: `ssh %s -l %s -f -N -R %s:%s:%s:%s`",
                connection.getHostname(), SSH_LOGIN,
                hostOnRemote, portOnRemote, hostToForward, portToForward
        ));
connection.requestRemotePortForwarding(hostOnRemote, portOnRemote, hostToForward, portToForward);

Сказка быль, да в ней намёк

Добавив такую рулу, при включенном порт-форвардинге мы можем локально получать и анализировать все, что придет на удаленной машине на указанный порт. WireMock при этом позволяет достать целиком все запросы по нужному условию и применить к ним привычные матчеры.

Но есть и ряд ограничений в таком подходе:

  • Потребуется SSH доступ на машину.
  • Порт-форвардинг должен быть на этой машине включен.
  • Потребуется останавливать сервисы, если захочется повеситься на их порт, чтобы замокать его. А это значит нужны права пользователю на остановку сервисов без пароля.
  • В некоторых организациях нельзя пробрасывать порт без санкций администраторов.