/* * Copyright 2019-present HiveMQ GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.hivemq.mqtt.handler.publish; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.hivemq.extension.sdk.api.annotations.NotNull; import com.hivemq.extension.sdk.api.annotations.Nullable; import com.hivemq.configuration.service.MqttConfigurationService; import com.hivemq.extension.sdk.api.packets.auth.DefaultAuthorizationBehaviour; import com.hivemq.extension.sdk.api.packets.auth.ModifiableDefaultPermissions; import com.hivemq.extension.sdk.api.packets.publish.AckReasonCode; import com.hivemq.extensions.handler.tasks.PublishAuthorizerResult; import com.hivemq.extensions.packets.general.ModifiableDefaultPermissionsImpl; import com.hivemq.logging.EventLog; import com.hivemq.mqtt.handler.disconnect.Mqtt5ServerDisconnector; import com.hivemq.mqtt.message.ProtocolVersion; import com.hivemq.mqtt.message.mqtt5.Mqtt5UserProperties; import com.hivemq.mqtt.message.puback.PUBACK; import com.hivemq.mqtt.message.publish.PUBLISH; import com.hivemq.mqtt.message.pubrec.PUBREC; import com.hivemq.mqtt.message.reason.Mqtt5DisconnectReasonCode; import com.hivemq.mqtt.message.reason.Mqtt5PubAckReasonCode; import com.hivemq.mqtt.message.reason.Mqtt5PubRecReasonCode; import com.hivemq.mqtt.services.InternalPublishService; import com.hivemq.util.ChannelAttributes; import com.hivemq.util.ChannelUtils; import com.hivemq.util.ReasonStrings; import io.netty.channel.ChannelHandlerContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Singleton; import static com.hivemq.util.ChannelAttributes.MQTT_VERSION; /** * This Service is responsible for PUBLISH message processing after interception and authorisation. * * @author Dominik Obermaier * @author Christoph Schäbel * @author Florian Limpöck */ @Singleton public class IncomingPublishService { private static final Logger log = LoggerFactory.getLogger(IncomingPublishService.class); private final @NotNull InternalPublishService publishService; private final @NotNull EventLog eventLog; private final @NotNull MqttConfigurationService mqttConfigurationService; private final @NotNull Mqtt5ServerDisconnector mqtt5ServerDisconnector; @Inject IncomingPublishService(final @NotNull InternalPublishService publishService, final @NotNull EventLog eventLog, final @NotNull MqttConfigurationService mqttConfigurationService, final @NotNull Mqtt5ServerDisconnector mqtt5ServerDisconnector) { this.publishService = publishService; this.eventLog = eventLog; this.mqttConfigurationService = mqttConfigurationService; this.mqtt5ServerDisconnector = mqtt5ServerDisconnector; } public void processPublish(@NotNull final ChannelHandlerContext ctx, @NotNull final PUBLISH publish, @Nullable final PublishAuthorizerResult authorizerResult) { final ProtocolVersion protocolVersion = ctx.channel().attr(MQTT_VERSION).get(); final int maxQos = mqttConfigurationService.maximumQos().getQosNumber(); final int qos = publish.getQoS().getQosNumber(); if (qos > maxQos) { if (ProtocolVersion.MQTTv5 == ctx.channel().attr(ChannelAttributes.MQTT_VERSION).get()) { // We must send a DISCONNECT with reason protocol error in this case final String clientId = ChannelUtils.getClientId(ctx.channel()); mqtt5ServerDisconnector.disconnect(ctx.channel(), "Client '" + clientId + "' (IP: {}) sent a PUBLISH with QoS exceeding the maximum configured QoS." + " Got QoS " + publish.getQoS() + ", maximum: " + mqttConfigurationService.maximumQos() + ". Disconnecting client.", "Sent PUBLISH with QoS (" + qos + ") higher than the allowed maximum (" + maxQos + ")", Mqtt5DisconnectReasonCode.QOS_NOT_SUPPORTED, String.format(ReasonStrings.CONNACK_QOS_NOT_SUPPORTED_PUBLISH, qos, maxQos) ); } else { if (log.isDebugEnabled()) { final String clientId = ChannelUtils.getClientId(ctx.channel()); log.debug("Client '" + clientId + "'(IP: {}) sent a PUBLISH with QoS exceeding the maximum configured QoS. Got QoS: {}, maximum: {}. Disconnecting client.", ChannelUtils.getChannelIP(ctx.channel()).or("UNKNOWN"), publish.getQoS(), mqttConfigurationService.maximumQos()); } finishBadPublish(ctx, "Sent PUBLISH with QoS (" + qos + ") higher than the allowed maximum (" + maxQos + ")"); } return; } if (ProtocolVersion.MQTTv3_1 == protocolVersion || ProtocolVersion.MQTTv3_1_1 == protocolVersion) { if (!isMessageSizeAllowed(ctx, publish)) { finishBadPublish(ctx, "Sent PUBLISH with a payload that is bigger than the allowed message size"); return; } } authorizePublish(ctx, publish, authorizerResult); } private void authorizePublish(@NotNull final ChannelHandlerContext ctx, @NotNull final PUBLISH publish, @Nullable final PublishAuthorizerResult authorizerResult) { if (authorizerResult != null && authorizerResult.getAckReasonCode() != null) { //decision has been made in PublishAuthorizer if (ctx.channel().attr(ChannelAttributes.INCOMING_PUBLISHES_DEFAULT_FAILED_SKIP_REST).get() != null) { //reason string and reason code null, because client disconnected previously finishUnauthorizedPublish(ctx, publish, null, null); } else if (authorizerResult.getAckReasonCode() == AckReasonCode.SUCCESS) { publishMessage(ctx, publish); } else { finishUnauthorizedPublish(ctx, publish, authorizerResult.getAckReasonCode(), authorizerResult.getReasonString()); } return; } final ModifiableDefaultPermissions permissions = ctx.channel().attr(ChannelAttributes.AUTH_PERMISSIONS).get(); final ModifiableDefaultPermissionsImpl defaultPermissions = (ModifiableDefaultPermissionsImpl) permissions; //if authorizers are present and no permissions are available and the default behaviour has not been changed //then we deny the publish if (authorizerResult != null && authorizerResult.isAuthorizerPresent() && (defaultPermissions == null || (defaultPermissions.asList().size() < 1 && defaultPermissions.getDefaultBehaviour() == DefaultAuthorizationBehaviour.ALLOW && !defaultPermissions.isDefaultAuthorizationBehaviourOverridden()))) { finishUnauthorizedPublish(ctx, publish, null, null); return; } if (DefaultPermissionsEvaluator.checkPublish(permissions, publish)) { publishMessage(ctx, publish); } else { finishUnauthorizedPublish(ctx, publish, null, null); } } private void finishBadPublish(final ChannelHandlerContext ctx, @NotNull final String reason) { if (ctx.channel().isActive()) { eventLog.clientWasDisconnected(ctx.channel(), reason); ctx.close(); } } private void finishUnauthorizedPublish(@NotNull final ChannelHandlerContext ctx, @NotNull final PUBLISH publish, @Nullable final AckReasonCode reasonCode, @Nullable final String reasonString) { ctx.channel().attr(ChannelAttributes.INCOMING_PUBLISHES_DEFAULT_FAILED_SKIP_REST).set(true); if (!ctx.channel().isActive()) { //no more processing needed. return; } final String reason = "Not authorized to publish on topic '" + publish.getTopic() + "' with QoS '" + publish.getQoS().getQosNumber() + "' and retain '" + publish.isRetain() + "'"; //MQTT 3.x.x -> disconnect (without DISCONNECT packet) if (ctx.channel().attr(ChannelAttributes.MQTT_VERSION).get() != ProtocolVersion.MQTTv5) { final String clientId = ChannelUtils.getClientId(ctx.channel()); log.debug("Client '{}' (IP: {}) is not authorized to publish on topic '{}' with QoS '{}' and retain '{}'. Disconnecting client.", clientId, ChannelUtils.getChannelIP(ctx.channel()).or("UNKNOWN"), publish.getTopic(), publish.getQoS().getQosNumber(), publish.isRetain()); finishBadPublish(ctx, reason); return; } //MQTT 5 -> send ACK with error code and then disconnect switch (publish.getQoS()) { case AT_MOST_ONCE: //just drop the message, no back channel to the client break; case AT_LEAST_ONCE: final PUBACK puback = new PUBACK(publish.getPacketIdentifier(), reasonCode != null ? Mqtt5PubAckReasonCode.from(reasonCode) : Mqtt5PubAckReasonCode.NOT_AUTHORIZED, reasonString != null ? reasonString : reason, Mqtt5UserProperties.NO_USER_PROPERTIES); ctx.writeAndFlush(puback); break; case EXACTLY_ONCE: final PUBREC pubrec = new PUBREC(publish.getPacketIdentifier(), reasonCode != null ? Mqtt5PubRecReasonCode.from(reasonCode) : Mqtt5PubRecReasonCode.NOT_AUTHORIZED, reasonString != null ? reasonString : reason, Mqtt5UserProperties.NO_USER_PROPERTIES); ctx.writeAndFlush(pubrec); break; } final String clientId = ChannelUtils.getClientId(ctx.channel()); mqtt5ServerDisconnector.disconnect(ctx.channel(), "Client '" + clientId + "' (IP: {}) is not authorized to publish on topic '" + publish.getTopic() + "' with QoS '" + publish.getQoS().getQosNumber() + "' and retain '" + publish.isRetain() + "'. Disconnecting client.", reason, Mqtt5DisconnectReasonCode.NOT_AUTHORIZED, reason ); } private void publishMessage(final ChannelHandlerContext ctx, @NotNull final PUBLISH publish) { final String clientId = ChannelUtils.getClientId(ctx.channel()); final ListenableFuture<PublishReturnCode> publishFinishedFuture = publishService.publish(publish, ctx.channel().eventLoop(), clientId); Futures.addCallback(publishFinishedFuture, new FutureCallback<>() { @Override public void onSuccess(@Nullable final PublishReturnCode result) { sendAck(ctx, publish, result); } @Override public void onFailure(@NotNull final Throwable t) { sendAck(ctx, publish, PublishReturnCode.FAILED); } }, ctx.channel().eventLoop()); } private void sendAck(@NotNull final ChannelHandlerContext ctx, final PUBLISH publish, @Nullable final PublishReturnCode publishReturnCode) { switch (publish.getQoS()) { case AT_MOST_ONCE: // do nothing break; case AT_LEAST_ONCE: if (publishReturnCode == PublishReturnCode.NO_MATCHING_SUBSCRIBERS) { ctx.writeAndFlush(new PUBACK(publish.getPacketIdentifier(), Mqtt5PubAckReasonCode.NO_MATCHING_SUBSCRIBERS, null, Mqtt5UserProperties.NO_USER_PROPERTIES)); } else { ctx.writeAndFlush(new PUBACK(publish.getPacketIdentifier())); } break; case EXACTLY_ONCE: if (publishReturnCode == PublishReturnCode.NO_MATCHING_SUBSCRIBERS) { ctx.writeAndFlush(new PUBREC(publish.getPacketIdentifier(), Mqtt5PubRecReasonCode.NO_MATCHING_SUBSCRIBERS, null, Mqtt5UserProperties.NO_USER_PROPERTIES)); } else { ctx.writeAndFlush(new PUBREC(publish.getPacketIdentifier())); } break; } } private boolean isMessageSizeAllowed(final ChannelHandlerContext ctx, @NotNull final PUBLISH publish) { final Long maxPublishSize = ctx.channel().attr(ChannelAttributes.MAX_PACKET_SIZE_SEND).get(); if (maxPublishSize != null && publish.getPayload() != null && maxPublishSize < publish.getPayload().length) { if (log.isDebugEnabled()) { final String clientId = ChannelUtils.getClientId(ctx.channel()); log.debug("Client '" + clientId + "' (IP: {}) published a message with {} bytes payload its max allowed size is {} bytes. Disconnecting client.", ChannelUtils.getChannelIP(ctx.channel()).or("UNKNOWN"), publish.getPayload().length, maxPublishSize); } return false; } return true; } }