From d429f3a7dbe8fc8cc43ebe565b6130b1cfce4ea1 Mon Sep 17 00:00:00 2001 From: xengineering Date: Sat, 20 Dec 2025 14:15:58 +0100 Subject: Add StartupConfiguration.Validate() This method makes it easy to validate a configuration. A call of it is now embedded into the StartupConfiguration.FromJSON() method which should always be the lowest level function to parse configurations. Thus configurations can usually be trusted. --- config.go | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++- config_test.go | 5 ++++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/config.go b/config.go index edd6f4b..7d3cab8 100644 --- a/config.go +++ b/config.go @@ -3,9 +3,33 @@ package main import ( _ "embed" "encoding/json" + "fmt" "log" + "net" + "net/url" + "regexp" + "strconv" + "time" ) +const ( + MQTT_BROKER_REGEX = `^tcp://127\.0\.0\.1:\d+$` + MQTT_CLIENT_ID_REGEX = `^[0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]{1,23}$` + MQTT_TOPIC_PREFIX_REGEX = `^[a-zA-Z0-9]{1,20}$` +) + +var ( + mqttBrokerRegexp *regexp.Regexp + mqttClientIDRegexp *regexp.Regexp + mqttTopicPrefixRegexp *regexp.Regexp +) + +func init() { + mqttBrokerRegexp = regexp.MustCompile(MQTT_BROKER_REGEX) + mqttClientIDRegexp = regexp.MustCompile(MQTT_CLIENT_ID_REGEX) + mqttTopicPrefixRegexp = regexp.MustCompile(MQTT_TOPIC_PREFIX_REGEX) +} + //go:embed configs/default.json var defaultConfig []byte @@ -25,8 +49,75 @@ type StartupConfig struct { Homematic HomematicConfig `json:"homematic"` } +func (sc StartupConfig) Validate() error { + if !mqttBrokerRegexp.MatchString(sc.MQTT.Broker) { + return fmt.Errorf( + "mqtt/broker configuration '%s' does not match regular expression '%s'.", + sc.MQTT.Broker, + MQTT_BROKER_REGEX, + ) + } + + if !mqttClientIDRegexp.MatchString(sc.MQTT.ClientID) { + return fmt.Errorf( + "mqtt/client-id configuration '%s' does not match regular expression '%s'.", + sc.MQTT.ClientID, + MQTT_CLIENT_ID_REGEX, + ) + } + + if !mqttTopicPrefixRegexp.MatchString(sc.MQTT.TopicPrefix) { + return fmt.Errorf( + "mqtt/topic-prefix configuration '%s' does not match regular expression '%s'.", + sc.MQTT.TopicPrefix, + MQTT_TOPIC_PREFIX_REGEX, + ) + } + + cu, err := url.Parse(sc.Homematic.CCU) + if err != nil { + return fmt.Errorf("homematic/ccu configuration '%s' cannot be parsed as URL: %v", sc.Homematic.CCU, err) + } + + if cu.Scheme != `http` { + return fmt.Errorf("homematic/ccu configuration '%s' must use scheme 'http'.", sc.Homematic.CCU) + } + + host, portstring, err := net.SplitHostPort(cu.Host) + if err != nil { + return fmt.Errorf("homematic/ccu configuration '%s' can't be split into hostname and port: %v", sc.Homematic.CCU, err) + } + + ip := net.ParseIP(host) + if ip == nil { + return fmt.Errorf("homematic/ccu configuration '%s' must use a plain IP address as host.", sc.Homematic.CCU) + } + + _, err = strconv.Atoi(portstring) + if err != nil { + return fmt.Errorf("homematic/ccu configuration '%s' must use a numeric port: %v", sc.Homematic.CCU, err) + } + + _, err = time.ParseDuration(sc.Homematic.PollingPeriod) + if err != nil { + return fmt.Errorf("homematic/polling-period configuration '%s' could not be parsed to duration: %v", sc.Homematic.PollingPeriod, err) + } + + return nil +} + func (sc *StartupConfig) FromJSON(data []byte) error { - return json.Unmarshal(data, sc) + err := json.Unmarshal(data, sc) + if err != nil { + return fmt.Errorf("Failed to unmarshal configuration: %w", err) + } + + err = sc.Validate() + if err != nil { + return fmt.Errorf("Could not validate configuration: %w", err) + } + + return nil } func GetStartupConfig() StartupConfig { diff --git a/config_test.go b/config_test.go index c568a34..0972d5d 100644 --- a/config_test.go +++ b/config_test.go @@ -11,4 +11,9 @@ func TestDefaultConfig(t *testing.T) { if err != nil { t.Fatalf("Failed parsing default config from JSON: %v", err) } + + err = config.Validate() + if err != nil { + t.Fatalf("Failed to validate default config: %v", err) + } } -- cgit v1.2.3-70-g09d2