/* * Copyright 2014-2016 CyberVision, Inc. * * 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 org.kaaproject.kaa.server.transports.tcp.transport; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import org.kaaproject.kaa.common.channels.protocols.kaatcp.messages.ConnAck; import org.kaaproject.kaa.common.channels.protocols.kaatcp.messages.ConnAck.ReturnCode; import org.kaaproject.kaa.common.channels.protocols.kaatcp.messages.Connect; import org.kaaproject.kaa.common.channels.protocols.kaatcp.messages.Disconnect; import org.kaaproject.kaa.common.channels.protocols.kaatcp.messages.Disconnect.DisconnectReason; import org.kaaproject.kaa.common.channels.protocols.kaatcp.messages.KaaSync; import org.kaaproject.kaa.common.channels.protocols.kaatcp.messages.KaaSyncMessageType; import org.kaaproject.kaa.common.channels.protocols.kaatcp.messages.MessageType; import org.kaaproject.kaa.common.channels.protocols.kaatcp.messages.MqttFrame; import org.kaaproject.kaa.common.channels.protocols.kaatcp.messages.SyncRequest; import org.kaaproject.kaa.server.common.server.NettyChannelContext; import org.kaaproject.kaa.server.transport.EndpointVerificationException; import org.kaaproject.kaa.server.transport.InvalidSdkTokenException; import org.kaaproject.kaa.server.transport.channel.ChannelType; import org.kaaproject.kaa.server.transport.message.ErrorBuilder; import org.kaaproject.kaa.server.transport.message.MessageBuilder; import org.kaaproject.kaa.server.transport.message.MessageHandler; import org.kaaproject.kaa.server.transport.session.SessionCreateListener; import org.kaaproject.kaa.server.transport.session.SessionInfo; import org.kaaproject.kaa.server.transports.tcp.transport.messages.NettyTcpConnectMessage; import org.kaaproject.kaa.server.transports.tcp.transport.messages.NettyTcpDisconnectMessage; import org.kaaproject.kaa.server.transports.tcp.transport.messages.NettyTcpPingMessage; import org.kaaproject.kaa.server.transports.tcp.transport.messages.NettyTcpSyncMessage; import org.kaaproject.kaa.server.transports.tcp.transport.netty.AbstractKaaTcpCommandProcessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.security.GeneralSecurityException; import java.util.UUID; public class TcpHandler extends SimpleChannelInboundHandler<AbstractKaaTcpCommandProcessor> implements SessionCreateListener { private static final Logger LOG = LoggerFactory.getLogger(TcpHandler.class); private static final boolean NOT_ZIPPED = false; private static final ErrorBuilder connectErrorConverter = new ErrorBuilder() { //NOSONAR @Override public Object[] build(Exception exception) { Object[] responses = new Object[1]; if (exception instanceof GeneralSecurityException || exception instanceof IOException || exception instanceof IllegalArgumentException || exception instanceof InvalidSdkTokenException) { responses[0] = new ConnAck(ReturnCode.REFUSE_BAD_CREDENTIALS); } else if (exception instanceof EndpointVerificationException) { responses[0] = new ConnAck(ReturnCode.REFUSE_VERIFICATION_FAILED); } else { responses[0] = new ConnAck(ReturnCode.REFUSE_SERVER_UNAVAILABLE); } return responses; } }; private static final MessageBuilder syncResponseConverter = new MessageBuilder() { //NOSONAR @Override public Object[] build(byte[] encriptedResponseData, byte[] encriptedResponseSignature, boolean isEncrypted) { Object[] responses = new Object[1]; responses[0] = new org.kaaproject.kaa.common.channels.protocols.kaatcp.messages .SyncResponse(encriptedResponseData, NOT_ZIPPED, isEncrypted); LOG.debug("Sending {} response objects", responses.length); return responses; } @Override public Object[] build(byte[] messageData, boolean isEncrypted) { return build(messageData, null, isEncrypted); } }; private static final ErrorBuilder syncErrorConverter = new ErrorBuilder() { //NOSONAR @Override public Object[] build(Exception exception) { Object[] responses = new Object[1]; if (exception instanceof GeneralSecurityException || exception instanceof IOException || exception instanceof IllegalArgumentException || exception instanceof InvalidSdkTokenException) { responses[0] = new Disconnect(DisconnectReason.BAD_REQUEST); } else if (exception instanceof EndpointVerificationException) { responses[0] = new Disconnect(DisconnectReason.CREDENTIALS_REVOKED); } else { responses[0] = new Disconnect(DisconnectReason.INTERNAL_ERROR); } return responses; } }; private final UUID uuid; private final MessageHandler handler; private volatile SessionInfo session; private volatile boolean sessionDisconnected; private MessageBuilder connectResponseConverter; /** * Create new instance of <code>TcpHandler</code>. * * @param uuid immutable universally unique identifier * @param akkaService handler of messages with the transport layer connection info */ public TcpHandler(UUID uuid, MessageHandler akkaService) { this.uuid = uuid; this.handler = akkaService; this.connectResponseConverter = new MessageBuilder() { volatile boolean connAckSent = false; @Override public Object[] build(byte[] encriptedResponseData, byte[] encriptedResponseSignature, boolean isEncrypted) { if (!connAckSent) { synchronized (this) { if (!connAckSent) { connAckSent = true; Object[] responses = new Object[2]; responses[0] = new ConnAck(ReturnCode.ACCEPTED); responses[1] = new org.kaaproject.kaa.common.channels.protocols.kaatcp.messages .SyncResponse(encriptedResponseData, NOT_ZIPPED, isEncrypted); LOG.debug("Sending {} response objects", responses.length); return responses; } } } return syncResponseConverter.build(encriptedResponseData, encriptedResponseSignature, isEncrypted); } @Override public Object[] build(byte[] messageData, boolean isEncrypted) { return build(messageData, null, isEncrypted); } }; } @Override protected void channelRead0(ChannelHandlerContext ctx, AbstractKaaTcpCommandProcessor msg) throws Exception { MqttFrame frame = msg.getRequest(); LOG.trace("[{}] Processing {}", uuid, frame); if (frame.getMessageType() == MessageType.CONNECT) { ChannelFuture closeFuture = ctx.channel().closeFuture(); closeFuture.addListener(new GenericFutureListener<Future<? super Void>>() { @Override public void operationComplete(Future<? super Void> future) throws Exception { if (!sessionDisconnected) { if (session != null) { handler.process(new NettyTcpDisconnectMessage(session)); LOG.trace("[{}] Channel is closed - sending disconnect", uuid); } else { LOG.trace("[{}] Session is not yet established. Skip sending disconnect", uuid); } sessionDisconnected = true; } else { LOG.trace("[{}] Channel is closed and disconnect is already sent", uuid); } } }); if (session == null) { handler.process( new NettyTcpConnectMessage(uuid, new NettyChannelContext(ctx), (Connect) frame, ChannelType.ASYNC, this, connectResponseConverter, connectErrorConverter) ); } else { LOG.info("[{}] Ignoring duplicate {} message ", uuid, MessageType.CONNECT); } } else { if (session != null) { switch (frame.getMessageType()) { case KAASYNC: if (((KaaSync) frame).getKaaSyncMessageType() == KaaSyncMessageType.SYNC) { handler.process( new NettyTcpSyncMessage((SyncRequest) frame, session, syncResponseConverter, syncErrorConverter)); } break; case PINGREQ: handler.process(new NettyTcpPingMessage(session)); break; case DISCONNECT: sessionDisconnected = true; handler.process(new NettyTcpDisconnectMessage(session)); break; default: break; } } else { LOG.info("[{}] Ignoring {} message due to incomplete CONNECT sequence", uuid, frame.getMessageType()); ctx.writeAndFlush(new ConnAck(ReturnCode.REFUSE_BAD_PROTOCOL)); } } } @Override public void onSessionCreated(SessionInfo session) { LOG.trace("[{}] Session info is set to {}", uuid, session); this.session = session; } }