├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── README.md
├── build.sbt
├── launch-test-server
├── project
├── build.properties
└── plugins.sbt
├── src
├── main
│ ├── resources
│ │ └── reference.conf
│ └── scala
│ │ └── com
│ │ └── tresata
│ │ └── akka
│ │ └── http
│ │ └── spnego
│ │ ├── KerberosConfiguration.scala
│ │ ├── SpnegoAuthenticator.scala
│ │ ├── SpnegoDirectives.scala
│ │ └── Token.scala
└── test
│ └── scala
│ └── com
│ └── tresata
│ └── akka
│ └── http
│ └── spnego
│ └── TokenSpec.scala
└── test-server
└── src
└── main
├── resources
├── application.conf
├── log4j.properties
└── testKeystore
└── scala
└── com
└── tresata
└── akka
└── http
└── spnego
└── Main.scala
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | pull_request:
4 | push:
5 | jobs:
6 | test:
7 | strategy:
8 | fail-fast: false
9 | matrix:
10 | include:
11 | - java: 8
12 | - java: 11
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: checkout
16 | uses: actions/checkout@v1
17 | - name: setup
18 | uses: olafurpg/setup-scala@v10
19 | with:
20 | java-version: "adopt@1.${{ matrix.java }}"
21 | - name: test
22 | run: sbt -v -Dfile.encoding=UTF8 +test
23 | shell: bash
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | lib_managed/
3 | tmp/
4 | .bsp/
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # akka-http-spnego
4 | akka-http-spnego provides Kerberos based authentication for akka-http using SPNEGO. SPNEGO is a way to do GSSAPI authentication between clients and servers using the HTTP authentication header.
5 |
6 | The included test-server project is both an example of how to use this project and a way to quickly test it within your Kerberos environment. It provides a secure ping-pong service (you do a GET to endpoint /ping and it responds with pong for the authenticated user).
7 | To run it first make sure:
8 | * Kerberos is properly installed and configured on both server and client
9 | * DNS is properly configured for the server (it's hostname resolves correctly to its ip-address and the other way around).
10 |
11 | Next do the following:
12 | * Create a keytab for HTTP/yourserver@YOURDOMAIN on the server
13 | * Edit test-server/src/main/resources/application.conf to set tresata.akka.http.spnego.kerberos.principal and tresata.akka.http.spnego.kerberos.keytab
14 | * Launch the test server with: ./launch-test-server
15 | * On the client make sure you are logged into Kerberos (with kinit)
16 | * On the client create a test connection with: curl -k --negotiate -u : -b ~/cookiejar.txt -c ~/cookiejar.txt https://yourserver:12345/ping
17 |
18 | Have fun!
19 | Team @ Tresata
20 |
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 | lazy val sharedSettings = Seq(
2 | organization := "com.tresata",
3 | version := "0.6.0-SNAPSHOT",
4 | licenses += "Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0.txt"),
5 | scalaVersion := "2.12.15",
6 | crossScalaVersions := Seq("2.12.15", "2.13.5"),
7 | scalacOptions ++= Seq("-unchecked", "-deprecation", "-target:jvm-1.8", "-feature", "-language:_", "-Xlint:-package-object-classes,-adapted-args,_",
8 | "-Ywarn-dead-code", "-Ywarn-value-discard", "-Ywarn-unused"),
9 | Test / compile / scalacOptions := (Test / compile / scalacOptions).value.filter(_ != "-Ywarn-value-discard").filter(_ != "-Ywarn-unused"),
10 | Compile / console / scalacOptions := (Compile / console / scalacOptions).value.filter(_ != "-Ywarn-unused-import"),
11 | Test / console / scalacOptions := (Test / console / scalacOptions).value.filter(_ != "-Ywarn-unused-import"),
12 | publishMavenStyle := true,
13 | pomIncludeRepository := { x => false },
14 | Test / publishArtifact := false,
15 | publishTo := {
16 | if (isSnapshot.value)
17 | Some("snapshots" at "https://server02.tresata.com:8084/artifactory/oss-libs-snapshot-local")
18 | else
19 | Some("releases" at "https://oss.sonatype.org/service/local/staging/deploy/maven2")
20 | },
21 | credentials += Credentials(Path.userHome / ".m2" / "credentials_sonatype"),
22 | credentials += Credentials(Path.userHome / ".m2" / "credentials_artifactory"),
23 | pomExtra := (
24 | https://github.com/tresata/akka-http-spnego
25 |
26 | git@github.com:tresata/akka-http-spnego.git
27 | scm:git:git@github.com:tresata/akka-http-spnego.git
28 |
29 |
30 |
31 | koertkuipers
32 | Koert Kuipers
33 | https://github.com/koertkuipers
34 |
35 |
36 | )
37 | )
38 |
39 | lazy val `akka-http-spnego` = (project in file(".")).settings(
40 | sharedSettings
41 | ).settings(
42 | name := "akka-http-spnego",
43 | libraryDependencies ++= Seq(
44 | "org.slf4j" % "slf4j-api" % "1.7.25" % "compile",
45 | "com.typesafe.akka" %% "akka-http" % "10.2.6" % "compile",
46 | "com.typesafe.akka" %% "akka-stream" % "2.6.16" % "compile",
47 | "commons-codec" % "commons-codec" % "1.15" % "compile",
48 | "org.scalatest" %% "scalatest-funspec" % "3.2.10" % "test"
49 | )
50 | )
51 |
52 | lazy val `test-server` = (project in file("test-server")).settings(
53 | sharedSettings
54 | ).settings(
55 | name := "test-server",
56 | libraryDependencies ++= Seq(
57 | "org.slf4j" % "slf4j-log4j12" % "1.7.25" % "compile",
58 | "com.typesafe.akka" %% "akka-slf4j" % "2.6.16" % "compile"
59 | ),
60 | publish := { },
61 | publishLocal := { }
62 | ).dependsOn(`akka-http-spnego`)
63 |
--------------------------------------------------------------------------------
/launch-test-server:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | sbt -Djavax.net.ssl.keyStore=test-server/src/main/resources/testKeystore -Djavax.net.ssl.keyStorePassword=changeit -Djavax.net.debug=ssl test-server/run
4 |
5 | # curl -k --negotiate -u : -b ~/cookiejar.txt -c ~/cookiejar.txt https://someserver:12345/ping
6 |
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.5.5
2 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.1.1")
2 |
--------------------------------------------------------------------------------
/src/main/resources/reference.conf:
--------------------------------------------------------------------------------
1 | tresata.akka.http.spnego {
2 | signature.secret = "secret"
3 | token.validity = 3600 seconds
4 | cookie.domain = ""
5 | cookie.path = ""
6 | kerberos.principal = "HTTP/server@DOMAIN.COM"
7 | kerberos.keytab = "/tmp/http.keytab"
8 | kerberos.debug = false
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/scala/com/tresata/akka/http/spnego/KerberosConfiguration.scala:
--------------------------------------------------------------------------------
1 | package com.tresata.akka.http.spnego
2 |
3 | import javax.security.auth.login.{Configuration, AppConfigurationEntry}
4 |
5 | import scala.collection.JavaConverters._
6 |
7 | case class KerberosConfiguration(keytab: String, principal: String, debug: Boolean) extends Configuration {
8 | val ticketCache = Option(System.getenv("KRB5CCNAME"))
9 |
10 | override def getAppConfigurationEntry(name: String): Array[AppConfigurationEntry] = Array(
11 | new AppConfigurationEntry(
12 | "com.sun.security.auth.module.Krb5LoginModule",
13 | AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
14 | (Map(
15 | "keyTab" -> keytab,
16 | "principal" -> principal,
17 | "useKeyTab" -> "true",
18 | "storeKey" -> "true",
19 | "doNotPrompt" -> "true",
20 | "useTicketCache" -> "true",
21 | "renewTGT" -> "true",
22 | "isInitiator" -> "false",
23 | "refreshKrb5Config" -> "true",
24 | "debug" -> debug.toString
25 | ) ++ ticketCache.map{ x => Map("ticketCache" -> x) }.getOrElse(Map.empty)).asJava
26 | )
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/scala/com/tresata/akka/http/spnego/SpnegoAuthenticator.scala:
--------------------------------------------------------------------------------
1 | package com.tresata.akka.http.spnego
2 |
3 | import java.io.IOException
4 | import java.nio.charset.StandardCharsets.UTF_8
5 | import java.security.{PrivilegedAction, PrivilegedExceptionAction, PrivilegedActionException}
6 | import javax.security.auth.Subject
7 | import javax.security.auth.login.LoginContext
8 | import javax.security.auth.kerberos.KerberosPrincipal
9 | import scala.concurrent.Future
10 | import scala.collection.JavaConverters._
11 |
12 | import com.typesafe.config.{Config, ConfigFactory}
13 |
14 | import org.ietf.jgss.{GSSManager, GSSCredential}
15 |
16 | import akka.event.LoggingAdapter
17 |
18 | import akka.http.scaladsl.model.HttpHeader
19 | import akka.http.scaladsl.model.headers.{Cookie, HttpCookie, HttpChallenge, RawHeader}
20 | import akka.http.scaladsl.server.{Directive0, Directive1, RequestContext, Rejection, AuthenticationFailedRejection, MalformedHeaderRejection}
21 | import akka.http.scaladsl.server.AuthenticationFailedRejection.{CredentialsMissing, CredentialsRejected}
22 | import akka.http.scaladsl.server.directives.CookieDirectives.setCookie
23 | import akka.http.scaladsl.server.directives.BasicDirectives.{extractExecutionContext, extractLog, extract, provide}
24 | import akka.http.scaladsl.server.directives.RouteDirectives.reject
25 | import akka.http.scaladsl.server.directives.FutureDirectives.onSuccess
26 |
27 | import org.apache.commons.codec.binary.Base64
28 |
29 | object SpnegoAuthenticator {
30 | private val cookieName = "akka.http.spnego"
31 | private val Authorization = "authorization"
32 | private val negotiate = "Negotiate"
33 | private val wwwAuthenticate = "WWW-Authenticate"
34 |
35 | def apply(config: Config = ConfigFactory.load())(implicit log: LoggingAdapter): SpnegoAuthenticator = {
36 | val principal = config.getString("tresata.akka.http.spnego.kerberos.principal")
37 | val keytab = config.getString("tresata.akka.http.spnego.kerberos.keytab")
38 | val debug = config.getBoolean("tresata.akka.http.spnego.kerberos.debug")
39 | val domain = Some(config.getString("tresata.akka.http.spnego.cookie.domain")).flatMap{ x => if (x == "") None else Some(x) }
40 | val path = Some(config.getString("tresata.akka.http.spnego.cookie.path")).flatMap{ x => if (x == "") None else Some(x) }
41 | val tokenValidity = config.getDuration("tresata.akka.http.spnego.token.validity")
42 | val signatureSecret = config.getString("tresata.akka.http.spnego.signature.secret")
43 |
44 | log.info("principal {}", principal)
45 | log.info("keytab {}", keytab)
46 | log.info("debug {}", debug)
47 | log.info("domain {}", domain)
48 | log.info("path {}", path)
49 | log.info("token validity {}", tokenValidity)
50 |
51 | val tokens = new Tokens(tokenValidity.toMillis, signatureSecret.getBytes(UTF_8))
52 | new SpnegoAuthenticator(principal, keytab, debug, domain, path, tokens)
53 | }
54 |
55 | def spnegoAuthenticate(config: Config = ConfigFactory.load()): Directive1[Token] = {
56 | extractExecutionContext.flatMap{ implicit ec =>
57 | extractLog.flatMap{ implicit log =>
58 | log.debug("creating spnego authenticator")
59 | val spnego = SpnegoAuthenticator(config)
60 | extract(ctx => Future(spnego.apply(ctx))).flatMap(onSuccess(_)).flatMap{
61 | case Left(rejection) => reject(rejection)
62 | case Right(token) => provide(token) & spnego.setSpnegoCookie(token)
63 | }
64 | }
65 | }
66 | }
67 | }
68 |
69 | class SpnegoAuthenticator(principal: String, keytab: String, debug: Boolean, domain: Option[String], path: Option[String], tokens: Tokens)(implicit log: LoggingAdapter) {
70 | import SpnegoAuthenticator._
71 |
72 | private val subject = new Subject(false, Set(new KerberosPrincipal(principal)).asJava, Set.empty[AnyRef].asJava, Set.empty[AnyRef].asJava)
73 | private val kerberosConfiguration = new KerberosConfiguration(keytab, principal, debug)
74 |
75 | private val loginContext = new LoginContext("", subject, null, kerberosConfiguration)
76 | loginContext.login()
77 |
78 | private val gssManager = Subject.doAs(loginContext.getSubject, new PrivilegedAction[GSSManager] {
79 | override def run: GSSManager = GSSManager.getInstance
80 | })
81 |
82 | private def cookieToken(ctx: RequestContext): Option[Either[Rejection, Token]] = try {
83 | ctx.request.headers.collectFirst{
84 | case Cookie(cookies) => cookies.find(_.name == cookieName).map{ cookie => log.debug("cookie found"); cookie }
85 | }.flatten.flatMap{ cookie =>
86 | Some(tokens.parse(cookie.value)).filter(!_.expired).map{ token => log.debug("spnego token inside cookie not expired"); token }
87 | }.map(Right(_))
88 | } catch {
89 | case e: TokenParseException => Some(Left(MalformedHeaderRejection(s"Cookie: ${cookieName}", e.getMessage, Some(e)))) // malformed token in cookie
90 | }
91 |
92 | private def clientToken(ctx: RequestContext): Option[Array[Byte]] = ctx.request.headers.collectFirst{
93 | case HttpHeader(Authorization, value) => value
94 | }.filter(_.startsWith(negotiate)).map{ authHeader =>
95 | log.debug("authorization header found")
96 | new Base64(0).decode(authHeader.substring(negotiate.length).trim)
97 | }
98 |
99 | private def challengeHeader(maybeServerToken: Option[Array[Byte]] = None): HttpHeader = RawHeader(
100 | wwwAuthenticate,
101 | negotiate + maybeServerToken.map(" " + new Base64(0).encodeToString(_)).getOrElse("")
102 | )
103 |
104 | private def kerberosCore(clientToken: Array[Byte]): Either[Rejection, Token] = {
105 | try {
106 | val (maybeServerToken, maybeToken) = Subject.doAs(loginContext.getSubject, new PrivilegedExceptionAction[(Option[Array[Byte]], Option[Token])] {
107 | override def run: (Option[Array[Byte]], Option[Token]) = {
108 | val gssContext = gssManager.createContext(null: GSSCredential)
109 | try {
110 | (
111 | Option(gssContext.acceptSecContext(clientToken, 0, clientToken.length)),
112 | if (gssContext.isEstablished) Some(tokens.create(gssContext.getSrcName.toString)) else None
113 | )
114 | } catch {
115 | case e: Throwable =>
116 | log.error(e, "error in establishing security context")
117 | throw e
118 | } finally {
119 | gssContext.dispose()
120 | }
121 | }
122 | })
123 | if (log.isDebugEnabled)
124 | log.debug("maybeServerToken {} maybeToken {}", maybeServerToken.map(new Base64(0).encodeToString(_)), maybeToken)
125 | maybeToken.map{ token =>
126 | log.debug("received new token")
127 | Right(token)
128 | }.getOrElse{
129 | log.debug("no token received but if there is a serverToken then negotiations are ongoing")
130 | Left(AuthenticationFailedRejection(CredentialsMissing, HttpChallenge(challengeHeader(maybeServerToken).value, None)))
131 | }
132 | } catch {
133 | case e: PrivilegedActionException => e.getException match {
134 | case e: IOException => throw e // server error
135 | case e: Throwable =>
136 | log.error(e, "negotiation failed")
137 | Left(AuthenticationFailedRejection(CredentialsRejected, HttpChallenge(challengeHeader().value, None))) // rejected
138 | }
139 | }
140 | }
141 |
142 | private def kerberosNegotiate(ctx: RequestContext): Option[Either[Rejection, Token]] = clientToken(ctx).map(kerberosCore)
143 |
144 | private def initiateNegotiations: Either[Rejection, Token] = {
145 | log.debug("no negotiation header found, initiating negotiations")
146 | Left(AuthenticationFailedRejection(CredentialsMissing, HttpChallenge(challengeHeader().value, None)))
147 | }
148 |
149 | def apply(ctx: RequestContext): Either[Rejection, Token] = cookieToken(ctx).orElse(kerberosNegotiate(ctx)).getOrElse(initiateNegotiations)
150 |
151 | def setSpnegoCookie(token: Token): Directive0 = setCookie(HttpCookie(cookieName, tokens.serialize(token), domain = domain, path = path))
152 | }
153 |
154 |
--------------------------------------------------------------------------------
/src/main/scala/com/tresata/akka/http/spnego/SpnegoDirectives.scala:
--------------------------------------------------------------------------------
1 | package com.tresata.akka.http.spnego
2 |
3 | import com.typesafe.config.{Config, ConfigFactory}
4 |
5 | import akka.http.scaladsl.server.Directive1
6 |
7 | trait SpnegoDirectives {
8 | def spnegoAuthenticate(config: Config = ConfigFactory.load()): Directive1[Token] = SpnegoAuthenticator.spnegoAuthenticate(config)
9 | }
10 |
11 | object SpnegoDirectives extends SpnegoDirectives
12 |
--------------------------------------------------------------------------------
/src/main/scala/com/tresata/akka/http/spnego/Token.scala:
--------------------------------------------------------------------------------
1 | package com.tresata.akka.http.spnego
2 |
3 | import java.nio.ByteBuffer
4 | import java.nio.charset.StandardCharsets.UTF_8
5 | import java.security.MessageDigest
6 | import scala.util.{Try, Success}
7 |
8 | import org.apache.commons.codec.binary.Base64
9 |
10 | case class TokenParseException(message: String) extends Exception(message)
11 |
12 | class Tokens(tokenValidity: Long, signatureSecret: Array[Byte]) {
13 | private def newExpiration: Long = System.currentTimeMillis + tokenValidity
14 |
15 | private[spnego] def sign(token: Token): String = {
16 | val md = MessageDigest.getInstance("SHA")
17 | md.update(token.principal.getBytes(UTF_8))
18 | val bb = ByteBuffer.allocate(8)
19 | bb.putLong(token.expiration)
20 | md.update(bb.array)
21 | md.update(signatureSecret)
22 | new Base64(0).encodeToString(md.digest)
23 | }
24 |
25 | def create(principal: String): Token = Token(principal, newExpiration)
26 |
27 | def parse(tokenString: String): Token = tokenString.split("&") match {
28 | case Array(principal, expirationString, signature) => Try(expirationString.toLong) match {
29 | case Success(expiration) => {
30 | val token = Token(principal, expiration)
31 | if (sign(token) != signature) throw TokenParseException("incorrect signature")
32 | token
33 | }
34 | case _ => throw TokenParseException("expiration not a long")
35 | }
36 | case _ => throw TokenParseException("incorrect number of fields")
37 | }
38 |
39 | def serialize(token: Token): String = List(token.principal, token.expiration, sign(token)).mkString("&")
40 | }
41 |
42 | case class Token private[spnego] (principal: String, expiration: Long) {
43 | def expired: Boolean = System.currentTimeMillis > expiration
44 | }
45 |
--------------------------------------------------------------------------------
/src/test/scala/com/tresata/akka/http/spnego/TokenSpec.scala:
--------------------------------------------------------------------------------
1 | package com.tresata.akka.http.spnego
2 |
3 | import java.nio.charset.StandardCharsets.UTF_8
4 | import org.scalatest.funspec.AnyFunSpec
5 |
6 | class TokenSpec extends AnyFunSpec {
7 | describe("Token") {
8 | val tokens = new Tokens(3600 * 1000, "secret".getBytes(UTF_8))
9 |
10 | val token = tokens.create("HTTP/someserver.example.com@EXAMPLE.COM")
11 |
12 | it("should not be expired immediately"){
13 | assert(token.expired === false)
14 | }
15 |
16 | it("should rountrip serialization"){
17 | assert(token === tokens.parse(tokens.serialize(token)))
18 | assert(token.expired === false)
19 | }
20 |
21 | it("should throw an exception when parsing a tokenString with an incorrect signature"){
22 | intercept[TokenParseException] {
23 | tokens.parse(List(token.principal, token.expiration, tokens.sign(token) + "a").mkString("&"))
24 | }
25 | }
26 |
27 | it("should throw an exception when serializing an incomplete tokenString"){
28 | intercept[TokenParseException] {
29 | tokens.parse("a&1")
30 | }
31 | }
32 |
33 | it("should throw an exception when serializing an illegal tokenString"){
34 | intercept[TokenParseException] {
35 | tokens.parse("a&b&c")
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/test-server/src/main/resources/application.conf:
--------------------------------------------------------------------------------
1 | tresata.akka.http.spnego {
2 | kerberos.principal = "HTTP/someserver@SOMEDOMAIN"
3 | kerberos.keytab = "/etc/http.keytab"
4 | kerberos.debug = true
5 | }
6 |
7 | akka {
8 | loglevel = DEBUG
9 | stdout-loglevel = DEBUG
10 | event-handlers = ["akka.event.slf4j.Slf4jEventHandler"]
11 | actor {
12 | debug.unhandled = on
13 | }
14 | }
15 |
16 | akka.http.server {
17 | verbose-error-messages = on
18 | }
19 |
--------------------------------------------------------------------------------
/test-server/src/main/resources/log4j.properties:
--------------------------------------------------------------------------------
1 | log4j.rootLogger=DEBUG,stdout
2 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender
3 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
4 | log4j.appender.stdout.layout.ConversionPattern=%c: %m%n
5 |
6 | #log4j.logger.com.tresata.akka.http.spnego=DEBUG
7 |
--------------------------------------------------------------------------------
/test-server/src/main/resources/testKeystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tresata/akka-http-spnego/189f337825f8d83ce5abbce3ca1e9fc7e10c2b9b/test-server/src/main/resources/testKeystore
--------------------------------------------------------------------------------
/test-server/src/main/scala/com/tresata/akka/http/spnego/Main.scala:
--------------------------------------------------------------------------------
1 | package com.tresata.akka.http.spnego
2 |
3 | import java.io.FileInputStream
4 | import java.security.{KeyStore, SecureRandom}
5 | import javax.net.ssl.{KeyManagerFactory, TrustManagerFactory, SSLContext}
6 |
7 | import akka.actor.ActorSystem
8 | import akka.stream.ActorMaterializer
9 | import akka.http.scaladsl.{ConnectionContext, Http}
10 | import akka.http.scaladsl.server.Directives._
11 |
12 | import com.tresata.akka.http.spnego.SpnegoDirectives._
13 |
14 | object Main extends App {
15 | implicit val system: ActorSystem = ActorSystem()
16 | implicit val materializer: ActorMaterializer = ActorMaterializer()
17 |
18 | val route = logRequestResult("debug") {
19 | spnegoAuthenticate(){ token =>
20 | get{
21 | path("ping") {
22 | complete(s"pong for user ${token.principal}")
23 | }
24 | }
25 | }
26 | }
27 |
28 | // very painful ssl stuff
29 | val keystore = System.getProperty("javax.net.ssl.keyStore")
30 | val password = System.getProperty("javax.net.ssl.keyStorePassword")
31 | val ks = KeyStore.getInstance("JKS")
32 | val fis = new FileInputStream(keystore)
33 | try ks.load(fis, password.toCharArray)
34 | finally fis.close()
35 | val kmf = KeyManagerFactory.getInstance("SunX509")
36 | kmf.init(ks, password.toCharArray)
37 | val tmf = TrustManagerFactory.getInstance("SunX509")
38 | tmf.init(ks)
39 | val sslContext = SSLContext.getInstance("TLS")
40 | sslContext.init(kmf.getKeyManagers, tmf.getTrustManagers, new SecureRandom)
41 | val connectionContext = ConnectionContext.httpsServer(sslContext)
42 |
43 | Http()
44 | .newServerAt("0.0.0.0", 12345)
45 | .enableHttps(connectionContext)
46 | .bind(route)
47 | }
48 |
--------------------------------------------------------------------------------