468a99f884c51011251ced2cb8b5debf012e843a

Author
Ayman Bagabas <ayman.bagabas@gmail.com>
Committer
Ayman Bagabas <ayman.bagabas@gmail.com>
Date

Message

feat(server): create ssh client keypair

Diff

This diff is truncated to protect this page.

  1diff --git a/cmd/soft/migrate_config.go b/cmd/soft/migrate_config.go
  2index 9c4f23e3857a3177e6bb610cc3301a47d33e42e0..200d46547d14c8d3894f0eb39b1ac7808f008715 100644
  3--- a/cmd/soft/migrate_config.go
  4+++ b/cmd/soft/migrate_config.go
  5@@ -23,12 +23,12 @@ var (
  6 	migrateConfig = &cobra.Command{
  7 		Use:   "migrate-config",
  8 		Short: "Migrate config to new format",
  9-		RunE: func(cmd *cobra.Command, args []string) error {
 10+		RunE: func(_ *cobra.Command, _ []string) error {
 11 			keyPath := os.Getenv("SOFT_SERVE_KEY_PATH")
 12 			reposPath := os.Getenv("SOFT_SERVE_REPO_PATH")
 13 			bindAddr := os.Getenv("SOFT_SERVE_BIND_ADDRESS")
 14 			cfg := config.DefaultConfig()
 15-			sb, err := sqlite.NewSqliteBackend(cfg.DataPath)
 16+			sb, err := sqlite.NewSqliteBackend(cfg)
 17 			if err != nil {
 18 				return fmt.Errorf("failed to create sqlite backend: %w", err)
 19 			}
 20@@ -72,7 +72,7 @@ var (
 21 				return fmt.Errorf("failed to get tree: %w", err)
 22 			}
 23 
 24-			isJson := false
 25+			isJson := false // nolint: revive
 26 			te, err := tree.TreeEntry("config.yaml")
 27 			if err != nil {
 28 				te, err = tree.TreeEntry("config.json")
 29@@ -236,7 +236,7 @@ func isGitDir(path string) bool {
 30 	return true
 31 }
 32 
 33-// copyFile copies a single file from src to dst
 34+// copyFile copies a single file from src to dst.
 35 func copyFile(src, dst string) error {
 36 	var err error
 37 	var srcfd *os.File
 38@@ -246,12 +246,12 @@ func copyFile(src, dst string) error {
 39 	if srcfd, err = os.Open(src); err != nil {
 40 		return err
 41 	}
 42-	defer srcfd.Close()
 43+	defer srcfd.Close() // nolint: errcheck
 44 
 45 	if dstfd, err = os.Create(dst); err != nil {
 46 		return err
 47 	}
 48-	defer dstfd.Close()
 49+	defer dstfd.Close() // nolint: errcheck
 50 
 51 	if _, err = io.Copy(dstfd, srcfd); err != nil {
 52 		return err
 53@@ -262,7 +262,7 @@ func copyFile(src, dst string) error {
 54 	return os.Chmod(dst, srcinfo.Mode())
 55 }
 56 
 57-// copyDir copies a whole directory recursively
 58+// copyDir copies a whole directory recursively.
 59 func copyDir(src string, dst string) error {
 60 	var err error
 61 	var fds []os.DirEntry
 62@@ -296,112 +296,7 @@ func copyDir(src string, dst string) error {
 63 	return nil
 64 }
 65 
 66-// func copyDir(src, dst string) error {
 67-// 	entries, err := os.ReadDir(src)
 68-// 	if err != nil {
 69-// 		return err
 70-// 	}
 71-// 	for _, entry := range entries {
 72-// 		sourcePath := filepath.Join(src, entry.Name())
 73-// 		destPath := filepath.Join(dst, entry.Name())
 74-//
 75-// 		fileInfo, err := os.Stat(sourcePath)
 76-// 		if err != nil {
 77-// 			return err
 78-// 		}
 79-//
 80-// 		stat, ok := fileInfo.Sys().(*syscall.Stat_t)
 81-// 		if !ok {
 82-// 			return fmt.Errorf("failed to get raw syscall.Stat_t data for '%s'", sourcePath)
 83-// 		}
 84-//
 85-// 		switch fileInfo.Mode() & os.ModeType {
 86-// 		case os.ModeDir:
 87-// 			if err := createIfNotExists(destPath, 0755); err != nil {
 88-// 				return err
 89-// 			}
 90-// 			if err := copyDir(sourcePath, destPath); err != nil {
 91-// 				return err
 92-// 			}
 93-// 		case os.ModeSymlink:
 94-// 			if err := copySymLink(sourcePath, destPath); err != nil {
 95-// 				return err
 96-// 			}
 97-// 		default:
 98-// 			if err := copyFile(sourcePath, destPath); err != nil {
 99-// 				return err
100-// 			}
101-// 		}
102-//
103-// 		if err := os.Lchown(destPath, int(stat.Uid), int(stat.Gid)); err != nil {
104-// 			return err
105diff --git a/server/backend/sqlite/sqlite.go b/server/backend/sqlite/sqlite.go
106index 5ddb9529ed278f53ce654ba3ed31aa1dd01320d1..1d07652d26b218a656a531115251abb962528d92 100644
107--- a/server/backend/sqlite/sqlite.go
108+++ b/server/backend/sqlite/sqlite.go
109@@ -10,10 +10,10 @@ import (
110 	"strings"
111 	"text/template"
112 
113-	"github.com/charmbracelet/keygen"
114 	"github.com/charmbracelet/log"
115 	"github.com/charmbracelet/soft-serve/git"
116 	"github.com/charmbracelet/soft-serve/server/backend"
117+	"github.com/charmbracelet/soft-serve/server/config"
118 	"github.com/charmbracelet/soft-serve/server/utils"
119 	"github.com/jmoiron/sqlx"
120 	_ "modernc.org/sqlite"
121@@ -26,9 +26,9 @@ var (
122 // SqliteBackend is a backend that uses a SQLite database as a Soft Serve
123 // backend.
124 type SqliteBackend struct {
125+	cfg              *config.Config
126 	dp               string
127 	db               *sqlx.DB
128-	ckp              string
129 	AdditionalAdmins []string
130 }
131 
132@@ -39,22 +39,12 @@ func (d *SqliteBackend) reposPath() string {
133 }
134 
135 // NewSqliteBackend creates a new SqliteBackend.
136-func NewSqliteBackend(dataPath string) (*SqliteBackend, error) {
137+func NewSqliteBackend(cfg *config.Config) (*SqliteBackend, error) {
138+	dataPath := cfg.DataPath
139 	if err := os.MkdirAll(dataPath, 0755); err != nil {
140 		return nil, err
141 	}
142 
143-	ckp := filepath.Join(dataPath, "ssh", "soft_serve_client")
144-	_, err := keygen.NewWithWrite(ckp, nil, keygen.Ed25519)
145-	if err != nil {
146-		return nil, err
147-	}
148-
149-	ckp, err = filepath.Abs(ckp)
150-	if err != nil {
151-		return nil, err
152-	}
153-
154 	db, err := sqlx.Connect("sqlite", filepath.Join(dataPath, "soft-serve.db"+
155 		"?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)"))
156 	if err != nil {
157@@ -62,9 +52,9 @@ func NewSqliteBackend(dataPath string) (*SqliteBackend, error) {
158 	}
159 
160 	d := &SqliteBackend{
161+		cfg: cfg,
162 		dp:  dataPath,
163 		db:  db,
164-		ckp: ckp,
165 	}
166 
167 	if err := d.init(); err != nil {
168@@ -186,8 +176,8 @@ func (d *SqliteBackend) ImportRepository(name string, remote string, opts backen
169 		CommandOptions: git.CommandOptions{
170 			Envs: []string{
171 				fmt.Sprintf(`GIT_SSH_COMMAND=ssh -o UserKnownHostsFile="%s" -o StrictHostKeyChecking=no -i "%s"`,
172-					filepath.Join(filepath.Dir(d.ckp), "known_hosts"),
173-					d.ckp,
174+					filepath.Join(d.cfg.DataPath, "ssh", "known_hosts"),
175+					filepath.Join(d.cfg.DataPath, d.cfg.SSH.ClientKeyPath),
176 				),
177 			},
178 		},
179diff --git a/server/config/config.go b/server/config/config.go
180index e9295409ac28808e18d576d08e477f04bd6664cf..79ab416df3d5ccc962d3d1f4e5d9d5d1bc0f52df 100644
181--- a/server/config/config.go
182+++ b/server/config/config.go
183@@ -21,6 +21,9 @@ type SSHConfig struct {
184 	// KeyPath is the path to the SSH server's private key.
185 	KeyPath string `env:"KEY_PATH" yaml:"key_path"`
186 
187+	// ClientKeyPath is the path to the SSH server's client private key.
188+	ClientKeyPath string `env:"CLIENT_KEY_PATH" yaml:"client_key_path"`
189+
190 	// InternalKeyPath is the path to the SSH server's internal private key.
191 	InternalKeyPath string `env:"INTERNAL_KEY_PATH" yaml:"internal_key_path"`
192 
193@@ -122,6 +125,11 @@ func DefaultConfig() *Config {
194 		dataPath = "data"
195 	}
196 
197+	dp, _ := filepath.Abs(dataPath)
198+	if dp != "" {
199+		dataPath = dp
200+	}
201+
202 	cfg := &Config{
203 		Name:     "Soft Serve",
204 		DataPath: dataPath,
205@@ -129,6 +137,7 @@ func DefaultConfig() *Config {
206 			ListenAddr:      ":23231",
207 			PublicURL:       "ssh://localhost:23231",
208 			KeyPath:         filepath.Join("ssh", "soft_serve_host"),
209+			ClientKeyPath:   filepath.Join("ssh", "soft_serve_client"),
210 			InternalKeyPath: filepath.Join("ssh", "soft_serve_internal"),
211 			MaxTimeout:      0,
212 			IdleTimeout:     120,
213diff --git a/server/config/file.go b/server/config/file.go
214index 8f295cf6abfddcf9d1b403a7ecdebe1cd9583d3b..f85c7247b564ce3d2c1f9a8ae65f70ff0849bccb 100644
215--- a/server/config/file.go
216+++ b/server/config/file.go
217@@ -24,6 +24,11 @@ ssh:
218   # The relative path to the SSH server's private key.
219   key_path: "{{ .SSH.KeyPath }}"
220 
221+  # The relative path to the SSH server's client private key.
222+  # This key will be used to authenticate the server to make git requests to
223+  # ssh remotes.
224+  client_key_path: "{{ .SSH.ClientKeyPath }}"
225+
226   # The relative path to the SSH server's internal api private key.
227   internal_key_path: "{{ .SSH.InternalKeyPath }}"
228 
229diff --git a/server/daemon_test.go b/server/daemon_test.go
230index 79b30d9959347ea179df6dd2970bd7280fc77e45..1e324dbb8387d9d1b291ce6d95aeef5c4f9903fe 100644
231--- a/server/daemon_test.go
232+++ b/server/daemon_test.go
233@@ -30,15 +30,16 @@ func TestMain(m *testing.M) {
234 	os.Setenv("SOFT_SERVE_GIT_MAX_TIMEOUT", "100")
235 	os.Setenv("SOFT_SERVE_GIT_IDLE_TIMEOUT", "1")
236 	os.Setenv("SOFT_SERVE_GIT_LISTEN_ADDR", fmt.Sprintf(":%d", randomPort()))
237-	fb, err := sqlite.NewSqliteBackend(tmp)
238+	cfg := config.DefaultConfig()
239+	d, err := NewGitDaemon(cfg)
240 	if err != nil {
241 		log.Fatal(err)
242 	}
243-	cfg := config.DefaultConfig().WithBackend(fb)
244-	d, err := NewGitDaemon(cfg)
245+	fb, err := sqlite.NewSqliteBackend(cfg)
246 	if err != nil {
247 		log.Fatal(err)
248 	}
249+	cfg = cfg.WithBackend(fb)
250 	testDaemon = d
251 	go func() {
252 		if err := d.Start(); err != ErrServerClosed {
253diff --git a/server/jobs.go b/server/jobs.go
254index df5fb0cc0d875ad01167207f141cad939ea524c2..6320a8f1b79d05eb5700f5039668ffd50cfed001 100644
255--- a/server/jobs.go
256+++ b/server/jobs.go
257@@ -1,8 +1,11 @@
258 package server
259 
260 import (
261+	"fmt"
262+	"path/filepath"
263+
264 	"github.com/charmbracelet/soft-serve/git"
265-	"github.com/charmbracelet/soft-serve/server/backend"
266+	"github.com/charmbracelet/soft-serve/server/config"
267 )
268 
269 var (
270@@ -12,7 +15,8 @@ var (
271 )
272 
273 // mirrorJob runs the (pull) mirror job task.
274-func mirrorJob(b backend.Backend) func() {
275+func mirrorJob(cfg *config.Config) func() {
276+	b := cfg.Backend
277 	logger := logger.WithPrefix("server.mirrorJob")
278 	return func() {
279 		repos, err := b.Repositories()
280@@ -31,6 +35,12 @@ func mirrorJob(b backend.Backend) func() {
281 				}
282 
283 				cmd := git.NewCommand("remote", "update", "--prune")
284+				cmd.AddEnvs(
285+					fmt.Sprintf(`GIT_SSH_COMMAND=ssh -o UserKnownHostsFile="%s" -o StrictHostKeyChecking=no -i "%s"`,
286+						filepath.Join(cfg.DataPath, "ssh", "known_hosts"),
287+						filepath.Join(cfg.DataPath, cfg.SSH.ClientKeyPath),
288+					),
289+				)
290 				if _, err := cmd.RunInDir(r.Path); err != nil {
291 					logger.Error("error running git remote update", "repo", repo.Name(), "err", err)
292 				}
293diff --git a/server/server.go b/server/server.go
294index eac408ebd670fc72eb17070c287a2d21b9bc119d..de227c54287853bf5cbb73a7bdeb1e68b52809d9 100644
295--- a/server/server.go
296+++ b/server/server.go
297@@ -39,7 +39,7 @@ type Server struct {
298 func NewServer(cfg *config.Config) (*Server, error) {
299 	var err error
300 	if cfg.Backend == nil {
301-		sb, err := sqlite.NewSqliteBackend(cfg.DataPath)
302+		sb, err := sqlite.NewSqliteBackend(cfg)
303 		if err != nil {
304 			logger.Fatal(err)
305 		}
306@@ -57,6 +57,16 @@ func NewServer(cfg *config.Config) (*Server, error) {
307 		if err != nil {
308 			return nil, err
309 		}
310+
311+		// Create client key.
312+		_, err = keygen.NewWithWrite(
313+			filepath.Join(cfg.DataPath, cfg.SSH.ClientKeyPath),
314+			nil,
315+			keygen.Ed25519,
316+		)
317+		if err != nil {
318+			return nil, err
319+		}
320 	}
321 
322 	srv := &Server{
323@@ -66,7 +76,7 @@ func NewServer(cfg *config.Config) (*Server, error) {
324 	}
325 
326 	// Add cron jobs.
327-	srv.Cron.AddFunc(jobSpecs["mirror"], mirrorJob(cfg.Backend))
328+	srv.Cron.AddFunc(jobSpecs["mirror"], mirrorJob(cfg))
329 
330 	srv.SSHServer, err = NewSSHServer(cfg, srv)
331 	if err != nil {
332diff --git a/server/session_test.go b/server/session_test.go
333index 1e6b6d8b781ad7b6f804f7dad2feefa3a84bf8f8..1e5b74fdbe90ec481535128de427750f8f34873c 100644
334--- a/server/session_test.go
335+++ b/server/session_test.go
336@@ -51,11 +51,12 @@ func setup(tb testing.TB) *gossh.Session {
337 		is.NoErr(os.Unsetenv("SOFT_SERVE_SSH_LISTEN_ADDR"))
338 		is.NoErr(os.RemoveAll(dp))
339 	})
340-	fb, err := sqlite.NewSqliteBackend(dp)
341+	cfg := config.DefaultConfig()
342+	fb, err := sqlite.NewSqliteBackend(cfg)
343 	if err != nil {
344 		log.Fatal(err)
345 	}
346-	cfg := config.DefaultConfig().WithBackend(fb)
347+	cfg = cfg.WithBackend(fb)
348 	return testsession.New(tb, &ssh.Server{
349 		Handler: bm.MiddlewareWithProgramHandler(SessionHandler(cfg), termenv.ANSI256)(func(s ssh.Session) {
350 			_, _, active := s.Pty()