diff --git a/build.sbt b/build.sbt index 90844e5..3c5e465 100644 --- a/build.sbt +++ b/build.sbt @@ -38,6 +38,7 @@ "com.mchange" % "c3p0" % "0.9.5.2", "com.typesafe" % "config" % "1.3.0", "com.typesafe.akka" %% "akka-actor" % "2.3.14", + "fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.0.0", "com.enragedginger" %% "akka-quartz-scheduler" % "1.4.0-akka-2.3.x" exclude("c3p0","c3p0"), "org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided", "javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided", diff --git a/src/main/resources/update/3_13.sql b/src/main/resources/update/3_13.sql new file mode 100644 index 0000000..8efe1a3 --- /dev/null +++ b/src/main/resources/update/3_13.sql @@ -0,0 +1 @@ +ALTER TABLE WEB_HOOK ADD COLUMN TOKEN VARCHAR(100); \ No newline at end of file diff --git a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala index 2c65df6..7867334 100644 --- a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala @@ -49,11 +49,12 @@ )(CollaboratorForm.apply) // for web hook url addition - case class WebHookForm(url: String, events: Set[WebHook.Event]) + case class WebHookForm(url: String, events: Set[WebHook.Event], token: Option[String]) def webHookForm(update:Boolean) = mapping( "url" -> trim(label("url", text(required, webHook(update)))), - "events" -> webhookEvents + "events" -> webhookEvents, + "token" -> optional(trim(label("token", text(maxlength(100))))) )(WebHookForm.apply) // for transfer ownership @@ -198,7 +199,7 @@ * Display the web hook edit page. */ get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository => - val webhook = WebHook(repository.owner, repository.name, "") + val webhook = WebHook(repository.owner, repository.name, "", None) html.edithooks(webhook, Set(WebHook.Push), repository, flash.get("info"), true) }) @@ -206,7 +207,7 @@ * Add the web hook URL. */ post("/:owner/:repository/settings/hooks/new", webHookForm(false))(ownerOnly { (form, repository) => - addWebHook(repository.owner, repository.name, form.url, form.events) + addWebHook(repository.owner, repository.name, form.url, form.events, form.token) flash += "info" -> s"Webhook ${form.url} created" redirect(s"/${repository.owner}/${repository.name}/settings/hooks") }) @@ -235,7 +236,8 @@ import scala.concurrent.ExecutionContext.Implicits.global val url = params("url") - val dummyWebHookInfo = WebHook(repository.owner, repository.name, url) + val token = Some(params("token")) + val dummyWebHookInfo = WebHook(repository.owner, repository.name, url, token) val dummyPayload = { val ownerAccount = getAccountByUserName(repository.owner).get val commits = if(repository.commitCount == 0) List.empty else git.log @@ -294,7 +296,7 @@ * Update web hook settings. */ post("/:owner/:repository/settings/hooks/edit", webHookForm(true))(ownerOnly { (form, repository) => - updateWebHook(repository.owner, repository.name, form.url, form.events) + updateWebHook(repository.owner, repository.name, form.url, form.events, form.token) flash += "info" -> s"webhook ${form.url} updated" redirect(s"/${repository.owner}/${repository.name}/settings/hooks") }) diff --git a/src/main/scala/gitbucket/core/model/WebHook.scala b/src/main/scala/gitbucket/core/model/WebHook.scala index 3889c00..9be55a8 100644 --- a/src/main/scala/gitbucket/core/model/WebHook.scala +++ b/src/main/scala/gitbucket/core/model/WebHook.scala @@ -7,7 +7,8 @@ class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate { val url = column[String]("URL") - def * = (userName, repositoryName, url) <> ((WebHook.apply _).tupled, WebHook.unapply) + val token = column[Option[String]]("TOKEN", O.Nullable) + def * = (userName, repositoryName, url, token) <> ((WebHook.apply _).tupled, WebHook.unapply) def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind) } @@ -16,7 +17,8 @@ case class WebHook( userName: String, repositoryName: String, - url: String + url: String, + token: Option[String] ) object WebHook { diff --git a/src/main/scala/gitbucket/core/service/WebHookService.scala b/src/main/scala/gitbucket/core/service/WebHookService.scala index d04d90b..4af5ec3 100644 --- a/src/main/scala/gitbucket/core/service/WebHookService.scala +++ b/src/main/scala/gitbucket/core/service/WebHookService.scala @@ -1,8 +1,13 @@ package gitbucket.core.service +import java.io.ByteArrayInputStream + +import fr.brouillard.oss.security.xhub.XHub +import fr.brouillard.oss.security.xhub.XHub.{XHubDigest, XHubConverter} import gitbucket.core.api._ import gitbucket.core.model.{WebHook, Account, Issue, PullRequest, IssueComment, WebHookEvent, CommitComment} import gitbucket.core.model.Profile._ +import org.apache.http.client.utils.URLEncodedUtils import profile.simple._ import gitbucket.core.util.JGitUtil.CommitInfo import gitbucket.core.util.RepositoryName @@ -33,8 +38,11 @@ /** get All WebHook informations of repository event */ def getWebHooksByEvent(owner: String, repository: String, event: WebHook.Event)(implicit s: Session): List[WebHook] = - WebHookEvents.filter(t => t.byRepository(owner, repository) && t.event === event.bind) - .list.map(t => WebHook(t.userName, t.repositoryName, t.url)) + WebHooks.filter(_.byRepository(owner, repository)) + .innerJoin(WebHookEvents).on { (wh, whe) => whe.byWebHook(wh) } + .filter{ case (wh, whe) => whe.event === event.bind} + .map{ case (wh, whe) => wh } + .list.distinct /** get All WebHook information from repository to url */ def getWebHook(owner: String, repository: String, url: String)(implicit s: Session): Option[(WebHook, Set[WebHook.Event])] = @@ -44,14 +52,15 @@ .map{ case (w,t) => w -> t.event } .list.groupBy(_._1).mapValues(_.map(_._2).toSet).headOption - def addWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event])(implicit s: Session): Unit = { - WebHooks insert WebHook(owner, repository, url) + def addWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], token: Option[String])(implicit s: Session): Unit = { + WebHooks insert WebHook(owner, repository, url, token) events.toSet.map{ event: WebHook.Event => WebHookEvents insert WebHookEvent(owner, repository, url, event) } } - def updateWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event])(implicit s: Session): Unit = { + def updateWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], token: Option[String])(implicit s: Session): Unit = { + WebHooks.filter(_.byPrimaryKey(owner, repository, url)).map(w => w.token).update(token) WebHookEvents.filter(_.byWebHook(owner, repository, url)).delete events.toSet.map{ event: WebHook.Event => WebHookEvents insert WebHookEvent(owner, repository, url, event) @@ -69,17 +78,17 @@ } } - def callWebHook(event: WebHook.Event, webHookURLs: List[WebHook], payload: WebHookPayload) + def callWebHook(event: WebHook.Event, webHooks: List[WebHook], payload: WebHookPayload) (implicit c: JsonFormat.Context): List[(WebHook, String, Future[HttpRequest], Future[HttpResponse])] = { import org.apache.http.impl.client.HttpClientBuilder import ExecutionContext.Implicits.global import org.apache.http.protocol.HttpContext import org.apache.http.client.methods.HttpPost - if(webHookURLs.nonEmpty){ + if(webHooks.nonEmpty){ val json = JsonFormat(payload) - webHookURLs.map { webHookUrl => + webHooks.map { webHook => val reqPromise = Promise[HttpRequest] val f = Future { val itcp = new org.apache.http.HttpRequestInterceptor{ @@ -89,19 +98,26 @@ } try{ val httpClient = HttpClientBuilder.create.addInterceptorLast(itcp).build - logger.debug(s"start web hook invocation for ${webHookUrl.url}") - val httpPost = new HttpPost(webHookUrl.url) + logger.debug(s"start web hook invocation for ${webHook.url}") + val httpPost = new HttpPost(webHook.url) httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded") httpPost.addHeader("X-Github-Event", event.name) httpPost.addHeader("X-Github-Delivery", java.util.UUID.randomUUID().toString) val params: java.util.List[NameValuePair] = new java.util.ArrayList() params.add(new BasicNameValuePair("payload", json)) - httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8")) + def postContent = new UrlEncodedFormEntity(params, "UTF-8") + httpPost.setEntity(postContent) + + if (!webHook.token.isEmpty) { + // TODO find a better way and see how to extract content from postContent + val contentAsBytes = URLEncodedUtils.format(params, "UTF-8").getBytes("UTF-8") + httpPost.addHeader("X-Hub-Signature", XHub.generateHeaderXHubToken(XHubConverter.HEXA_LOWERCASE, XHubDigest.SHA1, webHook.token.orNull, contentAsBytes)) + } val res = httpClient.execute(httpPost) httpPost.releaseConnection() - logger.debug(s"end web hook invocation for ${webHookUrl}") + logger.debug(s"end web hook invocation for ${webHook}") res }catch{ case e:Throwable => { @@ -113,12 +129,12 @@ } } f.onSuccess { - case s => logger.debug(s"Success: web hook request to ${webHookUrl.url}") + case s => logger.debug(s"Success: web hook request to ${webHook.url}") } f.onFailure { - case t => logger.error(s"Failed: web hook request to ${webHookUrl.url}", t) + case t => logger.error(s"Failed: web hook request to ${webHook.url}", t) } - (webHookUrl, json, reqPromise.future, f) + (webHook, json, reqPromise.future, f) } } else { Nil diff --git a/src/main/scala/gitbucket/core/servlet/AutoUpdate.scala b/src/main/scala/gitbucket/core/servlet/AutoUpdate.scala index 8cef2e8..ff45a32 100644 --- a/src/main/scala/gitbucket/core/servlet/AutoUpdate.scala +++ b/src/main/scala/gitbucket/core/servlet/AutoUpdate.scala @@ -21,6 +21,7 @@ * The history of versions. A head of this sequence is the current GitBucket version. */ val versions = Seq( + new Version(3, 13), new Version(3, 12), new Version(3, 11), new Version(3, 10), diff --git a/src/main/twirl/gitbucket/core/settings/edithooks.scala.html b/src/main/twirl/gitbucket/core/settings/edithooks.scala.html index ec3384a..fc4f530 100644 --- a/src/main/twirl/gitbucket/core/settings/edithooks.scala.html +++ b/src/main/twirl/gitbucket/core/settings/edithooks.scala.html @@ -30,6 +30,11 @@ } +
+ +
+ +

