com.hivemq.extensions.handler.IncomingPublishHandler Maven / Gradle / Ivy
Show all versions of hivemq-community-edition-embedded Show documentation
/*
* 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.extensions.handler;
import com.hivemq.bootstrap.ClientConnection;
import com.hivemq.configuration.service.FullConfigurationService;
import com.hivemq.extension.sdk.api.annotations.NotNull;
import com.hivemq.extension.sdk.api.async.TimeoutFallback;
import com.hivemq.extension.sdk.api.client.parameter.ClientInformation;
import com.hivemq.extension.sdk.api.client.parameter.ConnectionInformation;
import com.hivemq.extension.sdk.api.interceptor.publish.PublishInboundInterceptor;
import com.hivemq.extension.sdk.api.packets.publish.AckReasonCode;
import com.hivemq.extensions.ExtensionInformationUtil;
import com.hivemq.extensions.HiveMQExtension;
import com.hivemq.extensions.HiveMQExtensions;
import com.hivemq.extensions.client.ClientContextImpl;
import com.hivemq.extensions.executor.PluginOutPutAsyncer;
import com.hivemq.extensions.executor.PluginTaskExecutorService;
import com.hivemq.extensions.executor.task.PluginInOutTask;
import com.hivemq.extensions.executor.task.PluginInOutTaskContext;
import com.hivemq.extensions.interceptor.publish.parameter.PublishInboundInputImpl;
import com.hivemq.extensions.interceptor.publish.parameter.PublishInboundOutputImpl;
import com.hivemq.extensions.packets.publish.ModifiablePublishPacketImpl;
import com.hivemq.extensions.packets.publish.PublishPacketImpl;
import com.hivemq.mqtt.handler.disconnect.MqttServerDisconnector;
import com.hivemq.mqtt.message.ProtocolVersion;
import com.hivemq.mqtt.message.dropping.MessageDroppedService;
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.publish.PUBLISHFactory;
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.util.Exceptions;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* This handler intercepts every inbound PUBLISH message and delegates it to all registered {@link
* PublishInboundInterceptor}s for a specific client.
*
* When delivery of the PUBLISH is prevented, the message will be dropped.
*
* When the {@link AckReasonCode} for the PUBACK or PUBREC is not SUCCESS a MQTT 3 client will be disconnected, but an
* MQTT 5 client receives the PUBACK or PUBREC with the reason code.
*
* When the {@link AckReasonCode} is SUCCESS an Acknowledgement will be sent (PUBACK or PUBREC) dependent on the QoS.
*
* Same happens for TimeoutFallback.FAILURE if the output is marked as async and timed out.
*
* Multiple interceptors are called sequentially, beginning with the one with the highest priority.
*
* If any interceptor prevents delivery of a publish, no more interceptor will be called.
*
* @author Florian Limpöck
* @author Silvio Giebl
* @since 4.0.0
*/
@Singleton
public class IncomingPublishHandler {
private static final Logger log = LoggerFactory.getLogger(IncomingPublishHandler.class);
private final @NotNull PluginTaskExecutorService executorService;
private final @NotNull PluginOutPutAsyncer asyncer;
private final @NotNull HiveMQExtensions hiveMQExtensions;
private final @NotNull MessageDroppedService messageDroppedService;
private final @NotNull PluginAuthorizerService authorizerService;
private final @NotNull MqttServerDisconnector mqttDisconnector;
private final @NotNull FullConfigurationService configurationService;
@Inject
public IncomingPublishHandler(
final @NotNull PluginTaskExecutorService executorService,
final @NotNull PluginOutPutAsyncer asyncer,
final @NotNull HiveMQExtensions hiveMQExtensions,
final @NotNull MessageDroppedService messageDroppedService,
final @NotNull PluginAuthorizerService authorizerService,
final @NotNull MqttServerDisconnector mqttDisconnector,
final @NotNull FullConfigurationService configurationService) {
this.executorService = executorService;
this.asyncer = asyncer;
this.hiveMQExtensions = hiveMQExtensions;
this.messageDroppedService = messageDroppedService;
this.authorizerService = authorizerService;
this.mqttDisconnector = mqttDisconnector;
this.configurationService = configurationService;
}
/**
* intercepts the publish message when the channel is active, the client id is set and interceptors are available,
* otherwise delegates to authorizer
*
* @param ctx the context of the channel handler
* @param publish the publish to process
*/
public void interceptOrDelegate(
final @NotNull ChannelHandlerContext ctx, final @NotNull PUBLISH publish, final @NotNull String clientId) {
final Channel channel = ctx.channel();
final ClientContextImpl clientContext = ClientConnection.of(channel).getExtensionClientContext();
if (clientContext == null) {
ctx.executor().execute(() -> authorizerService.authorizePublish(ctx, publish));
return;
}
final List interceptors = clientContext.getPublishInboundInterceptors();
if (interceptors.isEmpty()) {
ctx.executor().execute(() -> authorizerService.authorizePublish(ctx, publish));
return;
}
final ClientInformation clientInfo = ExtensionInformationUtil.getAndSetClientInformation(channel, clientId);
final ConnectionInformation connectionInfo = ExtensionInformationUtil.getAndSetConnectionInformation(channel);
final PublishPacketImpl packet = new PublishPacketImpl(publish);
final PublishInboundInputImpl input = new PublishInboundInputImpl(clientInfo, connectionInfo, packet);
final ExtensionParameterHolder inputHolder = new ExtensionParameterHolder<>(input);
final ModifiablePublishPacketImpl modifiablePacket =
new ModifiablePublishPacketImpl(packet, configurationService);
final PublishInboundOutputImpl output = new PublishInboundOutputImpl(asyncer, modifiablePacket);
final ExtensionParameterHolder outputHolder = new ExtensionParameterHolder<>(output);
final PublishInboundInterceptorContext context = new PublishInboundInterceptorContext(clientId,
interceptors.size(),
ctx,
publish,
inputHolder,
outputHolder);
for (final PublishInboundInterceptor interceptor : interceptors) {
final HiveMQExtension extension =
hiveMQExtensions.getExtensionForClassloader(interceptor.getClass().getClassLoader());
if (extension == null) { // disabled extension would be null
context.finishInterceptor();
continue;
}
final PublishInboundInterceptorTask task =
new PublishInboundInterceptorTask(interceptor, extension.getId());
executorService.handlePluginInOutTaskExecution(context, inputHolder, outputHolder, task);
}
}
private class PublishInboundInterceptorContext extends PluginInOutTaskContext
implements Runnable {
private final int interceptorCount;
private final @NotNull AtomicInteger counter;
private final @NotNull ChannelHandlerContext ctx;
private final @NotNull PUBLISH publish;
private final @NotNull ExtensionParameterHolder inputHolder;
private final @NotNull ExtensionParameterHolder outputHolder;
PublishInboundInterceptorContext(
final @NotNull String clientId,
final int interceptorCount,
final @NotNull ChannelHandlerContext ctx,
final @NotNull PUBLISH publish,
final @NotNull ExtensionParameterHolder inputHolder,
final @NotNull ExtensionParameterHolder outputHolder) {
super(clientId);
this.interceptorCount = interceptorCount;
this.counter = new AtomicInteger(0);
this.ctx = ctx;
this.publish = publish;
this.inputHolder = inputHolder;
this.outputHolder = outputHolder;
}
@Override
public void pluginPost(final @NotNull PublishInboundOutputImpl output) {
if (output.isPreventDelivery()) {
finishInterceptor();
} else if (output.isTimedOut() && (output.getTimeoutFallback() == TimeoutFallback.FAILURE)) {
output.forciblyPreventPublishDelivery(output.getReasonCode(), output.getReasonString());
finishInterceptor();
} else {
if (output.getPublishPacket().isModified()) {
inputHolder.set(inputHolder.get().update(output));
}
if (!finishInterceptor()) {
outputHolder.set(output.update(inputHolder.get()));
}
}
}
public boolean finishInterceptor() {
if (counter.incrementAndGet() == interceptorCount) {
ctx.executor().execute(this);
return true;
}
return false;
}
@Override
public void run() {
final PublishInboundOutputImpl output = outputHolder.get();
if (output.isPreventDelivery()) {
dropMessage(output);
} else {
final PUBLISH finalPublish = PUBLISHFactory.merge(inputHolder.get().getPublishPacket(), publish);
authorizerService.authorizePublish(ctx, finalPublish);
}
}
private void dropMessage(final @NotNull PublishInboundOutputImpl output) {
final Channel channel = ctx.channel();
final String clientId = getIdentifier();
final ProtocolVersion protocolVersion = ClientConnection.of(channel).getProtocolVersion();
//MQTT 3
if (protocolVersion != ProtocolVersion.MQTTv5) {
if (output.getReasonCode() != AckReasonCode.SUCCESS) {
mqttDisconnector.disconnect(channel,
"Client '" +
clientId +
"' (IP: {}) sent a PUBLISH, but its onward delivery was prevented by a publish inbound interceptor. Disconnecting client.",
"Sent PUBLISH, but its onward delivery was prevented by a publish inbound interceptor",
Mqtt5DisconnectReasonCode.ADMINISTRATIVE_ACTION,
null);
} else {
switch (publish.getQoS()) {
case AT_MOST_ONCE:
//no ack for qos 0
break;
case AT_LEAST_ONCE:
ctx.writeAndFlush(new PUBACK(publish.getPacketIdentifier()));
break;
case EXACTLY_ONCE:
ctx.writeAndFlush(new PUBREC(publish.getPacketIdentifier()));
break;
}
}
//MQTT 5
} else {
switch (publish.getQoS()) {
case AT_MOST_ONCE:
//no ack for qos 0
break;
case AT_LEAST_ONCE:
final Mqtt5PubAckReasonCode ackReasonCode = Mqtt5PubAckReasonCode.from(output.getReasonCode());
ctx.writeAndFlush(new PUBACK(publish.getPacketIdentifier(),
ackReasonCode,
output.getReasonString(),
Mqtt5UserProperties.NO_USER_PROPERTIES));
break;
case EXACTLY_ONCE:
final Mqtt5PubRecReasonCode recReasonCode = Mqtt5PubRecReasonCode.from(output.getReasonCode());
ctx.writeAndFlush(new PUBREC(publish.getPacketIdentifier(),
recReasonCode,
output.getReasonString(),
Mqtt5UserProperties.NO_USER_PROPERTIES));
break;
}
}
messageDroppedService.extensionPrevented(clientId, publish.getTopic(), publish.getQoS().getQosNumber());
}
}
private static class PublishInboundInterceptorTask
implements PluginInOutTask {
private final @NotNull PublishInboundInterceptor interceptor;
private final @NotNull String extensionId;
private PublishInboundInterceptorTask(
final @NotNull PublishInboundInterceptor interceptor, final @NotNull String extensionId) {
this.interceptor = interceptor;
this.extensionId = extensionId;
}
@Override
public @NotNull PublishInboundOutputImpl apply(
final @NotNull PublishInboundInputImpl input, final @NotNull PublishInboundOutputImpl output) {
if (output.isPreventDelivery()) {
// it's already prevented so no further interceptors must be called.
return output;
}
try {
interceptor.onInboundPublish(input, output);
} catch (final Throwable e) {
log.warn("Uncaught exception was thrown from extension with id \"{}\" on inbound PUBLISH interception. " +
"Extensions are responsible for their own exception handling.", extensionId, e);
output.forciblyPreventPublishDelivery(output.getReasonCode(), output.getReasonString());
Exceptions.rethrowError(e);
}
return output;
}
@Override
public @NotNull ClassLoader getPluginClassLoader() {
return interceptor.getClass().getClassLoader();
}
}
}