Akka HTTP is a HTTP-based toolkit built on top of Akka Stream. Rather than a framework for rapid web server development, it’s principally designed as a suite of tools for building custom integration layers to wire potentially complex business logic with a REST/HTTP interface. Perhaps for that reason, one might be surprised that there isn’t any example code for something as common as running a HTTPS-by-default web server.
Almost every major website operates using the HTTPS protocol by default for security purpose these days. Under the protocol, the required SSL certificate and the bidirectional encryption of the communications between the web server and client does ensure the authenticity of the website as well as avoid man-in-the-middle attack. It might be an over-kill for, say, an information-only website, but the ‘lock’ icon indicating a valid SSL certificate on the web browser address bar certainly makes site visitors feel more secure.
In this blog post, I’ll assemble a snippet using Akka HTTP to illustrate how to set up a skeletal web server which redirects all plain-HTTP requests to the HTTPS listener. For testing purpose in a development environment, I’ll also include steps of creating a self-signed SSL certificate. Note that such self-signed certificate should only be used for internal testing purpose.
HTTP and HTTPS cannot serve on the same port
Intuitively, one would consider binding both HTTP and HTTPS services to the same port on which all requests are processed by a HTTPS handler. Unfortunately, HTTPS uses SSL/TLS protocol which for security reason can’t be simply downgraded to HTTP upon detecting unencrypted requests. A straight-forward solution would be to bind the HTTP and HTTPS services to separate ports and redirect all requests coming into the HTTP port to the HTTPS port.
First let’s create ‘build.sbt’ with necessary library dependencies under the project root subdirectory:
name := "akka-http-secureserver" version := "1.0" scalaVersion := "2.11.12" libraryDependencies ++= Seq( "com.typesafe.akka" %% "akka-actor" % "2.4.20", "com.typesafe.akka" %% "akka-stream" % "2.4.20", "com.typesafe.akka" %% "akka-http" % "10.0.11" )
Next, create the main application in, say, ${project-root}/src/main/SecureServer.scala:
import akka.actor.ActorSystem import akka.stream.ActorMaterializer import akka.stream.scaladsl._ import akka.http.scaladsl.model.{HttpEntity, ContentTypes, StatusCodes} import akka.http.scaladsl.{Http, ConnectionContext, HttpsConnectionContext} import akka.http.scaladsl.server.Directives._ import com.typesafe.sslconfig.akka.AkkaSSLConfig import java.security.{SecureRandom, KeyStore} import javax.net.ssl.{SSLContext, KeyManagerFactory, TrustManagerFactory} import java.io.InputStream import scala.io.StdIn object SecureServer { def main(args: Array[String]) { implicit val system = ActorSystem("my-system") implicit val materializer = ActorMaterializer() implicit val ec = system.dispatcher val password: Array[Char] = "mypassword".toCharArray // Unsafe to provide password here! val ks: KeyStore = KeyStore.getInstance("PKCS12") val keystore: InputStream = getClass.getClassLoader.getResourceAsStream("server.p12") require(keystore != null, "Keystore required!") ks.load(keystore, password) val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance("SunX509") keyManagerFactory.init(ks, password) val tmf: TrustManagerFactory = TrustManagerFactory.getInstance("SunX509") tmf.init(ks) val sslContext: SSLContext = SSLContext.getInstance("TLS") sslContext.init(keyManagerFactory.getKeyManagers, tmf.getTrustManagers, new SecureRandom) val httpsContext: HttpsConnectionContext = ConnectionContext.https(sslContext) val hostName = "dev.genuine.com" val portHttp = 8080 val portHttps = 8443 val route = scheme("http") { extract(_.request.uri) { uri => redirect( uri.withScheme("https").withAuthority(hostName, portHttps), StatusCodes.MovedPermanently ) } } ~ pathSingleSlash { get { complete( HttpEntity( ContentTypes.`text/html(UTF-8)`, "Welcome to Akka-HTTP!" ) ) } } ~ path("hello") { get { complete( HttpEntity( ContentTypes.`text/html(UTF-8)`, "Hello from Akka-HTTP!" ) ) } } Http().bindAndHandle(route, hostName, portHttp) Http().bindAndHandle(route, hostName, portHttps, connectionContext = httpsContext) println(s"Server online at https://${hostName}:${portHttps}/\nPress RETURN to stop...") StdIn.readLine() system.terminate() } }
The top half of the main code are initialization routines for the Akka actor system, stream materializer (which are what Akka HTTP is built on top of) and creating HTTPS connection context. The rest of the code is a standard Akka HTTP snippet with URL routing and server port binding. A good portion of the code is borrowed from this Akka server-side HTTPS support link.
Within the ‘scheme(“http”)’ routing code block is the core logic for HTTPS redirection:
// HTTPS redirection uri => redirect( uri.withScheme("https").withAuthority(hostName, portHttps), StatusCodes.MovedPermanently )
Note that there is no need for applying ‘withAuthority()’ if you’re using standard HTTPS port (i.e. 443).
Next step would be to put in place the PKCS #12 formatted file, ‘server.p12’, which consists of the PKCS private key and X.509 SSL certificate. It should be placed under ${project-root}/src/main/resources/. At the bottom of this blog post are steps for creating the server key/certificate using open-source library, OpenSSL.
Once the private key/certificate is in place, to run the server application from a Linux command prompt, simply use ‘sbt’ as below:
# Run SecureServer cd ${project-root} sbt "runMain SecureServer"
To test it out from a web browser, visit http://dev.genuine.com:8080/hello and you should see the URL get redirected to https://dev.genuine.com:8443/hello. The web browser will warn about security of the site and that’s just because the SSL certificate is a self-signed one.
Generating server key and self-signed SSL certificate in PKCS #12 format
# # Steps to generate private key and self-signed X.509 certificate in PKCS #12 format # ## Generate private key openssl genrsa -des3 -out server.key 2048 # -- Generating RSA private key, 2048 bit long modulus ..................................+++ .......................................................+++ e is 65537 (0x10001) Enter pass phrase for server.key: genuine Verifying - Enter pass phrase for server.key: # -- ## Generate CSR openssl req -new -key server.key -out server.csr # -- Enter pass phrase for server.key: Country Name (2 letter code) [AU]:US State or Province Name (full name) [Some-State]:California Locality Name (eg, city) []:Sunnyvale Organization Name (eg, company) [Internet Widgits Pty Ltd]:Genuine Organizational Unit Name (eg, section) []: Common Name (e.g. server FQDN or YOUR name) []:dev.genuine.com Email Address []:postmaster@genuine.com A challenge password []: # -- ## Remove pass phrase cp server.key server.key.orig openssl rsa -in server.key.orig -out server.key # -- Enter pass phrase for server.key.orig: writing RSA key # -- ## Generate certificate openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt # -- Signature ok subject=/C=US/ST=California/L=Sunnyvale/O=Genuine/CN=dev.genuine.com/emailAddress=postmaster@genuine.com Getting Private key # -- ## Convert to PKCS #12 or PFX format openssl pkcs12 -export -out server.p12 -inkey server.key -in server.crt # -- Enter Export Password: Verifying - Enter Export Password: # -- ## Move the PKCS #12 file to the server application resources subdirectory mv server.p12 ${project-root}/src/main/resources/