@@ -123,6 +128,7 @@ e.stopImmediatePropagation(); e.preventDefault(); var url = this.form.url.value; + var token = this.form.token.value; if(!/^https?:\/\/.+/.test(url)){ alert("invalid url"); return; @@ -132,7 +138,7 @@ $("#test-report").hide(); $.ajax({ method:'POST', - url:'@url(repository)/settings/hooks/test?url=' + encodeURIComponent(url), + url:'@url(repository)/settings/hooks/test?url=' + encodeURIComponent(url) + '&token=' + encodeURIComponent(token), success: function(e){ //console.log(e); $('#test-report-tab a:first').tab('show'); diff --git a/src/test/scala/gitbucket/core/service/WebHookServiceSpec.scala b/src/test/scala/gitbucket/core/service/WebHookServiceSpec.scala index 4320dd5..09b1e31 100644 --- a/src/test/scala/gitbucket/core/service/WebHookServiceSpec.scala +++ b/src/test/scala/gitbucket/core/service/WebHookServiceSpec.scala @@ -16,12 +16,12 @@ val (issue3, pullreq3) = generateNewPullRequest("user3/repo3/master3", "user2/repo2/master2", loginUser="root") val (issue32, pullreq32) = generateNewPullRequest("user3/repo3/master32", "user2/repo2/master2", loginUser="root") generateNewPullRequest("user2/repo2/master2", "user1/repo1/master2") - service.addWebHook("user1", "repo1", "webhook1-1", Set(WebHook.PullRequest)) - service.addWebHook("user1", "repo1", "webhook1-2", Set(WebHook.PullRequest)) - service.addWebHook("user2", "repo2", "webhook2-1", Set(WebHook.PullRequest)) - service.addWebHook("user2", "repo2", "webhook2-2", Set(WebHook.PullRequest)) - service.addWebHook("user3", "repo3", "webhook3-1", Set(WebHook.PullRequest)) - service.addWebHook("user3", "repo3", "webhook3-2", Set(WebHook.PullRequest)) + service.addWebHook("user1", "repo1", "webhook1-1", Set(WebHook.PullRequest), Some("key")) + service.addWebHook("user1", "repo1", "webhook1-2", Set(WebHook.PullRequest), Some("key")) + service.addWebHook("user2", "repo2", "webhook2-1", Set(WebHook.PullRequest), Some("key")) + service.addWebHook("user2", "repo2", "webhook2-2", Set(WebHook.PullRequest), Some("key")) + service.addWebHook("user3", "repo3", "webhook3-1", Set(WebHook.PullRequest), Some("key")) + service.addWebHook("user3", "repo3", "webhook3-2", Set(WebHook.PullRequest), Some("key")) assert(service.getPullRequestsByRequestForWebhook("user1","repo1","master1") == Map.empty) @@ -43,33 +43,33 @@ test("add and get and update and delete") { withTestDB { implicit session => val user1 = generateNewUserWithDBRepository("user1","repo1") - service.addWebHook("user1", "repo1", "http://example.com", Set(WebHook.PullRequest)) - assert(service.getWebHooks("user1", "repo1") == List((WebHook("user1","repo1","http://example.com"),Set(WebHook.PullRequest)))) - assert(service.getWebHook("user1", "repo1", "http://example.com") == Some((WebHook("user1","repo1","http://example.com"),Set(WebHook.PullRequest)))) - assert(service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) == List((WebHook("user1","repo1","http://example.com")))) + service.addWebHook("user1", "repo1", "http://example.com", Set(WebHook.PullRequest), Some("key")) + assert(service.getWebHooks("user1", "repo1") == List((WebHook("user1","repo1","http://example.com", Some("key")),Set(WebHook.PullRequest)))) + assert(service.getWebHook("user1", "repo1", "http://example.com") == Some((WebHook("user1","repo1","http://example.com", Some("key")),Set(WebHook.PullRequest)))) + assert(service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) == List((WebHook("user1","repo1","http://example.com", Some("key"))))) assert(service.getWebHooksByEvent("user1", "repo1", WebHook.Push) == Nil) assert(service.getWebHook("user1", "repo1", "http://example.com2") == None) assert(service.getWebHook("user2", "repo1", "http://example.com") == None) assert(service.getWebHook("user1", "repo2", "http://example.com") == None) - service.updateWebHook("user1", "repo1", "http://example.com", Set(WebHook.Push, WebHook.Issues)) - assert(service.getWebHook("user1", "repo1", "http://example.com") == Some((WebHook("user1","repo1","http://example.com"),Set(WebHook.Push, WebHook.Issues)))) + service.updateWebHook("user1", "repo1", "http://example.com", Set(WebHook.Push, WebHook.Issues), Some("key")) + assert(service.getWebHook("user1", "repo1", "http://example.com") == Some((WebHook("user1","repo1","http://example.com", Some("key")),Set(WebHook.Push, WebHook.Issues)))) assert(service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) == Nil) - assert(service.getWebHooksByEvent("user1", "repo1", WebHook.Push) == List((WebHook("user1","repo1","http://example.com")))) + assert(service.getWebHooksByEvent("user1", "repo1", WebHook.Push) == List((WebHook("user1","repo1","http://example.com", Some("key"))))) service.deleteWebHook("user1", "repo1", "http://example.com") assert(service.getWebHook("user1", "repo1", "http://example.com") == None) } } test("getWebHooks, getWebHooksByEvent") { withTestDB { implicit session => val user1 = generateNewUserWithDBRepository("user1","repo1") - service.addWebHook("user1", "repo1", "http://example.com/1", Set(WebHook.PullRequest)) - service.addWebHook("user1", "repo1", "http://example.com/2", Set(WebHook.Push)) - service.addWebHook("user1", "repo1", "http://example.com/3", Set(WebHook.PullRequest,WebHook.Push)) + service.addWebHook("user1", "repo1", "http://example.com/1", Set(WebHook.PullRequest), Some("key")) + service.addWebHook("user1", "repo1", "http://example.com/2", Set(WebHook.Push), Some("key")) + service.addWebHook("user1", "repo1", "http://example.com/3", Set(WebHook.PullRequest,WebHook.Push), Some("key")) assert(service.getWebHooks("user1", "repo1") == List( - WebHook("user1","repo1","http://example.com/1")->Set(WebHook.PullRequest), - WebHook("user1","repo1","http://example.com/2")->Set(WebHook.Push), - WebHook("user1","repo1","http://example.com/3")->Set(WebHook.PullRequest,WebHook.Push))) + WebHook("user1","repo1","http://example.com/1", Some("key"))->Set(WebHook.PullRequest), + WebHook("user1","repo1","http://example.com/2", Some("key"))->Set(WebHook.Push), + WebHook("user1","repo1","http://example.com/3", Some("key"))->Set(WebHook.PullRequest,WebHook.Push))) assert(service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) == List( - WebHook("user1","repo1","http://example.com/1"), - WebHook("user1","repo1","http://example.com/3"))) + WebHook("user1","repo1","http://example.com/1", Some("key")), + WebHook("user1","repo1","http://example.com/3", Some("key")))) } } } \ No newline at end of file