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

[DONT MERGE] Wiki Update: Updated Client.md #1732

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

TCROC
Copy link

@TCROC TCROC commented May 8, 2023

DO NOT MERGE THIS PR

This is simply an update to the Client.md page in the wiki. Specifically the aws section:

https://github.com/dotnet/MQTTnet/wiki/Client#connecting-with-amazon-aws

GitHub does not currently support PRs for Wikis. StackOverflow recommended this as the next best way to contribute to a wiki. Anyways, here is my contribution as promised. All that is changed is what is shown in the below image. Simply copying and pasting this file into the wiki should update it. Feel free to make any changes to it.

image

And here is a preview so you can look at it before merging it in:


These samples are only valid for version 3. Please check the Samples directory for samples and documentation for version 4.0+

This video tutorial shows how to setup a MQTTnet client for Windows (UWP and IoT Core): https://www.youtube.com/watch?v=PSerr2fvnyc

Preparation

The following code shows how to create a new MQTT client in the most simple way using the MqttFactory.

// Create a new MQTT client.
var factory = new MqttFactory();
var mqttClient = factory.CreateMqttClient();

Client options

All options for the MQTT client are bundled in one class named MqttClientOptions. It is possible to fill options manually in code via the properties but it is recommended to use the MqttClientOptionsBuilder. This class provides a fluent API and allows setting the options easily by providing several overloads and helper methods. The following code shows how to use the builder with several random options.

// Create TCP based options using the builder.
var options = new MqttClientOptionsBuilder()
    .WithClientId("Client1")
    .WithTcpServer("broker.hivemq.com")
    .WithCredentials("bud", "%spencer%")
    .WithTls()
    .WithCleanSession()
    .Build();

Securing passwords

The default implementation uses a string to hold the client password. However this is a security vulnerability because the password is stored in the heap as clear text. It is recommended to use a SecureString for this purpose. But this class is not available for all supported platforms (UWP, netstandard 1.3). This library does not implement it because for other platforms custom implementations like async encryption are required. It is recommended to implement an own IMqttClientCredentials class which returns the decrypted password but does not store it unencrypted.

TCP connection

The following code shows how to set the options of the MQTT client to make use of a TCP based connection.

// Use TCP connection.
var options = new MqttClientOptionsBuilder()
    .WithTcpServer("broker.hivemq.com", 1883) // Port is optional
    .Build();

Secure TCP connection

The following code shows how to use a TLS secured TCP connection (properties are only set as reference):

// Use secure TCP connection.
var options = new MqttClientOptionsBuilder()
    .WithTcpServer("broker.hivemq.com")
    .WithTls()
    .Build();

Certificate based authentication

The following code shows how to connect to the server by using certificate based authentication:

// Certificate based authentication
List<X509Certificate> certs = new List<X509Certificate>
{
    new X509Certificate2("myCert.pfx")
};

var options = new MqttClientOptionBuilder()
    .WithTcpServer(broker, port)
    .WithTls(new MqttClientOptionsBuilderTlsParameters
    {
        UseTls = true,
        Certificates = certs
    })
    .Build();

Using a Custom CA with TLS

If your server is using a certificate by an unrecognized CA (that is not in the system store), you'll need to add a callback
to verify if the certificate is valid. Note this does not work prior to .NET 5.

// Load the ca.crt. On some platforms you may also be able to use X509Certificate2.Import
X509Certificate2 caCrt = new X509Certificate2(File.ReadAllBytes("ca.crt"));

var options = new MqttClientOptionsBuilder()
    .WithTls(
        new MqttClientOptionsBuilderTlsParameters() {
            UseTls = true,
            SslProtocol = System.Security.Authentication.SslProtocols.Tls12,
            CertificateValidationHandler = (certContext) => {
                X509Chain chain = new X509Chain();
                chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
                chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
                chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
                chain.ChainPolicy.VerificationTime = DateTime.Now;
                chain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 0, 0);
                chain.ChainPolicy.CustomTrustStore.Add(caCrt);
                chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;

                // convert provided X509Certificate to X509Certificate2
                var x5092 = new X509Certificate2(certContext.Certificate);

                return chain.Build(x5092);
            }
        }
    )
    .Build();

TLS using a client certificate

The following code shows how to connect to the server by using a client certificate based authentication:

var caCert = X509Certificate.CreateFromCertFile(@"CA-cert.crt");
var clientCert = new X509Certificate2(@"client-certificate.pfx", "ExportPasswordUsedWhenCreatingPfxFile");

var options = new ManagedMqttClientOptionsBuilder()
    .WithClientOptions(new MqttClientOptionsBuilder()
    .WithClientId(Guid.NewGuid().ToString())
    .WithTcpServer(host, port)
    .WithTls(new MqttClientOptionsBuilderTlsParameters()
        {
            UseTls = true,
            SslProtocol = System.Security.Authentication.SslProtocols.Tls12,
            Certificates = new List<X509Certificate>()
            {
                clientCert, caCert
            }
        })
    .Build())
.Build();

The CA certificate is in the *.crt format, the client certificate should be in *.pfx and should have the password that was used to export the file from the private key and certificate originally. The *.pfx file can becreated using openssl as below:

openssl pkcs12 -export -out certificate.pfx -inkey privateKey.key -in clientCertificate.cer

Dealing with special certificates

In order to deal with special certificate errors a special validation callback is available (.NET Framework & netstandard). For UWP apps, a property is available.

// For .NET Framework & netstandard apps:
var options = new MqttClientOptionsBuilder()
    .WithTls(new MqttClientOptionsBuilderTlsParameters
    {
        UseTls = true,
        CertificateValidationCallback = (X509Certificate x, X509Chain y, SslPolicyErrors z, IMqttClientOptions o) =>
            {
                // TODO: Check conditions of certificate by using above parameters.
                return true;
            }
    })
    .Build();

// For UWP apps:
MqttTcpChannel.CustomIgnorableServerCertificateErrorsResolver = o =>
{
    if (o.Server == "server_with_revoked_cert")
    {
        return new []{ ChainValidationResult.Revoked };
    }

    return new ChainValidationResult[0];
};

WebSocket connection

In order to use a WebSocket communication channel the following code is required.

// Use WebSocket connection.
var options = new MqttClientOptionsBuilder()
    .WithWebSocketServer("broker.hivemq.com:8000/mqtt")
    .Build();

Also secure web socket connections can be used via calling the UseTls() method which will switch the protocol from ws:// to wss://. Usually the sub protocol is required which can be added to the URI directly or to a dedicated property.

Connecting

After setting up the MQTT client options a connection can be established. The following code shows how to connect with a server. The CancellationToken.None can be replaced by a valid CancellationToken, of course.

// Use WebSocket connection.
var options = new MqttClientOptionsBuilder()
    .WithWebSocketServer("broker.hivemq.com:8000/mqtt")
    .Build();

await mqttClient.ConnectAsync(options, CancellationToken.None); // Since 3.0.5 with CancellationToken

Reconnecting

If the connection to the server is lost the Disconnected event is fired. The event is also fired if a call to ConnectAsync has failed because the server is not reachable etc. This allows calling the ConnectAsync method only one time and dealing with retries etc. via consuming the Disconnected event. If the reconnect fails the Disconnected event is fired again. The following code shows how to setup this behavior including a short delay. The CancellationToken.None can be replaced by a valid CancellationToken, of course.

mqttClient.UseDisconnectedHandler(async e =>
{
    Console.WriteLine("### DISCONNECTED FROM SERVER ###");
    await Task.Delay(TimeSpan.FromSeconds(5));

    try
    {
        await mqttClient.ConnectAsync(options, CancellationToken.None); // Since 3.0.5 with CancellationToken
    }
    catch
    {
        Console.WriteLine("### RECONNECTING FAILED ###");
    }
});

Consuming messages

The following code shows how to handle incoming messages:

mqttClient.UseApplicationMessageReceivedHandler(e =>
{
    Console.WriteLine("### RECEIVED APPLICATION MESSAGE ###");
    Console.WriteLine($"+ Topic = {e.ApplicationMessage.Topic}");
    Console.WriteLine($"+ Payload = {Encoding.UTF8.GetString(e.ApplicationMessage.Payload)}");
    Console.WriteLine($"+ QoS = {e.ApplicationMessage.QualityOfServiceLevel}");
    Console.WriteLine($"+ Retain = {e.ApplicationMessage.Retain}");
    Console.WriteLine();

    Task.Run(() => mqttClient.PublishAsync("hello/world"));
});

