Latest Updates done, before integrating

This commit is contained in:
2026-04-12 10:13:53 +02:00
parent db46fcf0c6
commit 5354e34055
19 changed files with 820 additions and 41 deletions

View File

@@ -2,6 +2,7 @@ package config
import (
"fmt"
"net"
"os"
"time"
@@ -191,7 +192,8 @@ func (f *ForecastConfig) FetchIntervalParsed() time.Duration {
// EMSConfig holds operational settings for the EMS daemon.
type EMSConfig struct {
PollInterval string `yaml:"poll_interval"`
ListenAddr string `yaml:"listen_addr"`
ListenAddr string `yaml:"listen_addr"` // web UI address (authenticated)
MetricsAddr string `yaml:"metrics_addr"` // Prometheus /metrics address (no auth, internal only)
LogLevel string `yaml:"log_level"`
StateFile string `yaml:"state_file"`
RecoveryTimeout string `yaml:"recovery_timeout"`
@@ -201,6 +203,8 @@ type EMSConfig struct {
WWBoostDisableFile string `yaml:"ww_boost_disable_file"` // flag file path: presence = WW boost disabled
TripGoalFile string `yaml:"trip_goal_file"` // persisted active trip goal
SessionLogFile string `yaml:"session_log_file"` // JSONL log of completed charge sessions
HTTPUsername string `yaml:"http_username"` // login username (empty = no auth)
HTTPPassword string `yaml:"http_password"` // login password
}
func (e *EMSConfig) PollIntervalParsed() time.Duration {
@@ -226,6 +230,13 @@ func (e *EMSConfig) OverrideTimeoutParsed() time.Duration {
// Load reads and parses the YAML config file at the given path.
func Load(path string) (*Config, error) {
// Warn if config file is readable by group or others (contains credentials).
if info, err := os.Stat(path); err == nil {
if info.Mode().Perm()&0o077 != 0 {
fmt.Fprintf(os.Stderr, "WARNING: config file %s has permissions %o — should be 0600 (contains credentials)\n", path, info.Mode().Perm())
}
}
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading config file: %w", err)
@@ -256,5 +267,21 @@ func (c *Config) validate() error {
if c.EMS.ListenAddr == "" {
return fmt.Errorf("ems.listen_addr is required")
}
// Validate Shelly IPs to prevent SSRF via config manipulation.
for name, dev := range map[string]ShellyDevice{
"sg_ready": c.Shelly.SGReady,
"wallbox_a": c.Shelly.WallboxA,
"wallbox_b": c.Shelly.WallboxB,
} {
if dev.IP != "" && net.ParseIP(dev.IP) == nil {
return fmt.Errorf("shelly.%s.ip %q is not a valid IP address", name, dev.IP)
}
}
if c.EMS.HTTPUsername != "" && c.EMS.HTTPPassword == "" {
return fmt.Errorf("ems.http_username is set but ems.http_password is empty")
}
if c.EMS.HTTPPassword != "" && c.EMS.HTTPUsername == "" {
return fmt.Errorf("ems.http_password is set but ems.http_username is empty")
}
return nil
}