Skip to content
Open
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 @@ -17,7 +17,8 @@ If no valid IP address is encountered, this extractor will return RemoteAddress.

@@@ warning
Clients can send any values in these headers. If the client is not a trusted upstream, the IP address can be malicious.
For sensitive operations use the @apidoc[AttributeKeys.remoteAddress](AttributeKeys$) @ref[attribute](../../../common/http-model.md#attributes),
For sensitive operations use @ref[extractDirectClientIP](extractDirectClientIP.md), which reads the
@apidoc[AttributeKeys.remoteAddress](AttributeKeys$) @ref[attribute](../../../common/http-model.md#attributes) only,
or use the specific headers which are known to be set correctly by the infrastructure you do trust.
@@@

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# extractDirectClientIP

@@@ div { .group-scala }

## Signature

@@signature [MiscDirectives.scala](/http/src/main/scala/org/apache/pekko/http/scaladsl/server/directives/MiscDirectives.scala) { #extractDirectClientIP }

@@@

## Description

Provides the value of the @apidoc[AttributeKeys.remoteAddress](AttributeKeys$) @ref[attribute](../../../common/http-model.md#attributes), which is the address of the peer of the connection the request arrived on. It requires the `pekko.http.server.remote-address-attribute` setting to be `on` and provides `RemoteAddress.Unknown` otherwise.

Unlike @ref[extractClientIP](extractClientIP.md), this directive ignores the `X-Forwarded-For` and `X-Real-IP` headers, so the address it provides cannot be chosen by the client. Note that behind a proxy this is the address of the proxy and not of the client that the proxy forwarded the request for.

## Example

Scala
: @@snip [MiscDirectivesExamplesSpec.scala](/docs/src/test/scala/docs/http/scaladsl/server/directives/MiscDirectivesExamplesSpec.scala) { #extractDirectClientIP-example }

Java
: @@snip [MiscDirectivesExamplesTest.java](/docs/src/test/java/docs/http/javadsl/server/directives/MiscDirectivesExamplesTest.java) { #extractDirectClientIPExample }
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
@@@ index

* [extractClientIP](extractClientIP.md)
* [extractDirectClientIP](extractDirectClientIP.md)
* [rejectEmptyResponse](rejectEmptyResponse.md)
* [requestEntityEmpty](requestEntityEmpty.md)
* [requestEntityPresent](requestEntityPresent.md)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

package docs.http.javadsl.server.directives;

import org.apache.pekko.http.javadsl.model.AttributeKeys;
import org.apache.pekko.http.javadsl.model.HttpRequest;
import org.apache.pekko.http.javadsl.model.StatusCodes;
import org.apache.pekko.http.javadsl.model.headers.*;
Expand Down Expand Up @@ -51,6 +52,11 @@
import static org.apache.pekko.http.javadsl.server.Directives.extractClientIP;

// #extractClientIP
// #extractDirectClientIP
import static org.apache.pekko.http.javadsl.server.Directives.complete;
import static org.apache.pekko.http.javadsl.server.Directives.extractDirectClientIP;

// #extractDirectClientIP
// #requestEntity-empty-present-example
import static org.apache.pekko.http.javadsl.server.Directives.complete;
import static org.apache.pekko.http.javadsl.server.Directives.requestEntityEmpty;
Expand Down Expand Up @@ -170,6 +176,35 @@ public void testExtractClientIP() throws UnknownHostException {
// #extractClientIPExample
}

@Test
public void testExtractDirectClientIP() throws UnknownHostException {
// #extractDirectClientIPExample
final Route route =
extractDirectClientIP(
remoteAddr ->
complete(
"Client's IP is "
+ remoteAddr
.getAddress()
.map(InetAddress::getHostAddress)
.orElseGet(() -> "unknown")));

// tests:
final String ip = "192.168.1.2";
final org.apache.pekko.http.javadsl.model.RemoteAddress remoteAddress =
org.apache.pekko.http.javadsl.model.RemoteAddress.create(InetAddress.getByName(ip));

testRoute(route)
.run(HttpRequest.GET("/").addAttribute(AttributeKeys.remoteAddress, remoteAddress))
.assertEntity("Client's IP is " + ip);

// a client cannot choose the address by sending a header
testRoute(route)
.run(HttpRequest.GET("/").addHeader(XForwardedFor.create(remoteAddress)))
.assertEntity("Client's IP is unknown");
// #extractDirectClientIPExample
}

@Test
public void testRequestEntityEmpty() {
// #requestEntity-empty-present-example
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,25 @@ class MiscDirectivesExamplesSpec extends RoutingSpec with CompileOnlySpec {
// #extractClientIP-example
}

"extractDirectClientIP-example" in {
// #extractDirectClientIP-example
val route = extractDirectClientIP { ip =>
complete("Client's ip is " + ip.toOption.map(_.getHostAddress).getOrElse("unknown"))
}

// tests:
val remoteAddress = RemoteAddress(InetAddress.getByName("192.168.3.12"))
Get("/").withAttributes(Map(AttributeKeys.remoteAddress -> remoteAddress)) ~> route ~> check {
responseAs[String] shouldEqual "Client's ip is 192.168.3.12"
}

// a client cannot choose the address by sending a header
Get("/").withHeaders(`X-Forwarded-For`(RemoteAddress(InetAddress.getByName("1.2.3.4")))) ~> route ~> check {
responseAs[String] shouldEqual "Client's ip is unknown"
}
// #extractDirectClientIP-example
}

"rejectEmptyResponse-example" in {
// #rejectEmptyResponse-example
val route = rejectEmptyResponse {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,30 @@ class MiscDirectivesSpec extends RoutingSpec {
extractClientIP { echoComplete }
} ~> check { responseAs[String] shouldEqual "unknown" }
}
"extract from the remote address attribute" in {
Get().withAttributes(Map(AttributeKeys.remoteAddress -> remoteAddress("5.6.7.8"))) ~> {
extractClientIP { echoComplete }
} ~> check { responseAs[String] shouldEqual "5.6.7.8" }
}
}

"the extractDirectClientIP directive" should {
"extract from the remote address attribute" in {
Get().withAttributes(Map(AttributeKeys.remoteAddress -> remoteAddress("5.6.7.8"))) ~> {
extractDirectClientIP { echoComplete }
} ~> check { responseAs[String] shouldEqual "5.6.7.8" }
}
"ignore the X-Forwarded-For and X-Real-IP headers a client may have sent" in {
Get().withAttributes(Map(AttributeKeys.remoteAddress -> remoteAddress("5.6.7.8"))) ~>
addHeaders(`X-Forwarded-For`(remoteAddress("2.3.4.5")), RawHeader("x-real-ip", "1.2.3.4")) ~> {
extractDirectClientIP { echoComplete }
} ~> check { responseAs[String] shouldEqual "5.6.7.8" }
}
"extract unknown when the attribute is not set" in {
Get() ~> addHeader(`X-Forwarded-For`(remoteAddress("2.3.4.5"))) ~> {
extractDirectClientIP { echoComplete }
} ~> check { responseAs[String] shouldEqual "unknown" }
}
}

"the selectPreferredLanguage directive" should {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,30 @@ abstract class MiscDirectives extends MethodDirectives {
* Extracts the client's IP from either the X-Forwarded-For, Remote-Address, X-Real-IP header
* or [[pekko.http.javadsl.model.AttributeKeys.remoteAddress]] attribute
* (in that order of priority).
*
* Note that the headers are under the control of the client unless a trusted proxy in front of this server
* overwrites them. Use [[extractDirectClientIP]] where the address must not be chosen by the client, for example
* for access control or rate limiting.
*/
def extractClientIP(inner: JFunction[RemoteAddress, Route]): Route = RouteAdapter {
D.extractClientIP { ip => inner.apply(ip).delegate }
}

/**
* Extracts the client's IP from the [[pekko.http.javadsl.model.AttributeKeys.remoteAddress]] attribute alone, that
* is the address of the peer of the connection the request arrived on. Forwarding headers are ignored, so the
* address cannot be chosen by the client, but it is the address of the last proxy rather than of the client itself
* when the request was forwarded.
*
* Requires the `pekko.http.server.remote-address-attribute` setting to be `on` and extracts
* [[pekko.http.javadsl.model.RemoteAddresses.UNKNOWN]] otherwise.
*
* @since 2.0.0
*/
def extractDirectClientIP(inner: JFunction[RemoteAddress, Route]): Route = RouteAdapter {
D.extractDirectClientIP { ip => inner.apply(ip).delegate }
}

/**
* Rejects if the request entity is non-empty.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,28 @@ trait MiscDirectives {
* or [[pekko.http.scaladsl.model.AttributeKeys.remoteAddress]] attribute
* (in that order of priority).
*
* Note that the headers are under the control of the client unless a trusted proxy in front of this server
* overwrites them. Use [[extractDirectClientIP]] where the address must not be chosen by the client, for example
* for access control or rate limiting.
*
* @group misc
*/
def extractClientIP: Directive1[RemoteAddress] = MiscDirectives._extractClientIP

/**
* Extracts the client's IP from the [[pekko.http.scaladsl.model.AttributeKeys.remoteAddress]] attribute alone, that
* is the address of the peer of the connection the request arrived on. Forwarding headers are ignored, so the
* address cannot be chosen by the client, but it is the address of the last proxy rather than of the client itself
* when the request was forwarded.
*
* Requires the `pekko.http.server.remote-address-attribute` setting to be `on` and extracts
* [[pekko.http.scaladsl.model.RemoteAddress.Unknown]] otherwise.
*
* @since 2.0.0
* @group misc
*/
def extractDirectClientIP: Directive1[RemoteAddress] = MiscDirectives._extractDirectClientIP

/**
* Rejects if the request entity is non-empty.
*
Expand Down Expand Up @@ -116,13 +134,16 @@ object MiscDirectives extends MiscDirectives {
import RouteDirectives._
import RouteResult._

private val _extractClientIP: Directive1[RemoteAddress] =
headerValuePF { case `X-Forwarded-For`(Seq(address, _*)) => address } |
headerValuePF { case `X-Real-Ip`(address) => address } |
private val _extractDirectClientIP: Directive1[RemoteAddress] =
extractRequest.map { request =>
request.attribute(AttributeKeys.remoteAddress).getOrElse(RemoteAddress.Unknown)
}

private val _extractClientIP: Directive1[RemoteAddress] =
headerValuePF { case `X-Forwarded-For`(Seq(address, _*)) => address } |
headerValuePF { case `X-Real-Ip`(address) => address } |
_extractDirectClientIP

private val _requestEntityEmpty: Directive0 =
extract(_.request.entity.isKnownEmpty).flatMap(if (_) pass else reject)

Expand Down