Зачастую во время разработки и / или тестирования на реальных сценариях мы, разработчики, сталкиваемся с необходимостью запуска полноценного HTTPS- сервера, возможно, одновременно осуществляя некоторые насмешки. На платформе JVM раньше это было совсем не тривиально, если вы не знаете правильный инструмент для этой работы. В этом посте мы собираемся создать каркас полностью работающего HTTPS- сервера, используя потрясающую инфраструктуру Spray и язык Scala .
Для начала нам нужно сгенерировать сертификат x509 и закрытый ключ соответственно. К счастью, это очень легко сделать с помощью инструмента командной строки openssl .
|
1
2
3
4
5
6
7
8
|
openssl req -x509 -sha256 -newkey rsa:2048 -keyout certificate.key -out certificate.crt -days 1024 -nodes |
Поскольку мы работаем на платформе JVM, нашей основной целью является создание хранилища ключей Java ( JKS ), хранилища сертификатов безопасности. Однако, чтобы импортировать наш новый сгенерированный сертификат в JKS , мы должны экспортировать его в формате PKCS # 12, а затем создать из него хранилище ключей. Опять openssl о реске.
|
1
2
3
4
5
6
7
|
openssl pkcs12 -export -in certificate.crt -inkey certificate.key -out server.p12 -name sample-https-server -password pass:change-me-please |
Обратите внимание, что архив server.p12 защищен паролем. Теперь последний шаг включает инструмент командной строки из дистрибутива JDK, называемый keytool .
|
1
2
3
4
5
6
7
|
keytool -importkeystore -srcstorepass change-me-please -destkeystore sample-https-server.jks -deststorepass change-me-please -srckeystore server.p12 -srcstoretype PKCS12 -alias sample-https-server |
В результате получается защищенное паролем хранилище ключей sample-https-server.jks, которое мы можем использовать в нашем приложении сервера HTTPS для настройки контекста SSL. Spray имеет очень хорошую документацию и множество доступных примеров, одним из которых является пример SslConfiguration, который мы можем использовать для настройки KeyManager , TrustManager и SSLContext .
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
trait SslConfiguration { // If there is no SSLContext in scope implicitly, the default SSLContext is // going to be used. But we want non-default settings so we are making // custom SSLContext available here. implicit def sslContext: SSLContext = { val keyStoreResource = "/sample-https-server.jks" val password = "change-me-please" val keyStore = KeyStore.getInstance("jks") keyStore.load(getClass.getResourceAsStream(keyStoreResource), password.toCharArray) val keyManagerFactory = KeyManagerFactory.getInstance("SunX509") keyManagerFactory.init(keyStore, password.toCharArray) val trustManagerFactory = TrustManagerFactory.getInstance("SunX509") trustManagerFactory.init(keyStore) val context = SSLContext.getInstance("TLS") context.init(keyManagerFactory.getKeyManagers, trustManagerFactory.getTrustManagers, new SecureRandom) context } // If there is no ServerSSLEngineProvider in scope implicitly, // the default one is going to be used. But we would like to configure // cipher suites and protocols so we are making a custom ServerSSLEngineProvider // available here. implicit def sslEngineProvider: ServerSSLEngineProvider = { ServerSSLEngineProvider { engine => engine.setEnabledCipherSuites(Array("TLS_RSA_WITH_AES_128_CBC_SHA")) engine.setEnabledProtocols(Array( "TLSv1", "TLSv1.1", "TLSv1.2" )) engine } }} |
Здесь есть несколько моментов, на которые стоит обратить внимание. Прежде всего, использование нашего собственного хранилища ключей, созданного ранее (который для удобства мы загружаем как ресурс classpath):
|
1
2
|
val keyStoreResource = "/sample-https-server.jks"val password = "change-me-please" |
Кроме того, мы настраиваем только TLS ( TLS v1.0 , TLS v1.1 и TLS v1.2 ), без поддержки SSLv3 . Кроме того, мы включаем только один шифр: TLS_RSA_WITH_AES_128_CBC_SHA . Это было сделано в основном для иллюстрации, так как в большинстве случаев все поддерживаемые шифры могут быть включены.
|
1
2
|
engine.setEnabledCipherSuites(Array("TLS_RSA_WITH_AES_128_CBC_SHA"))engine.setEnabledProtocols(Array( "TLSv1", "TLSv1.1", "TLSv1.2" )) |
На этом мы готовы создать настоящий HTTPS- сервер, который благодаря Spray Framework занимает всего пару строк:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
class HttpsServer(val route: Route = RestService.defaultRoute) extends SslConfiguration { implicit val system = ActorSystem() implicit val timeout: Timeout = 3 seconds val settings = ServerSettings(system).copy(sslEncryption = true) val handler = system.actorOf(Props(new RestService(route)), name = "handler") def start(port: Int) = Await.ready( IO(Http) ? Http.Bind(handler, interface = "localhost", port = port, settings = Some(settings)), timeout.duration) def stop() = { IO(Http) ? Http.CloseAll system.stop(handler) }} |
Любой HTTPS- сервер, который вообще ничего не делает, не очень полезен. Именно здесь вступает в действие свойство route : используя расширения маршрутизации Spray , мы передаем сопоставления (или маршруты) для обработки запросов непосредственно субъекту службы HTTP ( RestService ).
|
1
2
3
4
5
|
class RestService(val route: Route) extends HttpServiceActor with ActorLogging { def receive = runRoute { route }} |
Маршрут по умолчанию просто так:
|
1
2
3
4
5
6
7
8
9
|
object RestService { val defaultRoute = path("") { get { complete { "OK!\n" } } }} |
По сути, это все, что нам нужно, и наш HTTPS- сервер готов к тест-драйву! Самый простой способ запустить его — использовать приложение Scala .
|
1
2
3
4
|
object HttpsServer extends App { val server = new HttpsServer server.start(10999)} |
Несмотря на то, что она написана на Scala , мы можем легко встроить ее в любое приложение Java (используя немного нестандартные соглашения для разработчиков имен Java), например:
|
1
2
3
4
5
6
|
public class HttpsServerRunner { public static void main(String[] args) { final HttpsServer server = new HttpsServer(RestService$.MODULE$.defaultRoute()); server.start(10999); }} |
После запуска и запуска (самый простой способ сделать это — запустить sbt ), к открытому маршруту по умолчанию нашего простого HTTPS- сервера можно получить доступ либо из браузера, либо с помощью клиента командной строки curl (аргумент командной строки -k отключает проверку сертификата SSL) :
|
1
2
3
4
5
6
7
8
9
|
$ curl -ki https://localhost:10999HTTP/1.1 200 OKServer: spray-can/1.3.3Date: Sun, 04 Oct 2015 01:25:47 GMTContent-Type: text/plain; charset=UTF-8Content-Length: 4OK! |
В качестве альтернативы, сертификат можно передать вместе с командой curl, чтобы выполнить полную проверку SSL-сертификата, например:
|
1
2
3
4
5
6
7
8
9
|
$ curl -i --cacert src/main/resources/certificate.crt https://localhost:10999HTTP/1.1 200 OKServer: spray-can/1.3.3Date: Sun, 04 Oct 2015 01:28:05 GMTContent-Type: text/plain; charset=UTF-8Content-Length: 4OK! |
Все выглядит отлично, но можем ли мы использовать HTTPS- сервер как часть набора интеграционных тестов для проверки / stub / mock, например, взаимодействия со сторонними сервисами? Ответ, да, абсолютно, благодаря правилам JUnit . Давайте посмотрим на простейшую реализацию HttpsServerRule :
|
01
02
03
04
05
06
07
08
09
10
11
|
class HttpsServerRule(@BeanProperty val port: Int, val route: Route) extends ExternalResource { val server = new HttpsServer(route) override def before() = server.start(port) override def after() = server.stop()}object HttpsServerRule { def apply(port: Int) = new HttpsServerRule(port, RestService.defaultRoute); def apply(port: Int, route: Route) = new HttpsServerRule(port, route);} |
Тестовый пример JUnit для нашей реализации по умолчанию использует великолепную библиотеку RestAssured, которая предоставляет Java DSL для простого тестирования служб REST.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
public class DefaultRestServiceTest { @Rule public HttpsServerRule server = HttpsServerRule$.MODULE$.apply(65200); @Test public void testServerIsUpAndRunning() { given() .auth().certificate("/sample-https-server.jks", "change-me-please") .when() .get("/") .then() .body(containsString("OK!")); }} |
Конечно, с реализацией по умолчанию вы мало что можете сделать, поэтому предоставление пользовательской опции является обязательным параметром. К счастью, мы уже разобрались с этим, приняв маршруты.
|
01
02
03
04
05
06
07
08
09
10
|
object CustomRestService { val route = path("api" / "user" / IntNumber) { id => get { complete { "a@b.com" } } }} |
И вот тестовый пример для этого:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
public class CustomRestServiceTest { @Rule public HttpsServerRule server = HttpsServerRule$.MODULE$.apply(65201, CustomRestService$.MODULE$.route()); @Test public void testServerIsUpAndRunning() { given() .auth().certificate("/sample-https-server.jks", "change-me-please") .when() .get("/api/user/1") .then() .body(containsString("a@b.com")); }} |
Как выясняется, создать полноценный HTTPS- сервер совсем не сложно и может быть очень весело, если вы знаете правильный инструмент для этого. Spray Framework — один из тех волшебных инструментов. Как многие из вас знают, Spray будет заменен на Akka HTTP, который недавно выпустил версию 1.0, но в данный момент ему не хватает многих функций (включая поддержку HTTPS ), что делает Spray жизнеспособным выбором.
- Полный проект доступен на Github .
| Ссылка: | Создание примера HTTPS-сервера для удовольствия и получения прибыли от нашего партнера по JCG Андрея Редько в блоге Андрея Редько {devmind} . |