It is also supported to use an async method instead of a synchronized one like in the above example.

⚠️ Publishing messages inside that received messages handler requires to use Task.Run when using a QoS > 0. The reason is that the message handler has to finish first before the next message is received. The reason is to preserve ordering of the application messages.

Subscribing to a topic

Once a connection with the server is established subscribing to a topic is possible. The following code shows how to subscribe to a topic after the MQTT client has connected.

mqttClient.UseConnectedHandler(async e =>
{
    Console.WriteLine("### CONNECTED WITH SERVER ###");

    // Subscribe to a topic
    await mqttClient.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("my/topic").Build());

    Console.WriteLine("### SUBSCRIBED ###");
});

Publishing messages

Application messages can be created using the properties directly or via using the MqttApplicationMessageBuilder. This class has some useful overloads which allows dealing with different payload formats easily. The API of the builder is a fluent API. The following code shows how to compose an application message and publishing them:

var message = new MqttApplicationMessageBuilder()
    .WithTopic("MyTopic")
    .WithPayload("Hello World")
    .WithExactlyOnceQoS()
    .WithRetainFlag()
    .Build();

await mqttClient.PublishAsync(message, CancellationToken.None); // Since 3.0.5 with CancellationToken

It is not required to fill all properties of an application message. The following code shows how to create a very basic application message:

var message = new MqttApplicationMessageBuilder()
    .WithTopic("/MQTTnet/is/awesome")
    .Build();

RPC calls

The extension MQTTnet.Extensions.Rpc (available as nuget) allows sending a request and waiting for the matching reply. This is done via defining a pattern which uses the topic to correlate the request and the response. From client usage it is possible to define a timeout. The following code shows how to send a RPC call.

var rpcClient = new MqttRpcClient(_mqttClient);

var timeout = TimeSpan.FromSeconds(5);
var qos = MqttQualityOfServiceLevel.AtMostOnce;

var response = await rpcClient.ExecuteAsync(timeout, "myMethod", payload, qos);

The device (Arduino, ESP8266 etc.) which responds to the request needs to parse the topic and reply to it. The following code shows how to implement the handler.

// If using the MQTT client PubSubClient it must be ensured 
// that the request topic for each method is subscribed like the following.
mqttClient.subscribe("MQTTnet.RPC/+/ping");
mqttClient.subscribe("MQTTnet.RPC/+/do_something");

// It is not allowed to change the structure of the topic.
// Otherwise RPC will not work.
// So method names can be separated using an _ or . but no +, # or /.
// If it is required to distinguish between devices
// own rules can be defined like the following:
mqttClient.subscribe("MQTTnet.RPC/+/deviceA.ping");
mqttClient.subscribe("MQTTnet.RPC/+/deviceB.ping");
mqttClient.subscribe("MQTTnet.RPC/+/deviceC.getTemperature");

// Within the callback of the MQTT client the topic must be checked
// if it belongs to MQTTnet RPC. The following code shows one
// possible way of doing this.
void mqtt_Callback(char *topic, byte *payload, unsigned int payloadLength)
{
	String topicString = String(topic);

	if (topicString.startsWith("MQTTnet.RPC/")) {
		String responseTopic = topicString + String("/response");

		if (topicString.endsWith("/deviceA.ping")) {
			mqtt_publish(responseTopic, "pong", false);
			return;
		}
	}
}

// Important notes:
// ! Do not send response message with the _retain_ flag set to true.
// ! All required data for a RPC call and the result must be placed into the payload.

Connecting with Amazon AWS

MQTTnet AWS Transport Support

dotnet 7 (specifically tested with 7.0.203):

Provider Transport Status
dotnet tcp
dotnet websocket
websocket4net websocket

Example Project: https://github.com/TCROC/aws-iot-custom-auth

Mono (specifically tested with Unity Game Engine 2021.3.24f1)

Provider Transport Status
mono tcp
mono websocket
websocket4net websocket

