Skip to content

Instantly share code, notes, and snippets.

@johandahlberg
Created June 23, 2016 17:19
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save johandahlberg/8bde4e6e13bab01c28d38ed0fff9952b to your computer and use it in GitHub Desktop.
Save johandahlberg/8bde4e6e13bab01c28d38ed0fff9952b to your computer and use it in GitHub Desktop.
package controllers
import java.nio.charset.StandardCharsets
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import models.RegisterFacebook
import org.apache.commons.codec.binary.Hex
import play.api.Logger
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import play.api.libs.json._
import play.api.mvc._
import scala.concurrent.Future
case class FacebookPostingMessages(`object`: String, entry: Seq[MessagePostEntry]) {
def messages(): Seq[Message] = {
entry.flatMap(_.messaging).map(_.message)
}
}
case class Correspondent(id: String)
case class MessagePostEntry(id: String, time: Long, messaging: Seq[Messaging])
case class Messaging(sender: Correspondent, recipient: Correspondent, timestamp: Long, message: Message)
case class Message(mid: String, seq: Long, text: String)
object MessageFormats {
implicit val messageFormat = Json.format[Message]
implicit val messageReads = Json.reads[Message]
implicit val correspondentFormat = Json.format[Correspondent]
implicit val correspondentReads = Json.reads[Correspondent]
implicit val messagingFormat = Json.format[Messaging]
implicit val messagingReads = Json.reads[Messaging]
implicit val entryFormat = Json.format[MessagePostEntry]
implicit val entryReads = Json.reads[MessagePostEntry]
implicit val facebookPostingMessagesFormats = Json.format[FacebookPostingMessages]
implicit val facebookPostingMessagesReads = Json.reads[FacebookPostingMessages]
}
class SHA1VerificationService {
private val secret = "<your key>"
private def computeSHA1Hash(payloadBytes: Array[Byte], secret: String): String = {
val HMAC_SHA1_ALGORITHM = "HmacSHA1"
val secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_SHA1_ALGORITHM)
val mac = Mac.getInstance(HMAC_SHA1_ALGORITHM)
mac.init(secretKeySpec)
val result: Array[Byte] = mac.doFinal(payloadBytes)
val computedHash = Hex.encodeHexString(result)
computedHash
}
def verifyPayload(payloadBytes: Array[Byte], expected: String): Boolean = {
val computedHash = computeSHA1Hash(payloadBytes, secret)
Logger.debug(s"Computed hash: $computedHash")
val matched = computedHash == expected
if (!matched)
Logger.warn(s"Payload could not be verified. Computed: $computedHash and expected: $expected")
matched
}
}
class WebhookController() extends Controller {
val verificationService = new SHA1VerificationService()
def withVerifiedPayload[A](action: Action[RawBuffer])= Action.async(parse.raw) { request =>
val xHubSignatureOption = request.headers.get("X-Hub-Signature")
Logger.debug("Attempting to verify payload signature.")
val verified =
for {
signature <- xHubSignatureOption
rawBody <- request.body.asBytes()
} yield {
val bodyBytes = rawBody.utf8String.getBytes
val incomingHash = signature.split("=").last
verificationService.verifyPayload(bodyBytes, incomingHash)
}
if(verified.getOrElse(false)) {
Logger.debug("Succeeded in verifying payload!")
action(request)
}
else {
Logger.warn("Could not verify the payload signature!")
Future { BadRequest("Bad signature!") }
}
}
def rawbuffer2JsValue(rawBuffer: RawBuffer): Option[JsValue] = {
for {
bytes <- rawBuffer.asBytes()
} yield {
Json.parse(bytes.utf8String)
}
}
def receiveFacebookMessages = withVerifiedPayload {
Action (parse.raw) {
request => {
val response =
for {
json <- rawbuffer2JsValue(request.body)
} yield {
import MessageFormats.facebookPostingMessagesReads
val messages = json.validate[FacebookPostingMessages]
messages.fold(
errors => {
Logger.warn("Failed to validate message post...")
BadRequest(Json.obj("status" ->"KO", "message" -> JsError.toJson(errors)))
},
messages => {
Logger.info(s"Successfully parsed ${messages.messages().length} messages.")
Ok
}
)
}
response.getOrElse({
Logger.warn("Invalid request posted.")
BadRequest("Invalid message posting...")
})
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment