summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorxengineering <me@xengineering.eu>2025-12-20 14:15:58 +0100
committerxengineering <me@xengineering.eu>2025-12-20 14:15:58 +0100
commitd429f3a7dbe8fc8cc43ebe565b6130b1cfce4ea1 (patch)
treef00d0911850110b983a1648342cc0646c90e9b6c
parent224d52d1033d8ccce5087c9bee5a63457830a13a (diff)
downloadsia-server-d429f3a7dbe8fc8cc43ebe565b6130b1cfce4ea1.tar
sia-server-d429f3a7dbe8fc8cc43ebe565b6130b1cfce4ea1.tar.zst
sia-server-d429f3a7dbe8fc8cc43ebe565b6130b1cfce4ea1.zip
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.
-rw-r--r--config.go93
-rw-r--r--config_test.go5
2 files changed, 97 insertions, 1 deletions
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)
+ }
}