mirror of
				https://github.com/gitbucket/gitbucket.git
				synced 2025-11-03 20:15:59 +01:00 
			
		
		
		
	Closes #2818 - Supporting custom SSH URL's when hosting behind a proxy
This commit is contained in:
		@@ -1,7 +1,6 @@
 | 
				
			|||||||
package gitbucket.core.controller
 | 
					package gitbucket.core.controller
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.io.FileInputStream
 | 
					import java.io.FileInputStream
 | 
				
			||||||
 | 
					 | 
				
			||||||
import gitbucket.core.admin.html
 | 
					import gitbucket.core.admin.html
 | 
				
			||||||
import gitbucket.core.plugin.PluginRegistry
 | 
					import gitbucket.core.plugin.PluginRegistry
 | 
				
			||||||
import gitbucket.core.service.SystemSettingsService._
 | 
					import gitbucket.core.service.SystemSettingsService._
 | 
				
			||||||
@@ -50,8 +49,20 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
 | 
				
			|||||||
    "limitVisibleRepositories" -> trim(label("limitVisibleRepositories", boolean())),
 | 
					    "limitVisibleRepositories" -> trim(label("limitVisibleRepositories", boolean())),
 | 
				
			||||||
    "ssh" -> mapping(
 | 
					    "ssh" -> mapping(
 | 
				
			||||||
      "enabled" -> trim(label("SSH access", boolean())),
 | 
					      "enabled" -> trim(label("SSH access", boolean())),
 | 
				
			||||||
      "host" -> trim(label("SSH host", optional(text()))),
 | 
					      "bindAddress" -> mapping(
 | 
				
			||||||
      "port" -> trim(label("SSH port", optional(number())))
 | 
					        "host" -> trim(label("Bind SSH host", optional(text()))),
 | 
				
			||||||
 | 
					        "port" -> trim(label("Bind SSH port", optional(number()))),
 | 
				
			||||||
 | 
					      )(
 | 
				
			||||||
 | 
					        (hostOption, portOption) =>
 | 
				
			||||||
 | 
					          hostOption.map(h => SshAddress(h, portOption.getOrElse(DefaultSshPort), GenericSshUser))
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      "publicAddress" -> mapping(
 | 
				
			||||||
 | 
					        "host" -> trim(label("Public SSH host", optional(text()))),
 | 
				
			||||||
 | 
					        "port" -> trim(label("Public SSH port", optional(number()))),
 | 
				
			||||||
 | 
					      )(
 | 
				
			||||||
 | 
					        (hostOption, portOption) =>
 | 
				
			||||||
 | 
					          hostOption.map(h => SshAddress(h, portOption.getOrElse(PublicSshPort), GenericSshUser))
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
    )(Ssh.apply),
 | 
					    )(Ssh.apply),
 | 
				
			||||||
    "useSMTP" -> trim(label("SMTP", boolean())),
 | 
					    "useSMTP" -> trim(label("SMTP", boolean())),
 | 
				
			||||||
    "smtp" -> optionalIfNotChecked(
 | 
					    "smtp" -> optionalIfNotChecked(
 | 
				
			||||||
@@ -116,8 +127,8 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
 | 
				
			|||||||
      if (settings.ssh.enabled && settings.baseUrl.isEmpty) {
 | 
					      if (settings.ssh.enabled && settings.baseUrl.isEmpty) {
 | 
				
			||||||
        Some("baseUrl" -> "Base URL is required if SSH access is enabled.")
 | 
					        Some("baseUrl" -> "Base URL is required if SSH access is enabled.")
 | 
				
			||||||
      } else None,
 | 
					      } else None,
 | 
				
			||||||
      if (settings.ssh.enabled && settings.ssh.sshHost.isEmpty) {
 | 
					      if (settings.ssh.enabled && settings.ssh.bindAddress.isEmpty) {
 | 
				
			||||||
        Some("sshHost" -> "SSH host is required if SSH access is enabled.")
 | 
					        Some("ssh.bindAddress.host" -> "SSH bind host is required if SSH access is enabled.")
 | 
				
			||||||
      } else None
 | 
					      } else None
 | 
				
			||||||
    ).flatten
 | 
					    ).flatten
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -308,12 +319,13 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
 | 
				
			|||||||
  post("/admin/system", form)(adminOnly { form =>
 | 
					  post("/admin/system", form)(adminOnly { form =>
 | 
				
			||||||
    saveSystemSettings(form)
 | 
					    saveSystemSettings(form)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (form.sshAddress != context.settings.sshAddress) {
 | 
					    if (form.ssh.bindAddress != context.settings.sshBindAddress) {
 | 
				
			||||||
      SshServer.stop()
 | 
					      SshServer.stop()
 | 
				
			||||||
      for {
 | 
					      for {
 | 
				
			||||||
        sshAddress <- form.sshAddress
 | 
					        bindAddress <- form.ssh.bindAddress
 | 
				
			||||||
 | 
					        publicAddress <- form.ssh.publicAddress.orElse(form.ssh.bindAddress)
 | 
				
			||||||
        baseUrl <- form.baseUrl
 | 
					        baseUrl <- form.baseUrl
 | 
				
			||||||
      } SshServer.start(sshAddress, baseUrl)
 | 
					      } SshServer.start(bindAddress, publicAddress, baseUrl)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    flash.update("info", "System settings has been updated.")
 | 
					    flash.update("info", "System settings has been updated.")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,6 +12,7 @@ import gitbucket.core.util.JGitUtil.FileInfo
 | 
				
			|||||||
import org.apache.commons.io.FileUtils
 | 
					import org.apache.commons.io.FileUtils
 | 
				
			||||||
import org.eclipse.jgit.api.Git
 | 
					import org.eclipse.jgit.api.Git
 | 
				
			||||||
import org.eclipse.jgit.lib.{Repository => _}
 | 
					import org.eclipse.jgit.lib.{Repository => _}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import scala.util.Using
 | 
					import scala.util.Using
 | 
				
			||||||
 | 
					
 | 
				
			||||||
trait RepositoryService {
 | 
					trait RepositoryService {
 | 
				
			||||||
@@ -835,12 +836,10 @@ object RepositoryService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  def httpUrl(owner: String, name: String)(implicit context: Context): String =
 | 
					  def httpUrl(owner: String, name: String)(implicit context: Context): String =
 | 
				
			||||||
    s"${context.baseUrl}/git/${owner}/${name}.git"
 | 
					    s"${context.baseUrl}/git/${owner}/${name}.git"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def sshUrl(owner: String, name: String)(implicit context: Context): Option[String] =
 | 
					  def sshUrl(owner: String, name: String)(implicit context: Context): Option[String] =
 | 
				
			||||||
    if (context.settings.ssh.enabled) {
 | 
					    context.settings.sshUrl(owner, name)
 | 
				
			||||||
      context.settings.sshAddress.map { x =>
 | 
					
 | 
				
			||||||
        s"ssh://${x.genericUser}@${x.host}:${x.port}/${owner}/${name}.git"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } else None
 | 
					 | 
				
			||||||
  def openRepoUrl(openUrl: String)(implicit context: Context): String =
 | 
					  def openRepoUrl(openUrl: String)(implicit context: Context): String =
 | 
				
			||||||
    s"github-${context.platform}://openRepo/${openUrl}"
 | 
					    s"github-${context.platform}://openRepo/${openUrl}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,9 +4,10 @@ import javax.servlet.http.HttpServletRequest
 | 
				
			|||||||
import com.nimbusds.jose.JWSAlgorithm
 | 
					import com.nimbusds.jose.JWSAlgorithm
 | 
				
			||||||
import com.nimbusds.oauth2.sdk.auth.Secret
 | 
					import com.nimbusds.oauth2.sdk.auth.Secret
 | 
				
			||||||
import com.nimbusds.oauth2.sdk.id.{ClientID, Issuer}
 | 
					import com.nimbusds.oauth2.sdk.id.{ClientID, Issuer}
 | 
				
			||||||
import gitbucket.core.service.SystemSettingsService._
 | 
					import gitbucket.core.service.SystemSettingsService.{getOptionValue, _}
 | 
				
			||||||
import gitbucket.core.util.ConfigUtil._
 | 
					import gitbucket.core.util.ConfigUtil._
 | 
				
			||||||
import gitbucket.core.util.Directory._
 | 
					import gitbucket.core.util.Directory._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import scala.util.Using
 | 
					import scala.util.Using
 | 
				
			||||||
 | 
					
 | 
				
			||||||
trait SystemSettingsService {
 | 
					trait SystemSettingsService {
 | 
				
			||||||
@@ -29,8 +30,14 @@ trait SystemSettingsService {
 | 
				
			|||||||
    props.setProperty(Notification, settings.notification.toString)
 | 
					    props.setProperty(Notification, settings.notification.toString)
 | 
				
			||||||
    props.setProperty(LimitVisibleRepositories, settings.limitVisibleRepositories.toString)
 | 
					    props.setProperty(LimitVisibleRepositories, settings.limitVisibleRepositories.toString)
 | 
				
			||||||
    props.setProperty(SshEnabled, settings.ssh.enabled.toString)
 | 
					    props.setProperty(SshEnabled, settings.ssh.enabled.toString)
 | 
				
			||||||
    settings.ssh.sshHost.foreach(x => props.setProperty(SshHost, x.trim))
 | 
					    settings.ssh.bindAddress.foreach { bindAddress =>
 | 
				
			||||||
    settings.ssh.sshPort.foreach(x => props.setProperty(SshPort, x.toString))
 | 
					      props.setProperty(SshBindAddressHost, bindAddress.host.trim())
 | 
				
			||||||
 | 
					      props.setProperty(SshBindAddressPort, bindAddress.port.toString)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    settings.ssh.publicAddress.foreach { publicAddress =>
 | 
				
			||||||
 | 
					      props.setProperty(SshPublicAddressHost, publicAddress.host.trim())
 | 
				
			||||||
 | 
					      props.setProperty(SshPublicAddressPort, publicAddress.port.toString)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    props.setProperty(UseSMTP, settings.useSMTP.toString)
 | 
					    props.setProperty(UseSMTP, settings.useSMTP.toString)
 | 
				
			||||||
    if (settings.useSMTP) {
 | 
					    if (settings.useSMTP) {
 | 
				
			||||||
      settings.smtp.foreach { smtp =>
 | 
					      settings.smtp.foreach { smtp =>
 | 
				
			||||||
@@ -95,6 +102,10 @@ trait SystemSettingsService {
 | 
				
			|||||||
        props.load(in)
 | 
					        props.load(in)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    loadSystemSettings(props)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def loadSystemSettings(props: java.util.Properties): SystemSettings = {
 | 
				
			||||||
    SystemSettings(
 | 
					    SystemSettings(
 | 
				
			||||||
      getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
 | 
					      getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
 | 
				
			||||||
      getOptionValue(props, Information, None),
 | 
					      getOptionValue(props, Information, None),
 | 
				
			||||||
@@ -112,9 +123,20 @@ trait SystemSettingsService {
 | 
				
			|||||||
      getValue(props, Notification, false),
 | 
					      getValue(props, Notification, false),
 | 
				
			||||||
      getValue(props, LimitVisibleRepositories, false),
 | 
					      getValue(props, LimitVisibleRepositories, false),
 | 
				
			||||||
      Ssh(
 | 
					      Ssh(
 | 
				
			||||||
        getValue(props, SshEnabled, false),
 | 
					        enabled = getValue(props, SshEnabled, false),
 | 
				
			||||||
        getOptionValue[String](props, SshHost, None).map(_.trim),
 | 
					        bindAddress = {
 | 
				
			||||||
        getOptionValue(props, SshPort, Some(DefaultSshPort))
 | 
					          // try the new-style configuration first
 | 
				
			||||||
 | 
					          getOptionValue[String](props, SshBindAddressHost, None)
 | 
				
			||||||
 | 
					            .map(h => SshAddress(h, getValue(props, SshBindAddressPort, DefaultSshPort), GenericSshUser))
 | 
				
			||||||
 | 
					            .orElse(
 | 
				
			||||||
 | 
					              // otherwise try to get old-style configuration
 | 
				
			||||||
 | 
					              getOptionValue[String](props, SshHost, None)
 | 
				
			||||||
 | 
					                .map(_.trim)
 | 
				
			||||||
 | 
					                .map(h => SshAddress(h, getValue(props, SshPort, DefaultSshPort), GenericSshUser))
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        publicAddress = getOptionValue[String](props, SshPublicAddressHost, None)
 | 
				
			||||||
 | 
					          .map(h => SshAddress(h, getValue(props, SshPublicAddressPort, PublicSshPort), GenericSshUser))
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      getValue(
 | 
					      getValue(
 | 
				
			||||||
        props,
 | 
					        props,
 | 
				
			||||||
@@ -182,7 +204,6 @@ trait SystemSettingsService {
 | 
				
			|||||||
      )
 | 
					      )
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object SystemSettingsService {
 | 
					object SystemSettingsService {
 | 
				
			||||||
@@ -214,7 +235,6 @@ object SystemSettingsService {
 | 
				
			|||||||
    upload: Upload,
 | 
					    upload: Upload,
 | 
				
			||||||
    repositoryViewer: RepositoryViewerSettings
 | 
					    repositoryViewer: RepositoryViewerSettings
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
 | 
					 | 
				
			||||||
    def baseUrl(request: HttpServletRequest): String =
 | 
					    def baseUrl(request: HttpServletRequest): String =
 | 
				
			||||||
      baseUrl.getOrElse(parseBaseUrl(request)).stripSuffix("/")
 | 
					      baseUrl.getOrElse(parseBaseUrl(request)).stripSuffix("/")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -231,11 +251,17 @@ object SystemSettingsService {
 | 
				
			|||||||
        .fold(base)(_ + base.dropWhile(_ != ':'))
 | 
					        .fold(base)(_ + base.dropWhile(_ != ':'))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def sshAddress: Option[SshAddress] =
 | 
					    def sshBindAddress: Option[SshAddress] =
 | 
				
			||||||
      ssh.sshHost.collect {
 | 
					      ssh.bindAddress
 | 
				
			||||||
        case host if ssh.enabled =>
 | 
					
 | 
				
			||||||
          SshAddress(host, ssh.sshPort.getOrElse(DefaultSshPort), "git")
 | 
					    def sshPublicAddress: Option[SshAddress] =
 | 
				
			||||||
      }
 | 
					      ssh.publicAddress.orElse(ssh.bindAddress)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def sshUrl: Option[String] =
 | 
				
			||||||
 | 
					      ssh.getUrl
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def sshUrl(owner: String, name: String): Option[String] =
 | 
				
			||||||
 | 
					      ssh.getUrl(owner: String, name: String)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  case class RepositoryOperation(
 | 
					  case class RepositoryOperation(
 | 
				
			||||||
@@ -248,9 +274,35 @@ object SystemSettingsService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  case class Ssh(
 | 
					  case class Ssh(
 | 
				
			||||||
    enabled: Boolean,
 | 
					    enabled: Boolean,
 | 
				
			||||||
    sshHost: Option[String],
 | 
					    bindAddress: Option[SshAddress],
 | 
				
			||||||
    sshPort: Option[Int]
 | 
					    publicAddress: Option[SshAddress]
 | 
				
			||||||
  )
 | 
					  ) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def getUrl: Option[String] =
 | 
				
			||||||
 | 
					      if (enabled) {
 | 
				
			||||||
 | 
					        publicAddress.map(_.getUrl).orElse(bindAddress.map(_.getUrl))
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        None
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def getUrl(owner: String, name: String): Option[String] =
 | 
				
			||||||
 | 
					      if (enabled) {
 | 
				
			||||||
 | 
					        publicAddress
 | 
				
			||||||
 | 
					          .map(_.getUrl(owner, name))
 | 
				
			||||||
 | 
					          .orElse(bindAddress.map(_.getUrl(owner, name)))
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        None
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  object Ssh {
 | 
				
			||||||
 | 
					    def apply(
 | 
				
			||||||
 | 
					      enabled: Boolean,
 | 
				
			||||||
 | 
					      bindAddress: Option[SshAddress],
 | 
				
			||||||
 | 
					      publicAddress: Option[SshAddress]
 | 
				
			||||||
 | 
					    ): Ssh =
 | 
				
			||||||
 | 
					      new Ssh(enabled, bindAddress, publicAddress.orElse(bindAddress))
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  case class Ldap(
 | 
					  case class Ldap(
 | 
				
			||||||
    host: String,
 | 
					    host: String,
 | 
				
			||||||
@@ -296,7 +348,25 @@ object SystemSettingsService {
 | 
				
			|||||||
    password: Option[String]
 | 
					    password: Option[String]
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  case class SshAddress(host: String, port: Int, genericUser: String)
 | 
					  case class SshAddress(host: String, port: Int, genericUser: String) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def isDefaultPort: Boolean =
 | 
				
			||||||
 | 
					      port == PublicSshPort
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def getUrl: String =
 | 
				
			||||||
 | 
					      if (isDefaultPort) {
 | 
				
			||||||
 | 
					        s"${genericUser}@${host}"
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        s"${genericUser}@${host}:${port}"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def getUrl(owner: String, name: String): String =
 | 
				
			||||||
 | 
					      if (isDefaultPort) {
 | 
				
			||||||
 | 
					        s"${genericUser}@${host}:${owner}/${name}.git"
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        s"ssh://${genericUser}@${host}:${port}/${owner}/${name}.git"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  case class WebHook(blockPrivateAddress: Boolean, whitelist: Seq[String])
 | 
					  case class WebHook(blockPrivateAddress: Boolean, whitelist: Seq[String])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -304,6 +374,8 @@ object SystemSettingsService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  case class RepositoryViewerSettings(maxFiles: Int)
 | 
					  case class RepositoryViewerSettings(maxFiles: Int)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  val GenericSshUser = "git"
 | 
				
			||||||
 | 
					  val PublicSshPort = 22
 | 
				
			||||||
  val DefaultSshPort = 29418
 | 
					  val DefaultSshPort = 29418
 | 
				
			||||||
  val DefaultSmtpPort = 25
 | 
					  val DefaultSmtpPort = 25
 | 
				
			||||||
  val DefaultLdapPort = 389
 | 
					  val DefaultLdapPort = 389
 | 
				
			||||||
@@ -325,6 +397,10 @@ object SystemSettingsService {
 | 
				
			|||||||
  private val SshEnabled = "ssh"
 | 
					  private val SshEnabled = "ssh"
 | 
				
			||||||
  private val SshHost = "ssh.host"
 | 
					  private val SshHost = "ssh.host"
 | 
				
			||||||
  private val SshPort = "ssh.port"
 | 
					  private val SshPort = "ssh.port"
 | 
				
			||||||
 | 
					  private val SshBindAddressHost = "ssh.bindAddress.host"
 | 
				
			||||||
 | 
					  private val SshBindAddressPort = "ssh.bindAddress.port"
 | 
				
			||||||
 | 
					  private val SshPublicAddressHost = "ssh.publicAddress.host"
 | 
				
			||||||
 | 
					  private val SshPublicAddressPort = "ssh.publicAddress.port"
 | 
				
			||||||
  private val UseSMTP = "useSMTP"
 | 
					  private val UseSMTP = "useSMTP"
 | 
				
			||||||
  private val SmtpHost = "smtp.host"
 | 
					  private val SmtpHost = "smtp.host"
 | 
				
			||||||
  private val SmtpPort = "smtp.port"
 | 
					  private val SmtpPort = "smtp.port"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,6 @@ package gitbucket.core.servlet
 | 
				
			|||||||
import java.io.File
 | 
					import java.io.File
 | 
				
			||||||
import java.util
 | 
					import java.util
 | 
				
			||||||
import java.util.Date
 | 
					import java.util.Date
 | 
				
			||||||
 | 
					 | 
				
			||||||
import scala.util.Using
 | 
					import scala.util.Using
 | 
				
			||||||
import gitbucket.core.api
 | 
					import gitbucket.core.api
 | 
				
			||||||
import gitbucket.core.api.JsonFormat.Context
 | 
					import gitbucket.core.api.JsonFormat.Context
 | 
				
			||||||
@@ -209,9 +208,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      val settings = loadSystemSettings()
 | 
					      val settings = loadSystemSettings()
 | 
				
			||||||
      val baseUrl = settings.baseUrl(request)
 | 
					      val baseUrl = settings.baseUrl(request)
 | 
				
			||||||
      val sshUrl = settings.sshAddress.map { x =>
 | 
					      val sshUrl = settings.sshUrl(owner, repository)
 | 
				
			||||||
        s"${x.genericUser}@${x.host}:${x.port}"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (!repository.endsWith(".wiki")) {
 | 
					      if (!repository.endsWith(".wiki")) {
 | 
				
			||||||
        val hook = new CommitLogHook(owner, repository, pusher, baseUrl, sshUrl)
 | 
					        val hook = new CommitLogHook(owner, repository, pusher, baseUrl, sshUrl)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,19 +9,23 @@ import org.apache.sshd.server.{Environment, ExitCallback, SessionAware}
 | 
				
			|||||||
import org.apache.sshd.server.command.{Command, CommandFactory}
 | 
					import org.apache.sshd.server.command.{Command, CommandFactory}
 | 
				
			||||||
import org.apache.sshd.server.session.ServerSession
 | 
					import org.apache.sshd.server.session.ServerSession
 | 
				
			||||||
import org.slf4j.LoggerFactory
 | 
					import org.slf4j.LoggerFactory
 | 
				
			||||||
import java.io.{File, InputStream, OutputStream}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.{File, InputStream, OutputStream}
 | 
				
			||||||
import org.eclipse.jgit.api.Git
 | 
					import org.eclipse.jgit.api.Git
 | 
				
			||||||
import Directory._
 | 
					import Directory._
 | 
				
			||||||
 | 
					import gitbucket.core.service.SystemSettingsService.SshAddress
 | 
				
			||||||
import gitbucket.core.ssh.PublicKeyAuthenticator.AuthType
 | 
					import gitbucket.core.ssh.PublicKeyAuthenticator.AuthType
 | 
				
			||||||
import org.eclipse.jgit.transport.{ReceivePack, UploadPack}
 | 
					import org.eclipse.jgit.transport.{ReceivePack, UploadPack}
 | 
				
			||||||
import org.apache.sshd.server.shell.UnknownCommand
 | 
					import org.apache.sshd.server.shell.UnknownCommand
 | 
				
			||||||
import org.eclipse.jgit.errors.RepositoryNotFoundException
 | 
					import org.eclipse.jgit.errors.RepositoryNotFoundException
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import scala.util.Using
 | 
					import scala.util.Using
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object GitCommand {
 | 
					object GitCommand {
 | 
				
			||||||
  val DefaultCommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-\+_.]+).git'\Z""".r
 | 
					  val DefaultCommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-\+_.]+).git'\Z""".r
 | 
				
			||||||
  val SimpleCommandRegex = """\Agit-(upload|receive)-pack '/(.+\.git)'\Z""".r
 | 
					  val SimpleCommandRegex = """\Agit-(upload|receive)-pack '/(.+\.git)'\Z""".r
 | 
				
			||||||
 | 
					  val DefaultCommandRegexPort22 = """\Agit-(upload|receive)-pack '/?([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-\+_.]+).git'\Z""".r
 | 
				
			||||||
 | 
					  val SimpleCommandRegexPort22 = """\Agit-(upload|receive)-pack '/?(.+\.git)'\Z""".r
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
abstract class GitCommand extends Command with SessionAware {
 | 
					abstract class GitCommand extends Command with SessionAware {
 | 
				
			||||||
@@ -159,7 +163,7 @@ class DefaultGitUploadPack(owner: String, repoName: String)
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String, sshUrl: Option[String])
 | 
					class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String, sshAddress: SshAddress)
 | 
				
			||||||
    extends DefaultGitCommand(owner, repoName)
 | 
					    extends DefaultGitCommand(owner, repoName)
 | 
				
			||||||
    with RepositoryService
 | 
					    with RepositoryService
 | 
				
			||||||
    with AccountService
 | 
					    with AccountService
 | 
				
			||||||
@@ -177,7 +181,8 @@ class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String, ss
 | 
				
			|||||||
        val repository = git.getRepository
 | 
					        val repository = git.getRepository
 | 
				
			||||||
        val receive = new ReceivePack(repository)
 | 
					        val receive = new ReceivePack(repository)
 | 
				
			||||||
        if (!repoName.endsWith(".wiki")) {
 | 
					        if (!repoName.endsWith(".wiki")) {
 | 
				
			||||||
          val hook = new CommitLogHook(owner, repoName, userName(authType), baseUrl, sshUrl)
 | 
					          val hook =
 | 
				
			||||||
 | 
					            new CommitLogHook(owner, repoName, userName(authType), baseUrl, Some(sshAddress.getUrl(owner, repoName)))
 | 
				
			||||||
          receive.setPreReceiveHook(hook)
 | 
					          receive.setPreReceiveHook(hook)
 | 
				
			||||||
          receive.setPostReceiveHook(hook)
 | 
					          receive.setPostReceiveHook(hook)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -227,7 +232,7 @@ class PluginGitReceivePack(repoName: String, routing: GitRepositoryRouting)
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GitCommandFactory(baseUrl: String, sshUrl: Option[String]) extends CommandFactory {
 | 
					class GitCommandFactory(baseUrl: String, sshAddress: SshAddress) extends CommandFactory {
 | 
				
			||||||
  private val logger = LoggerFactory.getLogger(classOf[GitCommandFactory])
 | 
					  private val logger = LoggerFactory.getLogger(classOf[GitCommandFactory])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  override def createCommand(command: String): Command = {
 | 
					  override def createCommand(command: String): Command = {
 | 
				
			||||||
@@ -238,17 +243,22 @@ class GitCommandFactory(baseUrl: String, sshUrl: Option[String]) extends Command
 | 
				
			|||||||
      case f if f.isDefinedAt(command) => f(command)
 | 
					      case f if f.isDefinedAt(command) => f(command)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pluginCommand match {
 | 
					    pluginCommand.getOrElse {
 | 
				
			||||||
      case Some(x) => x
 | 
					      val (simpleRegex, defaultRegex) =
 | 
				
			||||||
      case None =>
 | 
					        if (sshAddress.isDefaultPort) {
 | 
				
			||||||
 | 
					          (SimpleCommandRegexPort22, DefaultCommandRegexPort22)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          (SimpleCommandRegex, DefaultCommandRegex)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      command match {
 | 
					      command match {
 | 
				
			||||||
          case SimpleCommandRegex("upload", repoName) if (pluginRepository(repoName)) =>
 | 
					        case simpleRegex("upload", repoName) if pluginRepository(repoName) =>
 | 
				
			||||||
          new PluginGitUploadPack(repoName, routing(repoName))
 | 
					          new PluginGitUploadPack(repoName, routing(repoName))
 | 
				
			||||||
          case SimpleCommandRegex("receive", repoName) if (pluginRepository(repoName)) =>
 | 
					        case simpleRegex("receive", repoName) if pluginRepository(repoName) =>
 | 
				
			||||||
          new PluginGitReceivePack(repoName, routing(repoName))
 | 
					          new PluginGitReceivePack(repoName, routing(repoName))
 | 
				
			||||||
          case DefaultCommandRegex("upload", owner, repoName) => new DefaultGitUploadPack(owner, repoName)
 | 
					        case defaultRegex("upload", owner, repoName) =>
 | 
				
			||||||
          case DefaultCommandRegex("receive", owner, repoName) =>
 | 
					          new DefaultGitUploadPack(owner, repoName)
 | 
				
			||||||
            new DefaultGitReceivePack(owner, repoName, baseUrl, sshUrl)
 | 
					        case defaultRegex("receive", owner, repoName) =>
 | 
				
			||||||
 | 
					          new DefaultGitReceivePack(owner, repoName, baseUrl, sshAddress)
 | 
				
			||||||
        case _ => new UnknownCommand(command)
 | 
					        case _ => new UnknownCommand(command)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,6 +15,7 @@ class NoShell(sshAddress: SshAddress) extends Factory[Command] {
 | 
				
			|||||||
    private var callback: ExitCallback = null
 | 
					    private var callback: ExitCallback = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override def start(env: Environment): Unit = {
 | 
					    override def start(env: Environment): Unit = {
 | 
				
			||||||
 | 
					      val placeholderAddress = sshAddress.getUrl("OWNER", "REPOSITORY_NAME")
 | 
				
			||||||
      val message =
 | 
					      val message =
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
          | Welcome to
 | 
					          | Welcome to
 | 
				
			||||||
@@ -30,8 +31,8 @@ class NoShell(sshAddress: SshAddress) extends Factory[Command] {
 | 
				
			|||||||
          |
 | 
					          |
 | 
				
			||||||
          | Please use:
 | 
					          | Please use:
 | 
				
			||||||
          |
 | 
					          |
 | 
				
			||||||
          | git clone ssh://%s@%s:%d/OWNER/REPOSITORY_NAME.git
 | 
					          | git clone %s
 | 
				
			||||||
        """.stripMargin.format(sshAddress.genericUser, sshAddress.host, sshAddress.port).replace("\n", "\r\n") + "\r\n"
 | 
					        """.stripMargin.format(placeholderAddress).replace("\n", "\r\n") + "\r\n"
 | 
				
			||||||
      err.write(Constants.encode(message))
 | 
					      err.write(Constants.encode(message))
 | 
				
			||||||
      err.flush()
 | 
					      err.flush()
 | 
				
			||||||
      in.close()
 | 
					      in.close()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,24 +14,24 @@ object SshServer {
 | 
				
			|||||||
  private val server = org.apache.sshd.server.SshServer.setUpDefaultServer()
 | 
					  private val server = org.apache.sshd.server.SshServer.setUpDefaultServer()
 | 
				
			||||||
  private val active = new AtomicBoolean(false)
 | 
					  private val active = new AtomicBoolean(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private def configure(sshAddress: SshAddress, baseUrl: String) = {
 | 
					  private def configure(bindAddress: SshAddress, publicAddress: SshAddress, baseUrl: String) = {
 | 
				
			||||||
    server.setPort(sshAddress.port)
 | 
					    server.setPort(bindAddress.port)
 | 
				
			||||||
    val provider = new SimpleGeneratorHostKeyProvider(
 | 
					    val provider = new SimpleGeneratorHostKeyProvider(
 | 
				
			||||||
      java.nio.file.Paths.get(s"${Directory.GitBucketHome}/gitbucket.ser")
 | 
					      java.nio.file.Paths.get(s"${Directory.GitBucketHome}/gitbucket.ser")
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    provider.setAlgorithm("RSA")
 | 
					    provider.setAlgorithm("RSA")
 | 
				
			||||||
    provider.setOverwriteAllowed(false)
 | 
					    provider.setOverwriteAllowed(false)
 | 
				
			||||||
    server.setKeyPairProvider(provider)
 | 
					    server.setKeyPairProvider(provider)
 | 
				
			||||||
    server.setPublickeyAuthenticator(new PublicKeyAuthenticator(sshAddress.genericUser))
 | 
					    server.setPublickeyAuthenticator(new PublicKeyAuthenticator(bindAddress.genericUser))
 | 
				
			||||||
    server.setCommandFactory(
 | 
					    server.setCommandFactory(
 | 
				
			||||||
      new GitCommandFactory(baseUrl, Some(s"${sshAddress.genericUser}@${sshAddress.host}:${sshAddress.port}"))
 | 
					      new GitCommandFactory(baseUrl, publicAddress)
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    server.setShellFactory(new NoShell(sshAddress))
 | 
					    server.setShellFactory(new NoShell(publicAddress))
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def start(sshAddress: SshAddress, baseUrl: String) = {
 | 
					  def start(bindAddress: SshAddress, publicAddress: SshAddress, baseUrl: String) = {
 | 
				
			||||||
    if (active.compareAndSet(false, true)) {
 | 
					    if (active.compareAndSet(false, true)) {
 | 
				
			||||||
      configure(sshAddress, baseUrl)
 | 
					      configure(bindAddress, publicAddress, baseUrl)
 | 
				
			||||||
      server.start()
 | 
					      server.start()
 | 
				
			||||||
      logger.info(s"Start SSH Server Listen on ${server.getPort}")
 | 
					      logger.info(s"Start SSH Server Listen on ${server.getPort}")
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -59,13 +59,14 @@ class SshServerListener extends ServletContextListener with SystemSettingsServic
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  override def contextInitialized(sce: ServletContextEvent): Unit = {
 | 
					  override def contextInitialized(sce: ServletContextEvent): Unit = {
 | 
				
			||||||
    val settings = loadSystemSettings()
 | 
					    val settings = loadSystemSettings()
 | 
				
			||||||
    if (settings.sshAddress.isDefined && settings.baseUrl.isEmpty) {
 | 
					    if (settings.sshBindAddress.isDefined && settings.baseUrl.isEmpty) {
 | 
				
			||||||
      logger.error("Could not start SshServer because the baseUrl is not configured.")
 | 
					      logger.error("Could not start SshServer because the baseUrl is not configured.")
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    for {
 | 
					    for {
 | 
				
			||||||
      sshAddress <- settings.sshAddress
 | 
					      bindAddress <- settings.sshBindAddress
 | 
				
			||||||
 | 
					      publicAddress <- settings.sshPublicAddress
 | 
				
			||||||
      baseUrl <- settings.baseUrl
 | 
					      baseUrl <- settings.baseUrl
 | 
				
			||||||
    } SshServer.start(sshAddress, baseUrl)
 | 
					    } SshServer.start(bindAddress, publicAddress, baseUrl)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  override def contextDestroyed(sce: ServletContextEvent): Unit = {
 | 
					  override def contextDestroyed(sce: ServletContextEvent): Unit = {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,9 +22,7 @@ object Implicits {
 | 
				
			|||||||
  implicit def request2Session(implicit request: HttpServletRequest): JdbcBackend#Session = Database.getSession(request)
 | 
					  implicit def request2Session(implicit request: HttpServletRequest): JdbcBackend#Session = Database.getSession(request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  implicit def context2ApiJsonFormatContext(implicit context: Context): JsonFormat.Context =
 | 
					  implicit def context2ApiJsonFormatContext(implicit context: Context): JsonFormat.Context =
 | 
				
			||||||
    JsonFormat.Context(context.baseUrl, context.settings.sshAddress.map { x =>
 | 
					    JsonFormat.Context(context.baseUrl, context.settings.sshUrl)
 | 
				
			||||||
      s"${x.genericUser}@${x.host}:${x.port}"
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  implicit class RichSeq[A](private val seq: Seq[A]) extends AnyVal {
 | 
					  implicit class RichSeq[A](private val seq: Seq[A]) extends AnyVal {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,22 +19,40 @@
 | 
				
			|||||||
  <label class="checkbox">
 | 
					  <label class="checkbox">
 | 
				
			||||||
    <input type="checkbox" id="sshEnabled" name="ssh.enabled"@if(context.settings.ssh.enabled){ checked}/>
 | 
					    <input type="checkbox" id="sshEnabled" name="ssh.enabled"@if(context.settings.ssh.enabled){ checked}/>
 | 
				
			||||||
    Enable SSH access to git repository
 | 
					    Enable SSH access to git repository
 | 
				
			||||||
    <span class="muted normal">(Both SSH host and Base URL are required if SSH access is enabled)</span>
 | 
					    <span class="muted normal">(Both SSH bind host and Base URL are required if SSH access is enabled)</span>
 | 
				
			||||||
  </label>
 | 
					  </label>
 | 
				
			||||||
</fieldset>
 | 
					</fieldset>
 | 
				
			||||||
<div class="ssh">
 | 
					<div class="ssh">
 | 
				
			||||||
 | 
					  <div class="bindAddress">
 | 
				
			||||||
    <div class="form-group">
 | 
					    <div class="form-group">
 | 
				
			||||||
    <label class="control-label col-md-2" for="sshHost">SSH host</label>
 | 
					      <label class="control-label col-md-2" for="sshBindHost">SSH bind host</label>
 | 
				
			||||||
      <div class="col-md-10">
 | 
					      <div class="col-md-10">
 | 
				
			||||||
      <input type="text" id="sshHost" name="ssh.host" class="form-control" value="@context.settings.ssh.sshHost"/>
 | 
					        <input type="text" id="sshBindHost" name="ssh.bindAddress.host" class="form-control" value="@context.settings.ssh.bindAddress.map(_.host)"/>
 | 
				
			||||||
      <span id="error-ssh_host" class="error"></span>
 | 
					        <span id="error-ssh_bindAddress_host" class="error"></span>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="form-group">
 | 
					    <div class="form-group">
 | 
				
			||||||
    <label class="control-label col-md-2" for="sshPort">SSH port</label>
 | 
					      <label class="control-label col-md-2" for="sshBindPort">SSH bind port</label>
 | 
				
			||||||
      <div class="col-md-10">
 | 
					      <div class="col-md-10">
 | 
				
			||||||
      <input type="text" id="sshPort" name="ssh.port" class="form-control" value="@context.settings.ssh.sshPort"/>
 | 
					        <input type="text" id="sshBindPort" name="ssh.bindAddress.port" class="form-control" value="@context.settings.ssh.bindAddress.map(_.port)"/>
 | 
				
			||||||
      <span id="error-ssh_port" class="error"></span>
 | 
					        <span id="error-ssh_bindAddress_port" class="error"></span>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					  <div class="publicAddress">
 | 
				
			||||||
 | 
					    <div class="form-group">
 | 
				
			||||||
 | 
					      <label class="control-label col-md-2" for="sshPublicHost">SSH public host</label>
 | 
				
			||||||
 | 
					      <div class="col-md-10">
 | 
				
			||||||
 | 
					        <input type="text" id="sshPublicHost" name="ssh.publicAddress.host" class="form-control" value="@context.settings.ssh.publicAddress.map(_.host)"/>
 | 
				
			||||||
 | 
					        <span id="error-ssh_publicAddress_host" class="error"></span>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="form-group">
 | 
				
			||||||
 | 
					      <label class="control-label col-md-2" for="sshPublicPort">SSH public port</label>
 | 
				
			||||||
 | 
					      <div class="col-md-10">
 | 
				
			||||||
 | 
					        <input type="text" id="sshPublicPort" name="ssh.publicAddress.port" class="form-control" value="@context.settings.ssh.publicAddress.map(_.port)"/>
 | 
				
			||||||
 | 
					        <span id="error-ssh_publicAddress_port" class="error"></span>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -47,8 +47,8 @@ trait ServiceSpecBase {
 | 
				
			|||||||
      limitVisibleRepositories = false,
 | 
					      limitVisibleRepositories = false,
 | 
				
			||||||
      ssh = Ssh(
 | 
					      ssh = Ssh(
 | 
				
			||||||
        enabled = false,
 | 
					        enabled = false,
 | 
				
			||||||
        sshHost = None,
 | 
					        bindAddress = None,
 | 
				
			||||||
        sshPort = None
 | 
					        publicAddress = None
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      useSMTP = false,
 | 
					      useSMTP = false,
 | 
				
			||||||
      smtp = None,
 | 
					      smtp = None,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,113 @@
 | 
				
			|||||||
 | 
					package gitbucket.core.service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import gitbucket.core.service.SystemSettingsService.SshAddress
 | 
				
			||||||
 | 
					import org.scalatest.matchers.should.Matchers
 | 
				
			||||||
 | 
					import org.scalatest.wordspec.AnyWordSpecLike
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.Properties
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SystemSettingsServiceSpec extends AnyWordSpecLike with Matchers {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  "loadSystemSettings" should {
 | 
				
			||||||
 | 
					    "read old-style ssh configuration" in new SystemSettingsService {
 | 
				
			||||||
 | 
					      val props = new Properties()
 | 
				
			||||||
 | 
					      props.setProperty("ssh", "true")
 | 
				
			||||||
 | 
					      props.setProperty("ssh.host", "127.0.0.1")
 | 
				
			||||||
 | 
					      props.setProperty("ssh.port", "8022")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      val settings = loadSystemSettings(props)
 | 
				
			||||||
 | 
					      settings.ssh.enabled shouldBe true
 | 
				
			||||||
 | 
					      settings.ssh.bindAddress shouldBe Some(SshAddress("127.0.0.1", 8022, "git"))
 | 
				
			||||||
 | 
					      settings.ssh.publicAddress shouldBe settings.ssh.bindAddress
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    "read new-style ssh configuration" in new SystemSettingsService {
 | 
				
			||||||
 | 
					      val props = new Properties()
 | 
				
			||||||
 | 
					      props.setProperty("ssh", "true")
 | 
				
			||||||
 | 
					      props.setProperty("ssh.bindAddress.host", "127.0.0.1")
 | 
				
			||||||
 | 
					      props.setProperty("ssh.bindAddress.port", "8022")
 | 
				
			||||||
 | 
					      props.setProperty("ssh.publicAddress.host", "code.these.solutions")
 | 
				
			||||||
 | 
					      props.setProperty("ssh.publicAddress.port", "22")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      val settings = loadSystemSettings(props)
 | 
				
			||||||
 | 
					      settings.ssh.enabled shouldBe true
 | 
				
			||||||
 | 
					      settings.ssh.bindAddress shouldBe Some(SshAddress("127.0.0.1", 8022, "git"))
 | 
				
			||||||
 | 
					      settings.ssh.publicAddress shouldBe Some(SshAddress("code.these.solutions", 22, "git"))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    "default the ssh port if not specified" in new SystemSettingsService {
 | 
				
			||||||
 | 
					      val props = new Properties()
 | 
				
			||||||
 | 
					      props.setProperty("ssh", "true")
 | 
				
			||||||
 | 
					      props.setProperty("ssh.bindAddress.host", "127.0.0.1")
 | 
				
			||||||
 | 
					      props.setProperty("ssh.publicAddress.host", "code.these.solutions")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      val settings = loadSystemSettings(props)
 | 
				
			||||||
 | 
					      settings.ssh.enabled shouldBe true
 | 
				
			||||||
 | 
					      settings.ssh.bindAddress shouldBe Some(SshAddress("127.0.0.1", 29418, "git"))
 | 
				
			||||||
 | 
					      settings.ssh.publicAddress shouldBe Some(SshAddress("code.these.solutions", 22, "git"))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    "default the public address if not specified" in new SystemSettingsService {
 | 
				
			||||||
 | 
					      val props = new Properties()
 | 
				
			||||||
 | 
					      props.setProperty("ssh", "true")
 | 
				
			||||||
 | 
					      props.setProperty("ssh.bindAddress.host", "127.0.0.1")
 | 
				
			||||||
 | 
					      props.setProperty("ssh.bindAddress.port", "8022")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      val settings = loadSystemSettings(props)
 | 
				
			||||||
 | 
					      settings.ssh.enabled shouldBe true
 | 
				
			||||||
 | 
					      settings.ssh.bindAddress shouldBe Some(SshAddress("127.0.0.1", 8022, "git"))
 | 
				
			||||||
 | 
					      settings.ssh.publicAddress shouldBe settings.ssh.bindAddress
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    "return addresses even if ssh is not enabled" in new SystemSettingsService {
 | 
				
			||||||
 | 
					      val props = new Properties()
 | 
				
			||||||
 | 
					      props.setProperty("ssh", "false")
 | 
				
			||||||
 | 
					      props.setProperty("ssh.bindAddress.host", "127.0.0.1")
 | 
				
			||||||
 | 
					      props.setProperty("ssh.bindAddress.port", "8022")
 | 
				
			||||||
 | 
					      props.setProperty("ssh.publicAddress.host", "code.these.solutions")
 | 
				
			||||||
 | 
					      props.setProperty("ssh.publicAddress.port", "22")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      val settings = loadSystemSettings(props)
 | 
				
			||||||
 | 
					      settings.ssh.enabled shouldBe false
 | 
				
			||||||
 | 
					      settings.ssh.bindAddress shouldNot be(empty)
 | 
				
			||||||
 | 
					      settings.ssh.publicAddress shouldNot be(empty)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  "SshAddress" can {
 | 
				
			||||||
 | 
					    trait MockContext {
 | 
				
			||||||
 | 
					      val host = "code.these.solutions"
 | 
				
			||||||
 | 
					      val port = 1337
 | 
				
			||||||
 | 
					      val user = "git"
 | 
				
			||||||
 | 
					      lazy val sshAddress = SshAddress(host, port, user)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    "isDefaultPort" which {
 | 
				
			||||||
 | 
					      "returns true if using port 22" in new MockContext {
 | 
				
			||||||
 | 
					        override val port = 22
 | 
				
			||||||
 | 
					        sshAddress.isDefaultPort shouldBe true
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      "returns false if using a different port" in new MockContext {
 | 
				
			||||||
 | 
					        override val port = 8022
 | 
				
			||||||
 | 
					        sshAddress.isDefaultPort shouldBe false
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    "getUrl" which {
 | 
				
			||||||
 | 
					      "returns the port number when not using port 22" in new MockContext {
 | 
				
			||||||
 | 
					        override val port = 8022
 | 
				
			||||||
 | 
					        sshAddress.getUrl shouldBe "git@code.these.solutions:8022"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      "leaves off the port number when using port 22" in new MockContext {
 | 
				
			||||||
 | 
					        override val port = 22
 | 
				
			||||||
 | 
					        sshAddress.getUrl shouldBe "git@code.these.solutions"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    "getUrl for owner and repo" which {
 | 
				
			||||||
 | 
					      "returns an ssh-protocol url when not using port 22" in new MockContext {
 | 
				
			||||||
 | 
					        override val port = 8022
 | 
				
			||||||
 | 
					        sshAddress.getUrl("np-hard", "quantum-crypto-cracker") shouldBe
 | 
				
			||||||
 | 
					          "ssh://git@code.these.solutions:8022/np-hard/quantum-crypto-cracker.git"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      "returns a bare-protocol url when using port 22" in new MockContext {
 | 
				
			||||||
 | 
					        override val port = 22
 | 
				
			||||||
 | 
					        sshAddress.getUrl("syntactic", "brace-stretcher") shouldBe
 | 
				
			||||||
 | 
					          "git@code.these.solutions:syntactic/brace-stretcher.git"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,38 +1,84 @@
 | 
				
			|||||||
package gitbucket.core.ssh
 | 
					package gitbucket.core.ssh
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import gitbucket.core.service.SystemSettingsService.SshAddress
 | 
				
			||||||
import org.apache.sshd.server.shell.UnknownCommand
 | 
					import org.apache.sshd.server.shell.UnknownCommand
 | 
				
			||||||
import org.scalatest.funspec.AnyFunSpec
 | 
					import org.scalatest.matchers.should.Matchers
 | 
				
			||||||
 | 
					import org.scalatest.wordspec.AnyWordSpec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GitCommandFactorySpec extends AnyFunSpec {
 | 
					class GitCommandFactorySpec extends AnyWordSpec with Matchers {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  val factory = new GitCommandFactory("http://localhost:8080", None)
 | 
					  trait MockContext {
 | 
				
			||||||
 | 
					    val baseUrl = "https://some.example.tech:8080/code-context"
 | 
				
			||||||
  describe("createCommand") {
 | 
					    val sshHost = "localhost"
 | 
				
			||||||
    it("should return GitReceivePack when command is git-receive-pack") {
 | 
					    val sshPort = 2222
 | 
				
			||||||
      assert(factory.createCommand("git-receive-pack '/owner/repo.git'").isInstanceOf[DefaultGitReceivePack] == true)
 | 
					    lazy val factory = new GitCommandFactory(baseUrl, SshAddress(sshHost, sshPort, "git"))
 | 
				
			||||||
      assert(
 | 
					 | 
				
			||||||
        factory.createCommand("git-receive-pack '/owner/repo.wiki.git'").isInstanceOf[DefaultGitReceivePack] == true
 | 
					 | 
				
			||||||
      )
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    it("should return GitUploadPack when command is git-upload-pack") {
 | 
					 | 
				
			||||||
      assert(factory.createCommand("git-upload-pack '/owner/repo.git'").isInstanceOf[DefaultGitUploadPack] == true)
 | 
					 | 
				
			||||||
      assert(factory.createCommand("git-upload-pack '/owner/repo.wiki.git'").isInstanceOf[DefaultGitUploadPack] == true)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    it("should return UnknownCommand when command is not git-(upload|receive)-pack") {
 | 
					 | 
				
			||||||
      assert(factory.createCommand("git- '/owner/repo.git'").isInstanceOf[UnknownCommand] == true)
 | 
					 | 
				
			||||||
      assert(factory.createCommand("git-pack '/owner/repo.git'").isInstanceOf[UnknownCommand] == true)
 | 
					 | 
				
			||||||
      assert(factory.createCommand("git-a-pack '/owner/repo.git'").isInstanceOf[UnknownCommand] == true)
 | 
					 | 
				
			||||||
      assert(factory.createCommand("git-up-pack '/owner/repo.git'").isInstanceOf[UnknownCommand] == true)
 | 
					 | 
				
			||||||
      assert(factory.createCommand("\ngit-upload-pack '/owner/repo.git'").isInstanceOf[UnknownCommand] == true)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    it("should return UnknownCommand when git command has no valid arguments") {
 | 
					 | 
				
			||||||
      // must be: git-upload-pack '/owner/repository_name.git'
 | 
					 | 
				
			||||||
      assert(factory.createCommand("git-upload-pack").isInstanceOf[UnknownCommand] == true)
 | 
					 | 
				
			||||||
      assert(factory.createCommand("git-upload-pack /owner/repo.git").isInstanceOf[UnknownCommand] == true)
 | 
					 | 
				
			||||||
      assert(factory.createCommand("git-upload-pack 'owner/repo.git'").isInstanceOf[UnknownCommand] == true)
 | 
					 | 
				
			||||||
      assert(factory.createCommand("git-upload-pack '/ownerrepo.git'").isInstanceOf[UnknownCommand] == true)
 | 
					 | 
				
			||||||
      assert(factory.createCommand("git-upload-pack '/owner/repo.wiki'").isInstanceOf[UnknownCommand] == true)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  "createCommand" when {
 | 
				
			||||||
 | 
					    "receiving a git-receive-pack command" should {
 | 
				
			||||||
 | 
					      "return DefaultGitReceivePack" when {
 | 
				
			||||||
 | 
					        "the path matches owner/repo" in new MockContext {
 | 
				
			||||||
 | 
					          assert(factory.createCommand("git-receive-pack '/owner/repo.git'").isInstanceOf[DefaultGitReceivePack])
 | 
				
			||||||
 | 
					          assert(factory.createCommand("git-receive-pack '/owner/repo.wiki.git'").isInstanceOf[DefaultGitReceivePack])
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        "the leading slash is left off and running on port 22" in new MockContext {
 | 
				
			||||||
 | 
					          override val sshPort: Int = 22
 | 
				
			||||||
 | 
					          assert(factory.createCommand("git-receive-pack 'owner/repo.git'").isInstanceOf[DefaultGitReceivePack])
 | 
				
			||||||
 | 
					          assert(factory.createCommand("git-receive-pack 'owner/repo.wiki.git'").isInstanceOf[DefaultGitReceivePack])
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      "return UnknownCommand" when {
 | 
				
			||||||
 | 
					        "the ssh port is not 22 and the leading slash is missing" in new MockContext {
 | 
				
			||||||
 | 
					          override val sshPort: Int = 1337
 | 
				
			||||||
 | 
					          assert(factory.createCommand("git-receive-pack 'owner/repo.git'").isInstanceOf[UnknownCommand])
 | 
				
			||||||
 | 
					          assert(factory.createCommand("git-receive-pack 'owner/repo.wiki.git'").isInstanceOf[UnknownCommand])
 | 
				
			||||||
 | 
					          assert(factory.createCommand("git-receive-pack 'oranges.git'").isInstanceOf[UnknownCommand])
 | 
				
			||||||
 | 
					          assert(factory.createCommand("git-receive-pack 'apples.git'").isInstanceOf[UnknownCommand])
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        "the path is malformed" in new MockContext {
 | 
				
			||||||
 | 
					          assert(factory.createCommand("git-receive-pack '/owner/repo/wrong.git'").isInstanceOf[UnknownCommand])
 | 
				
			||||||
 | 
					          assert(factory.createCommand("git-receive-pack '/owner:repo.wiki.git'").isInstanceOf[UnknownCommand])
 | 
				
			||||||
 | 
					          assert(factory.createCommand("git-receive-pack '/oranges'").isInstanceOf[UnknownCommand])
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    "receiving a git-upload-pack command" should {
 | 
				
			||||||
 | 
					      "return DefaultGitUploadPack" when {
 | 
				
			||||||
 | 
					        "the path matches owner/repo" in new MockContext {
 | 
				
			||||||
 | 
					          assert(factory.createCommand("git-upload-pack '/owner/repo.git'").isInstanceOf[DefaultGitUploadPack])
 | 
				
			||||||
 | 
					          assert(factory.createCommand("git-upload-pack '/owner/repo.wiki.git'").isInstanceOf[DefaultGitUploadPack])
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        "the leading slash is left off and running on port 22" in new MockContext {
 | 
				
			||||||
 | 
					          override val sshPort = 22
 | 
				
			||||||
 | 
					          assert(factory.createCommand("git-upload-pack 'owner/repo.git'").isInstanceOf[DefaultGitUploadPack])
 | 
				
			||||||
 | 
					          assert(factory.createCommand("git-upload-pack 'owner/repo.wiki.git'").isInstanceOf[DefaultGitUploadPack])
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      "return UnknownCommand" when {
 | 
				
			||||||
 | 
					        "the ssh port is not 22 and the leading slash is missing" in new MockContext {
 | 
				
			||||||
 | 
					          override val sshPort = 1337
 | 
				
			||||||
 | 
					          assert(factory.createCommand("git-upload-pack 'owner/repo.git'").isInstanceOf[UnknownCommand])
 | 
				
			||||||
 | 
					          assert(factory.createCommand("git-upload-pack 'owner/repo.wiki.git'").isInstanceOf[UnknownCommand])
 | 
				
			||||||
 | 
					          assert(factory.createCommand("git-upload-pack 'oranges.git'").isInstanceOf[UnknownCommand])
 | 
				
			||||||
 | 
					          assert(factory.createCommand("git-upload-pack 'apples.git'").isInstanceOf[UnknownCommand])
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        "the path is malformed" in new MockContext {
 | 
				
			||||||
 | 
					          assert(factory.createCommand("git-upload-pack '/owner/repo'").isInstanceOf[UnknownCommand])
 | 
				
			||||||
 | 
					          assert(factory.createCommand("git-upload-pack '/owner:repo.wiki.git'").isInstanceOf[UnknownCommand])
 | 
				
			||||||
 | 
					          assert(factory.createCommand("git-upload-pack '/oranges'").isInstanceOf[UnknownCommand])
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    "receiving any command not matching git-(receive|upload)-pack" should {
 | 
				
			||||||
 | 
					      "return UnknownCommand" in new MockContext {
 | 
				
			||||||
 | 
					        assert(factory.createCommand("git-destroy-pack '/owner/repo.git'").isInstanceOf[UnknownCommand])
 | 
				
			||||||
 | 
					        assert(factory.createCommand("git-irrigate-pack '/apples.git'").isInstanceOf[UnknownCommand])
 | 
				
			||||||
 | 
					        assert(factory.createCommand("git-force-push '/stolen/nuke.git'").isInstanceOf[UnknownCommand])
 | 
				
			||||||
 | 
					        assert(factory.createCommand("git-delete '/backups.git'").isInstanceOf[UnknownCommand])
 | 
				
			||||||
 | 
					        assert(factory.createCommand("git-pack '/your/bags.git'").isInstanceOf[UnknownCommand])
 | 
				
			||||||
 | 
					        assert(factory.createCommand("git- '/bananas.git'").isInstanceOf[UnknownCommand])
 | 
				
			||||||
 | 
					        assert(factory.createCommand("99 tickets of bugs on the wall").isInstanceOf[UnknownCommand])
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -166,8 +166,8 @@ class AvatarImageProviderSpec extends AnyFunSpec {
 | 
				
			|||||||
      limitVisibleRepositories = false,
 | 
					      limitVisibleRepositories = false,
 | 
				
			||||||
      ssh = Ssh(
 | 
					      ssh = Ssh(
 | 
				
			||||||
        enabled = false,
 | 
					        enabled = false,
 | 
				
			||||||
        sshHost = None,
 | 
					        bindAddress = None,
 | 
				
			||||||
        sshPort = None
 | 
					        publicAddress = None
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      useSMTP = false,
 | 
					      useSMTP = false,
 | 
				
			||||||
      smtp = None,
 | 
					      smtp = None,
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user