~jan0sch/smederee
Showing details for patch 015899ec297cd8ec066af73ae3f940990f014c26.
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/ssh/DarcsSshCommand.scala new-smederee/modules/hub/src/main/scala/de/smederee/ssh/DarcsSshCommand.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/ssh/DarcsSshCommand.scala 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/ssh/DarcsSshCommand.scala 2025-02-02 14:31:50.613404972 +0000 @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2022 Wegtam GmbH + * + * Business Source License 1.1 - See file LICENSE for details! + * + * Change Date: 2025-06-21 + * Change License: GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + */ + +package de.smederee.ssh + +import java.io.{ File, InputStream, OutputStream } +import java.nio.file.Paths + +import cats.syntax.all._ +import de.smederee.hub.config._ +import de.smederee.hub.{ Username, VcsRepositoryName } +import org.apache.sshd.scp.common.ScpHelper +import org.apache.sshd.scp.server._ +import org.apache.sshd.server.channel.ChannelSession +import org.apache.sshd.server.command.{ Command, CommandFactory } +import org.apache.sshd.server.session.{ ServerSession, ServerSessionAware } +import org.apache.sshd.server.shell.UnknownCommand +import org.apache.sshd.server.{ Environment, ExitCallback } +import org.slf4j.LoggerFactory + +import scala.util.matching.Regex + +/** A base class for the specific darcs commands that we implement. + */ +abstract class DarcsSshCommand extends Command with ServerSessionAware { + private val log = LoggerFactory.getLogger(classOf[DarcsSshCommand]) + + protected var stdin: InputStream = null + protected var stdout: OutputStream = null + protected var stderr: OutputStream = null + protected var callback: ExitCallback = null + + override def destroy(channel: ChannelSession): Unit = () + override def setErrorStream(errorStream: OutputStream): Unit = this.stderr = errorStream + override def setExitCallback(callback: ExitCallback): Unit = this.callback = callback + override def setInputStream(inputStream: InputStream): Unit = this.stdin = inputStream + override def setOutputStream(outputStream: OutputStream): Unit = this.stdout = outputStream + override def setSession(session: ServerSession): Unit = () + +} + +/** Implementation of a ssh command to perform a darcs apply command. + * + * @param darcsConfiguration + * The configuration needed to properly execute underlying darcs commands and access repository data on the + * filesystem. + * @param owner + * The username of the owner of the requested repository. + * @param repository + * The name of the vcs repository. + */ +final class DarcsApply(darcsConfiguration: DarcsConfiguration, owner: Username, repository: VcsRepositoryName) + extends DarcsSshCommand { + private val log = LoggerFactory.getLogger(classOf[DarcsApply]) + + override def start(channel: ChannelSession, env: Environment): Unit = { + log.debug(s"DarcsApply for $owner/$repository") + val cmd = os.proc( + darcsConfiguration.executable.toString, + List("apply", "--all", "--repodir", s"$owner/$repository") + ) + cmd.call( + cwd = os.Path(darcsConfiguration.repositoriesDirectory.toPath), + stdin = this.stdin, + stdout = os.ProcessOutput((bytes, _) => this.stdout.write(bytes)), + stderr = os.ProcessOutput((bytes, _) => this.stderr.write(bytes)) + ) + } +} + +/** Implementation of a ssh command to perform a darcs transfer-mode command. + * + * @param darcsConfiguration + * The configuration needed to properly execute underlying darcs commands and access repository data on the + * filesystem. + * @param owner + * The username of the owner of the requested repository. + * @param repository + * The name of the vcs repository. + */ +final class DarcsTransferMode( + darcsConfiguration: DarcsConfiguration, + owner: Username, + repository: VcsRepositoryName +) extends DarcsSshCommand { + private val log = LoggerFactory.getLogger(classOf[DarcsSshCommand]) + + override def start(channel: ChannelSession, env: Environment): Unit = { + log.debug(s"DarcsTransferMode for $owner/$repository") + val cmd = os.proc( + darcsConfiguration.executable.toString, + List("transfer-mode", "--repodir", s"$owner/$repository") + ) + cmd.call( + cwd = os.Path(darcsConfiguration.repositoriesDirectory.toPath), + stdin = this.stdin, + stdout = os.ProcessOutput((bytes, _) => this.stdout.write(bytes)), + stderr = os.ProcessOutput((bytes, _) => this.stderr.write(bytes)) + ) + } +} + +/** The command factory is appended to the apache ssh server and responsible for parsing requested commands + * and delegating to the appropriate specific command implementation. + * + * @param darcsConfiguration + * The configuration needed to properly execute underlying darcs commands and access repository data on the + * filesystem. + */ +final class DarcsSshCommandFactory(darcsConfiguration: DarcsConfiguration) extends CommandFactory { + private val log = LoggerFactory.getLogger(classOf[DarcsSshCommandFactory]) + + override def createCommand(channel: ChannelSession, command: String): Command = { + log.warn(s"Requested SSH command: $command") + + command match { + case DarcsSshCommandFactory.FilterDarcsApplyCommand("apply", _, _, "--repodir", owner, repository) => + (SshUsername.from(owner), VcsRepositoryName.from(repository)) + .mapN((owner, repository) => new DarcsApply(darcsConfiguration, owner.toUsername, repository)) + .getOrElse(new UnknownCommand(command)) + case DarcsSshCommandFactory.FilterDarcsTransferModeCommand( + "transfer-mode", + "--repodir", + owner, + repository + ) => + (SshUsername.from(owner), VcsRepositoryName.from(repository)) + .mapN((owner, repository) => + new DarcsTransferMode(darcsConfiguration, owner.toUsername, repository) + ) + .getOrElse(new UnknownCommand(command)) + case DarcsSshCommandFactory.FilterScpCommand("-f", _, "--", path) => + new ScpCommand( + channel, + command, + null, + ScpHelper.DEFAULT_SEND_BUFFER_SIZE, + ScpHelper.DEFAULT_RECEIVE_BUFFER_SIZE, + null, + null + ) + case DarcsSshCommandFactory.FilterScpCommand("-f", null, null, path) => + new ScpCommand( + channel, + command, + null, + ScpHelper.DEFAULT_SEND_BUFFER_SIZE, + ScpHelper.DEFAULT_RECEIVE_BUFFER_SIZE, + null, + null + ) + case _ => new UnknownCommand(command) + } + } +} + +object DarcsSshCommandFactory { + // A regular expression which should match on our allowed darcs commands and their arguments. + val FilterDarcsApplyCommand: Regex = + "^darcs (apply) ((--all)\\s)?\\s*(--repodir) '([a-z][a-z0-9]{1,31})/([a-zA-Z0-9][a-zA-Z0-9\\-_]{1,63})'$".r + val FilterDarcsTransferModeCommand: Regex = + "^darcs (transfer-mode) (--repodir) ([a-z][a-z0-9]{1,31})/([a-zA-Z0-9][a-zA-Z0-9\\-_]{1,63})$".r + // After the initial transfer-mode call darcs will fallback to using scp. + val FilterScpCommand: Regex = "^scp (-f) ((--)\\s)?([^\u0000]+)$".r + // TODO Add support for sftp because that could be enforced from the client side. +} diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshServer.scala new-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshServer.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshServer.scala 2025-02-02 14:31:50.613404972 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshServer.scala 2025-02-02 14:31:50.613404972 +0000 @@ -17,7 +17,8 @@ import cats.effect.* import cats.syntax.all.* import com.comcast.ip4s.* -import de.smederee.hub.config.ConfigKey +import de.smederee.hub.Username +import de.smederee.hub.config._ import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory import org.apache.sshd.scp.server.ScpCommandFactory import org.apache.sshd.server.SshServer @@ -31,7 +32,7 @@ object SshUsername { given Eq[SshUsername] = Eq.fromUniversalEquals - val Format: Regex = "^[a-z][a-z0-9]{2,15}$".r + val Format: Regex = "^[a-z][a-z0-9]{2,31}$".r /** Create an instance of SshUsername from the given String type. * @@ -52,6 +53,17 @@ def from(source: String): Option[SshUsername] = Option(source).filter(string => Format.matches(string)) } +extension (sshUsername: SshUsername) { + + /** Convert to a [[Username]] instance. The format of both is identical so we just return the wrapped ssh + * username string. + * + * @return + * A proper username derived from the given ssh username. + */ + def toUsername: Username = Username(sshUsername) +} + /** Configuration for the SSH server. * * @param enabled @@ -93,10 +105,16 @@ /** A ssh server using the [Apache MINA SSHD](https://mina.apache.org/sshd-project/) library and the IO monad * from cats effect. * - * @param configuration + * @param darcsConfiguration + * The configuration needed to properly execute underlying darcs commands and access repository data on the + * filesystem. + * @param sshConfiguration * The configuration with all values needed to properly configure the underlying Apache sshd server. */ -final class SshServerProvider(configuration: SshServerConfiguration) { +final class SshServerProvider( + darcsConfiguration: DarcsConfiguration, + sshConfiguration: SshServerConfiguration +) { /** Create an instance of a ssh server with defaults configure it according to the given configuration. * @@ -105,20 +123,26 @@ */ private def createServer(): SshServer = { val server = SshServer.setUpDefaultServer() - server.setHost(configuration.host.toString) - server.setPort(configuration.port.toString.toInt) - val keyProvider = new SimpleGeneratorHostKeyProvider(configuration.serverKeyFile) + server.setHost(sshConfiguration.host.toString) + server.setPort(sshConfiguration.port.toString.toInt) + val keyProvider = new SimpleGeneratorHostKeyProvider(sshConfiguration.serverKeyFile) keyProvider.setAlgorithm("EC") keyProvider.setOverwriteAllowed(false) server.setKeyPairProvider(keyProvider) - server.setPublickeyAuthenticator(new SshAuthenticator(configuration.genericUser)) - server.setShellFactory(new NoLogin(configuration.genericUser, configuration.host, configuration.port)) + server.setPublickeyAuthenticator(new SshAuthenticator(sshConfiguration.genericUser)) + server.setShellFactory( + new NoLogin(sshConfiguration.genericUser, sshConfiguration.host, sshConfiguration.port) + ) + server.setFileSystemFactory( + new VirtualFileSystemFactory(darcsConfiguration.repositoriesDirectory.toPath) + ) // FIXME Use SCP/SFTP as long as darcs support is missing (see https://github.com/apache/mina-sshd/blob/master/docs/sftp.md) val sftpSubsystem = new SftpSubsystemFactory.Builder().build() server.setSubsystemFactories(Collections.singletonList(sftpSubsystem)) - server.setFileSystemFactory(new VirtualFileSystemFactory(Paths.get("/home/jens/tmp/smederee"))) - val scpCommand = new ScpCommandFactory.Builder().build() - server.setCommandFactory(scpCommand) + // val scpCommand = new ScpCommandFactory.Builder().build() + // server.setCommandFactory(scpCommand) + val darcsCommand = new DarcsSshCommandFactory(darcsConfiguration) + server.setCommandFactory(darcsCommand) server }