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