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, auth proto.Access) 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 := auth.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							}
50						default:
51							Fatal(s, git.ErrNotAuthed)
52						}
53						return
54					case "git-upload-archive", "git-upload-pack":
55						switch access {
56						case proto.ReadOnlyAccess, proto.ReadWriteAccess, proto.AdminAccess:
57							// try to upload <repo>.git first, then <repo>
58							err := git.GitPack(s, s, s.Stderr(), gc, repoDir, repo)
59							if err != nil {
60								err = git.GitPack(s, s, s.Stderr(), gc, repoDir, strings.TrimSuffix(repo, ".git"))
61							}
62							switch err {
63							case git.ErrInvalidRepo:
64								Fatal(s, git.ErrInvalidRepo)
65							case nil:
66							default:
67								log.Printf("unknown git error: %s", err)
68								Fatal(s, git.ErrSystemMalfunction)
69							}
70						default:
71							Fatal(s, git.ErrNotAuthed)
72						}
73						return
74					}
75				}
76			}()
77			sh(s)
78		}
79	}
80}
81
82// Fatal prints to the session's STDOUT as a git response and exit 1.
83func Fatal(s ssh.Session, v ...interface{}) {
84	git.WritePktline(s, v...)
85	s.Exit(1) // nolint: errcheck
86}