11package controllers
22
3- import java .io .File
43import javax .crypto .Mac
54import javax .crypto .spec .SecretKeySpec
65import javax .inject .{Named , Inject }
76
87import akka .actor .ActorRef
98import akka .pattern .{AskTimeoutException , ask }
10- import doc .{ DocVersions , DocRenderer }
9+ import doc .DocRenderer
1110import play .api .libs .MimeTypes
1211import play .api .libs .concurrent .Execution .Implicits .defaultContext
13- import play .api .libs .iteratee .{Enumerator , Iteratee }
12+ import play .api .libs .iteratee .Iteratee
13+ import play .api .libs .json .{JsError , JsSuccess , Json , JsPath }
1414import play .api .mvc ._
1515import play .twirl .api .Html
1616import settings .Settings
1717
18+ import scala .collection .mutable .ArrayBuffer
1819import scala .concurrent .Future
1920
2021object Application {
2122
2223 private [controllers] object MacBodyParser {
23- def apply (hmacHeader : String , secret : SecretKeySpec , algorithm : String ) =
24- new MacBodyParser (hmacHeader, secret, algorithm)
24+ def apply (hmacHeader : String , secret : SecretKeySpec , algorithm : String , maxBodySize : Int = 8192 ) =
25+ new MacBodyParser (hmacHeader, secret, algorithm, maxBodySize )
2526 }
2627
2728 private [controllers] class MacBodyParser (
2829 hmacHeader : String ,
2930 secret : SecretKeySpec ,
30- algorithm : String ) extends BodyParser [Unit ] {
31+ algorithm : String ,
32+ maxBodySize : Int ) extends BodyParser [Array [Byte ]] {
3133
3234 def hex2bytes (hex : String ): Array [Byte ] =
3335 hex.replaceAll(" [^0-9A-Fa-f]" , " " ).sliding(2 , 2 ).toArray.map(Integer .parseInt(_, 16 ).toByte)
3436
35- override def apply (request : RequestHeader ): Iteratee [Array [Byte ], Either [Result , Unit ]] = {
37+ override def apply (request : RequestHeader ): Iteratee [Array [Byte ], Either [Result , Array [ Byte ] ]] = {
3638 val hexSignature = request.headers.get(hmacHeader).map(_.dropWhile(_ != '=' ).drop(1 )).getOrElse(" " )
3739 val signature = hex2bytes(hexSignature)
38- Iteratee .fold[Array [Byte ], Mac ] {
40+ Iteratee .fold[Array [Byte ], ( Mac , ArrayBuffer [ Byte ]) ] {
3941 val mac = Mac .getInstance(algorithm)
4042 mac.init(secret)
41- mac
42- } { (mac, bytes) =>
43- mac.update(bytes)
44- mac
43+ (mac, ArrayBuffer .empty)
44+ } {
45+ case ((mac, buffer), bytes) =>
46+ mac.update(bytes)
47+ val newBuffer = if (buffer.length + bytes.length <= maxBodySize) buffer ++ bytes else buffer
48+ (mac, newBuffer)
4549 }.map {
46- case _ if signature.isEmpty => Left (Results .BadRequest (s " No $hmacHeader header present " ))
47- case mac if mac.doFinal().sameElements(signature) => Right (() )
48- case _ => Left (Results .Unauthorized (" Bad signature" ))
50+ case _ if signature.isEmpty => Left (Results .BadRequest (s " No $hmacHeader header present " ))
51+ case ( mac, buffer) if mac.doFinal().sameElements(signature) => Right (buffer.toArray )
52+ case _ => Left (Results .Unauthorized (" Bad signature" ))
4953 }
5054 }
5155 }
5256
5357 private def getDocRenderer (
5458 host : String ,
55- docRenderers : Map [String , ActorRef ],
59+ pathVersion : String => Option [String ],
60+ docRenderers : Map [String , Map [String , ActorRef ]],
5661 hostPrefixAliases : Map [String , String ]): Option [ActorRef ] = {
62+
5763 val hostPrefix = host.takeWhile(c => c != '.' && c != ':' )
58- docRenderers.get(hostPrefix).orElse {
59- hostPrefixAliases.get(hostPrefix) match {
60- case Some (aliasedHostPrefix) => docRenderers.get(aliasedHostPrefix)
61- case None => None
62- }
63- }
64+
65+ val resolvedHostPrefix = if (docRenderers.contains(hostPrefix)) Some (hostPrefix) else hostPrefixAliases.get(hostPrefix)
66+
67+ for {
68+ hp <- resolvedHostPrefix
69+ dv <- docRenderers.get(hp)
70+ pv <- pathVersion(hp)
71+ dr <- dv.get(pv)
72+ } yield dr
6473 }
6574}
6675
6776class Application @ Inject () (
68- @ Named (" ConductRDocRenderer" ) conductrDocRenderer : ActorRef ,
77+ @ Named (" ConductRDocRenderer10" ) conductrDocRenderer10 : ActorRef ,
78+ @ Named (" ConductRDocRenderer11" ) conductrDocRenderer11 : ActorRef ,
6979 settings : Settings ) extends Controller {
7080
7181 import Application ._
7282
73- private final val MacAlgorithm = " HmacSHA1"
74- private final val GitHubSignature = " X-Hub-Signature"
75-
76- private val docRenderers = Map (" conductr" -> conductrDocRenderer)
77-
78- private val secret = new SecretKeySpec (settings.play.crypto.secret.getBytes, MacAlgorithm )
79-
8083 def renderIndex = Action {
8184 Ok (views.html.conductr.index())
8285 }
8386
84- def renderDocsHome =
85- renderDocs(" " )
87+ def renderDocsHome ( version : String ) =
88+ renderDocs(" " , version )
8689
8790 def renderResources (path : String , version : String ) =
88- renderDocs(path)
91+ renderDocs(path, version )
8992
90- def renderDocs (path : String , version : String = DocVersions . Latest ) = Action .async { request =>
93+ def renderDocs (path : String , version : String ) = Action .async { request =>
9194 request.headers.get(HOST ) match {
9295 case Some (host) =>
93- getDocRenderer(host, docRenderers, settings.application.hostAliases) match {
96+ getDocRenderer(host, _ => Some (version), docRenderers, settings.application.hostAliases) match {
9497 case Some (docRenderer) =>
9598 docRenderer
9699 .ask(DocRenderer .Render (path))(settings.doc.renderer.timeout)
97100 .map {
98101 case html : Html => Ok (html)
99102 case resource : DocRenderer .Resource => renderResource(resource, path)
100- case DocRenderer .Redirect (rp) => Redirect (routes.Application .renderDocs(rp, DocVersions . Latest ))
103+ case DocRenderer .Redirect (rp, v ) => Redirect (routes.Application .renderDocs(rp, v ))
101104 case DocRenderer .NotFound (rp) => NotFound (s " Cannot find $rp" )
102105 case DocRenderer .NotReady => ServiceUnavailable (" Initializing documentation. Please try again in a minute." )
103106 }
@@ -112,26 +115,58 @@ class Application @Inject() (
112115 }
113116 }
114117
115- private def renderResource (resource : DocRenderer .Resource , path : String ): Result = {
116- val fileName = path.drop(path.lastIndexOf('/' ) + 1 )
117- Result (ResponseHeader (OK , Map [String , String ](
118- CONTENT_LENGTH -> resource.size.toString,
119- CONTENT_TYPE -> MimeTypes .forFileName(fileName).getOrElse(BINARY )
120- )), resource.content)
121- }
122-
123118 def update () = Action (MacBodyParser (GitHubSignature , secret, MacAlgorithm )) { request =>
124119 request.headers.get(HOST ) match {
125120 case Some (host) =>
126- getDocRenderer(host, docRenderers, settings.application.hostAliases) match {
127- case Some (docRenderer) =>
128- docRenderer ! DocRenderer .PropogateGetSite
129- Ok (" Site update requested" )
130- case None =>
131- NotFound (s " Unknown project: $host" )
121+ Json .parse(request.body).validate[String ](webhookRef) match {
122+ case JsSuccess (ref, _) =>
123+ val branch = ref.reverse.takeWhile(_ != '/' ).reverse
124+
125+ def branchToVersion (hostPrefix : String ): Option [String ] =
126+ branchesToVersions.get(hostPrefix).flatMap(_.get(branch))
127+
128+ getDocRenderer(host, branchToVersion, docRenderers, settings.application.hostAliases) match {
129+ case Some (docRenderer) =>
130+ docRenderer ! DocRenderer .PropogateGetSite
131+ Ok (" Site update requested" )
132+ case None =>
133+ Ok (s " Site update requested for Unknown project: $host - ignoring " )
134+ }
135+ case e : JsError =>
136+ BadRequest (s " Cannot parse webhook: $e" )
132137 }
133138 case None =>
134139 NotFound (" No host header" )
135140 }
136141 }
142+
143+ private final val MacAlgorithm = " HmacSHA1"
144+ private final val GitHubSignature = " X-Hub-Signature"
145+
146+ private val docRenderers = Map (
147+ " conductr" -> Map (
148+ " " -> conductrDocRenderer10,
149+ " 1.0.x" -> conductrDocRenderer10,
150+ " 1.1.x" -> conductrDocRenderer11
151+ )
152+ )
153+
154+ private val branchesToVersions = Map (
155+ " conductr" -> Map (
156+ " 1.0" -> " 1.0.x" ,
157+ " master" -> " 1.1.x"
158+ )
159+ )
160+
161+ private val secret = new SecretKeySpec (settings.play.crypto.secret.getBytes, MacAlgorithm )
162+ private val webhookRef = (JsPath \ " ref" ).read[String ]
163+
164+ private def renderResource (resource : DocRenderer .Resource , path : String ): Result = {
165+ val fileName = path.drop(path.lastIndexOf('/' ) + 1 )
166+ Result (ResponseHeader (OK , Map [String , String ](
167+ CONTENT_LENGTH -> resource.size.toString,
168+ CONTENT_TYPE -> MimeTypes .forFileName(fileName).getOrElse(BINARY )
169+ )), resource.content)
170+ }
171+
137172}
0 commit comments