1package cmd
2
3import (
4 "context"
5 "fmt"
6 "strings"
7
8 "github.com/charmbracelet/soft-serve/server/backend"
9 "github.com/charmbracelet/soft-serve/server/config"
10 "github.com/charmbracelet/ssh"
11 "github.com/charmbracelet/wish"
12 "github.com/spf13/cobra"
13)
14
15// ContextKey is a type that can be used as a key in a context.
16type ContextKey string
17
18// String returns the string representation of the ContextKey.
19func (c ContextKey) String() string {
20 return string(c) + "ContextKey"
21}
22
23var (
24 // ConfigCtxKey is the key for the config in the context.
25 ConfigCtxKey = ContextKey("config")
26 // SessionCtxKey is the key for the session in the context.
27 SessionCtxKey = ContextKey("session")
28)
29
30var (
31 // ErrUnauthorized is returned when the user is not authorized to perform action.
32 ErrUnauthorized = fmt.Errorf("Unauthorized")
33 // ErrRepoNotFound is returned when the repo is not found.
34 ErrRepoNotFound = fmt.Errorf("Repository not found")
35 // ErrFileNotFound is returned when the file is not found.
36 ErrFileNotFound = fmt.Errorf("File not found")
37)
38
39// rootCommand is the root command for the server.
40func rootCommand() *cobra.Command {
41 rootCmd := &cobra.Command{
42 Use: "soft",
43 Short: "Soft Serve is a self-hostable Git server for the command line.",
44 SilenceUsage: true,
45 }
46 // TODO: use command usage template to include hostname and port
47 rootCmd.CompletionOptions.DisableDefaultCmd = true
48 rootCmd.AddCommand(
49 adminCommand(),
50 branchCommand(),
51 collabCommand(),
52 createCommand(),
53 deleteCommand(),
54 descriptionCommand(),
55 listCommand(),
56 privateCommand(),
57 renameCommand(),
58 showCommand(),
59 tagCommand(),
60 )
61
62 return rootCmd
63}
64
65func fromContext(cmd *cobra.Command) (*config.Config, ssh.Session) {
66 ctx := cmd.Context()
67 cfg := ctx.Value(ConfigCtxKey).(*config.Config)
68 s := ctx.Value(SessionCtxKey).(ssh.Session)
69 return cfg, s
70}
71
72func checkIfReadable(cmd *cobra.Command, args []string) error {
73 var repo string
74 if len(args) > 0 {
75 repo = args[0]
76 }
77 cfg, s := fromContext(cmd)
78 rn := strings.TrimSuffix(repo, ".git")
79 auth := cfg.Access.AccessLevel(rn, s.PublicKey())
80 if auth < backend.ReadOnlyAccess {
81 return ErrUnauthorized
82 }
83 return nil
84}
85
86func checkIfAdmin(cmd *cobra.Command, args []string) error {
87 cfg, s := fromContext(cmd)
88 if !cfg.Backend.IsAdmin(s.PublicKey()) {
89 return ErrUnauthorized
90 }
91 return nil
92}
93
94func checkIfCollab(cmd *cobra.Command, args []string) error {
95 var repo string
96 if len(args) > 0 {
97 repo = args[0]
98 }
99 cfg, s := fromContext(cmd)
100 rn := strings.TrimSuffix(repo, ".git")
101 auth := cfg.Access.AccessLevel(rn, s.PublicKey())
102 if auth < backend.ReadWriteAccess {
103 return ErrUnauthorized
104 }
105 return nil
106}
107
108// Middleware is the Soft Serve middleware that handles SSH commands.
109func Middleware(cfg *config.Config) wish.Middleware {
110 return func(sh ssh.Handler) ssh.Handler {
111 return func(s ssh.Session) {
112 func() {
113 _, _, active := s.Pty()
114 if active {
115 return
116 }
117
118 // Ignore git server commands.
119 args := s.Command()
120 if len(args) > 0 {
121 if args[0] == "git-receive-pack" ||
122 args[0] == "git-upload-pack" ||
123 args[0] == "git-upload-archive" {
124 return
125 }
126 }
127
128 ctx := context.WithValue(s.Context(), ConfigCtxKey, cfg)
129 ctx = context.WithValue(ctx, SessionCtxKey, s)
130
131 rootCmd := rootCommand()
132 rootCmd.SetArgs(args)
133 if len(args) == 0 {
134 // otherwise it'll default to os.Args, which is not what we want.
135 rootCmd.SetArgs([]string{"--help"})
136 }
137 rootCmd.SetIn(s)
138 rootCmd.SetOut(s)
139 rootCmd.CompletionOptions.DisableDefaultCmd = true
140 rootCmd.SetErr(s.Stderr())
141 if err := rootCmd.ExecuteContext(ctx); err != nil {
142 _ = s.Exit(1)
143 }
144 }()
145 sh(s)
146 }
147 }
148}