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