Merge pull request #19349 from ktoso/wip-withoutSizeLimit-ktoso
+htc #19237 entity#withSizeLimit for Java + add withoutSizeLimit
This commit is contained in:
commit
b4f367e46a
3 changed files with 172 additions and 79 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue