Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions core/play/src/main/java/play/mvc/Http.java
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,15 @@ default Long id() {
*/
String remoteAddress();

/**
* The client port, if known.
*
* @return the remote port
*/
default Optional<Integer> remotePort() {
return Optional.empty();
}

/**
* @return true if the client is using SSL
*/
Expand Down Expand Up @@ -1115,6 +1124,7 @@ public RequestBuilder secure(boolean secure) {
req.withConnection(
RemoteConnection$.MODULE$.apply(
req.connection().remoteAddress(),
req.connection().remotePort(),
secure,
req.connection().clientCertificateChain()));
return this;
Expand Down Expand Up @@ -1332,6 +1342,15 @@ public String remoteAddress() {
return req.connection().remoteAddressString();
}

/**
* @return the remote port, if known
*/
@SuppressWarnings("unchecked")
public Optional<Integer> remotePort() {
return (Optional<Integer>)
(Optional<?>) OptionConverters.toJava(req.connection().remotePort());
}

/**
* @param remoteAddress sets the remote address
* @return the builder instance
Expand All @@ -1341,11 +1360,38 @@ public RequestBuilder remoteAddress(String remoteAddress) {
req.withConnection(
RemoteConnection$.MODULE$.apply(
remoteAddress,
req.connection().remotePort(),
req.connection().secure(),
req.connection().clientCertificateChain()));
return this;
}

/**
* @param remotePort sets the remote port
* @return the builder instance
*/
@SuppressWarnings("unchecked")
public RequestBuilder remotePort(Optional<Integer> remotePort) {
scala.Option<Object> scalaRemotePort =
(scala.Option<Object>) (scala.Option<?>) OptionConverters.toScala(remotePort);
req =
req.withConnection(
RemoteConnection$.MODULE$.apply(
req.connection().remoteAddress(),
scalaRemotePort,
req.connection().secure(),
req.connection().clientCertificateChain()));
return this;
}

/**
* @param remotePort sets the remote port
* @return the builder instance
*/
public RequestBuilder remotePort(int remotePort) {
return remotePort(Optional.of(remotePort));
}

/**
* @return the client X509Certificates if they have been set
*/
Expand All @@ -1363,6 +1409,7 @@ public RequestBuilder clientCertificateChain(List<X509Certificate> clientCertifi
req.withConnection(
RemoteConnection$.MODULE$.apply(
req.connection().remoteAddress(),
req.connection().remotePort(),
req.connection().secure(),
OptionConverters.toScala(
Optional.ofNullable(Scala.asScala(clientCertificateChain)))));
Expand Down
7 changes: 7 additions & 0 deletions core/play/src/main/scala/play/api/mvc/RequestHeader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ trait RequestHeader {
*/
final def remoteAddress: String = connection.remoteAddressString

/**
* The client port, if known.
*
* This method delegates to `connection.remotePort`.
*/
final def remotePort: Option[Int] = connection.remotePort

/**
* Is the client using SSL? This method delegates to `connection.secure`.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ trait RemoteConnection {
*/
def remoteAddressString: String = remoteAddress.getHostAddress

/**
* The remote client's port, if known.
*/
def remotePort: Option[Int] = None

/**
* Whether or not the connection was over a secure (e.g. HTTPS) connection.
*/
Expand All @@ -36,15 +41,19 @@ trait RemoteConnection {
*/
def clientCertificateChain: Option[Seq[X509Certificate]]

override def toString: String = s"RemoteAddress($remoteAddressString, secure=$secure, certs=$clientCertificateChain)"
override def toString: String =
s"RemoteAddress($remoteAddressString, port=$remotePort, secure=$secure, certs=$clientCertificateChain)"

override def equals(obj: scala.Any): Boolean = obj match {
case that: RemoteConnection =>
(this.remoteAddress == that.remoteAddress) &&
(this.remotePort == that.remotePort) &&
(this.secure == that.secure) &&
(this.clientCertificateChain == that.clientCertificateChain)
case _ => false
}

override def hashCode(): Int = (remoteAddress, remotePort, secure, clientCertificateChain).hashCode()
}

object RemoteConnection {
Expand All @@ -56,13 +65,27 @@ object RemoteConnection {
remoteAddressString: String,
secure: Boolean,
clientCertificateChain: Option[Seq[X509Certificate]]
): RemoteConnection = {
apply(remoteAddressString, None, secure, clientCertificateChain)
}

/**
* Create a RemoteConnection object. The address string is parsed lazily.
*/
def apply(
remoteAddressString: String,
remotePort: Option[Int],
secure: Boolean,
clientCertificateChain: Option[Seq[X509Certificate]]
): RemoteConnection = {
val s = secure
val ras = remoteAddressString
val rp = remotePort
val ccc = clientCertificateChain
new RemoteConnection {
override lazy val remoteAddress: InetAddress = InetAddresses.forString(ras)
override val remoteAddressString: String = ras
override val remotePort: Option[Int] = rp
override val secure: Boolean = s
override val clientCertificateChain: Option[Seq[X509Certificate]] = ccc
}
Expand All @@ -75,12 +98,26 @@ object RemoteConnection {
remoteAddress: InetAddress,
secure: Boolean,
clientCertificateChain: Option[Seq[X509Certificate]]
): RemoteConnection = {
apply(remoteAddress, None, secure, clientCertificateChain)
}

/**
* Create a RemoteConnection object.
*/
def apply(
remoteAddress: InetAddress,
remotePort: Option[Int],
secure: Boolean,
clientCertificateChain: Option[Seq[X509Certificate]]
): RemoteConnection = {
val s = secure
val ra = remoteAddress
val rp = remotePort
val ccc = clientCertificateChain
new RemoteConnection {
override val remoteAddress: InetAddress = ra
override val remotePort: Option[Int] = rp
override val secure: Boolean = s
override val clientCertificateChain: Option[Seq[X509Certificate]] = ccc
}
Expand Down
14 changes: 9 additions & 5 deletions core/play/src/main/scala/play/core/j/JavaHelpers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ trait JavaHelpers {
r.withConnection(new RemoteConnection {
override def remoteAddress: InetAddress = c.remoteAddress
override def remoteAddressString: String = c.remoteAddressString
override def remotePort: Option[Int] = c.remotePort
override def secure: Boolean = newSecure
override def clientCertificateChain: Option[Seq[X509Certificate]] = c.clientCertificateChain
})
Expand Down Expand Up @@ -195,11 +196,14 @@ object JavaHelpers extends JavaHelpers {
class RequestHeaderImpl(header: RequestHeader) extends JRequestHeader {
override def asScala: RequestHeader = header

override def uri: String = header.uri
override def method: String = header.method
override def version: String = header.version
override def remoteAddress: String = header.remoteAddress
override def secure: Boolean = header.secure
override def uri: String = header.uri
override def method: String = header.method
override def version: String = header.version
override def remoteAddress: String = header.remoteAddress
override def remotePort: Optional[Integer] = {
header.remotePort.map(Int.box).toJava
}
override def secure: Boolean = header.secure

override def attrs: TypedMap = new TypedMap(header.attrs)
override def withAttrs(newAttrs: TypedMap): JRequestHeader = header.withAttrs(newAttrs.asScala).asJava
Expand Down
10 changes: 8 additions & 2 deletions core/play/src/test/scala/play/api/mvc/RequestHeaderSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ class RequestHeaderSpec extends Specification {
val rh = dummyRequestHeader("GET", "/", Headers(HOST -> "playframework.com"))
rh.asJava.headers.contains("host") must beTrue
}
"keep the remote port" in {
val rh = dummyRequestHeader(remotePort = Some(12345))
rh.remotePort must beSome(12345)
rh.asJava.remotePort must beEqualTo(java.util.Optional.of(12345))
}
}

"have typed attributes" in {
Expand Down Expand Up @@ -231,10 +236,11 @@ class RequestHeaderSpec extends Specification {
requestMethod: String = "GET",
requestUri: String = "/",
headers: Headers = Headers(),
attrs: TypedMap = TypedMap.empty
attrs: TypedMap = TypedMap.empty,
remotePort: Option[Int] = None
): RequestHeader = {
new DefaultRequestFactory(HttpConfiguration()).createRequestHeader(
connection = RemoteConnection("", false, None),
connection = RemoteConnection("", remotePort, false, None),
method = requestMethod,
target = RequestTarget(requestUri, "", Map.empty),
version = "",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>
*/

package play.api.mvc.request

import com.google.common.net.InetAddresses
import org.specs2.mutable.Specification

class RemoteConnectionSpec extends Specification {
"RemoteConnection" should {
"default to an unknown remote port" in {
RemoteConnection("127.0.0.1", secure = false, None).remotePort must beNone
}

"store the remote port when created from an address string" in {
RemoteConnection("127.0.0.1", Some(12345), secure = false, None).remotePort must beSome(12345)
}

"store the remote port when created from an inet address" in {
RemoteConnection(InetAddresses.forString("127.0.0.1"), Some(12345), secure = false, None).remotePort must beSome(
12345
)
}

"compare connections with the same remote port as equal" in {
RemoteConnection("127.0.0.1", Some(12345), secure = false, None) must beEqualTo(
RemoteConnection("127.0.0.1", Some(12345), secure = false, None)
)
}

"include the remote port when comparing connections" in {
RemoteConnection("127.0.0.1", Some(12345), secure = false, None) must not(
beEqualTo(RemoteConnection("127.0.0.1", Some(54321), secure = false, None))
)
}

"use the same hash code for equal connections" in {
RemoteConnection("127.0.0.1", Some(12345), secure = false, None).hashCode must_== RemoteConnection(
"127.0.0.1",
Some(12345),
secure = false,
None
).hashCode
}
}
}
3 changes: 2 additions & 1 deletion core/play/src/test/scala/play/core/test/Fakes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,13 @@ class FakeRequestFactory(requestFactory: RequestFactory) {
version: String = "HTTP/1.1",
id: Long = 666,
secure: Boolean = false,
remotePort: Option[Int] = None,
clientCertificateChain: Option[Seq[X509Certificate]] = None,
attrs: TypedMap = TypedMap.empty
): FakeRequest[A] = {
val _uri = uri
val request: Request[A] = requestFactory.createRequest(
RemoteConnection(remoteAddress, secure, clientCertificateChain),
RemoteConnection(remoteAddress, remotePort, secure, clientCertificateChain),
method,
new RequestTarget {
override lazy val uri: URI = new URI(uriString)
Expand Down
15 changes: 15 additions & 0 deletions core/play/src/test/scala/play/mvc/RequestHeaderSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -116,5 +116,20 @@ class RequestHeaderSpec extends Specification {
requestHeader().asJava.hasBody must beFalse
}
}

"remote port" in {
"expose the scala request remote port" in {
val request = requestHeader().withConnection(RemoteConnection("127.0.0.1", Some(12345), secure = false, None))

request.asJava.remotePort must beEqualTo(java.util.Optional.of(12345))
}

"be set by the request builder" in {
val request = new Http.RequestBuilder().remoteAddress("127.0.0.1").remotePort(12345).build()

request.remotePort must beEqualTo(java.util.Optional.of(12345))
request.asScala.connection.remotePort must beSome(12345)
}
}
}
}
6 changes: 6 additions & 0 deletions documentation/manual/releases/release31/Highlights31.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ This section highlights the new features of Play 3.1. If you want to learn about

## Other Additions

### Remote Connection Port

Play now exposes the remote connection port, when known, through `RequestHeader.remotePort` and `RequestHeader.connection.remotePort` in Scala, and `Http.RequestHeader.remotePort()` in Java.

The Netty and Pekko HTTP server backends populate this value from the raw socket connection. If trusted forwarded headers replace the remote address and do not provide a port, the remote port is unknown.

### Trusting Single X-Forwarded-Proto Values

Play can now be configured to trust a single `X-Forwarded-Proto` value when `X-Forwarded-For` contains multiple addresses:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ ProxyPassReverse / http://localhost:9998

## Configuring trusted proxies

Play supports various forwarded headers used by proxies to indicate the incoming IP address and protocol of requests. Play uses this configuration to calculate the correct value for the `remoteAddress` and `secure` fields of `RequestHeader`.
Play supports various forwarded headers used by proxies to indicate the incoming IP address and protocol of requests. Play uses this configuration to calculate the correct value for the `remoteAddress` and `secure` fields of `RequestHeader`. `RequestHeader.remotePort` exposes the remote port when Play knows it, but forwarded port headers are not currently parsed.

It is trivial for an HTTP client, whether it's a browser or other client, to forge forwarded headers, thereby spoofing the IP address and protocol that Play reports, consequently, Play needs to know which proxies are trusted. Play provides a configuration option to configure a list of trusted proxies, and will validate the incoming forwarded headers to verify that they are trusted, taking the first untrusted IP address that it finds as the reported user remote address (or the last IP address if all proxies are trusted.)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ private[server] class NettyModelConversion(

/** Capture a request's connection info from its channel and headers. */
private def createRemoteConnection(channel: Channel, headers: Headers): RemoteConnection = {
val rawConnection = new RemoteConnection {
override lazy val remoteAddress: InetAddress = channel.remoteAddress().asInstanceOf[InetSocketAddress].getAddress
lazy val socketAddress = channel.remoteAddress().asInstanceOf[InetSocketAddress]
val rawConnection = new RemoteConnection {
override lazy val remoteAddress: InetAddress = socketAddress.getAddress
override lazy val remotePort: Option[Int] = Some(socketAddress.getPort)
private val sslHandler = Option(channel.pipeline().get(classOf[SslHandler]))
override def secure: Boolean = sslHandler.isDefined
override lazy val clientCertificateChain: Option[Seq[X509Certificate]] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ private[server] class PekkoModelConversion(
forwardedHeaderHandler.forwardedConnection(
new RemoteConnection {
override def remoteAddress: InetAddress = remoteAddressArg.getAddress
override def remotePort: Option[Int] = Some(remoteAddressArg.getPort)
override def secure: Boolean = secureProtocol
override def clientCertificateChain: Option[Seq[X509Certificate]] = {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ private[server] class ForwardedHeaderHandler(configuration: ForwardedHeaderHandl
def forwardedConnection(rawConnection: RemoteConnection, headers: Headers): RemoteConnection = new RemoteConnection {
// All public methods delegate to the lazily calculated connection info
override def remoteAddress: InetAddress = parsed.remoteAddress
override def remotePort: Option[Int] = parsed.remotePort
override def secure: Boolean = parsed.secure
override def clientCertificateChain: Option[Seq[X509Certificate]] = parsed.clientCertificateChain

Expand Down Expand Up @@ -102,6 +103,7 @@ private[server] class ForwardedHeaderHandler(configuration: ForwardedHeaderHandl
scan(
RemoteConnection(
parsedEntry.address,
None,
parsedEntry.secure,
None /* No cert chain for forward headers */
)
Expand Down
Loading
Loading