Simple HTTP/HTTPS server on Scala/Akka

Let me present a simple Scala script to serve local files over HTTP or HTTPS. I've used Scala/Akka to get the good performance and to play around with the programming language I really like.

Toolchain


We need Scala, SBT (Scala interactive build tool), Conscript (a distribution mechanism for Scala apps) and Scalas (the script runner for Scala). Conscript is needed only to install Scalas here, so you may want to install the runner manually (see the Scalas link above) if you don't want to install Conscript.

For Mac OS X we're going to use Homebrew to install Scala and SBT:

 $ brew install scala sbt

Next step is Conscript (please follow the installation instructions on the official site):

 $ wget https://dl.bintray.com/foundweekends/maven-releases/org/foundweekends/conscript/conscript_2.11/0.5.1/conscript_2.11-0.5.1-proguard.jar
 $ java -jar conscript_2.11-0.5.1-proguard.jar

Next step is installing Scalas (please follow the installation instructions on the official site):

 $ cs sbt/sbt --branch 0.13.13

It may show some errors, please dismiss those.

HTTPS self-signed certificate


Java has the handy tool to generate the certificate, just call it with the required parameters (in the example it creates a RSA certificate valid for 365 days):

 $ keytool -genkey -keyalg RSA -validity 365 -keystore /path/to/file -storetype PKCS12

where /path/to/file is the keystore file you can use with the HTTPS server.

Please note that the generated certificate is not trusted by browsers. If you have a legit public website, you can generate a certificate for free using Let's Encrypt service.

The script


Please note that SBT will download the dependencies, therefore the first start may take a while. After that the using of the script is straightforward (please use "--help" command if you're lost).

Sample output:

 $ /serve.scala --keystore=server.p12 --password=89WSTfO --port=9000

[INFO] [12/07/2016 22:02:59.682] [run-main-0] [http(akka://sys)] scala version 2.12.0
[INFO] [12/07/2016 22:02:59.891] [run-main-0] [http(akka://sys)] start: https://192.168.1.26:9000/
[INFO] [12/07/2016 22:03:33.832] [sys-akka.actor.default-dispatcher-9] [http(akka://sys)] GET /
[INFO] [12/07/2016 22:03:34.121] [sys-akka.actor.default-dispatcher-2] [http(akka://sys)] GET /favicon.ico

The code:

#!/usr/bin/env scalas
/***
scalaVersion := "2.13.0"
scalacOptions ++= Seq("-deprecation", "-feature")
libraryDependencies += "com.typesafe.akka" %% "akka-actor" % "2.5.23"
libraryDependencies += "com.typesafe.akka" %% "akka-stream" % "2.5.23"
libraryDependencies += "com.typesafe.akka" %% "akka-http" % "10.1.9"
libraryDependencies += "com.github.scopt" %% "scopt" % "3.7.1"
*/
import akka.actor.ActorSystem
import akka.event.Logging
import akka.http.scaladsl.{HttpsConnectionContext, Http}
import akka.http.scaladsl.model.HttpRequest
import akka.http.scaladsl.server.Directives.{rawPathPrefix, logRequest, getFromBrowseableDirectory}
import akka.http.scaladsl.server.directives.LoggingMagnet
import akka.http.scaladsl.unmarshalling.Unmarshal
import akka.stream.ActorMaterializer
import java.net.InetAddress
import java.io.{File, FileInputStream}
import java.security.{SecureRandom, KeyStore}
import javax.net.ssl.{SSLContext, TrustManagerFactory, KeyManagerFactory}
import scala.concurrent.{Await, Future}
import scala.concurrent.duration._
val DEFAULT_PORT = 8000
case class Config(
port: Int = DEFAULT_PORT,
keystore: Option[File] = None,
storepass: String = "",
open: Boolean = false,
help: Boolean = false
)
object WebServer {
def bind(
port: Int,
keystore: Option[File],
password: Array[Char]
): Future[Http.ServerBinding] = {
implicit val system = ActorSystem("sys")
implicit val materializer = ActorMaterializer()
val log = Logging(system, "http")
def logRequestInfo(req: HttpRequest): Unit = {
log.info(s"${req.method.name} ${req.uri.path}")
val entityFuture = Unmarshal(req.entity).to[String]
val entityContent = Await.result(entityFuture, 10.seconds)
if (!entityContent.isEmpty) {
log.info(entityContent)
}
}
val route =
rawPathPrefix("") {
logRequest(LoggingMagnet(_ => logRequestInfo)) {
getFromBrowseableDirectory(".")
}
}
val localhost = InetAddress.getLocalHost
val host = localhost.getHostAddress
if (keystore.isDefined) {
val ks = KeyStore.getInstance("PKCS12")
ks.load(new FileInputStream(keystore.get), password)
val keyManagerFactory = KeyManagerFactory.getInstance("SunX509")
keyManagerFactory.init(ks, password)
val tmf = TrustManagerFactory.getInstance("SunX509")
tmf.init(ks)
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(
keyManagerFactory.getKeyManagers, tmf.getTrustManagers, new SecureRandom)
val https = new HttpsConnectionContext(sslContext)
log.info(s"start: https://$host:$port/")
Http().bindAndHandle(route, host, port, connectionContext = https)
} else {
log.info(s"start: http://$host:$port/")
Http().bindAndHandle(route, host, port)
}
}
def run(port: Int, keystore: Option[File], password: Array[Char]): Unit = {
implicit val system = ActorSystem("sys")
implicit val materializer = ActorMaterializer()
// needed for the future flatMap/onComplete in the end
implicit val executionContext = system.dispatcher
val log = Logging(system, "http")
log.info(s"scala ${util.Properties.versionString}")
val bindingFuture = bind(port, keystore, password)
sys.addShutdownHook({
log.info("going to shutdown...")
bindingFuture
.flatMap(_.unbind()) // trigger unbinding from the port
.onComplete(_ => system.terminate()) // and shutdown when done
log.info("done.")
})
}
def main(args: Array[String]): Unit = {
val defaultConfig = Config()
val parser = new scopt.OptionParser[Config]("serve.scala") {
opt[File]("keystore").valueName("<filename>").
action( (x, c) => c.copy(keystore = Some(x)) ).
text("The keystore name.")
opt[String]("password").valueName("<password>").
action( (x, c) => c.copy(storepass = x) ).
text("The keystore password.")
opt[Int]('p', "port").valueName("<number>").
action( (x, c) => c.copy(port = x) ).
text(s"The port to serve from. Defaults to ${defaultConfig.port}")
opt[Unit]('h', "help").
action{ (_, c) =>
showUsage
c.copy(help = true)
}.text("Prints this usage text.")
}
parser.parse(args, defaultConfig) match {
case Some(config) =>
if (!config.help) {
run(config.port, config.keystore, config.storepass.toCharArray)
}
case None =>
run(defaultConfig.port, defaultConfig.keystore, defaultConfig.storepass.toCharArray)
}
}
}
WebServer.main(args)
view raw serve.scala hosted with ❤ by GitHub


Comments

Popular posts from this blog

Web application framework comparison by memory consumption

Trac Ticket Workflow

Python vs JS vs PHP for embedded systems