ssh.go

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