diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/HttpEntity.java b/akka-http-core/src/main/java/akka/http/javadsl/model/HttpEntity.java index a0118953fd..33a083d68f 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/HttpEntity.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/HttpEntity.java @@ -84,6 +84,45 @@ public interface HttpEntity { */ Source getDataBytes(); + /** + * Apply the given size limit to this entity by returning a new entity instance which automatically verifies that the + * data stream encapsulated by this instance produces at most `maxBytes` data bytes. In case this verification fails + * the respective stream will be terminated with an `EntityStreamException` either directly at materialization + * time (if the Content-Length is known) or whenever more data bytes than allowed have been read. + * + * When called on `Strict` entities the method will return the entity itself if the length is within the bound, + * otherwise a `Default` entity with a single element data stream. This allows for potential refinement of the + * entity size limit at a later point (before materialization of the data stream). + * + * By default all message entities produced by the HTTP layer automatically carry the limit that is defined in the + * application's `max-content-length` config setting. If the entity is transformed in a way that changes the + * Content-Length and then another limit is applied then this new limit will be evaluated against the new + * Content-Length. If the entity is transformed in a way that changes the Content-Length and no new limit is applied + * then the previous limit will be applied against the previous Content-Length. + * + * Note that the size limit applied via this method will only have any effect if the `Source` instance contained + * in this entity has been appropriately modified via the `HttpEntity.limitable` method. For all entities created + * by the HTTP layer itself this is always the case, but if you create entities yourself and would like them to + * properly respect limits defined via this method you need to make sure to apply `HttpEntity.limitable` yourself. + */ + HttpEntity withSizeLimit(long maxBytes); + + /** + * Lift the size limit from this entity by returning a new entity instance which skips the size verification. + * + * By default all message entities produced by the HTTP layer automatically carry the limit that is defined in the + * application's `max-content-length` config setting. It is recommended to always keep an upper limit on accepted + * entities to avoid potential attackers flooding you with too large requests/responses, so use this method with caution. + * + * Note that the size limit applied via this method will only have any effect if the `Source` instance contained + * in this entity has been appropriately modified via the `HttpEntity.limitable` method. For all entities created + * by the HTTP layer itself this is always the case, but if you create entities yourself and would like them to + * properly respect limits defined via this method you need to make sure to apply `HttpEntity.limitable` yourself. + * + * See [[withSizeLimit]] for more details. + */ + HttpEntity withoutSizeLimit(); + /** * Returns a future of a strict entity that contains the same data as this entity * which is only completed when the complete entity has been collected. As the diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpEntity.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpEntity.scala index 7c93146b0b..dc89ff58b2 100755 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpEntity.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpEntity.scala @@ -28,7 +28,7 @@ sealed trait HttpEntity extends jm.HttpEntity { /** * Determines whether this entity is known to be empty. */ - def isKnownEmpty: Boolean + override def isKnownEmpty: Boolean /** * The `ContentType` associated with this entity. @@ -74,59 +74,34 @@ sealed trait HttpEntity extends jm.HttpEntity { */ def withContentType(contentType: ContentType): HttpEntity - /** - * Apply the given size limit to this entity by returning a new entity instance which automatically verifies that the - * data stream encapsulated by this instance produces at most `maxBytes` data bytes. In case this verification fails - * the respective stream will be terminated with an `EntityStreamException` either directly at materialization - * time (if the Content-Length is known) or whenever more data bytes than allowed have been read. - * - * When called on `Strict` entities the method will return the entity itself if the length is within the bound, - * otherwise a `Default` entity with a single element data stream. This allows for potential refinement of the - * entity size limit at a later point (before materialization of the data stream). - * - * By default all message entities produced by the HTTP layer automatically carry the limit that is defined in the - * application's `max-content-length` config setting. If the entity is transformed in a way that changes the - * Content-Length and then another limit is applied then this new limit will be evaluated against the new - * Content-Length. If the entity is transformed in a way that changes the Content-Length and no new limit is applied - * then the previous limit will be applied against the previous Content-Length. - * - * Note that the size limit applied via this method will only have any effect if the `Source` instance contained - * in this entity has been appropriately modified via the `HttpEntity.limitable` method. For all entities created - * by the HTTP layer itself this is always the case, but if you create entities yourself and would like them to - * properly respect limits defined via this method you need to make sure to apply `HttpEntity.limitable` yourself. - */ - def withSizeLimit(maxBytes: Long): HttpEntity + /** Java API */ + override def getContentType: jm.ContentType = contentType /** Java API */ - def getContentType: jm.ContentType = contentType - - /** Java API */ - def getDataBytes: stream.javadsl.Source[ByteString, AnyRef] = + override def getDataBytes: stream.javadsl.Source[ByteString, AnyRef] = stream.javadsl.Source.fromGraph(dataBytes.asInstanceOf[Source[ByteString, AnyRef]]) /** Java API */ - def getContentLengthOption: japi.Option[JLong] = + override def getContentLengthOption: japi.Option[JLong] = japi.Option.fromScalaOption(contentLengthOption.asInstanceOf[Option[JLong]]) // Scala autoboxing // default implementations, should be overridden - def isCloseDelimited: Boolean = false - def isIndefiniteLength: Boolean = false - def isDefault: Boolean = false - def isChunked: Boolean = false + override def isCloseDelimited: Boolean = false + override def isIndefiniteLength: Boolean = false + override def isDefault: Boolean = false + override def isChunked: Boolean = false /** Java API */ - def toStrict(timeoutMillis: Long, materializer: Materializer): Future[jm.HttpEntity.Strict] = + override def toStrict(timeoutMillis: Long, materializer: Materializer): Future[jm.HttpEntity.Strict] = toStrict(timeoutMillis.millis)(materializer) } /* An entity that can be used for body parts */ sealed trait BodyPartEntity extends HttpEntity with jm.BodyPartEntity { - def withContentType(contentType: ContentType): BodyPartEntity + override def withContentType(contentType: ContentType): BodyPartEntity - /** - * See [[HttpEntity#withSizeLimit]]. - */ - def withSizeLimit(maxBytes: Long): BodyPartEntity + override def withSizeLimit(maxBytes: Long): BodyPartEntity + override def withoutSizeLimit: BodyPartEntity } /** @@ -225,11 +200,11 @@ object HttpEntity { final case class Strict(contentType: ContentType, data: ByteString) extends jm.HttpEntity.Strict with UniversalEntity { - def contentLength: Long = data.length + override def contentLength: Long = data.length - def isKnownEmpty: Boolean = data.isEmpty + override def isKnownEmpty: Boolean = data.isEmpty - def dataBytes: Source[ByteString, Unit] = Source(data :: Nil) + override def dataBytes: Source[ByteString, Unit] = Source(data :: Nil) override def toStrict(timeout: FiniteDuration)(implicit fm: Materializer) = FastFuture.successful(this) @@ -240,16 +215,16 @@ object HttpEntity { override def transformDataBytes(newContentLength: Long, transformer: Flow[ByteString, ByteString, Any]): UniversalEntity = HttpEntity.Default(contentType, newContentLength, Source.single(data) via transformer) - def withContentType(contentType: ContentType): HttpEntity.Strict = + override def withContentType(contentType: ContentType): HttpEntity.Strict = if (contentType == this.contentType) this else copy(contentType = contentType) - /** - * See [[HttpEntity#withSizeLimit]]. - */ - def withSizeLimit(maxBytes: Long): UniversalEntity = + override def withSizeLimit(maxBytes: Long): UniversalEntity = if (data.length <= maxBytes) this else HttpEntity.Default(contentType, data.length, limitableByteSource(Source.single(data))) withSizeLimit maxBytes + override def withoutSizeLimit: UniversalEntity = + withSizeLimit(SizeLimit.Disabled) + override def productPrefix = "HttpEntity.Strict" override def toString = { @@ -299,12 +274,12 @@ object HttpEntity { def withContentType(contentType: ContentType): HttpEntity.Default = if (contentType == this.contentType) this else copy(contentType = contentType) - /** - * See [[HttpEntity#withSizeLimit]]. - */ - def withSizeLimit(maxBytes: Long): HttpEntity.Default = + override def withSizeLimit(maxBytes: Long): HttpEntity.Default = copy(data = data withAttributes Attributes(SizeLimit(maxBytes, Some(contentLength)))) + override def withoutSizeLimit: HttpEntity.Default = + withSizeLimit(SizeLimit.Disabled) + override def productPrefix = "HttpEntity.Default" /** Java API */ @@ -320,17 +295,17 @@ object HttpEntity { type Self <: HttpEntity.WithoutKnownLength def contentType: ContentType def data: Source[ByteString, Any] - def contentLengthOption: Option[Long] = None - def isKnownEmpty = data eq Source.empty - def dataBytes: Source[ByteString, Any] = data + override def contentLengthOption: Option[Long] = None + override def isKnownEmpty = data eq Source.empty + override def dataBytes: Source[ByteString, Any] = data - /** - * See [[HttpEntity#withSizeLimit]]. - */ - def withSizeLimit(maxBytes: Long): Self = + override def withSizeLimit(maxBytes: Long): Self = withData(data withAttributes Attributes(SizeLimit(maxBytes))) - def transformDataBytes(transformer: Flow[ByteString, ByteString, Any]): Self = + override def withoutSizeLimit: Self = + withData(data withAttributes Attributes(SizeLimit(SizeLimit.Disabled))) + + override def transformDataBytes(transformer: Flow[ByteString, ByteString, Any]): Self = withData(data via transformer) def withData(data: Source[ByteString, Any]): Self @@ -346,10 +321,10 @@ object HttpEntity { type Self = HttpEntity.CloseDelimited override def isCloseDelimited: Boolean = true - def withContentType(contentType: ContentType): HttpEntity.CloseDelimited = + override def withContentType(contentType: ContentType): HttpEntity.CloseDelimited = if (contentType == this.contentType) this else copy(contentType = contentType) - def withData(data: Source[ByteString, Any]): HttpEntity.CloseDelimited = copy(data = data) + override def withData(data: Source[ByteString, Any]): HttpEntity.CloseDelimited = copy(data = data) override def productPrefix = "HttpEntity.CloseDelimited" } @@ -363,10 +338,10 @@ object HttpEntity { type Self = HttpEntity.IndefiniteLength override def isIndefiniteLength: Boolean = true - def withContentType(contentType: ContentType): HttpEntity.IndefiniteLength = + override def withContentType(contentType: ContentType): HttpEntity.IndefiniteLength = if (contentType == this.contentType) this else copy(contentType = contentType) - def withData(data: Source[ByteString, Any]): HttpEntity.IndefiniteLength = copy(data = data) + override def withData(data: Source[ByteString, Any]): HttpEntity.IndefiniteLength = copy(data = data) override def productPrefix = "HttpEntity.IndefiniteLength" } @@ -377,16 +352,19 @@ object HttpEntity { final case class Chunked(contentType: ContentType, chunks: Source[ChunkStreamPart, Any]) extends jm.HttpEntity.Chunked with MessageEntity { - def isKnownEmpty = chunks eq Source.empty - def contentLengthOption: Option[Long] = None + override def isKnownEmpty = chunks eq Source.empty + override def contentLengthOption: Option[Long] = None override def isChunked: Boolean = true - def dataBytes: Source[ByteString, Any] = chunks.map(_.data).filter(_.nonEmpty) + override def dataBytes: Source[ByteString, Any] = chunks.map(_.data).filter(_.nonEmpty) - def withSizeLimit(maxBytes: Long): HttpEntity.Chunked = + override def withSizeLimit(maxBytes: Long): HttpEntity.Chunked = copy(chunks = chunks withAttributes Attributes(SizeLimit(maxBytes))) + override def withoutSizeLimit: HttpEntity.Chunked = + withSizeLimit(SizeLimit.Disabled) + override def transformDataBytes(transformer: Flow[ByteString, ByteString, Any]): HttpEntity.Chunked = { val newData = chunks.map { @@ -494,6 +472,8 @@ object HttpEntity { override def preStart(ctx: LifecycleContext) = ctx.attributes.getFirst[SizeLimit] match { + case Some(limit: SizeLimit) if limit.isDisabled ⇒ + // "no limit" case Some(SizeLimit(bytes, cl @ Some(contentLength))) ⇒ if (contentLength > bytes) throw EntityStreamSizeException(bytes, cl) // else we still count but never throw an error @@ -514,5 +494,10 @@ object HttpEntity { /** * INTERNAL API */ - private case class SizeLimit(maxBytes: Long, contentLength: Option[Long] = None) extends Attributes.Attribute + private final case class SizeLimit(maxBytes: Long, contentLength: Option[Long] = None) extends Attributes.Attribute { + def isDisabled = maxBytes < 0 + } + private object SizeLimit { + val Disabled = -1 // any negative value will do + } } diff --git a/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/RouteDirectivesTest.java b/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/RouteDirectivesTest.java index e4111b7e32..af570fae28 100644 --- a/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/RouteDirectivesTest.java +++ b/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/RouteDirectivesTest.java @@ -4,26 +4,95 @@ package akka.http.javadsl.server.directives; +import akka.dispatch.Futures; +import akka.dispatch.Mapper; import akka.http.javadsl.model.HttpRequest; +import akka.http.javadsl.model.RequestEntity; import akka.http.javadsl.model.StatusCodes; import akka.http.javadsl.model.Uri; import akka.http.javadsl.model.headers.Location; +import akka.http.javadsl.server.RequestContext; +import akka.http.javadsl.server.RouteResult; import akka.http.javadsl.testkit.JUnitRouteTest; import akka.http.javadsl.testkit.TestRoute; +import akka.japi.function.Function; +import akka.stream.javadsl.Sink; +import akka.util.ByteString; import org.junit.Test; +import scala.concurrent.ExecutionContext$; +import scala.concurrent.forkjoin.ForkJoinPool; public class RouteDirectivesTest extends JUnitRouteTest { - @Test - public void testRedirection() { - Uri targetUri = Uri.create("http://example.com"); - TestRoute route = - testRoute( - redirect(targetUri, StatusCodes.FOUND) - ); + @Test + public void testRedirection() { + Uri targetUri = Uri.create("http://example.com"); + TestRoute route = + testRoute( + redirect(targetUri, StatusCodes.FOUND) + ); - route - .run(HttpRequest.create()) - .assertStatusCode(302) - .assertHeaderExists(Location.create(targetUri)); - } + route + .run(HttpRequest.create()) + .assertStatusCode(302) + .assertHeaderExists(Location.create(targetUri)); + } + + @Test + public void testEntitySizeLimit() { + TestRoute route = + testRoute( + path("no-limit") + .route( + handleWith(new Function() { + @Override + public RouteResult apply(final RequestContext ctx) throws Exception { + final RequestEntity entity = ctx.request().entity(); + return ctx.completeWith( + entity + .withoutSizeLimit() + .getDataBytes() + .runWith(Sink.head(), ctx.materializer()) + .map(new Mapper() { + @Override + public RouteResult apply(ByteString s) { + return ctx.complete(s.utf8String()); + } + }, ctx.executionContext())); + } + })), + path("limit-5") + .route( + handleWith(new Function() { + @Override + public RouteResult apply(final RequestContext ctx) throws Exception { + final RequestEntity entity = ctx.request().entity(); + return ctx.completeWith( + entity + .withSizeLimit(5) + .getDataBytes() + .runWith(Sink.head(), ctx.materializer()) + .map(new Mapper() { + @Override + public RouteResult apply(ByteString s) { + return ctx.complete(s.utf8String()); + } + }, ctx.executionContext())); + } + })) + ); + + route + .run(HttpRequest.create("/no-limit").withEntity("1234567890")) + .assertStatusCode(200) + .assertEntity("1234567890"); + + route + .run(HttpRequest.create("/limit-5").withEntity("12345")) + .assertStatusCode(200) + .assertEntity("12345"); + route + .run(HttpRequest.create("/limit-5").withEntity("1234567890")) + .assertStatusCode(500) + .assertEntity("There was an internal server error."); + } }