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