Merge pull request #13701 from AndriiLandiak/mqtt-client-id-length
MQTT: validate client id length based on protocol version
This commit is contained in:
		
						commit
						21016025f9
					
				@ -15,9 +15,6 @@
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.server.common.data;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @author Andrew Shvayka
 | 
			
		||||
 */
 | 
			
		||||
public class DataConstants {
 | 
			
		||||
 | 
			
		||||
    public static final String TENANT = "TENANT";
 | 
			
		||||
 | 
			
		||||
@ -15,11 +15,11 @@
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.mqtt;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Created by Valerii Sosliuk on 12/26/2017.
 | 
			
		||||
 */
 | 
			
		||||
import java.io.Serial;
 | 
			
		||||
 | 
			
		||||
public class ChannelClosedException extends RuntimeException {
 | 
			
		||||
 | 
			
		||||
    @Serial
 | 
			
		||||
    private static final long serialVersionUID = 6266638352424706909L;
 | 
			
		||||
 | 
			
		||||
    public ChannelClosedException() {
 | 
			
		||||
@ -40,4 +40,5 @@ public class ChannelClosedException extends RuntimeException {
 | 
			
		||||
    public ChannelClosedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
 | 
			
		||||
        super(message, cause, enableSuppression, writableStackTrace);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -21,9 +21,6 @@ import io.netty.handler.codec.mqtt.MqttPubAckMessage;
 | 
			
		||||
import io.netty.handler.codec.mqtt.MqttSubAckMessage;
 | 
			
		||||
import io.netty.handler.codec.mqtt.MqttUnsubAckMessage;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Created by Valerii Sosliuk on 12/30/2017.
 | 
			
		||||
 */
 | 
			
		||||
public interface MqttClientCallback {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -53,4 +50,5 @@ public interface MqttClientCallback {
 | 
			
		||||
 | 
			
		||||
    default void onDisconnect(MqttMessage mqttDisconnectMessage) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -28,23 +28,46 @@ import java.util.Random;
 | 
			
		||||
 | 
			
		||||
@SuppressWarnings({"WeakerAccess", "unused"})
 | 
			
		||||
public final class MqttClientConfig {
 | 
			
		||||
 | 
			
		||||
    @Getter
 | 
			
		||||
    private final SslContext sslContext;
 | 
			
		||||
    private final String randomClientId;
 | 
			
		||||
 | 
			
		||||
    @Getter
 | 
			
		||||
    @Setter
 | 
			
		||||
    private String ownerId; // [TenantId][IntegrationId] or [TenantId][RuleNodeId] for exceptions logging purposes
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Getter
 | 
			
		||||
    private String clientId;
 | 
			
		||||
    @Getter
 | 
			
		||||
    private int timeoutSeconds = 60;
 | 
			
		||||
    @Getter
 | 
			
		||||
    private MqttVersion protocolVersion = MqttVersion.MQTT_3_1;
 | 
			
		||||
    @Nullable private String username = null;
 | 
			
		||||
    @Nullable private String password = null;
 | 
			
		||||
    @Nullable
 | 
			
		||||
    @Getter
 | 
			
		||||
    @Setter
 | 
			
		||||
    private String username = null;
 | 
			
		||||
    @Nullable
 | 
			
		||||
    @Getter
 | 
			
		||||
    @Setter
 | 
			
		||||
    private String password = null;
 | 
			
		||||
    @Getter
 | 
			
		||||
    @Setter
 | 
			
		||||
    private boolean cleanSession = true;
 | 
			
		||||
    @Nullable private MqttLastWill lastWill;
 | 
			
		||||
    @Nullable
 | 
			
		||||
    @Getter
 | 
			
		||||
    @Setter
 | 
			
		||||
    private MqttLastWill lastWill;
 | 
			
		||||
    @Setter
 | 
			
		||||
    @Getter
 | 
			
		||||
    private Class<? extends Channel> channelClass = NioSocketChannel.class;
 | 
			
		||||
 | 
			
		||||
    @Getter
 | 
			
		||||
    @Setter
 | 
			
		||||
    private boolean reconnect = true;
 | 
			
		||||
    @Getter
 | 
			
		||||
    private long reconnectDelay = 1L;
 | 
			
		||||
    @Getter
 | 
			
		||||
    private int maxBytesInMessage = 8092;
 | 
			
		||||
 | 
			
		||||
    @Getter
 | 
			
		||||
@ -74,109 +97,37 @@ public final class MqttClientConfig {
 | 
			
		||||
    public MqttClientConfig(SslContext sslContext) {
 | 
			
		||||
        this.sslContext = sslContext;
 | 
			
		||||
        Random random = new Random();
 | 
			
		||||
        String id = "netty-mqtt/";
 | 
			
		||||
        StringBuilder id = new StringBuilder("netty-mqtt/");
 | 
			
		||||
        String[] options = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".split("");
 | 
			
		||||
        for(int i = 0; i < 8; i++){
 | 
			
		||||
            id += options[random.nextInt(options.length)];
 | 
			
		||||
        for (int i = 0; i < 8; i++) {
 | 
			
		||||
            id.append(options[random.nextInt(options.length)]);
 | 
			
		||||
        }
 | 
			
		||||
        this.clientId = id;
 | 
			
		||||
        this.randomClientId = id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    public String getClientId() {
 | 
			
		||||
        return clientId;
 | 
			
		||||
        this.clientId = id.toString();
 | 
			
		||||
        this.randomClientId = id.toString();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setClientId(@Nullable String clientId) {
 | 
			
		||||
        if(clientId == null){
 | 
			
		||||
        if (clientId == null) {
 | 
			
		||||
            this.clientId = randomClientId;
 | 
			
		||||
        }else{
 | 
			
		||||
        } else {
 | 
			
		||||
            this.clientId = clientId;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public int getTimeoutSeconds() {
 | 
			
		||||
        return timeoutSeconds;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setTimeoutSeconds(int timeoutSeconds) {
 | 
			
		||||
        if(timeoutSeconds != -1 && timeoutSeconds <= 0){
 | 
			
		||||
        if (timeoutSeconds != -1 && timeoutSeconds <= 0) {
 | 
			
		||||
            throw new IllegalArgumentException("timeoutSeconds must be > 0 or -1");
 | 
			
		||||
        }
 | 
			
		||||
        this.timeoutSeconds = timeoutSeconds;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public MqttVersion getProtocolVersion() {
 | 
			
		||||
        return protocolVersion;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setProtocolVersion(MqttVersion protocolVersion) {
 | 
			
		||||
        if(protocolVersion == null){
 | 
			
		||||
        if (protocolVersion == null) {
 | 
			
		||||
            throw new NullPointerException("protocolVersion");
 | 
			
		||||
        }
 | 
			
		||||
        this.protocolVersion = protocolVersion;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public String getUsername() {
 | 
			
		||||
        return username;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setUsername(@Nullable String username) {
 | 
			
		||||
        this.username = username;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public String getPassword() {
 | 
			
		||||
        return password;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setPassword(@Nullable String password) {
 | 
			
		||||
        this.password = password;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean isCleanSession() {
 | 
			
		||||
        return cleanSession;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setCleanSession(boolean cleanSession) {
 | 
			
		||||
        this.cleanSession = cleanSession;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public MqttLastWill getLastWill() {
 | 
			
		||||
        return lastWill;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setLastWill(@Nullable MqttLastWill lastWill) {
 | 
			
		||||
        this.lastWill = lastWill;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Class<? extends Channel> getChannelClass() {
 | 
			
		||||
        return channelClass;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setChannelClass(Class<? extends Channel> channelClass) {
 | 
			
		||||
        this.channelClass = channelClass;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public SslContext getSslContext() {
 | 
			
		||||
        return sslContext;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean isReconnect() {
 | 
			
		||||
        return reconnect;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setReconnect(boolean reconnect) {
 | 
			
		||||
        this.reconnect = reconnect;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public long getReconnectDelay() {
 | 
			
		||||
        return reconnectDelay;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets the reconnect delay in seconds. Defaults to 1 second.
 | 
			
		||||
     * @param reconnectDelay
 | 
			
		||||
@ -189,10 +140,6 @@ public final class MqttClientConfig {
 | 
			
		||||
        this.reconnectDelay = reconnectDelay;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public int getMaxBytesInMessage() {
 | 
			
		||||
        return maxBytesInMessage;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets the maximum number of bytes in the message for the {@link io.netty.handler.codec.mqtt.MqttDecoder}.
 | 
			
		||||
     * Default value is 8092 as specified by Netty. The absolute maximum size is 256MB as set by the MQTT spec.
 | 
			
		||||
@ -206,4 +153,5 @@ public final class MqttClientConfig {
 | 
			
		||||
        }
 | 
			
		||||
        this.maxBytesInMessage = maxBytesInMessage;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -46,7 +46,9 @@ import io.netty.handler.timeout.IdleStateHandler;
 | 
			
		||||
import io.netty.util.concurrent.DefaultPromise;
 | 
			
		||||
import io.netty.util.concurrent.Future;
 | 
			
		||||
import io.netty.util.concurrent.Promise;
 | 
			
		||||
import lombok.AccessLevel;
 | 
			
		||||
import lombok.Getter;
 | 
			
		||||
import lombok.Setter;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.thingsboard.common.util.ListeningExecutor;
 | 
			
		||||
 | 
			
		||||
@ -67,18 +69,28 @@ import java.util.concurrent.atomic.AtomicInteger;
 | 
			
		||||
@Slf4j
 | 
			
		||||
final class MqttClientImpl implements MqttClient {
 | 
			
		||||
 | 
			
		||||
    @Getter(AccessLevel.PACKAGE)
 | 
			
		||||
    private final Set<String> serverSubscriptions = new HashSet<>();
 | 
			
		||||
    @Getter(AccessLevel.PACKAGE)
 | 
			
		||||
    private final ConcurrentMap<Integer, MqttPendingUnsubscription> pendingServerUnsubscribes = new ConcurrentHashMap<>();
 | 
			
		||||
    @Getter(AccessLevel.PACKAGE)
 | 
			
		||||
    private final ConcurrentMap<Integer, MqttIncomingQos2Publish> qos2PendingIncomingPublishes = new ConcurrentHashMap<>();
 | 
			
		||||
    @Getter(AccessLevel.PACKAGE)
 | 
			
		||||
    private final ConcurrentMap<Integer, MqttPendingPublish> pendingPublishes = new ConcurrentHashMap<>();
 | 
			
		||||
    @Getter(AccessLevel.PACKAGE)
 | 
			
		||||
    private final HashMultimap<String, MqttSubscription> subscriptions = HashMultimap.create();
 | 
			
		||||
    @Getter(AccessLevel.PACKAGE)
 | 
			
		||||
    private final ConcurrentMap<Integer, MqttPendingSubscription> pendingSubscriptions = new ConcurrentHashMap<>();
 | 
			
		||||
    @Getter(AccessLevel.PACKAGE)
 | 
			
		||||
    private final Set<String> pendingSubscribeTopics = new HashSet<>();
 | 
			
		||||
    @Getter(AccessLevel.PACKAGE)
 | 
			
		||||
    private final HashMultimap<MqttHandler, MqttSubscription> handlerToSubscription = HashMultimap.create();
 | 
			
		||||
    private final AtomicInteger nextMessageId = new AtomicInteger(1);
 | 
			
		||||
 | 
			
		||||
    @Getter
 | 
			
		||||
    private final MqttClientConfig clientConfig;
 | 
			
		||||
 | 
			
		||||
    @Getter(AccessLevel.PACKAGE)
 | 
			
		||||
    private final MqttHandler defaultHandler;
 | 
			
		||||
 | 
			
		||||
    private final ReconnectStrategy reconnectStrategy;
 | 
			
		||||
@ -88,12 +100,15 @@ final class MqttClientImpl implements MqttClient {
 | 
			
		||||
    private volatile Channel channel;
 | 
			
		||||
 | 
			
		||||
    private volatile boolean disconnected = false;
 | 
			
		||||
    @Getter
 | 
			
		||||
    private volatile boolean reconnect = false;
 | 
			
		||||
    private String host;
 | 
			
		||||
    private int port;
 | 
			
		||||
    @Getter
 | 
			
		||||
    @Setter
 | 
			
		||||
    private MqttClientCallback callback;
 | 
			
		||||
 | 
			
		||||
    @Getter
 | 
			
		||||
    private final ListeningExecutor handlerExecutor;
 | 
			
		||||
 | 
			
		||||
    private final static int DISCONNECT_FALLBACK_DELAY_SECS = 1;
 | 
			
		||||
@ -240,11 +255,6 @@ final class MqttClientImpl implements MqttClient {
 | 
			
		||||
        this.eventLoop = eventLoop;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public ListeningExecutor getHandlerExecutor() {
 | 
			
		||||
        return this.handlerExecutor;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Subscribe on the given topic. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler
 | 
			
		||||
     *
 | 
			
		||||
@ -446,16 +456,6 @@ final class MqttClientImpl implements MqttClient {
 | 
			
		||||
        return future;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve the MqttClient configuration
 | 
			
		||||
     *
 | 
			
		||||
     * @return The {@link MqttClientConfig} instance we use
 | 
			
		||||
     */
 | 
			
		||||
    @Override
 | 
			
		||||
    public MqttClientConfig getClientConfig() {
 | 
			
		||||
        return clientConfig;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void disconnect() {
 | 
			
		||||
        if (disconnected) {
 | 
			
		||||
@ -480,25 +480,15 @@ final class MqttClientImpl implements MqttClient {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void setCallback(MqttClientCallback callback) {
 | 
			
		||||
        this.callback = callback;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    ///////////////////////////////////////////// PRIVATE API /////////////////////////////////////////////
 | 
			
		||||
 | 
			
		||||
    public boolean isReconnect() {
 | 
			
		||||
        return reconnect;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void onSuccessfulReconnect() {
 | 
			
		||||
        if (callback != null) {
 | 
			
		||||
            callback.onSuccessfulReconnect();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    ChannelFuture sendAndFlushPacket(Object message) {
 | 
			
		||||
        if (this.channel == null) {
 | 
			
		||||
            return null;
 | 
			
		||||
@ -576,7 +566,7 @@ final class MqttClientImpl implements MqttClient {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void checkSubscriptions(String topic, Promise<Void> promise) {
 | 
			
		||||
        if (!(this.subscriptions.containsKey(topic) && this.subscriptions.get(topic).size() != 0) && this.serverSubscriptions.contains(topic)) {
 | 
			
		||||
        if (!(this.subscriptions.containsKey(topic) && !this.subscriptions.get(topic).isEmpty()) && this.serverSubscriptions.contains(topic)) {
 | 
			
		||||
            MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.UNSUBSCRIBE, false, MqttQoS.AT_LEAST_ONCE, false, 0);
 | 
			
		||||
            MqttMessageIdVariableHeader variableHeader = getNewMessageId();
 | 
			
		||||
            MqttUnsubscribePayload payload = new MqttUnsubscribePayload(Collections.singletonList(topic));
 | 
			
		||||
@ -614,38 +604,6 @@ final class MqttClientImpl implements MqttClient {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ConcurrentMap<Integer, MqttPendingSubscription> getPendingSubscriptions() {
 | 
			
		||||
        return pendingSubscriptions;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    HashMultimap<String, MqttSubscription> getSubscriptions() {
 | 
			
		||||
        return subscriptions;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Set<String> getPendingSubscribeTopics() {
 | 
			
		||||
        return pendingSubscribeTopics;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    HashMultimap<MqttHandler, MqttSubscription> getHandlerToSubscription() {
 | 
			
		||||
        return handlerToSubscription;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Set<String> getServerSubscriptions() {
 | 
			
		||||
        return serverSubscriptions;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ConcurrentMap<Integer, MqttPendingUnsubscription> getPendingServerUnsubscribes() {
 | 
			
		||||
        return pendingServerUnsubscribes;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ConcurrentMap<Integer, MqttPendingPublish> getPendingPublishes() {
 | 
			
		||||
        return pendingPublishes;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ConcurrentMap<Integer, MqttIncomingQos2Publish> getQos2PendingIncomingPublishes() {
 | 
			
		||||
        return qos2PendingIncomingPublishes;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private class MqttChannelInitializer extends ChannelInitializer<SocketChannel> {
 | 
			
		||||
 | 
			
		||||
        private final Promise<MqttConnectResult> connectFuture;
 | 
			
		||||
@ -673,10 +631,7 @@ final class MqttClientImpl implements MqttClient {
 | 
			
		||||
            ch.pipeline().addLast("mqttPingHandler", new MqttPingHandler(MqttClientImpl.this.clientConfig.getTimeoutSeconds()));
 | 
			
		||||
            ch.pipeline().addLast("mqttHandler", new MqttChannelHandler(MqttClientImpl.this, connectFuture));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    MqttHandler getDefaultHandler() {
 | 
			
		||||
        return defaultHandler;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -17,14 +17,18 @@ package org.thingsboard.mqtt;
 | 
			
		||||
 | 
			
		||||
import io.netty.channel.ChannelFuture;
 | 
			
		||||
import io.netty.handler.codec.mqtt.MqttConnectReturnCode;
 | 
			
		||||
import lombok.Getter;
 | 
			
		||||
import lombok.ToString;
 | 
			
		||||
 | 
			
		||||
@ToString
 | 
			
		||||
@SuppressWarnings({"WeakerAccess", "unused"})
 | 
			
		||||
public final class MqttConnectResult {
 | 
			
		||||
 | 
			
		||||
    @Getter
 | 
			
		||||
    private final boolean success;
 | 
			
		||||
    @Getter
 | 
			
		||||
    private final MqttConnectReturnCode returnCode;
 | 
			
		||||
    @Getter
 | 
			
		||||
    private final ChannelFuture closeFuture;
 | 
			
		||||
 | 
			
		||||
    MqttConnectResult(boolean success, MqttConnectReturnCode returnCode, ChannelFuture closeFuture) {
 | 
			
		||||
@ -33,16 +37,4 @@ public final class MqttConnectResult {
 | 
			
		||||
        this.closeFuture = closeFuture;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean isSuccess() {
 | 
			
		||||
        return success;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public MqttConnectReturnCode getReturnCode() {
 | 
			
		||||
        return returnCode;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ChannelFuture getCloseFuture() {
 | 
			
		||||
        return closeFuture;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -15,16 +15,23 @@
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.mqtt;
 | 
			
		||||
 | 
			
		||||
import lombok.AccessLevel;
 | 
			
		||||
import lombok.Getter;
 | 
			
		||||
import lombok.Setter;
 | 
			
		||||
 | 
			
		||||
import java.util.regex.Pattern;
 | 
			
		||||
 | 
			
		||||
final class MqttSubscription {
 | 
			
		||||
 | 
			
		||||
    @Getter(AccessLevel.PACKAGE)
 | 
			
		||||
    private final String topic;
 | 
			
		||||
    private final Pattern topicRegex;
 | 
			
		||||
    @Getter
 | 
			
		||||
    private final MqttHandler handler;
 | 
			
		||||
 | 
			
		||||
    @Getter(AccessLevel.PACKAGE)
 | 
			
		||||
    private final boolean once;
 | 
			
		||||
 | 
			
		||||
    @Getter(AccessLevel.PACKAGE)
 | 
			
		||||
    @Setter(AccessLevel.PACKAGE)
 | 
			
		||||
    private volatile boolean called;
 | 
			
		||||
 | 
			
		||||
    MqttSubscription(String topic, MqttHandler handler, boolean once) {
 | 
			
		||||
@ -40,22 +47,6 @@ final class MqttSubscription {
 | 
			
		||||
        this.topicRegex = Pattern.compile(topic.replace("+", "[^/]+").replace("#", ".+") + "$");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    String getTopic() {
 | 
			
		||||
        return topic;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public MqttHandler getHandler() {
 | 
			
		||||
        return handler;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    boolean isOnce() {
 | 
			
		||||
        return once;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    boolean isCalled() {
 | 
			
		||||
        return called;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    boolean matches(String topic) {
 | 
			
		||||
        return this.topicRegex.matcher(topic).matches();
 | 
			
		||||
    }
 | 
			
		||||
@ -78,7 +69,4 @@ final class MqttSubscription {
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void setCalled(boolean called) {
 | 
			
		||||
        this.called = called;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -45,7 +45,6 @@ import org.thingsboard.server.common.msg.TbMsg;
 | 
			
		||||
import org.thingsboard.server.common.msg.TbMsgMetaData;
 | 
			
		||||
 | 
			
		||||
import javax.net.ssl.SSLException;
 | 
			
		||||
import java.nio.charset.Charset;
 | 
			
		||||
import java.nio.charset.StandardCharsets;
 | 
			
		||||
import java.util.concurrent.TimeUnit;
 | 
			
		||||
import java.util.concurrent.TimeoutException;
 | 
			
		||||
@ -64,12 +63,10 @@ import java.util.concurrent.TimeoutException;
 | 
			
		||||
)
 | 
			
		||||
public class TbMqttNode extends TbAbstractExternalNode {
 | 
			
		||||
 | 
			
		||||
    private static final Charset UTF8 = StandardCharsets.UTF_8;
 | 
			
		||||
 | 
			
		||||
    private static final String ERROR = "error";
 | 
			
		||||
    private static final int MQTT_3_MAX_CLIENT_ID_LENGTH = 23;
 | 
			
		||||
    private static final int MQTT_5_MAX_CLIENT_ID_LENGTH = 256;
 | 
			
		||||
 | 
			
		||||
    protected TbMqttNodeConfiguration mqttNodeConfiguration;
 | 
			
		||||
 | 
			
		||||
    protected MqttClient mqttClient;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
@ -87,9 +84,9 @@ public class TbMqttNode extends TbAbstractExternalNode {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onMsg(TbContext ctx, TbMsg msg) {
 | 
			
		||||
        String topic = TbNodeUtils.processPattern(this.mqttNodeConfiguration.getTopicPattern(), msg);
 | 
			
		||||
        String topic = TbNodeUtils.processPattern(mqttNodeConfiguration.getTopicPattern(), msg);
 | 
			
		||||
        var tbMsg = ackIfNeeded(ctx, msg);
 | 
			
		||||
        this.mqttClient.publish(topic, Unpooled.wrappedBuffer(getData(tbMsg, mqttNodeConfiguration.isParseToPlainText()).getBytes(UTF8)),
 | 
			
		||||
        this.mqttClient.publish(topic, Unpooled.wrappedBuffer(getData(tbMsg, mqttNodeConfiguration.isParseToPlainText()).getBytes(StandardCharsets.UTF_8)),
 | 
			
		||||
                        MqttQoS.AT_LEAST_ONCE, mqttNodeConfiguration.isRetainedMessage())
 | 
			
		||||
                .addListener(future -> {
 | 
			
		||||
                            if (future.isSuccess()) {
 | 
			
		||||
@ -103,7 +100,7 @@ public class TbMqttNode extends TbAbstractExternalNode {
 | 
			
		||||
 | 
			
		||||
    private TbMsg processException(TbMsg origMsg, Throwable e) {
 | 
			
		||||
        TbMsgMetaData metaData = origMsg.getMetaData().copy();
 | 
			
		||||
        metaData.putValue(ERROR, e.getClass() + ": " + e.getMessage());
 | 
			
		||||
        metaData.putValue("error", e.getClass() + ": " + e.getMessage());
 | 
			
		||||
        return origMsg.transform()
 | 
			
		||||
                .metaData(metaData)
 | 
			
		||||
                .build();
 | 
			
		||||
@ -111,8 +108,8 @@ public class TbMqttNode extends TbAbstractExternalNode {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void destroy() {
 | 
			
		||||
        if (this.mqttClient != null) {
 | 
			
		||||
            this.mqttClient.disconnect();
 | 
			
		||||
        if (mqttClient != null) {
 | 
			
		||||
            mqttClient.disconnect();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -123,11 +120,11 @@ public class TbMqttNode extends TbAbstractExternalNode {
 | 
			
		||||
    protected MqttClient initClient(TbContext ctx) throws Exception {
 | 
			
		||||
        MqttClientConfig config = new MqttClientConfig(getSslContext());
 | 
			
		||||
        config.setOwnerId(getOwnerId(ctx));
 | 
			
		||||
        if (!StringUtils.isEmpty(this.mqttNodeConfiguration.getClientId())) {
 | 
			
		||||
        if (!StringUtils.isEmpty(mqttNodeConfiguration.getClientId())) {
 | 
			
		||||
            config.setClientId(getClientId(ctx));
 | 
			
		||||
        }
 | 
			
		||||
        config.setCleanSession(this.mqttNodeConfiguration.isCleanSession());
 | 
			
		||||
        config.setProtocolVersion(this.mqttNodeConfiguration.getProtocolVersion());
 | 
			
		||||
        config.setCleanSession(mqttNodeConfiguration.isCleanSession());
 | 
			
		||||
        config.setProtocolVersion(mqttNodeConfiguration.getProtocolVersion());
 | 
			
		||||
 | 
			
		||||
        MqttClientSettings mqttClientSettings = ctx.getMqttClientSettings();
 | 
			
		||||
        config.setRetransmissionConfig(new MqttClientConfig.RetransmissionConfig(
 | 
			
		||||
@ -139,32 +136,32 @@ public class TbMqttNode extends TbAbstractExternalNode {
 | 
			
		||||
        prepareMqttClientConfig(config);
 | 
			
		||||
        MqttClient client = getMqttClient(ctx, config);
 | 
			
		||||
        client.setEventLoop(ctx.getSharedEventLoop());
 | 
			
		||||
        Promise<MqttConnectResult> connectFuture = client.connect(this.mqttNodeConfiguration.getHost(), this.mqttNodeConfiguration.getPort());
 | 
			
		||||
        Promise<MqttConnectResult> connectFuture = client.connect(mqttNodeConfiguration.getHost(), mqttNodeConfiguration.getPort());
 | 
			
		||||
        MqttConnectResult result;
 | 
			
		||||
        try {
 | 
			
		||||
            result = connectFuture.get(this.mqttNodeConfiguration.getConnectTimeoutSec(), TimeUnit.SECONDS);
 | 
			
		||||
            result = connectFuture.get(mqttNodeConfiguration.getConnectTimeoutSec(), TimeUnit.SECONDS);
 | 
			
		||||
        } catch (TimeoutException ex) {
 | 
			
		||||
            connectFuture.cancel(true);
 | 
			
		||||
            client.disconnect();
 | 
			
		||||
            String hostPort = this.mqttNodeConfiguration.getHost() + ":" + this.mqttNodeConfiguration.getPort();
 | 
			
		||||
            String hostPort = mqttNodeConfiguration.getHost() + ":" + mqttNodeConfiguration.getPort();
 | 
			
		||||
            throw new RuntimeException(String.format("Failed to connect to MQTT broker at %s.", hostPort));
 | 
			
		||||
        }
 | 
			
		||||
        if (!result.isSuccess()) {
 | 
			
		||||
            connectFuture.cancel(true);
 | 
			
		||||
            client.disconnect();
 | 
			
		||||
            String hostPort = this.mqttNodeConfiguration.getHost() + ":" + this.mqttNodeConfiguration.getPort();
 | 
			
		||||
            String hostPort = mqttNodeConfiguration.getHost() + ":" + mqttNodeConfiguration.getPort();
 | 
			
		||||
            throw new RuntimeException(String.format("Failed to connect to MQTT broker at %s. Result code is: %s", hostPort, result.getReturnCode()));
 | 
			
		||||
        }
 | 
			
		||||
        return client;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private String getClientId(TbContext ctx) throws TbNodeException {
 | 
			
		||||
        String clientId = this.mqttNodeConfiguration.isAppendClientIdSuffix() ?
 | 
			
		||||
                this.mqttNodeConfiguration.getClientId() + "_" + ctx.getServiceId() :
 | 
			
		||||
                this.mqttNodeConfiguration.getClientId();
 | 
			
		||||
        if (clientId.length() > 23) {
 | 
			
		||||
            throw new TbNodeException("Client ID is too long '" + clientId + "'. " +
 | 
			
		||||
                    "The length of Client ID cannot be longer than 23, but current length is " + clientId.length() + ".", true);
 | 
			
		||||
        String clientId = mqttNodeConfiguration.isAppendClientIdSuffix() ?
 | 
			
		||||
                mqttNodeConfiguration.getClientId() + "_" + ctx.getServiceId() :
 | 
			
		||||
                mqttNodeConfiguration.getClientId();
 | 
			
		||||
        int maxLength = mqttNodeConfiguration.getProtocolVersion() == MqttVersion.MQTT_3_1 ? MQTT_3_MAX_CLIENT_ID_LENGTH : MQTT_5_MAX_CLIENT_ID_LENGTH;
 | 
			
		||||
        if (clientId.length() > maxLength) {
 | 
			
		||||
            throw new TbNodeException("The length of Client ID cannot be longer than " + maxLength + ", but current length is " + clientId.length() + ".", true);
 | 
			
		||||
        }
 | 
			
		||||
        return clientId;
 | 
			
		||||
    }
 | 
			
		||||
@ -174,7 +171,7 @@ public class TbMqttNode extends TbAbstractExternalNode {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected void prepareMqttClientConfig(MqttClientConfig config) {
 | 
			
		||||
        ClientCredentials credentials = this.mqttNodeConfiguration.getCredentials();
 | 
			
		||||
        ClientCredentials credentials = mqttNodeConfiguration.getCredentials();
 | 
			
		||||
        if (credentials.getType() == CredentialsType.BASIC) {
 | 
			
		||||
            BasicCredentials basicCredentials = (BasicCredentials) credentials;
 | 
			
		||||
            config.setUsername(basicCredentials.getUsername());
 | 
			
		||||
@ -183,7 +180,7 @@ public class TbMqttNode extends TbAbstractExternalNode {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private SslContext getSslContext() throws SSLException {
 | 
			
		||||
        return this.mqttNodeConfiguration.isSsl() ? this.mqttNodeConfiguration.getCredentials().initSslContext() : null;
 | 
			
		||||
        return mqttNodeConfiguration.isSsl() ? mqttNodeConfiguration.getCredentials().initSslContext() : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private String getData(TbMsg tbMsg, boolean parseToPlainText) {
 | 
			
		||||
 | 
			
		||||
@ -212,40 +212,45 @@ public class TbMqttNodeTest extends AbstractRuleNodeUpgradeTest {
 | 
			
		||||
        assertThatNoException().isThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig))));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void givenClientIdIsTooLong_whenInit_thenThrowsException() {
 | 
			
		||||
        String invalidClientId = "vhfrbeb38ygwfwrgfwefgterhytjytj";
 | 
			
		||||
        mqttNodeConfig.setClientId(invalidClientId);
 | 
			
		||||
    @ParameterizedTest
 | 
			
		||||
    @MethodSource("provideInvalidClientIdScenarios")
 | 
			
		||||
    public void givenInvalidClientId_whenInit_thenThrowsException(MqttVersion version, int maxLength, int repeat, String serviceId, boolean appendSuffix) {
 | 
			
		||||
        String baseClientId = "x".repeat(repeat);
 | 
			
		||||
        mqttNodeConfig.setClientId(baseClientId);
 | 
			
		||||
        mqttNodeConfig.setAppendClientIdSuffix(appendSuffix);
 | 
			
		||||
        mqttNodeConfig.setProtocolVersion(version);
 | 
			
		||||
 | 
			
		||||
        given(ctxMock.getTenantId()).willReturn(TENANT_ID);
 | 
			
		||||
        given(ctxMock.getSelf()).willReturn(new RuleNode(RULE_NODE_ID));
 | 
			
		||||
 | 
			
		||||
        String clientId = appendSuffix ? baseClientId + "_" + serviceId : baseClientId;
 | 
			
		||||
        if (appendSuffix) {
 | 
			
		||||
            given(ctxMock.getServiceId()).willReturn(serviceId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        String expectedMessage = "The length of Client ID cannot be longer than " + maxLength + ", but current length is " + clientId.length() + ".";
 | 
			
		||||
 | 
			
		||||
        assertThatThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig))))
 | 
			
		||||
                .isInstanceOf(TbNodeException.class)
 | 
			
		||||
                .hasMessage("Client ID is too long '" + invalidClientId + "'. " +
 | 
			
		||||
                        "The length of Client ID cannot be longer than 23, but current length is " + invalidClientId.length() + ".")
 | 
			
		||||
                .hasMessage(expectedMessage)
 | 
			
		||||
                .extracting(e -> ((TbNodeException) e).isUnrecoverable())
 | 
			
		||||
                .isEqualTo(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void givenClientIdIsOkAndAppendClientIdSuffixIsTrue_whenInit_thenClientIdBecomesInvalidAndThrowsException() {
 | 
			
		||||
        String validClientId = "fertjnhnjj4ge";
 | 
			
		||||
        mqttNodeConfig.setClientId("fertjnhnjj4ge");
 | 
			
		||||
        mqttNodeConfig.setAppendClientIdSuffix(true);
 | 
			
		||||
    private static Stream<Arguments> provideInvalidClientIdScenarios() {
 | 
			
		||||
        return Stream.of(
 | 
			
		||||
                // MQTT_5, too long clientId
 | 
			
		||||
                Arguments.of(MqttVersion.MQTT_5, 256, 257, null, false),
 | 
			
		||||
 | 
			
		||||
        given(ctxMock.getTenantId()).willReturn(TENANT_ID);
 | 
			
		||||
        given(ctxMock.getSelf()).willReturn(new RuleNode(RULE_NODE_ID));
 | 
			
		||||
        String serviceId = "test-service";
 | 
			
		||||
        given(ctxMock.getServiceId()).willReturn(serviceId);
 | 
			
		||||
                // MQTT_5, base + suffix exceeds
 | 
			
		||||
                Arguments.of(MqttVersion.MQTT_5, 256, 250, "test-service", true),
 | 
			
		||||
 | 
			
		||||
        String resultedClientId = validClientId + "_" + serviceId;
 | 
			
		||||
        assertThatThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig))))
 | 
			
		||||
                .isInstanceOf(TbNodeException.class)
 | 
			
		||||
                .hasMessage("Client ID is too long '" + resultedClientId + "'. " +
 | 
			
		||||
                        "The length of Client ID cannot be longer than 23, but current length is " + resultedClientId.length() + ".")
 | 
			
		||||
                .extracting(e -> ((TbNodeException) e).isUnrecoverable())
 | 
			
		||||
                .isEqualTo(true);
 | 
			
		||||
                // MQTT_3_1, too long clientId
 | 
			
		||||
                Arguments.of(MqttVersion.MQTT_3_1, 23, 24, null, false),
 | 
			
		||||
 | 
			
		||||
                // MQTT_3_1, base + suffix exceeds
 | 
			
		||||
                Arguments.of(MqttVersion.MQTT_3_1, 23, 5, "verylongservicename", true)
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user