* #20535 add checkSameOrigin directive to WebSocketDirectives * refactoring + add docs * refactoring + cleanup in docs * fix types and conversions in the InvalidOriginHeaderRejection * simplify InvalidOriginHeaderRejection to InvalidOriginRejection
This commit is contained in:
parent
8ba36be6c4
commit
0eda4075ef
16 changed files with 237 additions and 27 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<T>`` 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -17,3 +17,4 @@ response headers use one of the :ref:`RespondWithDirectives-java`.
|
|||
optionalHeaderValueByName
|
||||
optionalHeaderValueByType
|
||||
optionalHeaderValuePF
|
||||
checkSameOrigin
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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``
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -17,3 +17,4 @@ response headers use one of the :ref:`RespondWithDirectives`.
|
|||
optionalHeaderValueByName
|
||||
optionalHeaderValueByType
|
||||
optionalHeaderValuePF
|
||||
checkSameOrigin
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 + '\''))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue