Initial commit
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
// 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 + "/"
|
||||
}
|
||||
Reference in New Issue
Block a user