package main import ( "bufio" "flag" "fmt" "log" "net" "net/http" "os" "path/filepath" "strings" "time" ) func main() { root := flag.String("root", "/srv/gitpocket", "data directory holding gitpocket.db and repos/") flag.Usage = func() { fmt.Fprintln(os.Stderr, `usage: gitpocket [-root dir] command serve [-listen addr] [-host name] [-name text] [-ssh-user name] [-proxy cidr,...] 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 registration [open|invite|closed] show or set who may register admin name | unadmin name grant or revoke site administration 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 org name [+user|-user]... create an organisation and add or remove members mail address send a test message through the configured mailer backup dir | file.db snapshot the database and bundle every changed repository into dir`) } 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 := writeQuota(*root, n); err != nil { log.Fatal(err) } } if q := readQuota(*root); q == 0 { fmt.Println("no quota") } else { fmt.Println(humanSize(q)) } case "registration": store := mustStore(*root) if len(args) == 2 { if !validRegistration(args[1]) { log.Fatal("registration is open, invite or closed") } if err := store.SetSetting("registration", args[1]); err != nil { log.Fatal(err) } } fmt.Println(store.Registration()) case "admin", "unadmin": if len(args) != 2 { os.Exit(2) } if err := mustStore(*root).SetAdmin(args[1], args[0] == "admin"); err != nil { log.Fatal("no such user") } 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) password := readLine() 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") } password := readLine() 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 := backup(*root, args[1], 14); err != nil { log.Fatal(err) } case "mail": if len(args) != 2 { os.Exit(2) } if err := loadMailer(*root).Send(args[1], "test message from gitpocket", "Mail delivery from this forge works.\n"); err != nil { log.Fatal(err) } fmt.Println("sent") case "org": if len(args) < 2 { os.Exit(2) } store := mustStore(*root) org, err := store.UserByName(args[1]) if err != nil { if org, err = store.CreateOrg(args[1]); err != nil { log.Fatal(err) } } else if !store.IsOrg(org.ID) { log.Fatalf("~%s is a user, not an organisation", args[1]) } for _, spec := range args[2:] { name := strings.TrimPrefix(strings.TrimLeft(spec, "+-"), "~") u, err := store.UserByName(name) if err != nil || store.IsOrg(u.ID) { log.Fatalf("%s: no such user", spec) } if err := store.SetMember(org.ID, u.ID, !strings.HasPrefix(spec, "-")); err != nil { log.Fatal(err) } } 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, "gitpocket.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") name := fs.String("name", "gitpocket", "site name shown in the header") sshUser := fs.String("ssh-user", "git", "unix user that serves git over ssh, shown in clone urls") proxies := fs.String("proxy", "", "comma separated networks besides loopback whose X-Forwarded-For is trusted") fs.Parse(args) for _, cidr := range strings.Split(*proxies, ",") { if cidr = strings.TrimSpace(cidr); cidr == "" { continue } _, n, err := net.ParseCIDR(cidr) if err != nil { log.Fatalf("-proxy %s: %v", cidr, err) } trustedProxies = append(trustedProxies, n) } store := mustStore(root) srv := &http.Server{ Addr: *listen, Handler: newServer(store, root, *host, *name, *sshUser).handler(), ReadHeaderTimeout: 10 * time.Second, IdleTimeout: 2 * time.Minute, MaxHeaderBytes: 64 << 10, } log.Printf("gitpocket listening on %s for %s", *listen, *host) log.Fatal(srv.ListenAndServe()) } func readLine() string { line, _ := bufio.NewReader(os.Stdin).ReadString('\n') return strings.TrimRight(line, "\r\n") }