From 1db1d6b9b5dcc8a12a3aec70ff59058ca6485146 Mon Sep 17 00:00:00 2001 From: coelho Date: Mon, 31 Mar 2014 00:20:38 -0700 Subject: [PATCH 01/11] Cleanup to pave the way towards ProtocolV2 --- pom.xml | 2 +- .../com/vexsoftware/votifier/Votifier.java | 46 ++-- .../votifier/model/ListenerLoader.java | 46 ++-- .../com/vexsoftware/votifier/model/Vote.java | 32 ++- .../votifier/model/VoteListener.java | 2 - .../votifier/model/VotifierEvent.java | 5 +- .../model/listeners/BasicVoteListener.java | 2 +- .../vexsoftware/votifier/net/Protocol.java | 26 ++ .../votifier/net/VoteAcceptor.java | 113 +++++++++ .../votifier/net/VoteClientThread.java | 108 +++++++++ .../votifier/net/VoteReceiver.java | 222 ------------------ .../votifier/net/impl/ProtocolV1.java | 83 +++++++ .../votifier/{ => util}/LogFilter.java | 8 +- .../votifier/{crypto => util/rsa}/RSA.java | 36 ++- .../votifier/{crypto => util/rsa}/RSAIO.java | 94 +++++--- .../{crypto => util/rsa}/RSAKeygen.java | 7 +- 16 files changed, 488 insertions(+), 344 deletions(-) create mode 100644 src/main/java/com/vexsoftware/votifier/net/Protocol.java create mode 100644 src/main/java/com/vexsoftware/votifier/net/VoteAcceptor.java create mode 100644 src/main/java/com/vexsoftware/votifier/net/VoteClientThread.java delete mode 100644 src/main/java/com/vexsoftware/votifier/net/VoteReceiver.java create mode 100644 src/main/java/com/vexsoftware/votifier/net/impl/ProtocolV1.java rename src/main/java/com/vexsoftware/votifier/{ => util}/LogFilter.java (79%) rename src/main/java/com/vexsoftware/votifier/{crypto => util/rsa}/RSA.java (63%) rename src/main/java/com/vexsoftware/votifier/{crypto => util/rsa}/RSAIO.java (55%) rename src/main/java/com/vexsoftware/votifier/{crypto => util/rsa}/RSAKeygen.java (93%) diff --git a/pom.xml b/pom.xml index a2ecf20..fc0fb45 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.bukkit - bukkit + craftbukkit 1.7.2-R0.1 jar compile diff --git a/src/main/java/com/vexsoftware/votifier/Votifier.java b/src/main/java/com/vexsoftware/votifier/Votifier.java index 6e7fe54..d4f3feb 100644 --- a/src/main/java/com/vexsoftware/votifier/Votifier.java +++ b/src/main/java/com/vexsoftware/votifier/Votifier.java @@ -18,25 +18,26 @@ package com.vexsoftware.votifier; -import java.io.*; +import java.io.File; import java.security.KeyPair; import java.util.ArrayList; import java.util.List; -import java.util.logging.*; +import java.util.logging.Level; +import java.util.logging.Logger; + import org.bukkit.Bukkit; 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; +import com.vexsoftware.votifier.net.VoteAcceptor; +import com.vexsoftware.votifier.util.LogFilter; +import com.vexsoftware.votifier.util.rsa.RSAIO; +import com.vexsoftware.votifier.util.rsa.RSAKeygen; /** * The main Votifier plugin class. - * - * @author Blake Beaupain - * @author Kramer Campbell */ public class Votifier extends JavaPlugin { @@ -46,9 +47,6 @@ public class Votifier extends JavaPlugin { /** Log entry prefix */ private static final String logPrefix = "[Votifier] "; - /** The Votifier instance. */ - private static Votifier instance; - /** The current Votifier version. */ private String version; @@ -56,7 +54,7 @@ public class Votifier extends JavaPlugin { private final List listeners = new ArrayList(); /** The vote receiver. */ - private VoteReceiver voteReceiver; + private VoteAcceptor voteReceiver; /** The RSA key pair. */ private KeyPair keyPair; @@ -73,8 +71,6 @@ public class Votifier extends JavaPlugin { @Override public void onEnable() { - Votifier.instance = this; - // Set the plugin version. version = getDescription().getVersion(); @@ -86,8 +82,7 @@ public void onEnable() { YamlConfiguration cfg = YamlConfiguration.loadConfiguration(config); File rsaDirectory = new File(getDataFolder() + "/rsa"); // Replace to remove a bug with Windows paths - SmilingDevil - String listenerDirectory = getDataFolder().toString() - .replace("\\", "/") + "/listeners"; + String listenerDirectory = getDataFolder().toString().replace("\\", "/") + "/listeners"; /* * Use IP address from server.properties as a default for @@ -96,8 +91,9 @@ public void onEnable() { * assigned to the server. */ String hostAddr = Bukkit.getServer().getIp(); - if (hostAddr == null || hostAddr.length() == 0) + if (hostAddr == null || hostAddr.length() == 0) { hostAddr = "0.0.0.0"; + } /* * Create configuration file if it does not exists; otherwise, load it @@ -165,11 +161,12 @@ public void onEnable() { String host = cfg.getString("host", hostAddr); int port = cfg.getInt("port", 8192); debug = cfg.getBoolean("debug", false); - if (debug) + if (debug) { LOG.info("DEBUG mode enabled!"); + } try { - voteReceiver = new VoteReceiver(this, host, port); + voteReceiver = new VoteAcceptor(this, host, port); voteReceiver.start(); LOG.info("Votifier enabled."); @@ -192,15 +189,6 @@ private void gracefulExit() { LOG.log(Level.SEVERE, "Votifier did not initialize properly!"); } - /** - * Gets the instance. - * - * @return The instance - */ - public static Votifier getInstance() { - return instance; - } - /** * Gets the version. * @@ -224,7 +212,7 @@ public List getListeners() { * * @return The vote receiver */ - public VoteReceiver getVoteReceiver() { + public VoteAcceptor getVoteReceiver() { return voteReceiver; } diff --git a/src/main/java/com/vexsoftware/votifier/model/ListenerLoader.java b/src/main/java/com/vexsoftware/votifier/model/ListenerLoader.java index ceba7c2..e57300d 100644 --- a/src/main/java/com/vexsoftware/votifier/model/ListenerLoader.java +++ b/src/main/java/com/vexsoftware/votifier/model/ListenerLoader.java @@ -1,15 +1,16 @@ package com.vexsoftware.votifier.model; import java.io.File; -import java.net.*; -import java.util.*; -import java.util.logging.*; -import com.vexsoftware.votifier.Votifier; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Loads vote listeners. Listeners that cannot be instantiated will be skipped. - * - * @author Blake Beaupain */ public class ListenerLoader { @@ -22,35 +23,33 @@ public class ListenerLoader { * @param directory * The directory */ - public static List load(String directory) /* throws Exception */{ + @SuppressWarnings("resource") + public static List load(String directory) { List listeners = new ArrayList(); File dir = new File(directory); // Verify configured vote listener directory exists if (!dir.exists()) { - LOG.log(Level.WARNING, - "No listeners loaded! Cannot find listener directory '" - + dir + "' "); + LOG.warning("No listeners loaded! Cannot find listener directory '" + dir + "' "); return listeners; } // Load the vote listener instances. ClassLoader loader; try { - loader = new URLClassLoader(new URL[] { dir.toURI().toURL() }, - VoteListener.class.getClassLoader()); - } catch (MalformedURLException ex) { + loader = new URLClassLoader(new URL[] { dir.toURI().toURL() }, VoteListener.class.getClassLoader()); + } catch (MalformedURLException exception) { LOG.log(Level.SEVERE, - "Error while configuring listener class loader", ex); + "Error while configuring listener class loader", + exception); return listeners; } + for (File file : dir.listFiles()) { if (!file.getName().endsWith(".class")) { continue; // Only load class files! } - String name = file.getName().substring(0, - file.getName().lastIndexOf(".")); - + String name = file.getName().substring(0, file.getName().lastIndexOf(".")); try { Class clazz = loader.loadClass(name); Object object = clazz.newInstance(); @@ -60,19 +59,16 @@ public static List load(String directory) /* throws Exception */{ } VoteListener listener = (VoteListener) object; listeners.add(listener); - LOG.info("Loaded vote listener: " - + listener.getClass().getSimpleName()); + LOG.info("Loaded vote listener: " + listener.getClass().getSimpleName()); } /* * Catch the usual definition and dependency problems with a loader * and skip the problem listener. */ - catch (Exception ex) { - LOG.log(Level.WARNING, "Error loading '" + name - + "' listener! Listener disabled."); - } catch (Error ex) { - LOG.log(Level.WARNING, "Error loading '" + name - + "' listener! Listener disabled."); + catch (Exception exception) { + LOG.warning("Error loading '" + name + "' listener! Listener disabled."); + } catch (Error exception) { + LOG.warning("Error loading '" + name + "' listener! Listener disabled."); } } return listeners; diff --git a/src/main/java/com/vexsoftware/votifier/model/Vote.java b/src/main/java/com/vexsoftware/votifier/model/Vote.java index c8ef4b0..748b4f6 100644 --- a/src/main/java/com/vexsoftware/votifier/model/Vote.java +++ b/src/main/java/com/vexsoftware/votifier/model/Vote.java @@ -20,8 +20,6 @@ /** * A model for a vote. - * - * @author Blake Beaupain */ public class Vote { @@ -34,21 +32,28 @@ public class Vote { /** The address of the voter. */ private String address; - /** The date and time of the vote. */ - private String timeStamp; + /** The unix timeStamp of the vote. */ + private long timeStamp; - @Override - public String toString() { - return "Vote (from:" + serviceName + " username:" + username - + " address:" + address + " timeStamp:" + timeStamp + ")"; + @Deprecated + public Vote() { + } - + + public Vote(String serviceName, String username, String address, long timeStamp) { + this.serviceName = serviceName; + this.username = username; + this.address = address; + this.timeStamp = timeStamp; + } + /** * Sets the serviceName. * * @param serviceName * The new serviceName */ + @Deprecated public void setServiceName(String serviceName) { this.serviceName = serviceName; } @@ -68,6 +73,7 @@ public String getServiceName() { * @param username * The new username */ + @Deprecated public void setUsername(String username) { this.username = username.length() <= 16 ? username : username.substring(0, 16); } @@ -87,6 +93,7 @@ public String getUsername() { * @param address * The new address */ + @Deprecated public void setAddress(String address) { this.address = address; } @@ -106,17 +113,18 @@ public String getAddress() { * @param timeStamp * The new time stamp */ + @Deprecated public void setTimeStamp(String timeStamp) { - this.timeStamp = timeStamp; + this.timeStamp = Long.parseLong(timeStamp); } - + /** * Gets the time stamp. * * @return The time stamp */ public String getTimeStamp() { - return timeStamp; + return Long.toString(timeStamp); } } diff --git a/src/main/java/com/vexsoftware/votifier/model/VoteListener.java b/src/main/java/com/vexsoftware/votifier/model/VoteListener.java index b6c8da4..a54fb25 100644 --- a/src/main/java/com/vexsoftware/votifier/model/VoteListener.java +++ b/src/main/java/com/vexsoftware/votifier/model/VoteListener.java @@ -20,8 +20,6 @@ /** * A listener for votes. - * - * @author Blake Beaupain */ public interface VoteListener { diff --git a/src/main/java/com/vexsoftware/votifier/model/VotifierEvent.java b/src/main/java/com/vexsoftware/votifier/model/VotifierEvent.java index eaa4f95..57c4d80 100644 --- a/src/main/java/com/vexsoftware/votifier/model/VotifierEvent.java +++ b/src/main/java/com/vexsoftware/votifier/model/VotifierEvent.java @@ -6,11 +6,9 @@ * {@code VotifierEvent} is a custom Bukkit event class that is sent * synchronously to CraftBukkit's main thread allowing other plugins to listener * for votes. - * - * @author frelling - * */ public class VotifierEvent extends Event { + /** * Event listener handler list. */ @@ -48,4 +46,5 @@ public HandlerList getHandlers() { public static HandlerList getHandlerList() { return handlers; } + } diff --git a/src/main/java/com/vexsoftware/votifier/model/listeners/BasicVoteListener.java b/src/main/java/com/vexsoftware/votifier/model/listeners/BasicVoteListener.java index e81cffb..8943581 100644 --- a/src/main/java/com/vexsoftware/votifier/model/listeners/BasicVoteListener.java +++ b/src/main/java/com/vexsoftware/votifier/model/listeners/BasicVoteListener.java @@ -31,7 +31,7 @@ public class BasicVoteListener implements VoteListener { /** The logger instance. */ - private Logger log = Logger.getLogger("BasicVoteListener"); + private static final Logger log = Logger.getLogger("BasicVoteListener"); public void voteMade(Vote vote) { log.info("Received: " + vote); diff --git a/src/main/java/com/vexsoftware/votifier/net/Protocol.java b/src/main/java/com/vexsoftware/votifier/net/Protocol.java new file mode 100644 index 0000000..6d97ec1 --- /dev/null +++ b/src/main/java/com/vexsoftware/votifier/net/Protocol.java @@ -0,0 +1,26 @@ +package com.vexsoftware.votifier.net; + +import java.io.InputStream; +import java.io.OutputStream; + +import com.vexsoftware.votifier.Votifier; +import com.vexsoftware.votifier.model.Vote; + +/** + * An interface to implement the Votifier protocol. + */ +public interface Protocol { + + /** + * Handle the protocol + * + * @param plugin + * Votifier plugin + * @param socket + * The receiving connection + * @throws Exception + * If an error occurs + */ + public Vote handleProtocol(Votifier plugin, InputStream in, OutputStream out) throws Exception; + +} diff --git a/src/main/java/com/vexsoftware/votifier/net/VoteAcceptor.java b/src/main/java/com/vexsoftware/votifier/net/VoteAcceptor.java new file mode 100644 index 0000000..d8b1fdf --- /dev/null +++ b/src/main/java/com/vexsoftware/votifier/net/VoteAcceptor.java @@ -0,0 +1,113 @@ +/* + * 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.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.util.logging.*; + +import com.vexsoftware.votifier.Votifier; + +/** + * The vote receiving acceptor. + */ +public class VoteAcceptor extends Thread { + + /** The logger instance. */ + private static final Logger LOG = Logger.getLogger("Votifier"); + + /** The plugin. */ + 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 VoteAcceptor(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 exception) { + LOG.severe("Error initializing vote receiver. Please verify that the configured"); + LOG.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.", + exception); + throw exception; + } + } + + /** + * Shuts the vote receiver down cleanly. + */ + public void shutdown() { + running = false; + if (server == null) { + return; + } + try { + server.close(); + } catch (Exception exception) { + LOG.warning("Unable to shut down vote receiver cleanly."); + } + } + + public void run() { + while (running) { + try { + Socket socket = server.accept(); + socket.setSoTimeout(5000); // Don't hang on slow connections. + new Thread(new VoteClientThread(plugin, socket)).start(); + } catch (SocketException exception) { + LOG.warning("Protocol error. Ignoring packet - " + exception.getLocalizedMessage()); + } catch (Exception exception) { + LOG.log(Level.WARNING, + "Exception caught while receiving a vote notification", + exception); + } + } + } + +} diff --git a/src/main/java/com/vexsoftware/votifier/net/VoteClientThread.java b/src/main/java/com/vexsoftware/votifier/net/VoteClientThread.java new file mode 100644 index 0000000..b7c8c98 --- /dev/null +++ b/src/main/java/com/vexsoftware/votifier/net/VoteClientThread.java @@ -0,0 +1,108 @@ +package com.vexsoftware.votifier.net; + +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.crypto.BadPaddingException; + +import com.vexsoftware.votifier.Votifier; +import com.vexsoftware.votifier.model.Vote; +import com.vexsoftware.votifier.model.VoteListener; +import com.vexsoftware.votifier.model.VotifierEvent; +import com.vexsoftware.votifier.net.impl.ProtocolV1; + +/** + * The vote client thread. + */ +public class VoteClientThread implements Runnable { + + /** The logger instance. */ + private static final Logger LOG = Logger.getLogger("Votifier"); + + /** The plugin. */ + private Votifier plugin; + + /** The socket. */ + private Socket socket; + + public VoteClientThread(Votifier plugin, Socket socket) { + this.plugin = plugin; + this.socket = socket; + } + + public void run() { + InputStream inputStream = null; + OutputStream outputStream = null; + try { + inputStream = socket.getInputStream(); + outputStream = socket.getOutputStream(); + + // Send them our version. + outputStream.write(("VOTIFIER " + plugin.getVersion() + "\r\n").getBytes("UTF-8")); + outputStream.flush(); + + // Read the vote with ProtocolV1 + final Vote vote = ProtocolV1.INSTANCE.handleProtocol(plugin, inputStream, outputStream); + + if (plugin.isDebug()) { + LOG.info("Received vote record -> " + vote); + } + + // Dispatch the vote to all listeners. + for (VoteListener listener : plugin.getListeners()) { + try { + listener.voteMade(vote); + } catch (Exception exception) { + String vlName = listener.getClass().getSimpleName(); + LOG.log(Level.WARNING, + "Exception caught while sending the vote notification to the '" + vlName + "' listener", + exception); + } + } + + // 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().runTask(plugin, new Runnable() { + public void run() { + plugin.getServer().getPluginManager().callEvent(new VotifierEvent(vote)); + } + }); + } catch (BadPaddingException exception) { + LOG.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.", + exception); + } catch(Exception exception) { + LOG.log(Level.WARNING, + "Exception caught while receiving a vote notification", + exception); + } finally { + if (outputStream != null) { + try { + inputStream.close(); + } catch(Exception exception) { + // ignore + } + } + if (outputStream != null) { + try { + outputStream.close(); + } catch(Exception exception) { + // ignore + } + } + if (socket != null) { + try { + socket.close(); + } catch(Exception exception) { + // ignore + } + } + } + } + +} 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/impl/ProtocolV1.java b/src/main/java/com/vexsoftware/votifier/net/impl/ProtocolV1.java new file mode 100644 index 0000000..bfb2a33 --- /dev/null +++ b/src/main/java/com/vexsoftware/votifier/net/impl/ProtocolV1.java @@ -0,0 +1,83 @@ +package com.vexsoftware.votifier.net.impl; + +import java.io.InputStream; +import java.io.OutputStream; + +import com.vexsoftware.votifier.Votifier; +import com.vexsoftware.votifier.model.Vote; +import com.vexsoftware.votifier.net.Protocol; +import com.vexsoftware.votifier.util.rsa.RSA; + +/** + * Version 1 of the Votifier protocol. + */ +public class ProtocolV1 implements Protocol { + + /** + * Singleton + */ + public static final ProtocolV1 INSTANCE = new ProtocolV1(); + + private ProtocolV1() { + // private + } + + /** + * Handle the protocol + * + * @param plugin + * Votifier plugin + * @param socket + * The receiving connection + * @throws Exception + * If an error occurs + */ + public Vote handleProtocol(Votifier plugin, InputStream in, OutputStream out) throws Exception { + // Read the 256 byte block. + byte[] block = new byte[256]; + in.read(block, 0, block.length); + + // Decrypt the block. + block = RSA.decrypt(block, plugin.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; + + return new Vote(serviceName, username, address, Long.parseLong(timeStamp)); + } + + /** + * 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; + } + builder.append((char) data[i]); + } + return builder.toString(); + } + +} diff --git a/src/main/java/com/vexsoftware/votifier/LogFilter.java b/src/main/java/com/vexsoftware/votifier/util/LogFilter.java similarity index 79% rename from src/main/java/com/vexsoftware/votifier/LogFilter.java rename to src/main/java/com/vexsoftware/votifier/util/LogFilter.java index efda930..4175739 100644 --- a/src/main/java/com/vexsoftware/votifier/LogFilter.java +++ b/src/main/java/com/vexsoftware/votifier/util/LogFilter.java @@ -1,14 +1,11 @@ -package com.vexsoftware.votifier; +package com.vexsoftware.votifier.util; import java.util.logging.*; /** * A custom log filter for prepending plugin identifier on all log messages. - * - * @author frelling - * */ -class LogFilter implements Filter { +public class LogFilter implements Filter { private String prefix; @@ -29,4 +26,5 @@ public boolean isLoggable(LogRecord record) { record.setMessage(prefix + record.getMessage()); return true; } + } diff --git a/src/main/java/com/vexsoftware/votifier/crypto/RSA.java b/src/main/java/com/vexsoftware/votifier/util/rsa/RSA.java similarity index 63% rename from src/main/java/com/vexsoftware/votifier/crypto/RSA.java rename to src/main/java/com/vexsoftware/votifier/util/rsa/RSA.java index 41df187..4bba904 100644 --- a/src/main/java/com/vexsoftware/votifier/crypto/RSA.java +++ b/src/main/java/com/vexsoftware/votifier/util/rsa/RSA.java @@ -16,18 +16,17 @@ * along with Votifier. If not, see . */ -package com.vexsoftware.votifier.crypto; +package com.vexsoftware.votifier.util.rsa; import java.security.PrivateKey; import java.security.PublicKey; +import java.security.Signature; import javax.crypto.Cipher; /** * Static RSA utility methods for encrypting and decrypting blocks of * information. - * - * @author Blake Beaupain */ public class RSA { @@ -38,9 +37,10 @@ public class RSA { * The data to encrypt * @param key * The key to encrypt with - * @return The encrypted data + * @return + * The encrypted data * @throws Exception - * If an error occurs + * If an error occurs */ public static byte[] encrypt(byte[] data, PublicKey key) throws Exception { Cipher cipher = Cipher.getInstance("RSA"); @@ -55,14 +55,36 @@ public static byte[] encrypt(byte[] data, PublicKey key) throws Exception { * The data to decrypt * @param key * The key to decrypt with - * @return The decrypted data + * @return + * The decrypted data * @throws Exception - * If an error occurs + * If an error occurs */ public static byte[] decrypt(byte[] data, PrivateKey key) throws Exception { Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.DECRYPT_MODE, key); return cipher.doFinal(data); } + + /** + * Verify if the signature against the public key matches the data + * + * @param data + * The data to compare against + * @param signatureData + * The signature of the data + * @param publicKey + * The keypair's public key used to generate the signature + * @return + * If the signature against the public key matches the data + * @throws Exception + * If an error occurs + */ + public static boolean verify(byte[] data, byte[] signatureData, PublicKey publicKey) throws Exception { + Signature signature = Signature.getInstance("SHA256withRSA"); + signature.initVerify(publicKey); + signature.update(data); + return signature.verify(signatureData); + } } diff --git a/src/main/java/com/vexsoftware/votifier/crypto/RSAIO.java b/src/main/java/com/vexsoftware/votifier/util/rsa/RSAIO.java similarity index 55% rename from src/main/java/com/vexsoftware/votifier/crypto/RSAIO.java rename to src/main/java/com/vexsoftware/votifier/util/rsa/RSAIO.java index a5727ee..d33f1bb 100644 --- a/src/main/java/com/vexsoftware/votifier/crypto/RSAIO.java +++ b/src/main/java/com/vexsoftware/votifier/util/rsa/RSAIO.java @@ -16,7 +16,7 @@ * along with Votifier. If not, see . */ -package com.vexsoftware.votifier.crypto; +package com.vexsoftware.votifier.util.rsa; import java.io.File; import java.io.FileInputStream; @@ -32,8 +32,6 @@ /** * Static utility methods for saving and loading RSA key pairs. - * - * @author Blake Beaupain */ public class RSAIO { @@ -45,27 +43,42 @@ public class RSAIO { * @param keyPair * The key pair to save * @throws Exception - * If an error occurs + * If an error occurs */ public static void save(File directory, KeyPair keyPair) throws Exception { PrivateKey privateKey = keyPair.getPrivate(); PublicKey publicKey = keyPair.getPublic(); // Store the public key. - X509EncodedKeySpec publicSpec = new X509EncodedKeySpec( - publicKey.getEncoded()); - FileOutputStream out = new FileOutputStream(directory + "/public.key"); - out.write(DatatypeConverter.printBase64Binary(publicSpec.getEncoded()) - .getBytes()); - out.close(); + X509EncodedKeySpec publicSpec = new X509EncodedKeySpec(publicKey.getEncoded()); + FileOutputStream out = null; + try { + out = new FileOutputStream(directory + "/public.key"); + out.write(DatatypeConverter.printBase64Binary(publicSpec.getEncoded()).getBytes()); + } catch(Exception exception) { + throw exception; + } finally { + try { + out.close(); + } catch(Exception exception) { + // ignore + } + } // Store the private key. - PKCS8EncodedKeySpec privateSpec = new PKCS8EncodedKeySpec( - privateKey.getEncoded()); - out = new FileOutputStream(directory + "/private.key"); - out.write(DatatypeConverter.printBase64Binary(privateSpec.getEncoded()) - .getBytes()); - out.close(); + PKCS8EncodedKeySpec privateSpec = new PKCS8EncodedKeySpec(privateKey.getEncoded()); + try { + out = new FileOutputStream(directory + "/private.key"); + out.write(DatatypeConverter.printBase64Binary(privateSpec.getEncoded()).getBytes()); + } catch(Exception exception) { + throw exception; + } finally { + try { + out.close(); + } catch(Exception exception) { + // ignore + } + } } /** @@ -81,29 +94,46 @@ 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); - encodedPublicKey = DatatypeConverter.parseBase64Binary(new String( - encodedPublicKey)); - in.close(); + FileInputStream in = null; + byte[] encodedPublicKey; + try { + in = new FileInputStream(directory + "/public.key"); + encodedPublicKey = new byte[(int) publicKeyFile.length()]; + in.read(encodedPublicKey); + encodedPublicKey = DatatypeConverter.parseBase64Binary(new String(encodedPublicKey)); + } catch(Exception exception) { + throw exception; + } finally { + try { + in.close(); + } catch(Exception exception) { + // ignore + } + } // 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); - encodedPrivateKey = DatatypeConverter.parseBase64Binary(new String( - encodedPrivateKey)); - in.close(); + byte[] encodedPrivateKey; + try { + in = new FileInputStream(directory + "/private.key"); + encodedPrivateKey = new byte[(int) privateKeyFile.length()]; + in.read(encodedPrivateKey); + encodedPrivateKey = DatatypeConverter.parseBase64Binary(new String(encodedPrivateKey)); + } catch(Exception exception) { + throw exception; + } finally { + try { + in.close(); + } catch(Exception exception) { + // ignore + } + } // Instantiate and return the key pair. KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec( - encodedPublicKey); + X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(encodedPublicKey); PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); - PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec( - encodedPrivateKey); + PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(encodedPrivateKey); PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec); return new KeyPair(publicKey, privateKey); } diff --git a/src/main/java/com/vexsoftware/votifier/crypto/RSAKeygen.java b/src/main/java/com/vexsoftware/votifier/util/rsa/RSAKeygen.java similarity index 93% rename from src/main/java/com/vexsoftware/votifier/crypto/RSAKeygen.java rename to src/main/java/com/vexsoftware/votifier/util/rsa/RSAKeygen.java index b8d4dd3..114cbfd 100644 --- a/src/main/java/com/vexsoftware/votifier/crypto/RSAKeygen.java +++ b/src/main/java/com/vexsoftware/votifier/util/rsa/RSAKeygen.java @@ -16,7 +16,7 @@ * along with Votifier. If not, see . */ -package com.vexsoftware.votifier.crypto; +package com.vexsoftware.votifier.util.rsa; import java.security.KeyPair; import java.security.KeyPairGenerator; @@ -25,8 +25,6 @@ /** * An RSA key pair generator. - * - * @author Blake Beaupain */ public class RSAKeygen { @@ -43,8 +41,7 @@ public class RSAKeygen { public static KeyPair generate(int bits) throws Exception { LOG.info("Votifier is generating an RSA key pair..."); KeyPairGenerator keygen = KeyPairGenerator.getInstance("RSA"); - RSAKeyGenParameterSpec spec = new RSAKeyGenParameterSpec(bits, - RSAKeyGenParameterSpec.F4); + RSAKeyGenParameterSpec spec = new RSAKeyGenParameterSpec(bits, RSAKeyGenParameterSpec.F4); keygen.initialize(spec); return keygen.generateKeyPair(); } From 10802583d440f0eabeb0d8b025afa017f044f9f7 Mon Sep 17 00:00:00 2001 From: coelho Date: Mon, 31 Mar 2014 02:01:56 -0700 Subject: [PATCH 02/11] Add ProtocolV2 --- .../com/vexsoftware/votifier/Votifier.java | 173 ++++++++++-------- .../votifier/model/ListenerLoader.java | 3 +- .../votifier/net/VoteClientThread.java | 27 ++- .../votifier/net/{impl => v1}/ProtocolV1.java | 4 +- .../votifier/net/v2/JsonMessage.java | 47 +++++ .../votifier/net/v2/JsonMessagePayload.java | 60 ++++++ .../votifier/net/v2/ProtocolV2.java | 93 ++++++++++ .../vexsoftware/votifier/util/rsa/RSAIO.java | 20 ++ src/main/resources/config.yml | 7 + 9 files changed, 352 insertions(+), 82 deletions(-) rename src/main/java/com/vexsoftware/votifier/net/{impl => v1}/ProtocolV1.java (97%) create mode 100644 src/main/java/com/vexsoftware/votifier/net/v2/JsonMessage.java create mode 100644 src/main/java/com/vexsoftware/votifier/net/v2/JsonMessagePayload.java create mode 100644 src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java create mode 100644 src/main/resources/config.yml diff --git a/src/main/java/com/vexsoftware/votifier/Votifier.java b/src/main/java/com/vexsoftware/votifier/Votifier.java index d4f3feb..c0c73b2 100644 --- a/src/main/java/com/vexsoftware/votifier/Votifier.java +++ b/src/main/java/com/vexsoftware/votifier/Votifier.java @@ -19,14 +19,19 @@ package com.vexsoftware.votifier; import java.io.File; +import java.net.URL; import java.security.KeyPair; +import java.security.PublicKey; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import java.util.logging.Level; import java.util.logging.Logger; import org.bukkit.Bukkit; -import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.plugin.java.JavaPlugin; import com.vexsoftware.votifier.model.ListenerLoader; @@ -51,16 +56,22 @@ public class Votifier extends JavaPlugin { private String version; /** The vote listeners. */ - private final List listeners = new ArrayList(); + private List listeners = new ArrayList(); + + /** The websites mapped to their public key. */ + private Map websites = new HashMap(); /** The vote receiver. */ - private VoteAcceptor voteReceiver; + private VoteAcceptor voteAcceptor; /** The RSA key pair. */ private KeyPair keyPair; /** Debug mode flag */ private boolean debug; + + /** Old protocol flag */ + private boolean oldProtocol; /** * Attach custom log filter to logger. @@ -68,6 +79,44 @@ public class Votifier extends JavaPlugin { static { LOG.setFilter(new LogFilter(logPrefix)); } + + @Override + public void onLoad() { + FileConfiguration config = super.getConfig(); + + if (!new File(getDataFolder() + "/config.yml").exists()) { + LOG.info("Configuring Votifier for the first time..."); + LOG.info("------------------------------------------------------------------------------"); + LOG.info("Assigning Votifier to listen on port 8192. If you are hosting Craftbukkit on a"); + LOG.info("shared server please check with your hosting provider to verify that this port"); + LOG.info("is available for your use. Chances are that your hosting provider will assign"); + LOG.info("a different port, which you need to specify in config.yml"); + LOG.info("------------------------------------------------------------------------------"); + + /* + * Use IP address from server.properties as a default for + * configurations. Do not use InetAddress.getLocalHost() as it most + * likely will return the main server address instead of the address + * assigned to the server. + */ + String hostAddr = Bukkit.getServer().getIp(); + if (hostAddr == null || hostAddr.length() == 0) { + hostAddr = "0.0.0.0"; + } + config.set("host", hostAddr); + + // Replace to remove a bug with Windows paths - SmilingDevil + config.set("listener_folder", getDataFolder().toString().replace("\\", "/") + "/listeners"); + } + + if (config.contains("websites")) { + return; + } + + config.options().copyDefaults(true); + saveConfig(); + reloadConfig(); + } @Override public void onEnable() { @@ -78,60 +127,10 @@ public void onEnable() { if (!getDataFolder().exists()) { getDataFolder().mkdir(); } - File config = new File(getDataFolder() + "/config.yml"); - YamlConfiguration cfg = YamlConfiguration.loadConfiguration(config); + FileConfiguration config = super.getConfig(); File rsaDirectory = new File(getDataFolder() + "/rsa"); - // Replace to remove a bug with Windows paths - SmilingDevil - String listenerDirectory = getDataFolder().toString().replace("\\", "/") + "/listeners"; - - /* - * Use IP address from server.properties as a default for - * configurations. Do not use InetAddress.getLocalHost() as it most - * likely will return the main server address instead of the address - * assigned to the server. - */ - String hostAddr = Bukkit.getServer().getIp(); - if (hostAddr == null || hostAddr.length() == 0) { - hostAddr = "0.0.0.0"; - } - - /* - * Create configuration file if it does not exists; otherwise, load it - */ - if (!config.exists()) { - try { - // First time run - do some initialization. - LOG.info("Configuring Votifier for the first time..."); - - // Initialize the configuration file. - config.createNewFile(); - - cfg.set("host", hostAddr); - cfg.set("port", 8192); - cfg.set("debug", false); - - /* - * Remind hosted server admins to be sure they have the right - * port number. - */ - LOG.info("------------------------------------------------------------------------------"); - LOG.info("Assigning Votifier to listen on port 8192. If you are hosting Craftbukkit on a"); - LOG.info("shared server please check with your hosting provider to verify that this port"); - LOG.info("is available for your use. Chances are that your hosting provider will assign"); - LOG.info("a different port, which you need to specify in config.yml"); - LOG.info("------------------------------------------------------------------------------"); - - cfg.set("listener_folder", listenerDirectory); - cfg.save(config); - } catch (Exception ex) { - LOG.log(Level.SEVERE, "Error creating configuration file", ex); - gracefulExit(); - return; - } - } else { - // Load configuration. - cfg = YamlConfiguration.loadConfiguration(config); - } + File listenerDirectory = new File(config.getString("listener_folder")); + listenerDirectory.mkdir(); /* * Create RSA directory and keys if it does not exist; otherwise, read @@ -140,34 +139,49 @@ public void onEnable() { try { if (!rsaDirectory.exists()) { rsaDirectory.mkdir(); - new File(listenerDirectory).mkdir(); keyPair = RSAKeygen.generate(2048); RSAIO.save(rsaDirectory, keyPair); } else { keyPair = RSAIO.load(rsaDirectory); } - } catch (Exception ex) { + } catch (Exception exception) { LOG.log(Level.SEVERE, - "Error reading configuration file or RSA keys", ex); + "Error reading configuration file or RSA keys", + exception); gracefulExit(); return; } // Load the vote listeners. - listenerDirectory = cfg.getString("listener_folder"); listeners.addAll(ListenerLoader.load(listenerDirectory)); + + // Load the website public keys + for (Entry website : config.getConfigurationSection("websites").getValues(false).entrySet()) { + try { + websites.put(website.getKey(), RSAIO.loadPublicKey(new URL((String) website.getValue()))); + if (!website.getKey().startsWith("https://")) { + 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); + } + } - // Initialize the receiver. - String host = cfg.getString("host", hostAddr); - int port = cfg.getInt("port", 8192); - debug = cfg.getBoolean("debug", false); + // Initialize the acceptor. + String host = config.getString("host"); + int port = config.getInt("port"); + oldProtocol = config.getBoolean("enable_old_protocol"); + debug = config.getBoolean("debug"); if (debug) { LOG.info("DEBUG mode enabled!"); } try { - voteReceiver = new VoteAcceptor(this, host, port); - voteReceiver.start(); + voteAcceptor = new VoteAcceptor(this, host, port); + voteAcceptor.start(); LOG.info("Votifier enabled."); } catch (Exception ex) { @@ -178,9 +192,9 @@ public void onEnable() { @Override public void onDisable() { - // Interrupt the vote receiver. - if (voteReceiver != null) { - voteReceiver.shutdown(); + // Interrupt the vote acceptor. + if (voteAcceptor != null) { + voteAcceptor.shutdown(); } LOG.info("Votifier disabled."); } @@ -206,14 +220,23 @@ public String getVersion() { public List getListeners() { return listeners; } + + /** + * Gets the websites mapped to their public key + * + * @return The websites + */ + public Map getWebsites() { + return websites; + } /** - * Gets the vote receiver. + * Gets the vote acceptor. * - * @return The vote receiver + * @return The vote acceptor */ - public VoteAcceptor getVoteReceiver() { - return voteReceiver; + public VoteAcceptor getVoteAcceptor() { + return voteAcceptor; } /** @@ -228,5 +251,9 @@ public KeyPair getKeyPair() { public boolean isDebug() { return debug; } + + public boolean isOldProtocol() { + return oldProtocol; + } } diff --git a/src/main/java/com/vexsoftware/votifier/model/ListenerLoader.java b/src/main/java/com/vexsoftware/votifier/model/ListenerLoader.java index e57300d..75a611c 100644 --- a/src/main/java/com/vexsoftware/votifier/model/ListenerLoader.java +++ b/src/main/java/com/vexsoftware/votifier/model/ListenerLoader.java @@ -24,9 +24,8 @@ public class ListenerLoader { * The directory */ @SuppressWarnings("resource") - public static List load(String directory) { + public static List load(File dir) { List listeners = new ArrayList(); - File dir = new File(directory); // Verify configured vote listener directory exists if (!dir.exists()) { diff --git a/src/main/java/com/vexsoftware/votifier/net/VoteClientThread.java b/src/main/java/com/vexsoftware/votifier/net/VoteClientThread.java index b7c8c98..36b92ac 100644 --- a/src/main/java/com/vexsoftware/votifier/net/VoteClientThread.java +++ b/src/main/java/com/vexsoftware/votifier/net/VoteClientThread.java @@ -1,5 +1,7 @@ package com.vexsoftware.votifier.net; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; @@ -12,7 +14,8 @@ import com.vexsoftware.votifier.model.Vote; import com.vexsoftware.votifier.model.VoteListener; import com.vexsoftware.votifier.model.VotifierEvent; -import com.vexsoftware.votifier.net.impl.ProtocolV1; +import com.vexsoftware.votifier.net.v1.ProtocolV1; +import com.vexsoftware.votifier.net.v2.ProtocolV2; /** * The vote client thread. @@ -37,15 +40,29 @@ public void run() { InputStream inputStream = null; OutputStream outputStream = null; try { - inputStream = socket.getInputStream(); - outputStream = socket.getOutputStream(); + inputStream = new BufferedInputStream(socket.getInputStream()); + outputStream = new BufferedOutputStream(socket.getOutputStream()); // Send them our version. outputStream.write(("VOTIFIER " + plugin.getVersion() + "\r\n").getBytes("UTF-8")); outputStream.flush(); + + // Which protocol do we use? + inputStream.mark(1); + int firstByte = inputStream.read(); + inputStream.reset(); + + Protocol protocol; + if (firstByte == (int) '{') { + protocol = ProtocolV2.INSTANCE; + } else if (this.plugin.isOldProtocol()) { + protocol = ProtocolV1.INSTANCE; + } else { + throw new Exception("Unknown protocol"); + } - // Read the vote with ProtocolV1 - final Vote vote = ProtocolV1.INSTANCE.handleProtocol(plugin, inputStream, outputStream); + // Read the vote + final Vote vote = protocol.handleProtocol(plugin, inputStream, outputStream); if (plugin.isDebug()) { LOG.info("Received vote record -> " + vote); diff --git a/src/main/java/com/vexsoftware/votifier/net/impl/ProtocolV1.java b/src/main/java/com/vexsoftware/votifier/net/v1/ProtocolV1.java similarity index 97% rename from src/main/java/com/vexsoftware/votifier/net/impl/ProtocolV1.java rename to src/main/java/com/vexsoftware/votifier/net/v1/ProtocolV1.java index bfb2a33..a95f556 100644 --- a/src/main/java/com/vexsoftware/votifier/net/impl/ProtocolV1.java +++ b/src/main/java/com/vexsoftware/votifier/net/v1/ProtocolV1.java @@ -1,4 +1,4 @@ -package com.vexsoftware.votifier.net.impl; +package com.vexsoftware.votifier.net.v1; import java.io.InputStream; import java.io.OutputStream; @@ -14,7 +14,7 @@ public class ProtocolV1 implements Protocol { /** - * Singleton + * Singleton. */ public static final ProtocolV1 INSTANCE = new ProtocolV1(); diff --git a/src/main/java/com/vexsoftware/votifier/net/v2/JsonMessage.java b/src/main/java/com/vexsoftware/votifier/net/v2/JsonMessage.java new file mode 100644 index 0000000..5b9b27e --- /dev/null +++ b/src/main/java/com/vexsoftware/votifier/net/v2/JsonMessage.java @@ -0,0 +1,47 @@ +package com.vexsoftware.votifier.net.v2; + +/** + * Json message + */ +public class JsonMessage { + + /** + * The service. + */ + public String service; + + /** + * The signature of the payload. + */ + public String signature; + + /** + * Json representation of a VoteMessagePayload + */ + public String payload; + + /** + * + * @return The service. + */ + public String getService() { + return service; + } + + /** + * + * @return The signature of the payload. + */ + public String getSignature() { + return signature; + } + + /** + * + * @return Json representation of a VoteMessagePayload + */ + public String getPayload() { + return payload; + } + +} diff --git a/src/main/java/com/vexsoftware/votifier/net/v2/JsonMessagePayload.java b/src/main/java/com/vexsoftware/votifier/net/v2/JsonMessagePayload.java new file mode 100644 index 0000000..6615acf --- /dev/null +++ b/src/main/java/com/vexsoftware/votifier/net/v2/JsonMessagePayload.java @@ -0,0 +1,60 @@ +package com.vexsoftware.votifier.net.v2; + +/** + * Json message payload + */ +public class JsonMessagePayload { + + /** + * Vote domain. + */ + public String domain; + + /** + * Vote username. + */ + public String username; + + /** + * Vote address. + */ + public String address; + + /** + * Vote unix timestamp. + */ + public long timestamp; + + /** + * + * @return Vote domain. + */ + public String getDomain() { + return domain; + } + + /** + * + * @return Vote username. + */ + public String getUsername() { + return username; + } + + /** + * + * @return Vote address. + */ + public String getAddress() { + return address; + } + + /** + * + * @return Vote unix timestamp. + */ + public long getTimestamp() { + return timestamp; + } + +} diff --git a/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java b/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java new file mode 100644 index 0000000..6f6602f --- /dev/null +++ b/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java @@ -0,0 +1,93 @@ +package com.vexsoftware.votifier.net.v2; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.security.PublicKey; +import java.util.HashSet; +import java.util.Set; + +import javax.xml.bind.DatatypeConverter; + +import org.bukkit.craftbukkit.libs.com.google.gson.Gson; + +import com.vexsoftware.votifier.Votifier; +import com.vexsoftware.votifier.model.Vote; +import com.vexsoftware.votifier.net.Protocol; +import com.vexsoftware.votifier.util.rsa.RSA; + +/** + * Version 2 of the Votifier protocol. + */ +public class ProtocolV2 implements Protocol { + + /** + * Singleton. + */ + public static final ProtocolV2 INSTANCE = new ProtocolV2(); + + /** + * Our instance of Gson. + */ + public static final Gson GSON = new Gson(); + + private Set replayCache = new HashSet(); + + private ProtocolV2() { + // private + } + + /** + * Handle the protocol + * + * @param plugin + * Votifier plugin + * @param socket + * The receiving connection + * @throws Exception + * If an error occurs + */ + public Vote handleProtocol(Votifier plugin, InputStream in, OutputStream out) throws Exception { + BufferedReader reader = null; + try { + reader = new BufferedReader(new InputStreamReader(in)); + JsonMessage jsonMessage = GSON.fromJson(reader.readLine(), JsonMessage.class); + + String service = jsonMessage.getService(); + PublicKey publicKey = plugin.getWebsites().get(service); + // Check if the service exists + if (publicKey == null) { + throw new Exception("Unknown service: " + service); + } + String payload = jsonMessage.getPayload(); + // Check the RSA signature + if (!RSA.verify(payload.getBytes("UTF-8"), DatatypeConverter.parseBase64Binary(jsonMessage.getSignature().trim()), publicKey)) { + throw new Exception("Signature not valid"); + } + + JsonMessagePayload jsonMessagePayload = GSON.fromJson(jsonMessage.getPayload(), JsonMessagePayload.class); + // Check for a replay attack + if (replayCache.contains(jsonMessage.getSignature().trim() + "\0" + jsonMessagePayload.getTimestamp())) { + throw new Exception("Replay attempt"); + } + // Check if the vote is older than 10 minutes. If so, it has expired and can no longer be redeemed + if (System.currentTimeMillis() - jsonMessagePayload.getTimestamp() > 10L * 60L * 1000L) { // 10 minutes + throw new Exception("Vote expired. Is your system time correct?"); + } + + return new Vote(service, jsonMessagePayload.getUsername(), jsonMessagePayload.getAddress(), jsonMessagePayload.getTimestamp()); + } catch(Exception exception) { + throw exception; + } finally { + if(reader != null) { + try { + reader.close(); + } catch(Exception exception) { + // ignore + } + } + } + } + +} diff --git a/src/main/java/com/vexsoftware/votifier/util/rsa/RSAIO.java b/src/main/java/com/vexsoftware/votifier/util/rsa/RSAIO.java index d33f1bb..e6526f5 100644 --- a/src/main/java/com/vexsoftware/votifier/util/rsa/RSAIO.java +++ b/src/main/java/com/vexsoftware/votifier/util/rsa/RSAIO.java @@ -21,6 +21,7 @@ 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; @@ -30,6 +31,8 @@ import javax.xml.bind.DatatypeConverter; +import net.minecraft.util.org.apache.commons.io.IOUtils; + /** * Static utility methods for saving and loading RSA key pairs. */ @@ -137,5 +140,22 @@ public static KeyPair load(File directory) throws Exception { PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec); 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/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..a8c6133 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,7 @@ +host: '' +port: 8192 +debug: false +listener_folder: '' +enable_old_protocol: true +websites: + Minestatus: https://www.minestatus.net/votifier/key \ No newline at end of file From 201f637d08bd4b1a5276d3b6bb4d343f489b8cdd Mon Sep 17 00:00:00 2001 From: coelho Date: Mon, 31 Mar 2014 02:21:26 -0700 Subject: [PATCH 03/11] Cleanup --- src/main/java/com/vexsoftware/votifier/Votifier.java | 2 +- src/main/java/com/vexsoftware/votifier/model/Vote.java | 2 +- .../java/com/vexsoftware/votifier/net/v2/ProtocolV2.java | 2 -- .../java/com/vexsoftware/votifier/util/rsa/RSAIO.java | 8 -------- 4 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/vexsoftware/votifier/Votifier.java b/src/main/java/com/vexsoftware/votifier/Votifier.java index c0c73b2..ce4563a 100644 --- a/src/main/java/com/vexsoftware/votifier/Votifier.java +++ b/src/main/java/com/vexsoftware/votifier/Votifier.java @@ -184,7 +184,7 @@ public void onEnable() { voteAcceptor.start(); LOG.info("Votifier enabled."); - } catch (Exception ex) { + } catch (Exception exception) { gracefulExit(); return; } diff --git a/src/main/java/com/vexsoftware/votifier/model/Vote.java b/src/main/java/com/vexsoftware/votifier/model/Vote.java index 748b4f6..92a44b9 100644 --- a/src/main/java/com/vexsoftware/votifier/model/Vote.java +++ b/src/main/java/com/vexsoftware/votifier/model/Vote.java @@ -42,7 +42,7 @@ public Vote() { public Vote(String serviceName, String username, String address, long timeStamp) { this.serviceName = serviceName; - this.username = username; + this.username = username.length() <= 16 ? username : username.substring(0, 16); this.address = address; this.timeStamp = timeStamp; } diff --git a/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java b/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java index 6f6602f..f192326 100644 --- a/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java +++ b/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java @@ -77,8 +77,6 @@ public Vote handleProtocol(Votifier plugin, InputStream in, OutputStream out) th } return new Vote(service, jsonMessagePayload.getUsername(), jsonMessagePayload.getAddress(), jsonMessagePayload.getTimestamp()); - } catch(Exception exception) { - throw exception; } finally { if(reader != null) { try { diff --git a/src/main/java/com/vexsoftware/votifier/util/rsa/RSAIO.java b/src/main/java/com/vexsoftware/votifier/util/rsa/RSAIO.java index e6526f5..ca0d134 100644 --- a/src/main/java/com/vexsoftware/votifier/util/rsa/RSAIO.java +++ b/src/main/java/com/vexsoftware/votifier/util/rsa/RSAIO.java @@ -58,8 +58,6 @@ public static void save(File directory, KeyPair keyPair) throws Exception { try { out = new FileOutputStream(directory + "/public.key"); out.write(DatatypeConverter.printBase64Binary(publicSpec.getEncoded()).getBytes()); - } catch(Exception exception) { - throw exception; } finally { try { out.close(); @@ -73,8 +71,6 @@ public static void save(File directory, KeyPair keyPair) throws Exception { try { out = new FileOutputStream(directory + "/private.key"); out.write(DatatypeConverter.printBase64Binary(privateSpec.getEncoded()).getBytes()); - } catch(Exception exception) { - throw exception; } finally { try { out.close(); @@ -104,8 +100,6 @@ public static KeyPair load(File directory) throws Exception { encodedPublicKey = new byte[(int) publicKeyFile.length()]; in.read(encodedPublicKey); encodedPublicKey = DatatypeConverter.parseBase64Binary(new String(encodedPublicKey)); - } catch(Exception exception) { - throw exception; } finally { try { in.close(); @@ -122,8 +116,6 @@ public static KeyPair load(File directory) throws Exception { encodedPrivateKey = new byte[(int) privateKeyFile.length()]; in.read(encodedPrivateKey); encodedPrivateKey = DatatypeConverter.parseBase64Binary(new String(encodedPrivateKey)); - } catch(Exception exception) { - throw exception; } finally { try { in.close(); From 1ac03b5faf96d83248270f1478258eb58a800fa8 Mon Sep 17 00:00:00 2001 From: coelho Date: Mon, 31 Mar 2014 02:26:54 -0700 Subject: [PATCH 04/11] Unix time is not in milliseconds --- src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java b/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java index f192326..435d11d 100644 --- a/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java +++ b/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java @@ -72,7 +72,7 @@ public Vote handleProtocol(Votifier plugin, InputStream in, OutputStream out) th throw new Exception("Replay attempt"); } // Check if the vote is older than 10 minutes. If so, it has expired and can no longer be redeemed - if (System.currentTimeMillis() - jsonMessagePayload.getTimestamp() > 10L * 60L * 1000L) { // 10 minutes + if ((System.currentTimeMillis() / 1000L) - jsonMessagePayload.getTimestamp() > 10L * 60L * 1000L) { // 10 minutes throw new Exception("Vote expired. Is your system time correct?"); } From ed8a3abfa23c5e575ed41fbcae4847e37bbaf5d2 Mon Sep 17 00:00:00 2001 From: coelho Date: Mon, 31 Mar 2014 05:07:09 -0700 Subject: [PATCH 05/11] Not all server lists on ProtocolV1 send a unix timestamp --- src/main/java/com/vexsoftware/votifier/model/Vote.java | 8 ++++---- .../java/com/vexsoftware/votifier/net/v1/ProtocolV1.java | 2 +- .../java/com/vexsoftware/votifier/net/v2/ProtocolV2.java | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/vexsoftware/votifier/model/Vote.java b/src/main/java/com/vexsoftware/votifier/model/Vote.java index 92a44b9..57d2ddf 100644 --- a/src/main/java/com/vexsoftware/votifier/model/Vote.java +++ b/src/main/java/com/vexsoftware/votifier/model/Vote.java @@ -33,14 +33,14 @@ public class Vote { private String address; /** The unix timeStamp of the vote. */ - private long timeStamp; + private String timeStamp; @Deprecated public Vote() { } - public Vote(String serviceName, String username, String address, long timeStamp) { + public Vote(String serviceName, String username, String address, String timeStamp) { this.serviceName = serviceName; this.username = username.length() <= 16 ? username : username.substring(0, 16); this.address = address; @@ -115,7 +115,7 @@ public String getAddress() { */ @Deprecated public void setTimeStamp(String timeStamp) { - this.timeStamp = Long.parseLong(timeStamp); + this.timeStamp = timeStamp; } /** @@ -124,7 +124,7 @@ public void setTimeStamp(String timeStamp) { * @return The time stamp */ public String getTimeStamp() { - return Long.toString(timeStamp); + return timeStamp; } } diff --git a/src/main/java/com/vexsoftware/votifier/net/v1/ProtocolV1.java b/src/main/java/com/vexsoftware/votifier/net/v1/ProtocolV1.java index a95f556..c47fab7 100644 --- a/src/main/java/com/vexsoftware/votifier/net/v1/ProtocolV1.java +++ b/src/main/java/com/vexsoftware/votifier/net/v1/ProtocolV1.java @@ -59,7 +59,7 @@ public Vote handleProtocol(Votifier plugin, InputStream in, OutputStream out) th String timeStamp = readString(block, position); position += timeStamp.length() + 1; - return new Vote(serviceName, username, address, Long.parseLong(timeStamp)); + return new Vote(serviceName, username, address, timeStamp); } /** diff --git a/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java b/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java index 435d11d..3c7426c 100644 --- a/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java +++ b/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java @@ -76,7 +76,7 @@ public Vote handleProtocol(Votifier plugin, InputStream in, OutputStream out) th throw new Exception("Vote expired. Is your system time correct?"); } - return new Vote(service, jsonMessagePayload.getUsername(), jsonMessagePayload.getAddress(), jsonMessagePayload.getTimestamp()); + return new Vote(service, jsonMessagePayload.getUsername(), jsonMessagePayload.getAddress(), Long.toString(jsonMessagePayload.getTimestamp())); } finally { if(reader != null) { try { From 0782b3cca52f7edd9546193d818871f0a01a4217 Mon Sep 17 00:00:00 2001 From: coelho Date: Mon, 31 Mar 2014 16:20:26 -0700 Subject: [PATCH 06/11] Votifier v1.9 -> v2.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index fc0fb45..aa74f66 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 06e44231752a3b122caad6414174b0a1f913c1c1 Mon Sep 17 00:00:00 2001 From: coelho Date: Mon, 31 Mar 2014 19:52:44 -0700 Subject: [PATCH 07/11] Rename domain -> host in the JsonMessagePayload --- .../votifier/net/v2/JsonMessagePayload.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/vexsoftware/votifier/net/v2/JsonMessagePayload.java b/src/main/java/com/vexsoftware/votifier/net/v2/JsonMessagePayload.java index 6615acf..9893514 100644 --- a/src/main/java/com/vexsoftware/votifier/net/v2/JsonMessagePayload.java +++ b/src/main/java/com/vexsoftware/votifier/net/v2/JsonMessagePayload.java @@ -6,9 +6,9 @@ public class JsonMessagePayload { /** - * Vote domain. + * Vote host. */ - public String domain; + public String host; /** * Vote username. @@ -27,10 +27,10 @@ public class JsonMessagePayload { /** * - * @return Vote domain. + * @return Vote host. */ - public String getDomain() { - return domain; + public String getHost() { + return host; } /** From 77d5938a043255feca2093446082c6b66ab844e4 Mon Sep 17 00:00:00 2001 From: coelho Date: Mon, 31 Mar 2014 20:32:17 -0700 Subject: [PATCH 08/11] Add missing toString method in Vote --- .../java/com/vexsoftware/votifier/model/Vote.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/vexsoftware/votifier/model/Vote.java b/src/main/java/com/vexsoftware/votifier/model/Vote.java index 57d2ddf..06d0c56 100644 --- a/src/main/java/com/vexsoftware/votifier/model/Vote.java +++ b/src/main/java/com/vexsoftware/votifier/model/Vote.java @@ -37,16 +37,16 @@ public class Vote { @Deprecated public Vote() { - + } - + public Vote(String serviceName, String username, String address, String timeStamp) { this.serviceName = serviceName; this.username = username.length() <= 16 ? username : username.substring(0, 16); this.address = address; this.timeStamp = timeStamp; } - + /** * Sets the serviceName. * @@ -117,7 +117,7 @@ public String getAddress() { public void setTimeStamp(String timeStamp) { this.timeStamp = timeStamp; } - + /** * Gets the time stamp. * @@ -127,4 +127,10 @@ public String getTimeStamp() { return timeStamp; } + @Override + public String toString() { + return "Vote (from:" + serviceName + " username:" + username + + " address:" + address + " timeStamp:" + timeStamp + ")"; + } + } From f3555e86a2a012bdd940b94bd7febc6154355ef0 Mon Sep 17 00:00:00 2001 From: coelho Date: Mon, 31 Mar 2014 20:37:18 -0700 Subject: [PATCH 09/11] Make the replayCache actually work --- .../java/com/vexsoftware/votifier/net/v2/ProtocolV2.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java b/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java index 3c7426c..fc33697 100644 --- a/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java +++ b/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java @@ -68,8 +68,12 @@ public Vote handleProtocol(Votifier plugin, InputStream in, OutputStream out) th JsonMessagePayload jsonMessagePayload = GSON.fromJson(jsonMessage.getPayload(), JsonMessagePayload.class); // Check for a replay attack - if (replayCache.contains(jsonMessage.getSignature().trim() + "\0" + jsonMessagePayload.getTimestamp())) { - throw new Exception("Replay attempt"); + String replayKey = jsonMessage.getSignature().trim() + "\0" + jsonMessagePayload.getTimestamp(); + synchronized(replayCache) { + if (replayCache.contains(replayKey)) { + throw new Exception("Replay attempt"); + } + replayCache.add(replayKey); } // Check if the vote is older than 10 minutes. If so, it has expired and can no longer be redeemed if ((System.currentTimeMillis() / 1000L) - jsonMessagePayload.getTimestamp() > 10L * 60L * 1000L) { // 10 minutes From 0054f7d9ed1afff9f5b0f1b87387e709c92c879f Mon Sep 17 00:00:00 2001 From: coelho Date: Mon, 31 Mar 2014 20:50:20 -0700 Subject: [PATCH 10/11] Better protect against a replay attack with a random token --- .../votifier/net/v2/JsonMessagePayload.java | 13 +++++++++++++ .../com/vexsoftware/votifier/net/v2/ProtocolV2.java | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/vexsoftware/votifier/net/v2/JsonMessagePayload.java b/src/main/java/com/vexsoftware/votifier/net/v2/JsonMessagePayload.java index 9893514..510689b 100644 --- a/src/main/java/com/vexsoftware/votifier/net/v2/JsonMessagePayload.java +++ b/src/main/java/com/vexsoftware/votifier/net/v2/JsonMessagePayload.java @@ -20,6 +20,11 @@ public class JsonMessagePayload { */ public String address; + /** + * Vote random number. + */ + private long random; + /** * Vote unix timestamp. */ @@ -48,6 +53,14 @@ public String getUsername() { public String getAddress() { return address; } + + /** + * + * @return Vote random number. + */ + public long getRandom() { + return random; + } /** * diff --git a/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java b/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java index fc33697..dd6858b 100644 --- a/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java +++ b/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java @@ -68,7 +68,7 @@ public Vote handleProtocol(Votifier plugin, InputStream in, OutputStream out) th JsonMessagePayload jsonMessagePayload = GSON.fromJson(jsonMessage.getPayload(), JsonMessagePayload.class); // Check for a replay attack - String replayKey = jsonMessage.getSignature().trim() + "\0" + jsonMessagePayload.getTimestamp(); + String replayKey = jsonMessage.getService() + "\0" + jsonMessagePayload.getRandom() + "\0" + jsonMessagePayload.getTimestamp(); synchronized(replayCache) { if (replayCache.contains(replayKey)) { throw new Exception("Replay attempt"); From 74774f023025011dd530fa4efafabed63a0a4d85 Mon Sep 17 00:00:00 2001 From: coelho Date: Tue, 6 May 2014 13:19:16 -0700 Subject: [PATCH 11/11] Remove the random token and implement a challenge system --- .../vexsoftware/votifier/net/Protocol.java | 10 +++++--- .../votifier/net/VoteClientThread.java | 14 ++++++++--- .../votifier/net/v1/ProtocolV1.java | 10 +++++--- .../votifier/net/v2/JsonMessagePayload.java | 10 ++++---- .../votifier/net/v2/ProtocolV2.java | 23 ++++++++----------- 5 files changed, 39 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/vexsoftware/votifier/net/Protocol.java b/src/main/java/com/vexsoftware/votifier/net/Protocol.java index 6d97ec1..95d9a19 100644 --- a/src/main/java/com/vexsoftware/votifier/net/Protocol.java +++ b/src/main/java/com/vexsoftware/votifier/net/Protocol.java @@ -16,11 +16,15 @@ public interface Protocol { * * @param plugin * Votifier plugin - * @param socket - * The receiving connection + * @param in + * The receiving connection's input stream + * @param out + * The receiving connection's output stream + * @param challenge + * The challenge issued in this connection * @throws Exception * If an error occurs */ - public Vote handleProtocol(Votifier plugin, InputStream in, OutputStream out) throws Exception; + public Vote handleProtocol(Votifier plugin, InputStream in, OutputStream out, String challenge) throws Exception; } diff --git a/src/main/java/com/vexsoftware/votifier/net/VoteClientThread.java b/src/main/java/com/vexsoftware/votifier/net/VoteClientThread.java index 36b92ac..41d992e 100644 --- a/src/main/java/com/vexsoftware/votifier/net/VoteClientThread.java +++ b/src/main/java/com/vexsoftware/votifier/net/VoteClientThread.java @@ -4,7 +4,9 @@ import java.io.BufferedOutputStream; import java.io.InputStream; import java.io.OutputStream; +import java.math.BigInteger; import java.net.Socket; +import java.security.SecureRandom; import java.util.logging.Level; import java.util.logging.Logger; @@ -21,10 +23,13 @@ * The vote client thread. */ public class VoteClientThread implements Runnable { - + /** The logger instance. */ private static final Logger LOG = Logger.getLogger("Votifier"); + /** The random instance. */ + private static final SecureRandom RANDOM = new SecureRandom(); + /** The plugin. */ private Votifier plugin; @@ -43,8 +48,11 @@ public void run() { inputStream = new BufferedInputStream(socket.getInputStream()); outputStream = new BufferedOutputStream(socket.getOutputStream()); + // Generate challenge + String challenge = new BigInteger(130, RANDOM).toString(32); // This seems pretty strong and is seeded from SecureRandom + // Send them our version. - outputStream.write(("VOTIFIER " + plugin.getVersion() + "\r\n").getBytes("UTF-8")); + outputStream.write(("VOTIFIER " + plugin.getVersion() + " " + challenge + "\r\n").getBytes("UTF-8")); outputStream.flush(); // Which protocol do we use? @@ -62,7 +70,7 @@ public void run() { } // Read the vote - final Vote vote = protocol.handleProtocol(plugin, inputStream, outputStream); + final Vote vote = protocol.handleProtocol(plugin, inputStream, outputStream, challenge); if (plugin.isDebug()) { LOG.info("Received vote record -> " + vote); diff --git a/src/main/java/com/vexsoftware/votifier/net/v1/ProtocolV1.java b/src/main/java/com/vexsoftware/votifier/net/v1/ProtocolV1.java index c47fab7..1770f71 100644 --- a/src/main/java/com/vexsoftware/votifier/net/v1/ProtocolV1.java +++ b/src/main/java/com/vexsoftware/votifier/net/v1/ProtocolV1.java @@ -27,12 +27,16 @@ private ProtocolV1() { * * @param plugin * Votifier plugin - * @param socket - * The receiving connection + * @param in + * The receiving connection's input stream + * @param out + * The receiving connection's output stream + * @param challenge + * The challenge issued in this connection * @throws Exception * If an error occurs */ - public Vote handleProtocol(Votifier plugin, InputStream in, OutputStream out) throws Exception { + public Vote handleProtocol(Votifier plugin, InputStream in, OutputStream out, String challenge) throws Exception { // Read the 256 byte block. byte[] block = new byte[256]; in.read(block, 0, block.length); diff --git a/src/main/java/com/vexsoftware/votifier/net/v2/JsonMessagePayload.java b/src/main/java/com/vexsoftware/votifier/net/v2/JsonMessagePayload.java index 510689b..ed6cf84 100644 --- a/src/main/java/com/vexsoftware/votifier/net/v2/JsonMessagePayload.java +++ b/src/main/java/com/vexsoftware/votifier/net/v2/JsonMessagePayload.java @@ -21,9 +21,9 @@ public class JsonMessagePayload { public String address; /** - * Vote random number. + * Vote challenge. */ - private long random; + private String challenge; /** * Vote unix timestamp. @@ -56,10 +56,10 @@ public String getAddress() { /** * - * @return Vote random number. + * @return Vote challenge. */ - public long getRandom() { - return random; + public String getChallenge() { + return challenge; } /** diff --git a/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java b/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java index dd6858b..a091ea2 100644 --- a/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java +++ b/src/main/java/com/vexsoftware/votifier/net/v2/ProtocolV2.java @@ -43,12 +43,16 @@ private ProtocolV2() { * * @param plugin * Votifier plugin - * @param socket - * The receiving connection + * @param in + * The receiving connection's input stream + * @param out + * The receiving connection's output stream + * @param challenge + * The challenge issued in this connection * @throws Exception * If an error occurs */ - public Vote handleProtocol(Votifier plugin, InputStream in, OutputStream out) throws Exception { + public Vote handleProtocol(Votifier plugin, InputStream in, OutputStream out, String challenge) throws Exception { BufferedReader reader = null; try { reader = new BufferedReader(new InputStreamReader(in)); @@ -67,17 +71,8 @@ public Vote handleProtocol(Votifier plugin, InputStream in, OutputStream out) th } JsonMessagePayload jsonMessagePayload = GSON.fromJson(jsonMessage.getPayload(), JsonMessagePayload.class); - // Check for a replay attack - String replayKey = jsonMessage.getService() + "\0" + jsonMessagePayload.getRandom() + "\0" + jsonMessagePayload.getTimestamp(); - synchronized(replayCache) { - if (replayCache.contains(replayKey)) { - throw new Exception("Replay attempt"); - } - replayCache.add(replayKey); - } - // Check if the vote is older than 10 minutes. If so, it has expired and can no longer be redeemed - if ((System.currentTimeMillis() / 1000L) - jsonMessagePayload.getTimestamp() > 10L * 60L * 1000L) { // 10 minutes - throw new Exception("Vote expired. Is your system time correct?"); + if(!jsonMessagePayload.getChallenge().equals(challenge)) { + throw new Exception("Challenge not valid"); } return new Vote(service, jsonMessagePayload.getUsername(), jsonMessagePayload.getAddress(), Long.toString(jsonMessagePayload.getTimestamp()));