integrate xhub4j, fixes #1117

This commit is contained in:
Matthieu Brouillard
2016-02-28 20:15:43 +01:00
parent 6db36e12b5
commit 356202e28a
8 changed files with 74 additions and 45 deletions

View File

@@ -39,6 +39,7 @@ libraryDependencies ++= Seq(
"com.mchange" % "c3p0" % "0.9.5.2", "com.mchange" % "c3p0" % "0.9.5.2",
"com.typesafe" % "config" % "1.3.0", "com.typesafe" % "config" % "1.3.0",
"com.typesafe.akka" %% "akka-actor" % "2.3.14", "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"), "com.enragedginger" %% "akka-quartz-scheduler" % "1.4.0-akka-2.3.x" exclude("c3p0","c3p0"),
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided", "org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided", "javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",

View File

@@ -0,0 +1 @@
ALTER TABLE WEB_HOOK ADD COLUMN TOKEN VARCHAR(100);

View File

@@ -49,11 +49,12 @@ trait RepositorySettingsControllerBase extends ControllerBase {
)(CollaboratorForm.apply) )(CollaboratorForm.apply)
// for web hook url addition // 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( def webHookForm(update:Boolean) = mapping(
"url" -> trim(label("url", text(required, webHook(update)))), "url" -> trim(label("url", text(required, webHook(update)))),
"events" -> webhookEvents "events" -> webhookEvents,
"token" -> optional(trim(label("token", text(maxlength(100)))))
)(WebHookForm.apply) )(WebHookForm.apply)
// for transfer ownership // for transfer ownership
@@ -198,7 +199,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Display the web hook edit page. * Display the web hook edit page.
*/ */
get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository => get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository =>
val webhook = WebHook(repository.owner, repository.name, "") val webhook = WebHook(repository.owner, repository.name, "", null)
html.edithooks(webhook, Set(WebHook.Push), repository, flash.get("info"), true) html.edithooks(webhook, Set(WebHook.Push), repository, flash.get("info"), true)
}) })
@@ -206,7 +207,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Add the web hook URL. * Add the web hook URL.
*/ */
post("/:owner/:repository/settings/hooks/new", webHookForm(false))(ownerOnly { (form, repository) => 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" flash += "info" -> s"Webhook ${form.url} created"
redirect(s"/${repository.owner}/${repository.name}/settings/hooks") redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
}) })
@@ -235,7 +236,8 @@ trait RepositorySettingsControllerBase extends ControllerBase {
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
val url = params("url") 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 dummyPayload = {
val ownerAccount = getAccountByUserName(repository.owner).get val ownerAccount = getAccountByUserName(repository.owner).get
val commits = if(repository.commitCount == 0) List.empty else git.log val commits = if(repository.commitCount == 0) List.empty else git.log
@@ -294,7 +296,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Update web hook settings. * Update web hook settings.
*/ */
post("/:owner/:repository/settings/hooks/edit", webHookForm(true))(ownerOnly { (form, repository) => 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" flash += "info" -> s"webhook ${form.url} updated"
redirect(s"/${repository.owner}/${repository.name}/settings/hooks") redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
}) })

View File

