Compare commits

..
6 Commits
Author SHA1 Message Date
Joe Hirschfeld 904ed87619 Release 2.6.0 2019-12-24 18:54:24 -08:00
Joe Hirschfeld 45b2a7a8a9 Remove jenkinsfile 2019-12-24 18:54:14 -08:00
Joe Hirschfeld 0ef4793df3 Do not error when server filter does not have a list 2019-12-24 18:50:49 -08:00
Joe Hirschfeld 4aa276a2d0 Update buildscript to upload to bintray 2019-12-24 18:47:56 -08:00
Joe Hirschfeld 1e2b22f06c Clean up repository
Bump plugin version
Use sponge gradle plugin for setting version in sponge
Use replacement for Velocity plugin version
Update repositories
2019-12-24 17:35:12 -08:00
Joseph Hirschfeld dbd9f86be9 Hotpatch 2.5.3 2019-06-03 11:57:33 -04:00
111 changed files with 4605 additions and 519 deletions

No files matched your search

-1
View File
@@ -1 +0,0 @@
custom: https://www.paypal.me/ibj
Vendored
-37
View File
@@ -1,37 +0,0 @@
pipeline {
agent {
docker { image 'openjdk:12-jdk' }
}
stages {
stage('Build') {
steps {
sh './gradlew build --no-daemon'
}
}
stage('Test') {
steps {
sh './gradlew test --no-daemon'
}
}
stage('Publish') {
when {
branch "master"
}
environment {
SNAPSHOT_REPO = credentials('ibj-nexus-snapshot-repo')
RELEASE_REPO = credentials('ibj-nexus-release-repo')
RAW_UPLOAD_PATH = credentials('ibj-nexus-raw-path')
REPO = credentials('ibj-nexus-access')
PUBLISH = 'true'
LATEST_BUILD = 'true'
}
steps {
sh './gradlew publishVersionedPublicationToIbjRepository'
sh './gradlew publishLatestToIbjBlockRaw'
}
}
}
}
+24 -14
View File
@@ -1,25 +1,35 @@
# NuVotifier
NuVotifier is a secure alternative to using the original Votifier project.
NuVotifier will work in place of Votifier - any vote listener that supports
Votifier will also support NuVotifier.
NuVotifier is a Bukkit plugin whose purpose is to be notified (aka *votified*)
when a vote is made on a Minecraft server top list for the server. NuVotifier
creates a *lightweight* server that waits for connections by Minecraft server
lists and uses a simple protocol to get the required information. NuVotifier is
*secure*, and makes sure that all vote notifications are delivered by authentic
top lists.
NuVotifier supports many different platforms using the same universal JAR:
## Why fork Votifier?
+ Bukkit / Spigot / Paper >=1.8
+ Sponge 7
+ BungeeCord / Waterfall
+ Velocity
Votifier has lagged in activity, has severe security flaws in its protocol, and
is vulnerable to denial of service attacks.
NuVotifier also adds forwarding and listener test commands not present in the
original version.
## Is NuVotifier a drop-in replacement for Votifier?
[Setup Guide](https://github.com/NuVotifier/NuVotifier/wiki/Setup-Guide)
Yes! Except for some minor non-breaking changes (look into the [technical qa
docs](docs/technical_qa.md) for more info), NuVotifier is 100% compatible with
the old Votifier with many more features.
[Troubleshooting Guide](https://github.com/NuVotifier/NuVotifier/wiki/Troubleshooting-Guide)
# Documentation / Configuration Guide
[Developer Information](https://github.com/NuVotifier/NuVotifier/wiki/Developer-Documentation)
All documentation is located under the [docs](docs/) directory. A server admin
configuration guide is available [here](docs/usage.md).
# Support
Feel free to join the MOSS Discord server using the below link:
[![Discord](https://i.imgur.com/HLPoNnY.png)](https://discord.gg/anUtuAC)
# License
NuVotifier is GNU GPLv3 licensed. This project's license can be viewed [here](LICENSE).
NuVotifier is GNU GPLv3 licensed. This project's license can be viewed
[here](LICENSE).
+15 -13
View File
@@ -1,18 +1,26 @@
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.+'
}
}
import org.apache.tools.ant.filters.ReplaceTokens
allprojects {
group 'io.ibj.nuvotifier'
version '3.0.0-SNAPSHOT'
group 'com.vexsoftware'
version '2.6.0'
}
subprojects {
apply plugin: 'java'
apply plugin: 'java-library'
sourceCompatibility = 1.8
repositories {
jcenter()
mavenCentral()
maven {
url "https://hub.spigotmc.org/nexus/content/repositories/snapshots"
@@ -28,17 +36,11 @@ subprojects {
}
}
dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-api:5.4.2"
testRuntime "org.junit.jupiter:junit-jupiter-engine:5.4.2"
testCompile "org.mockito:mockito-core:2.+"
}
processResources {
filter(ReplaceTokens, tokens: ["app.version": this.project.version])
}
test {
useJUnitPlatform()
}
}
if (System.env.PUBLISH) {
apply from: 'publish.gradle'
}
-4
View File
@@ -1,4 +0,0 @@
dependencies {
api group: "org.spigotmc", name: "spigot-api", version: "1.8.8-R0.1-SNAPSHOT"
implementation project(":model")
}
+5
View File
@@ -0,0 +1,5 @@
dependencies {
compileOnly group: "org.spigotmc", name: "spigot-api", version: "1.8.8-R0.1-SNAPSHOT"
compile project(":nuvotifier-common")
}
@@ -0,0 +1,57 @@
package com.vexsoftware.votifier;
import com.vexsoftware.votifier.platform.scheduler.ScheduledVotifierTask;
import com.vexsoftware.votifier.platform.scheduler.VotifierScheduler;
import org.bukkit.scheduler.BukkitTask;
import java.util.concurrent.TimeUnit;
class BukkitScheduler implements VotifierScheduler {
private final NuVotifierBukkit plugin;
public BukkitScheduler(NuVotifierBukkit plugin) {
this.plugin = plugin;
}
private int toTicks(int time, TimeUnit unit) {
return (int) (unit.toMillis(time) / 50);
}
@Override
public ScheduledVotifierTask sync(Runnable runnable) {
return new BukkitTaskWrapper(plugin.getServer().getScheduler().runTask(plugin, runnable));
}
@Override
public ScheduledVotifierTask onPool(Runnable runnable) {
return new BukkitTaskWrapper(plugin.getServer().getScheduler().runTaskAsynchronously(plugin, runnable));
}
@Override
public ScheduledVotifierTask delayedSync(Runnable runnable, int delay, TimeUnit unit) {
return new BukkitTaskWrapper(plugin.getServer().getScheduler().runTaskLater(plugin, runnable, toTicks(delay, unit)));
}
@Override
public ScheduledVotifierTask delayedOnPool(Runnable runnable, int delay, TimeUnit unit) {
return new BukkitTaskWrapper(plugin.getServer().getScheduler().runTaskLaterAsynchronously(plugin, runnable, toTicks(delay, unit)));
}
@Override
public ScheduledVotifierTask repeatOnPool(Runnable runnable, int delay, int repeat, TimeUnit unit) {
return new BukkitTaskWrapper(plugin.getServer().getScheduler().runTaskTimerAsynchronously(plugin, runnable, toTicks(delay, unit), toTicks(repeat, unit)));
}
private static class BukkitTaskWrapper implements ScheduledVotifierTask {
private final BukkitTask task;
private BukkitTaskWrapper(BukkitTask task) {
this.task = task;
}
@Override
public void cancel() {
task.cancel();
}
}
}
@@ -0,0 +1,392 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package com.vexsoftware.votifier;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import com.vexsoftware.votifier.forwarding.BukkitPluginMessagingForwardingSink;
import com.vexsoftware.votifier.support.forwarding.ForwardedVoteListener;
import com.vexsoftware.votifier.support.forwarding.ForwardingVoteSink;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.model.VotifierEvent;
import com.vexsoftware.votifier.net.VotifierServerBootstrap;
import com.vexsoftware.votifier.net.VotifierSession;
import com.vexsoftware.votifier.net.protocol.v1crypto.RSAIO;
import com.vexsoftware.votifier.net.protocol.v1crypto.RSAKeygen;
import com.vexsoftware.votifier.platform.JavaUtilLogger;
import com.vexsoftware.votifier.platform.LoggingAdapter;
import com.vexsoftware.votifier.platform.VotifierPlugin;
import com.vexsoftware.votifier.platform.scheduler.VotifierScheduler;
import com.vexsoftware.votifier.util.KeyCreator;
import com.vexsoftware.votifier.util.TokenUtil;
import io.netty.channel.Channel;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.KeyPair;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
/**
* The main Votifier plugin class.
*
* @author Blake Beaupain
* @author Kramer Campbell
*/
public class NuVotifierBukkit extends JavaPlugin implements VoteHandler, VotifierPlugin, ForwardedVoteListener {
/**
* The Votifier instance.
*/
private static NuVotifierBukkit instance;
/**
* The current Votifier version.
*/
private String version;
/**
* The server bootstrap.
*/
private VotifierServerBootstrap bootstrap;
/**
* The RSA key pair.
*/
private KeyPair keyPair;
/**
* Debug mode flag
*/
private boolean debug;
/**
* Keys used for websites.
*/
private Map<String, Key> tokens = new HashMap<>();
private ForwardingVoteSink forwardingMethod;
private VotifierScheduler scheduler;
private LoggingAdapter pluginLogger;
private boolean loadAndBind() {
scheduler = new BukkitScheduler(this);
pluginLogger = new JavaUtilLogger(getLogger());
if (!getDataFolder().exists()) {
getDataFolder().mkdir();
}
// Handle configuration.
File config = new File(getDataFolder(), "config.yml");
/*
* 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.
getLogger().info("Configuring Votifier for the first time...");
// Initialize the configuration file.
config.createNewFile();
// Load and manually replace variables in the configuration.
String cfgStr = new String(ByteStreams.toByteArray(getResource("bukkitConfig.yml")), StandardCharsets.UTF_8);
String token = TokenUtil.newToken();
cfgStr = cfgStr.replace("%default_token%", token).replace("%ip%", hostAddr);
Files.asCharSink(config, StandardCharsets.UTF_8).write(cfgStr);
/*
* Remind hosted server admins to be sure they have the right
* port number.
*/
getLogger().info("------------------------------------------------------------------------------");
getLogger().info("Assigning NuVotifier to listen on port 8192. If you are hosting Craftbukkit on a");
getLogger().info("shared server please check with your hosting provider to verify that this port");
getLogger().info("is available for your use. Chances are that your hosting provider will assign");
getLogger().info("a different port, which you need to specify in config.yml");
getLogger().info("------------------------------------------------------------------------------");
getLogger().info("Your default NuVotifier token is " + token + ".");
getLogger().info("You will need to provide this token when you submit your server to a voting");
getLogger().info("list.");
getLogger().info("------------------------------------------------------------------------------");
} catch (Exception ex) {
getLogger().log(Level.SEVERE, "Error creating configuration file", ex);
return false;
}
}
YamlConfiguration cfg;
File rsaDirectory = new File(getDataFolder(), "rsa");
// Load configuration.
cfg = YamlConfiguration.loadConfiguration(config);
/*
* Create RSA directory and keys if it does not exist; otherwise, read
* keys.
*/
try {
if (!rsaDirectory.exists()) {
rsaDirectory.mkdir();
keyPair = RSAKeygen.generate(2048);
RSAIO.save(rsaDirectory, keyPair);
} else {
keyPair = RSAIO.load(rsaDirectory);
}
} catch (Exception ex) {
getLogger().log(Level.SEVERE,
"Error reading configuration file or RSA tokens", ex);
return false;
}
// the quiet flag always runs priority to the debug flag
if (cfg.isBoolean("quiet")) {
debug = !cfg.getBoolean("quiet");
} else {
// otherwise, default to being noisy
debug = cfg.getBoolean("debug", true);
}
// Load Votifier tokens.
ConfigurationSection tokenSection = cfg.getConfigurationSection("tokens");
if (tokenSection != null) {
Map<String, Object> websites = tokenSection.getValues(false);
for (Map.Entry<String, Object> website : websites.entrySet()) {
tokens.put(website.getKey(), KeyCreator.createKeyFrom(website.getValue().toString()));
getLogger().info("Loaded token for website: " + website.getKey());
}
} else {
String token = TokenUtil.newToken();
tokenSection = cfg.createSection("tokens");
tokenSection.set("default", token);
tokens.put("default", KeyCreator.createKeyFrom(token));
try {
cfg.save(config);
} catch (IOException e) {
getLogger().log(Level.SEVERE,
"Error generating Votifier token", e);
return false;
}
getLogger().info("------------------------------------------------------------------------------");
getLogger().info("No tokens were found in your configuration, so we've generated one for you.");
getLogger().info("Your default Votifier token is " + token + ".");
getLogger().info("You will need to provide this token when you submit your server to a voting");
getLogger().info("list.");
getLogger().info("------------------------------------------------------------------------------");
}
// Initialize the receiver.
final String host = cfg.getString("host", hostAddr);
final int port = cfg.getInt("port", 8192);
if (!debug)
getLogger().info("QUIET mode enabled!");
if (port >= 0) {
final boolean disablev1 = cfg.getBoolean("disable-v1-protocol");
if (disablev1) {
getLogger().info("------------------------------------------------------------------------------");
getLogger().info("Votifier protocol v1 parsing has been disabled. Most voting websites do not");
getLogger().info("currently support the modern Votifier protocol in NuVotifier.");
getLogger().info("------------------------------------------------------------------------------");
}
this.bootstrap = new VotifierServerBootstrap(host, port, this, disablev1);
this.bootstrap.start(error -> {});
} else {
getLogger().info("------------------------------------------------------------------------------");
getLogger().info("Your Votifier port is less than 0, so we assume you do NOT want to start the");
getLogger().info("votifier port server! Votifier will not listen for votes over any port, and");
getLogger().info("will only listen for pluginMessaging forwarded votes!");
getLogger().info("------------------------------------------------------------------------------");
}
ConfigurationSection forwardingConfig = cfg.getConfigurationSection("forwarding");
if (forwardingConfig != null) {
String method = forwardingConfig.getString("method", "none").toLowerCase(); //Default to lower case for case-insensitive searches
if ("none".equals(method)) {
getLogger().info("Method none selected for vote forwarding: Votes will not be received from a forwarder.");
} else if ("pluginmessaging".equals(method)) {
String channel = forwardingConfig.getString("pluginMessaging.channel", "NuVotifier");
try {
forwardingMethod = new BukkitPluginMessagingForwardingSink(this, channel, this);
getLogger().info("Receiving votes over PluginMessaging channel '" + channel + "'.");
} catch (RuntimeException e) {
getLogger().log(Level.SEVERE, "NuVotifier could not set up PluginMessaging for vote forwarding!", e);
}
} else {
getLogger().severe("No vote forwarding method '" + method + "' known. Defaulting to noop implementation.");
}
}
return true;
}
private void halt() {
// Shut down the network handlers.
if (bootstrap != null) {
bootstrap.shutdown();
bootstrap = null;
}
if (forwardingMethod != null) {
forwardingMethod.halt();
forwardingMethod = null;
}
}
@Override
public void onEnable() {
NuVotifierBukkit.instance = this;
// Set the plugin version.
version = getDescription().getVersion();
if (!loadAndBind()) {
gracefulExit();
setEnabled(false); // safer to just bomb out
}
}
@Override
public void onDisable() {
halt();
getLogger().info("Votifier disabled.");
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (sender instanceof ConsoleCommandSender) {
reload();
} else {
sender.sendMessage(ChatColor.RED + "For security and stability, only console may run this command!");
}
return true;
}
private void reload() {
try {
halt();
} catch (Exception ex) {
getLogger().log(Level.SEVERE, "On halt, an exception was thrown. This may be fine!", ex);
}
if (loadAndBind()) {
getLogger().info("Reload was successful.");
} else {
try {
halt();
getLogger().log(Level.SEVERE, "On reload, there was a problem with the configuration. Votifier currently does nothing!");
} catch (Exception ex) {
getLogger().log(Level.SEVERE, "On reload, there was a problem loading, and we could not re-halt the server. Votifier is in an unstable state!", ex);
}
}
}
private void gracefulExit() {
getLogger().log(Level.SEVERE, "Votifier did not initialize properly!");
}
/**
* Gets the instance.
*
* @return The instance
*/
public static NuVotifierBukkit getInstance() {
return instance;
}
@Override
public LoggingAdapter getPluginLogger() {
return pluginLogger;
}
@Override
public VotifierScheduler getScheduler() {
return scheduler;
}
public boolean isDebug() {
return debug;
}
@Override
public Map<String, Key> getTokens() {
return tokens;
}
@Override
public KeyPair getProtocolV1Key() {
return keyPair;
}
@Override
public void onVoteReceived(Channel channel, final Vote vote, VotifierSession.ProtocolVersion protocolVersion) {
if (debug) {
if (protocolVersion == VotifierSession.ProtocolVersion.ONE) {
getLogger().info("Got a protocol v1 vote record from " + channel.remoteAddress() + " -> " + vote);
} else {
getLogger().info("Got a protocol v2 vote record from " + channel.remoteAddress() + " -> " + vote);
}
}
Bukkit.getScheduler().runTask(this, () -> Bukkit.getPluginManager().callEvent(new VotifierEvent(vote)));
}
@Override
public void onError(Channel channel, boolean alreadyHandledVote, Throwable throwable) {
if (debug) {
if (alreadyHandledVote) {
getLogger().log(Level.SEVERE, "Vote processed, however an exception " +
"occurred with a vote from " + channel.remoteAddress(), throwable);
} else {
getLogger().log(Level.SEVERE, "Unable to process vote from " + channel.remoteAddress(), throwable);
}
} else if (!alreadyHandledVote) {
getLogger().log(Level.SEVERE, "Unable to process vote from " + channel.remoteAddress());
}
}
@Override
public void onForward(final Vote v) {
if (debug) {
getLogger().info("Got a forwarded vote -> " + v);
}
Bukkit.getScheduler().runTask(this, () -> Bukkit.getPluginManager().callEvent(new VotifierEvent(v)));
}
}
@@ -0,0 +1,43 @@
package com.vexsoftware.votifier.forwarding;
import com.vexsoftware.votifier.NuVotifierBukkit;
import com.vexsoftware.votifier.support.forwarding.AbstractPluginMessagingForwardingSink;
import com.vexsoftware.votifier.support.forwarding.ForwardedVoteListener;
import org.apache.commons.lang.Validate;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.messaging.PluginMessageListener;
import java.util.logging.Level;
/**
* Created by Joe Hirschfeld on 10/20/2015.
*/
public class BukkitPluginMessagingForwardingSink extends AbstractPluginMessagingForwardingSink implements PluginMessageListener {
public BukkitPluginMessagingForwardingSink(Plugin p, String channel, ForwardedVoteListener listener) {
super(listener);
Validate.notNull(channel, "Channel cannot be null.");
this.channel = channel;
Bukkit.getMessenger().registerIncomingPluginChannel(p, channel, this);
this.p = p;
}
private final Plugin p;
private final String channel;
@Override
public void halt() {
Bukkit.getMessenger().unregisterIncomingPluginChannel(p, channel, this);
}
@Override
public void onPluginMessageReceived(String s, Player player, byte[] bytes) {
try {
this.handlePluginMessage(bytes);
} catch (Exception e) {
NuVotifierBukkit.getInstance().getLogger().log(Level.SEVERE, "There was an unknown error when processing a forwarded vote.", e);
}
}
}
@@ -1,6 +1,5 @@
package com.vexsoftware.votifier.model;
import io.ibj.nuvotifier.model.NuVote;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
@@ -20,14 +19,14 @@ public class VotifierEvent extends Event {
/**
* Encapsulated vote record.
*/
private NuVote vote;
private Vote vote;
/**
* Constructs a vote event that encapsulated the given vote record.
*
* @param vote vote record
*/
public VotifierEvent(final NuVote vote) {
public VotifierEvent(final Vote vote) {
this.vote = vote;
}
@@ -0,0 +1,25 @@
# The IP to listen to. Use 0.0.0.0 if you wish to listen to all interfaces on your server. (All IP addresses)
# This defaults to the IP you have configured your server to listen on, or 0.0.0.0 if you have not configured this.
host: %ip%
# Port to listen for new votes on
port: 8192
# Setting this option to true will disable handling of Protocol v1 packets. While the old protocol is not secure, this
# option is currently not recommended as most voting sites only support the old protocol at present. However, if you are
# using NuVotifier's proxy forwarding mechanism, enabling this option will increase your server's security.
disable-v1-protocol: false
# All tokens, labeled by the serviceName of each server list.
tokens:
# Default token for all server lists, if another isn't supplied.
default: %default_token%
# Configuration section for all vote forwarding to NuVotifier
forwarding:
# Sets whether to set up a remote method for fowarding. Supported methods:
# - none - Does not set up a forwarding method.
# - pluginMessaging - Sets up plugin messaging
method: none
pluginMessaging:
channel: nuvotifier:votes
+13
View File
@@ -0,0 +1,13 @@
name: Votifier
main: com.vexsoftware.votifier.NuVotifierBukkit
version: @app.version@
description: A plugin that gets notified when votes are made for the server on toplists.
authors: [Ichbinjoe, blakeman8192, Kramer, tuxed]
api-version: 1.13
commands:
nvreload:
description: Reloads the NuVotifier configuration
permission: nuvotifier.console
permission-message: "You do not have permission to run this command."
usage: "/nvreload"
-4
View File
@@ -1,4 +0,0 @@
dependencies {
api group: "net.md-5", name: "bungeecord-api", version: "1.8-SNAPSHOT"
implementation project(":model")
}
+4
View File
@@ -0,0 +1,4 @@
dependencies {
compileOnly group: "net.md-5", name: "bungeecord-api", version: "1.8-SNAPSHOT"
compile project(":nuvotifier-common")
}
@@ -0,0 +1,28 @@
package com.vexsoftware.votifier.bungee;
import com.google.common.base.Preconditions;
import com.vexsoftware.votifier.platform.BackendServer;
import net.md_5.bungee.api.config.ServerInfo;
public class BungeeBackendServer implements BackendServer {
private final ServerInfo info;
public BungeeBackendServer(ServerInfo info) {
this.info = Preconditions.checkNotNull(info, "info");
}
@Override
public String getName() {
return info.getName();
}
@Override
public boolean sendPluginMessage(String channel, byte[] data) {
return info.sendData(channel, data, false);
}
@Override
public String toString() {
return info.getName();
}
}
@@ -0,0 +1,53 @@
package com.vexsoftware.votifier.bungee;
import com.vexsoftware.votifier.platform.scheduler.ScheduledVotifierTask;
import com.vexsoftware.votifier.platform.scheduler.VotifierScheduler;
import net.md_5.bungee.api.scheduler.ScheduledTask;
import java.util.concurrent.TimeUnit;
class BungeeScheduler implements VotifierScheduler {
private final NuVotifier plugin;
public BungeeScheduler(NuVotifier plugin) {
this.plugin = plugin;
}
@Override
public ScheduledVotifierTask sync(Runnable runnable) {
return onPool(runnable);
}
@Override
public ScheduledVotifierTask onPool(Runnable runnable) {
return new BungeeTaskWrapper(plugin.getProxy().getScheduler().runAsync(plugin, runnable));
}
@Override
public ScheduledVotifierTask delayedSync(Runnable runnable, int delay, TimeUnit unit) {
return delayedOnPool(runnable, delay, unit);
}
@Override
public ScheduledVotifierTask delayedOnPool(Runnable runnable, int delay, TimeUnit unit) {
return new BungeeTaskWrapper(plugin.getProxy().getScheduler().schedule(plugin, runnable, delay, unit));
}
@Override
public ScheduledVotifierTask repeatOnPool(Runnable runnable, int delay, int repeat, TimeUnit unit) {
return new BungeeTaskWrapper(plugin.getProxy().getScheduler().schedule(plugin, runnable, delay, repeat, unit));
}
private static class BungeeTaskWrapper implements ScheduledVotifierTask {
private final ScheduledTask task;
private BungeeTaskWrapper(ScheduledTask task) {
this.task = task;
}
@Override
public void cancel() {
this.task.cancel();
}
}
}
@@ -0,0 +1,419 @@
package com.vexsoftware.votifier.bungee;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import com.vexsoftware.votifier.VoteHandler;
import com.vexsoftware.votifier.net.VotifierServerBootstrap;
import com.vexsoftware.votifier.platform.BackendServer;
import com.vexsoftware.votifier.platform.JavaUtilLogger;
import com.vexsoftware.votifier.platform.LoggingAdapter;
import com.vexsoftware.votifier.platform.ProxyVotifierPlugin;
import com.vexsoftware.votifier.bungee.events.VotifierEvent;
import com.vexsoftware.votifier.platform.scheduler.VotifierScheduler;
import com.vexsoftware.votifier.support.forwarding.ForwardingVoteSource;
import com.vexsoftware.votifier.support.forwarding.ServerFilter;
import com.vexsoftware.votifier.support.forwarding.cache.FileVoteCache;
import com.vexsoftware.votifier.support.forwarding.cache.MemoryVoteCache;
import com.vexsoftware.votifier.support.forwarding.cache.VoteCache;
import com.vexsoftware.votifier.support.forwarding.proxy.ProxyForwardingVoteSource;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.net.VotifierSession;
import com.vexsoftware.votifier.net.protocol.v1crypto.RSAIO;
import com.vexsoftware.votifier.net.protocol.v1crypto.RSAKeygen;
import com.vexsoftware.votifier.util.KeyCreator;
import com.vexsoftware.votifier.util.TokenUtil;
import io.netty.channel.Channel;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Command;
import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.config.Configuration;
import net.md_5.bungee.config.ConfigurationProvider;
import net.md_5.bungee.config.YamlConfiguration;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.KeyPair;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.logging.Level;
import java.util.stream.Collectors;
public class NuVotifier extends Plugin implements VoteHandler, ProxyVotifierPlugin {
/**
* The server channel.
*/
private VotifierServerBootstrap bootstrap;
/**
* The RSA key pair.
*/
private KeyPair keyPair;
/**
* Debug mode flag
*/
private boolean debug;
/**
* Keys used for websites.
*/
private Map<String, Key> tokens = new HashMap<>();
/**
* Method used to forward votes to downstream servers
*/
private ForwardingVoteSource forwardingMethod;
private VotifierScheduler scheduler;
private LoggingAdapter pluginLogger;
private void loadAndBind() {
scheduler = new BungeeScheduler(this);
pluginLogger = new JavaUtilLogger(getLogger());
if (!getDataFolder().exists()) {
getDataFolder().mkdir();
}
// Handle configuration.
File config = new File(getDataFolder() , "config.yml");
File rsaDirectory = new File(getDataFolder() , "rsa");
Configuration configuration;
if (!config.exists()) {
try {
// First time run - do some initialization.
getLogger().info("Configuring Votifier for the first time...");
// Initialize the configuration file.
config.createNewFile();
String cfgStr = new String(ByteStreams.toByteArray(getResourceAsStream("bungeeConfig.yml")), StandardCharsets.UTF_8);
String token = TokenUtil.newToken();
cfgStr = cfgStr.replace("%default_token%", token);
Files.asCharSink(config, StandardCharsets.UTF_8).write(cfgStr);
/*
* Remind hosted server admins to be sure they have the right
* port number.
*/
getLogger().info("------------------------------------------------------------------------------");
getLogger().info("Assigning NuVotifier to listen on port 8192. If you are hosting BungeeCord on a");
getLogger().info("shared server please check with your hosting provider to verify that this port");
getLogger().info("is available for your use. Chances are that your hosting provider will assign");
getLogger().info("a different port, which you need to specify in config.yml");
getLogger().info("------------------------------------------------------------------------------");
getLogger().info("Assigning NuVotifier to listen to interface 0.0.0.0. This is usually alright,");
getLogger().info("however, if you want NuVotifier to only listen to one interface for security ");
getLogger().info("reasons (or you use a shared host), you may change this in the config.yml.");
getLogger().info("------------------------------------------------------------------------------");
getLogger().info("Your default Votifier token is " + token + ".");
getLogger().info("You will need to provide this token when you submit your server to a voting");
getLogger().info("list.");
getLogger().info("------------------------------------------------------------------------------");
} catch (Exception ex) {
throw new RuntimeException("Unable to create configuration file", ex);
}
}
// Load the configuration.
try {
configuration = ConfigurationProvider.getProvider(YamlConfiguration.class).load(config);
} catch (IOException e) {
throw new RuntimeException("Unable to load configuration", e);
}
/*
* Create RSA directory and keys if it does not exist; otherwise, read
* keys.
*/
try {
if (!rsaDirectory.exists()) {
rsaDirectory.mkdir();
keyPair = RSAKeygen.generate(2048);
RSAIO.save(rsaDirectory, keyPair);
} else {
keyPair = RSAIO.load(rsaDirectory);
}
} catch (Exception ex) {
throw new RuntimeException("Error reading RSA tokens", ex);
}
// Load Votifier tokens.
Configuration tokenSection = configuration.getSection("tokens");
if (configuration.get("tokens") != null) {
for (String s : tokenSection.getKeys()) {
tokens.put(s, KeyCreator.createKeyFrom(tokenSection.getString(s)));
getLogger().info("Loaded token for website: " + s);
}
} else {
String token = TokenUtil.newToken();
configuration.set("tokens", ImmutableMap.of("default", token));
tokens.put("default", KeyCreator.createKeyFrom(token));
try {
ConfigurationProvider.getProvider(YamlConfiguration.class).save(configuration, config);
} catch (IOException e) {
throw new RuntimeException("Error generating Votifier token", e);
}
getLogger().info("------------------------------------------------------------------------------");
getLogger().info("No tokens were found in your configuration, so we've generated one for you.");
getLogger().info("Your default Votifier token is " + token + ".");
getLogger().info("You will need to provide this token when you submit your server to a voting");
getLogger().info("list.");
getLogger().info("------------------------------------------------------------------------------");
}
// Initialize the receiver.
final String host = configuration.getString("host", "0.0.0.0");
final int port = configuration.getInt("port", 8192);
if (configuration.get("quiet") != null)
debug = !configuration.getBoolean("quiet");
else
debug = configuration.getBoolean("debug", true);
if (!debug)
getLogger().info("QUIET mode enabled!");
final boolean disablev1 = configuration.getBoolean("disable-v1-protocol");
if (disablev1) {
getLogger().info("------------------------------------------------------------------------------");
getLogger().info("Votifier protocol v1 parsing has been disabled. Most voting websites do not");
getLogger().info("currently support the modern Votifier protocol in NuVotifier.");
getLogger().info("------------------------------------------------------------------------------");
}
// Must set up server asynchronously due to BungeeCord goofiness.
FutureTask<?> initTask = new FutureTask<>(Executors.callable(() -> {
this.bootstrap = new VotifierServerBootstrap(host, port, NuVotifier.this, disablev1);
this.bootstrap.start(err -> {});
}));
getProxy().getScheduler().runAsync(this, initTask);
try {
initTask.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException("Unable to start server", e);
}
Configuration fwdCfg = configuration.getSection("forwarding");
String fwdMethod = fwdCfg.getString("method", "none").toLowerCase();
if ("none".equals(fwdMethod)) {
getLogger().info("Method none selected for vote forwarding: Votes will not be forwarded to backend servers.");
} else if ("pluginmessaging".equals(fwdMethod)) {
String channel = fwdCfg.getString("pluginMessaging.channel", "NuVotifier");
String cacheMethod = fwdCfg.getString("pluginMessaging.cache", "file").toLowerCase();
VoteCache voteCache = null;
if ("none".equals(cacheMethod)) {
getLogger().info("Vote cache none selected for caching: votes that cannot be immediately delivered will be lost.");
} else if ("memory".equals(cacheMethod)) {
voteCache = new MemoryVoteCache(
ProxyServer.getInstance().getServers().size(), this,
fwdCfg.getInt("pluginMessaging.memory.cacheTime", -1));
getLogger().info("Using in-memory cache for votes that are not able to be delivered.");
} else if ("file".equals(cacheMethod)) {
try {
voteCache = new FileVoteCache(
ProxyServer.getInstance().getServers().size(), this,
new File(getDataFolder(), fwdCfg.getString("pluginMessaging.file.name")),
fwdCfg.getInt("pluginMessaging.file.cacheTime", -1));
} catch (IOException e) {
getLogger().log(Level.SEVERE, "Unload to load file cache. Votes will be lost!", e);
}
}
int dumpRate = fwdCfg.getInt("pluginMessaging.dumpRate", 5);
ServerFilter filter = new ServerFilter(
fwdCfg.getStringList("pluginMessaging.excludedServers"),
fwdCfg.getBoolean("pluginMessaging.whitelist", false)
);
if (!fwdCfg.getBoolean("pluginMessaging.onlySendToJoinedServer")) {
try {
forwardingMethod = new PluginMessagingForwardingSource(channel, filter, this, voteCache, dumpRate);
getLogger().info("Forwarding votes over PluginMessaging channel '" + channel + "' for vote forwarding!");
} catch (RuntimeException e) {
getLogger().log(Level.SEVERE, "NuVotifier could not set up PluginMessaging for vote forwarding!", e);
}
} else {
try {
String fallbackServer = fwdCfg.getString("pluginMessaging.joinedServerFallback", null);
if (fallbackServer != null && fallbackServer.isEmpty()) fallbackServer = null;
forwardingMethod = new OnlineForwardPluginMessagingForwardingSource(channel, this, filter, voteCache, fallbackServer, dumpRate);
} catch (RuntimeException e) {
getLogger().log(Level.SEVERE, "NuVotifier could not set up PluginMessaging for vote forwarding!", e);
}
}
} else if ("proxy".equals(fwdMethod)) {
Configuration serverSection = fwdCfg.getSection("proxy");
List<ProxyForwardingVoteSource.BackendServer> serverList = new ArrayList<>();
for (String s : serverSection.getKeys()) {
Configuration section = serverSection.getSection(s);
InetAddress address;
try {
address = InetAddress.getByName(section.getString("address"));
} catch (UnknownHostException e) {
getLogger().info("Address " + section.getString("address") + " couldn't be looked up. Ignoring!");
continue;
}
Key token = null;
try {
token = KeyCreator.createKeyFrom(section.getString("token", section.getString("key")));
} catch (IllegalArgumentException e) {
getLogger().log(Level.SEVERE,
"An exception occurred while attempting to add proxy target '" + s + "' - maybe your token is wrong? " +
"Votes will not be forwarded to this server!", e);
}
if (token != null) {
ProxyForwardingVoteSource.BackendServer server = new ProxyForwardingVoteSource.BackendServer(s,
new InetSocketAddress(address, section.getInt("port")),
token);
serverList.add(server);
}
}
forwardingMethod = new ProxyForwardingVoteSource(this, bootstrap::client, serverList, null);
getLogger().info("Forwarding votes from this NuVotifier instance to another NuVotifier server.");
} else {
getLogger().severe("No vote forwarding method '" + fwdMethod + "' known. Defaulting to noop implementation.");
}
}
@Override
public void onEnable() {
loadAndBind();
getProxy().getPluginManager().registerCommand(this, new Command("nvreload") {
@Override
public void execute(CommandSender sender, String[] args) {
if (sender instanceof ProxiedPlayer)
sender.sendMessage(ChatColor.RED + "For security and stability, only console may run this command!");
else
reload();
}
});
}
private void halt() {
// Shut down the network handlers.
if (bootstrap != null) {
bootstrap.shutdown();
bootstrap = null;
}
if (forwardingMethod != null) {
forwardingMethod.halt();
forwardingMethod = null;
}
}
public void reload() {
try {
halt();
} catch (Exception ex) {
getLogger().log(Level.SEVERE, "On halt, an exception was thrown. This may be fine!", ex);
}
try {
loadAndBind();
getLogger().info("Reload was successful.");
} catch (Exception ex) {
try {
halt();
getLogger().log(Level.SEVERE, "On reload, there was a problem with the configuration. Votifier currently does nothing!", ex);
} catch (Exception ex2) {
getLogger().log(Level.SEVERE, "On reload, there was a problem loading, and we could not re-halt the server. Votifier is in an unstable state!", ex);
getLogger().log(Level.SEVERE, "(halt exception)", ex2);
}
}
}
@Override
public void onDisable() {
halt();
getLogger().info("Votifier disabled.");
}
@Override
public void onVoteReceived(Channel channel, final Vote vote, VotifierSession.ProtocolVersion protocolVersion) {
if (debug) {
if (protocolVersion == VotifierSession.ProtocolVersion.ONE) {
getLogger().info("Got a protocol v1 vote record from " + channel.remoteAddress() + " -> " + vote);
} else {
getLogger().info("Got a protocol v2 vote record from " + channel.remoteAddress() + " -> " + vote);
}
}
getProxy().getScheduler().runAsync(this, () -> getProxy().getPluginManager().callEvent(new VotifierEvent(vote)));
if (forwardingMethod != null) {
getProxy().getScheduler().runAsync(this, () -> forwardingMethod.forward(vote));
}
}
@Override
public void onError(Channel channel, boolean alreadyHandledVote, Throwable throwable) {
if (debug) {
if (alreadyHandledVote) {
getLogger().log(Level.SEVERE, "Vote processed, however an exception " +
"occurred with a vote from " + channel.remoteAddress(), throwable);
} else {
getLogger().log(Level.SEVERE, "Unable to process vote from " + channel.remoteAddress(), throwable);
}
} else if (!alreadyHandledVote) {
getLogger().log(Level.SEVERE, "Unable to process vote from " + channel.remoteAddress());
}
}
@Override
public Map<String, Key> getTokens() {
return tokens;
}
@Override
public KeyPair getProtocolV1Key() {
return keyPair;
}
@Override
public LoggingAdapter getPluginLogger() {
return pluginLogger;
}
@Override
public VotifierScheduler getScheduler() {
return scheduler;
}
public boolean isDebug() {
return debug;
}
@Override
public Collection<BackendServer> getAllBackendServers() {
return getProxy().getServers().values().stream()
.map(BungeeBackendServer::new)
.collect(Collectors.toList());
}
@Override
public Optional<BackendServer> getServer(String name) {
ServerInfo info = getProxy().getServerInfo(name);
return Optional.ofNullable(info).map(BungeeBackendServer::new);
}
}
@@ -0,0 +1,60 @@
package com.vexsoftware.votifier.bungee;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.platform.BackendServer;
import com.vexsoftware.votifier.support.forwarding.AbstractPluginMessagingForwardingSource;
import com.vexsoftware.votifier.support.forwarding.ServerFilter;
import com.vexsoftware.votifier.support.forwarding.cache.VoteCache;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.event.PluginMessageEvent;
import net.md_5.bungee.api.event.ServerConnectedEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.event.EventHandler;
/**
* @author Joseph Hirschfeld
* @date 12/31/2015
*/
public final class OnlineForwardPluginMessagingForwardingSource extends AbstractPluginMessagingForwardingSource implements Listener {
public OnlineForwardPluginMessagingForwardingSource(String channel, NuVotifier nuVotifier, ServerFilter serverFilter,
VoteCache cache, String fallbackServer, int dumpRate) {
super(channel, serverFilter, nuVotifier, cache, dumpRate);
this.fallbackServer = fallbackServer;
ProxyServer.getInstance().getPluginManager().registerListener(nuVotifier, this);
}
private final String fallbackServer;
@Override
public void forward(Vote v) {
ProxiedPlayer p = ProxyServer.getInstance().getPlayer(v.getUsername());
if (p != null && p.getServer() != null &&
serverFilter.isAllowed(p.getServer().getInfo().getName())) {
if (forwardSpecific(new BungeeBackendServer(p.getServer().getInfo()), v)) {
return;
}
}
ServerInfo serverInfo = ProxyServer.getInstance().getServers().get(fallbackServer);
// nowhere to fall back to, yet still not online. lets save this vote yet!
if (serverInfo == null)
attemptToAddToPlayerCache(v, v.getUsername());
else if (!forwardSpecific(new BungeeBackendServer(serverInfo), v))
attemptToAddToCache(v, fallbackServer);
}
@EventHandler
public void onPluginMessage(PluginMessageEvent e) {
if (e.getTag().equals(channel)) e.setCancelled(true);
}
@EventHandler
public void onServerConnected(final ServerConnectedEvent e) { //Attempt to resend any votes that were previously cached.
BackendServer server = new BungeeBackendServer(e.getServer().getInfo());
handlePlayerSwitch(server, e.getPlayer().getName());
onServerConnect(server);
}
}
@@ -0,0 +1,33 @@
package com.vexsoftware.votifier.bungee;
import com.vexsoftware.votifier.support.forwarding.AbstractPluginMessagingForwardingSource;
import com.vexsoftware.votifier.support.forwarding.ServerFilter;
import com.vexsoftware.votifier.support.forwarding.cache.VoteCache;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.event.PluginMessageEvent;
import net.md_5.bungee.api.event.ServerConnectedEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.event.EventHandler;
public class PluginMessagingForwardingSource extends AbstractPluginMessagingForwardingSource implements Listener {
public PluginMessagingForwardingSource(String channel, ServerFilter serverFilter, NuVotifier nuVotifier, VoteCache cache, int dumpRate) {
super(channel, serverFilter, nuVotifier, cache, dumpRate);
ProxyServer.getInstance().getPluginManager().registerListener(nuVotifier, this);
}
protected PluginMessagingForwardingSource(String channel, NuVotifier nuVotifier, VoteCache voteCache, int dumpRate) {
super(channel, nuVotifier, voteCache, dumpRate);
ProxyServer.getInstance().getPluginManager().registerListener(nuVotifier, this);
}
@EventHandler
public void onPluginMessage(PluginMessageEvent e) {
if (e.getTag().equals(channel)) e.setCancelled(true);
}
@EventHandler
public void onServerConnected(final ServerConnectedEvent e) { //Attempt to resend any votes that were previously cached.
onServerConnect(new BungeeBackendServer(e.getServer().getInfo()));
}
}
@@ -1,6 +1,6 @@
package com.vexsoftware.votifier.bungee.events;
import io.ibj.nuvotifier.model.NuVote;
import com.vexsoftware.votifier.model.Vote;
import net.md_5.bungee.api.plugin.Event;
/**
@@ -11,13 +11,13 @@ public class VotifierEvent extends Event {
/**
* The vote.
**/
private final NuVote vote;
private final Vote vote;
public VotifierEvent(NuVote vote) {
public VotifierEvent(Vote vote) {
this.vote = vote;
}
public NuVote getVote() {
public Vote getVote() {
return vote;
}
}
+4
View File
@@ -0,0 +1,4 @@
name: NuVotifier
main: com.vexsoftware.votifier.bungee.NuVotifier
version: @app.version@
author: Ichbinjoe, blakeman8192, Kramer, tuxed
@@ -0,0 +1,70 @@
# The IP to listen to. Use 0.0.0.0 if you wish to listen to all interfaces on your server. (All IP addresses)
# This defaults to the IP you have configured your server to listen on, or 0.0.0.0 if you have not configured this.
host: 0.0.0.0
# Port to listen for new votes on
port: 8192
# All tokens, labeled by the serviceName of each server list.
tokens:
# Default token for all server lists, if another isn't supplied.
default: %default_token%
# Setting this option to true will disable handling of Protocol v1 packets. While the old protocol is not secure, this
# option is currently not recommended as most voting sites only support the old protocol at present.
disable-v1-protocol: false
# Configuration section for all vote forwarding to NuVotifier
forwarding:
# Sets whether to set up a remote method for fowarding. Supported methods:
# - none - Does not set up a forwarding method.
# - pluginMessaging - Sets up plugin messaging.
# - proxy - Proxies votes to other NuVotifier servers from this server.
method: none
pluginMessaging:
channel: nuvotifier:votes
#Only send votes to certain servers. If a server is present on excludedServers, then votes will NOT be forwarded
#to them. If excludedServers is not present, votes will be sent to all servers.
# If onlySendToJoinedServer is true and a player is on one of these servers, NuVotifier will process the vote as if the
# player was not on the network at all.
#excludedServers:
#- server1
#- server2
# If uncommented, turns excludedServers into an includedServers, where only servers on the list will be used.
#whitelist: true
# Sets which cache to use if there are no players on the server that the vote is being sent to. Supported caches:
# - none - Votes will simply be lost when no players are present on the server.
# - memory - Votes will be cached in memory when no players are present. When Bungee is restarted, the votes will be lost.
# - file - Votes will be saved to a local file every 3 minutes and on shutdown and reloaded when Bungee is restarted. This is suitable for
# most set ups.
cache: file
# Only sends the vote to the server in which the player is currently joined.
onlySendToJoinedServer: false
#Sets which server to send the vote in the event the player is not online when the vote is cast
#If you do not want the vote forwarded to a fallback, set this value to empty ('')
#ONLY USED IF onlySendToJoinedServer is true!!
joinedServerFallback: 'Hub'
# Defines how quickly to dump votes over a player's connection when offloading a cache in votes per second
dumpRate: 5
# Options for file caching.
file:
name: cached-votes.json
# days before a vote is considered 'dead' - removed from cache with a console warning
cacheTime: 10
# Options for memory caching.
memory:
# days before a vote is considered 'dead' and removed from memory. All votes are removed when the server restarts. -1 signifies no TTL
cacheTime: -1
# Specify servers to proxy votes for.
proxy:
Hub:
address: 127.0.0.1
port: 8193
token: MyKeyHere
Hub2:
address: 127.0.0.1
port: 8194
token: AnotherKeyHere
+7
View File
@@ -0,0 +1,7 @@
dependencies {
compile group: "io.netty", name: "netty-handler", version: "4.1.32.Final"
compile 'com.google.code.gson:gson:2.8.0'
compile 'com.google.guava:guava:24.1-jre'
testCompile group: "junit", name: "junit", version: "4.12"
testCompile group: "org.json", name: "json", version: "20180130" // retain this for testing reasons
}
@@ -0,0 +1,20 @@
package com.vexsoftware.votifier;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.net.VotifierSession;
import io.netty.channel.Channel;
public interface VoteHandler {
void onVoteReceived(Channel channel, Vote vote, VotifierSession.ProtocolVersion protocolVersion) throws Exception;
// these default members are kept for compatibility sake - there probably isn't anyone out there implementing these
// on their own, but we still deprecate them before removing them because we are nice!
default void onError(Channel channel, boolean voteAlreadyCompleted, Throwable throwable) {
onError(channel, throwable);
}
@Deprecated
default void onError(Channel channel, Throwable throwable) {
throw new RuntimeException("Unimplemented onError handler");
}
}
@@ -0,0 +1,213 @@
/*
* Copyright (C) 2011 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 <http://www.gnu.org/licenses/>.
*/
package com.vexsoftware.votifier.model;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* A model for a vote.
*
* @author Blake Beaupain
*/
public class Vote {
/**
* The name of the vote service.
*/
private String serviceName;
/**
* The username of the voter.
*/
private String username;
/**
* The address of the voter.
*/
private String address;
/**
* The date and time of the vote.
*/
private String timeStamp;
/**
* Timestamp (unix-millis) normalized and taken from a known source
*/
private final long localTimestamp;
@Deprecated
public Vote() {
localTimestamp = System.currentTimeMillis();
}
public Vote(String serviceName, String username, String address, String timeStamp) {
this(serviceName, username, address, timeStamp, System.currentTimeMillis());
}
public Vote(String serviceName, String username, String address, String timeStamp, long localTimestamp) {
this.serviceName = serviceName;
this.username = username;
this.address = address;
this.timeStamp = timeStamp;
this.localTimestamp = localTimestamp;
}
private static String getTimestamp(JsonElement object) {
try {
return Long.toString(object.getAsLong());
} catch (Exception e) {
return object.getAsString();
}
}
public Vote(JsonObject jsonObject) {
this(jsonObject.get("serviceName").getAsString(),
jsonObject.get("username").getAsString(),
jsonObject.get("address").getAsString(),
getTimestamp(jsonObject.get("timestamp")),
// maintained for backwards compatibility with <2.3.6 peers
(jsonObject.has("localTimestamp") ? jsonObject.get("localTimestamp").getAsLong() : System.currentTimeMillis()));
}
@Override
public String toString() {
return "Vote (from:" + serviceName + " username:" + username
+ " address:" + address + " timeStamp:" + timeStamp
+ " localTimestamp:" + localTimestamp + ")";
}
/**
* Sets the serviceName.
*
* @param serviceName The new serviceName
*/
@Deprecated
public void setServiceName(String serviceName) {
this.serviceName = serviceName;
}
/**
* Gets the serviceName.
*
* @return The serviceName
*/
public String getServiceName() {
return serviceName;
}
/**
* Sets the username.
*
* @param username The new username
*/
@Deprecated
public void setUsername(String username) {
this.username = username;
}
/**
* Gets the username.
*
* @return The username
*/
public String getUsername() {
return username;
}
/**
* Sets the address.
*
* @param address The new address
*/
@Deprecated
public void setAddress(String address) {
this.address = address;
}
/**
* Gets the address.
*
* @return The address
*/
public String getAddress() {
return address;
}
/**
* Sets the time stamp.
*
* @param timeStamp The new time stamp
*/
@Deprecated
public void setTimeStamp(String timeStamp) {
this.timeStamp = timeStamp;
}
/**
* Gets the time stamp.
*
* @return The time stamp
*/
public String getTimeStamp() {
return timeStamp;
}
/**
* Gets the local timestamp, in unix-millis. Calculated locally by a NuVotifier instance
*
* @return The local timestamp
*/
public long getLocalTimestamp() {
return localTimestamp;
}
public JsonObject serialize() {
JsonObject ret = new JsonObject();
ret.addProperty("serviceName", serviceName);
ret.addProperty("username", username);
ret.addProperty("address", address);
ret.addProperty("timestamp", timeStamp);
ret.addProperty("localTimestamp", localTimestamp);
return ret;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Vote vote = (Vote) o;
if (!serviceName.equals(vote.serviceName)) return false;
if (!username.equals(vote.username)) return false;
if (!address.equals(vote.address)) return false;
return timeStamp.equals(vote.timeStamp);
}
@Override
public int hashCode() {
int result = serviceName.hashCode();
result = 31 * result + username.hashCode();
result = 31 * result + address.hashCode();
result = 31 * result + timeStamp.hashCode();
return result;
}
}
@@ -0,0 +1,90 @@
package com.vexsoftware.votifier.net;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.vexsoftware.votifier.net.protocol.VoteInboundHandler;
import com.vexsoftware.votifier.net.protocol.VotifierGreetingHandler;
import com.vexsoftware.votifier.net.protocol.VotifierProtocolDifferentiator;
import com.vexsoftware.votifier.platform.VotifierPlugin;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
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 java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Objects;
import java.util.function.Consumer;
public class VotifierServerBootstrap {
private final String host;
private final int port;
private final NioEventLoopGroup bossLoopGroup;
private final NioEventLoopGroup eventLoopGroup;
private final VotifierPlugin plugin;
private final boolean v1Disable;
private Channel serverChannel;
public VotifierServerBootstrap(String host, int port, VotifierPlugin plugin, boolean v1Disable) {
this.host = host;
this.port = port;
this.plugin = plugin;
this.v1Disable = v1Disable;
this.bossLoopGroup = new NioEventLoopGroup(1, new ThreadFactoryBuilder().setDaemon(true).setNameFormat("Votifier NIO boss").build());
this.eventLoopGroup = new NioEventLoopGroup(1, new ThreadFactoryBuilder().setDaemon(true).setNameFormat("Votifier NIO worker").build());
}
public void start(Consumer<Throwable> error) {
Objects.requireNonNull(error, "error");
new ServerBootstrap()
.channel(NioServerSocketChannel.class)
.group(bossLoopGroup, eventLoopGroup)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel channel) {
channel.attr(VotifierSession.KEY).set(new VotifierSession());
channel.attr(VotifierPlugin.KEY).set(plugin);
channel.pipeline().addLast("greetingHandler", new VotifierGreetingHandler());
channel.pipeline().addLast("protocolDifferentiator", new VotifierProtocolDifferentiator(false, !v1Disable));
channel.pipeline().addLast("voteHandler", new VoteInboundHandler(plugin));
}
})
.bind(host, port)
.addListener((ChannelFutureListener) future -> {
if (future.isSuccess()) {
serverChannel = future.channel();
plugin.getPluginLogger().info("Votifier enabled on socket " + serverChannel.localAddress() + ".");
error.accept(null);
} else {
SocketAddress socketAddress = future.channel().localAddress();
if (socketAddress == null) {
socketAddress = new InetSocketAddress(host, port);
}
plugin.getPluginLogger().error("Votifier was not able to bind to " + socketAddress.toString(), future.cause());
error.accept(future.cause());
}
});
}
public Bootstrap client() {
return new Bootstrap()
.channel(NioSocketChannel.class)
.group(eventLoopGroup);
}
public void shutdown() {
if (serverChannel != null) {
try {
serverChannel.close().syncUninterruptibly();
} catch (Exception e) {
plugin.getPluginLogger().error("Unable to shutdown server channel", e);
}
}
eventLoopGroup.shutdownGracefully();
bossLoopGroup.shutdownGracefully();
}
}
@@ -1,5 +1,6 @@
package io.ibj.nuvotifier.legacyserver;
package com.vexsoftware.votifier.net;
import com.vexsoftware.votifier.util.TokenUtil;
import io.netty.util.AttributeKey;
public class VotifierSession {
@@ -39,14 +40,8 @@ public class VotifierSession {
}
public enum ProtocolVersion {
UNKNOWN("unknown"),
ONE("protocol v1"),
TWO("protocol v2"),
TEST("test");
public final String humanReadable;
ProtocolVersion(String hr) {
this.humanReadable = hr;
}
UNKNOWN,
ONE,
TWO
}
}
@@ -1,15 +1,15 @@
package io.ibj.nuvotifier.legacyserver.protocol;
package com.vexsoftware.votifier.net.protocol;
import com.google.gson.JsonObject;
import io.ibj.nuvotifier.legacyserver.GsonInst;
import io.ibj.nuvotifier.legacyserver.VotifierSession;
import io.ibj.nuvotifier.model.NuVote;
import io.ibj.nuvotifier.platform.VoteHandler;
import com.vexsoftware.votifier.VoteHandler;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.net.VotifierSession;
import com.vexsoftware.votifier.util.GsonInst;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class VoteInboundHandler extends SimpleChannelInboundHandler<NuVote> {
public class VoteInboundHandler extends SimpleChannelInboundHandler<Vote> {
private final VoteHandler handler;
public VoteInboundHandler(VoteHandler handler) {
@@ -17,10 +17,10 @@ public class VoteInboundHandler extends SimpleChannelInboundHandler<NuVote> {
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, final NuVote vote) throws Exception {
protected void channelRead0(ChannelHandlerContext ctx, final Vote vote) throws Exception {
VotifierSession session = ctx.channel().attr(VotifierSession.KEY).get();
handler.onVote(vote);
handler.onVoteReceived(ctx.channel(), vote, session.getVersion());
session.completeVote();
if (session.getVersion() == VotifierSession.ProtocolVersion.ONE) {
@@ -36,7 +36,7 @@ public class VoteInboundHandler extends SimpleChannelInboundHandler<NuVote> {
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
VotifierSession session = ctx.channel().attr(VotifierSession.KEY).get();
handler.onError(cause, session.hasCompletedVote(), ctx.channel().remoteAddress().toString());
handler.onError(ctx.channel(), session.hasCompletedVote(), cause);
if (session.getVersion() == VotifierSession.ProtocolVersion.TWO) {
JsonObject object = new JsonObject();
@@ -1,6 +1,6 @@
package io.ibj.nuvotifier.legacyserver.protocol;
package com.vexsoftware.votifier.net.protocol;
import io.ibj.nuvotifier.legacyserver.VotifierSession;
import com.vexsoftware.votifier.net.VotifierSession;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
@@ -1,8 +1,8 @@
package io.ibj.nuvotifier.legacyserver.protocol;
package com.vexsoftware.votifier.net.protocol;
import io.ibj.nuvotifier.legacyserver.protocol.v1crypto.RSA;
import io.ibj.nuvotifier.model.NuVote;
import io.ibj.nuvotifier.platform.V1KeyProvider;
import com.vexsoftware.votifier.platform.VotifierPlugin;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.net.protocol.v1crypto.RSA;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
@@ -15,13 +15,6 @@ import java.util.List;
* Decodes original protocol votes.
*/
public class VotifierProtocol1Decoder extends ByteToMessageDecoder {
private final V1KeyProvider keyProvider;
public VotifierProtocol1Decoder(V1KeyProvider keyProvider) {
this.keyProvider = keyProvider;
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> list) {
if (buf.readableBytes() < 256) {
@@ -33,8 +26,10 @@ public class VotifierProtocol1Decoder extends ByteToMessageDecoder {
// "Drain" the whole buffer
buf.readerIndex(buf.readableBytes());
VotifierPlugin plugin = ctx.channel().attr(VotifierPlugin.KEY).get();
try {
block = RSA.decrypt(block, keyProvider.getPrivateKey());
block = RSA.decrypt(block, plugin.getProtocolV1Key().getPrivate());
} catch (Exception e) {
throw new CorruptedFrameException("Could not decrypt data (is your key correct?)", e);
}
@@ -51,7 +46,7 @@ public class VotifierProtocol1Decoder extends ByteToMessageDecoder {
}
// Create the vote.
NuVote vote = NuVote.createLegacy(split[1], split[2], split[3], split[4]);
Vote vote = new Vote(split[1], split[2], split[3], split[4]);
list.add(vote);
// We are done, remove ourselves. Why? Sometimes, we will decode multiple vote messages.
@@ -1,10 +1,10 @@
package io.ibj.nuvotifier.legacyserver.protocol;
package com.vexsoftware.votifier.net.protocol;
import com.google.gson.JsonObject;
import io.ibj.nuvotifier.legacyserver.GsonInst;
import io.ibj.nuvotifier.legacyserver.VotifierSession;
import io.ibj.nuvotifier.platform.V2TokenProvider;
import com.vexsoftware.votifier.platform.VotifierPlugin;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.net.VotifierSession;
import com.vexsoftware.votifier.util.GsonInst;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.MessageToMessageDecoder;
@@ -23,12 +23,6 @@ import java.util.UUID;
public class VotifierProtocol2Decoder extends MessageToMessageDecoder<String> {
private static final SecureRandom RANDOM = new SecureRandom();
private final V2TokenProvider tokenProvider;
public VotifierProtocol2Decoder(V2TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
@Override
protected void decode(ChannelHandlerContext ctx, String s, List<Object> list) throws Exception {
JsonObject voteMessage = GsonInst.gson.fromJson(s, JsonObject.class);
@@ -43,9 +37,16 @@ public class VotifierProtocol2Decoder extends MessageToMessageDecoder<String> {
throw new CorruptedFrameException("Challenge is not valid");
}
String serviceName = votePayload.get("serviceName").getAsString();
// Get the key for the given service.
Key key = tokenProvider.getKeyForService(serviceName);
// Verify that we have keys available.
VotifierPlugin plugin = ctx.channel().attr(VotifierPlugin.KEY).get();
Key key = plugin.getTokens().get(votePayload.get("serviceName").getAsString());
if (key == null) {
key = plugin.getTokens().get("default");
if (key == null) {
throw new RuntimeException("Unknown service '" + votePayload.get("serviceName").getAsString() + "'");
}
}
// Verify signature.
String sigHash = voteMessage.get("signature").getAsString();
@@ -60,16 +61,12 @@ public class VotifierProtocol2Decoder extends MessageToMessageDecoder<String> {
UUID.fromString(votePayload.get("uuid").getAsString());
}
String username = votePayload.get("username").getAsString();
if (username.length() > 16) {
if (votePayload.get("username").getAsString().length() > 16) {
throw new CorruptedFrameException("Username too long");
}
String address = votePayload.get("address").getAsString();
long timestamp = votePayload.get("timestamp").getAsLong();
// Create the vote.
Vote vote = new Vote(serviceName, username, address, Long.toString(timestamp));
Vote vote = new Vote(votePayload);
list.add(vote);
ctx.pipeline().remove(this);
@@ -1,8 +1,6 @@
package io.ibj.nuvotifier.legacyserver.protocol;
package com.vexsoftware.votifier.net.protocol;
import io.ibj.nuvotifier.legacyserver.VotifierSession;
import io.ibj.nuvotifier.platform.V1KeyProvider;
import io.ibj.nuvotifier.platform.V2TokenProvider;
import com.vexsoftware.votifier.net.VotifierSession;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
@@ -20,14 +18,11 @@ import java.util.List;
public class VotifierProtocolDifferentiator extends ByteToMessageDecoder {
private static final short PROTOCOL_2_MAGIC = 0x733A;
private final boolean testMode;
private final boolean allowv1;
private final V1KeyProvider v1;
private final V2TokenProvider v2;
public VotifierProtocolDifferentiator(V1KeyProvider v1, V2TokenProvider v2, boolean testMode) {
public VotifierProtocolDifferentiator(boolean testMode, boolean allowv1) {
this.testMode = testMode;
this.v1 = v1;
this.v2 = v2;
this.allowv1 = allowv1;
}
@Override
@@ -48,28 +43,24 @@ public class VotifierProtocolDifferentiator extends ByteToMessageDecoder {
VotifierSession session = ctx.channel().attr(VotifierSession.KEY).get();
if (readMagic == PROTOCOL_2_MAGIC) {
if (v2 == null) {
throw new CorruptedFrameException("This server only accepts v1 Votifier packets.");
}
// Short 0x733A + Message = Protocol v2 Vote
session.setVersion(VotifierSession.ProtocolVersion.TWO);
if (!testMode) {
ctx.pipeline().addAfter("protocolDifferentiator", "protocol2LengthDecoder", new LengthFieldBasedFrameDecoder(1024, 2, 2, 0, 4));
ctx.pipeline().addAfter("protocol2LengthDecoder", "protocol2StringDecoder", new StringDecoder(StandardCharsets.UTF_8));
ctx.pipeline().addAfter("protocol2StringDecoder", "protocol2VoteDecoder", new VotifierProtocol2Decoder(v2));
ctx.pipeline().addAfter("protocol2StringDecoder", "protocol2VoteDecoder", new VotifierProtocol2Decoder());
ctx.pipeline().addAfter("protocol2VoteDecoder", "protocol2StringEncoder", new StringEncoder(StandardCharsets.UTF_8));
ctx.pipeline().remove(this);
}
} else {
if (v1 == null) {
if (!allowv1) {
throw new CorruptedFrameException("This server only accepts well-formed Votifier v2 packets.");
}
// Probably Protocol v1 Vote Message
session.setVersion(VotifierSession.ProtocolVersion.ONE);
if (!testMode) {
ctx.pipeline().addAfter("protocolDifferentiator", "protocol1Handler", new VotifierProtocol1Decoder(v1));
ctx.pipeline().addAfter("protocolDifferentiator", "protocol1Handler", new VotifierProtocol1Decoder());
ctx.pipeline().remove(this);
}
}
@@ -16,7 +16,7 @@
* along with Votifier. If not, see <http://www.gnu.org/licenses/>.
*/
package io.ibj.nuvotifier.legacyserver.protocol.v1crypto;
package com.vexsoftware.votifier.net.protocol.v1crypto;
import javax.crypto.Cipher;
import java.security.PrivateKey;
@@ -16,7 +16,7 @@
* along with Votifier. If not, see <http://www.gnu.org/licenses/>.
*/
package io.ibj.nuvotifier.legacyserver.protocol.v1crypto;
package com.vexsoftware.votifier.net.protocol.v1crypto;
import java.io.IOException;
import java.util.Base64;
@@ -16,7 +16,7 @@
* along with Votifier. If not, see <http://www.gnu.org/licenses/>.
*/
package io.ibj.nuvotifier.legacyserver.protocol.v1crypto;
package com.vexsoftware.votifier.net.protocol.v1crypto;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
@@ -1,4 +1,4 @@
package io.ibj.nuvotifier.multiplatform;
package com.vexsoftware.votifier.platform;
public interface BackendServer {
String getName();
@@ -1,4 +1,6 @@
package io.ibj.nuvotifier.platform;
package com.vexsoftware.votifier.platform;
import com.vexsoftware.votifier.platform.LoggingAdapter;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -1,4 +1,4 @@
package io.ibj.nuvotifier.platform;
package com.vexsoftware.votifier.platform;
public interface LoggingAdapter {
void error(String s);
@@ -1,9 +1,9 @@
package io.ibj.nuvotifier.multiplatform;
package com.vexsoftware.votifier.platform;
import java.util.Collection;
import java.util.Optional;
public interface ProxyVotifierPlugin {
public interface ProxyVotifierPlugin extends VotifierPlugin {
Collection<BackendServer> getAllBackendServers();
Optional<BackendServer> getServer(String name);
@@ -0,0 +1,25 @@
package com.vexsoftware.votifier.platform;
import com.vexsoftware.votifier.VoteHandler;
import com.vexsoftware.votifier.platform.scheduler.VotifierScheduler;
import io.netty.util.AttributeKey;
import java.security.Key;
import java.security.KeyPair;
import java.util.Map;
public interface VotifierPlugin extends VoteHandler {
AttributeKey<VotifierPlugin> KEY = AttributeKey.valueOf("votifier_plugin");
Map<String, Key> getTokens();
KeyPair getProtocolV1Key();
LoggingAdapter getPluginLogger();
VotifierScheduler getScheduler();
default boolean isDebug() {
return false;
}
}
@@ -1,4 +1,6 @@
package io.ibj.nuvotifier.platform.scheduler;
package com.vexsoftware.votifier.platform.scheduler;
import com.google.common.base.Preconditions;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
@@ -8,9 +10,7 @@ public class ScheduledExecutorServiceVotifierScheduler implements VotifierSchedu
private final ScheduledExecutorService service;
public ScheduledExecutorServiceVotifierScheduler(ScheduledExecutorService service) {
if (service == null)
throw new NullPointerException("Service is null");
this.service = service;
this.service = Preconditions.checkNotNull(service, "service");
}
@Override
@@ -1,4 +1,4 @@
package io.ibj.nuvotifier.platform.scheduler;
package com.vexsoftware.votifier.platform.scheduler;
public interface ScheduledVotifierTask {
void cancel();
@@ -1,4 +1,4 @@
package io.ibj.nuvotifier.platform.scheduler;
package com.vexsoftware.votifier.platform.scheduler;
import java.util.concurrent.TimeUnit;
@@ -0,0 +1,46 @@
package com.vexsoftware.votifier.support.forwarding;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.vexsoftware.votifier.model.Vote;
import java.io.CharArrayReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public abstract class AbstractPluginMessagingForwardingSink implements ForwardingVoteSink {
public AbstractPluginMessagingForwardingSink(ForwardedVoteListener listener) {
this.listener = listener;
}
private final ForwardedVoteListener listener;
public void handlePluginMessage(byte[] message) {
String strMessage = new String(message, StandardCharsets.UTF_8);
try (CharArrayReader reader = new CharArrayReader(strMessage.toCharArray())) {
JsonReader r = new JsonReader(reader);
r.setLenient(true);
while (r.peek() != JsonToken.END_DOCUMENT) {
r.beginObject();
JsonObject o = new JsonObject();
while (r.hasNext()) {
String name = r.nextName();
if (r.peek() == JsonToken.NUMBER)
o.add(name, new JsonPrimitive(r.nextLong()));
else
o.add(name, new JsonPrimitive(r.nextString()));
}
r.endObject();
Vote v = new Vote(o);
listener.onForward(v);
}
} catch (IOException e) {
e.printStackTrace(); // Should never happen.
}
}
}
@@ -0,0 +1,156 @@
package com.vexsoftware.votifier.support.forwarding;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.platform.BackendServer;
import com.vexsoftware.votifier.platform.ProxyVotifierPlugin;
import com.vexsoftware.votifier.support.forwarding.cache.FileVoteCache;
import com.vexsoftware.votifier.support.forwarding.cache.VoteCache;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
public abstract class AbstractPluginMessagingForwardingSource implements ForwardingVoteSource {
public AbstractPluginMessagingForwardingSource(String channel, ServerFilter serverFilter, ProxyVotifierPlugin plugin, VoteCache cache, int dumpRate) {
this.channel = channel;
this.plugin = plugin;
this.cache = cache;
this.serverFilter = serverFilter;
this.dumpRate = dumpRate;
}
protected AbstractPluginMessagingForwardingSource(String channel, ProxyVotifierPlugin plugin, VoteCache voteCache, int dumpRate) {
this(channel, null, plugin, voteCache, dumpRate);
}
protected final ProxyVotifierPlugin plugin;
protected final String channel;
protected final VoteCache cache;
protected final ServerFilter serverFilter;
private final int dumpRate;
@Override
public void forward(Vote v) {
byte[] rawData = v.serialize().toString().getBytes(StandardCharsets.UTF_8);
for (BackendServer server : plugin.getAllBackendServers()) {
if (!serverFilter.isAllowed(server.getName())) continue;
if (!forwardSpecific(server, rawData)) attemptToAddToCache(v, server.getName());
}
}
protected boolean forwardSpecific(BackendServer connection, Vote vote) {
byte[] rawData = vote.serialize().toString().getBytes(StandardCharsets.UTF_8);
return forwardSpecific(connection, rawData);
}
protected boolean forwardSpecific(BackendServer connection, Collection<Vote> votes) {
StringBuilder data = new StringBuilder();
for (Vote v : votes) {
data.append(v.serialize().toString());
}
return forwardSpecific(connection, data.toString().getBytes(StandardCharsets.UTF_8));
}
private boolean forwardSpecific(BackendServer connection, byte[] data) {
return connection.sendPluginMessage(channel, data);
}
@Override
public void halt() {
if (cache instanceof FileVoteCache) {
try {
((FileVoteCache) cache).halt();
} catch (IOException e) {
plugin.getPluginLogger().error("Unable to save cached votes, votes will be lost.", e);
}
}
}
protected void onServerConnect(BackendServer server) {
if (cache == null) return;
final Collection<Vote> cachedVotes = cache.evict(server.getName());
dumpVotesToServer(cachedVotes, server, "server '" + server + "'", failedVotes -> {
for (Vote v : failedVotes)
cache.addToCache(v, server.getName());
});
}
protected void attemptToAddToCache(Vote v, String server) {
if (cache != null) {
cache.addToCache(v, server);
if (plugin.isDebug())
plugin.getPluginLogger().info("Added to forwarding cache: " + v + " -> " + server);
} else if (plugin.isDebug())
plugin.getPluginLogger().error("Could not immediately send vote to backend, vote lost! " + v + " -> " + server);
}
protected void attemptToAddToPlayerCache(Vote v, String player) {
if (cache != null) {
cache.addToCachePlayer(v, player);
if (plugin.isDebug())
plugin.getPluginLogger().info("Added to forwarding cache: " + v + " -> (player) " + player);
} else if (plugin.isDebug())
plugin.getPluginLogger().error("Could not immediately send vote to backend, vote lost! " + v + " -> (player) " + player);
}
// returns a collection of failed votes
private void dumpVotesToServer(Collection<Vote> cachedVotes, BackendServer target, String identifier, Consumer<Collection<Vote>> cb) {
dumpVotesToServer(cachedVotes, target, identifier, 0, cb);
}
private void dumpVotesToServer(Collection<Vote> cachedVotes, BackendServer target, String identifier, int evictedAlready, Consumer<Collection<Vote>> cb) {
if (!cachedVotes.isEmpty()) {
plugin.getScheduler().delayedOnPool(() -> {
int evicted = 0;
Iterator<Vote> vi = cachedVotes.iterator();
Collection<Vote> chunk = new ArrayList<>(dumpRate);
while (vi.hasNext() && evicted < dumpRate) {
chunk.add(vi.next());
vi.remove();
}
if (forwardSpecific(target, chunk)) {
evicted += chunk.size();
if (evicted >= dumpRate && !cachedVotes.isEmpty()) {
// if we evicted everything we could but still need to evict more
dumpVotesToServer(cachedVotes, target, identifier, evictedAlready + evicted, cb);
return;
}
} else {
// so since our forwarding failed, break like we are done
cachedVotes.addAll(chunk);
}
if (plugin.isDebug()) {
plugin.getPluginLogger().info("Successfully evicted " + (evictedAlready + evicted) + " votes to " + identifier + ".");
if (!cachedVotes.isEmpty()) {
plugin.getPluginLogger().info("Held " + cachedVotes.size() + " votes for " + identifier + ".");
}
}
// cachedVotes contains any votes which have yet to be evicted
cb.accept(cachedVotes);
}, 1, TimeUnit.SECONDS);
} else {
cb.accept(cachedVotes);
}
}
protected void handlePlayerSwitch(BackendServer server, String playerName) {
if (cache == null) return;
if (!serverFilter.isAllowed(server.getName())) return;
final Collection<Vote> cachedVotes = cache.evictPlayer(playerName);
dumpVotesToServer(cachedVotes, server, "player '" + playerName + "'", failedVotes -> {
for (Vote v : failedVotes)
cache.addToCachePlayer(v, playerName);
});
}
}
@@ -0,0 +1,17 @@
package com.vexsoftware.votifier.support.forwarding;
import com.vexsoftware.votifier.model.Vote;
/**
* Represents a method to call when a vote is forwarded through the {@link ForwardingVoteSink}.
*/
public interface ForwardedVoteListener {
/**
* Called whenever a vote is forwarded from a {@link ForwardingVoteSink}
*
* @param v Vote that was forwarded
*/
void onForward(Vote v);
}
@@ -0,0 +1,13 @@
package com.vexsoftware.votifier.support.forwarding;
/**
* Represents a method at which to receive forwarded votes.
*/
public interface ForwardingVoteSink {
/**
* Stop or close any oustanding network interfaces. Occurs on the onDisable method of a plugin.
*/
void halt();
}
@@ -0,0 +1,22 @@
package com.vexsoftware.votifier.support.forwarding;
import com.vexsoftware.votifier.model.Vote;
/**
* Represents a source for forwarding votes through to servers.
*/
public interface ForwardingVoteSource {
/**
* Forwards a vote to all servers set up to receive votes.
*
* @param v Vote to forward to servers
*/
void forward(Vote v);
/**
* Stop or close any oustanding network interfaces. Occurs on the onDisable method of a plugin.
*/
void halt();
}
@@ -0,0 +1,26 @@
package com.vexsoftware.votifier.support.forwarding;
import java.util.Collection;
import java.util.Collections;
public class ServerFilter {
public ServerFilter(Collection<String> names, boolean whitelist) {
if (names == null)
names = Collections.EMPTY_SET;
this.names = names;
this.whitelist = whitelist;
}
public ServerFilter() {
this(Collections.EMPTY_SET, true);
}
private final Collection<String> names;
private final boolean whitelist;
public boolean isAllowed(String name) {
return names.contains(name) == whitelist;
}
}
@@ -0,0 +1,164 @@
package com.vexsoftware.votifier.support.forwarding.cache;
import com.google.common.io.Files;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.platform.LoggingAdapter;
import com.vexsoftware.votifier.platform.VotifierPlugin;
import com.vexsoftware.votifier.platform.scheduler.ScheduledVotifierTask;
import com.vexsoftware.votifier.util.GsonInst;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeUnit;
public class FileVoteCache extends MemoryVoteCache {
private final LoggingAdapter l;
private final File cacheFile;
private final ScheduledVotifierTask saveTask;
public FileVoteCache(int initialMemorySize, final VotifierPlugin plugin, File cacheFile, long voteTTL) throws IOException {
super(initialMemorySize, plugin, voteTTL);
this.cacheFile = cacheFile;
this.l = plugin.getPluginLogger();
load();
saveTask = plugin.getScheduler().repeatOnPool(() -> {
try {
save();
} catch (IOException e) {
l.error("Unable to save cached votes, votes will be lost if you restart.", e);
}
}, 3, 3, TimeUnit.MINUTES);
}
private static Set<String> keySet(JsonObject object) {
Set<String> set = new HashSet<>();
for (Map.Entry<String, JsonElement> entry : object.entrySet()) {
set.add(entry.getKey());
}
return set;
}
private void load() throws IOException {
// Load the cache from disk
JsonObject object;
try (BufferedReader reader = Files.newReader(cacheFile, StandardCharsets.UTF_8)) {
object = GsonInst.gson.fromJson(reader, JsonObject.class);
} catch (FileNotFoundException e) {
object = new JsonObject();
}
boolean resave = false;
// First, lets figure out if we are converting from a pre2.3.6 cache
if (!object.has("players") || !object.has("servers") || !object.has("version") || object.size() != 3) {
JsonObject oldObject = object;
object = new JsonObject();
object.add("servers", oldObject);
object.add("players", new JsonObject());
object.addProperty("version", 2);
resave = true;
}
if (object.get("version").getAsInt() != 2)
throw new IllegalStateException("Could not read cache file! Unknown version '" + object.get("version").getAsInt() + "' read.");
JsonObject players = object.getAsJsonObject("players");
JsonObject servers = object.getAsJsonObject("servers");
for (String player : keySet(players)) {
playerVoteCache.putAll(player, readVotes(players.getAsJsonArray(player)));
}
for (String server : keySet(servers)) {
voteCache.putAll(server, readVotes(servers.getAsJsonArray(server)));
}
if (resave) {
File replacementFile;
for (int i = 0; ; i++) {
replacementFile = new File(cacheFile.getParentFile(), cacheFile.getName() + ".bak." + i);
if (!replacementFile.exists())
break;
}
if (!cacheFile.renameTo(replacementFile)) {
l.error("Backup movement failed! Will not save.");
return;
}
l.warn("Saving new vote cache format to file - backup moved to " + replacementFile.getAbsolutePath());
save();
}
}
private Collection<Vote> readVotes(JsonArray voteArray) {
List<Vote> votes = new ArrayList<>(voteArray.size());
for (int i = 0; i < voteArray.size(); i++) {
JsonObject voteObject = voteArray.get(i).getAsJsonObject();
Vote v = new Vote(voteObject);
if (hasTimedOut(v))
l.warn("Purging out of date vote.", v);
else
votes.add(v);
}
return votes;
}
public void save() throws IOException {
cacheLock.lock();
JsonObject votesObject = new JsonObject();
votesObject.addProperty("version", 2);
try {
votesObject.add("players", serializeMap(playerVoteCache.asMap()));
votesObject.add("servers", serializeMap(voteCache.asMap()));
} finally {
cacheLock.unlock();
}
try (BufferedWriter writer = Files.newWriter(cacheFile, StandardCharsets.UTF_8)) {
GsonInst.gson.toJson(votesObject, writer);
}
}
public JsonObject serializeMap(Map<String, Collection<Vote>> map) {
JsonObject o = new JsonObject();
Iterator<Map.Entry<String, Collection<Vote>>> entryItr = map.entrySet().iterator();
while (entryItr.hasNext()) {
Map.Entry<String, Collection<Vote>> entry = entryItr.next();
JsonArray array = new JsonArray();
Iterator<Vote> voteItr = entry.getValue().iterator();
while (voteItr.hasNext()) {
Vote vote = voteItr.next();
// if the vote is no longer valid, notify and remove
if (hasTimedOut(vote)) {
l.warn("Purging out of date vote.", vote);
voteItr.remove();
} else {
array.add(vote.serialize());
}
}
// if, during our iteration, we TTL invalidated all of the votes
if (entry.getValue().isEmpty())
entryItr.remove();
o.add(entry.getKey(), array);
}
return o;
}
public void halt() throws IOException {
saveTask.cancel();
save();
}
}
@@ -0,0 +1,120 @@
package com.vexsoftware.votifier.support.forwarding.cache;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.platform.LoggingAdapter;
import com.vexsoftware.votifier.platform.VotifierPlugin;
import com.vexsoftware.votifier.platform.scheduler.ScheduledVotifierTask;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by Joe Hirschfeld on 10/20/2015.
*/
public class MemoryVoteCache implements VoteCache {
private final LoggingAdapter l;
private final long voteTTL;
protected final Multimap<String, Vote> voteCache;
protected final Multimap<String, Vote> playerVoteCache;
protected final ReentrantLock cacheLock = new ReentrantLock();
private final ScheduledVotifierTask sweepTask;
public MemoryVoteCache(int initialSize, VotifierPlugin p, long voteTTL) {
voteCache = HashMultimap.create(initialSize, 10); // 10 is a complete guess. I have no idea .-.
playerVoteCache = HashMultimap.create();
this.voteTTL = voteTTL;
this.l = p.getPluginLogger();
this.sweepTask = p.getScheduler().repeatOnPool(this::sweep, 12, 12, TimeUnit.HOURS);
}
@Override
public Collection<String> getCachedServers() {
cacheLock.lock();
try {
return ImmutableList.copyOf(voteCache.keySet());
} finally {
cacheLock.unlock();
}
}
@Override
public void addToCache(Vote v, String server) {
if (server == null) throw new NullPointerException();
cacheLock.lock();
try {
voteCache.put(server, v);
} finally {
cacheLock.unlock();
}
}
@Override
public void addToCachePlayer(Vote v, String player) {
if (player == null) throw new NullPointerException();
cacheLock.lock();
try {
playerVoteCache.put(player, v);
} finally {
cacheLock.unlock();
}
}
@Override
public Collection<Vote> evictPlayer(String player) {
if (player == null) throw new NullPointerException();
cacheLock.lock();
try {
return new HashSet<>(playerVoteCache.removeAll(player));
} finally {
cacheLock.unlock();
}
}
@Override
public Collection<Vote> evict(String server) {
if (server == null) throw new NullPointerException();
cacheLock.lock();
try {
return new HashSet<>(voteCache.removeAll(server));
} finally {
cacheLock.unlock();
}
}
public void sweep() {
cacheLock.lock();
try {
sweep(voteCache);
sweep(playerVoteCache);
} finally {
cacheLock.unlock();
}
}
private void sweep(Multimap<?, Vote> m) {
Iterator<Vote> vi = m.values().iterator();
while (vi.hasNext()) {
Vote v = vi.next();
if (hasTimedOut(v)) {
l.warn("Purging out of date vote.", v);
vi.remove();
}
}
}
protected boolean hasTimedOut(Vote v) {
if (voteTTL == -1) return false;
// scale voteTTL to milliseconds
return v.getLocalTimestamp() + voteTTL * 24 * 60 * 60 * 1000 < System.currentTimeMillis();
}
}
@@ -0,0 +1,56 @@
package com.vexsoftware.votifier.support.forwarding.cache;
import com.vexsoftware.votifier.model.Vote;
import java.util.Collection;
import java.util.Collections;
/**
* Represents a method of caching votes for forwarding to a server that it was not previously capable of sending.
*/
public interface VoteCache {
/**
* Returns the names of all server in which the cache is holding votes for
*
* @return Collection of all cached servers
*/
Collection<String> getCachedServers();
/**
* Adds a vote to the vote cache for later sending
*
* @param v Vote to add to cache
* @param server Server to add vote under
*/
void addToCache(Vote v, String server);
/**
* Adds a vote to the vote cache for later sending, specifically keyed to a player. This is for the instance when the
* player is rewarded, however is not online any servers able to recieve said vote.
*
* @param v Vote to add to the cache
* @param player Server to add vote under
*/
default void addToCachePlayer(Vote v, String player) {
}
/**
* Evicts all votes from the vote cache and returns a collection of vote objects
*
* @param server Server name of which to evict the votes from the cache
* @return A collection of all votes which were previously in the cache under the server
*/
Collection<Vote> evict(String server);
/**
* Evicts all votes from the vote cache for a specific player not assigned to a server and returns a collection of
* vote objects
*
* @param player Player name of the votes to evict from the cache
* @return A collection of all votes which were previously in the cache under the player
*/
default Collection<Vote> evictPlayer(String player) {
return Collections.EMPTY_LIST;
}
}
@@ -0,0 +1,128 @@
package com.vexsoftware.votifier.support.forwarding.proxy;
import com.vexsoftware.votifier.platform.VotifierPlugin;
import com.vexsoftware.votifier.support.forwarding.ForwardingVoteSource;
import com.vexsoftware.votifier.support.forwarding.cache.VoteCache;
import com.vexsoftware.votifier.support.forwarding.proxy.client.VotifierProtocol2Encoder;
import com.vexsoftware.votifier.support.forwarding.proxy.client.VotifierProtocol2HandshakeHandler;
import com.vexsoftware.votifier.support.forwarding.proxy.client.VotifierResponseHandler;
import com.vexsoftware.votifier.model.Vote;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.timeout.ReadTimeoutHandler;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
public class ProxyForwardingVoteSource implements ForwardingVoteSource {
private static final int MAX_RETRIES = 5;
private final VotifierPlugin plugin;
private final Supplier<Bootstrap> nettyBootstrap;
private final List<BackendServer> backendServers;
private final VoteCache voteCache;
private static final StringDecoder STRING_DECODER = new StringDecoder(StandardCharsets.US_ASCII);
public ProxyForwardingVoteSource(VotifierPlugin plugin, Supplier<Bootstrap> nettyBootstrap, List<BackendServer> backendServers, VoteCache voteCache) {
this.plugin = plugin;
this.nettyBootstrap = nettyBootstrap;
this.backendServers = backendServers;
this.voteCache = voteCache;
}
@Override
public void forward(Vote v) {
for (final BackendServer server : backendServers) {
forwardVote(server, v, 0);
}
}
private int fib(int t) {
if (t <= 1) return 1;
return fib(t - 2) + fib(t - 1);
}
private void forwardVote(final BackendServer server, final Vote v, final int tries) {
nettyBootstrap.get()
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel channel) {
channel.pipeline().addLast(new DelimiterBasedFrameDecoder(256, true, Delimiters.lineDelimiter()));
channel.pipeline().addLast(new ReadTimeoutHandler(5, TimeUnit.SECONDS));
channel.pipeline().addLast(STRING_DECODER);
channel.pipeline().addLast(new VotifierProtocol2Encoder(server.key));
channel.pipeline().addLast(new VotifierProtocol2HandshakeHandler(v, new VotifierResponseHandler() {
@Override
public void onSuccess() {
if (plugin.isDebug()) {
plugin.getPluginLogger().info("Successfully forwarded vote " + v + " to " + server.address + ".");
}
}
@Override
public void onFailure(Throwable error) {
handleFailure(server, v, error, tries);
}
}, plugin));
}
})
.connect(server.address)
.addListener((ChannelFutureListener) future -> {
if (!future.isSuccess()) {
handleFailure(server, v, future.cause(), tries);
}
});
}
private void handleFailure(final BackendServer server, final Vote v, Throwable cause, final int tries) {
int nextDelay = fib(tries + 1);
boolean willRetry = tries < MAX_RETRIES;
String msg = "Unable to send vote to " + server.address + ".";
if (willRetry) {
msg += " Will retry sending in " + nextDelay + " second(s).";
} else {
if (voteCache == null) {
msg += " This vote will be lost!";
} else {
voteCache.addToCache(v, server.name);
}
}
if (plugin.isDebug()) {
plugin.getPluginLogger().error(msg, cause);
} else {
plugin.getPluginLogger().error(msg);
}
if (willRetry) {
plugin.getScheduler().delayedOnPool(() -> forwardVote(server, v, tries + 1), nextDelay, TimeUnit.SECONDS);
}
}
@Override
public void halt() {
}
public static class BackendServer {
private final String name;
private final InetSocketAddress address;
private final Key key;
public BackendServer(String name, InetSocketAddress address, Key key) {
this.name = name;
this.address = address;
this.key = key;
}
}
}
@@ -0,0 +1,29 @@
package com.vexsoftware.votifier.support.forwarding.proxy.client;
import com.vexsoftware.votifier.model.Vote;
public class VoteRequest {
private final String challenge;
private final Vote vote;
public VoteRequest(String challenge, Vote vote) {
this.challenge = challenge;
this.vote = vote;
}
public String getChallenge() {
return challenge;
}
public Vote getVote() {
return vote;
}
@Override
public String toString() {
return "VoteRequest{" +
"challenge='" + challenge + '\'' +
", vote=" + vote +
'}';
}
}
@@ -0,0 +1,46 @@
package com.vexsoftware.votifier.support.forwarding.proxy.client;
import com.google.gson.JsonObject;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import javax.crypto.Mac;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Base64;
public class VotifierProtocol2Encoder extends MessageToByteEncoder<VoteRequest> {
private static final short MAGIC = 0x733A;
private final Key key;
public VotifierProtocol2Encoder(Key key) {
this.key = key;
}
@Override
protected void encode(ChannelHandlerContext ctx, VoteRequest req, ByteBuf buf) throws Exception {
JsonObject object = new JsonObject();
JsonObject payloadObject = req.getVote().serialize();
payloadObject.addProperty("challenge", req.getChallenge());
String payload = payloadObject.toString();
object.addProperty("payload", payload);
// Generate the MAC
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(key);
mac.update(payload.getBytes(StandardCharsets.UTF_8));
String computed = Base64.getEncoder().encodeToString(mac.doFinal());
object.addProperty("signature", computed);
// JSON message is ready for encoding.
String finalMessage = object.toString();
buf.writeShort(MAGIC);
buf.writeShort(finalMessage.length());
ByteBuf messageBytes = Unpooled.copiedBuffer(finalMessage, StandardCharsets.UTF_8);
buf.writeBytes(messageBytes);
messageBytes.release();
}
}
@@ -0,0 +1,42 @@
package com.vexsoftware.votifier.support.forwarding.proxy.client;
import com.vexsoftware.votifier.platform.VotifierPlugin;
import com.vexsoftware.votifier.model.Vote;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.CorruptedFrameException;
public class VotifierProtocol2HandshakeHandler extends SimpleChannelInboundHandler<String> {
private final Vote toSend;
private final VotifierResponseHandler responseHandler;
private final VotifierPlugin nuVotifier;
public VotifierProtocol2HandshakeHandler(Vote toSend, VotifierResponseHandler responseHandler, VotifierPlugin nuVotifier) {
this.toSend = toSend;
this.responseHandler = responseHandler;
this.nuVotifier = nuVotifier;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, String s) {
String[] handshakeContents = s.split(" ");
if (handshakeContents.length != 3) {
throw new CorruptedFrameException("Handshake is not valid.");
}
VoteRequest request = new VoteRequest(handshakeContents[2], toSend);
if (nuVotifier.isDebug()) {
nuVotifier.getPluginLogger().info("Sent request: " + request.toString());
}
ctx.writeAndFlush(request);
ctx.pipeline().addLast(new VotifierProtocol2ResponseHandler(responseHandler));
ctx.pipeline().remove(this);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
responseHandler.onFailure(cause);
ctx.close();
}
}
@@ -0,0 +1,33 @@
package com.vexsoftware.votifier.support.forwarding.proxy.client;
import com.google.gson.JsonObject;
import com.vexsoftware.votifier.util.GsonInst;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class VotifierProtocol2ResponseHandler extends SimpleChannelInboundHandler<String> {
private final VotifierResponseHandler responseHandler;
public VotifierProtocol2ResponseHandler(VotifierResponseHandler responseHandler) {
this.responseHandler = responseHandler;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
JsonObject object = GsonInst.gson.fromJson(msg, JsonObject.class);
String status = object.get("status").getAsString();
if (status.equals("ok")) {
responseHandler.onSuccess();
} else {
responseHandler.onFailure(new Exception("Remote server error: " + object.get("cause").getAsString() +
": " + object.get("error").getAsString()));
}
ctx.close();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
responseHandler.onFailure(cause);
ctx.close();
}
}
@@ -0,0 +1,7 @@
package com.vexsoftware.votifier.support.forwarding.proxy.client;
public interface VotifierResponseHandler {
void onSuccess();
void onFailure(Throwable error);
}
@@ -1,8 +1,10 @@
package io.ibj.nuvotifier.legacyserver;
package com.vexsoftware.votifier.util;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
public class GsonInst {
public static final Gson gson = new GsonBuilder().create();
}
@@ -0,0 +1,11 @@
package com.vexsoftware.votifier.util;
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");
}
}
@@ -1,4 +1,4 @@
package io.ibj.nuvotifier.legacyserver;
package com.vexsoftware.votifier.util;
import java.math.BigInteger;
import java.security.SecureRandom;
@@ -0,0 +1,80 @@
package com.vexsoftware.votifier.util.standalone;
import com.vexsoftware.votifier.net.VotifierServerBootstrap;
import com.vexsoftware.votifier.platform.JavaUtilLogger;
import com.vexsoftware.votifier.platform.LoggingAdapter;
import com.vexsoftware.votifier.platform.VotifierPlugin;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.net.VotifierSession;
import com.vexsoftware.votifier.platform.scheduler.ScheduledExecutorServiceVotifierScheduler;
import com.vexsoftware.votifier.platform.scheduler.VotifierScheduler;
import io.netty.channel.Channel;
import java.net.InetSocketAddress;
import java.security.Key;
import java.security.KeyPair;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
import java.util.logging.Logger;
public class StandaloneVotifierPlugin implements VotifierPlugin {
private final Map<String, Key> tokens;
private final VoteReceiver receiver;
private final KeyPair v1Key;
private final InetSocketAddress bind;
private final VotifierScheduler scheduler;
private VotifierServerBootstrap bootstrap;
public StandaloneVotifierPlugin(Map<String, Key> tokens, VoteReceiver receiver, KeyPair v1Key, InetSocketAddress bind) {
this.receiver = receiver;
this.bind = bind;
this.tokens = Collections.unmodifiableMap(new HashMap<>(tokens));
this.v1Key = v1Key;
this.scheduler = new ScheduledExecutorServiceVotifierScheduler(Executors.newScheduledThreadPool(1));
}
public void start() {
start(o -> {});
}
public void start(Consumer<Throwable> error) {
if (bootstrap != null) {
bootstrap.shutdown();
}
this.bootstrap = new VotifierServerBootstrap(bind.getHostString(), bind.getPort(), this, v1Key == null);
this.bootstrap.start(error);
}
@Override
public Map<String, Key> getTokens() {
return tokens;
}
@Override
public KeyPair getProtocolV1Key() {
return v1Key;
}
@Override
public LoggingAdapter getPluginLogger() {
return new JavaUtilLogger(Logger.getAnonymousLogger());
}
@Override
public VotifierScheduler getScheduler() {
return scheduler;
}
@Override
public void onVoteReceived(Channel channel, Vote vote, VotifierSession.ProtocolVersion protocolVersion) throws Exception {
receiver.onVote(vote);
}
@Override
public void onError(Channel channel, boolean alreadyHandledVote, Throwable throwable) {
receiver.onException(throwable);
}
}
@@ -0,0 +1,8 @@
package com.vexsoftware.votifier.util.standalone;
import com.vexsoftware.votifier.model.Vote;
public interface VoteReceiver {
void onVote(Vote vote) throws Exception;
void onException(Throwable throwable);
}
@@ -0,0 +1,57 @@
package com.vexsoftware.votifier.util.standalone;
import com.vexsoftware.votifier.net.protocol.v1crypto.RSAIO;
import com.vexsoftware.votifier.util.KeyCreator;
import java.io.File;
import java.net.InetSocketAddress;
import java.security.Key;
import java.security.KeyPair;
import java.security.interfaces.RSAKey;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class VotifierServerBuilder {
private final Map<String, Key> keyMap = new HashMap<>();
private KeyPair v1Key;
private InetSocketAddress bind;
private VoteReceiver receiver;
public VotifierServerBuilder addToken(String service, String token) {
Objects.requireNonNull(service, "service");
Objects.requireNonNull(token, "key");
keyMap.put(service, KeyCreator.createKeyFrom(token));
return this;
}
public VotifierServerBuilder v1Key(KeyPair v1Key) {
this.v1Key = Objects.requireNonNull(v1Key, "v1Key");
if (!(v1Key.getPrivate() instanceof RSAKey)) {
throw new IllegalArgumentException("Provided key is not an RSA key.");
}
return this;
}
public VotifierServerBuilder v1KeyFolder(File file) throws Exception {
this.v1Key = RSAIO.load(Objects.requireNonNull(file, "file"));
return this;
}
public VotifierServerBuilder bind(InetSocketAddress bind) {
this.bind = Objects.requireNonNull(bind, "bind");
return this;
}
public VotifierServerBuilder receiver(VoteReceiver receiver) {
this.receiver = Objects.requireNonNull(receiver, "receiver");
return this;
}
public StandaloneVotifierPlugin create() {
Objects.requireNonNull(bind, "need an address to bind to");
Objects.requireNonNull(receiver, "need a receiver for votes");
return new StandaloneVotifierPlugin(keyMap, receiver, v1Key, bind);
}
}
@@ -0,0 +1,102 @@
package com.vexsoftware.votifier.net.protocol;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.net.VotifierSession;
import com.vexsoftware.votifier.platform.LoggingAdapter;
import com.vexsoftware.votifier.platform.VotifierPlugin;
import com.vexsoftware.votifier.platform.scheduler.VotifierScheduler;
import com.vexsoftware.votifier.util.KeyCreator;
import io.netty.channel.Channel;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class TestVotifierPlugin implements VotifierPlugin {
private static final byte[] PUBLIC_KEY;
private static final byte[] PRIVATE_KEY;
static byte[] r(String u) throws Exception {
URL resourceUrl = TestVotifierPlugin.class.getResource(u);
Path resourcePath = Paths.get(resourceUrl.toURI());
return Base64.getDecoder().decode(new String(Files.readAllBytes(resourcePath), StandardCharsets.UTF_8));
}
static {
try {
PUBLIC_KEY = r("/public.key");
PRIVATE_KEY = r("/private.key");
} catch (Exception e) {
throw new AssertionError(e);
}
}
public static final TestVotifierPlugin I = new TestVotifierPlugin();
public static TestVotifierPlugin getI() {
return I;
}
private final Map<String, Key> keyMap = new HashMap<>();
private final KeyPair keyPair;
public TestVotifierPlugin() {
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(
PUBLIC_KEY);
PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(
PRIVATE_KEY);
PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
keyPair = new KeyPair(publicKey, privateKey);
} catch (Exception e) {
throw new AssertionError(e);
}
keyMap.put("default", KeyCreator.createKeyFrom("test"));
}
@Override
public Map<String, Key> getTokens() {
return keyMap;
}
@Override
public KeyPair getProtocolV1Key() {
return keyPair;
}
@Override
public LoggingAdapter getPluginLogger() {
return null;
}
@Override
public VotifierScheduler getScheduler() {
return null;
}
public void specificKeysOnly() {
keyMap.clear();
keyMap.put("Test", KeyCreator.createKeyFrom("test"));
}
public void restoreDefault() {
keyMap.clear();
keyMap.put("default", KeyCreator.createKeyFrom("test"));
}
@Override
public void onVoteReceived(Channel channel, Vote vote, VotifierSession.ProtocolVersion protocolVersion) throws Exception {
}
}
@@ -0,0 +1,31 @@
package com.vexsoftware.votifier.net.protocol;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.net.protocol.v1crypto.RSA;
import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.List;
public class VoteUtil {
public static byte[] encodePOJOv1(Vote vote) throws Exception {
return encodePOJOv1(vote, TestVotifierPlugin.getI().getProtocolV1Key().getPublic());
}
public static byte[] encodePOJOv1(Vote vote, PublicKey key) throws Exception {
List<String> ordered = new ArrayList<>();
ordered.add("VOTE");
ordered.add(vote.getServiceName());
ordered.add(vote.getUsername());
ordered.add(vote.getAddress());
ordered.add(vote.getTimeStamp());
StringBuilder builder = new StringBuilder();
for (String s : ordered) {
builder.append(s).append('\n'); // naive join needed!
}
return RSA.encrypt(builder.toString().getBytes(StandardCharsets.UTF_8), key);
}
}
@@ -0,0 +1,89 @@
package com.vexsoftware.votifier.net.protocol;
import com.vexsoftware.votifier.platform.VotifierPlugin;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.net.VotifierSession;
import com.vexsoftware.votifier.net.protocol.v1crypto.RSA;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.CorruptedFrameException;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import static org.junit.Assert.*;
public class VotifierProtocol1DecoderTest {
private static final VotifierSession SESSION = new VotifierSession();
private EmbeddedChannel createChannel() {
EmbeddedChannel channel = new EmbeddedChannel(new VotifierProtocol1Decoder());
channel.attr(VotifierSession.KEY).set(SESSION);
channel.attr(VotifierPlugin.KEY).set(TestVotifierPlugin.getI());
return channel;
}
@Test
public void testSuccessfulDecode() throws Exception {
Vote votePojo = new Vote("Test", "test", "test", "test");
// Send the vote
EmbeddedChannel channel = createChannel();
byte[] encrypted = VoteUtil.encodePOJOv1(votePojo);
ByteBuf encryptedByteBuf = Unpooled.wrappedBuffer(encrypted);
assertTrue(channel.writeInbound(encryptedByteBuf));
Object presumedVote = channel.readInbound();
assertEquals(votePojo, presumedVote);
assertFalse(channel.finish());
}
private void verifyFailure(String bad) throws Exception {
// Send the bad vote
EmbeddedChannel channel = createChannel();
byte[] encrypted = RSA.encrypt(bad.getBytes(StandardCharsets.UTF_8), TestVotifierPlugin.getI().getProtocolV1Key().getPublic());
ByteBuf encryptedByteBuf = Unpooled.wrappedBuffer(encrypted);
try {
channel.writeInbound(encryptedByteBuf);
} finally {
channel.close();
}
}
@Test(expected = CorruptedFrameException.class)
public void testFailureDecodeMissingField() throws Exception {
verifyFailure("VOTE\nTest\ntest\ntest"); // missing field
}
@Test(expected = CorruptedFrameException.class)
public void testFailureDecodeBadOpcode() throws Exception {
verifyFailure("TEST\nTest\ntest\ntest\ntest\n");
}
@Test(expected = CorruptedFrameException.class)
public void testFailureDecodeBadRsa() throws Exception {
// Decode our bad RSA key
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(TestVotifierPlugin.r("/bad_public.key"));
PublicKey badPublicKey = keyFactory.generatePublic(publicKeySpec);
// Send the bad vote
EmbeddedChannel channel = createChannel();
byte[] encrypted = VoteUtil.encodePOJOv1(new Vote("Test", "test", "test", "test"), badPublicKey);
ByteBuf encryptedByteBuf = Unpooled.wrappedBuffer(encrypted);
try {
channel.writeInbound(encryptedByteBuf);
} finally {
channel.close();
}
}
}
@@ -0,0 +1,148 @@
package com.vexsoftware.votifier.net.protocol;
import com.google.gson.JsonObject;
import com.vexsoftware.votifier.platform.VotifierPlugin;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.net.VotifierSession;
import com.vexsoftware.votifier.util.GsonInst;
import com.vexsoftware.votifier.util.KeyCreator;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import org.json.JSONObject;
import org.junit.Test;
import javax.crypto.Mac;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Base64;
import static org.junit.Assert.*;
public class VotifierProtocol2DecoderTest {
private static final VotifierSession SESSION = new VotifierSession();
private EmbeddedChannel createChannel() {
EmbeddedChannel channel = new EmbeddedChannel(new VotifierProtocol2Decoder());
channel.attr(VotifierSession.KEY).set(SESSION);
channel.attr(VotifierPlugin.KEY).set(TestVotifierPlugin.getI());
return channel;
}
private void sendVote(Vote vote, Key key, boolean expectSuccess) throws Exception {
// Create a well-formed request
EmbeddedChannel channel = createChannel();
JSONObject object = new JSONObject();
JsonObject payload = vote.serialize();
payload.addProperty("challenge", SESSION.getChallenge());
String payloadEncoded = GsonInst.gson.toJson(payload);
object.put("payload", payloadEncoded);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(key);
object.put("signature",
Base64.getEncoder().encodeToString(mac.doFinal(payloadEncoded.getBytes(StandardCharsets.UTF_8))));
if (expectSuccess) {
assertTrue(channel.writeInbound(object.toString()));
assertEquals(vote, channel.readInbound());
assertFalse(channel.finish());
} else {
try {
channel.writeInbound(object.toString());
} finally {
channel.close();
}
}
}
@Test
public void testSuccessfulDecode() throws Exception {
sendVote(new Vote("Test", "test", "test", "0"), TestVotifierPlugin.getI().getTokens().get("default"), true);
}
@Test(expected = DecoderException.class)
public void testFailureDecodeBadPacket() {
// Create a well-formed request
EmbeddedChannel channel = createChannel();
Vote vote = new Vote("Test", "test", "test", "0");
JSONObject object = new JSONObject();
JsonObject payload = vote.serialize();
payload.addProperty("challenge", SESSION.getChallenge());
object.put("payload", GsonInst.gson.toJson(payload));
// We "forget" the signature.
try {
channel.writeInbound(object.toString());
} finally {
channel.close();
}
}
@Test(expected = DecoderException.class)
public void testFailureDecodeBadVoteField() throws Exception {
// Create a well-formed request
EmbeddedChannel channel = createChannel();
Vote vote = new Vote("Test", "test", "test", "0");
JSONObject object = new JSONObject();
JsonObject payload = vote.serialize();
String payloadEncoded = GsonInst.gson.toJson(payload);
// We "forget" the challenge.
object.put("payload", payloadEncoded);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(TestVotifierPlugin.getI().getTokens().get("default"));
object.put("signature",
Base64.getEncoder().encodeToString(mac.doFinal(payloadEncoded.getBytes(StandardCharsets.UTF_8))));
try {
channel.writeInbound(object.toString());
} finally {
channel.close();
}
}
@Test(expected = DecoderException.class)
public void testFailureDecodeBadChallenge() throws Exception {
// Create a well-formed request
EmbeddedChannel channel = createChannel();
Vote vote = new Vote("Test", "test", "test", "0");
JSONObject object = new JSONObject();
JsonObject payload = vote.serialize();
// We provide the wrong challenge.
payload.addProperty("challenge", "not a challenge for me");
object.put("payload", payload.toString());
String payloadEncoded = GsonInst.gson.toJson(payload);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(TestVotifierPlugin.getI().getTokens().get("default"));
object.put("signature",
Base64.getEncoder().encode(mac.doFinal(payloadEncoded.getBytes(StandardCharsets.UTF_8))));
try {
channel.writeInbound(object.toString());
} finally {
channel.close();
}
}
@Test(expected = DecoderException.class)
public void testFailureDecodeNonExistentKey() throws Exception {
TestVotifierPlugin.getI().specificKeysOnly();
Vote vote = new Vote("Bad Service", "test", "test", "0");
try {
sendVote(vote, TestVotifierPlugin.getI().getTokens().get("Test"), false);
} finally {
TestVotifierPlugin.getI().restoreDefault();
}
}
@Test(expected = CorruptedFrameException.class)
public void testFailureDecodeBadSignature() throws Exception {
Vote vote = new Vote("Bad Service", "test", "test", "0");
sendVote(vote, KeyCreator.createKeyFrom("BadKey"), false);
}
}
@@ -0,0 +1,95 @@
package com.vexsoftware.votifier.net.protocol;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.net.VotifierSession;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.DecoderException;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class VotifierProtocolDifferentiatorTest {
@Test
public void v1Test() {
EmbeddedChannel channel = new EmbeddedChannel(new VotifierProtocolDifferentiator(true, true));
VotifierSession session = new VotifierSession();
channel.attr(VotifierSession.KEY).set(session);
ByteBuf test = Unpooled.buffer(256);
for (int i = 0; i < 256; i++) {
test.writeByte(0);
}
channel.writeInbound(test);
assertEquals(VotifierSession.ProtocolVersion.ONE, session.getVersion());
channel.close();
}
@Test
public void v2Test() {
EmbeddedChannel channel = new EmbeddedChannel(new VotifierProtocolDifferentiator(true, false));
VotifierSession session = new VotifierSession();
channel.attr(VotifierSession.KEY).set(session);
ByteBuf test = Unpooled.buffer();
test.writeShort(0x733A);
channel.writeInbound(test);
assertEquals(VotifierSession.ProtocolVersion.TWO, session.getVersion());
channel.close();
}
@Test(expected = DecoderException.class)
public void failIfv1NotSupported() {
EmbeddedChannel channel = new EmbeddedChannel(new VotifierProtocolDifferentiator(true, false));
VotifierSession session = new VotifierSession();
channel.attr(VotifierSession.KEY).set(session);
ByteBuf test = Unpooled.buffer(256);
for (int i = 0; i < 256; i++) {
test.writeByte(0);
}
channel.writeInbound(test);
assertEquals(VotifierSession.ProtocolVersion.ONE, session.getVersion());
channel.close();
}
@Test(expected = DecoderException.class)
public void failOnBadPacketTest() {
EmbeddedChannel channel = new EmbeddedChannel(new VotifierProtocolDifferentiator(true, false));
ByteBuf buf = Unpooled.buffer();
for (int i = 0; i < 3; i++) {
buf.writeByte(0);
}
try {
channel.writeInbound(buf);
} finally {
channel.close();
}
}
@Test
public void tryIdentifyRealVotev1() throws Exception {
EmbeddedChannel channel = new EmbeddedChannel(new VotifierProtocolDifferentiator(true, true));
VotifierSession session = new VotifierSession();
channel.attr(VotifierSession.KEY).set(session);
Vote votePojo = new Vote("Test", "test", "test", "test");
byte[] encrypted = VoteUtil.encodePOJOv1(votePojo);
ByteBuf encryptedByteBuf = Unpooled.wrappedBuffer(encrypted);
channel.writeInbound(encryptedByteBuf);
assertEquals(VotifierSession.ProtocolVersion.ONE, session.getVersion());
channel.close();
}
}
@@ -0,0 +1,49 @@
package com.vexsoftware.votifier.support.forwarding;
import com.google.common.collect.Lists;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.support.forwarding.ForwardedVoteListener;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class PMForwardingSinkTest {
@Test
public void testSuccessfulMultiDecode() throws Exception {
List<Vote> receivedVotes = Lists.newArrayList();
ForwardedVoteListener vl = receivedVotes::add;
List<Vote> sentVotes = Lists.newArrayList(
new Vote("serviceA", "usernameA", "1.1.1.1", "1546300800"),
new Vote("serviceB", "usernameBBBBBBB", "1.2.23.4", "1514764800", System.currentTimeMillis())
);
StringBuilder message = new StringBuilder();
for (Vote v : sentVotes) {
message.append(v.serialize());
}
byte[] messageBytes = message.toString().getBytes();
System.out.println(message.toString());
AbstractPluginMessagingForwardingSink sink = new AbstractPluginMessagingForwardingSink(vl) {
@Override
public void halt() {
}
};
sink.handlePluginMessage(messageBytes);
assertEquals(sentVotes.size(), receivedVotes.size());
for (int i = 0; i < receivedVotes.size(); i++) {
assertEquals(sentVotes.get(i), receivedVotes.get(i));
}
}
}
+1
View File
@@ -0,0 +1 @@
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArrOsFuGPoHDZEFRHXriZRqrUXXjUOhVabC0Xu+WPtVbQrvNngN0igo0ZJIZELflhsFNBUJAnREo+wColfDg1rk2juEUYDt+gRzrN3WiqgzZolF3E4Dg4LrXh6cPuu4AeOZW9CtjXt2JwPBhlWLZb2RFhA6FziFLeMBa42/5VLFCQAmiuuU3OCiBskR3Jz/E5zqc7V/j247OMm+WRJ/k8OFTEFB+zFINOZY1ud3ltwkzQIPi1luGNW8Yt8+CEg/6hDiVlpFwgsazt4uKI/UfU5oP0kAKjQUfFGvNaBtJ0RlsBeJ7VDpOfIG6dDb8pe4IaA9aJLVcXWEn30pHsyYl2NwIDAQAB
+1
View File
@@ -0,0 +1 @@
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCd5osoNvoLM52pN8U15QkRXpGVxk0WaoNLWgN24pnW30FIAO3bp8qD2FXJzX2hvw43LIwudVEbwmdzeytVWDNuigLqEsJJsXCHh9ORKjGVHvS240r6idfxJ139Jz2d3oSZG8acHdHgs/H/M98O8lz9Zb2KHtm+rxpiwsV2SdhMtfMDJmRx4p0JklgJ8TV1ZApoypbGtRKXHOirVSs6zfgaYaeglUNITAgsZZNZk+j9Pp572FxyYryxkUaWVn09RAD07XvWKJc3f7NBElA2LpuZXTYJh3BXaI/N05tdgPej5o99PXaA2UMhdqo778zoQoeWzOvbNMUqZROqGW5WDwhdAgMBAAECggEAM8xcXB3k2uXc05UrCaoP1nXadnKt+SuJ1J87wbVGWDm21aMQZF1fzu6Z/fB+RSBCzpC7j8YGjH+ypiyCo6WigaGeoSD0QW6HBr7lBZr81vlnt9NKVnP/tel/KLvFt9M2Vm/JMI+LuRXuzhQ7GVEidjH5AqVqMuaIVlukoVBKN6d20KihcHeCjOZLAoL727QYGrHNr0eH9wQpgMzj3A51ubi5ma3bDvnSvN4ZoP3BhmDTRVnHEh4hx/XFYELi3RDGN2SDjegNXlZ+e03BHCGRryOCuwY1eua78gkrWN8sr3QAdWhEN/Pdwq0BpVsdsbyPzmgoamAMmBbKyBnEJxuvlQKBgQDtLQIhFVYA7vNtpwnXV3ahyEPCNf+tLJYsLYjKWRpoTw6bI3FfgMAEPGbDXLsmLi37mvRmCJpoU9j4nfRd/umoEIW6xuaYDdGRpyaiOlkEXyJd0n3uv/3mF7VtyltrPK3Mt2Gb7kGaZqe07B4a5UC02CCvpKqpbFfC0XxHP32GkwKBgQCqbs2D/pNcdA9iHm+AhX/oylK+BfiruTvLx/c2wOFleQOiNZUZCC6zELIDzuQXPlk9mlNshnciIItcjnyLGkDr2lvJpt6Bi65Jx5hYhlWrms883/hRUSjgKtJ9PFWl8wUVTJJsrOg0fm2ZM+YXpO9AvziRTEPj2MeibQjjlfsbTwKBgHc1ZBiUzMFzWM8IYh3mTC5MpiH7WiMEggZ6bIKn/mnnMoQvcFB8OgbnBQ+0kCxEkcAgxi5yrIk7e5aabbTA51gd1dlnF4fdijQXO4fWo99aob9eiQVzYn4/9GTWjNR9jy2SDnF9c2PTrP92njsOd1NuPh+KOG0nk4TMHhcBhmoZAoGAF4ZPy8ZZ5MsITIzS6zSCIUm7/gqo9lsNC+XAE8iv0+fOPpeSyu+8Mm89gxUNmr+6Zaxlk3dD/mvrkI0CV9j52RQ80yq7T7pOBlUMjDuOm680p7xqdB71u2ttvmeTkuJmczgshd+f9Y715c2qFt1nQNKkJjoWrN6BP8Q5E+Dh1jUCgYEApBuFGwmcSob6rIzinM4nJRyMLXqIy6nKCy8bBLkWCFPGztjIxhKBA/pErADhkLaLyM5reeMPNBbxruAFJ3mzfOneKf9lt0oHq2/jq0vJvYUj9tZjD2Sw6dhj0JqQhKIkhux1rU1xAAedY21JGTS+Be0dFfSv5uDrErPJiN+Qyt0=
+1
View File
@@ -0,0 +1 @@
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAneaLKDb6CzOdqTfFNeUJEV6RlcZNFmqDS1oDduKZ1t9BSADt26fKg9hVyc19ob8ONyyMLnVRG8Jnc3srVVgzbooC6hLCSbFwh4fTkSoxlR70tuNK+onX8Sdd/Sc9nd6EmRvGnB3R4LPx/zPfDvJc/WW9ih7Zvq8aYsLFdknYTLXzAyZkceKdCZJYCfE1dWQKaMqWxrUSlxzoq1UrOs34GmGnoJVDSEwILGWTWZPo/T6ee9hccmK8sZFGllZ9PUQA9O171iiXN3+zQRJQNi6bmV02CYdwV2iPzdObXYD3o+aPfT12gNlDIXaqO+/M6EKHlszr2zTFKmUTqhluVg8IXQIDAQAB
-16
View File
@@ -1,16 +0,0 @@
plugins {
id 'java'
}
group 'io.ibj.nuvotifier'
version '3.0.0-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
testCompile group: 'junit', name: 'junit', version: '4.12'
}
-10
View File
@@ -1,10 +0,0 @@
dependencies {
api project(":model")
api project(":platform")
compile 'com.google.code.gson:gson:2.8.0'
compile 'com.google.guava:guava:24.1-jre'
implementation group: "io.netty", name: "netty-handler", version: "4.1.32.Final"
}
@@ -1,99 +0,0 @@
package io.ibj.nuvotifier.legacyserver;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import io.ibj.nuvotifier.legacyserver.protocol.VoteInboundHandler;
import io.ibj.nuvotifier.legacyserver.protocol.VotifierGreetingHandler;
import io.ibj.nuvotifier.legacyserver.protocol.VotifierProtocolDifferentiator;
import io.ibj.nuvotifier.platform.LoggingAdapter;
import io.ibj.nuvotifier.platform.V1KeyProvider;
import io.ibj.nuvotifier.platform.V2TokenProvider;
import io.ibj.nuvotifier.platform.VoteHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOutboundInvoker;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.concurrent.DefaultPromise;
import io.netty.util.concurrent.ImmediateEventExecutor;
import io.netty.util.concurrent.PromiseCombiner;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
public class VotifierServer implements AutoCloseable {
private NioEventLoopGroup bossLoopGroup;
private NioEventLoopGroup eventLoopGroup;
private Set<Channel> serverChannels;
private final List<InetSocketAddress> bindAddresses;
private final V1KeyProvider v1KeyProvider;
private final V2TokenProvider v2TokenProvider;
private final LoggingAdapter loggingAdapter;
private final boolean testMode;
private final VoteHandler voteHandler;
VotifierServer(VotifierServerBuilder builder) {
this.bindAddresses = builder.bindAddresses;
this.v1KeyProvider = builder.v1KeyProvider;
this.v2TokenProvider = builder.v2TokenProvider;
this.loggingAdapter = builder.loggingAdapter;
this.testMode = builder.testMode;
this.voteHandler = builder.voteHandler;
}
public void start(Consumer<Throwable> error) {
this.bossLoopGroup = new NioEventLoopGroup(1, new ThreadFactoryBuilder().setDaemon(true).setNameFormat("Votifier NIO boss").build());
this.eventLoopGroup = new NioEventLoopGroup(1, new ThreadFactoryBuilder().setDaemon(true).setNameFormat("Votifier NIO worker").build());
Objects.requireNonNull(error, "error");
ServerBootstrap bootstrap = new ServerBootstrap()
.channel(NioServerSocketChannel.class)
.group(bossLoopGroup, eventLoopGroup)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel channel) {
channel.attr(VotifierSession.KEY).set(new VotifierSession());
channel.pipeline().addLast("greetingHandler", new VotifierGreetingHandler());
channel.pipeline().addLast("protocolDifferentiator", new VotifierProtocolDifferentiator(v1KeyProvider, v2TokenProvider, testMode));
channel.pipeline().addLast("voteHandler", new VoteInboundHandler(voteHandler));
}
});
ChannelFutureListener listener = future -> {
Channel serverChannel = future.channel();
if (future.isSuccess()) {
serverChannels.add(serverChannel);
loggingAdapter.info("Votifier enabled on socket " + serverChannel.localAddress() + ".");
error.accept(null);
} else {
loggingAdapter.error("Votifier was not able to bind to " + serverChannel.localAddress().toString(), future.cause());
error.accept(future.cause());
}
};
for (InetSocketAddress socketAddress : this.bindAddresses) {
bootstrap.bind(socketAddress).addListener(listener);
}
}
@Override
public void close() throws Exception {
if (!serverChannels.isEmpty()) {
PromiseCombiner closePromises = new PromiseCombiner();
serverChannels.stream().map(ChannelOutboundInvoker::close).forEach(closePromises::add);
DefaultPromise<Void> closesDone = new DefaultPromise<>(ImmediateEventExecutor.INSTANCE);
closePromises.finish(closesDone);
closesDone.awaitUninterruptibly();
}
eventLoopGroup.shutdownGracefully();
bossLoopGroup.shutdownGracefully();
}
}
@@ -1,65 +0,0 @@
package io.ibj.nuvotifier.legacyserver;
import io.ibj.nuvotifier.platform.LoggingAdapter;
import io.ibj.nuvotifier.platform.V1KeyProvider;
import io.ibj.nuvotifier.platform.V2TokenProvider;
import io.ibj.nuvotifier.platform.VoteHandler;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
public class VotifierServerBuilder {
// Which addresses to bind to? We support multiple now!
List<InetSocketAddress> bindAddresses = new ArrayList<>();
// Provides the server the ability to decode v1 keys. Defaults to
V1KeyProvider v1KeyProvider = null;
// Provides the server the ability to validate v2 token payloads
V2TokenProvider v2TokenProvider = null;
// Logging adapter which will be used to communicate various logs
LoggingAdapter loggingAdapter = null;
// Whether not the server is in 'test mode'. Spoiler: unless you are testing, its not.
boolean testMode = false;
// Handler to invoke when a vote is sent.
VoteHandler voteHandler = null;
public VotifierServerBuilder addBindAddress(InetSocketAddress bindAddress) {
this.bindAddresses.add(bindAddress);
return this;
}
public VotifierServerBuilder setV1KeyProvider(V1KeyProvider v1KeyProvider) {
this.v1KeyProvider = v1KeyProvider;
return this;
}
public VotifierServerBuilder setV2TokenProvider(V2TokenProvider v2TokenProvider) {
this.v2TokenProvider = v2TokenProvider;
return this;
}
public VotifierServerBuilder setLoggingAdapter(LoggingAdapter loggingAdapter) {
this.loggingAdapter = loggingAdapter;
return this;
}
public VotifierServerBuilder setTestMode(boolean testMode) {
this.testMode = testMode;
return this;
}
public VotifierServerBuilder setVoteHandler(VoteHandler voteHandler) {
this.voteHandler = voteHandler;
return this;
}
public VotifierServer build() {
return new VotifierServer(this);
}
}
-3
View File
@@ -1,3 +0,0 @@
# model
This module contains the 'model' for Votifier. It includes the legacy 'Vote' object as well as a 'NuVote' object which plugins which require extended information should depend upon.
View File
Whitespace-only changes.
@@ -1,89 +0,0 @@
/*
* Copyright (C) 2011 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 <http://www.gnu.org/licenses/>.
*/
package com.vexsoftware.votifier.model;
/**
* A model for a vote.
*
* @author Blake Beaupain
*/
public class Vote {
/** The name of the vote service. */
private final String serviceName;
/** The username of the voter. */
private final String username;
/** The address of the voter. */
private final String address;
/** The date and time of the vote. */
private final String timeStamp;
public Vote(String serviceName, String username, String address, String timeStamp) {
this.serviceName = serviceName;
this.username = username;
this.address = address;
this.timeStamp = timeStamp;
}
@Override
public String toString() {
return "Vote (from:" + serviceName + " username:" + username
+ " address:" + address + " timeStamp:" + timeStamp + ")";
}
/**
* Gets the serviceName.
*
* @return The serviceName
*/
public String getServiceName() {
return serviceName;
}
/**
* Gets the username.
*
* @return The username
*/
public String getUsername() {
return username;
}
/**
* Gets the address.
*
* @return The address
*/
public String getAddress() {
return address;
}
/**
* Gets the time stamp.
*
* @return The time stamp
*/
public String getTimeStamp() {
return timeStamp;
}
}
@@ -1,17 +0,0 @@
package io.ibj.nuvotifier.model;
import com.vexsoftware.votifier.model.Vote;
/**
* This is the NuVote object. It extends the old 'Vote' object. The old 'Vote' object is frozen - i.e. no changes will be
* made to it. API additions to Vote happen in this file.
*/
public class NuVote extends Vote {
private NuVote(String serviceName, String username, String address, String timeStamp) {
super(serviceName, username, address, timeStamp);
}
public static NuVote createLegacy(String serviceName, String username, String address, String timeStamp) {
return new NuVote(serviceName, username, address, timeStamp);
}
}
View File
Whitespace-only changes.
-3
View File
@@ -1,3 +0,0 @@
dependencies {
api project(":model")
}
@@ -1,7 +0,0 @@
package io.ibj.nuvotifier.platform;
import java.security.PrivateKey;
public interface V1KeyProvider {
PrivateKey getPrivateKey();
}
@@ -1,7 +0,0 @@
package io.ibj.nuvotifier.platform;
import java.security.Key;
public interface V2TokenProvider {
Key getKeyForService(String serviceName);
}
@@ -1,12 +0,0 @@
package io.ibj.nuvotifier.platform;
import io.ibj.nuvotifier.model.NuVote;
public interface VoteHandler {
void onVote(NuVote vote);
default void onError(Throwable throwable, boolean voteAlreadyCompleted, String remoteAddress) {
throw new RuntimeException("Unimplemented onError handler");
}
}
+27
View File
@@ -0,0 +1,27 @@
subprojects {
buildscript {
}
apply plugin: 'maven-publish'
apply plugin: 'com.jfrog.bintray'
bintray {
user = System.getenv("BINTRAY_USER")
key = System.getenv("BINTRAY_KEY")
pkg {
repo = 'public'
name = 'nuvotifier'
version {
name = project.version
}
publications = ['DefaultPublication']
}
}
publishing {
publications {
DefaultPublication(MavenPublication) {
from components.java
}
}
}
}
+12 -14
View File
@@ -1,19 +1,17 @@
rootProject.name = 'nuvotifier'
// The model for a 'Vote'.
include 'model'
include 'common'
findProject(':common')?.name = 'nuvotifier-common'
// The 'platform' api.
include 'platform'
include 'multiplatform'
include 'bukkit'
findProject(':bukkit')?.name = 'nuvotifier-bukkit'
include 'bungeecord'
findProject(':bungeecord')?.name = 'nuvotifier-bungeecord'
include 'sponge'
findProject(':sponge')?.name = 'nuvotifier-sponge'
// Servers
include 'legacyserver'
// Apis
include 'bukkit-api'
include 'bungeecord-api'
include 'sponge-api'
include 'velocity-api'
include 'engine'
include 'universal'
findProject(':universal')?.name = 'nuvotifier-universal'
include 'velocity'
findProject(':velocity')?.name = 'nuvotifier-velocity'
-4
View File
@@ -1,4 +0,0 @@
dependencies {
api group: "org.spongepowered", name:"spongeapi", version: "7.1.0"
implementation project(":model")
}
+18
View File
@@ -0,0 +1,18 @@
plugins {
id 'org.spongepowered.plugin' version '0.9.0'
}
dependencies {
compileOnly group: "org.spongepowered", name: "spongeapi", version: "7.1.0"
annotationProcessor group: "org.spongepowered", name: "spongeapi", version: "7.1.0"
compile project(":nuvotifier-common")
}
sponge {
plugin {
id = 'nuvotifier'
meta {
version = project.version
}
}
}
@@ -0,0 +1,42 @@
package com.vexsoftware.votifier.sponge;
import com.vexsoftware.votifier.platform.LoggingAdapter;
import org.slf4j.Logger;
public class SLF4JLogger implements LoggingAdapter {
private final Logger l;
public SLF4JLogger(Logger l) {
this.l = l;
}
@Override
public void error(String s) {
l.error(s);
}
@Override
public void error(String s, Object... o) {
l.error(s, o);
}
@Override
public void warn(String s) {
l.warn(s);
}
@Override
public void warn(String s, Object... o) {
l.warn(s, o);
}
@Override
public void info(String s) {
l.info(s);
}
@Override
public void info(String s, Object... o) {
l.info(s, o);
}
}
@@ -0,0 +1,58 @@
package com.vexsoftware.votifier.sponge;
import com.vexsoftware.votifier.platform.scheduler.ScheduledVotifierTask;
import com.vexsoftware.votifier.platform.scheduler.VotifierScheduler;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.scheduler.Task;
import java.util.concurrent.TimeUnit;
class SpongeScheduler implements VotifierScheduler {
private final VotifierPlugin plugin;
SpongeScheduler(VotifierPlugin plugin) {
this.plugin = plugin;
}
private Task.Builder taskBuilder(Runnable runnable) {
return Sponge.getScheduler().createTaskBuilder().execute(runnable);
}
@Override
public ScheduledVotifierTask sync(Runnable runnable) {
return new TaskWrapper(taskBuilder(runnable).submit(plugin));
}
@Override
public ScheduledVotifierTask onPool(Runnable runnable) {
return new TaskWrapper(taskBuilder(runnable).async().submit(plugin));
}
@Override
public ScheduledVotifierTask delayedSync(Runnable runnable, int delay, TimeUnit unit) {
return new TaskWrapper(taskBuilder(runnable).delay(delay, unit).submit(plugin));
}
@Override
public ScheduledVotifierTask delayedOnPool(Runnable runnable, int delay, TimeUnit unit) {
return new TaskWrapper(taskBuilder(runnable).delay(delay, unit).async().submit(plugin));
}
@Override
public ScheduledVotifierTask repeatOnPool(Runnable runnable, int delay, int repeat, TimeUnit unit) {
return new TaskWrapper(taskBuilder(runnable).delay(delay, unit).interval(repeat, unit).submit(plugin));
}
private static class TaskWrapper implements ScheduledVotifierTask {
private final Task task;
private TaskWrapper(Task task) {
this.task = task;
}
@Override
public void cancel() {
task.cancel();
}
}
}
@@ -0,0 +1,246 @@
package com.vexsoftware.votifier.sponge;
import com.google.inject.Inject;
import com.vexsoftware.votifier.VoteHandler;
import com.vexsoftware.votifier.model.Vote;
import com.vexsoftware.votifier.net.VotifierServerBootstrap;
import com.vexsoftware.votifier.net.VotifierSession;
import com.vexsoftware.votifier.net.protocol.v1crypto.RSAIO;
import com.vexsoftware.votifier.net.protocol.v1crypto.RSAKeygen;
import com.vexsoftware.votifier.platform.LoggingAdapter;
import com.vexsoftware.votifier.platform.scheduler.VotifierScheduler;
import com.vexsoftware.votifier.sponge.config.ConfigLoader;
import com.vexsoftware.votifier.sponge.event.VotifierEvent;
import com.vexsoftware.votifier.sponge.forwarding.SpongePluginMessagingForwardingSink;
import com.vexsoftware.votifier.support.forwarding.ForwardedVoteListener;
import com.vexsoftware.votifier.support.forwarding.ForwardingVoteSink;
import com.vexsoftware.votifier.util.KeyCreator;
import io.netty.channel.Channel;
import org.slf4j.Logger;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.config.ConfigDir;
import org.spongepowered.api.event.Listener;
import org.spongepowered.api.event.game.state.GameStartedServerEvent;
import org.spongepowered.api.event.game.state.GameStoppingServerEvent;
import org.spongepowered.api.plugin.Plugin;
import java.io.File;
import java.security.Key;
import java.security.KeyPair;
import java.util.HashMap;
import java.util.Map;
@Plugin(id = "nuvotifier", name = "NuVotifier", version="SOMEONE_FUCKED_UP_THE_BUILD", authors = "Ichbinjoe",
description = "Safe, smart, and secure Votifier server plugin")
public class VotifierPlugin implements VoteHandler, com.vexsoftware.votifier.platform.VotifierPlugin, ForwardedVoteListener {
@Inject
public Logger logger;
private SLF4JLogger loggerAdapter;
@Inject
@ConfigDir(sharedRoot = false)
public File configDir;
private VotifierScheduler scheduler;
@Listener
public void onServerStart(GameStartedServerEvent event) {
this.scheduler = new SpongeScheduler(this);
this.loggerAdapter = new SLF4JLogger(logger);
// Load configuration.
ConfigLoader.loadConfig(this);
/*
* Create RSA directory and keys if it does not exist; otherwise, read
* keys.
*/
File rsaDirectory = new File(configDir, "rsa");
try {
if (!rsaDirectory.exists()) {
rsaDirectory.mkdir();
keyPair = RSAKeygen.generate(2048);
RSAIO.save(rsaDirectory, keyPair);
} else {
keyPair = RSAIO.load(rsaDirectory);
}
} catch (Exception ex) {
logger.error("Error creating or reading RSA tokens", ex);
gracefulExit();
return;
}
debug = ConfigLoader.getSpongeConfig().debug;
// Load Votifier tokens.
ConfigLoader.getSpongeConfig().tokens.forEach((s, s2) -> {
tokens.put(s, KeyCreator.createKeyFrom(s2));
logger.info("Loaded token for website: " + s);
});
// Initialize the receiver.
final String host = ConfigLoader.getSpongeConfig().host;
final int port = ConfigLoader.getSpongeConfig().port;
if (!debug)
logger.info("QUIET mode enabled!");
if (port >= 0) {
final boolean disablev1 = ConfigLoader.getSpongeConfig().disableV1Protocol;
if (disablev1) {
logger.info("------------------------------------------------------------------------------");
logger.info("Votifier protocol v1 parsing has been disabled. Most voting websites do not");
logger.info("currently support the modern Votifier protocol in NuVotifier.");
logger.info("------------------------------------------------------------------------------");
}
this.bootstrap = new VotifierServerBootstrap(host, port, this, disablev1);
this.bootstrap.start(err -> {});
} else {
getLogger().info("------------------------------------------------------------------------------");
getLogger().info("Your Votifier port is less than 0, so we assume you do NOT want to start the");
getLogger().info("votifier port server! Votifier will not listen for votes over any port, and");
getLogger().info("will only listen for pluginMessaging forwarded votes!");
getLogger().info("------------------------------------------------------------------------------");
}
if (ConfigLoader.getSpongeConfig().forwarding != null) {
String method = ConfigLoader.getSpongeConfig().forwarding.method.toLowerCase(); //Default to lower case for case-insensitive searches
if ("none".equals(method)) {
getLogger().info("Method none selected for vote forwarding: Votes will not be received from a forwarder.");
} else if ("pluginmessaging".equals(method)) {
String channel = ConfigLoader.getSpongeConfig().forwarding.pluginMessaging.channel;
try {
forwardingMethod = new SpongePluginMessagingForwardingSink(this, channel, this);
getLogger().info("Receiving votes over PluginMessaging channel '" + channel + "'.");
} catch (RuntimeException e) {
logger.error("NuVotifier could not set up PluginMessaging for vote forwarding!", e);
}
} else {
logger.error("No vote forwarding method '" + method + "' known. Defaulting to noop implementation.");
}
}
}
@Listener
public void onServerStop(GameStoppingServerEvent event) {
if (bootstrap != null) {
bootstrap.shutdown();
bootstrap = null;
}
if (forwardingMethod != null)
forwardingMethod.halt();
logger.info("Votifier disabled.");
}
public Logger getLogger() {
return logger;
}
/**
* The current Votifier version.
*/
private String version;
/**
* The server bootstrap.
*/
private VotifierServerBootstrap bootstrap;
/**
* The RSA key pair.
*/
private KeyPair keyPair;
/**
* Debug mode flag
*/
private boolean debug;
/**
* Keys used for websites.
*/
private Map<String, Key> tokens = new HashMap<>();
private ForwardingVoteSink forwardingMethod;
private void gracefulExit() {
logger.error("Votifier did not initialize properly!");
}
@Override
public LoggingAdapter getPluginLogger() {
return loggerAdapter;
}
@Override
public VotifierScheduler getScheduler() {
return scheduler;
}
public boolean isDebug() {
return debug;
}
@Override
public Map<String, Key> getTokens() {
return tokens;
}
@Override
public KeyPair getProtocolV1Key() {
return keyPair;
}
public File getConfigDir() {
return configDir;
}
@Override
public void onVoteReceived(Channel channel, final Vote vote, VotifierSession.ProtocolVersion protocolVersion) {
if (debug) {
if (protocolVersion == VotifierSession.ProtocolVersion.ONE) {
logger.info("Got a protocol v1 vote record from " + channel.remoteAddress() + " -> " + vote);
} else {
logger.info("Got a protocol v2 vote record from " + channel.remoteAddress() + " -> " + vote);
}
}
Sponge.getScheduler().createTaskBuilder()
.execute(() -> {
VotifierEvent event = new VotifierEvent(vote, Sponge.getCauseStackManager().getCurrentCause());
Sponge.getEventManager().post(event);
})
.submit(this);
}
@Override
public void onError(Channel channel, boolean alreadyHandledVote, Throwable throwable) {
if (debug) {
if (alreadyHandledVote) {
logger.error("Vote processed, however an exception " +
"occurred with a vote from " + channel.remoteAddress(), throwable);
} else {
logger.error("Unable to process vote from " + channel.remoteAddress(), throwable);
}
} else if (!alreadyHandledVote) {
logger.error("Unable to process vote from " + channel.remoteAddress());
}
}
@Override
public void onForward(final Vote v) {
if (debug) {
logger.info("Got a forwarded vote -> " + v);
}
Sponge.getScheduler().createTaskBuilder()
.execute(() -> {
VotifierEvent event = new VotifierEvent(v, Sponge.getCauseStackManager().getCurrentCause());
Sponge.getEventManager().post(event);
})
.submit(this);
}
}
@@ -0,0 +1,39 @@
package com.vexsoftware.votifier.sponge.config;
import com.google.common.reflect.TypeToken;
import com.vexsoftware.votifier.sponge.VotifierPlugin;
import ninja.leaping.configurate.ConfigurationNode;
import ninja.leaping.configurate.ConfigurationOptions;
import ninja.leaping.configurate.loader.ConfigurationLoader;
import ninja.leaping.configurate.yaml.YAMLConfigurationLoader;
import java.io.File;
public class ConfigLoader {
private static VotifierPlugin plugin;
private static SpongeConfig spongeConfig;
public static void loadConfig(VotifierPlugin pl) {
plugin = pl;
if (!plugin.getConfigDir().exists()) {
plugin.getConfigDir().mkdirs();
}
try {
File file = new File(plugin.getConfigDir(), "config.yml");
if (!file.exists()) {
file.createNewFile();
}
ConfigurationLoader loader = YAMLConfigurationLoader.builder().setFile(file).build();
ConfigurationNode config = loader.load(ConfigurationOptions.defaults().setShouldCopyDefaults(true));
spongeConfig = config.getValue(TypeToken.of(SpongeConfig.class), new SpongeConfig());
loader.save(config);
} catch (Exception e) {
plugin.getLogger().error("Could not load config.", e);
}
}
public static SpongeConfig getSpongeConfig() {
return spongeConfig;
}
}
@@ -0,0 +1,55 @@
package com.vexsoftware.votifier.sponge.config;
import com.vexsoftware.votifier.util.TokenUtil;
import ninja.leaping.configurate.objectmapping.Setting;
import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable;
import org.spongepowered.api.Sponge;
import java.util.Collections;
import java.util.Map;
@ConfigSerializable
public class SpongeConfig {
@Setting(comment = "The IP to listen to. Use 0.0.0.0 if you wish to listen to all interfaces on your server. (All IP addresses)\n" +
"This defaults to the IP you have configured your server to listen on, or 0.0.0.0 if you have not configured this.")
public String host = Sponge.getServer().getBoundAddress().isPresent() ? Sponge.getServer().getBoundAddress().get().getAddress().getHostAddress() : "0.0.0.0";
@Setting(comment = "Port to listen for new votes on")
public int port = 8192;
@Setting(comment = "Whether or not to print debug messages. In a production system, this should be set to false.\n" +
"This is useful when initially setting up NuVotifier to ensure votes are being delivered.")
public boolean debug = true;
@Setting( value = "disable-v1-protocol", comment = "Setting this option to true will disable handling of Protocol v1 packets. While the old protocol is not secure, this\n" +
"option is currently not recommended as most voting sites only support the old protocol at present. However, if you are\n" +
"using NuVotifier's proxy forwarding mechanism, enabling this option will increase your server's security.")
public boolean disableV1Protocol = false;
@Setting(comment = "All tokens, labeled by the serviceName of each server list.\n" +
"Default token for all server lists, if another isn't supplied.")
public Map<String, String> tokens = Collections.singletonMap("default", TokenUtil.newToken());
@Setting(comment = "Configuration section for all vote forwarding to NuVotifier")
public Forwarding forwarding = new Forwarding();
@ConfigSerializable
public static class Forwarding {
@Setting(comment = "Sets whether to set up a remote method for fowarding. Supported methods:\n" +
"- none - Does not set up a forwarding method.\n" +
"- pluginMessaging - Sets up plugin messaging")
public String method = "none";
@Setting
public PluginMessaging pluginMessaging = new PluginMessaging();
@ConfigSerializable
public static class PluginMessaging {
@Setting
public String channel = "nuvotifier:votes";
}
}
}
@@ -0,0 +1,39 @@
package com.vexsoftware.votifier.sponge.forwarding;
import com.vexsoftware.votifier.sponge.VotifierPlugin;
import com.vexsoftware.votifier.support.forwarding.AbstractPluginMessagingForwardingSink;
import com.vexsoftware.votifier.support.forwarding.ForwardedVoteListener;
import org.spongepowered.api.Platform;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.network.ChannelBinding;
import org.spongepowered.api.network.ChannelBuf;
import org.spongepowered.api.network.RawDataListener;
import org.spongepowered.api.network.RemoteConnection;
public class SpongePluginMessagingForwardingSink extends AbstractPluginMessagingForwardingSink implements RawDataListener {
public SpongePluginMessagingForwardingSink(VotifierPlugin p, String channel, ForwardedVoteListener listener) {
super(listener);
this.channelBinding = Sponge.getChannelRegistrar().createRawChannel(p, channel);
this.channelBinding.addListener(Platform.Type.SERVER, this);
this.p = p;
}
private final VotifierPlugin p;
private final ChannelBinding.RawDataChannel channelBinding;
@Override
public void halt() {
channelBinding.removeListener(this);
}
@Override
public void handlePayload(ChannelBuf channelBuf, RemoteConnection remoteConnection, Platform.Type type) {
byte[] msgDirBuf = channelBuf.readBytes(channelBuf.available());
try {
this.handlePluginMessage(msgDirBuf);
} catch (Exception e) {
p.getLogger().error("There was an unknown error when processing a forwarded vote.", e);
}
}
}
@@ -0,0 +1,25 @@
# The IP to listen to. Use 0.0.0.0 if you wish to listen to all interfaces on your server. (All IP addresses)
# This defaults to the IP you have configured your server to listen on, or 0.0.0.0 if you have not configured this.
host: %ip%
# Port to listen for new votes on
port: 8192
# Setting this option to true will disable handling of Protocol v1 packets. While the old protocol is not secure, this
# option is currently not recommended as most voting sites only support the old protocol at present. However, if you are
# using NuVotifier's proxy forwarding mechanism, enabling this option will increase your server's security.
disable-v1-protocol: false
# All tokens, labeled by the serviceName of each server list.
tokens:
# Default token for all server lists, if another isn't supplied.
default: %default_token%
# Configuration section for all vote forwarding to NuVotifier
forwarding:
# Sets whether to set up a remote method for fowarding. Supported methods:
# - none - Does not set up a forwarding method.
# - pluginMessaging - Sets up plugin messaging
method: none
pluginMessaging:
channel: nuvotifier:votes
+70
View File
@@ -0,0 +1,70 @@
import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator
import java.nio.file.FileAlreadyExistsException
import java.nio.file.Files
import java.nio.file.Paths
import java.util.regex.Pattern
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.4'
}
}
apply plugin: 'com.github.johnrengelman.shadow'
dependencies {
compile project(":nuvotifier-bukkit")
compile project(":nuvotifier-bungeecord")
compile project(":nuvotifier-sponge")
compile project(":nuvotifier-velocity")
}
shadowJar {
dependencies {
// All current implementations include Google Guava already, and explicitly shading in Guava causes the plugin
// to fail to initialize on Sponge.
exclude(dependency('com.google.guava:guava:24.1-jre'))
}
baseName = 'nuvotifier'
classifier = null
relocate "io.netty", "com.vexsoftware.votifier.netty"
relocate "org.json", "com.vexsoftware.votifier.json"
relocate new SimpleRelocator("com.google.code", "com.vexsoftware.votifier.google.code", new ArrayList<String>(), Collections.singletonList("com.vexsoftware.votifier.sponge"))
relocate "com.google.guava", "com.vexsoftware.votifier.google.guava"
relocate "org.apache.commons.io", "com.vexsoftware.votifier.commons.io"
}
artifacts {
tasks.shadowJar.outputs.files.each {
archives(it) {
builtBy shadowJar
}
}
}
def artifactPattern = Pattern.compile(/(.*)-([0-9]+\.)*[0-9]+(-SNAPSHOT)?\.jar/)
task('linkLatest') {
dependsOn tasks.shadowJar
doLast {
tasks.shadowJar.outputs.files.each {
System.println(it.absolutePath)
def noVerMatcher = artifactPattern.matcher(it.absolutePath)
if (!noVerMatcher.matches())
throw new IllegalStateException("wtf")
def noVer = Paths.get(noVerMatcher.group(1) + ".jar")
def versnd = it.toPath()
try {
Files.createSymbolicLink(noVer, versnd)
} catch (FileAlreadyExistsException ignored) {}
}
}
}
build.dependsOn linkLatest
-4
View File
@@ -1,4 +0,0 @@
dependencies {
api group: "com.velocitypowered", name: "velocity-api", version: "1.0-SNAPSHOT"
implementation project(":model")
}
Loaded 100 of 111 files, more files were not shown because too many files have changed in this diff. Show more