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}