diff --git a/akka-docs/rst/scala/code/docs/http/scaladsl/server/directives/MiscDirectivesExamplesSpec.scala b/akka-docs/rst/scala/code/docs/http/scaladsl/server/directives/MiscDirectivesExamplesSpec.scala index c888383064..b2762d376f 100644 --- a/akka-docs/rst/scala/code/docs/http/scaladsl/server/directives/MiscDirectivesExamplesSpec.scala +++ b/akka-docs/rst/scala/code/docs/http/scaladsl/server/directives/MiscDirectivesExamplesSpec.scala @@ -8,6 +8,7 @@ import akka.http.scaladsl.model._ import akka.http.scaladsl.server._ import headers._ import docs.http.scaladsl.server.RoutingSpec +import java.net.InetAddress class MiscDirectivesExamplesSpec extends RoutingSpec { @@ -17,7 +18,7 @@ class MiscDirectivesExamplesSpec extends RoutingSpec { } // tests: - Get("/").withHeaders(`Remote-Address`(RemoteAddress("192.168.3.12"))) ~> route ~> check { + Get("/").withHeaders(`Remote-Address`(RemoteAddress(InetAddress.getByName("192.168.3.12")))) ~> route ~> check { responseAs[String] shouldEqual "Client's ip is 192.168.3.12" } } diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/RemoteAddress.java b/akka-http-core/src/main/java/akka/http/javadsl/model/RemoteAddress.java index 8e391dd46a..3be65a3656 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/RemoteAddress.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/RemoteAddress.java @@ -26,9 +26,6 @@ public abstract class RemoteAddress { public static RemoteAddress create(InetSocketAddress address) { return akka.http.scaladsl.model.RemoteAddress.apply(address); } - public static RemoteAddress create(String address) { - return akka.http.scaladsl.model.RemoteAddress.apply(address); - } public static RemoteAddress create(byte[] address) { return akka.http.scaladsl.model.RemoteAddress.apply(address); } diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/headers/XRealIp.java b/akka-http-core/src/main/java/akka/http/javadsl/model/headers/XRealIp.java new file mode 100644 index 0000000000..7ba6bc59da --- /dev/null +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/headers/XRealIp.java @@ -0,0 +1,16 @@ +/** + * Copyright (C) 2009-2016 Typesafe Inc. + */ + +package akka.http.javadsl.model.headers; + +/** + * Model for the `X-Real-Ip` header. + */ +public abstract class XRealIp extends akka.http.scaladsl.model.HttpHeader { + public abstract akka.http.javadsl.model.RemoteAddress address(); + + public static XRealIp create(akka.http.javadsl.model.RemoteAddress address) { + return new akka.http.scaladsl.model.headers.X$minusReal$minusIp(((akka.http.scaladsl.model.RemoteAddress) address)); + } +} diff --git a/akka-http-core/src/main/scala/akka/http/impl/model/parser/HeaderParser.scala b/akka-http-core/src/main/scala/akka/http/impl/model/parser/HeaderParser.scala index d765e91aaa..85bce4314f 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/model/parser/HeaderParser.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/model/parser/HeaderParser.scala @@ -154,7 +154,8 @@ private[http] object HeaderParser { "upgrade", "user-agent", "www-authenticate", - "x-forwarded-for") + "x-forwarded-for", + "x-real-ip") abstract class Settings { def uriParsingMode: Uri.ParsingMode diff --git a/akka-http-core/src/main/scala/akka/http/impl/model/parser/SimpleHeaders.scala b/akka-http-core/src/main/scala/akka/http/impl/model/parser/SimpleHeaders.scala index be3f4f74f6..4ba4df24f8 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/model/parser/SimpleHeaders.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/model/parser/SimpleHeaders.scala @@ -221,4 +221,9 @@ private[parser] trait SimpleHeaders { this: Parser with CommonRules with CommonA def addr = rule { (`ip-v4-address` | `ip-v6-address`) ~> (RemoteAddress(_)) | "unknown" ~ push(RemoteAddress.Unknown) } rule { oneOrMore(addr).separatedBy(listSep) ~ EOI ~> (`X-Forwarded-For`(_)) } } + + def `x-real-ip` = rule { + (`ip-v4-address` | `ip-v6-address`) ~ EOI ~> (b ⇒ `X-Real-Ip`(RemoteAddress(b))) + } + } \ No newline at end of file diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/RemoteAddress.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/RemoteAddress.scala index 0fa2605dc2..12493a0afa 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/RemoteAddress.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/RemoteAddress.scala @@ -45,9 +45,6 @@ object RemoteAddress { def isUnknown = false } - def apply(s: String): RemoteAddress = - try IP(InetAddress.getByName(s)) catch { case _: UnknownHostException ⇒ Unknown } - def apply(a: InetAddress, port: Option[Int] = None): IP = IP(a, port) def apply(a: InetSocketAddress): IP = IP(a.getAddress, Some(a.getPort)) diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/headers/headers.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/headers/headers.scala index b2671eab03..e78293bfa1 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/headers/headers.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/headers/headers.scala @@ -896,8 +896,7 @@ final case class `WWW-Authenticate`(challenges: immutable.Seq[HttpChallenge]) ex } // http://en.wikipedia.org/wiki/X-Forwarded-For -object `X-Forwarded-For` extends ModeledCompanion[`X-Forwarded-For`] { - def apply(first: String, more: String*): `X-Forwarded-For` = apply(RemoteAddress(first), more.map(RemoteAddress(_)): _*) +object `X-Forwarded-For` extends ModeledCompanion[`X-Forwarded-For`] { def apply(first: RemoteAddress, more: RemoteAddress*): `X-Forwarded-For` = apply(immutable.Seq(first +: more: _*)) implicit val addressesRenderer = Renderer.defaultSeqRenderer[RemoteAddress] // cache } @@ -911,3 +910,10 @@ final case class `X-Forwarded-For`(addresses: immutable.Seq[RemoteAddress]) exte /** Java API */ def getAddresses: Iterable[jm.RemoteAddress] = addresses.asJava } + +object `X-Real-Ip` extends ModeledCompanion[`X-Real-Ip`] +final case class `X-Real-Ip`(address:RemoteAddress) extends jm.headers.XRealIp + with RequestHeader { + def renderValue[R <: Rendering](r: R): r.type = r ~~ address + protected def companion = `X-Real-Ip` +} diff --git a/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/HttpHeaderParserSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/HttpHeaderParserSpec.scala index 5653afbc67..8d59a7efa0 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/HttpHeaderParserSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/HttpHeaderParserSpec.scala @@ -128,6 +128,13 @@ class HttpHeaderParserSpec extends WordSpec with Matchers with BeforeAndAfterAll parseAndCache("Origin: localhost:8080\r\nx")() shouldEqual RawHeader("origin", "localhost:8080") } + "parse and cache an X-Forwarded-For with a hostname in it as a RawHeader" in new TestSetup() { + parseAndCache("X-Forwarded-For: 1.2.3.4, akka.io\r\nx")() shouldEqual RawHeader("x-forwarded-for", "1.2.3.4, akka.io") + } + + "parse and cache an X-Real-Ip with a hostname as it's value as a RawHeader" in new TestSetup() { + parseAndCache("X-Real-Ip: akka.io\r\nx")() shouldEqual RawHeader("x-real-ip", "akka.io") + } "parse and cache a raw header" in new TestSetup(primed = false) { insert("hello: bob", 'Hello) val (ixA, headerA) = parseLine("Fancy-Pants: foo\r\nx") diff --git a/akka-http-core/src/test/scala/akka/http/impl/model/parser/HttpHeaderSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/model/parser/HttpHeaderSpec.scala index 4b84322c4d..055899fcd7 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/model/parser/HttpHeaderSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/model/parser/HttpHeaderSpec.scala @@ -17,6 +17,7 @@ import MediaRanges._ import HttpCharsets._ import HttpEncodings._ import HttpMethods._ +import java.net.InetAddress class HttpHeaderSpec extends FreeSpec with Matchers { val `application/vnd.spray` = MediaType.applicationBinary("vnd.spray", MediaType.Compressible) @@ -545,14 +546,14 @@ class HttpHeaderSpec extends FreeSpec with Matchers { } "X-Forwarded-For" in { - "X-Forwarded-For: 1.2.3.4" =!= `X-Forwarded-For`("1.2.3.4") - "X-Forwarded-For: 234.123.5.6, 8.8.8.8" =!= `X-Forwarded-For`("234.123.5.6", "8.8.8.8") - "X-Forwarded-For: 1.2.3.4, unknown" =!= `X-Forwarded-For`(RemoteAddress("1.2.3.4"), RemoteAddress.Unknown) - "X-Forwarded-For: 192.0.2.43, 2001:db8:cafe:0:0:0:0:17" =!= `X-Forwarded-For`("192.0.2.43", "2001:db8:cafe::17") - "X-Forwarded-For: 1234:5678:9abc:def1:2345:6789:abcd:ef00" =!= `X-Forwarded-For`("1234:5678:9abc:def1:2345:6789:abcd:ef00") - "X-Forwarded-For: 1234:567:9a:d:2:67:abc:ef00" =!= `X-Forwarded-For`("1234:567:9a:d:2:67:abc:ef00") + "X-Forwarded-For: 1.2.3.4" =!= `X-Forwarded-For`(remoteAddress("1.2.3.4")) + "X-Forwarded-For: 234.123.5.6, 8.8.8.8" =!= `X-Forwarded-For`(remoteAddress("234.123.5.6"), remoteAddress("8.8.8.8")) + "X-Forwarded-For: 1.2.3.4, unknown" =!= `X-Forwarded-For`(remoteAddress("1.2.3.4"), RemoteAddress.Unknown) + "X-Forwarded-For: 192.0.2.43, 2001:db8:cafe:0:0:0:0:17" =!= `X-Forwarded-For`(remoteAddress("192.0.2.43"), remoteAddress("2001:db8:cafe::17")) + "X-Forwarded-For: 1234:5678:9abc:def1:2345:6789:abcd:ef00" =!= `X-Forwarded-For`(remoteAddress("1234:5678:9abc:def1:2345:6789:abcd:ef00")) + "X-Forwarded-For: 1234:567:9a:d:2:67:abc:ef00" =!= `X-Forwarded-For`(remoteAddress("1234:567:9a:d:2:67:abc:ef00")) "X-Forwarded-For: 2001:db8:85a3::8a2e:370:7334" =!=> "2001:db8:85a3:0:0:8a2e:370:7334" - "X-Forwarded-For: 1:2:3:4:5:6:7:8" =!= `X-Forwarded-For`("1:2:3:4:5:6:7:8") + "X-Forwarded-For: 1:2:3:4:5:6:7:8" =!= `X-Forwarded-For`(remoteAddress("1:2:3:4:5:6:7:8")) "X-Forwarded-For: ::2:3:4:5:6:7:8" =!=> "0:2:3:4:5:6:7:8" "X-Forwarded-For: ::3:4:5:6:7:8" =!=> "0:0:3:4:5:6:7:8" "X-Forwarded-For: ::4:5:6:7:8" =!=> "0:0:0:4:5:6:7:8" @@ -574,6 +575,44 @@ class HttpHeaderSpec extends FreeSpec with Matchers { "X-Forwarded-For: 1:2:3:4:5::7:8" =!=> "1:2:3:4:5:0:7:8" "X-Forwarded-For: 1:2:3:4:5:6::8" =!=> "1:2:3:4:5:6:0:8" "X-Forwarded-For: ::" =!=> "0:0:0:0:0:0:0:0" + "X-Forwarded-For: 1.2.3.4, akka.io" =!= + ErrorInfo( + "Illegal HTTP header 'X-Forwarded-For': Invalid input 'k', expected HEXDIG, h8, ':', ch16o or cc (line 1, column 11)", + "1.2.3.4, akka.io\n ^") + } + + "X-Real-Ip" in { + "X-Real-Ip: 1.2.3.4" =!= `X-Real-Ip`(remoteAddress("1.2.3.4")) + "X-Real-Ip: 2001:db8:cafe:0:0:0:0:17" =!= `X-Real-Ip`(remoteAddress("2001:db8:cafe:0:0:0:0:17")) + "X-Real-Ip: 1234:5678:9abc:def1:2345:6789:abcd:ef00" =!= `X-Real-Ip`(remoteAddress("1234:5678:9abc:def1:2345:6789:abcd:ef00")) + "X-Real-Ip: 1234:567:9a:d:2:67:abc:ef00" =!= `X-Real-Ip`(remoteAddress("1234:567:9a:d:2:67:abc:ef00")) + "X-Real-Ip: 2001:db8:85a3::8a2e:370:7334" =!=> "2001:db8:85a3:0:0:8a2e:370:7334" + "X-Real-Ip: 1:2:3:4:5:6:7:8" =!= `X-Real-Ip`(remoteAddress("1:2:3:4:5:6:7:8")) + "X-Real-Ip: ::2:3:4:5:6:7:8" =!=> "0:2:3:4:5:6:7:8" + "X-Real-Ip: ::3:4:5:6:7:8" =!=> "0:0:3:4:5:6:7:8" + "X-Real-Ip: ::4:5:6:7:8" =!=> "0:0:0:4:5:6:7:8" + "X-Real-Ip: ::5:6:7:8" =!=> "0:0:0:0:5:6:7:8" + "X-Real-Ip: ::6:7:8" =!=> "0:0:0:0:0:6:7:8" + "X-Real-Ip: ::7:8" =!=> "0:0:0:0:0:0:7:8" + "X-Real-Ip: ::8" =!=> "0:0:0:0:0:0:0:8" + "X-Real-Ip: 1:2:3:4:5:6:7::" =!=> "1:2:3:4:5:6:7:0" + "X-Real-Ip: 1:2:3:4:5:6::" =!=> "1:2:3:4:5:6:0:0" + "X-Real-Ip: 1:2:3:4:5::" =!=> "1:2:3:4:5:0:0:0" + "X-Real-Ip: 1:2:3:4::" =!=> "1:2:3:4:0:0:0:0" + "X-Real-Ip: 1:2:3::" =!=> "1:2:3:0:0:0:0:0" + "X-Real-Ip: 1:2::" =!=> "1:2:0:0:0:0:0:0" + "X-Real-Ip: 1::" =!=> "1:0:0:0:0:0:0:0" + "X-Real-Ip: 1::3:4:5:6:7:8" =!=> "1:0:3:4:5:6:7:8" + "X-Real-Ip: 1:2::4:5:6:7:8" =!=> "1:2:0:4:5:6:7:8" + "X-Real-Ip: 1:2:3::5:6:7:8" =!=> "1:2:3:0:5:6:7:8" + "X-Real-Ip: 1:2:3:4::6:7:8" =!=> "1:2:3:4:0:6:7:8" + "X-Real-Ip: 1:2:3:4:5::7:8" =!=> "1:2:3:4:5:0:7:8" + "X-Real-Ip: 1:2:3:4:5:6::8" =!=> "1:2:3:4:5:6:0:8" + "X-Real-Ip: ::" =!=> "0:0:0:0:0:0:0:0" + "X-Real-Ip: akka.io" =!= + ErrorInfo( + "Illegal HTTP header 'X-Real-Ip': Invalid input 'k', expected HEXDIG, h8, ':', ch16o or cc (line 1, column 2)", + "akka.io\n ^") } "RawHeader" in { @@ -663,4 +702,6 @@ class HttpHeaderSpec extends FreeSpec with Matchers { val info = result.errors.head fail(s"Input `${header.header}` failed to parse:\n${info.summary}\n${info.detail}") } + + def remoteAddress(ip: String) = RemoteAddress(InetAddress.getByName(ip)) } diff --git a/akka-http-core/src/test/scala/akka/http/scaladsl/model/headers/HeaderSpec.scala b/akka-http-core/src/test/scala/akka/http/scaladsl/model/headers/HeaderSpec.scala index b652ea6b43..b8a6d3e8f1 100644 --- a/akka-http-core/src/test/scala/akka/http/scaladsl/model/headers/HeaderSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/scaladsl/model/headers/HeaderSpec.scala @@ -6,7 +6,7 @@ package akka.http.scaladsl.model.headers import akka.http.impl.util._ import org.scalatest._ - +import java.net.InetAddress import akka.http.scaladsl.model._ class HeaderSpec extends FreeSpec with Matchers { @@ -99,7 +99,8 @@ class HeaderSpec extends FreeSpec with Matchers { `Transfer-Encoding`(TransferEncodings.chunked), Upgrade(Vector(UpgradeProtocol("HTTP", Some("2.0")))), `User-Agent`("Akka HTTP Client 2.4"), - `X-Forwarded-For`(RemoteAddress("192.168.0.1"))) + `X-Forwarded-For`(RemoteAddress(InetAddress.getByName("192.168.0.1"))), + `X-Real-Ip`(RemoteAddress(InetAddress.getByName("192.168.1.1")))) requestHeaders.foreach { header ⇒ header shouldBe 'renderInRequests diff --git a/akka-http-tests/src/test/java/akka/http/javadsl/server/values/RequestValTest.java b/akka-http-tests/src/test/java/akka/http/javadsl/server/values/RequestValTest.java index a903a39ad2..4247a8afc6 100644 --- a/akka-http-tests/src/test/java/akka/http/javadsl/server/values/RequestValTest.java +++ b/akka-http-tests/src/test/java/akka/http/javadsl/server/values/RequestValTest.java @@ -13,8 +13,11 @@ import akka.http.javadsl.server.RequestVals; import akka.http.javadsl.server.Unmarshallers; import akka.http.javadsl.testkit.JUnitRouteTest; import akka.http.javadsl.testkit.TestRoute; + import org.junit.Test; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.util.regex.Pattern; public class RequestValTest extends JUnitRouteTest { @@ -62,16 +65,16 @@ public class RequestValTest extends JUnitRouteTest { } @Test - public void testClientIpExtraction() { + public void testClientIpExtraction() throws UnknownHostException{ TestRoute route = testRoute(completeWithValueToString(RequestVals.clientIP())); route - .run(HttpRequest.create().addHeader(XForwardedFor.create(RemoteAddress.create("127.0.0.2")))) + .run(HttpRequest.create().addHeader(XForwardedFor.create(RemoteAddress.create(InetAddress.getByName("127.0.0.2"))))) .assertStatusCode(200) .assertEntity("127.0.0.2"); route - .run(HttpRequest.create().addHeader(akka.http.javadsl.model.headers.RemoteAddress.create(RemoteAddress.create("127.0.0.3")))) + .run(HttpRequest.create().addHeader(akka.http.javadsl.model.headers.RemoteAddress.create(RemoteAddress.create(InetAddress.getByName("127.0.0.3"))))) .assertStatusCode(200) .assertEntity("127.0.0.3"); diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/MiscDirectivesSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/MiscDirectivesSpec.scala index 9af72926c7..b7ccd234a3 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/MiscDirectivesSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/MiscDirectivesSpec.scala @@ -10,22 +10,23 @@ import scala.concurrent.duration._ import scala.util.Try import akka.http.scaladsl.model._ import headers._ +import java.net.InetAddress class MiscDirectivesSpec extends RoutingSpec { "the extractClientIP directive" should { "extract from a X-Forwarded-For header" in { - Get() ~> addHeaders(`X-Forwarded-For`("2.3.4.5"), RawHeader("x-real-ip", "1.2.3.4")) ~> { + Get() ~> addHeaders(`X-Forwarded-For`(remoteAddress("2.3.4.5")), RawHeader("x-real-ip", "1.2.3.4")) ~> { extractClientIP { echoComplete } } ~> check { responseAs[String] shouldEqual "2.3.4.5" } } "extract from a Remote-Address header" in { - Get() ~> addHeaders(RawHeader("x-real-ip", "1.2.3.4"), `Remote-Address`(RemoteAddress("5.6.7.8"))) ~> { + Get() ~> addHeaders(`X-Real-Ip`(remoteAddress("1.2.3.4")), `Remote-Address`(remoteAddress("5.6.7.8"))) ~> { extractClientIP { echoComplete } } ~> check { responseAs[String] shouldEqual "5.6.7.8" } } "extract from a X-Real-IP header" in { - Get() ~> addHeader(RawHeader("x-real-ip", "1.2.3.4")) ~> { + Get() ~> addHeader(`X-Real-Ip`(remoteAddress("1.2.3.4"))) ~> { extractClientIP { echoComplete } } ~> check { responseAs[String] shouldEqual "1.2.3.4" } } @@ -85,4 +86,6 @@ class MiscDirectivesSpec extends RoutingSpec { } } } + + def remoteAddress(ip: String) = RemoteAddress(InetAddress.getByName(ip)) } diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/MiscDirectives.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/MiscDirectives.scala index 5071a7ac41..bd18ea4d50 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/MiscDirectives.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/MiscDirectives.scala @@ -66,7 +66,7 @@ object MiscDirectives extends MiscDirectives { private val _extractClientIP: Directive1[RemoteAddress] = headerValuePF { case `X-Forwarded-For`(Seq(address, _*)) ⇒ address } | headerValuePF { case `Remote-Address`(address) ⇒ address } | - headerValuePF { case h if h.is("x-real-ip") ⇒ RemoteAddress(h.value) } + headerValuePF { case `X-Real-Ip`(address) ⇒ address } private val _requestEntityEmpty: Directive0 = extract(_.request.entity.isKnownEmpty).flatMap(if (_) pass else reject)