ssh.go

 1package ssh
 2
 3import (
 4	"fmt"
 5	"log"
 6	"path/filepath"
 7	"strings"
 8
 9	"github.com/charmbracelet/soft-serve/server/git"
10	"github.com/charmbracelet/wish"
11	"github.com/gliderlabs/ssh"
12)
13
14// Middleware adds Git server functionality to the ssh.Server. Repos are stored
15// in the specified repo directory. The provided Hooks implementation will be
16// checked for access on a per repo basis for a ssh.Session public key.
17// Hooks.Push and Hooks.Fetch will be called on successful completion of
18// their commands.
19func Middleware(repoDir string, gh git.Hooks) wish.Middleware {
20	return func(sh ssh.Handler) ssh.Handler {
21		return func(s ssh.Session) {
22			func() {
23				cmd := s.Command()
24				if len(cmd) == 2 && strings.HasPrefix(cmd[0], "git") {
25					gc := cmd[0]
26					// repo should be in the form of "repo.git"
27					repo := strings.TrimPrefix(cmd[1], "/")
28					repo = filepath.Clean(repo)
29					if strings.Contains(repo, "/") {
30						log.Printf("invalid repo: %s", repo)
31						Fatal(s, fmt.Errorf("%s: %s", git.ErrInvalidRepo, "user repos not supported"))
32						return
33					}
34					pk := s.PublicKey()
35					access := gh.AuthRepo(strings.TrimSuffix(repo, ".git"), pk)
36					// git bare repositories should end in ".git"
37					// https://git-scm.com/docs/gitrepository-layout
38					if !strings.HasSuffix(repo, ".git") {
39						repo += ".git"
40					}
41					switch gc {
42					case "git-receive-pack":
43						switch access {
44						case git.ReadWriteAccess, git.AdminAccess:
45							err := git.GitPack(s, s, s.Stderr(), gc, repoDir, repo)
46							if err != nil {
47								Fatal(s, git.ErrSystemMalfunction)
48							} else {
49								gh.Push(repo, pk)
50							}
51						default:
52							Fatal(s, git.ErrNotAuthed)
53						}
54						return
55					case "git-upload-archive", "git-upload-pack":
56						switch access {
57						case git.ReadOnlyAccess, git.ReadWriteAccess, git.AdminAccess:
58							// try to upload <repo>.git first, then <repo>
59							err := git.GitPack(s, s, s.Stderr(), gc, repoDir, repo)
60							if err != nil {
61								err = git.GitPack(s, s, s.Stderr(), gc, repoDir, strings.TrimSuffix(repo, ".git"))
62							}
63							switch err {
64							case git.ErrInvalidRepo:
65								Fatal(s, git.ErrInvalidRepo)
66							case nil:
67								gh.Fetch(repo, pk)
68							default:
69								log.Printf("unknown git error: %s", err)
70								Fatal(s, git.ErrSystemMalfunction)
71							}
72						default:
73							Fatal(s, git.ErrNotAuthed)
74						}
75						return
76					}
77				}
78			}()
79			sh(s)
80		}
81	}
82}
83
84// Fatal prints to the session's STDOUT as a git response and exit 1.
85func Fatal(s ssh.Session, v ...interface{}) {
86	git.WritePktline(s, v...)
87	s.Exit(1) // nolint: errcheck
88}