diff --git a/pom.xml b/pom.xml index a2ecf20..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 @@ -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,36 @@ 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.commons.io + com.vexsoftware.votifier.commons.io + + + + org.apache.maven.plugins 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 6e7fe54..b60d277 100644 --- a/src/main/java/com/vexsoftware/votifier/Votifier.java +++ b/src/main/java/com/vexsoftware/votifier/Votifier.java @@ -19,18 +19,32 @@ package com.vexsoftware.votifier; import java.io.*; +import java.security.Key; import java.security.KeyPair; -import java.util.ArrayList; -import java.util.List; +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; +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; +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 +69,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 +81,9 @@ public class Votifier extends JavaPlugin { /** Debug mode flag */ private boolean debug; + /** Keys used for websites. */ + private Map tokens = new HashMap<>(); + /** * Attach custom log filter to logger. */ @@ -125,6 +145,14 @@ public void onEnable() { LOG.info("a different port, which you need to specify in config.yml"); LOG.info("------------------------------------------------------------------------------"); + String token = TokenUtil.newToken(); + 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("------------------------------------------------------------------------------"); + cfg.set("listener_folder", listenerDirectory); cfg.save(config); } catch (Exception ex) { @@ -152,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; } @@ -161,6 +189,19 @@ public void onEnable() { listenerDirectory = cfg.getString("listener_folder"); listeners.addAll(ListenerLoader.load(listenerDirectory)); + // Load Votifier tokens. + ConfigurationSection tokenSection = cfg.getConfigurationSection("tokens"); + + if (tokenSection != null) { + Map websites = tokenSection.getValues(false); + for (Map.Entry website : websites.entrySet()) { + 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."); + } + // Initialize the receiver. String host = cfg.getString("host", hostAddr); int port = cfg.getInt("port", 8192); @@ -168,23 +209,40 @@ 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); + + 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) + .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() { - // Interrupt the vote receiver. - if (voteReceiver != null) { - voteReceiver.shutdown(); - } + // Shut down the network handlers. + if (serverChannel != null) + serverChannel.close(); + serverGroup.shutdownGracefully(); LOG.info("Votifier disabled."); } @@ -219,15 +277,6 @@ public List getListeners() { return listeners; } - /** - * Gets the vote receiver. - * - * @return The vote receiver - */ - public VoteReceiver getVoteReceiver() { - return voteReceiver; - } - /** * Gets the keyPair. * @@ -241,4 +290,7 @@ public boolean isDebug() { return debug; } + 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 a5727ee..da86cee 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"); 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..82c01da --- /dev/null +++ b/src/main/java/com/vexsoftware/votifier/net/VotifierSession.java @@ -0,0 +1,36 @@ +package com.vexsoftware.votifier.net; + +import io.netty.util.AttributeKey; + +import static com.vexsoftware.votifier.TokenUtil.newToken; + +public class VotifierSession { + public static final AttributeKey KEY = AttributeKey.valueOf("votifier_session"); + private ProtocolVersion version = ProtocolVersion.UNKNOWN; + private final String challenge; + + public VotifierSession() { + challenge = newToken(); + } + + 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..941fa19 --- /dev/null +++ b/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocol2VoteDecoder.java @@ -0,0 +1,68 @@ +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.crypto.Mac; +import javax.xml.bind.DatatypeConverter; +import java.nio.charset.StandardCharsets; +import java.security.Key; +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"); + } + + // Deserialize the payload. + JSONObject votePayload = new JSONObject(voteMessage.getString("payload")); + + // Verify that we have keys available. + Key key = Votifier.getInstance().getTokens().get(votePayload.getString("serviceName")); + + 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"); + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(key); + mac.update(voteMessage.getString("payload").getBytes(StandardCharsets.UTF_8)); + String computed = DatatypeConverter.printBase64Binary(mac.doFinal()); + + if (!sigHash.equals(computed)) { + throw new RuntimeException("Signature is not valid (invalid token?)"); + } + + // Create the vote. + 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..30cb284 --- /dev/null +++ b/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocolDifferentiator.java @@ -0,0 +1,51 @@ +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(); + buf.readerIndex(0); + short readMagic = buf.readShort(); + + // 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)); + 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); + } 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)"); + } + } +}