Logging: - structured slog with LOG_FORMAT=json|text and a stable key vocabulary - periodic stats record with queue depths, db pool, goroutines, memory, dns/probe - no per-certificate logging (hot path is counters only) UI (Bucketeer branding, DESIGN.md): - light+dark themes (OS default, persisted header toggle, no first-paint flash) - official mascot: lossless WebP light/dark logos + favicon, embedded and served with cache headers (1.2MB source PNGs -> ~27/22KB webp) - Assets/Contents views, pagination (offset) and native virtual scrolling (content-visibility) so large result sets do not blow up the DOM - search with debounce/abort + URL state, exposure chips, sort, summary counts Readability + indexing: - internal/index listers for S3-compatible, GCS, Azure, Firebase, OpenSearch (metadata only: keys/sizes/dates; never object contents) - asset_entries table + store write/search; readable resources are indexed and searchable in the UI - deep, unauthenticated, read-only DB probes (PROBE_DEEP): Redis PING, PostgreSQL StartupMessage, MongoDB OP_MSG listDatabases; OpenSearch/Neptune HTTP reads - Azure account-level listing fixed: 403 is reported reachable, not auth_required, since anonymous access is per-container DNS: - curated reliable resolvers by default; DNS_SERVERS=fresh adds the compiled-in Fresh-Resolvers snapshot with a bounded startup sample (warming all 6.7k at once broke the container resolver) - random healthy selection + failover + per-server cooldown Other: prober Close fixes a shutdown hang; config/README updated.
179 lines
4.7 KiB
Go
179 lines
4.7 KiB
Go
// Package config loads all ctcloud configuration from environment variables.
|
|
// There are no configuration files at runtime.
|
|
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Config struct {
|
|
DatabaseURL string
|
|
DBMaxConns int
|
|
|
|
DNSServers []string
|
|
DNSWorkers int
|
|
DNSTimeout time.Duration
|
|
DNSCacheSize int
|
|
DNSMaxCNAMEdepth int
|
|
DNSPositiveTTL time.Duration
|
|
DNSNegativeTTL time.Duration
|
|
|
|
DedupSize int
|
|
|
|
DirectQueueSize int
|
|
DNSQueueSize int
|
|
DBQueueSize int
|
|
|
|
DBBatchSize int
|
|
DBFlushInterval time.Duration
|
|
|
|
ActiveProbing bool
|
|
ProbeWorkers int
|
|
ProbeTimeout time.Duration
|
|
ProbeQueueSize int
|
|
ProbeDeep bool
|
|
IndexEnabled bool
|
|
IndexMaxObjects int
|
|
|
|
MetricsAddr string
|
|
UIUser string
|
|
UIPass string
|
|
LogLevel string
|
|
LogFormat string
|
|
StatsInterval time.Duration
|
|
|
|
LogListSource string
|
|
StartAtEnd bool
|
|
ShutdownTimeout time.Duration
|
|
DedupeTTL time.Duration
|
|
}
|
|
|
|
// DefaultLogListSource is the public CT log list used by certspotter.
|
|
const DefaultLogListSource = "https://www.gstatic.com/ct/log_list/v3/log_list.json"
|
|
|
|
func Load() (*Config, error) {
|
|
c := &Config{
|
|
DatabaseURL: os.Getenv("DATABASE_URL"),
|
|
DBMaxConns: envInt("DB_MAX_CONNS", 8),
|
|
DNSServers: envList("DNS_SERVERS", nil),
|
|
DNSWorkers: envInt("DNS_WORKERS", 16),
|
|
DNSTimeout: envDuration("DNS_TIMEOUT", 5*time.Second),
|
|
DNSCacheSize: envInt("DNS_CACHE_SIZE", 1<<18),
|
|
DedupSize: envInt("DEDUP_SIZE", 1<<18),
|
|
DNSMaxCNAMEdepth: envInt("DNS_MAX_CNAME_DEPTH", 8),
|
|
DNSPositiveTTL: envDuration("DNS_POSITIVE_TTL", 10*time.Minute),
|
|
DNSNegativeTTL: envDuration("DNS_NEGATIVE_TTL", 1*time.Minute),
|
|
DirectQueueSize: envInt("DIRECT_QUEUE_SIZE", 8192),
|
|
DNSQueueSize: envInt("DNS_QUEUE_SIZE", 65536),
|
|
DBQueueSize: envInt("DB_QUEUE_SIZE", 8192),
|
|
DBBatchSize: envInt("DB_BATCH_SIZE", 500),
|
|
DBFlushInterval: envDuration("DB_FLUSH_INTERVAL", 2*time.Second),
|
|
ActiveProbing: envBool("ACTIVE_PROBING", false),
|
|
ProbeWorkers: envInt("PROBE_WORKERS", 8),
|
|
ProbeTimeout: envDuration("PROBE_TIMEOUT", 5*time.Second),
|
|
ProbeQueueSize: envInt("PROBE_QUEUE_SIZE", 1<<16),
|
|
ProbeDeep: envBool("PROBE_DEEP", true),
|
|
IndexEnabled: envBool("INDEX_ENABLED", true),
|
|
IndexMaxObjects: envInt("INDEX_MAX_OBJECTS", 1000),
|
|
MetricsAddr: envString("METRICS_ADDR", ":9090"),
|
|
UIUser: envString("UI_USER", ""),
|
|
UIPass: envString("UI_PASS", ""),
|
|
LogLevel: envString("LOG_LEVEL", "info"),
|
|
LogFormat: envString("LOG_FORMAT", "text"),
|
|
StatsInterval: envDuration("STATS_INTERVAL", 60*time.Second),
|
|
LogListSource: envString("LOG_LIST_SOURCE", DefaultLogListSource),
|
|
StartAtEnd: envBool("START_AT_END", false),
|
|
ShutdownTimeout: envDuration("SHUTDOWN_TIMEOUT", 20*time.Second),
|
|
DedupeTTL: envDuration("DEDUP_TTL", time.Hour),
|
|
}
|
|
if err := c.validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
func (c *Config) validate() error {
|
|
if c.DatabaseURL == "" {
|
|
return fmt.Errorf("DATABASE_URL is required")
|
|
}
|
|
if c.DNSWorkers < 1 {
|
|
return fmt.Errorf("DNS_WORKERS must be >= 1")
|
|
}
|
|
if c.DNSMaxCNAMEdepth < 1 {
|
|
return fmt.Errorf("DNS_MAX_CNAME_DEPTH must be >= 1")
|
|
}
|
|
if c.DirectQueueSize < 1 || c.DNSQueueSize < 1 || c.DBQueueSize < 1 {
|
|
return fmt.Errorf("queue sizes must be >= 1")
|
|
}
|
|
if c.DBBatchSize < 1 {
|
|
return fmt.Errorf("DB_BATCH_SIZE must be >= 1")
|
|
}
|
|
switch c.LogLevel {
|
|
case "debug", "info", "warn", "error":
|
|
default:
|
|
return fmt.Errorf("LOG_LEVEL must be one of debug, info, warn, error")
|
|
}
|
|
switch c.LogFormat {
|
|
case "text", "json":
|
|
default:
|
|
return fmt.Errorf("LOG_FORMAT must be text or json")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func envList(key string, def []string) []string {
|
|
v := os.Getenv(key)
|
|
if v == "" {
|
|
return def
|
|
}
|
|
parts := strings.Split(v, ",")
|
|
out := make([]string, 0, len(parts))
|
|
for _, p := range parts {
|
|
if p = strings.TrimSpace(p); p != "" {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return def
|
|
}
|
|
return out
|
|
}
|
|
|
|
func envString(key, def string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return def
|
|
}
|
|
|
|
func envInt(key string, def int) int {
|
|
if v := os.Getenv(key); v != "" {
|
|
if n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil {
|
|
return n
|
|
}
|
|
}
|
|
return def
|
|
}
|
|
|
|
func envBool(key string, def bool) bool {
|
|
if v := os.Getenv(key); v != "" {
|
|
if b, err := strconv.ParseBool(strings.TrimSpace(v)); err == nil {
|
|
return b
|
|
}
|
|
}
|
|
return def
|
|
}
|
|
|
|
func envDuration(key string, def time.Duration) time.Duration {
|
|
if v := os.Getenv(key); v != "" {
|
|
if d, err := time.ParseDuration(strings.TrimSpace(v)); err == nil {
|
|
return d
|
|
}
|
|
}
|
|
return def
|
|
}
|