diff --git a/docs/src/main/paradox/routing-dsl/directives/misc-directives/extractClientIP.md b/docs/src/main/paradox/routing-dsl/directives/misc-directives/extractClientIP.md index 2832a417a5..c3f3b56e9b 100644 --- a/docs/src/main/paradox/routing-dsl/directives/misc-directives/extractClientIP.md +++ b/docs/src/main/paradox/routing-dsl/directives/misc-directives/extractClientIP.md @@ -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. @@@ diff --git a/docs/src/main/paradox/routing-dsl/directives/misc-directives/extractDirectClientIP.md b/docs/src/main/paradox/routing-dsl/directives/misc-directives/extractDirectClientIP.md new file mode 100644 index 0000000000..36534103c3 --- /dev/null +++ b/docs/src/main/paradox/routing-dsl/directives/misc-directives/extractDirectClientIP.md @@ -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 } diff --git a/docs/src/main/paradox/routing-dsl/directives/misc-directives/index.md b/docs/src/main/paradox/routing-dsl/directives/misc-directives/index.md index 470710c654..7418de3f4a 100644 --- a/docs/src/main/paradox/routing-dsl/directives/misc-directives/index.md +++ b/docs/src/main/paradox/routing-dsl/directives/misc-directives/index.md @@ -5,6 +5,7 @@ @@@ index * [extractClientIP](extractClientIP.md) +* [extractDirectClientIP](extractDirectClientIP.md) * [rejectEmptyResponse](rejectEmptyResponse.md) * [requestEntityEmpty](requestEntityEmpty.md) * [requestEntityPresent](requestEntityPresent.md) diff --git a/docs/src/test/java/docs/http/javadsl/server/directives/MiscDirectivesExamplesTest.java b/docs/src/test/java/docs/http/javadsl/server/directives/MiscDirectivesExamplesTest.java index f5abce2c3f..1ca0ee022a 100644 --- a/docs/src/test/java/docs/http/javadsl/server/directives/MiscDirectivesExamplesTest.java +++ b/docs/src/test/java/docs/http/javadsl/server/directives/MiscDirectivesExamplesTest.java @@ -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.*; @@ -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; @@ -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 diff --git a/docs/src/test/scala/docs/http/scaladsl/server/directives/MiscDirectivesExamplesSpec.scala b/docs/src/test/scala/docs/http/scaladsl/server/directives/MiscDirectivesExamplesSpec.scala index 192e752f3a..9addf39f45 100644 --- a/docs/src/test/scala/docs/http/scaladsl/server/directives/MiscDirectivesExamplesSpec.scala +++ b/docs/src/test/scala/docs/http/scaladsl/server/directives/MiscDirectivesExamplesSpec.scala @@ -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 { diff --git a/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/server/directives/MiscDirectivesSpec.scala b/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/server/directives/MiscDirectivesSpec.scala index 978efc58f6..5222358ab3 100644 --- a/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/server/directives/MiscDirectivesSpec.scala +++ b/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/server/directives/MiscDirectivesSpec.scala @@ -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 { diff --git a/http/src/main/scala/org/apache/pekko/http/javadsl/server/directives/MiscDirectives.scala b/http/src/main/scala/org/apache/pekko/http/javadsl/server/directives/MiscDirectives.scala index 5e4aa1797e..38fa94100f 100644 --- a/http/src/main/scala/org/apache/pekko/http/javadsl/server/directives/MiscDirectives.scala +++ b/http/src/main/scala/org/apache/pekko/http/javadsl/server/directives/MiscDirectives.scala @@ -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. */ diff --git a/http/src/main/scala/org/apache/pekko/http/scaladsl/server/directives/MiscDirectives.scala b/http/src/main/scala/org/apache/pekko/http/scaladsl/server/directives/MiscDirectives.scala index e127f1aad6..91a1aba9f6 100644 --- a/http/src/main/scala/org/apache/pekko/http/scaladsl/server/directives/MiscDirectives.scala +++ b/http/src/main/scala/org/apache/pekko/http/scaladsl/server/directives/MiscDirectives.scala @@ -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. * @@ -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)