Статьи

Создание примера HTTPS-сервера для удовольствия и получения прибыли

Зачастую во время разработки и / или тестирования на реальных сценариях мы, разработчики, сталкиваемся с необходимостью запуска полноценного 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:10999
 
HTTP/1.1 200 OK
Server: spray-can/1.3.3
Date: Sun, 04 Oct 2015 01:25:47 GMT
Content-Type: text/plain; charset=UTF-8
Content-Length: 4
 
OK!

В качестве альтернативы, сертификат можно передать вместе с командой curl, чтобы выполнить полную проверку SSL-сертификата, например:

1
2
3
4
5
6
7
8
9
$  curl -i --cacert src/main/resources/certificate.crt  https://localhost:10999
 
HTTP/1.1 200 OK
Server: spray-can/1.3.3
Date: Sun, 04 Oct 2015 01:28:05 GMT
Content-Type: text/plain; charset=UTF-8
Content-Length: 4
 
OK!

Все выглядит отлично, но можем ли мы использовать 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()
            .baseUri("https://localhost:" + server.getPort())
            .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 {
          "[email protected]"
        }
      }
    }
}

И вот тестовый пример для этого:

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()
            .baseUri("https://localhost:" + server.getPort())
            .auth().certificate("/sample-https-server.jks", "change-me-please")
            .when()
            .get("/api/user/1")
            .then()
            .body(containsString("[email protected]"));
    }
}

Как выясняется, создать полноценный HTTPS- сервер совсем не сложно и может быть очень весело, если вы знаете правильный инструмент для этого. Spray Framework — один из тех волшебных инструментов. Как многие из вас знают, Spray будет заменен на Akka HTTP, который недавно выпустил версию 1.0, но в данный момент ему не хватает многих функций (включая поддержку HTTPS ), что делает Spray жизнеспособным выбором.

  • Полный проект доступен на Github .