diff --git a/src/main/java/net/montoyo/wd/miniserv/AbstractClient.java b/src/main/java/net/montoyo/wd/miniserv/AbstractClient.java new file mode 100644 index 0000000..9ea75eb --- /dev/null +++ b/src/main/java/net/montoyo/wd/miniserv/AbstractClient.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.miniserv; + +import net.montoyo.wd.utilities.Log; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.util.ArrayDeque; + +public abstract class AbstractClient { + + private final ByteBuffer sendBuffer = ByteBuffer.allocateDirect(8192); + private final ArrayDeque sendQueue = new ArrayDeque<>(); + private final PacketReader packetReader = new PacketReader(); + private final PacketWriter packetWriter = new PacketWriter(); + private final Method[] packetHandlers = new Method[PacketID.values().length]; + protected SocketChannel socket; + protected Selector selector; + private SelectionKey writeKey; + + public AbstractClient() { + sendBuffer.limit(0); + + Method[] methods = getClass().getMethods(); + for(Method m: methods) { + PacketHandler ph = m.getAnnotation(PacketHandler.class); + + if(ph != null) { + if(packetHandlers[ph.value().ordinal()] != null) + Log.warning("AbstractClient: several packet handlers for %s, ignoring %s", ph.value().toString(), m.getName()); + else if(m.getParameterCount() != 1 || m.getParameterTypes()[0] != DataInputStream.class) + Log.warning("AbstractClient: found invalid packet handler %s", m.getName()); + else + packetHandlers[ph.value().ordinal()] = m; + } + } + + for(int i = 0; i < packetHandlers.length; i++) { + if(packetHandlers[i] == null) + Log.warning("AbstractClient: no packet handler for %s", PacketID.fromInt(i).toString()); + } + } + + protected abstract void onWriteError(); + + public void readyRead(ByteBuffer bb) { + while(bb.remaining() > 0) { + if(packetReader.readFrom(bb)) { //End of packet + byte[] pkt = packetReader.getPacketData(); + + if(pkt != null) { + try { + DataInputStream dis = new DataInputStream(new ByteArrayInputStream(pkt)); + int pid = dis.readByte() & 0xFF; + + if(pid >= packetHandlers.length) + Log.error("Caught invalid packet ID %d", pid); + else if(packetHandlers[pid] != null) { + try { + packetHandlers[pid].invoke(this, dis); //This is slow, I know... sorry + } catch(IllegalAccessException ex) { + Log.errorEx("This shouldn't have happened", ex); + } catch(InvocationTargetException ex) { + Log.warningEx("Caught exception while handling packet %d", ex.getTargetException(), pid); + } + } + } catch(IOException ex) { + Log.warningEx("IOException while trying to handle packet", ex); + } + } + + packetReader.reset(); + } + } + } + + public void readyWrite() throws Throwable { + if(sendBuffer.remaining() > 0 || fillSendBuffer()) { + if(socket.write(sendBuffer) < 0) + onWriteError(); + } + } + + private boolean fillSendBuffer() { + sendBuffer.clear(); + + do { + if(packetWriter.writeTo(sendBuffer)) { + OutgoingPacket pkt; + synchronized(sendQueue) { + pkt = sendQueue.poll(); + + if(pkt == null) { + if(writeKey != null) { + writeKey.cancel(); + writeKey = null; + } + + return sendBuffer.remaining() > 0; + } + } + + packetWriter.reset(pkt.finish()); + } + } while(sendBuffer.remaining() > 0); + + return true; + } + + public void sendPacket(OutgoingPacket pkt) { + synchronized(sendQueue) { + sendQueue.offer(pkt); + + if(writeKey == null) { + try { + writeKey = socket.register(selector, SelectionKey.OP_WRITE); + } catch(ClosedChannelException ex) { + Log.warningEx("Couldn't send packet", ex); + } + } + } + } + +} diff --git a/src/main/java/net/montoyo/wd/miniserv/KeyParameters.java b/src/main/java/net/montoyo/wd/miniserv/KeyParameters.java new file mode 100644 index 0000000..79162b6 --- /dev/null +++ b/src/main/java/net/montoyo/wd/miniserv/KeyParameters.java @@ -0,0 +1,15 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.miniserv; + +public abstract class KeyParameters { + + public static final int RSA_KEY_SIZE = 2048; + public static final int KEY_SIZE = 32; + public static final int CHALLENGE_SIZE = 32; + public static final String MAC_ALGORITHM = "HmacSHA256"; + public static final String RSA_CIPHER = "RSA/ECB/PKCS1Padding"; + +} diff --git a/src/main/java/net/montoyo/wd/miniserv/PacketHandler.java b/src/main/java/net/montoyo/wd/miniserv/PacketHandler.java new file mode 100644 index 0000000..0864262 --- /dev/null +++ b/src/main/java/net/montoyo/wd/miniserv/PacketHandler.java @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.miniserv; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface PacketHandler { + + PacketID value(); + +} diff --git a/src/main/java/net/montoyo/wd/miniserv/PacketID.java b/src/main/java/net/montoyo/wd/miniserv/PacketID.java index fa0e966..42456a0 100644 --- a/src/main/java/net/montoyo/wd/miniserv/PacketID.java +++ b/src/main/java/net/montoyo/wd/miniserv/PacketID.java @@ -6,6 +6,7 @@ package net.montoyo.wd.miniserv; public enum PacketID { + AUTHENTICATE, //C->S and S->C PING, //C->S and S->C BEGIN_FILE_UPLOAD, //C->S FILE_PART, //C->S and S->C diff --git a/src/main/java/net/montoyo/wd/miniserv/client/Client.java b/src/main/java/net/montoyo/wd/miniserv/client/Client.java new file mode 100644 index 0000000..e0dabb2 --- /dev/null +++ b/src/main/java/net/montoyo/wd/miniserv/client/Client.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.miniserv.client; + +import net.montoyo.wd.miniserv.*; +import net.montoyo.wd.net.SMessageMiniservConnect; +import net.montoyo.wd.utilities.Log; +import net.montoyo.wd.utilities.Util; + +import javax.crypto.*; +import javax.crypto.spec.SecretKeySpec; +import java.io.DataInputStream; +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.security.*; +import java.security.interfaces.RSAPublicKey; + +public class Client extends AbstractClient implements Runnable { + + private static Client instance; + + public static Client getInstance() { + if(instance == null) + instance = new Client(); + + return instance; + } + + private final SecureRandom random = new SecureRandom(); + private KeyPair keyPair; + private byte[] key; + private SocketAddress address; + private boolean running = true; + private final ByteBuffer readBuffer = ByteBuffer.allocateDirect(8192); + private final Thread thread = new Thread(this); + private boolean authenticated = false; + + public SMessageMiniservConnect beginConnection() { + if(keyPair == null) { + try { + KeyPairGenerator keygen = KeyPairGenerator.getInstance("RSA"); + keygen.initialize(KeyParameters.RSA_KEY_SIZE); + keyPair = keygen.generateKeyPair(); + } catch(NoSuchAlgorithmException ex) { + Log.warningEx("RSA is unsupported?!?!", ex); + return null; + } + } + + RSAPublicKey pubKey = (RSAPublicKey) keyPair.getPublic(); + return new SMessageMiniservConnect(pubKey.getModulus().toByteArray(), pubKey.getPublicExponent().toByteArray()); + } + + public boolean decryptKey(byte[] encKey) { + try { + Cipher cipher = Cipher.getInstance(KeyParameters.RSA_CIPHER); + cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate(), random); + key = cipher.doFinal(encKey); + return true; + } catch(NoSuchAlgorithmException | NoSuchPaddingException ex) { + Log.warningEx("%s unsupported...", ex, KeyParameters.RSA_CIPHER); + } catch(InvalidKeyException ex) { + Log.warningEx("The generated key is invalid...", ex); + } catch(IllegalBlockSizeException | BadPaddingException ex) { + Log.warningEx("Could not decrypt key", ex); + } + + return false; + } + + public byte[] authenticate(byte[] challenge) { + try { + Mac mac = Mac.getInstance(KeyParameters.MAC_ALGORITHM); + mac.init(new SecretKeySpec(key, KeyParameters.MAC_ALGORITHM)); + return mac.doFinal(challenge); + } catch(NoSuchAlgorithmException ex) { + Log.warningEx("%s unsupported...", ex, KeyParameters.MAC_ALGORITHM); + } catch(InvalidKeyException ex) { + Log.warningEx("The key given by the server is invalid", ex); + } + + return null; + } + + public void start(SocketAddress addr) { + address = addr; + thread.start(); + } + + @Override + public void run() { + try { + socket = SocketChannel.open(); + socket.connect(address); + socket.configureBlocking(false); + + selector = Selector.open(); + socket.register(selector, SelectionKey.OP_READ); + } catch(IOException ex) { + Log.errorEx("Couldn't start client", ex); + return; + } + + while(running) { + try { + unsafeLoop(); + } catch(Throwable t) { + Log.errorEx("MiniServ Client crashed", t); + break; + } + } + + Util.silentClose(socket); + Util.silentClose(selector); + } + + private void unsafeLoop() throws Throwable { + selector.select(); + + for(SelectionKey key: selector.selectedKeys()) { + if(key.isReadable()) { + readBuffer.clear(); + int rd = socket.read(readBuffer); + + if(rd <= 0) { + Log.warning("Connection was closed, stopping..."); + running = false; + } else { + readBuffer.position(0); + readBuffer.limit(rd); + readyRead(readBuffer); + } + } + + if(key.isWritable()) { + try { + readyWrite(); + } catch(Throwable t) { + Log.errorEx("Caught error while sending data to miniserv, stopping...", t); + running = false; + } + } + } + } + + @Override + protected void onWriteError() { + running = false; + Log.error("Write error, stopping..."); + } + + @PacketHandler(PacketID.AUTHENTICATE) + public void handleAuth(DataInputStream dis) { + //TODO: Do some stuff + } + +} diff --git a/src/main/java/net/montoyo/wd/miniserv/server/ClientManager.java b/src/main/java/net/montoyo/wd/miniserv/server/ClientManager.java new file mode 100644 index 0000000..a0032f2 --- /dev/null +++ b/src/main/java/net/montoyo/wd/miniserv/server/ClientManager.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.miniserv.server; + +import net.montoyo.wd.miniserv.KeyParameters; +import net.montoyo.wd.utilities.Log; + +import javax.crypto.*; +import javax.crypto.spec.SecretKeySpec; +import java.math.BigInteger; +import java.security.*; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; +import java.util.Arrays; +import java.util.HashMap; +import java.util.UUID; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +public class ClientManager { + + private final SecureRandom random = new SecureRandom(); //SecureRandom is thread safe + private final HashMap keys = new HashMap<>(); + private final ReentrantReadWriteLock keyLock = new ReentrantReadWriteLock(); + + public byte[] getClientKey(UUID uuid) { + keyLock.readLock().lock(); + byte[] key = keys.get(uuid); + keyLock.readLock().unlock(); + + if(key == null) { + key = new byte[KeyParameters.KEY_SIZE]; + random.nextBytes(key); + + keyLock.writeLock().lock(); + keys.put(uuid, key); + keyLock.writeLock().unlock(); + } + + return key; + } + + public void revokeClientKey(UUID id) { + keyLock.writeLock().lock(); + keys.remove(id); + keyLock.writeLock().unlock(); + } + + public byte[] generateChallenge() { + byte[] ret = new byte[KeyParameters.CHALLENGE_SIZE]; + random.nextBytes(ret); + return ret; + } + + public boolean verifyClient(UUID client, byte[] challenge, byte[] hmac) { + keyLock.readLock().lock(); + byte[] key = keys.get(client); + keyLock.readLock().unlock(); + + if(challenge == null || hmac == null || key == null) + return false; + + try { + Mac mac = Mac.getInstance(KeyParameters.MAC_ALGORITHM); + mac.init(new SecretKeySpec(key, KeyParameters.MAC_ALGORITHM)); + byte[] result = mac.doFinal(challenge); + + return Arrays.equals(hmac, result); + } catch(NoSuchAlgorithmException ex) { + Log.warningEx("%s is not supported?!?!", ex, KeyParameters.MAC_ALGORITHM); + } catch(InvalidKeyException ex) { + Log.warningEx("The generated key is invalid", ex); + } + + return false; + } + + public byte[] encryptClientKey(UUID client, byte[] modulus, byte[] exponent) { + RSAPublicKeySpec keySpec = new RSAPublicKeySpec(new BigInteger(modulus), new BigInteger(exponent)); + + try { + PublicKey key = KeyFactory.getInstance("RSA").generatePublic(keySpec); + Cipher cipher = Cipher.getInstance(KeyParameters.RSA_CIPHER); + + cipher.init(Cipher.ENCRYPT_MODE, key, random); + return cipher.doFinal(getClientKey(client)); + } catch(NoSuchAlgorithmException | NoSuchPaddingException ex) { + Log.warningEx("%s is not supported?!?!", ex, KeyParameters.RSA_CIPHER); + } catch(InvalidKeySpecException | InvalidKeyException ex) { + Log.warningEx("A client sent a malicious key", ex); + } catch(IllegalBlockSizeException | BadPaddingException ex) { + Log.warningEx("Could not encrypt client key", ex); + } + + return null; + } + +} diff --git a/src/main/java/net/montoyo/wd/miniserv/server/Server.java b/src/main/java/net/montoyo/wd/miniserv/server/Server.java index fb747e5..98e6fb0 100644 --- a/src/main/java/net/montoyo/wd/miniserv/server/Server.java +++ b/src/main/java/net/montoyo/wd/miniserv/server/Server.java @@ -5,6 +5,7 @@ package net.montoyo.wd.miniserv.server; import net.montoyo.wd.utilities.Log; +import net.montoyo.wd.utilities.Util; import java.net.InetSocketAddress; import java.nio.ByteBuffer; @@ -32,8 +33,8 @@ public class Server extends Thread { public void start() { try { server = ServerSocketChannel.open(); + server.bind(new InetSocketAddress(port)); server.configureBlocking(false); - server.socket().bind(new InetSocketAddress(port)); selector = Selector.open(); server.register(selector, SelectionKey.OP_ACCEPT); @@ -81,7 +82,9 @@ public class Server extends Thread { clientMap.put(chan, toAdd); clientList.add(toAdd); } - } else if(key.isReadable()) { + } + + if(key.isReadable()) { ServerClient cli = clientMap.get(key.channel()); if(cli == null) @@ -103,7 +106,9 @@ public class Server extends Thread { cli.setShouldRemove(); } } - } else if(key.isWritable()) { + } + + if(key.isWritable()) { ServerClient cli = clientMap.get(key.channel()); if(cli == null) @@ -130,10 +135,7 @@ public class Server extends Thread { private void removeClient(ServerClient cli) { clientMap.remove(cli.getChannel()); clientList.remove(cli); - - try { - cli.getChannel().close(); - } catch(Throwable t) {} + Util.silentClose(cli.getChannel()); } } diff --git a/src/main/java/net/montoyo/wd/miniserv/server/ServerClient.java b/src/main/java/net/montoyo/wd/miniserv/server/ServerClient.java index f4ebc93..e35499c 100644 --- a/src/main/java/net/montoyo/wd/miniserv/server/ServerClient.java +++ b/src/main/java/net/montoyo/wd/miniserv/server/ServerClient.java @@ -4,95 +4,26 @@ package net.montoyo.wd.miniserv.server; -import net.montoyo.wd.miniserv.OutgoingPacket; -import net.montoyo.wd.miniserv.PacketID; -import net.montoyo.wd.miniserv.PacketReader; -import net.montoyo.wd.miniserv.PacketWriter; -import net.montoyo.wd.utilities.Log; +import net.montoyo.wd.miniserv.*; -import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.ClosedChannelException; -import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; -import java.util.ArrayDeque; -public class ServerClient { +public class ServerClient extends AbstractClient { - private final SocketChannel socket; - private final Selector selector; - private SelectionKey writeKey; private boolean remove; - private final ByteBuffer sendBuffer = ByteBuffer.allocateDirect(8192); - private final ArrayDeque sendQueue = new ArrayDeque<>(); - private final PacketReader packetReader = new PacketReader(); - private final PacketWriter packetWriter = new PacketWriter(); + private boolean isAuthenticated; ServerClient(SocketChannel s, Selector ss) { socket = s; selector = ss; - sendBuffer.limit(0); //Set empty } - void readyRead(ByteBuffer bb) { - while(bb.remaining() > 0) { - if(packetReader.readFrom(bb)) { //End of packet - byte[] pkt = packetReader.getPacketData(); - if(pkt != null) { - try { - handlePacket(pkt); - } catch(IOException ex) { - Log.warningEx("IOException while trying to handle packet", ex); - } - } - - packetReader.reset(); - } - } - } - - private void handlePacket(byte[] pkt) throws IOException { - DataInputStream dis = new DataInputStream(new ByteArrayInputStream(pkt)); - PacketID pid = PacketID.fromInt(dis.readByte()); - - if(pid == null) { - Log.warning("Caught packet with invalid ID from client"); - return; - } - - OutgoingPacket response = null; - - switch(pid) { - case PING: - response = new OutgoingPacket(); - response.writeByte(PacketID.PING.ordinal()); - break; - - case BEGIN_FILE_UPLOAD: - break; - - case FILE_PART: - break; - - case GET_FILE: - break; - } - - if(response != null) - sendPacket(response); - } - - void readyWrite() throws Throwable { - if(sendBuffer.remaining() > 0 || fillSendBuffer()) { - if(socket.write(sendBuffer) < 0) - remove = true; - } else if(writeKey != null) { - writeKey.cancel(); - writeKey = null; - } + @Override + protected void onWriteError() { + remove = true; } void setShouldRemove() { @@ -107,32 +38,16 @@ public class ServerClient { return socket; } - private boolean fillSendBuffer() { - sendBuffer.clear(); - - do { - if(packetWriter.writeTo(sendBuffer)) { - OutgoingPacket pkt = sendQueue.poll(); - if(pkt == null) - return sendBuffer.remaining() > 0; - - packetWriter.reset(pkt.finish()); - } - } while(sendBuffer.remaining() > 0); - - return true; + @PacketHandler(PacketID.AUTHENTICATE) + public void handleAuthPacket(DataInputStream dis) throws IOException { + //TODO: Do some stuff } - public void sendPacket(OutgoingPacket pkt) { - sendQueue.offer(pkt); - - if(writeKey == null) { - try { - writeKey = socket.register(selector, SelectionKey.OP_WRITE); - } catch(ClosedChannelException ex) { - Log.warningEx("Couldn't send packet", ex); - } - } + @PacketHandler(PacketID.PING) + public void handlePing(DataInputStream dis) { + OutgoingPacket pkt = new OutgoingPacket(); + pkt.writeByte(PacketID.PING.ordinal()); + sendPacket(pkt); } } diff --git a/src/main/java/net/montoyo/wd/net/CMessageMiniservKey.java b/src/main/java/net/montoyo/wd/net/CMessageMiniservKey.java new file mode 100644 index 0000000..95f5580 --- /dev/null +++ b/src/main/java/net/montoyo/wd/net/CMessageMiniservKey.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.net; + +import io.netty.buffer.ByteBuf; +import net.minecraftforge.fml.common.network.simpleimpl.IMessage; +import net.minecraftforge.fml.common.network.simpleimpl.IMessageHandler; +import net.minecraftforge.fml.common.network.simpleimpl.MessageContext; +import net.minecraftforge.fml.relauncher.Side; + +@Message(messageId = 11, side = Side.CLIENT) +public class CMessageMiniservKey implements IMessage { + + private byte[] encryptedKey; + + public CMessageMiniservKey() { + } + + public CMessageMiniservKey(byte[] key) { + encryptedKey = key; + } + + @Override + public void fromBytes(ByteBuf buf) { + encryptedKey = new byte[buf.readByte() & 0xFF]; + buf.readBytes(encryptedKey); + } + + @Override + public void toBytes(ByteBuf buf) { + buf.writeByte(encryptedKey.length); + buf.writeBytes(encryptedKey); + } + + public static class Handler implements IMessageHandler { + + @Override + public IMessage onMessage(CMessageMiniservKey message, MessageContext ctx) { + //TODO: Start client thread + return null; + } + + } + +} diff --git a/src/main/java/net/montoyo/wd/net/Messages.java b/src/main/java/net/montoyo/wd/net/Messages.java index ce5c98b..24462e1 100644 --- a/src/main/java/net/montoyo/wd/net/Messages.java +++ b/src/main/java/net/montoyo/wd/net/Messages.java @@ -11,7 +11,7 @@ import net.minecraftforge.fml.common.network.simpleimpl.SimpleNetworkWrapper; import java.lang.reflect.Modifier; import java.util.ArrayList; -public class Messages { +public abstract class Messages { private static DefaultHandler DEFAULT_HANDLER = new DefaultHandler(); private static Class[] messages; @@ -27,6 +27,8 @@ public class Messages { l.add(SMessagePadCtrl.class); l.add(SMessageRedstoneCtrl.class); l.add(CMessageJSResponse.class); + l.add(SMessageMiniservConnect.class); + l.add(CMessageMiniservKey.class); messages = l.toArray(new Class[0]); } diff --git a/src/main/java/net/montoyo/wd/net/SMessageMiniservConnect.java b/src/main/java/net/montoyo/wd/net/SMessageMiniservConnect.java new file mode 100644 index 0000000..0ff105c --- /dev/null +++ b/src/main/java/net/montoyo/wd/net/SMessageMiniservConnect.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.net; + +import io.netty.buffer.ByteBuf; +import net.minecraftforge.fml.common.network.simpleimpl.IMessage; +import net.minecraftforge.fml.common.network.simpleimpl.IMessageHandler; +import net.minecraftforge.fml.common.network.simpleimpl.MessageContext; +import net.minecraftforge.fml.relauncher.Side; + +@Message(messageId = 10, side = Side.SERVER) +public class SMessageMiniservConnect implements IMessage { + + private byte[] modulus; + private byte[] exponent; + + public SMessageMiniservConnect() { + } + + public SMessageMiniservConnect(byte[] mod, byte[] exp) { + modulus = mod; + exponent = exp; + } + + @Override + public void fromBytes(ByteBuf buf) { + int sz = buf.readByte() & 0xFF; + modulus = new byte[sz]; + buf.readBytes(modulus); + + sz = buf.readByte() & 0xFF; + exponent = new byte[sz]; + buf.readBytes(exponent); + } + + @Override + public void toBytes(ByteBuf buf) { + buf.writeByte(modulus.length); + buf.writeBytes(modulus); + buf.writeByte(exponent.length); + buf.writeBytes(exponent); + } + + public static class Handler implements IMessageHandler { + + @Override + public IMessage onMessage(SMessageMiniservConnect message, MessageContext ctx) { + //TODO: Generate key + return null; + } + + } + +}