Add support for Client Certificates for MQTT and disabling Retained messages

For my usecase, I want to connect to a cloud broker(AWS IoT). This service uses Client Certificates. AWS IoT has an incomplete MQTT implementation and does not support QoS (Exactly Once). It also does not support Retain messages in LWT and other situations.

This change is to get feedback on how we prefer to deal with these configuration edge cases. It's not clear to me that QoS2 is actually useful because of how HWS republishes all messages on a recurring basis.  Should we add additional options for supporting Retains/QoS2? Should we remove the use of QoS2 and make the option I've already added apply to all retains?

Additionally, I'm having some problems with the UI. I am not familiar with this UI framework, but the second new textbox and save button are now out of frame/invisible in my build. It looks like there should be a scrollbar, so not sure of the best way to fix this.

Testing:
I have this working with AWS IoT in my AWS account. I have configured my HomeAssistant mosquitto broker to bridge to AWS IoT and my HWS sensors are working great.
pull/151/head
Preston Tamkin 3 years ago
parent d86513c772
commit e145cc774c

@ -16,6 +16,9 @@ namespace UserInterface.ViewModels
private bool isConnected;
private int? port;
private bool useTLS;
private bool retainLWT = true;
private string rootCaPath;
private string clientCertPath;
public bool IsConnected { get => isConnected; set => this.RaiseAndSetIfChanged(ref isConnected, value); }
public string Message { get => message; set => this.RaiseAndSetIfChanged(ref message, value); }
@ -29,6 +32,13 @@ namespace UserInterface.ViewModels
public bool UseTLS { get => useTLS; set => this.RaiseAndSetIfChanged(ref useTLS, value); }
public bool RetainLWT { get => retainLWT; set => this.RaiseAndSetIfChanged(ref retainLWT, value); }
public string RootCAPath { get => rootCaPath; set => this.RaiseAndSetIfChanged(ref rootCaPath, value); }
public string ClientCertPath { get => clientCertPath; set => this.RaiseAndSetIfChanged(ref clientCertPath, value); }
public void Update(MqttSettings settings)
{
this.Host = settings.Host;
@ -36,6 +46,9 @@ namespace UserInterface.ViewModels
this.Password = settings.Password;
this.Port = settings.Port;
this.UseTLS = settings.UseTLS;
this.RetainLWT = settings.RetainLWT;
this.RootCAPath = settings.RootCAPath;
this.ClientCertPath = settings.ClientCertPath;
}
public void UpdateStatus(MqqtClientStatus status)

