This is an automated email from the ASF dual-hosted git repository.

rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit d7cb658e1b07214823500659fbdefdc576c45f71
Author: Quan Tran <hqt...@linagora.com>
AuthorDate: Wed Apr 2 10:30:10 2025 +0700

    [ENHANCEMENT] JMAP original client IP: should extract the first client IP 
address
    
    X-Forwarded-For: <client>, <proxy>, …, <proxyN>
---
 .../org/apache/james/jmap/routes/JMAPApiRoutes.scala  | 19 ++++++++++++++++++-
 .../apache/james/jmap/routes/JMAPApiRoutesTest.scala  | 17 +++++++++++++++++
 2 files changed, 35 insertions(+), 1 deletion(-)

diff --git 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
index 328a0b5f60..fe73e3f3f5 100644
--- 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
@@ -23,6 +23,8 @@ import java.util.stream
 import java.util.stream.Stream
 
 import com.fasterxml.jackson.core.exc.StreamConstraintsException
+import com.google.common.annotations.VisibleForTesting
+import com.google.common.base.Splitter
 import io.netty.handler.codec.http.HttpHeaderNames.{CONTENT_LENGTH, 
CONTENT_TYPE}
 import io.netty.handler.codec.http.HttpMethod
 import io.netty.handler.codec.http.HttpResponseStatus.OK
@@ -45,11 +47,23 @@ import reactor.core.publisher.{Mono, SynchronousSink}
 import reactor.core.scala.publisher.SMono
 import reactor.netty.http.server.{HttpServerRequest, HttpServerResponse}
 
+import scala.jdk.OptionConverters._
 import scala.util.Try
 
 object JMAPApiRoutes {
   val LOGGER: Logger = LoggerFactory.getLogger(classOf[JMAPApiRoutes])
   val ORIGINAL_IP_HEADER: String = 
System.getProperty("james.jmap.mdc.original.ip.header", "x-forwarded-for")
+
+  @VisibleForTesting
+  def extractOriginalClientIP(originalIpHeader: String): String =
+    Option(originalIpHeader)
+      .flatMap(value => Splitter.on(',')
+        .trimResults
+        .omitEmptyStrings
+        .splitToStream(value)
+        .findFirst()
+        .toScala)
+      .getOrElse("")
 }
 
 case class StreamConstraintsExceptionWithInput(cause: 
StreamConstraintsException, input: Array[Byte]) extends RuntimeException(cause)
@@ -80,9 +94,12 @@ class JMAPApiRoutes @Inject() 
(@Named(InjectionKeys.RFC_8621) val authenticator:
       .`then`()
       .contextWrite(ReactorUtils.context("MDCBuilder.IP", MDCBuilder.create()
         .addToContext(MDCBuilder.IP, 
Option(httpServerRequest.hostAddress()).map(_.toString()).getOrElse(""))
-        .addToContext(ORIGINAL_IP_HEADER, 
Option(httpServerRequest.requestHeaders().get(ORIGINAL_IP_HEADER)).getOrElse(""))
+        .addToContext(ORIGINAL_IP_HEADER, 
extractOriginalClientIP(httpServerRequest))
         .addToContext("User-Agent", 
Option(httpServerRequest.requestHeaders().get("User-Agent")).getOrElse(""))))
 
+  private def extractOriginalClientIP(httpServerRequest: HttpServerRequest): 
String =
+    
JMAPApiRoutes.extractOriginalClientIP(httpServerRequest.requestHeaders().get(ORIGINAL_IP_HEADER))
+
   private def requestAsJsonStream(httpServerRequest: HttpServerRequest): 
SMono[RequestObject] =
     SMono.fromPublisher(httpServerRequest
       .receive()
diff --git 
a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala
 
b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala
index 02034ebbb2..a4063dda44 100644
--- 
a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala
@@ -272,6 +272,23 @@ class JMAPApiRoutesTest extends AnyFlatSpec with 
BeforeAndAfter with Matchers {
     jmapServer.stop()
   }
 
+  "Extract original client IP address" should "work well" in {
+    assert(JMAPApiRoutes.extractOriginalClientIP("203.0.113.195, 
2001:db8:85a3:8d3:1319:8a2e:370:7348")
+      .equals("203.0.113.195"))
+
+    assert(JMAPApiRoutes.extractOriginalClientIP("203.0.113.195")
+      .equals("203.0.113.195"))
+
+    assert(JMAPApiRoutes.extractOriginalClientIP("203.0.113.195  ")
+      .equals("203.0.113.195"))
+
+    assert(JMAPApiRoutes.extractOriginalClientIP(null)
+      .equals(""))
+
+    assert(JMAPApiRoutes.extractOriginalClientIP("")
+      .equals(""))
+  }
+
   "RFC-8621 version, GET" should "not supported and return 404 status" in {
     val headers: Headers = Headers.headers(
       new Header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER),


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscr...@james.apache.org
For additional commands, e-mail: notifications-h...@james.apache.org

Reply via email to