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