@@ -7,7 +7,8 @@ trait WebHookComponent extends TemplateComponent { self: Profile =>
class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate { class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate {
val url = column[String]("URL") 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) def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
} }
@@ -16,7 +17,8 @@ trait WebHookComponent extends TemplateComponent { self: Profile =>
case class WebHook( case class WebHook(
userName: String, userName: String,
repositoryName: String, repositoryName: String,
url: String url: String,
token: Option[String]
) )
object WebHook { object WebHook {

View File

@@ -1,8 +1,13 @@
package gitbucket.core.service 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.api._
import gitbucket.core.model.{WebHook, Account, Issue, PullRequest, IssueComment, WebHookEvent, CommitComment} import gitbucket.core.model.{WebHook, Account, Issue, PullRequest, IssueComment, WebHookEvent, CommitComment}
import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile._
import org.apache.http.client.utils.URLEncodedUtils
import profile.simple._ import profile.simple._
import gitbucket.core.util.JGitUtil.CommitInfo import gitbucket.core.util.JGitUtil.CommitInfo
import gitbucket.core.util.RepositoryName import gitbucket.core.util.RepositoryName
@@ -33,8 +38,11 @@ trait WebHookService {
/** get All WebHook informations of repository event */ /** get All WebHook informations of repository event */
def getWebHooksByEvent(owner: String, repository: String, event: WebHook.Event)(implicit s: Session): List[WebHook] = 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) WebHooks.filter(_.byRepository(owner, repository))
.list.map(t => WebHook(t.userName, t.repositoryName, t.url)) .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 */ /** get All WebHook information from repository to url */
def getWebHook(owner: String, repository: String, url: String)(implicit s: Session): Option[(WebHook, Set[WebHook.Event])] = def getWebHook(owner: String, repository: String, url: String)(implicit s: Session): Option[(WebHook, Set[WebHook.Event])] =
@@ -44,14 +52,15 @@ trait WebHookService {
.map{ case (w,t) => w -> t.event } .map{ case (w,t) => w -> t.event }
.list.groupBy(_._1).mapValues(_.map(_._2).toSet).headOption .list.groupBy(_._1).mapValues(_.map(_._2).toSet).headOption
def addWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event])(implicit s: Session): Unit = { 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) WebHooks insert WebHook(owner, repository, url, token)
events.toSet.map{ event: WebHook.Event => events.toSet.map{ event: WebHook.Event =>
WebHookEvents insert WebHookEvent(owner, repository, url, 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 WebHookEvents.filter(_.byWebHook(owner, repository, url)).delete
events.toSet.map{ event: WebHook.Event => events.toSet.map{ event: WebHook.Event =>
WebHookEvents insert WebHookEvent(owner, repository, url, event) WebHookEvents insert WebHookEvent(owner, repository, url, event)
@@ -69,17 +78,17 @@ trait WebHookService {
} }
} }
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])] = { (implicit c: JsonFormat.Context): List[(WebHook, String, Future[HttpRequest], Future[HttpResponse])] = {
import org.apache.http.impl.client.HttpClientBuilder import org.apache.http.impl.client.HttpClientBuilder
import ExecutionContext.Implicits.global import ExecutionContext.Implicits.global
import org.apache.http.protocol.HttpContext import org.apache.http.protocol.HttpContext
import org.apache.http.client.methods.HttpPost import org.apache.http.client.methods.HttpPost
if(webHookURLs.nonEmpty){ if(webHooks.nonEmpty){
val json = JsonFormat(payload) val json = JsonFormat(payload)
webHookURLs.map { webHookUrl => webHooks.map { webHook =>
val reqPromise = Promise[HttpRequest] val reqPromise = Promise[HttpRequest]
val f = Future { val f = Future {
val itcp = new org.apache.http.HttpRequestInterceptor{ val itcp = new org.apache.http.HttpRequestInterceptor{
@@ -89,19 +98,26 @@ trait WebHookService {
} }
try{ try{
val httpClient = HttpClientBuilder.create.addInterceptorLast(itcp).build val httpClient = HttpClientBuilder.create.addInterceptorLast(itcp).build
logger.debug(s"start web hook invocation for ${webHookUrl.url}") logger.debug(s"start web hook invocation for ${webHook.url}")
val httpPost = new HttpPost(webHookUrl.url) val httpPost = new HttpPost(webHook.url)
httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded") httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded")
httpPost.addHeader("X-Github-Event", event.name) httpPost.addHeader("X-Github-Event", event.name)
httpPost.addHeader("X-Github-Delivery", java.util.UUID.randomUUID().toString) httpPost.addHeader("X-Github-Delivery", java.util.UUID.randomUUID().toString)
val params: java.util.List[NameValuePair] = new java.util.ArrayList() val params: java.util.List[NameValuePair] = new java.util.ArrayList()
params.add(new BasicNameValuePair("payload", json)) 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) val res = httpClient.execute(httpPost)
httpPost.releaseConnection() httpPost.releaseConnection()
logger.debug(s"end web hook invocation for ${webHookUrl}") logger.debug(s"end web hook invocation for ${webHook}")
res res
}catch{ }catch{
case e:Throwable => { case e:Throwable => {
@@ -113,12 +129,12 @@ trait WebHookService {
} }
} }
f.onSuccess { 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 { 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 { } else {
Nil Nil

View File

@@ -21,6 +21,7 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current GitBucket version. * The history of versions. A head of this sequence is the current GitBucket version.
*/ */
val versions = Seq( val versions = Seq(
new Version(3, 13),
new Version(3, 12), new Version(3, 12),
new Version(3, 11), new Version(3, 11),
new Version(3, 10), new Version(3, 10),

View File

@@ -30,6 +30,11 @@
} }
<button class="btn btn-default" id="test">Test Hook</button> <button class="btn btn-default" id="test">Test Hook</button>
</fieldset> </fieldset>
<fieldset class="form-group">
<label class="strong">Security Token</label>
<div></div>
<input type="text" name="token" id="token" placeholder="leave blank for no X-Hub-Signature usage" value="@webHook.token" class="form-control" style="display: inline; width: 500px; vertical-align: middle;" />
</fieldset>
<hr /> <hr />
<label class="strong">Which events would you like to trigger this webhook?</label> <label class="strong">Which events would you like to trigger this webhook?</label>
<div> <div>
@@ -123,6 +128,7 @@ $(function(){
e.stopImmediatePropagation(); e.stopImmediatePropagation();
e.preventDefault(); e.preventDefault();
var url = this.form.url.value; var url = this.form.url.value;
var token = this.form.token.value;
if(!/^https?:\/\/.+/.test(url)){ if(!/^https?:\/\/.+/.test(url)){
alert("invalid url"); alert("invalid url");
return; return;
@@ -132,7 +138,7 @@ $(function(){
$("#test-report").hide(); $("#test-report").hide();
$.ajax({ $.ajax({
method:'POST', 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){ success: function(e){
//console.log(e); //console.log(e);
$('#test-report-tab a:first').tab('show'); $('#test-report-tab a:first').tab('show');

View File

@@ -16,12 +16,12 @@ class WebHookServiceSpec extends FunSuite with ServiceSpecBase {
val (issue3, pullreq3) = generateNewPullRequest("user3/repo3/master3", "user2/repo2/master2", loginUser="root") val (issue3, pullreq3) = generateNewPullRequest("user3/repo3/master3", "user2/repo2/master2", loginUser="root")
val (issue32, pullreq32) = generateNewPullRequest("user3/repo3/master32", "user2/repo2/master2", loginUser="root") val (issue32, pullreq32) = generateNewPullRequest("user3/repo3/master32", "user2/repo2/master2", loginUser="root")
generateNewPullRequest("user2/repo2/master2", "user1/repo1/master2") generateNewPullRequest("user2/repo2/master2", "user1/repo1/master2")
service.addWebHook("user1", "repo1", "webhook1-1", Set(WebHook.PullRequest)) service.addWebHook("user1", "repo1", "webhook1-1", Set(WebHook.PullRequest), Some("key"))
service.addWebHook("user1", "repo1", "webhook1-2", Set(WebHook.PullRequest)) service.addWebHook("user1", "repo1", "webhook1-2", Set(WebHook.PullRequest), Some("key"))
service.addWebHook("user2", "repo2", "webhook2-1", Set(WebHook.PullRequest)) service.addWebHook("user2", "repo2", "webhook2-1", Set(WebHook.PullRequest), Some("key"))
service.addWebHook("user2", "repo2", "webhook2-2", Set(WebHook.PullRequest)) service.addWebHook("user2", "repo2", "webhook2-2", Set(WebHook.PullRequest), Some("key"))
service.addWebHook("user3", "repo3", "webhook3-1", Set(WebHook.PullRequest)) service.addWebHook("user3", "repo3", "webhook3-1", Set(WebHook.PullRequest), Some("key"))
service.addWebHook("user3", "repo3", "webhook3-2", Set(WebHook.PullRequest)) service.addWebHook("user3", "repo3", "webhook3-2", Set(WebHook.PullRequest), Some("key"))
assert(service.getPullRequestsByRequestForWebhook("user1","repo1","master1") == Map.empty) assert(service.getPullRequestsByRequestForWebhook("user1","repo1","master1") == Map.empty)
@@ -43,33 +43,33 @@ class WebHookServiceSpec extends FunSuite with ServiceSpecBase {
test("add and get and update and delete") { withTestDB { implicit session => test("add and get and update and delete") { withTestDB { implicit session =>
val user1 = generateNewUserWithDBRepository("user1","repo1") val user1 = generateNewUserWithDBRepository("user1","repo1")
service.addWebHook("user1", "repo1", "http://example.com", Set(WebHook.PullRequest)) service.addWebHook("user1", "repo1", "http://example.com", Set(WebHook.PullRequest), Some("key"))
assert(service.getWebHooks("user1", "repo1") == List((WebHook("user1","repo1","http://example.com"),Set(WebHook.PullRequest)))) 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"),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")))) 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.getWebHooksByEvent("user1", "repo1", WebHook.Push) == Nil)
assert(service.getWebHook("user1", "repo1", "http://example.com2") == None) assert(service.getWebHook("user1", "repo1", "http://example.com2") == None)
assert(service.getWebHook("user2", "repo1", "http://example.com") == None) assert(service.getWebHook("user2", "repo1", "http://example.com") == None)
assert(service.getWebHook("user1", "repo2", "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)) 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"),Set(WebHook.Push, WebHook.Issues)))) 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.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") service.deleteWebHook("user1", "repo1", "http://example.com")
assert(service.getWebHook("user1", "repo1", "http://example.com") == None) assert(service.getWebHook("user1", "repo1", "http://example.com") == None)
} } } }
test("getWebHooks, getWebHooksByEvent") { withTestDB { implicit session => test("getWebHooks, getWebHooksByEvent") { withTestDB { implicit session =>
val user1 = generateNewUserWithDBRepository("user1","repo1") val user1 = generateNewUserWithDBRepository("user1","repo1")
service.addWebHook("user1", "repo1", "http://example.com/1", Set(WebHook.PullRequest)) service.addWebHook("user1", "repo1", "http://example.com/1", Set(WebHook.PullRequest), Some("key"))
service.addWebHook("user1", "repo1", "http://example.com/2", Set(WebHook.Push)) 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)) service.addWebHook("user1", "repo1", "http://example.com/3", Set(WebHook.PullRequest,WebHook.Push), Some("key"))
assert(service.getWebHooks("user1", "repo1") == List( assert(service.getWebHooks("user1", "repo1") == List(
WebHook("user1","repo1","http://example.com/1")->Set(WebHook.PullRequest), WebHook("user1","repo1","http://example.com/1", Some("key"))->Set(WebHook.PullRequest),
WebHook("user1","repo1","http://example.com/2")->Set(WebHook.Push), WebHook("user1","repo1","http://example.com/2", Some("key"))->Set(WebHook.Push),
WebHook("user1","repo1","http://example.com/3")->Set(WebHook.PullRequest,WebHook.Push))) WebHook("user1","repo1","http://example.com/3", Some("key"))->Set(WebHook.PullRequest,WebHook.Push)))
assert(service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) == List( assert(service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) == List(
WebHook("user1","repo1","http://example.com/1"), WebHook("user1","repo1","http://example.com/1", Some("key")),
WebHook("user1","repo1","http://example.com/3"))) WebHook("user1","repo1","http://example.com/3", Some("key"))))
} } } }
} }