package config import ( "fmt" "os" "path/filepath" "strings" "github.com/adrg/xdg" toml "github.com/pelletier/go-toml/v2" ) type Config struct { Issuer string `toml:"issuer"` ClientID string `toml:"client_id"` ClientSecretFile string `toml:"client_secret_file,omitempty"` CacheHost string `toml:"cache_host"` NetrcPath string `toml:"netrc_path"` // ClientSecret is populated at load time by reading ClientSecretFile. ClientSecret string `toml:"-"` } const configPathEnvVar = "NIX_CACHE_LOGIN_CONFIG" var systemConfigPath = "/etc/nix-cache-login/config.toml" // Load reads the config from the given path, or from the default XDG location. func Load(path string) (*Config, error) { path = resolveConfigPath(path) data, err := os.ReadFile(path) if err != nil && path == defaultXDGConfigPath() { // On servers, allow a system-wide fallback when per-user XDG config is absent. if systemData, systemErr := os.ReadFile(systemConfigPath); systemErr == nil { data = systemData err = nil } else { return nil, fmt.Errorf("reading config file: %w", err) } } if err != nil { return nil, fmt.Errorf("reading config file: %w", err) } var cfg Config if err := toml.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("parsing config file: %w", err) } cfg.NetrcPath = expandPath(cfg.NetrcPath) cfg.ClientSecretFile = expandPath(cfg.ClientSecretFile) if cfg.ClientSecretFile != "" { secret, err := os.ReadFile(cfg.ClientSecretFile) if err != nil { return nil, fmt.Errorf("reading client_secret_file: %w", err) } cfg.ClientSecret = strings.TrimSpace(string(secret)) } if err := cfg.validate(); err != nil { return nil, err } return &cfg, nil } func resolveConfigPath(path string) string { if path != "" { return path } if envPath := strings.TrimSpace(os.Getenv(configPathEnvVar)); envPath != "" { return envPath } return defaultXDGConfigPath() } func defaultXDGConfigPath() string { return filepath.Join(xdg.ConfigHome, "nix-cache-login", "config.toml") } func (c *Config) validate() error { if c.Issuer == "" { return fmt.Errorf("config: issuer is required") } if c.ClientID == "" { return fmt.Errorf("config: client_id is required") } if c.CacheHost == "" { return fmt.Errorf("config: cache_host is required") } if c.NetrcPath == "" { return fmt.Errorf("config: netrc_path is required") } return nil } // expandPath expands environment variables in a path. XDG base directory // variables are resolved using the xdg library, which applies the XDG spec // fallbacks (e.g. $HOME/.config when $XDG_CONFIG_HOME is unset). This ensures // correct behaviour in systemd user services, which do not set XDG variables. func expandPath(s string) string { return os.Expand(s, func(key string) string { switch key { case "XDG_CONFIG_HOME": return xdg.ConfigHome case "XDG_DATA_HOME": return xdg.DataHome case "XDG_CACHE_HOME": return xdg.CacheHome case "XDG_STATE_HOME": return xdg.StateHome case "XDG_RUNTIME_DIR": return xdg.RuntimeDir default: return os.Getenv(key) } }) } // RefreshTokenPath returns the path to the stored refresh token. func RefreshTokenPath() string { return filepath.Join(xdg.ConfigHome, "nix-cache-login", "refresh-token") }