Skip to content

Commit

Permalink
Feature: Introduce MQTT Client SDK (#47)
Browse files Browse the repository at this point in the history
  • Loading branch information
denzelst authored Oct 13, 2024
2 parents 08f350d + 71d92ba commit 871e345
Show file tree
Hide file tree
Showing 21 changed files with 329 additions and 23 deletions.
3 changes: 3 additions & 0 deletions .devcontainer/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ AWS_ACCESS_KEY_ID="see AWS"
AWS_SECRET_ACCESS_KEY="see AWS"
AWS_DEFAULT_REGION="ap-southeast-2"

MQTT_BROKER_HOSTNAME="queue.vibegrow.pro"
MQTT_CLIENT_ID="test-api"

POSTGRES_DB="mortein"
POSTGRES_HOST="database"
POSTGRES_PASSWORD="password"
Expand Down
4 changes: 4 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[*.cs]

# CA1848: Use the LoggerMessage delegates
dotnet_diagnostic.CA1848.severity = none
2 changes: 1 addition & 1 deletion .github/workflows/deploy-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
- env:
PARAMS_TEMP_FILE: /tmp/template-params
run: |
echo '${{ toJson(secrets) }}' \
echo '${{ toJson(vars) }}' \
| jq -r '
def transformkey(key): key | ascii_downcase | gsub("(?<word>[a-z]+)"; "\(.word[0:1] | ascii_upcase)\(.word[1:])") | gsub("_"; "");
[to_entries[] | {key: transformkey(.key), value: .value} | "\(.key)=\(.value)"] | join(";")
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/status-checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ on:
branches:
- main

permissions:
contents: read
id-token: write

jobs:
npm:
runs-on: ubuntu-24.04
Expand Down Expand Up @@ -87,6 +91,13 @@ jobs:
echo '${{ toJson(vars) }}' \
| jq -r 'to_entries[] | "\(.key)=\(.value)"' >> .devcontainer/.env
echo 'GITHUB_ACTIONS=true' >> .devcontainer/.env
- uses: aws-actions/configure-aws-credentials@v4
with:
audience: sts.amazon.com
aws-region: ${{ vars.AWS_DEFAULT_REGION }}
role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/github-actions

- name: .NET Tests
uses: devcontainers/ci@v0.3.1900000349
with:
Expand Down
13 changes: 13 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "C#: Mortein",
"type": "coreclr",
"request": "launch",
"program": "${workspaceFolder}/Mortein/bin/Debug/net8.0/Mortein.dll",
"console": "integratedTerminal",
"justMyCode": false
}
]
}
20 changes: 20 additions & 0 deletions Mortein.IntegrationTests/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,19 @@
"Amazon.Lambda.Core": "2.2.0"
}
},
"AWSSDK.Core": {
"type": "Transitive",
"resolved": "3.7.400.33",
"contentHash": "PZMgJzl+ld73WXa+8FlBEMPF4q4r1Aqj76tnp1sKQGkWY1JLvchW50v1DLrUpkdzBuvVFi4EEMF/QlRGGUw17Q=="
},
"AWSSDK.S3": {
"type": "Transitive",
"resolved": "3.7.404.5",
"contentHash": "owBlG8J79XXyJ3KK1nq2p4vltQ6CJtbdU7uMvwnOGuVNy9o5pgdW3hjjGh+sdVikHeU7JRSw0hl9KmvpdAaw7A==",
"dependencies": {
"AWSSDK.Core": "[3.7.400.33, 4.0.0)"
}
},
"Microsoft.AspNetCore.OpenApi": {
"type": "Transitive",
"resolved": "7.0.10",
Expand Down Expand Up @@ -496,6 +509,11 @@
"Newtonsoft.Json": "13.0.1"
}
},
"MQTTnet": {
"type": "Transitive",
"resolved": "4.3.7.1207",
"contentHash": "ah7aHXoedWp5m5a4zy2u4phOEVj0QFYzOb5tFKQeV8RRBrxp+1QREF4ymZuG8D+hzB2dhtrrG81WxTFv0PzOeQ=="
},
"NETStandard.Library": {
"type": "Transitive",
"resolved": "2.0.3",
Expand Down Expand Up @@ -685,9 +703,11 @@
"mortein": {
"type": "Project",
"dependencies": {
"AWSSDK.S3": "[3.7.404.5, )",
"Amazon.Lambda.AspNetCoreServer": "[9.0.0, )",
"Amazon.Lambda.Core": "[2.2.0, )",
"Amazon.Lambda.Serialization.SystemTextJson": "[2.4.1, )",
"MQTTnet": "[4.3.7.1207, )",
"Microsoft.AspNetCore.OpenApi": "[7.0.10, )",
"Microsoft.EntityFrameworkCore": "[8.0.8, )",
"Mortein.Types": "[1.3.0, )",
Expand Down
20 changes: 20 additions & 0 deletions Mortein.UnitTests/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,19 @@
"Amazon.Lambda.Core": "2.2.0"
}
},
"AWSSDK.Core": {
"type": "Transitive",
"resolved": "3.7.400.33",
"contentHash": "PZMgJzl+ld73WXa+8FlBEMPF4q4r1Aqj76tnp1sKQGkWY1JLvchW50v1DLrUpkdzBuvVFi4EEMF/QlRGGUw17Q=="
},
"AWSSDK.S3": {
"type": "Transitive",
"resolved": "3.7.404.5",
"contentHash": "owBlG8J79XXyJ3KK1nq2p4vltQ6CJtbdU7uMvwnOGuVNy9o5pgdW3hjjGh+sdVikHeU7JRSw0hl9KmvpdAaw7A==",
"dependencies": {
"AWSSDK.Core": "[3.7.400.33, 4.0.0)"
}
},
"Microsoft.AspNetCore.OpenApi": {
"type": "Transitive",
"resolved": "7.0.10",
Expand Down Expand Up @@ -239,6 +252,11 @@
"Newtonsoft.Json": "13.0.1"
}
},
"MQTTnet": {
"type": "Transitive",
"resolved": "4.3.7.1207",
"contentHash": "ah7aHXoedWp5m5a4zy2u4phOEVj0QFYzOb5tFKQeV8RRBrxp+1QREF4ymZuG8D+hzB2dhtrrG81WxTFv0PzOeQ=="
},
"NETStandard.Library": {
"type": "Transitive",
"resolved": "2.0.3",
Expand Down Expand Up @@ -400,9 +418,11 @@
"mortein": {
"type": "Project",
"dependencies": {
"AWSSDK.S3": "[3.7.404.5, )",
"Amazon.Lambda.AspNetCoreServer": "[9.0.0, )",
"Amazon.Lambda.Core": "[2.2.0, )",
"Amazon.Lambda.Serialization.SystemTextJson": "[2.4.1, )",
"MQTTnet": "[4.3.7.1207, )",
"Microsoft.AspNetCore.OpenApi": "[7.0.10, )",
"Microsoft.EntityFrameworkCore": "[8.0.8, )",
"Mortein.Types": "[1.3.0, )",
Expand Down
2 changes: 1 addition & 1 deletion Mortein/Controllers/DeviceController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Mortein.Controllers;
/// <summary>
/// API controller for devices.
/// </summary>
///
///
/// <param name="context">The context which enables interaction with the database.</param>
[ApiController]
[Route("[controller]")]
Expand Down
8 changes: 4 additions & 4 deletions Mortein/Controllers/HealthcheckDataController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Mortein.Controllers;
/// <summary>
/// API controller for healthcheck data.
/// </summary>
///
///
/// <param name="context">The context which enables interaction with the database.</param>
[ApiController]
[Route("Device/{deviceId}/[controller]")]
Expand All @@ -21,7 +21,7 @@ public class HealthcheckDataController(DatabaseContext context) : ControllerBase
///
/// <remarks>
/// Retrieve all healthcheck data by device ID.
///
///
/// The data is in descending order by timestamp; that is, the latest datum is first.
/// </remarks>
[HttpGet()]
Expand All @@ -41,9 +41,9 @@ public IEnumerable<HealthcheckDatum> GetAllHealthcheckDataForDevice(Guid deviceI
/// <remarks>
/// Retrieve the latest healthcheck datum by device ID.
/// </remarks>
///
///
/// <returns>The latest datum for the device.</returns>
///
///
/// <param name="deviceId">The ID of the device for which to get the latest datum.</param>
[HttpGet("Latest")]
[ProducesResponseType<HealthcheckDatum>(StatusCodes.Status200OK)]
Expand Down
2 changes: 1 addition & 1 deletion Mortein/LambdaEntryPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public class LambdaEntryPoint : Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFu
/// The builder has configuration, logging and Amazon API Gateway already configured. The
/// startup class needs to be configured in this method using the UseStartup() method.
/// </summary>
///
///
/// <param name="builder">The IWebHostBuilder to configure.</param>
protected override void Init(IWebHostBuilder builder)
{
Expand Down
2 changes: 2 additions & 0 deletions Mortein/Mortein.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@
<PackageReference Include="Amazon.Lambda.AspNetCoreServer" Version="9.0.0" />
<PackageReference Include="Amazon.Lambda.Core" Version="2.2.0" />
<PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.4.1" />
<PackageReference Include="AWSSDK.S3" Version="3.7.404.5" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
<PackageReference Include="MQTTnet" Version="4.3.7.1207" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.7.3" />
</ItemGroup>
Expand Down
72 changes: 72 additions & 0 deletions Mortein/Mqtt/Extensions/ServiceCollectionExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using Mortein.Mqtt.Services;
using MQTTnet.Client;

namespace Mortein.Mqtt.Extensions;

/// <summary>
/// Extension methods for setting up MQTT client related services in an
/// <see cref="IServiceCollection" />.
/// </summary>
public static class ServiceCollectionExtension
{
/// <summary>
/// Registers a hosted MQTT client as a service in the <see cref="IServiceCollection" />.
/// </summary>
///
/// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
///
/// <returns>The same service collection so that multiple calls can be chained.</returns>
public static IServiceCollection AddMqttClientHostedService(this IServiceCollection services)
{
services.AddMqttClientServiceWithConfig(optionsBuilder =>
{
var certificate = MqttAuthentication.GetAwsMqttCertificate();
optionsBuilder
.WithClientId(Environment.GetEnvironmentVariable("MQTT_CLIENT_ID"))
.WithoutPacketFragmentation()
.WithTcpServer(Environment.GetEnvironmentVariable("MQTT_BROKER_HOSTNAME"))
.WithTlsOptions(options =>
{
options
.UseTls()
.WithAllowUntrustedCertificates(false)
.WithClientCertificates([certificate.Result])
.WithIgnoreCertificateChainErrors(false)
.WithIgnoreCertificateRevocationErrors(false);
});
});
return services;
}

/// <summary>
/// Registers a hosted MQTT client as a service in the <see cref="IServiceCollection" />.
/// </summary>
///
/// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
/// <param name="configure">
/// A required action to configure the <see cref="MqttClientOptionsBuilder" /> for the client.
/// </param>
///
/// <returns>The same service collection so that multiple calls can be chained.</returns>
private static IServiceCollection AddMqttClientServiceWithConfig(this IServiceCollection services, Action<MqttClientOptionsBuilder> configure)
{
services.AddSingleton(_ =>
{
var optionBuilder = new MqttClientOptionsBuilder();
configure(optionBuilder);
return optionBuilder.Build();
});
services.AddSingleton<MqttClientService>();
services.AddSingleton<IHostedService>(serviceProvider =>
{
return serviceProvider.GetService<MqttClientService>()!;
});
services.AddSingleton(serviceProvider =>
{
var mqttClientService = serviceProvider.GetService<MqttClientService>();
var mqttClientServiceProvider = new MqttClientServiceProvider(mqttClientService!);
return mqttClientServiceProvider;
});
return services;
}
}
43 changes: 43 additions & 0 deletions Mortein/Mqtt/MqttAuthentication.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Security.Cryptography.X509Certificates;
using Amazon;
using Amazon.S3;
using Amazon.S3.Model;

namespace Mortein.Mqtt;

/// <summary>
/// Provides authentication for the MQTT service.
/// </summary>
public static class MqttAuthentication
{
private static readonly string credentialBucketName = "api-mqtt-certificate";
private static readonly string objectName = "api.pfx";

private static readonly AmazonS3Client s3 = new(RegionEndpoint.APSoutheast2);

private static async Task<GetObjectResponse?> GetCredentialFileObject()
{
return await s3.GetObjectAsync(credentialBucketName, objectName);
}

/// <summary>
/// Return the <see cref="X509Certificate2"/> with which to authenticate to the MQTT broker.
/// </summary>
///
/// <returns>The <see cref="X509Certificate2"/> with which to authenticate to the MQTT broker</returns>
public static async Task<X509Certificate2> GetAwsMqttCertificate()
{
var credentialFileObject = (await GetCredentialFileObject())!;

byte[] credentialBytes;

using var credentialFileStream = credentialFileObject.ResponseStream;
using var memoryStream = new MemoryStream();
{
credentialFileStream.CopyTo(memoryStream);
credentialBytes = memoryStream.ToArray();
}

return new(credentialBytes);
}
}
4 changes: 4 additions & 0 deletions Mortein/Mqtt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# MQTT

Everything in this directory comes from /~https://github.com/rafiulgits/mqtt-client-dotnet-core with
limited changes.
6 changes: 6 additions & 0 deletions Mortein/Mqtt/Services/IMqttClientService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Mortein.Mqtt.Services;

/// <summary>
/// Represents an ASP.NET service type which provides a managed MQTT client.
/// </summary>
public interface IMqttClientService : IHostedService { }
Loading

0 comments on commit 871e345

Please sign in to comment.