Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

server side TLS support #339

Merged
merged 3 commits into from Oct 4, 2012
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
108 changes: 79 additions & 29 deletions framework/src/play/src/main/scala/play/core/server/NettyServer.scala
@@ -1,15 +1,9 @@
package play.core.server

import org.jboss.netty.buffer._
import org.jboss.netty.channel._
import org.jboss.netty.bootstrap._
import org.jboss.netty.channel.Channels._
import org.jboss.netty.handler.codec.http._
import org.jboss.netty.channel.socket.nio._
import org.jboss.netty.handler.stream._
import org.jboss.netty.handler.codec.http.HttpHeaders._
import org.jboss.netty.handler.codec.http.HttpHeaders.Names._
import org.jboss.netty.handler.codec.http.HttpHeaders.Values._
import org.jboss.netty.channel.group._
import org.jboss.netty.handler.ssl._

Expand All @@ -18,15 +12,12 @@ import javax.net.ssl._
import java.util.concurrent._

import play.core._
import play.core.server.websocket._
import play.api._
import play.api.mvc._
import play.api.libs.iteratee._
import play.api.libs.iteratee.Input._
import play.api.libs.concurrent._
import play.core.server.netty._

import scala.collection.JavaConverters._
import java.security.cert.X509Certificate
import java.io.{File, FileInputStream}
import utils.IO

/**
* provides a stopable Server
Expand All @@ -48,26 +39,75 @@ class NettyServer(appProvider: ApplicationProvider, port: Int, sslPort: Option[I
Executors.newCachedThreadPool()))

class PlayPipelineFactory(secure: Boolean = false) extends ChannelPipelineFactory {

def getPipeline = {
val newPipeline = pipeline()

if (secure) {
val keyStore = KeyStore.getInstance("JKS")
keyStore.load(FakeKeyStore.asInputStream, FakeKeyStore.getKeyStorePassword)
val kmf = KeyManagerFactory.getInstance("SunX509")
kmf.init(keyStore, FakeKeyStore.getCertificatePassword)
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(kmf.getKeyManagers, null, null)
val sslEngine = sslContext.createSSLEngine
sslEngine.setUseClientMode(false)
newPipeline.addLast("ssl", new SslHandler(sslEngine))
sslContext.map { ctxt =>
val sslEngine = ctxt.createSSLEngine
sslEngine.setUseClientMode(false)
newPipeline.addLast("ssl", new SslHandler(sslEngine))
}
}

newPipeline.addLast("decoder", new HttpRequestDecoder(4096, 8192, 8192))
newPipeline.addLast("encoder", new HttpResponseEncoder())
newPipeline.addLast("handler", defaultUpStreamHandler)
newPipeline
}

lazy val sslContext: Option[SSLContext] = //the sslContext should be reused on each connection
for (tlsPort <- sslPort;
app <- appProvider.get.right.toOption )
yield {
val config = app.configuration
val ksAttr = "https.port" + tlsPort + ".keystore"
val keyStore = KeyStore.getInstance(config.getString(ksAttr + ".type").getOrElse("JKS"))
val kmfOpt: Option[Option[KeyManagerFactory]] = for (
path <- config.getString(ksAttr + ".location");
alias <- config.getString(ksAttr + ".alias");
password <- config.getString(ksAttr + ".password").orElse(Some("")).map(_.toCharArray);
algorithm <- config.getString(ksAttr + ".algorithm").orElse(Option(KeyManagerFactory.getDefaultAlgorithm))
) yield {
//Logger("play").info("path="+path+" alias="+alias+" password="+password+" alg="+algorithm)
val file = new File(path)
if (file.isFile) {
IO.use(new FileInputStream(file)) {
in =>
keyStore.load(in, password)
}
Logger("play").info("for port " + tlsPort + " using keystore at " + file)
val kmf = KeyManagerFactory.getInstance(algorithm)
kmf.init(keyStore, password) //there should be a certificate keystore
Some(kmf)
} else None
}
val kmf = kmfOpt.flatMap(a => a).orElse {
Logger("play").warn("using localhost fake keystore for ssl connection on port " + sslPort.get)
keyStore.load(FakeKeyStore.asInputStream, FakeKeyStore.getKeyStorePassword)
val kmf = KeyManagerFactory.getInstance("SunX509")
kmf.init(keyStore, FakeKeyStore.getCertificatePassword)
Some(kmf)
}.get

val sslContext = SSLContext.getInstance("TLS")
val tm = config.getString(ksAttr + ".trust").map {
case "noCA" => {
Logger("play").warn("secure http server (https) on port " + sslPort.get + " with no client " +
"side CA verification. Requires http://webid.info/ for client certifiate verification.")
Array[TrustManager](noCATrustManager)
}
case path => {
Logger("play").info("no trust info for port " + tlsPort)
null
} //for the moment
}.getOrElse {
Logger("play").info("no trust attribute for port " + tlsPort)
null
}
sslContext.init(kmf.getKeyManagers, tm, new SecureRandom())
sslContext
}

}

// Keep a reference on all opened channels (useful to close everything properly, especially in DEV mode)
Expand Down Expand Up @@ -134,13 +174,19 @@ class NettyServer(appProvider: ApplicationProvider, port: Int, sslPort: Option[I

}

object noCATrustManager extends X509TrustManager {
val nullArray = Array[X509Certificate]()
def checkClientTrusted(x509Certificates: Array[X509Certificate], s: String) {}
def checkServerTrusted(x509Certificates: Array[X509Certificate], s: String) {}
def getAcceptedIssuers() = nullArray
}

/**
* bootstraps Play application with a NettyServer backened
*/
object NettyServer {

import java.io._
import java.net._

/**
* creates a NettyServer based on the application represented by applicationPath
Expand Down Expand Up @@ -170,11 +216,13 @@ object NettyServer {
}

try {
val app = new StaticApplication(applicationPath)
val config = app.application.configuration
val server = new NettyServer(
new StaticApplication(applicationPath),
Option(System.getProperty("http.port")).map(Integer.parseInt(_)).getOrElse(9000),
Option(System.getProperty("https.port")).map(Integer.parseInt(_)),
Option(System.getProperty("http.address")).getOrElse("0.0.0.0"))
app,
config.getInt("http.port").getOrElse(9000),
config.getInt("https.port"),
config.getString("http.address").getOrElse("0.0.0.0"))

Runtime.getRuntime.addShutdownHook(new Thread {
override def run {
Expand Down Expand Up @@ -214,7 +262,9 @@ object NettyServer {
play.utils.Threads.withContextClassLoader(this.getClass.getClassLoader) {
try {
val appProvider = new ReloadableApplication(sbtLink)
new NettyServer(appProvider, port, mode = Mode.Dev)
new NettyServer(appProvider, port,
Option(System.getProperty("https.port")).map(Integer.parseInt(_)),
mode = Mode.Dev)
} catch {
case e => {
throw e match {
Expand Down