1package cmd
2
3import (
4 "context"
5 "fmt"
6
7 "github.com/charmbracelet/soft-serve/server/backend"
8 "github.com/charmbracelet/soft-serve/server/config"
9 "github.com/charmbracelet/soft-serve/server/utils"
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 blobCommand(),
59 tagCommand(),
60 treeCommand(),
61 )
62
63 return rootCmd
64}
65
66func fromContext(cmd *cobra.Command) (*config.Config, ssh.Session) {
67 ctx := cmd.Context()
68 cfg := ctx.Value(ConfigCtxKey).(*config.Config)
69 s := ctx.Value(SessionCtxKey).(ssh.Session)
70 return cfg, s
71}
72
73func checkIfReadable(cmd *cobra.Command, args []string) error {
74 var repo string
75 if len(args) > 0 {
76 repo = args[0]
77 }
78 cfg, s := fromContext(cmd)
79 rn := utils.SanitizeRepo(repo)
80 auth := cfg.Backend.AccessLevel(rn, s.PublicKey())
81 if auth < backend.ReadOnlyAccess {
82 return ErrUnauthorized
83 }
84 return nil
85}
86
87func checkIfAdmin(cmd *cobra.Command, args []string) error {
88 cfg, s := fromContext(cmd)
89 if !cfg.Backend.IsAdmin(s.PublicKey()) {
90 return ErrUnauthorized
91 }
92 return nil
93}
94
95func checkIfCollab(cmd *cobra.Command, args []string) error {
96 var repo string
97 if len(args) > 0 {
98 repo = args[0]
99 }
100 cfg, s := fromContext(cmd)
101 rn := utils.SanitizeRepo(repo)
102 auth := cfg.Backend.AccessLevel(rn, s.PublicKey())
103 if auth < backend.ReadWriteAccess {
104 return ErrUnauthorized
105 }
106 return nil
107}
108
109// Middleware is the Soft Serve middleware that handles SSH commands.
110func Middleware(cfg *config.Config) wish.Middleware {
111 return func(sh ssh.Handler) ssh.Handler {
112 return func(s ssh.Session) {
113 func() {
114 _, _, active := s.Pty()
115 if active {
116 return
117 }
118
119 // Ignore git server commands.
120 args := s.Command()
121 if len(args) > 0 {
122 if args[0] == "git-receive-pack" ||
123 args[0] == "git-upload-pack" ||
124 args[0] == "git-upload-archive" {
125 return
126 }
127 }
128
129 ctx := context.WithValue(s.Context(), ConfigCtxKey, cfg)
130 ctx = context.WithValue(ctx, SessionCtxKey, s)
131
132 rootCmd := rootCommand()
133 rootCmd.SetArgs(args)
134 if len(args) == 0 {
135 // otherwise it'll default to os.Args, which is not what we want.
136 rootCmd.SetArgs([]string{"--help"})
137 }
138 rootCmd.SetIn(s)
139 rootCmd.SetOut(s)
140 rootCmd.CompletionOptions.DisableDefaultCmd = true
141 rootCmd.SetErr(s.Stderr())
142 if err := rootCmd.ExecuteContext(ctx); err != nil {
143 _ = s.Exit(1)
144 }
145 }()
146 sh(s)
147 }
148 }
149}