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.
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.
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.
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:
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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) |
Comments
Post a Comment