~jan0sch/smederee

Showing details for patch 015899ec297cd8ec066af73ae3f940990f014c26.
2022-08-28 (Sun), 7:38 PM - Jens Grassel - 015899ec297cd8ec066af73ae3f940990f014c26

SSH: Proof of Concept for basic cloning and pull

- clone via ssh works
- pull via ssh works
- push doesn't work (darcs apply command)
Summary of changes
1 files added
  • modules/hub/src/main/scala/de/smederee/ssh/DarcsSshCommand.scala
1 files modified with 36 lines added and 12 lines removed
  • modules/hub/src/main/scala/de/smederee/ssh/SshServer.scala with 36 added and 12 removed lines
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
   }