feat(server): disable git daemon

Ayman Bagabas created

Change summary

server/server.go       | 42 ++++++++++++++---------
server/server_test.go  | 75 ++++++++++++++++++++++++++-----------------
server/session_test.go | 32 ++++++++++++------
3 files changed, 91 insertions(+), 58 deletions(-)

Detailed changes

server/server.go 🔗

@@ -75,11 +75,13 @@ func NewServer(cfg *config.Config) *Server {
 		sh.IdleTimeout = time.Duration(cfg.SSH.IdleTimeout) * time.Second
 	}
 	s.SSHServer = sh
-	d, err := daemon.NewDaemon(cfg)
-	if err != nil {
-		log.Fatalln(err)
+	if cfg.Git.Enabled {
+		d, err := daemon.NewDaemon(cfg)
+		if err != nil {
+			log.Fatalln(err)
+		}
+		s.GitServer = d
 	}
-	s.GitServer = d
 	return s
 }
 
@@ -92,13 +94,15 @@ func (s *Server) Reload() error {
 // Start starts the SSH server.
 func (s *Server) Start() error {
 	var errg errgroup.Group
-	errg.Go(func() error {
-		log.Printf("Starting Git server on %s:%d", s.Config.Host, s.Config.Git.Port)
-		if err := s.GitServer.Start(); err != daemon.ErrServerClosed {
-			return err
-		}
-		return nil
-	})
+	if s.Config.Git.Enabled {
+		errg.Go(func() error {
+			log.Printf("Starting Git server on %s:%d", s.Config.Host, s.Config.Git.Port)
+			if err := s.GitServer.Start(); err != daemon.ErrServerClosed {
+				return err
+			}
+			return nil
+		})
+	}
 	errg.Go(func() error {
 		log.Printf("Starting SSH server on %s:%d", s.Config.Host, s.Config.SSH.Port)
 		if err := s.SSHServer.ListenAndServe(); err != ssh.ErrServerClosed {
@@ -112,12 +116,14 @@ func (s *Server) Start() error {
 // Shutdown lets the server gracefully shutdown.
 func (s *Server) Shutdown(ctx context.Context) error {
 	var errg errgroup.Group
+	if s.Config.Git.Enabled {
+		errg.Go(func() error {
+			return s.GitServer.Shutdown(ctx)
+		})
+	}
 	errg.Go(func() error {
 		return s.SSHServer.Shutdown(ctx)
 	})
-	errg.Go(func() error {
-		return s.GitServer.Shutdown(ctx)
-	})
 	return errg.Wait()
 }
 
@@ -127,8 +133,10 @@ func (s *Server) Close() error {
 	errg.Go(func() error {
 		return s.SSHServer.Close()
 	})
-	errg.Go(func() error {
-		return s.GitServer.Close()
-	})
+	if s.Config.Git.Enabled {
+		errg.Go(func() error {
+			return s.GitServer.Close()
+		})
+	}
 	return errg.Wait()
 }

server/server_test.go 🔗

@@ -2,35 +2,29 @@ package server
 
 import (
 	"fmt"
-	"log"
 	"net"
 	"os"
 	"path/filepath"
+	"strconv"
+	"strings"
 	"testing"
 
 	"github.com/charmbracelet/keygen"
-	ggit "github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/server/config"
 	"github.com/gliderlabs/ssh"
+	"github.com/go-git/go-billy/v5/memfs"
 	"github.com/go-git/go-git/v5"
 	gconfig "github.com/go-git/go-git/v5/config"
 	"github.com/go-git/go-git/v5/plumbing/object"
 	gssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
+	"github.com/go-git/go-git/v5/storage/memory"
 	"github.com/matryer/is"
-	cssh "golang.org/x/crypto/ssh"
-)
-
-var (
-	cfg = &config.Config{
-		Host: "",
-		Git:  config.GitConfig{Port: 9418},
-	}
+	gossh "golang.org/x/crypto/ssh"
 )
 
 func TestPushRepo(t *testing.T) {
 	is := is.New(t)
-	_, pkPath := createKeyPair(t)
-	s := setupServer(t)
+	s, cfg, pkPath := setupServer(t)
 	err := s.Reload()
 	is.NoErr(err)
 	rp := t.TempDir()
@@ -59,8 +53,9 @@ func TestPushRepo(t *testing.T) {
 	auth, err := gssh.NewPublicKeysFromFile("git", pkPath, "")
 	is.NoErr(err)
 	auth.HostKeyCallbackHelper = gssh.HostKeyCallbackHelper{
-		HostKeyCallback: cssh.InsecureIgnoreHostKey(),
+		HostKeyCallback: gossh.InsecureIgnoreHostKey(),
 	}
+	t.Logf("pushing to ssh://localhost:%d/%s", cfg.SSH.Port, "testrepo")
 	err = r.Push(&git.PushOptions{
 		RemoteName: "origin",
 		Auth:       auth,
@@ -70,21 +65,23 @@ func TestPushRepo(t *testing.T) {
 
 func TestCloneRepo(t *testing.T) {
 	is := is.New(t)
-	_, pkPath := createKeyPair(t)
-	s := setupServer(t)
-	log.Print("starting server")
+	s, cfg, pkPath := setupServer(t)
+	t.Log("starting server")
 	err := s.Reload()
-	log.Print("reloaded server")
+	t.Log("reloaded server")
 	is.NoErr(err)
 	dst := t.TempDir()
+	t.Cleanup(func() { is.NoErr(os.RemoveAll(dst)) })
 	url := fmt.Sprintf("ssh://localhost:%d/config", cfg.SSH.Port)
-	log.Print("cloning repo")
-	err = ggit.Clone(url, dst, ggit.CloneOptions{
-		CommandOptions: ggit.CommandOptions{
-			Envs: []string{
-				fmt.Sprintf(`GIT_SSH_COMMAND=ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i %s -F /dev/null`, pkPath),
-			},
-		},
+	t.Log("cloning repo")
+	pk, err := gssh.NewPublicKeysFromFile("git", pkPath, "")
+	is.NoErr(err)
+	pk.HostKeyCallbackHelper = gssh.HostKeyCallbackHelper{
+		HostKeyCallback: gossh.InsecureIgnoreHostKey(),
+	}
+	_, err = git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
+		URL:  url,
+		Auth: pk,
 	})
 	is.NoErr(err)
 }
@@ -95,20 +92,34 @@ func randomPort() int {
 	return addr.Addr().(*net.TCPAddr).Port
 }
 
-func setupServer(t *testing.T) *Server {
+func setupServer(t *testing.T) (*Server, *config.Config, string) {
 	t.Helper()
-	cfg.DataPath = t.TempDir()
-	cfg.SSH.Port = randomPort()
+	is := is.New(t)
+	pub, pkPath := createKeyPair(t)
+	dp := t.TempDir()
+	is.NoErr(os.Setenv("SOFT_SERVE_DATA_PATH", dp))
+	is.NoErr(os.Setenv("SOFT_SERVE_INITIAL_ADMIN_KEY", authorizedKey(pub)))
+	is.NoErr(os.Setenv("SOFT_SERVE_GIT_ENABLED", "false"))
+	is.NoErr(os.Setenv("SOFT_SERVE_SSH_PORT", strconv.Itoa(randomPort())))
+	// is.NoErr(os.Setenv("SOFT_SERVE_DB_DRIVER", "fake"))
+	t.Cleanup(func() {
+		is.NoErr(os.Unsetenv("SOFT_SERVE_DATA_PATH"))
+		is.NoErr(os.Unsetenv("SOFT_SERVE_SSH_PORT"))
+		is.NoErr(os.Unsetenv("SOFT_SERVE_INITIAL_ADMIN_KEY"))
+		is.NoErr(os.Unsetenv("SOFT_SERVE_GIT_ENABLED"))
+		// is.NoErr(os.Unsetenv("SOFT_SERVE_DB_DRIVER"))
+		is.NoErr(os.RemoveAll(dp))
+	})
+	cfg := config.DefaultConfig() //.WithDB(&fakedb.FakeDB{})
 	s := NewServer(cfg)
 	go func() {
-		log.Print("starting server")
+		t.Log("starting server")
 		s.Start()
 	}()
 	t.Cleanup(func() {
 		s.Close()
-		os.RemoveAll(cfg.DataPath)
 	})
-	return s
+	return s, cfg, pkPath
 }
 
 func createKeyPair(t *testing.T) (ssh.PublicKey, string) {
@@ -121,3 +132,7 @@ func createKeyPair(t *testing.T) (ssh.PublicKey, string) {
 	is.NoErr(err)
 	return pubkey, filepath.Join(keyDir, "id_ed25519")
 }
+
+func authorizedKey(pk ssh.PublicKey) string {
+	return strings.TrimSpace(string(gossh.MarshalAuthorizedKey(pk)))
+}

server/session_test.go 🔗

@@ -4,11 +4,11 @@ import (
 	"bytes"
 	"errors"
 	"os"
+	"strconv"
 	"strings"
 	"testing"
 	"time"
 
-	"github.com/charmbracelet/soft-serve/proto"
 	cm "github.com/charmbracelet/soft-serve/server/cmd"
 	"github.com/charmbracelet/soft-serve/server/config"
 	bm "github.com/charmbracelet/wish/bubbletea"
@@ -28,6 +28,12 @@ func TestSession(t *testing.T) {
 		defer s.Close()
 		err := s.RequestPty("xterm", 80, 40, nil)
 		is.NoErr(err)
+		go func() {
+			time.Sleep(1 * time.Second)
+			s.Signal(gossh.SIGTERM)
+			// FIXME: exit with code 0 instead of forcibly closing the session
+			s.Close()
+		}()
 		err = s.Run("config")
 		// Session writes error and exits
 		is.True(strings.Contains(out.String(), cm.ErrUnauthorized.Error()))
@@ -54,16 +60,20 @@ func TestSession(t *testing.T) {
 
 func setup(tb testing.TB) *gossh.Session {
 	tb.Helper()
-	cfg := &config.Config{
-		Host:     "",
-		SSH:      config.SSHConfig{Port: randomPort()},
-		Git:      config.GitConfig{Port: 9418},
-		DataPath: tb.TempDir(),
-		InitialAdminKeys: []string{
-			"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMJlb/qf2B2kMNdBxfpCQqI2ctPcsOkdZGVh5zTRhKtH",
-		},
-		AnonAccess: proto.ReadOnlyAccess,
-	}
+	is := is.New(tb)
+	dp := tb.TempDir()
+	is.NoErr(os.Setenv("SOFT_SERVE_DATA_PATH", dp))
+	is.NoErr(os.Setenv("SOFT_SERVE_ANON_ACCESS", "read-only"))
+	is.NoErr(os.Setenv("SOFT_SERVE_GIT_PORT", "9418"))
+	is.NoErr(os.Setenv("SOFT_SERVE_SSH_PORT", strconv.Itoa(randomPort())))
+	tb.Cleanup(func() {
+		is.NoErr(os.Unsetenv("SOFT_SERVE_DATA_PATH"))
+		is.NoErr(os.Unsetenv("SOFT_SERVE_ANON_ACCESS"))
+		is.NoErr(os.Unsetenv("SOFT_SERVE_GIT_PORT"))
+		is.NoErr(os.Unsetenv("SOFT_SERVE_SSH_PORT"))
+		is.NoErr(os.RemoveAll(dp))
+	})
+	cfg := config.DefaultConfig()
 	return testsession.New(tb, &ssh.Server{
 		Handler: bm.MiddlewareWithProgramHandler(SessionHandler(cfg), termenv.ANSI256)(func(s ssh.Session) {
 			_, _, active := s.Pty()