257 lines
7.3 KiB
Go
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
|
|
}
|