@ -19,12 +19,69 @@
<StackPanel Orientation="Vertical" Margin="30 0 0 0">
<ContentControl Margin="0 20 0 10">Use TLS</ContentControl>
<CheckBox IsChecked="{Binding UseTLS}" HorizontalAlignment="Left" Margin="0 3 0 0"/>
<StackPanel Margin="0 20 0 10" HorizontalAlignment="Left" Orientation="Horizontal">
<ContentControl>Retain LastWillAndTestament</ContentControl>
<TextBlock Cursor="Help" Margin="5 0 0 0" VerticalAlignment="Bottom" TextDecorations="Underline">
(What's this?)
<ToolTip.Tip>
<StackPanel>
<TextBlock>
[Experimental]
If set, sets Retain on the Last Will and Testament message.
Only turn this off if you use a broker that does not support this(e.g. AWS IoT Core)
Defaults to True
</TextBlock>
</StackPanel>
</ToolTip.Tip>
</TextBlock>
</StackPanel>
<CheckBox IsChecked="{Binding RetainLWT}" HorizontalAlignment="Left" Margin="0 3 0 0"/>
</StackPanel>
</StackPanel>
<ContentControl Margin="0 20 0 10">Username</ContentControl>
<TextBox Text="{Binding Username}" MinWidth="150"/>
<ContentControl Margin="0 20 0 10">Password</ContentControl>
<TextBox Text="{Binding Password}" MinWidth="150" PasswordChar="•"/>
<Button Width="75" HorizontalAlignment="Right" Margin="0 40 0 10" Click="Configure">Save</Button>
<StackPanel Margin="0 20 0 10" HorizontalAlignment="Left" Orientation="Horizontal">
<ContentControl>Root Cert Path (.pem/.crt)</ContentControl>
<TextBlock Cursor="Help" Margin="5 0 0 0" VerticalAlignment="Bottom" TextDecorations="Underline">
(What's this?)
<ToolTip.Tip>
<StackPanel>
<TextBlock>
[Experimental]
If set, use this certificate in the TLS configuration for the MQTT connection.
This will be a pem or crt file provided by your broker.
</TextBlock>
</StackPanel>
</ToolTip.Tip>
</TextBlock>
</StackPanel>
<TextBox Text="{Binding RootCAPath}" MinWidth="150"/>
<StackPanel Margin="0 20 0 10" HorizontalAlignment="Left" Orientation="Horizontal">
<ContentControl>Client Cert Path (.pfx)</ContentControl>
<TextBlock Cursor="Help" Margin="5 0 0 0" VerticalAlignment="Bottom" TextDecorations="Underline">
(What's this?)
<ToolTip.Tip>
<StackPanel>
<TextBlock>
[Experimental]
If set, use this certificate in the TLS configuration for the MQTT connection.
This should be the private key .pfx file for a device created in your broker corresponding to this Windows PC.
</TextBlock>
</StackPanel>
</ToolTip.Tip>
</TextBlock>
</StackPanel>
<TextBox Text="{Binding ClientCertPath}" MinWidth="150"/>
<Button Width="75" HorizontalAlignment="Right" Margin="0 40 0 10" Click="Configure">Save</Button>
</StackPanel>
</UserControl>

@ -50,7 +50,7 @@ namespace UserInterface.Views
ICollection<ValidationResult> results;
if (model.IsValid(model, out results))
{
var result = this.client.InvokeAsync(x => x.WriteMqttBrokerSettingsAsync(new MqttSettings() { Host = model.Host, Username = model.Username, Password = model.Password ?? "", Port = model.Port, UseTLS = model.UseTLS }));
var result = this.client.InvokeAsync(x => x.WriteMqttBrokerSettingsAsync(new MqttSettings() { Host = model.Host, Username = model.Username, Password = model.Password ?? "", Port = model.Port, UseTLS = model.UseTLS, RootCAPath = model.RootCAPath, ClientCertPath = model.ClientCertPath, RetainLWT = model.RetainLWT }));
}
}

@ -8,18 +8,18 @@
<ContentControl FontSize="18" FontWeight="Bold">Settings</ContentControl>
<StackPanel Margin="0 20 0 10" HorizontalAlignment="Left" Orientation="Horizontal">
<ContentControl>Name prefix</ContentControl>
<TextBlock Cursor="Help" Margin="5 0 0 0" VerticalAlignment="Bottom" TextDecorations="Underline">(What's this?)
<ToolTip.Tip>
<StackPanel>
<TextBlock>
[Experimental]
This allows you to set a name which will be used to prefix all sensor- and command names. For example:
If a sensor is called "ActiveWindow" and the name prefix is set to "laptop", the sensor will be named "laptop-ActiveWindow" and its entityId will be "laptop_activewindow".
</TextBlock>
<TextBlock Cursor="Help" Margin="5 0 0 0" VerticalAlignment="Bottom" TextDecorations="Underline">(What's this?)
<ToolTip.Tip>
<StackPanel>
<TextBlock>
[Experimental]
This allows you to set a name which will be used to prefix all sensor- and command names. For example:
If a sensor is called "ActiveWindow" and the name prefix is set to "laptop", the sensor will be named "laptop-ActiveWindow" and its entityId will be "laptop_activewindow".
</TextBlock>
</StackPanel>
</ToolTip.Tip>
</TextBlock>
</StackPanel>
</ToolTip.Tip>
</TextBlock>
</StackPanel>
<TextBox Text="{Binding NamePrefix}" HorizontalAlignment="Left" Width="100"/>
<Button Width="75" HorizontalAlignment="Right" Margin="0 40 0 10" Click="Configure">Save</Button>

@ -11,6 +11,10 @@ namespace hass_workstation_service.Communication.InterProcesCommunication.Models
public string Password { get; set; }
public int? Port { get; set; }
public bool UseTLS { get; set; }
public bool RetainLWT { get; set; }
public string RootCAPath { get; set; }
public string ClientCertPath { get; set; }
}
public class MqqtClientStatus

@ -116,7 +116,7 @@ namespace hass_workstation_service.Communication
var message = new MqttApplicationMessageBuilder()
.WithTopic($"homeassistant/{discoverable.Domain}/{this.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(discoverable.GetAutoDiscoveryConfig().NamePrefix, discoverable.ObjectId)}/config")
.WithPayload(clearConfig ? "" : JsonSerializer.Serialize(discoverable.GetAutoDiscoveryConfig(), discoverable.GetAutoDiscoveryConfig().GetType(), options))
.WithRetainFlag()
//.WithRetainFlag()
.Build();
await this.Publish(message);
// if clearconfig is true, also remove previous state messages
@ -125,7 +125,7 @@ namespace hass_workstation_service.Communication
var stateMessage = new MqttApplicationMessageBuilder()
.WithTopic($"homeassistant/{discoverable.Domain}/{this.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(discoverable.GetAutoDiscoveryConfig().NamePrefix, discoverable.ObjectId)}/state")
.WithPayload("")
.WithRetainFlag()
// .WithRetainFlag()
.Build();
await this.Publish(stateMessage);
}

@ -5,6 +5,7 @@ using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using System.Threading.Tasks;
using hass_workstation_service.Communication;
@ -20,6 +21,7 @@ using MQTTnet.Client.Options;
using MQTTnet.Extensions.ManagedClient;
using Serilog;
namespace hass_workstation_service.Data
{
public class ConfigurationService : IConfigurationService
@ -307,22 +309,54 @@ namespace hass_workstation_service.Data
if (configuredBroker != null && configuredBroker.Host != null)
{
var mqttClientOptions = new MqttClientOptionsBuilder()
var mqttClientOptionsBuilder = new MqttClientOptionsBuilder()
.WithTcpServer(configuredBroker.Host, configuredBroker.Port)
.WithTls(new MqttClientOptionsBuilderTlsParameters()
{
UseTls = configuredBroker.UseTLS,
AllowUntrustedCertificates = true,
SslProtocol = configuredBroker.UseTLS ? System.Security.Authentication.SslProtocols.Tls12 : System.Security.Authentication.SslProtocols.None
})
.WithCredentials(configuredBroker.Username, configuredBroker.Password.ToString())
.WithKeepAlivePeriod(TimeSpan.FromSeconds(30))
.WithWillMessage(new MqttApplicationMessageBuilder()
.WithRetainFlag()
.WithKeepAlivePeriod(TimeSpan.FromSeconds(30));
/* Start LWT */
var lwtMessage = new MqttApplicationMessageBuilder()
.WithTopic($"homeassistant/sensor/{_deviceConfigModel.Name}/availability")
.WithPayload("offline")
.Build())
.Build();
.WithPayload("offline");
if (configuredBroker.RetainLWT) {
lwtMessage.WithRetainFlag();
}
mqttClientOptionsBuilder.WithWillMessage(lwtMessage.Build());
/* End LWT */
/* Start TLS/Certificate configuration */
var tlsParameters = new MqttClientOptionsBuilderTlsParameters()
{
UseTls = configuredBroker.UseTLS,
AllowUntrustedCertificates = true,
SslProtocol = configuredBroker.UseTLS ? System.Security.Authentication.SslProtocols.Tls12 : System.Security.Authentication.SslProtocols.None
};
var certs = new List<X509Certificate>();
if (configuredBroker.RootCAPath != null) {
certs.Add(new X509Certificate2(configuredBroker.RootCAPath));
}
if (configuredBroker.ClientCertPath != null)
{
certs.Add(new X509Certificate2(configuredBroker.ClientCertPath));
}
if (certs.Count > 0) {
// IF certs are configured, let's add them here
tlsParameters.Certificates = certs;
}
mqttClientOptionsBuilder.WithTls(tlsParameters);
/* End TLS/Certificate Configuration */
var mqttClientOptions = mqttClientOptionsBuilder.Build();
return new ManagedMqttClientOptionsBuilder().WithClientOptions(mqttClientOptions).Build();
}
else
@ -541,7 +575,10 @@ namespace hass_workstation_service.Data
Username = settings.Username,
Password = settings.Password ?? "",
Port = settings.Port ?? 1883,
UseTLS = settings.UseTLS
UseTLS = settings.UseTLS,
RetainLWT = settings.RetainLWT,
RootCAPath = settings.RootCAPath,
ClientCertPath = settings.ClientCertPath
};
await JsonSerializer.SerializeAsync(stream, configuredBroker);
@ -560,7 +597,10 @@ namespace hass_workstation_service.Data
Username = broker?.Username,
Password = broker?.Password,
Port = broker?.Port,
UseTLS = broker?.UseTLS ?? false
UseTLS = broker?.UseTLS ?? false,
RetainLWT = broker?.RetainLWT ?? true,
RootCAPath = broker?.RootCAPath,
ClientCertPath = broker?.RootCAPath
};
}

@ -8,11 +8,33 @@ namespace hass_workstation_service.Data
private string username;
private string password;
private int? port;
private string rootCAPath;
private string clientCertPath;
public string Host { get; set; }
public int Port { get => port ?? 1883; set => port = value; }
public bool UseTLS { get; set; }
// Before this option, Retains was the default, so let's keep that here to not break backwards compatibility
public bool RetainLWT { get; set; } = true;
public string RootCAPath {
get
{
if (rootCAPath!= null) return rootCAPath;
return "";
}
set => rootCAPath = value;
}
public string ClientCertPath {
get
{
if (clientCertPath != null) return clientCertPath;
return "";
}
set => clientCertPath = value;
}
public string Username
{

@ -40,8 +40,8 @@ namespace hass_workstation_service.Domain.Commands
var message = new MqttApplicationMessageBuilder()
.WithTopic(GetAutoDiscoveryConfig().State_topic)
.WithPayload(state)
.WithExactlyOnceQoS()
.WithRetainFlag()
//.WithExactlyOnceQoS()
//.WithRetainFlag()
.Build();
await Publisher.Publish(message);
PreviousPublishedState = state;

@ -40,8 +40,8 @@ namespace hass_workstation_service.Domain.Sensors
var message = new MqttApplicationMessageBuilder()
.WithTopic(GetAutoDiscoveryConfig().State_topic)
.WithPayload(state)
.WithExactlyOnceQoS()
.WithRetainFlag()
//.WithExactlyOnceQoS()
//.WithRetainFlag()
.Build();
await Publisher.Publish(message);
PreviousPublishedState = state;

@ -43,7 +43,7 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="UserInterface.pdb">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</Content>
</ItemGroup>

Loading…
Cancel
Save