package main import ( "errors" "flag" "fmt" "os" "text/tabwriter" "time" "send/internal/auth" "send/internal/config" "send/internal/store" ) const tokenUsage = `send token - manage upload credentials Usage: send token add [options] send token list [options] send token rm [options] A token grants its own size and lifetime limits. Any limit left unset is inherited from the running server's defaults, so a token with no options behaves exactly like the anonymous tier but may claim vanity names. Options: ` func tokenCommand(args []string) error { if len(args) == 0 { return errors.New("token: expected add, list or rm") } sub, rest := args[0], args[1:] // The name is positional and must come first; stdlib flag stops parsing at // the first non-flag argument. name := "" if len(rest) > 0 && (len(rest[0]) == 0 || rest[0][0] != '-') { name, rest = rest[0], rest[1:] } var ( dataDir, tokensPath string maxSize, maxExpiry, defExpiry string vanity, admin bool ) fs := config.NewSet("send token", config.EnvPrefix) fs.SetOutput(os.Stderr) fs.String(&dataDir, "data", "d", "./data", "DIR", "directory holding the data") fs.String(&tokensPath, "tokens", "", "", "FILE", "token file location (default /tokens.json)") fs.String(&maxSize, "max-size", "s", "", "SIZE", "per-upload cap for this token; 'unlimited' to remove it") fs.String(&maxExpiry, "max-expiry", "e", "", "DURATION", "longest lifetime this token may request; 'never' to remove the cap") fs.String(&defExpiry, "default-expiry", "", "", "DURATION", "lifetime applied when this token does not ask for one") fs.Bool(&vanity, "vanity", "", false, "allow this token to claim custom names") fs.Bool(&admin, "admin", "", false, "allow this token to delete anyone's files") if err := fs.Parse(rest); err != nil { if errors.Is(err, flag.ErrHelp) { fs.PrintUsage(os.Stdout, tokenUsage) return flag.ErrHelp } return err } if tokensPath == "" { tokensPath = dataDir + "/tokens.json" } // Match the server's permissions for anything this command has to create. setUmask() file, err := auth.Load(tokensPath) if err != nil { return err } switch sub { case "add": if name == "" { return errors.New("token add: a name is required") } return tokenAdd(file, name, maxSize, maxExpiry, defExpiry, vanity, admin) case "list": return tokenList(file) case "rm", "remove", "delete": if name == "" { return errors.New("token rm: a name is required") } if err := file.Remove(name); err != nil { return err } fmt.Printf("Removed token %q.\n", name) return nil default: return fmt.Errorf("token: unknown subcommand %q", sub) } } func tokenAdd(file *auth.File, name, maxSize, maxExpiry, defExpiry string, vanity, admin bool) error { secret, err := store.NewSecret() if err != nil { return err } t := &auth.Token{ Name: name, Hash: auth.HashSecret(secret), AllowVanity: vanity, Admin: admin, Created: time.Now().UTC().Truncate(time.Second), } // Only options actually given are recorded; everything else stays absent // so it keeps tracking the server's defaults. if maxSize != "" { t.MaxSize = &maxSize } if maxExpiry != "" { t.MaxExpiry = &maxExpiry } if defExpiry != "" { t.DefaultExpiry = &defExpiry } if err := file.Add(t); err != nil { return err } fmt.Printf("Added token %q to %s\n\n", name, file.Path()) fmt.Printf(" %s\n\n", secret) fmt.Println("This is the only time it is shown; only its hash is stored.") fmt.Println("Send it as: Authorization: Bearer ") return nil } func tokenList(file *auth.File) error { tokens := file.List() if len(tokens) == 0 { fmt.Printf("No tokens in %s\n", file.Path()) return nil } w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) fmt.Fprintln(w, "NAME\tMAX SIZE\tMAX EXPIRY\tDEFAULT\tVANITY\tADMIN\tCREATED") for _, t := range tokens { fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", t.Name, inherited(t.MaxSize), inherited(t.MaxExpiry), inherited(t.DefaultExpiry), yesNo(t.AllowVanity), yesNo(t.Admin), t.Created.Format("2006-01-02")) } return w.Flush() } func inherited(s *string) string { if s == nil { return "(default)" } return *s } func yesNo(b bool) string { if b { return "yes" } return "no" }