Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ case class FakeRequest[A](method: String, uri: String, headers: FakeHeaders, bod
_copy(body = AnyContentAsFormUrlEncoded(data.groupBy(_._1).mapValues(_.map(_._2))))
}

def certs = Promise.pure(IndexedSeq.empty)
def certs(required:Boolean) = Promise.pure(Seq.empty)

/**
* Sets a JSON body to this request.
Expand Down
22 changes: 22 additions & 0 deletions framework/src/play/src/main/java/play/mvc/Http.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
import org.codehaus.jackson.*;

import play.i18n.Lang;
import play.libs.F;

import java.security.cert.Certificate;

/**
* Defines HTTP standard objects.
Expand Down Expand Up @@ -288,6 +291,25 @@ public String getHeader(String headerName) {
return headers[0];
}

/**
* Request a client certificate from the user.
*
* Calling this method will request the user to select an X509 Certificate from their key chain if they have one,
* or return a cached certificate chain if the user has already selected one during the current TLS session.
* Since requesting something of the user could take a lot of time, this is returned immediately as a Future.
* The first element of the Certificate is the user's Certificate, the other elements of the chain if any, are the
* certificates that were used to sign the first one (which is the usual Certificate Authority based approach).
*
* @param required Whether a certificate is required or is optional. If required, the server will close the SSL
* connection if the client doesn't provide a certificate. Note that until this bug is fixed:
* https://bugs.openjdk.java.net/show_bug.cgi?id=100281, it is recommended that you always use
* required, since in some circumstances (varies from browser to browser) Java won't request a
* certificate at all, which will result in this method always returning no certificate.
* @return a Promise of the Certificate Chain, whose first element identifies the user. The promise will
* contain an Error if something went wrong (eg: the request is not made on an httpS connection)
*/
public abstract F.Promise<List<Certificate>> certs(boolean required);

}

/**
Expand Down
9 changes: 0 additions & 9 deletions framework/src/play/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
# Reference configuration for Play 2.0

# Root logger
logger.root=ERROR

# Logger used by the framework:
logger.play=INFO

# Logger provided to your application:
logger.application=DEBUG

#default timeout for promises
promise.akka.actor.typed.timeout=5s

Expand Down
46 changes: 43 additions & 3 deletions framework/src/play/src/main/scala/play/api/mvc/Http.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package play.api.mvc {

import play.api._
import play.api.libs.iteratee._
import play.api.libs.Crypto

import scala.annotation._
import java.security.cert.Certificate
import scala.concurrent.Future

/**
* The HTTP request header. Note that it doesn’t contain the request body yet.
Expand Down Expand Up @@ -52,6 +53,40 @@ package play.api.mvc {
*/
def headers: Headers

/**
* Request a client certificate from the user.
*
* Calling this method will request the user to select an X509 Certificate from their key chain if they have one,
* or return a cached certificate chain if the user has already selected one during the current TLS session.
* Since requesting something of the user could take a lot of time, this is returned immediately as a Future.
* The first element of the Certificate is the user's Certificate, the other elements of the chain if any, are the
* certificates that were used to sign the first one (which is the usual Certificate Authority based approach).
*
* For example:
* {{{
* import play.api.libs.concurrent._
* def index = Action { req =>
* Async {
* //timeouts should be set as transport specific options as explained in Netty's ChannelFuture
* //if done that way, then timeouts will break the connection anyway.
* req.certs().extend1 {
* case Redeemed(certs) => Ok("your certs are: \n\n "+certs )
* case Thrown(e) => InternalServerError("received error: \n"+e )
* }
* }
* }
* }}}
*
* @param required Whether a certificate is required or is optional. If required, the server will close the SSL
* connection if the client doesn't provide a certificate. Note that until this bug is fixed:
* https://bugs.openjdk.java.net/show_bug.cgi?id=100281, it is recommended that you always use
* required, since in some circumstances (varies from browser to browser) Java won't request a
* certificate at all, which will result in this method always returning no certificate.
* @return a Promise of the Certificate Chain, whose first element identifies the user. The promise will
* contain an Error if something went wrong (eg: the request is not made on an httpS connection)
*/
def certs(required:Boolean): Future[Seq[Certificate]]

/**
* The client IP address.
*
Expand Down Expand Up @@ -152,9 +187,10 @@ package play.api.mvc {
version: String = this.version,
queryString: Map[String, Seq[String]] = this.queryString,
headers: Headers = this.headers,
remoteAddress: String = this.remoteAddress
remoteAddress: String = this.remoteAddress,
certs: Boolean => Future[Seq[Certificate]] = this.certs
): RequestHeader = {
val (_id, _tags, _uri, _path, _method, _version, _queryString, _headers, _remoteAddress) = (id, tags, uri, path, method, version, queryString, headers, remoteAddress)
val (_id, _tags, _uri, _path, _method, _version, _queryString, _headers, _remoteAddress, _certs) = (id, tags, uri, path, method, version, queryString, headers, remoteAddress, certs)
new RequestHeader {
val id = _id
val tags = _tags
Expand All @@ -165,6 +201,7 @@ package play.api.mvc {
val queryString = _queryString
val headers = _headers
val remoteAddress = _remoteAddress
def certs(required:Boolean) = _certs(required)
}
}

Expand Down Expand Up @@ -198,6 +235,7 @@ package play.api.mvc {
def path = self.path
def method = self.method
def version = self.version
def certs(required:Boolean) = self.certs(required)
def queryString = self.queryString
def headers = self.headers
def remoteAddress = self.remoteAddress
Expand All @@ -217,6 +255,7 @@ package play.api.mvc {
def version = rh.version
def queryString = rh.queryString
def headers = rh.headers
def certs(required:Boolean) = rh.certs(required)
lazy val remoteAddress = rh.remoteAddress
def username = None
val body = a
Expand All @@ -232,6 +271,7 @@ package play.api.mvc {
def body = request.body
def headers = request.headers
def queryString = request.queryString
def certs(required:Boolean) = request.certs(required)
def path = request.path
def uri = request.uri
def method = request.method
Expand Down
19 changes: 10 additions & 9 deletions framework/src/play/src/main/scala/play/core/j/JavaHelpers.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package play.core.j

import play.api.mvc._
import play.mvc.{ Action => JAction, Result => JResult }
import play.mvc.Http.{ Context => JContext, Request => JRequest, RequestBody => JBody, Cookies => JCookies, Cookie => JCookie }

import scala.collection.JavaConverters._

import play.mvc.{ Result => JResult }
import play.mvc.Http.{ Context => JContext, Request => JRequest, Cookies => JCookies, Cookie => JCookie }
import play.api.libs.concurrent.execution.defaultContext

class EitherToFEither[A,B]() extends play.libs.F.Function[Either[A,B],play.libs.F.Either[A,B]] {

Expand Down Expand Up @@ -55,7 +52,7 @@ trait JavaHelpers {

/**
* creates a java request (with an empty body) from a scala RequestHeader
* @param request incoming requestHeader
* @param req incoming requestHeader
*/
def createJavaRequest(req: RequestHeader): JRequest = {
new JRequest {
Expand Down Expand Up @@ -91,14 +88,16 @@ trait JavaHelpers {
yield new JCookie(cookie.name, cookie.value, cookie.maxAge, cookie.path, cookie.domain.getOrElse(null), cookie.secure, cookie.httpOnly)).getOrElse(null)
}

def certs(required: Boolean) = new play.libs.F.Promise(req.certs(required).map(c => c.asJava))

override def toString = req.toString

}
}

