com.hivemq.extensions.handler.ClientLifecycleEventHandler 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.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.hivemq.bootstrap.ClientConnectionContext;
import com.hivemq.extension.sdk.api.annotations.NotNull;
import com.hivemq.extension.sdk.api.annotations.Nullable;
import com.hivemq.extension.sdk.api.events.client.ClientLifecycleEventListener;
import com.hivemq.extension.sdk.api.events.client.ClientLifecycleEventListenerProvider;
import com.hivemq.extension.sdk.api.events.client.parameters.ClientLifecycleEventListenerProviderInput;
import com.hivemq.extensions.HiveMQExtensions;
import com.hivemq.extensions.events.LifecycleEventListeners;
import com.hivemq.extensions.events.OnAuthFailedEvent;
import com.hivemq.extensions.events.OnAuthSuccessEvent;
import com.hivemq.extensions.events.OnClientDisconnectEvent;
import com.hivemq.extensions.events.OnServerDisconnectEvent;
import com.hivemq.extensions.events.client.parameters.AuthenticationFailedInputImpl;
import com.hivemq.extensions.events.client.parameters.AuthenticationSuccessfulInputImpl;
import com.hivemq.extensions.events.client.parameters.ClientEventListeners;
import com.hivemq.extensions.events.client.parameters.ClientInitiatedDisconnectInputImpl;
import com.hivemq.extensions.events.client.parameters.ClientLifecycleEventListenerProviderInputImpl;
import com.hivemq.extensions.events.client.parameters.ConnectionStartInputImpl;
import com.hivemq.extensions.events.client.parameters.ServerInitiatedDisconnectInputImpl;
import com.hivemq.extensions.executor.PluginTaskExecutorService;
import com.hivemq.extensions.executor.task.PluginInTask;
import com.hivemq.extensions.executor.task.PluginInTaskContext;
import com.hivemq.extensions.executor.task.PluginTaskInput;
import com.hivemq.mqtt.message.connect.CONNECT;
import com.hivemq.util.Exceptions;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.util.Map;
/**
* This handler fires all client lifecycle events available.
*
*
* - OnMqttConnect
* - OnAuthSuccess
* - OnAuthFailed
* - OnClientDisconnect
* - OnServerDisconnect
*
*
* onConnect happens by channel read after decoding the connect message.
*
* onAuthSuccess happens on every kind of successful connect.
*
* onAuthFailed happens if any authenticator responds with failed authentication.
*
* onClientDisconnect happens if a client disconnects gracefully (sends DISCONNECT) or ungracefully (channel closed).
*
* onServerDisconnect happens if the server sends a DISCONNECT to a client or closes the channel forcefully.
*
* It also removes the {@link ClientLifecycleEventListener}s at extension stop.
*
* @author Florian Limpöck
* @since 4.0.0
*/
public class ClientLifecycleEventHandler extends SimpleChannelInboundHandler {
private static final Logger log = LoggerFactory.getLogger(ClientLifecycleEventHandler.class);
private final @NotNull LifecycleEventListeners lifecycleEventListeners;
private final @NotNull PluginTaskExecutorService pluginTaskExecutorService;
private final @NotNull HiveMQExtensions hiveMQExtensions;
@VisibleForTesting
@Nullable ClientLifecycleEventListenerProviderInput providerInput;
@Inject
public ClientLifecycleEventHandler(
final @NotNull LifecycleEventListeners lifecycleEventListeners,
final @NotNull PluginTaskExecutorService pluginTaskExecutorService,
final @NotNull HiveMQExtensions hiveMQExtensions) {
this.lifecycleEventListeners = lifecycleEventListeners;
this.pluginTaskExecutorService = pluginTaskExecutorService;
this.hiveMQExtensions = hiveMQExtensions;
}
@Override
protected void channelRead0(final @NotNull ChannelHandlerContext ctx, final @NotNull CONNECT connect) {
//ConnectPacketModifier must happen before here
try {
fireOnMqttConnect(ctx, connect);
} catch (final Exception e) {
log.debug("Firing OnMqttConnectEvent failed: ", e);
}
ctx.fireChannelRead(connect);
}
@Override
public void userEventTriggered(@NotNull final ChannelHandlerContext ctx, @NotNull final Object evt)
throws Exception {
Preconditions.checkNotNull(evt, "A user event must never be null");
if (evt instanceof OnAuthSuccessEvent) {
try {
fireOnAuthSuccess(ctx);
} catch (final Exception e) {
log.debug("Firing OnAuthSuccessEvent failed: ", e);
}
} else if (evt instanceof OnAuthFailedEvent) {
try {
fireOnAuthFailed(ctx, (OnAuthFailedEvent) evt);
} catch (final Exception e) {
log.debug("Firing OnAuthFailedEvent failed: ", e);
}
} else if (evt instanceof OnClientDisconnectEvent) {
try {
fireOnClientDisconnect(ctx, (OnClientDisconnectEvent) evt);
} catch (final Exception e) {
log.debug("Firing OnClientDisconnectEvent failed: ", e);
}
} else if (evt instanceof OnServerDisconnectEvent) {
try {
fireOnServerDisconnect(ctx, (OnServerDisconnectEvent) evt);
} catch (final Exception e) {
log.debug("Firing OnServerDisconnectEvent failed: ", e);
}
} else {
super.userEventTriggered(ctx, evt);
}
}
private void fireOnServerDisconnect(
final @NotNull ChannelHandlerContext ctx, final @NotNull OnServerDisconnectEvent disconnectEvent) {
final String clientId = ClientConnectionContext.of(ctx.channel()).getClientId();
if (clientId == null) {
//should never happen
return;
}
final Map pluginEventListenerProviderMap =
lifecycleEventListeners.getClientLifecycleEventListenerProviderMap();
//No event listener provider set through any extension
if (pluginEventListenerProviderMap.isEmpty()) {
return;
}
final ClientEventListeners eventListeners = getClientEventListeners(ctx);
if (providerInput == null) {
providerInput = new ClientLifecycleEventListenerProviderInputImpl(clientId, ctx.channel());
}
final PluginInTaskContext taskContext = new ProviderInTaskContext(clientId);
final ServerInitiatedDisconnectInputImpl disconnectInput = new ServerInitiatedDisconnectInputImpl(clientId,
ctx.channel(),
disconnectEvent.getReasonCode(),
disconnectEvent.getReasonString(),
disconnectEvent.getUserProperties());
for (final Map.Entry eventListenerEntry : pluginEventListenerProviderMap.entrySet()) {
final EventTask authFailedTask =
new EventTask<>(eventListenerEntry.getValue(),
providerInput,
eventListenerEntry.getKey(),
eventListeners);
pluginTaskExecutorService.handlePluginInTaskExecution(taskContext, disconnectInput, authFailedTask);
}
}
private void fireOnClientDisconnect(
final @NotNull ChannelHandlerContext ctx, final @NotNull OnClientDisconnectEvent disconnectEvent) {
final String clientId = ClientConnectionContext.of(ctx.channel()).getClientId();
if (clientId == null) {
//should never happen
return;
}
final Map pluginEventListenerProviderMap =
lifecycleEventListeners.getClientLifecycleEventListenerProviderMap();
//No event listener provider set through any extension
if (pluginEventListenerProviderMap.isEmpty()) {
return;
}
final ClientEventListeners eventListeners = getClientEventListeners(ctx);
if (providerInput == null) {
providerInput = new ClientLifecycleEventListenerProviderInputImpl(clientId, ctx.channel());
}
final PluginInTaskContext taskContext = new ProviderInTaskContext(clientId);
final ClientInitiatedDisconnectInputImpl disconnectInput = new ClientInitiatedDisconnectInputImpl(clientId,
ctx.channel(),
disconnectEvent.getReasonCode(),
disconnectEvent.getReasonString(),
disconnectEvent.getUserProperties(),
disconnectEvent.isGraceful());
for (final Map.Entry eventListenerEntry : pluginEventListenerProviderMap.entrySet()) {
final EventTask authFailedTask =
new EventTask<>(eventListenerEntry.getValue(),
providerInput,
eventListenerEntry.getKey(),
eventListeners);
pluginTaskExecutorService.handlePluginInTaskExecution(taskContext, disconnectInput, authFailedTask);
}
}
private void fireOnAuthFailed(
final @NotNull ChannelHandlerContext ctx, final @NotNull OnAuthFailedEvent authFailedEvent) {
final String clientId = ClientConnectionContext.of(ctx.channel()).getClientId();
if (clientId == null) {
//should never happen
return;
}
final Map pluginEventListenerProviderMap =
lifecycleEventListeners.getClientLifecycleEventListenerProviderMap();
//No event listener provider set through any extension
if (pluginEventListenerProviderMap.isEmpty()) {
return;
}
final ClientEventListeners eventListeners = getClientEventListeners(ctx);
if (providerInput == null) {
providerInput = new ClientLifecycleEventListenerProviderInputImpl(clientId, ctx.channel());
}
final PluginInTaskContext taskContext = new ProviderInTaskContext(clientId);
final AuthenticationFailedInputImpl failedInput = new AuthenticationFailedInputImpl(ctx.channel(),
clientId,
authFailedEvent.getReasonCode(),
authFailedEvent.getReasonString(),
authFailedEvent.getUserProperties());
for (final Map.Entry eventListenerEntry : pluginEventListenerProviderMap.entrySet()) {
final EventTask authFailedTask =
new EventTask<>(eventListenerEntry.getValue(),
providerInput,
eventListenerEntry.getKey(),
eventListeners);
pluginTaskExecutorService.handlePluginInTaskExecution(taskContext, failedInput, authFailedTask);
}
}
private void fireOnAuthSuccess(final @NotNull ChannelHandlerContext ctx) {
final String clientId = ClientConnectionContext.of(ctx.channel()).getClientId();
if (clientId == null) {
//should never happen
return;
}
final Map pluginEventListenerProviderMap =
lifecycleEventListeners.getClientLifecycleEventListenerProviderMap();
//No event listener provider set through any extension
if (pluginEventListenerProviderMap.isEmpty()) {
return;
}
final ClientEventListeners eventListeners = getClientEventListeners(ctx);
if (providerInput == null) {
providerInput = new ClientLifecycleEventListenerProviderInputImpl(clientId, ctx.channel());
}
final PluginInTaskContext taskContext = new ProviderInTaskContext(clientId);
final AuthenticationSuccessfulInputImpl input = new AuthenticationSuccessfulInputImpl(clientId, ctx.channel());
for (final Map.Entry eventListenerEntry : pluginEventListenerProviderMap.entrySet()) {
final EventTask authSuccessTask =
new EventTask<>(eventListenerEntry.getValue(),
providerInput,
eventListenerEntry.getKey(),
eventListeners);
pluginTaskExecutorService.handlePluginInTaskExecution(taskContext, input, authSuccessTask);
}
}
private void fireOnMqttConnect(@NotNull final ChannelHandlerContext ctx, @NotNull final CONNECT connect) {
final Map pluginEventListenerProviderMap =
lifecycleEventListeners.getClientLifecycleEventListenerProviderMap();
//No event listener provider set through any extension
if (pluginEventListenerProviderMap.isEmpty()) {
return;
}
final ClientEventListeners eventListeners = getClientEventListeners(ctx);
if (providerInput == null) {
providerInput =
new ClientLifecycleEventListenerProviderInputImpl(connect.getClientIdentifier(), ctx.channel());
}
final PluginInTaskContext taskContext = new ProviderInTaskContext(connect.getClientIdentifier());
final ConnectionStartInputImpl connectionStartInput = new ConnectionStartInputImpl(connect, ctx.channel());
for (final Map.Entry eventListenerEntry : pluginEventListenerProviderMap.entrySet()) {
final EventTask connectEventTask = new EventTask<>(eventListenerEntry.getValue(),
providerInput,
eventListenerEntry.getKey(),
eventListeners);
pluginTaskExecutorService.handlePluginInTaskExecution(taskContext, connectionStartInput, connectEventTask);
}
}
@NotNull
private ClientEventListeners getClientEventListeners(final @NotNull ChannelHandlerContext ctx) {
final ClientConnectionContext clientConnectionContext = ClientConnectionContext.of(ctx.channel());
if (clientConnectionContext.getExtensionClientEventListeners() == null) {
clientConnectionContext.setExtensionClientEventListeners(new ClientEventListeners(hiveMQExtensions));
}
return clientConnectionContext.getExtensionClientEventListeners();
}
private static class ProviderInTaskContext extends PluginInTaskContext {
ProviderInTaskContext(final @NotNull String identifier) {
super(identifier);
}
}
private static class EventTask implements PluginInTask {
private final @NotNull ClientLifecycleEventListenerProvider eventListenerProvider;
private final @NotNull ClientLifecycleEventListenerProviderInput eventListenerProviderInput;
private final @NotNull String pluginId;
private final @NotNull ClientEventListeners eventListeners;
EventTask(
final @NotNull ClientLifecycleEventListenerProvider eventListenerProvider,
final @NotNull ClientLifecycleEventListenerProviderInput eventListenerProviderInput,
final @NotNull String pluginId,
final @NotNull ClientEventListeners eventListeners) {
this.eventListenerProvider = eventListenerProvider;
this.eventListenerProviderInput = eventListenerProviderInput;
this.pluginId = pluginId;
this.eventListeners = eventListeners;
}
@Override
public void accept(final @NotNull T pluginTaskInput) {
final ClientLifecycleEventListener eventListener = updateAndGetEventListener();
if (eventListener == null) {
return;
}
try {
if (pluginTaskInput instanceof ConnectionStartInputImpl) {
eventListener.onMqttConnectionStart((ConnectionStartInputImpl) pluginTaskInput);
} else if (pluginTaskInput instanceof AuthenticationSuccessfulInputImpl) {
eventListener.onAuthenticationSuccessful((AuthenticationSuccessfulInputImpl) pluginTaskInput);
} else if (pluginTaskInput instanceof AuthenticationFailedInputImpl) {
eventListener.onAuthenticationFailedDisconnect((AuthenticationFailedInputImpl) pluginTaskInput);
} else if (pluginTaskInput instanceof ClientInitiatedDisconnectInputImpl) {
final ClientInitiatedDisconnectInputImpl taskInput =
(ClientInitiatedDisconnectInputImpl) pluginTaskInput;
if (taskInput.isGraceful()) {
eventListener.onClientInitiatedDisconnect(taskInput);
} else {
eventListener.onConnectionLost(taskInput);
}
} else if (pluginTaskInput instanceof ServerInitiatedDisconnectInputImpl) {
eventListener.onServerInitiatedDisconnect((ServerInitiatedDisconnectInputImpl) pluginTaskInput);
}
} catch (final Throwable e) {
log.warn(
"Uncaught exception was thrown from extension with id \"{}\" on a client lifecycle event. Extensions are responsible on their own to handle exceptions.",
pluginId,
e);
Exceptions.rethrowError(e);
}
}
@Override
public @NotNull ClassLoader getPluginClassLoader() {
return eventListenerProvider.getClass().getClassLoader();
}
private @Nullable ClientLifecycleEventListener updateAndGetEventListener() {
boolean contains = false;
ClientLifecycleEventListener eventListener = null;
for (final Map.Entry pluginEventListenerEntry : eventListeners.getPluginEventListenersMap()
.entrySet()) {
final String id = pluginEventListenerEntry.getKey();
final ClientLifecycleEventListener listener = pluginEventListenerEntry.getValue();
if (listener.getClass().getClassLoader().equals(eventListenerProvider.getClass().getClassLoader()) &&
id.equals(pluginId)) {
contains = true;
eventListener = listener;
}
}
if (!contains) {
try {
eventListener = eventListenerProvider.getClientLifecycleEventListener(eventListenerProviderInput);
if (eventListener != null) {
eventListeners.put(pluginId, eventListener);
}
} catch (final Throwable t) {
log.warn(
"Uncaught exception was thrown from extension with id \"{}\" in client lifecycle event listener provider. " +
"Extensions are responsible on their own to handle exceptions.",
pluginId,
t);
Exceptions.rethrowError(t);
}
}
return eventListener;
}
}
}