Add option to disallow WebHook to private addresses (#2397)

This commit is contained in:
Naoki Takezoe
2019-12-29 16:13:24 +09:00
committed by GitHub
parent 04bc92001f
commit 5257c83563
23 changed files with 246 additions and 94 deletions

View File

@@ -42,6 +42,7 @@ libraryDependencies ++= Seq(
"io.github.gitbucket" % "markedj" % "1.0.16",
"org.apache.commons" % "commons-compress" % "1.19",
"org.apache.commons" % "commons-email" % "1.5",
"commons-net" % "commons-net" % "3.6",
"org.apache.httpcomponents" % "httpclient" % "4.5.10",
"org.apache.sshd" % "apache-sshd" % "2.1.0" exclude ("org.slf4j", "slf4j-jdk14") exclude ("org.apache.sshd", "sshd-mina") exclude ("org.apache.sshd", "sshd-netty"),
"org.apache.tika" % "tika-core" % "1.23",

View File

@@ -526,7 +526,8 @@ trait AccountControllerBase extends AccountManagementControllerBase {
WebHookPushPayload.createDummyPayload(ownerAccount)
}
val (webHook, json, reqFuture, resFuture) = callWebHook(WebHook.Push, List(dummyWebHookInfo), dummyPayload).head
val (webHook, json, reqFuture, resFuture) =
callWebHook(WebHook.Push, List(dummyWebHookInfo), dummyPayload, context.settings).head
val toErrorMap: PartialFunction[Throwable, Map[String, String]] = {
case e: java.net.UnknownHostException => Map("error" -> ("Unknown host " + e.getMessage))

View File

@@ -324,13 +324,14 @@ trait PullRequestsControllerBase extends ControllerBase {
pullreq.branch,
loginAccount,
s"Merge branch '${alias}' into ${pullreq.requestBranch}",
Some(pullreq)
Some(pullreq),
context.settings
) match {
case None => // conflict
flash.update("error", s"Can't automatic merging branch '${alias}' into ${pullreq.requestBranch}.")
case Some(oldId) =>
// update pull request
updatePullRequests(owner, name, pullreq.requestBranch, loginAccount, "synchronize")
updatePullRequests(owner, name, pullreq.requestBranch, loginAccount, "synchronize", context.settings)
flash.update("info", s"Merge branch '${alias}' into ${pullreq.requestBranch}")
}
}
@@ -357,7 +358,15 @@ trait PullRequestsControllerBase extends ControllerBase {
val owner = repository.owner
val name = repository.name
mergePullRequest(repository, issueId, context.loginAccount.get, form.message, form.strategy, form.isDraft) match {
mergePullRequest(
repository,
issueId,
context.loginAccount.get,
form.message,
form.strategy,
form.isDraft,
context.settings
) match {
case Right(objectId) => redirect(s"/${owner}/${name}/pull/${issueId}")
case Left(message) => Some(BadRequest(message))
}
@@ -558,7 +567,8 @@ trait PullRequestsControllerBase extends ControllerBase {
commitIdFrom = form.commitIdFrom,
commitIdTo = form.commitIdTo,
isDraft = form.isDraft,
loginAccount = context.loginAccount.get
loginAccount = context.loginAccount.get,
settings = context.settings
)
// insert labels

View File

@@ -294,7 +294,8 @@ trait RepositorySettingsControllerBase extends ControllerBase {
)
}
val (webHook, json, reqFuture, resFuture) = callWebHook(WebHook.Push, List(dummyWebHookInfo), dummyPayload).head
val (webHook, json, reqFuture, resFuture) =
callWebHook(WebHook.Push, List(dummyWebHookInfo), dummyPayload, context.settings).head
val toErrorMap: PartialFunction[Throwable, Map[String, String]] = {
case e: java.net.UnknownHostException => Map("error" -> ("Unknown host " + e.getMessage))

View File

@@ -354,7 +354,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
path = form.path,
files = files.toIndexedSeq,
message = form.message.getOrElse("Add files via upload"),
loginAccount = context.loginAccount.get
loginAccount = context.loginAccount.get,
settings = context.settings
) {
case (git, headTip, builder, inserter) =>
JGitUtil.processTree(git, headTip) { (path, tree) =>
@@ -441,7 +442,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
charset = form.charset,
message = form.message.getOrElse(s"Create ${form.newFileName}"),
commit = form.commit,
loginAccount = context.loginAccount.get
loginAccount = context.loginAccount.get,
settings = context.settings
)
redirect(
@@ -465,7 +467,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
form.message.getOrElse(s"Rename ${form.oldFileName.get} to ${form.newFileName}")
},
commit = form.commit,
loginAccount = context.loginAccount.get
loginAccount = context.loginAccount.get,
settings = context.settings
)
redirect(
@@ -485,7 +488,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
charset = "",
message = form.message.getOrElse(s"Delete ${form.fileName}"),
commit = form.commit,
loginAccount = context.loginAccount.get
loginAccount = context.loginAccount.get,
settings = context.settings
)
redirect(

View File

@@ -90,17 +90,11 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
)(OIDC.apply)
),
"skinName" -> trim(label("AdminLTE skin name", text(required))),
"showMailAddress" -> trim(label("Show mail address", boolean())) //,
// "pluginNetworkInstall" -> trim(label("Network plugin installation", boolean())),
// "proxy" -> optionalIfNotChecked(
// "useProxy",
// mapping(
// "host" -> trim(label("Proxy host", text(required))),
// "port" -> trim(label("Proxy port", number())),
// "user" -> trim(label("Keystore", optional(text()))),
// "password" -> trim(label("Keystore", optional(text())))
// )(Proxy.apply)
// )
"showMailAddress" -> trim(label("Show mail address", boolean())),
"webhook" -> mapping(
"blockPrivateAddress" -> trim(label("Block private address", boolean())),
"whitelist" -> trim(label("Whitelist", multiLineText()))
)(WebHook.apply)
)(SystemSettings.apply).verifying { settings =>
Vector(
if (settings.ssh.enabled && settings.baseUrl.isEmpty) {
@@ -524,6 +518,17 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
()
})
private def multiLineText(constraints: Constraint*): SingleValueType[Seq[String]] =
new SingleValueType[Seq[String]](constraints: _*) {
def convert(value: String, messages: Messages): Seq[String] = {
if (value == null) {
Nil
} else {
value.split("\n").toIndexedSeq.map(_.trim)
}
}
}
private def members: Constraint = new Constraint() {
override def validate(name: String, value: String, messages: Messages): Option[String] = {
if (value.split(",").exists {

View File

@@ -191,7 +191,7 @@ trait WikiControllerBase extends ControllerBase {
form.pageName,
commitId
)
callWebHookOf(repository.owner, repository.name, WebHook.Gollum) {
callWebHookOf(repository.owner, repository.name, WebHook.Gollum, context.settings) {
getAccountByUserName(repository.owner).map { repositoryUser =>
WebHookGollumPayload("edited", form.pageName, commitId, repository, repositoryUser, loginAccount)
}
@@ -229,7 +229,7 @@ trait WikiControllerBase extends ControllerBase {
commitId =>
updateLastActivityDate(repository.owner, repository.name)
recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName)
callWebHookOf(repository.owner, repository.name, WebHook.Gollum) {
callWebHookOf(repository.owner, repository.name, WebHook.Gollum, context.settings) {
getAccountByUserName(repository.owner).map { repositoryUser =>
WebHookGollumPayload("created", form.pageName, commitId, repository, repositoryUser, loginAccount)
}

View File

@@ -115,7 +115,8 @@ trait ApiPullRequestControllerBase extends ControllerBase {
commitIdFrom = commitIdFrom.getName,
commitIdTo = commitIdTo.getName,
isDraft = false,
loginAccount = context.loginAccount.get
loginAccount = context.loginAccount.get,
settings = context.settings
)
getApiPullRequest(repository, issueId).map(JsonFormat(_))
case _ =>
@@ -143,7 +144,8 @@ trait ApiPullRequestControllerBase extends ControllerBase {
commitIdFrom = commitIdFrom.getName,
commitIdTo = commitIdTo.getName,
isDraft = false,
loginAccount = context.loginAccount.get
loginAccount = context.loginAccount.get,
settings = context.settings
)
getApiPullRequest(repository, createPullReqAlt.issue).map(JsonFormat(_))
case _ =>

View File

@@ -127,17 +127,14 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase {
branch,
path,
Some(paths.last),
if (data.sha.isDefined) {
Some(paths.last)
} else {
None
},
data.sha.map(_ => paths.last),
StringUtil.base64Decode(data.content),
data.message,
commit,
context.loginAccount.get,
data.committer.map(_.name).getOrElse(context.loginAccount.get.fullName),
data.committer.map(_.email).getOrElse(context.loginAccount.get.mailAddress)
data.committer.map(_.email).getOrElse(context.loginAccount.get.mailAddress),
context.settings
)
ApiContents("file", paths.last, path, objectId.name, None, None)(RepositoryName(repository))
}

View File

@@ -94,7 +94,8 @@ trait CommitsService {
repository,
issue,
pullRequest,
loginAccount
loginAccount,
context.settings
)
}
case None =>

View File

@@ -80,16 +80,17 @@ trait HandleCommentService {
// call web hooks
action match {
case None => commentId foreach (callIssueCommentWebHook(repository, issue, _, loginAccount))
case None =>
commentId foreach (callIssueCommentWebHook(repository, issue, _, loginAccount, context.settings))
case Some(act) =>
val webHookAction = act match {
case "close" => "closed"
case "reopen" => "reopened"
}
if (issue.isPullRequest)
callPullRequestWebHook(webHookAction, repository, issue.issueId, loginAccount)
callPullRequestWebHook(webHookAction, repository, issue.issueId, loginAccount, context.settings)
else
callIssuesWebHook(webHookAction, repository, issue, loginAccount)
callIssuesWebHook(webHookAction, repository, issue, loginAccount, context.settings)
}
// call hooks

View File

@@ -57,7 +57,7 @@ trait IssueCreationService {
createReferComment(owner, name, issue, title + " " + body.getOrElse(""), loginAccount)
// call web hooks
callIssuesWebHook("opened", repository, issue, loginAccount)
callIssuesWebHook("opened", repository, issue, loginAccount, context.settings)
// call hooks
PluginRegistry().getIssueHooks.foreach(_.created(issue, repository))

View File

@@ -8,6 +8,7 @@ import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.util.Directory._
import gitbucket.core.util.{JGitUtil, LockUtil}
import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.service.SystemSettingsService.SystemSettings
import org.eclipse.jgit.merge.{MergeStrategy, Merger, RecursiveMerger}
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.transport.RefSpec
@@ -167,7 +168,8 @@ trait MergeService {
remoteBranch: String,
loginAccount: Account,
message: String,
pullreq: Option[PullRequest]
pullreq: Option[PullRequest],
settings: SystemSettings
)(implicit s: Session, c: JsonFormat.Context): Option[ObjectId] = {
val localUserName = localRepository.owner
val localRepositoryName = localRepository.name
@@ -212,7 +214,7 @@ trait MergeService {
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, localUserName, localRepositoryName)
.foreach { issueId =>
getIssue(localRepository.owner, localRepository.name, issueId.toString).foreach { issue =>
callIssuesWebHook("closed", localRepository, issue, loginAccount)
callIssuesWebHook("closed", localRepository, issue, loginAccount, settings)
PluginRegistry().getIssueHooks
.foreach(
_.closedByCommitComment(issue, localRepository, commit.fullMessage, loginAccount)
@@ -223,7 +225,7 @@ trait MergeService {
}
pullreq.foreach { pullreq =>
callWebHookOf(localRepository.owner, localRepository.name, WebHook.Push) {
callWebHookOf(localRepository.owner, localRepository.name, WebHook.Push, settings) {
for {
ownerAccount <- getAccountByUserName(localRepository.owner)
} yield {
@@ -251,7 +253,8 @@ trait MergeService {
loginAccount: Account,
message: String,
strategy: String,
isDraft: Boolean
isDraft: Boolean,
settings: SystemSettings
)(implicit s: Session, c: JsonFormat.Context, context: Context): Either[String, ObjectId] = {
if (!isDraft) {
if (repository.repository.options.mergeOptions.split(",").contains(strategy)) {
@@ -333,7 +336,7 @@ trait MergeService {
repository.name
).foreach { issueId =>
getIssue(repository.owner, repository.name, issueId.toString).foreach { issue =>
callIssuesWebHook("closed", repository, issue, loginAccount)
callIssuesWebHook("closed", repository, issue, loginAccount, context.settings)
PluginRegistry().getIssueHooks
.foreach(_.closedByCommitComment(issue, repository, commit.fullMessage, loginAccount))
}
@@ -347,7 +350,7 @@ trait MergeService {
repository.name
).foreach { issueId =>
getIssue(repository.owner, repository.name, issueId.toString).foreach { issue =>
callIssuesWebHook("closed", repository, issue, loginAccount)
callIssuesWebHook("closed", repository, issue, loginAccount, context.settings)
PluginRegistry().getIssueHooks
.foreach(_.closedByCommitComment(issue, repository, issueContent, loginAccount))
}
@@ -355,16 +358,23 @@ trait MergeService {
closeIssuesFromMessage(message, loginAccount.userName, repository.owner, repository.name)
.foreach { issueId =>
getIssue(repository.owner, repository.name, issueId.toString).foreach { issue =>
callIssuesWebHook("closed", repository, issue, loginAccount)
callIssuesWebHook("closed", repository, issue, loginAccount, context.settings)
PluginRegistry().getIssueHooks
.foreach(_.closedByCommitComment(issue, repository, issueContent, loginAccount))
}
}
}
callPullRequestWebHook("closed", repository, issueId, context.loginAccount.get)
callPullRequestWebHook("closed", repository, issueId, context.loginAccount.get, context.settings)
updatePullRequests(repository.owner, repository.name, pullreq.branch, loginAccount, "closed")
updatePullRequests(
repository.owner,
repository.name,
pullreq.branch,
loginAccount,
"closed",
settings
)
// call hooks
PluginRegistry().getPullRequestHooks.foreach { h =>

View File

@@ -8,6 +8,7 @@ import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.api.JsonFormat
import gitbucket.core.controller.Context
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.SystemSettingsService.SystemSettings
import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.JGitUtil
@@ -106,7 +107,8 @@ trait PullRequestService {
commitIdFrom: String,
commitIdTo: String,
isDraft: Boolean,
loginAccount: Account
loginAccount: Account,
settings: SystemSettings
)(implicit s: Session, context: Context): Unit = {
getIssue(originRepository.owner, originRepository.name, issueId.toString).foreach { baseIssue =>
PullRequests insert PullRequest(
@@ -142,7 +144,7 @@ trait PullRequestService {
)
// call web hook
callPullRequestWebHook("opened", originRepository, issueId, loginAccount)
callPullRequestWebHook("opened", originRepository, issueId, loginAccount, settings)
getIssue(originRepository.owner, originRepository.name, issueId.toString) foreach { issue =>
// extract references and create refer comment
@@ -226,7 +228,14 @@ trait PullRequestService {
/**
* Fetch pull request contents into refs/pull/${issueId}/head and update pull request table.
*/
def updatePullRequests(owner: String, repository: String, branch: String, loginAccount: Account, action: String)(
def updatePullRequests(
owner: String,
repository: String,
branch: String,
loginAccount: Account,
action: String,
settings: SystemSettings
)(
implicit s: Session,
c: JsonFormat.Context
): Unit = {
@@ -275,7 +284,8 @@ trait PullRequestService {
action,
getRepository(owner, repository).get,
pullreq.requestBranch,
loginAccount
loginAccount,
settings
)
}
}

View File

@@ -4,6 +4,7 @@ import gitbucket.core.model.{Account, WebHook}
import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.SystemSettingsService.SystemSettings
import gitbucket.core.service.WebHookService.WebHookPushPayload
import gitbucket.core.util.Directory.getRepositoryDir
import gitbucket.core.util.JGitUtil.CommitInfo
@@ -12,6 +13,7 @@ import org.eclipse.jgit.api.Git
import org.eclipse.jgit.dircache.{DirCache, DirCacheBuilder}
import org.eclipse.jgit.lib._
import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack}
import scala.util.Using
trait RepositoryCommitFileService {
@@ -24,12 +26,13 @@ trait RepositoryCommitFileService {
branch: String,
path: String,
message: String,
loginAccount: Account
loginAccount: Account,
settings: SystemSettings
)(
f: (Git, ObjectId, DirCacheBuilder, ObjectInserter) => Unit
)(implicit s: Session, c: JsonFormat.Context) = {
// prepend path to the filename
_commitFile(repository, branch, message, loginAccount, loginAccount.fullName, loginAccount.mailAddress)(f)
_commitFile(repository, branch, message, loginAccount, loginAccount.fullName, loginAccount.mailAddress, settings)(f)
}
def commitFile(
@@ -42,7 +45,8 @@ trait RepositoryCommitFileService {
charset: String,
message: String,
commit: String,
loginAccount: Account
loginAccount: Account,
settings: SystemSettings
)(implicit s: Session, c: JsonFormat.Context): ObjectId = {
commitFile(
repository,
@@ -55,7 +59,8 @@ trait RepositoryCommitFileService {
commit,
loginAccount,
loginAccount.fullName,
loginAccount.mailAddress
loginAccount.mailAddress,
settings
)
}
@@ -70,7 +75,8 @@ trait RepositoryCommitFileService {
commit: String,
loginAccount: Account,
fullName: String,
mailAddress: String
mailAddress: String,
settings: SystemSettings
)(implicit s: Session, c: JsonFormat.Context): ObjectId = {
val newPath = newFileName.map { newFileName =>
@@ -80,7 +86,7 @@ trait RepositoryCommitFileService {
if (path.length == 0) oldFileName else s"${path}/${oldFileName}"
}
_commitFile(repository, branch, message, loginAccount, fullName, mailAddress) {
_commitFile(repository, branch, message, loginAccount, fullName, mailAddress, settings) {
case (git, headTip, builder, inserter) =>
if (headTip.getName == commit) {
val permission = JGitUtil
@@ -111,7 +117,8 @@ trait RepositoryCommitFileService {
message: String,
loginAccount: Account,
committerName: String,
committerMailAddress: String
committerMailAddress: String,
settings: SystemSettings
)(
f: (Git, ObjectId, DirCacheBuilder, ObjectInserter) => Unit
)(implicit s: Session, c: JsonFormat.Context): ObjectId = {
@@ -165,7 +172,7 @@ trait RepositoryCommitFileService {
refUpdate.update()
// update pull request
updatePullRequests(repository.owner, repository.name, branch, loginAccount, "synchronize")
updatePullRequests(repository.owner, repository.name, branch, loginAccount, "synchronize", settings)
// record activity
val commitInfo = new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
@@ -178,7 +185,7 @@ trait RepositoryCommitFileService {
if (branch == repository.repository.defaultBranch) {
closeIssuesFromMessage(message, committerName, repository.owner, repository.name).foreach { issueId =>
getIssue(repository.owner, repository.name, issueId.toString).foreach { issue =>
callIssuesWebHook("closed", repository, issue, loginAccount)
callIssuesWebHook("closed", repository, issue, loginAccount, settings)
PluginRegistry().getIssueHooks
.foreach(_.closedByCommitComment(issue, repository, message, loginAccount))
}
@@ -191,7 +198,7 @@ trait RepositoryCommitFileService {
}
val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
callWebHookOf(repository.owner, repository.name, WebHook.Push) {
callWebHookOf(repository.owner, repository.name, WebHook.Push, settings) {
getAccountByUserName(repository.owner).map { ownerAccount =>
WebHookPushPayload(
git,

View File

@@ -70,6 +70,8 @@ trait SystemSettingsService {
}
props.setProperty(SkinName, settings.skinName.toString)
props.setProperty(ShowMailAddress, settings.showMailAddress.toString)
props.setProperty(WebHookBlockPrivateAddress, settings.webHook.blockPrivateAddress.toString)
props.setProperty(WebHookWhitelist, settings.webHook.whitelist.mkString("\n"))
Using.resource(new java.io.FileOutputStream(GitBucketConf)) { out =>
props.store(out, null)
@@ -146,7 +148,8 @@ trait SystemSettingsService {
None
},
getValue(props, SkinName, "skin-blue"),
getValue(props, ShowMailAddress, false)
getValue(props, ShowMailAddress, false),
WebHook(getValue(props, WebHookBlockPrivateAddress, false), getSeqValue(props, WebHookWhitelist, ""))
)
}
}
@@ -175,7 +178,8 @@ object SystemSettingsService {
oidcAuthentication: Boolean,
oidc: Option[OIDC],
skinName: String,
showMailAddress: Boolean
showMailAddress: Boolean,
webHook: WebHook
) {
def baseUrl(request: HttpServletRequest): String =
@@ -252,7 +256,7 @@ object SystemSettingsService {
case class SshAddress(host: String, port: Int, genericUser: String)
case class Lfs(serverUrl: Option[String])
case class WebHook(blockPrivateAddress: Boolean, whitelist: Seq[String])
val DefaultSshPort = 29418
val DefaultSmtpPort = 25
@@ -303,6 +307,8 @@ object SystemSettingsService {
private val PluginProxyPort = "plugin.proxy.port"
private val PluginProxyUser = "plugin.proxy.user"
private val PluginProxyPassword = "plugin.proxy.password"
private val WebHookBlockPrivateAddress = "webhook.block_private_address"
private val WebHookWhitelist = "webhook.whitelist"
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = {
getConfigValue(key).getOrElse {
@@ -316,6 +322,16 @@ object SystemSettingsService {
}
}
private def getSeqValue[A: ClassTag](props: java.util.Properties, key: String, default: A): Seq[A] = {
getValue[String](props, key, "").split("\n").toIndexedSeq.map { value =>
if (value == null || value.isEmpty) {
default
} else {
convertType(value).asInstanceOf[A]
}
}
}
private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] = {
getConfigValue(key).orElse {
defining(props.getProperty(key)) { value =>

View File

@@ -3,24 +3,26 @@ package gitbucket.core.service
import fr.brouillard.oss.security.xhub.XHub
import fr.brouillard.oss.security.xhub.XHub.{XHubConverter, XHubDigest}
import gitbucket.core.api._
import gitbucket.core.controller.Context
import gitbucket.core.model.{
Account,
AccountWebHook,
AccountWebHookEvent,
CommitComment,
Issue,
IssueComment,
Label,
PullRequest,
WebHook,
RepositoryWebHook,
RepositoryWebHookEvent,
AccountWebHook,
AccountWebHookEvent
WebHook
}
import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile.blockingApi._
import org.apache.http.client.utils.URLEncodedUtils
import gitbucket.core.util.JGitUtil.CommitInfo
import gitbucket.core.util.{RepositoryName, StringUtil}
import gitbucket.core.util.Implicits._
import gitbucket.core.util.{HttpClientUtil, RepositoryName, StringUtil}
import gitbucket.core.service.RepositoryService.RepositoryInfo
import org.apache.http.NameValuePair
import org.apache.http.client.entity.UrlEncodedFormEntity
@@ -34,6 +36,7 @@ import scala.util.{Failure, Success}
import org.apache.http.HttpRequest
import org.apache.http.HttpResponse
import gitbucket.core.model.WebHookContentType
import gitbucket.core.service.SystemSettingsService.SystemSettings
import org.apache.http.client.entity.EntityBuilder
import org.apache.http.entity.ContentType
@@ -201,20 +204,28 @@ trait WebHookService {
def deleteAccountWebHook(owner: String, url: String)(implicit s: Session): Unit =
AccountWebHooks.filter(_.byPrimaryKey(owner, url)).delete
def callWebHookOf(owner: String, repository: String, event: WebHook.Event)(
def callWebHookOf(owner: String, repository: String, event: WebHook.Event, settings: SystemSettings)(
makePayload: => Option[WebHookPayload]
)(implicit s: Session, c: JsonFormat.Context): Unit = {
val webHooks = getWebHooksByEvent(owner, repository, event)
if (webHooks.nonEmpty) {
makePayload.map(callWebHook(event, webHooks, _))
makePayload.map(callWebHook(event, webHooks, _, settings))
}
val accountWebHooks = getAccountWebHooksByEvent(owner, event)
if (accountWebHooks.nonEmpty) {
makePayload.map(callWebHook(event, accountWebHooks, _))
makePayload.map(callWebHook(event, accountWebHooks, _, settings))
}
}
def callWebHook(event: WebHook.Event, webHooks: List[WebHook], payload: WebHookPayload)(
private def validateTargetAddress(settings: SystemSettings, url: String): Boolean = {
val host = new java.net.URL(url).getHost
!settings.webHook.blockPrivateAddress ||
!HttpClientUtil.isPrivateAddress(host) ||
settings.webHook.whitelist.exists(range => HttpClientUtil.inIpRange(range, host))
}
def callWebHook(event: WebHook.Event, webHooks: List[WebHook], payload: WebHookPayload, settings: SystemSettings)(
implicit c: JsonFormat.Context
): List[(WebHook, String, Future[HttpRequest], Future[HttpResponse])] = {
import org.apache.http.impl.client.HttpClientBuilder
@@ -234,6 +245,9 @@ trait WebHookService {
}
}
try {
if (!validateTargetAddress(settings, webHook.url)) {
throw new IllegalArgumentException(s"Illegal address: ${webHook.url}")
}
val httpClient = HttpClientBuilder.create.useSystemProperties.addInterceptorLast(itcp).build
logger.debug(s"start web hook invocation for ${webHook.url}")
val httpPost = new HttpPost(webHook.url)
@@ -302,7 +316,6 @@ trait WebHookService {
} else {
Nil
}
// logger.debug("end callWebHook")
}
}
@@ -315,9 +328,10 @@ trait WebHookPullRequestService extends WebHookService {
action: String,
repository: RepositoryService.RepositoryInfo,
issue: Issue,
sender: Account
sender: Account,
settings: SystemSettings
)(implicit s: Session, context: JsonFormat.Context): Unit = {
callWebHookOf(repository.owner, repository.name, WebHook.Issues) {
callWebHookOf(repository.owner, repository.name, WebHook.Issues, settings) {
val users =
getAccountsByUserNames(Set(repository.owner, issue.openedUserName) ++ issue.assignedUserName, Set(sender))
for {
@@ -346,10 +360,11 @@ trait WebHookPullRequestService extends WebHookService {
action: String,
repository: RepositoryService.RepositoryInfo,
issueId: Int,
sender: Account
sender: Account,
settings: SystemSettings
)(implicit s: Session, c: JsonFormat.Context): Unit = {
import WebHookService._
callWebHookOf(repository.owner, repository.name, WebHook.PullRequest) {
callWebHookOf(repository.owner, repository.name, WebHook.PullRequest, settings) {
for {
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
users = getAccountsByUserNames(
@@ -408,7 +423,8 @@ trait WebHookPullRequestService extends WebHookService {
action: String,
requestRepository: RepositoryService.RepositoryInfo,
requestBranch: String,
sender: Account
sender: Account,
settings: SystemSettings
)(implicit s: Session, c: JsonFormat.Context): Unit = {
import WebHookService._
for {
@@ -439,7 +455,7 @@ trait WebHookPullRequestService extends WebHookService {
mergedComment = getMergedComment(baseRepo.owner, baseRepo.name, issue.issueId)
)
callWebHook(WebHook.PullRequest, webHooks, payload)
callWebHook(WebHook.PullRequest, webHooks, payload, settings)
}
}
@@ -453,10 +469,11 @@ trait WebHookPullRequestReviewCommentService extends WebHookService {
repository: RepositoryService.RepositoryInfo,
issue: Issue,
pullRequest: PullRequest,
sender: Account
sender: Account,
settings: SystemSettings
)(implicit s: Session, c: JsonFormat.Context): Unit = {
import WebHookService._
callWebHookOf(repository.owner, repository.name, WebHook.PullRequestReviewComment) {
callWebHookOf(repository.owner, repository.name, WebHook.PullRequestReviewComment, settings) {
val users =
getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
for {
@@ -498,9 +515,10 @@ trait WebHookIssueCommentService extends WebHookPullRequestService {
repository: RepositoryService.RepositoryInfo,
issue: Issue,
issueCommentId: Int,
sender: Account
sender: Account,
settings: SystemSettings
)(implicit s: Session, c: JsonFormat.Context): Unit = {
callWebHookOf(repository.owner, repository.name, WebHook.IssueComment) {
callWebHookOf(repository.owner, repository.name, WebHook.IssueComment, settings) {
for {
issueComment <- getComment(repository.owner, repository.name, issueCommentId.toString())
users = getAccountsByUserNames(

View File

@@ -239,7 +239,8 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
with MilestonesService
with WebHookPullRequestService
with WebHookPullRequestReviewCommentService
with CommitsService {
with CommitsService
with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
private var existIds: Seq[String] = Nil
@@ -269,6 +270,8 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
}
def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
val settings = loadSystemSettings()
Database() withTransaction { implicit session =>
try {
Using.resource(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
@@ -317,7 +320,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
getAccountByUserName(pusher).foreach { pusherAccount =>
closeIssuesFromMessage(commit.fullMessage, pusher, owner, repository).foreach { issueId =>
getIssue(owner, repository, issueId.toString).foreach { issue =>
callIssuesWebHook("closed", repositoryInfo, issue, pusherAccount)
callIssuesWebHook("closed", repositoryInfo, issue, pusherAccount, settings)
PluginRegistry().getIssueHooks
.foreach(_.closedByCommitComment(issue, repositoryInfo, commit.fullMessage, pusherAccount))
}
@@ -337,7 +340,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
}.isDefined) {
markMergeAndClosePullRequest(pusher, owner, repository, pull)
getAccountByUserName(pusher).foreach { pusherAccount =>
callPullRequestWebHook("closed", repositoryInfo, pull.issueId, pusherAccount)
callPullRequestWebHook("closed", repositoryInfo, pull.issueId, pusherAccount, settings)
}
}
}
@@ -365,14 +368,14 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
case ReceiveCommand.Type.CREATE | ReceiveCommand.Type.UPDATE |
ReceiveCommand.Type.UPDATE_NONFASTFORWARD =>
getAccountByUserName(pusher).foreach { pusherAccount =>
updatePullRequests(owner, repository, branchName, pusherAccount, "synchronize")
updatePullRequests(owner, repository, branchName, pusherAccount, "synchronize", settings)
}
case _ =>
}
}
// call web hook
callWebHookOf(owner, repository, WebHook.Push) {
callWebHookOf(owner, repository, WebHook.Push, settings) {
for {
pusherAccount <- getAccountByUserName(pusher)
ownerAccount <- getAccountByUserName(owner)
@@ -390,7 +393,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
}
}
if (command.getType == ReceiveCommand.Type.CREATE) {
callWebHookOf(owner, repository, WebHook.Create) {
callWebHookOf(owner, repository, WebHook.Create, settings) {
for {
pusherAccount <- getAccountByUserName(pusher)
ownerAccount <- getAccountByUserName(owner)
@@ -428,11 +431,14 @@ class WikiCommitHook(owner: String, repository: String, pusher: String, baseUrl:
extends PostReceiveHook
with WebHookService
with AccountService
with RepositoryService {
with RepositoryService
with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[WikiCommitHook])
override def onPostReceive(receivePack: ReceivePack, commands: util.Collection[ReceiveCommand]): Unit = {
val settings = loadSystemSettings()
Database() withTransaction { implicit session =>
try {
commands.asScala.headOption.foreach { command =>
@@ -468,7 +474,7 @@ class WikiCommitHook(owner: String, repository: String, pusher: String, baseUrl:
(commits.head._1, fileName, commits.last._3)
}
callWebHookOf(owner, repository, WebHook.Gollum) {
callWebHookOf(owner, repository, WebHook.Gollum, settings) {
for {
pusherAccount <- getAccountByUserName(pusher)
repositoryUser <- getAccountByUserName(owner)

View File

@@ -1,6 +1,9 @@
package gitbucket.core.util
import java.net.{InetAddress, URL}
import gitbucket.core.service.SystemSettingsService
import org.apache.commons.net.util.SubnetUtils
import org.apache.http.HttpHost
import org.apache.http.auth.{AuthScope, UsernamePasswordCredentials}
import org.apache.http.impl.client.{BasicCredentialsProvider, CloseableHttpClient, HttpClientBuilder}
@@ -32,4 +35,19 @@ object HttpClientUtil {
}
}
def isPrivateAddress(address: String): Boolean = {
val ipAddress = InetAddress.getByName(address)
ipAddress.isSiteLocalAddress || ipAddress.isLinkLocalAddress || ipAddress.isLoopbackAddress
}
def inIpRange(ipRange: String, ipAddress: String): Boolean = {
if (ipRange.contains('/')) {
val utils = new SubnetUtils(ipRange)
utils.setInclusiveHostCount(true)
utils.getInfo.isInRange(ipAddress)
} else {
ipRange == ipAddress
}
}
}

View File

@@ -283,6 +283,23 @@
Send notifications
</label>
</fieldset>
<!--====================================================================-->
<!-- Web hook -->
<!--====================================================================-->
<hr>
<label class="strong">Web hook</label>
<fieldset>
<label class="checkbox" for="blockPrivateAddress">
<input type="checkbox" id="blockPrivateAddress" name="webhook.blockPrivateAddress"@if(context.settings.webHook.blockPrivateAddress){ checked}/>
Block sending to private addresses
</label>
</fieldset>
<div class="webhook">
<label><span class="strong">IP whitelist</span></label>
<fieldset>
<textarea name="webhook.whitelist" class="form-control" style="height: 100px;">@context.settings.webHook.whitelist.mkString("\n")</textarea>
</fieldset>
</div>
<script>
$(function(){
$('#skinName').change(function(evt) {
@@ -344,5 +361,9 @@ $(function(){
$('#notification').prop('checked', false);
}
}).change();
$('#blockPrivateAddress').change(function(){
$('.webhook textarea').prop('disabled', !$(this).prop('checked'));
}).change();
});
</script>

View File

@@ -51,7 +51,11 @@ trait ServiceSpecBase extends MockitoSugar {
oidcAuthentication = false,
oidc = None,
skinName = "skin-blue",
showMailAddress = false
showMailAddress = false,
webHook = SystemSettingsService.WebHook(
blockPrivateAddress = false,
whitelist = Nil
)
)
def withTestDB[A](action: (Session) => A): A = {
@@ -137,7 +141,8 @@ trait ServiceSpecBase extends MockitoSugar {
commitIdFrom = baesBranch,
commitIdTo = requestBranch,
isDraft = false,
loginAccount = loginAccount.get
loginAccount = loginAccount.get,
settings = createSystemSettings()
)
dummyService.getPullRequest(baseUserName, baseRepositoryName, issueId).get
}

View File

@@ -0,0 +1,14 @@
package gitbucket.core.util
import org.scalatest.FunSuite
class HttpClientUtilSpec extends FunSuite {
test("isPrivateAddress") {
assert(HttpClientUtil.isPrivateAddress("localhost") == true)
assert(HttpClientUtil.isPrivateAddress("192.168.10.2") == true)
assert(HttpClientUtil.isPrivateAddress("169.254.169.254") == true)
assert(HttpClientUtil.isPrivateAddress("www.google.com") == false)
}
}

View File

@@ -2,12 +2,12 @@ package gitbucket.core.view
import java.text.SimpleDateFormat
import java.util.Date
import javax.servlet.http.{HttpServletRequest, HttpSession}
import javax.servlet.http.{HttpServletRequest, HttpSession}
import gitbucket.core.controller.Context
import gitbucket.core.model.Account
import gitbucket.core.service.RequestCache
import gitbucket.core.service.SystemSettingsService.{Ssh, SystemSettings}
import gitbucket.core.service.SystemSettingsService.{Ssh, SystemSettings, WebHook}
import org.mockito.Mockito._
import org.scalatest.FunSpec
import org.scalatestplus.mockito.MockitoSugar
@@ -137,7 +137,11 @@ class AvatarImageProviderSpec extends FunSpec with MockitoSugar {
oidcAuthentication = false,
oidc = None,
skinName = "skin-blue",
showMailAddress = false
showMailAddress = false,
webHook = WebHook(
blockPrivateAddress = false,
whitelist = Nil
)
)
/**