/**
* creates a java context from a scala RequestHeader
* @param request
* @param req
*/
def createJavaContext(req: RequestHeader): JContext = {
new JContext(
Expand All @@ -112,7 +111,7 @@ trait JavaHelpers {

/**
* creates a java context from a scala Request[RequestBody]
* @param request
* @param req
*/
def createJavaContext(req: Request[RequestBody]): JContext = {
new JContext(req.id, new JRequest {
Expand Down Expand Up @@ -148,6 +147,8 @@ trait JavaHelpers {
yield new JCookie(cookie.name, cookie.value, cookie.maxAge, cookie.path, cookie.domain.getOrElse(null), cookie.secure, cookie.httpOnly)).getOrElse(null)
}

def certs(required: Boolean) = new play.libs.F.Promise(req.certs(required).map(c => c.asJava))

override def toString = req.toString

},
Expand Down
107 changes: 67 additions & 40 deletions framework/src/play/src/main/scala/play/core/server/NettyServer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -59,62 +59,89 @@ class NettyServer(appProvider: ApplicationProvider, port: Int, sslPort: Option[I
newPipeline
}

lazy val sslContext: Option[SSLContext] = //the sslContext should be reused on each connection
Option(System.getProperty("https.keyStore")) map { path =>
lazy val sslContext: Option[SSLContext] = { //the sslContext should be reused on each connection
for (
keyStore <- loadKeyStore();
keyManagers <- loadKeyManagers(keyStore);
trustManagers <- loadTrustManagers(keyStore)
) yield {
// Configure the SSL context
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(keyManagers, trustManagers, null)
sslContext
}
}
}

private def loadKeyStore() = {
Option(System.getProperty("https.keyStore")) match {
case Some(path) => {
// Load the configured key store
val keyStore = KeyStore.getInstance(System.getProperty("https.keyStoreType", "JKS"))
val password = System.getProperty("https.keyStorePassword", "").toCharArray
val algorithm = System.getProperty("https.keyStoreAlgorithm", KeyManagerFactory.getDefaultAlgorithm)
val file = new File(path)
if (file.isFile) {
IO.use(new FileInputStream(file)) {
in =>
keyStore.load(in, password)
in => keyStore.load(in, password)
}
Logger("play").debug("Using HTTPS keystore at " + file.getAbsolutePath)
try {
val kmf = KeyManagerFactory.getInstance(algorithm)
kmf.init(keyStore, password)
Some(kmf)
} catch {
case e: Exception => {
Logger("play").error("Error loading HTTPS keystore from " + file.getAbsolutePath, e)
None
}
}
Some(keyStore)
} else {
Logger("play").error("Unable to find HTTPS keystore at \"" + file.getAbsolutePath + "\"")
None
}
} orElse {

// Load a generated key store
}
case None => {
Logger("play").warn("Using generated key with self signed certificate for HTTPS. This should not be used in production.")
Some(FakeKeyStore.keyManagerFactory(applicationProvider.path))

} flatMap { a => a } map { kmf =>
// Load the configured trust manager
val tm = Option(System.getProperty("https.trustStore")).map {
case "noCA" => {
Logger("play").warn("HTTPS configured with no client " +
"side CA verification. Requires http://webid.info/ for client certifiate verification.")
Array[TrustManager](noCATrustManager)
}
case _ => {
Logger("play").debug("Using default trust store for client side CA verification")
null
}
}.getOrElse {
Logger("play").debug("Using default trust store for client side CA verification")
null
}
FakeKeyStore.keyStore(applicationProvider.path)
}
}
}

// Configure the SSL context
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(kmf.getKeyManagers, tm, null)
sslContext
private def loadKeyManagers(keyStore: KeyStore) = {
try {
val algorithm = System.getProperty("https.keyStoreAlgorithm", KeyManagerFactory.getDefaultAlgorithm)
val kmf = KeyManagerFactory.getInstance(algorithm)
val password = System.getProperty("https.keyStoreKeyPassword", System.getProperty("https.keyStorePassword", "")).toCharArray
kmf.init(keyStore, password)
Some(kmf.getKeyManagers)
} catch {
case e: Exception => {
Logger("play").error("Error loading key managers", e)
None
}
}
}

private def loadTrustManagers(keyStore: KeyStore) = {
val algorithm = System.getProperty("https.trustStoreAlgorithm", TrustManagerFactory.getDefaultAlgorithm)

System.getProperty("https.trustStore", "keystore") match {
case "noCA" => {
Logger("play").warn("HTTPS configured with no client " +
"side CA verification. Requires http://webid.info/ for client certifiate verification.")
Some(Array[TrustManager](noCATrustManager))
}
case "keystore" => {
Logger("play").debug("Using configured key store as the trust store")
try {
val tmf = TrustManagerFactory.getInstance(algorithm)
tmf.init(keyStore)
Some(tmf.getTrustManagers)
} catch {
case e: Exception => {
Logger("play").error("Error loading trust managers", e)
None
}
}
}
case "default" => Some(null) // Use the Java default trust store
case unknown => {
Logger("play").error("Unknown trust store type, must be one of [keystore, noCA, default]: " + unknown)
None
}
}
}

// Keep a reference on all opened channels (useful to close everything properly, especially in DEV mode)
val allChannels = new DefaultChannelGroup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ object FakeKeyStore {
val GeneratedKeyStore = "conf/generated.keystore"
val DnName = "CN=localhost, OU=Unit Testing, O=Mavericks, L=Moon Base 1, ST=Cyberspace, C=CY"

def keyManagerFactory(appPath: File): Option[KeyManagerFactory] = {
def keyStore(appPath: File): Option[KeyStore] = {
try {
val keyStore = KeyStore.getInstance("JKS")
val keyStoreFile = new File(appPath, GeneratedKeyStore)
Expand All @@ -40,11 +40,7 @@ object FakeKeyStore {
} else {
IO.use(new FileInputStream(keyStoreFile)) { in => keyStore.load(in, "".toCharArray)}
}

// Load the key and certificate into a key manager factory
val kmf = KeyManagerFactory.getInstance("SunX509")
kmf.init(keyStore, "".toCharArray)
Some(kmf)
Some(keyStore)
} catch {
case e: Exception => {
Logger("play").error("Error loading fake key store", e)
Expand Down
Loading