All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.fisco.bcos.sdk.amop.topic.AmopMsgHandler Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2014-2020  [fisco-dev]
 *
 * 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.fisco.bcos.sdk.amop.topic;

import static org.fisco.bcos.sdk.amop.topic.TopicManager.topicNeedVerifyPrefix;
import static org.fisco.bcos.sdk.amop.topic.TopicManager.verifyChannelPrefix;

import com.fasterxml.jackson.core.JsonProcessingException;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.socket.SocketChannel;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import org.fisco.bcos.sdk.amop.Amop;
import org.fisco.bcos.sdk.amop.AmopCallback;
import org.fisco.bcos.sdk.channel.Channel;
import org.fisco.bcos.sdk.channel.ResponseCallback;
import org.fisco.bcos.sdk.channel.model.Options;
import org.fisco.bcos.sdk.crypto.CryptoSuite;
import org.fisco.bcos.sdk.crypto.keystore.KeyTool;
import org.fisco.bcos.sdk.model.AmopMsg;
import org.fisco.bcos.sdk.model.CryptoType;
import org.fisco.bcos.sdk.model.Message;
import org.fisco.bcos.sdk.model.MsgType;
import org.fisco.bcos.sdk.model.Response;
import org.fisco.bcos.sdk.network.MsgHandler;
import org.fisco.bcos.sdk.utils.Hex;
import org.fisco.bcos.sdk.utils.ObjectMapperFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class AmopMsgHandler implements MsgHandler {
    private static Logger logger = LoggerFactory.getLogger(AmopMsgHandler.class);
    private TopicManager topicManager;
    private Channel channel;
    private long defaultTimeout = 5000;
    private Map seq2Callback = new ConcurrentHashMap<>();
    private boolean isRunning = false;

    public AmopMsgHandler(Channel channel, TopicManager topicManager) {
        this.topicManager = topicManager;
        this.channel = channel;
    }

    public void setIsRunning(boolean isRunning) {
        this.isRunning = isRunning;
    }

    @Override
    public void onConnect(ChannelHandlerContext ctx) {
        if (!isRunning) {
            logger.warn("Amop on connect, amop is not running, exit.");
            return;
        }

        String host = ((SocketChannel) ctx.channel()).remoteAddress().getAddress().getHostAddress();
        Integer port = ((SocketChannel) ctx.channel()).remoteAddress().getPort();
        String ipAndPort = host + ":" + port;
        logger.info("Node connected, update topics to node. node:" + ipAndPort);
        try {
            Set topics = topicManager.getSubByPeer(ipAndPort);
            byte[] topicBytes =
                    ObjectMapperFactory.getObjectMapper().writeValueAsBytes(topics.toArray());
            Message msg = new Message();
            msg.setType((short) MsgType.AMOP_CLIENT_TOPICS.getType());
            msg.setResult(0);
            msg.setSeq(newSeq());
            msg.setData(topicBytes);
            ctx.writeAndFlush(msg);
        } catch (JsonProcessingException e) {
            logger.warn("Amop on connect, subscribe error: {}", e.getMessage());
        }
    }

    @Override
    public void onMessage(ChannelHandlerContext ctx, Message msg) {
        logger.trace("receive msg, msg type:{}, seq:{}", msg.getType(), msg.getSeq());
        if (!isRunning) {
            logger.warn("Amop on msg, amop is not running, exit.");
        }

        if (msg.getType() == (short) MsgType.AMOP_RESPONSE.getType()) {
            // Receive a signed Amop message for authorization.
            onAmopResponse(ctx, msg);
            return;
        }

        if (msg.getType() == (short) MsgType.REQUEST_TOPICCERT.getType()) {
            // As amop private topic message sender
            onVerifyRequest(ctx, msg);
        } else if (msg.getType() == (short) MsgType.AMOP_REQUEST.getType()
                || msg.getType() == (short) MsgType.AMOP_MULBROADCAST.getType()) {
            AmopMsg amopMsg = new AmopMsg(msg);
            try {
                amopMsg.decodeAmopBody(msg.getData());
            } catch (Exception e) {
                logger.error(
                        "Receive an invalid message, msg type:{}, seq:{}",
                        msg.getType(),
                        msg.getSeq());
                return;
            }
            if (isVerifyingPrivateTopic(amopMsg)) {
                // Receive a private topic authorization message.
                onPrivateTopicRandomValue(ctx, amopMsg);
            } else {
                // Receive an Amop message.
                onAmopMsg(ctx, amopMsg);
            }
        } else {
            logger.error(
                    "amop module receive a not supported type message, type:{}", msg.getType());
        }
    }

    @Override
    public void onDisconnect(ChannelHandlerContext ctx) {}

    public void onVerifyRequest(ChannelHandlerContext ctx, Message msg) {
        logger.trace(
                "private topic verify step 1: node request random number. seq:{} type:{}, content:{}",
                msg.getSeq(),
                msg.getType(),
                new String(msg.getData()));
        // Response to node at the first time.
        responseVerifyRequest(ctx, msg);

        // Start a verify procedure
        // Read message data to RequestVerifyData
        String content = new String(msg.getData());
        RequestVerifyData data;
        try {
            data =
                    ObjectMapperFactory.getObjectMapper()
                            .readValue(content, RequestVerifyData.class);
        } catch (JsonProcessingException e) {
            logger.error(
                    "receive request start private topic verify message, message is invalid, seq:{} msgtype:{}",
                    msg.getSeq(),
                    msg.getType());
            return;
        }
        String topic = data.getTopic();
        String nodeId = data.getNodeId();

        logger.trace(
                "private topic verify step 1: node request random number. seq:{} topic:{} nodeId:{}",
                msg.getSeq(),
                topic,
                nodeId);
        // Reply random value to node
        String rmdString = UUID.randomUUID().toString().replaceAll("-", "");
        AmopMsg respMsg = new AmopMsg();
        respMsg.setType((short) MsgType.AMOP_REQUEST.getType());
        respMsg.setSeq(Amop.newSeq());
        respMsg.setResult(0);
        respMsg.setData(rmdString.getBytes());
        respMsg.setTopic(data.getTopicForCert());
        // send message out
        Options opt = new Options();
        opt.setTimeout(defaultTimeout);
        ResponseCallback callback =
                new ResponseCallback() {
                    @Override
                    public void onResponse(Response response) {
                        if (0 != response.getErrorCode()) {
                            logger.error(
                                    "get random value signature of amop private topic failed :{}:{}",
                                    response.getErrorCode(),
                                    response.getErrorMessage());
                            return;
                        }
                        AmopMsg amopMsg = new AmopMsg();
                        amopMsg.decodeAmopBody(response.getContentBytes());
                        int valid = checkSignature(topic, rmdString.getBytes(), amopMsg.getData());
                        try {
                            sendUpdateTopicStatus(valid, topic, nodeId, ctx);
                        } catch (JsonProcessingException e) {
                            logger.error("update topic status error: {}", e.getMessage());
                        }
                    }
                };
        logger.trace(
                "private topic verify step 2: send out random number. seq:{} topic:{} data:{}",
                respMsg.getSeq(),
                respMsg.getTopic(),
                new String(respMsg.getData()));
        channel.asyncSendToRandom(respMsg.getMessage(), callback, opt);
    }

    public void responseVerifyRequest(ChannelHandlerContext ctx, Message msg) {
        Message response = new Message();
        response.setSeq(msg.getSeq());
        response.setResult(0);
        response.setType((short) MsgType.REQUEST_TOPICCERT.getType());
        response.setData("".getBytes());
        ctx.writeAndFlush(response);
    }

    public int checkSignature(String topic, byte[] randomValue, byte[] signature) {
        List pubKeys = topicManager.getPublicKeysByTopic(topic);
        Iterator pks = pubKeys.iterator();
        while (pks.hasNext()) {
            KeyTool keyTool = pks.next();
            CryptoSuite cryptoSuite = new CryptoSuite(CryptoType.ECDSA_TYPE);
            if (cryptoSuite.verify(
                    keyTool,
                    Hex.toHexString(cryptoSuite.hash(randomValue)),
                    Hex.toHexString(signature))) {
                return 0;
            }
        }
        return 1;
    }

    private boolean isVerifyingPrivateTopic(AmopMsg amopMsg) {
        return amopMsg.getTopic().length() > verifyChannelPrefix.length()
                && verifyChannelPrefix.equals(
                        amopMsg.getTopic().substring(0, verifyChannelPrefix.length()));
    }

    public static boolean isPrivateTopic(String topic) {
        return topic.length() > topicNeedVerifyPrefix.length()
                && topicNeedVerifyPrefix.equals(topic.substring(0, topicNeedVerifyPrefix.length()));
    }

    public static String removePrivateTopicPrefix(String topic) {
        if (isPrivateTopic(topic)) {
            return topic.substring(topicNeedVerifyPrefix.length());
        } else {
            return topic;
        }
    }

    private String getSimpleTopic(String fullTopic) {
        return fullTopic.substring(verifyChannelPrefix.length(), fullTopic.length() - 33);
    }

    public void onPrivateTopicRandomValue(ChannelHandlerContext ctx, AmopMsg msg) {
        logger.trace(
                "private topic verify step 2: receive random value, seq:{} type:{} topic:{} data:{}",
                msg.getSeq(),
                msg.getType(),
                msg.getTopic(),
                new String(msg.getData()));
        byte[] randValue = msg.getData();
        String topic = msg.getTopic();
        KeyTool keyTool = topicManager.getPrivateKeyByTopic(getSimpleTopic(topic));
        String signature = "";
        if (null == keyTool) {
            logger.error("topic:{} not subscribed, reject message", getSimpleTopic(topic));
            return;
        } else {
            CryptoSuite cryptoSuite = new CryptoSuite(CryptoType.ECDSA_TYPE);
            try {
                signature = cryptoSuite.sign(keyTool, Hex.toHexString(cryptoSuite.hash(randValue)));
            } catch (Exception e) {
                logger.error(
                        "please check the public key of topic {} is correct configured, error {}",
                        topic,
                        e.getMessage());
            }
        }
        sendSignedRandomNumber(signature, topic, msg.getSeq(), ctx);
    }

    public void onAmopMsg(ChannelHandlerContext ctx, AmopMsg amopMsg) {
        logger.debug(
                "receive a Amop message. seq:{} msgtype:{}", amopMsg.getSeq(), amopMsg.getType());
        if (!topicManager.isSubTopic(amopMsg.getTopic())) {
            logger.warn(
                    "receive an amop msg which is not subscribed, topic:{}", amopMsg.getTopic());
            return;
        }
        AmopCallback callback = topicManager.getCallback(amopMsg.getTopic());
        if (callback == null) {
            logger.error(
                    "can not process Amop message, callback for topic {} is not found",
                    amopMsg.getTopic());
            return;
        }
        AmopMsgIn msgIn = new AmopMsgIn();
        msgIn.setTopic(amopMsg.getTopic());
        if (isPrivateTopic(amopMsg.getTopic())) {
            msgIn.setTopic(removePrivateTopicPrefix(amopMsg.getTopic()));
            msgIn.setTopicType(TopicType.PRIVATE_TOPIC);
        }
        msgIn.setMessageID(amopMsg.getSeq());
        msgIn.setContent(amopMsg.getData());
        msgIn.setResult(amopMsg.getResult());
        msgIn.setCtx(ctx);
        msgIn.setType(amopMsg.getType());
        byte[] content = callback.receiveAmopMsg(msgIn);

        // Response the amop msg
        if (amopMsg.getType() == (short) MsgType.AMOP_MULBROADCAST.getType()) {
            // If received a broadcast msg, do not response.
            return;
        }
        amopMsg.setResult(0);
        amopMsg.setType((short) MsgType.AMOP_RESPONSE.getType());
        amopMsg.setData(content);
        logger.trace(
                "Send response, seq:{} topic:{} content:{}",
                amopMsg.getSeq(),
                amopMsg.getTopic(),
                new String(content));
        ctx.writeAndFlush(amopMsg.getMessage());
    }

    public void onAmopResponse(ChannelHandlerContext ctx, Message msg) {
        logger.debug("receive amop response. seq:{} msgtype:{} ", msg.getSeq(), msg.getType());
        ResponseCallback callback = seq2Callback.get(msg.getSeq());
        if (null != callback) {
            Response resp = new Response();
            resp.setMessageID(msg.getSeq());
            resp.setErrorCode(msg.getResult());
            if (msg.getResult() != 0) {
                resp.setErrorMessage("response errors");
            }
            // 103: the AMOP_requests or the AMOP_multicast_requests have been rejected due to
            // over bandwidth limit
            if (msg.getResult()
                    == AmopRespError.REJECT_AMOP_REQ_FOR_OVER_BANDWIDTHLIMIT.getError()) {
                logger.error(
                        "AMOP request was rejected due to over bandwidth limit, message: {}",
                        msg.getSeq());
                resp.setErrorMessage("AMOP request was rejected due to over bandwidth limit");
            }

            if (msg.getResult() == AmopRespError.NO_AVAILABLE_SESSION.getError()) {
                logger.error(
                        "AMOP request was rejected due to over bandwidth limit, message: {}",
                        msg.getSeq());
                resp.setErrorMessage("AMOP request was rejected due to over bandwidth limit");
            }

            if (msg.getData() != null) {
                AmopMsg amopMsg = new AmopMsg();
                amopMsg.decodeAmopBody(msg.getData());
                resp.setContent(new String(amopMsg.getData()));
            }
            callback.onResponse(resp);
        } else {
            logger.error("can not found response callback, timeout:{}", msg.getData());
            return;
        }
    }

    private void sendSignedRandomNumber(
            String signature, String topic, String seq, ChannelHandlerContext ctx) {
        AmopMsg msg = new AmopMsg();
        msg.setTopic(topic);
        msg.setResult(0);
        msg.setSeq(seq);
        msg.setType((short) MsgType.AMOP_RESPONSE.getType());
        msg.setData(Hex.decode(signature));
        logger.trace(
                "private topic verify step 3: sign on random value and send out, seq:{} type:{} topic:{} data:{}",
                msg.getSeq(),
                msg.getType(),
                msg.getTopic(),
                new String(msg.getData()));
        ctx.writeAndFlush(msg.getMessage());
    }

    public void sendUpdateTopicStatus(
            int valid, String topic, String nodeId, ChannelHandlerContext ctx)
            throws JsonProcessingException {
        UpdateTopicStatus updateTopicStatus = new UpdateTopicStatus();
        updateTopicStatus.setCheckResult(valid);
        updateTopicStatus.setNodeId(nodeId);
        updateTopicStatus.setTopic(topic);
        String jsonStr =
                ObjectMapperFactory.getObjectMapper().writeValueAsString(updateTopicStatus);

        Message msg = new Message();
        msg.setData(jsonStr.getBytes());
        msg.setSeq(newSeq());
        msg.setResult(0);
        msg.setType((short) MsgType.UPDATE_TOPIICSTATUS.getType());
        logger.info(
                "private topic verify step4: finish signature verify, send out msg to update topic status, seq:{} topic:{} valid:{}",
                msg.getSeq(),
                topic,
                valid);
        ctx.writeAndFlush(msg);
    }

    public void addCallback(String seq, ResponseCallback callback) {
        seq2Callback.put(seq, callback);
    }

    private String newSeq() {
        return UUID.randomUUID().toString().replaceAll("-", "");
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy