Skip to content

Commit

Permalink
[#2955] Fix MQTT5 Connect Reason Codes
Browse files Browse the repository at this point in the history
MQTT5 defines new reason codes to be included in CONNACK packets when
connection establishment fails. The abstract adapter base class has been
changed accordingly.

Also added integration tests based on HiveMQ client for testing
connection establishment.
  • Loading branch information
sophokles73 committed Nov 27, 2024
1 parent 7dfcc6a commit cd163d4
Show file tree
Hide file tree
Showing 8 changed files with 1,263 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2016, 2023 Contributors to the Eclipse Foundation
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
Expand Down Expand Up @@ -41,6 +41,9 @@
import org.eclipse.hono.adapter.AbstractProtocolAdapterBase;
import org.eclipse.hono.adapter.AdapterConnectionsExceededException;
import org.eclipse.hono.adapter.AuthorizationException;
import org.eclipse.hono.adapter.ConnectionDurationExceededException;
import org.eclipse.hono.adapter.DataVolumeExceededException;
import org.eclipse.hono.adapter.TenantConnectionsExceededException;
import org.eclipse.hono.adapter.auth.device.AuthHandler;
import org.eclipse.hono.adapter.auth.device.ChainAuthHandler;
import org.eclipse.hono.adapter.auth.device.CredentialsApiAuthProvider;
Expand Down Expand Up @@ -90,6 +93,7 @@
import io.netty.handler.codec.mqtt.MqttConnectReturnCode;
import io.netty.handler.codec.mqtt.MqttProperties;
import io.netty.handler.codec.mqtt.MqttQoS;
import io.netty.handler.codec.mqtt.MqttVersion;
import io.opentracing.Span;
import io.opentracing.SpanContext;
import io.opentracing.log.Fields;
Expand Down Expand Up @@ -518,7 +522,8 @@ final void handleEndpointConnection(final MqttEndpoint endpoint) {
log.debug("rejecting connection request from client [clientId: {}], cause:",
endpoint.clientIdentifier(), t);

final MqttConnectReturnCode code = getConnectReturnCode(t);
final boolean isPreMqtt5 = ((int) MqttVersion.MQTT_5.protocolLevel()) > endpoint.protocolVersion();
final var code = isPreMqtt5 ? getMqtt3ConnackReturnCode(t) : getMqtt5ConnackReasonCode(t);
rejectConnectionRequest(endpoint, code, span);
TracingHelper.logError(span, t);
}
Expand Down Expand Up @@ -1106,18 +1111,18 @@ final MqttDeviceEndpoint createMqttDeviceEndpoint(
return mqttDeviceEndpoint;
}

private static MqttConnectReturnCode getConnectReturnCode(final Throwable e) {

if (e instanceof MqttConnectionException) {
return ((MqttConnectionException) e).code();
} else if (e instanceof AuthorizationException) {
if (e instanceof AdapterConnectionsExceededException) {
return MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE;
} else {
return MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED;
}
} else if (e instanceof ServiceInvocationException) {
switch (((ServiceInvocationException) e).getErrorCode()) {
private static MqttConnectReturnCode getMqtt3ConnackReturnCode(final Throwable e) {
if (e instanceof MqttConnectionException connectionException) {
return connectionException.code();
}
if (e instanceof AdapterConnectionsExceededException) {
return MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE;
}
if (e instanceof AuthorizationException) {
return MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED;
}
if (e instanceof ServiceInvocationException exception) {
switch (exception.getErrorCode()) {
case HttpURLConnection.HTTP_UNAUTHORIZED:
case HttpURLConnection.HTTP_NOT_FOUND:
return MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD;
Expand All @@ -1126,9 +1131,39 @@ private static MqttConnectReturnCode getConnectReturnCode(final Throwable e) {
default:
return MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED;
}
} else {
return MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED;
}

return MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED;
}

private static MqttConnectReturnCode getMqtt5ConnackReasonCode(final Throwable e) {

if (e instanceof MqttConnectionException connectionException) {
return connectionException.code();
}
if (e instanceof AdapterConnectionsExceededException) {
return MqttConnectReturnCode.CONNECTION_REFUSED_USE_ANOTHER_SERVER;
}
if (e instanceof TenantConnectionsExceededException
|| e instanceof DataVolumeExceededException
|| e instanceof ConnectionDurationExceededException) {
return MqttConnectReturnCode.CONNECTION_REFUSED_QUOTA_EXCEEDED;
}
if (e instanceof AuthorizationException) {
return MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED_5;
}
if (e instanceof ServiceInvocationException exception) {
switch (exception.getErrorCode()) {
case HttpURLConnection.HTTP_UNAUTHORIZED:
case HttpURLConnection.HTTP_NOT_FOUND:
return MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USERNAME_OR_PASSWORD;
case HttpURLConnection.HTTP_UNAVAILABLE:
return MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE_5;
default:
return MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED_5;
}
}
return MqttConnectReturnCode.CONNECTION_REFUSED_UNSPECIFIED_ERROR;
}

/**
Expand Down
6 changes: 6 additions & 0 deletions bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,12 @@ quarkus.vertx.max-event-loop-execute-time=${max.event-loop.execute-time:20000}
</dependency>

<!-- Testing -->
<dependency>
<groupId>com.hivemq</groupId>
<artifactId>hivemq-mqtt-client</artifactId>
<version>1.3.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.hono</groupId>
<artifactId>core-test-utils</artifactId>
Expand Down
44 changes: 29 additions & 15 deletions site/documentation/content/user-guide/mqtt-adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ consumers and for receiving commands from applications and sending back response

The MQTT adapter is **not** a general purpose MQTT broker. In particular the adapter

* supports MQTT 3.1.1 only.
* supports clients connecting using MQTT 3.1.1 or 5.0 only.
* does not maintain session state for clients and thus always sets the *session present* flag in its CONNACK packet
to `0`, regardless of the value of the *clean session* flag provided in a client's CONNECT packet.
* ignores any *Will* included in a client's CONNECT packet.
Expand All @@ -23,7 +23,7 @@ The MQTT adapter is **not** a general purpose MQTT broker. In particular the ada
## Authentication

The MQTT adapter by default requires clients (devices or gateway components) to authenticate during connection
establishment. The adapter supports both the authentication based on the username/password provided in an MQTT CONNECT
establishment. The adapter supports both authentication based on the username/password provided in an MQTT CONNECT
packet as well as client certificate based authentication as part of a TLS handshake for that purpose.

The adapter tries to authenticate the device using these mechanisms in the following order
Expand All @@ -46,7 +46,8 @@ in order to support this mechanism.

The MQTT adapter supports authenticating clients based on credentials provided during MQTT connection establishment.
This means that clients need to provide a *user* and a *password* field in their MQTT CONNECT packet as defined in
[MQTT Version 3.1.1, Section 3.1](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718028)
[MQTT Version 3.1.1, Section 3.1.3](https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718031)
and [MQTT Version 5.0, Section 3.1.3](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901058)
when connecting to the MQTT adapter. The username provided in the *user* field must match the pattern
*auth-id@tenant*, e.g. `sensor1@DEFAULT_TENANT`.

Expand All @@ -68,8 +69,9 @@ concepts.
The MQTT adapter supports authenticating clients based on a signed
[JSON Web Token](https://www.rfc-editor.org/rfc/rfc7519) (JWT) provided during MQTT connection establishment. This requires
a client to provide a *client identifier*, a *user* and a *password* field in its MQTT CONNECT packet as defined in
[MQTT Version 3.1.1, Section 3.1](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718028)
when connecting to the MQTT adapter. The JWT must be sent in the password field. The content of the *user* field is
[MQTT Version 3.1.1, Section 3.1.3](https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718031)
and [MQTT Version 5.0, Section 3.1.3](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901058)
when connecting to the MQTT adapter. The JWT must be sent in the *password* field. The content of the *user* field is
ignored. The information about the tenant and the authentication identifier can be presented to the protocol adapter in
one of two ways:

Expand Down Expand Up @@ -107,26 +109,38 @@ a client tries to connect and/or send a message to the adapter.

### Connection Limits

The adapter rejects a client’s connection attempt with return code
The adapter rejects a client’s attempt to connect using

* `0x03` (*Connection Refused: server unavailable*), if the maximum number of connections per protocol adapter instance
is reached
* `0x05` (*Connection Refused: not authorized*), if the maximum number of simultaneously connected devices for the
tenant is reached.
* MQTT 3.1.1 with return code `0x03` (*Server Unavailable*),
* MQTT 5.0 with reason code `0x9C` (*Use Another Server*),

if the maximum number of connections per protocol adapter instance is reached.

The adapter rejects a client’s attempt to connect using

* MQTT 3.1.1 with return code `0x05` (*Not Authorized*),
* MQTT 5.0 with reason code `0x97` (*Quota Exceeded*),

if the maximum number of simultaneously connected devices for the tenant is reached.

### Connection Duration Limits

The adapter rejects a client’s connection attempt with return code `0x05` (*Connection Refused: not authorized*), if the
[connection duration limit]({{< relref "/concepts/resource-limits#connection-duration-limit" >}}) that has been
configured for the client’s tenant is exceeded.
The adapter rejects a client’s attempt to connect using

* MQTT 3.1.1 with return code `0x05` (*Not Authorized*)
* MQTT 5.0 with reason code `0x97` (*Quota Exceeded*)

if the [connection duration limit]({{< relref "/concepts/resource-limits#connection-duration-limit" >}})
that has been configured for the client’s tenant is exceeded.

### Message Limits

The adapter

* rejects a client's connection attempt with return code `0x05` (*Connection Refused: not authorized*),
* rejects a client's attempt to connect using MQTT 3.1.1 with return code `0x05` (*Not Authorized*),
* rejects a client's attempt to connect using MQTT 5.0 with reason code `0x97` (*Quota Exceeded*),
* discards any MQTT PUBLISH packet containing telemetry data or an event that is sent by a client and
* rejects any AMQP 1.0 message containing a command sent by a north bound application
* rejects any command messages sent by a north bound application

if the [message limit]({{< relref "/concepts/resource-limits.md" >}}) that has been configured for the device’s tenant
is exceeded.
Expand Down
5 changes: 5 additions & 0 deletions tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,11 @@
<artifactId>vertx-mqtt</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.hivemq</groupId>
<artifactId>hivemq-mqtt-client</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.californium</groupId>
<artifactId>californium-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
import io.vertx.mqtt.MqttConnectionException;

/**
* Integration tests for checking connection to the MQTT adapter.
* Integration tests for checking MQTT 3.1.1 based connection to the MQTT adapter.
*
*/
@ExtendWith(VertxExtension.class)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2016, 2023 Contributors to the Eclipse Foundation
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
Expand Down Expand Up @@ -36,7 +36,7 @@
import io.vertx.mqtt.messages.MqttConnAckMessage;

/**
* Base class for MQTT adapter integration tests.
* Base class for MQTT adapter integration tests using MQTT 3.1.1.
*
*/
public abstract class MqttTestBase {
Expand Down Expand Up @@ -70,7 +70,7 @@ public abstract class MqttTestBase {
protected Context context;

/**
* Creates default AMQP client options.
* Creates default MQTT client options.
*/
@BeforeAll
public static void init() {
Expand Down
Loading

0 comments on commit cd163d4

Please sign in to comment.