ssh.go

 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}