Initial commit
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
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 <name> [options]
|
||||
send token list [options]
|
||||
send token rm <name> [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 <data>/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 <token>")
|
||||
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"
|
||||
}
|
||||
Reference in New Issue
Block a user