cmd/gitbayd/main.go
312 lines · 7981 bytes
1// gitbayd is the forge server daemon. The same binary also runs in hook mode
2// (invoked by git via core.hooksPath) and hosts the host-local admin commands.
3package main
4
5import (
6 "fmt"
7 "log/slog"
8 "net"
9 "net/http"
10 "os"
11 "path/filepath"
12 "strconv"
13
14 "github.com/spf13/cobra"
15 "golang.org/x/crypto/ssh"
16
17 "gitbay.org/gitbay/internal/config"
18 "gitbay.org/gitbay/internal/control"
19 "gitbay.org/gitbay/internal/gitd"
20 "gitbay.org/gitbay/internal/hookd"
21 "gitbay.org/gitbay/internal/httpd"
22 "gitbay.org/gitbay/internal/policy"
23 "gitbay.org/gitbay/internal/sshd"
24 "gitbay.org/gitbay/internal/store"
25)
26
27func openStore(cfg config.Config) (*store.Store, error) {
28 s, err := store.Open(filepath.Join(cfg.Server.Root, "gitbay.db"))
29 if err != nil {
30 return nil, err
31 }
32 if err := s.MigrateUp(); err != nil {
33 s.Close()
34 return nil, err
35 }
36 return s, nil
37}
38
39var configPath string
40
41func main() {
42 root := &cobra.Command{
43 Use: "gitbayd",
44 Short: "gitbay server daemon",
45 SilenceUsage: true,
46 SilenceErrors: true,
47 }
48 root.PersistentFlags().StringVar(&configPath, "config", "/etc/gitbay/config.toml", "path to config file")
49
50 root.AddCommand(
51 checkConfigCmd(),
52 serveCmd(),
53 migrateCmd(),
54 adminCmd(),
55 hookCmd(),
56 authorizedKeysCmd(),
57 shellCmd(),
58 )
59
60 if err := root.Execute(); err != nil {
61 fmt.Fprintln(os.Stderr, "gitbayd:", err)
62 os.Exit(1)
63 }
64}
65
66func checkConfigCmd() *cobra.Command {
67 var noHost bool
68 cmd := &cobra.Command{
69 Use: "check-config",
70 Short: "validate the configuration and exit",
71 RunE: func(cmd *cobra.Command, args []string) error {
72 cfg, err := config.Load(configPath)
73 if err != nil {
74 return err
75 }
76 if !noHost {
77 if err := cfg.CheckHost(); err != nil {
78 return err
79 }
80 }
81 fmt.Println("config ok")
82 return nil
83 },
84 }
85 cmd.Flags().BoolVar(&noHost, "no-host-checks", false, "skip host environment probes (port binding, paths)")
86 return cmd
87}
88
89func serveCmd() *cobra.Command {
90 return &cobra.Command{
91 Use: "serve",
92 Short: "run the ssh, http, and git listeners",
93 RunE: func(cmd *cobra.Command, args []string) error {
94 cfg, err := config.Load(configPath)
95 if err != nil {
96 return err
97 }
98 st, err := openStore(cfg)
99 if err != nil {
100 return err
101 }
102 defer st.Close()
103
104 // Regenerate hook scripts so a moved binary self-heals, then
105 // start the hook policy socket.
106 self, err := os.Executable()
107 if err != nil {
108 return err
109 }
110 if err := hookd.WriteHookScripts(control.HooksDir(cfg.Server.Root), self); err != nil {
111 return err
112 }
113 stopHookd, err := hookd.Serve(cfg, st)
114 if err != nil {
115 return err
116 }
117 defer stopHookd()
118
119 errCh := make(chan error, 3)
120 if cfg.SSH.Mode == "embedded" {
121 srv, err := sshd.New(cfg, st)
122 if err != nil {
123 return err
124 }
125 ln, err := net.Listen("tcp", net.JoinHostPort("", strconv.Itoa(cfg.SSH.Port)))
126 if err != nil {
127 return err
128 }
129 slog.Info("ssh listening", "addr", ln.Addr())
130 go func() { errCh <- srv.Serve(ln) }()
131 } else {
132 // system mode: the host sshd owns the SSH port and invokes
133 // this binary via AuthorizedKeysCommand + forced command.
134 slog.Info("ssh handled by host sshd (ssh.mode = system)")
135 }
136
137
138 web := httpd.New(cfg, st)
139 hs := &http.Server{Addr: cfg.HTTP.Addr, Handler: web.Handler()}
140 go func() {
141 slog.Info("http listening", "addr", cfg.HTTP.Addr, "tls", cfg.HTTP.TLS)
142 switch cfg.HTTP.TLS {
143 case "off":
144 errCh <- hs.ListenAndServe()
145 case "files":
146 errCh <- hs.ListenAndServeTLS(cfg.HTTP.CertFile, cfg.HTTP.KeyFile)
147 default:
148 errCh <- fmt.Errorf("http.tls = %q not implemented yet; use \"files\" or \"off\"", cfg.HTTP.TLS)
149 }
150 }()
151
152 if cfg.GitDaemon.Enabled {
153 gln, err := net.Listen("tcp", net.JoinHostPort("", strconv.Itoa(cfg.GitDaemon.Port)))
154 if err != nil {
155 return err
156 }
157 slog.Info("git-daemon listening", "addr", gln.Addr())
158 go func() { errCh <- gitd.New(cfg, st).Serve(gln) }()
159 }
160
161 return <-errCh
162 },
163 }
164}
165
166func migrateCmd() *cobra.Command {
167 var to int
168 cmd := &cobra.Command{
169 Use: "migrate",
170 Short: "apply schema migrations",
171 RunE: func(cmd *cobra.Command, args []string) error {
172 cfg, err := config.Load(configPath)
173 if err != nil {
174 return err
175 }
176 s, err := store.Open(cfg.Server.Root + "/gitbay.db")
177 if err != nil {
178 return err
179 }
180 defer s.Close()
181 if err := s.MigrateTo(to); err != nil {
182 return err
183 }
184 v, err := s.Version()
185 if err != nil {
186 return err
187 }
188 fmt.Println("schema version", v)
189 return nil
190 },
191 }
192 cmd.Flags().IntVar(&to, "to", -1, "target schema version (-1 = latest)")
193 return cmd
194}
195
196func adminCmd() *cobra.Command {
197 admin := &cobra.Command{
198 Use: "admin",
199 Short: "host-local administration",
200 }
201 notImplemented := func(use, short string) *cobra.Command {
202 return &cobra.Command{
203 Use: use,
204 Short: short,
205 RunE: func(cmd *cobra.Command, args []string) error {
206 return fmt.Errorf("not implemented")
207 },
208 }
209 }
210 userCmd := &cobra.Command{Use: "user", Short: "manage users"}
211 userCmd.AddCommand(adminUserCreateCmd())
212 emailCmd := &cobra.Command{Use: "email", Short: "manage user emails"}
213 emailCmd.AddCommand(adminEmailVerifyCmd())
214 admin.AddCommand(
215 userCmd,
216 emailCmd,
217 notImplemented("invite", "issue registration invites"),
218 notImplemented("backup", "consistent backup: repos first, then database"),
219 notImplemented("gc", "run git gc across repositories"),
220 notImplemented("stats", "instance statistics"),
221 )
222 return admin
223}
224
225func adminUserCreateCmd() *cobra.Command {
226 var keyPath, email string
227 var verified, isAdmin bool
228 cmd := &cobra.Command{
229 Use: "create <username>",
230 Short: "create a user (host-local bootstrap; the only path in closed mode)",
231 Args: cobra.ExactArgs(1),
232 RunE: func(cmd *cobra.Command, args []string) error {
233 username := args[0]
234 if err := policy.ValidateOwnerName(username); err != nil {
235 return err
236 }
237 cfg, err := config.Load(configPath)
238 if err != nil {
239 return err
240 }
241 st, err := openStore(cfg)
242 if err != nil {
243 return err
244 }
245 defer st.Close()
246
247 uid, err := st.CreateUser(username, isAdmin)
248 if err != nil {
249 return err
250 }
251 if email != "" {
252 verifiedBy := ""
253 if verified {
254 verifiedBy = "admin"
255 }
256 if err := st.AddEmail(uid, email, verifiedBy, true); err != nil {
257 return err
258 }
259 }
260 if keyPath != "" {
261 raw, err := os.ReadFile(keyPath)
262 if err != nil {
263 return err
264 }
265 pub, _, _, _, err := ssh.ParseAuthorizedKey(raw)
266 if err != nil {
267 return fmt.Errorf("%s: not a public key in authorized_keys format: %w", keyPath, err)
268 }
269 fp := ssh.FingerprintSHA256(pub)
270 if err := st.AddSSHKey(uid, fp, pub.Type(), pub.Marshal(), "full"); err != nil {
271 return err
272 }
273 fmt.Println("key", fp)
274 }
275 fmt.Println("created user", username)
276 return nil
277 },
278 }
279 cmd.Flags().StringVar(&keyPath, "key", "", "path to an SSH public key to register")
280 cmd.Flags().StringVar(&email, "email", "", "primary email address")
281 cmd.Flags().BoolVar(&verified, "verified", false, "mark the email verified (admin assertion)")
282 cmd.Flags().BoolVar(&isAdmin, "admin", false, "grant instance admin")
283 return cmd
284}
285
286func adminEmailVerifyCmd() *cobra.Command {
287 return &cobra.Command{
288 Use: "verify <username> <address>",
289 Short: "mark an email verified by admin assertion",
290 Args: cobra.ExactArgs(2),
291 RunE: func(cmd *cobra.Command, args []string) error {
292 cfg, err := config.Load(configPath)
293 if err != nil {
294 return err
295 }
296 st, err := openStore(cfg)
297 if err != nil {
298 return err
299 }
300 defer st.Close()
301 u, err := st.UserByUsername(args[0])
302 if err != nil {
303 return fmt.Errorf("user %s: %w", args[0], err)
304 }
305 if err := st.VerifyEmail(u.ID, args[1], "admin"); err != nil {
306 return fmt.Errorf("no address %s on user %s", args[1], args[0])
307 }
308 fmt.Println("verified", args[1])
309 return nil
310 },
311 }
312}