9

Accessing the byte array of a request body is straightforward as long as one uses the appropriate body parsers when defining an Action, like request.body.asRaw....

However, I'm building an ActionBuilder for HMAC-secured Actions right now, where access to the body is inevitable. The Problem is that the definition of ActionBuilders is generic in terms of the request type and thus also of the body parser:

def invokeBlock[A](request: Request[A], block: HmacRequest[A] => Future[SimpleResult])

As A doesn't have any type constraints, there doesn't seem to be any way to get access to the request body from a Request[_].

In my specific case, it would work to do something like:

request.body.asInstanceOf[AnyContentAsJson].json.toString()...

but that isn't an acceptable solution for me.

I also tried defining a custom body parser and applying it to the Request[_], but the results turned out empty.

How do I get access to the body of a Request[_] (a byte array representation would suffice)?


Update: it would also be an acceptable solution if I can get access to the request body within the ActionBuilder, for instance through wrapping the whole processing in another action that does custom parsing. But I don't see how that would work... The solution should be reusable in the sense that arbitrary user-defined actions can be used along with the HMAC-functionality, without interfering with any of the user logic.

4

2 回答 2

2

我们解决这个问题的方法(在 Play 2.3 中)是构建一个 BodyParser,它并行运行 2 个 BodyParsers。使用它,您可以运行 BodyParsers.parse.raw 或除主程序之外的任何其他内容。将原始解析器与验证相结合(此处未显示),并生成一个 Left[Result] 以及您喜欢的任何错误消息和状态,以实现您想要的结果。

import scala.concurrent.ExecutionContext    

import play.api.libs.concurrent.Execution.defaultContext
import play.api.libs.iteratee.Enumeratee
import play.api.libs.iteratee.Iteratee
import play.api.mvc.BodyParser
import play.api.mvc.RequestHeader
import play.api.mvc.Result  

/**
 * A BodyParser which executes any two provided BodyParsers in parallel.
 *
 * The results are combined in the following way:
 * If any wrapped parser's Iteratee encounters an Error, that's the result.
 * Else if the first parser's Iteratee finally yields a Left, this is used as the result.
 * Else if the second parser's Iteratee yields a Left, this is used as the result.
 * Else both Right results are combined in a Right[(A, B)].
 *
 * This can be used to e.g. provide the request's body both as a RawBuffer and a json-parsed
 * custom model class, or to feed the body through a HMAC module in addition to parsing it.
 *
 * @author Jürgen Strobel <juergen@strobel.info>
 */
final case class DualBodyParser[+A, +B](
    a: BodyParser[A],
    b: BodyParser[B]
)(
    implicit ec: ExecutionContext = defaultContext
)
extends BodyParser[(A, B)]
{
    def apply(v1: RequestHeader): Iteratee[Array[Byte], Either[Result, (A, B)]] =
        Enumeratee.zipWith(a(v1), b(v1)) {
            case (Left(va), _) => Left(va)
            case (_, Left(vb)) => Left(vb)
            case (Right(va), Right(vb)) => Right((va, vb))
        }
}

我们还创建了一个自定义的 ActionBuilder 变体,它使用 DualBodyParser 和我们自己的验证逻辑包装任何用户提供的 BodyParser,并且只有在身份验证成功后才将第二个的结果再次拼接到用户提供的代码块中。请注意,ActionBuilder 的这种变体复制并粘贴了大部分原始 ActionBuilder 代码,但需要一个参数来注入我们的身份验证逻辑,因此它是一个类而不是对象。将有不同的方法来解决这个问题。在 BodyParser 中封装完整的身份验证逻辑在 TODO 列表中,并且可能更容易和更好地组合。

/**
 * An FooAuthenticatedAction does Foo authentication before invoking its action block.
 *
 * This replicates ActionBuilder and Action almost fully.
 * It splices a parser.tolerantText BodyParser in, does Foo authentication with the
 * body text, and then continues to call the provided block with the result of the
 * provided body parser (if any).
 *
 * @param fooHelper An FooHelper configured to handle the incoming requests.
 *
 * @author Jürgen Strobel <juergen@strobel.info>
 */
case class FooAuthenticatedAction(fooHelper: FooHelper) extends ActionFunction[Request, Request] {
  self =>

    final def apply[A](bodyParser: BodyParser[A])(block: Request[A] => Result): Action[(String, A)] =
        async(bodyParser) { req: Request[A] =>
            Future.successful(block(req))
        }

    final def apply(block: Request[AnyContent] => Result): Action[(String, AnyContent)] =
        apply(BodyParsers.parse.anyContent)(block)

    final def apply(block: => Result): Action[(String, AnyContent)] =
        apply(_ => block)

    final def async(block: => Future[Result]): Action[(String, AnyContent)] =
        async(_ => block)

    final def async(block: Request[AnyContent] => Future[Result]): Action[(String, AnyContent)] =
        async(BodyParsers.parse.anyContent)(block)

    final def async[A](bodyParser: BodyParser[A])(block: Request[A] => Future[Result]): Action[(String, A)] =
        composeAction(
            new Action[(String, A)] {
                def parser = DualBodyParser(parse.tolerantText, composeParser(bodyParser))
                def apply(request: Request[(String, A)]) = try {
                    fooHelper.authenticate(request map (_._1)) match {
                        case Left(error) => failUnauthorized(error)
                        case Right(_key) => invokeBlock(request map (_._2), block)
                    }
                } catch {
                    case e: NotImplementedError => throw new RuntimeException(e)
                    case e: LinkageError => throw new RuntimeException(e)
                }
                override def executionContext = self.executionContext
            }
        )

    /**
     * This produces the Result if authentication fails.
     */
    def failUnauthorized(error: String): Future[Result] =
        Future.successful( Unauthorized(error) )

    protected def composeParser[A](bodyParser: BodyParser[A]): BodyParser[A] = bodyParser

    protected def composeAction[A](action: Action[A]): Action[A] = action

    // we don't use/support this atm
    /**
      override def andThen[Q[_]](other: ActionFunction[R, Q]): ActionBuilder[Q] = new ActionBuilder[Q] 
    **/

    def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) =
        block(request)
}
于 2015-07-13T12:18:18.987 回答
0

The request class only has got one field for body, when a body parser has parsed the request body successfully, that will lead to an instance of Request[A] being created. Normally it's not interesting to have the raw bytes along with the A instance as this would take double the amount of memory for each request.

A body parser can either continue consuming or return early for each chunk of bytes it is fed with. Maybe you could implement the Hmac validation stuff as a wrapping body parser?

For each chunk of input (Array[Byte]) you would gather up the bytes with the wrapping iteratee/enumeratee. When the end of input comes you trigger the hmac signature calculation/validation on those bytes and can return a BadRequest if it is not valid or push the entire body to the actual body parser.

于 2013-11-13T14:57:16.090 回答