Skip to content

Instantly share code, notes, and snippets.

@nuald
Last active July 19, 2019 06:44
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save nuald/cd058bd2ef1c755664f530fd64198574 to your computer and use it in GitHub Desktop.
Simple HTTP Server on Scala/Akka (directory listing)
#!/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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment