From ac2c80691b5e57f38cd9c0174fcd36f82086e760 Mon Sep 17 00:00:00 2001 From: Tux Date: Mon, 6 Apr 2015 01:22:53 -0400 Subject: [PATCH 1/6] Implement Protocol v2 and converted codebase to Netty. --- pom.xml | 50 ++++ .../com/vexsoftware/votifier/Votifier.java | 97 +++++--- .../vexsoftware/votifier/crypto/RSAIO.java | 33 ++- .../votifier/net/VoteInboundHandler.java | 63 +++++ .../votifier/net/VoteReceiver.java | 222 ------------------ .../votifier/net/VotifierSession.java | 39 +++ .../net/protocol/VotifierGreetingHandler.java | 22 ++ .../protocol/VotifierProtocol1Decoder.java | 94 ++++++++ .../VotifierProtocol2VoteDecoder.java | 62 +++++ .../VotifierProtocolDifferentiator.java | 54 +++++ 10 files changed, 477 insertions(+), 259 deletions(-) create mode 100644 src/main/java/com/vexsoftware/votifier/net/VoteInboundHandler.java delete mode 100644 src/main/java/com/vexsoftware/votifier/net/VoteReceiver.java create mode 100644 src/main/java/com/vexsoftware/votifier/net/VotifierSession.java create mode 100644 src/main/java/com/vexsoftware/votifier/net/protocol/VotifierGreetingHandler.java create mode 100644 src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocol1Decoder.java create mode 100644 src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocol2VoteDecoder.java create mode 100644 src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocolDifferentiator.java diff --git a/pom.xml b/pom.xml index a2ecf20..52237c7 100644 --- a/pom.xml +++ b/pom.xml @@ -23,6 +23,30 @@ bukkit 1.7.2-R0.1 jar + provided + + + + io.netty + netty-handler + 4.0.23.Final + jar + compile + + + + org.json + json + 20141113 + jar + compile + + + + commons-io + commons-io + 2.4 + jar compile @@ -40,6 +64,32 @@ 1.7 + + org.apache.maven.plugins + maven-shade-plugin + 2.3 + + + package + + shade + + + + + true + + + io.netty + com.vexsoftware.votifier.netty + + + org.json + com.vexsoftware.votifier.json + + + + org.apache.maven.plugins diff --git a/src/main/java/com/vexsoftware/votifier/Votifier.java b/src/main/java/com/vexsoftware/votifier/Votifier.java index 6e7fe54..714ba75 100644 --- a/src/main/java/com/vexsoftware/votifier/Votifier.java +++ b/src/main/java/com/vexsoftware/votifier/Votifier.java @@ -19,18 +19,30 @@ package com.vexsoftware.votifier; import java.io.*; +import java.net.URL; import java.security.KeyPair; -import java.util.ArrayList; -import java.util.List; +import java.security.PublicKey; +import java.util.*; import java.util.logging.*; + +import com.vexsoftware.votifier.net.VoteInboundHandler; +import com.vexsoftware.votifier.net.VotifierSession; +import com.vexsoftware.votifier.net.protocol.VotifierGreetingHandler; +import com.vexsoftware.votifier.net.protocol.VotifierProtocolDifferentiator; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; import org.bukkit.Bukkit; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.plugin.java.JavaPlugin; import com.vexsoftware.votifier.crypto.RSAIO; import com.vexsoftware.votifier.crypto.RSAKeygen; import com.vexsoftware.votifier.model.ListenerLoader; import com.vexsoftware.votifier.model.VoteListener; -import com.vexsoftware.votifier.net.VoteReceiver; /** * The main Votifier plugin class. @@ -55,8 +67,11 @@ public class Votifier extends JavaPlugin { /** The vote listeners. */ private final List listeners = new ArrayList(); - /** The vote receiver. */ - private VoteReceiver voteReceiver; + /** The server channel. */ + private Channel serverChannel; + + /** The event group handling the channel. */ + private NioEventLoopGroup serverGroup; /** The RSA key pair. */ private KeyPair keyPair; @@ -64,6 +79,9 @@ public class Votifier extends JavaPlugin { /** Debug mode flag */ private boolean debug; + /** Keys used for websites. */ + private Map keys = new HashMap<>(); + /** * Attach custom log filter to logger. */ @@ -113,6 +131,7 @@ public void onEnable() { cfg.set("host", hostAddr); cfg.set("port", 8192); cfg.set("debug", false); + cfg.set("websites", Collections.emptyMap()); /* * Remind hosted server admins to be sure they have the right @@ -161,6 +180,28 @@ public void onEnable() { listenerDirectory = cfg.getString("listener_folder"); listeners.addAll(ListenerLoader.load(listenerDirectory)); + // Load Votifier keys. + ConfigurationSection websiteSection = cfg.getConfigurationSection("websites"); + + if (websiteSection != null) { + Map websites = websiteSection.getValues(false); + for (Map.Entry website : websites.entrySet()) { + try { + keys.put(website.getKey(), RSAIO.loadPublicKey(new URL((String) website.getValue()))); + if (!(website.getKey().startsWith("https://") || website.getKey().startsWith("file://"))) { + LOG.warning("You are loading a public key (" + website.getKey() + ") over a non-SSL connection. This is insecure!"); + } + LOG.info("Loaded public key for website: " + website.getKey()); + } catch (Exception exception) { + LOG.log(Level.WARNING, + "Error loading public key for website: " + website.getKey(), + exception); + } + } + } else { + LOG.warning("No websites are listed in your configuration."); + } + // Initialize the receiver. String host = cfg.getString("host", hostAddr); int port = cfg.getInt("port", 8192); @@ -168,23 +209,30 @@ public void onEnable() { if (debug) LOG.info("DEBUG mode enabled!"); - try { - voteReceiver = new VoteReceiver(this, host, port); - voteReceiver.start(); - - LOG.info("Votifier enabled."); - } catch (Exception ex) { - gracefulExit(); - return; - } + serverGroup = new NioEventLoopGroup(1); + serverChannel = new ServerBootstrap() + .channel(NioServerSocketChannel.class) + .group(serverGroup) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(NioSocketChannel channel) throws Exception { + channel.attr(VotifierSession.KEY).set(new VotifierSession()); + channel.pipeline().addLast("greetingHandler", new VotifierGreetingHandler()); + channel.pipeline().addLast("protocolDifferentiator", new VotifierProtocolDifferentiator()); + channel.pipeline().addLast("voteHandler", new VoteInboundHandler()); + } + }) + .bind(host, port) + .channel(); + + LOG.info("Votifier enabled."); } @Override public void onDisable() { - // Interrupt the vote receiver. - if (voteReceiver != null) { - voteReceiver.shutdown(); - } + // Shut down the network handlers. + serverChannel.close(); + serverGroup.shutdownGracefully(); LOG.info("Votifier disabled."); } @@ -219,15 +267,6 @@ public List getListeners() { return listeners; } - /** - * Gets the vote receiver. - * - * @return The vote receiver - */ - public VoteReceiver getVoteReceiver() { - return voteReceiver; - } - /** * Gets the keyPair. * @@ -241,4 +280,8 @@ public boolean isDebug() { return debug; } + public Map getKeys() { + return keys; + } + } diff --git a/src/main/java/com/vexsoftware/votifier/crypto/RSAIO.java b/src/main/java/com/vexsoftware/votifier/crypto/RSAIO.java index a5727ee..373dab7 100644 --- a/src/main/java/com/vexsoftware/votifier/crypto/RSAIO.java +++ b/src/main/java/com/vexsoftware/votifier/crypto/RSAIO.java @@ -18,9 +18,13 @@ package com.vexsoftware.votifier.crypto; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; + import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; +import java.net.URL; import java.security.KeyFactory; import java.security.KeyPair; import java.security.PrivateKey; @@ -32,8 +36,6 @@ /** * Static utility methods for saving and loading RSA key pairs. - * - * @author Blake Beaupain */ public class RSAIO { @@ -81,21 +83,15 @@ public static void save(File directory, KeyPair keyPair) throws Exception { public static KeyPair load(File directory) throws Exception { // Read the public key file. File publicKeyFile = new File(directory + "/public.key"); - FileInputStream in = new FileInputStream(directory + "/public.key"); - byte[] encodedPublicKey = new byte[(int) publicKeyFile.length()]; - in.read(encodedPublicKey); + byte[] encodedPublicKey = FileUtils.readFileToByteArray(publicKeyFile); encodedPublicKey = DatatypeConverter.parseBase64Binary(new String( encodedPublicKey)); - in.close(); // Read the private key file. File privateKeyFile = new File(directory + "/private.key"); - in = new FileInputStream(directory + "/private.key"); - byte[] encodedPrivateKey = new byte[(int) privateKeyFile.length()]; - in.read(encodedPrivateKey); + byte[] encodedPrivateKey = FileUtils.readFileToByteArray(privateKeyFile); encodedPrivateKey = DatatypeConverter.parseBase64Binary(new String( encodedPrivateKey)); - in.close(); // Instantiate and return the key pair. KeyFactory keyFactory = KeyFactory.getInstance("RSA"); @@ -108,4 +104,21 @@ public static KeyPair load(File directory) throws Exception { return new KeyPair(publicKey, privateKey); } + /** + * Loads an RSA public key from a URL. + * + * @param url + * The URL that has the public key + * @return + * The public key + * @throws Exception + * If an error occurs + */ + public static PublicKey loadPublicKey(URL url) throws Exception { + String publicKey = new String(IOUtils.toByteArray(url), "UTF-8").replaceAll("(-+BEGIN PUBLIC KEY-+\\r?\\n|-+END PUBLIC KEY-+\\r?\\n?)", ""); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(DatatypeConverter.parseBase64Binary(publicKey)); + return keyFactory.generatePublic(publicKeySpec); + } + } diff --git a/src/main/java/com/vexsoftware/votifier/net/VoteInboundHandler.java b/src/main/java/com/vexsoftware/votifier/net/VoteInboundHandler.java new file mode 100644 index 0000000..80c2cad --- /dev/null +++ b/src/main/java/com/vexsoftware/votifier/net/VoteInboundHandler.java @@ -0,0 +1,63 @@ +package com.vexsoftware.votifier.net; + +import com.vexsoftware.votifier.Votifier; +import com.vexsoftware.votifier.model.Vote; +import com.vexsoftware.votifier.model.VoteListener; +import com.vexsoftware.votifier.model.VotifierEvent; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import org.bukkit.Bukkit; +import org.json.JSONObject; + +import java.util.logging.Level; + +public class VoteInboundHandler extends SimpleChannelInboundHandler { + @Override + protected void channelRead0(ChannelHandlerContext ctx, final Vote vote) throws Exception { + // Fire a synchronous task and close the connection. + Votifier.getInstance().getServer().getScheduler().scheduleSyncDelayedTask(Votifier.getInstance(), new Runnable() { + @Override + public void run() { + for (VoteListener listener : Votifier.getInstance().getListeners()) { + try { + listener.voteMade(vote); + } catch (Exception ex) { + String vlName = listener.getClass().getSimpleName(); + Votifier.getInstance().getLogger().log(Level.WARNING, + "Exception caught while sending the vote notification to the '" + + vlName + "' listener", ex); + } + } + Bukkit.getServer().getPluginManager().callEvent(new VotifierEvent(vote)); + } + }); + + VotifierSession session = ctx.channel().attr(VotifierSession.KEY).get(); + + if (session.getVersion() == VotifierSession.ProtocolVersion.ONE) { + ctx.close(); + } else { + JSONObject object = new JSONObject(); + object.put("status", "ok"); + ctx.writeAndFlush(object.toString()).addListener(ChannelFutureListener.CLOSE); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + VotifierSession session = ctx.channel().attr(VotifierSession.KEY).get(); + + Votifier.getInstance().getLogger().log(Level.SEVERE, "Exception while processing vote from " + ctx.channel().remoteAddress(), cause); + + if (session.getVersion() == VotifierSession.ProtocolVersion.TWO) { + JSONObject object = new JSONObject(); + object.put("status", "error"); + object.put("cause", cause.getClass().getSimpleName()); + object.put("error", cause.getMessage()); + ctx.writeAndFlush(object.toString()).addListener(ChannelFutureListener.CLOSE); + } else { + ctx.close(); + } + } +} diff --git a/src/main/java/com/vexsoftware/votifier/net/VoteReceiver.java b/src/main/java/com/vexsoftware/votifier/net/VoteReceiver.java deleted file mode 100644 index 60173d8..0000000 --- a/src/main/java/com/vexsoftware/votifier/net/VoteReceiver.java +++ /dev/null @@ -1,222 +0,0 @@ -/* - * Copyright (C) 2012 Vex Software LLC - * This file is part of Votifier. - * - * Votifier is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Votifier 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Votifier. If not, see . - */ - -package com.vexsoftware.votifier.net; - -import java.io.BufferedWriter; -import java.io.InputStream; -import java.io.OutputStreamWriter; -import java.net.InetSocketAddress; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.SocketException; -import java.util.logging.*; -import javax.crypto.BadPaddingException; -import org.bukkit.Bukkit; - -import com.vexsoftware.votifier.Votifier; -import com.vexsoftware.votifier.crypto.RSA; -import com.vexsoftware.votifier.model.*; - -/** - * The vote receiving server. - * - * @author Blake Beaupain - * @author Kramer Campbell - */ -public class VoteReceiver extends Thread { - - /** The logger instance. */ - private static final Logger LOG = Logger.getLogger("Votifier"); - - private final Votifier plugin; - - /** The host to listen on. */ - private final String host; - - /** The port to listen on. */ - private final int port; - - /** The server socket. */ - private ServerSocket server; - - /** The running flag. */ - private boolean running = true; - - /** - * Instantiates a new vote receiver. - * - * @param host - * The host to listen on - * @param port - * The port to listen on - */ - public VoteReceiver(final Votifier plugin, String host, int port) - throws Exception { - this.plugin = plugin; - this.host = host; - this.port = port; - - initialize(); - } - - private void initialize() throws Exception { - try { - server = new ServerSocket(); - server.bind(new InetSocketAddress(host, port)); - } catch (Exception ex) { - LOG.log(Level.SEVERE, - "Error initializing vote receiver. Please verify that the configured"); - LOG.log(Level.SEVERE, - "IP address and port are not already in use. This is a common problem"); - LOG.log(Level.SEVERE, - "with hosting services and, if so, you should check with your hosting provider.", - ex); - throw new Exception(ex); - } - } - - /** - * Shuts the vote receiver down cleanly. - */ - public void shutdown() { - running = false; - if (server == null) - return; - try { - server.close(); - } catch (Exception ex) { - LOG.log(Level.WARNING, "Unable to shut down vote receiver cleanly."); - } - } - - @Override - public void run() { - - // Main loop. - while (running) { - try { - Socket socket = server.accept(); - socket.setSoTimeout(5000); // Don't hang on slow connections. - BufferedWriter writer = new BufferedWriter( - new OutputStreamWriter(socket.getOutputStream())); - InputStream in = socket.getInputStream(); - - // Send them our version. - writer.write("VOTIFIER " + Votifier.getInstance().getVersion()); - writer.newLine(); - writer.flush(); - - // Read the 256 byte block. - byte[] block = new byte[256]; - in.read(block, 0, block.length); - - // Decrypt the block. - block = RSA.decrypt(block, Votifier.getInstance().getKeyPair() - .getPrivate()); - int position = 0; - - // Perform the opcode check. - String opcode = readString(block, position); - position += opcode.length() + 1; - if (!opcode.equals("VOTE")) { - // Something went wrong in RSA. - throw new Exception("Unable to decode RSA"); - } - - // Parse the block. - String serviceName = readString(block, position); - position += serviceName.length() + 1; - String username = readString(block, position); - position += username.length() + 1; - String address = readString(block, position); - position += address.length() + 1; - String timeStamp = readString(block, position); - position += timeStamp.length() + 1; - - // Create the vote. - final Vote vote = new Vote(); - vote.setServiceName(serviceName); - vote.setUsername(username); - vote.setAddress(address); - vote.setTimeStamp(timeStamp); - - if (plugin.isDebug()) - LOG.info("Received vote record -> " + vote); - - // Dispatch the vote to all listeners. - for (VoteListener listener : Votifier.getInstance() - .getListeners()) { - try { - listener.voteMade(vote); - } catch (Exception ex) { - String vlName = listener.getClass().getSimpleName(); - LOG.log(Level.WARNING, - "Exception caught while sending the vote notification to the '" - + vlName + "' listener", ex); - } - } - - // Call event in a synchronized fashion to ensure that the - // custom event runs in the - // the main server thread, not this one. - plugin.getServer().getScheduler() - .scheduleSyncDelayedTask(plugin, new Runnable() { - public void run() { - Bukkit.getServer().getPluginManager() - .callEvent(new VotifierEvent(vote)); - } - }); - - // Clean up. - writer.close(); - in.close(); - socket.close(); - } catch (SocketException ex) { - LOG.log(Level.WARNING, "Protocol error. Ignoring packet - " - + ex.getLocalizedMessage()); - } catch (BadPaddingException ex) { - LOG.log(Level.WARNING, - "Unable to decrypt vote record. Make sure that that your public key"); - LOG.log(Level.WARNING, - "matches the one you gave the server list.", ex); - } catch (Exception ex) { - LOG.log(Level.WARNING, - "Exception caught while receiving a vote notification", - ex); - } - } - } - - /** - * Reads a string from a block of data. - * - * @param data - * The data to read from - * @return The string - */ - private String readString(byte[] data, int offset) { - StringBuilder builder = new StringBuilder(); - for (int i = offset; i < data.length; i++) { - if (data[i] == '\n') - break; // Delimiter reached. - builder.append((char) data[i]); - } - return builder.toString(); - } -} diff --git a/src/main/java/com/vexsoftware/votifier/net/VotifierSession.java b/src/main/java/com/vexsoftware/votifier/net/VotifierSession.java new file mode 100644 index 0000000..94eab11 --- /dev/null +++ b/src/main/java/com/vexsoftware/votifier/net/VotifierSession.java @@ -0,0 +1,39 @@ +package com.vexsoftware.votifier.net; + +import io.netty.util.AttributeKey; + +import java.math.BigInteger; +import java.security.SecureRandom; + +public class VotifierSession { + public static final AttributeKey KEY = AttributeKey.valueOf("votifier_session"); + + private static final SecureRandom RANDOM = new SecureRandom(); + private ProtocolVersion version = ProtocolVersion.UNKNOWN; + private final String challenge; + + public VotifierSession() { + challenge = new BigInteger(130, RANDOM).toString(32); + } + + public void setVersion(ProtocolVersion version) { + if (this.version != ProtocolVersion.UNKNOWN) + throw new IllegalStateException("Protocol version already switched"); + + this.version = version; + } + + public ProtocolVersion getVersion() { + return version; + } + + public String getChallenge() { + return challenge; + } + + public enum ProtocolVersion { + UNKNOWN, + ONE, + TWO + } +} diff --git a/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierGreetingHandler.java b/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierGreetingHandler.java new file mode 100644 index 0000000..80abe80 --- /dev/null +++ b/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierGreetingHandler.java @@ -0,0 +1,22 @@ +package com.vexsoftware.votifier.net.protocol; + +import com.vexsoftware.votifier.Votifier; +import com.vexsoftware.votifier.net.VotifierSession; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; + +import java.nio.charset.StandardCharsets; + +/** + * Handles the Votifier greeting. + */ +public class VotifierGreetingHandler extends ChannelInboundHandlerAdapter { + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + /* Send the version string and challenge. */ + VotifierSession session = ctx.channel().attr(VotifierSession.KEY).get(); + String version = "VOTIFIER " + Votifier.getInstance().getVersion() + " " + session.getChallenge() + "\n"; + ctx.writeAndFlush(Unpooled.copiedBuffer(version, StandardCharsets.UTF_8)); + } +} diff --git a/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocol1Decoder.java b/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocol1Decoder.java new file mode 100644 index 0000000..af64e13 --- /dev/null +++ b/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocol1Decoder.java @@ -0,0 +1,94 @@ +package com.vexsoftware.votifier.net.protocol; + +import com.vexsoftware.votifier.Votifier; +import com.vexsoftware.votifier.crypto.RSA; +import com.vexsoftware.votifier.model.Vote; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.CorruptedFrameException; + +import java.util.List; + +/** + * Decodes original protocol votes. + */ +public class VotifierProtocol1Decoder extends ByteToMessageDecoder { + private static final boolean WARNING = false; + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List list) throws Exception { + int readable = buf.readableBytes(); + + if (readable < 256) { + return; + } + + byte[] block = new byte[256]; + buf.getBytes(0, block); + + try { + block = RSA.decrypt(block, Votifier.getInstance().getKeyPair() + .getPrivate()); + } catch (Exception e) { + throw new CorruptedFrameException("Could not decrypt data", e); + } + int position = 0; + + // Perform the opcode check. + String opcode = readString(block, position); + position += opcode.length() + 1; + if (!opcode.equals("VOTE")) { + throw new CorruptedFrameException("VOTE opcode not found"); + } + + // Parse the block. + String serviceName = readString(block, position); + position += serviceName.length() + 1; + String username = readString(block, position); + position += username.length() + 1; + String address = readString(block, position); + position += address.length() + 1; + String timeStamp = readString(block, position); + position += timeStamp.length() + 1; + + // Create the vote. + final Vote vote = new Vote(); + vote.setServiceName(serviceName); + vote.setUsername(username); + vote.setAddress(address); + vote.setTimeStamp(timeStamp); + + /* Warning for using v1, when v2 is standardizing. */ + if (WARNING) { + Votifier.getInstance().getLogger().warning(serviceName + " has sent a protocol 1 message. This version is DEPRECATED and will be removed in a future Votifier release."); + } + + if (Votifier.getInstance().isDebug()) + Votifier.getInstance().getLogger().info("Received protocol v1 vote record -> " + vote); + + list.add(vote); + + // We are done, remove ourselves. Why? Sometimes, we will decode multiple vote messages. + // Netty doesn't like this, so we must remove ourselves from the pipeline. With Protocol 1, + // ending votes is a "fire and forget" operation, so this is safe. + ctx.pipeline().remove(this); + } + + /** + * Reads a string from a block of data. + * + * @param data + * The data to read from + * @return The string + */ + private static String readString(byte[] data, int offset) { + StringBuilder builder = new StringBuilder(); + for (int i = offset; i < data.length; i++) { + if (data[i] == '\n') + break; // Delimiter reached. + builder.append((char) data[i]); + } + return builder.toString(); + } +} diff --git a/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocol2VoteDecoder.java b/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocol2VoteDecoder.java new file mode 100644 index 0000000..6610143 --- /dev/null +++ b/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocol2VoteDecoder.java @@ -0,0 +1,62 @@ +package com.vexsoftware.votifier.net.protocol; + +import com.vexsoftware.votifier.Votifier; +import com.vexsoftware.votifier.model.Vote; +import com.vexsoftware.votifier.net.VotifierSession; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageDecoder; +import org.json.JSONObject; + +import javax.xml.bind.DatatypeConverter; +import java.nio.charset.StandardCharsets; +import java.security.PublicKey; +import java.security.Signature; +import java.util.List; + +/** + * Decodes protocol 2 JSON votes. + */ +public class VotifierProtocol2VoteDecoder extends MessageToMessageDecoder { + @Override + protected void decode(ChannelHandlerContext ctx, String s, List list) throws Exception { + JSONObject voteMessage = new JSONObject(s); + VotifierSession session = ctx.channel().attr(VotifierSession.KEY).get(); + + // Verify challenge. + if (!voteMessage.getString("challenge").equals(session.getChallenge())) { + throw new RuntimeException("Challenge is not valid"); + } + + // Verify this key belongs to the service. + JSONObject votePayload = new JSONObject(voteMessage.getString("payload")); + PublicKey key = Votifier.getInstance().getKeys().get(votePayload.getString("serviceName")); + + if (key == null) { + throw new RuntimeException("Unknown service '" + votePayload.getString("serviceName") + "'"); + } + + // Verify signature. + String sigHash = voteMessage.getString("signature"); + byte[] sigArray = DatatypeConverter.parseBase64Binary(sigHash); + Signature signature = Signature.getInstance("SHA256withRSA"); + signature.initVerify(key); + signature.update(voteMessage.getString("payload").getBytes(StandardCharsets.UTF_8)); + + if (!signature.verify(sigArray)) { + throw new RuntimeException("Signature is not valid"); + } + + Vote vote = new Vote(); + vote.setServiceName(votePayload.getString("serviceName")); + vote.setUsername(votePayload.getString("username")); + vote.setAddress(votePayload.getString("address")); + vote.setTimeStamp(votePayload.getString("timestamp")); + + if (Votifier.getInstance().isDebug()) + Votifier.getInstance().getLogger().info("Received protocol v2 vote record -> " + vote); + + list.add(vote); + + ctx.pipeline().remove(this); + } +} diff --git a/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocolDifferentiator.java b/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocolDifferentiator.java new file mode 100644 index 0000000..84e07c8 --- /dev/null +++ b/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocolDifferentiator.java @@ -0,0 +1,54 @@ +package com.vexsoftware.votifier.net.protocol; + +import com.vexsoftware.votifier.net.VotifierSession; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.CorruptedFrameException; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; +import io.netty.handler.codec.string.StringDecoder; +import io.netty.handler.codec.string.StringEncoder; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * Attempts to determine if original protocol or protocol v2 is being used. + */ +public class VotifierProtocolDifferentiator extends ByteToMessageDecoder { + private static final short PROTOCOL_2_MAGIC = 0x733A; + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List list) throws Exception { + // Determine the number of bytes that are available. + int readable = buf.readableBytes(); + buf.retain(); + + VotifierSession session = ctx.channel().attr(VotifierSession.KEY).get(); + + if (readable == 256) { + // 256 bytes = Protocol v1 Vote Message + ctx.pipeline().addAfter("protocolDifferentiator", "protocol1Handler", new VotifierProtocol1Decoder()); + session.setVersion(VotifierSession.ProtocolVersion.ONE); + ctx.pipeline().remove(this); + } else if (readable > 0) { + buf.readerIndex(0); + short readMagic = buf.readShort(); + + if (readMagic != PROTOCOL_2_MAGIC) { + throw new CorruptedFrameException("Expected Protocol v2 magic 0x733A (29498) but got " + readMagic); + } + + // Reset reader index again + buf.readerIndex(0); + + // Short 0x733A + Message = Protocol v2 Vote + ctx.pipeline().addAfter("protocolDifferentiator", "protocol2LengthDecoder", new LengthFieldBasedFrameDecoder(1024, 2, 2, 0, 4)); + ctx.pipeline().addAfter("protocol2LengthDecoder", "protocol2StringDecoder", new StringDecoder(StandardCharsets.UTF_8)); + ctx.pipeline().addAfter("protocol2StringDecoder", "protocol2VoteDecoder", new VotifierProtocol2VoteDecoder()); + ctx.pipeline().addAfter("protocol2VoteDecoder", "protocol2StringEncoder", new StringEncoder(StandardCharsets.UTF_8)); + session.setVersion(VotifierSession.ProtocolVersion.TWO); + ctx.pipeline().remove(this); + } + } +} From 809f2bc328c186c508d2ef32b18ff1c0084f8801 Mon Sep 17 00:00:00 2001 From: Tux Date: Mon, 6 Apr 2015 01:26:05 -0400 Subject: [PATCH 2/6] Need to relocate commons-io --- pom.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pom.xml b/pom.xml index 52237c7..184eaf1 100644 --- a/pom.xml +++ b/pom.xml @@ -87,6 +87,10 @@ org.json com.vexsoftware.votifier.json + + org.apache.commons.io + com.vexsoftware.votifier.commons.io + From 7408e37dee3e06175705e2281c1a592eb2f35791 Mon Sep 17 00:00:00 2001 From: Tux Date: Mon, 6 Apr 2015 01:41:35 -0400 Subject: [PATCH 3/6] Bump to 2.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 184eaf1..55b69f4 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.vexsoftware votifier - 1.9 + 2.0 Votifier UTF-8 From 86402face80b24c66489e7f07f36bdbb77849b38 Mon Sep 17 00:00:00 2001 From: Tux Date: Thu, 9 Apr 2015 16:32:12 -0400 Subject: [PATCH 4/6] Add future listener for server connection success. --- .../com/vexsoftware/votifier/Votifier.java | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/vexsoftware/votifier/Votifier.java b/src/main/java/com/vexsoftware/votifier/Votifier.java index 714ba75..15c32a3 100644 --- a/src/main/java/com/vexsoftware/votifier/Votifier.java +++ b/src/main/java/com/vexsoftware/votifier/Votifier.java @@ -31,6 +31,8 @@ import com.vexsoftware.votifier.net.protocol.VotifierProtocolDifferentiator; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelInitializer; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; @@ -210,7 +212,8 @@ public void onEnable() { LOG.info("DEBUG mode enabled!"); serverGroup = new NioEventLoopGroup(1); - serverChannel = new ServerBootstrap() + + new ServerBootstrap() .channel(NioServerSocketChannel.class) .group(serverGroup) .childHandler(new ChannelInitializer() { @@ -223,15 +226,24 @@ protected void initChannel(NioSocketChannel channel) throws Exception { } }) .bind(host, port) - .channel(); - - LOG.info("Votifier enabled."); + .addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + if (future.isSuccess()) { + serverChannel = future.channel(); + LOG.info("Votifier enabled."); + } else { + LOG.log(Level.SEVERE, "Votifier was not able to bind to " + future.channel().localAddress(), future.cause()); + } + } + }); } @Override public void onDisable() { // Shut down the network handlers. - serverChannel.close(); + if (serverChannel != null) + serverChannel.close(); serverGroup.shutdownGracefully(); LOG.info("Votifier disabled."); } From 96f119878586ead6a42981abb40ed46be88019ef Mon Sep 17 00:00:00 2001 From: Tux Date: Sat, 18 Apr 2015 12:25:36 -0400 Subject: [PATCH 5/6] Add token support to prevent malicious attacks that attempt to circumvent limitations. --- .../com/vexsoftware/votifier/TokenUtil.java | 16 ++++++++++++++++ .../com/vexsoftware/votifier/Votifier.java | 14 ++++++++++++++ .../votifier/net/VotifierSession.java | 7 ++----- .../VotifierProtocol2VoteDecoder.java | 19 ++++++++++++++----- 4 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/vexsoftware/votifier/TokenUtil.java diff --git a/src/main/java/com/vexsoftware/votifier/TokenUtil.java b/src/main/java/com/vexsoftware/votifier/TokenUtil.java new file mode 100644 index 0000000..2a36473 --- /dev/null +++ b/src/main/java/com/vexsoftware/votifier/TokenUtil.java @@ -0,0 +1,16 @@ +package com.vexsoftware.votifier; + +import java.math.BigInteger; +import java.security.SecureRandom; + +public class TokenUtil { + private TokenUtil() { + + } + + private static final SecureRandom RANDOM = new SecureRandom(); + + public static String newToken() { + return new BigInteger(130, RANDOM).toString(32); + } +} diff --git a/src/main/java/com/vexsoftware/votifier/Votifier.java b/src/main/java/com/vexsoftware/votifier/Votifier.java index 15c32a3..8262e23 100644 --- a/src/main/java/com/vexsoftware/votifier/Votifier.java +++ b/src/main/java/com/vexsoftware/votifier/Votifier.java @@ -81,6 +81,9 @@ public class Votifier extends JavaPlugin { /** Debug mode flag */ private boolean debug; + /** Token used for verifying votes. */ + private String token; + /** Keys used for websites. */ private Map keys = new HashMap<>(); @@ -146,6 +149,13 @@ public void onEnable() { LOG.info("a different port, which you need to specify in config.yml"); LOG.info("------------------------------------------------------------------------------"); + String token = TokenUtil.newToken(); + cfg.set("token", token); + LOG.info("Your Votifier token is " + token + "."); + LOG.info("You will need to provide this token when you submit your server to a voting"); + LOG.info("list."); + LOG.info("------------------------------------------------------------------------------"); + cfg.set("listener_folder", listenerDirectory); cfg.save(config); } catch (Exception ex) { @@ -208,6 +218,7 @@ public void onEnable() { String host = cfg.getString("host", hostAddr); int port = cfg.getInt("port", 8192); debug = cfg.getBoolean("debug", false); + token = cfg.getString("token"); if (debug) LOG.info("DEBUG mode enabled!"); @@ -296,4 +307,7 @@ public Map getKeys() { return keys; } + public String getToken() { + return token; + } } diff --git a/src/main/java/com/vexsoftware/votifier/net/VotifierSession.java b/src/main/java/com/vexsoftware/votifier/net/VotifierSession.java index 94eab11..82c01da 100644 --- a/src/main/java/com/vexsoftware/votifier/net/VotifierSession.java +++ b/src/main/java/com/vexsoftware/votifier/net/VotifierSession.java @@ -2,18 +2,15 @@ import io.netty.util.AttributeKey; -import java.math.BigInteger; -import java.security.SecureRandom; +import static com.vexsoftware.votifier.TokenUtil.newToken; public class VotifierSession { public static final AttributeKey KEY = AttributeKey.valueOf("votifier_session"); - - private static final SecureRandom RANDOM = new SecureRandom(); private ProtocolVersion version = ProtocolVersion.UNKNOWN; private final String challenge; public VotifierSession() { - challenge = new BigInteger(130, RANDOM).toString(32); + challenge = newToken(); } public void setVersion(ProtocolVersion version) { diff --git a/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocol2VoteDecoder.java b/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocol2VoteDecoder.java index 6610143..415ed8b 100644 --- a/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocol2VoteDecoder.java +++ b/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocol2VoteDecoder.java @@ -27,12 +27,18 @@ protected void decode(ChannelHandlerContext ctx, String s, List list) th throw new RuntimeException("Challenge is not valid"); } - // Verify this key belongs to the service. - JSONObject votePayload = new JSONObject(voteMessage.getString("payload")); - PublicKey key = Votifier.getInstance().getKeys().get(votePayload.getString("serviceName")); + // Verify that the service exists. + PublicKey key = Votifier.getInstance().getKeys().get(voteMessage.getString("serviceName")); if (key == null) { - throw new RuntimeException("Unknown service '" + votePayload.getString("serviceName") + "'"); + throw new RuntimeException("Unknown service '" + voteMessage.getString("serviceName") + "'"); + } + + // Verify that the service got a token from us. + String token = voteMessage.getString("token"); + + if (token == null || !token.equals(Votifier.getInstance().getToken())) { + throw new RuntimeException("Got invalid token " + token + ", wanted " + Votifier.getInstance().getToken()); } // Verify signature. @@ -46,8 +52,11 @@ protected void decode(ChannelHandlerContext ctx, String s, List list) th throw new RuntimeException("Signature is not valid"); } + // + JSONObject votePayload = new JSONObject(voteMessage.getString("payload")); + Vote vote = new Vote(); - vote.setServiceName(votePayload.getString("serviceName")); + vote.setServiceName(voteMessage.getString("serviceName")); vote.setUsername(votePayload.getString("username")); vote.setAddress(votePayload.getString("address")); vote.setTimeStamp(votePayload.getString("timestamp")); From 35adaacc5e39a7b114e29110bbeabf678bcbfe73 Mon Sep 17 00:00:00 2001 From: Tux Date: Mon, 20 Apr 2015 01:51:18 -0400 Subject: [PATCH 6/6] Convert to HMAC-based authentication. --- .../com/vexsoftware/votifier/Votifier.java | 47 ++++++------------- .../votifier/crypto/KeyCreator.java | 11 +++++ .../vexsoftware/votifier/crypto/RSAIO.java | 17 ------- .../VotifierProtocol2VoteDecoder.java | 41 ++++++++-------- .../VotifierProtocolDifferentiator.java | 29 +++++------- 5 files changed, 58 insertions(+), 87 deletions(-) create mode 100644 src/main/java/com/vexsoftware/votifier/crypto/KeyCreator.java diff --git a/src/main/java/com/vexsoftware/votifier/Votifier.java b/src/main/java/com/vexsoftware/votifier/Votifier.java index 8262e23..b60d277 100644 --- a/src/main/java/com/vexsoftware/votifier/Votifier.java +++ b/src/main/java/com/vexsoftware/votifier/Votifier.java @@ -19,12 +19,12 @@ package com.vexsoftware.votifier; import java.io.*; -import java.net.URL; +import java.security.Key; import java.security.KeyPair; -import java.security.PublicKey; import java.util.*; import java.util.logging.*; +import com.vexsoftware.votifier.crypto.KeyCreator; import com.vexsoftware.votifier.net.VoteInboundHandler; import com.vexsoftware.votifier.net.VotifierSession; import com.vexsoftware.votifier.net.protocol.VotifierGreetingHandler; @@ -81,11 +81,8 @@ public class Votifier extends JavaPlugin { /** Debug mode flag */ private boolean debug; - /** Token used for verifying votes. */ - private String token; - /** Keys used for websites. */ - private Map keys = new HashMap<>(); + private Map tokens = new HashMap<>(); /** * Attach custom log filter to logger. @@ -136,7 +133,6 @@ public void onEnable() { cfg.set("host", hostAddr); cfg.set("port", 8192); cfg.set("debug", false); - cfg.set("websites", Collections.emptyMap()); /* * Remind hosted server admins to be sure they have the right @@ -150,8 +146,9 @@ public void onEnable() { LOG.info("------------------------------------------------------------------------------"); String token = TokenUtil.newToken(); - cfg.set("token", token); - LOG.info("Your Votifier token is " + token + "."); + ConfigurationSection tokenSection = cfg.createSection("tokens"); + tokenSection.set("default", token); + LOG.info("Your default Votifier token is " + token + "."); LOG.info("You will need to provide this token when you submit your server to a voting"); LOG.info("list."); LOG.info("------------------------------------------------------------------------------"); @@ -183,7 +180,7 @@ public void onEnable() { } } catch (Exception ex) { LOG.log(Level.SEVERE, - "Error reading configuration file or RSA keys", ex); + "Error reading configuration file or RSA tokens", ex); gracefulExit(); return; } @@ -192,23 +189,14 @@ public void onEnable() { listenerDirectory = cfg.getString("listener_folder"); listeners.addAll(ListenerLoader.load(listenerDirectory)); - // Load Votifier keys. - ConfigurationSection websiteSection = cfg.getConfigurationSection("websites"); + // Load Votifier tokens. + ConfigurationSection tokenSection = cfg.getConfigurationSection("tokens"); - if (websiteSection != null) { - Map websites = websiteSection.getValues(false); + if (tokenSection != null) { + Map websites = tokenSection.getValues(false); for (Map.Entry website : websites.entrySet()) { - try { - keys.put(website.getKey(), RSAIO.loadPublicKey(new URL((String) website.getValue()))); - if (!(website.getKey().startsWith("https://") || website.getKey().startsWith("file://"))) { - LOG.warning("You are loading a public key (" + website.getKey() + ") over a non-SSL connection. This is insecure!"); - } - LOG.info("Loaded public key for website: " + website.getKey()); - } catch (Exception exception) { - LOG.log(Level.WARNING, - "Error loading public key for website: " + website.getKey(), - exception); - } + tokens.put(website.getKey(), KeyCreator.createKeyFrom(website.getValue().toString())); + LOG.info("Loaded token for website: " + website.getKey()); } } else { LOG.warning("No websites are listed in your configuration."); @@ -218,7 +206,6 @@ public void onEnable() { String host = cfg.getString("host", hostAddr); int port = cfg.getInt("port", 8192); debug = cfg.getBoolean("debug", false); - token = cfg.getString("token"); if (debug) LOG.info("DEBUG mode enabled!"); @@ -303,11 +290,7 @@ public boolean isDebug() { return debug; } - public Map getKeys() { - return keys; - } - - public String getToken() { - return token; + public Map getTokens() { + return tokens; } } diff --git a/src/main/java/com/vexsoftware/votifier/crypto/KeyCreator.java b/src/main/java/com/vexsoftware/votifier/crypto/KeyCreator.java new file mode 100644 index 0000000..9c2679d --- /dev/null +++ b/src/main/java/com/vexsoftware/votifier/crypto/KeyCreator.java @@ -0,0 +1,11 @@ +package com.vexsoftware.votifier.crypto; + +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.Key; + +public class KeyCreator { + public static Key createKeyFrom(String token) { + return new SecretKeySpec(token.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); + } +} diff --git a/src/main/java/com/vexsoftware/votifier/crypto/RSAIO.java b/src/main/java/com/vexsoftware/votifier/crypto/RSAIO.java index 373dab7..da86cee 100644 --- a/src/main/java/com/vexsoftware/votifier/crypto/RSAIO.java +++ b/src/main/java/com/vexsoftware/votifier/crypto/RSAIO.java @@ -104,21 +104,4 @@ public static KeyPair load(File directory) throws Exception { return new KeyPair(publicKey, privateKey); } - /** - * Loads an RSA public key from a URL. - * - * @param url - * The URL that has the public key - * @return - * The public key - * @throws Exception - * If an error occurs - */ - public static PublicKey loadPublicKey(URL url) throws Exception { - String publicKey = new String(IOUtils.toByteArray(url), "UTF-8").replaceAll("(-+BEGIN PUBLIC KEY-+\\r?\\n|-+END PUBLIC KEY-+\\r?\\n?)", ""); - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(DatatypeConverter.parseBase64Binary(publicKey)); - return keyFactory.generatePublic(publicKeySpec); - } - } diff --git a/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocol2VoteDecoder.java b/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocol2VoteDecoder.java index 415ed8b..941fa19 100644 --- a/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocol2VoteDecoder.java +++ b/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocol2VoteDecoder.java @@ -7,10 +7,10 @@ import io.netty.handler.codec.MessageToMessageDecoder; import org.json.JSONObject; +import javax.crypto.Mac; import javax.xml.bind.DatatypeConverter; import java.nio.charset.StandardCharsets; -import java.security.PublicKey; -import java.security.Signature; +import java.security.Key; import java.util.List; /** @@ -27,36 +27,33 @@ protected void decode(ChannelHandlerContext ctx, String s, List list) th throw new RuntimeException("Challenge is not valid"); } - // Verify that the service exists. - PublicKey key = Votifier.getInstance().getKeys().get(voteMessage.getString("serviceName")); - - if (key == null) { - throw new RuntimeException("Unknown service '" + voteMessage.getString("serviceName") + "'"); - } + // Deserialize the payload. + JSONObject votePayload = new JSONObject(voteMessage.getString("payload")); - // Verify that the service got a token from us. - String token = voteMessage.getString("token"); + // Verify that we have keys available. + Key key = Votifier.getInstance().getTokens().get(votePayload.getString("serviceName")); - if (token == null || !token.equals(Votifier.getInstance().getToken())) { - throw new RuntimeException("Got invalid token " + token + ", wanted " + Votifier.getInstance().getToken()); + if (key == null) { + key = Votifier.getInstance().getTokens().get("default"); + if (key == null) { + throw new RuntimeException("Unknown service '" + votePayload.getString("serviceName") + "'"); + } } // Verify signature. String sigHash = voteMessage.getString("signature"); - byte[] sigArray = DatatypeConverter.parseBase64Binary(sigHash); - Signature signature = Signature.getInstance("SHA256withRSA"); - signature.initVerify(key); - signature.update(voteMessage.getString("payload").getBytes(StandardCharsets.UTF_8)); + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(key); + mac.update(voteMessage.getString("payload").getBytes(StandardCharsets.UTF_8)); + String computed = DatatypeConverter.printBase64Binary(mac.doFinal()); - if (!signature.verify(sigArray)) { - throw new RuntimeException("Signature is not valid"); + if (!sigHash.equals(computed)) { + throw new RuntimeException("Signature is not valid (invalid token?)"); } - // - JSONObject votePayload = new JSONObject(voteMessage.getString("payload")); - + // Create the vote. Vote vote = new Vote(); - vote.setServiceName(voteMessage.getString("serviceName")); + vote.setServiceName(votePayload.getString("serviceName")); vote.setUsername(votePayload.getString("username")); vote.setAddress(votePayload.getString("address")); vote.setTimeStamp(votePayload.getString("timestamp")); diff --git a/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocolDifferentiator.java b/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocolDifferentiator.java index 84e07c8..30cb284 100644 --- a/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocolDifferentiator.java +++ b/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocolDifferentiator.java @@ -23,25 +23,15 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List list) // Determine the number of bytes that are available. int readable = buf.readableBytes(); buf.retain(); + buf.readerIndex(0); + short readMagic = buf.readShort(); - VotifierSession session = ctx.channel().attr(VotifierSession.KEY).get(); - - if (readable == 256) { - // 256 bytes = Protocol v1 Vote Message - ctx.pipeline().addAfter("protocolDifferentiator", "protocol1Handler", new VotifierProtocol1Decoder()); - session.setVersion(VotifierSession.ProtocolVersion.ONE); - ctx.pipeline().remove(this); - } else if (readable > 0) { - buf.readerIndex(0); - short readMagic = buf.readShort(); + // Reset reader index again + buf.readerIndex(0); - if (readMagic != PROTOCOL_2_MAGIC) { - throw new CorruptedFrameException("Expected Protocol v2 magic 0x733A (29498) but got " + readMagic); - } - - // Reset reader index again - buf.readerIndex(0); + VotifierSession session = ctx.channel().attr(VotifierSession.KEY).get(); + if (readMagic == PROTOCOL_2_MAGIC) { // Short 0x733A + Message = Protocol v2 Vote ctx.pipeline().addAfter("protocolDifferentiator", "protocol2LengthDecoder", new LengthFieldBasedFrameDecoder(1024, 2, 2, 0, 4)); ctx.pipeline().addAfter("protocol2LengthDecoder", "protocol2StringDecoder", new StringDecoder(StandardCharsets.UTF_8)); @@ -49,6 +39,13 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List list) ctx.pipeline().addAfter("protocol2VoteDecoder", "protocol2StringEncoder", new StringEncoder(StandardCharsets.UTF_8)); session.setVersion(VotifierSession.ProtocolVersion.TWO); ctx.pipeline().remove(this); + } else if (readable == 256) { + // 256 bytes = Protocol v1 Vote Message + ctx.pipeline().addAfter("protocolDifferentiator", "protocol1Handler", new VotifierProtocol1Decoder()); + session.setVersion(VotifierSession.ProtocolVersion.ONE); + ctx.pipeline().remove(this); + } else { + throw new CorruptedFrameException("Unrecognized protocol (missing 0x733A header or 256-byte v1 block)"); } } }