PageRenderTime 59ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/transport/server/play-server/src/main/scala/play/core/server/common/ServerResultUtils.scala

http://github.com/playframework/Play20
Scala | 322 lines | 192 code | 26 blank | 104 comment | 26 complexity | 62bb4ae0f368b2aae6511e10585b2a76 MD5 | raw file
Possible License(s): Apache-2.0
  1. /*
  2. * Copyright (C) Lightbend Inc. <https://www.lightbend.com>
  3. */
  4. package play.core.server.common
  5. import akka.stream.Materializer
  6. import akka.stream.scaladsl.Sink
  7. import akka.util.ByteString
  8. import play.api.Logger
  9. import play.api.mvc._
  10. import play.api.http._
  11. import play.api.http.HeaderNames._
  12. import play.api.http.Status._
  13. import play.api.mvc.request.RequestAttrKey
  14. import play.core.utils.AsciiBitSet
  15. import play.core.utils.AsciiRange
  16. import play.core.utils.AsciiSet
  17. import scala.annotation.tailrec
  18. import scala.concurrent.Future
  19. import scala.util.control.NonFatal
  20. private[play] final class ServerResultUtils(
  21. sessionBaker: SessionCookieBaker,
  22. flashBaker: FlashCookieBaker,
  23. cookieHeaderEncoding: CookieHeaderEncoding
  24. ) {
  25. private val logger = Logger(getClass)
  26. /**
  27. * Determine whether the connection should be closed, and what header, if any, should be added to the response.
  28. */
  29. def determineConnectionHeader(request: RequestHeader, result: Result): ConnectionHeader = {
  30. if (request.version == HttpProtocol.HTTP_1_1) {
  31. if (result.header.headers.get(CONNECTION).exists(_.equalsIgnoreCase(CLOSE))) {
  32. // Close connection, header already exists
  33. DefaultClose
  34. } else if ((result.body.isInstanceOf[HttpEntity.Streamed] && result.body.contentLength.isEmpty)
  35. || request.headers.get(CONNECTION).exists(_.equalsIgnoreCase(CLOSE))) {
  36. // We need to close the connection and set the header
  37. SendClose
  38. } else {
  39. DefaultKeepAlive
  40. }
  41. } else {
  42. if (result.header.headers.get(CONNECTION).exists(_.equalsIgnoreCase(CLOSE))) {
  43. DefaultClose
  44. } else if ((result.body.isInstanceOf[HttpEntity.Streamed] && result.body.contentLength.isEmpty) ||
  45. request.headers.get(CONNECTION).forall(!_.equalsIgnoreCase(KEEP_ALIVE))) {
  46. DefaultClose
  47. } else {
  48. SendKeepAlive
  49. }
  50. }
  51. }
  52. /**
  53. * Validate the result.
  54. *
  55. * Returns the validated result, which may be an error result if validation failed.
  56. */
  57. def validateResult(request: RequestHeader, result: Result, httpErrorHandler: HttpErrorHandler)(
  58. implicit mat: Materializer
  59. ): Future[Result] = {
  60. if (request.version == HttpProtocol.HTTP_1_0 && result.body.isInstanceOf[HttpEntity.Chunked]) {
  61. cancelEntity(result.body)
  62. val exception = new ServerResultException("HTTP 1.0 client does not support chunked response", result, null)
  63. val errorResult: Future[Result] = httpErrorHandler.onServerError(request, exception)
  64. import play.core.Execution.Implicits.trampoline
  65. errorResult.map { originalErrorResult: Result =>
  66. // Update the original error with a new status code and a "Connection: close" header
  67. import originalErrorResult.{ header => h }
  68. val newHeader = h.copy(
  69. status = Status.HTTP_VERSION_NOT_SUPPORTED,
  70. headers = h.headers + (CONNECTION -> CLOSE)
  71. )
  72. originalErrorResult.copy(header = newHeader)
  73. }
  74. } else if (!mayHaveEntity(result.header.status) && !result.body.isKnownEmpty) {
  75. cancelEntity(result.body)
  76. Future.successful(result.copy(body = HttpEntity.Strict(ByteString.empty, result.body.contentType)))
  77. } else {
  78. Future.successful(result)
  79. }
  80. }
  81. /** Set of characters that are allowed in a header name. */
  82. private[this] val allowedHeaderNameChars: AsciiBitSet = {
  83. /*
  84. * From https://tools.ietf.org/html/rfc7230#section-3.2:
  85. * field-name = token
  86. * From https://tools.ietf.org/html/rfc7230#section-3.2.6:
  87. * token = 1*tchar
  88. * tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
  89. * / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
  90. * / DIGIT / ALPHA
  91. */
  92. val TChar = AsciiSet('!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '^', '_', '`', '|',
  93. '~') ||| AsciiSet.Sets.Digit ||| AsciiSet.Sets.Alpha
  94. TChar.toBitSet
  95. }
  96. def validateHeaderNameChars(headerName: String): Unit =
  97. validateString(allowedHeaderNameChars, "header name", headerName)
  98. /** Set of characters that are allowed in a header name. */
  99. private[this] val allowedHeaderValueChars: AsciiBitSet = {
  100. /*
  101. * From https://tools.ietf.org/html/rfc7230#section-3.2:
  102. * field-value = *( field-content / obs-fold )
  103. * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
  104. * field-vchar = VCHAR / obs-text
  105. * From https://tools.ietf.org/html/rfc7230#section-3.2.6:
  106. * obs-text = %x80-FF
  107. */
  108. val ObsText = new AsciiRange(0x80, 0xFF)
  109. val FieldVChar = AsciiSet.Sets.VChar ||| ObsText
  110. val FieldContent = FieldVChar ||| AsciiSet(' ', '\t')
  111. FieldContent.toBitSet
  112. }
  113. def validateHeaderValueChars(headerValue: String): Unit =
  114. validateString(allowedHeaderValueChars, "header value", headerValue)
  115. private def validateString(allowedSet: AsciiBitSet, setDescription: String, string: String): Unit = {
  116. @tailrec def loop(i: Int): Unit = {
  117. if (i < string.length) {
  118. val c = string.charAt(i)
  119. if (!allowedSet.get(c))
  120. throw new IllegalArgumentException(s"Invalid $setDescription character: '$c' (${c.toInt})")
  121. loop(i + 1)
  122. }
  123. }
  124. loop(0)
  125. }
  126. /**
  127. * Handles result conversion in a safe way.
  128. *
  129. * 1. Tries to convert the `Result`.
  130. * 2. If there's an error, calls the `HttpErrorHandler` to get a new
  131. * `Result`, then converts that.
  132. * 3. If there's an error with *that* `Result`, uses the
  133. * `DefaultHttpErrorHandler` to get another `Result`, then converts
  134. * that.
  135. * 4. Hopefully there are no more errors. :)
  136. * 5. If calling an `HttpErrorHandler` throws an exception, then a
  137. * fallback response is returned, without an conversion.
  138. */
  139. def resultConversionWithErrorHandling[R](
  140. requestHeader: RequestHeader,
  141. result: Result,
  142. errorHandler: HttpErrorHandler
  143. )(resultConverter: Result => Future[R])(fallbackResponse: => R): Future[R] = {
  144. import play.core.Execution.Implicits.trampoline
  145. def handleConversionError(conversionError: Throwable): Future[R] = {
  146. try {
  147. // Log some information about the error
  148. if (logger.isErrorEnabled) {
  149. val prettyHeaders =
  150. result.header.headers.map { case (name, value) => s"<$name>: <$value>" }.mkString("[", ", ", "]")
  151. val msg =
  152. s"Exception occurred while converting Result with headers $prettyHeaders. Calling HttpErrorHandler to get alternative Result."
  153. logger.error(msg, conversionError)
  154. }
  155. // Call the HttpErrorHandler to generate an alternative error
  156. errorHandler
  157. .onServerError(
  158. requestHeader,
  159. new ServerResultException("Error converting Play Result for server backend", result, conversionError)
  160. )
  161. .flatMap { errorResult =>
  162. // Convert errorResult using normal conversion logic. This time use
  163. // the DefaultErrorHandler if there are any problems, e.g. if the
  164. // current HttpErrorHandler returns an invalid Result.
  165. resultConversionWithErrorHandling(requestHeader, errorResult, DefaultHttpErrorHandler)(resultConverter)(
  166. fallbackResponse
  167. )
  168. }
  169. } catch {
  170. case NonFatal(onErrorError) =>
  171. // Conservatively handle exceptions thrown by HttpErrorHandlers by
  172. // returning a fallback response.
  173. logger.error("Error occurred during error handling. Original error: ", conversionError)
  174. logger.error("Error occurred during error handling. Error handling error: ", onErrorError)
  175. Future.successful(fallbackResponse)
  176. }
  177. }
  178. try {
  179. // Try to convert the result
  180. resultConverter(result).recoverWith { case t => handleConversionError(t) }
  181. } catch {
  182. case NonFatal(e) => handleConversionError(e)
  183. }
  184. }
  185. /** Whether the given status may have an entity or not. */
  186. def mayHaveEntity(status: Int): Boolean = status match {
  187. case CONTINUE | SWITCHING_PROTOCOLS | NO_CONTENT | NOT_MODIFIED =>
  188. false
  189. case _ =>
  190. true
  191. }
  192. /**
  193. * Cancel the entity.
  194. *
  195. * While theoretically, an Akka streams Source is not supposed to hold resources, in practice, this is very often not
  196. * the case, for example, the response from an Akka HTTP client may have an associated Source that must be consumed
  197. * (or cancelled) before the associated connection can be returned to the connection pool.
  198. */
  199. def cancelEntity(entity: HttpEntity)(implicit mat: Materializer) = {
  200. entity match {
  201. case HttpEntity.Chunked(chunks, _) => chunks.runWith(Sink.cancelled)
  202. case HttpEntity.Streamed(data, _, _) => data.runWith(Sink.cancelled)
  203. case _ =>
  204. }
  205. }
  206. /**
  207. * The connection header logic to use for the result.
  208. */
  209. sealed trait ConnectionHeader {
  210. def willClose: Boolean
  211. def header: Option[String]
  212. }
  213. /**
  214. * A `Connection: keep-alive` header should be sent. Used to
  215. * force an HTTP 1.0 connection to remain open.
  216. */
  217. case object SendKeepAlive extends ConnectionHeader {
  218. override def willClose = false
  219. override def header = Some(KEEP_ALIVE)
  220. }
  221. /**
  222. * A `Connection: close` header should be sent. Used to
  223. * force an HTTP 1.1 connection to close.
  224. */
  225. case object SendClose extends ConnectionHeader {
  226. override def willClose = true
  227. override def header = Some(CLOSE)
  228. }
  229. /**
  230. * No `Connection` header should be sent. Used on an HTTP 1.0
  231. * connection where the default behavior is to close the connection,
  232. * or when the response already has a Connection: close header.
  233. */
  234. case object DefaultClose extends ConnectionHeader {
  235. override def willClose = true
  236. override def header = None
  237. }
  238. /**
  239. * No `Connection` header should be sent. Used on an HTTP 1.1
  240. * connection where the default behavior is to keep the connection
  241. * open.
  242. */
  243. case object DefaultKeepAlive extends ConnectionHeader {
  244. override def willClose = false
  245. override def header = None
  246. }
  247. // Values for the Connection header
  248. private val KEEP_ALIVE = "keep-alive"
  249. private val CLOSE = "close"
  250. /**
  251. * Bake the cookies and prepare the new Set-Cookie header.
  252. */
  253. def prepareCookies(requestHeader: RequestHeader, result: Result): Result = {
  254. val requestHasFlash = requestHeader.attrs.get(RequestAttrKey.Flash) match {
  255. case None =>
  256. // The request didn't have a flash object in it, either because we
  257. // used a custom RequestFactory which didn't install the flash object
  258. // or because there was an error in request processing which caused
  259. // us to bypass the application's RequestFactory. In this case we
  260. // can assume that there is no flash object we need to clear.
  261. false
  262. case Some(flashCell) =>
  263. // The request had a flash object and it was non-empty, so the flash
  264. // cookie value may need to be cleared.
  265. !flashCell.value.isEmpty
  266. }
  267. result.bakeCookies(cookieHeaderEncoding, sessionBaker, flashBaker, requestHasFlash)
  268. }
  269. /**
  270. * Given a map of headers, split it into a sequence of individual headers.
  271. * Most headers map into a single pair in the new sequence. The exception is
  272. * the `Set-Cookie` header which we split into a pair for each cookie it
  273. * contains. This allows us to work around issues with clients that can't
  274. * handle combined headers. (Also RFC6265 says multiple headers shouldn't
  275. * be folded together, which Play's API unfortunately does.)
  276. */
  277. def splitSetCookieHeaders(headers: Map[String, String]): Iterable[(String, String)] = {
  278. if (headers.contains(SET_COOKIE)) {
  279. // Rewrite the headers with Set-Cookie split into separate headers
  280. headers.toSeq.flatMap {
  281. case (SET_COOKIE, value) =>
  282. splitSetCookieHeaderValue(value)
  283. .map { cookiePart =>
  284. SET_COOKIE -> cookiePart
  285. }
  286. case (name, value) =>
  287. Seq((name, value))
  288. }
  289. } else {
  290. // No Set-Cookie header so we can just use the headers as they are
  291. headers
  292. }
  293. }
  294. def splitSetCookieHeaderValue(value: String): Seq[String] =
  295. cookieHeaderEncoding.SetCookieHeaderSeparatorRegex.split(value)
  296. }