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() *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 // TODO: use command usage template to include hostname and port
55 rootCmd.CompletionOptions.DisableDefaultCmd = true
56 rootCmd.AddCommand(
57 adminCommand(),
58 blobCommand(),
59 branchCommand(),
60 collabCommand(),
61 createCommand(),
62 deleteCommand(),
63 descriptionCommand(),
64 hookCommand(),
65 listCommand(),
66 privateCommand(),
67 projectName(),
68 renameCommand(),
69 settingCommand(),
70 tagCommand(),
71 treeCommand(),
72 )
73
74 return rootCmd
75}
76
77func fromContext(cmd *cobra.Command) (*config.Config, ssh.Session) {
78 ctx := cmd.Context()
79 cfg := ctx.Value(ConfigCtxKey).(*config.Config)
80 s := ctx.Value(SessionCtxKey).(ssh.Session)
81 return cfg, s
82}
83
84func checkIfReadable(cmd *cobra.Command, args []string) error {
85 var repo string
86 if len(args) > 0 {
87 repo = args[0]
88 }
89 cfg, s := fromContext(cmd)
90 rn := utils.SanitizeRepo(repo)
91 auth := cfg.Backend.AccessLevel(rn, s.PublicKey())
92 if auth < backend.ReadOnlyAccess {
93 return ErrUnauthorized
94 }
95 return nil
96}
97
98func checkIfAdmin(cmd *cobra.Command, args []string) error {
99 cfg, s := fromContext(cmd)
100 if !cfg.Backend.IsAdmin(s.PublicKey()) {
101 return ErrUnauthorized
102 }
103 return nil
104}
105
106func checkIfCollab(cmd *cobra.Command, args []string) error {
107 var repo string
108 if len(args) > 0 {
109 repo = args[0]
110 }
111 cfg, s := fromContext(cmd)
112 rn := utils.SanitizeRepo(repo)
113 auth := cfg.Backend.AccessLevel(rn, s.PublicKey())
114 if auth < backend.ReadWriteAccess {
115 return ErrUnauthorized
116 }
117 return nil
118}
119
120// Middleware is the Soft Serve middleware that handles SSH commands.
121func Middleware(cfg *config.Config, hooks hooks.Hooks) wish.Middleware {
122 return func(sh ssh.Handler) ssh.Handler {
123 return func(s ssh.Session) {
124 func() {
125 _, _, active := s.Pty()
126 if active {
127 return
128 }
129
130 // Ignore git server commands.
131 args := s.Command()
132 if len(args) > 0 {
133 if args[0] == "git-receive-pack" ||
134 args[0] == "git-upload-pack" ||
135 args[0] == "git-upload-archive" {
136 return
137 }
138 }
139
140 ctx := context.WithValue(s.Context(), ConfigCtxKey, cfg)
141 ctx = context.WithValue(ctx, SessionCtxKey, s)
142 ctx = context.WithValue(ctx, HooksCtxKey, hooks)
143
144 rootCmd := rootCommand()
145 rootCmd.SetArgs(args)
146 if len(args) == 0 {
147 // otherwise it'll default to os.Args, which is not what we want.
148 rootCmd.SetArgs([]string{"--help"})
149 }
150 rootCmd.SetIn(s)
151 rootCmd.SetOut(s)
152 rootCmd.CompletionOptions.DisableDefaultCmd = true
153 rootCmd.SetErr(s.Stderr())
154 if err := rootCmd.ExecuteContext(ctx); err != nil {
155 _ = s.Exit(1)
156 }
157 }()
158 sh(s)
159 }
160 }
161}