diff --git a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala index d82bb2d..8264ea3 100644 --- a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala @@ -1,7 +1,6 @@ package gitbucket.core.controller import java.io.FileInputStream - import gitbucket.core.admin.html import gitbucket.core.plugin.PluginRegistry import gitbucket.core.service.SystemSettingsService._ @@ -50,8 +49,20 @@ "limitVisibleRepositories" -> trim(label("limitVisibleRepositories", boolean())), "ssh" -> mapping( "enabled" -> trim(label("SSH access", boolean())), - "host" -> trim(label("SSH host", optional(text()))), - "port" -> trim(label("SSH port", optional(number()))) + "bindAddress" -> mapping( + "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), "useSMTP" -> trim(label("SMTP", boolean())), "smtp" -> optionalIfNotChecked( @@ -116,8 +127,8 @@ if (settings.ssh.enabled && settings.baseUrl.isEmpty) { Some("baseUrl" -> "Base URL is required if SSH access is enabled.") } else None, - if (settings.ssh.enabled && settings.ssh.sshHost.isEmpty) { - Some("sshHost" -> "SSH host is required if SSH access is enabled.") + if (settings.ssh.enabled && settings.ssh.bindAddress.isEmpty) { + Some("ssh.bindAddress.host" -> "SSH bind host is required if SSH access is enabled.") } else None ).flatten } @@ -308,12 +319,13 @@ post("/admin/system", form)(adminOnly { form => saveSystemSettings(form) - if (form.sshAddress != context.settings.sshAddress) { + if (form.ssh.bindAddress != context.settings.sshBindAddress) { SshServer.stop() for { - sshAddress <- form.sshAddress + bindAddress <- form.ssh.bindAddress + publicAddress <- form.ssh.publicAddress.orElse(form.ssh.bindAddress) baseUrl <- form.baseUrl - } SshServer.start(sshAddress, baseUrl) + } SshServer.start(bindAddress, publicAddress, baseUrl) } flash.update("info", "System settings has been updated.") diff --git a/src/main/scala/gitbucket/core/service/RepositoryService.scala b/src/main/scala/gitbucket/core/service/RepositoryService.scala index 72faa4b..6fd641b 100644 --- a/src/main/scala/gitbucket/core/service/RepositoryService.scala +++ b/src/main/scala/gitbucket/core/service/RepositoryService.scala @@ -12,6 +12,7 @@ import org.apache.commons.io.FileUtils import org.eclipse.jgit.api.Git import org.eclipse.jgit.lib.{Repository => _} + import scala.util.Using trait RepositoryService { @@ -835,12 +836,10 @@ def httpUrl(owner: String, name: String)(implicit context: Context): String = s"${context.baseUrl}/git/${owner}/${name}.git" + def sshUrl(owner: String, name: String)(implicit context: Context): Option[String] = - if (context.settings.ssh.enabled) { - context.settings.sshAddress.map { x => - s"ssh://${x.genericUser}@${x.host}:${x.port}/${owner}/${name}.git" - } - } else None + context.settings.sshUrl(owner, name) + def openRepoUrl(openUrl: String)(implicit context: Context): String = s"github-${context.platform}://openRepo/${openUrl}" diff --git a/src/main/scala/gitbucket/core/service/SystemSettingsService.scala b/src/main/scala/gitbucket/core/service/SystemSettingsService.scala index 142078d..1b3b815 100644 --- a/src/main/scala/gitbucket/core/service/SystemSettingsService.scala +++ b/src/main/scala/gitbucket/core/service/SystemSettingsService.scala @@ -4,9 +4,10 @@ import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.oauth2.sdk.auth.Secret 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.Directory._ + import scala.util.Using trait SystemSettingsService { @@ -29,8 +30,14 @@ props.setProperty(Notification, settings.notification.toString) props.setProperty(LimitVisibleRepositories, settings.limitVisibleRepositories.toString) props.setProperty(SshEnabled, settings.ssh.enabled.toString) - settings.ssh.sshHost.foreach(x => props.setProperty(SshHost, x.trim)) - settings.ssh.sshPort.foreach(x => props.setProperty(SshPort, x.toString)) + settings.ssh.bindAddress.foreach { bindAddress => + 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) if (settings.useSMTP) { settings.smtp.foreach { smtp => @@ -95,6 +102,10 @@ props.load(in) } } + loadSystemSettings(props) + } + + def loadSystemSettings(props: java.util.Properties): SystemSettings = { SystemSettings( getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")), getOptionValue(props, Information, None), @@ -112,9 +123,20 @@ getValue(props, Notification, false), getValue(props, LimitVisibleRepositories, false), Ssh( - getValue(props, SshEnabled, false), - getOptionValue[String](props, SshHost, None).map(_.trim), - getOptionValue(props, SshPort, Some(DefaultSshPort)) + enabled = getValue(props, SshEnabled, false), + bindAddress = { + // 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( props, @@ -182,7 +204,6 @@ ) ) } - } object SystemSettingsService { @@ -214,7 +235,6 @@ upload: Upload, repositoryViewer: RepositoryViewerSettings ) { - def baseUrl(request: HttpServletRequest): String = baseUrl.getOrElse(parseBaseUrl(request)).stripSuffix("/") @@ -231,11 +251,17 @@ .fold(base)(_ + base.dropWhile(_ != ':')) } - def sshAddress: Option[SshAddress] = - ssh.sshHost.collect { - case host if ssh.enabled => - SshAddress(host, ssh.sshPort.getOrElse(DefaultSshPort), "git") - } + def sshBindAddress: Option[SshAddress] = + ssh.bindAddress + + 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( @@ -248,9 +274,35 @@ case class Ssh( enabled: Boolean, - sshHost: Option[String], - sshPort: Option[Int] - ) + bindAddress: Option[SshAddress], + 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( host: String, @@ -296,7 +348,25 @@ 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]) @@ -304,6 +374,8 @@ case class RepositoryViewerSettings(maxFiles: Int) + val GenericSshUser = "git" + val PublicSshPort = 22 val DefaultSshPort = 29418 val DefaultSmtpPort = 25 val DefaultLdapPort = 389 @@ -325,6 +397,10 @@ private val SshEnabled = "ssh" private val SshHost = "ssh.host" 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 SmtpHost = "smtp.host" private val SmtpPort = "smtp.port" diff --git a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala index 1847d54..62fa628 100644 --- a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala +++ b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala @@ -3,7 +3,6 @@ import java.io.File import java.util import java.util.Date - import scala.util.Using import gitbucket.core.api import gitbucket.core.api.JsonFormat.Context @@ -209,9 +208,7 @@ val settings = loadSystemSettings() val baseUrl = settings.baseUrl(request) - val sshUrl = settings.sshAddress.map { x => - s"${x.genericUser}@${x.host}:${x.port}" - } + val sshUrl = settings.sshUrl(owner, repository) if (!repository.endsWith(".wiki")) { val hook = new CommitLogHook(owner, repository, pusher, baseUrl, sshUrl) diff --git a/src/main/scala/gitbucket/core/ssh/GitCommand.scala b/src/main/scala/gitbucket/core/ssh/GitCommand.scala index 233942e..c2ebcfd 100644 --- a/src/main/scala/gitbucket/core/ssh/GitCommand.scala +++ b/src/main/scala/gitbucket/core/ssh/GitCommand.scala @@ -9,19 +9,23 @@ import org.apache.sshd.server.command.{Command, CommandFactory} import org.apache.sshd.server.session.ServerSession import org.slf4j.LoggerFactory -import java.io.{File, InputStream, OutputStream} +import java.io.{File, InputStream, OutputStream} import org.eclipse.jgit.api.Git import Directory._ +import gitbucket.core.service.SystemSettingsService.SshAddress import gitbucket.core.ssh.PublicKeyAuthenticator.AuthType import org.eclipse.jgit.transport.{ReceivePack, UploadPack} import org.apache.sshd.server.shell.UnknownCommand import org.eclipse.jgit.errors.RepositoryNotFoundException + import scala.util.Using object GitCommand { 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 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 { @@ -159,7 +163,7 @@ } } -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) with RepositoryService with AccountService @@ -177,7 +181,8 @@ val repository = git.getRepository val receive = new ReceivePack(repository) 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.setPostReceiveHook(hook) } @@ -227,7 +232,7 @@ } } -class GitCommandFactory(baseUrl: String, sshUrl: Option[String]) extends CommandFactory { +class GitCommandFactory(baseUrl: String, sshAddress: SshAddress) extends CommandFactory { private val logger = LoggerFactory.getLogger(classOf[GitCommandFactory]) override def createCommand(command: String): Command = { @@ -238,19 +243,24 @@ case f if f.isDefinedAt(command) => f(command) } - pluginCommand match { - case Some(x) => x - case None => - command match { - case SimpleCommandRegex("upload", repoName) if (pluginRepository(repoName)) => - new PluginGitUploadPack(repoName, routing(repoName)) - case SimpleCommandRegex("receive", repoName) if (pluginRepository(repoName)) => - new PluginGitReceivePack(repoName, routing(repoName)) - case DefaultCommandRegex("upload", owner, repoName) => new DefaultGitUploadPack(owner, repoName) - case DefaultCommandRegex("receive", owner, repoName) => - new DefaultGitReceivePack(owner, repoName, baseUrl, sshUrl) - case _ => new UnknownCommand(command) + pluginCommand.getOrElse { + val (simpleRegex, defaultRegex) = + if (sshAddress.isDefaultPort) { + (SimpleCommandRegexPort22, DefaultCommandRegexPort22) + } else { + (SimpleCommandRegex, DefaultCommandRegex) } + command match { + case simpleRegex("upload", repoName) if pluginRepository(repoName) => + new PluginGitUploadPack(repoName, routing(repoName)) + case simpleRegex("receive", repoName) if pluginRepository(repoName) => + new PluginGitReceivePack(repoName, routing(repoName)) + case defaultRegex("upload", owner, repoName) => + new DefaultGitUploadPack(owner, repoName) + case defaultRegex("receive", owner, repoName) => + new DefaultGitReceivePack(owner, repoName, baseUrl, sshAddress) + case _ => new UnknownCommand(command) + } } } diff --git a/src/main/scala/gitbucket/core/ssh/NoShell.scala b/src/main/scala/gitbucket/core/ssh/NoShell.scala index b350e26..4b00a15 100644 --- a/src/main/scala/gitbucket/core/ssh/NoShell.scala +++ b/src/main/scala/gitbucket/core/ssh/NoShell.scala @@ -15,6 +15,7 @@ private var callback: ExitCallback = null override def start(env: Environment): Unit = { + val placeholderAddress = sshAddress.getUrl("OWNER", "REPOSITORY_NAME") val message = """ | Welcome to @@ -30,8 +31,8 @@ | | Please use: | - | git clone ssh://%s@%s:%d/OWNER/REPOSITORY_NAME.git - """.stripMargin.format(sshAddress.genericUser, sshAddress.host, sshAddress.port).replace("\n", "\r\n") + "\r\n" + | git clone %s + """.stripMargin.format(placeholderAddress).replace("\n", "\r\n") + "\r\n" err.write(Constants.encode(message)) err.flush() in.close() diff --git a/src/main/scala/gitbucket/core/ssh/SshServerListener.scala b/src/main/scala/gitbucket/core/ssh/SshServerListener.scala index ce4360e..0ee992e 100644 --- a/src/main/scala/gitbucket/core/ssh/SshServerListener.scala +++ b/src/main/scala/gitbucket/core/ssh/SshServerListener.scala @@ -14,24 +14,24 @@ private val server = org.apache.sshd.server.SshServer.setUpDefaultServer() private val active = new AtomicBoolean(false) - private def configure(sshAddress: SshAddress, baseUrl: String) = { - server.setPort(sshAddress.port) + private def configure(bindAddress: SshAddress, publicAddress: SshAddress, baseUrl: String) = { + server.setPort(bindAddress.port) val provider = new SimpleGeneratorHostKeyProvider( java.nio.file.Paths.get(s"${Directory.GitBucketHome}/gitbucket.ser") ) provider.setAlgorithm("RSA") provider.setOverwriteAllowed(false) server.setKeyPairProvider(provider) - server.setPublickeyAuthenticator(new PublicKeyAuthenticator(sshAddress.genericUser)) + server.setPublickeyAuthenticator(new PublicKeyAuthenticator(bindAddress.genericUser)) 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)) { - configure(sshAddress, baseUrl) + configure(bindAddress, publicAddress, baseUrl) server.start() logger.info(s"Start SSH Server Listen on ${server.getPort}") } @@ -59,13 +59,14 @@ override def contextInitialized(sce: ServletContextEvent): Unit = { 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.") } for { - sshAddress <- settings.sshAddress + bindAddress <- settings.sshBindAddress + publicAddress <- settings.sshPublicAddress baseUrl <- settings.baseUrl - } SshServer.start(sshAddress, baseUrl) + } SshServer.start(bindAddress, publicAddress, baseUrl) } override def contextDestroyed(sce: ServletContextEvent): Unit = { diff --git a/src/main/scala/gitbucket/core/util/Implicits.scala b/src/main/scala/gitbucket/core/util/Implicits.scala index de701ca..e45d982 100644 --- a/src/main/scala/gitbucket/core/util/Implicits.scala +++ b/src/main/scala/gitbucket/core/util/Implicits.scala @@ -22,9 +22,7 @@ implicit def request2Session(implicit request: HttpServletRequest): JdbcBackend#Session = Database.getSession(request) implicit def context2ApiJsonFormatContext(implicit context: Context): JsonFormat.Context = - JsonFormat.Context(context.baseUrl, context.settings.sshAddress.map { x => - s"${x.genericUser}@${x.host}:${x.port}" - }) + JsonFormat.Context(context.baseUrl, context.settings.sshUrl) implicit class RichSeq[A](private val seq: Seq[A]) extends AnyVal { diff --git a/src/main/twirl/gitbucket/core/admin/settings_integrations.scala.html b/src/main/twirl/gitbucket/core/admin/settings_integrations.scala.html index ea3412d..2bd052a 100644 --- a/src/main/twirl/gitbucket/core/admin/settings_integrations.scala.html +++ b/src/main/twirl/gitbucket/core/admin/settings_integrations.scala.html @@ -19,22 +19,40 @@