Example Project: https://github.com/TCROC/mqttnet-unity-alpn.git

^ This project is currently being used to debug TCP as well

Websockets

AWS Sample:

https://github.com/aws-samples/aws-iot-core-dotnet-app-mqtt-over-websockets-sigv4

TCP with certificates

NOTE: The currently tested Unity Game Engine 2021.3.24f1 does not support TCP

  1. Remove the Amazon root certificate and just pass the client certificate in the .pfx format:
List<X509Certificate> certs = new List<X509Certificate>
{
    new X509Certificate2("ClientCertPath", "ClientCertPass", X509KeyStorageFlags.Exportable)
};
  1. Use the following client code:
var clientOptions = new MqttClientOptionsBuilder()
    .WithTcpServer(endpoint, port)
    .WithKeepAlivePeriod(new TimeSpan(0, 0, 0, 300))
    .WithTls(new MqttClientOptionsBuilderTlsParameters
    {
        UseTls = true,
#pragma warning disable CS0618 // Type or member is obsolete
        CertificateValidationCallback = (X509Certificate x, X509Chain y, System.Net.Security.SslPolicyErrors z, IMqttClientOptions o) =>
#pragma warning restore CS0618 // Type or member is obsolete
        {
            return true;
        },
        AllowUntrustedCertificates = false,
        IgnoreCertificateChainErrors = false,
        IgnoreCertificateRevocationErrors = false,
        Certificates = certs
    })
    .WithProtocolVersion(MqttProtocolVersion.V311)
    .Build();

Troubleshooting

Before digging dip on certificates, make sure that

  • you have the correct URL. Copy it from "Interact" section of the "thing".
  • you can connect to that URL using openssl:
openssl s_client -CAfile awsca.pem -cert xxx.pem.crt -key xxx.pem.key -connect yyy.iot.region.amazonaws.com:8883
  • the certificate attached to the thing is activated.
  • you have attached one policy (or several) that allows the thing to connect, subscribe, publish and receive.
  • you're not using a feature that isn't supported (Retained messages, will messages, Unsupported QoS 2).

If you're absolutely sure of the above, then start troubleshooting certificates.

Some small snippets

Using the client in ASP.NET Core

When using the client there is no difference between the .Net Framework, .Net Core or ASP.NET Core. The configuration above applies. The client cannot be made available using dependency injection.

A sample example is available here mqtt-client-dotnet-core

@dotnet-policy-service
Copy link

@TCROC please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.

@dotnet-policy-service agree [company="{your company}"]

Options:

  • (default - no company specified) I have sole ownership of intellectual property rights to my Submissions and I am not making Submissions in the course of work for my employer.
@dotnet-policy-service agree
  • (when company given) I am making Submissions in the course of work for my employer (or my employer has intellectual property rights in my Submissions by contract or applicable law). I have permission from my employer to make Submissions and enter into this Agreement on behalf of my employer. By signing below, the defined term “You” includes me and my employer.
@dotnet-policy-service agree company="Microsoft"
Contributor License Agreement

Contribution License Agreement

This Contribution License Agreement ( “Agreement” ) is agreed to by the party signing below ( “You” ),
and conveys certain license rights to the .NET Foundation ( “.NET Foundation” ) for Your contributions to
.NET Foundation open source projects. This Agreement is effective as of the latest signature date below.

1. Definitions.

“Code” means the computer software code, whether in human-readable or machine-executable form,
that is delivered by You to .NET Foundation under this Agreement.

“Project” means any of the projects owned or managed by .NET Foundation and offered under a license
approved by the Open Source Initiative (www.opensource.org).

“Submit” is the act of uploading, submitting, transmitting, or distributing code or other content to any
Project, including but not limited to communication on electronic mailing lists, source code control
systems, and issue tracking systems that are managed by, or on behalf of, the Project for the purpose of
discussing and improving that Project, but excluding communication that is conspicuously marked or
otherwise designated in writing by You as “Not a Submission.”

“Submission” means the Code and any other copyrightable material Submitted by You, including any
associated comments and documentation.

2. Your Submission. You must agree to the terms of this Agreement before making a Submission to any
Project. This Agreement covers any and all Submissions that You, now or in the future (except as
described in Section 4 below), Submit to any Project.

