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}