package main import ( "flag" "fmt" "log" "net/http" "os" "path/filepath" "strconv" "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`) } 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 "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()) }