6efe53122728af4c9d5bc7af7829e90d6ad33b45
- Author
- Ayman Bagabas <ayman.bagabas@gmail.com>
- Committer
- Ayman Bagabas <ayman.bagabas@gmail.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/cmd/soft/hook.go b/cmd/soft/hook.go
2index 4054303c5f927c7fb464b19ed18475d8f1686619..99a26d86f37463e5ae1dab05afc624aa30516996 100644
3--- a/cmd/soft/hook.go
4+++ b/cmd/soft/hook.go
5@@ -135,6 +135,7 @@ func init() {
6 hookCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "path to config file")
7 }
8
9+// TODO: use ssh controlmaster
10 func commonInit() (c *gossh.Client, s *gossh.Session, err error) {
11 cfg, err := config.ParseConfig(configPath)
12 if err != nil {
13@@ -173,11 +174,11 @@ func commonInit() (c *gossh.Client, s *gossh.Session, err error) {
14
15 func newClient(cfg *config.Config) (*gossh.Client, error) {
16 // Only accept the server's host key.
17- pk, err := keygen.New(cfg.SSH.KeyPath, keygen.WithKeyType(keygen.Ed25519))
18+ pk, err := keygen.New(cfg.Internal.KeyPath, keygen.WithKeyType(keygen.Ed25519))
19 if err != nil {
20 return nil, err
21 }
22- ik, err := keygen.New(cfg.SSH.InternalKeyPath, keygen.WithKeyType(keygen.Ed25519))
23+ ik, err := keygen.New(cfg.Internal.InternalKeyPath, keygen.WithKeyType(keygen.Ed25519))
24 if err != nil {
25 return nil, err
26 }
27@@ -188,7 +189,7 @@ func newClient(cfg *config.Config) (*gossh.Client, error) {
28 },
29 HostKeyCallback: gossh.FixedHostKey(pk.PublicKey()),
30 }
31- c, err := gossh.Dial("tcp", cfg.SSH.ListenAddr, cc)
32+ c, err := gossh.Dial("tcp", cfg.Internal.ListenAddr, cc)
33 if err != nil {
34 return nil, err
35 }
36diff --git a/cmd/soft/serve.go b/cmd/soft/serve.go
37index f0d3a42151137795ef3d650b75fc50f60644f876..55e0d2aca2c24c381d795c754149ad269edd5f83 100644
38--- a/cmd/soft/serve.go
39+++ b/cmd/soft/serve.go
40@@ -2,6 +2,7 @@ package main
41
42 import (
43 "context"
44+ "fmt"
45 "os"
46 "os/signal"
47 "syscall"
48@@ -24,7 +25,7 @@ var (
49 cfg := config.DefaultConfig()
50 s, err := server.NewServer(ctx, cfg)
51 if err != nil {
52- return err
53+ return fmt.Errorf("start server: %w", err)
54 }
55
56 done := make(chan os.Signal, 1)
57diff --git a/server/backend/sqlite/db.go b/server/backend/sqlite/db.go
58index a7f971890281eb19cf2ed67c163586b7fecabcfc..839d9105db96bc4a624cca5c1dca357758fd5a8a 100644
59--- a/server/backend/sqlite/db.go
60+++ b/server/backend/sqlite/db.go
61@@ -61,6 +61,8 @@ func (d *SqliteBackend) init() error {
62 }
63
64 // Add initial keys
65+ // Don't use cfg.AdminKeys since it also includes the internal key
66+ // used for internal api access.
67 for _, k := range d.cfg.InitialAdminKeys {
68 pk, _, err := backend.ParseAuthorizedKey(k)
69 if err != nil {
70diff --git a/server/backend/sqlite/sqlite.go b/server/backend/sqlite/sqlite.go
71index 0d1f1c015dc6134715ca67367f623d3200f5f61a..aca1f641132cb585af5d8c739d5815bf3cf60544 100644
72--- a/server/backend/sqlite/sqlite.go
73+++ b/server/backend/sqlite/sqlite.go
74@@ -186,8 +186,7 @@ func (d *SqliteBackend) ImportRepository(name string, remote string, opts backen
75 Envs: []string{
76 fmt.Sprintf(`GIT_SSH_COMMAND=ssh -o UserKnownHostsFile="%s" -o StrictHostKeyChecking=no -i "%s"`,
77 filepath.Join(d.cfg.DataPath, "ssh", "known_hosts"),
78- // FIXME: upstream keygen appends _ed25519 to the key path.
79- d.cfg.SSH.ClientKeyPath+"_ed25519",
80+ d.cfg.Internal.ClientKeyPath,
81 ),
82 },
83 },
84diff --git a/server/backend/sqlite/user.go b/server/backend/sqlite/user.go
85index 9f7e48273d3e12ffa31821debae11ca1855ae5db..456581d4d74c43a507d33194089db8d0611c98dd 100644
86--- a/server/backend/sqlite/user.go
87+++ b/server/backend/sqlite/user.go
88@@ -118,9 +118,8 @@ func (d *SqliteBackend) AccessLevel(repo string, username string) backend.Access
89 //
90 // It implements backend.Backend.
91 func (d *SqliteBackend) AccessLevelByPublicKey(repo string, pk ssh.PublicKey) backend.AccessLevel {
92- for _, k := range append(d.cfg.InitialAdminKeys, d.cfg.InternalPublicKey) {
93- ik, _, err := backend.ParseAuthorizedKey(k)
94- if err == nil && backend.KeysEqual(pk, ik) {
95+ for _, k := range d.cfg.AdminKeys() {
96+ if backend.KeysEqual(pk, k) {
97 return backend.AdminAccess
98 }
99 }
100diff --git a/server/cmd/cmd.go b/server/cmd/cmd.go
101index 340670408f85db787ec11968ac897bcd7555f059..a1d36d893bdf8b9f9e3a00ad6f77561c5328db36 100644
102--- a/server/cmd/cmd.go
103+++ b/server/cmd/cmd.go
104@@ -11,7 +11,7 @@ import (
105 "github.com/charmbracelet/log"
106 "github.com/charmbracelet/soft-serve/server/backend"
107 "github.com/charmbracelet/soft-serve/server/config"
108- "github.com/charmbracelet/soft-serve/server/hooks"
109+ "github.com/charmbracelet/soft-serve/server/errors"
110 "github.com/charmbracelet/soft-serve/server/utils"
111 "github.com/charmbracelet/ssh"
112 "github.com/charmbracelet/wish"
113@@ -35,15 +35,6 @@ var (
114 HooksCtxKey = ContextKey("hooks")
115 )
116
117-var (
118- // ErrUnauthorized is returned when the user is not authorized to perform action.
119- ErrUnauthorized = fmt.Errorf("Unauthorized")
120- // ErrRepoNotFound is returned when the repo is not found.
121- ErrRepoNotFound = fmt.Errorf("Repository not found")
122- // ErrFileNotFound is returned when the file is not found.
123- ErrFileNotFound = fmt.Errorf("File not found")
124-)
125-
126 var (
127 logger = log.WithPrefix("server.cmd")
128 )
129@@ -136,7 +127,6 @@ func rootCommand(cfg *config.Config, s ssh.Session) *cobra.Command {
130 })
131 rootCmd.CompletionOptions.DisableDefaultCmd = true
132 rootCmd.AddCommand(
133- hookCommand(),
134 repoCommand(),
135 )
136
137@@ -176,15 +166,14 @@ func checkIfReadable(cmd *cobra.Command, args []string) error {
138 rn := utils.SanitizeRepo(repo)
139 auth := cfg.Backend.AccessLevelByPublicKey(rn, s.PublicKey())
140 if auth < backend.ReadOnlyAccess {
141- return ErrUnauthorized
142+ return errors.ErrUnauthorized
143 }
144 return nil
145 }
146
147 func isPublicKeyAdmin(cfg *config.Config, pk ssh.PublicKey) bool {
148- for _, k := range cfg.InitialAdminKeys {
149- pk2, _, err := backend.ParseAuthorizedKey(k)
150- if err == nil && backend.KeysEqual(pk, pk2) {
151+ for _, k := range cfg.AdminKeys() {
152+ if backend.KeysEqual(pk, k) {
153 return true
154 }
155 }
156@@ -199,11 +188,11 @@ func checkIfAdmin(cmd *cobra.Command, _ []string) error {
157
158 user, _ := cfg.Backend.UserByPublicKey(s.PublicKey())
159 if user == nil {
160- return ErrUnauthorized
161+ return errors.ErrUnauthorized
162 }
163
164 if !user.IsAdmin() {
165- return ErrUnauthorized
166+ return errors.ErrUnauthorized
167 }
168
169 return nil
170@@ -218,13 +207,13 @@ func checkIfCollab(cmd *cobra.Command, args []string) error {
171 rn := utils.SanitizeRepo(repo)
172 auth := cfg.Backend.AccessLevelByPublicKey(rn, s.PublicKey())
173 if auth < backend.ReadWriteAccess {
174- return ErrUnauthorized
175+ return errors.ErrUnauthorized
176 }
177 return nil
178 }
179
180 // Middleware is the Soft Serve middleware that handles SSH commands.
181-func Middleware(cfg *config.Config, hooks hooks.Hooks) wish.Middleware {
182+func Middleware(cfg *config.Config) wish.Middleware {
183 return func(sh ssh.Handler) ssh.Handler {
184 return func(s ssh.Session) {
185 func() {
186@@ -245,7 +234,6 @@ func Middleware(cfg *config.Config, hooks hooks.Hooks) wish.Middleware {
187
188 ctx := context.WithValue(s.Context(), ConfigCtxKey, cfg)
189 ctx = context.WithValue(ctx, SessionCtxKey, s)
190- ctx = context.WithValue(ctx, HooksCtxKey, hooks)
191
192 rootCmd := rootCommand(cfg, s)
193 rootCmd.SetArgs(args)
194diff --git a/server/cmd/tree.go b/server/cmd/tree.go
195index 0ca08a922d90d3a2dd88631454d15411d841a80a..19ea3720d4d99e10887944b1eff12129b516e927 100644
196--- a/server/cmd/tree.go
197+++ b/server/cmd/tree.go
198@@ -4,6 +4,7 @@ import (
199 "fmt"
200
201 "github.com/charmbracelet/soft-serve/git"
202+ "github.com/charmbracelet/soft-serve/server/errors"
203 "github.com/dustin/go-humanize"
204 "github.com/spf13/cobra"
205 )
206@@ -58,7 +59,7 @@ func treeCommand() *cobra.Command {
207 if path != "" && path != "/" {
208 te, err := tree.TreeEntry(path)
209 if err == git.ErrRevisionNotExist {
210- return ErrFileNotFound
211+ return errors.ErrFileNotFound
212 }
213 if err != nil {
214 return err
215diff --git a/server/cmd/user.go b/server/cmd/user.go
216index 8c9a904d71428dd5d0e5e1510e2cfae8a4a959e7..2cb4f9aa179dd15e17fec133c11024cc980bbd93 100644
217--- a/server/cmd/user.go
218+++ b/server/cmd/user.go
219@@ -142,7 +142,6 @@ func userCommand() *cobra.Command {
220 PersistentPreRunE: checkIfAdmin,
221 RunE: func(cmd *cobra.Command, args []string) error {
222 cfg, s := fromContext(cmd)
223- ak := backend.MarshalAuthorizedKey(s.PublicKey())
224 username := args[0]
225
226 user, err := cfg.Backend.User(username)
227@@ -151,8 +150,8 @@ func userCommand() *cobra.Command {
228 }
229
230 isAdmin := user.IsAdmin()
231- for _, k := range cfg.InitialAdminKeys {
232- if ak == k {
233+ for _, k := range cfg.AdminKeys() {
234+ if backend.KeysEqual(k, s.PublicKey()) {
235 isAdmin = true
236 break
237 }
238diff --git a/server/config/config.go b/server/config/config.go
239index 13ecd1268a79c32ebf67cf7551e922c090ba29cf..c9665755bf246cfbc63c78d58ee9ed455f180d59 100644
240--- a/server/config/config.go
241+++ b/server/config/config.go
242@@ -10,6 +10,7 @@ import (
243 "github.com/caarlos0/env/v7"
244 "github.com/charmbracelet/log"
245 "github.com/charmbracelet/soft-serve/server/backend"
246+ "golang.org/x/crypto/ssh"
247 "gopkg.in/yaml.v3"
248 )
249
250@@ -24,12 +25,6 @@ type SSHConfig struct {
251 // KeyPath is the path to the SSH server's private key.
252 KeyPath string `env:"KEY_PATH" yaml:"key_path"`
253
254- // ClientKeyPath is the path to the SSH server's client private key.
255- ClientKeyPath string `env:"CLIENT_KEY_PATH" yaml:"client_key_path"`
256-
257- // InternalKeyPath is the path to the SSH server's internal private key.
258- InternalKeyPath string `env:"INTERNAL_KEY_PATH" yaml:"internal_key_path"`
259-
260 // MaxTimeout is the maximum number of seconds a connection can take.
261 MaxTimeout int `env:"MAX_TIMEOUT" yaml:"max_timeout`
262
263@@ -73,6 +68,22 @@ type StatsConfig struct {
264 ListenAddr string `env:"LISTEN_ADDR" yaml:"listen_addr"`
265 }
266
267+// InternalConfig is the configuration for the internal server.
268+// This is used for internal communication between the Soft Serve client and server.
269+type InternalConfig struct {
270+ // ListenAddr is the address on which the internal server will listen.
271+ ListenAddr string `env:"LISTEN_ADDR" yaml:"listen_addr"`
272+
273+ // KeyPath is the path to the SSH server's host private key.
274+ KeyPath string `env:"KEY_PATH" yaml:"key_path"`
275+
276+ // InternalKeyPath is the path to the server's internal private key.
277+ InternalKeyPath string `env:"INTERNAL_KEY_PATH" yaml:"internal_key_path"`
278+
279+ // ClientKeyPath is the path to the server's client private key.
280+ ClientKeyPath string `env:"CLIENT_KEY_PATH" yaml:"client_key_path"`
281+}
282+
283 // Config is the configuration for Soft Serve.
284 type Config struct {
285 // Name is the name of the server.
286@@ -90,6 +101,9 @@ type Config struct {
287 // Stats is the configuration for the stats server.
288 Stats StatsConfig `envPrefix:"STATS_" yaml:"stats"`
289
290+ // Internal is the configuration for the internal server.
291+ Internal InternalConfig `envPrefix:"INTERNAL_" yaml:"internal"`
292+
293 // InitialAdminKeys is a list of public keys that will be added to the list of admins.
294 InitialAdminKeys []string `env:"INITIAL_ADMIN_KEYS" envSeparator:"\n" yaml:"initial_admin_keys"`
295
296@@ -98,12 +112,6 @@ type Config struct {
297
298 // Backend is the Git backend to use.
299 Backend backend.Backend `yaml:"-"`
300-
301- // InternalPublicKey is the public key of the internal SSH key.
302- InternalPublicKey string `yaml:"-"`
303-
304- // ClientPublicKey is the public key of the client SSH key.
305- ClientPublicKey string `yaml:"-"`
306 }
307
308 func parseConfig(path string) (*Config, error) {
309@@ -112,13 +120,11 @@ func parseConfig(path string) (*Config, error) {
310 Name: "Soft Serve",
311 DataPath: dataPath,
312 SSH: SSHConfig{
313- ListenAddr: ":23231",
314- PublicURL: "ssh://localhost:23231",
315- KeyPath: filepath.Join("ssh", "soft_serve_host_ed25519"),
316- ClientKeyPath: filepath.Join("ssh", "soft_serve_client_ed25519"),
317- InternalKeyPath: filepath.Join("ssh", "soft_serve_internal_ed25519"),
318- MaxTimeout: 0,
319- IdleTimeout: 120,
320+ ListenAddr: ":23231",
321+ PublicURL: "ssh://localhost:23231",
322+ KeyPath: filepath.Join("ssh", "soft_serve_host_ed25519"),
323+ MaxTimeout: 0,
324+ IdleTimeout: 120,
325 },
326 Git: GitConfig{
327 ListenAddr: ":9418",
328@@ -127,11 +133,17 @@ func parseConfig(path string) (*Config, error) {
329 MaxConnections: 32,
330 },
331 HTTP: HTTPConfig{
332- ListenAddr: ":8080",
333- PublicURL: "http://localhost:8080",
334+ ListenAddr: ":23232",
335+ PublicURL: "http://localhost:23232",
336 },
337 Stats: StatsConfig{
338- ListenAddr: ":8081",
339+ ListenAddr: "localhost:23233",
340+ },
341+ Internal: InternalConfig{
342diff --git a/server/config/file.go b/server/config/file.go
343index d436e14b1022bcfa339aab8194f6b32e6f2957a4..81a0931e3bc489a1f502ca355ef309733577f836 100644
344--- a/server/config/file.go
345+++ b/server/config/file.go
346@@ -24,14 +24,6 @@ ssh:
347 # The path to the SSH server's private key.
348 key_path: "{{ .SSH.KeyPath }}"
349
350- # The path to the SSH server's client private key.
351- # This key will be used to authenticate the server to make git requests to
352- # ssh remotes.
353- client_key_path: "{{ .SSH.ClientKeyPath }}"
354-
355- # The path to the SSH server's internal api private key.
356- internal_key_path: "{{ .SSH.InternalKeyPath }}"
357-
358 # The maximum number of seconds a connection can take.
359 # A value of 0 means no timeout.
360 max_timeout: {{ .SSH.MaxTimeout }}
361@@ -75,6 +67,22 @@ stats:
362 # The address on which the stats server will listen.
363 listen_addr: "{{ .Stats.ListenAddr }}"
364
365+# The internal server configuration.
366+internal:
367+ # The address on which the internal server will listen.
368+ listen_addr: "{{ .Internal.ListenAddr }}"
369+
370+ # The path to the Internal server's host private key.
371+ key_path: "{{ .Internal.KeyPath }}"
372+
373+ # The path to the Internal server's client private key.
374+ # This key will be used to authenticate the server to make git requests to
375+ # ssh remotes.
376+ client_key_path: "{{ .Internal.ClientKeyPath }}"
377+
378+ # The path to the Internal server's internal api private key.
379+ internal_key_path: "{{ .Internal.InternalKeyPath }}"
380+
381 # Additional admin keys.
382 #initial_admin_keys:
383 # - "ssh-rsa AAAAB3NzaC1yc2..."
384diff --git a/server/errors/errors.go b/server/errors/errors.go
385new file mode 100644
386index 0000000000000000000000000000000000000000..20399d09c42b8b13fbaaa69a0ac8df1a1986ad56
387--- /dev/null
388+++ b/server/errors/errors.go
389@@ -0,0 +1,12 @@
390+package errors
391+
392+import "fmt"
393+
394+var (
395+ // ErrUnauthorized is returned when the user is not authorized to perform action.
396+ ErrUnauthorized = fmt.Errorf("Unauthorized")
397+ // ErrRepoNotFound is returned when the repo is not found.
398+ ErrRepoNotFound = fmt.Errorf("Repository not found")
399+ // ErrFileNotFound is returned when the file is not found.
400+ ErrFileNotFound = fmt.Errorf("File not found")
401+)
402diff --git a/server/hooks.go b/server/hooks.go
403index a4f9ed5683f2c728cdf38854dd53f256ad780c22..5258901c06585fc0cfff8b543f830d0a5338c692 100644
404--- a/server/hooks.go
405+++ b/server/hooks.go
406@@ -3,6 +3,7 @@ package server
407 import (
408 "io"
409
410+ "github.com/charmbracelet/log"
411 "github.com/charmbracelet/soft-serve/server/hooks"
412 )
413
414@@ -11,42 +12,43 @@ var _ hooks.Hooks = (*Server)(nil)
415 // PostReceive is called by the git post-receive hook.
416 //
417 // It implements Hooks.
418-func (*Server) PostReceive(stdout io.Writer, stderr io.Writer, repo string, args []hooks.HookArg) {
419- logger.Debug("post-receive hook called", "repo", repo, "args", args)
420+func (*Server) PostReceive(stdin io.Reader, stdout io.Writer, stderr io.Writer, repo string, args []hooks.HookArg) {
421+ io.WriteString(stdout, "Hello, world!\n")
422+ log.WithPrefix("server.hooks").Debug("post-receive hook called", "repo", repo, "args", args)
423 }
424
425 // PreReceive is called by the git pre-receive hook.
426 //
427 // It implements Hooks.
428-func (*Server) PreReceive(stdout io.Writer, stderr io.Writer, repo string, args []hooks.HookArg) {
429- logger.Debug("pre-receive hook called", "repo", repo, "args", args)
430+func (*Server) PreReceive(stdin io.Reader, stdout io.Writer, stderr io.Writer, repo string, args []hooks.HookArg) {
431+ log.WithPrefix("server.hooks").Debug("pre-receive hook called", "repo", repo, "args", args)
432 }
433
434 // Update is called by the git update hook.
435 //
436 // It implements Hooks.
437-func (*Server) Update(stdout io.Writer, stderr io.Writer, repo string, arg hooks.HookArg) {
438- logger.Debug("update hook called", "repo", repo, "arg", arg)
439+func (*Server) Update(stdin io.Reader, stdout io.Writer, stderr io.Writer, repo string, arg hooks.HookArg) {
440+ log.WithPrefix("server.hooks").Debug("update hook called", "repo", repo, "arg", arg)
441 }
442
443 // PostUpdate is called by the git post-update hook.
444 //
445 // It implements Hooks.
446-func (s *Server) PostUpdate(stdout io.Writer, stderr io.Writer, repo string, args ...string) {
447+func (s *Server) PostUpdate(stdin io.Reader, stdout io.Writer, stderr io.Writer, repo string, args ...string) {
448 rr, err := s.Config.Backend.Repository(repo)
449 if err != nil {
450- logger.WithPrefix("server.hooks.post-update").Error("error getting repository", "repo", repo, "err", err)
451+ log.WithPrefix("server.hooks.post-update").Error("error getting repository", "repo", repo, "err", err)
452 return
453 }
454
455 r, err := rr.Open()
456 if err != nil {
457- logger.WithPrefix("server.hooks.post-update").Error("error opening repository", "repo", repo, "err", err)
458+ log.WithPrefix("server.hooks.post-update").Error("error opening repository", "repo", repo, "err", err)
459 return
460 }
461
462 if err := r.UpdateServerInfo(); err != nil {
463- logger.WithPrefix("server.hooks.post-update").Error("error updating server info", "repo", repo, "err", err)
464+ log.WithPrefix("server.hooks.post-update").Error("error updating server info", "repo", repo, "err", err)
465 return
466 }
467 }
468diff --git a/server/hooks/hooks.go b/server/hooks/hooks.go
469index fb47b729b9a99a7df02edcf3cfe3fa955ca73ac7..639950413741d4cebd47eebfcffc47b0e5107303 100644
470--- a/server/hooks/hooks.go
471+++ b/server/hooks/hooks.go
472@@ -11,8 +11,8 @@ type HookArg struct {
473
474 // Hooks provides an interface for git server-side hooks.
475 type Hooks interface {
476- PreReceive(stdout io.Writer, stderr io.Writer, repo string, args []HookArg)
477- Update(stdout io.Writer, stderr io.Writer, repo string, arg HookArg)
478- PostReceive(stdout io.Writer, stderr io.Writer, repo string, args []HookArg)
479- PostUpdate(stdout io.Writer, stderr io.Writer, repo string, args ...string)
480+ PreReceive(stdin io.Reader, stdout io.Writer, stderr io.Writer, repo string, args []HookArg)
481+ Update(stdin io.Reader, stdout io.Writer, stderr io.Writer, repo string, arg HookArg)
482+ PostReceive(stdin io.Reader, stdout io.Writer, stderr io.Writer, repo string, args []HookArg)
483+ PostUpdate(stdin io.Reader, stdout io.Writer, stderr io.Writer, repo string, args ...string)
484 }
485diff --git a/server/internal/cmd.go b/server/internal/cmd.go
486new file mode 100644
487index 0000000000000000000000000000000000000000..62709bed11248fa73e540decbb53ca03a437921e
488--- /dev/null
489+++ b/server/internal/cmd.go
490@@ -0,0 +1,84 @@
491+package internal
492+
493+import (
494+ "context"
495+
496+ "github.com/charmbracelet/soft-serve/server/config"
497+ "github.com/charmbracelet/soft-serve/server/hooks"
498+ "github.com/charmbracelet/ssh"
499+ "github.com/charmbracelet/wish"
500+ "github.com/spf13/cobra"
501+)
502+
503+var (
504+ hooksCtxKey = "hooks"
505+ sessionCtxKey = "session"
506+ configCtxKey = "config"
507+)
508+
509+// rootCommand is the root command for the server.
510+func rootCommand(cfg *config.Config, s ssh.Session) *cobra.Command {
511+ rootCmd := &cobra.Command{
512+ Short: "Soft Serve internal API.",
513+ SilenceUsage: true,
514+ }
515+
516+ rootCmd.SetIn(s)
517+ rootCmd.SetOut(s)
518+ rootCmd.SetErr(s)
519+ rootCmd.CompletionOptions.DisableDefaultCmd = true
520+
521+ rootCmd.AddCommand(
522+ hookCommand(),
523+ )
524+
525+ return rootCmd
526+}
527+
528+// Middleware returns the middleware for the server.
529+func (i *InternalServer) Middleware(hooks hooks.Hooks) wish.Middleware {
530+ return func(sh ssh.Handler) ssh.Handler {
531+ return func(s ssh.Session) {
532+ _, _, active := s.Pty()
533+ if active {
534+ return
535+ }
536+
537+ // Ignore git server commands.
538+ args := s.Command()
539+ if len(args) > 0 {
540+ if args[0] == "git-receive-pack" ||
541+ args[0] == "git-upload-pack" ||
542+ args[0] == "git-upload-archive" {
543+ return
544+ }
545+ }
546+
547+ ctx := context.WithValue(s.Context(), hooksCtxKey, hooks)
548+ ctx = context.WithValue(ctx, sessionCtxKey, s)
549+ ctx = context.WithValue(ctx, configCtxKey, i.cfg)
550+
551+ rootCmd := rootCommand(i.cfg, s)
552+ rootCmd.SetArgs(args)
553+ if len(args) == 0 {
554+ // otherwise it'll default to os.Args, which is not what we want.
555+ rootCmd.SetArgs([]string{"--help"})
556+ }
557+ rootCmd.SetIn(s)
558+ rootCmd.SetOut(s)
559+ rootCmd.CompletionOptions.DisableDefaultCmd = true
560+ rootCmd.SetErr(s.Stderr())
561+ if err := rootCmd.ExecuteContext(ctx); err != nil {
562+ _ = s.Exit(1)
563+ }
564+ sh(s)
565+ }
566+ }
567+}
568+
569+func fromContext(cmd *cobra.Command) (*config.Config, ssh.Session) {
570+ ctx := cmd.Context()
571+ cfg := ctx.Value(configCtxKey).(*config.Config)
572+ s := ctx.Value(sessionCtxKey).(ssh.Session)
573+ return cfg, s
574+}
575diff --git a/server/cmd/hook.go b/server/internal/hook.go
576rename from server/cmd/hook.go
577rename to server/internal/hook.go
578index 7e520e5c8dc0a774c25a0eb0a15845219fbf820d..a4d4ecbaf7254f85ea798f0026009b8154724ba7 100644
579--- a/server/cmd/hook.go
580+++ b/server/internal/hook.go
581@@ -1,4 +1,4 @@
582-package cmd
583+package internal
584
585 import (
586 "bufio"
587@@ -6,7 +6,9 @@ import (
588 "strings"
589
590 "github.com/charmbracelet/keygen"
591+ "github.com/charmbracelet/log"
592 "github.com/charmbracelet/soft-serve/server/backend"
593+ "github.com/charmbracelet/soft-serve/server/errors"
594 "github.com/charmbracelet/soft-serve/server/hooks"
595 "github.com/charmbracelet/ssh"
596 "github.com/spf13/cobra"
597@@ -15,12 +17,11 @@ import (
598 // hookCommand handles Soft Serve internal API git hook requests.
599 func hookCommand() *cobra.Command {
600 preReceiveCmd := &cobra.Command{
601- Use: "pre-receive",
602- Short: "Run git pre-receive hook",
603- PersistentPreRunE: checkIfInternal,
604+ Use: "pre-receive",
605+ Short: "Run git pre-receive hook",
606 RunE: func(cmd *cobra.Command, args []string) error {
607 _, s := fromContext(cmd)
608- hks := cmd.Context().Value(HooksCtxKey).(hooks.Hooks)
609+ hks := cmd.Context().Value(hooksCtxKey).(hooks.Hooks)
610 repoName := getRepoName(s)
611 opts := make([]hooks.HookArg, 0)
612 scanner := bufio.NewScanner(s)
613@@ -35,21 +36,20 @@ func hookCommand() *cobra.Command {
614 RefName: fields[2],
615 })
616 }
617- hks.PreReceive(s, s.Stderr(), repoName, opts)
618+ hks.PreReceive(s, s, s.Stderr(), repoName, opts)
619 return nil
620 },
621 }
622
623 updateCmd := &cobra.Command{
624- Use: "update",
625- Short: "Run git update hook",
626- Args: cobra.ExactArgs(3),
627- PersistentPreRunE: checkIfInternal,
628+ Use: "update",
629+ Short: "Run git update hook",
630+ Args: cobra.ExactArgs(3),
631 RunE: func(cmd *cobra.Command, args []string) error {
632 _, s := fromContext(cmd)
633- hks := cmd.Context().Value(HooksCtxKey).(hooks.Hooks)
634+ hks := cmd.Context().Value(hooksCtxKey).(hooks.Hooks)
635 repoName := getRepoName(s)
636- hks.Update(s, s.Stderr(), repoName, hooks.HookArg{
637+ hks.Update(s, s, s.Stderr(), repoName, hooks.HookArg{
638 RefName: args[0],
639 OldSha: args[1],
640 NewSha: args[2],
641@@ -59,12 +59,11 @@ func hookCommand() *cobra.Command {
642 }
643
644 postReceiveCmd := &cobra.Command{
645- Use: "post-receive",
646- Short: "Run git post-receive hook",
647- PersistentPreRunE: checkIfInternal,
648+ Use: "post-receive",
649+ Short: "Run git post-receive hook",
650 RunE: func(cmd *cobra.Command, _ []string) error {
651 _, s := fromContext(cmd)
652- hks := cmd.Context().Value(HooksCtxKey).(hooks.Hooks)
653+ hks := cmd.Context().Value(hooksCtxKey).(hooks.Hooks)
654 repoName := getRepoName(s)
655 opts := make([]hooks.HookArg, 0)
656 scanner := bufio.NewScanner(s)
657@@ -79,20 +78,19 @@ func hookCommand() *cobra.Command {
658 RefName: fields[2],
659 })
660 }
661- hks.PostReceive(s, s.Stderr(), repoName, opts)
662+ hks.PostReceive(s, s, s.Stderr(), repoName, opts)
663 return nil
664 },
665 }
666
667 postUpdateCmd := &cobra.Command{
668- Use: "post-update",
669- Short: "Run git post-update hook",
670- PersistentPreRunE: checkIfInternal,
671+ Use: "post-update",
672+ Short: "Run git post-update hook",
673 RunE: func(cmd *cobra.Command, args []string) error {
674 _, s := fromContext(cmd)
675- hks := cmd.Context().Value(HooksCtxKey).(hooks.Hooks)
676+ hks := cmd.Context().Value(hooksCtxKey).(hooks.Hooks)
677 repoName := getRepoName(s)
678- hks.PostUpdate(s, s.Stderr(), repoName, args...)
679+ hks.PostUpdate(s, s, s.Stderr(), repoName, args...)
680 return nil
681diff --git a/server/internal/internal.go b/server/internal/internal.go
682new file mode 100644
683index 0000000000000000000000000000000000000000..52114c4ca0afbfdc2fd1da7fa81d914c68139777
684--- /dev/null
685+++ b/server/internal/internal.go
686@@ -0,0 +1,86 @@
687+package internal
688+
689+import (
690+ "context"
691+ "fmt"
692+
693+ "github.com/charmbracelet/keygen"
694+ "github.com/charmbracelet/soft-serve/server/backend"
695+ "github.com/charmbracelet/soft-serve/server/config"
696+ "github.com/charmbracelet/soft-serve/server/hooks"
697+ "github.com/charmbracelet/ssh"
698+ "github.com/charmbracelet/wish"
699+)
700+
701+// InternalServer is a internal interface to communicate with the server.
702+type InternalServer struct {
703+ cfg *config.Config
704+ s *ssh.Server
705+ kp *keygen.SSHKeyPair
706+ ckp *keygen.SSHKeyPair
707+}
708+
709+// NewInternalServer returns a new internal server.
710+func NewInternalServer(cfg *config.Config, hooks hooks.Hooks) (*InternalServer, error) {
711+ i := &InternalServer{cfg: cfg}
712+
713+ // Create internal key.
714+ ikp, err := keygen.New(
715+ cfg.Internal.InternalKeyPath,
716+ keygen.WithKeyType(keygen.Ed25519),
717+ keygen.WithWrite(),
718+ )
719+ if err != nil {
720+ return nil, fmt.Errorf("internal key: %w", err)
721+ }
722+
723+ i.kp = ikp
724+
725+ // Create client key.
726+ ckp, err := keygen.New(
727+ cfg.Internal.ClientKeyPath,
728+ keygen.WithKeyType(keygen.Ed25519),
729+ keygen.WithWrite(),
730+ )
731+ if err != nil {
732+ return nil, fmt.Errorf("client key: %w", err)
733+ }
734+
735+ i.ckp = ckp
736+
737+ s, err := wish.NewServer(
738+ wish.WithAddress(cfg.Internal.ListenAddr),
739+ wish.WithHostKeyPath(cfg.Internal.KeyPath),
740+ wish.WithPublicKeyAuth(i.PublicKeyHandler),
741+ wish.WithMiddleware(
742+ i.Middleware(hooks),
743+ ),
744+ )
745+ if err != nil {
746+ return nil, fmt.Errorf("wish: %w", err)
747+ }
748+
749+ i.s = s
750+
751+ return i, nil
752+}
753+
754+// PublicKeyHandler handles public key authentication.
755+func (i *InternalServer) PublicKeyHandler(ctx ssh.Context, pk ssh.PublicKey) bool {
756+ return backend.KeysEqual(i.kp.PublicKey(), pk)
757+}
758+
759+// Start starts the internal server.
760+func (i *InternalServer) Start() error {
761+ return i.s.ListenAndServe()
762+}
763+
764+// Shutdown shuts down the internal server.
765+func (i *InternalServer) Shutdown(ctx context.Context) error {
766+ return i.s.Shutdown(ctx)
767+}
768+
769+// Close closes the internal server.
770+func (i *InternalServer) Close() error {
771+ return i.s.Close()
772+}
773diff --git a/server/jobs.go b/server/jobs.go
774index e68c7b83c4527462ef89220c3c46a43f0cd66b16..37ecbeb36a33c540a38bfd5ace33615f0cce05ce 100644
775--- a/server/jobs.go
776+++ b/server/jobs.go
777@@ -38,8 +38,7 @@ func mirrorJob(cfg *config.Config) func() {
778 cmd.AddEnvs(
779 fmt.Sprintf(`GIT_SSH_COMMAND=ssh -o UserKnownHostsFile="%s" -o StrictHostKeyChecking=no -i "%s"`,
780 filepath.Join(cfg.DataPath, "ssh", "known_hosts"),
781- // FIXME: upstream keygen appends _ed25519 to the key path.
782- cfg.SSH.ClientKeyPath+"_ed25519",
783+ cfg.Internal.ClientKeyPath,
784 ),
785 )
786 if _, err := cmd.RunInDir(r.Path); err != nil {
787diff --git a/server/server.go b/server/server.go
788index 84fe0fda6292303e9610fb63fe7b76b1d16bd498..ecc2c7c357897d71adc4610fb252ca357a291470 100644
789--- a/server/server.go
790+++ b/server/server.go
791@@ -3,9 +3,9 @@ package server
792 import (
793 "context"
794 "errors"
795+ "fmt"
796 "net/http"
797
798- "github.com/charmbracelet/keygen"
799 "github.com/charmbracelet/log"
800
801 "github.com/charmbracelet/soft-serve/server/backend"
802@@ -13,6 +13,7 @@ import (
803 "github.com/charmbracelet/soft-serve/server/config"
804 "github.com/charmbracelet/soft-serve/server/cron"
805 "github.com/charmbracelet/soft-serve/server/daemon"
806+ "github.com/charmbracelet/soft-serve/server/internal"
807 sshsrv "github.com/charmbracelet/soft-serve/server/ssh"
808 "github.com/charmbracelet/soft-serve/server/stats"
809 "github.com/charmbracelet/soft-serve/server/web"
810@@ -26,14 +27,15 @@ var (
811
812 // Server is the Soft Serve server.
813 type Server struct {
814- SSHServer *sshsrv.SSHServer
815- GitDaemon *daemon.GitDaemon
816- HTTPServer *web.HTTPServer
817- StatsServer *stats.StatsServer
818- Cron *cron.CronScheduler
819- Config *config.Config
820- Backend backend.Backend
821- ctx context.Context
822+ SSHServer *sshsrv.SSHServer
823+ GitDaemon *daemon.GitDaemon
824+ HTTPServer *web.HTTPServer
825+ StatsServer *stats.StatsServer
826+ InternalServer *internal.InternalServer
827+ Cron *cron.CronScheduler
828+ Config *config.Config
829+ Backend backend.Backend
830+ ctx context.Context
831 }
832
833 // NewServer returns a new *ssh.Server configured to serve Soft Serve. The SSH
834@@ -46,32 +48,10 @@ func NewServer(ctx context.Context, cfg *config.Config) (*Server, error) {
835 if cfg.Backend == nil {
836 sb, err := sqlite.NewSqliteBackend(ctx, cfg)
837 if err != nil {
838- logger.Fatal(err)
839+ return nil, fmt.Errorf("create backend: %w", err)
840 }
841
842 cfg = cfg.WithBackend(sb)
843-
844- // Create internal key.
845- ikp, err := keygen.New(
846- cfg.SSH.InternalKeyPath,
847- keygen.WithKeyType(keygen.Ed25519),
848- keygen.WithWrite(),
849- )
850- if err != nil {
851- return nil, err
852- }
853- cfg.InternalPublicKey = ikp.AuthorizedKey()
854-
855- // Create client key.
856- ckp, err := keygen.New(
857- cfg.SSH.ClientKeyPath,
858- keygen.WithKeyType(keygen.Ed25519),
859- keygen.WithWrite(),
860- )
861- if err != nil {
862- return nil, err
863- }
864- cfg.ClientPublicKey = ckp.AuthorizedKey()
865 }
866
867 srv := &Server{
868@@ -84,24 +64,29 @@ func NewServer(ctx context.Context, cfg *config.Config) (*Server, error) {
869 // Add cron jobs.
870 srv.Cron.AddFunc(jobSpecs["mirror"], mirrorJob(cfg))
871
872- srv.SSHServer, err = sshsrv.NewSSHServer(cfg, srv)
873+ srv.SSHServer, err = sshsrv.NewSSHServer(cfg)
874 if err != nil {
875- return nil, err
876+ return nil, fmt.Errorf("create ssh server: %w", err)
877 }
878
879 srv.GitDaemon, err = daemon.NewGitDaemon(cfg)
880 if err != nil {
881- return nil, err
882+ return nil, fmt.Errorf("create git daemon: %w", err)
883 }
884
885 srv.HTTPServer, err = web.NewHTTPServer(cfg)
886 if err != nil {
887- return nil, err
888+ return nil, fmt.Errorf("create http server: %w", err)
889 }
890
891diff --git a/server/ssh/session.go b/server/ssh/session.go
892index 754fdbe68aeaeef79c379127339da9450d51d509..92b65dde70e93b393a30b5259fefe29de71ab40b 100644
893--- a/server/ssh/session.go
894+++ b/server/ssh/session.go
895@@ -6,8 +6,8 @@ import (
896 "github.com/aymanbagabas/go-osc52"
897 tea "github.com/charmbracelet/bubbletea"
898 "github.com/charmbracelet/soft-serve/server/backend"
899- cm "github.com/charmbracelet/soft-serve/server/cmd"
900 "github.com/charmbracelet/soft-serve/server/config"
901+ "github.com/charmbracelet/soft-serve/server/errors"
902 "github.com/charmbracelet/soft-serve/ui"
903 "github.com/charmbracelet/soft-serve/ui/common"
904 "github.com/charmbracelet/ssh"
905@@ -41,7 +41,7 @@ func SessionHandler(cfg *config.Config) bm.ProgramHandler {
906 initialRepo = cmd[0]
907 auth := cfg.Backend.AccessLevelByPublicKey(initialRepo, s.PublicKey())
908 if auth < backend.ReadOnlyAccess {
909- wish.Fatalln(s, cm.ErrUnauthorized)
910+ wish.Fatalln(s, errors.ErrUnauthorized)
911 return nil
912 }
913 }
914diff --git a/server/ssh/ssh.go b/server/ssh/ssh.go
915index 3477419fd596c9cfd3d8e124babcb459c0166fe0..a407d8fc929541f26ff18baba0e511163a247775 100644
916--- a/server/ssh/ssh.go
917+++ b/server/ssh/ssh.go
918@@ -14,7 +14,6 @@ import (
919 cm "github.com/charmbracelet/soft-serve/server/cmd"
920 "github.com/charmbracelet/soft-serve/server/config"
921 "github.com/charmbracelet/soft-serve/server/git"
922- "github.com/charmbracelet/soft-serve/server/hooks"
923 "github.com/charmbracelet/soft-serve/server/utils"
924 "github.com/charmbracelet/ssh"
925 "github.com/charmbracelet/wish"
926@@ -82,7 +81,7 @@ type SSHServer struct {
927 }
928
929 // NewSSHServer returns a new SSHServer.
930-func NewSSHServer(cfg *config.Config, hooks hooks.Hooks) (*SSHServer, error) {
931+func NewSSHServer(cfg *config.Config) (*SSHServer, error) {
932 var err error
933 s := &SSHServer{cfg: cfg}
934 logger := logger.StandardLog(log.StandardLogOptions{ForceLevel: log.DebugLevel})
935@@ -92,7 +91,7 @@ func NewSSHServer(cfg *config.Config, hooks hooks.Hooks) (*SSHServer, error) {
936 // BubbleTea middleware.
937 bm.MiddlewareWithProgramHandler(SessionHandler(cfg), termenv.ANSI256),
938 // CLI middleware.
939- cm.Middleware(cfg, hooks),
940+ cm.Middleware(cfg),
941 // Git middleware.
942 s.Middleware(cfg),
943 // Logging middleware.