diff --git a/akka-docs/rst/java/code/docs/http/javadsl/server/directives/HeaderDirectivesExamplesTest.java b/akka-docs/rst/java/code/docs/http/javadsl/server/directives/HeaderDirectivesExamplesTest.java index beac154880..e274a0ef9f 100644 --- a/akka-docs/rst/java/code/docs/http/javadsl/server/directives/HeaderDirectivesExamplesTest.java +++ b/akka-docs/rst/java/code/docs/http/javadsl/server/directives/HeaderDirectivesExamplesTest.java @@ -13,12 +13,13 @@ import akka.http.javadsl.model.HttpRequest; import akka.http.javadsl.model.StatusCodes; import akka.http.javadsl.model.headers.Host; import akka.http.javadsl.model.headers.HttpOrigin; +import akka.http.javadsl.model.headers.HttpOriginRange; import akka.http.javadsl.model.headers.Origin; import akka.http.javadsl.model.headers.RawHeader; -import akka.http.javadsl.server.Rejections; import akka.http.javadsl.server.Route; import akka.http.javadsl.testkit.JUnitRouteTest; import akka.japi.JavaPartialFunction; +import akka.http.javadsl.testkit.TestRoute; import scala.PartialFunction; public class HeaderDirectivesExamplesTest extends JUnitRouteTest { @@ -227,4 +228,34 @@ public class HeaderDirectivesExamplesTest extends JUnitRouteTest { .assertEntity("The port was not provided explicitly"); //#optionalHeaderValuePF } + + @Test + public void testCheckSameOrigin() { + //#checkSameOrigin + final HttpOrigin validOriginHeader = + HttpOrigin.create("http://localhost", Host.create("8080")); + + final HttpOriginRange validOriginRange = HttpOriginRange.create(validOriginHeader); + + final TestRoute route = testRoute( + checkSameOrigin(validOriginRange, + () -> complete("Result"))); + + route + .run(HttpRequest.create().addHeader(Origin.create(validOriginHeader))) + .assertStatusCode(StatusCodes.OK) + .assertEntity("Result"); + + route + .run(HttpRequest.create()) + .assertStatusCode(StatusCodes.BAD_REQUEST); + + final HttpOrigin invalidOriginHeader = + HttpOrigin.create("http://invalid.com", Host.create("8080")); + + route + .run(HttpRequest.create().addHeader(Origin.create(invalidOriginHeader))) + .assertStatusCode(StatusCodes.FORBIDDEN); + //#checkSameOrigin + } } diff --git a/akka-docs/rst/java/http/routing-dsl/directives/alphabetically.rst b/akka-docs/rst/java/http/routing-dsl/directives/alphabetically.rst index 2773555667..93a10fbd12 100644 --- a/akka-docs/rst/java/http/routing-dsl/directives/alphabetically.rst +++ b/akka-docs/rst/java/http/routing-dsl/directives/alphabetically.rst @@ -19,6 +19,7 @@ Directive Description :ref:`-authorizeAsync-java-` Applies the given asynchronous authorization check to the request :ref:`-cancelRejection-java-` Adds a ``TransformationRejection`` cancelling all rejections equal to the given one to the rejections potentially coming back from the inner route. :ref:`-cancelRejections-java-` Adds a ``TransformationRejection`` cancelling all matching rejections to the rejections potentially coming back from the inner route +:ref:`-checkSameOrigin-java-` Checks that the request comes from the same origin :ref:`-complete-java-` Completes the request using the given arguments :ref:`-completeOrRecoverWith-java-` "Unwraps" a ``CompletionStage`` and runs the inner route when the future has failed with the error as an extraction of type ``Throwable`` :ref:`-completeWith-java-` Uses the marshaller for a given type to extract a completion function diff --git a/akka-docs/rst/java/http/routing-dsl/directives/header-directives/checkSameOrigin.rst b/akka-docs/rst/java/http/routing-dsl/directives/header-directives/checkSameOrigin.rst new file mode 100644 index 0000000000..6d1de6df73 --- /dev/null +++ b/akka-docs/rst/java/http/routing-dsl/directives/header-directives/checkSameOrigin.rst @@ -0,0 +1,17 @@ +.. _-checkSameOrigin-java-: + +checkSameOrigin +=============== + +Description +----------- +Checks that request comes from the same origin. Extracts the ``Origin`` header value and verifies that allowed range +contains the obtained value. In the case of absent of the ``Origin`` header rejects with a ``MissingHeaderRejection``. +If the origin value is not in the allowed range rejects with an ``InvalidOriginHeaderRejection`` +and ``StatusCodes.FORBIDDEN`` status. + +Example +------- +Checking the ``Origin`` header: + +.. includecode:: ../../../../code/docs/http/javadsl/server/directives/HeaderDirectivesExamplesTest.java#checkSameOrigin diff --git a/akka-docs/rst/java/http/routing-dsl/directives/header-directives/index.rst b/akka-docs/rst/java/http/routing-dsl/directives/header-directives/index.rst index c4dee3dc8c..8461fdc3cb 100644 --- a/akka-docs/rst/java/http/routing-dsl/directives/header-directives/index.rst +++ b/akka-docs/rst/java/http/routing-dsl/directives/header-directives/index.rst @@ -17,3 +17,4 @@ response headers use one of the :ref:`RespondWithDirectives-java`. optionalHeaderValueByName optionalHeaderValueByType optionalHeaderValuePF + checkSameOrigin diff --git a/akka-docs/rst/scala/code/docs/http/scaladsl/server/directives/HeaderDirectivesExamplesSpec.scala b/akka-docs/rst/scala/code/docs/http/scaladsl/server/directives/HeaderDirectivesExamplesSpec.scala index 0284c627f8..714273846b 100644 --- a/akka-docs/rst/scala/code/docs/http/scaladsl/server/directives/HeaderDirectivesExamplesSpec.scala +++ b/akka-docs/rst/scala/code/docs/http/scaladsl/server/directives/HeaderDirectivesExamplesSpec.scala @@ -4,13 +4,11 @@ package docs.http.scaladsl.server.directives +import akka.http.scaladsl.model.StatusCodes._ import akka.http.scaladsl.model._ -import akka.http.scaladsl.server.MissingHeaderRejection -import akka.http.scaladsl.server.Route -import akka.http.scaladsl.server.util.ClassMagnet +import akka.http.scaladsl.model.headers._ +import akka.http.scaladsl.server.{ InvalidOriginRejection, MissingHeaderRejection, Route } import docs.http.scaladsl.server.RoutingSpec -import headers._ -import StatusCodes._ import org.scalatest.Inside class HeaderDirectivesExamplesSpec extends RoutingSpec with Inside { @@ -186,4 +184,38 @@ class HeaderDirectivesExamplesSpec extends RoutingSpec with Inside { responseAs[String] shouldEqual "No Origin header found." } } + "checkSameOrigin-0" in { + val correctOrigin = HttpOrigin("http://localhost:8080") + val route = checkSameOrigin(HttpOriginRange(correctOrigin)) { + complete("Result") + } + + // tests: + // handle request with correct origin headers + Get("abc") ~> Origin(correctOrigin) ~> route ~> check { + status shouldEqual StatusCodes.OK + responseAs[String] shouldEqual "Result" + } + + // reject request with missed origin header + Get("abc") ~> route ~> check { + inside(rejection) { + case MissingHeaderRejection(headerName) ⇒ headerName shouldEqual Origin.name + } + } + + // rejects request with invalid origin headers + val invalidHttpOrigin = HttpOrigin("http://invalid.com") + val invalidOriginHeader = Origin(invalidHttpOrigin) + Get("abc") ~> invalidOriginHeader ~> route ~> check { + inside(rejection) { + case InvalidOriginRejection(invalidOrigins) ⇒ + invalidOrigins shouldEqual Seq(invalidHttpOrigin) + } + } + Get("abc") ~> invalidOriginHeader ~> Route.seal(route) ~> check { + status shouldEqual StatusCodes.Forbidden + responseAs[String] should include(s"${invalidHttpOrigin.value}") + } + } } diff --git a/akka-docs/rst/scala/http/routing-dsl/directives/alphabetically.rst b/akka-docs/rst/scala/http/routing-dsl/directives/alphabetically.rst index f4204a5a67..7e718bff58 100644 --- a/akka-docs/rst/scala/http/routing-dsl/directives/alphabetically.rst +++ b/akka-docs/rst/scala/http/routing-dsl/directives/alphabetically.rst @@ -29,6 +29,7 @@ Directive Description given one to the rejections potentially coming back from the inner route. :ref:`-cancelRejections-` Adds a ``TransformationRejection`` cancelling all matching rejections to the rejections potentially coming back from the inner route +:ref:`-checkSameOrigin-` Checks that the request comes from the same origin :ref:`-complete-` Completes the request using the given arguments :ref:`-completeOrRecoverWith-` "Unwraps" a ``Future[T]`` and runs the inner route when the future has failed with the error as an extraction of type ``Throwable`` diff --git a/akka-docs/rst/scala/http/routing-dsl/directives/header-directives/checkSameOrigin.rst b/akka-docs/rst/scala/http/routing-dsl/directives/header-directives/checkSameOrigin.rst new file mode 100644 index 0000000000..cd8cafcddc --- /dev/null +++ b/akka-docs/rst/scala/http/routing-dsl/directives/header-directives/checkSameOrigin.rst @@ -0,0 +1,23 @@ +.. _-checkSameOrigin-: + +checkSameOrigin +=============== + +Signature +--------- + +.. includecode2:: /../../akka-http/src/main/scala/akka/http/scaladsl/server/directives/HeaderDirectives.scala + :snippet: checkSameOrigin + +Description +----------- +Checks that request comes from the same origin. Extracts the ``Origin`` header value and verifies that allowed range +contains the obtained value. In the case of absent of the ``Origin`` header rejects with a ``MissingHeaderRejection``. +If the origin value is not in the allowed range rejects with an ``InvalidOriginHeaderRejection`` +and ``StatusCodes.Forbidden`` status. + +Example +------- + +.. includecode2:: ../../../../code/docs/http/scaladsl/server/directives/HeaderDirectivesExamplesSpec.scala + :snippet: checkSameOrigin-0 diff --git a/akka-docs/rst/scala/http/routing-dsl/directives/header-directives/index.rst b/akka-docs/rst/scala/http/routing-dsl/directives/header-directives/index.rst index 0e08cd0350..78feab239c 100644 --- a/akka-docs/rst/scala/http/routing-dsl/directives/header-directives/index.rst +++ b/akka-docs/rst/scala/http/routing-dsl/directives/header-directives/index.rst @@ -17,3 +17,4 @@ response headers use one of the :ref:`RespondWithDirectives`. optionalHeaderValueByName optionalHeaderValueByType optionalHeaderValuePF + checkSameOrigin diff --git a/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/HeaderDirectivesTest.java b/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/HeaderDirectivesTest.java index 79176309b9..8fe7fac43f 100644 --- a/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/HeaderDirectivesTest.java +++ b/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/HeaderDirectivesTest.java @@ -182,4 +182,28 @@ public class HeaderDirectivesTest extends JUnitRouteTest { .assertStatusCode(StatusCodes.BAD_REQUEST); } + @Test + public void testCheckSameOrigin() { + final HttpOrigin validOriginHeader = HttpOrigin.create("http://localhost", Host.create("8080")); + + final HttpOriginRange validOriginRange = HttpOriginRange.create(validOriginHeader); + + TestRoute route = testRoute(checkSameOrigin(validOriginRange, () -> complete("Result"))); + + route + .run(HttpRequest.create().addHeader(Origin.create(validOriginHeader))) + .assertStatusCode(StatusCodes.OK) + .assertEntity("Result"); + + route + .run(HttpRequest.create()) + .assertStatusCode(StatusCodes.BAD_REQUEST); + + final HttpOrigin invalidOriginHeader = HttpOrigin.create("http://invalid.com", Host.create("8080")); + + route + .run(HttpRequest.create().addHeader(Origin.create(invalidOriginHeader))) + .assertStatusCode(StatusCodes.FORBIDDEN); + } + } diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/HeaderDirectivesSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/HeaderDirectivesSpec.scala index b8cf7698e5..4fe55462cc 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/HeaderDirectivesSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/HeaderDirectivesSpec.scala @@ -159,4 +159,37 @@ class HeaderDirectivesSpec extends RoutingSpec with Inside { } } } + + "The checkSameOrigin directive" should { + val correctOrigin = HttpOrigin("http://localhost:8080") + val route = checkSameOrigin(HttpOriginRange(correctOrigin)) { + complete("Result") + } + "handle request with correct origin headers" in { + Get("abc") ~> Origin(correctOrigin) ~> route ~> check { + status shouldEqual StatusCodes.OK + responseAs[String] shouldEqual "Result" + } + } + "reject request with missed origin header" in { + Get("abc") ~> route ~> check { + inside(rejection) { + case MissingHeaderRejection(headerName) ⇒ headerName shouldEqual Origin.name + } + } + } + "reject requests with invalid origin header value" in { + val invalidHttpOrigin = HttpOrigin("http://invalid.com") + val invalidOriginHeader = Origin(invalidHttpOrigin) + Get("abc") ~> invalidOriginHeader ~> route ~> check { + inside(rejection) { + case InvalidOriginRejection(invalidOrigins) ⇒ invalidOrigins shouldEqual Seq(invalidHttpOrigin) + } + } + Get("abc") ~> invalidOriginHeader ~> Route.seal(route) ~> check { + status shouldEqual StatusCodes.Forbidden + responseAs[String] should include(s"${invalidHttpOrigin.value}") + } + } + } } diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/Rejections.scala b/akka-http/src/main/scala/akka/http/javadsl/server/Rejections.scala index d396444b4f..6be926f852 100644 --- a/akka-http/src/main/scala/akka/http/javadsl/server/Rejections.scala +++ b/akka-http/src/main/scala/akka/http/javadsl/server/Rejections.scala @@ -10,9 +10,7 @@ import akka.http.scaladsl.model import akka.http.scaladsl.server.ContentNegotiator.Alternative import akka.http.scaladsl.server._ import akka.http.javadsl.model._ -import akka.http.javadsl.model.headers.HttpEncoding -import akka.http.javadsl.model.headers.ByteRange -import akka.http.javadsl.model.headers.HttpChallenge +import akka.http.javadsl.model.headers.{ ByteRange, HttpEncoding, HttpChallenge } import java.util.Optional import java.util.function.{ Function ⇒ JFunction } import java.lang.{ Iterable ⇒ JIterable } @@ -102,6 +100,14 @@ trait MalformedHeaderRejection extends Rejection { def getCause: Optional[Throwable] } +/** + * Rejection created by [[akka.http.scaladsl.server.directives.HeaderDirectives.checkSameOrigin]]. + * Signals that the request was rejected because `Origin` header value is invalid. + */ +trait InvalidOriginRejection extends Rejection { + def getInvalidOrigins: java.util.List[akka.http.javadsl.model.headers.HttpOrigin] +} + /** * Rejection created by unmarshallers. * Signals that the request was rejected because the requests content-type is unsupported. diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/directives/HeaderDirectives.scala b/akka-http/src/main/scala/akka/http/javadsl/server/directives/HeaderDirectives.scala index e0f2fa9b71..c650e700b1 100644 --- a/akka-http/src/main/scala/akka/http/javadsl/server/directives/HeaderDirectives.scala +++ b/akka-http/src/main/scala/akka/http/javadsl/server/directives/HeaderDirectives.scala @@ -11,9 +11,10 @@ import akka.actor.ReflectiveDynamicAccess import scala.compat.java8.OptionConverters import scala.compat.java8.OptionConverters._ import akka.http.impl.util.JavaMapping.Implicits._ -import akka.http.javadsl.model.HttpHeader -import akka.http.javadsl.server.Route -import akka.http.scaladsl.model.headers.{ModeledCustomHeader, ModeledCustomHeaderCompanion} +import akka.http.javadsl.model.{HttpHeader, StatusCodes} +import akka.http.javadsl.model.headers.HttpOriginRange +import akka.http.javadsl.server.{InvalidOriginRejection, MissingHeaderRejection, Route} +import akka.http.scaladsl.model.headers.{ModeledCustomHeader, ModeledCustomHeaderCompanion, Origin} import akka.http.scaladsl.server.directives.{HeaderMagnet, BasicDirectives => B, HeaderDirectives => D} import akka.stream.ActorMaterializer @@ -24,6 +25,18 @@ abstract class HeaderDirectives extends FutureDirectives { private type ScalaHeaderMagnet = HeaderMagnet[akka.http.scaladsl.model.HttpHeader] + /** + * Checks that request comes from the same origin. Extracts the [[Origin]] header value and verifies that + * allowed range contains the obtained value. In the case of absent of the [[Origin]] header rejects + * with [[MissingHeaderRejection]]. If the origin value is not in the allowed range + * rejects with an [[InvalidOriginRejection]] and [[StatusCodes.FORBIDDEN]] status. + * + * @group header + */ + def checkSameOrigin(allowed: HttpOriginRange, inner: jf.Supplier[Route]): Route = RouteAdapter { + D.checkSameOrigin(allowed.asScala) { inner.get().delegate } + } + /** * Extracts an HTTP header value using the given function. If the function result is undefined for all headers the * request is rejected with an empty rejection set. If the given function throws an exception the request is rejected diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/Rejection.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/Rejection.scala index 31d05b1ab8..7a2a266dca 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/Rejection.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/Rejection.scala @@ -20,6 +20,8 @@ import akka.http.impl.util.JavaMapping.Implicits._ import akka.http.javadsl.server.RoutingJavaMapping import RoutingJavaMapping._ import akka.pattern.CircuitBreakerOpenException +import akka.http.javadsl.model.headers.{ HttpOrigin ⇒ JHttpOrigin } +import akka.http.scaladsl.model.headers.{ HttpOrigin ⇒ SHttpOrigin } import scala.collection.JavaConverters._ import scala.compat.java8.OptionConverters @@ -92,6 +94,15 @@ final case class MissingHeaderRejection(headerName: String) final case class MalformedHeaderRejection(headerName: String, errorMsg: String, cause: Option[Throwable] = None) extends jserver.MalformedHeaderRejection with RejectionWithOptionalCause +/** + * Rejection created by [[akka.http.scaladsl.server.directives.HeaderDirectives.checkSameOrigin]]. + * Signals that the request was rejected because `Origin` header value is invalid. + */ +final case class InvalidOriginRejection(invalidOrigins: immutable.Seq[SHttpOrigin]) + extends jserver.InvalidOriginRejection with Rejection { + override def getInvalidOrigins: java.util.List[JHttpOrigin] = invalidOrigins.map(_.asJava).asJava +} + /** * Rejection created by unmarshallers. * Signals that the request was rejected because the requests content-type is unsupported. diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/RejectionHandler.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/RejectionHandler.scala index b8b598ef8c..f816a6d22f 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/RejectionHandler.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/RejectionHandler.scala @@ -4,13 +4,14 @@ package akka.http.scaladsl.server -import scala.annotation.tailrec -import scala.reflect.ClassTag -import scala.collection.immutable -import akka.http.scaladsl.model.headers._ +import akka.http.scaladsl.model.StatusCodes._ import akka.http.scaladsl.model._ -import StatusCodes._ -import AuthenticationFailedRejection._ +import akka.http.scaladsl.model.headers._ +import akka.http.scaladsl.server.AuthenticationFailedRejection._ + +import scala.annotation.tailrec +import scala.collection.immutable +import scala.reflect.ClassTag trait RejectionHandler extends (immutable.Seq[Rejection] ⇒ Option[Route]) { self ⇒ import RejectionHandler._ @@ -164,6 +165,10 @@ object RejectionHandler { case MissingHeaderRejection(headerName) ⇒ complete((BadRequest, "Request is missing required HTTP header '" + headerName + '\'')) } + .handle { + case InvalidOriginRejection(invalidOrigin) ⇒ + complete((Forbidden, s"Invalid `Origin` header values: ${invalidOrigin.mkString(", ")}")) + } .handle { case MissingQueryParamRejection(paramName) ⇒ complete((NotFound, "Request is missing required query parameter '" + paramName + '\'')) diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/HeaderDirectives.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/HeaderDirectives.scala index cf33994316..172b22e4d9 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/HeaderDirectives.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/HeaderDirectives.scala @@ -5,16 +5,12 @@ package akka.http.scaladsl.server package directives -import akka.http.javadsl.model.headers.CustomHeader -import akka.http.scaladsl.model.headers.{ModeledCustomHeaderCompanion, ModeledCustomHeader, RawHeader} +import akka.http.impl.util._ +import akka.http.scaladsl.model._ +import akka.http.scaladsl.model.headers.{ HttpOriginRange, ModeledCustomHeader, ModeledCustomHeaderCompanion, Origin } -import scala.annotation.implicitNotFound import scala.reflect.ClassTag import scala.util.control.NonFatal -import akka.http.javadsl.{ model => jm } -import akka.http.scaladsl.server.util.ClassMagnet -import akka.http.scaladsl.model._ -import akka.http.impl.util._ /** * @groupname header Header directives @@ -24,6 +20,21 @@ trait HeaderDirectives { import BasicDirectives._ import RouteDirectives._ + /** + * Checks that request comes from the same origin. Extracts the [[Origin]] header value and verifies that + * allowed range contains the obtained value. In the case of absent of the [[Origin]] header rejects + * with [[MissingHeaderRejection]]. If the origin value is not in the allowed range + * rejects with an [[InvalidOriginRejection]] and [[StatusCodes.Forbidden]] status. + * + * @group header + */ + def checkSameOrigin(allowed: HttpOriginRange): Directive0 = { + headerValueByType[Origin]().flatMap { origin ⇒ + if (origin.origins.exists(allowed.matches)) pass + else reject(InvalidOriginRejection(origin.origins)) + } + } + /** * Extracts an HTTP header value using the given function. If the function result is undefined for all headers the * request is rejected with an empty rejection set. If the given function throws an exception the request is rejected diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/WebSocketDirectives.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/WebSocketDirectives.scala index 711b3410cd..de382d8788 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/WebSocketDirectives.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/WebSocketDirectives.scala @@ -15,9 +15,9 @@ import akka.stream.scaladsl.Flow * @groupprio websocket 230 */ trait WebSocketDirectives { - import RouteDirectives._ - import HeaderDirectives._ import BasicDirectives._ + import HeaderDirectives._ + import RouteDirectives._ /** * Extract the [[UpgradeToWebSocket]] header if existent. Rejects with an [[ExpectedWebSocketRequestRejection]], otherwise.