148 lines
4.8 KiB
Go
148 lines
4.8 KiB
Go
// Package config holds the server's runtime options and the flag plumbing that
|
|
// populates them.
|
|
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Config is the fully-resolved server configuration.
|
|
type Config struct {
|
|
Listen string
|
|
DataDir string
|
|
BasePath string // normalised: always "/" or "/prefix/"
|
|
PublicURL string // absolute origin+path for generated links; "" => relative
|
|
|
|
MaxSize int64 // per-upload cap; Unlimited means no cap
|
|
MaxExpiry time.Duration // longest lifetime a caller may request
|
|
DefaultExpiry time.Duration // lifetime when the caller does not ask
|
|
MaxTotalBytes int64 // whole-store quota
|
|
MinFreeBytes int64 // refuse uploads below this much free disk
|
|
|
|
TokensPath string
|
|
TrustedProxy string // comma-separated CIDRs allowed to set X-Forwarded-For
|
|
SweepInterval time.Duration
|
|
|
|
UploadRate int // uploads per hour per client
|
|
UploadBurst int
|
|
MaxConcurrent int
|
|
trustedProxyNets []*net.IPNet
|
|
}
|
|
|
|
const EnvPrefix = "SEND_"
|
|
|
|
// Register wires every option onto s. Short forms exist only for the options
|
|
// reached often; everything else is long-only, by design.
|
|
func (c *Config) Register(s *Set) {
|
|
s.String(&c.Listen, "listen", "l", "127.0.0.1:8080", "ADDR",
|
|
"address to listen on; keep it on loopback behind a reverse proxy")
|
|
s.String(&c.DataDir, "data", "d", "./data", "DIR",
|
|
"directory holding uploaded objects and their metadata")
|
|
s.String(&c.BasePath, "base-url", "b", "/", "PATH",
|
|
"path prefix this service is mounted under")
|
|
s.String(&c.PublicURL, "public-url", "u", "", "URL",
|
|
"absolute base URL used in generated links; relative links when empty")
|
|
|
|
s.Size(&c.MaxSize, "max-size", "s", "2GiB",
|
|
"largest upload accepted from an anonymous caller")
|
|
s.Duration(&c.MaxExpiry, "max-expiry", "e", "3d",
|
|
"longest lifetime an anonymous caller may request")
|
|
s.Duration(&c.DefaultExpiry, "default-expiry", "", "3d",
|
|
"lifetime applied when the caller does not ask for one")
|
|
|
|
s.Size(&c.MaxTotalBytes, "max-total-bytes", "", "unlimited",
|
|
"refuse uploads once stored data exceeds this total")
|
|
s.Size(&c.MinFreeBytes, "min-free-bytes", "", "1GiB",
|
|
"refuse uploads when the filesystem has less free space than this")
|
|
s.String(&c.TokensPath, "tokens", "", "", "FILE",
|
|
"token file location (default <data>/tokens.json)")
|
|
s.String(&c.TrustedProxy, "trusted-proxy", "", "", "CIDRS",
|
|
"comma-separated networks whose X-Forwarded-For header is believed")
|
|
s.Duration(&c.SweepInterval, "sweep-interval", "", "1m",
|
|
"how often expired objects are swept from disk")
|
|
|
|
s.Int(&c.UploadRate, "upload-rate", "", 60,
|
|
"uploads permitted per hour per client address")
|
|
s.Int(&c.UploadBurst, "upload-burst", "", 10,
|
|
"uploads permitted back-to-back before the rate applies")
|
|
s.Int(&c.MaxConcurrent, "max-concurrent", "", 8,
|
|
"uploads allowed to be in flight at once")
|
|
}
|
|
|
|
// Normalise validates interdependent options and canonicalises the derived
|
|
// ones. It must be called after parsing and before the config is used.
|
|
func (c *Config) Normalise() error {
|
|
c.BasePath = NormalisePath(c.BasePath)
|
|
|
|
if c.PublicURL != "" {
|
|
u, err := url.Parse(c.PublicURL)
|
|
if err != nil {
|
|
return fmt.Errorf("--public-url: %w", err)
|
|
}
|
|
if !u.IsAbs() {
|
|
return fmt.Errorf("--public-url: %q is not absolute", c.PublicURL)
|
|
}
|
|
c.PublicURL = strings.TrimSuffix(u.String(), "/")
|
|
}
|
|
|
|
if c.TokensPath == "" {
|
|
c.TokensPath = c.DataDir + "/tokens.json"
|
|
}
|
|
if c.MaxExpiry != Unlimited && (c.DefaultExpiry == Unlimited || c.DefaultExpiry > c.MaxExpiry) {
|
|
return fmt.Errorf("--default-expiry (%s) exceeds --max-expiry (%s)",
|
|
FormatDuration(c.DefaultExpiry), FormatDuration(c.MaxExpiry))
|
|
}
|
|
if c.SweepInterval <= 0 {
|
|
return fmt.Errorf("--sweep-interval must be positive")
|
|
}
|
|
if c.MaxConcurrent < 1 {
|
|
return fmt.Errorf("--max-concurrent must be at least 1")
|
|
}
|
|
|
|
for _, cidr := range strings.Split(c.TrustedProxy, ",") {
|
|
cidr = strings.TrimSpace(cidr)
|
|
if cidr == "" {
|
|
continue
|
|
}
|
|
// Accept both a bare address and a network.
|
|
if ip := net.ParseIP(cidr); ip != nil {
|
|
bits := 32
|
|
if ip.To4() == nil {
|
|
bits = 128
|
|
}
|
|
c.trustedProxyNets = append(c.trustedProxyNets,
|
|
&net.IPNet{IP: ip, Mask: net.CIDRMask(bits, bits)})
|
|
continue
|
|
}
|
|
_, n, err := net.ParseCIDR(cidr)
|
|
if err != nil {
|
|
return fmt.Errorf("--trusted-proxy: %w", err)
|
|
}
|
|
c.trustedProxyNets = append(c.trustedProxyNets, n)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TrustsProxy reports whether X-Forwarded-For from ip should be believed.
|
|
func (c *Config) TrustsProxy(ip net.IP) bool {
|
|
for _, n := range c.trustedProxyNets {
|
|
if n.Contains(ip) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// NormalisePath canonicalises a mount prefix to "/" or "/prefix/".
|
|
func NormalisePath(p string) string {
|
|
p = strings.Trim(strings.TrimSpace(p), "/")
|
|
if p == "" {
|
|
return "/"
|
|
}
|
|
return "/" + p + "/"
|
|
}
|