3. Originality of Work. You represent that each of Your Submissions is entirely Your original work.

Should You wish to Submit materials that are not Your original work, You may Submit them separately
to the Project if You (a) retain all copyright and license information that was in the materials as You
received them, (b) in the description accompanying Your Submission, include the phrase “Submission
containing materials of a third party:” followed by the names of the third party and any licenses or other
restrictions of which You are aware, and (c) follow any other instructions in the Project’s written

4. Your Employer. References to “employer” in this Agreement include Your employer or anyone else
for whom You are acting in making Your Submission, e.g. as a contractor, vendor, or agent. If Your
Submission is made in the course of Your work for an employer or Your employer has intellectual
property rights in Your Submission by contract or applicable law, You must secure permission from Your
employer to make the Submission before signing this Agreement. In that case, the term “You” in this
Agreement will refer to You and the employer collectively. If You change employers in the future and
desire to Submit additional Submissions for the new employer, then You agree to sign a new Agreement
and secure permission from the new employer before Submitting those Submissions.

5. Licenses.

a. Copyright License. You grant .NET Foundation, and those who receive the Submission directly
or indirectly from .NET Foundation, a perpetual, worldwide, non-exclusive, royalty-free, irrevocable

license in the Submission to reproduce, prepare derivative works of, publicly display, publicly perform,
and distribute the Submission and such derivative works, and to sublicense any or all of the foregoing
rights to third parties.

b. Patent License. You grant .NET Foundation, and those who receive the Submission directly or
indirectly from .NET Foundation, a perpetual, worldwide, non-exclusive, royalty-free, irrevocable license
under Your patent claims that are necessarily infringed by the Submission or the combination of the
Submission with the Project to which it was Submitted to make, have made, use, offer to sell, sell and
import or otherwise dispose of the Submission alone or with the Project.

c. Other Rights Reserved. Each party reserves all rights not expressly granted in this Agreement.
No additional licenses or rights whatsoever (including, without limitation, any implied licenses) are
granted by implication, exhaustion, estoppel or otherwise.

6. Representations and Warranties. You represent that You are legally entitled to grant the above
licenses. You represent that each of Your Submissions is entirely Your original work (except as You may
have disclosed under Section 3 ). You represent that You have secured permission from Your employer to
make the Submission in cases where Your Submission is made in the course of Your work for Your
employer or Your employer has intellectual property rights in Your Submission by contract or applicable
law. If You are signing this Agreement on behalf of Your employer, You represent and warrant that You
have the necessary authority to bind the listed employer to the obligations contained in this Agreement.
You are not expected to provide support for Your Submission, unless You choose to do so. UNLESS
REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING, AND EXCEPT FOR THE WARRANTIES
EXPRESSLY STATED IN SECTIONS 3, 4, AND 6 , THE SUBMISSION PROVIDED UNDER THIS AGREEMENT IS
PROVIDED WITHOUT WARRANTY OF ANY KIND, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY OF
NONINFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.

7. Notice to .NET Foundation. You agree to notify .NET Foundation in writing of any facts or
circumstances of which You later become aware that would make Your representations in this
Agreement inaccurate in any respect.

8. Information about Submissions. You agree that contributions to Projects and information about
contributions may be maintained indefinitely and disclosed publicly, including Your name and other
information that You submit with Your Submission.

9. Governing Law/Jurisdiction. This Agreement is governed by the laws of the State of Washington, and
the parties consent to exclusive jurisdiction and venue in the federal courts sitting in King County,
Washington, unless no federal subject matter jurisdiction exists, in which case the parties consent to
exclusive jurisdiction and venue in the Superior Court of King County, Washington. The parties waive all
defenses of lack of personal jurisdiction and forum non-conveniens.

10. Entire Agreement/Assignment. This Agreement is the entire agreement between the parties, and
supersedes any and all prior agreements, understandings or communications, written or oral, between
the parties relating to the subject matter hereof. This Agreement may be assigned by .NET Foundation.

.NET Foundation dedicates this Contribution License Agreement to the public domain according to the Creative Commons CC0 1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant