package main import ( "flag" "fmt" "log" "net/http" "os" "path/filepath" "strconv" "strings" "time" ) func main() { root := flag.String("root", "/srv/basin", "data directory holding basin.db and repos/") flag.Usage = func() { fmt.Fprintln(os.Stderr, `usage: basin [-root dir] command serve [-listen addr] [-host name] [-open] [-ssh-user name] run the web server keys user home type key sshd AuthorizedKeysCommand shell user forced command for ssh git access invite print a single use registration code quota [size] show or set the per user disk quota, 0 disables user name create a user, password read from stdin passwd name set a user's password, read from stdin ban name | unban name block or restore an account delete name remove an account and its repositories feature owner/repo... | -owner/repo set or clear the featured projects, in order mail address send a test message through the configured mailer backup file write a consistent copy of the database`) } flag.Parse() args := flag.Args() if len(args) == 0 { flag.Usage() os.Exit(2) } switch args[0] { case "serve": serve(*root, args[1:]) case "keys": binary, _ := os.Executable() os.Exit(keys(*root, binary, args[1:])) case "hook": if len(args) != 3 { os.Exit(2) } os.Exit(hook(*root, args[1], args[2])) case "shell": if len(args) != 2 { os.Exit(2) } os.Exit(shell(*root, args[1])) case "quota": if len(args) == 2 { n, err := parseSize(args[1]) if err != nil { log.Fatal(err) } if err := os.WriteFile(filepath.Join(*root, "quota"), []byte(strconv.FormatInt(n, 10)+"\n"), 0o644); err != nil { log.Fatal(err) } } if q := readQuota(*root); q == 0 { fmt.Println("no quota") } else { fmt.Println(humanSize(q)) } case "invite": store := mustStore(*root) code, err := store.CreateInvite() if err != nil { log.Fatal(err) } fmt.Println(code) case "user": if len(args) != 2 { os.Exit(2) } store := mustStore(*root) var password string fmt.Fscanln(os.Stdin, &password) if _, err := store.CreateUser(args[1], password); err != nil { log.Fatal(err) } case "passwd": if len(args) != 2 { os.Exit(2) } store := mustStore(*root) u, err := store.UserByName(args[1]) if err != nil { log.Fatal("no such user") } var password string fmt.Fscanln(os.Stdin, &password) if err := store.SetPassword(u.ID, password); err != nil { log.Fatal(err) } store.DeleteSessions(u.ID) case "backup": if len(args) != 2 { os.Exit(2) } if err := mustStore(*root).Backup(args[1]); err != nil { log.Fatal(err) } case "mail": if len(args) != 2 { os.Exit(2) } if err := loadMailer(*root).Send(args[1], "test message from basin", "Mail delivery from this forge works.\n"); err != nil { log.Fatal(err) } fmt.Println("sent") case "feature": if len(args) < 2 { os.Exit(2) } store := mustStore(*root) for i, spec := range args[1:] { owner, name, ok := strings.Cut(strings.TrimPrefix(spec, "~"), "/") rank := i + 1 if strings.HasPrefix(spec, "-") { owner, name, ok = strings.Cut(strings.TrimPrefix(spec, "-~"), "/") rank = 0 } if !ok { log.Fatalf("%s: expected owner/repo", spec) } if err := store.SetFeatured(owner, name, rank); err != nil { log.Fatalf("%s: no such repository", spec) } } case "ban", "unban": if len(args) != 2 { os.Exit(2) } if err := mustStore(*root).SetBanned(args[1], args[0] == "ban"); err != nil { log.Fatal(err) } case "delete": if len(args) != 2 { os.Exit(2) } store := mustStore(*root) if _, err := store.UserByName(args[1]); err != nil { log.Fatal("no such user") } if err := store.DeleteUser(args[1]); err != nil { log.Fatal(err) } os.RemoveAll(filepath.Join(*root, "repos", args[1])) default: flag.Usage() os.Exit(2) } } func mustStore(root string) *Store { if err := os.MkdirAll(filepath.Join(root, "repos"), 0o755); err != nil { log.Fatal(err) } store, err := openStore(filepath.Join(root, "basin.db"), false) if err != nil { log.Fatal(err) } return store } func serve(root string, args []string) { fs := flag.NewFlagSet("serve", flag.ExitOnError) listen := fs.String("listen", "127.0.0.1:3200", "address to listen on") host := fs.String("host", "localhost", "public hostname used in clone urls") open := fs.Bool("open", false, "allow registration without an invite") sshUser := fs.String("ssh-user", "git", "unix user that serves git over ssh, shown in clone urls") fs.Parse(args) store := mustStore(root) srv := &http.Server{ Addr: *listen, Handler: newServer(store, root, *host, *open, *sshUser).handler(), ReadHeaderTimeout: 10 * time.Second, } log.Printf("basin listening on %s for %s", *listen, *host) log.Fatal(srv.ListenAndServe()) }