Merge pull request #19349 from ktoso/wip-withoutSizeLimit-ktoso

+htc #19237 entity#withSizeLimit for Java + add withoutSizeLimit
This commit is contained in:
Konrad Malawski 2016-01-05 14:39:51 +01:00
commit b4f367e46a
3 changed files with 172 additions and 79 deletions

View file

@ -84,6 +84,45 @@ public interface HttpEntity {
*/
Source<ByteString, Object> 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

View file

@ -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
}
}