diff --git a/assets/js/components/Config/MqttModal.vue b/assets/js/components/Config/MqttModal.vue index ee22621aa..8949d5d52 100644 --- a/assets/js/components/Config/MqttModal.vue +++ b/assets/js/components/Config/MqttModal.vue @@ -63,6 +63,23 @@ + + + + + + + + + + + + + @@ -70,10 +87,12 @@ diff --git a/assets/js/components/Config/PropertyCertField.vue b/assets/js/components/Config/PropertyCertField.vue new file mode 100644 index 000000000..9a470115c --- /dev/null +++ b/assets/js/components/Config/PropertyCertField.vue @@ -0,0 +1,55 @@ + + + + + + {{ $t("config.general.readFromFile") }} + + + + + + + diff --git a/cmd/configure/helper.go b/cmd/configure/helper.go index 8474df914..de356db2e 100644 --- a/cmd/configure/helper.go +++ b/cmd/configure/helper.go @@ -238,11 +238,17 @@ func (c *CmdConfigure) configureMQTT(_ templates.Template) (map[string]interface _, paramPort := templates.ConfigDefaults.ParamByName("port") _, paramUser := templates.ConfigDefaults.ParamByName("user") _, paramPassword := templates.ConfigDefaults.ParamByName("password") + _, paramCaCert := templates.ConfigDefaults.ParamByName("caCert") + _, paramClientCert := templates.ConfigDefaults.ParamByName("clientCert") + _, paramClientKey := templates.ConfigDefaults.ParamByName("clientKey") host := c.askParam(paramHost) port := c.askParam(paramPort) user := c.askParam(paramUser) password := c.askParam(paramPassword) + caCert := c.askParam(paramCaCert) + clientCert := c.askParam(paramClientCert) + clientKey := c.askParam(paramClientKey) fmt.Println() fmt.Println("--------------------------------------------") @@ -250,14 +256,17 @@ func (c *CmdConfigure) configureMQTT(_ templates.Template) (map[string]interface broker := fmt.Sprintf("%s:%s", host, port) mqttConfig := map[string]interface{}{ - "broker": broker, - "user": user, - "password": password, + "broker": broker, + "user": user, + "password": password, + "caCert": caCert, + "clientCert": clientCert, + "clientKey": clientKey, } log := util.NewLogger("mqtt") - if mqtt.Instance, err = mqtt.RegisteredClient(log, broker, user, password, "", 1, false); err == nil { + if mqtt.Instance, err = mqtt.RegisteredClient(log, broker, user, password, "", 1, false, caCert, clientCert, clientKey); err == nil { return mqttConfig, nil } diff --git a/cmd/setup.go b/cmd/setup.go index 96011aead..e09688280 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -597,7 +597,7 @@ func configureMqtt(conf *globalconfig.Mqtt) error { log := util.NewLogger("mqtt") - instance, err := mqtt.RegisteredClient(log, conf.Broker, conf.User, conf.Password, conf.ClientID, 1, conf.Insecure, func(options *paho.ClientOptions) { + instance, err := mqtt.RegisteredClient(log, conf.Broker, conf.User, conf.Password, conf.ClientID, 1, conf.Insecure, conf.CaCert, conf.ClientCert, conf.ClientKey, func(options *paho.ClientOptions) { topic := fmt.Sprintf("%s/status", strings.Trim(conf.Topic, "/")) options.SetWill(topic, "offline", 1, true) diff --git a/i18n/de.toml b/i18n/de.toml index 4c3588721..5410fabbf 100644 --- a/i18n/de.toml +++ b/i18n/de.toml @@ -93,6 +93,7 @@ experimental = "Experimentell" off = "aus" on = "an" password = "Passwort" +readFromFile = "Aus Datei lesen" remove = "Entfernen" save = "Speichern" telemetry = "Telemetrie" @@ -155,8 +156,11 @@ description = "Verbinde evcc mit einem MQTT-Broker, um Daten mit anderen Systeme descriptionClientId = "Autor der Nachrichten. Wenn leer, wird `evcc-[rand]` verwendet." descriptionTopic = "Leer lassen, um das Publizieren zu deaktivieren." labelBroker = "Broker" +labelCaCert = "Serverzertifikat (CA)" labelCheckInsecure = "Erlaube unsichere Verbindungen" +labelClientCert = "Clientzertifikat" labelClientId = "Client ID" +labelClientKey = "Client-Key" labelInsecure = "Zertifikatüberprüfung" labelPassword = "Passwort" labelTopic = "Thema" diff --git a/i18n/en.toml b/i18n/en.toml index 55d170fcc..5d9b81f56 100644 --- a/i18n/en.toml +++ b/i18n/en.toml @@ -93,6 +93,7 @@ experimental = "Experimental" off = "off" on = "on" password = "Password" +readFromFile = "Read from file" remove = "Remove" save = "Save" telemetry = "Telemetry" @@ -154,8 +155,11 @@ description = "Connect to an MQTT broker to exchange data with other systems on descriptionClientId = "Author of the messages. If empty `evcc-[rand]` is used." descriptionTopic = "Leave empty to disable publishing." labelBroker = "Broker" +labelCaCert = "Server certificate (CA)" labelCheckInsecure = "Allow self-signed certificates" +labelClientCert = "Client certificate" labelClientId = "Client ID" +labelClientKey = "Client key" labelInsecure = "Certificate validation" labelPassword = "Password" labelTopic = "Topic" diff --git a/provider/mqtt/client.go b/provider/mqtt/client.go index c8b195739..35cb2d416 100644 --- a/provider/mqtt/client.go +++ b/provider/mqtt/client.go @@ -2,6 +2,7 @@ package mqtt import ( "crypto/tls" + "crypto/x509" "fmt" "math/rand/v2" "strings" @@ -25,11 +26,14 @@ func ClientID() string { // Config is the public configuration type Config struct { - Broker string `json:"broker"` - User string `json:"user"` - Password string `json:"password"` - ClientID string `json:"clientID"` - Insecure bool `json:"insecure"` + Broker string `json:"broker"` + User string `json:"user"` + Password string `json:"password"` + ClientID string `json:"clientID"` + Insecure bool `json:"insecure"` + CaCert string `json:"caCert"` + ClientCert string `json:"clientCert"` + ClientKey string `json:"clientKey"` } // Client encapsulates mqtt publish/subscribe functions @@ -48,7 +52,7 @@ type Option func(*paho.ClientOptions) const secure = "tls://" // NewClient creates new Mqtt publisher -func NewClient(log *util.Logger, broker, user, password, clientID string, qos byte, insecure bool, opts ...Option) (*Client, error) { +func NewClient(log *util.Logger, broker, user, password, clientID string, qos byte, insecure bool, caCert, clientCert, clientKey string, opts ...Option) (*Client, error) { broker, isSecure := strings.CutPrefix(broker, secure) // strip schema as it breaks net.SplitHostPort @@ -74,9 +78,24 @@ func NewClient(log *util.Logger, broker, user, password, clientID string, qos by options.SetConnectionLostHandler(mc.ConnectionLostHandler) options.SetConnectTimeout(request.Timeout) - if insecure { - options.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) + tlsConfig := &tls.Config{ + InsecureSkipVerify: insecure, } + if caCert != "" { + caCertPool := x509.NewCertPool() + if ok := caCertPool.AppendCertsFromPEM([]byte(caCert)); !ok { + return nil, fmt.Errorf("failed to add ca cert to cert pool") + } + tlsConfig.RootCAs = caCertPool + } + if clientCert != "" && clientKey != "" { + clientKeyPair, err := tls.X509KeyPair([]byte(clientCert), []byte(clientKey)) + if err != nil { + return nil, fmt.Errorf("failed to add client cert: %w", err) + } + tlsConfig.Certificates = []tls.Certificate{clientKeyPair} + } + options.SetTLSConfig(tlsConfig) // additional options for _, o := range opts { diff --git a/provider/mqtt/registry.go b/provider/mqtt/registry.go index a24ff4452..fdb86cdce 100644 --- a/provider/mqtt/registry.go +++ b/provider/mqtt/registry.go @@ -31,7 +31,7 @@ var ( ) // RegisteredClient reuses an registered Mqtt publisher or creates a new one -func RegisteredClient(log *util.Logger, broker, user, password, clientID string, qos byte, insecure bool, opts ...Option) (*Client, error) { +func RegisteredClient(log *util.Logger, broker, user, password, clientID string, qos byte, insecure bool, caCert, clientCert, clientKey string, opts ...Option) (*Client, error) { key := fmt.Sprintf("%s.%s:%s", broker, user, password) mu.Lock() @@ -42,7 +42,7 @@ func RegisteredClient(log *util.Logger, broker, user, password, clientID string, clientID = ClientID() } - if client, err = NewClient(log, broker, user, password, clientID, qos, insecure, opts...); err == nil { + if client, err = NewClient(log, broker, user, password, clientID, qos, insecure, caCert, clientCert, clientKey, opts...); err == nil { registry.Add(key, client) } } @@ -57,7 +57,7 @@ func RegisteredClientOrDefault(log *util.Logger, cc Config) (*Client, error) { var err error if cc.Broker != "" { - client, err = RegisteredClient(log, cc.Broker, cc.User, cc.Password, cc.ClientID, 1, cc.Insecure) + client, err = RegisteredClient(log, cc.Broker, cc.User, cc.Password, cc.ClientID, 1, cc.Insecure, cc.CaCert, cc.ClientCert, cc.ClientKey) } if client == nil && err == nil { diff --git a/util/templates/includes/mqtt.tpl b/util/templates/includes/mqtt.tpl index 35f091278..4200a544b 100644 --- a/util/templates/includes/mqtt.tpl +++ b/util/templates/includes/mqtt.tpl @@ -9,4 +9,13 @@ password: {{ .password }} {{- if ne .timeout "30s" }} timeout: {{ .timeout }} {{- end }} +{{- if .caCert }} +caCert: {{ .caCert }} +{{- end }} +{{- if .clientCert }} +clientCert: {{ .clientCert }} +{{- end }} +{{- if .clientKey }} +clientKey: {{ .clientKey }} +{{- end }} {{- end }}