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}