/*
* Copyright (c) [2016] [ <ether.camp> ]
* This file is part of the ethereumJ library.
*
* The ethereumJ library is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The ethereumJ library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the ethereumJ library. If not, see <http://www.gnu.org/licenses/>.
*/
package org.ethereum.net.rlpx;
import com.google.common.io.ByteStreams;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.timeout.ReadTimeoutException;
import org.ethereum.config.SystemProperties;
import org.ethereum.crypto.ECIESCoder;
import org.ethereum.crypto.ECKey;
import org.ethereum.net.message.Message;
import org.ethereum.net.p2p.DisconnectMessage;
import org.ethereum.net.p2p.HelloMessage;
import org.ethereum.net.p2p.P2pMessageCodes;
import org.ethereum.net.p2p.P2pMessageFactory;
import org.ethereum.net.rlpx.discover.NodeManager;
import org.ethereum.net.server.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.InvalidCipherTextException;
import org.spongycastle.math.ec.ECPoint;
import org.spongycastle.util.encoders.Hex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.List;
import static org.ethereum.net.rlpx.FrameCodec.Frame;
import static org.ethereum.util.ByteUtil.bigEndianToShort;
/**
* The Netty handler which manages initial negotiation with peer
* (when either we initiating connection or remote peer initiates)
*
* The initial handshake includes:
* - first AuthInitiate -> AuthResponse messages when peers exchange with secrets
* - second P2P Hello messages when P2P protocol and subprotocol capabilities are negotiated
*
* After the handshake is done this handler reports secrets and other data to the Channel
* which installs further handlers depending on the protocol parameters.
* This handler is finally removed from the pipeline.
*/
@Component
@Scope("prototype")
public class HandshakeHandler extends ByteToMessageDecoder {
private static final Logger loggerWire = LoggerFactory.getLogger("wire");
private static final Logger loggerNet = LoggerFactory.getLogger("net");
private FrameCodec frameCodec;
private final ECKey myKey;
private byte[] nodeId;
private byte[] remoteId;
private EncryptionHandshake handshake;
private byte[] initiatePacket;
private Channel channel;
private boolean isHandshakeDone;
private final SystemProperties config;
private final NodeManager nodeManager;
@Autowired
public HandshakeHandler(final SystemProperties config, final NodeManager nodeManager) {
this.config = config;
this.nodeManager = nodeManager;
myKey = config.getMyKey();
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
channel.setInetSocketAddress((InetSocketAddress) ctx.channel().remoteAddress());
if (remoteId.length == 64) {
channel.initWithNode(remoteId);
initiate(ctx);
} else {
handshake = new EncryptionHandshake();
nodeId = myKey.getNodeId();
}
}
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
loggerWire.debug("Decoding handshake... (" + in.readableBytes() + " bytes available)");
decodeHandshake(ctx, in);
if (isHandshakeDone) {
loggerWire.debug("Handshake done, removing HandshakeHandler from pipeline.");
ctx.pipeline().remove(this);
}
}
public void initiate(ChannelHandlerContext ctx) throws Exception {
loggerNet.debug("RLPX protocol activated");
nodeId = myKey.getNodeId();
handshake = new EncryptionHandshake(ECKey.fromNodeId(this.remoteId).getPubKeyPoint());
Object msg;
if (config.eip8()) {
AuthInitiateMessageV4 initiateMessage = handshake.createAuthInitiateV4(myKey);
initiatePacket = handshake.encryptAuthInitiateV4(initiateMessage);
msg = initiateMessage;
} else {
AuthInitiateMessage initiateMessage = handshake.createAuthInitiate(null, myKey);
initiatePacket = handshake.encryptAuthMessage(initiateMessage);
msg = initiateMessage;
}
final ByteBuf byteBufMsg = ctx.alloc().buffer(initiatePacket.length);
byteBufMsg.writeBytes(initiatePacket);
ctx.writeAndFlush(byteBufMsg).sync();
channel.getNodeStatistics().rlpxAuthMessagesSent.add();
if (loggerNet.isDebugEnabled())
loggerNet.debug("To: {} Send: {}", ctx.channel().remoteAddress(), msg);
}
// consume handshake, producing no resulting message to upper layers
private void decodeHandshake(final ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
if (handshake.isInitiator()) {
if (frameCodec == null) {
byte[] responsePacket = new byte[AuthResponseMessage.getLength() + ECIESCoder.getOverhead()];
if (!buffer.isReadable(responsePacket.length))
return;
buffer.readBytes(responsePacket);
try {
// trying to decode as pre-EIP-8
AuthResponseMessage response = handshake.handleAuthResponse(myKey, initiatePacket, responsePacket);
loggerNet.debug("From: {} Recv: {}", ctx.channel().remoteAddress(), response);
} catch (Throwable t) {
// it must be format defined by EIP-8 then
responsePacket = readEIP8Packet(buffer, responsePacket);
if (responsePacket == null) return;
AuthResponseMessageV4 response = handshake.handleAuthResponseV4(myKey, initiatePacket, responsePacket);
loggerNet.debug("From: {} Recv: {}", ctx.channel().remoteAddress(), response);
}
EncryptionHandshake.Secrets secrets = this.handshake.getSecrets();
this.frameCodec = new FrameCodec(secrets);
loggerNet.debug("auth exchange done");
channel.sendHelloMessage(ctx, frameCodec, Hex.toHexString(nodeId), null);
} else {
loggerWire.info("MessageCodec: Buffer bytes: " + buffer.readableBytes());
List<Frame> frames = frameCodec.readFrames(buffer);
if (frames == null || frames.isEmpty())
return;
Frame frame = frames.get(0);
byte[] payload = ByteStreams.toByteArray(frame.getStream());
if (frame.getType() == P2pMessageCodes.HELLO.asByte()) {
HelloMessage helloMessage = new HelloMessage(payload);
if (loggerNet.isDebugEnabled())
loggerNet.debug("From: {} Recv: {}", ctx.channel().remoteAddress(), helloMessage);
isHandshakeDone = true;
this.channel.publicRLPxHandshakeFinished(ctx, frameCodec, helloMessage);
} else {
DisconnectMessage message = new DisconnectMessage(payload);
if (loggerNet.isDebugEnabled())
loggerNet.debug("From: {} Recv: {}", channel, message);
channel.getNodeStatistics().nodeDisconnectedRemote(message.getReason());
}
}
} else {
loggerWire.debug("Not initiator.");
if (frameCodec == null) {
loggerWire.debug("FrameCodec == null");
byte[] authInitPacket = new byte[AuthInitiateMessage.getLength() + ECIESCoder.getOverhead()];
if (!buffer.isReadable(authInitPacket.length))
return;
buffer.readBytes(authInitPacket);
this.handshake = new EncryptionHandshake();
byte[] responsePacket;
try {
// trying to decode as pre-EIP-8
AuthInitiateMessage initiateMessage = handshake.decryptAuthInitiate(authInitPacket, myKey);
loggerNet.debug("From: {} Recv: {}", ctx.channel().remoteAddress(), initiateMessage);
AuthResponseMessage response = handshake.makeAuthInitiate(initiateMessage, myKey);
loggerNet.debug("To: {} Send: {}", ctx.channel().remoteAddress(), response);
responsePacket = handshake.encryptAuthResponse(response);
} catch (Throwable t) {
// it must be format defined by EIP-8 then
try {
authInitPacket = readEIP8Packet(buffer, authInitPacket);
if (authInitPacket == null) return;
AuthInitiateMessageV4 initiateMessage = handshake.decryptAuthInitiateV4(authInitPacket, myKey);
loggerNet.debug("From: {} Recv: {}", ctx.channel().remoteAddress(), initiateMessage);
AuthResponseMessageV4 response = handshake.makeAuthInitiateV4(initiateMessage, myKey);
loggerNet.debug("To: {} Send: {}", ctx.channel().remoteAddress(), response);
responsePacket = handshake.encryptAuthResponseV4(response);
} catch (InvalidCipherTextException ce) {
loggerNet.warn("Can't decrypt AuthInitiateMessage from " + ctx.channel().remoteAddress() +
". Most likely the remote peer used wrong public key (NodeID) to encrypt message.");
return;
}
}
handshake.agreeSecret(authInitPacket, responsePacket);
EncryptionHandshake.Secrets secrets = this.handshake.getSecrets();
this.frameCodec = new FrameCodec(secrets);
ECPoint remotePubKey = this.handshake.getRemotePublicKey();
byte[] compressed = remotePubKey.getEncoded();
this.remoteId = new byte[compressed.length - 1];
System.arraycopy(compressed, 1, this.remoteId, 0, this.remoteId.length);
final ByteBuf byteBufMsg = ctx.alloc().buffer(responsePacket.length);
byteBufMsg.writeBytes(responsePacket);
ctx.writeAndFlush(byteBufMsg).sync();
} else {
List<Frame> frames = frameCodec.readFrames(buffer);
if (frames == null || frames.isEmpty())
return;
Frame frame = frames.get(0);
Message message = new P2pMessageFactory().create((byte) frame.getType(),
ByteStreams.toByteArray(frame.getStream()));
loggerNet.debug("From: {} Recv: {}", ctx.channel().remoteAddress(), message);
if (frame.getType() == P2pMessageCodes.DISCONNECT.asByte()) {
loggerNet.debug("Active remote peer disconnected right after handshake.");
return;
}
if (frame.getType() != P2pMessageCodes.HELLO.asByte()) {
throw new RuntimeException("The message type is not HELLO or DISCONNECT: " + message);
}
final HelloMessage inboundHelloMessage = (HelloMessage) message;
// now we know both remote nodeId and port
// let's set node, that will cause registering node in NodeManager
channel.initWithNode(remoteId, inboundHelloMessage.getListenPort());
// Secret authentication finish here
channel.sendHelloMessage(ctx, frameCodec, Hex.toHexString(nodeId), inboundHelloMessage);
isHandshakeDone = true;
this.channel.publicRLPxHandshakeFinished(ctx, frameCodec, inboundHelloMessage);
channel.getNodeStatistics().rlpxInHello.add();
}
}
}
private byte[] readEIP8Packet(ByteBuf buffer, byte[] plainPacket) {
int size = bigEndianToShort(plainPacket);
if (size < plainPacket.length)
throw new IllegalArgumentException("AuthResponse packet size is too low");
int bytesLeft = size - plainPacket.length + 2;
byte[] restBytes = new byte[bytesLeft];
if (!buffer.isReadable(restBytes.length))
return null;
buffer.readBytes(restBytes);
byte[] fullResponse = new byte[size + 2];
System.arraycopy(plainPacket, 0, fullResponse, 0, plainPacket.length);
System.arraycopy(restBytes, 0, fullResponse, plainPacket.length, restBytes.length);
return fullResponse;
}
public void setRemoteId(String remoteId, Channel channel){
this.remoteId = Hex.decode(remoteId);
this.channel = channel;
}
public byte[] getRemoteId() {
return remoteId;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
if (channel.isDiscoveryMode()) {
loggerNet.trace("Handshake failed: " + cause);
} else {
if (cause instanceof IOException || cause instanceof ReadTimeoutException) {
loggerNet.debug("Handshake failed: " + ctx.channel().remoteAddress() + ": " + cause);
} else {
loggerNet.warn("Handshake failed: ", cause);
}
}
ctx.close();
}
}