Binding to 0.0.0.0 used to demand credentials because the admin console and the tile routes shared one listener. That forced a choice between keeping the console off the network and letting other machines fetch tiles. Admin routes are now loopback-only by default: the service can bind 0.0.0.0 so /tiles, /vectors, the catalog and the probes reach the LAN, while /admin and /api/v1/admin answer 403 to anything arriving from another host. Credentials are only required when ADMIN_ALLOW_REMOTE=true actually opens the console up, which is the case worth guarding. Tests cover both sides: a remote admin request is refused, a remote tile request still reaches the token check.
196 lines
5.5 KiB
Go
196 lines
5.5 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Config struct {
|
|
Host string
|
|
Port int
|
|
APIBaseURL string
|
|
TilePublicBaseURL string
|
|
VectorPublicBaseURL string
|
|
SQLitePath string
|
|
BasemapScanRoot string
|
|
VectorScanRoot string
|
|
AutoScanOnStart bool
|
|
Env string
|
|
ServiceName string
|
|
ServiceVersion string
|
|
Commit string
|
|
|
|
AdminToken string
|
|
AdminUser string
|
|
AdminPassword string
|
|
AdminAllowRemote bool
|
|
CORSAllowedOrigins []string
|
|
TokenDefaultTTL time.Duration
|
|
|
|
HealthCheckInterval time.Duration
|
|
TileJobCommand string
|
|
TileJobVRTCommand string
|
|
TileJobLogDir string
|
|
}
|
|
|
|
func Load() (Config, error) {
|
|
if err := LoadEnvDefaults(); err != nil {
|
|
return Config{}, err
|
|
}
|
|
|
|
port, err := intEnv("API_PORT", 8910)
|
|
if err != nil {
|
|
return Config{}, fmt.Errorf("parse API_PORT: %w", err)
|
|
}
|
|
autoScanOnStart, err := boolEnv("AUTO_SCAN_ON_START", true)
|
|
if err != nil {
|
|
return Config{}, fmt.Errorf("parse AUTO_SCAN_ON_START: %w", err)
|
|
}
|
|
adminAllowRemote, err := boolEnv("ADMIN_ALLOW_REMOTE", false)
|
|
if err != nil {
|
|
return Config{}, fmt.Errorf("parse ADMIN_ALLOW_REMOTE: %w", err)
|
|
}
|
|
tokenDefaultTTL, err := durationEnv("TOKEN_DEFAULT_TTL", 720*time.Hour)
|
|
if err != nil {
|
|
return Config{}, fmt.Errorf("parse TOKEN_DEFAULT_TTL: %w", err)
|
|
}
|
|
healthCheckInterval, err := durationEnv("HEALTH_CHECK_INTERVAL", 5*time.Minute)
|
|
if err != nil {
|
|
return Config{}, fmt.Errorf("parse HEALTH_CHECK_INTERVAL: %w", err)
|
|
}
|
|
|
|
cfg := Config{
|
|
Host: stringEnv("API_HOST", "127.0.0.1"),
|
|
Port: port,
|
|
APIBaseURL: stringEnv("API_BASE_URL", ""),
|
|
TilePublicBaseURL: stringEnv("TILE_PUBLIC_BASE_URL", ""),
|
|
VectorPublicBaseURL: stringEnv("VECTOR_PUBLIC_BASE_URL", ""),
|
|
SQLitePath: stringEnv("SQLITE_PATH", filepath.Clean(filepath.Join("data", "basemap-pusher.db"))),
|
|
BasemapScanRoot: stringEnv("BASEMAP_SCAN_ROOT", filepath.Clean(filepath.Join("data", "tiles"))),
|
|
VectorScanRoot: stringEnv("VECTOR_SCAN_ROOT", filepath.Clean(filepath.Join("data", "geojson"))),
|
|
AutoScanOnStart: autoScanOnStart,
|
|
Env: stringEnv("APP_ENV", "dev"),
|
|
ServiceName: stringEnv("SERVICE_NAME", "basemap-pusher"),
|
|
ServiceVersion: stringEnv("SERVICE_VERSION", "0.2.0"),
|
|
Commit: stringEnv("SERVICE_COMMIT", "local"),
|
|
AdminToken: strings.TrimSpace(stringEnv("ADMIN_TOKEN", "")),
|
|
AdminUser: strings.TrimSpace(stringEnv("ADMIN_USER", "")),
|
|
AdminPassword: stringEnv("ADMIN_PASSWORD", ""),
|
|
AdminAllowRemote: adminAllowRemote,
|
|
CORSAllowedOrigins: listEnv("CORS_ALLOWED_ORIGINS"),
|
|
TokenDefaultTTL: tokenDefaultTTL,
|
|
HealthCheckInterval: healthCheckInterval,
|
|
TileJobCommand: stringEnv("TILE_JOB_COMMAND", ""),
|
|
TileJobVRTCommand: stringEnv("TILE_JOB_VRT_COMMAND", ""),
|
|
TileJobLogDir: stringEnv("TILE_JOB_LOG_DIR", ""),
|
|
}
|
|
if cfg.APIBaseURL == "" {
|
|
cfg.APIBaseURL = deriveBaseURL(cfg.Host, cfg.Port)
|
|
}
|
|
if cfg.TilePublicBaseURL == "" {
|
|
cfg.TilePublicBaseURL = cfg.APIBaseURL
|
|
}
|
|
if cfg.VectorPublicBaseURL == "" {
|
|
cfg.VectorPublicBaseURL = cfg.APIBaseURL
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func (c Config) ListenAddr() string {
|
|
return fmt.Sprintf("%s:%d", c.Host, c.Port)
|
|
}
|
|
|
|
// HasAdminCredentials reports whether the admin console has any credential
|
|
// configured. Without one, the admin surface is only safe on loopback.
|
|
func (c Config) HasAdminCredentials() bool {
|
|
return c.AdminToken != "" || (c.AdminUser != "" && c.AdminPassword != "")
|
|
}
|
|
|
|
// BindsToNonLoopback reports whether the listen address is reachable from
|
|
// outside the host, which makes credentials mandatory.
|
|
func (c Config) BindsToNonLoopback() bool {
|
|
return !isLoopbackHost(c.Host)
|
|
}
|
|
|
|
func deriveBaseURL(host string, port int) string {
|
|
if host == "" || host == "0.0.0.0" || host == "::" {
|
|
host = "127.0.0.1"
|
|
}
|
|
return fmt.Sprintf("http://%s:%d", host, port)
|
|
}
|
|
|
|
func isLoopbackHost(host string) bool {
|
|
host = strings.TrimSpace(host)
|
|
if host == "" || strings.EqualFold(host, "localhost") {
|
|
return true
|
|
}
|
|
ip := net.ParseIP(strings.Trim(host, "[]"))
|
|
if ip == nil {
|
|
return false
|
|
}
|
|
return ip.IsLoopback()
|
|
}
|
|
|
|
func stringEnv(key, fallback string) string {
|
|
value, ok := os.LookupEnv(key)
|
|
if ok && value != "" {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func intEnv(key string, fallback int) (int, error) {
|
|
value, ok := os.LookupEnv(key)
|
|
if ok && value != "" {
|
|
parsed, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return parsed, nil
|
|
}
|
|
return fallback, nil
|
|
}
|
|
|
|
func boolEnv(key string, fallback bool) (bool, error) {
|
|
value, ok := os.LookupEnv(key)
|
|
if ok && value != "" {
|
|
parsed, err := strconv.ParseBool(value)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return parsed, nil
|
|
}
|
|
return fallback, nil
|
|
}
|
|
|
|
func durationEnv(key string, fallback time.Duration) (time.Duration, error) {
|
|
value, ok := os.LookupEnv(key)
|
|
if ok && strings.TrimSpace(value) != "" {
|
|
parsed, err := time.ParseDuration(strings.TrimSpace(value))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return parsed, nil
|
|
}
|
|
return fallback, nil
|
|
}
|
|
|
|
func listEnv(key string) []string {
|
|
raw := strings.FieldsFunc(stringEnv(key, ""), func(r rune) bool {
|
|
return r == ',' || r == ';' || r == '\n' || r == '\r' || r == '\t' || r == ' '
|
|
})
|
|
values := make([]string, 0, len(raw))
|
|
for _, item := range raw {
|
|
item = strings.TrimSpace(item)
|
|
if item != "" {
|
|
values = append(values, item)
|
|
}
|
|
}
|
|
return values
|
|
}
|