Skip to content
Permalink
Browse files

Adding a secure flag to requests.

This is an updated version of
#1326, incorporating
suggestions in the comments, and including tests.

Play-Cache test was running out of perm gen space
  • Loading branch information...
kamatsuoka committed Oct 12, 2013
1 parent 1cd948e commit 10d6e5a1490c674f158296d50da3565d4ff709e8
@@ -36,7 +36,7 @@
RequestHeader newRequest = request.copy(request.id(),
request.tags().$plus(new Tuple2<String, String>(requestTag, newToken)),
request.uri(), request.path(), request.method(), request.version(), request.queryString(),
request.headers(), request.remoteAddress());
request.headers(), request.remoteAddress(), request.secure());

// Create a new context that will have the new RequestHeader. This ensures that the CSRF.getToken call
// used in templates will find the token.
@@ -0,0 +1,112 @@
/*
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
*/
package play.it.http

import play.api.mvc._
import play.api.test._
import play.api.test.TestServer
import java.io.{File, InputStream}
import javax.net.ssl.{SSLContext, HttpsURLConnection, X509TrustManager}
import java.security.cert.X509Certificate
import scala.io.Source
import java.net.URL

/**
* Specs for the "secure" flag on requests
*/
object SecureFlagSpec extends PlaySpecification {

sequential

/** An action whose result is just "true" or "false" depending on the value of result.secure */
val secureFlagAction = Action {
request => Results.Ok(request.secure.toString)
}

// this step seems necessary to allow the generated keystore to be written
new File("conf").mkdir()

def withServer[T](action: EssentialAction, sslPort: Option[Int] = None)(block: Port => T) = {
val port = testServerPort
running(TestServer(port, sslPort = sslPort, application = FakeApplication(
withRoutes = {
case _ => action
}
))) {
block(port)
}
}

"Play https server" should {

val sslPort = 19943

"show that requests are secure in the absence of X_FORWARDED_PROTO" in withServer(secureFlagAction, Some(sslPort)) { _ =>
val conn = createConn(sslPort)
Source.fromInputStream(conn.getContent.asInstanceOf[InputStream]).getLines().next must_== "true"
}
"show that requests are secure in the absence of X_FORWARDED_PROTO" in withServer(secureFlagAction, Some(sslPort)) { _ =>
val conn = createConn(sslPort)
Source.fromInputStream(conn.getContent.asInstanceOf[InputStream]).getLines().next must_== "true"
}
"show that requests are secure if X_FORWARDED_PROTO is https" in withServer(secureFlagAction, Some(sslPort)) { _ =>
val conn = createConn(sslPort, Some("https"))
Source.fromInputStream(conn.getContent.asInstanceOf[InputStream]).getLines().next must_== "true"
}
"not show that requests are secure if X_FORWARDED_PROTO is http" in withServer(secureFlagAction, Some(sslPort)) { _ =>
val conn = createConn(sslPort, Some("http"))
Source.fromInputStream(conn.getContent.asInstanceOf[InputStream]).getLines().next must_== "false"
}
}

"Play http server" should {
"not show that requests are secure in the absence of X_FORWARDED_PROTO" in withServer(secureFlagAction) { port =>
val responses = BasicHttpClient.makeRequests(port)(
BasicRequest("GET", "/", "HTTP/1.1", Map(), "foo")
)
responses.length must_== 1
responses(0).body must_== Left("false")
}
"show that requests are secure if X_FORWARDED_PROTO is https" in withServer(secureFlagAction) { port =>
val responses = BasicHttpClient.makeRequests(port)(
BasicRequest("GET", "/", "HTTP/1.1", Map((X_FORWARDED_PROTO, "https")), "foo")
)
responses.length must_== 1
responses(0).body must_== Left("true")
}
"not show that requests are secure if X_FORWARDED_PROTO is http" in withServer(secureFlagAction) { port =>
val responses = BasicHttpClient.makeRequests(port)(
BasicRequest("GET", "/", "HTTP/1.1", Map((X_FORWARDED_PROTO, "http")), "foo")
)
responses.length must_== 1
responses(0).body must_== Left("false")
}
}

// the following are adapted from SslSpec

def createConn(sslPort: Int, forwardedProto: Option[String] = None) = {
val conn = new URL("https://localhost:" + sslPort + "/").openConnection().asInstanceOf[HttpsURLConnection]
forwardedProto.foreach(proto => conn.setRequestProperty(X_FORWARDED_PROTO, proto))
conn.setSSLSocketFactory(sslFactory)
conn
}

def sslFactory = {
val ctx = SSLContext.getInstance("TLS")
ctx.init(null, Array(MockTrustManager()), null)
ctx.getSocketFactory
}

case class MockTrustManager() extends X509TrustManager {
val nullArray = Array[X509Certificate]()

def checkClientTrusted(x509Certificates: Array[X509Certificate], s: String) {}

def checkServerTrusted(x509Certificates: Array[X509Certificate], s: String) {}

def getAcceptedIssuers = nullArray
}

}
@@ -104,6 +104,7 @@ class DummyRequest(data: Map[String, Array[String]]) extends play.mvc.Http.Reque
def accepts(mediaType: String) = false
def headers() = new java.util.HashMap[String, Array[String]]()
val remoteAddress = "127.0.0.1"
def secure() = false
def body() = new Http.RequestBody {
override def asFormUrlEncoded = data.asJava
override def asRaw = null
@@ -59,7 +59,7 @@ public FakeRequest withHeader(String name, String value) {
public FakeRequest withAnyContent(AnyContent content, String contentType, String method) {
Map<String, Seq<String>> map = new HashMap<String, Seq<String>>(Scala.asJava(fake.headers().toMap()));
map.put("Content-Type", Scala.toSeq(new String[] {contentType}));
fake = new play.api.test.FakeRequest(method, fake.uri(), new play.api.test.FakeHeaders(Scala.asScala(map).toSeq()), content, fake.remoteAddress(), fake.version(), fake.id(), fake.tags());
fake = new play.api.test.FakeRequest(method, fake.uri(), new play.api.test.FakeHeaders(Scala.asScala(map).toSeq()), content, fake.remoteAddress(), fake.version(), fake.id(), fake.tags(), fake.secure());
return this;
}

@@ -101,7 +101,7 @@ public FakeRequest withJsonBody(JsonNode node, String method) {
Map<String, Seq<String>> map = new HashMap<String, Seq<String>>(Scala.asJava(fake.headers().toMap()));
map.put("Content-Type", Scala.toSeq(new String[] {"application/json"}));
AnyContentAsJson content = new AnyContentAsJson(play.api.libs.json.Json.parse(node.toString()));
fake = new play.api.test.FakeRequest(method, fake.uri(), new play.api.test.FakeHeaders(Scala.asScala(map).toSeq()), content, fake.remoteAddress(), fake.version(), fake.id(), fake.tags());
fake = new play.api.test.FakeRequest(method, fake.uri(), new play.api.test.FakeHeaders(Scala.asScala(map).toSeq()), content, fake.remoteAddress(), fake.version(), fake.id(), fake.tags(), fake.secure());
return this;
}

@@ -30,7 +30,7 @@ case class FakeHeaders(override val data: Seq[(String, Seq[String])] = Seq.empty
* @param body The request body.
* @param remoteAddress The client IP.
*/
case class FakeRequest[A](method: String, uri: String, headers: FakeHeaders, body: A, remoteAddress: String = "127.0.0.1", version: String = "HTTP/1.1", id: Long = 666, tags: Map[String, String] = Map.empty[String, String]) extends Request[A] {
case class FakeRequest[A](method: String, uri: String, headers: FakeHeaders, body: A, remoteAddress: String = "127.0.0.1", version: String = "HTTP/1.1", id: Long = 666, tags: Map[String, String] = Map.empty[String, String], secure: Boolean = false) extends Request[A] {

private def _copy[B](
id: Long = this.id,
@@ -41,9 +41,10 @@ case class FakeRequest[A](method: String, uri: String, headers: FakeHeaders, bod
version: String = this.version,
headers: FakeHeaders = this.headers,
remoteAddress: String = this.remoteAddress,
secure: Boolean = this.secure,
body: B = this.body): FakeRequest[B] = {
new FakeRequest[B](
method, uri, headers, body, remoteAddress, version, id, tags
method, uri, headers, body, remoteAddress, version, id, tags, secure
)
}

@@ -232,6 +232,15 @@ public String toString() {
*/
public abstract String remoteAddress();

/**
* Is the client using SSL?
*
* If the <code>X-Forwarded-Proto</code> header is present, then this method will return true
* if the value in that header is "https", if either the local address is 127.0.0.1, or if
* <code>trustxforwarded</code> is configured to be true in the application configuration file.
*/
public abstract boolean secure();

/**
* The request host.
*/
@@ -68,6 +68,15 @@ package play.api.mvc {
*/
def remoteAddress: String

/**
* Is the client using SSL?
*
* If the <code>X-Forwarded-Proto</code> header is present, then this method will return true
* if the value in that header is "https", if either the local address is 127.0.0.1, or if
* <code>trustxforwarded</code> is configured to be true in the application configuration file.
*/
def secure: Boolean

// -- Computed

/**
@@ -187,8 +196,9 @@ package play.api.mvc {
version: String = this.version,
queryString: Map[String, Seq[String]] = this.queryString,
headers: Headers = this.headers,
remoteAddress: String = this.remoteAddress): RequestHeader = {
val (_id, _tags, _uri, _path, _method, _version, _queryString, _headers, _remoteAddress) = (id, tags, uri, path, method, version, queryString, headers, remoteAddress)
remoteAddress: String = this.remoteAddress,
secure: Boolean = this.secure): RequestHeader = {
val (_id, _tags, _uri, _path, _method, _version, _queryString, _headers, _remoteAddress, _secure) = (id, tags, uri, path, method, version, queryString, headers, remoteAddress, secure)
new RequestHeader {
val id = _id
val tags = _tags
@@ -199,6 +209,7 @@ package play.api.mvc {
val queryString = _queryString
val headers = _headers
val remoteAddress = _remoteAddress
val secure = _secure
}
}

@@ -240,6 +251,7 @@ package play.api.mvc {
def queryString = self.queryString
def headers = self.headers
def remoteAddress = self.remoteAddress
def secure = self.secure
lazy val body = f(self.body)
}

@@ -257,6 +269,7 @@ package play.api.mvc {
def queryString = rh.queryString
def headers = rh.headers
lazy val remoteAddress = rh.remoteAddress
lazy val secure = rh.secure
def username = None
val body = a
}
@@ -276,6 +289,7 @@ package play.api.mvc {
def method = request.method
def version = request.version
def remoteAddress = request.remoteAddress
def secure = request.secure
}

/**
@@ -68,6 +68,8 @@ trait JavaHelpers {

def remoteAddress = req.remoteAddress

def secure = req.secure

def host = req.host

def path = req.path
@@ -128,6 +130,8 @@ trait JavaHelpers {

def remoteAddress = req.remoteAddress

def secure = req.secure

def host = req.host

def path = req.path
@@ -14,14 +14,14 @@ import play.core._
import server.Server
import play.api._
import play.api.mvc._
import play.api.http.HeaderNames.X_FORWARDED_FOR
import play.api.http.HeaderNames.{X_FORWARDED_FOR, X_FORWARDED_PROTO}
import play.api.libs.iteratee._
import play.api.libs.iteratee.Input._
import scala.collection.JavaConverters._
import scala.util.control.Exception
import com.typesafe.netty.http.pipelining.{OrderedDownstreamChannelEvent, OrderedUpstreamMessageEvent}
import scala.concurrent.Future
import java.net.URI
import java.net.{SocketAddress, URI}
import java.io.IOException


@@ -76,17 +76,29 @@ private[play] class PlayDefaultUpstreamHandler(server: Server, allChannels: Defa
val rHeaders = getHeaders(nettyHttpRequest)

def rRemoteAddress = e.getRemoteAddress match {
case ra: java.net.InetSocketAddress => {
case ra: java.net.InetSocketAddress =>
val remoteAddress = ra.getAddress.getHostAddress
(for {
xff <- rHeaders.get(X_FORWARDED_FOR)
app <- server.applicationProvider.get.toOption
trustxforwarded <- app.configuration.getBoolean("trustxforwarded").orElse(Some(false))
if remoteAddress == "127.0.0.1" || trustxforwarded
} yield xff).getOrElse(remoteAddress)
}
forwardedHeader(remoteAddress, X_FORWARDED_FOR).getOrElse(remoteAddress)
}

def rSecure = e.getRemoteAddress match {
case ra: java.net.InetSocketAddress =>
val remoteAddress = ra.getAddress.getHostAddress
val fh = forwardedHeader(remoteAddress, X_FORWARDED_PROTO)
fh.map(_ == "https").getOrElse(ctx.getPipeline.get(classOf[SslHandler]) != null)
}

/**
* Gets the value of a header, if the remote address is localhost or
* if the trustxforwarded configuration property is true
*/
def forwardedHeader(remoteAddress: String, headerName: String) = for {
headerValue <- rHeaders.get(headerName)
app <- server.applicationProvider.get.toOption
trustxforwarded <- app.configuration.getBoolean("trustxforwarded").orElse(Some(false))
if remoteAddress == "127.0.0.1" || trustxforwarded
} yield headerValue

def tryToCreateRequest = {
val parameters = Map.empty[String, Seq[String]] ++ nettyUri.getParameters.asScala.mapValues(_.asScala)
createRequestHeader(parameters)
@@ -104,6 +116,7 @@ private[play] class PlayDefaultUpstreamHandler(server: Server, allChannels: Defa
def queryString = parameters
def headers = rHeaders
lazy val remoteAddress = rRemoteAddress
lazy val secure = rSecure
def username = None
}
untaggedRequestHeader
@@ -41,7 +41,7 @@ class HttpSpec extends Specification {

"RequestHeader" should {
"parse quoted and unquoted charset" in {
case class TestRequestHeader(headers: Headers, method: String = "GET", uri: String = "/", path: String = "", remoteAddress: String = "127.0.0.1", version: String = "HTTP/1.1", id: Long = 666, tags: Map[String, String] = Map.empty[String, String], queryString: Map[String, Seq[String]] = Map()) extends RequestHeader
case class TestRequestHeader(headers: Headers, method: String = "GET", uri: String = "/", path: String = "", remoteAddress: String = "127.0.0.1", version: String = "HTTP/1.1", id: Long = 666, tags: Map[String, String] = Map.empty[String, String], queryString: Map[String, Seq[String]] = Map(), secure: Boolean = false) extends RequestHeader

TestRequestHeader(headers = new Headers {
val data = Seq(play.api.http.HeaderNames.CONTENT_TYPE -> Seq("""text/xml; charset="utf-8""""))
@@ -52,6 +52,7 @@ class RequestHeaderSpec extends Specification {
def version = ""
def queryString = Map()
def remoteAddress = ""
def secure = false
lazy val headers = new Headers { val data = headersMap.toSeq }
}
}
@@ -0,0 +1 @@
javaOptions in Test += "-XX:MaxPermSize=128m"

0 comments on commit 10d6e5a

Please sign in to comment.
You can’t perform that action at this time.