diff --git a/build.sbt b/build.sbt index 572f3fe..8a38d8d 100644 --- a/build.sbt +++ b/build.sbt @@ -42,6 +42,7 @@ "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", diff --git a/src/main/scala/gitbucket/core/controller/AccountController.scala b/src/main/scala/gitbucket/core/controller/AccountController.scala index 48b542e..2c9bfa4 100644 --- a/src/main/scala/gitbucket/core/controller/AccountController.scala +++ b/src/main/scala/gitbucket/core/controller/AccountController.scala @@ -526,7 +526,8 @@ 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)) diff --git a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala index 4c0bfd8..15d18ef 100644 --- a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala +++ b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala @@ -324,13 +324,14 @@ 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 @@ 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 @@ commitIdFrom = form.commitIdFrom, commitIdTo = form.commitIdTo, isDraft = form.isDraft, - loginAccount = context.loginAccount.get + loginAccount = context.loginAccount.get, + settings = context.settings ) // insert labels diff --git a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala index b4b509e..7f22e24 100644 --- a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala @@ -294,7 +294,8 @@ ) } - 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)) diff --git a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala index 4d80143..aaa848c 100644 --- a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala @@ -354,7 +354,8 @@ 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 @@ 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 @@ 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 @@ charset = "", message = form.message.getOrElse(s"Delete ${form.fileName}"), commit = form.commit, - loginAccount = context.loginAccount.get + loginAccount = context.loginAccount.get, + settings = context.settings ) redirect( diff --git a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala index 7d9aefc..d2cf204 100644 --- a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala @@ -90,17 +90,11 @@ )(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 @@ () }) + 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 { diff --git a/src/main/scala/gitbucket/core/controller/WikiController.scala b/src/main/scala/gitbucket/core/controller/WikiController.scala index 0441df4..d7de385 100644 --- a/src/main/scala/gitbucket/core/controller/WikiController.scala +++ b/src/main/scala/gitbucket/core/controller/WikiController.scala @@ -191,7 +191,7 @@ 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 @@ 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) } diff --git a/src/main/scala/gitbucket/core/controller/api/ApiPullRequestControllerBase.scala b/src/main/scala/gitbucket/core/controller/api/ApiPullRequestControllerBase.scala index 3a827ab..ea8d5e3 100644 --- a/src/main/scala/gitbucket/core/controller/api/ApiPullRequestControllerBase.scala +++ b/src/main/scala/gitbucket/core/controller/api/ApiPullRequestControllerBase.scala @@ -115,7 +115,8 @@ 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 @@ 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 _ => diff --git a/src/main/scala/gitbucket/core/controller/api/ApiRepositoryContentsControllerBase.scala b/src/main/scala/gitbucket/core/controller/api/ApiRepositoryContentsControllerBase.scala index edcefc4..6021793 100644 --- a/src/main/scala/gitbucket/core/controller/api/ApiRepositoryContentsControllerBase.scala +++ b/src/main/scala/gitbucket/core/controller/api/ApiRepositoryContentsControllerBase.scala @@ -127,17 +127,14 @@ 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)) } diff --git a/src/main/scala/gitbucket/core/service/CommitsService.scala b/src/main/scala/gitbucket/core/service/CommitsService.scala index f3b8e84..f8793fc 100644 --- a/src/main/scala/gitbucket/core/service/CommitsService.scala +++ b/src/main/scala/gitbucket/core/service/CommitsService.scala @@ -94,7 +94,8 @@ repository, issue, pullRequest, - loginAccount + loginAccount, + context.settings ) } case None => diff --git a/src/main/scala/gitbucket/core/service/HandleCommentService.scala b/src/main/scala/gitbucket/core/service/HandleCommentService.scala index 28a2a4e..c9da544 100644 --- a/src/main/scala/gitbucket/core/service/HandleCommentService.scala +++ b/src/main/scala/gitbucket/core/service/HandleCommentService.scala @@ -80,16 +80,17 @@ // 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 diff --git a/src/main/scala/gitbucket/core/service/IssueCreationService.scala b/src/main/scala/gitbucket/core/service/IssueCreationService.scala index 0c223c0..9ed6814 100644 --- a/src/main/scala/gitbucket/core/service/IssueCreationService.scala +++ b/src/main/scala/gitbucket/core/service/IssueCreationService.scala @@ -57,7 +57,7 @@ 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)) diff --git a/src/main/scala/gitbucket/core/service/MergeService.scala b/src/main/scala/gitbucket/core/service/MergeService.scala index ac897b1..28aa87b 100644 --- a/src/main/scala/gitbucket/core/service/MergeService.scala +++ b/src/main/scala/gitbucket/core/service/MergeService.scala @@ -8,6 +8,7 @@ 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 @@ 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 @@ 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 @@ } 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 @@ 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 @@ 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 @@ 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 @@ 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 => diff --git a/src/main/scala/gitbucket/core/service/PullRequestService.scala b/src/main/scala/gitbucket/core/service/PullRequestService.scala index 10e9958..6e0caf2 100644 --- a/src/main/scala/gitbucket/core/service/PullRequestService.scala +++ b/src/main/scala/gitbucket/core/service/PullRequestService.scala @@ -8,6 +8,7 @@ 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 @@ 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 @@ ) // 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 @@ /** * 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 @@ action, getRepository(owner, repository).get, pullreq.requestBranch, - loginAccount + loginAccount, + settings ) } } diff --git a/src/main/scala/gitbucket/core/service/RepositoryCommitFileService.scala b/src/main/scala/gitbucket/core/service/RepositoryCommitFileService.scala index dd95c82..9929199 100644 --- a/src/main/scala/gitbucket/core/service/RepositoryCommitFileService.scala +++ b/src/main/scala/gitbucket/core/service/RepositoryCommitFileService.scala @@ -4,6 +4,7 @@ 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.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 @@ 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 @@ 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 @@ commit, loginAccount, loginAccount.fullName, - loginAccount.mailAddress + loginAccount.mailAddress, + settings ) } @@ -70,7 +75,8 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ } 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, diff --git a/src/main/scala/gitbucket/core/service/SystemSettingsService.scala b/src/main/scala/gitbucket/core/service/SystemSettingsService.scala index 34d8fd1..5d02559 100644 --- a/src/main/scala/gitbucket/core/service/SystemSettingsService.scala +++ b/src/main/scala/gitbucket/core/service/SystemSettingsService.scala @@ -70,6 +70,8 @@ } 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 @@ 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 @@ oidcAuthentication: Boolean, oidc: Option[OIDC], skinName: String, - showMailAddress: Boolean + showMailAddress: Boolean, + webHook: WebHook ) { def baseUrl(request: HttpServletRequest): String = @@ -252,7 +256,7 @@ 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 @@ 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 @@ } } + 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 => diff --git a/src/main/scala/gitbucket/core/service/WebHookService.scala b/src/main/scala/gitbucket/core/service/WebHookService.scala index 7889c4d..9a55873 100644 --- a/src/main/scala/gitbucket/core/service/WebHookService.scala +++ b/src/main/scala/gitbucket/core/service/WebHookService.scala @@ -3,24 +3,26 @@ 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 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 @@ 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 @@ } } 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 @@ } else { Nil } - // logger.debug("end callWebHook") } } @@ -315,9 +328,10 @@ 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 @@ 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 @@ 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 @@ mergedComment = getMergedComment(baseRepo.owner, baseRepo.name, issue.issueId) ) - callWebHook(WebHook.PullRequest, webHooks, payload) + callWebHook(WebHook.PullRequest, webHooks, payload, settings) } } @@ -453,10 +469,11 @@ 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 @@ 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( diff --git a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala index 51e5896..1de4e1a 100644 --- a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala +++ b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala @@ -239,7 +239,8 @@ 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 @@ } 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 @@ 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 @@ }.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 @@ 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 @@ } } 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 @@ 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 @@ (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) diff --git a/src/main/scala/gitbucket/core/util/HttpClientUtil.scala b/src/main/scala/gitbucket/core/util/HttpClientUtil.scala index b01d435..3f0ba46 100644 --- a/src/main/scala/gitbucket/core/util/HttpClientUtil.scala +++ b/src/main/scala/gitbucket/core/util/HttpClientUtil.scala @@ -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 @@ } } + 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 + } + } + } diff --git a/src/main/twirl/gitbucket/core/admin/settings_system.scala.html b/src/main/twirl/gitbucket/core/admin/settings_system.scala.html index 1c30de4..6ed7748 100644 --- a/src/main/twirl/gitbucket/core/admin/settings_system.scala.html +++ b/src/main/twirl/gitbucket/core/admin/settings_system.scala.html @@ -283,6 +283,23 @@ Send notifications + + + +
+ +
+ +
+
+ +
+ +
+
diff --git a/src/test/scala/gitbucket/core/service/ServiceSpecBase.scala b/src/test/scala/gitbucket/core/service/ServiceSpecBase.scala index cc8cd01..1311d1a 100644 --- a/src/test/scala/gitbucket/core/service/ServiceSpecBase.scala +++ b/src/test/scala/gitbucket/core/service/ServiceSpecBase.scala @@ -51,7 +51,11 @@ 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 @@ commitIdFrom = baesBranch, commitIdTo = requestBranch, isDraft = false, - loginAccount = loginAccount.get + loginAccount = loginAccount.get, + settings = createSystemSettings() ) dummyService.getPullRequest(baseUserName, baseRepositoryName, issueId).get } diff --git a/src/test/scala/gitbucket/core/util/HttpClientUtilSpec.scala b/src/test/scala/gitbucket/core/util/HttpClientUtilSpec.scala new file mode 100644 index 0000000..f453a20 --- /dev/null +++ b/src/test/scala/gitbucket/core/util/HttpClientUtilSpec.scala @@ -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) + } + +} diff --git a/src/test/scala/gitbucket/core/view/AvatarImageProviderSpec.scala b/src/test/scala/gitbucket/core/view/AvatarImageProviderSpec.scala index 6578c4f..9cc1dfe 100644 --- a/src/test/scala/gitbucket/core/view/AvatarImageProviderSpec.scala +++ b/src/test/scala/gitbucket/core/view/AvatarImageProviderSpec.scala @@ -2,12 +2,12 @@ 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 @@ oidcAuthentication = false, oidc = None, skinName = "skin-blue", - showMailAddress = false + showMailAddress = false, + webHook = WebHook( + blockPrivateAddress = false, + whitelist = Nil + ) ) /**