Files

257 lines
7.3 KiB
Go

package config
import (
"errors"
"fmt"
"math"
"net/netip"
"net/url"
"os"
"strconv"
"strings"
"time"
"git.misaka.ren/M1saka/token_thief/db"
)
// Config 保存从环境变量加载的全部运行配置。
type Config struct {
ListenAddr string
UpstreamURL *url.URL
ClickHouseURL string
MaxBodyBytes int64
MaxRequestBytes int64
LogQueueSize int
LogQueueBytes int64
LogBatchSize int
LogBatchInterval time.Duration
LogWorkers int
FilterFile string
DBReconnectInterval time.Duration
ReadTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
UpstreamTimeout time.Duration
UpstreamResponseTimeout time.Duration
UpstreamStreamIdleTimeout time.Duration
UpstreamTLSInsecureSkipVerify bool
TrustedProxies []netip.Prefix
}
// Load 从进程环境读取配置;缺少必填字段时返回错误。
func Load() (*Config, error) {
maxBodyBytes, err := getEnvInt64("MAX_BODY_BYTES", 1<<20)
if err != nil {
return nil, err
}
maxRequestBytes, err := getEnvInt64("MAX_REQUEST_BYTES", 16<<20)
if err != nil {
return nil, err
}
logQueueSize, err := getEnvInt("LOG_QUEUE_SIZE", 256)
if err != nil {
return nil, err
}
logQueueBytes, err := getEnvInt64("LOG_QUEUE_BYTES", 64<<20)
if err != nil {
return nil, err
}
logBatchSize, err := getEnvInt("LOG_BATCH_SIZE", 50)
if err != nil {
return nil, err
}
logBatchInterval, err := getEnvDuration("LOG_BATCH_INTERVAL", 2*time.Second)
if err != nil {
return nil, err
}
logWorkers, err := getEnvInt("LOG_WORKERS", 2)
if err != nil {
return nil, err
}
dbReconnectInterval, err := getEnvDuration("DB_RECONNECT_INTERVAL", 10*time.Second)
if err != nil {
return nil, err
}
readTimeout, err := getEnvDuration("READ_TIMEOUT", 30*time.Second)
if err != nil {
return nil, err
}
writeTimeout, err := getEnvDuration("WRITE_TIMEOUT", 10*time.Minute)
if err != nil {
return nil, err
}
idleTimeout, err := getEnvDuration("IDLE_TIMEOUT", 5*time.Minute)
if err != nil {
return nil, err
}
upstreamTimeout, err := getEnvDuration("UPSTREAM_TIMEOUT", 30*time.Second)
if err != nil {
return nil, err
}
upstreamResponseTimeout, err := getEnvDuration("UPSTREAM_RESPONSE_TIMEOUT", 30*time.Second)
if err != nil {
return nil, err
}
upstreamStreamIdleTimeout, err := getEnvDuration("UPSTREAM_STREAM_IDLE_TIMEOUT", 2*time.Minute)
if err != nil {
return nil, err
}
upstreamTLSInsecureSkipVerify, err := getEnvBool("UPSTREAM_TLS_INSECURE_SKIP_VERIFY", false)
if err != nil {
return nil, err
}
trustedProxies, err := getEnvPrefixes("TRUSTED_PROXIES")
if err != nil {
return nil, err
}
cfg := &Config{
ListenAddr: getEnv("LISTEN_ADDR", ":8080"),
ClickHouseURL: os.Getenv("CLICKHOUSE_URL"),
MaxBodyBytes: maxBodyBytes,
MaxRequestBytes: maxRequestBytes,
LogQueueSize: logQueueSize,
LogQueueBytes: logQueueBytes,
LogBatchSize: logBatchSize,
LogBatchInterval: logBatchInterval,
LogWorkers: logWorkers,
FilterFile: getEnv("FILTER_FILE", "./filter.yaml"),
DBReconnectInterval: dbReconnectInterval,
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
IdleTimeout: idleTimeout,
UpstreamTimeout: upstreamTimeout,
UpstreamResponseTimeout: upstreamResponseTimeout,
UpstreamStreamIdleTimeout: upstreamStreamIdleTimeout,
UpstreamTLSInsecureSkipVerify: upstreamTLSInsecureSkipVerify,
TrustedProxies: trustedProxies,
}
upstream := os.Getenv("UPSTREAM_URL")
if upstream == "" {
return nil, errors.New("UPSTREAM_URL is required")
}
u, err := url.Parse(upstream)
if err != nil {
return nil, fmt.Errorf("invalid UPSTREAM_URL: %w", err)
}
if (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
return nil, fmt.Errorf("invalid UPSTREAM_URL: %q", upstream)
}
cfg.UpstreamURL = u
if cfg.ClickHouseURL == "" {
return nil, errors.New("CLICKHOUSE_URL is required")
}
if _, err := db.ClickHouseOptions(cfg.ClickHouseURL); err != nil {
return nil, err
}
positiveValues := []struct {
key string
value int64
}{
{key: "MAX_BODY_BYTES", value: cfg.MaxBodyBytes},
{key: "MAX_REQUEST_BYTES", value: cfg.MaxRequestBytes},
{key: "LOG_QUEUE_SIZE", value: int64(cfg.LogQueueSize)},
{key: "LOG_QUEUE_BYTES", value: cfg.LogQueueBytes},
{key: "LOG_BATCH_SIZE", value: int64(cfg.LogBatchSize)},
{key: "LOG_BATCH_INTERVAL", value: int64(cfg.LogBatchInterval)},
{key: "LOG_WORKERS", value: int64(cfg.LogWorkers)},
{key: "DB_RECONNECT_INTERVAL", value: int64(cfg.DBReconnectInterval)},
{key: "READ_TIMEOUT", value: int64(cfg.ReadTimeout)},
{key: "WRITE_TIMEOUT", value: int64(cfg.WriteTimeout)},
{key: "IDLE_TIMEOUT", value: int64(cfg.IdleTimeout)},
{key: "UPSTREAM_TIMEOUT", value: int64(cfg.UpstreamTimeout)},
{key: "UPSTREAM_RESPONSE_TIMEOUT", value: int64(cfg.UpstreamResponseTimeout)},
{key: "UPSTREAM_STREAM_IDLE_TIMEOUT", value: int64(cfg.UpstreamStreamIdleTimeout)},
}
for _, item := range positiveValues {
if item.value <= 0 {
return nil, fmt.Errorf("%s must be greater than zero", item.key)
}
}
if cfg.MaxRequestBytes == math.MaxInt64 {
return nil, errors.New("MAX_REQUEST_BYTES is too large")
}
return cfg, nil
}
func getEnv(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func getEnvInt(key string, def int) (int, error) {
if v := os.Getenv(key); v != "" {
n, err := strconv.Atoi(v)
if err != nil {
return 0, fmt.Errorf("invalid %s: %w", key, err)
}
return n, nil
}
return def, nil
}
func getEnvInt64(key string, def int64) (int64, error) {
if v := os.Getenv(key); v != "" {
n, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return 0, fmt.Errorf("invalid %s: %w", key, err)
}
return n, nil
}
return def, nil
}
func getEnvDuration(key string, def time.Duration) (time.Duration, error) {
if v := os.Getenv(key); v != "" {
d, err := time.ParseDuration(v)
if err != nil {
return 0, fmt.Errorf("invalid %s: %w", key, err)
}
return d, nil
}
return def, nil
}
func getEnvBool(key string, def bool) (bool, error) {
if v := os.Getenv(key); v != "" {
b, err := strconv.ParseBool(v)
if err != nil {
return false, fmt.Errorf("invalid %s: %w", key, err)
}
return b, nil
}
return def, nil
}
func getEnvPrefixes(key string) ([]netip.Prefix, error) {
v := os.Getenv(key)
if v == "" {
return nil, nil
}
parts := strings.Split(v, ",")
prefixes := make([]netip.Prefix, 0, len(parts))
for _, part := range parts {
value := strings.TrimSpace(part)
if value == "" {
return nil, fmt.Errorf("invalid %s: empty IP or CIDR", key)
}
if addr, err := netip.ParseAddr(value); err == nil {
prefixes = append(prefixes, netip.PrefixFrom(addr, addr.BitLen()))
continue
}
prefix, err := netip.ParsePrefix(value)
if err != nil || !prefix.IsValid() || prefix != prefix.Masked() {
return nil, fmt.Errorf("invalid %s entry %q", key, value)
}
prefixes = append(prefixes, prefix)
}
return prefixes, nil
}