Last active
July 19, 2019 06:44
Star
You must be signed in to star a gist
Simple HTTP Server on Scala/Akka (directory listing)
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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment