Often during development or/and testing against real-life scenarios we, developers, are facing a need to run a full-fledged HTTPS server, possibly doing some mocking at the same time. On JVM platform, it used to be not trivial at all unless you know the right tool for the job. In this post we are going to create a skeleton of fully operational HTTPS server using terrific Spray framework and Scala language.
To begin with, we need to generate x509 certificate and private key respectively. Luckily, it is very easy to do using openssl command line tool.
openssl req -x509 -sha256 -newkey rsa:2048 -keyout certificate.key -out certificate.crt -days 1024 -nodes
As we are on JVM platform, our essential goal is to have a Java keystore (JKS), a repository of security certificates. However, to import our newly generated certificate into JKS, we have to export it in PKCS #12 format and then create keystore out of it. Again, openssl on the resque.
openssl pkcs12 -export -in certificate.crt -inkey certificate.key -out server.p12 -name sample-https-server -password pass:change-me-pleasePlease note that the archive server.p12 is protected by a password. Now, the final step involves the command line tool from JDK distribution called keytool.
keytool -importkeystore -srcstorepass change-me-please -destkeystore sample-https-server.jks -deststorepass change-me-please -srckeystore server.p12 -srcstoretype PKCS12 -alias sample-https-serverThe result is password-protected sample-https-server.jks keystore which we can use in our HTTPS server application to configure SSL context. Spray has very good documentation and plenty of examples available, one of those is sample SslConfiguration which we can use to configure KeyManager, TrustManager and SSLContext.
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 } } }There are a few points to highlight here. First of all, usage of our own keystore created previously (which for convenience we are loading as classpath resource):
val keyStoreResource = "/sample-https-server.jks" val password = "change-me-please"Also, we are configuring TLS only (TLS v1.0, TLS v1.1 and TLS v1.2), no SSLv3 support. In addition to that, we are enabling only one cipher: TLS_RSA_WITH_AES_128_CBC_SHA. It has been done mostly for illustration, as in most cases all supported ciphers could be enabled.
engine.setEnabledCipherSuites(Array("TLS_RSA_WITH_AES_128_CBC_SHA")) engine.setEnabledProtocols(Array( "TLSv1", "TLSv1.1", "TLSv1.2" ))
With that, we are ready to create a real HTTPS server, which thanks to Spray framework is just a couple of lines long:
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) } }
Any HTTPS server which does nothing at all is not very useful. That is where route property comes into play: using Spray routing extensions, we are passing the mappings (or routes) to handle the requests straight to HTTP service actor (RestService).
class RestService(val route: Route) extends HttpServiceActor with ActorLogging { def receive = runRoute { route } }With default route being just that:
object RestService { val defaultRoute = path("") { get { complete { "OK!\n" } } } }Basically, that is all we need and our HTTPS server is ready to take a test drive! The simplest way to run it is by using Scala application.
object HttpsServer extends App { val server = new HttpsServer server.start(10999) }
Despite being written in Scala, we can easily embed it into any Java application (using a bit non-standard for Java developers naming conventions), for example:
public class HttpsServerRunner { public static void main(String[] args) { final HttpsServer server = new HttpsServer(RestService$.MODULE$.defaultRoute()); server.start(10999); } }
Once up and running (the easiest way to do that is sbt run), the exposed default route of our simple HTTPS server could be accessed either from the browser or using curl command line client (-k command line argument turns off SSL certificate verification):
$ 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!Alternatively, the certificate could be passed along with the curl command so a complete SSL certificate verification takes place, for example:
$ 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!
All is looking great but could we use HTTPS server as part of integration test suite to verify / stub / mock, for example, the interactions with third-party services? The answer is, yes, absolutely, thanks to JUnit rules. Let us take a look on the simplest implementation possible of HttpsServerRule:
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); }
The JUnit test case for our default implementation uses brilliant RestAssured library which provides a Java DSL for easy testing of REST services.
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!")); } }
For sure, not much you can do with default implementation so providing the custom one is a must-have option. Luckily, we sorted that out early on by accepting the routes.
object CustomRestService { val route = path("api" / "user" / IntNumber) { id => get { complete { "a@b.com" } } } }
And here is a test case for it:
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("a@b.com")); } }
As it turns out, creating a full-blown HTTPS server is not hard at all and could be really fun, once you know the right tool to do that. Spray framework is one of those magic tools. As many of you are aware, Spray is going to be replaced by Akka HTTP which had seen a 1.0 release recently but at the moment lacks a lot of features (including HTTPS support), keeping Spray as a viable choice.
The complete project is available on Github.