diff options
Diffstat (limited to 'src/main/scala')
6 files changed, 177 insertions, 0 deletions
diff --git a/src/main/scala/com/getbootstrap/rorschach/server/ActorWithLogging.scala b/src/main/scala/com/getbootstrap/rorschach/server/ActorWithLogging.scala new file mode 100644 index 0000000..c9c3a29 --- /dev/null +++ b/src/main/scala/com/getbootstrap/rorschach/server/ActorWithLogging.scala @@ -0,0 +1,6 @@ +package com.chrisrebert.lmvtfy.server + +import akka.actor.Actor +import akka.actor.ActorLogging + +trait ActorWithLogging extends Actor with ActorLogging diff --git a/src/main/scala/com/getbootstrap/rorschach/server/Boot.scala b/src/main/scala/com/getbootstrap/rorschach/server/Boot.scala new file mode 100644 index 0000000..dce7a3a --- /dev/null +++ b/src/main/scala/com/getbootstrap/rorschach/server/Boot.scala @@ -0,0 +1,43 @@ +package com.chrisrebert.lmvtfy.server + +import scala.concurrent.duration._ +import scala.util.Try +import akka.actor.{ActorSystem, Props} +import akka.io.IO +import spray.can.Http +import akka.pattern.ask +import akka.routing.SmallestMailboxPool +import akka.util.Timeout +import com.chrisrebert.lmvtfy.github.GitHubIssueCommenter + + +object Boot extends App { + val arguments = args.toSeq + val maybePort = arguments match { + case Seq(portStr: String) => { + Try{ portStr.toInt }.toOption + } + case _ => None + } + maybePort match { + case Some(port) => run(port) + case _ => { + System.err.println("USAGE: lmvtfy <port-number>") + System.exit(1) + } + } + + def run(port: Int) { + implicit val system = ActorSystem("on-spray-can") + // import actorSystem.dispatcher + + val commenter = system.actorOf(Props(classOf[GitHubIssueCommenter])) + val localValidator = system.actorOf(Props(classOf[ValidatorSingletonActor], commenter), "validator-service") + val exampleFetcherPool = system.actorOf(SmallestMailboxPool(5).props(Props(classOf[LiveExampleFetcher], localValidator)), "example-fetcher-pool") + val issueCommentEventHandler = system.actorOf(Props(classOf[IssueCommentEventHandler], exampleFetcherPool), "issue-comment-event-handler") + val webService = system.actorOf(Props(classOf[LmvtfyActor], issueCommentEventHandler), "lmvtfy-service") + + implicit val timeout = Timeout(15.seconds) + IO(Http) ? Http.Bind(webService, interface = "0.0.0.0", port = port) + } +} diff --git a/src/main/scala/com/getbootstrap/rorschach/server/GitHubIssuesWebHooksDirectives.scala b/src/main/scala/com/getbootstrap/rorschach/server/GitHubIssuesWebHooksDirectives.scala new file mode 100644 index 0000000..4ab9ca9 --- /dev/null +++ b/src/main/scala/com/getbootstrap/rorschach/server/GitHubIssuesWebHooksDirectives.scala @@ -0,0 +1,23 @@ +package com.chrisrebert.lmvtfy.server + +import com.chrisrebert.lmvtfy.github.{IssueOrCommentEvent, GitHubJsonProtocol} +import scala.util.{Success, Failure, Try} +import spray.json._ +import spray.routing.{Directive1, ValidationRejection} +import spray.routing.directives.{BasicDirectives, RouteDirectives} + +trait GitHubIssuesWebHooksDirectives { + import RouteDirectives.reject + import BasicDirectives.provide + import HubSignatureDirectives.stringEntityMatchingHubSignature + import GitHubJsonProtocol._ + + def authenticatedIssueOrCommentEvent(secretKey: Array[Byte]): Directive1[IssueOrCommentEvent] = stringEntityMatchingHubSignature(secretKey).flatMap{ entityJsonString => + Try{ entityJsonString.parseJson.convertTo[IssueOrCommentEvent] } match { + case Failure(err) => reject(ValidationRejection("JSON either malformed or does not match expected schema!")) + case Success(event) => provide(event) + } + } +} + +object GitHubIssuesWebHooksDirectives extends GitHubIssuesWebHooksDirectives diff --git a/src/main/scala/com/getbootstrap/rorschach/server/HubSignatureDirectives.scala b/src/main/scala/com/getbootstrap/rorschach/server/HubSignatureDirectives.scala new file mode 100644 index 0000000..9de2f15 --- /dev/null +++ b/src/main/scala/com/getbootstrap/rorschach/server/HubSignatureDirectives.scala @@ -0,0 +1,47 @@ +package com.chrisrebert.lmvtfy.server + +import scala.util.{Try,Success,Failure} +import spray.routing.{Directive1, MalformedHeaderRejection, MalformedRequestContentRejection, ValidationRejection} +import spray.routing.directives.{BasicDirectives, HeaderDirectives, RouteDirectives, MarshallingDirectives} +import com.chrisrebert.lmvtfy.util.{HmacSha1,Utf8ByteArray} + +trait HubSignatureDirectives { + + import BasicDirectives.provide + import HeaderDirectives.headerValueByName + import RouteDirectives.reject + import MarshallingDirectives.{entity, as} + + private val xHubSignature = "X-Hub-Signature" + private val hubSignatureHeaderValue = headerValueByName(xHubSignature) + + val hubSignature: Directive1[Array[Byte]] = hubSignatureHeaderValue.flatMap { algoEqHex => + val bytesFromHexOption = algoEqHex.split('=') match { + case Array("sha1", hex) => Try{ javax.xml.bind.DatatypeConverter.parseHexBinary(hex) }.toOption + case _ => None + } + bytesFromHexOption match { + case Some(bytesFromHex) => provide(bytesFromHex) + case None => reject(MalformedHeaderRejection(xHubSignature, "Malformed HMAC")) + } + } + + private val bytesEntity = entity(as[Array[Byte]]) + + def stringEntityMatchingHubSignature(secretKey: Array[Byte]): Directive1[String] = hubSignature.flatMap { signature => + bytesEntity.flatMap { dataBytes => + val hmac = new HmacSha1(mac = signature, secretKey = secretKey, data = dataBytes) + if (hmac.isValid) { + dataBytes.utf8String match { + case Success(string) => provide(string) + case Failure(exc) => reject(MalformedRequestContentRejection("Request body is not valid UTF-8", Some(exc))) + } + } + else { + reject(ValidationRejection("Incorrect HMAC")) + } + } + } +} + +object HubSignatureDirectives extends HubSignatureDirectives diff --git a/src/main/scala/com/getbootstrap/rorschach/server/Settings.scala b/src/main/scala/com/getbootstrap/rorschach/server/Settings.scala new file mode 100644 index 0000000..76b4eb6 --- /dev/null +++ b/src/main/scala/com/getbootstrap/rorschach/server/Settings.scala @@ -0,0 +1,23 @@ +package com.chrisrebert.lmvtfy.server + +import scala.collection.JavaConversions._ +import com.typesafe.config.Config +import akka.actor.ActorSystem +import akka.actor.Extension +import akka.actor.ExtensionId +import akka.actor.ExtensionIdProvider +import akka.actor.ExtendedActorSystem +import akka.util.ByteString +import com.chrisrebert.lmvtfy.util.Utf8String + +class SettingsImpl(config: Config) extends Extension { + val RepoFullNames: Set[String] = config.getStringList("lmvtfy.github-repos-to-watch").toSet + val BotUsername: String = config.getString("lmvtfy.username") + val BotPassword: String = config.getString("lmvtfy.password") + val WebHookSecretKey: ByteString = ByteString(config.getString("lmvtfy.web-hook-secret-key").utf8Bytes) +} +object Settings extends ExtensionId[SettingsImpl] with ExtensionIdProvider { + override def lookup() = Settings + override def createExtension(system: ExtendedActorSystem) = new SettingsImpl(system.settings.config) + override def get(system: ActorSystem): SettingsImpl = super.get(system) +} diff --git a/src/main/scala/com/getbootstrap/rorschach/util/HmacSha1.scala b/src/main/scala/com/getbootstrap/rorschach/util/HmacSha1.scala new file mode 100644 index 0000000..2856fea --- /dev/null +++ b/src/main/scala/com/getbootstrap/rorschach/util/HmacSha1.scala @@ -0,0 +1,35 @@ +package com.chrisrebert.lmvtfy.util + +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import java.security.{NoSuchAlgorithmException, InvalidKeyException, SignatureException} +import java.security.MessageDigest + +object HmacSha1 { + private val HmacSha1Algorithm = "HmacSHA1" + + implicit class HexByteArray(array: Array[Byte]) { + import javax.xml.bind.DatatypeConverter + def asHexBytes: String = DatatypeConverter.printHexBinary(array).toLowerCase + } +} + +case class HmacSha1(mac: Array[Byte], secretKey: Array[Byte], data: Array[Byte]) { + import HmacSha1.HmacSha1Algorithm + import HmacSha1.HexByteArray + + @throws[NoSuchAlgorithmException]("if HMAC-SHA1 is not supported") + @throws[InvalidKeyException]("if the secret key is malformed") + @throws[SignatureException]("under unknown circumstances") + private lazy val correct: Array[Byte] = { + val key = new SecretKeySpec(secretKey, HmacSha1Algorithm) + val mac = Mac.getInstance(HmacSha1Algorithm) + mac.init(key) + mac.doFinal(data) + } + + lazy val isValid: Boolean = MessageDigest.isEqual(mac, correct) + + def givenHex = mac.asHexBytes + def correctHex = correct.asHexBytes +} |