Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Introduce MQTT Client SDK #47

Merged
merged 20 commits into from
Oct 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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